diff --git a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts
index 94582207ffc..a4c49cb6dfd 100644
--- a/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts
+++ b/packages/lexical-link/src/__tests__/unit/LexicalLinkNode.test.ts
@@ -15,14 +15,19 @@ import {
} from '@lexical/link';
import {$createMarkNode, $isMarkNode} from '@lexical/mark';
import {
+ $createLineBreakNode,
$createParagraphNode,
$createTextNode,
$getRoot,
+ $getSelection,
+ $isLineBreakNode,
+ $isRangeSelection,
+ $isTextNode,
$selectAll,
ParagraphNode,
+ RangeSelection,
SerializedParagraphNode,
- TextNode,
-} from 'lexical/src';
+} from 'lexical';
import {initializeUnitTest} from 'lexical/src/__tests__/utils';
const editorConfig = Object.freeze({
@@ -47,20 +52,20 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('/');
+ const linkNode = $createLinkNode('/');
expect(linkNode.__type).toBe('link');
expect(linkNode.__url).toBe('/');
});
- expect(() => new LinkNode('')).toThrow();
+ expect(() => $createLinkNode('')).toThrow();
});
test('LineBreakNode.clone()', async () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('/');
+ const linkNode = $createLinkNode('/');
const linkNodeClone = LinkNode.clone(linkNode);
@@ -73,7 +78,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo');
+ const linkNode = $createLinkNode('https://example.com/foo');
expect(linkNode.getURL()).toBe('https://example.com/foo');
});
@@ -83,7 +88,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo');
+ const linkNode = $createLinkNode('https://example.com/foo');
expect(linkNode.getURL()).toBe('https://example.com/foo');
@@ -97,7 +102,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
target: '_blank',
});
@@ -109,7 +114,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
target: '_blank',
});
@@ -125,7 +130,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
rel: 'noopener noreferrer',
target: '_blank',
});
@@ -138,7 +143,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
rel: 'noopener',
target: '_blank',
});
@@ -155,7 +160,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
title: 'Hello world',
});
@@ -167,7 +172,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
title: 'Hello world',
});
@@ -183,7 +188,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo');
+ const linkNode = $createLinkNode('https://example.com/foo');
expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
'',
@@ -201,7 +206,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
rel: 'noopener noreferrer',
target: '_blank',
title: 'Hello world',
@@ -226,7 +231,7 @@ describe('LexicalLinkNode tests', () => {
await editor.update(() => {
// eslint-disable-next-line no-script-url
- const linkNode = new LinkNode('javascript:alert(0)');
+ const linkNode = $createLinkNode('javascript:alert(0)');
expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
'',
);
@@ -237,7 +242,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo');
+ const linkNode = $createLinkNode('https://example.com/foo');
const domElement = linkNode.createDOM(editorConfig);
@@ -245,7 +250,7 @@ describe('LexicalLinkNode tests', () => {
'',
);
- const newLinkNode = new LinkNode('https://example.com/bar');
+ const newLinkNode = $createLinkNode('https://example.com/bar');
const result = newLinkNode.updateDOM(
linkNode,
domElement,
@@ -263,7 +268,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
rel: 'noopener noreferrer',
target: '_blank',
title: 'Hello world',
@@ -275,7 +280,7 @@ describe('LexicalLinkNode tests', () => {
'',
);
- const newLinkNode = new LinkNode('https://example.com/bar', {
+ const newLinkNode = $createLinkNode('https://example.com/bar', {
rel: 'noopener',
target: '_self',
title: 'World hello',
@@ -297,7 +302,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
rel: 'noopener noreferrer',
target: '_blank',
title: 'Hello world',
@@ -309,7 +314,7 @@ describe('LexicalLinkNode tests', () => {
'',
);
- const newLinkNode = new LinkNode('https://example.com/bar');
+ const newLinkNode = $createLinkNode('https://example.com/bar');
const result = newLinkNode.updateDOM(
linkNode,
domElement,
@@ -327,7 +332,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo');
+ const linkNode = $createLinkNode('https://example.com/foo');
expect(linkNode.canInsertTextBefore()).toBe(false);
});
@@ -337,7 +342,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo');
+ const linkNode = $createLinkNode('https://example.com/foo');
expect(linkNode.canInsertTextAfter()).toBe(false);
});
@@ -347,7 +352,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo');
+ const linkNode = $createLinkNode('https://example.com/foo');
const createdLinkNode = $createLinkNode('https://example.com/foo');
@@ -362,7 +367,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('https://example.com/foo', {
+ const linkNode = $createLinkNode('https://example.com/foo', {
rel: 'noopener noreferrer',
target: '_blank',
title: 'Hello world',
@@ -388,7 +393,7 @@ describe('LexicalLinkNode tests', () => {
const {editor} = testEnv;
await editor.update(() => {
- const linkNode = new LinkNode('');
+ const linkNode = $createLinkNode('');
expect($isLinkNode(linkNode)).toBe(true);
});
@@ -397,14 +402,27 @@ describe('LexicalLinkNode tests', () => {
test('$toggleLink applies the title attribute when creating', async () => {
const {editor} = testEnv;
await editor.update(() => {
- const p = new ParagraphNode();
- p.append(new TextNode('Some text'));
+ const p = $createParagraphNode();
+ const textNode = $createTextNode('Some text');
+ p.append(textNode);
$getRoot().append(p);
- });
-
- await editor.update(() => {
$selectAll();
$toggleLink('https://lexical.dev/', {title: 'Lexical Website'});
+ const linkNode = p.getFirstChild() as LinkNode;
+ expect($isLinkNode(linkNode)).toBe(true);
+ expect(linkNode.getTitle()).toBe('Lexical Website');
+ const selection = $getSelection() as RangeSelection;
+ expect($isRangeSelection(selection)).toBe(true);
+ expect(selection.anchor).toMatchObject({
+ key: textNode.getKey(),
+ offset: 0,
+ type: 'text',
+ });
+ expect(selection.focus).toMatchObject({
+ key: textNode.getKey(),
+ offset: textNode.getTextContentSize(),
+ type: 'text',
+ });
});
const paragraph = editor!.getEditorState().toJSON().root
@@ -442,6 +460,7 @@ describe('LexicalLinkNode tests', () => {
expect(textNode.getTextContent()).toBe('some ');
// Check link node and its nested structure
+ expect($isLinkNode(linkNode)).toBe(true);
if ($isLinkNode(linkNode)) {
expect(linkNode.getURL()).toBe('https://example.com/foo');
expect(linkNode.getRel()).toBe('noreferrer');
@@ -470,6 +489,7 @@ describe('LexicalLinkNode tests', () => {
expect(textNode.getTextContent()).toBe('some ');
// Check mark node is preserved and moved up to paragraph level
+ expect($isMarkNode(markNode)).toBe(true);
if ($isMarkNode(markNode)) {
expect(markNode.getType()).toBe('mark');
expect(markNode.getIDs()).toEqual(['knetk']);
@@ -477,5 +497,64 @@ describe('LexicalLinkNode tests', () => {
}
});
});
+
+ test('$toggleLink adds link with embedded LineBreakNode', async () => {
+ const {editor} = testEnv;
+ await editor.update(() => {
+ const paragraph = $createParagraphNode();
+ const precedingText = $createTextNode('some '); // space after
+ const textNode = $createTextNode('text');
+ paragraph.append(precedingText, textNode, $createLineBreakNode());
+ $getRoot().clear().append(paragraph);
+ paragraph.select(1);
+ $toggleLink('https://example.com/foo', {
+ rel: 'noreferrer',
+ });
+ });
+
+ editor.read(() => {
+ const paragraph = $getRoot().getFirstChild() as ParagraphNode;
+ const [precedingText, linkNode] = paragraph.getChildren();
+
+ // Check first text node
+ expect(precedingText.getTextContent()).toBe('some ');
+
+ // Check link node and its nested structure
+ expect($isLinkNode(linkNode)).toBe(true);
+ if ($isLinkNode(linkNode)) {
+ expect(linkNode.getURL()).toBe('https://example.com/foo');
+ expect(linkNode.getRel()).toBe('noreferrer');
+ expect(
+ linkNode.getChildren().map((node) => node.getTextContent()),
+ ).toEqual(['text', '\n']);
+ expect($getSelection()).toMatchObject({
+ anchor: {
+ key: linkNode.getFirstChildOrThrow().getKey(),
+ offset: 0,
+ type: 'text',
+ },
+ focus: {key: linkNode.getKey(), offset: 2, type: 'element'},
+ });
+ }
+ });
+
+ await editor.update(() => {
+ $selectAll();
+ $toggleLink(null);
+ });
+
+ // Verify structure after link removal
+ editor.read(() => {
+ const paragraph = $getRoot().getFirstChild() as ParagraphNode;
+ const children = paragraph.getChildren();
+ expect(children.map((node) => node.getTextContent())).toEqual([
+ 'some text',
+ '\n',
+ ]);
+ const [textNode, lineBreakNode] = children;
+ expect($isTextNode(textNode)).toBe(true);
+ expect($isLineBreakNode(lineBreakNode)).toBe(true);
+ });
+ });
});
});