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