chore: allow tsc to typecheck tests, fix type issues in those tests (#5982)

This commit is contained in:
Bob Ippolito
2024-05-02 14:58:18 -07:00
committed by GitHub
parent fdce31d056
commit a0bb9b056a
25 changed files with 565 additions and 401 deletions

View File

@ -178,7 +178,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertText('function');
$getSelection()!.insertText('function');
});
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML).toBe(
@ -187,12 +187,12 @@ describe('LexicalCodeNode tests', () => {
// CodeNode should only render diffs, make sure that the TabNode is not cloned when
// appending more text
let tabKey;
let tabKey: string;
await editor.update(() => {
tabKey = $dfs()
.find(({node}) => $isTabNode(node))
.find(({node}) => $isTabNode(node))!
.node.getKey();
$getSelection().insertText('foo');
$getSelection()!.insertText('foo');
});
expect(
editor.getEditorState().read(() => {
@ -214,7 +214,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertText('function');
$getSelection()!.insertText('function');
});
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => {
@ -238,7 +238,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertText('function');
$getSelection()!.insertText('function');
});
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => {
@ -253,8 +253,8 @@ describe('LexicalCodeNode tests', () => {
await editor.update(() => {
const root = $getRoot();
const codeTab = root.getFirstDescendant();
const codeText = root.getLastDescendant();
const codeTab = root.getFirstDescendant()!;
const codeText = root.getLastDescendant()!;
const selection = $createRangeSelection();
selection.anchor.set(codeTab.getKey(), 0, 'text');
selection.focus.set(codeText.getKey(), 'function'.length, 'text');
@ -275,7 +275,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertText('function');
$getSelection()!.insertText('function');
});
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => {
@ -290,8 +290,8 @@ describe('LexicalCodeNode tests', () => {
await editor.update(() => {
const root = $getRoot();
const codeTab = root.getFirstDescendant();
const codeText = root.getLastDescendant();
const codeTab = root.getFirstDescendant()!;
const codeText = root.getLastDescendant()!;
const selection = $createRangeSelection();
selection.anchor.set(codeTab.getKey(), 0, 'text');
selection.focus.set(codeText.getKey(), 0, 'text');
@ -313,12 +313,12 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertRawText('hello\tworld\nhello\tworld');
$getSelection()!.insertRawText('hello\tworld\nhello\tworld');
});
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => {
const firstCodeText = $getRoot().getFirstDescendant();
const lastCodeText = $getRoot().getLastDescendant();
const firstCodeText = $getRoot().getFirstDescendant()!;
const lastCodeText = $getRoot().getLastDescendant()!;
const selection = $createRangeSelection();
selection.anchor.set(firstCodeText.getKey(), 1, 'text');
selection.focus.set(lastCodeText.getKey(), 1, 'text');
@ -347,7 +347,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertRawText('hello\n');
$getSelection()!.insertRawText('hello\n');
});
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML)
@ -365,7 +365,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertRawText('\thello');
$getSelection()!.insertRawText('\thello');
});
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => {
@ -389,7 +389,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertRawText('abc\tdef\nghi\tjkl');
$getSelection()!.insertRawText('abc\tdef\nghi\tjkl');
});
const keyEvent = new KeyboardEventMock();
keyEvent.altKey = true;
@ -409,16 +409,16 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
$getSelection().insertRawText('abc\tdef\nghi\tjkl\nmno\tpqr');
$getSelection()!.insertRawText('abc\tdef\nghi\tjkl\nmno\tpqr');
});
// TODO consolidate editor.update - there's some bad logic in updateAndRetainSelection
await editor.update(() => {
const firstCodeText = $getRoot().getFirstDescendant();
const firstCodeText = $getRoot().getFirstDescendant()!;
const secondCodeText = firstCodeText
.getNextSibling() // tab
.getNextSibling() // def
.getNextSibling() // linebreak
.getNextSibling(); // ghi;
.getNextSibling()! // tab
.getNextSibling()! // def
.getNextSibling()! // linebreak
.getNextSibling()!; // ghi;
const selection = $createRangeSelection();
selection.anchor.set(firstCodeText.getKey(), 1, 'text');
selection.focus.set(secondCodeText.getKey(), 1, 'text');
@ -455,7 +455,7 @@ describe('LexicalCodeNode tests', () => {
const code = $createCodeNode();
root.append(code);
code.selectStart();
const selection = $getSelection();
const selection = $getSelection()!;
if (tabOrSpaces === 'tab') {
selection.insertRawText('\t\tfunction foo\n\t\tfunction bar');
} else {
@ -570,7 +570,7 @@ describe('LexicalCodeNode tests', () => {
const firstChild = code.getFirstChild();
invariant($isTextNode(firstChild));
if (tabOrSpaces === 'tab') {
firstChild.getNextSibling().selectNext(0, 0);
firstChild.getNextSibling()!.selectNext(0, 0);
} else {
firstChild.select(4, 4);
}
@ -605,7 +605,7 @@ describe('LexicalCodeNode tests', () => {
$isLineBreakNode(dfsNode.node),
)[0].node;
if (tabOrSpaces === 'tab') {
const firstTab = linebreak.getNextSibling();
const firstTab = linebreak.getNextSibling()!;
firstTab.selectNext();
} else {
linebreak.selectNext(4, 4);
@ -687,7 +687,7 @@ describe('LexicalCodeNode tests', () => {
$isLineBreakNode(dfsNode.node),
)[0].node;
if (tabOrSpaces === 'tab') {
const firstTab = linebreak.getNextSibling();
const firstTab = linebreak.getNextSibling()!;
firstTab.selectNext(0, 0);
} else {
linebreak.selectNext(2, 2);

View File

@ -16,7 +16,7 @@
*
*/
import type {RangeSelection} from 'lexical';
import type {EditorState, LexicalEditor, RangeSelection} from 'lexical';
import {$generateHtmlFromNodes} from '@lexical/html';
import {JSDOM} from 'jsdom';
@ -33,14 +33,17 @@ import {
import {createHeadlessEditor} from '../..';
describe('LexicalHeadlessEditor', () => {
let editor;
let editor: LexicalEditor;
async function update(updateFn) {
async function update(updateFn: () => void) {
editor.update(updateFn);
await Promise.resolve();
}
function assertEditorState(editorState, nodes) {
function assertEditorState(
editorState: EditorState,
nodes: Record<string, unknown>[],
) {
const nodesFromState = Array.from(editorState._nodeMap.values());
expect(nodesFromState).toEqual(
nodes.map((node) => expect.objectContaining(node)),

View File

@ -34,12 +34,12 @@ import {
} from 'lexical/src';
import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils';
import React from 'react';
import {createRoot} from 'react-dom/client';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils';
describe('LexicalHistory tests', () => {
let container: HTMLDivElement | null = null;
let reactRoot;
let reactRoot: Root;
beforeEach(() => {
container = document.createElement('div');

View File

@ -6,7 +6,12 @@
*
*/
import {$createParagraphNode, $getRoot, TextNode} from 'lexical';
import {
$createParagraphNode,
$createRangeSelection,
$getRoot,
TextNode,
} from 'lexical';
import {
expectHtmlToBeEqual,
html,
@ -147,10 +152,10 @@ describe('LexicalListItemNode tests', () => {
});
describe('ListItemNode.replace()', () => {
let listNode;
let listItemNode1;
let listItemNode2;
let listItemNode3;
let listNode: ListNode;
let listItemNode1: ListItemNode;
let listItemNode2: ListItemNode;
let listItemNode3: ListItemNode;
beforeEach(async () => {
const {editor} = testEnv;
@ -391,7 +396,7 @@ describe('LexicalListItemNode tests', () => {
// - B
test('siblings are not nested', async () => {
const {editor} = testEnv;
let x;
let x: ListItemNode;
await editor.update(() => {
const root = $getRoot();
@ -459,7 +464,7 @@ describe('LexicalListItemNode tests', () => {
// - B
test('the previous sibling is nested', async () => {
const {editor} = testEnv;
let x;
let x: ListItemNode;
await editor.update(() => {
const root = $getRoot();
@ -539,7 +544,7 @@ describe('LexicalListItemNode tests', () => {
// - B
test('the next sibling is nested', async () => {
const {editor} = testEnv;
let x;
let x: ListItemNode;
await editor.update(() => {
const root = $getRoot();
@ -619,7 +624,7 @@ describe('LexicalListItemNode tests', () => {
// - B
test('both siblings are nested', async () => {
const {editor} = testEnv;
let x;
let x: ListItemNode;
await editor.update(() => {
const root = $getRoot();
@ -708,7 +713,7 @@ describe('LexicalListItemNode tests', () => {
// - B
test('the previous sibling is nested deeper than the next sibling', async () => {
const {editor} = testEnv;
let x;
let x: ListItemNode;
await editor.update(() => {
const root = $getRoot();
@ -818,7 +823,7 @@ describe('LexicalListItemNode tests', () => {
// - B2
test('the next sibling is nested deeper than the previous sibling', async () => {
const {editor} = testEnv;
let x;
let x: ListItemNode;
await editor.update(() => {
const root = $getRoot();
@ -929,7 +934,7 @@ describe('LexicalListItemNode tests', () => {
// - B2
test('both siblings are deeply nested', async () => {
const {editor} = testEnv;
let x;
let x: ListItemNode;
await editor.update(() => {
const root = $getRoot();
@ -1052,10 +1057,10 @@ describe('LexicalListItemNode tests', () => {
});
describe('ListItemNode.insertNewAfter(): non-empty list items', () => {
let listNode;
let listItemNode1;
let listItemNode2;
let listItemNode3;
let listNode: ListNode;
let listItemNode1: ListItemNode;
let listItemNode2: ListItemNode;
let listItemNode3: ListItemNode;
beforeEach(async () => {
const {editor} = testEnv;
@ -1103,7 +1108,7 @@ describe('LexicalListItemNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode1.insertNewAfter();
listItemNode1.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
@ -1134,7 +1139,7 @@ describe('LexicalListItemNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode3.insertNewAfter();
listItemNode3.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
@ -1165,7 +1170,7 @@ describe('LexicalListItemNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
listItemNode3.insertNewAfter();
listItemNode3.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
@ -1217,7 +1222,7 @@ describe('LexicalListItemNode tests', () => {
);
await editor.update(() => {
listItemNode1.insertNewAfter();
listItemNode1.insertNewAfter($createRangeSelection());
});
expectHtmlToBeEqual(
@ -1264,9 +1269,9 @@ describe('LexicalListItemNode tests', () => {
});
describe('ListItemNode.setIndent()', () => {
let listNode;
let listItemNode1;
let listItemNode2;
let listNode: ListNode;
let listItemNode1: ListItemNode;
let listItemNode2: ListItemNode;
beforeEach(async () => {
const {editor} = testEnv;
@ -1296,7 +1301,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
editor.getRootElement().innerHTML,
editor.getRootElement()!.innerHTML,
html`
<ul>
<li value="1">
@ -1330,7 +1335,7 @@ describe('LexicalListItemNode tests', () => {
});
expectHtmlToBeEqual(
editor.getRootElement().innerHTML,
editor.getRootElement()!.innerHTML,
html`
<ul>
<li value="1" dir="ltr">

View File

@ -238,7 +238,7 @@ describe('LexicalListNode tests', () => {
expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect($isListItemNode(listNode.getFirstChild())).toBe(true);
expect(listNode.getFirstChild<ListItemNode>().getFirstChild()).toBe(
expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe(
nestedListNode,
);
});

View File

@ -8,10 +8,10 @@
import {$createTextNode, $getRoot, ParagraphNode, TextNode} from 'lexical';
import {createTestConnection, waitForReact} from './utils';
import {Client, createTestConnection, waitForReact} from './utils';
describe('Collaboration', () => {
let container = null;
let container: null | HTMLDivElement = null;
beforeEach(() => {
container = document.createElement('div');
@ -19,11 +19,11 @@ describe('Collaboration', () => {
});
afterEach(() => {
document.body.removeChild(container);
document.body.removeChild(container!);
container = null;
});
async function expectCorrectInitialContent(client1, client2) {
async function expectCorrectInitialContent(client1: Client, client2: Client) {
// Should be empty, as client has not yet updated
expect(client1.getHTML()).toEqual('');
expect(client1.getHTML()).toEqual(client2.getHTML());
@ -42,8 +42,8 @@ describe('Collaboration', () => {
const client1 = connector.createClient('1');
const client2 = connector.createClient('2');
client1.start(container);
client2.start(container);
client1.start(container!);
client2.start(container!);
await expectCorrectInitialContent(client1, client2);
@ -56,7 +56,7 @@ describe('Collaboration', () => {
const text = $createTextNode('Hello world');
paragraph.append(text);
paragraph!.append(text);
});
});
@ -71,8 +71,8 @@ describe('Collaboration', () => {
client2.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const text = paragraph.getFirstChild<TextNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = paragraph.getFirstChild<TextNode>()!;
text.spliceText(6, 5, 'metaverse');
});
@ -97,8 +97,8 @@ describe('Collaboration', () => {
const client1 = connector.createClient('1');
const client2 = connector.createClient('2');
client1.start(container);
client2.start(container);
client1.start(container!);
client2.start(container!);
await expectCorrectInitialContent(client1, client2);
@ -109,7 +109,7 @@ describe('Collaboration', () => {
client1.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = $createTextNode('Hello world');
paragraph.append(text);
@ -125,7 +125,7 @@ describe('Collaboration', () => {
client2.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = $createTextNode('Hello world');
paragraph.append(text);
@ -157,8 +157,8 @@ describe('Collaboration', () => {
client1.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const text = paragraph.getFirstChild<TextNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = paragraph.getFirstChild<TextNode>()!;
text.spliceText(11, 11, '');
});
@ -175,8 +175,8 @@ describe('Collaboration', () => {
client2.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const text = paragraph.getFirstChild<TextNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = paragraph.getFirstChild<TextNode>()!;
text.spliceText(11, 11, '!');
});
@ -203,8 +203,8 @@ describe('Collaboration', () => {
const connector = createTestConnection();
const client1 = connector.createClient('1');
const client2 = connector.createClient('2');
client1.start(container);
client2.start(container);
client1.start(container!);
client2.start(container!);
await expectCorrectInitialContent(client1, client2);
@ -213,7 +213,7 @@ describe('Collaboration', () => {
client1.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const text = $createTextNode('Hello world');
paragraph.append(text);
});
@ -235,8 +235,8 @@ describe('Collaboration', () => {
client1.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
paragraph.getFirstChild().remove();
const paragraph = root.getFirstChild<ParagraphNode>()!;
paragraph.getFirstChild()!.remove();
});
});
@ -250,9 +250,9 @@ describe('Collaboration', () => {
client2.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
paragraph.getFirstChild<TextNode>().spliceText(11, 0, 'Hello world');
paragraph.getFirstChild<TextNode>()!.spliceText(11, 0, 'Hello world');
});
});
@ -297,15 +297,15 @@ describe('Collaboration', () => {
uuid: Math.floor(Math.random() * 10000),
};
client1.start(container, awarenessData1);
client2.start(container, awarenessData2);
client1.start(container!, awarenessData1);
client2.start(container!, awarenessData2);
await expectCorrectInitialContent(client1, client2);
expect(client1.awareness.getLocalState().awarenessData).toEqual(
expect(client1.awareness.getLocalState()!.awarenessData).toEqual(
awarenessData1,
);
expect(client2.awareness.getLocalState().awarenessData).toEqual(
expect(client2.awareness.getLocalState()!.awarenessData).toEqual(
awarenessData2,
);

View File

@ -52,11 +52,11 @@ const $createSelectionByPath = ({
const root = $getRoot();
const anchorNode = anchorPath.reduce(
(node, index) => node.getChildAtIndex(index),
(node, index) => node.getChildAtIndex(index)!,
root,
);
const focusNode = focusPath.reduce(
(node, index) => node.getChildAtIndex(index),
(node, index) => node.getChildAtIndex(index)!,
root,
);
@ -95,11 +95,11 @@ const $replaceTextByPath = ({
focusOffset,
focusPath,
});
selection.insertText(text);
selection.insertText(text!);
};
describe('CollaborationWithCollisions', () => {
let container = null;
let container: HTMLDivElement | null = null;
beforeEach(() => {
container = document.createElement('div');
@ -107,7 +107,7 @@ describe('CollaborationWithCollisions', () => {
});
afterEach(() => {
document.body.removeChild(container);
document.body.removeChild(container!);
container = null;
});
@ -131,7 +131,7 @@ describe('CollaborationWithCollisions', () => {
},
() => {
// Second client deletes first paragraph
$getRoot().getFirstChild().remove();
$getRoot().getFirstChild()!.remove();
},
],
expectedHTML: null,
@ -152,7 +152,7 @@ describe('CollaborationWithCollisions', () => {
},
() => {
// Second client deletes first paragraph
$getRoot().getFirstChild().remove();
$getRoot().getFirstChild()!.remove();
},
],
expectedHTML: null,
@ -199,7 +199,7 @@ describe('CollaborationWithCollisions', () => {
const connection = createTestConnection();
const clients = createAndStartClients(
connection,
container,
container!,
testCase.clients.length,
);

View File

@ -7,14 +7,15 @@
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {createRoot} from 'react-dom/client';
import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils';
import {LexicalComposer} from '../../LexicalComposer';
describe('LexicalNodeHelpers tests', () => {
let container = null;
let reactRoot;
let container: HTMLDivElement | null = null;
let reactRoot: Root;
beforeEach(() => {
container = document.createElement('div');
@ -23,7 +24,7 @@ describe('LexicalNodeHelpers tests', () => {
});
afterEach(() => {
document.body.removeChild(container);
document.body.removeChild(container!);
container = null;
jest.restoreAllMocks();

View File

@ -22,8 +22,10 @@ import {
$getRoot,
$getSelection,
$isRangeSelection,
LexicalEditor,
} from 'lexical';
import {createRoot} from 'react-dom/client';
import * as React from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils';
import {LexicalComposer} from '../../LexicalComposer';
@ -48,8 +50,8 @@ const RICH_TEXT_NODES = [
];
describe('LexicalNodeHelpers tests', () => {
let container = null;
let reactRoot;
let container: HTMLDivElement | null = null;
let reactRoot: Root;
beforeEach(() => {
container = document.createElement('div');
@ -58,7 +60,7 @@ describe('LexicalNodeHelpers tests', () => {
});
afterEach(() => {
document.body.removeChild(container);
document.body.removeChild(container!);
container = null;
jest.restoreAllMocks();
@ -113,7 +115,7 @@ describe('LexicalNodeHelpers tests', () => {
reactRoot.render(<App />);
});
const text = editor.getEditorState().read($rootTextContent);
const text = editor!.getEditorState().read($rootTextContent);
expect(text).toBe('foo');
});
}
@ -164,9 +166,9 @@ describe('LexicalNodeHelpers tests', () => {
reactRoot.render(<App />);
});
await editor.focus();
await editor!.focus();
await editor.getEditorState().read(() => {
await editor!.getEditorState().read(() => {
expect($rootTextContent()).toBe('foo');
const selection = $getSelection();
@ -183,7 +185,7 @@ describe('LexicalNodeHelpers tests', () => {
for (const plugin of ['PlainTextPlugin', 'RichTextPlugin']) {
it(`${plugin} can hide placeholder when non-editable`, async () => {
let editor;
let editor: LexicalEditor;
function GrabEditor() {
[editor] = useLexicalComposerContext();
@ -232,7 +234,7 @@ describe('LexicalNodeHelpers tests', () => {
});
function placeholderText() {
const placeholderContainer = container.querySelector('.placeholder');
const placeholderContainer = container!.querySelector('.placeholder');
return placeholderContainer && placeholderContainer.textContent;
}

View File

@ -54,7 +54,7 @@ describe('LexicalNodeHelpers tests', () => {
paragraph.append(overflowRight);
});
return [overflowLeftKey, overflowRightKey];
return [overflowLeftKey!, overflowRightKey!];
}
it('merges an empty overflow node (left overflow selected)', async () => {
@ -63,8 +63,9 @@ describe('LexicalNodeHelpers tests', () => {
await initializeEditorWithLeftRightOverflowNodes();
await editor.update(() => {
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey);
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
const text1 = $createTextNode('1');
const text2 = $createTextNode('2');
@ -77,8 +78,9 @@ describe('LexicalNodeHelpers tests', () => {
});
await editor.update(() => {
const paragraph = $getRoot().getFirstChild<ParagraphNode>();
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
mergePrevious(overflowRight);
@ -110,8 +112,9 @@ describe('LexicalNodeHelpers tests', () => {
let text1Key: NodeKey;
await editor.update(() => {
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey);
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
const text1 = $createTextNode('1');
const text2 = $createTextNode('2');
@ -133,9 +136,10 @@ describe('LexicalNodeHelpers tests', () => {
});
await editor.update(() => {
const paragraph = $getRoot().getFirstChild<ParagraphNode>();
const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
mergePrevious(overflowRight);
@ -168,8 +172,9 @@ describe('LexicalNodeHelpers tests', () => {
let text3Key: NodeKey;
await editor.update(() => {
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey);
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
const overflowLeft = $getNodeByKey<OverflowNode>(overflowLeftKey)!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
const text1 = $createTextNode('1');
const text2 = $createTextNode('2');
@ -202,8 +207,9 @@ describe('LexicalNodeHelpers tests', () => {
});
await editor.update(() => {
const paragraph = $getRoot().getFirstChild<ParagraphNode>();
const overflowRight = $getNodeByKey<OverflowNode>(overflowRightKey);
const paragraph = $getRoot().getFirstChild<ParagraphNode>()!;
const overflowRight =
$getNodeByKey<OverflowNode>(overflowRightKey)!;
mergePrevious(overflowRight);

View File

@ -11,17 +11,19 @@ import {
$createTextNode,
$getRoot,
createEditor,
LexicalEditor,
ParagraphNode,
} from 'lexical';
import * as React from 'react';
import {createRoot} from 'react-dom/client';
import {createRef} from 'react';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils';
import {useLexicalIsTextContentEmpty} from '../../useLexicalIsTextContentEmpty';
describe('useLexicalIsTextContentEmpty', () => {
let container = null;
let reactRoot;
let container: HTMLDivElement | null = null;
let reactRoot: Root;
beforeEach(() => {
container = document.createElement('div');
@ -30,13 +32,13 @@ describe('useLexicalIsTextContentEmpty', () => {
});
afterEach(() => {
document.body.removeChild(container);
document.body.removeChild(container!);
container = null;
jest.restoreAllMocks();
});
function useLexicalEditor(rootElementRef) {
function useLexicalEditor(rootElementRef: React.RefObject<HTMLDivElement>) {
const editor = React.useMemo(
() =>
createEditor({
@ -58,8 +60,8 @@ describe('useLexicalIsTextContentEmpty', () => {
}
test('hook works', async () => {
const ref = React.createRef<HTMLDivElement>();
let editor;
const ref = createRef<HTMLDivElement>();
let editor: LexicalEditor;
let hasText = false;
function TestBase() {

View File

@ -6,9 +6,10 @@
*
*/
import {UserState} from '@lexical/yjs';
import {Provider, UserState} from '@lexical/yjs';
import {LexicalEditor} from 'lexical';
import * as React from 'react';
import {Container} from 'react-dom';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils';
import * as Y from 'yjs';
@ -21,7 +22,17 @@ import {ContentEditable} from '../../LexicalContentEditable';
import LexicalErrorBoundary from '../../LexicalErrorBoundary';
import {RichTextPlugin} from '../../LexicalRichTextPlugin';
function Editor({doc, provider, setEditor, awarenessData}) {
function Editor({
doc,
provider,
setEditor,
awarenessData,
}: {
doc: Y.Doc;
provider: Provider;
setEditor: (editor: LexicalEditor) => void;
awarenessData?: object | undefined;
}) {
const context = useCollaborationContext();
const [editor] = useLexicalComposerContext();
@ -49,20 +60,20 @@ function Editor({doc, provider, setEditor, awarenessData}) {
);
}
class Client {
export class Client implements Provider {
_id: string;
_reactRoot: Root;
_container: HTMLDivElement;
_editor: LexicalEditor;
_reactRoot: Root | null = null;
_container: HTMLDivElement | null = null;
_editor: LexicalEditor | null = null;
_connection: {
_clients: Client[];
_clients: Map<string, Client>;
};
_connected: boolean;
_doc: Y.Doc;
_awarenessState: unknown;
_connected: boolean = false;
_doc: Y.Doc = new Y.Doc();
_listeners: Map<string, Set<(data: unknown) => void>>;
_updates: Uint8Array[];
_listeners = new Map<string, Set<(data: unknown) => void>>();
_updates: Uint8Array[] = [];
_awarenessState: UserState | null = null;
awareness: {
getLocalState: () => UserState | null;
getStates: () => Map<number, UserState>;
@ -71,54 +82,37 @@ class Client {
setLocalState: (state: UserState) => void;
};
constructor(id, connection) {
constructor(id: Client['_id'], connection: Client['_connection']) {
this._id = id;
this._reactRoot = null;
this._container = null;
this._connection = connection;
this._connected = false;
this._doc = new Y.Doc();
this._awarenessState = {};
this._onUpdate = this._onUpdate.bind(this);
this._doc.on('update', this._onUpdate);
this._listeners = new Map();
this._updates = [];
this._editor = null;
this.awareness = {
getLocalState() {
return this._awarenessState;
},
getStates() {
const states: Map<number, UserState> = new Map();
states[0] = this._awarenessState as UserState;
return states;
},
off() {
getLocalState: () => this._awarenessState,
getStates: () => new Map([[0, this._awarenessState!]]),
off: () => {
// TODO
},
on() {
on: () => {
// TODO
},
setLocalState(state) {
setLocalState: (state) => {
this._awarenessState = state;
},
};
}
_onUpdate(update, origin, transaction) {
_onUpdate(update: Uint8Array, origin: unknown, transaction: unknown) {
if (origin !== this._connection && this._connected) {
this._broadcastUpdate(update);
}
}
_broadcastUpdate(update) {
_broadcastUpdate(update: Uint8Array) {
this._connection._clients.forEach((client) => {
if (client !== this) {
if (client._connected) {
@ -154,7 +148,7 @@ class Client {
this._connected = false;
}
start(rootContainer, awarenessData?) {
start(rootContainer: Container, awarenessData?: object) {
const container = document.createElement('div');
const reactRoot = createRoot(container);
this._container = container;
@ -185,15 +179,16 @@ class Client {
stop() {
ReactTestUtils.act(() => {
this._reactRoot.render(null);
this._reactRoot!.render(null);
});
this._container.parentNode.removeChild(this._container);
this.getContainer().parentNode!.removeChild(this.getContainer());
this._container = null;
}
on(type, callback) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(type: string, callback: (arg: any) => void) {
let listenerSet = this._listeners.get(type);
if (listenerSet === undefined) {
@ -205,7 +200,8 @@ class Client {
listenerSet.add(callback);
}
off(type, callback) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
off(type: string, callback: (arg: any) => void) {
const listenerSet = this._listeners.get(type);
if (listenerSet !== undefined) {
@ -213,7 +209,7 @@ class Client {
}
}
_dispatch(type, data) {
_dispatch(type: string, data: unknown) {
const listenerSet = this._listeners.get(type);
if (listenerSet !== undefined) {
@ -222,7 +218,7 @@ class Client {
}
getHTML() {
return (this._container.firstChild as HTMLElement).innerHTML;
return (this.getContainer().firstChild as HTMLElement).innerHTML;
}
getDocJSON() {
@ -230,36 +226,32 @@ class Client {
}
getEditorState() {
return this._editor.getEditorState();
return this.getEditor().getEditorState();
}
getEditor() {
return this._editor;
return this._editor!;
}
getContainer() {
return this._container;
return this._container!;
}
async focus() {
this._container.focus();
this.getContainer().focus();
await Promise.resolve().then();
}
update(cb) {
this._editor.update(cb);
update(cb: () => void) {
this.getEditor().update(cb);
}
}
class TestConnection {
_clients: Map<string, Client>;
_clients = new Map<string, Client>();
constructor() {
this._clients = new Map();
}
createClient(id) {
createClient(id: string) {
const client = new Client(id, this);
this._clients.set(id, client);
@ -272,7 +264,7 @@ export function createTestConnection() {
return new TestConnection();
}
export async function waitForReact(cb) {
export async function waitForReact(cb: () => void) {
await ReactTestUtils.act(async () => {
cb();
await Promise.resolve().then();

View File

@ -11,7 +11,13 @@ import {
$isHeadingNode,
HeadingNode,
} from '@lexical/rich-text';
import {$createTextNode, $getRoot, $getSelection, ParagraphNode} from 'lexical';
import {
$createTextNode,
$getRoot,
$getSelection,
ParagraphNode,
RangeSelection,
} from 'lexical';
import {initializeUnitTest} from 'lexical/src/__tests__/utils';
const editorConfig = Object.freeze({
@ -81,7 +87,7 @@ describe('LexicalHeadingNode tests', () => {
test('HeadingNode.insertNewAfter()', async () => {
const {editor} = testEnv;
let headingNode;
let headingNode: HeadingNode;
await editor.update(() => {
const root = $getRoot();
headingNode = new HeadingNode('h1');
@ -91,7 +97,7 @@ describe('LexicalHeadingNode tests', () => {
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><h1><br></h1></div>',
);
await editor.update(() => {
const selection = $getSelection();
const selection = $getSelection() as RangeSelection;
const result = headingNode.insertNewAfter(selection);
expect(result).toBeInstanceOf(ParagraphNode);
expect(result.getDirection()).toEqual(headingNode.getDirection());
@ -122,7 +128,7 @@ describe('LexicalHeadingNode tests', () => {
test('creates a h2 with text and can insert a new paragraph after', async () => {
const {editor} = testEnv;
let headingNode;
let headingNode: HeadingNode;
const text = 'hello world';
await editor.update(() => {
const root = $getRoot();

View File

@ -6,8 +6,8 @@
*
*/
import {$createQuoteNode} from '@lexical/rich-text';
import {$getRoot, ParagraphNode} from 'lexical';
import {$createQuoteNode, QuoteNode} from '@lexical/rich-text';
import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';
import {initializeUnitTest} from 'lexical/src/__tests__/utils';
const editorConfig = Object.freeze({
@ -64,7 +64,7 @@ describe('LexicalQuoteNode tests', () => {
test('QuoteNode.insertNewAfter()', async () => {
const {editor} = testEnv;
let quoteNode;
let quoteNode: QuoteNode;
await editor.update(() => {
const root = $getRoot();
quoteNode = $createQuoteNode();
@ -74,7 +74,7 @@ describe('LexicalQuoteNode tests', () => {
'<div contenteditable="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" data-lexical-editor="true"><blockquote><br></blockquote></div>',
);
await editor.update(() => {
const result = quoteNode.insertNewAfter();
const result = quoteNode.insertNewAfter($createRangeSelection());
expect(result).toBeInstanceOf(ParagraphNode);
expect(result.getDirection()).toEqual(quoteNode.getDirection());
});

View File

@ -30,7 +30,13 @@ import {
$isElementNode,
$isRangeSelection,
$setSelection,
DecoratorNode,
ElementNode,
LexicalEditor,
LexicalNode,
ParagraphNode,
PointType,
TextNode,
} from 'lexical';
import {
$assertRangeSelection,
@ -72,6 +78,13 @@ import {
undo,
} from '../utils';
interface ExpectedSelection {
anchorPath: number[];
anchorOffset: number;
focusPath: number[];
focusOffset: number;
}
initializeClipboard();
jest.mock('shared/environment', () => {
@ -116,7 +129,7 @@ describe('LexicalSelection tests', () => {
container = null;
});
let editor = null;
let editor: LexicalEditor | null = null;
async function init() {
function TestBase() {
@ -162,8 +175,8 @@ describe('LexicalSelection tests', () => {
}}>
<RichTextPlugin
contentEditable={
// eslint-disable-next-line jsx-a11y/aria-role
<ContentEditable role={null} spellCheck={null} />
// 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}
@ -175,32 +188,41 @@ describe('LexicalSelection tests', () => {
}
ReactTestUtils.act(() => {
createRoot(container).render(<TestBase />);
createRoot(container!).render(<TestBase />);
});
editor.getRootElement().focus();
editor!.getRootElement()!.focus();
await Promise.resolve().then();
// Focus first element
setNativeSelectionWithPaths(editor.getRootElement(), [0, 0], 0, [0, 0], 0);
setNativeSelectionWithPaths(
editor!.getRootElement()!,
[0, 0],
0,
[0, 0],
0,
);
}
async function update(fn) {
async function update(fn: () => void) {
await ReactTestUtils.act(async () => {
await editor.update(fn);
await editor!.update(fn);
});
return Promise.resolve().then();
}
test('Expect initial output to be a block with no text.', () => {
expect(container.innerHTML).toBe(
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>',
);
});
function assertSelection(rootElement, expectedSelection) {
const actualSelection = window.getSelection();
function assertSelection(
rootElement: HTMLElement,
expectedSelection: ExpectedSelection,
) {
const actualSelection = window.getSelection()!;
expect(actualSelection.anchorNode).toBe(
getNodeFromPath(expectedSelection.anchorPath, rootElement),
@ -1107,13 +1129,13 @@ describe('LexicalSelection tests', () => {
const name = testUnit.name || 'Test case';
test(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor);
await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches
expect(container.innerHTML).toBe(testUnit.expectedHTML);
expect(container!.innerHTML).toBe(testUnit.expectedHTML);
// Validate selection matches
const rootElement = editor.getRootElement();
const rootElement = editor!.getRootElement()!;
const expectedSelection = testUnit.expectedSelection;
assertSelection(rootElement, expectedSelection);
@ -1122,10 +1144,10 @@ describe('LexicalSelection tests', () => {
test('insert text one selected node element selection', async () => {
await ReactTestUtils.act(async () => {
await editor.update(() => {
await editor!.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const elementNode = $createTestElementNode();
const text = $createTextNode('foo');
@ -1146,10 +1168,10 @@ describe('LexicalSelection tests', () => {
test('getNodes resolves nested block nodes', async () => {
await ReactTestUtils.act(async () => {
await editor.update(() => {
await editor!.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const elementNode = $createTestElementNode();
const text = $createTextNode();
@ -1157,7 +1179,7 @@ describe('LexicalSelection tests', () => {
paragraph.append(elementNode);
elementNode.append(text);
const selectedNodes = $getSelection().getNodes();
const selectedNodes = $getSelection()!.getNodes();
expect(selectedNodes.length).toBe(1);
expect(selectedNodes[0].getKey()).toBe(text.getKey());
@ -1166,7 +1188,23 @@ describe('LexicalSelection tests', () => {
});
describe('Block selection moves when new nodes are inserted', () => {
[
const baseCases: {
name: string;
anchorOffset: number;
focusOffset: number;
fn: (
paragraph: ElementNode,
text: TextNode,
) => {
expectedAnchor: LexicalNode;
expectedAnchorOffset: number;
expectedFocus: LexicalNode;
expectedFocusOffset: number;
};
fnBefore?: (paragraph: ElementNode, text: TextNode) => void;
invertSelection?: true;
only?: true;
}[] = [
// Collapsed selection on end; add/remove/replace beginning
{
anchorOffset: 2,
@ -1313,8 +1351,8 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling();
const lastChild = paragraph.getLastChild();
const originalText2 = originalText1.getPreviousSibling()!;
const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2');
lastChild.insertBefore(newText);
@ -1335,7 +1373,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, text) => {
const lastChild = paragraph.getLastChild();
const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2');
lastChild.insertAfter(newText);
@ -1352,7 +1390,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling();
const originalText2 = originalText1.getPreviousSibling()!;
const [, text] = originalText1.splitText(1);
return {
@ -1372,7 +1410,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, text) => {
const lastChild = paragraph.getLastChild();
const lastChild = paragraph.getLastChild()!;
lastChild.remove();
return {
@ -1389,7 +1427,7 @@ describe('LexicalSelection tests', () => {
anchorOffset: 0,
fn: (paragraph, text) => {
const newText = $createTextNode('replacement');
const lastChild = paragraph.getLastChild();
const lastChild = paragraph.getLastChild()!;
lastChild.replace(newText);
return {
@ -1406,7 +1444,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, text) => {
const lastChild = paragraph.getLastChild();
const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2');
lastChild.insertBefore(newText);
@ -1439,7 +1477,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling();
const originalText2 = originalText1.getPreviousSibling()!;
const [, text] = originalText1.splitText(1);
return {
@ -1459,7 +1497,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 1,
fn: (paragraph, originalText1) => {
const lastChild = paragraph.getLastChild();
const lastChild = paragraph.getLastChild()!;
lastChild.remove();
return {
@ -1480,7 +1518,7 @@ describe('LexicalSelection tests', () => {
anchorOffset: 1,
fn: (paragraph, originalText1) => {
const newText = $createTextNode('replacement');
const lastChild = paragraph.getLastChild();
const lastChild = paragraph.getLastChild()!;
lastChild.replace(newText);
return {
@ -1501,8 +1539,8 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling();
const lastChild = paragraph.getLastChild();
const originalText2 = originalText1.getPreviousSibling()!;
const lastChild = paragraph.getLastChild()!;
const newText = $createTextNode('2');
lastChild.insertBefore(newText);
@ -1523,7 +1561,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling();
const originalText2 = originalText1.getPreviousSibling()!;
const newText = $createTextNode('2');
originalText1.insertAfter(newText);
@ -1544,7 +1582,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling();
const originalText2 = originalText1.getPreviousSibling()!;
originalText1.splitText(1);
return {
@ -1564,7 +1602,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = originalText1.getPreviousSibling();
const originalText2 = originalText1.getPreviousSibling()!;
originalText1.remove();
return {
@ -1605,7 +1643,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 3,
fn: (paragraph, originalText1) => {
const originalText2 = paragraph.getLastChild();
const originalText2 = paragraph.getLastChild()!;
const newText = $createTextNode('new');
originalText1.insertBefore(newText);
@ -1626,7 +1664,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
const originalText2 = paragraph.getLastChild();
const originalText2 = paragraph.getLastChild()!;
const newText = $createTextNode('new');
originalText1.insertBefore(newText);
@ -1647,7 +1685,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 1,
fn: (paragraph, originalText1) => {
originalText1.getNextSibling().remove();
originalText1.getNextSibling()!.remove();
return {
expectedAnchor: originalText1,
@ -1662,7 +1700,7 @@ describe('LexicalSelection tests', () => {
{
anchorOffset: 0,
fn: (paragraph, originalText1) => {
originalText1.getNextSibling().remove();
originalText1.getNextSibling()!.remove();
return {
expectedAnchor: originalText1,
@ -1674,8 +1712,9 @@ describe('LexicalSelection tests', () => {
focusOffset: 1,
name: 'remove - Remove with non-collapsed selection at offset',
},
]
.reduce((testSuite, testCase) => {
];
baseCases
.flatMap((testCase) => {
// Test inverse selection
const inverse = {
...testCase,
@ -1684,9 +1723,8 @@ describe('LexicalSelection tests', () => {
invertSelection: true,
name: testCase.name + ' (inverse selection)',
};
return testSuite.concat(testCase, inverse);
}, [])
return [testCase, inverse];
})
.forEach(
({
name,
@ -1703,10 +1741,10 @@ describe('LexicalSelection tests', () => {
const test_ = only === true ? test.only : test;
test_(name, async () => {
await ReactTestUtils.act(async () => {
await editor.update(() => {
await editor!.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const textNode = $createTextNode('foo');
// Note: line break can't be selected by the DOM
const linebreak = $createLineBreakNode();
@ -1755,7 +1793,7 @@ describe('LexicalSelection tests', () => {
describe('Selection correctly resolves to a sibling ElementNode when a node is removed', () => {
test('', async () => {
await ReactTestUtils.act(async () => {
await editor.update(() => {
await editor!.update(() => {
const root = $getRoot();
const listNode = $createListNode('bullet');
@ -1785,8 +1823,8 @@ describe('LexicalSelection tests', () => {
describe('Selection correctly resolves to a sibling ElementNode when a selected node child is removed', () => {
test('', async () => {
await ReactTestUtils.act(async () => {
let paragraphNodeKey;
await editor.update(() => {
let paragraphNodeKey: string;
await editor!.update(() => {
const root = $getRoot();
const paragraphNode = $createParagraphNode();
@ -1808,7 +1846,7 @@ describe('LexicalSelection tests', () => {
listNode.remove();
});
await editor.getEditorState().read(() => {
await editor!.getEditorState().read(() => {
const selection = $assertRangeSelection($getSelection());
expect(selection.anchor.key).toBe(paragraphNodeKey);
expect(selection.focus.key).toBe(paragraphNodeKey);
@ -1820,7 +1858,7 @@ describe('LexicalSelection tests', () => {
describe('Selection correctly resolves to a sibling ElementNode that has multiple children with the correct offset when a node is removed', () => {
test('', async () => {
await ReactTestUtils.act(async () => {
await editor.update(() => {
await editor!.update(() => {
// Arrange
// Root
// |- Paragraph
@ -1871,10 +1909,10 @@ describe('LexicalSelection tests', () => {
test('isBackward', async () => {
await ReactTestUtils.act(async () => {
await editor.update(() => {
await editor!.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const paragraphKey = paragraph.getKey();
const textNode = $createTextNode('foo');
const textNodeKey = textNode.getKey();
@ -1914,7 +1952,18 @@ describe('LexicalSelection tests', () => {
});
describe('Decorator text content for selection', () => {
[
const baseCases: {
name: string;
fn: (opts: {
textNode1: TextNode;
textNode2: TextNode;
decorator: DecoratorNode<unknown>;
paragraph: ParagraphNode;
anchor: PointType;
focus: PointType;
}) => string;
invertSelection?: true;
}[] = [
{
fn: ({textNode1, anchor, focus}) => {
anchor.set(textNode1.getKey(), 1, 'text');
@ -1971,23 +2020,24 @@ describe('LexicalSelection tests', () => {
},
name: 'Included if decorator is selected as the only node',
},
]
.reduce((testSuite, testCase) => {
];
baseCases
.flatMap((testCase) => {
const inverse = {
...testCase,
invertSelection: true,
name: testCase.name + ' (inverse selection)',
};
return testSuite.concat(testCase, inverse);
}, [])
return [testCase, inverse];
})
.forEach(({name, fn, invertSelection}) => {
it(name, async () => {
await ReactTestUtils.act(async () => {
await editor.update(() => {
await editor!.update(() => {
const root = $getRoot();
const paragraph = root.getFirstChild<ParagraphNode>();
const paragraph = root.getFirstChild<ParagraphNode>()!;
const textNode1 = $createTextNode('1');
const textNode2 = $createTextNode('2');
const decorator = $createTestDecoratorNode();
@ -2113,7 +2163,7 @@ describe('LexicalSelection tests', () => {
it('adjust offset for inline elements text formatting', async () => {
init();
await editor.update(() => {
await editor!.update(() => {
const root = $getRoot();
const text1 = $createTextNode('--');
@ -2154,7 +2204,11 @@ describe('LexicalSelection tests', () => {
});
describe('Node.replace', () => {
let text1, text2, text3, paragraph, testEditor;
let text1: TextNode,
text2: TextNode,
text3: TextNode,
paragraph: ParagraphNode,
testEditor: LexicalEditor;
beforeEach(async () => {
testEditor = createTestEditor();
@ -2481,7 +2535,7 @@ describe('LexicalSelection tests', () => {
expect(rootChildren[0].__type).toBe('heading');
expect(rootChildren[1].__type).toBe('heading');
expect(rootChildren.length).toBe(2);
const sel = $getSelection();
const sel = $getSelection()!;
expect(sel.getNodes().length).toBe(2);
});
});
@ -2540,7 +2594,7 @@ describe('LexicalSelection tests', () => {
const paragraph = column.getFirstChild();
invariant($isElementNode(paragraph));
if (paragraph.getFirstChild()) {
paragraph.getFirstChild().remove();
paragraph.getFirstChild()!.remove();
}
root.append(table);

View File

@ -25,6 +25,9 @@ import {
$isParagraphNode,
$isRangeSelection,
$setSelection,
ElementNode,
LexicalEditor,
ParagraphNode,
RangeSelection,
TextNode,
} from 'lexical';
@ -59,9 +62,12 @@ Range.prototype.getBoundingClientRect = function (): DOMRect {
};
};
function createParagraphWithNodes(editor, nodes) {
function createParagraphWithNodes(
editor: LexicalEditor,
nodes: {text: string; key: string; mergeable?: boolean}[],
) {
const paragraph = $createParagraphNode();
const nodeMap = editor._pendingEditorState._nodeMap;
const nodeMap = editor._pendingEditorState!._nodeMap;
for (let i = 0; i < nodes.length; i++) {
const {text, key, mergeable} = nodes[i];
@ -81,7 +87,9 @@ function createParagraphWithNodes(editor, nodes) {
describe('LexicalSelectionHelpers tests', () => {
describe('Collapsed', () => {
test('Can handle a text point', () => {
const setupTestCase = (cb) => {
const setupTestCase = (
cb: (selection: RangeSelection, node: ElementNode) => void,
) => {
const editor = createTestEditor();
editor.update(() => {
@ -119,7 +127,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'text',
});
const selection = $getSelection();
cb(selection, element);
cb(selection as RangeSelection, element);
});
};
@ -137,7 +145,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, state) => {
selection.insertText('Test');
expect($getNodeByKey('a').getTextContent()).toBe('Testa');
expect($getNodeByKey('a')!.getTextContent()).toBe('Testa');
expect(selection.anchor).toEqual(
expect.objectContaining({
@ -162,7 +170,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.anchor).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 3,
type: 'text',
}),
@ -170,7 +178,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 3,
type: 'text',
}),
@ -224,11 +232,11 @@ describe('LexicalSelectionHelpers tests', () => {
selection.formatText('bold');
selection.insertText('Test');
expect(element.getFirstChild().getTextContent()).toBe('Test');
expect(element.getFirstChild()!.getTextContent()).toBe('Test');
expect(selection.anchor).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 4,
type: 'text',
}),
@ -236,15 +244,15 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 4,
type: 'text',
}),
);
expect(element.getFirstChild().getNextSibling().getTextContent()).toBe(
'a',
);
expect(
element.getFirstChild()!.getNextSibling()!.getTextContent(),
).toBe('a');
});
// Extract selection
@ -414,7 +422,7 @@ describe('LexicalSelectionHelpers tests', () => {
const editor = createTestEditor();
const domElement = document.createElement('div');
let element;
let element: ParagraphNode;
editor.setRootElement(domElement);
@ -690,7 +698,9 @@ describe('LexicalSelectionHelpers tests', () => {
});
test('Can handle an element point on empty element', () => {
const setupTestCase = (cb) => {
const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor();
editor.update(() => {
@ -712,7 +722,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'element',
});
const selection = $getSelection();
cb(selection, element);
cb(selection as RangeSelection, element);
});
};
@ -729,7 +739,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText
setupTestCase((selection, element) => {
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -753,7 +763,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertParagraph
setupTestCase((selection, element) => {
selection.insertParagraph();
const nextElement = element.getNextSibling();
const nextElement = element.getNextSibling()!;
expect(selection.anchor).toEqual(
expect.objectContaining({
@ -797,7 +807,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => {
selection.formatText('bold');
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -825,7 +835,9 @@ describe('LexicalSelectionHelpers tests', () => {
});
test('Can handle a start element point', () => {
const setupTestCase = (cb) => {
const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor();
editor.update(() => {
@ -863,7 +875,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'element',
});
const selection = $getSelection();
cb(selection, element);
cb(selection as RangeSelection, element);
});
};
@ -880,7 +892,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText
setupTestCase((selection, element) => {
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -948,7 +960,7 @@ describe('LexicalSelectionHelpers tests', () => {
selection.formatText('bold');
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -976,7 +988,9 @@ describe('LexicalSelectionHelpers tests', () => {
});
test('Can handle an end element point', () => {
const setupTestCase = (cb) => {
const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor();
editor.update(() => {
@ -1014,7 +1028,7 @@ describe('LexicalSelectionHelpers tests', () => {
type: 'element',
});
const selection = $getSelection();
cb(selection, element);
cb(selection as RangeSelection, element);
});
};
@ -1031,7 +1045,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText
setupTestCase((selection, element) => {
selection.insertText('Test');
const lastChild = element.getLastChild();
const lastChild = element.getLastChild()!;
expect(lastChild.getTextContent()).toBe('Test');
@ -1055,7 +1069,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertParagraph
setupTestCase((selection, element) => {
selection.insertParagraph();
const nextSibling = element.getNextSibling();
const nextSibling = element.getNextSibling()!;
expect(selection.anchor).toEqual(
expect.objectContaining({
@ -1099,7 +1113,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => {
selection.formatText('bold');
selection.insertText('Test');
const lastChild = element.getLastChild();
const lastChild = element.getLastChild()!;
expect(lastChild.getTextContent()).toBe('Test');
@ -1271,7 +1285,9 @@ describe('LexicalSelectionHelpers tests', () => {
describe('Simple range', () => {
test('Can handle multiple text points', () => {
const setupTestCase = (cb) => {
const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor();
editor.update(() => {
@ -1333,7 +1349,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, state) => {
selection.insertText('Test');
expect($getNodeByKey('a').getTextContent()).toBe('Test');
expect($getNodeByKey('a')!.getTextContent()).toBe('Test');
expect(selection.anchor).toEqual(
expect.objectContaining({
@ -1358,7 +1374,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.anchor).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 3,
type: 'text',
}),
@ -1366,7 +1382,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 3,
type: 'text',
}),
@ -1420,11 +1436,11 @@ describe('LexicalSelectionHelpers tests', () => {
selection.formatText('bold');
selection.insertText('Test');
expect(element.getFirstChild().getTextContent()).toBe('Test');
expect(element.getFirstChild()!.getTextContent()).toBe('Test');
expect(selection.anchor).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 4,
type: 'text',
}),
@ -1432,7 +1448,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual(
expect.objectContaining({
key: element.getFirstChild().getKey(),
key: element.getFirstChild()!.getKey(),
offset: 4,
type: 'text',
}),
@ -1446,7 +1462,9 @@ describe('LexicalSelectionHelpers tests', () => {
});
test('Can handle multiple element points', () => {
const setupTestCase = (cb) => {
const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor();
editor.update(() => {
@ -1504,7 +1522,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText
setupTestCase((selection, element) => {
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -1571,7 +1589,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => {
selection.formatText('bold');
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -1601,7 +1619,9 @@ describe('LexicalSelectionHelpers tests', () => {
});
test('Can handle a mix of text and element points', () => {
const setupTestCase = (cb) => {
const setupTestCase = (
cb: (selection: RangeSelection, el: ElementNode) => void,
) => {
const editor = createTestEditor();
editor.update(() => {
@ -1668,7 +1688,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertText
setupTestCase((selection, element) => {
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -1692,7 +1712,7 @@ describe('LexicalSelectionHelpers tests', () => {
// insertParagraph
setupTestCase((selection, element) => {
selection.insertParagraph();
const nextElement = element.getNextSibling();
const nextElement = element.getNextSibling()!;
expect(selection.anchor).toEqual(
expect.objectContaining({
@ -1736,7 +1756,7 @@ describe('LexicalSelectionHelpers tests', () => {
setupTestCase((selection, element) => {
selection.formatText('bold');
selection.insertText('Test');
const firstChild = element.getFirstChild();
const firstChild = element.getFirstChild()!;
expect(firstChild.getTextContent()).toBe('Test');
@ -2121,7 +2141,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.anchor).toEqual(
expect.objectContaining({
key: paragraph
.getChildAtIndex(paragraph.getChildrenSize() - 2)
.getChildAtIndex(paragraph.getChildrenSize() - 2)!
.getKey(),
offset: 1,
type: 'text',
@ -2131,7 +2151,7 @@ describe('LexicalSelectionHelpers tests', () => {
expect(selection.focus).toEqual(
expect.objectContaining({
key: paragraph
.getChildAtIndex(paragraph.getChildrenSize() - 2)
.getChildAtIndex(paragraph.getChildrenSize() - 2)!
.getKey(),
offset: 1,
type: 'text',
@ -2547,7 +2567,7 @@ describe('extract', () => {
const selection = $getSelection();
expect($isRangeSelection(selection)).toBeTruthy();
expect(selection.extract()).toEqual([text]);
expect(selection!.extract()).toEqual([text]);
});
});
});
@ -2605,7 +2625,7 @@ describe('insertNodes', () => {
});
await editor.update(() => {
const selection = $createRangeSelection();
const text = $getRoot().getLastDescendant();
const text = $getRoot().getLastDescendant()!;
selection.anchor.set(text.getKey(), 0, 'text');
selection.focus.set(text.getKey(), 0, 'text');
@ -2629,7 +2649,7 @@ describe('insertNodes', () => {
$createParagraphNode().append(emptyTextNode, $createTextNode('text')),
);
emptyTextNode.select(0, 0);
const selection = $getSelection();
const selection = $getSelection()!;
expect($isRangeSelection(selection)).toBeTruthy();
selection.insertNodes([$createTextNode('foo')]);
@ -2692,7 +2712,7 @@ describe('$patchStyleText', () => {
const link = $createLinkNode('https://');
link.append($createTextNode('link'));
const a = $getNodeByKey('a');
const a = $getNodeByKey('a')!;
a.insertAfter(link);
setAnchorPoint({
@ -2793,7 +2813,7 @@ describe('$patchStyleText', () => {
const link = $createLinkNode('https://');
link.append($createTextNode('link'));
const a = $getNodeByKey('a');
const a = $getNodeByKey('a')!;
a.insertAfter(link);
setAnchorPoint({
@ -2846,7 +2866,7 @@ describe('$patchStyleText', () => {
const link = $createLinkNode('https://');
link.append($createTextNode('link'));
const a = $getNodeByKey('a');
const a = $getNodeByKey('a')!;
a.insertAfter(link);
// Select from the end of the link _element_

View File

@ -12,6 +12,8 @@ import {
$isNodeSelection,
$isRangeSelection,
$isTextNode,
LexicalEditor,
PointType,
} from 'lexical';
Object.defineProperty(HTMLElement.prototype, 'contentEditable', {
@ -49,7 +51,7 @@ if (!Selection.prototype.modify) {
};
const getWordsFromString = function (string: string): Array<Segment> {
const segments = [];
const segments: Segment[] = [];
let wordString = '';
let nonWordString = '';
let i;
@ -90,7 +92,8 @@ if (!Selection.prototype.modify) {
// This is not a thorough implementation, it was more to get tests working
// given the refactor to use this selection method.
const symbol = Object.getOwnPropertySymbols(this)[0];
const impl = this[symbol];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const impl = (this as any)[symbol];
const focus = impl._focus;
const anchor = impl._anchor;
@ -164,11 +167,11 @@ if (!Selection.prototype.modify) {
}
}
} else if (granularity === 'word') {
const anchorNode = this.anchorNode;
const anchorNode = this.anchorNode!;
const targetTextContent =
direction === 'backward'
? anchorNode.textContent.slice(0, this.anchorOffset)
: anchorNode.textContent.slice(this.anchorOffset);
? anchorNode.textContent!.slice(0, this.anchorOffset)
: anchorNode.textContent!.slice(this.anchorOffset);
const segments = getWordsFromString(targetTextContent);
const segmentsLength = segments.length;
let index = anchor.offset;
@ -218,27 +221,27 @@ if (!Selection.prototype.modify) {
};
}
export function printWhitespace(whitespaceCharacter) {
export function printWhitespace(whitespaceCharacter: string) {
return whitespaceCharacter.charCodeAt(0) === 160
? '&nbsp;'
: whitespaceCharacter;
}
export function insertText(text) {
export function insertText(text: string) {
return {
text,
type: 'insert_text',
};
}
export function insertTokenNode(text) {
export function insertTokenNode(text: string) {
return {
text,
type: 'insert_token_node',
};
}
export function insertSegmentedNode(text) {
export function insertSegmentedNode(text: string) {
return {
text,
type: 'insert_segmented_node',
@ -385,10 +388,10 @@ export function pasteHTML(text: string) {
}
export function moveNativeSelection(
anchorPath,
anchorOffset,
focusPath,
focusOffset,
anchorPath: number[],
anchorOffset: number,
focusPath: number[],
focusOffset: number,
) {
return {
anchorOffset,
@ -399,7 +402,7 @@ export function moveNativeSelection(
};
}
export function getNodeFromPath(path, rootElement) {
export function getNodeFromPath(path: number[], rootElement: Node) {
let node = rootElement;
for (let i = 0; i < path.length; i++) {
@ -410,12 +413,12 @@ export function getNodeFromPath(path, rootElement) {
}
export function setNativeSelection(
anchorNode,
anchorOffset,
focusNode,
focusOffset,
anchorNode: Node,
anchorOffset: number,
focusNode: Node,
focusOffset: number,
) {
const domSelection = window.getSelection();
const domSelection = window.getSelection()!;
const range = document.createRange();
range.setStart(anchorNode, anchorOffset);
range.setEnd(focusNode, focusOffset);
@ -427,18 +430,18 @@ export function setNativeSelection(
}
export function setNativeSelectionWithPaths(
rootElement,
anchorPath,
anchorOffset,
focusPath,
focusOffset,
rootElement: Node,
anchorPath: number[],
anchorOffset: number,
focusPath: number[],
focusOffset: number,
) {
const anchorNode = getNodeFromPath(anchorPath, rootElement);
const focusNode = getNodeFromPath(focusPath, rootElement);
setNativeSelection(anchorNode, anchorOffset, focusNode, focusOffset);
}
function getLastTextNode(startingNode) {
function getLastTextNode(startingNode: Node) {
let node = startingNode;
mainLoop: while (node !== null) {
@ -477,7 +480,7 @@ function getLastTextNode(startingNode) {
return null;
}
function getNextTextNode(startingNode) {
function getNextTextNode(startingNode: Node) {
let node = startingNode;
mainLoop: while (node !== null) {
@ -517,12 +520,14 @@ function getNextTextNode(startingNode) {
}
function moveNativeSelectionBackward() {
const domSelection = window.getSelection();
let {anchorNode, anchorOffset} = domSelection;
const domSelection = window.getSelection()!;
let anchorNode = domSelection.anchorNode!;
let anchorOffset = domSelection.anchorOffset!;
if (domSelection.isCollapsed) {
const target =
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode;
const target = (
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode
)!;
const keyDownEvent = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
@ -539,7 +544,7 @@ function moveNativeSelectionBackward() {
if (lastTextNode === null) {
throw new Error('moveNativeSelectionBackward: TODO');
} else {
const textLength = lastTextNode.nodeValue.length;
const textLength = lastTextNode.nodeValue!.length;
setNativeSelection(
lastTextNode,
textLength,
@ -557,7 +562,7 @@ function moveNativeSelectionBackward() {
}
} else if (anchorNode.nodeType === 1) {
if (anchorNode.nodeName === 'BR') {
const parentNode = anchorNode.parentNode;
const parentNode = anchorNode.parentNode!;
const childNodes = Array.from(parentNode.childNodes);
anchorOffset = childNodes.indexOf(anchorNode as ChildNode);
anchorNode = parentNode;
@ -584,12 +589,14 @@ function moveNativeSelectionBackward() {
}
function moveNativeSelectionForward() {
const domSelection = window.getSelection();
const {anchorNode, anchorOffset} = domSelection;
const domSelection = window.getSelection()!;
const anchorNode = domSelection.anchorNode!;
const anchorOffset = domSelection.anchorOffset!;
if (domSelection.isCollapsed) {
const target =
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode;
const target = (
anchorNode.nodeType === 1 ? anchorNode : anchorNode.parentNode
)!;
const keyDownEvent = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
@ -600,7 +607,7 @@ function moveNativeSelectionForward() {
if (!keyDownEvent.defaultPrevented) {
if (anchorNode.nodeType === 3) {
const text = anchorNode.nodeValue;
const text = anchorNode.nodeValue!;
if (text.length === anchorOffset) {
const nextTextNode = getNextTextNode(anchorNode);
@ -635,8 +642,13 @@ function moveNativeSelectionForward() {
}
}
export async function applySelectionInputs(inputs, update, editor) {
const rootElement = editor.getRootElement();
export async function applySelectionInputs(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputs: Record<string, any>[],
update: (fn: () => void) => Promise<void>,
editor: LexicalEditor,
) {
const rootElement = editor.getRootElement()!;
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
@ -644,7 +656,7 @@ export async function applySelectionInputs(inputs, update, editor) {
for (let j = 0; j < times; j++) {
await update(() => {
const selection = $getSelection();
const selection = $getSelection()!;
switch (input.type) {
case 'insert_text': {
@ -800,7 +812,7 @@ export async function applySelectionInputs(inputs, update, editor) {
}),
{
clipboardData: {
getData: (type) => {
getData: (type: string) => {
if (type === 'text/plain') {
return input.text;
}
@ -823,7 +835,7 @@ export async function applySelectionInputs(inputs, update, editor) {
}),
{
clipboardData: {
getData: (type) => {
getData: (type: string) => {
if (type === 'application/x-lexical-editor') {
return input.text;
}
@ -846,7 +858,7 @@ export async function applySelectionInputs(inputs, update, editor) {
}),
{
clipboardData: {
getData: (type) => {
getData: (type: string) => {
if (type === 'text/html') {
return input.text;
}
@ -865,7 +877,9 @@ export async function applySelectionInputs(inputs, update, editor) {
}
}
export function setAnchorPoint(point) {
export function setAnchorPoint(
point: Pick<PointType, 'type' | 'offset' | 'key'>,
) {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
@ -884,7 +898,9 @@ export function setAnchorPoint(point) {
anchor.key = point.key;
}
export function setFocusPoint(point) {
export function setFocusPoint(
point: Pick<PointType, 'type' | 'offset' | 'key'>,
) {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {

View File

@ -13,23 +13,27 @@ import {
$createTextNode,
$getRoot,
$setSelection,
EditorState,
ParagraphNode,
RootNode,
TextNode,
} from 'lexical';
import {createTestEditor} from 'lexical/src/__tests__/utils';
import {createRef, useEffect, useMemo} from 'react';
import {createRoot} from 'react-dom/client';
import {createRoot, Root} from 'react-dom/client';
import * as ReactTestUtils from 'react-dom/test-utils';
describe('table selection', () => {
let originalText;
let parsedParagraph;
let parsedRoot;
let parsedText;
let paragraphKey;
let textKey;
let parsedEditorState;
let reactRoot;
let originalText: TextNode;
let parsedParagraph: ParagraphNode;
let parsedRoot: RootNode;
let parsedText: TextNode;
let paragraphKey: string;
let textKey: string;
let parsedEditorState: EditorState;
let reactRoot: Root;
let container: HTMLDivElement | null = null;
let editor: LexicalEditor = null;
let editor: LexicalEditor | null = null;
beforeEach(() => {
container = document.createElement('div');
@ -37,7 +41,10 @@ describe('table selection', () => {
document.body.appendChild(container);
});
function useLexicalEditor(rootElementRef, onError) {
function useLexicalEditor(
rootElementRef: React.RefObject<HTMLDivElement>,
onError?: () => void,
) {
const editorInHook = useMemo(
() =>
createTestEditor({
@ -77,8 +84,8 @@ describe('table selection', () => {
});
}
async function update(fn) {
editor.update(fn);
async function update(fn: () => void) {
editor!.update(fn);
return Promise.resolve().then();
}
@ -101,15 +108,15 @@ describe('table selection', () => {
});
const stringifiedEditorState = JSON.stringify(
editor.getEditorState().toJSON(),
editor!.getEditorState().toJSON(),
);
parsedEditorState = editor.parseEditorState(stringifiedEditorState);
parsedEditorState = editor!.parseEditorState(stringifiedEditorState);
parsedEditorState.read(() => {
parsedRoot = $getRoot();
parsedParagraph = parsedRoot.getFirstChild();
parsedParagraph = parsedRoot.getFirstChild()!;
paragraphKey = parsedParagraph.getKey();
parsedText = parsedParagraph.getFirstChild();
parsedText = parsedParagraph.getFirstChild()!;
textKey = parsedText.getKey();
});
});

View File

@ -22,6 +22,7 @@ import {
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 'react-dom/test-utils';
@ -72,7 +73,7 @@ Range.prototype.getBoundingClientRect = function (): DOMRect {
};
describe('LexicalEventHelpers', () => {
let container = null;
let container: HTMLDivElement | null = null;
beforeEach(async () => {
container = document.createElement('div');
@ -81,15 +82,15 @@ describe('LexicalEventHelpers', () => {
});
afterEach(() => {
document.body.removeChild(container);
document.body.removeChild(container!);
container = null;
});
let editor = null;
let editor: LexicalEditor | null = null;
async function init() {
function TestBase() {
function TestPlugin(): JSX.Element {
function TestPlugin(): null {
[editor] = useLexicalComposerContext();
return null;
@ -146,8 +147,8 @@ describe('LexicalEventHelpers', () => {
}}>
<RichTextPlugin
contentEditable={
// eslint-disable-next-line jsx-a11y/aria-role
<ContentEditable role={null} spellCheck={null} />
// 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}
@ -159,20 +160,20 @@ describe('LexicalEventHelpers', () => {
}
ReactTestUtils.act(() => {
createRoot(container).render(<TestBase />);
createRoot(container!).render(<TestBase />);
});
}
async function update(fn) {
async function update(fn: () => void) {
await ReactTestUtils.act(async () => {
await editor.update(fn);
await editor!.update(fn);
});
return Promise.resolve().then();
}
test('Expect initial output to be a block with no text', () => {
expect(container.innerHTML).toBe(
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>',
);
});
@ -344,10 +345,10 @@ describe('LexicalEventHelpers', () => {
const name = testUnit.name || 'Test case';
test(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor);
await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches
expect(container.innerHTML).toBe(testUnit.expectedHTML);
expect(container!.innerHTML).toBe(testUnit.expectedHTML);
});
});
});
@ -400,10 +401,10 @@ describe('LexicalEventHelpers', () => {
const name = testUnit.name || 'Test case';
test(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor);
await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches
expect(container.innerHTML).toBe(testUnit.expectedHTML);
expect(container!.innerHTML).toBe(testUnit.expectedHTML);
});
});
});
@ -673,12 +674,14 @@ describe('LexicalEventHelpers', () => {
const name = testUnit.name || 'Test case';
// eslint-disable-next-line no-only-tests/no-only-tests, dot-notation
const test_ = testUnit['only'] ? test.only : test;
const test_ = 'only' in testUnit && testUnit['only'] ? test.only : test;
test_(name + ` (#${i + 1})`, async () => {
await applySelectionInputs(testUnit.inputs, update, editor);
await applySelectionInputs(testUnit.inputs, update, editor!);
// Validate HTML matches
expect(container.firstChild.innerHTML).toBe(testUnit.expectedHTML);
expect((container!.firstChild as HTMLElement).innerHTML).toBe(
testUnit.expectedHTML,
);
});
});
});

View File

@ -127,7 +127,7 @@ describe('LexicalNodeHelpers tests', () => {
editor.getEditorState().read(() => {
const expectedNodes = expectedKeys.map(({depth, node: nodeKey}) => ({
depth,
node: $getNodeByKey(nodeKey).getLatest(),
node: $getNodeByKey(nodeKey)!.getLatest(),
}));
const first = expectedNodes[0];
@ -147,10 +147,10 @@ describe('LexicalNodeHelpers tests', () => {
test('DFS triggers getLatest()', async () => {
const editor: LexicalEditor = testEnv.editor;
let rootKey;
let paragraphKey;
let block1Key;
let block2Key;
let rootKey: string;
let paragraphKey: string;
let block1Key: string;
let block2Key: string;
await editor.update(() => {
const root = $getRoot();
@ -179,14 +179,14 @@ describe('LexicalNodeHelpers tests', () => {
block1.append(block3);
expect($dfs(root)).toEqual([
expect($dfs(root!)).toEqual([
{
depth: 0,
node: root.getLatest(),
node: root!.getLatest(),
},
{
depth: 1,
node: paragraph.getLatest(),
node: paragraph!.getLatest(),
},
{
depth: 2,
@ -198,7 +198,7 @@ describe('LexicalNodeHelpers tests', () => {
},
{
depth: 2,
node: block2.getLatest(),
node: block2!.getLatest(),
},
]);
});

View File

@ -17,7 +17,7 @@ import {$splitNode} from '../../index';
describe('LexicalUtils#splitNode', () => {
let editor: LexicalEditor;
const update = async (updateFn) => {
const update = async (updateFn: () => void) => {
editor.update(updateFn);
await Promise.resolve();
};
@ -115,7 +115,7 @@ describe('LexicalUtils#splitNode', () => {
let nodeToSplit: ElementNode = $getRoot();
for (const index of testCase.splitPath) {
nodeToSplit = nodeToSplit.getChildAtIndex(index);
nodeToSplit = nodeToSplit.getChildAtIndex(index)!;
if (!$isElementNode(nodeToSplit)) {
throw new Error('Expected node to be element');
}

View File

@ -25,7 +25,7 @@ import {$insertNodeToNearestRoot} from '../..';
describe('LexicalUtils#insertNodeToNearestRoot', () => {
let editor: LexicalEditor;
const update = async (updateFn) => {
const update = async (updateFn: () => void) => {
editor.update(updateFn);
await Promise.resolve();
};
@ -155,7 +155,7 @@ describe('LexicalUtils#insertNodeToNearestRoot', () => {
'Expected node to be element (to traverse the tree)',
);
}
selectionNode = selectionNode.getChildAtIndex(index);
selectionNode = selectionNode.getChildAtIndex(index)!;
}
// Calling selectionNode.select() would "normalize" selection and move it

View File

@ -37,6 +37,55 @@ If you've used React Hooks before, you can think of `$` functions as being somet
Internally, we've found this scales really well and developers get to grips with it in almost no time at all.
## When does reconciliation happen?
Reconciliation is scheduled with
[queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask),
which means that it will happen very soon, but asynchronously. This is similar
to something like `setTimeout(reconcile, 0)` with a bit more immediacy or
`Promise.resolve().then(reconcile)` with less overhead. This is done so
that all of the updates that occur as a result of a single logical event will
be batched into one reconciliation.
You can force a reconciliation to take place synchronously with the discrete
option to `editor.update` (demonstrated below).
## Why do tests use `await editor.update(…)`
You may notice that many tests look like this:
```js
await editor.update(updateA);
await editor.update(updateB);
```
An astute observer would notice that this seems very strange, since
`editor.update()` returns `void` and not `Promise<void>`. However,
it does happen to work as you would want it to because
the implementation of Promise uses the same microtask queue.
It's not recommended to rely on this in browser code as it could depend on
implementation details of the compilers, bundlers, and VM. It's best to stick
to using the `discrete` or the `onUpdate` callback options to be sure that
the reconciliation has taken place.
Ignoring any other microtasks that were scheduled elsewhere,
it is roughly equivalent to this synchronous code:
```js
editor.update(updateA, {discrete: true});
editor.update(updateB, {discrete: true});
```
At a high level, very roughly, the order of operations looks like this:
1. `editor.update()` is called
2. `updateA()` is called and updates the editor state
3. `editor.update()` schedules a reconciliation microtask and returns
4. `await` schedules a resume microtask and yields control to the task executor
5. the reconciliation microtask runs, reconciling the editor state with the DOM
6. the resume microtask runs
## How do I listen for user text insertions?
Listening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how

View File

@ -207,7 +207,6 @@
"include": ["./libdefs", "./packages"],
"exclude": [
"./libdefs/*.js",
"**/lexical-*/src/__tests__/**",
"**/dist/**",
"**/npm/**",
"**/node_modules/**",

View File

@ -10,7 +10,6 @@
"**/dist/**",
"**/npm/**",
"**/node_modules/**",
"./packages/playwright-core/**",
"./packages/lexical-devtools/**"
],
"extends": "./tsconfig.json"