2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import {createHeadlessEditor} from '@lexical/headless';
10 import {AutoLinkNode, LinkNode} from '@lexical/link';
11 import {ListItemNode, ListNode} from '@lexical/list';
13 import {HeadingNode, QuoteNode} from '@lexical/rich-text';
14 import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
27 SerializedElementNode,
28 SerializedLexicalNode,
32 import * as ReactTestUtils from 'lexical/shared/react-test-utils';
37 LexicalNodeReplacement,
38 } from '../../LexicalEditor';
39 import {resetRandomKey} from '../../LexicalUtils';
43 readonly container: HTMLDivElement;
44 readonly editor: LexicalEditor;
45 readonly outerHTML: string;
46 readonly innerHTML: string;
49 export function initializeUnitTest(
50 runTests: (testEnv: TestEnv) => void,
51 editorConfig: CreateEditorArgs = {namespace: 'test', theme: {}},
54 _container: null as HTMLDivElement | null,
55 _editor: null as LexicalEditor | null,
57 if (!this._container) {
58 throw new Error('testEnv.container not initialized.');
60 return this._container;
62 set container(container) {
63 this._container = container;
67 throw new Error('testEnv.editor not initialized.');
72 this._editor = editor;
75 return (this.container.firstChild as HTMLElement).innerHTML;
78 return this.container.innerHTML;
81 this._container = null;
86 beforeEach(async () => {
89 testEnv.container = document.createElement('div');
90 document.body.appendChild(testEnv.container);
92 const useLexicalEditor = (
93 rootElementRef: React.RefObject<HTMLDivElement>,
95 const lexicalEditor = React.useMemo(() => {
96 const lexical = createTestEditor(editorConfig);
100 React.useEffect(() => {
101 const rootElement = rootElementRef.current;
102 lexicalEditor.setRootElement(rootElement);
103 }, [rootElementRef, lexicalEditor]);
104 return lexicalEditor;
107 const Editor = () => {
108 testEnv.editor = useLexicalEditor(ref);
109 const context = createLexicalComposerContext(
111 editorConfig?.theme ?? {},
114 <LexicalComposerContext.Provider value={[testEnv.editor, context]}>
115 <div ref={ref} contentEditable={true} />
117 </LexicalComposerContext.Provider>
121 ReactTestUtils.act(() => {
122 createRoot(testEnv.container).render(<Editor />);
127 document.body.removeChild(testEnv.container);
134 export function initializeClipboard() {
135 Object.defineProperty(window, 'DragEvent', {
136 value: class DragEvent {},
138 Object.defineProperty(window, 'ClipboardEvent', {
139 value: class ClipboardEvent {},
143 export type SerializedTestElementNode = SerializedElementNode;
145 export class TestElementNode extends ElementNode {
146 static getType(): string {
150 static clone(node: TestElementNode) {
151 return new TestElementNode(node.__key);
155 serializedNode: SerializedTestElementNode,
156 ): TestInlineElementNode {
157 const node = $createTestInlineElementNode();
158 node.setFormat(serializedNode.format);
159 node.setIndent(serializedNode.indent);
160 node.setDirection(serializedNode.direction);
164 exportJSON(): SerializedTestElementNode {
166 ...super.exportJSON(),
173 return document.createElement('div');
181 export function $createTestElementNode(): TestElementNode {
182 return new TestElementNode();
185 type SerializedTestTextNode = SerializedTextNode;
187 export class TestTextNode extends TextNode {
192 static clone(node: TestTextNode): TestTextNode {
193 return new TestTextNode(node.__text, node.__key);
196 static importJSON(serializedNode: SerializedTestTextNode): TestTextNode {
197 return new TestTextNode(serializedNode.text);
200 exportJSON(): SerializedTestTextNode {
202 ...super.exportJSON(),
209 export type SerializedTestInlineElementNode = SerializedElementNode;
211 export class TestInlineElementNode extends ElementNode {
212 static getType(): string {
213 return 'test_inline_block';
216 static clone(node: TestInlineElementNode) {
217 return new TestInlineElementNode(node.__key);
221 serializedNode: SerializedTestInlineElementNode,
222 ): TestInlineElementNode {
223 const node = $createTestInlineElementNode();
224 node.setFormat(serializedNode.format);
225 node.setIndent(serializedNode.indent);
226 node.setDirection(serializedNode.direction);
230 exportJSON(): SerializedTestInlineElementNode {
232 ...super.exportJSON(),
233 type: 'test_inline_block',
239 return document.createElement('a');
251 export function $createTestInlineElementNode(): TestInlineElementNode {
252 return new TestInlineElementNode();
255 export type SerializedTestShadowRootNode = SerializedElementNode;
257 export class TestShadowRootNode extends ElementNode {
258 static getType(): string {
259 return 'test_shadow_root';
262 static clone(node: TestShadowRootNode) {
263 return new TestElementNode(node.__key);
267 serializedNode: SerializedTestShadowRootNode,
268 ): TestShadowRootNode {
269 const node = $createTestShadowRootNode();
270 node.setFormat(serializedNode.format);
271 node.setIndent(serializedNode.indent);
272 node.setDirection(serializedNode.direction);
276 exportJSON(): SerializedTestShadowRootNode {
278 ...super.exportJSON(),
285 return document.createElement('div');
297 export function $createTestShadowRootNode(): TestShadowRootNode {
298 return new TestShadowRootNode();
301 export type SerializedTestSegmentedNode = SerializedTextNode;
303 export class TestSegmentedNode extends TextNode {
304 static getType(): string {
305 return 'test_segmented';
308 static clone(node: TestSegmentedNode): TestSegmentedNode {
309 return new TestSegmentedNode(node.__text, node.__key);
313 serializedNode: SerializedTestSegmentedNode,
314 ): TestSegmentedNode {
315 const node = $createTestSegmentedNode(serializedNode.text);
316 node.setFormat(serializedNode.format);
317 node.setDetail(serializedNode.detail);
318 node.setMode(serializedNode.mode);
319 node.setStyle(serializedNode.style);
323 exportJSON(): SerializedTestSegmentedNode {
325 ...super.exportJSON(),
326 type: 'test_segmented',
332 export function $createTestSegmentedNode(text: string): TestSegmentedNode {
333 return new TestSegmentedNode(text).setMode('segmented');
336 export type SerializedTestExcludeFromCopyElementNode = SerializedElementNode;
338 export class TestExcludeFromCopyElementNode extends ElementNode {
339 static getType(): string {
340 return 'test_exclude_from_copy_block';
343 static clone(node: TestExcludeFromCopyElementNode) {
344 return new TestExcludeFromCopyElementNode(node.__key);
348 serializedNode: SerializedTestExcludeFromCopyElementNode,
349 ): TestExcludeFromCopyElementNode {
350 const node = $createTestExcludeFromCopyElementNode();
351 node.setFormat(serializedNode.format);
352 node.setIndent(serializedNode.indent);
353 node.setDirection(serializedNode.direction);
357 exportJSON(): SerializedTestExcludeFromCopyElementNode {
359 ...super.exportJSON(),
360 type: 'test_exclude_from_copy_block',
366 return document.createElement('div');
378 export function $createTestExcludeFromCopyElementNode(): TestExcludeFromCopyElementNode {
379 return new TestExcludeFromCopyElementNode();
382 export type SerializedTestDecoratorNode = SerializedLexicalNode;
384 export class TestDecoratorNode extends DecoratorNode<JSX.Element> {
385 static getType(): string {
386 return 'test_decorator';
389 static clone(node: TestDecoratorNode) {
390 return new TestDecoratorNode(node.__key);
394 serializedNode: SerializedTestDecoratorNode,
395 ): TestDecoratorNode {
396 return $createTestDecoratorNode();
399 exportJSON(): SerializedTestDecoratorNode {
401 ...super.exportJSON(),
402 type: 'test_decorator',
409 'test-decorator': (domNode: HTMLElement) => {
411 conversion: () => ({node: $createTestDecoratorNode()}),
419 element: document.createElement('test-decorator'),
424 return 'Hello world';
428 return document.createElement('span');
436 return <Decorator text={'Hello world'} />;
440 function Decorator({text}: {text: string}): JSX.Element {
441 return <span>{text}</span>;
444 export function $createTestDecoratorNode(): TestDecoratorNode {
445 return new TestDecoratorNode();
448 const DEFAULT_NODES: NonNullable<InitialConfigType['nodes']> = [
464 TestExcludeFromCopyElementNode,
466 TestInlineElementNode,
471 export function createTestEditor(
474 editorState?: EditorState;
475 theme?: EditorThemeClasses;
476 parentEditor?: LexicalEditor;
477 nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;
478 onError?: (error: Error) => void;
479 disableEvents?: boolean;
484 const customNodes = config.nodes || [];
485 const editor = createEditor({
486 namespace: config.namespace,
491 nodes: DEFAULT_NODES.concat(customNodes),
496 export function createTestHeadlessEditor(
497 editorState?: EditorState,
499 return createHeadlessEditor({
501 onError: (error) => {
507 export function $assertRangeSelection(selection: unknown): RangeSelection {
508 if (!$isRangeSelection(selection)) {
509 throw new Error(`Expected RangeSelection, got ${selection}`);
514 export function invariant(cond?: boolean, message?: string): asserts cond {
518 throw new Error(`Invariant: ${message}`);
521 export class ClipboardDataMock {
522 getData: jest.Mock<string, [string]>;
523 setData: jest.Mock<void, [string, string]>;
526 this.getData = jest.fn();
527 this.setData = jest.fn();
531 export class DataTransferMock implements DataTransfer {
532 _data: Map<string, string> = new Map();
533 get dropEffect(): DataTransfer['dropEffect'] {
534 throw new Error('Getter not implemented.');
536 get effectAllowed(): DataTransfer['effectAllowed'] {
537 throw new Error('Getter not implemented.');
539 get files(): FileList {
540 throw new Error('Getter not implemented.');
542 get items(): DataTransferItemList {
543 throw new Error('Getter not implemented.');
545 get types(): ReadonlyArray<string> {
546 return Array.from(this._data.keys());
548 clearData(dataType?: string): void {
551 getData(dataType: string): string {
552 return this._data.get(dataType) || '';
554 setData(dataType: string, data: string): void {
555 this._data.set(dataType, data);
557 setDragImage(image: Element, x: number, y: number): void {
562 export class EventMock implements Event {
563 get bubbles(): boolean {
564 throw new Error('Getter not implemented.');
566 get cancelBubble(): boolean {
567 throw new Error('Gettter not implemented.');
569 get cancelable(): boolean {
570 throw new Error('Gettter not implemented.');
572 get composed(): boolean {
573 throw new Error('Gettter not implemented.');
575 get currentTarget(): EventTarget | null {
576 throw new Error('Gettter not implemented.');
578 get defaultPrevented(): boolean {
579 throw new Error('Gettter not implemented.');
581 get eventPhase(): number {
582 throw new Error('Gettter not implemented.');
584 get isTrusted(): boolean {
585 throw new Error('Gettter not implemented.');
587 get returnValue(): boolean {
588 throw new Error('Gettter not implemented.');
590 get srcElement(): EventTarget | null {
591 throw new Error('Gettter not implemented.');
593 get target(): EventTarget | null {
594 throw new Error('Gettter not implemented.');
596 get timeStamp(): number {
597 throw new Error('Gettter not implemented.');
600 throw new Error('Gettter not implemented.');
602 composedPath(): EventTarget[] {
603 throw new Error('Method not implemented.');
607 bubbles?: boolean | undefined,
608 cancelable?: boolean | undefined,
610 throw new Error('Method not implemented.');
612 stopImmediatePropagation(): void {
615 stopPropagation(): void {
619 CAPTURING_PHASE = 1 as const;
620 AT_TARGET = 2 as const;
621 BUBBLING_PHASE = 3 as const;
627 export class KeyboardEventMock extends EventMock implements KeyboardEvent {
629 get charCode(): number {
630 throw new Error('Getter not implemented.');
633 throw new Error('Getter not implemented.');
636 get isComposing(): boolean {
637 throw new Error('Getter not implemented.');
640 throw new Error('Getter not implemented.');
642 get keyCode(): number {
643 throw new Error('Getter not implemented.');
645 get location(): number {
646 throw new Error('Getter not implemented.');
649 get repeat(): boolean {
650 throw new Error('Getter not implemented.');
653 constructor(type: void | string) {
656 getModifierState(keyArg: string): boolean {
657 throw new Error('Method not implemented.');
661 bubblesArg?: boolean | undefined,
662 cancelableArg?: boolean | undefined,
663 viewArg?: Window | null | undefined,
664 keyArg?: string | undefined,
665 locationArg?: number | undefined,
666 ctrlKey?: boolean | undefined,
667 altKey?: boolean | undefined,
668 shiftKey?: boolean | undefined,
669 metaKey?: boolean | undefined,
671 throw new Error('Method not implemented.');
673 DOM_KEY_LOCATION_STANDARD = 0 as const;
674 DOM_KEY_LOCATION_LEFT = 1 as const;
675 DOM_KEY_LOCATION_RIGHT = 2 as const;
676 DOM_KEY_LOCATION_NUMPAD = 3 as const;
677 get detail(): number {
678 throw new Error('Getter not implemented.');
680 get view(): Window | null {
681 throw new Error('Getter not implemented.');
683 get which(): number {
684 throw new Error('Getter not implemented.');
688 bubblesArg?: boolean | undefined,
689 cancelableArg?: boolean | undefined,
690 viewArg?: Window | null | undefined,
691 detailArg?: number | undefined,
693 throw new Error('Method not implemented.');
697 export function tabKeyboardEvent() {
698 return new KeyboardEventMock('keydown');
701 export function shiftTabKeyboardEvent() {
702 const keyboardEvent = new KeyboardEventMock('keydown');
703 keyboardEvent.shiftKey = true;
704 return keyboardEvent;
707 export function generatePermutations<T>(
709 maxLength = values.length,
711 if (maxLength > values.length) {
712 throw new Error('maxLength over values.length');
714 const result: T[][] = [];
715 const current: T[] = [];
716 const seen = new Set();
717 (function permutationsImpl() {
718 if (current.length > maxLength) {
721 result.push(current.slice());
722 for (let i = 0; i < values.length; i++) {
723 const key = values[i];
737 // This tag function is just used to trigger prettier auto-formatting.
738 // (https://prettier.io/blog/2020/08/24/2.1.0.html#api)
739 export function html(
740 partials: TemplateStringsArray,
744 for (let i = 0; i < partials.length; i++) {
745 output += partials[i];
746 if (i < partials.length - 1) {