2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import type {LexicalEditor, LexicalNode} from 'lexical';
11 import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
13 $createRangeSelection,
19 $createTestDecoratorNode,
21 } from 'lexical/__tests__/utils';
23 import {$insertNodeToNearestRoot} from '../..';
25 describe('LexicalUtils#insertNodeToNearestRoot', () => {
26 let editor: LexicalEditor;
28 const update = async (updateFn: () => void) => {
29 editor.update(updateFn);
30 await Promise.resolve();
33 beforeEach(async () => {
34 editor = createTestEditor();
35 editor._headless = true;
38 const testCases: Array<{
42 selectionPath: Array<number>;
43 selectionOffset: number;
47 _: 'insert into paragraph in between two text nodes',
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],
55 _: 'insert into nested list items',
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>' +
61 '<test-decorator></test-decorator>' +
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>' +
68 '<li><span>Before</span></li>' +
69 '<ul><li><span>Helloworld</span></li></ul>' +
70 '<li><span>After</span></li>' +
72 selectionOffset: 5, // Selection on text node after "Hello" world
73 selectionPath: [0, 1, 0, 0, 0],
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
83 _: 'insert in the end of paragraph',
85 '<p><span style="white-space: pre-wrap;">Hello world</span></p>' +
86 '<test-decorator></test-decorator>' +
88 initialHtml: '<p>Hello world</p>',
89 selectionOffset: 12, // Selection on text node after "Hello" world
90 selectionPath: [0, 0],
93 _: 'insert in the beginning of paragraph',
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],
103 _: 'insert with selection on root start',
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>',
110 '<test-decorator></test-decorator>' +
111 '<p><span>Before</span></p>' +
112 '<p><span>After</span></p>',
117 _: 'insert with selection on root child',
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>',
127 _: 'insert with selection on root end',
129 '<p><span style="white-space: pre-wrap;">Before</span></p>' +
130 '<test-decorator></test-decorator>',
131 initialHtml: '<p>Before</p>',
137 for (const testCase of testCases) {
138 it(testCase._, async () => {
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);
149 .append(...nodesToInsert);
151 let selectionNode: LexicalNode = $getRoot();
152 for (const index of testCase.selectionPath) {
153 if (!$isElementNode(selectionNode)) {
155 'Expected node to be element (to traverse the tree)',
158 selectionNode = selectionNode.getChildAtIndex(index)!;
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);
172 $insertNodeToNearestRoot($createTestDecoratorNode());
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(
180 expect(actualHtml).toEqual(testCase.expectedHtml);