2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
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';
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';
30 jest.mock('lexical/shared/environment', () => {
31 const originalModule = jest.requireActual('lexical/shared/environment');
32 return {...originalModule, IS_FIREFOX: true};
35 Range.prototype.getBoundingClientRect = function (): DOMRect {
54 initializeClipboard();
56 Range.prototype.getBoundingClientRect = function (): DOMRect {
75 describe('LexicalEventHelpers', () => {
76 let container: HTMLDivElement | null = null;
78 beforeEach(async () => {
79 container = document.createElement('div');
80 document.body.appendChild(container);
85 document.body.removeChild(container!);
89 let editor: LexicalEditor | null = null;
91 async function init() {
93 function TestPlugin(): null {
94 [editor] = useLexicalComposerContext();
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',
127 image: 'editor-image',
129 listitem: 'editor-listitem',
130 olDepth: ['editor-list-ol'],
131 ulDepth: ['editor-list-ul'],
133 paragraph: 'editor-paragraph',
134 placeholder: 'editor-placeholder',
135 quote: 'editor-quote',
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',
150 // eslint-disable-next-line jsx-a11y/aria-role, @typescript-eslint/no-explicit-any
151 <ContentEditable role={null as any} spellCheck={null as any} />
154 ErrorBoundary={LexicalErrorBoundary}
162 ReactTestUtils.act(() => {
163 createRoot(container!).render(<TestBase />);
167 async function update(fn: () => void) {
168 await ReactTestUtils.act(async () => {
169 await editor!.update(fn);
172 return Promise.resolve().then();
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>',
181 describe('onPasteForRichText', () => {
182 describe('baseline', () => {
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',
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',
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',
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>',
207 `<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,
210 name: 'should produce the correct editor state from a pasted HTML ul element',
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>',
217 `<meta charset='utf-8'><ol><li>To tell you</li><li>I’m sorry</li></ol>`,
220 name: 'should produce the correct editor state from pasted HTML ol element',
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',
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',
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',
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',
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',
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>',
257 `<meta charset='utf-8'><h1>Lyrics to Hello by Adele</h1>A thousand times`,
260 name: 'should produce the correct editor state from pasted heading node followed by a DOM Text Node',
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>',
267 `<meta charset='utf-8'><a href="https://facebook.com">Facebook</a>`,
270 name: 'should produce the correct editor state from a pasted HTML anchor element',
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>',
277 `<meta charset='utf-8'>Welcome to<a href="https://facebook.com">Facebook!</a>`,
280 name: 'should produce the correct editor state from a pasted combination of an HTML text node followed by an anchor node',
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>',
287 `<meta charset='utf-8'>Welcome to<a href="https://facebook.com">Facebook!</a>We hope you like it here.`,
290 name: 'should produce the correct editor state from a pasted combination of HTML anchor elements and text nodes',
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>',
297 `<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`,
300 name: 'should ignore DOM node types that do not have transformers, but still process their children.',
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>',
307 `<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`,
310 name: 'should ignore multiple levels of DOM node types that do not have transformers, but still process their children.',
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>',
317 `<meta charset='utf-8'>Welcome to<b><a href="https://facebook.com">Facebook!</a></b>We hope you like it here.`,
320 name: 'should preserve formatting from HTML tags on deeply nested text nodes.',
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>',
327 `<meta charset='utf-8'>Welcome to<b><a href="https://facebook.com">Facebook!</a>We hope you like it here.</b>`,
330 name: 'should preserve formatting from HTML tags on deeply nested and top level text nodes.',
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>',
337 `<meta charset='utf-8'>Welcome to<b><i><a href="https://facebook.com">Facebook!</a>We hope you like it here.</i></b>`,
340 name: 'should preserve multiple types of formatting on deeply nested text nodes and top level text nodes',
344 suite.forEach((testUnit, i) => {
345 const name = testUnit.name || 'Test case';
347 test(name + ` (#${i + 1})`, async () => {
348 await applySelectionInputs(testUnit.inputs, update, editor!);
350 // Validate HTML matches
351 expect(container!.innerHTML).toBe(testUnit.expectedHTML);
356 describe('Google Docs', () => {
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>',
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>`,
366 name: 'should produce the correct editor state from Normal text',
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>',
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>`,
376 name: 'should produce the correct editor state from bold text',
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>',
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>`,
386 name: 'should produce the correct editor state from italic text',
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>',
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>`,
396 name: 'should produce the correct editor state from strikethrough text',
400 suite.forEach((testUnit, i) => {
401 const name = testUnit.name || 'Test case';
403 test(name + ` (#${i + 1})`, async () => {
404 await applySelectionInputs(testUnit.inputs, update, editor!);
406 // Validate HTML matches
407 expect(container!.innerHTML).toBe(testUnit.expectedHTML);
412 describe('W3 spacing', () => {
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',
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)',
427 // MS Office got it right
429 '<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true"> hello world</span></p>',
431 pasteHTML(' <span style="white-space: pre"> hello </span> world '),
433 name: 'pre + inline (inline collapses with pre)',
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)',
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',
449 '<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">hello world</span></p>',
458 name: 'remove beginning + end spaces on the block',
462 '<p class="editor-paragraph" dir="ltr"><strong class="editor-text-bold" data-lexical-text="true">hello world</strong></p>',
473 name: 'remove beginning + end spaces on the block (2)',
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>',
487 name: 'remove beginning + end spaces on the block + anonymous inlines collapsible rules',
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)',
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)',
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)',
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)',
514 expectedHTML: '<p class="editor-paragraph"><br></p>',
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',
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',
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>',
550 name: 'collapsible spaces with nested structures',
552 // TODO no proper support for divs #4465
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>',
568 // name: 'collapsible spaces with nested structures (2)',
572 '<p class="editor-paragraph" dir="ltr"><strong class="editor-text-bold" data-lexical-text="true">a b</strong></p>',
585 name: 'collapsible spaces with nested structures (3)',
589 '<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">a</span><br><span data-lexical-text="true">b</span></p>',
599 name: 'forced line break should remain',
603 '<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">a</span><br><span data-lexical-text="true">b</span></p>',
613 name: 'forced line break with tabs',
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
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)',
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>',
628 <pre>\na\r\nb\r\n</pre>
631 name: 'pre (no touchy) (2)',
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>',
637 pasteHTML(`<span style="white-space: pre">\na\r\nb\r\n</span>`),
639 name: 'white-space: pre (no touchy) (2)',
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>',
646 '\n<p class="p1">paragraph1</p>\n<p class="p1">paragraph2</p>\n',
649 name: 'two Apple Notes paragraphs',
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>',
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',
659 name: 'two Apple Notes lines + two paragraphs separated by an empty paragraph',
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>',
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',
669 name: 'two lines + two paragraphs separated by an empty paragraph (2)',
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>',
676 '<p class="p1"><span>line 1</span><span><br></span><span>line 2</span></p>',
679 name: 'two lines and br in spans',
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>',
685 pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),
687 name: 'empty block node in li behaves like a line break',
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',
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',
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',
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>',
712 `<div>a<div>b b<div>c<div><div></div>z</div></div>d e</div>fg</div>`,
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',
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',
731 suite.forEach((testUnit, i) => {
732 const name = testUnit.name || 'Test case';
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!);
739 // Validate HTML matches
740 expect((container!.firstChild as HTMLElement).innerHTML).toBe(
741 testUnit.expectedHTML,