]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListItemNode.test.ts
a1ccd50207c2ef74eadecd1aa5e854ed9d013566
[bookstack] / resources / js / wysiwyg / lexical / list / __tests__ / unit / LexicalListItemNode.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 {
10   $createParagraphNode,
11   $createRangeSelection,
12   $getRoot,
13   TextNode,
14 } from 'lexical';
15 import {
16   expectHtmlToBeEqual,
17   html,
18   initializeUnitTest,
19 } from 'lexical/__tests__/utils';
20
21 import {
22   $createListItemNode,
23   $isListItemNode,
24   ListItemNode,
25   ListNode,
26 } from '../..';
27
28 const editorConfig = Object.freeze({
29   namespace: '',
30   theme: {
31     list: {
32       listitem: 'my-listItem-item-class',
33       nested: {
34         listitem: 'my-nested-list-listItem-class',
35       },
36     },
37   },
38 });
39
40 describe('LexicalListItemNode tests', () => {
41   initializeUnitTest((testEnv) => {
42     test('ListItemNode.constructor', async () => {
43       const {editor} = testEnv;
44
45       await editor.update(() => {
46         const listItemNode = new ListItemNode();
47
48         expect(listItemNode.getType()).toBe('listitem');
49
50         expect(listItemNode.getTextContent()).toBe('');
51       });
52
53       expect(() => new ListItemNode()).toThrow();
54     });
55
56     test('ListItemNode.createDOM()', async () => {
57       const {editor} = testEnv;
58
59       await editor.update(() => {
60         const listItemNode = new ListItemNode();
61
62         expectHtmlToBeEqual(
63           listItemNode.createDOM(editorConfig).outerHTML,
64           html`
65             <li class="my-listItem-item-class" value="1"></li>
66           `,
67         );
68
69         expectHtmlToBeEqual(
70           listItemNode.createDOM({
71             namespace: '',
72             theme: {},
73           }).outerHTML,
74           html`
75             <li value="1"></li>
76           `,
77         );
78       });
79     });
80
81     describe('ListItemNode.updateDOM()', () => {
82       test('base', async () => {
83         const {editor} = testEnv;
84
85         await editor.update(() => {
86           const listItemNode = new ListItemNode();
87
88           const domElement = listItemNode.createDOM(editorConfig);
89
90           expectHtmlToBeEqual(
91             domElement.outerHTML,
92             html`
93               <li class="my-listItem-item-class" value="1"></li>
94             `,
95           );
96           const newListItemNode = new ListItemNode();
97
98           const result = newListItemNode.updateDOM(
99             listItemNode,
100             domElement,
101             editorConfig,
102           );
103
104           expect(result).toBe(false);
105
106           expectHtmlToBeEqual(
107             domElement.outerHTML,
108             html`
109               <li class="my-listItem-item-class" value="1"></li>
110             `,
111           );
112         });
113       });
114
115       test('nested list', async () => {
116         const {editor} = testEnv;
117
118         await editor.update(() => {
119           const parentListNode = new ListNode('bullet', 1);
120           const parentlistItemNode = new ListItemNode();
121
122           parentListNode.append(parentlistItemNode);
123           const domElement = parentlistItemNode.createDOM(editorConfig);
124
125           expectHtmlToBeEqual(
126             domElement.outerHTML,
127             html`
128               <li class="my-listItem-item-class" value="1"></li>
129             `,
130           );
131           const nestedListNode = new ListNode('bullet', 1);
132           nestedListNode.append(new ListItemNode());
133           parentlistItemNode.append(nestedListNode);
134           const result = parentlistItemNode.updateDOM(
135             parentlistItemNode,
136             domElement,
137             editorConfig,
138           );
139
140           expect(result).toBe(false);
141
142           expectHtmlToBeEqual(
143             domElement.outerHTML,
144             html`
145               <li
146                 class="my-listItem-item-class my-nested-list-listItem-class"
147                 value="1"></li>
148             `,
149           );
150         });
151       });
152     });
153
154     describe('ListItemNode.replace()', () => {
155       let listNode: ListNode;
156       let listItemNode1: ListItemNode;
157       let listItemNode2: ListItemNode;
158       let listItemNode3: ListItemNode;
159
160       beforeEach(async () => {
161         const {editor} = testEnv;
162
163         await editor.update(() => {
164           const root = $getRoot();
165           listNode = new ListNode('bullet', 1);
166           listItemNode1 = new ListItemNode();
167
168           listItemNode1.append(new TextNode('one'));
169           listItemNode2 = new ListItemNode();
170
171           listItemNode2.append(new TextNode('two'));
172           listItemNode3 = new ListItemNode();
173
174           listItemNode3.append(new TextNode('three'));
175           root.append(listNode);
176           listNode.append(listItemNode1, listItemNode2, listItemNode3);
177         });
178
179         expectHtmlToBeEqual(
180           testEnv.outerHTML,
181           html`
182             <div
183               contenteditable="true"
184               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
185               data-lexical-editor="true">
186               <ul>
187                 <li dir="ltr" value="1">
188                   <span data-lexical-text="true">one</span>
189                 </li>
190                 <li dir="ltr" value="2">
191                   <span data-lexical-text="true">two</span>
192                 </li>
193                 <li dir="ltr" value="3">
194                   <span data-lexical-text="true">three</span>
195                 </li>
196               </ul>
197             </div>
198           `,
199         );
200       });
201
202       test('another list item node', async () => {
203         const {editor} = testEnv;
204
205         await editor.update(() => {
206           const newListItemNode = new ListItemNode();
207
208           newListItemNode.append(new TextNode('bar'));
209           listItemNode1.replace(newListItemNode);
210         });
211
212         expectHtmlToBeEqual(
213           testEnv.outerHTML,
214           html`
215             <div
216               contenteditable="true"
217               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
218               data-lexical-editor="true">
219               <ul>
220                 <li dir="ltr" value="1">
221                   <span data-lexical-text="true">bar</span>
222                 </li>
223                 <li dir="ltr" value="2">
224                   <span data-lexical-text="true">two</span>
225                 </li>
226                 <li dir="ltr" value="3">
227                   <span data-lexical-text="true">three</span>
228                 </li>
229               </ul>
230             </div>
231           `,
232         );
233       });
234
235       test('first list item with a non list item node', async () => {
236         const {editor} = testEnv;
237
238         await editor.update(() => {
239           return;
240         });
241
242         expectHtmlToBeEqual(
243           testEnv.outerHTML,
244           html`
245             <div
246               contenteditable="true"
247               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
248               data-lexical-editor="true">
249               <ul>
250                 <li dir="ltr" value="1">
251                   <span data-lexical-text="true">one</span>
252                 </li>
253                 <li dir="ltr" value="2">
254                   <span data-lexical-text="true">two</span>
255                 </li>
256                 <li dir="ltr" value="3">
257                   <span data-lexical-text="true">three</span>
258                 </li>
259               </ul>
260             </div>
261           `,
262         );
263
264         await editor.update(() => {
265           const paragraphNode = $createParagraphNode();
266           listItemNode1.replace(paragraphNode);
267         });
268
269         expectHtmlToBeEqual(
270           testEnv.outerHTML,
271           html`
272             <div
273               contenteditable="true"
274               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
275               data-lexical-editor="true">
276               <p><br /></p>
277               <ul>
278                 <li dir="ltr" value="1">
279                   <span data-lexical-text="true">two</span>
280                 </li>
281                 <li dir="ltr" value="2">
282                   <span data-lexical-text="true">three</span>
283                 </li>
284               </ul>
285             </div>
286           `,
287         );
288       });
289
290       test('last list item with a non list item node', async () => {
291         const {editor} = testEnv;
292
293         await editor.update(() => {
294           const paragraphNode = $createParagraphNode();
295           listItemNode3.replace(paragraphNode);
296         });
297
298         expectHtmlToBeEqual(
299           testEnv.outerHTML,
300           html`
301             <div
302               contenteditable="true"
303               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
304               data-lexical-editor="true">
305               <ul>
306                 <li dir="ltr" value="1">
307                   <span data-lexical-text="true">one</span>
308                 </li>
309                 <li dir="ltr" value="2">
310                   <span data-lexical-text="true">two</span>
311                 </li>
312               </ul>
313               <p><br /></p>
314             </div>
315           `,
316         );
317       });
318
319       test('middle list item with a non list item node', async () => {
320         const {editor} = testEnv;
321
322         await editor.update(() => {
323           const paragraphNode = $createParagraphNode();
324           listItemNode2.replace(paragraphNode);
325         });
326
327         expectHtmlToBeEqual(
328           testEnv.outerHTML,
329           html`
330             <div
331               contenteditable="true"
332               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
333               data-lexical-editor="true">
334               <ul>
335                 <li dir="ltr" value="1">
336                   <span data-lexical-text="true">one</span>
337                 </li>
338               </ul>
339               <p><br /></p>
340               <ul>
341                 <li dir="ltr" value="1">
342                   <span data-lexical-text="true">three</span>
343                 </li>
344               </ul>
345             </div>
346           `,
347         );
348       });
349
350       test('the only list item with a non list item node', async () => {
351         const {editor} = testEnv;
352
353         await editor.update(() => {
354           listItemNode2.remove();
355           listItemNode3.remove();
356         });
357
358         expectHtmlToBeEqual(
359           testEnv.outerHTML,
360           html`
361             <div
362               contenteditable="true"
363               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
364               data-lexical-editor="true">
365               <ul>
366                 <li dir="ltr" value="1">
367                   <span data-lexical-text="true">one</span>
368                 </li>
369               </ul>
370             </div>
371           `,
372         );
373
374         await editor.update(() => {
375           const paragraphNode = $createParagraphNode();
376           listItemNode1.replace(paragraphNode);
377         });
378
379         expectHtmlToBeEqual(
380           testEnv.outerHTML,
381           html`
382             <div
383               contenteditable="true"
384               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
385               data-lexical-editor="true">
386               <p><br /></p>
387             </div>
388           `,
389         );
390       });
391     });
392
393     describe('ListItemNode.remove()', () => {
394       // - A
395       // - x
396       // - B
397       test('siblings are not nested', async () => {
398         const {editor} = testEnv;
399         let x: ListItemNode;
400
401         await editor.update(() => {
402           const root = $getRoot();
403           const parent = new ListNode('bullet', 1);
404
405           const A_listItem = new ListItemNode();
406           A_listItem.append(new TextNode('A'));
407
408           x = new ListItemNode();
409           x.append(new TextNode('x'));
410
411           const B_listItem = new ListItemNode();
412           B_listItem.append(new TextNode('B'));
413
414           parent.append(A_listItem, x, B_listItem);
415           root.append(parent);
416         });
417
418         expectHtmlToBeEqual(
419           testEnv.outerHTML,
420           html`
421             <div
422               contenteditable="true"
423               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
424               data-lexical-editor="true">
425               <ul>
426                 <li dir="ltr" value="1">
427                   <span data-lexical-text="true">A</span>
428                 </li>
429                 <li dir="ltr" value="2">
430                   <span data-lexical-text="true">x</span>
431                 </li>
432                 <li dir="ltr" value="3">
433                   <span data-lexical-text="true">B</span>
434                 </li>
435               </ul>
436             </div>
437           `,
438         );
439
440         await editor.update(() => x.remove());
441
442         expectHtmlToBeEqual(
443           testEnv.outerHTML,
444           html`
445             <div
446               contenteditable="true"
447               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
448               data-lexical-editor="true">
449               <ul>
450                 <li dir="ltr" value="1">
451                   <span data-lexical-text="true">A</span>
452                 </li>
453                 <li dir="ltr" value="2">
454                   <span data-lexical-text="true">B</span>
455                 </li>
456               </ul>
457             </div>
458           `,
459         );
460       });
461
462       //   - A
463       // - x
464       // - B
465       test('the previous sibling is nested', async () => {
466         const {editor} = testEnv;
467         let x: ListItemNode;
468
469         await editor.update(() => {
470           const root = $getRoot();
471           const parent = new ListNode('bullet', 1);
472
473           const A_listItem = new ListItemNode();
474           const A_nestedList = new ListNode('bullet', 1);
475           const A_nestedListItem = new ListItemNode();
476           A_listItem.append(A_nestedList);
477           A_nestedList.append(A_nestedListItem);
478           A_nestedListItem.append(new TextNode('A'));
479
480           x = new ListItemNode();
481           x.append(new TextNode('x'));
482
483           const B_listItem = new ListItemNode();
484           B_listItem.append(new TextNode('B'));
485
486           parent.append(A_listItem, x, B_listItem);
487           root.append(parent);
488         });
489
490         expectHtmlToBeEqual(
491           testEnv.outerHTML,
492           html`
493             <div
494               contenteditable="true"
495               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
496               data-lexical-editor="true">
497               <ul>
498                 <li value="1">
499                   <ul>
500                     <li dir="ltr" value="1">
501                       <span data-lexical-text="true">A</span>
502                     </li>
503                   </ul>
504                 </li>
505                 <li dir="ltr" value="1">
506                   <span data-lexical-text="true">x</span>
507                 </li>
508                 <li dir="ltr" value="2">
509                   <span data-lexical-text="true">B</span>
510                 </li>
511               </ul>
512             </div>
513           `,
514         );
515
516         await editor.update(() => x.remove());
517
518         expectHtmlToBeEqual(
519           testEnv.outerHTML,
520           html`
521             <div
522               contenteditable="true"
523               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
524               data-lexical-editor="true">
525               <ul>
526                 <li value="1">
527                   <ul>
528                     <li dir="ltr" value="1">
529                       <span data-lexical-text="true">A</span>
530                     </li>
531                   </ul>
532                 </li>
533                 <li dir="ltr" value="1">
534                   <span data-lexical-text="true">B</span>
535                 </li>
536               </ul>
537             </div>
538           `,
539         );
540       });
541
542       // - A
543       // - x
544       //   - B
545       test('the next sibling is nested', async () => {
546         const {editor} = testEnv;
547         let x: ListItemNode;
548
549         await editor.update(() => {
550           const root = $getRoot();
551           const parent = new ListNode('bullet', 1);
552
553           const A_listItem = new ListItemNode();
554           A_listItem.append(new TextNode('A'));
555
556           x = new ListItemNode();
557           x.append(new TextNode('x'));
558
559           const B_listItem = new ListItemNode();
560           const B_nestedList = new ListNode('bullet', 1);
561           const B_nestedListItem = new ListItemNode();
562           B_listItem.append(B_nestedList);
563           B_nestedList.append(B_nestedListItem);
564           B_nestedListItem.append(new TextNode('B'));
565
566           parent.append(A_listItem, x, B_listItem);
567           root.append(parent);
568         });
569
570         expectHtmlToBeEqual(
571           testEnv.outerHTML,
572           html`
573             <div
574               contenteditable="true"
575               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
576               data-lexical-editor="true">
577               <ul>
578                 <li dir="ltr" value="1">
579                   <span data-lexical-text="true">A</span>
580                 </li>
581                 <li dir="ltr" value="2">
582                   <span data-lexical-text="true">x</span>
583                 </li>
584                 <li value="3">
585                   <ul>
586                     <li dir="ltr" value="1">
587                       <span data-lexical-text="true">B</span>
588                     </li>
589                   </ul>
590                 </li>
591               </ul>
592             </div>
593           `,
594         );
595
596         await editor.update(() => x.remove());
597
598         expectHtmlToBeEqual(
599           testEnv.outerHTML,
600           html`
601             <div
602               contenteditable="true"
603               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
604               data-lexical-editor="true">
605               <ul>
606                 <li dir="ltr" value="1">
607                   <span data-lexical-text="true">A</span>
608                 </li>
609                 <li value="2">
610                   <ul>
611                     <li dir="ltr" value="1">
612                       <span data-lexical-text="true">B</span>
613                     </li>
614                   </ul>
615                 </li>
616               </ul>
617             </div>
618           `,
619         );
620       });
621
622       //   - A
623       // - x
624       //   - B
625       test('both siblings are nested', async () => {
626         const {editor} = testEnv;
627         let x: ListItemNode;
628
629         await editor.update(() => {
630           const root = $getRoot();
631           const parent = new ListNode('bullet', 1);
632
633           const A_listItem = new ListItemNode();
634           const A_nestedList = new ListNode('bullet', 1);
635           const A_nestedListItem = new ListItemNode();
636           A_listItem.append(A_nestedList);
637           A_nestedList.append(A_nestedListItem);
638           A_nestedListItem.append(new TextNode('A'));
639
640           x = new ListItemNode();
641           x.append(new TextNode('x'));
642
643           const B_listItem = new ListItemNode();
644           const B_nestedList = new ListNode('bullet', 1);
645           const B_nestedListItem = new ListItemNode();
646           B_listItem.append(B_nestedList);
647           B_nestedList.append(B_nestedListItem);
648           B_nestedListItem.append(new TextNode('B'));
649
650           parent.append(A_listItem, x, B_listItem);
651           root.append(parent);
652         });
653
654         expectHtmlToBeEqual(
655           testEnv.outerHTML,
656           html`
657             <div
658               contenteditable="true"
659               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
660               data-lexical-editor="true">
661               <ul>
662                 <li value="1">
663                   <ul>
664                     <li dir="ltr" value="1">
665                       <span data-lexical-text="true">A</span>
666                     </li>
667                   </ul>
668                 </li>
669                 <li dir="ltr" value="1">
670                   <span data-lexical-text="true">x</span>
671                 </li>
672                 <li value="2">
673                   <ul>
674                     <li dir="ltr" value="1">
675                       <span data-lexical-text="true">B</span>
676                     </li>
677                   </ul>
678                 </li>
679               </ul>
680             </div>
681           `,
682         );
683
684         await editor.update(() => x.remove());
685
686         expectHtmlToBeEqual(
687           testEnv.outerHTML,
688           html`
689             <div
690               contenteditable="true"
691               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
692               data-lexical-editor="true">
693               <ul>
694                 <li value="1">
695                   <ul>
696                     <li dir="ltr" value="1">
697                       <span data-lexical-text="true">A</span>
698                     </li>
699                     <li dir="ltr" value="2">
700                       <span data-lexical-text="true">B</span>
701                     </li>
702                   </ul>
703                 </li>
704               </ul>
705             </div>
706           `,
707         );
708       });
709
710       //  - A1
711       //     - A2
712       // - x
713       //   - B
714       test('the previous sibling is nested deeper than the next sibling', async () => {
715         const {editor} = testEnv;
716         let x: ListItemNode;
717
718         await editor.update(() => {
719           const root = $getRoot();
720           const parent = new ListNode('bullet', 1);
721
722           const A_listItem = new ListItemNode();
723           const A_nestedList = new ListNode('bullet', 1);
724           const A_nestedListItem1 = new ListItemNode();
725           const A_nestedListItem2 = new ListItemNode();
726           const A_deeplyNestedList = new ListNode('bullet', 1);
727           const A_deeplyNestedListItem = new ListItemNode();
728           A_listItem.append(A_nestedList);
729           A_nestedList.append(A_nestedListItem1);
730           A_nestedList.append(A_nestedListItem2);
731           A_nestedListItem1.append(new TextNode('A1'));
732           A_nestedListItem2.append(A_deeplyNestedList);
733           A_deeplyNestedList.append(A_deeplyNestedListItem);
734           A_deeplyNestedListItem.append(new TextNode('A2'));
735
736           x = new ListItemNode();
737           x.append(new TextNode('x'));
738
739           const B_listItem = new ListItemNode();
740           const B_nestedList = new ListNode('bullet', 1);
741           const B_nestedlistItem = new ListItemNode();
742           B_listItem.append(B_nestedList);
743           B_nestedList.append(B_nestedlistItem);
744           B_nestedlistItem.append(new TextNode('B'));
745
746           parent.append(A_listItem, x, B_listItem);
747           root.append(parent);
748         });
749
750         expectHtmlToBeEqual(
751           testEnv.outerHTML,
752           html`
753             <div
754               contenteditable="true"
755               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
756               data-lexical-editor="true">
757               <ul>
758                 <li value="1">
759                   <ul>
760                     <li dir="ltr" value="1">
761                       <span data-lexical-text="true">A1</span>
762                     </li>
763                     <li value="2">
764                       <ul>
765                         <li dir="ltr" value="1">
766                           <span data-lexical-text="true">A2</span>
767                         </li>
768                       </ul>
769                     </li>
770                   </ul>
771                 </li>
772                 <li dir="ltr" value="1">
773                   <span data-lexical-text="true">x</span>
774                 </li>
775                 <li value="2">
776                   <ul>
777                     <li dir="ltr" value="1">
778                       <span data-lexical-text="true">B</span>
779                     </li>
780                   </ul>
781                 </li>
782               </ul>
783             </div>
784           `,
785         );
786
787         await editor.update(() => x.remove());
788
789         expectHtmlToBeEqual(
790           testEnv.outerHTML,
791           html`
792             <div
793               contenteditable="true"
794               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
795               data-lexical-editor="true">
796               <ul>
797                 <li value="1">
798                   <ul>
799                     <li dir="ltr" value="1">
800                       <span data-lexical-text="true">A1</span>
801                     </li>
802                     <li value="2">
803                       <ul>
804                         <li dir="ltr" value="1">
805                           <span data-lexical-text="true">A2</span>
806                         </li>
807                       </ul>
808                     </li>
809                     <li dir="ltr" value="2">
810                       <span data-lexical-text="true">B</span>
811                     </li>
812                   </ul>
813                 </li>
814               </ul>
815             </div>
816           `,
817         );
818       });
819
820       //   - A
821       // - x
822       //     - B1
823       //   - B2
824       test('the next sibling is nested deeper than the previous sibling', async () => {
825         const {editor} = testEnv;
826         let x: ListItemNode;
827
828         await editor.update(() => {
829           const root = $getRoot();
830           const parent = new ListNode('bullet', 1);
831
832           const A_listItem = new ListItemNode();
833           const A_nestedList = new ListNode('bullet', 1);
834           const A_nestedListItem = new ListItemNode();
835           A_listItem.append(A_nestedList);
836           A_nestedList.append(A_nestedListItem);
837           A_nestedListItem.append(new TextNode('A'));
838
839           x = new ListItemNode();
840           x.append(new TextNode('x'));
841
842           const B_listItem = new ListItemNode();
843           const B_nestedList = new ListNode('bullet', 1);
844           const B_nestedListItem1 = new ListItemNode();
845           const B_nestedListItem2 = new ListItemNode();
846           const B_deeplyNestedList = new ListNode('bullet', 1);
847           const B_deeplyNestedListItem = new ListItemNode();
848           B_listItem.append(B_nestedList);
849           B_nestedList.append(B_nestedListItem1);
850           B_nestedList.append(B_nestedListItem2);
851           B_nestedListItem1.append(B_deeplyNestedList);
852           B_nestedListItem2.append(new TextNode('B2'));
853           B_deeplyNestedList.append(B_deeplyNestedListItem);
854           B_deeplyNestedListItem.append(new TextNode('B1'));
855
856           parent.append(A_listItem, x, B_listItem);
857           root.append(parent);
858         });
859
860         expectHtmlToBeEqual(
861           testEnv.outerHTML,
862           html`
863             <div
864               contenteditable="true"
865               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
866               data-lexical-editor="true">
867               <ul>
868                 <li value="1">
869                   <ul>
870                     <li dir="ltr" value="1">
871                       <span data-lexical-text="true">A</span>
872                     </li>
873                   </ul>
874                 </li>
875                 <li dir="ltr" value="1">
876                   <span data-lexical-text="true">x</span>
877                 </li>
878                 <li value="2">
879                   <ul>
880                     <li value="1">
881                       <ul>
882                         <li dir="ltr" value="1">
883                           <span data-lexical-text="true">B1</span>
884                         </li>
885                       </ul>
886                     </li>
887                     <li dir="ltr" value="1">
888                       <span data-lexical-text="true">B2</span>
889                     </li>
890                   </ul>
891                 </li>
892               </ul>
893             </div>
894           `,
895         );
896
897         await editor.update(() => x.remove());
898
899         expectHtmlToBeEqual(
900           testEnv.outerHTML,
901           html`
902             <div
903               contenteditable="true"
904               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
905               data-lexical-editor="true">
906               <ul>
907                 <li value="1">
908                   <ul>
909                     <li dir="ltr" value="1">
910                       <span data-lexical-text="true">A</span>
911                     </li>
912                     <li value="2">
913                       <ul>
914                         <li dir="ltr" value="1">
915                           <span data-lexical-text="true">B1</span>
916                         </li>
917                       </ul>
918                     </li>
919                     <li dir="ltr" value="2">
920                       <span data-lexical-text="true">B2</span>
921                     </li>
922                   </ul>
923                 </li>
924               </ul>
925             </div>
926           `,
927         );
928       });
929
930       //   - A1
931       //     - A2
932       // - x
933       //     - B1
934       //   - B2
935       test('both siblings are deeply nested', async () => {
936         const {editor} = testEnv;
937         let x: ListItemNode;
938
939         await editor.update(() => {
940           const root = $getRoot();
941           const parent = new ListNode('bullet', 1);
942
943           const A_listItem = new ListItemNode();
944           const A_nestedList = new ListNode('bullet', 1);
945           const A_nestedListItem1 = new ListItemNode();
946           const A_nestedListItem2 = new ListItemNode();
947           const A_deeplyNestedList = new ListNode('bullet', 1);
948           const A_deeplyNestedListItem = new ListItemNode();
949           A_listItem.append(A_nestedList);
950           A_nestedList.append(A_nestedListItem1);
951           A_nestedList.append(A_nestedListItem2);
952           A_nestedListItem1.append(new TextNode('A1'));
953           A_nestedListItem2.append(A_deeplyNestedList);
954           A_deeplyNestedList.append(A_deeplyNestedListItem);
955           A_deeplyNestedListItem.append(new TextNode('A2'));
956
957           x = new ListItemNode();
958           x.append(new TextNode('x'));
959
960           const B_listItem = new ListItemNode();
961           const B_nestedList = new ListNode('bullet', 1);
962           const B_nestedListItem1 = new ListItemNode();
963           const B_nestedListItem2 = new ListItemNode();
964           const B_deeplyNestedList = new ListNode('bullet', 1);
965           const B_deeplyNestedListItem = new ListItemNode();
966           B_listItem.append(B_nestedList);
967           B_nestedList.append(B_nestedListItem1);
968           B_nestedList.append(B_nestedListItem2);
969           B_nestedListItem1.append(B_deeplyNestedList);
970           B_nestedListItem2.append(new TextNode('B2'));
971           B_deeplyNestedList.append(B_deeplyNestedListItem);
972           B_deeplyNestedListItem.append(new TextNode('B1'));
973
974           parent.append(A_listItem, x, B_listItem);
975           root.append(parent);
976         });
977
978         expectHtmlToBeEqual(
979           testEnv.outerHTML,
980           html`
981             <div
982               contenteditable="true"
983               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
984               data-lexical-editor="true">
985               <ul>
986                 <li value="1">
987                   <ul>
988                     <li dir="ltr" value="1">
989                       <span data-lexical-text="true">A1</span>
990                     </li>
991                     <li value="2">
992                       <ul>
993                         <li dir="ltr" value="1">
994                           <span data-lexical-text="true">A2</span>
995                         </li>
996                       </ul>
997                     </li>
998                   </ul>
999                 </li>
1000                 <li dir="ltr" value="1">
1001                   <span data-lexical-text="true">x</span>
1002                 </li>
1003                 <li value="2">
1004                   <ul>
1005                     <li value="1">
1006                       <ul>
1007                         <li dir="ltr" value="1">
1008                           <span data-lexical-text="true">B1</span>
1009                         </li>
1010                       </ul>
1011                     </li>
1012                     <li dir="ltr" value="1">
1013                       <span data-lexical-text="true">B2</span>
1014                     </li>
1015                   </ul>
1016                 </li>
1017               </ul>
1018             </div>
1019           `,
1020         );
1021
1022         await editor.update(() => x.remove());
1023
1024         expectHtmlToBeEqual(
1025           testEnv.outerHTML,
1026           html`
1027             <div
1028               contenteditable="true"
1029               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1030               data-lexical-editor="true">
1031               <ul>
1032                 <li value="1">
1033                   <ul>
1034                     <li dir="ltr" value="1">
1035                       <span data-lexical-text="true">A1</span>
1036                     </li>
1037                     <li value="2">
1038                       <ul>
1039                         <li dir="ltr" value="1">
1040                           <span data-lexical-text="true">A2</span>
1041                         </li>
1042                         <li dir="ltr" value="2">
1043                           <span data-lexical-text="true">B1</span>
1044                         </li>
1045                       </ul>
1046                     </li>
1047                     <li dir="ltr" value="2">
1048                       <span data-lexical-text="true">B2</span>
1049                     </li>
1050                   </ul>
1051                 </li>
1052               </ul>
1053             </div>
1054           `,
1055         );
1056       });
1057     });
1058
1059     describe('ListItemNode.insertNewAfter(): non-empty list items', () => {
1060       let listNode: ListNode;
1061       let listItemNode1: ListItemNode;
1062       let listItemNode2: ListItemNode;
1063       let listItemNode3: ListItemNode;
1064
1065       beforeEach(async () => {
1066         const {editor} = testEnv;
1067
1068         await editor.update(() => {
1069           const root = $getRoot();
1070           listNode = new ListNode('bullet', 1);
1071           listItemNode1 = new ListItemNode();
1072
1073           listItemNode2 = new ListItemNode();
1074
1075           listItemNode3 = new ListItemNode();
1076
1077           root.append(listNode);
1078           listNode.append(listItemNode1, listItemNode2, listItemNode3);
1079           listItemNode1.append(new TextNode('one'));
1080           listItemNode2.append(new TextNode('two'));
1081           listItemNode3.append(new TextNode('three'));
1082         });
1083
1084         expectHtmlToBeEqual(
1085           testEnv.outerHTML,
1086           html`
1087             <div
1088               contenteditable="true"
1089               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1090               data-lexical-editor="true">
1091               <ul>
1092                 <li dir="ltr" value="1">
1093                   <span data-lexical-text="true">one</span>
1094                 </li>
1095                 <li dir="ltr" value="2">
1096                   <span data-lexical-text="true">two</span>
1097                 </li>
1098                 <li dir="ltr" value="3">
1099                   <span data-lexical-text="true">three</span>
1100                 </li>
1101               </ul>
1102             </div>
1103           `,
1104         );
1105       });
1106
1107       test('first list item', async () => {
1108         const {editor} = testEnv;
1109
1110         await editor.update(() => {
1111           listItemNode1.insertNewAfter($createRangeSelection());
1112         });
1113
1114         expectHtmlToBeEqual(
1115           testEnv.outerHTML,
1116           html`
1117             <div
1118               contenteditable="true"
1119               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1120               data-lexical-editor="true">
1121               <ul>
1122                 <li dir="ltr" value="1">
1123                   <span data-lexical-text="true">one</span>
1124                 </li>
1125                 <li value="2"><br /></li>
1126                 <li dir="ltr" value="3">
1127                   <span data-lexical-text="true">two</span>
1128                 </li>
1129                 <li dir="ltr" value="4">
1130                   <span data-lexical-text="true">three</span>
1131                 </li>
1132               </ul>
1133             </div>
1134           `,
1135         );
1136       });
1137
1138       test('last list item', async () => {
1139         const {editor} = testEnv;
1140
1141         await editor.update(() => {
1142           listItemNode3.insertNewAfter($createRangeSelection());
1143         });
1144
1145         expectHtmlToBeEqual(
1146           testEnv.outerHTML,
1147           html`
1148             <div
1149               contenteditable="true"
1150               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1151               data-lexical-editor="true">
1152               <ul>
1153                 <li dir="ltr" value="1">
1154                   <span data-lexical-text="true">one</span>
1155                 </li>
1156                 <li dir="ltr" value="2">
1157                   <span data-lexical-text="true">two</span>
1158                 </li>
1159                 <li dir="ltr" value="3">
1160                   <span data-lexical-text="true">three</span>
1161                 </li>
1162                 <li value="4"><br /></li>
1163               </ul>
1164             </div>
1165           `,
1166         );
1167       });
1168
1169       test('middle list item', async () => {
1170         const {editor} = testEnv;
1171
1172         await editor.update(() => {
1173           listItemNode3.insertNewAfter($createRangeSelection());
1174         });
1175
1176         expectHtmlToBeEqual(
1177           testEnv.outerHTML,
1178           html`
1179             <div
1180               contenteditable="true"
1181               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1182               data-lexical-editor="true">
1183               <ul>
1184                 <li dir="ltr" value="1">
1185                   <span data-lexical-text="true">one</span>
1186                 </li>
1187                 <li dir="ltr" value="2">
1188                   <span data-lexical-text="true">two</span>
1189                 </li>
1190                 <li dir="ltr" value="3">
1191                   <span data-lexical-text="true">three</span>
1192                 </li>
1193                 <li value="4"><br /></li>
1194               </ul>
1195             </div>
1196           `,
1197         );
1198       });
1199
1200       test('the only list item', async () => {
1201         const {editor} = testEnv;
1202
1203         await editor.update(() => {
1204           listItemNode2.remove();
1205           listItemNode3.remove();
1206         });
1207
1208         expectHtmlToBeEqual(
1209           testEnv.outerHTML,
1210           html`
1211             <div
1212               contenteditable="true"
1213               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1214               data-lexical-editor="true">
1215               <ul>
1216                 <li dir="ltr" value="1">
1217                   <span data-lexical-text="true">one</span>
1218                 </li>
1219               </ul>
1220             </div>
1221           `,
1222         );
1223
1224         await editor.update(() => {
1225           listItemNode1.insertNewAfter($createRangeSelection());
1226         });
1227
1228         expectHtmlToBeEqual(
1229           testEnv.outerHTML,
1230           html`
1231             <div
1232               contenteditable="true"
1233               style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1234               data-lexical-editor="true">
1235               <ul>
1236                 <li dir="ltr" value="1">
1237                   <span data-lexical-text="true">one</span>
1238                 </li>
1239                 <li value="2"><br /></li>
1240               </ul>
1241             </div>
1242           `,
1243         );
1244       });
1245     });
1246
1247     test('$createListItemNode()', async () => {
1248       const {editor} = testEnv;
1249
1250       await editor.update(() => {
1251         const listItemNode = new ListItemNode();
1252
1253         const createdListItemNode = $createListItemNode();
1254
1255         expect(listItemNode.__type).toEqual(createdListItemNode.__type);
1256         expect(listItemNode.__parent).toEqual(createdListItemNode.__parent);
1257         expect(listItemNode.__key).not.toEqual(createdListItemNode.__key);
1258       });
1259     });
1260
1261     test('$isListItemNode()', async () => {
1262       const {editor} = testEnv;
1263
1264       await editor.update(() => {
1265         const listItemNode = new ListItemNode();
1266
1267         expect($isListItemNode(listItemNode)).toBe(true);
1268       });
1269     });
1270
1271     describe('ListItemNode.setIndent()', () => {
1272       let listNode: ListNode;
1273       let listItemNode1: ListItemNode;
1274       let listItemNode2: ListItemNode;
1275
1276       beforeEach(async () => {
1277         const {editor} = testEnv;
1278
1279         await editor.update(() => {
1280           const root = $getRoot();
1281           listNode = new ListNode('bullet', 1);
1282           listItemNode1 = new ListItemNode();
1283
1284           listItemNode2 = new ListItemNode();
1285
1286           root.append(listNode);
1287           listNode.append(listItemNode1, listItemNode2);
1288           listItemNode1.append(new TextNode('one'));
1289           listItemNode2.append(new TextNode('two'));
1290         });
1291       });
1292       it('indents and outdents list item', async () => {
1293         const {editor} = testEnv;
1294
1295         await editor.update(() => {
1296           listItemNode1.setIndent(3);
1297         });
1298
1299         await editor.update(() => {
1300           expect(listItemNode1.getIndent()).toBe(3);
1301         });
1302
1303         expectHtmlToBeEqual(
1304           editor.getRootElement()!.innerHTML,
1305           html`
1306             <ul>
1307               <li value="1">
1308                 <ul>
1309                   <li value="1">
1310                     <ul>
1311                       <li value="1">
1312                         <ul>
1313                           <li dir="ltr" value="1">
1314                             <span data-lexical-text="true">one</span>
1315                           </li>
1316                         </ul>
1317                       </li>
1318                     </ul>
1319                   </li>
1320                 </ul>
1321               </li>
1322               <li dir="ltr" value="1">
1323                 <span data-lexical-text="true">two</span>
1324               </li>
1325             </ul>
1326           `,
1327         );
1328
1329         await editor.update(() => {
1330           listItemNode1.setIndent(0);
1331         });
1332
1333         await editor.update(() => {
1334           expect(listItemNode1.getIndent()).toBe(0);
1335         });
1336
1337         expectHtmlToBeEqual(
1338           editor.getRootElement()!.innerHTML,
1339           html`
1340             <ul>
1341               <li dir="ltr" value="1">
1342                 <span data-lexical-text="true">one</span>
1343               </li>
1344               <li dir="ltr" value="2">
1345                 <span data-lexical-text="true">two</span>
1346               </li>
1347             </ul>
1348           `,
1349         );
1350       });
1351
1352       it('handles fractional indent values', async () => {
1353         const {editor} = testEnv;
1354
1355         await editor.update(() => {
1356           listItemNode1.setIndent(0.5);
1357         });
1358
1359         await editor.update(() => {
1360           expect(listItemNode1.getIndent()).toBe(0);
1361         });
1362       });
1363     });
1364   });
1365 });