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