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 {$createLinkNode} from '@lexical/link';
10 import {$createListItemNode, $createListNode} from '@lexical/list';
11 import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
12 import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
13 import {ContentEditable} from '@lexical/react/LexicalContentEditable';
14 import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
15 import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
16 import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
17 import {$createHeadingNode} from '@lexical/rich-text';
20 $getSelectionStyleValueForProperty,
23 } from '@lexical/selection';
24 import {$createTableNodeWithDimensions} from '@lexical/table';
28 $createRangeSelection,
46 $assertRangeSelection,
47 $createTestDecoratorNode,
48 $createTestElementNode,
53 } from 'lexical/__tests__/utils';
54 import {createRoot, Root} from 'react-dom/client';
55 import * as ReactTestUtils from 'lexical/shared/react-test-utils';
61 convertToSegmentedNode,
81 setNativeSelectionWithPaths,
85 interface ExpectedSelection {
92 initializeClipboard();
94 jest.mock('lexical/shared/environment', () => {
95 const originalModule = jest.requireActual('lexical/shared/environment');
97 return {...originalModule, IS_FIREFOX: true};
100 Range.prototype.getBoundingClientRect = function (): DOMRect {
119 describe('LexicalSelection tests', () => {
120 let container: HTMLElement;
122 let editor: LexicalEditor | null = null;
124 beforeEach(async () => {
125 container = document.createElement('div');
126 document.body.appendChild(container);
127 reactRoot = createRoot(container);
131 afterEach(async () => {
132 // Ensure we are clearing out any React state and running effects with
134 await ReactTestUtils.act(async () => {
136 await Promise.resolve().then();
138 document.body.removeChild(container);
141 async function init() {
142 function TestBase() {
143 function TestPlugin() {
144 [editor] = useLexicalComposerContext();
156 h1: 'editor-heading-h1',
157 h2: 'editor-heading-h2',
158 h3: 'editor-heading-h3',
159 h4: 'editor-heading-h4',
160 h5: 'editor-heading-h5',
161 h6: 'editor-heading-h6',
163 image: 'editor-image',
165 ol: 'editor-list-ol',
166 ul: 'editor-list-ul',
168 listitem: 'editor-listitem',
169 paragraph: 'editor-paragraph',
170 quote: 'editor-quote',
172 bold: 'editor-text-bold',
173 code: 'editor-text-code',
174 hashtag: 'editor-text-hashtag',
175 italic: 'editor-text-italic',
176 link: 'editor-text-link',
177 strikethrough: 'editor-text-strikethrough',
178 underline: 'editor-text-underline',
179 underlineStrikethrough: 'editor-text-underlineStrikethrough',
185 // eslint-disable-next-line jsx-a11y/aria-role, @typescript-eslint/no-explicit-any
186 <ContentEditable role={null as any} spellCheck={null as any} />
189 ErrorBoundary={LexicalErrorBoundary}
198 await ReactTestUtils.act(async () => {
199 reactRoot.render(<TestBase />);
200 await Promise.resolve().then();
203 await Promise.resolve().then();
204 // Focus first element
205 setNativeSelectionWithPaths(
206 editor!.getRootElement()!,
214 async function update(fn: () => void) {
215 await ReactTestUtils.act(async () => {
216 await editor!.update(fn);
220 test('Expect initial output to be a block with no text.', () => {
221 expect(container!.innerHTML).toBe(
222 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><br></p></div>',
226 function assertSelection(
227 rootElement: HTMLElement,
228 expectedSelection: ExpectedSelection,
230 const actualSelection = window.getSelection()!;
232 expect(actualSelection.anchorNode).toBe(
233 getNodeFromPath(expectedSelection.anchorPath, rootElement),
235 expect(actualSelection.anchorOffset).toBe(expectedSelection.anchorOffset);
236 expect(actualSelection.focusNode).toBe(
237 getNodeFromPath(expectedSelection.focusPath, rootElement),
239 expect(actualSelection.focusOffset).toBe(expectedSelection.focusOffset);
242 // eslint-disable-next-line @typescript-eslint/no-unused-vars
243 const GRAPHEME_SCENARIOS = [
245 description: 'grapheme cluster',
246 // Hangul grapheme cluster.
247 // https://www.compart.com/en/unicode/U+AC01
248 grapheme: '\u1100\u1161\u11A8',
251 description: 'extended grapheme cluster',
252 // Tamil 'ni' grapheme cluster.
253 // http://unicode.org/reports/tr29/#Table_Sample_Grapheme_Clusters
254 grapheme: '\u0BA8\u0BBF',
257 description: 'tailored grapheme cluster',
258 // Devangari 'kshi' tailored grapheme cluster.
259 // http://unicode.org/reports/tr29/#Table_Sample_Grapheme_Clusters
260 grapheme: '\u0915\u094D\u0937\u093F',
263 description: 'Emoji sequence combined using zero-width joiners',
264 // https://emojipedia.org/family-woman-woman-girl-boy/
266 '\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66',
269 description: 'Emoji sequence with skin-tone modifier',
270 // https://emojipedia.org/clapping-hands-medium-skin-tone/
271 grapheme: '\uD83D\uDC4F\uD83C\uDFFD',
278 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello</span></p></div>',
281 anchorPath: [0, 0, 0],
283 focusPath: [0, 0, 0],
292 name: 'Simple typing',
296 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
297 '<strong class="editor-text-bold" data-lexical-text="true">Hello</strong></p></div>',
300 anchorPath: [0, 0, 0],
302 focusPath: [0, 0, 0],
312 name: 'Simple typing in bold',
316 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
317 '<em class="editor-text-italic" data-lexical-text="true">Hello</em></p></div>',
320 anchorPath: [0, 0, 0],
322 focusPath: [0, 0, 0],
332 name: 'Simple typing in italic',
336 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
337 '<strong class="editor-text-bold editor-text-italic" data-lexical-text="true">Hello</strong></p></div>',
340 anchorPath: [0, 0, 0],
342 focusPath: [0, 0, 0],
353 name: 'Simple typing in italic + bold',
357 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
358 '<span class="editor-text-underline" data-lexical-text="true">Hello</span></p></div>',
361 anchorPath: [0, 0, 0],
363 focusPath: [0, 0, 0],
373 name: 'Simple typing in underline',
377 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
378 '<span class="editor-text-strikethrough" data-lexical-text="true">Hello</span></p></div>',
381 anchorPath: [0, 0, 0],
383 focusPath: [0, 0, 0],
386 formatStrikeThrough(),
393 name: 'Simple typing in strikethrough',
397 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
398 '<span class="editor-text-underlineStrikethrough" data-lexical-text="true">Hello</span></p></div>',
401 anchorPath: [0, 0, 0],
403 focusPath: [0, 0, 0],
407 formatStrikeThrough(),
414 name: 'Simple typing in underline + strikethrough',
418 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">1246</span></p></div>',
421 anchorPath: [0, 0, 0],
423 focusPath: [0, 0, 0],
439 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
440 '<span data-lexical-text="true">Dominic Gannaway</span>' +
444 anchorPath: [0, 0, 0],
446 focusPath: [0, 0, 0],
448 inputs: [insertTokenNode('Dominic Gannaway')],
449 name: 'Creation of an token node',
453 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
454 '<span data-lexical-text="true">Dominic Gannaway</span>' +
463 insertText('Dominic Gannaway'),
464 moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 16),
465 convertToTokenNode(),
467 name: 'Convert text to an token node',
471 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
472 '<span data-lexical-text="true">Dominic Gannaway</span>' +
480 inputs: [insertSegmentedNode('Dominic Gannaway')],
481 name: 'Creation of a segmented node',
485 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
486 '<span data-lexical-text="true">Dominic Gannaway</span>' +
495 insertText('Dominic Gannaway'),
496 moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 16),
497 convertToSegmentedNode(),
499 name: 'Convert text to a segmented node',
503 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
504 '<p class="editor-paragraph"><br></p>' +
505 '<p class="editor-paragraph" dir="ltr">' +
506 '<strong class="editor-text-bold" data-lexical-text="true">Hello world</strong>' +
508 '<p class="editor-paragraph"><br></p>' +
518 insertText('Hello world'),
520 moveNativeSelection([0], 0, [2], 0),
523 name: 'Format selection that starts and ends on element and retain selection',
527 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
528 '<p class="editor-paragraph"><br></p>' +
529 '<p class="editor-paragraph" dir="ltr">' +
530 '<strong class="editor-text-bold" data-lexical-text="true">Hello</strong>' +
532 '<p class="editor-paragraph" dir="ltr">' +
533 '<strong class="editor-text-bold" data-lexical-text="true">world</strong>' +
535 '<p class="editor-paragraph"><br></p>' +
549 moveNativeSelection([0], 0, [3], 0),
552 name: 'Format multiline text selection that starts and ends on element and retain selection',
556 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
557 '<p class="editor-paragraph" dir="ltr">' +
558 '<span data-lexical-text="true">He</span>' +
559 '<strong class="editor-text-bold" data-lexical-text="true">llo</strong>' +
561 '<p class="editor-paragraph" dir="ltr">' +
562 '<strong class="editor-text-bold" data-lexical-text="true">wo</strong>' +
563 '<span data-lexical-text="true">rld</span>' +
568 anchorPath: [0, 1, 0],
570 focusPath: [1, 0, 0],
576 moveNativeSelection([0, 0, 0], 2, [1, 0, 0], 2),
579 name: 'Format multiline text selection that starts and ends within text',
583 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
584 '<p class="editor-paragraph"><br></p>' +
585 '<p class="editor-paragraph" dir="ltr">' +
586 '<span data-lexical-text="true">Hello </span>' +
587 '<strong class="editor-text-bold" data-lexical-text="true">world</strong>' +
589 '<p class="editor-paragraph"><br></p>' +
593 anchorPath: [1, 1, 0],
599 insertText('Hello world'),
601 moveNativeSelection([1, 0, 0], 6, [2], 0),
604 name: 'Format selection that starts on text and ends on element and retain selection',
608 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
609 '<p class="editor-paragraph"><br></p>' +
610 '<p class="editor-paragraph" dir="ltr">' +
611 '<strong class="editor-text-bold" data-lexical-text="true">Hello</strong>' +
612 '<span data-lexical-text="true"> world</span>' +
614 '<p class="editor-paragraph"><br></p>' +
620 focusPath: [1, 0, 0],
624 insertText('Hello world'),
626 moveNativeSelection([0], 0, [1, 0, 0], 5),
629 name: 'Format selection that starts on element and ends on text and retain selection',
634 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
635 '<p class="editor-paragraph"><br></p>' +
636 '<p class="editor-paragraph" dir="ltr">' +
637 '<strong class="editor-text-bold" data-lexical-text="true">Hello</strong><strong class="editor-text-bold" data-lexical-text="true"> world</strong>' +
639 '<p class="editor-paragraph"><br></p>' +
643 anchorPath: [1, 0, 0],
649 insertTokenNode('Hello'),
650 insertText(' world'),
652 moveNativeSelection([1, 0, 0], 2, [2], 0),
655 name: 'Format selection that starts on middle of token node should format complete node',
660 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
661 '<p class="editor-paragraph"><br></p>' +
662 '<p class="editor-paragraph" dir="ltr">' +
663 '<strong class="editor-text-bold" data-lexical-text="true">Hello </strong><strong class="editor-text-bold" data-lexical-text="true">world</strong>' +
665 '<p class="editor-paragraph"><br></p>' +
671 focusPath: [1, 1, 0],
675 insertText('Hello '),
676 insertTokenNode('world'),
678 moveNativeSelection([0], 0, [1, 1, 0], 2),
681 name: 'Format selection that ends on middle of token node should format complete node',
686 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
687 '<p class="editor-paragraph"><br></p>' +
688 '<p class="editor-paragraph" dir="ltr">' +
689 '<strong class="editor-text-bold" data-lexical-text="true">Hello</strong><span data-lexical-text="true"> world</span>' +
691 '<p class="editor-paragraph"><br></p>' +
695 anchorPath: [1, 0, 0],
697 focusPath: [1, 0, 0],
701 insertTokenNode('Hello'),
702 insertText(' world'),
704 moveNativeSelection([1, 0, 0], 2, [1, 0, 0], 3),
707 name: 'Format token node if it is the single one selected',
712 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true">' +
713 '<p class="editor-paragraph"><br></p>' +
714 '<p class="editor-paragraph" dir="ltr">' +
715 '<strong class="editor-text-bold" data-lexical-text="true">Hello </strong><strong class="editor-text-bold" data-lexical-text="true">beautiful</strong><strong class="editor-text-bold" data-lexical-text="true"> world</strong>' +
717 '<p class="editor-paragraph"><br></p>' +
727 insertText('Hello '),
728 insertTokenNode('beautiful'),
729 insertText(' world'),
731 moveNativeSelection([0], 0, [2], 0),
734 name: 'Format selection that contains a token node in the middle should format the token node',
737 // Tests need fixing:
738 // ...GRAPHEME_SCENARIOS.flatMap(({description, grapheme}) => [
740 // name: `Delete backward eliminates entire ${description} (${grapheme})`,
741 // inputs: [insertText(grapheme + grapheme), deleteBackward(1)],
742 // expectedHTML: `<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir=\"ltr\"><span>${grapheme}</span></p></div>`,
743 // expectedSelection: {
744 // anchorPath: [0, 0, 0],
745 // anchorOffset: grapheme.length,
746 // focusPath: [0, 0, 0],
747 // focusOffset: grapheme.length,
749 // setup: emptySetup,
752 // name: `Delete forward eliminates entire ${description} (${grapheme})`,
754 // insertText(grapheme + grapheme),
755 // moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 0),
758 // expectedHTML: `<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir=\"ltr\"><span>${grapheme}</span></p></div>`,
759 // expectedSelection: {
760 // anchorPath: [0, 0, 0],
762 // focusPath: [0, 0, 0],
765 // setup: emptySetup,
768 // name: `Move backward skips over grapheme cluster (${grapheme})`,
769 // inputs: [insertText(grapheme + grapheme), moveBackward(1)],
770 // expectedHTML: `<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir=\"ltr\"><span>${grapheme}${grapheme}</span></p></div>`,
771 // expectedSelection: {
772 // anchorPath: [0, 0, 0],
773 // anchorOffset: grapheme.length,
774 // focusPath: [0, 0, 0],
775 // focusOffset: grapheme.length,
777 // setup: emptySetup,
780 // name: `Move forward skips over grapheme cluster (${grapheme})`,
782 // insertText(grapheme + grapheme),
783 // moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 0),
786 // expectedHTML: `<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p dir=\"ltr\"><span>${grapheme}${grapheme}</span></p></div>`,
787 // expectedSelection: {
788 // anchorPath: [0, 0, 0],
789 // anchorOffset: grapheme.length,
790 // focusPath: [0, 0, 0],
791 // focusOffset: grapheme.length,
793 // setup: emptySetup,
797 // name: 'Jump to beginning and insert',
803 // moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 0),
810 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">abc123</span></p></div>',
811 // expectedSelection: {
812 // anchorPath: [0, 0, 0],
814 // focusPath: [0, 0, 0],
819 // name: 'Select and replace',
821 // insertText('Hello draft!'),
822 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 11),
823 // insertText('lexical'),
826 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello lexical!</span></p></div>',
827 // expectedSelection: {
828 // anchorPath: [0, 0, 0],
830 // focusPath: [0, 0, 0],
835 // name: 'Select and bold',
837 // insertText('Hello draft!'),
838 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 11),
842 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span>' +
843 // '<strong class="editor-text-bold" data-lexical-text="true">draft</strong><span data-lexical-text="true">!</span></p></div>',
844 // expectedSelection: {
845 // anchorPath: [0, 1, 0],
847 // focusPath: [0, 1, 0],
852 // name: 'Select and italic',
854 // insertText('Hello draft!'),
855 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 11),
859 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span>' +
860 // '<em class="editor-text-italic" data-lexical-text="true">draft</em><span data-lexical-text="true">!</span></p></div>',
861 // expectedSelection: {
862 // anchorPath: [0, 1, 0],
864 // focusPath: [0, 1, 0],
869 // name: 'Select and bold + italic',
871 // insertText('Hello draft!'),
872 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 11),
877 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span>' +
878 // '<strong class="editor-text-bold editor-text-italic" data-lexical-text="true">draft</strong><span data-lexical-text="true">!</span></p></div>',
879 // expectedSelection: {
880 // anchorPath: [0, 1, 0],
882 // focusPath: [0, 1, 0],
887 // name: 'Select and underline',
889 // insertText('Hello draft!'),
890 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 11),
891 // formatUnderline(),
894 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span>' +
895 // '<span class="editor-text-underline" data-lexical-text="true">draft</span><span data-lexical-text="true">!</span></p></div>',
896 // expectedSelection: {
897 // anchorPath: [0, 1, 0],
899 // focusPath: [0, 1, 0],
904 // name: 'Select and strikethrough',
906 // insertText('Hello draft!'),
907 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 11),
908 // formatStrikeThrough(),
911 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span>' +
912 // '<span class="editor-text-strikethrough" data-lexical-text="true">draft</span><span data-lexical-text="true">!</span></p></div>',
913 // expectedSelection: {
914 // anchorPath: [0, 1, 0],
916 // focusPath: [0, 1, 0],
921 // name: 'Select and underline + strikethrough',
923 // insertText('Hello draft!'),
924 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 11),
925 // formatUnderline(),
926 // formatStrikeThrough(),
929 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span>' +
930 // '<span class="editor-text-underlineStrikethrough" data-lexical-text="true">draft</span><span data-lexical-text="true">!</span></p></div>',
931 // expectedSelection: {
932 // anchorPath: [0, 1, 0],
934 // focusPath: [0, 1, 0],
939 // name: 'Select and replace all',
941 // insertText('This is broken.'),
942 // moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 15),
943 // insertText('This works!'),
946 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">This works!</span></p></div>',
947 // expectedSelection: {
948 // anchorPath: [0, 0, 0],
950 // focusPath: [0, 0, 0],
955 // name: 'Select and delete',
957 // insertText('A lion.'),
958 // moveNativeSelection([0, 0, 0], 2, [0, 0, 0], 6),
960 // insertText('duck'),
961 // moveNativeSelection([0, 0, 0], 2, [0, 0, 0], 6),
964 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">A duck.</span></p></div>',
965 // expectedSelection: {
966 // anchorPath: [0, 0, 0],
968 // focusPath: [0, 0, 0],
973 // name: 'Inserting a paragraph',
974 // inputs: [insertParagraph()],
976 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true"><br></span></p>' +
977 // '<p class="editor-paragraph"><span data-lexical-text="true"><br></span></p></div>',
978 // expectedSelection: {
979 // anchorPath: [1, 0, 0],
981 // focusPath: [1, 0, 0],
986 // name: 'Inserting a paragraph and then removing it',
987 // inputs: [insertParagraph(), deleteBackward(1)],
989 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true"><br></span></p></div>',
990 // expectedSelection: {
991 // anchorPath: [0, 0, 0],
993 // focusPath: [0, 0, 0],
998 // name: 'Inserting a paragraph part way through text',
1000 // insertText('Hello world'),
1001 // moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 6),
1002 // insertParagraph(),
1005 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span></p>' +
1006 // '<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">world</span></p></div>',
1007 // expectedSelection: {
1008 // anchorPath: [1, 0, 0],
1010 // focusPath: [1, 0, 0],
1015 // name: 'Inserting two paragraphs and then deleting via selection',
1017 // insertText('123'),
1018 // insertParagraph(),
1019 // insertText('456'),
1020 // moveNativeSelection([0, 0, 0], 0, [1, 0, 0], 3),
1021 // deleteBackward(1),
1024 // '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true"><br></span></p></div>',
1025 // expectedSelection: {
1026 // anchorPath: [0, 0, 0],
1028 // focusPath: [0, 0, 0],
1034 whitespaceCharacter: ' ',
1035 whitespaceName: 'space',
1038 whitespaceCharacter: '\u00a0',
1039 whitespaceName: 'non-breaking space',
1042 whitespaceCharacter: '\u2000',
1043 whitespaceName: 'en quad',
1046 whitespaceCharacter: '\u2001',
1047 whitespaceName: 'em quad',
1050 whitespaceCharacter: '\u2002',
1051 whitespaceName: 'en space',
1054 whitespaceCharacter: '\u2003',
1055 whitespaceName: 'em space',
1058 whitespaceCharacter: '\u2004',
1059 whitespaceName: 'three-per-em space',
1062 whitespaceCharacter: '\u2005',
1063 whitespaceName: 'four-per-em space',
1066 whitespaceCharacter: '\u2006',
1067 whitespaceName: 'six-per-em space',
1070 whitespaceCharacter: '\u2007',
1071 whitespaceName: 'figure space',
1074 whitespaceCharacter: '\u2008',
1075 whitespaceName: 'punctuation space',
1078 whitespaceCharacter: '\u2009',
1079 whitespaceName: 'thin space',
1082 whitespaceCharacter: '\u200A',
1083 whitespaceName: 'hair space',
1085 ].flatMap(({whitespaceCharacter, whitespaceName}) => [
1087 expectedHTML: `<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello${printWhitespace(
1088 whitespaceCharacter,
1089 )}</span></p></div>`,
1090 expectedSelection: {
1092 anchorPath: [0, 0, 0],
1094 focusPath: [0, 0, 0],
1097 insertText(`Hello${whitespaceCharacter}world`),
1098 deleteWordBackward(1),
1100 name: `Type two words separated by a ${whitespaceName}, delete word backward from end`,
1103 expectedHTML: `<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">${printWhitespace(
1104 whitespaceCharacter,
1105 )}world</span></p></div>`,
1106 expectedSelection: {
1108 anchorPath: [0, 0, 0],
1110 focusPath: [0, 0, 0],
1113 insertText(`Hello${whitespaceCharacter}world`),
1114 moveNativeSelection([0, 0, 0], 0, [0, 0, 0], 0),
1115 deleteWordForward(1),
1117 name: `Type two words separated by a ${whitespaceName}, delete word forward from beginning`,
1121 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello</span></p></div>',
1122 expectedSelection: {
1124 anchorPath: [0, 0, 0],
1126 focusPath: [0, 0, 0],
1129 insertText(`Hello${whitespaceCharacter}world`),
1130 moveNativeSelection([0, 0, 0], 5, [0, 0, 0], 5),
1131 deleteWordForward(1),
1133 name: `Type two words separated by a ${whitespaceName}, delete word forward from beginning of preceding whitespace`,
1137 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">world</span></p></div>',
1138 expectedSelection: {
1140 anchorPath: [0, 0, 0],
1142 focusPath: [0, 0, 0],
1145 insertText(`Hello${whitespaceCharacter}world`),
1146 moveNativeSelection([0, 0, 0], 6, [0, 0, 0], 6),
1147 deleteWordBackward(1),
1149 name: `Type two words separated by a ${whitespaceName}, delete word backward from end of trailing whitespace`,
1153 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello world</span></p></div>',
1154 expectedSelection: {
1156 anchorPath: [0, 0, 0],
1158 focusPath: [0, 0, 0],
1160 inputs: [insertText('Hello world'), deleteWordBackward(1), undo(1)],
1161 name: `Type a word, delete it and undo the deletion`,
1165 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">Hello </span></p></div>',
1166 expectedSelection: {
1168 anchorPath: [0, 0, 0],
1170 focusPath: [0, 0, 0],
1173 insertText('Hello world'),
1174 deleteWordBackward(1),
1178 name: `Type a word, delete it and undo the deletion`,
1182 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
1183 '<span data-lexical-text="true">this is weird test</span></p></div>',
1184 expectedSelection: {
1186 anchorPath: [0, 0, 0],
1188 focusPath: [0, 0, 0],
1191 insertText('this is weird test'),
1192 moveNativeSelection([0, 0, 0], 14, [0, 0, 0], 14),
1195 name: 'Type a sentence, move the caret to the middle and move with the arrows to the start',
1199 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr">' +
1200 '<span data-lexical-text="true">Hello </span>' +
1201 '<span data-lexical-text="true">Bob</span>' +
1203 expectedSelection: {
1205 anchorPath: [0, 1, 0],
1207 focusPath: [0, 1, 0],
1210 insertText('Hello '),
1211 insertTokenNode('Bob'),
1216 name: 'Type a text and token text, move the caret to the end of the first text',
1220 '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">ABD</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">EFG</span></p></div>',
1221 expectedSelection: {
1223 anchorPath: [0, 0, 0],
1225 focusPath: [0, 0, 0],
1228 pastePlain('ABD\tEFG'),
1232 deleteWordForward(1),
1234 name: 'Paste text, move selection and delete word forward',
1239 suite.forEach((testUnit, i) => {
1240 const name = testUnit.name || 'Test case';
1242 test(name + ` (#${i + 1})`, async () => {
1243 await applySelectionInputs(testUnit.inputs, update, editor!);
1245 // Validate HTML matches
1246 expect(container.innerHTML).toBe(testUnit.expectedHTML);
1248 // Validate selection matches
1249 const rootElement = editor!.getRootElement()!;
1250 const expectedSelection = testUnit.expectedSelection;
1252 assertSelection(rootElement, expectedSelection);
1256 test('insert text one selected node element selection', async () => {
1257 await ReactTestUtils.act(async () => {
1258 await editor!.update(() => {
1259 const root = $getRoot();
1261 const paragraph = root.getFirstChild<ParagraphNode>()!;
1263 const elementNode = $createTestElementNode();
1264 const text = $createTextNode('foo');
1266 paragraph.append(elementNode);
1267 elementNode.append(text);
1269 const selection = $createRangeSelection();
1270 selection.anchor.set(text.__key, 0, 'text');
1271 selection.focus.set(paragraph.__key, 1, 'element');
1273 selection.insertText('');
1275 expect(root.getTextContent()).toBe('');
1280 test('getNodes resolves nested block nodes', async () => {
1281 await ReactTestUtils.act(async () => {
1282 await editor!.update(() => {
1283 const root = $getRoot();
1285 const paragraph = root.getFirstChild<ParagraphNode>()!;
1287 const elementNode = $createTestElementNode();
1288 const text = $createTextNode();
1290 paragraph.append(elementNode);
1291 elementNode.append(text);
1293 const selectedNodes = $getSelection()!.getNodes();
1295 expect(selectedNodes.length).toBe(1);
1296 expect(selectedNodes[0].getKey()).toBe(text.getKey());
1301 describe('Block selection moves when new nodes are inserted', () => {
1304 anchorOffset: number;
1305 focusOffset: number;
1307 paragraph: ElementNode,
1310 expectedAnchor: LexicalNode;
1311 expectedAnchorOffset: number;
1312 expectedFocus: LexicalNode;
1313 expectedFocusOffset: number;
1315 fnBefore?: (paragraph: ElementNode, text: TextNode) => void;
1316 invertSelection?: true;
1319 // Collapsed selection on end; add/remove/replace beginning
1322 fn: (paragraph, text) => {
1323 const newText = $createTextNode('2');
1324 text.insertBefore(newText);
1327 expectedAnchor: paragraph,
1328 expectedAnchorOffset: 3,
1329 expectedFocus: paragraph,
1330 expectedFocusOffset: 3,
1334 name: 'insertBefore - Collapsed selection on end; add beginning',
1338 fn: (paragraph, text) => {
1339 const newText = $createTextNode('2');
1340 text.insertAfter(newText);
1343 expectedAnchor: paragraph,
1344 expectedAnchorOffset: 3,
1345 expectedFocus: paragraph,
1346 expectedFocusOffset: 3,
1350 name: 'insertAfter - Collapsed selection on end; add beginning',
1354 fn: (paragraph, text) => {
1358 expectedAnchor: paragraph,
1359 expectedAnchorOffset: 3,
1360 expectedFocus: paragraph,
1361 expectedFocusOffset: 3,
1365 name: 'splitText - Collapsed selection on end; add beginning',
1369 fn: (paragraph, text) => {
1373 expectedAnchor: paragraph,
1374 expectedAnchorOffset: 0,
1375 expectedFocus: paragraph,
1376 expectedFocusOffset: 0,
1380 name: 'remove - Collapsed selection on end; add beginning',
1384 fn: (paragraph, text) => {
1385 const newText = $createTextNode('replacement');
1386 text.replace(newText);
1389 expectedAnchor: paragraph,
1390 expectedAnchorOffset: 1,
1391 expectedFocus: paragraph,
1392 expectedFocusOffset: 1,
1396 name: 'replace - Collapsed selection on end; replace beginning',
1398 // All selected; add/remove/replace on beginning
1401 fn: (paragraph, text) => {
1402 const newText = $createTextNode('2');
1403 text.insertBefore(newText);
1406 expectedAnchor: text,
1407 expectedAnchorOffset: 0,
1408 expectedFocus: paragraph,
1409 expectedFocusOffset: 3,
1413 name: 'insertBefore - All selected; add on beginning',
1417 fn: (paragraph, originalText) => {
1418 const [, text] = originalText.splitText(1);
1421 expectedAnchor: text,
1422 expectedAnchorOffset: 0,
1423 expectedFocus: paragraph,
1424 expectedFocusOffset: 3,
1428 name: 'splitNodes - All selected; add on beginning',
1432 fn: (paragraph, text) => {
1436 expectedAnchor: paragraph,
1437 expectedAnchorOffset: 0,
1438 expectedFocus: paragraph,
1439 expectedFocusOffset: 0,
1443 name: 'remove - All selected; remove on beginning',
1447 fn: (paragraph, text) => {
1448 const newText = $createTextNode('replacement');
1449 text.replace(newText);
1452 expectedAnchor: paragraph,
1453 expectedAnchorOffset: 0,
1454 expectedFocus: paragraph,
1455 expectedFocusOffset: 1,
1459 name: 'replace - All selected; replace on beginning',
1461 // Selection beginning; add/remove/replace on end
1464 fn: (paragraph, originalText1) => {
1465 const originalText2 = originalText1.getPreviousSibling()!;
1466 const lastChild = paragraph.getLastChild()!;
1467 const newText = $createTextNode('2');
1468 lastChild.insertBefore(newText);
1471 expectedAnchor: originalText2,
1472 expectedAnchorOffset: 0,
1473 expectedFocus: originalText1,
1474 expectedFocusOffset: 0,
1477 fnBefore: (paragraph, originalText1) => {
1478 const originalText2 = $createTextNode('bar');
1479 originalText1.insertBefore(originalText2);
1482 name: 'insertBefore - Selection beginning; add on end',
1486 fn: (paragraph, text) => {
1487 const lastChild = paragraph.getLastChild()!;
1488 const newText = $createTextNode('2');
1489 lastChild.insertAfter(newText);
1492 expectedAnchor: text,
1493 expectedAnchorOffset: 0,
1494 expectedFocus: paragraph,
1495 expectedFocusOffset: 1,
1499 name: 'insertAfter - Selection beginning; add on end',
1503 fn: (paragraph, originalText1) => {
1504 const originalText2 = originalText1.getPreviousSibling()!;
1505 const [, text] = originalText1.splitText(1);
1508 expectedAnchor: originalText2,
1509 expectedAnchorOffset: 0,
1510 expectedFocus: text,
1511 expectedFocusOffset: 0,
1514 fnBefore: (paragraph, originalText1) => {
1515 const originalText2 = $createTextNode('bar');
1516 originalText1.insertBefore(originalText2);
1519 name: 'splitText - Selection beginning; add on end',
1523 fn: (paragraph, text) => {
1524 const lastChild = paragraph.getLastChild()!;
1528 expectedAnchor: text,
1529 expectedAnchorOffset: 0,
1530 expectedFocus: text,
1531 expectedFocusOffset: 3,
1535 name: 'remove - Selection beginning; remove on end',
1539 fn: (paragraph, text) => {
1540 const newText = $createTextNode('replacement');
1541 const lastChild = paragraph.getLastChild()!;
1542 lastChild.replace(newText);
1545 expectedAnchor: paragraph,
1546 expectedAnchorOffset: 0,
1547 expectedFocus: paragraph,
1548 expectedFocusOffset: 1,
1552 name: 'replace - Selection beginning; replace on end',
1554 // All selected; add/remove/replace in end offset [1, 2] -> [1, N, 2]
1557 fn: (paragraph, text) => {
1558 const lastChild = paragraph.getLastChild()!;
1559 const newText = $createTextNode('2');
1560 lastChild.insertBefore(newText);
1563 expectedAnchor: text,
1564 expectedAnchorOffset: 0,
1565 expectedFocus: paragraph,
1566 expectedFocusOffset: 2,
1570 name: 'insertBefore - All selected; add in end offset',
1574 fn: (paragraph, text) => {
1575 const newText = $createTextNode('2');
1576 text.insertAfter(newText);
1579 expectedAnchor: text,
1580 expectedAnchorOffset: 0,
1581 expectedFocus: paragraph,
1582 expectedFocusOffset: 2,
1586 name: 'insertAfter - All selected; add in end offset',
1590 fn: (paragraph, originalText1) => {
1591 const originalText2 = originalText1.getPreviousSibling()!;
1592 const [, text] = originalText1.splitText(1);
1595 expectedAnchor: originalText2,
1596 expectedAnchorOffset: 0,
1597 expectedFocus: text,
1598 expectedFocusOffset: 0,
1601 fnBefore: (paragraph, originalText1) => {
1602 const originalText2 = $createTextNode('bar');
1603 originalText1.insertBefore(originalText2);
1606 name: 'splitText - All selected; add in end offset',
1610 fn: (paragraph, originalText1) => {
1611 const lastChild = paragraph.getLastChild()!;
1615 expectedAnchor: originalText1,
1616 expectedAnchorOffset: 0,
1617 expectedFocus: originalText1,
1618 expectedFocusOffset: 3,
1621 fnBefore: (paragraph, originalText1) => {
1622 const originalText2 = $createTextNode('bar');
1623 originalText1.insertBefore(originalText2);
1626 name: 'remove - All selected; remove in end offset',
1630 fn: (paragraph, originalText1) => {
1631 const newText = $createTextNode('replacement');
1632 const lastChild = paragraph.getLastChild()!;
1633 lastChild.replace(newText);
1636 expectedAnchor: paragraph,
1637 expectedAnchorOffset: 1,
1638 expectedFocus: paragraph,
1639 expectedFocusOffset: 2,
1642 fnBefore: (paragraph, originalText1) => {
1643 const originalText2 = $createTextNode('bar');
1644 originalText1.insertBefore(originalText2);
1647 name: 'replace - All selected; replace in end offset',
1649 // All selected; add/remove/replace in middle [1, 2, 3] -> [1, 2, N, 3]
1652 fn: (paragraph, originalText1) => {
1653 const originalText2 = originalText1.getPreviousSibling()!;
1654 const lastChild = paragraph.getLastChild()!;
1655 const newText = $createTextNode('2');
1656 lastChild.insertBefore(newText);
1659 expectedAnchor: originalText2,
1660 expectedAnchorOffset: 0,
1661 expectedFocus: paragraph,
1662 expectedFocusOffset: 3,
1665 fnBefore: (paragraph, originalText1) => {
1666 const originalText2 = $createTextNode('bar');
1667 originalText1.insertBefore(originalText2);
1670 name: 'insertBefore - All selected; add in middle',
1674 fn: (paragraph, originalText1) => {
1675 const originalText2 = originalText1.getPreviousSibling()!;
1676 const newText = $createTextNode('2');
1677 originalText1.insertAfter(newText);
1680 expectedAnchor: originalText2,
1681 expectedAnchorOffset: 0,
1682 expectedFocus: paragraph,
1683 expectedFocusOffset: 3,
1686 fnBefore: (paragraph, originalText1) => {
1687 const originalText2 = $createTextNode('bar');
1688 originalText1.insertBefore(originalText2);
1691 name: 'insertAfter - All selected; add in middle',
1695 fn: (paragraph, originalText1) => {
1696 const originalText2 = originalText1.getPreviousSibling()!;
1697 originalText1.splitText(1);
1700 expectedAnchor: originalText2,
1701 expectedAnchorOffset: 0,
1702 expectedFocus: paragraph,
1703 expectedFocusOffset: 3,
1706 fnBefore: (paragraph, originalText1) => {
1707 const originalText2 = $createTextNode('bar');
1708 originalText1.insertBefore(originalText2);
1711 name: 'splitText - All selected; add in middle',
1715 fn: (paragraph, originalText1) => {
1716 const originalText2 = originalText1.getPreviousSibling()!;
1717 originalText1.remove();
1720 expectedAnchor: originalText2,
1721 expectedAnchorOffset: 0,
1722 expectedFocus: paragraph,
1723 expectedFocusOffset: 1,
1726 fnBefore: (paragraph, originalText1) => {
1727 const originalText2 = $createTextNode('bar');
1728 originalText1.insertBefore(originalText2);
1731 name: 'remove - All selected; remove in middle',
1735 fn: (paragraph, originalText1) => {
1736 const newText = $createTextNode('replacement');
1737 originalText1.replace(newText);
1740 expectedAnchor: paragraph,
1741 expectedAnchorOffset: 0,
1742 expectedFocus: paragraph,
1743 expectedFocusOffset: 2,
1746 fnBefore: (paragraph, originalText1) => {
1747 const originalText2 = $createTextNode('bar');
1748 originalText1.insertBefore(originalText2);
1751 name: 'replace - All selected; replace in middle',
1756 fn: (paragraph, originalText1) => {
1757 const originalText2 = paragraph.getLastChild()!;
1758 const newText = $createTextNode('new');
1759 originalText1.insertBefore(newText);
1762 expectedAnchor: originalText2,
1763 expectedAnchorOffset: 'bar'.length,
1764 expectedFocus: originalText2,
1765 expectedFocusOffset: 'bar'.length,
1768 fnBefore: (paragraph, originalText1) => {
1769 const originalText2 = $createTextNode('bar');
1770 paragraph.append(originalText2);
1773 name: "Selection resolves to the end of text node when it's at the end (1)",
1777 fn: (paragraph, originalText1) => {
1778 const originalText2 = paragraph.getLastChild()!;
1779 const newText = $createTextNode('new');
1780 originalText1.insertBefore(newText);
1783 expectedAnchor: originalText1,
1784 expectedAnchorOffset: 0,
1785 expectedFocus: originalText2,
1786 expectedFocusOffset: 'bar'.length,
1789 fnBefore: (paragraph, originalText1) => {
1790 const originalText2 = $createTextNode('bar');
1791 paragraph.append(originalText2);
1794 name: "Selection resolves to the end of text node when it's at the end (2)",
1798 fn: (paragraph, originalText1) => {
1799 originalText1.getNextSibling()!.remove();
1802 expectedAnchor: originalText1,
1803 expectedAnchorOffset: 3,
1804 expectedFocus: originalText1,
1805 expectedFocusOffset: 3,
1809 name: 'remove - Remove with collapsed selection at offset #4221',
1813 fn: (paragraph, originalText1) => {
1814 originalText1.getNextSibling()!.remove();
1817 expectedAnchor: originalText1,
1818 expectedAnchorOffset: 0,
1819 expectedFocus: originalText1,
1820 expectedFocusOffset: 3,
1824 name: 'remove - Remove with non-collapsed selection at offset',
1828 .flatMap((testCase) => {
1829 // Test inverse selection
1832 anchorOffset: testCase.focusOffset,
1833 focusOffset: testCase.anchorOffset,
1834 invertSelection: true,
1835 name: testCase.name + ' (inverse selection)',
1837 return [testCase, inverse];
1851 // eslint-disable-next-line no-only-tests/no-only-tests
1852 const test_ = only === true ? test.only : test;
1853 test_(name, async () => {
1854 await ReactTestUtils.act(async () => {
1855 await editor!.update(() => {
1856 const root = $getRoot();
1858 const paragraph = root.getFirstChild<ParagraphNode>()!;
1859 const textNode = $createTextNode('foo');
1860 // Note: line break can't be selected by the DOM
1861 const linebreak = $createLineBreakNode();
1863 const selection = $getSelection();
1865 if (!$isRangeSelection(selection)) {
1869 const anchor = selection.anchor;
1870 const focus = selection.focus;
1872 paragraph.append(textNode, linebreak);
1874 fnBefore(paragraph, textNode);
1876 anchor.set(paragraph.getKey(), anchorOffset, 'element');
1877 focus.set(paragraph.getKey(), focusOffset, 'element');
1881 expectedAnchorOffset,
1883 expectedFocusOffset,
1884 } = fn(paragraph, textNode);
1886 if (invertSelection !== true) {
1887 expect(selection.anchor.key).toBe(expectedAnchor.__key);
1888 expect(selection.anchor.offset).toBe(expectedAnchorOffset);
1889 expect(selection.focus.key).toBe(expectedFocus.__key);
1890 expect(selection.focus.offset).toBe(expectedFocusOffset);
1892 expect(selection.anchor.key).toBe(expectedFocus.__key);
1893 expect(selection.anchor.offset).toBe(expectedFocusOffset);
1894 expect(selection.focus.key).toBe(expectedAnchor.__key);
1895 expect(selection.focus.offset).toBe(expectedAnchorOffset);
1904 describe('Selection correctly resolves to a sibling ElementNode when a node is removed', () => {
1905 test('', async () => {
1906 await ReactTestUtils.act(async () => {
1907 await editor!.update(() => {
1908 const root = $getRoot();
1910 const listNode = $createListNode('bullet');
1911 const listItemNode = $createListItemNode();
1912 const paragraph = $createParagraphNode();
1914 root.append(listNode);
1916 listNode.append(listItemNode);
1917 listItemNode.select();
1918 listNode.insertAfter(paragraph);
1919 listItemNode.remove();
1921 const selection = $getSelection();
1923 if (!$isRangeSelection(selection)) {
1927 expect(selection.anchor.getNode().__type).toBe('paragraph');
1928 expect(selection.focus.getNode().__type).toBe('paragraph');
1934 describe('Selection correctly resolves to a sibling ElementNode when a selected node child is removed', () => {
1935 test('', async () => {
1936 await ReactTestUtils.act(async () => {
1937 let paragraphNodeKey: string;
1938 await editor!.update(() => {
1939 const root = $getRoot();
1941 const paragraphNode = $createParagraphNode();
1942 paragraphNodeKey = paragraphNode.__key;
1943 const listNode = $createListNode('number');
1944 const listItemNode1 = $createListItemNode();
1945 const textNode1 = $createTextNode('foo');
1946 const listItemNode2 = $createListItemNode();
1947 const listNode2 = $createListNode('number');
1948 const listItemNode2x1 = $createListItemNode();
1950 listNode.append(listItemNode1, listItemNode2);
1951 listItemNode1.append(textNode1);
1952 listItemNode2.append(listNode2);
1953 listNode2.append(listItemNode2x1);
1954 root.append(paragraphNode, listNode);
1956 listItemNode2.select();
1960 await editor!.getEditorState().read(() => {
1961 const selection = $assertRangeSelection($getSelection());
1962 expect(selection.anchor.key).toBe(paragraphNodeKey);
1963 expect(selection.focus.key).toBe(paragraphNodeKey);
1969 describe('Selection correctly resolves to a sibling ElementNode that has multiple children with the correct offset when a node is removed', () => {
1970 test('', async () => {
1971 await ReactTestUtils.act(async () => {
1972 await editor!.update(() => {
1981 const root = $getRoot();
1983 const paragraph = $createParagraphNode();
1984 const link = $createLinkNode('bullet');
1985 const textOne = $createTextNode('Hello');
1986 const br = $createLineBreakNode();
1987 const textTwo = $createTextNode('world');
1988 const textThree = $createTextNode(' ');
1990 root.append(paragraph);
1991 link.append(textOne);
1993 link.append(textTwo);
1995 paragraph.append(link);
1996 paragraph.append(textThree);
2002 const expectedKey = link.getKey();
2004 const selection = $getSelection();
2006 if (!$isRangeSelection(selection)) {
2010 const {anchor, focus} = selection;
2012 expect(anchor.getNode().getKey()).toBe(expectedKey);
2013 expect(focus.getNode().getKey()).toBe(expectedKey);
2014 expect(anchor.offset).toBe(3);
2015 expect(focus.offset).toBe(3);
2021 test('isBackward', async () => {
2022 await ReactTestUtils.act(async () => {
2023 await editor!.update(() => {
2024 const root = $getRoot();
2026 const paragraph = root.getFirstChild<ParagraphNode>()!;
2027 const paragraphKey = paragraph.getKey();
2028 const textNode = $createTextNode('foo');
2029 const textNodeKey = textNode.getKey();
2030 // Note: line break can't be selected by the DOM
2031 const linebreak = $createLineBreakNode();
2033 const selection = $getSelection();
2035 if (!$isRangeSelection(selection)) {
2039 const anchor = selection.anchor;
2040 const focus = selection.focus;
2041 paragraph.append(textNode, linebreak);
2042 anchor.set(textNodeKey, 0, 'text');
2043 focus.set(textNodeKey, 0, 'text');
2045 expect(selection.isBackward()).toBe(false);
2047 anchor.set(paragraphKey, 1, 'element');
2048 focus.set(paragraphKey, 1, 'element');
2050 expect(selection.isBackward()).toBe(false);
2052 anchor.set(paragraphKey, 0, 'element');
2053 focus.set(paragraphKey, 1, 'element');
2055 expect(selection.isBackward()).toBe(false);
2057 anchor.set(paragraphKey, 1, 'element');
2058 focus.set(paragraphKey, 0, 'element');
2060 expect(selection.isBackward()).toBe(true);
2065 describe('Decorator text content for selection', () => {
2069 textNode1: TextNode;
2070 textNode2: TextNode;
2071 decorator: DecoratorNode<unknown>;
2072 paragraph: ParagraphNode;
2076 invertSelection?: true;
2079 fn: ({textNode1, anchor, focus}) => {
2080 anchor.set(textNode1.getKey(), 1, 'text');
2081 focus.set(textNode1.getKey(), 1, 'text');
2085 name: 'Not included if cursor right before it',
2088 fn: ({textNode2, anchor, focus}) => {
2089 anchor.set(textNode2.getKey(), 0, 'text');
2090 focus.set(textNode2.getKey(), 0, 'text');
2094 name: 'Not included if cursor right after it',
2097 fn: ({textNode1, textNode2, decorator, anchor, focus}) => {
2098 anchor.set(textNode1.getKey(), 1, 'text');
2099 focus.set(textNode2.getKey(), 0, 'text');
2101 return decorator.getTextContent();
2103 name: 'Included if decorator is selected within text',
2106 fn: ({textNode1, textNode2, decorator, anchor, focus}) => {
2107 anchor.set(textNode1.getKey(), 0, 'text');
2108 focus.set(textNode2.getKey(), 0, 'text');
2110 return textNode1.getTextContent() + decorator.getTextContent();
2112 name: 'Included if decorator is selected with another node before it',
2115 fn: ({textNode1, textNode2, decorator, anchor, focus}) => {
2116 anchor.set(textNode1.getKey(), 1, 'text');
2117 focus.set(textNode2.getKey(), 1, 'text');
2119 return decorator.getTextContent() + textNode2.getTextContent();
2121 name: 'Included if decorator is selected with another node after it',
2124 fn: ({paragraph, textNode1, textNode2, decorator, anchor, focus}) => {
2127 anchor.set(paragraph.getKey(), 0, 'element');
2128 focus.set(paragraph.getKey(), 1, 'element');
2130 return decorator.getTextContent();
2132 name: 'Included if decorator is selected as the only node',
2136 .flatMap((testCase) => {
2139 invertSelection: true,
2140 name: testCase.name + ' (inverse selection)',
2143 return [testCase, inverse];
2145 .forEach(({name, fn, invertSelection}) => {
2146 it(name, async () => {
2147 await ReactTestUtils.act(async () => {
2148 await editor!.update(() => {
2149 const root = $getRoot();
2151 const paragraph = root.getFirstChild<ParagraphNode>()!;
2152 const textNode1 = $createTextNode('1');
2153 const textNode2 = $createTextNode('2');
2154 const decorator = $createTestDecoratorNode();
2156 paragraph.append(textNode1, decorator, textNode2);
2158 const selection = $getSelection();
2160 if (!$isRangeSelection(selection)) {
2164 const expectedTextContent = fn({
2165 anchor: invertSelection ? selection.focus : selection.anchor,
2167 focus: invertSelection ? selection.anchor : selection.focus,
2173 expect(selection.getTextContent()).toBe(expectedTextContent);
2180 describe('insertParagraph', () => {
2181 test('three text nodes at offset 0 on third node', async () => {
2182 const testEditor = createTestEditor();
2183 const element = document.createElement('div');
2184 testEditor.setRootElement(element);
2186 await testEditor.update(() => {
2187 const root = $getRoot();
2189 const paragraph = $createParagraphNode();
2190 const text = $createTextNode('Hello ');
2191 const text2 = $createTextNode('awesome');
2193 text2.toggleFormat('bold');
2195 const text3 = $createTextNode(' world');
2197 paragraph.append(text, text2, text3);
2198 root.append(paragraph);
2201 key: text3.getKey(),
2207 key: text3.getKey(),
2212 const selection = $getSelection();
2214 if (!$isRangeSelection(selection)) {
2218 selection.insertParagraph();
2221 expect(element.innerHTML).toBe(
2222 '<p dir="ltr"><span data-lexical-text="true">Hello </span><strong data-lexical-text="true">awesome</strong></p><p dir="ltr"><span data-lexical-text="true"> world</span></p>',
2226 test('four text nodes at offset 0 on third node', async () => {
2227 const testEditor = createTestEditor();
2228 const element = document.createElement('div');
2229 testEditor.setRootElement(element);
2231 await testEditor.update(() => {
2232 const root = $getRoot();
2234 const paragraph = $createParagraphNode();
2235 const text = $createTextNode('Hello ');
2236 const text2 = $createTextNode('awesome ');
2238 text2.toggleFormat('bold');
2240 const text3 = $createTextNode('beautiful');
2241 const text4 = $createTextNode(' world');
2243 text4.toggleFormat('bold');
2245 paragraph.append(text, text2, text3, text4);
2246 root.append(paragraph);
2249 key: text3.getKey(),
2255 key: text3.getKey(),
2260 const selection = $getSelection();
2262 if (!$isRangeSelection(selection)) {
2266 selection.insertParagraph();
2269 expect(element.innerHTML).toBe(
2270 '<p dir="ltr"><span data-lexical-text="true">Hello </span><strong data-lexical-text="true">awesome </strong></p><p dir="ltr"><span data-lexical-text="true">beautiful</span><strong data-lexical-text="true"> world</strong></p>',
2274 it('adjust offset for inline elements text formatting', async () => {
2277 await ReactTestUtils.act(async () => {
2278 await editor!.update(() => {
2279 const root = $getRoot();
2281 const text1 = $createTextNode('--');
2282 const text2 = $createTextNode('abc');
2283 const text3 = $createTextNode('--');
2286 $createParagraphNode().append(
2288 $createLinkNode('https://lexical.dev').append(text2),
2294 key: text1.getKey(),
2300 key: text3.getKey(),
2305 const selection = $getSelection();
2307 if (!$isRangeSelection(selection)) {
2311 selection.formatText('bold');
2313 expect(text2.hasFormat('bold')).toBe(true);
2319 describe('Node.replace', () => {
2320 let text1: TextNode,
2323 paragraph: ParagraphNode,
2324 testEditor: LexicalEditor;
2326 beforeEach(async () => {
2327 testEditor = createTestEditor();
2329 const element = document.createElement('div');
2330 testEditor.setRootElement(element);
2332 await testEditor.update(() => {
2333 const root = $getRoot();
2335 paragraph = $createParagraphNode();
2336 text1 = $createTextNode('Hello ');
2337 text2 = $createTextNode('awesome');
2339 text2.toggleFormat('bold');
2341 text3 = $createTextNode(' world');
2343 paragraph.append(text1, text2, text3);
2344 root.append(paragraph);
2351 text2.replace($createTestDecoratorNode());
2358 name: 'moves selection to to next text node if replacing with decorator',
2362 text3.replace($createTestDecoratorNode());
2364 text2.replace($createTestDecoratorNode());
2367 key: paragraph.__key,
2371 name: 'moves selection to parent if next sibling is not a text node',
2373 ].forEach((testCase) => {
2374 test(testCase.name, async () => {
2375 await testEditor.update(() => {
2376 const {key, offset} = testCase.fn();
2378 const selection = $getSelection();
2380 if (!$isRangeSelection(selection)) {
2384 expect(selection.anchor.key).toBe(key);
2385 expect(selection.anchor.offset).toBe(offset);
2386 expect(selection.focus.key).toBe(key);
2387 expect(selection.focus.offset).toBe(offset);
2393 describe('Testing that $getStyleObjectFromRawCSS handles unformatted css text ', () => {
2394 test('', async () => {
2395 const testEditor = createTestEditor();
2396 const element = document.createElement('div');
2397 testEditor.setRootElement(element);
2399 await testEditor.update(() => {
2400 const root = $getRoot();
2401 const paragraph = $createParagraphNode();
2402 const textNode = $createTextNode('Hello, World!');
2404 ' font-family : Arial ; color : red ;top : 50px',
2406 $addNodeStyle(textNode);
2407 paragraph.append(textNode);
2408 root.append(paragraph);
2410 const selection = $createRangeSelection();
2411 $setSelection(selection);
2412 selection.insertParagraph();
2414 key: textNode.getKey(),
2420 key: textNode.getKey(),
2425 const cssFontFamilyValue = $getSelectionStyleValueForProperty(
2430 expect(cssFontFamilyValue).toBe('Arial');
2432 const cssColorValue = $getSelectionStyleValueForProperty(
2437 expect(cssColorValue).toBe('red');
2439 const cssTopValue = $getSelectionStyleValueForProperty(
2444 expect(cssTopValue).toBe('50px');
2449 describe('Testing that getStyleObjectFromRawCSS handles values with colons', () => {
2450 test('', async () => {
2451 const testEditor = createTestEditor();
2452 const element = document.createElement('div');
2453 testEditor.setRootElement(element);
2455 await testEditor.update(() => {
2456 const root = $getRoot();
2457 const paragraph = $createParagraphNode();
2458 const textNode = $createTextNode('Hello, World!');
2460 'font-family: double:prefix:Arial; color: color:white; font-size: 30px',
2462 $addNodeStyle(textNode);
2463 paragraph.append(textNode);
2464 root.append(paragraph);
2466 const selection = $createRangeSelection();
2467 $setSelection(selection);
2468 selection.insertParagraph();
2470 key: textNode.getKey(),
2476 key: textNode.getKey(),
2481 const cssFontFamilyValue = $getSelectionStyleValueForProperty(
2486 expect(cssFontFamilyValue).toBe('double:prefix:Arial');
2488 const cssColorValue = $getSelectionStyleValueForProperty(
2493 expect(cssColorValue).toBe('color:white');
2495 const cssFontSizeValue = $getSelectionStyleValueForProperty(
2500 expect(cssFontSizeValue).toBe('30px');
2505 describe('$patchStyle', () => {
2506 it('should patch the style with the new style object', async () => {
2507 await ReactTestUtils.act(async () => {
2508 await editor!.update(() => {
2509 const root = $getRoot();
2510 const paragraph = $createParagraphNode();
2511 const textNode = $createTextNode('Hello, World!');
2512 textNode.setStyle('font-family: serif; color: red;');
2513 $addNodeStyle(textNode);
2514 paragraph.append(textNode);
2515 root.append(paragraph);
2517 const selection = $createRangeSelection();
2518 $setSelection(selection);
2519 selection.insertParagraph();
2521 key: textNode.getKey(),
2527 key: textNode.getKey(),
2534 'font-family': 'Arial',
2537 $patchStyleText(selection, newStyle);
2539 const cssFontFamilyValue = $getSelectionStyleValueForProperty(
2544 expect(cssFontFamilyValue).toBe('Arial');
2546 const cssColorValue = $getSelectionStyleValueForProperty(
2551 expect(cssColorValue).toBe('blue');
2556 it('should patch the style with property function', async () => {
2557 await ReactTestUtils.act(async () => {
2558 await editor!.update(() => {
2559 const currentColor = 'red';
2560 const nextColor = 'blue';
2562 const root = $getRoot();
2563 const paragraph = $createParagraphNode();
2564 const textNode = $createTextNode('Hello, World!');
2565 textNode.setStyle(`color: ${currentColor};`);
2566 $addNodeStyle(textNode);
2567 paragraph.append(textNode);
2568 root.append(paragraph);
2570 const selection = $createRangeSelection();
2571 $setSelection(selection);
2572 selection.insertParagraph();
2574 key: textNode.getKey(),
2580 key: textNode.getKey(),
2587 (current: string | null, target: LexicalNode | RangeSelection) =>
2592 $patchStyleText(selection, newStyle);
2594 const cssColorValue = $getSelectionStyleValueForProperty(
2600 expect(cssColorValue).toBe(nextColor);
2601 expect(newStyle.color).toHaveBeenCalledTimes(1);
2603 const lastCall = newStyle.color.mock.lastCall!;
2604 expect(lastCall[0]).toBe(currentColor);
2605 // @ts-ignore - It expected to be a LexicalNode
2606 expect($isTextNode(lastCall[1])).toBeTruthy();
2612 describe('$setBlocksType', () => {
2613 test('Collapsed selection in text', async () => {
2614 const testEditor = createTestEditor();
2615 const element = document.createElement('div');
2616 testEditor.setRootElement(element);
2618 await testEditor.update(() => {
2619 const root = $getRoot();
2620 const paragraph1 = $createParagraphNode();
2621 const text1 = $createTextNode('text 1');
2622 const paragraph2 = $createParagraphNode();
2623 const text2 = $createTextNode('text 2');
2624 root.append(paragraph1, paragraph2);
2625 paragraph1.append(text1);
2626 paragraph2.append(text2);
2628 const selection = $createRangeSelection();
2629 $setSelection(selection);
2632 offset: text1.__text.length,
2637 offset: text1.__text.length,
2641 $setBlocksType(selection, () => {
2642 return $createHeadingNode('h1');
2645 const rootChildren = root.getChildren();
2646 expect(rootChildren[0].__type).toBe('heading');
2647 expect(rootChildren[1].__type).toBe('paragraph');
2648 expect(rootChildren.length).toBe(2);
2652 test('Collapsed selection in element', async () => {
2653 const testEditor = createTestEditor();
2654 const element = document.createElement('div');
2655 testEditor.setRootElement(element);
2657 await testEditor.update(() => {
2658 const root = $getRoot();
2659 const paragraph1 = $createParagraphNode();
2660 const paragraph2 = $createParagraphNode();
2661 root.append(paragraph1, paragraph2);
2663 const selection = $createRangeSelection();
2664 $setSelection(selection);
2676 $setBlocksType(selection, () => {
2677 return $createHeadingNode('h1');
2680 const rootChildren = root.getChildren();
2681 expect(rootChildren[0].__type).toBe('heading');
2682 expect(rootChildren[1].__type).toBe('paragraph');
2683 expect(rootChildren.length).toBe(2);
2687 test('Two elements, same top-element', async () => {
2688 const testEditor = createTestEditor();
2689 const element = document.createElement('div');
2690 testEditor.setRootElement(element);
2692 await testEditor.update(() => {
2693 const root = $getRoot();
2694 const paragraph1 = $createParagraphNode();
2695 const text1 = $createTextNode('text 1');
2696 const paragraph2 = $createParagraphNode();
2697 const text2 = $createTextNode('text 2');
2698 root.append(paragraph1, paragraph2);
2699 paragraph1.append(text1);
2700 paragraph2.append(text2);
2702 const selection = $createRangeSelection();
2703 $setSelection(selection);
2711 offset: text1.__text.length,
2715 $setBlocksType(selection, () => {
2716 return $createHeadingNode('h1');
2719 const rootChildren = root.getChildren();
2720 expect(rootChildren[0].__type).toBe('heading');
2721 expect(rootChildren[1].__type).toBe('heading');
2722 expect(rootChildren.length).toBe(2);
2726 test('Two empty elements, same top-element', async () => {
2727 const testEditor = createTestEditor();
2728 const element = document.createElement('div');
2729 testEditor.setRootElement(element);
2731 await testEditor.update(() => {
2732 const root = $getRoot();
2733 const paragraph1 = $createParagraphNode();
2734 const paragraph2 = $createParagraphNode();
2735 root.append(paragraph1, paragraph2);
2737 const selection = $createRangeSelection();
2738 $setSelection(selection);
2740 key: paragraph1.__key,
2745 key: paragraph2.__key,
2750 $setBlocksType(selection, () => {
2751 return $createHeadingNode('h1');
2754 const rootChildren = root.getChildren();
2755 expect(rootChildren[0].__type).toBe('heading');
2756 expect(rootChildren[1].__type).toBe('heading');
2757 expect(rootChildren.length).toBe(2);
2758 const sel = $getSelection()!;
2759 expect(sel.getNodes().length).toBe(2);
2763 test('Two elements, same top-element', async () => {
2764 const testEditor = createTestEditor();
2765 const element = document.createElement('div');
2766 testEditor.setRootElement(element);
2768 await testEditor.update(() => {
2769 const root = $getRoot();
2770 const paragraph1 = $createParagraphNode();
2771 const text1 = $createTextNode('text 1');
2772 const paragraph2 = $createParagraphNode();
2773 const text2 = $createTextNode('text 2');
2774 root.append(paragraph1, paragraph2);
2775 paragraph1.append(text1);
2776 paragraph2.append(text2);
2778 const selection = $createRangeSelection();
2779 $setSelection(selection);
2787 offset: text1.__text.length,
2791 $setBlocksType(selection, () => {
2792 return $createHeadingNode('h1');
2795 const rootChildren = root.getChildren();
2796 expect(rootChildren[0].__type).toBe('heading');
2797 expect(rootChildren[1].__type).toBe('heading');
2798 expect(rootChildren.length).toBe(2);
2802 test('Collapsed in element inside top-element', async () => {
2803 const testEditor = createTestEditor();
2804 const element = document.createElement('div');
2805 testEditor.setRootElement(element);
2807 await testEditor.update(() => {
2808 const root = $getRoot();
2809 const table = $createTableNodeWithDimensions(1, 1);
2810 const row = table.getFirstChild();
2811 invariant($isElementNode(row));
2812 const column = row.getFirstChild();
2813 invariant($isElementNode(column));
2814 const paragraph = column.getFirstChild();
2815 invariant($isElementNode(paragraph));
2816 if (paragraph.getFirstChild()) {
2817 paragraph.getFirstChild()!.remove();
2821 const selection = $createRangeSelection();
2822 $setSelection(selection);
2824 key: paragraph.__key,
2829 key: paragraph.__key,
2834 const columnChildrenPrev = column.getChildren();
2835 expect(columnChildrenPrev[0].__type).toBe('paragraph');
2836 $setBlocksType(selection, () => {
2837 return $createHeadingNode('h1');
2840 const columnChildrenAfter = column.getChildren();
2841 expect(columnChildrenAfter[0].__type).toBe('heading');
2842 expect(columnChildrenAfter.length).toBe(1);
2846 test('Collapsed in text inside top-element', async () => {
2847 const testEditor = createTestEditor();
2848 const element = document.createElement('div');
2849 testEditor.setRootElement(element);
2851 await testEditor.update(() => {
2852 const root = $getRoot();
2853 const table = $createTableNodeWithDimensions(1, 1);
2854 const row = table.getFirstChild();
2855 invariant($isElementNode(row));
2856 const column = row.getFirstChild();
2857 invariant($isElementNode(column));
2858 const paragraph = column.getFirstChild();
2859 invariant($isElementNode(paragraph));
2860 const text = $createTextNode('foo');
2862 paragraph.append(text);
2864 const selectionz = $createRangeSelection();
2865 $setSelection(selectionz);
2868 offset: text.__text.length,
2873 offset: text.__text.length,
2876 const selection = $getSelection() as RangeSelection;
2878 const columnChildrenPrev = column.getChildren();
2879 expect(columnChildrenPrev[0].__type).toBe('paragraph');
2880 $setBlocksType(selection, () => {
2881 return $createHeadingNode('h1');
2884 const columnChildrenAfter = column.getChildren();
2885 expect(columnChildrenAfter[0].__type).toBe('heading');
2886 expect(columnChildrenAfter.length).toBe(1);
2890 test('Full editor selection with a mix of top-elements', async () => {
2891 const testEditor = createTestEditor();
2892 const element = document.createElement('div');
2893 testEditor.setRootElement(element);
2895 await testEditor.update(() => {
2896 const root = $getRoot();
2898 const paragraph1 = $createParagraphNode();
2899 const paragraph2 = $createParagraphNode();
2900 const text1 = $createTextNode();
2901 const text2 = $createTextNode();
2902 paragraph1.append(text1);
2903 paragraph2.append(text2);
2904 root.append(paragraph1, paragraph2);
2906 const table = $createTableNodeWithDimensions(1, 2);
2907 const row = table.getFirstChild();
2908 invariant($isElementNode(row));
2909 const columns = row.getChildren();
2912 const column1 = columns[0];
2913 const paragraph3 = $createParagraphNode();
2914 const paragraph4 = $createParagraphNode();
2915 const text3 = $createTextNode();
2916 const text4 = $createTextNode();
2917 paragraph1.append(text3);
2918 paragraph2.append(text4);
2919 invariant($isElementNode(column1));
2920 column1.append(paragraph3, paragraph4);
2922 const column2 = columns[1];
2923 const paragraph5 = $createParagraphNode();
2924 const paragraph6 = $createParagraphNode();
2925 invariant($isElementNode(column2));
2926 column2.append(paragraph5, paragraph6);
2928 const paragraph7 = $createParagraphNode();
2929 root.append(paragraph7);
2931 const selectionz = $createRangeSelection();
2932 $setSelection(selectionz);
2934 key: paragraph1.__key,
2939 key: paragraph7.__key,
2943 const selection = $getSelection() as RangeSelection;
2945 $setBlocksType(selection, () => {
2946 return $createHeadingNode('h1');
2948 expect(JSON.stringify(testEditor._pendingEditorState?.toJSON())).toBe(
2949 '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":3,"rowSpan":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"tablecell","version":1,"backgroundColor":null,"colSpan":1,"headerState":1,"rowSpan":1}],"direction":null,"format":"","indent":0,"type":"tablerow","version":1}],"direction":null,"format":"","indent":0,"type":"table","version":1},{"children":[],"direction":null,"format":"","indent":0,"type":"heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
2954 test('Paragraph with links to heading with links', async () => {
2955 const testEditor = createTestEditor();
2956 const element = document.createElement('div');
2957 testEditor.setRootElement(element);
2959 await testEditor.update(() => {
2960 const root = $getRoot();
2961 const paragraph = $createParagraphNode();
2962 const text1 = $createTextNode('Links: ');
2963 const text2 = $createTextNode('link1');
2964 const text3 = $createTextNode('link2');
2968 $createLinkNode('https://lexical.dev').append(text2),
2969 $createTextNode(' '),
2970 $createLinkNode('https://playground.lexical.dev').append(text3),
2974 const paragraphChildrenKeys = [...paragraph.getChildrenKeys()];
2975 const selection = $createRangeSelection();
2976 $setSelection(selection);
2978 key: text1.getKey(),
2983 key: text3.getKey(),
2988 $setBlocksType(selection, () => {
2989 return $createHeadingNode('h1');
2992 const rootChildren = root.getChildren();
2993 expect(rootChildren.length).toBe(1);
2994 invariant($isElementNode(rootChildren[0]));
2995 expect(rootChildren[0].getType()).toBe('heading');
2996 expect(rootChildren[0].getChildrenKeys()).toEqual(
2997 paragraphChildrenKeys,
3002 test('Nested list', async () => {
3003 const testEditor = createTestEditor();
3004 const element = document.createElement('div');
3005 testEditor.setRootElement(element);
3007 await testEditor.update(() => {
3008 const root = $getRoot();
3009 const ul1 = $createListNode('bullet');
3010 const text1 = $createTextNode('1');
3011 const li1 = $createListItemNode().append(text1);
3012 const li1_wrapper = $createListItemNode();
3013 const ul2 = $createListNode('bullet');
3014 const text1_1 = $createTextNode('1.1');
3015 const li1_1 = $createListItemNode().append(text1_1);
3016 ul1.append(li1, li1_wrapper);
3017 li1_wrapper.append(ul2);
3021 const selection = $createRangeSelection();
3022 $setSelection(selection);
3024 key: text1.getKey(),
3029 key: text1_1.getKey(),
3034 $setBlocksType(selection, () => {
3035 return $createHeadingNode('h1');
3038 expect(element.innerHTML).toStrictEqual(
3039 `<h1><span data-lexical-text="true">1</span></h1><h1 style="padding-inline-start: calc(1 * 40px);"><span data-lexical-text="true">1.1</span></h1>`,
3043 test('Nested list with listItem twice indented from his father', async () => {
3044 const testEditor = createTestEditor();
3045 const element = document.createElement('div');
3046 testEditor.setRootElement(element);
3048 await testEditor.update(() => {
3049 const root = $getRoot();
3050 const ul1 = $createListNode('bullet');
3051 const li1_wrapper = $createListItemNode();
3052 const ul2 = $createListNode('bullet');
3053 const text1_1 = $createTextNode('1.1');
3054 const li1_1 = $createListItemNode().append(text1_1);
3055 ul1.append(li1_wrapper);
3056 li1_wrapper.append(ul2);
3060 const selection = $createRangeSelection();
3061 $setSelection(selection);
3063 key: text1_1.getKey(),
3068 key: text1_1.getKey(),
3073 $setBlocksType(selection, () => {
3074 return $createHeadingNode('h1');
3077 expect(element.innerHTML).toStrictEqual(
3078 `<h1 style="padding-inline-start: calc(1 * 40px);"><span data-lexical-text="true">1.1</span></h1>`,