]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts
c4dedd47d137dc8b93b3466c0c162754b2e35bfd
[bookstack] / resources / js / wysiwyg / lexical / headless / __tests__ / unit / LexicalHeadlessEditor.test.ts
1 /**
2  * @jest-environment node
3  */
4
5 // Jest environment should be at the very top of the file. overriding environment for this test
6 // to ensure that headless editor works within node environment
7 // https://jestjs.io/docs/configuration#testenvironment-string
8
9 /* eslint-disable header/header */
10
11 /**
12  * Copyright (c) Meta Platforms, Inc. and affiliates.
13  *
14  * This source code is licensed under the MIT license found in the
15  * LICENSE file in the root directory of this source tree.
16  *
17  */
18
19 import type {EditorState, LexicalEditor, RangeSelection} from 'lexical';
20
21 import {$generateHtmlFromNodes} from '@lexical/html';
22 import {JSDOM} from 'jsdom';
23 import {
24   $createParagraphNode,
25   $createTextNode,
26   $getRoot,
27   $getSelection,
28   COMMAND_PRIORITY_NORMAL,
29   CONTROLLED_TEXT_INSERTION_COMMAND,
30   ParagraphNode,
31 } from 'lexical';
32
33 import {createHeadlessEditor} from '../..';
34
35 describe('LexicalHeadlessEditor', () => {
36   let editor: LexicalEditor;
37
38   async function update(updateFn: () => void) {
39     editor.update(updateFn);
40     await Promise.resolve();
41   }
42
43   function assertEditorState(
44     editorState: EditorState,
45     nodes: Record<string, unknown>[],
46   ) {
47     const nodesFromState = Array.from(editorState._nodeMap.values());
48     expect(nodesFromState).toEqual(
49       nodes.map((node) => expect.objectContaining(node)),
50     );
51   }
52
53   beforeEach(() => {
54     editor = createHeadlessEditor({
55       namespace: '',
56       onError: (error) => {
57         throw error;
58       },
59     });
60   });
61
62   it('should be headless environment', async () => {
63     expect(typeof window === 'undefined').toBe(true);
64     expect(typeof document === 'undefined').toBe(true);
65     expect(typeof navigator === 'undefined').toBe(true);
66   });
67
68   it('can update editor', async () => {
69     await update(() => {
70       $getRoot().append(
71         $createParagraphNode().append(
72           $createTextNode('Hello').toggleFormat('bold'),
73           $createTextNode('world'),
74         ),
75       );
76     });
77
78     assertEditorState(editor.getEditorState(), [
79       {
80         __key: 'root',
81       },
82       {
83         __type: 'paragraph',
84       },
85       {
86         __format: 1,
87         __text: 'Hello',
88         __type: 'text',
89       },
90       {
91         __format: 0,
92         __text: 'world',
93         __type: 'text',
94       },
95     ]);
96   });
97
98   it('can set editor state from json', async () => {
99     editor.setEditorState(
100       editor.parseEditorState(
101         '{"root":{"children":[{"children":[{"detail":0,"format":1,"mode":"normal","style":"","text":"Hello","type":"text","version":1},{"detail":0,"format":0,"mode":"normal","style":"","text":"world","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}',
102       ),
103     );
104
105     assertEditorState(editor.getEditorState(), [
106       {
107         __key: 'root',
108       },
109       {
110         __type: 'paragraph',
111       },
112       {
113         __format: 1,
114         __text: 'Hello',
115         __type: 'text',
116       },
117       {
118         __format: 0,
119         __text: 'world',
120         __type: 'text',
121       },
122     ]);
123   });
124
125   it('can register listeners', async () => {
126     const onUpdate = jest.fn();
127     const onCommand = jest.fn();
128     const onTransform = jest.fn();
129     const onTextContent = jest.fn();
130
131     editor.registerUpdateListener(onUpdate);
132     editor.registerCommand(
133       CONTROLLED_TEXT_INSERTION_COMMAND,
134       onCommand,
135       COMMAND_PRIORITY_NORMAL,
136     );
137     editor.registerNodeTransform(ParagraphNode, onTransform);
138     editor.registerTextContentListener(onTextContent);
139
140     await update(() => {
141       $getRoot().append(
142         $createParagraphNode().append(
143           $createTextNode('Hello').toggleFormat('bold'),
144           $createTextNode('world'),
145         ),
146       );
147       editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, 'foo');
148     });
149
150     expect(onUpdate).toBeCalled();
151     expect(onCommand).toBeCalledWith('foo', expect.anything());
152     expect(onTransform).toBeCalledWith(
153       expect.objectContaining({__type: 'paragraph'}),
154     );
155     expect(onTextContent).toBeCalledWith('Helloworld');
156   });
157
158   it('can preserve selection for pending editor state (within update loop)', async () => {
159     await update(() => {
160       const textNode = $createTextNode('Hello world');
161       $getRoot().append($createParagraphNode().append(textNode));
162       textNode.select(1, 2);
163     });
164
165     await update(() => {
166       const selection = $getSelection() as RangeSelection;
167       expect(selection.anchor).toEqual(
168         expect.objectContaining({offset: 1, type: 'text'}),
169       );
170       expect(selection.focus).toEqual(
171         expect.objectContaining({offset: 2, type: 'text'}),
172       );
173     });
174   });
175
176   function setupDom() {
177     const jsdom = new JSDOM();
178
179     const _window = global.window;
180     const _document = global.document;
181
182     // @ts-expect-error
183     global.window = jsdom.window;
184     global.document = jsdom.window.document;
185
186     return () => {
187       global.window = _window;
188       global.document = _document;
189     };
190   }
191
192   it('can generate html from the nodes when dom is set', async () => {
193     editor.setEditorState(
194       // "hello world"
195       editor.parseEditorState(
196         `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"hello world","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`,
197       ),
198     );
199
200     const cleanup = setupDom();
201
202     const html = editor
203       .getEditorState()
204       .read(() => $generateHtmlFromNodes(editor, null));
205
206     cleanup();
207
208     expect(html).toBe(
209       '<p>hello world</p>',
210     );
211   });
212 });