diff --git a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts index 3ad6cbad8b8..94582207ffc 100644 --- a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts +++ b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts @@ -13,7 +13,10 @@ import { LinkNode, SerializedLinkNode, } from '@lexical/link'; +import {$createMarkNode, $isMarkNode} from '@lexical/mark'; import { + $createParagraphNode, + $createTextNode, $getRoot, $selectAll, ParagraphNode, @@ -409,5 +412,70 @@ describe('LexicalLinkNode tests', () => { const link = paragraph.children[0] as SerializedLinkNode; expect(link.title).toBe('Lexical Website'); }); + + test('$toggleLink correctly removes link when textnode has children(like marknode)', async () => { + const {editor} = testEnv; + await editor.update(() => { + const paragraph = $createParagraphNode(); + const precedingText = $createTextNode('some '); // space after + const textNode = $createTextNode('text'); + + paragraph.append(precedingText, textNode); + + const linkNode = $createLinkNode('https://example.com/foo', { + rel: 'noreferrer', + }); + textNode.insertAfter(linkNode); + linkNode.append(textNode); + + const markNode = $createMarkNode(['knetk']); + textNode.insertBefore(markNode); + markNode.append(textNode); + $getRoot().append(paragraph); + }); + + editor.read(() => { + const paragraph = $getRoot().getFirstChild() as ParagraphNode; + const [textNode, linkNode] = paragraph.getChildren(); + + // Check first text node + expect(textNode.getTextContent()).toBe('some '); + + // Check link node and its nested structure + if ($isLinkNode(linkNode)) { + expect(linkNode.getURL()).toBe('https://example.com/foo'); + expect(linkNode.getRel()).toBe('noreferrer'); + + // Check mark node nested inside link + const markNode = linkNode.getFirstChild(); + if ($isMarkNode(markNode)) { + expect(markNode.getType()).toBe('mark'); + expect(markNode.getIDs()).toEqual(['knetk']); + expect(markNode.getTextContent()).toBe('text'); + } + } + }); + + await editor.update(() => { + $selectAll(); + $toggleLink(null); + }); + + // Verify structure after link removal + editor.read(() => { + const paragraph = $getRoot().getFirstChild() as ParagraphNode; + const [textNode, markNode] = paragraph.getChildren(); + + // Check text node remains unchanged + expect(textNode.getTextContent()).toBe('some '); + + // Check mark node is preserved and moved up to paragraph level + if ($isMarkNode(markNode)) { + expect(markNode.getType()).toBe('mark'); + expect(markNode.getIDs()).toEqual(['knetk']); + expect(markNode.getTextContent()).toBe('text'); + } + }); + }); }); }); diff --git a/packages/lexical/src/__tests__/utils/index.tsx b/packages/lexical/src/__tests__/utils/index.tsx index dff3b2adaee..4bb096f874d 100644 --- a/packages/lexical/src/__tests__/utils/index.tsx +++ b/packages/lexical/src/__tests__/utils/index.tsx @@ -11,6 +11,7 @@ import {HashtagNode} from '@lexical/hashtag'; import {createHeadlessEditor} from '@lexical/headless'; import {AutoLinkNode, LinkNode} from '@lexical/link'; import {ListItemNode, ListNode} from '@lexical/list'; +import {MarkNode} from '@lexical/mark'; import {OverflowNode} from '@lexical/overflow'; import { InitialConfigType, @@ -486,6 +487,7 @@ const DEFAULT_NODES: NonNullable = [ TestInlineElementNode, TestShadowRootNode, TestTextNode, + MarkNode, ]; export function TestComposer({