]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/selection/__tests__/unit/LexicalSelectionHelpers.test.ts
01390ed7180b6f9d73207c6428c8aa59b3aa31b5
[bookstack] / resources / js / wysiwyg / lexical / selection / __tests__ / unit / LexicalSelectionHelpers.test.ts
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 {$createLinkNode} from '@lexical/link';
10 import {$createHeadingNode, $isHeadingNode} from '@lexical/rich-text';
11 import {
12   $getSelectionStyleValueForProperty,
13   $patchStyleText,
14 } from '@lexical/selection';
15 import {
16   $createLineBreakNode,
17   $createParagraphNode,
18   $createRangeSelection,
19   $createTextNode,
20   $getNodeByKey,
21   $getRoot,
22   $getSelection,
23   $insertNodes,
24   $isElementNode,
25   $isParagraphNode,
26   $isRangeSelection,
27   $setSelection,
28   ElementNode,
29   LexicalEditor,
30   LexicalNode,
31   ParagraphNode,
32   RangeSelection,
33   TextModeType,
34   TextNode,
35 } from 'lexical';
36 import {
37   $createTestDecoratorNode,
38   $createTestElementNode,
39   $createTestShadowRootNode,
40   createTestEditor,
41   createTestHeadlessEditor,
42   invariant,
43   TestDecoratorNode,
44 } from 'lexical/src/__tests__/utils';
45
46 import {$setAnchorPoint, $setFocusPoint} from '../utils';
47
48 Range.prototype.getBoundingClientRect = function (): DOMRect {
49   const rect = {
50     bottom: 0,
51     height: 0,
52     left: 0,
53     right: 0,
54     top: 0,
55     width: 0,
56     x: 0,
57     y: 0,
58   };
59   return {
60     ...rect,
61     toJSON() {
62       return rect;
63     },
64   };
65 };
66
67 function $createParagraphWithNodes(
68   editor: LexicalEditor,
69   nodes: {text: string; key: string; mergeable?: boolean}[],
70 ) {
71   const paragraph = $createParagraphNode();
72   const nodeMap = editor._pendingEditorState!._nodeMap;
73
74   for (let i = 0; i < nodes.length; i++) {
75     const {text, key, mergeable} = nodes[i];
76     const textNode = new TextNode(text, key);
77     nodeMap.set(key, textNode);
78
79     if (!mergeable) {
80       textNode.toggleUnmergeable();
81     }
82
83     paragraph.append(textNode);
84   }
85
86   return paragraph;
87 }
88
89 describe('LexicalSelectionHelpers tests', () => {
90   describe('Collapsed', () => {
91     test('Can handle a text point', () => {
92       const setupTestCase = (
93         cb: (selection: RangeSelection, node: ElementNode) => void,
94       ) => {
95         const editor = createTestEditor();
96
97         editor.update(() => {
98           const root = $getRoot();
99
100           const element = $createParagraphWithNodes(editor, [
101             {
102               key: 'a',
103               mergeable: false,
104               text: 'a',
105             },
106             {
107               key: 'b',
108               mergeable: false,
109               text: 'b',
110             },
111             {
112               key: 'c',
113               mergeable: false,
114               text: 'c',
115             },
116           ]);
117
118           root.append(element);
119
120           $setAnchorPoint({
121             key: 'a',
122             offset: 0,
123             type: 'text',
124           });
125
126           $setFocusPoint({
127             key: 'a',
128             offset: 0,
129             type: 'text',
130           });
131           const selection = $getSelection();
132           cb(selection as RangeSelection, element);
133         });
134       };
135
136       // getNodes
137       setupTestCase((selection, state) => {
138         expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);
139       });
140
141       // getTextContent
142       setupTestCase((selection) => {
143         expect(selection.getTextContent()).toEqual('');
144       });
145
146       // insertText
147       setupTestCase((selection, state) => {
148         selection.insertText('Test');
149
150         expect($getNodeByKey('a')!.getTextContent()).toBe('Testa');
151
152         expect(selection.anchor).toEqual(
153           expect.objectContaining({
154             key: 'a',
155             offset: 4,
156             type: 'text',
157           }),
158         );
159
160         expect(selection.focus).toEqual(
161           expect.objectContaining({
162             key: 'a',
163             offset: 4,
164             type: 'text',
165           }),
166         );
167       });
168
169       // insertNodes
170       setupTestCase((selection, element) => {
171         selection.insertNodes([$createTextNode('foo')]);
172
173         expect(selection.anchor).toEqual(
174           expect.objectContaining({
175             key: element.getFirstChild()!.getKey(),
176             offset: 3,
177             type: 'text',
178           }),
179         );
180
181         expect(selection.focus).toEqual(
182           expect.objectContaining({
183             key: element.getFirstChild()!.getKey(),
184             offset: 3,
185             type: 'text',
186           }),
187         );
188       });
189
190       // insertParagraph
191       setupTestCase((selection) => {
192         selection.insertParagraph();
193
194         expect(selection.anchor).toEqual(
195           expect.objectContaining({
196             key: 'a',
197             offset: 0,
198             type: 'text',
199           }),
200         );
201
202         expect(selection.focus).toEqual(
203           expect.objectContaining({
204             key: 'a',
205             offset: 0,
206             type: 'text',
207           }),
208         );
209       });
210
211       // insertLineBreak
212       setupTestCase((selection, element) => {
213         selection.insertLineBreak(true);
214
215         expect(selection.anchor).toEqual(
216           expect.objectContaining({
217             key: element.getKey(),
218             offset: 0,
219             type: 'element',
220           }),
221         );
222
223         expect(selection.focus).toEqual(
224           expect.objectContaining({
225             key: element.getKey(),
226             offset: 0,
227             type: 'element',
228           }),
229         );
230       });
231
232       // Format text
233       setupTestCase((selection, element) => {
234         selection.formatText('bold');
235         selection.insertText('Test');
236
237         expect(element.getFirstChild()!.getTextContent()).toBe('Test');
238
239         expect(selection.anchor).toEqual(
240           expect.objectContaining({
241             key: element.getFirstChild()!.getKey(),
242             offset: 4,
243             type: 'text',
244           }),
245         );
246
247         expect(selection.focus).toEqual(
248           expect.objectContaining({
249             key: element.getFirstChild()!.getKey(),
250             offset: 4,
251             type: 'text',
252           }),
253         );
254
255         expect(
256           element.getFirstChild()!.getNextSibling()!.getTextContent(),
257         ).toBe('a');
258       });
259
260       // Extract selection
261       setupTestCase((selection, state) => {
262         expect(selection.extract()).toEqual([$getNodeByKey('a')]);
263       });
264     });
265
266     test('Has correct text point after removal after merge', async () => {
267       const editor = createTestEditor();
268
269       const domElement = document.createElement('div');
270       let element;
271
272       editor.setRootElement(domElement);
273
274       editor.update(() => {
275         const root = $getRoot();
276
277         element = $createParagraphWithNodes(editor, [
278           {
279             key: 'a',
280             mergeable: true,
281             text: 'a',
282           },
283           {
284             key: 'bb',
285             mergeable: true,
286             text: 'bb',
287           },
288           {
289             key: 'empty',
290             mergeable: true,
291             text: '',
292           },
293           {
294             key: 'cc',
295             mergeable: true,
296             text: 'cc',
297           },
298           {
299             key: 'd',
300             mergeable: true,
301             text: 'd',
302           },
303         ]);
304
305         root.append(element);
306
307         $setAnchorPoint({
308           key: 'bb',
309           offset: 1,
310           type: 'text',
311         });
312
313         $setFocusPoint({
314           key: 'cc',
315           offset: 1,
316           type: 'text',
317         });
318       });
319
320       await Promise.resolve().then();
321
322       editor.getEditorState().read(() => {
323         const selection = $getSelection();
324
325         if (!$isRangeSelection(selection)) {
326           return;
327         }
328
329         expect(selection.anchor).toEqual(
330           expect.objectContaining({
331             key: 'a',
332             offset: 2,
333             type: 'text',
334           }),
335         );
336
337         expect(selection.focus).toEqual(
338           expect.objectContaining({
339             key: 'a',
340             offset: 4,
341             type: 'text',
342           }),
343         );
344       });
345     });
346
347     test('Has correct text point after removal after merge (2)', async () => {
348       const editor = createTestEditor();
349
350       const domElement = document.createElement('div');
351       let element;
352
353       editor.setRootElement(domElement);
354
355       editor.update(() => {
356         const root = $getRoot();
357
358         element = $createParagraphWithNodes(editor, [
359           {
360             key: 'a',
361             mergeable: true,
362             text: 'a',
363           },
364           {
365             key: 'empty',
366             mergeable: true,
367             text: '',
368           },
369           {
370             key: 'b',
371             mergeable: true,
372             text: 'b',
373           },
374           {
375             key: 'c',
376             mergeable: true,
377             text: 'c',
378           },
379         ]);
380
381         root.append(element);
382
383         $setAnchorPoint({
384           key: 'a',
385           offset: 0,
386           type: 'text',
387         });
388
389         $setFocusPoint({
390           key: 'c',
391           offset: 1,
392           type: 'text',
393         });
394       });
395
396       await Promise.resolve().then();
397
398       editor.getEditorState().read(() => {
399         const selection = $getSelection();
400
401         if (!$isRangeSelection(selection)) {
402           return;
403         }
404
405         expect(selection.anchor).toEqual(
406           expect.objectContaining({
407             key: 'a',
408             offset: 0,
409             type: 'text',
410           }),
411         );
412
413         expect(selection.focus).toEqual(
414           expect.objectContaining({
415             key: 'a',
416             offset: 3,
417             type: 'text',
418           }),
419         );
420       });
421     });
422
423     test('Has correct text point adjust to element point after removal of a single empty text node', async () => {
424       const editor = createTestEditor();
425
426       const domElement = document.createElement('div');
427       let element: ParagraphNode;
428
429       editor.setRootElement(domElement);
430
431       editor.update(() => {
432         const root = $getRoot();
433
434         element = $createParagraphWithNodes(editor, [
435           {
436             key: 'a',
437             mergeable: true,
438             text: '',
439           },
440         ]);
441
442         root.append(element);
443
444         $setAnchorPoint({
445           key: 'a',
446           offset: 0,
447           type: 'text',
448         });
449
450         $setFocusPoint({
451           key: 'a',
452           offset: 0,
453           type: 'text',
454         });
455       });
456
457       await Promise.resolve().then();
458
459       editor.getEditorState().read(() => {
460         const selection = $getSelection();
461
462         if (!$isRangeSelection(selection)) {
463           return;
464         }
465
466         expect(selection.anchor).toEqual(
467           expect.objectContaining({
468             key: element.getKey(),
469             offset: 0,
470             type: 'element',
471           }),
472         );
473
474         expect(selection.focus).toEqual(
475           expect.objectContaining({
476             key: element.getKey(),
477             offset: 0,
478             type: 'element',
479           }),
480         );
481       });
482     });
483
484     test('Has correct element point after removal of an empty text node in a group #1', async () => {
485       const editor = createTestEditor();
486
487       const domElement = document.createElement('div');
488       let element;
489
490       editor.setRootElement(domElement);
491
492       editor.update(() => {
493         const root = $getRoot();
494
495         element = $createParagraphWithNodes(editor, [
496           {
497             key: 'a',
498             mergeable: true,
499             text: '',
500           },
501           {
502             key: 'b',
503             mergeable: false,
504             text: 'b',
505           },
506         ]);
507
508         root.append(element);
509
510         $setAnchorPoint({
511           key: element.getKey(),
512           offset: 2,
513           type: 'element',
514         });
515
516         $setFocusPoint({
517           key: element.getKey(),
518           offset: 2,
519           type: 'element',
520         });
521       });
522
523       await Promise.resolve().then();
524
525       editor.getEditorState().read(() => {
526         const selection = $getSelection();
527
528         if (!$isRangeSelection(selection)) {
529           return;
530         }
531
532         expect(selection.anchor).toEqual(
533           expect.objectContaining({
534             key: 'b',
535             offset: 1,
536             type: 'text',
537           }),
538         );
539
540         expect(selection.focus).toEqual(
541           expect.objectContaining({
542             key: 'b',
543             offset: 1,
544             type: 'text',
545           }),
546         );
547       });
548     });
549
550     test('Has correct element point after removal of an empty text node in a group #2', async () => {
551       const editor = createTestEditor();
552
553       const domElement = document.createElement('div');
554       let element;
555
556       editor.setRootElement(domElement);
557
558       editor.update(() => {
559         const root = $getRoot();
560
561         element = $createParagraphWithNodes(editor, [
562           {
563             key: 'a',
564             mergeable: true,
565             text: '',
566           },
567           {
568             key: 'b',
569             mergeable: false,
570             text: 'b',
571           },
572           {
573             key: 'c',
574             mergeable: true,
575             text: 'c',
576           },
577           {
578             key: 'd',
579             mergeable: true,
580             text: 'd',
581           },
582         ]);
583
584         root.append(element);
585
586         $setAnchorPoint({
587           key: element.getKey(),
588           offset: 4,
589           type: 'element',
590         });
591
592         $setFocusPoint({
593           key: element.getKey(),
594           offset: 4,
595           type: 'element',
596         });
597       });
598
599       await Promise.resolve().then();
600
601       editor.getEditorState().read(() => {
602         const selection = $getSelection();
603
604         if (!$isRangeSelection(selection)) {
605           return;
606         }
607
608         expect(selection.anchor).toEqual(
609           expect.objectContaining({
610             key: 'c',
611             offset: 2,
612             type: 'text',
613           }),
614         );
615
616         expect(selection.focus).toEqual(
617           expect.objectContaining({
618             key: 'c',
619             offset: 2,
620             type: 'text',
621           }),
622         );
623       });
624     });
625
626     test('Has correct text point after removal of an empty text node in a group #3', async () => {
627       const editor = createTestEditor();
628
629       const domElement = document.createElement('div');
630       let element;
631
632       editor.setRootElement(domElement);
633
634       editor.update(() => {
635         const root = $getRoot();
636
637         element = $createParagraphWithNodes(editor, [
638           {
639             key: 'a',
640             mergeable: true,
641             text: '',
642           },
643           {
644             key: 'b',
645             mergeable: false,
646             text: 'b',
647           },
648           {
649             key: 'c',
650             mergeable: true,
651             text: 'c',
652           },
653           {
654             key: 'd',
655             mergeable: true,
656             text: 'd',
657           },
658         ]);
659
660         root.append(element);
661
662         $setAnchorPoint({
663           key: 'd',
664           offset: 1,
665           type: 'text',
666         });
667
668         $setFocusPoint({
669           key: 'd',
670           offset: 1,
671           type: 'text',
672         });
673       });
674
675       await Promise.resolve().then();
676
677       editor.getEditorState().read(() => {
678         const selection = $getSelection();
679
680         if (!$isRangeSelection(selection)) {
681           return;
682         }
683
684         expect(selection.anchor).toEqual(
685           expect.objectContaining({
686             key: 'c',
687             offset: 2,
688             type: 'text',
689           }),
690         );
691
692         expect(selection.focus).toEqual(
693           expect.objectContaining({
694             key: 'c',
695             offset: 2,
696             type: 'text',
697           }),
698         );
699       });
700     });
701
702     test('Can handle an element point on empty element', () => {
703       const setupTestCase = (
704         cb: (selection: RangeSelection, el: ElementNode) => void,
705       ) => {
706         const editor = createTestEditor();
707
708         editor.update(() => {
709           const root = $getRoot();
710
711           const element = $createParagraphWithNodes(editor, []);
712
713           root.append(element);
714
715           $setAnchorPoint({
716             key: element.getKey(),
717             offset: 0,
718             type: 'element',
719           });
720
721           $setFocusPoint({
722             key: element.getKey(),
723             offset: 0,
724             type: 'element',
725           });
726           const selection = $getSelection();
727           cb(selection as RangeSelection, element);
728         });
729       };
730
731       // getNodes
732       setupTestCase((selection, element) => {
733         expect(selection.getNodes()).toEqual([element]);
734       });
735
736       // getTextContent
737       setupTestCase((selection) => {
738         expect(selection.getTextContent()).toEqual('');
739       });
740
741       // insertText
742       setupTestCase((selection, element) => {
743         selection.insertText('Test');
744         const firstChild = element.getFirstChild()!;
745
746         expect(firstChild.getTextContent()).toBe('Test');
747
748         expect(selection.anchor).toEqual(
749           expect.objectContaining({
750             key: firstChild.getKey(),
751             offset: 4,
752             type: 'text',
753           }),
754         );
755
756         expect(selection.focus).toEqual(
757           expect.objectContaining({
758             key: firstChild.getKey(),
759             offset: 4,
760             type: 'text',
761           }),
762         );
763       });
764
765       // insertParagraph
766       setupTestCase((selection, element) => {
767         selection.insertParagraph();
768         const nextElement = element.getNextSibling()!;
769
770         expect(selection.anchor).toEqual(
771           expect.objectContaining({
772             key: nextElement.getKey(),
773             offset: 0,
774             type: 'element',
775           }),
776         );
777
778         expect(selection.focus).toEqual(
779           expect.objectContaining({
780             key: nextElement.getKey(),
781             offset: 0,
782             type: 'element',
783           }),
784         );
785       });
786
787       // insertLineBreak
788       setupTestCase((selection, element) => {
789         selection.insertLineBreak(true);
790
791         expect(selection.anchor).toEqual(
792           expect.objectContaining({
793             key: element.getKey(),
794             offset: 0,
795             type: 'element',
796           }),
797         );
798
799         expect(selection.focus).toEqual(
800           expect.objectContaining({
801             key: element.getKey(),
802             offset: 0,
803             type: 'element',
804           }),
805         );
806       });
807
808       // Format text
809       setupTestCase((selection, element) => {
810         selection.formatText('bold');
811         selection.insertText('Test');
812         const firstChild = element.getFirstChild()!;
813
814         expect(firstChild.getTextContent()).toBe('Test');
815
816         expect(selection.anchor).toEqual(
817           expect.objectContaining({
818             key: firstChild.getKey(),
819             offset: 4,
820             type: 'text',
821           }),
822         );
823
824         expect(selection.focus).toEqual(
825           expect.objectContaining({
826             key: firstChild.getKey(),
827             offset: 4,
828             type: 'text',
829           }),
830         );
831       });
832
833       // Extract selection
834       setupTestCase((selection, element) => {
835         expect(selection.extract()).toEqual([element]);
836       });
837     });
838
839     test('Can handle a start element point', () => {
840       const setupTestCase = (
841         cb: (selection: RangeSelection, el: ElementNode) => void,
842       ) => {
843         const editor = createTestEditor();
844
845         editor.update(() => {
846           const root = $getRoot();
847
848           const element = $createParagraphWithNodes(editor, [
849             {
850               key: 'a',
851               mergeable: false,
852               text: 'a',
853             },
854             {
855               key: 'b',
856               mergeable: false,
857               text: 'b',
858             },
859             {
860               key: 'c',
861               mergeable: false,
862               text: 'c',
863             },
864           ]);
865
866           root.append(element);
867
868           $setAnchorPoint({
869             key: element.getKey(),
870             offset: 0,
871             type: 'element',
872           });
873
874           $setFocusPoint({
875             key: element.getKey(),
876             offset: 0,
877             type: 'element',
878           });
879           const selection = $getSelection();
880           cb(selection as RangeSelection, element);
881         });
882       };
883
884       // getNodes
885       setupTestCase((selection, state) => {
886         expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);
887       });
888
889       // getTextContent
890       setupTestCase((selection) => {
891         expect(selection.getTextContent()).toEqual('');
892       });
893
894       // insertText
895       setupTestCase((selection, element) => {
896         selection.insertText('Test');
897         const firstChild = element.getFirstChild()!;
898
899         expect(firstChild.getTextContent()).toBe('Test');
900
901         expect(selection.anchor).toEqual(
902           expect.objectContaining({
903             key: firstChild.getKey(),
904             offset: 4,
905             type: 'text',
906           }),
907         );
908
909         expect(selection.focus).toEqual(
910           expect.objectContaining({
911             key: firstChild.getKey(),
912             offset: 4,
913             type: 'text',
914           }),
915         );
916       });
917
918       // insertParagraph
919       setupTestCase((selection, element) => {
920         selection.insertParagraph();
921
922         expect(selection.anchor).toEqual(
923           expect.objectContaining({
924             key: 'a',
925             offset: 0,
926             type: 'text',
927           }),
928         );
929
930         expect(selection.focus).toEqual(
931           expect.objectContaining({
932             key: 'a',
933             offset: 0,
934             type: 'text',
935           }),
936         );
937       });
938
939       // insertLineBreak
940       setupTestCase((selection, element) => {
941         selection.insertLineBreak(true);
942
943         expect(selection.anchor).toEqual(
944           expect.objectContaining({
945             key: element.getKey(),
946             offset: 0,
947             type: 'element',
948           }),
949         );
950
951         expect(selection.focus).toEqual(
952           expect.objectContaining({
953             key: element.getKey(),
954             offset: 0,
955             type: 'element',
956           }),
957         );
958       });
959
960       // Format text
961       setupTestCase((selection, element) => {
962         selection.formatText('bold');
963         selection.insertText('Test');
964
965         const firstChild = element.getFirstChild()!;
966
967         expect(firstChild.getTextContent()).toBe('Test');
968
969         expect(selection.anchor).toEqual(
970           expect.objectContaining({
971             key: firstChild.getKey(),
972             offset: 4,
973             type: 'text',
974           }),
975         );
976
977         expect(selection.focus).toEqual(
978           expect.objectContaining({
979             key: firstChild.getKey(),
980             offset: 4,
981             type: 'text',
982           }),
983         );
984       });
985
986       // Extract selection
987       setupTestCase((selection, element) => {
988         expect(selection.extract()).toEqual([$getNodeByKey('a')]);
989       });
990     });
991
992     test('Can handle an end element point', () => {
993       const setupTestCase = (
994         cb: (selection: RangeSelection, el: ElementNode) => void,
995       ) => {
996         const editor = createTestEditor();
997
998         editor.update(() => {
999           const root = $getRoot();
1000
1001           const element = $createParagraphWithNodes(editor, [
1002             {
1003               key: 'a',
1004               mergeable: false,
1005               text: 'a',
1006             },
1007             {
1008               key: 'b',
1009               mergeable: false,
1010               text: 'b',
1011             },
1012             {
1013               key: 'c',
1014               mergeable: false,
1015               text: 'c',
1016             },
1017           ]);
1018
1019           root.append(element);
1020
1021           $setAnchorPoint({
1022             key: element.getKey(),
1023             offset: 3,
1024             type: 'element',
1025           });
1026
1027           $setFocusPoint({
1028             key: element.getKey(),
1029             offset: 3,
1030             type: 'element',
1031           });
1032           const selection = $getSelection();
1033           cb(selection as RangeSelection, element);
1034         });
1035       };
1036
1037       // getNodes
1038       setupTestCase((selection, state) => {
1039         expect(selection.getNodes()).toEqual([$getNodeByKey('c')]);
1040       });
1041
1042       // getTextContent
1043       setupTestCase((selection) => {
1044         expect(selection.getTextContent()).toEqual('');
1045       });
1046
1047       // insertText
1048       setupTestCase((selection, element) => {
1049         selection.insertText('Test');
1050         const lastChild = element.getLastChild()!;
1051
1052         expect(lastChild.getTextContent()).toBe('Test');
1053
1054         expect(selection.anchor).toEqual(
1055           expect.objectContaining({
1056             key: lastChild.getKey(),
1057             offset: 4,
1058             type: 'text',
1059           }),
1060         );
1061
1062         expect(selection.focus).toEqual(
1063           expect.objectContaining({
1064             key: lastChild.getKey(),
1065             offset: 4,
1066             type: 'text',
1067           }),
1068         );
1069       });
1070
1071       // insertParagraph
1072       setupTestCase((selection, element) => {
1073         selection.insertParagraph();
1074         const nextSibling = element.getNextSibling()!;
1075
1076         expect(selection.anchor).toEqual(
1077           expect.objectContaining({
1078             key: nextSibling.getKey(),
1079             offset: 0,
1080             type: 'element',
1081           }),
1082         );
1083
1084         expect(selection.focus).toEqual(
1085           expect.objectContaining({
1086             key: nextSibling.getKey(),
1087             offset: 0,
1088             type: 'element',
1089           }),
1090         );
1091       });
1092
1093       // insertLineBreak
1094       setupTestCase((selection, element) => {
1095         selection.insertLineBreak();
1096
1097         expect(selection.anchor).toEqual(
1098           expect.objectContaining({
1099             key: element.getKey(),
1100             offset: 4,
1101             type: 'element',
1102           }),
1103         );
1104
1105         expect(selection.focus).toEqual(
1106           expect.objectContaining({
1107             key: element.getKey(),
1108             offset: 4,
1109             type: 'element',
1110           }),
1111         );
1112       });
1113
1114       // Format text
1115       setupTestCase((selection, element) => {
1116         selection.formatText('bold');
1117         selection.insertText('Test');
1118         const lastChild = element.getLastChild()!;
1119
1120         expect(lastChild.getTextContent()).toBe('Test');
1121
1122         expect(selection.anchor).toEqual(
1123           expect.objectContaining({
1124             key: lastChild.getKey(),
1125             offset: 4,
1126             type: 'text',
1127           }),
1128         );
1129
1130         expect(selection.focus).toEqual(
1131           expect.objectContaining({
1132             key: lastChild.getKey(),
1133             offset: 4,
1134             type: 'text',
1135           }),
1136         );
1137       });
1138
1139       // Extract selection
1140       setupTestCase((selection, element) => {
1141         expect(selection.extract()).toEqual([$getNodeByKey('c')]);
1142       });
1143     });
1144
1145     test('Has correct element point after merge from middle', async () => {
1146       const editor = createTestEditor();
1147
1148       const domElement = document.createElement('div');
1149       let element;
1150
1151       editor.setRootElement(domElement);
1152
1153       editor.update(() => {
1154         const root = $getRoot();
1155
1156         element = $createParagraphWithNodes(editor, [
1157           {
1158             key: 'a',
1159             mergeable: true,
1160             text: 'a',
1161           },
1162           {
1163             key: 'b',
1164             mergeable: true,
1165             text: 'b',
1166           },
1167           {
1168             key: 'c',
1169             mergeable: true,
1170             text: 'c',
1171           },
1172         ]);
1173
1174         root.append(element);
1175
1176         $setAnchorPoint({
1177           key: element.getKey(),
1178           offset: 2,
1179           type: 'element',
1180         });
1181
1182         $setFocusPoint({
1183           key: element.getKey(),
1184           offset: 2,
1185           type: 'element',
1186         });
1187       });
1188
1189       await Promise.resolve().then();
1190
1191       editor.getEditorState().read(() => {
1192         const selection = $getSelection();
1193
1194         if (!$isRangeSelection(selection)) {
1195           return;
1196         }
1197
1198         expect(selection.anchor).toEqual(
1199           expect.objectContaining({
1200             key: 'a',
1201             offset: 2,
1202             type: 'text',
1203           }),
1204         );
1205
1206         expect(selection.focus).toEqual(
1207           expect.objectContaining({
1208             key: 'a',
1209             offset: 2,
1210             type: 'text',
1211           }),
1212         );
1213       });
1214     });
1215
1216     test('Has correct element point after merge from end', async () => {
1217       const editor = createTestEditor();
1218
1219       const domElement = document.createElement('div');
1220       let element;
1221
1222       editor.setRootElement(domElement);
1223
1224       editor.update(() => {
1225         const root = $getRoot();
1226
1227         element = $createParagraphWithNodes(editor, [
1228           {
1229             key: 'a',
1230             mergeable: true,
1231             text: 'a',
1232           },
1233           {
1234             key: 'b',
1235             mergeable: true,
1236             text: 'b',
1237           },
1238           {
1239             key: 'c',
1240             mergeable: true,
1241             text: 'c',
1242           },
1243         ]);
1244
1245         root.append(element);
1246
1247         $setAnchorPoint({
1248           key: element.getKey(),
1249           offset: 3,
1250           type: 'element',
1251         });
1252
1253         $setFocusPoint({
1254           key: element.getKey(),
1255           offset: 3,
1256           type: 'element',
1257         });
1258       });
1259
1260       await Promise.resolve().then();
1261
1262       editor.getEditorState().read(() => {
1263         const selection = $getSelection();
1264
1265         if (!$isRangeSelection(selection)) {
1266           return;
1267         }
1268
1269         expect(selection.anchor).toEqual(
1270           expect.objectContaining({
1271             key: 'a',
1272             offset: 3,
1273             type: 'text',
1274           }),
1275         );
1276
1277         expect(selection.focus).toEqual(
1278           expect.objectContaining({
1279             key: 'a',
1280             offset: 3,
1281             type: 'text',
1282           }),
1283         );
1284       });
1285     });
1286   });
1287
1288   describe('Simple range', () => {
1289     test('Can handle multiple text points', () => {
1290       const setupTestCase = (
1291         cb: (selection: RangeSelection, el: ElementNode) => void,
1292       ) => {
1293         const editor = createTestEditor();
1294
1295         editor.update(() => {
1296           const root = $getRoot();
1297
1298           const element = $createParagraphWithNodes(editor, [
1299             {
1300               key: 'a',
1301               mergeable: false,
1302               text: 'a',
1303             },
1304             {
1305               key: 'b',
1306               mergeable: false,
1307               text: 'b',
1308             },
1309             {
1310               key: 'c',
1311               mergeable: false,
1312               text: 'c',
1313             },
1314           ]);
1315
1316           root.append(element);
1317
1318           $setAnchorPoint({
1319             key: 'a',
1320             offset: 0,
1321             type: 'text',
1322           });
1323
1324           $setFocusPoint({
1325             key: 'b',
1326             offset: 0,
1327             type: 'text',
1328           });
1329           const selection = $getSelection();
1330           if (!$isRangeSelection(selection)) {
1331             return;
1332           }
1333           cb(selection, element);
1334         });
1335       };
1336
1337       // getNodes
1338       setupTestCase((selection, state) => {
1339         expect(selection.getNodes()).toEqual([
1340           $getNodeByKey('a'),
1341           $getNodeByKey('b'),
1342         ]);
1343       });
1344
1345       // getTextContent
1346       setupTestCase((selection) => {
1347         expect(selection.getTextContent()).toEqual('a');
1348       });
1349
1350       // insertText
1351       setupTestCase((selection, state) => {
1352         selection.insertText('Test');
1353
1354         expect($getNodeByKey('a')!.getTextContent()).toBe('Test');
1355
1356         expect(selection.anchor).toEqual(
1357           expect.objectContaining({
1358             key: 'a',
1359             offset: 4,
1360             type: 'text',
1361           }),
1362         );
1363
1364         expect(selection.focus).toEqual(
1365           expect.objectContaining({
1366             key: 'a',
1367             offset: 4,
1368             type: 'text',
1369           }),
1370         );
1371       });
1372
1373       // insertNodes
1374       setupTestCase((selection, element) => {
1375         selection.insertNodes([$createTextNode('foo')]);
1376
1377         expect(selection.anchor).toEqual(
1378           expect.objectContaining({
1379             key: element.getFirstChild()!.getKey(),
1380             offset: 3,
1381             type: 'text',
1382           }),
1383         );
1384
1385         expect(selection.focus).toEqual(
1386           expect.objectContaining({
1387             key: element.getFirstChild()!.getKey(),
1388             offset: 3,
1389             type: 'text',
1390           }),
1391         );
1392       });
1393
1394       // insertParagraph
1395       setupTestCase((selection) => {
1396         selection.insertParagraph();
1397
1398         expect(selection.anchor).toEqual(
1399           expect.objectContaining({
1400             key: 'b',
1401             offset: 0,
1402             type: 'text',
1403           }),
1404         );
1405
1406         expect(selection.focus).toEqual(
1407           expect.objectContaining({
1408             key: 'b',
1409             offset: 0,
1410             type: 'text',
1411           }),
1412         );
1413       });
1414
1415       // insertLineBreak
1416       setupTestCase((selection, element) => {
1417         selection.insertLineBreak(true);
1418
1419         expect(selection.anchor).toEqual(
1420           expect.objectContaining({
1421             key: element.getKey(),
1422             offset: 0,
1423             type: 'element',
1424           }),
1425         );
1426
1427         expect(selection.focus).toEqual(
1428           expect.objectContaining({
1429             key: element.getKey(),
1430             offset: 0,
1431             type: 'element',
1432           }),
1433         );
1434       });
1435
1436       // Format text
1437       setupTestCase((selection, element) => {
1438         selection.formatText('bold');
1439         selection.insertText('Test');
1440
1441         expect(element.getFirstChild()!.getTextContent()).toBe('Test');
1442
1443         expect(selection.anchor).toEqual(
1444           expect.objectContaining({
1445             key: element.getFirstChild()!.getKey(),
1446             offset: 4,
1447             type: 'text',
1448           }),
1449         );
1450
1451         expect(selection.focus).toEqual(
1452           expect.objectContaining({
1453             key: element.getFirstChild()!.getKey(),
1454             offset: 4,
1455             type: 'text',
1456           }),
1457         );
1458       });
1459
1460       // Extract selection
1461       setupTestCase((selection, state) => {
1462         expect(selection.extract()).toEqual([{...$getNodeByKey('a')}]);
1463       });
1464     });
1465
1466     test('Can handle multiple element points', () => {
1467       const setupTestCase = (
1468         cb: (selection: RangeSelection, el: ElementNode) => void,
1469       ) => {
1470         const editor = createTestEditor();
1471
1472         editor.update(() => {
1473           const root = $getRoot();
1474
1475           const element = $createParagraphWithNodes(editor, [
1476             {
1477               key: 'a',
1478               mergeable: false,
1479               text: 'a',
1480             },
1481             {
1482               key: 'b',
1483               mergeable: false,
1484               text: 'b',
1485             },
1486             {
1487               key: 'c',
1488               mergeable: false,
1489               text: 'c',
1490             },
1491           ]);
1492
1493           root.append(element);
1494
1495           $setAnchorPoint({
1496             key: element.getKey(),
1497             offset: 0,
1498             type: 'element',
1499           });
1500
1501           $setFocusPoint({
1502             key: element.getKey(),
1503             offset: 1,
1504             type: 'element',
1505           });
1506           const selection = $getSelection();
1507           if (!$isRangeSelection(selection)) {
1508             return;
1509           }
1510           cb(selection, element);
1511         });
1512       };
1513
1514       // getNodes
1515       setupTestCase((selection) => {
1516         expect(selection.getNodes()).toEqual([$getNodeByKey('a')]);
1517       });
1518
1519       // getTextContent
1520       setupTestCase((selection) => {
1521         expect(selection.getTextContent()).toEqual('a');
1522       });
1523
1524       // insertText
1525       setupTestCase((selection, element) => {
1526         selection.insertText('Test');
1527         const firstChild = element.getFirstChild()!;
1528
1529         expect(firstChild.getTextContent()).toBe('Test');
1530
1531         expect(selection.anchor).toEqual(
1532           expect.objectContaining({
1533             key: firstChild.getKey(),
1534             offset: 4,
1535             type: 'text',
1536           }),
1537         );
1538
1539         expect(selection.focus).toEqual(
1540           expect.objectContaining({
1541             key: firstChild.getKey(),
1542             offset: 4,
1543             type: 'text',
1544           }),
1545         );
1546       });
1547
1548       // insertParagraph
1549       setupTestCase((selection, element) => {
1550         selection.insertParagraph();
1551
1552         expect(selection.anchor).toEqual(
1553           expect.objectContaining({
1554             key: 'b',
1555             offset: 0,
1556             type: 'text',
1557           }),
1558         );
1559
1560         expect(selection.focus).toEqual(
1561           expect.objectContaining({
1562             key: 'b',
1563             offset: 0,
1564             type: 'text',
1565           }),
1566         );
1567       });
1568
1569       // insertLineBreak
1570       setupTestCase((selection, element) => {
1571         selection.insertLineBreak(true);
1572
1573         expect(selection.anchor).toEqual(
1574           expect.objectContaining({
1575             key: element.getKey(),
1576             offset: 0,
1577             type: 'element',
1578           }),
1579         );
1580
1581         expect(selection.focus).toEqual(
1582           expect.objectContaining({
1583             key: element.getKey(),
1584             offset: 0,
1585             type: 'element',
1586           }),
1587         );
1588       });
1589
1590       // Format text
1591       setupTestCase((selection, element) => {
1592         selection.formatText('bold');
1593         selection.insertText('Test');
1594         const firstChild = element.getFirstChild()!;
1595
1596         expect(firstChild.getTextContent()).toBe('Test');
1597
1598         expect(selection.anchor).toEqual(
1599           expect.objectContaining({
1600             key: firstChild.getKey(),
1601             offset: 4,
1602             type: 'text',
1603           }),
1604         );
1605
1606         expect(selection.focus).toEqual(
1607           expect.objectContaining({
1608             key: firstChild.getKey(),
1609             offset: 4,
1610             type: 'text',
1611           }),
1612         );
1613       });
1614
1615       // Extract selection
1616       setupTestCase((selection, element) => {
1617         const firstChild = element.getFirstChild();
1618
1619         expect(selection.extract()).toEqual([firstChild]);
1620       });
1621     });
1622
1623     test('Can handle a mix of text and element points', () => {
1624       const setupTestCase = (
1625         cb: (selection: RangeSelection, el: ElementNode) => void,
1626       ) => {
1627         const editor = createTestEditor();
1628
1629         editor.update(() => {
1630           const root = $getRoot();
1631
1632           const element = $createParagraphWithNodes(editor, [
1633             {
1634               key: 'a',
1635               mergeable: false,
1636               text: 'a',
1637             },
1638             {
1639               key: 'b',
1640               mergeable: false,
1641               text: 'b',
1642             },
1643             {
1644               key: 'c',
1645               mergeable: false,
1646               text: 'c',
1647             },
1648           ]);
1649
1650           root.append(element);
1651
1652           $setAnchorPoint({
1653             key: element.getKey(),
1654             offset: 0,
1655             type: 'element',
1656           });
1657
1658           $setFocusPoint({
1659             key: 'c',
1660             offset: 1,
1661             type: 'text',
1662           });
1663           const selection = $getSelection();
1664           if (!$isRangeSelection(selection)) {
1665             return;
1666           }
1667           cb(selection, element);
1668         });
1669       };
1670
1671       // isBefore
1672       setupTestCase((selection, state) => {
1673         expect(selection.anchor.isBefore(selection.focus)).toEqual(true);
1674       });
1675
1676       // getNodes
1677       setupTestCase((selection, state) => {
1678         expect(selection.getNodes()).toEqual([
1679           $getNodeByKey('a'),
1680           $getNodeByKey('b'),
1681           $getNodeByKey('c'),
1682         ]);
1683       });
1684
1685       // getTextContent
1686       setupTestCase((selection) => {
1687         expect(selection.getTextContent()).toEqual('abc');
1688       });
1689
1690       // insertText
1691       setupTestCase((selection, element) => {
1692         selection.insertText('Test');
1693         const firstChild = element.getFirstChild()!;
1694
1695         expect(firstChild.getTextContent()).toBe('Test');
1696
1697         expect(selection.anchor).toEqual(
1698           expect.objectContaining({
1699             key: firstChild.getKey(),
1700             offset: 4,
1701             type: 'text',
1702           }),
1703         );
1704
1705         expect(selection.focus).toEqual(
1706           expect.objectContaining({
1707             key: firstChild.getKey(),
1708             offset: 4,
1709             type: 'text',
1710           }),
1711         );
1712       });
1713
1714       // insertParagraph
1715       setupTestCase((selection, element) => {
1716         selection.insertParagraph();
1717         const nextElement = element.getNextSibling()!;
1718
1719         expect(selection.anchor).toEqual(
1720           expect.objectContaining({
1721             key: nextElement.getKey(),
1722             offset: 0,
1723             type: 'element',
1724           }),
1725         );
1726
1727         expect(selection.focus).toEqual(
1728           expect.objectContaining({
1729             key: nextElement.getKey(),
1730             offset: 0,
1731             type: 'element',
1732           }),
1733         );
1734       });
1735
1736       // insertLineBreak
1737       setupTestCase((selection, element) => {
1738         selection.insertLineBreak(true);
1739
1740         expect(selection.anchor).toEqual(
1741           expect.objectContaining({
1742             key: element.getKey(),
1743             offset: 0,
1744             type: 'element',
1745           }),
1746         );
1747
1748         expect(selection.focus).toEqual(
1749           expect.objectContaining({
1750             key: element.getKey(),
1751             offset: 0,
1752             type: 'element',
1753           }),
1754         );
1755       });
1756
1757       // Format text
1758       setupTestCase((selection, element) => {
1759         selection.formatText('bold');
1760         selection.insertText('Test');
1761         const firstChild = element.getFirstChild()!;
1762
1763         expect(firstChild.getTextContent()).toBe('Test');
1764
1765         expect(selection.anchor).toEqual(
1766           expect.objectContaining({
1767             key: firstChild.getKey(),
1768             offset: 4,
1769             type: 'text',
1770           }),
1771         );
1772
1773         expect(selection.focus).toEqual(
1774           expect.objectContaining({
1775             key: firstChild.getKey(),
1776             offset: 4,
1777             type: 'text',
1778           }),
1779         );
1780       });
1781
1782       // Extract selection
1783       setupTestCase((selection, element) => {
1784         expect(selection.extract()).toEqual([
1785           $getNodeByKey('a'),
1786           $getNodeByKey('b'),
1787           $getNodeByKey('c'),
1788         ]);
1789       });
1790     });
1791   });
1792
1793   describe('can insert non-element nodes correctly', () => {
1794     describe('with an empty paragraph node selected', () => {
1795       test('a single text node', async () => {
1796         const editor = createTestEditor();
1797
1798         const element = document.createElement('div');
1799
1800         editor.setRootElement(element);
1801
1802         await editor.update(() => {
1803           const root = $getRoot();
1804
1805           const paragraph = $createParagraphNode();
1806           root.append(paragraph);
1807
1808           $setAnchorPoint({
1809             key: paragraph.getKey(),
1810             offset: 0,
1811             type: 'element',
1812           });
1813
1814           $setFocusPoint({
1815             key: paragraph.getKey(),
1816             offset: 0,
1817             type: 'element',
1818           });
1819
1820           const selection = $getSelection();
1821
1822           if (!$isRangeSelection(selection)) {
1823             return;
1824           }
1825
1826           selection.insertNodes([$createTextNode('foo')]);
1827         });
1828
1829         expect(element.innerHTML).toBe(
1830           '<p dir="ltr"><span data-lexical-text="true">foo</span></p>',
1831         );
1832       });
1833
1834       test('two text nodes', async () => {
1835         const editor = createTestEditor();
1836
1837         const element = document.createElement('div');
1838
1839         editor.setRootElement(element);
1840
1841         await editor.update(() => {
1842           const root = $getRoot();
1843
1844           const paragraph = $createParagraphNode();
1845           root.append(paragraph);
1846
1847           $setAnchorPoint({
1848             key: paragraph.getKey(),
1849             offset: 0,
1850             type: 'element',
1851           });
1852
1853           $setFocusPoint({
1854             key: paragraph.getKey(),
1855             offset: 0,
1856             type: 'element',
1857           });
1858           const selection = $getSelection();
1859
1860           if (!$isRangeSelection(selection)) {
1861             return;
1862           }
1863
1864           selection.insertNodes([
1865             $createTextNode('foo'),
1866             $createTextNode('bar'),
1867           ]);
1868         });
1869
1870         expect(element.innerHTML).toBe(
1871           '<p dir="ltr"><span data-lexical-text="true">foobar</span></p>',
1872         );
1873       });
1874
1875       test('link insertion without parent element', async () => {
1876         const editor = createTestEditor();
1877
1878         const element = document.createElement('div');
1879
1880         editor.setRootElement(element);
1881
1882         await editor.update(() => {
1883           const root = $getRoot();
1884
1885           const paragraph = $createParagraphNode();
1886           root.append(paragraph);
1887
1888           $setAnchorPoint({
1889             key: paragraph.getKey(),
1890             offset: 0,
1891             type: 'element',
1892           });
1893
1894           $setFocusPoint({
1895             key: paragraph.getKey(),
1896             offset: 0,
1897             type: 'element',
1898           });
1899           const link = $createLinkNode('https://');
1900           link.append($createTextNode('ello worl'));
1901
1902           const selection = $getSelection();
1903
1904           if (!$isRangeSelection(selection)) {
1905             return;
1906           }
1907
1908           selection.insertNodes([
1909             $createTextNode('h'),
1910             link,
1911             $createTextNode('d'),
1912           ]);
1913         });
1914
1915         expect(element.innerHTML).toBe(
1916           '<p dir="ltr"><span data-lexical-text="true">h</span><a href="https://" dir="ltr"><span data-lexical-text="true">ello worl</span></a><span data-lexical-text="true">d</span></p>',
1917         );
1918       });
1919
1920       test('a single heading node with a child text node', async () => {
1921         const editor = createTestEditor();
1922
1923         const element = document.createElement('div');
1924
1925         editor.setRootElement(element);
1926
1927         await editor.update(() => {
1928           const root = $getRoot();
1929
1930           const paragraph = $createParagraphNode();
1931           root.append(paragraph);
1932
1933           $setAnchorPoint({
1934             key: paragraph.getKey(),
1935             offset: 0,
1936             type: 'element',
1937           });
1938
1939           $setFocusPoint({
1940             key: paragraph.getKey(),
1941             offset: 0,
1942             type: 'element',
1943           });
1944
1945           const heading = $createHeadingNode('h1');
1946           const child = $createTextNode('foo');
1947
1948           heading.append(child);
1949
1950           const selection = $getSelection();
1951
1952           if (!$isRangeSelection(selection)) {
1953             return;
1954           }
1955           selection.insertNodes([heading]);
1956         });
1957
1958         expect(element.innerHTML).toBe(
1959           '<h1 dir="ltr"><span data-lexical-text="true">foo</span></h1>',
1960         );
1961       });
1962     });
1963
1964     describe('with a paragraph node selected on some existing text', () => {
1965       test('a single text node', async () => {
1966         const editor = createTestEditor();
1967
1968         const element = document.createElement('div');
1969
1970         editor.setRootElement(element);
1971
1972         await editor.update(() => {
1973           const root = $getRoot();
1974
1975           const paragraph = $createParagraphNode();
1976           const text = $createTextNode('Existing text...');
1977
1978           paragraph.append(text);
1979           root.append(paragraph);
1980
1981           $setAnchorPoint({
1982             key: text.getKey(),
1983             offset: 16,
1984             type: 'text',
1985           });
1986
1987           $setFocusPoint({
1988             key: text.getKey(),
1989             offset: 16,
1990             type: 'text',
1991           });
1992
1993           const selection = $getSelection();
1994
1995           if (!$isRangeSelection(selection)) {
1996             return;
1997           }
1998           selection.insertNodes([$createTextNode('foo')]);
1999         });
2000
2001         expect(element.innerHTML).toBe(
2002           '<p dir="ltr"><span data-lexical-text="true">Existing text...foo</span></p>',
2003         );
2004       });
2005
2006       test('two text nodes', async () => {
2007         const editor = createTestEditor();
2008
2009         const element = document.createElement('div');
2010
2011         editor.setRootElement(element);
2012
2013         await editor.update(() => {
2014           const root = $getRoot();
2015
2016           const paragraph = $createParagraphNode();
2017           const text = $createTextNode('Existing text...');
2018
2019           paragraph.append(text);
2020           root.append(paragraph);
2021
2022           $setAnchorPoint({
2023             key: text.getKey(),
2024             offset: 16,
2025             type: 'text',
2026           });
2027
2028           $setFocusPoint({
2029             key: text.getKey(),
2030             offset: 16,
2031             type: 'text',
2032           });
2033
2034           const selection = $getSelection();
2035
2036           if (!$isRangeSelection(selection)) {
2037             return;
2038           }
2039
2040           selection.insertNodes([
2041             $createTextNode('foo'),
2042             $createTextNode('bar'),
2043           ]);
2044         });
2045
2046         expect(element.innerHTML).toBe(
2047           '<p dir="ltr"><span data-lexical-text="true">Existing text...foobar</span></p>',
2048         );
2049       });
2050
2051       test('a single heading node with a child text node', async () => {
2052         const editor = createTestEditor();
2053
2054         const element = document.createElement('div');
2055
2056         editor.setRootElement(element);
2057
2058         await editor.update(() => {
2059           const root = $getRoot();
2060
2061           const paragraph = $createParagraphNode();
2062           const text = $createTextNode('Existing text...');
2063
2064           paragraph.append(text);
2065           root.append(paragraph);
2066
2067           $setAnchorPoint({
2068             key: text.getKey(),
2069             offset: 16,
2070             type: 'text',
2071           });
2072
2073           $setFocusPoint({
2074             key: text.getKey(),
2075             offset: 16,
2076             type: 'text',
2077           });
2078
2079           const heading = $createHeadingNode('h1');
2080           const child = $createTextNode('foo');
2081
2082           heading.append(child);
2083
2084           const selection = $getSelection();
2085
2086           if (!$isRangeSelection(selection)) {
2087             return;
2088           }
2089
2090           selection.insertNodes([heading]);
2091         });
2092
2093         expect(element.innerHTML).toBe(
2094           '<p dir="ltr"><span data-lexical-text="true">Existing text...foo</span></p>',
2095         );
2096       });
2097
2098       test('a paragraph with a child text and a child italic text and a child text', async () => {
2099         const editor = createTestEditor();
2100
2101         const element = document.createElement('div');
2102
2103         editor.setRootElement(element);
2104
2105         await editor.update(() => {
2106           const root = $getRoot();
2107
2108           const paragraph = $createParagraphNode();
2109           const text = $createTextNode('AE');
2110
2111           paragraph.append(text);
2112           root.append(paragraph);
2113
2114           $setAnchorPoint({
2115             key: text.getKey(),
2116             offset: 1,
2117             type: 'text',
2118           });
2119
2120           $setFocusPoint({
2121             key: text.getKey(),
2122             offset: 1,
2123             type: 'text',
2124           });
2125
2126           const insertedParagraph = $createParagraphNode();
2127           const insertedTextB = $createTextNode('B');
2128           const insertedTextC = $createTextNode('C');
2129           const insertedTextD = $createTextNode('D');
2130
2131           insertedTextC.toggleFormat('italic');
2132
2133           insertedParagraph.append(insertedTextB, insertedTextC, insertedTextD);
2134
2135           const selection = $getSelection();
2136
2137           if (!$isRangeSelection(selection)) {
2138             return;
2139           }
2140
2141           selection.insertNodes([insertedParagraph]);
2142
2143           expect(selection.anchor).toEqual(
2144             expect.objectContaining({
2145               key: paragraph
2146                 .getChildAtIndex(paragraph.getChildrenSize() - 2)!
2147                 .getKey(),
2148               offset: 1,
2149               type: 'text',
2150             }),
2151           );
2152
2153           expect(selection.focus).toEqual(
2154             expect.objectContaining({
2155               key: paragraph
2156                 .getChildAtIndex(paragraph.getChildrenSize() - 2)!
2157                 .getKey(),
2158               offset: 1,
2159               type: 'text',
2160             }),
2161           );
2162         });
2163
2164         expect(element.innerHTML).toBe(
2165           '<p dir="ltr"><span data-lexical-text="true">AB</span><em data-lexical-text="true">C</em><span data-lexical-text="true">DE</span></p>',
2166         );
2167       });
2168     });
2169
2170     describe('with a fully-selected text node', () => {
2171       test('a single text node', async () => {
2172         const editor = createTestEditor();
2173
2174         const element = document.createElement('div');
2175
2176         editor.setRootElement(element);
2177
2178         await editor.update(() => {
2179           const root = $getRoot();
2180
2181           const paragraph = $createParagraphNode();
2182           root.append(paragraph);
2183
2184           const text = $createTextNode('Existing text...');
2185           paragraph.append(text);
2186
2187           $setAnchorPoint({
2188             key: text.getKey(),
2189             offset: 0,
2190             type: 'text',
2191           });
2192
2193           $setFocusPoint({
2194             key: text.getKey(),
2195             offset: 'Existing text...'.length,
2196             type: 'text',
2197           });
2198
2199           const selection = $getSelection();
2200
2201           if (!$isRangeSelection(selection)) {
2202             return;
2203           }
2204           selection.insertNodes([$createTextNode('foo')]);
2205         });
2206
2207         expect(element.innerHTML).toBe(
2208           '<p dir="ltr"><span data-lexical-text="true">foo</span></p>',
2209         );
2210       });
2211     });
2212
2213     describe('with a fully-selected text node followed by an inline element', () => {
2214       test('a single text node', async () => {
2215         const editor = createTestEditor();
2216
2217         const element = document.createElement('div');
2218
2219         editor.setRootElement(element);
2220
2221         await editor.update(() => {
2222           const root = $getRoot();
2223
2224           const paragraph = $createParagraphNode();
2225           root.append(paragraph);
2226
2227           const text = $createTextNode('Existing text...');
2228           paragraph.append(text);
2229
2230           const link = $createLinkNode('https://');
2231           link.append($createTextNode('link'));
2232           paragraph.append(link);
2233
2234           $setAnchorPoint({
2235             key: text.getKey(),
2236             offset: 0,
2237             type: 'text',
2238           });
2239
2240           $setFocusPoint({
2241             key: text.getKey(),
2242             offset: 'Existing text...'.length,
2243             type: 'text',
2244           });
2245
2246           const selection = $getSelection();
2247
2248           if (!$isRangeSelection(selection)) {
2249             return;
2250           }
2251           selection.insertNodes([$createTextNode('foo')]);
2252         });
2253
2254         expect(element.innerHTML).toBe(
2255           '<p dir="ltr"><span data-lexical-text="true">foo</span><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a></p>',
2256         );
2257       });
2258     });
2259
2260     describe('with a fully-selected text node preceded by an inline element', () => {
2261       test('a single text node', async () => {
2262         const editor = createTestEditor();
2263
2264         const element = document.createElement('div');
2265
2266         editor.setRootElement(element);
2267
2268         await editor.update(() => {
2269           const root = $getRoot();
2270
2271           const paragraph = $createParagraphNode();
2272           root.append(paragraph);
2273
2274           const link = $createLinkNode('https://');
2275           link.append($createTextNode('link'));
2276           paragraph.append(link);
2277
2278           const text = $createTextNode('Existing text...');
2279           paragraph.append(text);
2280
2281           $setAnchorPoint({
2282             key: text.getKey(),
2283             offset: 0,
2284             type: 'text',
2285           });
2286
2287           $setFocusPoint({
2288             key: text.getKey(),
2289             offset: 'Existing text...'.length,
2290             type: 'text',
2291           });
2292
2293           const selection = $getSelection();
2294
2295           if (!$isRangeSelection(selection)) {
2296             return;
2297           }
2298           selection.insertNodes([$createTextNode('foo')]);
2299         });
2300
2301         expect(element.innerHTML).toBe(
2302           '<p dir="ltr"><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a><span data-lexical-text="true">foo</span></p>',
2303         );
2304       });
2305     });
2306
2307     test.skip('can insert a linebreak node before an inline element node', async () => {
2308       const editor = createTestEditor();
2309       const element = document.createElement('div');
2310       editor.setRootElement(element);
2311
2312       await editor.update(() => {
2313         const root = $getRoot();
2314         const paragraph = $createParagraphNode();
2315         root.append(paragraph);
2316         const link = $createLinkNode('https://lexical.dev/');
2317         paragraph.append(link);
2318         const text = $createTextNode('Lexical');
2319         link.append(text);
2320         text.select(0, 0);
2321
2322         $insertNodes([$createLineBreakNode()]);
2323       });
2324
2325       // TODO #5109 ElementNode should have a way to control when other nodes can be inserted inside
2326       expect(element.innerHTML).toBe(
2327         '<p><a href="https://lexical.dev/" dir="ltr"><br><span data-lexical-text="true">Lexical</span></a></p>',
2328       );
2329     });
2330   });
2331
2332   describe('can insert block element nodes correctly', () => {
2333     describe('with a fully-selected text node', () => {
2334       test('a paragraph node', async () => {
2335         const editor = createTestEditor();
2336
2337         const element = document.createElement('div');
2338
2339         editor.setRootElement(element);
2340
2341         await editor.update(() => {
2342           const root = $getRoot();
2343
2344           const paragraph = $createParagraphNode();
2345           root.append(paragraph);
2346
2347           const text = $createTextNode('Existing text...');
2348           paragraph.append(text);
2349
2350           $setAnchorPoint({
2351             key: text.getKey(),
2352             offset: 0,
2353             type: 'text',
2354           });
2355
2356           $setFocusPoint({
2357             key: text.getKey(),
2358             offset: 'Existing text...'.length,
2359             type: 'text',
2360           });
2361
2362           const paragraphToInsert = $createParagraphNode();
2363           paragraphToInsert.append($createTextNode('foo'));
2364
2365           const selection = $getSelection();
2366
2367           if (!$isRangeSelection(selection)) {
2368             return;
2369           }
2370           selection.insertNodes([paragraphToInsert]);
2371         });
2372
2373         expect(element.innerHTML).toBe(
2374           '<p dir="ltr"><span data-lexical-text="true">foo</span></p>',
2375         );
2376       });
2377     });
2378
2379     describe('with a fully-selected text node followed by an inline element', () => {
2380       test('a paragraph node', async () => {
2381         const editor = createTestEditor();
2382
2383         const element = document.createElement('div');
2384
2385         editor.setRootElement(element);
2386
2387         await editor.update(() => {
2388           const root = $getRoot();
2389
2390           const paragraph = $createParagraphNode();
2391           root.append(paragraph);
2392
2393           const text = $createTextNode('Existing text...');
2394           paragraph.append(text);
2395
2396           const link = $createLinkNode('https://');
2397           link.append($createTextNode('link'));
2398           paragraph.append(link);
2399
2400           $setAnchorPoint({
2401             key: text.getKey(),
2402             offset: 0,
2403             type: 'text',
2404           });
2405
2406           $setFocusPoint({
2407             key: text.getKey(),
2408             offset: 'Existing text...'.length,
2409             type: 'text',
2410           });
2411
2412           const paragraphToInsert = $createParagraphNode();
2413           paragraphToInsert.append($createTextNode('foo'));
2414
2415           const selection = $getSelection();
2416
2417           if (!$isRangeSelection(selection)) {
2418             return;
2419           }
2420           selection.insertNodes([paragraphToInsert]);
2421         });
2422
2423         expect(element.innerHTML).toBe(
2424           '<p dir="ltr"><span data-lexical-text="true">foo</span><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a></p>',
2425         );
2426       });
2427     });
2428
2429     describe('with a fully-selected text node preceded by an inline element', () => {
2430       test('a paragraph node', async () => {
2431         const editor = createTestEditor();
2432
2433         const element = document.createElement('div');
2434
2435         editor.setRootElement(element);
2436
2437         await editor.update(() => {
2438           const root = $getRoot();
2439
2440           const paragraph = $createParagraphNode();
2441           root.append(paragraph);
2442
2443           const link = $createLinkNode('https://');
2444           link.append($createTextNode('link'));
2445           paragraph.append(link);
2446
2447           const text = $createTextNode('Existing text...');
2448           paragraph.append(text);
2449
2450           $setAnchorPoint({
2451             key: text.getKey(),
2452             offset: 0,
2453             type: 'text',
2454           });
2455
2456           $setFocusPoint({
2457             key: text.getKey(),
2458             offset: 'Existing text...'.length,
2459             type: 'text',
2460           });
2461
2462           const paragraphToInsert = $createParagraphNode();
2463           paragraphToInsert.append($createTextNode('foo'));
2464
2465           const selection = $getSelection();
2466
2467           if (!$isRangeSelection(selection)) {
2468             return;
2469           }
2470           selection.insertNodes([paragraphToInsert]);
2471         });
2472
2473         expect(element.innerHTML).toBe(
2474           '<p dir="ltr"><a href="https://" dir="ltr"><span data-lexical-text="true">link</span></a><span data-lexical-text="true">foo</span></p>',
2475         );
2476       });
2477     });
2478
2479     test('Can insert link into empty paragraph', async () => {
2480       const editor = createTestEditor();
2481       const element = document.createElement('div');
2482       editor.setRootElement(element);
2483
2484       await editor.update(() => {
2485         const root = $getRoot();
2486         const paragraph = $createParagraphNode();
2487         root.append(paragraph);
2488         const linkNode = $createLinkNode('https://lexical.dev');
2489         const linkTextNode = $createTextNode('Lexical');
2490         linkNode.append(linkTextNode);
2491         $insertNodes([linkNode]);
2492       });
2493       expect(element.innerHTML).toBe(
2494         '<p><a href="https://lexical.dev" dir="ltr"><span data-lexical-text="true">Lexical</span></a></p>',
2495       );
2496     });
2497
2498     test('Can insert link into empty paragraph (2)', async () => {
2499       const editor = createTestEditor();
2500       const element = document.createElement('div');
2501       editor.setRootElement(element);
2502
2503       await editor.update(() => {
2504         const root = $getRoot();
2505         const paragraph = $createParagraphNode();
2506         root.append(paragraph);
2507         const linkNode = $createLinkNode('https://lexical.dev');
2508         const linkTextNode = $createTextNode('Lexical');
2509         linkNode.append(linkTextNode);
2510         const textNode2 = $createTextNode('...');
2511         $insertNodes([linkNode, textNode2]);
2512       });
2513       expect(element.innerHTML).toBe(
2514         '<p><a href="https://lexical.dev" dir="ltr"><span data-lexical-text="true">Lexical</span></a><span data-lexical-text="true">...</span></p>',
2515       );
2516     });
2517
2518     test('Can insert an ElementNode after ShadowRoot', async () => {
2519       const editor = createTestEditor();
2520       const element = document.createElement('div');
2521       editor.setRootElement(element);
2522
2523       await editor.update(() => {
2524         const root = $getRoot();
2525         const paragraph = $createParagraphNode();
2526         root.append(paragraph);
2527         paragraph.selectStart();
2528         const element1 = $createTestShadowRootNode();
2529         const element2 = $createTestElementNode();
2530         $insertNodes([element1, element2]);
2531       });
2532       expect([
2533         '<div><br></div><div><br></div>',
2534         '<div><br></div><p><br></p>',
2535       ]).toContain(element.innerHTML);
2536     });
2537   });
2538 });
2539
2540 describe('extract', () => {
2541   test('Should return the selected node when collapsed on a TextNode', async () => {
2542     const editor = createTestEditor();
2543
2544     const element = document.createElement('div');
2545
2546     editor.setRootElement(element);
2547
2548     await editor.update(() => {
2549       const root = $getRoot();
2550
2551       const paragraph = $createParagraphNode();
2552       const text = $createTextNode('Existing text...');
2553
2554       paragraph.append(text);
2555       root.append(paragraph);
2556
2557       $setAnchorPoint({
2558         key: text.getKey(),
2559         offset: 16,
2560         type: 'text',
2561       });
2562
2563       $setFocusPoint({
2564         key: text.getKey(),
2565         offset: 16,
2566         type: 'text',
2567       });
2568
2569       const selection = $getSelection();
2570       expect($isRangeSelection(selection)).toBeTruthy();
2571
2572       expect(selection!.extract()).toEqual([text]);
2573     });
2574   });
2575 });
2576
2577 describe('insertNodes', () => {
2578   afterEach(() => {
2579     jest.clearAllMocks();
2580   });
2581
2582   it('can insert element next to top level decorator node', async () => {
2583     const editor = createTestEditor();
2584     const element = document.createElement('div');
2585     editor.setRootElement(element);
2586
2587     jest.spyOn(TestDecoratorNode.prototype, 'isInline').mockReturnValue(false);
2588
2589     await editor.update(() => {
2590       $getRoot().append(
2591         $createParagraphNode(),
2592         $createTestDecoratorNode(),
2593         $createParagraphNode().append($createTextNode('Text after')),
2594       );
2595     });
2596
2597     await editor.update(() => {
2598       const selectionNode = $getRoot().getFirstChild();
2599       invariant($isElementNode(selectionNode));
2600       const selection = selectionNode.select();
2601       selection.insertNodes([
2602         $createParagraphNode().append($createTextNode('Text before')),
2603       ]);
2604     });
2605
2606     expect(element.innerHTML).toBe(
2607       '<p dir="ltr"><span data-lexical-text="true">Text before</span></p>' +
2608         '<span data-lexical-decorator="true" contenteditable="false"></span>' +
2609         '<p dir="ltr"><span data-lexical-text="true">Text after</span></p>',
2610     );
2611   });
2612
2613   it('can insert when previous selection was null', async () => {
2614     const editor = createTestHeadlessEditor();
2615     await editor.update(() => {
2616       const selection = $createRangeSelection();
2617       selection.anchor.set('root', 0, 'element');
2618       selection.focus.set('root', 0, 'element');
2619
2620       selection.insertNodes([
2621         $createParagraphNode().append($createTextNode('Text')),
2622       ]);
2623
2624       expect($getRoot().getTextContent()).toBe('Text');
2625
2626       $setSelection(null);
2627     });
2628     await editor.update(() => {
2629       const selection = $createRangeSelection();
2630       const text = $getRoot().getLastDescendant()!;
2631       selection.anchor.set(text.getKey(), 0, 'text');
2632       selection.focus.set(text.getKey(), 0, 'text');
2633
2634       selection.insertNodes([
2635         $createParagraphNode().append($createTextNode('Before ')),
2636       ]);
2637
2638       expect($getRoot().getTextContent()).toBe('Before Text');
2639     });
2640   });
2641
2642   it('can insert when before empty text node', async () => {
2643     const editor = createTestEditor();
2644     const element = document.createElement('div');
2645     editor.setRootElement(element);
2646
2647     await editor.update(() => {
2648       // Empty text node to test empty text split
2649       const emptyTextNode = $createTextNode('');
2650       $getRoot().append(
2651         $createParagraphNode().append(emptyTextNode, $createTextNode('text')),
2652       );
2653       emptyTextNode.select(0, 0);
2654       const selection = $getSelection()!;
2655       expect($isRangeSelection(selection)).toBeTruthy();
2656       selection.insertNodes([$createTextNode('foo')]);
2657
2658       expect($getRoot().getTextContent()).toBe('footext');
2659     });
2660   });
2661
2662   it('last node is LineBreakNode', async () => {
2663     const editor = createTestEditor();
2664     const element = document.createElement('div');
2665     editor.setRootElement(element);
2666
2667     await editor.update(() => {
2668       // Empty text node to test empty text split
2669       const paragraph = $createParagraphNode();
2670       $getRoot().append(paragraph);
2671       const selection = paragraph.select();
2672       expect($isRangeSelection(selection)).toBeTruthy();
2673
2674       const newHeading = $createHeadingNode('h1').append(
2675         $createTextNode('heading'),
2676       );
2677       selection.insertNodes([newHeading, $createLineBreakNode()]);
2678     });
2679     editor.getEditorState().read(() => {
2680       expect(element.innerHTML).toBe(
2681         '<h1 dir="ltr"><span data-lexical-text="true">heading</span></h1><p><br></p>',
2682       );
2683       const selectedNode = ($getSelection() as RangeSelection).anchor.getNode();
2684       expect($isParagraphNode(selectedNode)).toBeTruthy();
2685       expect($isHeadingNode(selectedNode.getPreviousSibling())).toBeTruthy();
2686     });
2687   });
2688 });
2689
2690 describe('$patchStyleText', () => {
2691   test('can patch a selection anchored to the end of a TextNode before an inline element', async () => {
2692     const editor = createTestEditor();
2693     const element = document.createElement('div');
2694     editor.setRootElement(element);
2695
2696     await editor.update(() => {
2697       const root = $getRoot();
2698
2699       const paragraph = $createParagraphWithNodes(editor, [
2700         {
2701           key: 'a',
2702           mergeable: false,
2703           text: 'a',
2704         },
2705         {
2706           key: 'b',
2707           mergeable: false,
2708           text: 'b',
2709         },
2710       ]);
2711
2712       root.append(paragraph);
2713
2714       const link = $createLinkNode('https://');
2715       link.append($createTextNode('link'));
2716
2717       const a = $getNodeByKey('a')!;
2718       a.insertAfter(link);
2719
2720       $setAnchorPoint({
2721         key: 'a',
2722         offset: 1,
2723         type: 'text',
2724       });
2725       $setFocusPoint({
2726         key: 'b',
2727         offset: 1,
2728         type: 'text',
2729       });
2730
2731       const selection = $getSelection();
2732       if (!$isRangeSelection(selection)) {
2733         return;
2734       }
2735       $patchStyleText(selection, {'text-emphasis': 'filled'});
2736     });
2737
2738     expect(element.innerHTML).toBe(
2739       '<p dir="ltr"><span data-lexical-text="true">a</span>' +
2740         '<a href="https://" dir="ltr">' +
2741         '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2742         '</a>' +
2743         '<span style="text-emphasis: filled;" data-lexical-text="true">b</span></p>',
2744     );
2745   });
2746
2747   test('can patch a selection anchored to the end of a TextNode at the end of a paragraph', async () => {
2748     const editor = createTestEditor();
2749     const element = document.createElement('div');
2750     editor.setRootElement(element);
2751
2752     await editor.update(() => {
2753       const root = $getRoot();
2754
2755       const paragraph1 = $createParagraphWithNodes(editor, [
2756         {
2757           key: 'a',
2758           mergeable: false,
2759           text: 'a',
2760         },
2761       ]);
2762       const paragraph2 = $createParagraphWithNodes(editor, [
2763         {
2764           key: 'b',
2765           mergeable: false,
2766           text: 'b',
2767         },
2768       ]);
2769
2770       root.append(paragraph1);
2771       root.append(paragraph2);
2772
2773       $setAnchorPoint({
2774         key: 'a',
2775         offset: 1,
2776         type: 'text',
2777       });
2778       $setFocusPoint({
2779         key: 'b',
2780         offset: 1,
2781         type: 'text',
2782       });
2783
2784       const selection = $getSelection();
2785       if (!$isRangeSelection(selection)) {
2786         return;
2787       }
2788       $patchStyleText(selection, {'text-emphasis': 'filled'});
2789     });
2790
2791     expect(element.innerHTML).toBe(
2792       '<p dir="ltr"><span data-lexical-text="true">a</span></p>' +
2793         '<p dir="ltr"><span style="text-emphasis: filled;" data-lexical-text="true">b</span></p>',
2794     );
2795   });
2796
2797   test('can patch a selection that ends on an element', async () => {
2798     const editor = createTestEditor();
2799     const element = document.createElement('div');
2800     editor.setRootElement(element);
2801
2802     await editor.update(() => {
2803       const root = $getRoot();
2804
2805       const paragraph = $createParagraphWithNodes(editor, [
2806         {
2807           key: 'a',
2808           mergeable: false,
2809           text: 'a',
2810         },
2811       ]);
2812
2813       root.append(paragraph);
2814
2815       const link = $createLinkNode('https://');
2816       link.append($createTextNode('link'));
2817
2818       const a = $getNodeByKey('a')!;
2819       a.insertAfter(link);
2820
2821       $setAnchorPoint({
2822         key: 'a',
2823         offset: 0,
2824         type: 'text',
2825       });
2826       // Select to end of the link _element_
2827       $setFocusPoint({
2828         key: link.getKey(),
2829         offset: 1,
2830         type: 'element',
2831       });
2832
2833       const selection = $getSelection();
2834       if (!$isRangeSelection(selection)) {
2835         return;
2836       }
2837       $patchStyleText(selection, {'text-emphasis': 'filled'});
2838     });
2839
2840     expect(element.innerHTML).toBe(
2841       '<p dir="ltr">' +
2842         '<span style="text-emphasis: filled;" data-lexical-text="true">a</span>' +
2843         '<a href="https://" dir="ltr">' +
2844         '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2845         '</a>' +
2846         '</p>',
2847     );
2848   });
2849
2850   test('can patch a reversed selection that ends on an element', async () => {
2851     const editor = createTestEditor();
2852     const element = document.createElement('div');
2853     editor.setRootElement(element);
2854
2855     await editor.update(() => {
2856       const root = $getRoot();
2857
2858       const paragraph = $createParagraphWithNodes(editor, [
2859         {
2860           key: 'a',
2861           mergeable: false,
2862           text: 'a',
2863         },
2864       ]);
2865
2866       root.append(paragraph);
2867
2868       const link = $createLinkNode('https://');
2869       link.append($createTextNode('link'));
2870
2871       const a = $getNodeByKey('a')!;
2872       a.insertAfter(link);
2873
2874       // Select from the end of the link _element_
2875       $setAnchorPoint({
2876         key: link.getKey(),
2877         offset: 1,
2878         type: 'element',
2879       });
2880       $setFocusPoint({
2881         key: 'a',
2882         offset: 0,
2883         type: 'text',
2884       });
2885
2886       const selection = $getSelection();
2887       if (!$isRangeSelection(selection)) {
2888         return;
2889       }
2890       $patchStyleText(selection, {'text-emphasis': 'filled'});
2891     });
2892
2893     expect(element.innerHTML).toBe(
2894       '<p dir="ltr">' +
2895         '<span style="text-emphasis: filled;" data-lexical-text="true">a</span>' +
2896         '<a href="https://" dir="ltr">' +
2897         '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2898         '</a>' +
2899         '</p>',
2900     );
2901   });
2902
2903   test('can patch a selection that starts and ends on an element', async () => {
2904     const editor = createTestEditor();
2905     const element = document.createElement('div');
2906     editor.setRootElement(element);
2907
2908     await editor.update(() => {
2909       const root = $getRoot();
2910
2911       const paragraph = $createParagraphNode();
2912       root.append(paragraph);
2913
2914       const link = $createLinkNode('https://');
2915       link.append($createTextNode('link'));
2916       paragraph.append(link);
2917
2918       $setAnchorPoint({
2919         key: link.getKey(),
2920         offset: 0,
2921         type: 'element',
2922       });
2923       $setFocusPoint({
2924         key: link.getKey(),
2925         offset: 1,
2926         type: 'element',
2927       });
2928
2929       const selection = $getSelection();
2930       if (!$isRangeSelection(selection)) {
2931         return;
2932       }
2933       $patchStyleText(selection, {'text-emphasis': 'filled'});
2934     });
2935
2936     expect(element.innerHTML).toBe(
2937       '<p>' +
2938         '<a href="https://" dir="ltr">' +
2939         '<span style="text-emphasis: filled;" data-lexical-text="true">link</span>' +
2940         '</a>' +
2941         '</p>',
2942     );
2943   });
2944
2945   test('can clear a style', async () => {
2946     const editor = createTestEditor();
2947     const element = document.createElement('div');
2948     editor.setRootElement(element);
2949
2950     await editor.update(() => {
2951       const root = $getRoot();
2952
2953       const paragraph = $createParagraphNode();
2954       root.append(paragraph);
2955
2956       const text = $createTextNode('text');
2957       paragraph.append(text);
2958
2959       $setAnchorPoint({
2960         key: text.getKey(),
2961         offset: 0,
2962         type: 'text',
2963       });
2964       $setFocusPoint({
2965         key: text.getKey(),
2966         offset: text.getTextContentSize(),
2967         type: 'text',
2968       });
2969
2970       const selection = $getSelection();
2971       if (!$isRangeSelection(selection)) {
2972         return;
2973       }
2974       $patchStyleText(selection, {'text-emphasis': 'filled'});
2975       $patchStyleText(selection, {'text-emphasis': null});
2976     });
2977
2978     expect(element.innerHTML).toBe(
2979       '<p dir="ltr"><span data-lexical-text="true">text</span></p>',
2980     );
2981   });
2982
2983   test('can toggle a style on a collapsed selection', async () => {
2984     const editor = createTestEditor();
2985     const element = document.createElement('div');
2986     editor.setRootElement(element);
2987
2988     await editor.update(() => {
2989       const root = $getRoot();
2990
2991       const paragraph = $createParagraphNode();
2992       root.append(paragraph);
2993
2994       const text = $createTextNode('text');
2995       paragraph.append(text);
2996
2997       $setAnchorPoint({
2998         key: text.getKey(),
2999         offset: 0,
3000         type: 'text',
3001       });
3002       $setFocusPoint({
3003         key: text.getKey(),
3004         offset: 0,
3005         type: 'text',
3006       });
3007
3008       const selection = $getSelection();
3009       if (!$isRangeSelection(selection)) {
3010         return;
3011       }
3012       $patchStyleText(selection, {'text-emphasis': 'filled'});
3013
3014       expect(
3015         $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),
3016       ).toEqual('filled');
3017
3018       $patchStyleText(selection, {'text-emphasis': null});
3019
3020       expect(
3021         $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),
3022       ).toEqual('');
3023
3024       $patchStyleText(selection, {'text-emphasis': 'filled'});
3025
3026       expect(
3027         $getSelectionStyleValueForProperty(selection, 'text-emphasis', ''),
3028       ).toEqual('filled');
3029     });
3030   });
3031
3032   test('updates cached styles when setting on a collapsed selection', async () => {
3033     const editor = createTestEditor();
3034     const element = document.createElement('div');
3035     editor.setRootElement(element);
3036
3037     await editor.update(() => {
3038       const root = $getRoot();
3039
3040       const paragraph = $createParagraphNode();
3041       root.append(paragraph);
3042
3043       const text = $createTextNode('text');
3044       paragraph.append(text);
3045
3046       $setAnchorPoint({
3047         key: text.getKey(),
3048         offset: 0,
3049         type: 'text',
3050       });
3051       $setFocusPoint({
3052         key: text.getKey(),
3053         offset: 0,
3054         type: 'text',
3055       });
3056
3057       // First fetch the initial style -- this will cause the CSS cache to be
3058       // populated with an empty string pointing to an empty style object.
3059       const selection = $getSelection();
3060       if (!$isRangeSelection(selection)) {
3061         return;
3062       }
3063       $getSelectionStyleValueForProperty(selection, 'color', '');
3064
3065       // Now when we set the style, we should _not_ touch the previously created
3066       // empty style object, but create a new one instead.
3067       $patchStyleText(selection, {color: 'red'});
3068
3069       // We can check that result by clearing the style and re-querying it.
3070       ($getSelection() as RangeSelection).setStyle('');
3071
3072       const color = $getSelectionStyleValueForProperty(
3073         $getSelection() as RangeSelection,
3074         'color',
3075         '',
3076       );
3077       expect(color).toEqual('');
3078     });
3079   });
3080
3081   test.each<TextModeType>(['token', 'segmented'])(
3082     'can update style of text node that is in %s mode',
3083     async (mode) => {
3084       const editor = createTestEditor();
3085
3086       const element = document.createElement('div');
3087       editor.setRootElement(element);
3088
3089       await editor.update(() => {
3090         const root = $getRoot();
3091
3092         const paragraph = $createParagraphNode();
3093         root.append(paragraph);
3094
3095         const text = $createTextNode('first').setFormat('bold');
3096         paragraph.append(text);
3097
3098         const textInMode = $createTextNode('second').setMode(mode);
3099         paragraph.append(textInMode);
3100
3101         $setAnchorPoint({
3102           key: text.getKey(),
3103           offset: 'fir'.length,
3104           type: 'text',
3105         });
3106
3107         $setFocusPoint({
3108           key: textInMode.getKey(),
3109           offset: 'sec'.length,
3110           type: 'text',
3111         });
3112
3113         const selection = $getSelection();
3114         $patchStyleText(selection!, {'font-size': '15px'});
3115       });
3116
3117       expect(element.innerHTML).toBe(
3118         '<p dir="ltr">' +
3119           '<strong data-lexical-text="true">fir</strong>' +
3120           '<strong style="font-size: 15px;" data-lexical-text="true">st</strong>' +
3121           '<span style="font-size: 15px;" data-lexical-text="true">second</span>' +
3122           '</p>',
3123       );
3124     },
3125   );
3126
3127   test('preserve backward selection when changing style of 2 different text nodes', async () => {
3128     const editor = createTestEditor();
3129
3130     const element = document.createElement('div');
3131
3132     editor.setRootElement(element);
3133
3134     editor.update(() => {
3135       const root = $getRoot();
3136
3137       const paragraph = $createParagraphNode();
3138       root.append(paragraph);
3139
3140       const firstText = $createTextNode('first ').setFormat('bold');
3141       paragraph.append(firstText);
3142
3143       const secondText = $createTextNode('second').setFormat('italic');
3144       paragraph.append(secondText);
3145
3146       $setAnchorPoint({
3147         key: secondText.getKey(),
3148         offset: 'sec'.length,
3149         type: 'text',
3150       });
3151
3152       $setFocusPoint({
3153         key: firstText.getKey(),
3154         offset: 'fir'.length,
3155         type: 'text',
3156       });
3157
3158       const selection = $getSelection();
3159
3160       $patchStyleText(selection!, {'font-size': '11px'});
3161
3162       const [newAnchor, newFocus] = selection!.getStartEndPoints()!;
3163
3164       const newAnchorNode: LexicalNode = newAnchor.getNode();
3165       expect(newAnchorNode.getTextContent()).toBe('sec');
3166       expect(newAnchor.offset).toBe('sec'.length);
3167
3168       const newFocusNode: LexicalNode = newFocus.getNode();
3169       expect(newFocusNode.getTextContent()).toBe('st ');
3170       expect(newFocus.offset).toBe(0);
3171     });
3172   });
3173 });