2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
15 } from '@lexical/link';
20 SerializedParagraphNode,
23 import {initializeUnitTest} from 'lexical/__tests__/utils';
25 const editorConfig = Object.freeze({
28 link: 'my-link-class',
30 bold: 'my-bold-class',
31 code: 'my-code-class',
32 hashtag: 'my-hashtag-class',
33 italic: 'my-italic-class',
34 strikethrough: 'my-strikethrough-class',
35 underline: 'my-underline-class',
36 underlineStrikethrough: 'my-underline-strikethrough-class',
41 describe('LexicalLinkNode tests', () => {
42 initializeUnitTest((testEnv) => {
43 test('LinkNode.constructor', async () => {
44 const {editor} = testEnv;
46 await editor.update(() => {
47 const linkNode = new LinkNode('/');
49 expect(linkNode.__type).toBe('link');
50 expect(linkNode.__url).toBe('/');
53 expect(() => new LinkNode('')).toThrow();
56 test('LineBreakNode.clone()', async () => {
57 const {editor} = testEnv;
59 await editor.update(() => {
60 const linkNode = new LinkNode('/');
62 const linkNodeClone = LinkNode.clone(linkNode);
64 expect(linkNodeClone).not.toBe(linkNode);
65 expect(linkNodeClone).toStrictEqual(linkNode);
69 test('LinkNode.getURL()', async () => {
70 const {editor} = testEnv;
72 await editor.update(() => {
73 const linkNode = new LinkNode('https://example.com/foo');
75 expect(linkNode.getURL()).toBe('https://example.com/foo');
79 test('LinkNode.setURL()', async () => {
80 const {editor} = testEnv;
82 await editor.update(() => {
83 const linkNode = new LinkNode('https://example.com/foo');
85 expect(linkNode.getURL()).toBe('https://example.com/foo');
87 linkNode.setURL('https://example.com/bar');
89 expect(linkNode.getURL()).toBe('https://example.com/bar');
93 test('LinkNode.getTarget()', async () => {
94 const {editor} = testEnv;
96 await editor.update(() => {
97 const linkNode = new LinkNode('https://example.com/foo', {
101 expect(linkNode.getTarget()).toBe('_blank');
105 test('LinkNode.setTarget()', async () => {
106 const {editor} = testEnv;
108 await editor.update(() => {
109 const linkNode = new LinkNode('https://example.com/foo', {
113 expect(linkNode.getTarget()).toBe('_blank');
115 linkNode.setTarget('_self');
117 expect(linkNode.getTarget()).toBe('_self');
121 test('LinkNode.getRel()', async () => {
122 const {editor} = testEnv;
124 await editor.update(() => {
125 const linkNode = new LinkNode('https://example.com/foo', {
126 rel: 'noopener noreferrer',
130 expect(linkNode.getRel()).toBe('noopener noreferrer');
134 test('LinkNode.setRel()', async () => {
135 const {editor} = testEnv;
137 await editor.update(() => {
138 const linkNode = new LinkNode('https://example.com/foo', {
143 expect(linkNode.getRel()).toBe('noopener');
145 linkNode.setRel('noopener noreferrer');
147 expect(linkNode.getRel()).toBe('noopener noreferrer');
151 test('LinkNode.getTitle()', async () => {
152 const {editor} = testEnv;
154 await editor.update(() => {
155 const linkNode = new LinkNode('https://example.com/foo', {
156 title: 'Hello world',
159 expect(linkNode.getTitle()).toBe('Hello world');
163 test('LinkNode.setTitle()', async () => {
164 const {editor} = testEnv;
166 await editor.update(() => {
167 const linkNode = new LinkNode('https://example.com/foo', {
168 title: 'Hello world',
171 expect(linkNode.getTitle()).toBe('Hello world');
173 linkNode.setTitle('World hello');
175 expect(linkNode.getTitle()).toBe('World hello');
179 test('LinkNode.createDOM()', async () => {
180 const {editor} = testEnv;
182 await editor.update(() => {
183 const linkNode = new LinkNode('https://example.com/foo');
185 expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
186 '<a href="https://example.com/foo" class="my-link-class"></a>',
193 ).toBe('<a href="https://example.com/foo"></a>');
197 test('LinkNode.createDOM() with target, rel and title', async () => {
198 const {editor} = testEnv;
200 await editor.update(() => {
201 const linkNode = new LinkNode('https://example.com/foo', {
202 rel: 'noopener noreferrer',
204 title: 'Hello world',
207 expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
208 '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-link-class"></a>',
216 '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world"></a>',
221 test('LinkNode.createDOM() sanitizes javascript: URLs', async () => {
222 const {editor} = testEnv;
224 await editor.update(() => {
225 // eslint-disable-next-line no-script-url
226 const linkNode = new LinkNode('javascript:alert(0)');
227 expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
228 '<a href="about:blank" class="my-link-class"></a>',
233 test('LinkNode.updateDOM()', async () => {
234 const {editor} = testEnv;
236 await editor.update(() => {
237 const linkNode = new LinkNode('https://example.com/foo');
239 const domElement = linkNode.createDOM(editorConfig);
241 expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
242 '<a href="https://example.com/foo" class="my-link-class"></a>',
245 const newLinkNode = new LinkNode('https://example.com/bar');
246 const result = newLinkNode.updateDOM(
252 expect(result).toBe(false);
253 expect(domElement.outerHTML).toBe(
254 '<a href="https://example.com/bar" class="my-link-class"></a>',
259 test('LinkNode.updateDOM() with target, rel and title', async () => {
260 const {editor} = testEnv;
262 await editor.update(() => {
263 const linkNode = new LinkNode('https://example.com/foo', {
264 rel: 'noopener noreferrer',
266 title: 'Hello world',
269 const domElement = linkNode.createDOM(editorConfig);
271 expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
272 '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-link-class"></a>',
275 const newLinkNode = new LinkNode('https://example.com/bar', {
278 title: 'World hello',
280 const result = newLinkNode.updateDOM(
286 expect(result).toBe(false);
287 expect(domElement.outerHTML).toBe(
288 '<a href="https://example.com/bar" target="_self" rel="noopener" title="World hello" class="my-link-class"></a>',
293 test('LinkNode.updateDOM() with undefined target, undefined rel and undefined title', async () => {
294 const {editor} = testEnv;
296 await editor.update(() => {
297 const linkNode = new LinkNode('https://example.com/foo', {
298 rel: 'noopener noreferrer',
300 title: 'Hello world',
303 const domElement = linkNode.createDOM(editorConfig);
305 expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
306 '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-link-class"></a>',
309 const newLinkNode = new LinkNode('https://example.com/bar');
310 const result = newLinkNode.updateDOM(
316 expect(result).toBe(false);
317 expect(domElement.outerHTML).toBe(
318 '<a href="https://example.com/bar" class="my-link-class"></a>',
323 test('LinkNode.canInsertTextBefore()', async () => {
324 const {editor} = testEnv;
326 await editor.update(() => {
327 const linkNode = new LinkNode('https://example.com/foo');
329 expect(linkNode.canInsertTextBefore()).toBe(false);
333 test('LinkNode.canInsertTextAfter()', async () => {
334 const {editor} = testEnv;
336 await editor.update(() => {
337 const linkNode = new LinkNode('https://example.com/foo');
339 expect(linkNode.canInsertTextAfter()).toBe(false);
343 test('$createLinkNode()', async () => {
344 const {editor} = testEnv;
346 await editor.update(() => {
347 const linkNode = new LinkNode('https://example.com/foo');
349 const createdLinkNode = $createLinkNode('https://example.com/foo');
351 expect(linkNode.__type).toEqual(createdLinkNode.__type);
352 expect(linkNode.__parent).toEqual(createdLinkNode.__parent);
353 expect(linkNode.__url).toEqual(createdLinkNode.__url);
354 expect(linkNode.__key).not.toEqual(createdLinkNode.__key);
358 test('$createLinkNode() with target, rel and title', async () => {
359 const {editor} = testEnv;
361 await editor.update(() => {
362 const linkNode = new LinkNode('https://example.com/foo', {
363 rel: 'noopener noreferrer',
365 title: 'Hello world',
368 const createdLinkNode = $createLinkNode('https://example.com/foo', {
369 rel: 'noopener noreferrer',
371 title: 'Hello world',
374 expect(linkNode.__type).toEqual(createdLinkNode.__type);
375 expect(linkNode.__parent).toEqual(createdLinkNode.__parent);
376 expect(linkNode.__url).toEqual(createdLinkNode.__url);
377 expect(linkNode.__target).toEqual(createdLinkNode.__target);
378 expect(linkNode.__rel).toEqual(createdLinkNode.__rel);
379 expect(linkNode.__title).toEqual(createdLinkNode.__title);
380 expect(linkNode.__key).not.toEqual(createdLinkNode.__key);
384 test('$isLinkNode()', async () => {
385 const {editor} = testEnv;
387 await editor.update(() => {
388 const linkNode = new LinkNode('');
390 expect($isLinkNode(linkNode)).toBe(true);
394 test('$toggleLink applies the title attribute when creating', async () => {
395 const {editor} = testEnv;
396 await editor.update(() => {
397 const p = new ParagraphNode();
398 p.append(new TextNode('Some text'));
399 $getRoot().append(p);
402 await editor.update(() => {
404 $toggleLink('https://lexical.dev/', {title: 'Lexical Website'});
407 const paragraph = editor!.getEditorState().toJSON().root
408 .children[0] as SerializedParagraphNode;
409 const link = paragraph.children[0] as SerializedLinkNode;
410 expect(link.title).toBe('Lexical Website');