2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
11 $createRangeSelection,
19 } from 'lexical/__tests__/utils';
28 const editorConfig = Object.freeze({
32 listitem: 'my-listItem-item-class',
34 listitem: 'my-nested-list-listItem-class',
40 describe('LexicalListItemNode tests', () => {
41 initializeUnitTest((testEnv) => {
42 test('ListItemNode.constructor', async () => {
43 const {editor} = testEnv;
45 await editor.update(() => {
46 const listItemNode = new ListItemNode();
48 expect(listItemNode.getType()).toBe('listitem');
50 expect(listItemNode.getTextContent()).toBe('');
53 expect(() => new ListItemNode()).toThrow();
56 test('ListItemNode.createDOM()', async () => {
57 const {editor} = testEnv;
59 await editor.update(() => {
60 const listItemNode = new ListItemNode();
63 listItemNode.createDOM(editorConfig).outerHTML,
65 <li value="1" class="my-listItem-item-class"></li>
70 listItemNode.createDOM({
81 describe('ListItemNode.updateDOM()', () => {
82 test('base', async () => {
83 const {editor} = testEnv;
85 await editor.update(() => {
86 const listItemNode = new ListItemNode();
88 const domElement = listItemNode.createDOM(editorConfig);
93 <li value="1" class="my-listItem-item-class"></li>
96 const newListItemNode = new ListItemNode();
98 const result = newListItemNode.updateDOM(
104 expect(result).toBe(false);
107 domElement.outerHTML,
109 <li value="1" class="my-listItem-item-class"></li>
115 test('nested list', async () => {
116 const {editor} = testEnv;
118 await editor.update(() => {
119 const parentListNode = new ListNode('bullet', 1);
120 const parentlistItemNode = new ListItemNode();
122 parentListNode.append(parentlistItemNode);
123 const domElement = parentlistItemNode.createDOM(editorConfig);
126 domElement.outerHTML,
128 <li value="1" class="my-listItem-item-class"></li>
131 const nestedListNode = new ListNode('bullet', 1);
132 nestedListNode.append(new ListItemNode());
133 parentlistItemNode.append(nestedListNode);
134 const result = parentlistItemNode.updateDOM(
140 expect(result).toBe(false);
143 domElement.outerHTML,
145 <li value="1" class="my-listItem-item-class my-nested-list-listItem-class"></li>
152 describe('ListItemNode.replace()', () => {
153 let listNode: ListNode;
154 let listItemNode1: ListItemNode;
155 let listItemNode2: ListItemNode;
156 let listItemNode3: ListItemNode;
158 beforeEach(async () => {
159 const {editor} = testEnv;
161 await editor.update(() => {
162 const root = $getRoot();
163 listNode = new ListNode('bullet', 1);
164 listItemNode1 = new ListItemNode();
166 listItemNode1.append(new TextNode('one'));
167 listItemNode2 = new ListItemNode();
169 listItemNode2.append(new TextNode('two'));
170 listItemNode3 = new ListItemNode();
172 listItemNode3.append(new TextNode('three'));
173 root.append(listNode);
174 listNode.append(listItemNode1, listItemNode2, listItemNode3);
181 contenteditable="true"
182 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
183 data-lexical-editor="true">
186 <span data-lexical-text="true">one</span>
189 <span data-lexical-text="true">two</span>
192 <span data-lexical-text="true">three</span>
200 test('another list item node', async () => {
201 const {editor} = testEnv;
203 await editor.update(() => {
204 const newListItemNode = new ListItemNode();
206 newListItemNode.append(new TextNode('bar'));
207 listItemNode1.replace(newListItemNode);
214 contenteditable="true"
215 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
216 data-lexical-editor="true">
219 <span data-lexical-text="true">bar</span>
222 <span data-lexical-text="true">two</span>
225 <span data-lexical-text="true">three</span>
233 test('first list item with a non list item node', async () => {
234 const {editor} = testEnv;
236 await editor.update(() => {
244 contenteditable="true"
245 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
246 data-lexical-editor="true">
249 <span data-lexical-text="true">one</span>
252 <span data-lexical-text="true">two</span>
255 <span data-lexical-text="true">three</span>
262 await editor.update(() => {
263 const paragraphNode = $createParagraphNode();
264 listItemNode1.replace(paragraphNode);
271 contenteditable="true"
272 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
273 data-lexical-editor="true">
277 <span data-lexical-text="true">two</span>
280 <span data-lexical-text="true">three</span>
288 test('last list item with a non list item node', async () => {
289 const {editor} = testEnv;
291 await editor.update(() => {
292 const paragraphNode = $createParagraphNode();
293 listItemNode3.replace(paragraphNode);
300 contenteditable="true"
301 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
302 data-lexical-editor="true">
305 <span data-lexical-text="true">one</span>
308 <span data-lexical-text="true">two</span>
317 test('middle list item with a non list item node', async () => {
318 const {editor} = testEnv;
320 await editor.update(() => {
321 const paragraphNode = $createParagraphNode();
322 listItemNode2.replace(paragraphNode);
329 contenteditable="true"
330 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
331 data-lexical-editor="true">
334 <span data-lexical-text="true">one</span>
340 <span data-lexical-text="true">three</span>
348 test('the only list item with a non list item node', async () => {
349 const {editor} = testEnv;
351 await editor.update(() => {
352 listItemNode2.remove();
353 listItemNode3.remove();
360 contenteditable="true"
361 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
362 data-lexical-editor="true">
365 <span data-lexical-text="true">one</span>
372 await editor.update(() => {
373 const paragraphNode = $createParagraphNode();
374 listItemNode1.replace(paragraphNode);
381 contenteditable="true"
382 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
383 data-lexical-editor="true">
391 describe('ListItemNode.remove()', () => {
395 test('siblings are not nested', async () => {
396 const {editor} = testEnv;
399 await editor.update(() => {
400 const root = $getRoot();
401 const parent = new ListNode('bullet', 1);
403 const A_listItem = new ListItemNode();
404 A_listItem.append(new TextNode('A'));
406 x = new ListItemNode();
407 x.append(new TextNode('x'));
409 const B_listItem = new ListItemNode();
410 B_listItem.append(new TextNode('B'));
412 parent.append(A_listItem, x, B_listItem);
420 contenteditable="true"
421 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
422 data-lexical-editor="true">
425 <span data-lexical-text="true">A</span>
428 <span data-lexical-text="true">x</span>
431 <span data-lexical-text="true">B</span>
438 await editor.update(() => x.remove());
444 contenteditable="true"
445 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
446 data-lexical-editor="true">
449 <span data-lexical-text="true">A</span>
452 <span data-lexical-text="true">B</span>
463 test('the previous sibling is nested', async () => {
464 const {editor} = testEnv;
467 await editor.update(() => {
468 const root = $getRoot();
469 const parent = new ListNode('bullet', 1);
471 const A_listItem = new ListItemNode();
472 const A_nestedList = new ListNode('bullet', 1);
473 const A_nestedListItem = new ListItemNode();
474 A_listItem.append(A_nestedList);
475 A_nestedList.append(A_nestedListItem);
476 A_nestedListItem.append(new TextNode('A'));
478 x = new ListItemNode();
479 x.append(new TextNode('x'));
481 const B_listItem = new ListItemNode();
482 B_listItem.append(new TextNode('B'));
484 parent.append(A_listItem, x, B_listItem);
492 contenteditable="true"
493 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
494 data-lexical-editor="true">
499 <span data-lexical-text="true">A</span>
504 <span data-lexical-text="true">x</span>
507 <span data-lexical-text="true">B</span>
514 await editor.update(() => x.remove());
520 contenteditable="true"
521 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
522 data-lexical-editor="true">
527 <span data-lexical-text="true">A</span>
532 <span data-lexical-text="true">B</span>
543 test('the next sibling is nested', async () => {
544 const {editor} = testEnv;
547 await editor.update(() => {
548 const root = $getRoot();
549 const parent = new ListNode('bullet', 1);
551 const A_listItem = new ListItemNode();
552 A_listItem.append(new TextNode('A'));
554 x = new ListItemNode();
555 x.append(new TextNode('x'));
557 const B_listItem = new ListItemNode();
558 const B_nestedList = new ListNode('bullet', 1);
559 const B_nestedListItem = new ListItemNode();
560 B_listItem.append(B_nestedList);
561 B_nestedList.append(B_nestedListItem);
562 B_nestedListItem.append(new TextNode('B'));
564 parent.append(A_listItem, x, B_listItem);
572 contenteditable="true"
573 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
574 data-lexical-editor="true">
577 <span data-lexical-text="true">A</span>
580 <span data-lexical-text="true">x</span>
585 <span data-lexical-text="true">B</span>
594 await editor.update(() => x.remove());
600 contenteditable="true"
601 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
602 data-lexical-editor="true">
605 <span data-lexical-text="true">A</span>
610 <span data-lexical-text="true">B</span>
623 test('both siblings are nested', async () => {
624 const {editor} = testEnv;
627 await editor.update(() => {
628 const root = $getRoot();
629 const parent = new ListNode('bullet', 1);
631 const A_listItem = new ListItemNode();
632 const A_nestedList = new ListNode('bullet', 1);
633 const A_nestedListItem = new ListItemNode();
634 A_listItem.append(A_nestedList);
635 A_nestedList.append(A_nestedListItem);
636 A_nestedListItem.append(new TextNode('A'));
638 x = new ListItemNode();
639 x.append(new TextNode('x'));
641 const B_listItem = new ListItemNode();
642 const B_nestedList = new ListNode('bullet', 1);
643 const B_nestedListItem = new ListItemNode();
644 B_listItem.append(B_nestedList);
645 B_nestedList.append(B_nestedListItem);
646 B_nestedListItem.append(new TextNode('B'));
648 parent.append(A_listItem, x, B_listItem);
656 contenteditable="true"
657 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
658 data-lexical-editor="true">
663 <span data-lexical-text="true">A</span>
668 <span data-lexical-text="true">x</span>
673 <span data-lexical-text="true">B</span>
682 await editor.update(() => x.remove());
688 contenteditable="true"
689 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
690 data-lexical-editor="true">
695 <span data-lexical-text="true">A</span>
698 <span data-lexical-text="true">B</span>
712 test('the previous sibling is nested deeper than the next sibling', async () => {
713 const {editor} = testEnv;
716 await editor.update(() => {
717 const root = $getRoot();
718 const parent = new ListNode('bullet', 1);
720 const A_listItem = new ListItemNode();
721 const A_nestedList = new ListNode('bullet', 1);
722 const A_nestedListItem1 = new ListItemNode();
723 const A_nestedListItem2 = new ListItemNode();
724 const A_deeplyNestedList = new ListNode('bullet', 1);
725 const A_deeplyNestedListItem = new ListItemNode();
726 A_listItem.append(A_nestedList);
727 A_nestedList.append(A_nestedListItem1);
728 A_nestedList.append(A_nestedListItem2);
729 A_nestedListItem1.append(new TextNode('A1'));
730 A_nestedListItem2.append(A_deeplyNestedList);
731 A_deeplyNestedList.append(A_deeplyNestedListItem);
732 A_deeplyNestedListItem.append(new TextNode('A2'));
734 x = new ListItemNode();
735 x.append(new TextNode('x'));
737 const B_listItem = new ListItemNode();
738 const B_nestedList = new ListNode('bullet', 1);
739 const B_nestedlistItem = new ListItemNode();
740 B_listItem.append(B_nestedList);
741 B_nestedList.append(B_nestedlistItem);
742 B_nestedlistItem.append(new TextNode('B'));
744 parent.append(A_listItem, x, B_listItem);
752 contenteditable="true"
753 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
754 data-lexical-editor="true">
759 <span data-lexical-text="true">A1</span>
764 <span data-lexical-text="true">A2</span>
771 <span data-lexical-text="true">x</span>
776 <span data-lexical-text="true">B</span>
785 await editor.update(() => x.remove());
791 contenteditable="true"
792 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
793 data-lexical-editor="true">
798 <span data-lexical-text="true">A1</span>
803 <span data-lexical-text="true">A2</span>
808 <span data-lexical-text="true">B</span>
822 test('the next sibling is nested deeper than the previous sibling', async () => {
823 const {editor} = testEnv;
826 await editor.update(() => {
827 const root = $getRoot();
828 const parent = new ListNode('bullet', 1);
830 const A_listItem = new ListItemNode();
831 const A_nestedList = new ListNode('bullet', 1);
832 const A_nestedListItem = new ListItemNode();
833 A_listItem.append(A_nestedList);
834 A_nestedList.append(A_nestedListItem);
835 A_nestedListItem.append(new TextNode('A'));
837 x = new ListItemNode();
838 x.append(new TextNode('x'));
840 const B_listItem = new ListItemNode();
841 const B_nestedList = new ListNode('bullet', 1);
842 const B_nestedListItem1 = new ListItemNode();
843 const B_nestedListItem2 = new ListItemNode();
844 const B_deeplyNestedList = new ListNode('bullet', 1);
845 const B_deeplyNestedListItem = new ListItemNode();
846 B_listItem.append(B_nestedList);
847 B_nestedList.append(B_nestedListItem1);
848 B_nestedList.append(B_nestedListItem2);
849 B_nestedListItem1.append(B_deeplyNestedList);
850 B_nestedListItem2.append(new TextNode('B2'));
851 B_deeplyNestedList.append(B_deeplyNestedListItem);
852 B_deeplyNestedListItem.append(new TextNode('B1'));
854 parent.append(A_listItem, x, B_listItem);
862 contenteditable="true"
863 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
864 data-lexical-editor="true">
869 <span data-lexical-text="true">A</span>
874 <span data-lexical-text="true">x</span>
881 <span data-lexical-text="true">B1</span>
886 <span data-lexical-text="true">B2</span>
895 await editor.update(() => x.remove());
901 contenteditable="true"
902 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
903 data-lexical-editor="true">
908 <span data-lexical-text="true">A</span>
913 <span data-lexical-text="true">B1</span>
918 <span data-lexical-text="true">B2</span>
933 test('both siblings are deeply nested', async () => {
934 const {editor} = testEnv;
937 await editor.update(() => {
938 const root = $getRoot();
939 const parent = new ListNode('bullet', 1);
941 const A_listItem = new ListItemNode();
942 const A_nestedList = new ListNode('bullet', 1);
943 const A_nestedListItem1 = new ListItemNode();
944 const A_nestedListItem2 = new ListItemNode();
945 const A_deeplyNestedList = new ListNode('bullet', 1);
946 const A_deeplyNestedListItem = new ListItemNode();
947 A_listItem.append(A_nestedList);
948 A_nestedList.append(A_nestedListItem1);
949 A_nestedList.append(A_nestedListItem2);
950 A_nestedListItem1.append(new TextNode('A1'));
951 A_nestedListItem2.append(A_deeplyNestedList);
952 A_deeplyNestedList.append(A_deeplyNestedListItem);
953 A_deeplyNestedListItem.append(new TextNode('A2'));
955 x = new ListItemNode();
956 x.append(new TextNode('x'));
958 const B_listItem = new ListItemNode();
959 const B_nestedList = new ListNode('bullet', 1);
960 const B_nestedListItem1 = new ListItemNode();
961 const B_nestedListItem2 = new ListItemNode();
962 const B_deeplyNestedList = new ListNode('bullet', 1);
963 const B_deeplyNestedListItem = new ListItemNode();
964 B_listItem.append(B_nestedList);
965 B_nestedList.append(B_nestedListItem1);
966 B_nestedList.append(B_nestedListItem2);
967 B_nestedListItem1.append(B_deeplyNestedList);
968 B_nestedListItem2.append(new TextNode('B2'));
969 B_deeplyNestedList.append(B_deeplyNestedListItem);
970 B_deeplyNestedListItem.append(new TextNode('B1'));
972 parent.append(A_listItem, x, B_listItem);
980 contenteditable="true"
981 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
982 data-lexical-editor="true">
987 <span data-lexical-text="true">A1</span>
992 <span data-lexical-text="true">A2</span>
999 <span data-lexical-text="true">x</span>
1006 <span data-lexical-text="true">B1</span>
1011 <span data-lexical-text="true">B2</span>
1020 await editor.update(() => x.remove());
1022 expectHtmlToBeEqual(
1026 contenteditable="true"
1027 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1028 data-lexical-editor="true">
1033 <span data-lexical-text="true">A1</span>
1038 <span data-lexical-text="true">A2</span>
1041 <span data-lexical-text="true">B1</span>
1046 <span data-lexical-text="true">B2</span>
1057 describe('ListItemNode.insertNewAfter(): non-empty list items', () => {
1058 let listNode: ListNode;
1059 let listItemNode1: ListItemNode;
1060 let listItemNode2: ListItemNode;
1061 let listItemNode3: ListItemNode;
1063 beforeEach(async () => {
1064 const {editor} = testEnv;
1066 await editor.update(() => {
1067 const root = $getRoot();
1068 listNode = new ListNode('bullet', 1);
1069 listItemNode1 = new ListItemNode();
1071 listItemNode2 = new ListItemNode();
1073 listItemNode3 = new ListItemNode();
1075 root.append(listNode);
1076 listNode.append(listItemNode1, listItemNode2, listItemNode3);
1077 listItemNode1.append(new TextNode('one'));
1078 listItemNode2.append(new TextNode('two'));
1079 listItemNode3.append(new TextNode('three'));
1082 expectHtmlToBeEqual(
1086 contenteditable="true"
1087 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1088 data-lexical-editor="true">
1091 <span data-lexical-text="true">one</span>
1094 <span data-lexical-text="true">two</span>
1097 <span data-lexical-text="true">three</span>
1105 test('first list item', async () => {
1106 const {editor} = testEnv;
1108 await editor.update(() => {
1109 listItemNode1.insertNewAfter($createRangeSelection());
1112 expectHtmlToBeEqual(
1116 contenteditable="true"
1117 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1118 data-lexical-editor="true">
1121 <span data-lexical-text="true">one</span>
1123 <li value="2"><br></li>
1125 <span data-lexical-text="true">two</span>
1128 <span data-lexical-text="true">three</span>
1136 test('last list item', async () => {
1137 const {editor} = testEnv;
1139 await editor.update(() => {
1140 listItemNode3.insertNewAfter($createRangeSelection());
1143 expectHtmlToBeEqual(
1147 contenteditable="true"
1148 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1149 data-lexical-editor="true">
1152 <span data-lexical-text="true">one</span>
1155 <span data-lexical-text="true">two</span>
1158 <span data-lexical-text="true">three</span>
1160 <li value="4"><br></li>
1167 test('middle list item', async () => {
1168 const {editor} = testEnv;
1170 await editor.update(() => {
1171 listItemNode3.insertNewAfter($createRangeSelection());
1174 expectHtmlToBeEqual(
1178 contenteditable="true"
1179 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1180 data-lexical-editor="true">
1183 <span data-lexical-text="true">one</span>
1186 <span data-lexical-text="true">two</span>
1189 <span data-lexical-text="true">three</span>
1191 <li value="4"><br></li>
1198 test('the only list item', async () => {
1199 const {editor} = testEnv;
1201 await editor.update(() => {
1202 listItemNode2.remove();
1203 listItemNode3.remove();
1206 expectHtmlToBeEqual(
1210 contenteditable="true"
1211 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1212 data-lexical-editor="true">
1215 <span data-lexical-text="true">one</span>
1222 await editor.update(() => {
1223 listItemNode1.insertNewAfter($createRangeSelection());
1226 expectHtmlToBeEqual(
1230 contenteditable="true"
1231 style="user-select: text; white-space: pre-wrap; word-break: break-word;"
1232 data-lexical-editor="true">
1235 <span data-lexical-text="true">one</span>
1237 <li value="2"><br></li>
1245 test('$createListItemNode()', async () => {
1246 const {editor} = testEnv;
1248 await editor.update(() => {
1249 const listItemNode = new ListItemNode();
1251 const createdListItemNode = $createListItemNode();
1253 expect(listItemNode.__type).toEqual(createdListItemNode.__type);
1254 expect(listItemNode.__parent).toEqual(createdListItemNode.__parent);
1255 expect(listItemNode.__key).not.toEqual(createdListItemNode.__key);
1259 test('$isListItemNode()', async () => {
1260 const {editor} = testEnv;
1262 await editor.update(() => {
1263 const listItemNode = new ListItemNode();
1265 expect($isListItemNode(listItemNode)).toBe(true);
1269 describe('ListItemNode.setIndent()', () => {
1270 let listNode: ListNode;
1271 let listItemNode1: ListItemNode;
1272 let listItemNode2: ListItemNode;
1274 beforeEach(async () => {
1275 const {editor} = testEnv;
1277 await editor.update(() => {
1278 const root = $getRoot();
1279 listNode = new ListNode('bullet', 1);
1280 listItemNode1 = new ListItemNode();
1282 listItemNode2 = new ListItemNode();
1284 root.append(listNode);
1285 listNode.append(listItemNode1, listItemNode2);
1286 listItemNode1.append(new TextNode('one'));
1287 listItemNode2.append(new TextNode('two'));
1290 it('indents and outdents list item', async () => {
1291 const {editor} = testEnv;
1293 await editor.update(() => {
1294 listItemNode1.setIndent(3);
1297 await editor.update(() => {
1298 expect(listItemNode1.getIndent()).toBe(3);
1301 expectHtmlToBeEqual(
1302 editor.getRootElement()!.innerHTML,
1312 <span data-lexical-text="true">one</span>
1321 <span data-lexical-text="true">two</span>
1327 await editor.update(() => {
1328 listItemNode1.setIndent(0);
1331 await editor.update(() => {
1332 expect(listItemNode1.getIndent()).toBe(0);
1335 expectHtmlToBeEqual(
1336 editor.getRootElement()!.innerHTML,
1340 <span data-lexical-text="true">one</span>
1343 <span data-lexical-text="true">two</span>
1350 it('handles fractional indent values', async () => {
1351 const {editor} = testEnv;
1353 await editor.update(() => {
1354 listItemNode1.setIndent(0.5);
1357 await editor.update(() => {
1358 expect(listItemNode1.getIndent()).toBe(0);