Lexical: Added tests to cover recent changes
Some checks failed
test-js / build (push) Has been cancelled

Also updated list tests to new test process.
This commit is contained in:
Dan Brown 2025-03-28 18:29:00 +00:00
parent 9bfcadd95f
commit bb44334224
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
4 changed files with 240 additions and 196 deletions

View File

@ -37,6 +37,7 @@ import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {EditorUiContext} from "../../../../ui/framework/core";
import {EditorUIManager} from "../../../../ui/framework/manager";
import {ImageNode} from "@lexical/rich-text/LexicalImageNode";
type TestEnv = {
readonly container: HTMLDivElement;
@ -484,6 +485,9 @@ export function createTestContext(): EditorUiContext {
const editor = createTestEditor({
namespace: 'testing',
theme: {},
nodes: [
ImageNode,
]
});
editor.setRootElement(editorDOM);

View File

@ -6,7 +6,7 @@
*
*/
import {ParagraphNode, TextNode} from 'lexical';
import {initializeUnitTest} from 'lexical/__tests__/utils';
import {createTestContext} from 'lexical/__tests__/utils';
import {
$createListItemNode,
@ -16,6 +16,7 @@ import {
ListItemNode,
ListNode,
} from '../..';
import {$htmlToBlockNodes} from "../../../../utils/nodes";
const editorConfig = Object.freeze({
namespace: '',
@ -46,123 +47,122 @@ const editorConfig = Object.freeze({
});
describe('LexicalListNode tests', () => {
initializeUnitTest((testEnv) => {
test('ListNode.constructor', async () => {
const {editor} = testEnv;
test('ListNode.constructor', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
expect(listNode.getType()).toBe('list');
expect(listNode.getTag()).toBe('ul');
expect(listNode.getTextContent()).toBe('');
});
// @ts-expect-error
expect(() => $createListNode()).toThrow();
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
expect(listNode.getType()).toBe('list');
expect(listNode.getTag()).toBe('ul');
expect(listNode.getTextContent()).toBe('');
});
test('ListNode.getTag()', async () => {
const {editor} = testEnv;
// @ts-expect-error
expect(() => $createListNode()).toThrow();
});
await editor.update(() => {
const ulListNode = $createListNode('bullet', 1);
expect(ulListNode.getTag()).toBe('ul');
const olListNode = $createListNode('number', 1);
expect(olListNode.getTag()).toBe('ol');
const checkListNode = $createListNode('check', 1);
expect(checkListNode.getTag()).toBe('ul');
});
test('ListNode.getTag()', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const ulListNode = $createListNode('bullet', 1);
expect(ulListNode.getTag()).toBe('ul');
const olListNode = $createListNode('number', 1);
expect(olListNode.getTag()).toBe('ol');
const checkListNode = $createListNode('check', 1);
expect(checkListNode.getTag()).toBe('ul');
});
});
test('ListNode.createDOM()', async () => {
const {editor} = testEnv;
test('ListNode.createDOM()', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
expect(listNode.createDOM(editorConfig).outerHTML).toBe(
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
expect(listNode.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-1"></ul>',
);
expect(
);
expect(
listNode.createDOM({
namespace: '',
theme: {
list: {},
},
}).outerHTML,
).toBe('<ul></ul>');
expect(
).toBe('<ul></ul>');
expect(
listNode.createDOM({
namespace: '',
theme: {},
}).outerHTML,
).toBe('<ul></ul>');
});
).toBe('<ul></ul>');
});
});
test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => {
const {editor} = testEnv;
test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode1 = $createListNode('bullet');
const listNode2 = $createListNode('bullet');
const listNode3 = $createListNode('bullet');
const listNode4 = $createListNode('bullet');
const listNode5 = $createListNode('bullet');
const listNode6 = $createListNode('bullet');
const listNode7 = $createListNode('bullet');
await editor.update(() => {
const listNode1 = $createListNode('bullet');
const listNode2 = $createListNode('bullet');
const listNode3 = $createListNode('bullet');
const listNode4 = $createListNode('bullet');
const listNode5 = $createListNode('bullet');
const listNode6 = $createListNode('bullet');
const listNode7 = $createListNode('bullet');
const listItem1 = $createListItemNode();
const listItem2 = $createListItemNode();
const listItem3 = $createListItemNode();
const listItem4 = $createListItemNode();
const listItem1 = $createListItemNode();
const listItem2 = $createListItemNode();
const listItem3 = $createListItemNode();
const listItem4 = $createListItemNode();
listNode1.append(listItem1);
listItem1.append(listNode2);
listNode2.append(listItem2);
listItem2.append(listNode3);
listNode3.append(listItem3);
listItem3.append(listNode4);
listNode4.append(listItem4);
listNode4.append(listNode5);
listNode5.append(listNode6);
listNode6.append(listNode7);
listNode1.append(listItem1);
listItem1.append(listNode2);
listNode2.append(listItem2);
listItem2.append(listNode3);
listNode3.append(listItem3);
listItem3.append(listNode4);
listNode4.append(listItem4);
listNode4.append(listNode5);
listNode5.append(listNode6);
listNode6.append(listNode7);
expect(listNode1.createDOM(editorConfig).outerHTML).toBe(
expect(listNode1.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-1"></ul>',
);
expect(
);
expect(
listNode1.createDOM({
namespace: '',
theme: {
list: {},
},
}).outerHTML,
).toBe('<ul></ul>');
expect(
).toBe('<ul></ul>');
expect(
listNode1.createDOM({
namespace: '',
theme: {},
}).outerHTML,
).toBe('<ul></ul>');
expect(listNode2.createDOM(editorConfig).outerHTML).toBe(
).toBe('<ul></ul>');
expect(listNode2.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-2"></ul>',
);
expect(listNode3.createDOM(editorConfig).outerHTML).toBe(
);
expect(listNode3.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-3"></ul>',
);
expect(listNode4.createDOM(editorConfig).outerHTML).toBe(
);
expect(listNode4.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-4"></ul>',
);
expect(listNode5.createDOM(editorConfig).outerHTML).toBe(
);
expect(listNode5.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-5"></ul>',
);
expect(listNode6.createDOM(editorConfig).outerHTML).toBe(
);
expect(listNode6.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-6"></ul>',
);
expect(listNode7.createDOM(editorConfig).outerHTML).toBe(
);
expect(listNode7.createDOM(editorConfig).outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-7"></ul>',
);
expect(
);
expect(
listNode5.createDOM({
namespace: '',
theme: {
@ -176,123 +176,135 @@ describe('LexicalListNode tests', () => {
},
},
}).outerHTML,
).toBe('<ul class="my-ul-list-class my-ul-list-class-2"></ul>');
});
).toBe('<ul class="my-ul-list-class my-ul-list-class-2"></ul>');
});
});
test('ListNode.updateDOM()', async () => {
const {editor} = testEnv;
test('ListNode.updateDOM()', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
const domElement = listNode.createDOM(editorConfig);
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
const domElement = listNode.createDOM(editorConfig);
expect(domElement.outerHTML).toBe(
expect(domElement.outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-1"></ul>',
);
);
const newListNode = $createListNode('number', 1);
const result = newListNode.updateDOM(
const newListNode = $createListNode('number', 1);
const result = newListNode.updateDOM(
listNode,
domElement,
editorConfig,
);
);
expect(result).toBe(true);
expect(domElement.outerHTML).toBe(
expect(result).toBe(true);
expect(domElement.outerHTML).toBe(
'<ul class="my-ul-list-class my-ul-list-class-1"></ul>',
);
});
});
test('ListNode.append() should properly transform a ListItemNode', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listNode = new ListNode('bullet', 1);
const listItemNode = new ListItemNode();
const textNode = new TextNode('Hello');
listItemNode.append(textNode);
const nodesToAppend = [listItemNode];
expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect(listNode.getFirstChild()).toBe(listItemNode);
expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');
});
});
test('ListNode.append() should properly transform a ListNode', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listNode = new ListNode('bullet', 1);
const nestedListNode = new ListNode('bullet', 1);
const listItemNode = new ListItemNode();
const textNode = new TextNode('Hello');
listItemNode.append(textNode);
nestedListNode.append(listItemNode);
const nodesToAppend = [nestedListNode];
expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect($isListItemNode(listNode.getFirstChild())).toBe(true);
expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe(
nestedListNode,
);
});
});
test('ListNode.append() should properly transform a ParagraphNode', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listNode = new ListNode('bullet', 1);
const paragraph = new ParagraphNode();
const textNode = new TextNode('Hello');
paragraph.append(textNode);
const nodesToAppend = [paragraph];
expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect($isListItemNode(listNode.getFirstChild())).toBe(true);
expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');
});
});
test('$createListNode()', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
const createdListNode = $createListNode('bullet');
expect(listNode.__type).toEqual(createdListNode.__type);
expect(listNode.__parent).toEqual(createdListNode.__parent);
expect(listNode.__tag).toEqual(createdListNode.__tag);
expect(listNode.__key).not.toEqual(createdListNode.__key);
});
});
test('$isListNode()', async () => {
const {editor} = testEnv;
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
expect($isListNode(listNode)).toBe(true);
});
});
test('$createListNode() with tag name (backward compatibility)', async () => {
const {editor} = testEnv;
await editor.update(() => {
const numberList = $createListNode('number', 1);
const bulletList = $createListNode('bullet', 1);
expect(numberList.__listType).toBe('number');
expect(bulletList.__listType).toBe('bullet');
});
);
});
});
test('ListNode.append() should properly transform a ListItemNode', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = new ListNode('bullet', 1);
const listItemNode = new ListItemNode();
const textNode = new TextNode('Hello');
listItemNode.append(textNode);
const nodesToAppend = [listItemNode];
expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect(listNode.getFirstChild()).toBe(listItemNode);
expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');
});
});
test('ListNode.append() should properly transform a ListNode', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = new ListNode('bullet', 1);
const nestedListNode = new ListNode('bullet', 1);
const listItemNode = new ListItemNode();
const textNode = new TextNode('Hello');
listItemNode.append(textNode);
nestedListNode.append(listItemNode);
const nodesToAppend = [nestedListNode];
expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect($isListItemNode(listNode.getFirstChild())).toBe(true);
expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe(
nestedListNode,
);
});
});
test('ListNode.append() should properly transform a ParagraphNode', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = new ListNode('bullet', 1);
const paragraph = new ParagraphNode();
const textNode = new TextNode('Hello');
paragraph.append(textNode);
const nodesToAppend = [paragraph];
expect(listNode.append(...nodesToAppend)).toBe(listNode);
expect($isListItemNode(listNode.getFirstChild())).toBe(true);
expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');
});
});
test('$createListNode()', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
const createdListNode = $createListNode('bullet');
expect(listNode.__type).toEqual(createdListNode.__type);
expect(listNode.__parent).toEqual(createdListNode.__parent);
expect(listNode.__tag).toEqual(createdListNode.__tag);
expect(listNode.__key).not.toEqual(createdListNode.__key);
});
});
test('$isListNode()', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const listNode = $createListNode('bullet', 1);
expect($isListNode(listNode)).toBe(true);
});
});
test('$createListNode() with tag name (backward compatibility)', async () => {
const {editor} = createTestContext();
await editor.update(() => {
const numberList = $createListNode('number', 1);
const bulletList = $createListNode('bullet', 1);
expect(numberList.__listType).toBe('number');
expect(bulletList.__listType).toBe('bullet');
});
});
test('importDOM handles old editor expected task list format', async () => {
const {editor} = createTestContext();
let list!: ListNode;
editor.update(() => {
const nodes = $htmlToBlockNodes(editor, `<ul><li class="task-list-item"><input checked="" disabled="" type="checkbox"> A</li></ul>`);
list = nodes[0] as ListNode;
});
expect(list).toBeInstanceOf(ListNode);
expect(list.getListType()).toBe('check');
});
});

