]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalLinkNode.test.ts
fe978849b153776a5f9d2698845ed8f91dc5c3c3
[bookstack] / resources / js / wysiwyg / lexical / link / __tests__ / unit / LexicalLinkNode.test.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import {
10   $createLinkNode,
11   $isLinkNode,
12   $toggleLink,
13   LinkNode,
14   SerializedLinkNode,
15 } from '@lexical/link';
16 import {
17   $getRoot,
18   $selectAll,
19   ParagraphNode,
20   SerializedParagraphNode,
21   TextNode,
22 } from 'lexical/src';
23 import {initializeUnitTest} from 'lexical/__tests__/utils';
24
25 const editorConfig = Object.freeze({
26   namespace: '',
27   theme: {
28     link: 'my-link-class',
29     text: {
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',
37     },
38   },
39 });
40
41 describe('LexicalLinkNode tests', () => {
42   initializeUnitTest((testEnv) => {
43     test('LinkNode.constructor', async () => {
44       const {editor} = testEnv;
45
46       await editor.update(() => {
47         const linkNode = new LinkNode('/');
48
49         expect(linkNode.__type).toBe('link');
50         expect(linkNode.__url).toBe('/');
51       });
52
53       expect(() => new LinkNode('')).toThrow();
54     });
55
56     test('LineBreakNode.clone()', async () => {
57       const {editor} = testEnv;
58
59       await editor.update(() => {
60         const linkNode = new LinkNode('/');
61
62         const linkNodeClone = LinkNode.clone(linkNode);
63
64         expect(linkNodeClone).not.toBe(linkNode);
65         expect(linkNodeClone).toStrictEqual(linkNode);
66       });
67     });
68
69     test('LinkNode.getURL()', async () => {
70       const {editor} = testEnv;
71
72       await editor.update(() => {
73         const linkNode = new LinkNode('https://example.com/foo');
74
75         expect(linkNode.getURL()).toBe('https://example.com/foo');
76       });
77     });
78
79     test('LinkNode.setURL()', async () => {
80       const {editor} = testEnv;
81
82       await editor.update(() => {
83         const linkNode = new LinkNode('https://example.com/foo');
84
85         expect(linkNode.getURL()).toBe('https://example.com/foo');
86
87         linkNode.setURL('https://example.com/bar');
88
89         expect(linkNode.getURL()).toBe('https://example.com/bar');
90       });
91     });
92
93     test('LinkNode.getTarget()', async () => {
94       const {editor} = testEnv;
95
96       await editor.update(() => {
97         const linkNode = new LinkNode('https://example.com/foo', {
98           target: '_blank',
99         });
100
101         expect(linkNode.getTarget()).toBe('_blank');
102       });
103     });
104
105     test('LinkNode.setTarget()', async () => {
106       const {editor} = testEnv;
107
108       await editor.update(() => {
109         const linkNode = new LinkNode('https://example.com/foo', {
110           target: '_blank',
111         });
112
113         expect(linkNode.getTarget()).toBe('_blank');
114
115         linkNode.setTarget('_self');
116
117         expect(linkNode.getTarget()).toBe('_self');
118       });
119     });
120
121     test('LinkNode.getRel()', async () => {
122       const {editor} = testEnv;
123
124       await editor.update(() => {
125         const linkNode = new LinkNode('https://example.com/foo', {
126           rel: 'noopener noreferrer',
127           target: '_blank',
128         });
129
130         expect(linkNode.getRel()).toBe('noopener noreferrer');
131       });
132     });
133
134     test('LinkNode.setRel()', async () => {
135       const {editor} = testEnv;
136
137       await editor.update(() => {
138         const linkNode = new LinkNode('https://example.com/foo', {
139           rel: 'noopener',
140           target: '_blank',
141         });
142
143         expect(linkNode.getRel()).toBe('noopener');
144
145         linkNode.setRel('noopener noreferrer');
146
147         expect(linkNode.getRel()).toBe('noopener noreferrer');
148       });
149     });
150
151     test('LinkNode.getTitle()', async () => {
152       const {editor} = testEnv;
153
154       await editor.update(() => {
155         const linkNode = new LinkNode('https://example.com/foo', {
156           title: 'Hello world',
157         });
158
159         expect(linkNode.getTitle()).toBe('Hello world');
160       });
161     });
162
163     test('LinkNode.setTitle()', async () => {
164       const {editor} = testEnv;
165
166       await editor.update(() => {
167         const linkNode = new LinkNode('https://example.com/foo', {
168           title: 'Hello world',
169         });
170
171         expect(linkNode.getTitle()).toBe('Hello world');
172
173         linkNode.setTitle('World hello');
174
175         expect(linkNode.getTitle()).toBe('World hello');
176       });
177     });
178
179     test('LinkNode.createDOM()', async () => {
180       const {editor} = testEnv;
181
182       await editor.update(() => {
183         const linkNode = new LinkNode('https://example.com/foo');
184
185         expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
186           '<a href="https://example.com/foo" class="my-link-class"></a>',
187         );
188         expect(
189           linkNode.createDOM({
190             namespace: '',
191             theme: {},
192           }).outerHTML,
193         ).toBe('<a href="https://example.com/foo"></a>');
194       });
195     });
196
197     test('LinkNode.createDOM() with target, rel and title', async () => {
198       const {editor} = testEnv;
199
200       await editor.update(() => {
201         const linkNode = new LinkNode('https://example.com/foo', {
202           rel: 'noopener noreferrer',
203           target: '_blank',
204           title: 'Hello world',
205         });
206
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>',
209         );
210         expect(
211           linkNode.createDOM({
212             namespace: '',
213             theme: {},
214           }).outerHTML,
215         ).toBe(
216           '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world"></a>',
217         );
218       });
219     });
220
221     test('LinkNode.createDOM() sanitizes javascript: URLs', async () => {
222       const {editor} = testEnv;
223
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>',
229         );
230       });
231     });
232
233     test('LinkNode.updateDOM()', async () => {
234       const {editor} = testEnv;
235
236       await editor.update(() => {
237         const linkNode = new LinkNode('https://example.com/foo');
238
239         const domElement = linkNode.createDOM(editorConfig);
240
241         expect(linkNode.createDOM(editorConfig).outerHTML).toBe(
242           '<a href="https://example.com/foo" class="my-link-class"></a>',
243         );
244
245         const newLinkNode = new LinkNode('https://example.com/bar');
246         const result = newLinkNode.updateDOM(
247           linkNode,
248           domElement,
249           editorConfig,
250         );
251
252         expect(result).toBe(false);
253         expect(domElement.outerHTML).toBe(
254           '<a href="https://example.com/bar" class="my-link-class"></a>',
255         );
256       });
257     });
258
259     test('LinkNode.updateDOM() with target, rel and title', async () => {
260       const {editor} = testEnv;
261
262       await editor.update(() => {
263         const linkNode = new LinkNode('https://example.com/foo', {
264           rel: 'noopener noreferrer',
265           target: '_blank',
266           title: 'Hello world',
267         });
268
269         const domElement = linkNode.createDOM(editorConfig);
270
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>',
273         );
274
275         const newLinkNode = new LinkNode('https://example.com/bar', {
276           rel: 'noopener',
277           target: '_self',
278           title: 'World hello',
279         });
280         const result = newLinkNode.updateDOM(
281           linkNode,
282           domElement,
283           editorConfig,
284         );
285
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>',
289         );
290       });
291     });
292
293     test('LinkNode.updateDOM() with undefined target, undefined rel and undefined title', async () => {
294       const {editor} = testEnv;
295
296       await editor.update(() => {
297         const linkNode = new LinkNode('https://example.com/foo', {
298           rel: 'noopener noreferrer',
299           target: '_blank',
300           title: 'Hello world',
301         });
302
303         const domElement = linkNode.createDOM(editorConfig);
304
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>',
307         );
308
309         const newLinkNode = new LinkNode('https://example.com/bar');
310         const result = newLinkNode.updateDOM(
311           linkNode,
312           domElement,
313           editorConfig,
314         );
315
316         expect(result).toBe(false);
317         expect(domElement.outerHTML).toBe(
318           '<a href="https://example.com/bar" class="my-link-class"></a>',
319         );
320       });
321     });
322
323     test('LinkNode.canInsertTextBefore()', async () => {
324       const {editor} = testEnv;
325
326       await editor.update(() => {
327         const linkNode = new LinkNode('https://example.com/foo');
328
329         expect(linkNode.canInsertTextBefore()).toBe(false);
330       });
331     });
332
333     test('LinkNode.canInsertTextAfter()', async () => {
334       const {editor} = testEnv;
335
336       await editor.update(() => {
337         const linkNode = new LinkNode('https://example.com/foo');
338
339         expect(linkNode.canInsertTextAfter()).toBe(false);
340       });
341     });
342
343     test('$createLinkNode()', async () => {
344       const {editor} = testEnv;
345
346       await editor.update(() => {
347         const linkNode = new LinkNode('https://example.com/foo');
348
349         const createdLinkNode = $createLinkNode('https://example.com/foo');
350
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);
355       });
356     });
357
358     test('$createLinkNode() with target, rel and title', async () => {
359       const {editor} = testEnv;
360
361       await editor.update(() => {
362         const linkNode = new LinkNode('https://example.com/foo', {
363           rel: 'noopener noreferrer',
364           target: '_blank',
365           title: 'Hello world',
366         });
367
368         const createdLinkNode = $createLinkNode('https://example.com/foo', {
369           rel: 'noopener noreferrer',
370           target: '_blank',
371           title: 'Hello world',
372         });
373
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);
381       });
382     });
383
384     test('$isLinkNode()', async () => {
385       const {editor} = testEnv;
386
387       await editor.update(() => {
388         const linkNode = new LinkNode('');
389
390         expect($isLinkNode(linkNode)).toBe(true);
391       });
392     });
393
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);
400       });
401
402       await editor.update(() => {
403         $selectAll();
404         $toggleLink('https://lexical.dev/', {title: 'Lexical Website'});
405       });
406
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');
411     });
412   });
413 });