2 * @jest-environment node
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
9 /* eslint-disable header/header */
12 * Copyright (c) Meta Platforms, Inc. and affiliates.
14 * This source code is licensed under the MIT license found in the
15 * LICENSE file in the root directory of this source tree.
19 import type {EditorState, LexicalEditor, RangeSelection} from 'lexical';
21 import {$generateHtmlFromNodes} from '@lexical/html';
22 import {JSDOM} from 'jsdom';
28 COMMAND_PRIORITY_NORMAL,
29 CONTROLLED_TEXT_INSERTION_COMMAND,
33 import {createHeadlessEditor} from '../..';
35 describe('LexicalHeadlessEditor', () => {
36 let editor: LexicalEditor;
38 async function update(updateFn: () => void) {
39 editor.update(updateFn);
40 await Promise.resolve();
43 function assertEditorState(
44 editorState: EditorState,
45 nodes: Record<string, unknown>[],
47 const nodesFromState = Array.from(editorState._nodeMap.values());
48 expect(nodesFromState).toEqual(
49 nodes.map((node) => expect.objectContaining(node)),
54 editor = createHeadlessEditor({
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);
68 it('can update editor', async () => {
71 $createParagraphNode().append(
72 $createTextNode('Hello').toggleFormat('bold'),
73 $createTextNode('world'),
78 assertEditorState(editor.getEditorState(), [
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}}',
105 assertEditorState(editor.getEditorState(), [
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();
131 editor.registerUpdateListener(onUpdate);
132 editor.registerCommand(
133 CONTROLLED_TEXT_INSERTION_COMMAND,
135 COMMAND_PRIORITY_NORMAL,
137 editor.registerNodeTransform(ParagraphNode, onTransform);
138 editor.registerTextContentListener(onTextContent);
142 $createParagraphNode().append(
143 $createTextNode('Hello').toggleFormat('bold'),
144 $createTextNode('world'),
147 editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, 'foo');
150 expect(onUpdate).toBeCalled();
151 expect(onCommand).toBeCalledWith('foo', expect.anything());
152 expect(onTransform).toBeCalledWith(
153 expect.objectContaining({__type: 'paragraph'}),
155 expect(onTextContent).toBeCalledWith('Helloworld');
158 it('can preserve selection for pending editor state (within update loop)', async () => {
160 const textNode = $createTextNode('Hello world');
161 $getRoot().append($createParagraphNode().append(textNode));
162 textNode.select(1, 2);
166 const selection = $getSelection() as RangeSelection;
167 expect(selection.anchor).toEqual(
168 expect.objectContaining({offset: 1, type: 'text'}),
170 expect(selection.focus).toEqual(
171 expect.objectContaining({offset: 2, type: 'text'}),
176 function setupDom() {
177 const jsdom = new JSDOM();
179 const _window = global.window;
180 const _document = global.document;
183 global.window = jsdom.window;
184 global.document = jsdom.window.document;
187 global.window = _window;
188 global.document = _document;
192 it('can generate html from the nodes when dom is set', async () => {
193 editor.setEditorState(
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}}`,
200 const cleanup = setupDom();
204 .read(() => $generateHtmlFromNodes(editor, null));
209 '<p>hello world</p>',