Lexical: Fixed auto-link issue

Added extra test helper to check the editor state directly via string
notation access rather than juggling types/objects to access deep
properties.
This commit is contained in:
Dan Brown 2025-01-15 14:15:58 +00:00
parent 786a434c03
commit 0d1a237f81
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
4 changed files with 86 additions and 80 deletions

View File

@ -37,8 +37,6 @@ 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 {turtle} from "@codemirror/legacy-modes/mode/turtle";
type TestEnv = {
readonly container: HTMLDivElement;
@ -47,6 +45,9 @@ type TestEnv = {
readonly innerHTML: string;
};
/**
* @deprecated - Consider using `createTestContext` instead within the test case.
*/
export function initializeUnitTest(
runTests: (testEnv: TestEnv) => void,
editorConfig: CreateEditorArgs = {namespace: 'test', theme: {}},
@ -795,6 +796,30 @@ export function expectNodeShapeToMatch(editor: LexicalEditor, expected: nodeShap
expect(shape.children).toMatchObject(expected);
}
/**
* Expect a given prop within the JSON editor state structure to be the given value.
* Uses dot notation for the provided `propPath`. Example:
* 0.5.cat => First child, Sixth child, cat property
*/
export function expectEditorStateJSONPropToEqual(editor: LexicalEditor, propPath: string, expected: any) {
let currentItem: any = editor.getEditorState().toJSON().root;
let currentPath = [];
const pathParts = propPath.split('.');
for (const part of pathParts) {
currentPath.push(part);
const childAccess = Number.isInteger(Number(part)) && Array.isArray(currentItem.children);
const target = childAccess ? currentItem.children : currentItem;
if (typeof target[part] === 'undefined') {
throw new Error(`Could not resolve editor state at path ${currentPath.join('.')}`)
}
currentItem = target[part];
}
expect(currentItem).toBe(expected);
}
function formatHtml(s: string): string {
return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();
}

View File

@ -1,91 +1,76 @@
import {initializeUnitTest} from "lexical/__tests__/utils";
import {SerializedLinkNode} from "@lexical/link";
import {
createTestContext,
dispatchKeydownEventForNode, expectEditorStateJSONPropToEqual,
expectNodeShapeToMatch
} from "lexical/__tests__/utils";
import {
$getRoot,
ParagraphNode,
SerializedParagraphNode,
SerializedTextNode,
TextNode
} from "lexical";
import {registerAutoLinks} from "../auto-links";
describe('Auto-link service tests', () => {
initializeUnitTest((testEnv) => {
test('space after link in text', async () => {
const {editor} = createTestContext();
registerAutoLinks(editor);
let pNode!: ParagraphNode;
test('space after link in text', async () => {
const {editor} = testEnv;
editor.updateAndCommit(() => {
pNode = new ParagraphNode();
const text = new TextNode('Some https://example.com?test=true text');
pNode.append(text);
$getRoot().append(pNode);
registerAutoLinks(editor);
let pNode!: ParagraphNode;
editor.update(() => {
pNode = new ParagraphNode();
const text = new TextNode('Some https://example.com?test=true text');
pNode.append(text);
$getRoot().append(pNode);
text.select(34, 34);
});
editor.commitUpdates();
const pDomEl = editor.getElementByKey(pNode.getKey());
const event = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: ' ',
keyCode: 62,
});
pDomEl?.dispatchEvent(event);
editor.commitUpdates();
const paragraph = editor!.getEditorState().toJSON().root
.children[0] as SerializedParagraphNode;
expect(paragraph.children[1].type).toBe('link');
const link = paragraph.children[1] as SerializedLinkNode;
expect(link.url).toBe('https://example.com?test=true');
const linkText = link.children[0] as SerializedTextNode;
expect(linkText.text).toBe('https://example.com?test=true');
text.select(34, 34);
});
test('enter after link in text', async () => {
const {editor} = testEnv;
dispatchKeydownEventForNode(pNode, editor, ' ');
registerAutoLinks(editor);
let pNode!: ParagraphNode;
expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');
});
editor.update(() => {
pNode = new ParagraphNode();
const text = new TextNode('Some https://example.com?test=true text');
pNode.append(text);
$getRoot().append(pNode);
test('space after link at end of line', async () => {
const {editor} = createTestContext();
registerAutoLinks(editor);
let pNode!: ParagraphNode;
text.select(34, 34);
});
editor.updateAndCommit(() => {
pNode = new ParagraphNode();
const text = new TextNode('Some https://example.com?test=true');
pNode.append(text);
$getRoot().append(pNode);
editor.commitUpdates();
const pDomEl = editor.getElementByKey(pNode.getKey());
const event = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'Enter',
keyCode: 66,
});
pDomEl?.dispatchEvent(event);
editor.commitUpdates();
const paragraph = editor!.getEditorState().toJSON().root
.children[0] as SerializedParagraphNode;
expect(paragraph.children[1].type).toBe('link');
const link = paragraph.children[1] as SerializedLinkNode;
expect(link.url).toBe('https://example.com?test=true');
const linkText = link.children[0] as SerializedTextNode;
expect(linkText.text).toBe('https://example.com?test=true');
text.selectEnd();
});
dispatchKeydownEventForNode(pNode, editor, ' ');
expectNodeShapeToMatch(editor, [{type: 'paragraph', children: [
{text: 'Some '},
{type: 'link', children: [{text: 'https://example.com?test=true'}]}
]}]);
expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
});
test('enter after link in text', async () => {
const {editor} = createTestContext();
registerAutoLinks(editor);
let pNode!: ParagraphNode;
editor.updateAndCommit(() => {
pNode = new ParagraphNode();
const text = new TextNode('Some https://example.com?test=true text');
pNode.append(text);
$getRoot().append(pNode);
text.select(34, 34);
});
dispatchKeydownEventForNode(pNode, editor, 'Enter');
expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');
});
});

View File

@ -43,7 +43,7 @@ function handlePotentialLinkEvent(node: TextNode, selection: BaseSelection, edit
linkNode.append(new TextNode(textSegment));
const splits = node.splitText(startIndex, cursorPoint);
const targetIndex = splits.length === 3 ? 1 : 0;
const targetIndex = startIndex > 0 ? 1 : 0;
const targetText = splits[targetIndex];
if (targetText) {
targetText.replace(linkNode);

View File

@ -2,11 +2,7 @@
## In progress
Reorg
- Merge custom nodes into original nodes
- Reduce down to use CommonBlockNode where possible
- Remove existing formatType/ElementFormatType references (replaced with alignment).
- Remove existing indent references (replaced with inset).
//
## Main Todo