]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalUtilsSplitNode.test.tsx
f04bb5d2e46fe18ae803c78e0946e5e6dcd074a2
[bookstack] / resources / js / wysiwyg / lexical / utils / __tests__ / unit / LexicalUtilsSplitNode.test.tsx
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 type {ElementNode, LexicalEditor} from 'lexical';
10
11 import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
12 import {$getRoot, $isElementNode} from 'lexical';
13 import {createTestEditor} from 'lexical/__tests__/utils';
14
15 import {$splitNode} from '../../index';
16
17 describe('LexicalUtils#splitNode', () => {
18   let editor: LexicalEditor;
19
20   const update = async (updateFn: () => void) => {
21     editor.update(updateFn);
22     await Promise.resolve();
23   };
24
25   beforeEach(async () => {
26     editor = createTestEditor();
27     editor._headless = true;
28   });
29
30   const testCases: Array<{
31     _: string;
32     expectedHtml: string;
33     initialHtml: string;
34     splitPath: Array<number>;
35     splitOffset: number;
36     only?: boolean;
37   }> = [
38     {
39       _: 'split paragraph in between two text nodes',
40       expectedHtml:
41         '<p><span style="white-space: pre-wrap;">Hello</span></p><p><span style="white-space: pre-wrap;">world</span></p>',
42       initialHtml: '<p><span>Hello</span><span>world</span></p>',
43       splitOffset: 1,
44       splitPath: [0],
45     },
46     {
47       _: 'split paragraph before the first text node',
48       expectedHtml:
49         '<p><br></p><p><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></p>',
50       initialHtml: '<p><span>Hello</span><span>world</span></p>',
51       splitOffset: 0,
52       splitPath: [0],
53     },
54     {
55       _: 'split paragraph after the last text node',
56       expectedHtml:
57         '<p><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></p><p><br></p>',
58       initialHtml: '<p><span>Hello</span><span>world</span></p>',
59       splitOffset: 2, // Any offset that is higher than children size
60       splitPath: [0],
61     },
62     {
63       _: 'split list items between two text nodes',
64       expectedHtml:
65         '<ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul>' +
66         '<ul><li><span style="white-space: pre-wrap;">world</span></li></ul>',
67       initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
68       splitOffset: 1, // Any offset that is higher than children size
69       splitPath: [0, 0],
70     },
71     {
72       _: 'split list items before the first text node',
73       expectedHtml:
74         '<ul><li></li></ul>' +
75         '<ul><li><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></li></ul>',
76       initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
77       splitOffset: 0, // Any offset that is higher than children size
78       splitPath: [0, 0],
79     },
80     {
81       _: 'split nested list items',
82       expectedHtml:
83         '<ul>' +
84         '<li><span style="white-space: pre-wrap;">Before</span></li>' +
85         '<li><ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul></li>' +
86         '</ul>' +
87         '<ul>' +
88         '<li><ul><li><span style="white-space: pre-wrap;">world</span></li></ul></li>' +
89         '<li><span style="white-space: pre-wrap;">After</span></li>' +
90         '</ul>',
91       initialHtml:
92         '<ul>' +
93         '<li><span>Before</span></li>' +
94         '<ul><li><span>Hello</span><span>world</span></li></ul>' +
95         '<li><span>After</span></li>' +
96         '</ul>',
97       splitOffset: 1, // Any offset that is higher than children size
98       splitPath: [0, 1, 0, 0],
99     },
100   ];
101
102   for (const testCase of testCases) {
103     it(testCase._, async () => {
104       await update(() => {
105         // Running init, update, assert in the same update loop
106         // to skip text nodes normalization (then separate text
107         // nodes will still be separate and represented by its own
108         // spans in html output) and make assertions more precise
109         const parser = new DOMParser();
110         const dom = parser.parseFromString(testCase.initialHtml, 'text/html');
111         const nodesToInsert = $generateNodesFromDOM(editor, dom);
112         $getRoot()
113           .clear()
114           .append(...nodesToInsert);
115
116         let nodeToSplit: ElementNode = $getRoot();
117         for (const index of testCase.splitPath) {
118           nodeToSplit = nodeToSplit.getChildAtIndex(index)!;
119           if (!$isElementNode(nodeToSplit)) {
120             throw new Error('Expected node to be element');
121           }
122         }
123
124         $splitNode(nodeToSplit, testCase.splitOffset);
125
126         // Cleaning up list value attributes as it's not really needed in this test
127         // and it clutters expected output
128         const actualHtml = $generateHtmlFromNodes(editor).replace(
129           /\svalue="\d{1,}"/g,
130           '',
131         );
132         expect(actualHtml).toEqual(testCase.expectedHtml);
133       });
134     });
135   }
136
137   it('throws when splitting root', async () => {
138     await update(() => {
139       expect(() => $splitNode($getRoot(), 0)).toThrow();
140     });
141   });
142 });