]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Fixed auto-link issue
authorDan Brown <redacted>
Wed, 15 Jan 2025 14:15:58 +0000 (14:15 +0000)
committerDan Brown <redacted>
Wed, 15 Jan 2025 14:15:58 +0000 (14:15 +0000)
Added extra test helper to check the editor state directly via string
notation access rather than juggling types/objects to access deep
properties.

resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
resources/js/wysiwyg/services/__tests__/auto-links.test.ts
resources/js/wysiwyg/services/auto-links.ts
resources/js/wysiwyg/todo.md

index b13bba6977e882a2fc7970687e9715779c027809..d54a64ce89a6f9b6f64322c5e772f4d5ff36fff1 100644 (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();
 }
index 30dc925659ef2475309b9fb712f79a06692648f9..add61c495a2243a7796504c69e93544458ca36d3 100644 (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} = testEnv;
-
-            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);
-            });
+    test('space 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);
+        });
 
-            editor.commitUpdates();
+        dispatchKeydownEventForNode(pNode, editor, ' ');
 
-            const pDomEl = editor.getElementByKey(pNode.getKey());
-            const event = new KeyboardEvent('keydown', {
-                bubbles: true,
-                cancelable: true,
-                key: ' ',
-                keyCode: 62,
-            });
-            pDomEl?.dispatchEvent(event);
+        expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
+        expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');
+    });
 
-            editor.commitUpdates();
+    test('space after link at end of line', async () => {
+        const {editor} = createTestContext();
+        registerAutoLinks(editor);
+        let pNode!: ParagraphNode;
 
-            const paragraph = editor!.getEditorState().toJSON().root
-                .children[0] as SerializedParagraphNode;
-            expect(paragraph.children[1].type).toBe('link');
+        editor.updateAndCommit(() => {
+            pNode = new ParagraphNode();
+            const text = new TextNode('Some https://example.com?test=true');
+            pNode.append(text);
+            $getRoot().append(pNode);
 
-            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();
         });
 
-        test('enter after link in text', async () => {
-            const {editor} = testEnv;
-
-            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);
+        dispatchKeydownEventForNode(pNode, editor, ' ');
 
-                text.select(34, 34);
-            });
+        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');
+    });
 
-            editor.commitUpdates();
+    test('enter after link in text', async () => {
+        const {editor} = createTestContext();
+        registerAutoLinks(editor);
+        let pNode!: ParagraphNode;
 
-            const pDomEl = editor.getElementByKey(pNode.getKey());
-            const event = new KeyboardEvent('keydown', {
-                bubbles: true,
-                cancelable: true,
-                key: 'Enter',
-                keyCode: 66,
-            });
-            pDomEl?.dispatchEvent(event);
+        editor.updateAndCommit(() => {
+            pNode = new ParagraphNode();
+            const text = new TextNode('Some https://example.com?test=true text');
+            pNode.append(text);
+            $getRoot().append(pNode);
 
-            editor.commitUpdates();
+            text.select(34, 34);
+        });
 
-            const paragraph = editor!.getEditorState().toJSON().root
-                .children[0] as SerializedParagraphNode;
-            expect(paragraph.children[1].type).toBe('link');
+        dispatchKeydownEventForNode(pNode, editor, 'Enter');
 
-            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');
-        });
+        expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
+        expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');
     });
 });
\ No newline at end of file
index 1c3b1c73010c750f121ca0e81c05ce6c9965d198..62cd459940c0471f03c985a7c3276a4cec1b79e0 100644 (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);
index 817a235a712dcb2c8d1276ebbf5da898bdbb7978..a49cccd26dcbf775be02def3cf574bf25fa05f17 100644 (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