]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/utils/__tests__/unit/LexicalEventHelpers.test.ts
d76937ed606d30d9dc1a313544f6674071cd01da
[bookstack] / resources / js / wysiwyg / lexical / utils / __tests__ / unit / LexicalEventHelpers.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 import {AutoLinkNode, LinkNode} from '@lexical/link';
9 import {ListItemNode, ListNode} from '@lexical/list';
10 import {registerRichText} from '@lexical/rich-text';
11 import {
12   applySelectionInputs,
13   pasteHTML,
14 } from '@lexical/selection/__tests__/utils';
15 import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
16 import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';
17 import {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils';
18 import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
19 import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
20
21 jest.mock('lexical/shared/environment', () => {
22   const originalModule = jest.requireActual('lexical/shared/environment');
23   return {...originalModule, IS_FIREFOX: true};
24 });
25
26 Range.prototype.getBoundingClientRect = function (): DOMRect {
27   const rect = {
28     bottom: 0,
29     height: 0,
30     left: 0,
31     right: 0,
32     top: 0,
33     width: 0,
34     x: 0,
35     y: 0,
36   };
37   return {
38     ...rect,
39     toJSON() {
40       return rect;
41     },
42   };
43 };
44
45 initializeClipboard();
46
47 Range.prototype.getBoundingClientRect = function (): DOMRect {
48   const rect = {
49     bottom: 0,
50     height: 0,
51     left: 0,
52     right: 0,
53     top: 0,
54     width: 0,
55     x: 0,
56     y: 0,
57   };
58   return {
59     ...rect,
60     toJSON() {
61       return rect;
62     },
63   };
64 };
65
66 describe('LexicalEventHelpers', () => {
67   let container: HTMLDivElement | null = null;
68
69   beforeEach(async () => {
70     container = document.createElement('div');
71     document.body.appendChild(container);
72     await init();
73   });
74
75   afterEach(() => {
76     document.body.removeChild(container!);
77     container = null;
78   });
79
80   let editor: LexicalEditor | null = null;
81
82   async function init() {
83
84     const config = {
85       nodes: [
86         LinkNode,
87         HeadingNode,
88         ListNode,
89         ListItemNode,
90         QuoteNode,
91         TableNode,
92         TableCellNode,
93         TableRowNode,
94         AutoLinkNode,
95       ],
96       theme: {
97         code: 'editor-code',
98         heading: {
99           h1: 'editor-heading-h1',
100           h2: 'editor-heading-h2',
101           h3: 'editor-heading-h3',
102           h4: 'editor-heading-h4',
103           h5: 'editor-heading-h5',
104           h6: 'editor-heading-h6',
105         },
106         image: 'editor-image',
107         list: {
108           listitem: 'editor-listitem',
109           olDepth: ['editor-list-ol'],
110           ulDepth: ['editor-list-ul'],
111         },
112         paragraph: 'editor-paragraph',
113         placeholder: 'editor-placeholder',
114         quote: 'editor-quote',
115         text: {
116           bold: 'editor-text-bold',
117           code: 'editor-text-code',
118           hashtag: 'editor-text-hashtag',
119           italic: 'editor-text-italic',
120           link: 'editor-text-link',
121           strikethrough: 'editor-text-strikethrough',
122           underline: 'editor-text-underline',
123           underlineStrikethrough: 'editor-text-underlineStrikethrough',
124         },
125       },
126     };
127
128     editor = createTestEditor(config);
129     registerRichText(editor);
130
131     const root = document.createElement('div');
132     root.setAttribute('contenteditable', 'true');
133     container?.append(root);
134
135     editor.setRootElement(root);
136
137     editor.update(() => {
138       $insertNodes([$createParagraphNode()])
139     });
140     editor.commitUpdates();
141   }
142
143   async function update(fn: () => void) {
144     await editor!.update(fn);
145     editor?.commitUpdates();
146
147     return Promise.resolve().then();
148   }
149
150   test('Expect initial output to be a block with no text', () => {
151     expect(container!.innerHTML).toBe(
152       '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><br></p></div>',
153     );
154   });
155
156   describe('onPasteForRichText', () => {
157     describe('baseline', () => {
158       const suite = [
159         {
160           expectedHTML:
161             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><h1 class="editor-heading-h1"><span data-lexical-text="true">Hello</span></h1></div>',
162           inputs: [pasteHTML(`<meta charset='utf-8'><h1>Hello</h1>`)],
163           name: 'should produce the correct editor state from a pasted HTML h1 element',
164         },
165         {
166           expectedHTML:
167             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><h2 class="editor-heading-h2"><span data-lexical-text="true">From</span></h2></div>',
168           inputs: [pasteHTML(`<meta charset='utf-8'><h2>From</h2>`)],
169           name: 'should produce the correct editor state from a pasted HTML h2 element',
170         },
171         {
172           expectedHTML:
173             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><h3 class="editor-heading-h3"><span data-lexical-text="true">The</span></h3></div>',
174           inputs: [pasteHTML(`<meta charset='utf-8'><h3>The</h3>`)],
175           name: 'should produce the correct editor state from a pasted HTML h3 element',
176         },
177         {
178           expectedHTML:
179             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Other side</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">I must have called</span></li></ul></div>',
180           inputs: [
181             pasteHTML(
182               `<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,
183             ),
184           ],
185           name: 'should produce the correct editor state from a pasted HTML ul element',
186         },
187         {
188           expectedHTML:
189             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">To tell you</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">I’m sorry</span></li></ol></div>',
190           inputs: [
191             pasteHTML(
192               `<meta charset='utf-8'><ol><li>To tell you</li><li>I’m sorry</li></ol>`,
193             ),
194           ],
195           name: 'should produce the correct editor state from pasted HTML ol element',
196         },
197         {
198           expectedHTML:
199             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">A thousand times</span></p></div>',
200           inputs: [pasteHTML(`<meta charset='utf-8'>A thousand times`)],
201           name: 'should produce the correct editor state from pasted DOM Text Node',
202         },
203         {
204           expectedHTML:
205             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><strong class="editor-text-bold" data-lexical-text="true">Bold</strong></p></div>',
206           inputs: [pasteHTML(`<meta charset='utf-8'><b>Bold</b>`)],
207           name: 'should produce the correct editor state from a pasted HTML b element',
208         },
209         {
210           expectedHTML:
211             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><em class="editor-text-italic" data-lexical-text="true">Italic</em></p></div>',
212           inputs: [pasteHTML(`<meta charset='utf-8'><i>Italic</i>`)],
213           name: 'should produce the correct editor state from a pasted HTML i element',
214         },
215         {
216           expectedHTML:
217             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><em class="editor-text-italic" data-lexical-text="true">Italic</em></p></div>',
218           inputs: [pasteHTML(`<meta charset='utf-8'><em>Italic</em>`)],
219           name: 'should produce the correct editor state from a pasted HTML em element',
220         },
221         {
222           expectedHTML:
223             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span class="editor-text-underline" data-lexical-text="true">Underline</span></p></div>',
224           inputs: [pasteHTML(`<meta charset='utf-8'><u>Underline</u>`)],
225           name: 'should produce the correct editor state from a pasted HTML u element',
226         },
227         {
228           expectedHTML:
229             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><h1 class="editor-heading-h1"><span data-lexical-text="true">Lyrics to Hello by Adele</span></h1><p class="editor-paragraph"><span data-lexical-text="true">A thousand times</span></p></div>',
230           inputs: [
231             pasteHTML(
232               `<meta charset='utf-8'><h1>Lyrics to Hello by Adele</h1>A thousand times`,
233             ),
234           ],
235           name: 'should produce the correct editor state from pasted heading node followed by a DOM Text Node',
236         },
237         {
238           expectedHTML:
239             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><a href="https://facebook.com"><span data-lexical-text="true">Facebook</span></a></p></div>',
240           inputs: [
241             pasteHTML(
242               `<meta charset='utf-8'><a href="https://facebook.com">Facebook</a>`,
243             ),
244           ],
245           name: 'should produce the correct editor state from a pasted HTML anchor element',
246         },
247         {
248           expectedHTML:
249             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">Welcome to</span><a href="https://facebook.com"><span data-lexical-text="true">Facebook!</span></a></p></div>',
250           inputs: [
251             pasteHTML(
252               `<meta charset='utf-8'>Welcome to<a href="https://facebook.com">Facebook!</a>`,
253             ),
254           ],
255           name: 'should produce the correct editor state from a pasted combination of an HTML text node followed by an anchor node',
256         },
257         {
258           expectedHTML:
259             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">Welcome to</span><a href="https://facebook.com"><span data-lexical-text="true">Facebook!</span></a><span data-lexical-text="true">We hope you like it here.</span></p></div>',
260           inputs: [
261             pasteHTML(
262               `<meta charset='utf-8'>Welcome to<a href="https://facebook.com">Facebook!</a>We hope you like it here.`,
263             ),
264           ],
265           name: 'should produce the correct editor state from a pasted combination of HTML anchor elements and text nodes',
266         },
267         {
268           expectedHTML:
269             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Hello</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">from the other</span></li><li value="3" class="editor-listitem"><span data-lexical-text="true">side</span></li></ul></div>',
270           inputs: [
271             pasteHTML(
272               `<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`,
273             ),
274           ],
275           name: 'should ignore DOM node types that do not have transformers, but still process their children.',
276         },
277         {
278           expectedHTML:
279             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><ul class="editor-list-ul"><li value="1" class="editor-listitem"><span data-lexical-text="true">Hello</span></li><li value="2" class="editor-listitem"><span data-lexical-text="true">from the other</span></li><li value="3" class="editor-listitem"><span data-lexical-text="true">side</span></li></ul></div>',
280           inputs: [
281             pasteHTML(
282               `<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`,
283             ),
284           ],
285           name: 'should ignore multiple levels of DOM node types that do not have transformers, but still process their children.',
286         },
287         {
288           expectedHTML:
289             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">Welcome to</span><a href="https://facebook.com"><strong class="editor-text-bold" data-lexical-text="true">Facebook!</strong></a><span data-lexical-text="true">We hope you like it here.</span></p></div>',
290           inputs: [
291             pasteHTML(
292               `<meta charset='utf-8'>Welcome to<b><a href="https://facebook.com">Facebook!</a></b>We hope you like it here.`,
293             ),
294           ],
295           name: 'should preserve formatting from HTML tags on deeply nested text nodes.',
296         },
297         {
298           expectedHTML:
299             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">Welcome to</span><a href="https://facebook.com"><strong class="editor-text-bold" data-lexical-text="true">Facebook!</strong></a><strong class="editor-text-bold" data-lexical-text="true">We hope you like it here.</strong></p></div>',
300           inputs: [
301             pasteHTML(
302               `<meta charset='utf-8'>Welcome to<b><a href="https://facebook.com">Facebook!</a>We hope you like it here.</b>`,
303             ),
304           ],
305           name: 'should preserve formatting from HTML tags on deeply nested and top level text nodes.',
306         },
307         {
308           expectedHTML:
309             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span data-lexical-text="true">Welcome to</span><a href="https://facebook.com"><strong class="editor-text-bold editor-text-italic" data-lexical-text="true">Facebook!</strong></a><strong class="editor-text-bold editor-text-italic" data-lexical-text="true">We hope you like it here.</strong></p></div>',
310           inputs: [
311             pasteHTML(
312               `<meta charset='utf-8'>Welcome to<b><i><a href="https://facebook.com">Facebook!</a>We hope you like it here.</i></b>`,
313             ),
314           ],
315           name: 'should preserve multiple types of formatting on deeply nested text nodes and top level text nodes',
316         },
317       ];
318
319       suite.forEach((testUnit, i) => {
320         const name = testUnit.name || 'Test case';
321
322         test(name + ` (#${i + 1})`, async () => {
323           await applySelectionInputs(testUnit.inputs, update, editor!);
324
325           // Validate HTML matches
326           expect(container!.innerHTML).toBe(testUnit.expectedHTML);
327         });
328       });
329     });
330
331     describe('Google Docs', () => {
332       const suite = [
333         {
334           expectedHTML:
335             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</span></p></div>',
336           inputs: [
337             pasteHTML(
338               `<b style="font-weight:normal;" id="docs-internal-guid-2c706577-7fff-f54a-fe65-12f480020fac"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
339             ),
340           ],
341           name: 'should produce the correct editor state from Normal text',
342         },
343         {
344           expectedHTML:
345             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><strong class="editor-text-bold" style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</strong></p></div>',
346           inputs: [
347             pasteHTML(
348               `<b style="font-weight:normal;" id="docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
349             ),
350           ],
351           name: 'should produce the correct editor state from bold text',
352         },
353         {
354           expectedHTML:
355             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><em class="editor-text-italic" style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</em></p></div>',
356           inputs: [
357             pasteHTML(
358               `<b style="font-weight:normal;" id="docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:italic;font-variant:normal;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
359             ),
360           ],
361           name: 'should produce the correct editor state from italic text',
362         },
363         {
364           expectedHTML:
365             '<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><p class="editor-paragraph"><span class="editor-text-strikethrough" style="color: rgb(0, 0, 0);" data-lexical-text="true">Get schwifty!</span></p></div>',
366           inputs: [
367             pasteHTML(
368               `<b style="font-weight:normal;" id="docs-internal-guid-9db03964-7fff-c26c-8b1e-9484fb3b54a4"><span style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:line-through;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Get schwifty!</span></b>`,
369             ),
370           ],
371           name: 'should produce the correct editor state from strikethrough text',
372         },
373       ];
374
375       suite.forEach((testUnit, i) => {
376         const name = testUnit.name || 'Test case';
377
378         test(name + ` (#${i + 1})`, async () => {
379           await applySelectionInputs(testUnit.inputs, update, editor!);
380
381           // Validate HTML matches
382           expect(container!.innerHTML).toBe(testUnit.expectedHTML);
383         });
384       });
385     });
386
387     describe('W3 spacing', () => {
388       const suite = [
389         {
390           expectedHTML:
391             '<p class="editor-paragraph"><span data-lexical-text="true">hello world</span></p>',
392           inputs: [pasteHTML('<span>hello world</span>')],
393           name: 'inline hello world',
394         },
395         {
396           expectedHTML:
397             '<p class="editor-paragraph"><span data-lexical-text="true">hello world</span></p>',
398           inputs: [pasteHTML('<span>    hello  </span>world  ')],
399           name: 'inline hello world (2)',
400         },
401         {
402           // MS Office got it right
403           expectedHTML:
404             '<p class="editor-paragraph"><span data-lexical-text="true"> hello world</span></p>',
405           inputs: [
406             pasteHTML(' <span style="white-space: pre"> hello </span> world  '),
407           ],
408           name: 'pre + inline (inline collapses with pre)',
409         },
410         {
411           expectedHTML:
412             '<p class="editor-paragraph"><span data-lexical-text="true">  a b</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">c  </span></p>',
413           inputs: [pasteHTML('<p style="white-space: pre">  a b\tc  </p>')],
414           name: 'white-space: pre (1) (no touchy)',
415         },
416         {
417           expectedHTML:
418             '<p class="editor-paragraph"><span data-lexical-text="true">a b c</span></p>',
419           inputs: [pasteHTML('<p>\ta\tb  <span>c\t</span>\t</p>')],
420           name: 'tabs are collapsed',
421         },
422         {
423           expectedHTML:
424             '<p class="editor-paragraph"><span data-lexical-text="true">hello world</span></p>',
425           inputs: [
426             pasteHTML(`
427               <div>
428                 hello
429                 world
430               </div>
431             `),
432           ],
433           name: 'remove beginning + end spaces on the block',
434         },
435         {
436           expectedHTML:
437             '<p class="editor-paragraph"><strong class="editor-text-bold" data-lexical-text="true">hello world</strong></p>',
438           inputs: [
439             pasteHTML(`
440               <div>
441                 <strong>
442                   hello
443                   world
444                 </strong>
445               </div>
446           `),
447           ],
448           name: 'remove beginning + end spaces on the block (2)',
449         },
450         {
451           expectedHTML:
452             '<p class="editor-paragraph"><span data-lexical-text="true">a </span><strong class="editor-text-bold" data-lexical-text="true">b</strong><span data-lexical-text="true"> c</span></p>',
453           inputs: [
454             pasteHTML(`
455               <div>
456                 a
457                 <strong>b</strong>
458                 c
459               </div>
460           `),
461           ],
462           name: 'remove beginning + end spaces on the block + anonymous inlines collapsible rules',
463         },
464         {
465           expectedHTML:
466             '<p class="editor-paragraph"><strong class="editor-text-bold" data-lexical-text="true">a </strong><span data-lexical-text="true">b</span></p>',
467           inputs: [pasteHTML('<div><strong>a </strong>b</div>')],
468           name: 'collapsibles and neighbors (1)',
469         },
470         {
471           expectedHTML:
472             '<p class="editor-paragraph"><span data-lexical-text="true">a</span><strong class="editor-text-bold" data-lexical-text="true"> b</strong></p>',
473           inputs: [pasteHTML('<div>a<strong> b</strong></div>')],
474           name: 'collapsibles and neighbors (2)',
475         },
476         {
477           expectedHTML:
478             '<p class="editor-paragraph"><strong class="editor-text-bold" data-lexical-text="true">a </strong><span data-lexical-text="true">b</span></p>',
479           inputs: [pasteHTML('<div><strong>a </strong><span></span>b</div>')],
480           name: 'collapsibles and neighbors (3)',
481         },
482         {
483           expectedHTML:
484             '<p class="editor-paragraph"><span data-lexical-text="true">a</span><strong class="editor-text-bold" data-lexical-text="true"> b</strong></p>',
485           inputs: [pasteHTML('<div>a<span></span><strong> b</strong></div>')],
486           name: 'collapsibles and neighbors (4)',
487         },
488         {
489           expectedHTML: '<p class="editor-paragraph"><br></p>',
490           inputs: [
491             pasteHTML(`
492               <p>
493               </p>
494           `),
495           ],
496           name: 'empty block',
497         },
498         {
499           expectedHTML:
500             '<p class="editor-paragraph"><span data-lexical-text="true">a</span></p>',
501           inputs: [pasteHTML('<span> </span><span>a</span>')],
502           name: 'redundant inline at start',
503         },
504         {
505           expectedHTML:
506             '<p class="editor-paragraph"><span data-lexical-text="true">a</span></p>',
507           inputs: [pasteHTML('<span>a</span><span> </span>')],
508           name: 'redundant inline at end',
509         },
510         {
511           expectedHTML:
512             '<p class="editor-paragraph"><span data-lexical-text="true">a</span></p><p class="editor-paragraph"><span data-lexical-text="true">b</span></p>',
513           inputs: [
514             pasteHTML(`
515             <div>
516               <p>
517                 a
518               </p>
519               <p>
520                 b
521               </p>
522             </div>
523             `),
524           ],
525           name: 'collapsible spaces with nested structures',
526         },
527         {
528           expectedHTML:
529             '<p class="editor-paragraph"><strong class="editor-text-bold" data-lexical-text="true">a b</strong></p>',
530           inputs: [
531             pasteHTML(`
532             <div>
533               <strong>
534                 a
535               </strong>
536               <strong>
537                 b
538               </strong>
539             </div>
540             `),
541           ],
542           name: 'collapsible spaces with nested structures (3)',
543         },
544         {
545           expectedHTML:
546             '<p class="editor-paragraph"><span data-lexical-text="true">a</span><br><span data-lexical-text="true">b</span></p>',
547           inputs: [
548             pasteHTML(`
549             <p>
550             a
551             <br>
552             b
553             </p>
554             `),
555           ],
556           name: 'forced line break should remain',
557         },
558         {
559           expectedHTML:
560             '<p class="editor-paragraph"><span data-lexical-text="true">a</span><br><span data-lexical-text="true">b</span></p>',
561           inputs: [
562             pasteHTML(`
563             <p>
564             a
565             \t<br>\t
566             b
567             </p>
568             `),
569           ],
570           name: 'forced line break with tabs',
571         },
572         {
573           expectedHTML:
574             '<p class="editor-paragraph"><span data-lexical-text="true">paragraph1</span></p><p class="editor-paragraph"><span data-lexical-text="true">paragraph2</span></p>',
575           inputs: [
576             pasteHTML(
577               '\n<p class="p1">paragraph1</p>\n<p class="p1">paragraph2</p>\n',
578             ),
579           ],
580           name: 'two Apple Notes paragraphs',
581         },
582         {
583           expectedHTML:
584             '<p class="editor-paragraph"><span data-lexical-text="true">line 1</span><br><span data-lexical-text="true">line 2</span></p><p class="editor-paragraph"><br></p><p class="editor-paragraph"><span data-lexical-text="true">paragraph 1</span></p><p class="editor-paragraph"><span data-lexical-text="true">paragraph 2</span></p>',
585           inputs: [
586             pasteHTML(
587               '\n<p class="p1">line 1<br>\nline 2</p>\n<p class="p2"><br></p>\n<p class="p1">paragraph 1</p>\n<p class="p1">paragraph 2</p>\n',
588             ),
589           ],
590           name: 'two Apple Notes lines + two paragraphs separated by an empty paragraph',
591         },
592         {
593           expectedHTML:
594             '<p class="editor-paragraph"><span data-lexical-text="true">line 1</span><br><span data-lexical-text="true">line 2</span></p><p class="editor-paragraph"><br></p><p class="editor-paragraph"><span data-lexical-text="true">paragraph 1</span></p><p class="editor-paragraph"><span data-lexical-text="true">paragraph 2</span></p>',
595           inputs: [
596             pasteHTML(
597               '\n<p class="p1">line 1<br>\nline 2</p>\n<p class="p2">\n<br>\n</p>\n<p class="p1">paragraph 1</p>\n<p class="p1">paragraph 2</p>\n',
598             ),
599           ],
600           name: 'two lines + two paragraphs separated by an empty paragraph (2)',
601         },
602         {
603           expectedHTML:
604             '<p class="editor-paragraph"><span data-lexical-text="true">line 1</span><br><span data-lexical-text="true">line 2</span></p>',
605           inputs: [
606             pasteHTML(
607               '<p class="p1"><span>line 1</span><span><br></span><span>line 2</span></p>',
608             ),
609           ],
610           name: 'two lines and br in spans',
611         },
612         {
613           expectedHTML:
614             '<ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">1</span><br><span data-lexical-text="true">2</span></li><li value="2" class="editor-listitem"><br></li><li value="3" class="editor-listitem"><span data-lexical-text="true">3</span></li></ol>',
615           inputs: [
616             pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),
617           ],
618           name: 'empty block node in li behaves like a line break',
619         },
620         {
621           expectedHTML:
622             '<p class="editor-paragraph"><span data-lexical-text="true">1</span><br><span data-lexical-text="true">2</span></p>',
623           inputs: [pasteHTML('<div>1<div></div>2</div>')],
624           name: 'empty block node in div behaves like a line break',
625         },
626         {
627           expectedHTML:
628             '<p class="editor-paragraph"><span data-lexical-text="true">12</span></p>',
629           inputs: [pasteHTML('<div>1<text></text>2</div>')],
630           name: 'empty inline node does not behave like a line break',
631         },
632         {
633           expectedHTML:
634             '<p class="editor-paragraph"><span data-lexical-text="true">1</span></p><p class="editor-paragraph"><span data-lexical-text="true">2</span></p>',
635           inputs: [pasteHTML('<div><div>1</div><div></div><div>2</div></div>')],
636           name: 'empty block node between non inline siblings does not behave like a line break',
637         },
638         {
639           expectedHTML:
640             '<p class="editor-paragraph"><span data-lexical-text="true">a</span></p><p class="editor-paragraph"><span data-lexical-text="true">b b</span></p><p class="editor-paragraph"><span data-lexical-text="true">c</span></p><p class="editor-paragraph"><span data-lexical-text="true">z</span></p><p class="editor-paragraph"><span data-lexical-text="true">d e</span></p><p class="editor-paragraph"><span data-lexical-text="true">fg</span></p>',
641           inputs: [
642             pasteHTML(
643               `<div>a<div>b b<div>c<div><div></div>z</div></div>d e</div>fg</div>`,
644             ),
645           ],
646           name: 'nested divs',
647         },
648         {
649           expectedHTML:
650             '<ol class="editor-list-ol"><li value="1" class="editor-listitem"><span data-lexical-text="true">1</span></li><li value="2" class="editor-listitem"><br></li><li value="3" class="editor-listitem"><span data-lexical-text="true">3</span></li></ol>',
651           inputs: [pasteHTML('<ol><li>1</li><li><br /></li><li>3</li></ol>')],
652           name: 'only br in a li',
653         },
654         {
655           expectedHTML:
656             '<p class="editor-paragraph"><span data-lexical-text="true">1</span></p><p class="editor-paragraph"><span data-lexical-text="true">2</span></p><p class="editor-paragraph"><span data-lexical-text="true">3</span></p>',
657           inputs: [pasteHTML('1<p>2<br /></p>3')],
658           name: 'last br in a block node is ignored',
659         },
660       ];
661
662       suite.forEach((testUnit, i) => {
663         const name = testUnit.name || 'Test case';
664
665         // eslint-disable-next-line no-only-tests/no-only-tests, dot-notation
666         const test_ = 'only' in testUnit && testUnit['only'] ? test.only : test;
667         test_(name + ` (#${i + 1})`, async () => {
668           await applySelectionInputs(testUnit.inputs, update, editor!);
669
670           // Validate HTML matches
671           expect((container!.firstChild as HTMLElement).innerHTML).toBe(
672             testUnit.expectedHTML,
673           );
674         });
675       });
676     });
677   });
678 });