View File

@ -1,7 +1,7 @@
import {
createTestContext, destroyFromContext,
dispatchKeydownEventForNode,
dispatchKeydownEventForSelectedNode,
dispatchKeydownEventForSelectedNode, expectNodeShapeToMatch,
} from "lexical/__tests__/utils";
import {
$createParagraphNode, $createTextNode,
@ -13,6 +13,7 @@ import {registerKeyboardHandling} from "../keyboard-handling";
import {registerRichText} from "@lexical/rich-text";
import {EditorUiContext} from "../../ui/framework/core";
import {$createListItemNode, $createListNode, ListItemNode, ListNode} from "@lexical/list";
import {$createImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode";
describe('Keyboard-handling service tests', () => {
@ -127,4 +128,34 @@ describe('Keyboard-handling service tests', () => {
expect(selectedNode?.getKey()).toBe(innerList.getChildren()[0].getKey());
});
});
test('Images: up on selected image creates new paragraph if none above', () => {
let image!: ImageNode;
editor.updateAndCommit(() => {
const root = $getRoot();
const imageWrap = $createParagraphNode();
image = $createImageNode('https://example.com/cat.png');
imageWrap.append(image);
root.append(imageWrap);
image.select();
});
expectNodeShapeToMatch(editor, [{
type: 'paragraph',
children: [
{type: 'image'}
],
}]);
dispatchKeydownEventForNode(image, editor, 'ArrowUp');
expectNodeShapeToMatch(editor, [{
type: 'paragraph',
}, {
type: 'paragraph',
children: [
{type: 'image'}
],
}]);
});
});

View File

@ -79,22 +79,19 @@ function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event:
const nearestBlock = $getNearestNodeBlockParent(node) || node;
let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling();
requestAnimationFrame(() => {
editor.update(() => {
if (!target) {
target = $createParagraphNode();
if (after) {
nearestBlock.insertAfter(target)
} else {
nearestBlock.insertBefore(target);
}
editor.update(() => {
if (!target) {
target = $createParagraphNode();
if (after) {
nearestBlock.insertAfter(target)
} else {
nearestBlock.insertBefore(target);
}
}
target.selectStart();
});
target.selectStart();
});
return true;
}