]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/utils/__tests__/unit/LexlcaiUtilsInsertNodeToNearestRoot.test.tsx
9664b2d80ca3f1718199eb69d8a4fda6f0875bee
[bookstack] / resources / js / wysiwyg / lexical / utils / __tests__ / unit / LexlcaiUtilsInsertNodeToNearestRoot.test.tsx
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {LexicalEditor, LexicalNode} from 'lexical';
10
11 import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
12 import {
13   $createRangeSelection,
14   $getRoot,
15   $isElementNode,
16   $setSelection,
17 } from 'lexical';
18 import {
19   $createTestDecoratorNode,
20   createTestEditor,
21 } from 'lexical/__tests__/utils';
22
23 import {$insertNodeToNearestRoot} from '../..';
24
25 describe('LexicalUtils#insertNodeToNearestRoot', () => {
26   let editor: LexicalEditor;
27
28   const update = async (updateFn: () => void) => {
29     editor.update(updateFn);
30     await Promise.resolve();
31   };
32
33   beforeEach(async () => {
34     editor = createTestEditor();
35     editor._headless = true;
36   });
37
38   const testCases: Array<{
39     _: string;
40     expectedHtml: string;
41     initialHtml: string;
42     selectionPath: Array<number>;
43     selectionOffset: number;
44     only?: boolean;
45   }> = [
46     {
47       _: 'insert into paragraph in between two text nodes',
48       expectedHtml:
49         '<p><span style="white-space: pre-wrap;">Hello</span></p><test-decorator></test-decorator><p><span style="white-space: pre-wrap;">world</span></p>',
50       initialHtml: '<p><span>Helloworld</span></p>',
51       selectionOffset: 5, // Selection on text node after "Hello" world
52       selectionPath: [0, 0],
53     },
54     {
55       _: 'insert into nested list items',
56       expectedHtml:
57         '<ul>' +
58         '<li><span style="white-space: pre-wrap;">Before</span></li>' +
59         '<li><ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul></li>' +
60         '</ul>' +
61         '<test-decorator></test-decorator>' +
62         '<ul>' +
63         '<li><ul><li><span style="white-space: pre-wrap;">world</span></li></ul></li>' +
64         '<li><span style="white-space: pre-wrap;">After</span></li>' +
65         '</ul>',
66       initialHtml:
67         '<ul>' +
68         '<li><span>Before</span></li>' +
69         '<ul><li><span>Helloworld</span></li></ul>' +
70         '<li><span>After</span></li>' +
71         '</ul>',
72       selectionOffset: 5, // Selection on text node after "Hello" world
73       selectionPath: [0, 1, 0, 0, 0],
74     },
75     {
76       _: 'insert into empty paragraph',
77       expectedHtml: '<p><br></p><test-decorator></test-decorator><p><br></p>',
78       initialHtml: '<p></p>',
79       selectionOffset: 0, // Selection on text node after "Hello" world
80       selectionPath: [0],
81     },
82     {
83       _: 'insert in the end of paragraph',
84       expectedHtml:
85         '<p><span style="white-space: pre-wrap;">Hello world</span></p>' +
86         '<test-decorator></test-decorator>' +
87         '<p><br></p>',
88       initialHtml: '<p>Hello world</p>',
89       selectionOffset: 12, // Selection on text node after "Hello" world
90       selectionPath: [0, 0],
91     },
92     {
93       _: 'insert in the beginning of paragraph',
94       expectedHtml:
95         '<p><br></p>' +
96         '<test-decorator></test-decorator>' +
97         '<p><span style="white-space: pre-wrap;">Hello world</span></p>',
98       initialHtml: '<p>Hello world</p>',
99       selectionOffset: 0, // Selection on text node after "Hello" world
100       selectionPath: [0, 0],
101     },
102     {
103       _: 'insert with selection on root start',
104       expectedHtml:
105         '<test-decorator></test-decorator>' +
106         '<test-decorator></test-decorator>' +
107         '<p><span style="white-space: pre-wrap;">Before</span></p>' +
108         '<p><span style="white-space: pre-wrap;">After</span></p>',
109       initialHtml:
110         '<test-decorator></test-decorator>' +
111         '<p><span>Before</span></p>' +
112         '<p><span>After</span></p>',
113       selectionOffset: 0,
114       selectionPath: [],
115     },
116     {
117       _: 'insert with selection on root child',
118       expectedHtml:
119         '<p><span style="white-space: pre-wrap;">Before</span></p>' +
120         '<test-decorator></test-decorator>' +
121         '<p><span style="white-space: pre-wrap;">After</span></p>',
122       initialHtml: '<p>Before</p><p>After</p>',
123       selectionOffset: 1,
124       selectionPath: [],
125     },
126     {
127       _: 'insert with selection on root end',
128       expectedHtml:
129         '<p><span style="white-space: pre-wrap;">Before</span></p>' +
130         '<test-decorator></test-decorator>',
131       initialHtml: '<p>Before</p>',
132       selectionOffset: 1,
133       selectionPath: [],
134     },
135   ];
136
137   for (const testCase of testCases) {
138     it(testCase._, async () => {
139       await update(() => {
140         // Running init, update, assert in the same update loop
141         // to skip text nodes normalization (then separate text
142         // nodes will still be separate and represented by its own
143         // spans in html output) and make assertions more precise
144         const parser = new DOMParser();
145         const dom = parser.parseFromString(testCase.initialHtml, 'text/html');
146         const nodesToInsert = $generateNodesFromDOM(editor, dom);
147         $getRoot()
148           .clear()
149           .append(...nodesToInsert);
150
151         let selectionNode: LexicalNode = $getRoot();
152         for (const index of testCase.selectionPath) {
153           if (!$isElementNode(selectionNode)) {
154             throw new Error(
155               'Expected node to be element (to traverse the tree)',
156             );
157           }
158           selectionNode = selectionNode.getChildAtIndex(index)!;
159         }
160
161         // Calling selectionNode.select() would "normalize" selection and move it
162         // to text node (if available), while for the purpose of the test we'd want
163         // to use whatever was passed (e.g. keep selection on root node)
164         const selection = $createRangeSelection();
165         const type = $isElementNode(selectionNode) ? 'element' : 'text';
166         selection.anchor.key = selection.focus.key = selectionNode.getKey();
167         selection.anchor.offset = selection.focus.offset =
168           testCase.selectionOffset;
169         selection.anchor.type = selection.focus.type = type;
170         $setSelection(selection);
171
172         $insertNodeToNearestRoot($createTestDecoratorNode());
173
174         // Cleaning up list value attributes as it's not really needed in this test
175         // and it clutters expected output
176         const actualHtml = $generateHtmlFromNodes(editor).replace(
177           /\svalue="\d{1,}"/g,
178           '',
179         );
180         expect(actualHtml).toEqual(testCase.expectedHtml);
181       });
182     });
183   }
184 });