BookStackApp/BookStack

View on GitHub
resources/js/wysiwyg/lexical/link/__tests__/unit/LexicalAutoLinkNode.test.ts

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

import {
  $createAutoLinkNode,
  $isAutoLinkNode,
  $toggleLink,
  AutoLinkNode,
  SerializedAutoLinkNode,
} from '@lexical/link';
import {
  $getRoot,
  $selectAll,
  ParagraphNode,
  SerializedParagraphNode,
  TextNode,
} from 'lexical';
import {initializeUnitTest} from 'lexical/__tests__/utils';

const editorConfig = Object.freeze({
  namespace: '',
  theme: {
    link: 'my-autolink-class',
    text: {
      bold: 'my-bold-class',
      code: 'my-code-class',
      hashtag: 'my-hashtag-class',
      italic: 'my-italic-class',
      strikethrough: 'my-strikethrough-class',
      underline: 'my-underline-class',
      underlineStrikethrough: 'my-underline-strikethrough-class',
    },
  },
});

describe('LexicalAutoAutoLinkNode tests', () => {
  initializeUnitTest((testEnv) => {
    test('AutoAutoLinkNode.constructor', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const actutoLinkNode = new AutoLinkNode('/');

        expect(actutoLinkNode.__type).toBe('autolink');
        expect(actutoLinkNode.__url).toBe('/');
        expect(actutoLinkNode.__isUnlinked).toBe(false);
      });

      expect(() => new AutoLinkNode('')).toThrow();
    });

    test('AutoAutoLinkNode.constructor with isUnlinked param set to true', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const actutoLinkNode = new AutoLinkNode('/', {
          isUnlinked: true,
        });

        expect(actutoLinkNode.__type).toBe('autolink');
        expect(actutoLinkNode.__url).toBe('/');
        expect(actutoLinkNode.__isUnlinked).toBe(true);
      });

      expect(() => new AutoLinkNode('')).toThrow();
    });

    ///

    test('LineBreakNode.clone()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('/');

        const clone = AutoLinkNode.clone(autoLinkNode);

        expect(clone).not.toBe(autoLinkNode);
        expect(clone).toStrictEqual(autoLinkNode);
      });
    });

    test('AutoLinkNode.getURL()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo');

        expect(autoLinkNode.getURL()).toBe('https://example.com/foo');
      });
    });

    test('AutoLinkNode.setURL()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo');

        expect(autoLinkNode.getURL()).toBe('https://example.com/foo');

        autoLinkNode.setURL('https://example.com/bar');

        expect(autoLinkNode.getURL()).toBe('https://example.com/bar');
      });
    });

    test('AutoLinkNode.getTarget()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          target: '_blank',
        });

        expect(autoLinkNode.getTarget()).toBe('_blank');
      });
    });

    test('AutoLinkNode.setTarget()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          target: '_blank',
        });

        expect(autoLinkNode.getTarget()).toBe('_blank');

        autoLinkNode.setTarget('_self');

        expect(autoLinkNode.getTarget()).toBe('_self');
      });
    });

    test('AutoLinkNode.getRel()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          rel: 'noopener noreferrer',
          target: '_blank',
        });

        expect(autoLinkNode.getRel()).toBe('noopener noreferrer');
      });
    });

    test('AutoLinkNode.setRel()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          rel: 'noopener',
          target: '_blank',
        });

        expect(autoLinkNode.getRel()).toBe('noopener');

        autoLinkNode.setRel('noopener noreferrer');

        expect(autoLinkNode.getRel()).toBe('noopener noreferrer');
      });
    });

    test('AutoLinkNode.getTitle()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          title: 'Hello world',
        });

        expect(autoLinkNode.getTitle()).toBe('Hello world');
      });
    });

    test('AutoLinkNode.setTitle()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          title: 'Hello world',
        });

        expect(autoLinkNode.getTitle()).toBe('Hello world');

        autoLinkNode.setTitle('World hello');

        expect(autoLinkNode.getTitle()).toBe('World hello');
      });
    });

    test('AutoLinkNode.getIsUnlinked()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('/', {
          isUnlinked: true,
        });
        expect(autoLinkNode.getIsUnlinked()).toBe(true);
      });
    });

    test('AutoLinkNode.setIsUnlinked()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('/');
        expect(autoLinkNode.getIsUnlinked()).toBe(false);
        autoLinkNode.setIsUnlinked(true);
        expect(autoLinkNode.getIsUnlinked()).toBe(true);
      });
    });

    test('AutoLinkNode.createDOM()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo');

        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
          '<a href="https://example.com/foo" class="my-autolink-class"></a>',
        );
        expect(
          autoLinkNode.createDOM({
            namespace: '',
            theme: {},
          }).outerHTML,
        ).toBe('<a href="https://example.com/foo"></a>');
      });
    });

    test('AutoLinkNode.createDOM() for unlinked', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          isUnlinked: true,
        });

        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
          `<span>${autoLinkNode.getTextContent()}</span>`,
        );
      });
    });

    test('AutoLinkNode.createDOM() with target, rel and title', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          rel: 'noopener noreferrer',
          target: '_blank',
          title: 'Hello world',
        });

        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
          '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-autolink-class"></a>',
        );
        expect(
          autoLinkNode.createDOM({
            namespace: '',
            theme: {},
          }).outerHTML,
        ).toBe(
          '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world"></a>',
        );
      });
    });

    test('AutoLinkNode.createDOM() sanitizes javascript: URLs', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        // eslint-disable-next-line no-script-url
        const autoLinkNode = new AutoLinkNode('javascript:alert(0)');
        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
          '<a href="about:blank" class="my-autolink-class"></a>',
        );
      });
    });

    test('AutoLinkNode.updateDOM()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo');

        const domElement = autoLinkNode.createDOM(editorConfig);

        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
          '<a href="https://example.com/foo" class="my-autolink-class"></a>',
        );

        const newAutoLinkNode = new AutoLinkNode('https://example.com/bar');
        const result = newAutoLinkNode.updateDOM(
          autoLinkNode,
          domElement,
          editorConfig,
        );

        expect(result).toBe(false);
        expect(domElement.outerHTML).toBe(
          '<a href="https://example.com/bar" class="my-autolink-class"></a>',
        );
      });
    });

    test('AutoLinkNode.updateDOM() with target, rel and title', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          rel: 'noopener noreferrer',
          target: '_blank',
          title: 'Hello world',
        });

        const domElement = autoLinkNode.createDOM(editorConfig);

        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
          '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-autolink-class"></a>',
        );

        const newAutoLinkNode = new AutoLinkNode('https://example.com/bar', {
          rel: 'noopener',
          target: '_self',
          title: 'World hello',
        });
        const result = newAutoLinkNode.updateDOM(
          autoLinkNode,
          domElement,
          editorConfig,
        );

        expect(result).toBe(false);
        expect(domElement.outerHTML).toBe(
          '<a href="https://example.com/bar" target="_self" rel="noopener" title="World hello" class="my-autolink-class"></a>',
        );
      });
    });

    test('AutoLinkNode.updateDOM() with undefined target, undefined rel and undefined title', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          rel: 'noopener noreferrer',
          target: '_blank',
          title: 'Hello world',
        });

        const domElement = autoLinkNode.createDOM(editorConfig);

        expect(autoLinkNode.createDOM(editorConfig).outerHTML).toBe(
          '<a href="https://example.com/foo" target="_blank" rel="noopener noreferrer" title="Hello world" class="my-autolink-class"></a>',
        );

        const newNode = new AutoLinkNode('https://example.com/bar');
        const result = newNode.updateDOM(
          autoLinkNode,
          domElement,
          editorConfig,
        );

        expect(result).toBe(false);
        expect(domElement.outerHTML).toBe(
          '<a href="https://example.com/bar" class="my-autolink-class"></a>',
        );
      });
    });

    test('AutoLinkNode.updateDOM() with isUnlinked "true"', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          isUnlinked: false,
        });

        const domElement = autoLinkNode.createDOM(editorConfig);
        expect(domElement.outerHTML).toBe(
          '<a href="https://example.com/foo" class="my-autolink-class"></a>',
        );

        const newAutoLinkNode = new AutoLinkNode('https://example.com/bar', {
          isUnlinked: true,
        });
        const newDomElement = newAutoLinkNode.createDOM(editorConfig);
        expect(newDomElement.outerHTML).toBe(
          `<span>${newAutoLinkNode.getTextContent()}</span>`,
        );

        const result = newAutoLinkNode.updateDOM(
          autoLinkNode,
          domElement,
          editorConfig,
        );
        expect(result).toBe(true);
      });
    });

    test('AutoLinkNode.canInsertTextBefore()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo');

        expect(autoLinkNode.canInsertTextBefore()).toBe(false);
      });
    });

    test('AutoLinkNode.canInsertTextAfter()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo');
        expect(autoLinkNode.canInsertTextAfter()).toBe(false);
      });
    });

    test('$createAutoLinkNode()', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo');
        const createdAutoLinkNode = $createAutoLinkNode(
          'https://example.com/foo',
        );

        expect(autoLinkNode.__type).toEqual(createdAutoLinkNode.__type);
        expect(autoLinkNode.__parent).toEqual(createdAutoLinkNode.__parent);
        expect(autoLinkNode.__url).toEqual(createdAutoLinkNode.__url);
        expect(autoLinkNode.__isUnlinked).toEqual(
          createdAutoLinkNode.__isUnlinked,
        );
        expect(autoLinkNode.__key).not.toEqual(createdAutoLinkNode.__key);
      });
    });

    test('$createAutoLinkNode() with target, rel, isUnlinked and title', async () => {
      const {editor} = testEnv;

      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('https://example.com/foo', {
          rel: 'noopener noreferrer',
          target: '_blank',
          title: 'Hello world',
        });

        const createdAutoLinkNode = $createAutoLinkNode(
          'https://example.com/foo',
          {
            isUnlinked: true,
            rel: 'noopener noreferrer',
            target: '_blank',
            title: 'Hello world',
          },
        );

        expect(autoLinkNode.__type).toEqual(createdAutoLinkNode.__type);
        expect(autoLinkNode.__parent).toEqual(createdAutoLinkNode.__parent);
        expect(autoLinkNode.__url).toEqual(createdAutoLinkNode.__url);
        expect(autoLinkNode.__target).toEqual(createdAutoLinkNode.__target);
        expect(autoLinkNode.__rel).toEqual(createdAutoLinkNode.__rel);
        expect(autoLinkNode.__title).toEqual(createdAutoLinkNode.__title);
        expect(autoLinkNode.__key).not.toEqual(createdAutoLinkNode.__key);
        expect(autoLinkNode.__isUnlinked).not.toEqual(
          createdAutoLinkNode.__isUnlinked,
        );
      });
    });

    test('$isAutoLinkNode()', async () => {
      const {editor} = testEnv;
      await editor.update(() => {
        const autoLinkNode = new AutoLinkNode('');
        expect($isAutoLinkNode(autoLinkNode)).toBe(true);
      });
    });

    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'));
        $getRoot().append(p);
      });

      await editor.update(() => {
        $selectAll();
        $toggleLink('https://lexical.dev/', {title: 'Lexical Website'});
      });

      const paragraph = editor!.getEditorState().toJSON().root
        .children[0] as SerializedParagraphNode;
      const link = paragraph.children[0] as SerializedAutoLinkNode;
      expect(link.title).toBe('Lexical Website');
    });
  });
});