mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-30 04:23:11 +03:00
Lexical: Imported core lexical libs
Imported at 0.17.1, Modified to work in-app. Added & configured test dependancies. Tests need to be altered to avoid using non-included deps including react dependancies.
This commit is contained in:
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {
|
||||
addClassNamesToElement,
|
||||
removeClassNamesFromElement,
|
||||
} from '@lexical/utils';
|
||||
|
||||
describe('LexicalElementHelpers tests', () => {
|
||||
describe('addClassNamesToElement() and removeClassNamesFromElement()', () => {
|
||||
test('basic', async () => {
|
||||
const element = document.createElement('div');
|
||||
addClassNamesToElement(element, 'test-class');
|
||||
|
||||
expect(element.className).toEqual('test-class');
|
||||
|
||||
removeClassNamesFromElement(element, 'test-class');
|
||||
|
||||
expect(element.className).toEqual('');
|
||||
});
|
||||
|
||||
test('empty', async () => {
|
||||
const element = document.createElement('div');
|
||||
addClassNamesToElement(
|
||||
element,
|
||||
null,
|
||||
undefined,
|
||||
false,
|
||||
true,
|
||||
'',
|
||||
' ',
|
||||
' \t\n',
|
||||
);
|
||||
|
||||
expect(element.className).toEqual('');
|
||||
});
|
||||
|
||||
test('multiple', async () => {
|
||||
const element = document.createElement('div');
|
||||
addClassNamesToElement(element, 'a', 'b', 'c');
|
||||
|
||||
expect(element.className).toEqual('a b c');
|
||||
|
||||
removeClassNamesFromElement(element, 'a', 'b', 'c');
|
||||
|
||||
expect(element.className).toEqual('');
|
||||
});
|
||||
|
||||
test('space separated', async () => {
|
||||
const element = document.createElement('div');
|
||||
addClassNamesToElement(element, 'a b c');
|
||||
|
||||
expect(element.className).toEqual('a b c');
|
||||
|
||||
removeClassNamesFromElement(element, 'a b c');
|
||||
|
||||
expect(element.className).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
test('multiple spaces', async () => {
|
||||
const classNames = ' a b c \t\n ';
|
||||
const element = document.createElement('div');
|
||||
addClassNamesToElement(element, classNames);
|
||||
|
||||
expect(element.className).toEqual('a b c');
|
||||
|
||||
removeClassNamesFromElement(element, classNames);
|
||||
|
||||
expect(element.className).toEqual('');
|
||||
});
|
||||
});
|
@ -0,0 +1,747 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {CodeHighlightNode, CodeNode} from '@lexical/code';
|
||||
import {HashtagNode} from '@lexical/hashtag';
|
||||
import {AutoLinkNode, LinkNode} from '@lexical/link';
|
||||
import {ListItemNode, ListNode} from '@lexical/list';
|
||||
import {OverflowNode} from '@lexical/overflow';
|
||||
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
|
||||
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
|
||||
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
||||
import {
|
||||
applySelectionInputs,
|
||||
pasteHTML,
|
||||
} from '@lexical/selection/src/__tests__/utils';
|
||||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
|
||||
import {LexicalEditor} from 'lexical';
|
||||
import {initializeClipboard, TestComposer} from 'lexical/src/__tests__/utils';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import * as ReactTestUtils from 'lexical/shared/react-test-utils';
|
||||
|
||||
jest.mock('lexical/shared/environment', () => {
|
||||
const originalModule = jest.requireActual('lexical/shared/environment');
|
||||
return {...originalModule, IS_FIREFOX: true};
|
||||
});
|
||||
|
||||
Range.prototype.getBoundingClientRect = function (): DOMRect {
|
||||
const rect = {
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
return {
|
||||
...rect,
|
||||
toJSON() {
|
||||
return rect;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
initializeClipboard();
|
||||
|
||||
Range.prototype.getBoundingClientRect = function (): DOMRect {
|
||||
const rect = {
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
return {
|
||||
...rect,
|
||||
toJSON() {
|
||||
return rect;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('LexicalEventHelpers', () => {
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
beforeEach(async () => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
await init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container!);
|
||||
container = null;
|
||||
});
|
||||
|
||||
let editor: LexicalEditor | null = null;
|
||||
|
||||
async function init() {
|
||||
function TestBase() {
|
||||
function TestPlugin(): null {
|
||||
[editor] = useLexicalComposerContext();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TestComposer
|
||||
config={{
|
||||
nodes: [
|
||||
LinkNode,
|
||||
HeadingNode,
|
||||
ListNode,
|
||||
ListItemNode,
|
||||
QuoteNode,
|
||||
CodeNode,
|
||||
TableNode,
|
||||
TableCellNode,
|
||||
TableRowNode,
|
||||
HashtagNode,
|
||||
CodeHighlightNode,
|
||||
AutoLinkNode,
|
||||
OverflowNode,
|
||||
],
|
||||
theme: {
|
||||
code: 'editor-code',
|
||||
heading: {
|
||||
h1: 'editor-heading-h1',
|
||||
h2: 'editor-heading-h2',
|
||||
h3: 'editor-heading-h3',
|
||||
h4: 'editor-heading-h4',
|
||||
h5: 'editor-heading-h5',
|
||||
h6: 'editor-heading-h6',
|
||||
},
|
||||
image: 'editor-image',
|
||||
list: {
|
||||
listitem: 'editor-listitem',
|
||||
olDepth: ['editor-list-ol'],
|
||||
ulDepth: ['editor-list-ul'],
|
||||
},
|
||||
paragraph: 'editor-paragraph',
|
||||
placeholder: 'editor-placeholder',
|
||||
quote: 'editor-quote',
|
||||
text: {
|
||||
bold: 'editor-text-bold',
|
||||
code: 'editor-text-code',
|
||||
hashtag: 'editor-text-hashtag',
|
||||
italic: 'editor-text-italic',
|
||||
link: 'editor-text-link',
|
||||
strikethrough: 'editor-text-strikethrough',
|
||||
underline: 'editor-text-underline',
|
||||
underlineStrikethrough: 'editor-text-underlineStrikethrough',
|
||||
},
|
||||
},
|
||||
}}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
// eslint-disable-next-line jsx-a11y/aria-role, @typescript-eslint/no-explicit-any
|
||||
<ContentEditable role={null as any} spellCheck={null as any} />
|
||||
}
|
||||
placeholder={null}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<AutoFocusPlugin />
|
||||
<TestPlugin />
|
||||
</TestComposer>
|
||||
);
|
||||
}
|
||||
|
||||
ReactTestUtils.act(() => {
|
||||
createRoot(container!).render(<TestBase />);
|
||||
});
|
||||
}
|
||||
|
||||
async function update(fn: () => void) {
|
||||
await ReactTestUtils.act(async () => {
|
||||
await editor!.update(fn);
|
||||
});
|
||||
|
||||
return Promise.resolve().then();
|
||||
}
|
||||
|
||||
test('Expect initial output to be a block with no text', () => {
|
||||
expect(container!.innerHTML).toBe(
|
||||
'<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>',
|
||||
);
|
||||
});
|
||||
|
||||
describe('onPasteForRichText', () => {
|
||||
describe('baseline', () => {
|
||||
const suite = [
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'><h1>Hello</h1>`)],
|
||||
name: 'should produce the correct editor state from a pasted HTML h1 element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'><h2>From</h2>`)],
|
||||
name: 'should produce the correct editor state from a pasted HTML h2 element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'><h3>The</h3>`)],
|
||||
name: 'should produce the correct editor state from a pasted HTML h3 element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'><ul><li>Other side</li><li>I must have called</li></ul>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from a pasted HTML ul element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'><ol><li>To tell you</li><li>I’m sorry</li></ol>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from pasted HTML ol element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'>A thousand times`)],
|
||||
name: 'should produce the correct editor state from pasted DOM Text Node',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'><b>Bold</b>`)],
|
||||
name: 'should produce the correct editor state from a pasted HTML b element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'><i>Italic</i>`)],
|
||||
name: 'should produce the correct editor state from a pasted HTML i element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'><em>Italic</em>`)],
|
||||
name: 'should produce the correct editor state from a pasted HTML em element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<meta charset='utf-8'><u>Underline</u>`)],
|
||||
name: 'should produce the correct editor state from a pasted HTML u element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'><h1>Lyrics to Hello by Adele</h1>A thousand times`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from pasted heading node followed by a DOM Text Node',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'><a href="https://facebook.com">Facebook</a>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from a pasted HTML anchor element',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'>Welcome to<a href="https://facebook.com">Facebook!</a>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from a pasted combination of an HTML text node followed by an anchor node',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'>Welcome to<a href="https://facebook.com">Facebook!</a>We hope you like it here.`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from a pasted combination of HTML anchor elements and text nodes',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist>`,
|
||||
),
|
||||
],
|
||||
name: 'should ignore DOM node types that do not have transformers, but still process their children.',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'><doesnotexist><doesnotexist><ul><li>Hello</li><li>from the other</li><li>side</li></ul></doesnotexist></doesnotexist>`,
|
||||
),
|
||||
],
|
||||
name: 'should ignore multiple levels of DOM node types that do not have transformers, but still process their children.',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'>Welcome to<b><a href="https://facebook.com">Facebook!</a></b>We hope you like it here.`,
|
||||
),
|
||||
],
|
||||
name: 'should preserve formatting from HTML tags on deeply nested text nodes.',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'>Welcome to<b><a href="https://facebook.com">Facebook!</a>We hope you like it here.</b>`,
|
||||
),
|
||||
],
|
||||
name: 'should preserve formatting from HTML tags on deeply nested and top level text nodes.',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<meta charset='utf-8'>Welcome to<b><i><a href="https://facebook.com">Facebook!</a>We hope you like it here.</i></b>`,
|
||||
),
|
||||
],
|
||||
name: 'should preserve multiple types of formatting on deeply nested text nodes and top level text nodes',
|
||||
},
|
||||
];
|
||||
|
||||
suite.forEach((testUnit, i) => {
|
||||
const name = testUnit.name || 'Test case';
|
||||
|
||||
test(name + ` (#${i + 1})`, async () => {
|
||||
await applySelectionInputs(testUnit.inputs, update, editor!);
|
||||
|
||||
// Validate HTML matches
|
||||
expect(container!.innerHTML).toBe(testUnit.expectedHTML);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Docs', () => {
|
||||
const suite = [
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<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>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from Normal text',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<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>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from bold text',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<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>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from italic text',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<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>`,
|
||||
),
|
||||
],
|
||||
name: 'should produce the correct editor state from strikethrough text',
|
||||
},
|
||||
];
|
||||
|
||||
suite.forEach((testUnit, i) => {
|
||||
const name = testUnit.name || 'Test case';
|
||||
|
||||
test(name + ` (#${i + 1})`, async () => {
|
||||
await applySelectionInputs(testUnit.inputs, update, editor!);
|
||||
|
||||
// Validate HTML matches
|
||||
expect(container!.innerHTML).toBe(testUnit.expectedHTML);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('W3 spacing', () => {
|
||||
const suite = [
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">hello world</span></p>',
|
||||
inputs: [pasteHTML('<span>hello world</span>')],
|
||||
name: 'inline hello world',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">hello world</span></p>',
|
||||
inputs: [pasteHTML('<span> hello </span>world ')],
|
||||
name: 'inline hello world (2)',
|
||||
},
|
||||
{
|
||||
// MS Office got it right
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true"> hello world</span></p>',
|
||||
inputs: [
|
||||
pasteHTML(' <span style="white-space: pre"> hello </span> world '),
|
||||
],
|
||||
name: 'pre + inline (inline collapses with pre)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML('<p style="white-space: pre"> a b\tc </p>')],
|
||||
name: 'white-space: pre (1) (no touchy)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">a b c</span></p>',
|
||||
inputs: [pasteHTML('<p>\ta\tb <span>c\t</span>\t</p>')],
|
||||
name: 'tabs are collapsed',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">hello world</span></p>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<div>
|
||||
hello
|
||||
world
|
||||
</div>
|
||||
`),
|
||||
],
|
||||
name: 'remove beginning + end spaces on the block',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><strong class="editor-text-bold" data-lexical-text="true">hello world</strong></p>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<div>
|
||||
<strong>
|
||||
hello
|
||||
world
|
||||
</strong>
|
||||
</div>
|
||||
`),
|
||||
],
|
||||
name: 'remove beginning + end spaces on the block (2)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<div>
|
||||
a
|
||||
<strong>b</strong>
|
||||
c
|
||||
</div>
|
||||
`),
|
||||
],
|
||||
name: 'remove beginning + end spaces on the block + anonymous inlines collapsible rules',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML('<div><strong>a </strong>b</div>')],
|
||||
name: 'collapsibles and neighbors (1)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML('<div>a<strong> b</strong></div>')],
|
||||
name: 'collapsibles and neighbors (2)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML('<div><strong>a </strong><span></span>b</div>')],
|
||||
name: 'collapsibles and neighbors (3)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML('<div>a<span></span><strong> b</strong></div>')],
|
||||
name: 'collapsibles and neighbors (4)',
|
||||
},
|
||||
{
|
||||
expectedHTML: '<p class="editor-paragraph"><br></p>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<p>
|
||||
</p>
|
||||
`),
|
||||
],
|
||||
name: 'empty block',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">a</span></p>',
|
||||
inputs: [pasteHTML('<span> </span><span>a</span>')],
|
||||
name: 'redundant inline at start',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">a</span></p>',
|
||||
inputs: [pasteHTML('<span>a</span><span> </span>')],
|
||||
name: 'redundant inline at end',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<div>
|
||||
<p>
|
||||
a
|
||||
</p>
|
||||
<p>
|
||||
b
|
||||
</p>
|
||||
</div>
|
||||
`),
|
||||
],
|
||||
name: 'collapsible spaces with nested structures',
|
||||
},
|
||||
// TODO no proper support for divs #4465
|
||||
// {
|
||||
// expectedHTML:
|
||||
// '<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>',
|
||||
// inputs: [
|
||||
// pasteHTML(`
|
||||
// <div>
|
||||
// <div>
|
||||
// a
|
||||
// </div>
|
||||
// <div>
|
||||
// b
|
||||
// </div>
|
||||
// </div>
|
||||
// `),
|
||||
// ],
|
||||
// name: 'collapsible spaces with nested structures (2)',
|
||||
// },
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><strong class="editor-text-bold" data-lexical-text="true">a b</strong></p>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<div>
|
||||
<strong>
|
||||
a
|
||||
</strong>
|
||||
<strong>
|
||||
b
|
||||
</strong>
|
||||
</div>
|
||||
`),
|
||||
],
|
||||
name: 'collapsible spaces with nested structures (3)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">a</span><br><span data-lexical-text="true">b</span></p>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<p>
|
||||
a
|
||||
<br>
|
||||
b
|
||||
</p>
|
||||
`),
|
||||
],
|
||||
name: 'forced line break should remain',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">a</span><br><span data-lexical-text="true">b</span></p>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<p>
|
||||
a
|
||||
\t<br>\t
|
||||
b
|
||||
</p>
|
||||
`),
|
||||
],
|
||||
name: 'forced line break with tabs',
|
||||
},
|
||||
// The 3 below are not correct, they're missing the first \n -> <br> but that's a fault with
|
||||
// the implementation of DOMParser, it works correctly in Safari
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML(`<pre>\na\r\nb\r\n</pre>`)],
|
||||
name: 'pre (no touchy) (1)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(`
|
||||
<pre>\na\r\nb\r\n</pre>
|
||||
`),
|
||||
],
|
||||
name: 'pre (no touchy) (2)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(`<span style="white-space: pre">\na\r\nb\r\n</span>`),
|
||||
],
|
||||
name: 'white-space: pre (no touchy) (2)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
'\n<p class="p1">paragraph1</p>\n<p class="p1">paragraph2</p>\n',
|
||||
),
|
||||
],
|
||||
name: 'two Apple Notes paragraphs',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
'\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',
|
||||
),
|
||||
],
|
||||
name: 'two Apple Notes lines + two paragraphs separated by an empty paragraph',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
'\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',
|
||||
),
|
||||
],
|
||||
name: 'two lines + two paragraphs separated by an empty paragraph (2)',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph" dir="ltr"><span data-lexical-text="true">line 1</span><br><span data-lexical-text="true">line 2</span></p>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
'<p class="p1"><span>line 1</span><span><br></span><span>line 2</span></p>',
|
||||
),
|
||||
],
|
||||
name: 'two lines and br in spans',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML('<ol><li>1<div></div>2</li><li></li><li>3</li></ol>'),
|
||||
],
|
||||
name: 'empty block node in li behaves like a line break',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph"><span data-lexical-text="true">1</span><br><span data-lexical-text="true">2</span></p>',
|
||||
inputs: [pasteHTML('<div>1<div></div>2</div>')],
|
||||
name: 'empty block node in div behaves like a line break',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph"><span data-lexical-text="true">12</span></p>',
|
||||
inputs: [pasteHTML('<div>1<text></text>2</div>')],
|
||||
name: 'empty inline node does not behave like a line break',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<p class="editor-paragraph"><span data-lexical-text="true">1</span></p><p class="editor-paragraph"><span data-lexical-text="true">2</span></p>',
|
||||
inputs: [pasteHTML('<div><div>1</div><div></div><div>2</div></div>')],
|
||||
name: 'empty block node between non inline siblings does not behave like a line break',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [
|
||||
pasteHTML(
|
||||
`<div>a<div>b b<div>c<div><div></div>z</div></div>d e</div>fg</div>`,
|
||||
),
|
||||
],
|
||||
name: 'nested divs',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML('<ol><li>1</li><li><br /></li><li>3</li></ol>')],
|
||||
name: 'only br in a li',
|
||||
},
|
||||
{
|
||||
expectedHTML:
|
||||
'<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>',
|
||||
inputs: [pasteHTML('1<p>2<br /></p>3')],
|
||||
name: 'last br in a block node is ignored',
|
||||
},
|
||||
];
|
||||
|
||||
suite.forEach((testUnit, i) => {
|
||||
const name = testUnit.name || 'Test case';
|
||||
|
||||
// eslint-disable-next-line no-only-tests/no-only-tests, dot-notation
|
||||
const test_ = 'only' in testUnit && testUnit['only'] ? test.only : test;
|
||||
test_(name + ` (#${i + 1})`, async () => {
|
||||
await applySelectionInputs(testUnit.inputs, update, editor!);
|
||||
|
||||
// Validate HTML matches
|
||||
expect((container!.firstChild as HTMLElement).innerHTML).toBe(
|
||||
testUnit.expectedHTML,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$createTextNode,
|
||||
$getNodeByKey,
|
||||
$getRoot,
|
||||
$isElementNode,
|
||||
LexicalEditor,
|
||||
NodeKey,
|
||||
} from 'lexical';
|
||||
import {
|
||||
$createTestElementNode,
|
||||
initializeUnitTest,
|
||||
invariant,
|
||||
} from 'lexical/src/__tests__/utils';
|
||||
|
||||
import {$dfs} from '../..';
|
||||
|
||||
describe('LexicalNodeHelpers tests', () => {
|
||||
initializeUnitTest((testEnv) => {
|
||||
/**
|
||||
* R
|
||||
* P1 P2
|
||||
* B1 B2 T4 T5 B3
|
||||
* T1 T2 T3 T6
|
||||
*
|
||||
* DFS: R, P1, B1, T1, B2, T2, T3, P2, T4, T5, B3, T6
|
||||
*/
|
||||
test('DFS node order', async () => {
|
||||
const editor: LexicalEditor = testEnv.editor;
|
||||
|
||||
let expectedKeys: Array<{
|
||||
depth: number;
|
||||
node: NodeKey;
|
||||
}> = [];
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph1 = $createParagraphNode();
|
||||
const paragraph2 = $createParagraphNode();
|
||||
|
||||
const block1 = $createTestElementNode();
|
||||
const block2 = $createTestElementNode();
|
||||
const block3 = $createTestElementNode();
|
||||
|
||||
const text1 = $createTextNode('text1');
|
||||
const text2 = $createTextNode('text2');
|
||||
const text3 = $createTextNode('text3');
|
||||
const text4 = $createTextNode('text4');
|
||||
const text5 = $createTextNode('text5');
|
||||
const text6 = $createTextNode('text6');
|
||||
|
||||
root.append(paragraph1, paragraph2);
|
||||
paragraph1.append(block1, block2);
|
||||
paragraph2.append(text4, text5);
|
||||
|
||||
text5.toggleFormat('bold'); // Prevent from merging with text 4
|
||||
|
||||
paragraph2.append(block3);
|
||||
block1.append(text1);
|
||||
block2.append(text2, text3);
|
||||
|
||||
text3.toggleFormat('bold'); // Prevent from merging with text2
|
||||
|
||||
block3.append(text6);
|
||||
|
||||
expectedKeys = [
|
||||
{
|
||||
depth: 0,
|
||||
node: root.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
node: paragraph1.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: block1.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
node: text1.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: block2.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
node: text2.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
node: text3.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
node: paragraph2.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: text4.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: text5.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: block3.getKey(),
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
node: text6.getKey(),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
editor.getEditorState().read(() => {
|
||||
const expectedNodes = expectedKeys.map(({depth, node: nodeKey}) => ({
|
||||
depth,
|
||||
node: $getNodeByKey(nodeKey)!.getLatest(),
|
||||
}));
|
||||
|
||||
const first = expectedNodes[0];
|
||||
const second = expectedNodes[1];
|
||||
const last = expectedNodes[expectedNodes.length - 1];
|
||||
const secondToLast = expectedNodes[expectedNodes.length - 2];
|
||||
|
||||
expect($dfs(first.node, last.node)).toEqual(expectedNodes);
|
||||
expect($dfs(second.node, secondToLast.node)).toEqual(
|
||||
expectedNodes.slice(1, expectedNodes.length - 1),
|
||||
);
|
||||
expect($dfs()).toEqual(expectedNodes);
|
||||
expect($dfs($getRoot())).toEqual(expectedNodes);
|
||||
});
|
||||
});
|
||||
|
||||
test('DFS triggers getLatest()', async () => {
|
||||
const editor: LexicalEditor = testEnv.editor;
|
||||
|
||||
let rootKey: string;
|
||||
let paragraphKey: string;
|
||||
let block1Key: string;
|
||||
let block2Key: string;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = $createParagraphNode();
|
||||
const block1 = $createTestElementNode();
|
||||
const block2 = $createTestElementNode();
|
||||
|
||||
rootKey = root.getKey();
|
||||
paragraphKey = paragraph.getKey();
|
||||
block1Key = block1.getKey();
|
||||
block2Key = block2.getKey();
|
||||
|
||||
root.append(paragraph);
|
||||
paragraph.append(block1, block2);
|
||||
});
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getNodeByKey(rootKey);
|
||||
const paragraph = $getNodeByKey(paragraphKey);
|
||||
const block1 = $getNodeByKey(block1Key);
|
||||
const block2 = $getNodeByKey(block2Key);
|
||||
|
||||
const block3 = $createTestElementNode();
|
||||
invariant($isElementNode(block1));
|
||||
|
||||
block1.append(block3);
|
||||
|
||||
expect($dfs(root!)).toEqual([
|
||||
{
|
||||
depth: 0,
|
||||
node: root!.getLatest(),
|
||||
},
|
||||
{
|
||||
depth: 1,
|
||||
node: paragraph!.getLatest(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: block1.getLatest(),
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
node: block3.getLatest(),
|
||||
},
|
||||
{
|
||||
depth: 2,
|
||||
node: block2!.getLatest(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('DFS of empty ParagraphNode returns only itself', async () => {
|
||||
const editor: LexicalEditor = testEnv.editor;
|
||||
|
||||
let paragraphKey: string;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
|
||||
const paragraph = $createParagraphNode();
|
||||
const paragraph2 = $createParagraphNode();
|
||||
const text = $createTextNode('test');
|
||||
|
||||
paragraphKey = paragraph.getKey();
|
||||
|
||||
paragraph2.append(text);
|
||||
root.append(paragraph, paragraph2);
|
||||
});
|
||||
await editor.update(() => {
|
||||
const paragraph = $getNodeByKey(paragraphKey)!;
|
||||
|
||||
expect($dfs(paragraph ?? undefined)).toEqual([
|
||||
{
|
||||
depth: 1,
|
||||
node: paragraph?.getLatest(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {
|
||||
$isRootTextContentEmpty,
|
||||
$isRootTextContentEmptyCurry,
|
||||
$rootTextContent,
|
||||
} from '@lexical/text';
|
||||
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
|
||||
import {initializeUnitTest} from 'lexical/src/__tests__/utils';
|
||||
|
||||
describe('LexicalRootHelpers tests', () => {
|
||||
initializeUnitTest((testEnv) => {
|
||||
it('textContent', async () => {
|
||||
const editor = testEnv.editor;
|
||||
|
||||
expect(editor.getEditorState().read($rootTextContent)).toBe('');
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const paragraph = $createParagraphNode();
|
||||
const text = $createTextNode('foo');
|
||||
root.append(paragraph);
|
||||
paragraph.append(text);
|
||||
|
||||
expect($rootTextContent()).toBe('foo');
|
||||
});
|
||||
|
||||
expect(editor.getEditorState().read($rootTextContent)).toBe('foo');
|
||||
});
|
||||
|
||||
it('isBlank', async () => {
|
||||
const editor = testEnv.editor;
|
||||
|
||||
expect(
|
||||
editor
|
||||
.getEditorState()
|
||||
.read($isRootTextContentEmptyCurry(editor.isComposing())),
|
||||
).toBe(true);
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const paragraph = $createParagraphNode();
|
||||
const text = $createTextNode('foo');
|
||||
root.append(paragraph);
|
||||
paragraph.append(text);
|
||||
|
||||
expect($isRootTextContentEmpty(editor.isComposing())).toBe(false);
|
||||
});
|
||||
|
||||
expect(
|
||||
editor
|
||||
.getEditorState()
|
||||
.read($isRootTextContentEmptyCurry(editor.isComposing())),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {objectKlassEquals} from '@lexical/utils';
|
||||
import {initializeUnitTest} from 'lexical/src/__tests__/utils';
|
||||
|
||||
class MyEvent extends Event {}
|
||||
|
||||
class MyEvent2 extends Event {}
|
||||
|
||||
let MyEventShadow: typeof Event = MyEvent;
|
||||
|
||||
{
|
||||
// eslint-disable-next-line no-shadow
|
||||
class MyEvent extends Event {}
|
||||
MyEventShadow = MyEvent;
|
||||
}
|
||||
|
||||
describe('LexicalUtilsKlassEqual tests', () => {
|
||||
initializeUnitTest((testEnv) => {
|
||||
it('objectKlassEquals', async () => {
|
||||
const eventInstance = new MyEvent('');
|
||||
expect(eventInstance instanceof MyEvent).toBeTruthy();
|
||||
expect(objectKlassEquals(eventInstance, MyEvent)).toBeTruthy();
|
||||
expect(eventInstance instanceof MyEvent2).toBeFalsy();
|
||||
expect(objectKlassEquals(eventInstance, MyEvent2)).toBeFalsy();
|
||||
expect(eventInstance instanceof MyEventShadow).toBeFalsy();
|
||||
expect(objectKlassEquals(eventInstance, MyEventShadow)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import type {ElementNode, LexicalEditor} from 'lexical';
|
||||
|
||||
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
|
||||
import {$getRoot, $isElementNode} from 'lexical';
|
||||
import {createTestEditor} from 'lexical/src/__tests__/utils';
|
||||
|
||||
import {$splitNode} from '../../index';
|
||||
|
||||
describe('LexicalUtils#splitNode', () => {
|
||||
let editor: LexicalEditor;
|
||||
|
||||
const update = async (updateFn: () => void) => {
|
||||
editor.update(updateFn);
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
editor = createTestEditor();
|
||||
editor._headless = true;
|
||||
});
|
||||
|
||||
const testCases: Array<{
|
||||
_: string;
|
||||
expectedHtml: string;
|
||||
initialHtml: string;
|
||||
splitPath: Array<number>;
|
||||
splitOffset: number;
|
||||
only?: boolean;
|
||||
}> = [
|
||||
{
|
||||
_: 'split paragraph in between two text nodes',
|
||||
expectedHtml:
|
||||
'<p><span style="white-space: pre-wrap;">Hello</span></p><p><span style="white-space: pre-wrap;">world</span></p>',
|
||||
initialHtml: '<p><span>Hello</span><span>world</span></p>',
|
||||
splitOffset: 1,
|
||||
splitPath: [0],
|
||||
},
|
||||
{
|
||||
_: 'split paragraph before the first text node',
|
||||
expectedHtml:
|
||||
'<p><br></p><p><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></p>',
|
||||
initialHtml: '<p><span>Hello</span><span>world</span></p>',
|
||||
splitOffset: 0,
|
||||
splitPath: [0],
|
||||
},
|
||||
{
|
||||
_: 'split paragraph after the last text node',
|
||||
expectedHtml:
|
||||
'<p><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></p><p><br></p>',
|
||||
initialHtml: '<p><span>Hello</span><span>world</span></p>',
|
||||
splitOffset: 2, // Any offset that is higher than children size
|
||||
splitPath: [0],
|
||||
},
|
||||
{
|
||||
_: 'split list items between two text nodes',
|
||||
expectedHtml:
|
||||
'<ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul>' +
|
||||
'<ul><li><span style="white-space: pre-wrap;">world</span></li></ul>',
|
||||
initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
|
||||
splitOffset: 1, // Any offset that is higher than children size
|
||||
splitPath: [0, 0],
|
||||
},
|
||||
{
|
||||
_: 'split list items before the first text node',
|
||||
expectedHtml:
|
||||
'<ul><li></li></ul>' +
|
||||
'<ul><li><span style="white-space: pre-wrap;">Hello</span><span style="white-space: pre-wrap;">world</span></li></ul>',
|
||||
initialHtml: '<ul><li><span>Hello</span><span>world</span></li></ul>',
|
||||
splitOffset: 0, // Any offset that is higher than children size
|
||||
splitPath: [0, 0],
|
||||
},
|
||||
{
|
||||
_: 'split nested list items',
|
||||
expectedHtml:
|
||||
'<ul>' +
|
||||
'<li><span style="white-space: pre-wrap;">Before</span></li>' +
|
||||
'<li><ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul></li>' +
|
||||
'</ul>' +
|
||||
'<ul>' +
|
||||
'<li><ul><li><span style="white-space: pre-wrap;">world</span></li></ul></li>' +
|
||||
'<li><span style="white-space: pre-wrap;">After</span></li>' +
|
||||
'</ul>',
|
||||
initialHtml:
|
||||
'<ul>' +
|
||||
'<li><span>Before</span></li>' +
|
||||
'<ul><li><span>Hello</span><span>world</span></li></ul>' +
|
||||
'<li><span>After</span></li>' +
|
||||
'</ul>',
|
||||
splitOffset: 1, // Any offset that is higher than children size
|
||||
splitPath: [0, 1, 0, 0],
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
it(testCase._, async () => {
|
||||
await update(() => {
|
||||
// Running init, update, assert in the same update loop
|
||||
// to skip text nodes normalization (then separate text
|
||||
// nodes will still be separate and represented by its own
|
||||
// spans in html output) and make assertions more precise
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(testCase.initialHtml, 'text/html');
|
||||
const nodesToInsert = $generateNodesFromDOM(editor, dom);
|
||||
$getRoot()
|
||||
.clear()
|
||||
.append(...nodesToInsert);
|
||||
|
||||
let nodeToSplit: ElementNode = $getRoot();
|
||||
for (const index of testCase.splitPath) {
|
||||
nodeToSplit = nodeToSplit.getChildAtIndex(index)!;
|
||||
if (!$isElementNode(nodeToSplit)) {
|
||||
throw new Error('Expected node to be element');
|
||||
}
|
||||
}
|
||||
|
||||
$splitNode(nodeToSplit, testCase.splitOffset);
|
||||
|
||||
// Cleaning up list value attributes as it's not really needed in this test
|
||||
// and it clutters expected output
|
||||
const actualHtml = $generateHtmlFromNodes(editor).replace(
|
||||
/\svalue="\d{1,}"/g,
|
||||
'',
|
||||
);
|
||||
expect(actualHtml).toEqual(testCase.expectedHtml);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('throws when splitting root', async () => {
|
||||
await update(() => {
|
||||
expect(() => $splitNode($getRoot(), 0)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import type {LexicalEditor, LexicalNode} from 'lexical';
|
||||
|
||||
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
|
||||
import {
|
||||
$createRangeSelection,
|
||||
$getRoot,
|
||||
$isElementNode,
|
||||
$setSelection,
|
||||
} from 'lexical';
|
||||
import {
|
||||
$createTestDecoratorNode,
|
||||
createTestEditor,
|
||||
} from 'lexical/src/__tests__/utils';
|
||||
|
||||
import {$insertNodeToNearestRoot} from '../..';
|
||||
|
||||
describe('LexicalUtils#insertNodeToNearestRoot', () => {
|
||||
let editor: LexicalEditor;
|
||||
|
||||
const update = async (updateFn: () => void) => {
|
||||
editor.update(updateFn);
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
editor = createTestEditor();
|
||||
editor._headless = true;
|
||||
});
|
||||
|
||||
const testCases: Array<{
|
||||
_: string;
|
||||
expectedHtml: string;
|
||||
initialHtml: string;
|
||||
selectionPath: Array<number>;
|
||||
selectionOffset: number;
|
||||
only?: boolean;
|
||||
}> = [
|
||||
{
|
||||
_: 'insert into paragraph in between two text nodes',
|
||||
expectedHtml:
|
||||
'<p><span style="white-space: pre-wrap;">Hello</span></p><test-decorator></test-decorator><p><span style="white-space: pre-wrap;">world</span></p>',
|
||||
initialHtml: '<p><span>Helloworld</span></p>',
|
||||
selectionOffset: 5, // Selection on text node after "Hello" world
|
||||
selectionPath: [0, 0],
|
||||
},
|
||||
{
|
||||
_: 'insert into nested list items',
|
||||
expectedHtml:
|
||||
'<ul>' +
|
||||
'<li><span style="white-space: pre-wrap;">Before</span></li>' +
|
||||
'<li><ul><li><span style="white-space: pre-wrap;">Hello</span></li></ul></li>' +
|
||||
'</ul>' +
|
||||
'<test-decorator></test-decorator>' +
|
||||
'<ul>' +
|
||||
'<li><ul><li><span style="white-space: pre-wrap;">world</span></li></ul></li>' +
|
||||
'<li><span style="white-space: pre-wrap;">After</span></li>' +
|
||||
'</ul>',
|
||||
initialHtml:
|
||||
'<ul>' +
|
||||
'<li><span>Before</span></li>' +
|
||||
'<ul><li><span>Helloworld</span></li></ul>' +
|
||||
'<li><span>After</span></li>' +
|
||||
'</ul>',
|
||||
selectionOffset: 5, // Selection on text node after "Hello" world
|
||||
selectionPath: [0, 1, 0, 0, 0],
|
||||
},
|
||||
{
|
||||
_: 'insert into empty paragraph',
|
||||
expectedHtml: '<p><br></p><test-decorator></test-decorator><p><br></p>',
|
||||
initialHtml: '<p></p>',
|
||||
selectionOffset: 0, // Selection on text node after "Hello" world
|
||||
selectionPath: [0],
|
||||
},
|
||||
{
|
||||
_: 'insert in the end of paragraph',
|
||||
expectedHtml:
|
||||
'<p><span style="white-space: pre-wrap;">Hello world</span></p>' +
|
||||
'<test-decorator></test-decorator>' +
|
||||
'<p><br></p>',
|
||||
initialHtml: '<p>Hello world</p>',
|
||||
selectionOffset: 12, // Selection on text node after "Hello" world
|
||||
selectionPath: [0, 0],
|
||||
},
|
||||
{
|
||||
_: 'insert in the beginning of paragraph',
|
||||
expectedHtml:
|
||||
'<p><br></p>' +
|
||||
'<test-decorator></test-decorator>' +
|
||||
'<p><span style="white-space: pre-wrap;">Hello world</span></p>',
|
||||
initialHtml: '<p>Hello world</p>',
|
||||
selectionOffset: 0, // Selection on text node after "Hello" world
|
||||
selectionPath: [0, 0],
|
||||
},
|
||||
{
|
||||
_: 'insert with selection on root start',
|
||||
expectedHtml:
|
||||
'<test-decorator></test-decorator>' +
|
||||
'<test-decorator></test-decorator>' +
|
||||
'<p><span style="white-space: pre-wrap;">Before</span></p>' +
|
||||
'<p><span style="white-space: pre-wrap;">After</span></p>',
|
||||
initialHtml:
|
||||
'<test-decorator></test-decorator>' +
|
||||
'<p><span>Before</span></p>' +
|
||||
'<p><span>After</span></p>',
|
||||
selectionOffset: 0,
|
||||
selectionPath: [],
|
||||
},
|
||||
{
|
||||
_: 'insert with selection on root child',
|
||||
expectedHtml:
|
||||
'<p><span style="white-space: pre-wrap;">Before</span></p>' +
|
||||
'<test-decorator></test-decorator>' +
|
||||
'<p><span style="white-space: pre-wrap;">After</span></p>',
|
||||
initialHtml: '<p>Before</p><p>After</p>',
|
||||
selectionOffset: 1,
|
||||
selectionPath: [],
|
||||
},
|
||||
{
|
||||
_: 'insert with selection on root end',
|
||||
expectedHtml:
|
||||
'<p><span style="white-space: pre-wrap;">Before</span></p>' +
|
||||
'<test-decorator></test-decorator>',
|
||||
initialHtml: '<p>Before</p>',
|
||||
selectionOffset: 1,
|
||||
selectionPath: [],
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
it(testCase._, async () => {
|
||||
await update(() => {
|
||||
// Running init, update, assert in the same update loop
|
||||
// to skip text nodes normalization (then separate text
|
||||
// nodes will still be separate and represented by its own
|
||||
// spans in html output) and make assertions more precise
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(testCase.initialHtml, 'text/html');
|
||||
const nodesToInsert = $generateNodesFromDOM(editor, dom);
|
||||
$getRoot()
|
||||
.clear()
|
||||
.append(...nodesToInsert);
|
||||
|
||||
let selectionNode: LexicalNode = $getRoot();
|
||||
for (const index of testCase.selectionPath) {
|
||||
if (!$isElementNode(selectionNode)) {
|
||||
throw new Error(
|
||||
'Expected node to be element (to traverse the tree)',
|
||||
);
|
||||
}
|
||||
selectionNode = selectionNode.getChildAtIndex(index)!;
|
||||
}
|
||||
|
||||
// Calling selectionNode.select() would "normalize" selection and move it
|
||||
// to text node (if available), while for the purpose of the test we'd want
|
||||
// to use whatever was passed (e.g. keep selection on root node)
|
||||
const selection = $createRangeSelection();
|
||||
const type = $isElementNode(selectionNode) ? 'element' : 'text';
|
||||
selection.anchor.key = selection.focus.key = selectionNode.getKey();
|
||||
selection.anchor.offset = selection.focus.offset =
|
||||
testCase.selectionOffset;
|
||||
selection.anchor.type = selection.focus.type = type;
|
||||
$setSelection(selection);
|
||||
|
||||
$insertNodeToNearestRoot($createTestDecoratorNode());
|
||||
|
||||
// Cleaning up list value attributes as it's not really needed in this test
|
||||
// and it clutters expected output
|
||||
const actualHtml = $generateHtmlFromNodes(editor).replace(
|
||||
/\svalue="\d{1,}"/g,
|
||||
'',
|
||||
);
|
||||
expect(actualHtml).toEqual(testCase.expectedHtml);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
|
||||
describe('mergeRegister', () => {
|
||||
it('calls all of the clean-up functions', () => {
|
||||
const cleanup = jest.fn();
|
||||
mergeRegister(cleanup, cleanup)();
|
||||
expect(cleanup).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('calls the clean-up functions in reverse order', () => {
|
||||
const cleanup = jest.fn();
|
||||
mergeRegister(cleanup.bind(null, 1), cleanup.bind(null, 2))();
|
||||
expect(cleanup.mock.calls.map(([v]) => v)).toEqual([2, 1]);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user