]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts
ffcefd7c8d90751b64703e9ecb03aeb34973c2cb
[bookstack] / resources / js / wysiwyg / lexical / link / __tests__ / unit / LexicalAutoLinkNode.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   $createAutoLinkNode,
11   $isAutoLinkNode,
12   $toggleLink,
13   AutoLinkNode,
14   SerializedAutoLinkNode,
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-autolink-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('LexicalAutoAutoLinkNode tests', () => {
42   initializeUnitTest((testEnv) => {
43     test('AutoAutoLinkNode.constructor', async () => {
44       const {editor} = testEnv;
45
46       await editor.update(() => {
47         const actutoLinkNode = new AutoLinkNode('/');
48
49         expect(actutoLinkNode.__type).toBe('autolink');
50         expect(actutoLinkNode.__url).toBe('/');
51         expect(actutoLinkNode.__isUnlinked).toBe(false);
52       });
53
54       expect(() => new AutoLinkNode('')).toThrow();
55     });
56
57     test('AutoAutoLinkNode.constructor with isUnlinked param set to true', async () => {
58       const {editor} = testEnv;
59
60       await editor.update(() => {
61         const actutoLinkNode = new AutoLinkNode('/', {
62           isUnlinked: true,
63         });
64
65         expect(actutoLinkNode.__type).toBe('autolink');
66         expect(actutoLinkNode.__url).toBe('/');
67         expect(actutoLinkNode.__isUnlinked).toBe(true);
68       });
69
70       expect(() => new AutoLinkNode('')).toThrow();
71     });
72
73     ///
74
75     test('LineBreakNode.clone()', async () => {
76       const {editor} = testEnv;
77
78       await editor.update(() => {
79         const autoLinkNode = new AutoLinkNode('/');
80
81         const clone = AutoLinkNode.clone(autoLinkNode);
82
83         expect(clone).not.toBe(autoLinkNode);
84         expect(clone).toStrictEqual(autoLinkNode);
85       });
86     });
87
88     test('AutoLinkNode.getURL()', async () => {
89       const {editor} = testEnv;
90
91       await editor.update(() => {
92         const autoLinkNode = new AutoLinkNode('https://example.com/foo');
93
94         expect(autoLinkNode.getURL()).toBe('https://example.com/foo');
95       });
96     });
97
98     test('AutoLinkNode.setURL()', async () => {
99       const {editor} = testEnv;
100
101       await editor.update(() => {
102         const autoLinkNode = new AutoLinkNode('https://example.com/foo');
103
104         expect(autoLinkNode.getURL()).toBe('https://example.com/foo');
105
106         autoLinkNode.setURL('https://example.com/bar');
107
108         expect(autoLinkNode.getURL()).toBe('https://example.com/bar');
109       });
110     });
111
112     test('AutoLinkNode.getTarget()', async () => {
113       const {editor} = testEnv;
114
115       await editor.update(() => {
116         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
117           target: '_blank',
118         });
119
120         expect(autoLinkNode.getTarget()).toBe('_blank');
121       });
122     });
123
124     test('AutoLinkNode.setTarget()', async () => {
125       const {editor} = testEnv;
126
127       await editor.update(() => {
128         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
129           target: '_blank',
130         });
131
132         expect(autoLinkNode.getTarget()).toBe('_blank');
133
134         autoLinkNode.setTarget('_self');
135
136         expect(autoLinkNode.getTarget()).toBe('_self');
137       });
138     });
139
140     test('AutoLinkNode.getRel()', async () => {
141       const {editor} = testEnv;
142
143       await editor.update(() => {
144         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
145           rel: 'noopener noreferrer',
146           target: '_blank',
147         });
148
149         expect(autoLinkNode.getRel()).toBe('noopener noreferrer');
150       });
151     });
152
153     test('AutoLinkNode.setRel()', async () => {
154       const {editor} = testEnv;
155
156       await editor.update(() => {
157         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
158           rel: 'noopener',
159           target: '_blank',
160         });
161
162         expect(autoLinkNode.getRel()).toBe('noopener');
163
164         autoLinkNode.setRel('noopener noreferrer');
165
166         expect(autoLinkNode.getRel()).toBe('noopener noreferrer');
167       });
168     });
169
170     test('AutoLinkNode.getTitle()', async () => {
171       const {editor} = testEnv;
172
173       await editor.update(() => {
174         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
175           title: 'Hello world',
176         });
177
178         expect(autoLinkNode.getTitle()).toBe('Hello world');
179       });
180     });
181
182     test('AutoLinkNode.setTitle()', async () => {
183       const {editor} = testEnv;
184
185       await editor.update(() => {
186         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
187           title: 'Hello world',
188         });
189
190         expect(autoLinkNode.getTitle()).toBe('Hello world');
191
192         autoLinkNode.setTitle('World hello');
193
194         expect(autoLinkNode.getTitle()).toBe('World hello');
195       });
196     });
197
198     test('AutoLinkNode.getIsUnlinked()', async () => {
199       const {editor} = testEnv;
200
201       await editor.update(() => {
202         const autoLinkNode = new AutoLinkNode('/', {
203           isUnlinked: true,
204         });
205         expect(autoLinkNode.getIsUnlinked()).toBe(true);
206       });
207     });
208
209     test('AutoLinkNode.setIsUnlinked()', async () => {
210       const {editor} = testEnv;
211
212       await editor.update(() => {
213         const autoLinkNode = new AutoLinkNode('/');
214         expect(autoLinkNode.getIsUnlinked()).toBe(false);
215         autoLinkNode.setIsUnlinked(true);
216         expect(autoLinkNode.getIsUnlinked()).toBe(true);
217       });
218     });
219
220     test('AutoLinkNode.createDOM()', async () => {
221       const {editor} = testEnv;
222
223       await editor.update(() => {
224         const autoLinkNode = new AutoLinkNode('https://example.com/foo');
225
226         expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
227           '<a href="https://example.com/foo" class="my-autolink-class"></a>',
228         );
229         expect(
230           autoLinkNode.createDOM({
231             namespace: '',
232             theme: {},
233           }).outerHTML,
234         ).toBe('<a href="https://example.com/foo"></a>');
235       });
236     });
237
238     test('AutoLinkNode.createDOM() for unlinked', async () => {
239       const {editor} = testEnv;
240
241       await editor.update(() => {
242         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
243           isUnlinked: true,
244         });
245
246         expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
247           `<span>${autoLinkNode.getTextContent()}</span>`,
248         );
249       });
250     });
251
252     test('AutoLinkNode.createDOM() with target, rel and title', async () => {
253       const {editor} = testEnv;
254
255       await editor.update(() => {
256         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
257           rel: 'noopener noreferrer',
258           target: '_blank',
259           title: 'Hello world',
260         });
261
262         expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
263           '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-autolink-class"></a>',
264         );
265         expect(
266           autoLinkNode.createDOM({
267             namespace: '',
268             theme: {},
269           }).outerHTML,
270         ).toBe(
271           '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world"></a>',
272         );
273       });
274     });
275
276     test('AutoLinkNode.createDOM() sanitizes javascript: URLs', async () => {
277       const {editor} = testEnv;
278
279       await editor.update(() => {
280         // eslint-disable-next-line no-script-url
281         const autoLinkNode = new AutoLinkNode('javascript:alert(0)');
282         expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
283           '<a href="about:blank" class="my-autolink-class"></a>',
284         );
285       });
286     });
287
288     test('AutoLinkNode.updateDOM()', async () => {
289       const {editor} = testEnv;
290
291       await editor.update(() => {
292         const autoLinkNode = new AutoLinkNode('https://example.com/foo');
293
294         const domElement = autoLinkNode.createDOM(editorConfig);
295
296         expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
297           '<a href="https://example.com/foo" class="my-autolink-class"></a>',
298         );
299
300         const newAutoLinkNode = new AutoLinkNode('https://example.com/bar');
301         const result = newAutoLinkNode.updateDOM(
302           autoLinkNode,
303           domElement,
304           editorConfig,
305         );
306
307         expect(result).toBe(false);
308         expect(domElement.outerHTML).toBe(
309           '<a href="https://example.com/bar" class="my-autolink-class"></a>',
310         );
311       });
312     });
313
314     test('AutoLinkNode.updateDOM() with target, rel and title', async () => {
315       const {editor} = testEnv;
316
317       await editor.update(() => {
318         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
319           rel: 'noopener noreferrer',
320           target: '_blank',
321           title: 'Hello world',
322         });
323
324         const domElement = autoLinkNode.createDOM(editorConfig);
325
326         expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
327           '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-autolink-class"></a>',
328         );
329
330         const newAutoLinkNode = new AutoLinkNode('https://example.com/bar', {
331           rel: 'noopener',
332           target: '_self',
333           title: 'World hello',
334         });
335         const result = newAutoLinkNode.updateDOM(
336           autoLinkNode,
337           domElement,
338           editorConfig,
339         );
340
341         expect(result).toBe(false);
342         expect(domElement.outerHTML).toBe(
343           '<a href="https://example.com/bar" target="_self" rel="noopener" title="World hello" class="my-autolink-class"></a>',
344         );
345       });
346     });
347
348     test('AutoLinkNode.updateDOM() with undefined target, undefined rel and undefined title', async () => {
349       const {editor} = testEnv;
350
351       await editor.update(() => {
352         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
353           rel: 'noopener noreferrer',
354           target: '_blank',
355           title: 'Hello world',
356         });
357
358         const domElement = autoLinkNode.createDOM(editorConfig);
359
360         expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
361           '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-autolink-class"></a>',
362         );
363
364         const newNode = new AutoLinkNode('https://example.com/bar');
365         const result = newNode.updateDOM(
366           autoLinkNode,
367           domElement,
368           editorConfig,
369         );
370
371         expect(result).toBe(false);
372         expect(domElement.outerHTML).toBe(
373           '<a href="https://example.com/bar" class="my-autolink-class"></a>',
374         );
375       });
376     });
377
378     test('AutoLinkNode.updateDOM() with isUnlinked "true"', async () => {
379       const {editor} = testEnv;
380
381       await editor.update(() => {
382         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
383           isUnlinked: false,
384         });
385
386         const domElement = autoLinkNode.createDOM(editorConfig);
387         expect(domElement.outerHTML).toBe(
388           '<a href="https://example.com/foo" class="my-autolink-class"></a>',
389         );
390
391         const newAutoLinkNode = new AutoLinkNode('https://example.com/bar', {
392           isUnlinked: true,
393         });
394         const newDomElement = newAutoLinkNode.createDOM(editorConfig);
395         expect(newDomElement.outerHTML).toBe(
396           `<span>${newAutoLinkNode.getTextContent()}</span>`,
397         );
398
399         const result = newAutoLinkNode.updateDOM(
400           autoLinkNode,
401           domElement,
402           editorConfig,
403         );
404         expect(result).toBe(true);
405       });
406     });
407
408     test('AutoLinkNode.canInsertTextBefore()', async () => {
409       const {editor} = testEnv;
410
411       await editor.update(() => {
412         const autoLinkNode = new AutoLinkNode('https://example.com/foo');
413
414         expect(autoLinkNode.canInsertTextBefore()).toBe(false);
415       });
416     });
417
418     test('AutoLinkNode.canInsertTextAfter()', async () => {
419       const {editor} = testEnv;
420
421       await editor.update(() => {
422         const autoLinkNode = new AutoLinkNode('https://example.com/foo');
423         expect(autoLinkNode.canInsertTextAfter()).toBe(false);
424       });
425     });
426
427     test('$createAutoLinkNode()', async () => {
428       const {editor} = testEnv;
429
430       await editor.update(() => {
431         const autoLinkNode = new AutoLinkNode('https://example.com/foo');
432         const createdAutoLinkNode = $createAutoLinkNode(
433           'https://example.com/foo',
434         );
435
436         expect(autoLinkNode.__type).toEqual(createdAutoLinkNode.__type);
437         expect(autoLinkNode.__parent).toEqual(createdAutoLinkNode.__parent);
438         expect(autoLinkNode.__url).toEqual(createdAutoLinkNode.__url);
439         expect(autoLinkNode.__isUnlinked).toEqual(
440           createdAutoLinkNode.__isUnlinked,
441         );
442         expect(autoLinkNode.__key).not.toEqual(createdAutoLinkNode.__key);
443       });
444     });
445
446     test('$createAutoLinkNode() with target, rel, isUnlinked and title', async () => {
447       const {editor} = testEnv;
448
449       await editor.update(() => {
450         const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
451           rel: 'noopener noreferrer',
452           target: '_blank',
453           title: 'Hello world',
454         });
455
456         const createdAutoLinkNode = $createAutoLinkNode(
457           'https://example.com/foo',
458           {
459             isUnlinked: true,
460             rel: 'noopener noreferrer',
461             target: '_blank',
462             title: 'Hello world',
463           },
464         );
465
466         expect(autoLinkNode.__type).toEqual(createdAutoLinkNode.__type);
467         expect(autoLinkNode.__parent).toEqual(createdAutoLinkNode.__parent);
468         expect(autoLinkNode.__url).toEqual(createdAutoLinkNode.__url);
469         expect(autoLinkNode.__target).toEqual(createdAutoLinkNode.__target);
470         expect(autoLinkNode.__rel).toEqual(createdAutoLinkNode.__rel);
471         expect(autoLinkNode.__title).toEqual(createdAutoLinkNode.__title);
472         expect(autoLinkNode.__key).not.toEqual(createdAutoLinkNode.__key);
473         expect(autoLinkNode.__isUnlinked).not.toEqual(
474           createdAutoLinkNode.__isUnlinked,
475         );
476       });
477     });
478
479     test('$isAutoLinkNode()', async () => {
480       const {editor} = testEnv;
481       await editor.update(() => {
482         const autoLinkNode = new AutoLinkNode('');
483         expect($isAutoLinkNode(autoLinkNode)).toBe(true);
484       });
485     });
486
487     test('$toggleLink applies the title attribute when creating', async () => {
488       const {editor} = testEnv;
489       await editor.update(() => {
490         const p = new ParagraphNode();
491         p.append(new TextNode('Some text'));
492         $getRoot().append(p);
493       });
494
495       await editor.update(() => {
496         $selectAll();
497         $toggleLink('https://lexical.dev/', {title: 'Lexical Website'});
498       });
499
500       const paragraph = editor!.getEditorState().toJSON().root
501         .children[0] as SerializedParagraphNode;
502       const link = paragraph.children[0] as SerializedAutoLinkNode;
503       expect(link.title).toBe('Lexical Website');
504     });
505   });
506 });