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 {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
26 SerializedElementNode,
27 SerializedLexicalNode,
35 LexicalNodeReplacement,
36 } from '../../LexicalEditor';
37 import {resetRandomKey} from '../../LexicalUtils';
38 import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
39 import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
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 editorEl = document.createElement('div');
93 editorEl.setAttribute('contenteditable', 'true');
94 testEnv.container.append(editorEl);
96 const lexicalEditor = createTestEditor(editorConfig);
97 lexicalEditor.setRootElement(editorEl);
98 testEnv.editor = lexicalEditor;
102 document.body.removeChild(testEnv.container);
109 export function initializeClipboard() {
110 Object.defineProperty(window, 'DragEvent', {
111 value: class DragEvent {},
113 Object.defineProperty(window, 'ClipboardEvent', {
114 value: class ClipboardEvent {},
118 export type SerializedTestElementNode = SerializedElementNode;
120 export class TestElementNode extends ElementNode {
121 static getType(): string {
125 static clone(node: TestElementNode) {
126 return new TestElementNode(node.__key);
130 serializedNode: SerializedTestElementNode,
131 ): TestInlineElementNode {
132 const node = $createTestInlineElementNode();
133 node.setDirection(serializedNode.direction);
137 exportJSON(): SerializedTestElementNode {
139 ...super.exportJSON(),
146 return document.createElement('div');
154 export function $createTestElementNode(): TestElementNode {
155 return new TestElementNode();
158 type SerializedTestTextNode = SerializedTextNode;
160 export class TestTextNode extends TextNode {
165 static clone(node: TestTextNode): TestTextNode {
166 return new TestTextNode(node.__text, node.__key);
169 static importJSON(serializedNode: SerializedTestTextNode): TestTextNode {
170 return new TestTextNode(serializedNode.text);
173 exportJSON(): SerializedTestTextNode {
175 ...super.exportJSON(),
182 export type SerializedTestInlineElementNode = SerializedElementNode;
184 export class TestInlineElementNode extends ElementNode {
185 static getType(): string {
186 return 'test_inline_block';
189 static clone(node: TestInlineElementNode) {
190 return new TestInlineElementNode(node.__key);
194 serializedNode: SerializedTestInlineElementNode,
195 ): TestInlineElementNode {
196 const node = $createTestInlineElementNode();
197 node.setDirection(serializedNode.direction);
201 exportJSON(): SerializedTestInlineElementNode {
203 ...super.exportJSON(),
204 type: 'test_inline_block',
210 return document.createElement('a');
222 export function $createTestInlineElementNode(): TestInlineElementNode {
223 return new TestInlineElementNode();
226 export type SerializedTestShadowRootNode = SerializedElementNode;
228 export class TestShadowRootNode extends ElementNode {
229 static getType(): string {
230 return 'test_shadow_root';
233 static clone(node: TestShadowRootNode) {
234 return new TestElementNode(node.__key);
238 serializedNode: SerializedTestShadowRootNode,
239 ): TestShadowRootNode {
240 const node = $createTestShadowRootNode();
241 node.setDirection(serializedNode.direction);
245 exportJSON(): SerializedTestShadowRootNode {
247 ...super.exportJSON(),
254 return document.createElement('div');
266 export function $createTestShadowRootNode(): TestShadowRootNode {
267 return new TestShadowRootNode();
270 export type SerializedTestSegmentedNode = SerializedTextNode;
272 export class TestSegmentedNode extends TextNode {
273 static getType(): string {
274 return 'test_segmented';
277 static clone(node: TestSegmentedNode): TestSegmentedNode {
278 return new TestSegmentedNode(node.__text, node.__key);
282 serializedNode: SerializedTestSegmentedNode,
283 ): TestSegmentedNode {
284 const node = $createTestSegmentedNode(serializedNode.text);
285 node.setFormat(serializedNode.format);
286 node.setDetail(serializedNode.detail);
287 node.setMode(serializedNode.mode);
288 node.setStyle(serializedNode.style);
292 exportJSON(): SerializedTestSegmentedNode {
294 ...super.exportJSON(),
295 type: 'test_segmented',
301 export function $createTestSegmentedNode(text: string): TestSegmentedNode {
302 return new TestSegmentedNode(text).setMode('segmented');
305 export type SerializedTestExcludeFromCopyElementNode = SerializedElementNode;
307 export class TestExcludeFromCopyElementNode extends ElementNode {
308 static getType(): string {
309 return 'test_exclude_from_copy_block';
312 static clone(node: TestExcludeFromCopyElementNode) {
313 return new TestExcludeFromCopyElementNode(node.__key);
317 serializedNode: SerializedTestExcludeFromCopyElementNode,
318 ): TestExcludeFromCopyElementNode {
319 const node = $createTestExcludeFromCopyElementNode();
320 node.setDirection(serializedNode.direction);
324 exportJSON(): SerializedTestExcludeFromCopyElementNode {
326 ...super.exportJSON(),
327 type: 'test_exclude_from_copy_block',
333 return document.createElement('div');
345 export function $createTestExcludeFromCopyElementNode(): TestExcludeFromCopyElementNode {
346 return new TestExcludeFromCopyElementNode();
349 export type SerializedTestDecoratorNode = SerializedLexicalNode;
351 export class TestDecoratorNode extends DecoratorNode<HTMLElement> {
352 static getType(): string {
353 return 'test_decorator';
356 static clone(node: TestDecoratorNode) {
357 return new TestDecoratorNode(node.__key);
361 serializedNode: SerializedTestDecoratorNode,
362 ): TestDecoratorNode {
363 return $createTestDecoratorNode();
366 exportJSON(): SerializedTestDecoratorNode {
368 ...super.exportJSON(),
369 type: 'test_decorator',
376 'test-decorator': (domNode: HTMLElement) => {
378 conversion: () => ({node: $createTestDecoratorNode()}),
386 element: document.createElement('test-decorator'),
391 return 'Hello world';
395 return document.createElement('span');
403 const decorator = document.createElement('span');
404 decorator.textContent = 'Hello world';
409 export function $createTestDecoratorNode(): TestDecoratorNode {
410 return new TestDecoratorNode();
413 const DEFAULT_NODES: NonNullable<ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>> = [
425 TestExcludeFromCopyElementNode,
427 TestInlineElementNode,
432 export function createTestEditor(
435 editorState?: EditorState;
436 theme?: EditorThemeClasses;
437 parentEditor?: LexicalEditor;
438 nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;
439 onError?: (error: Error) => void;
440 disableEvents?: boolean;
445 const customNodes = config.nodes || [];
446 const editor = createEditor({
447 namespace: config.namespace,
452 nodes: DEFAULT_NODES.concat(customNodes),
457 export function createTestHeadlessEditor(
458 editorState?: EditorState,
460 return createHeadlessEditor({
462 onError: (error) => {
468 export function $assertRangeSelection(selection: unknown): RangeSelection {
469 if (!$isRangeSelection(selection)) {
470 throw new Error(`Expected RangeSelection, got ${selection}`);
475 export function invariant(cond?: boolean, message?: string): asserts cond {
479 throw new Error(`Invariant: ${message}`);
482 export class ClipboardDataMock {
483 getData: jest.Mock<string, [string]>;
484 setData: jest.Mock<void, [string, string]>;
487 this.getData = jest.fn();
488 this.setData = jest.fn();
492 export class DataTransferMock implements DataTransfer {
493 _data: Map<string, string> = new Map();
494 get dropEffect(): DataTransfer['dropEffect'] {
495 throw new Error('Getter not implemented.');
497 get effectAllowed(): DataTransfer['effectAllowed'] {
498 throw new Error('Getter not implemented.');
500 get files(): FileList {
501 throw new Error('Getter not implemented.');
503 get items(): DataTransferItemList {
504 throw new Error('Getter not implemented.');
506 get types(): ReadonlyArray<string> {
507 return Array.from(this._data.keys());
509 clearData(dataType?: string): void {
512 getData(dataType: string): string {
513 return this._data.get(dataType) || '';
515 setData(dataType: string, data: string): void {
516 this._data.set(dataType, data);
518 setDragImage(image: Element, x: number, y: number): void {
523 export class EventMock implements Event {
524 get bubbles(): boolean {
525 throw new Error('Getter not implemented.');
527 get cancelBubble(): boolean {
528 throw new Error('Gettter not implemented.');
530 get cancelable(): boolean {
531 throw new Error('Gettter not implemented.');
533 get composed(): boolean {
534 throw new Error('Gettter not implemented.');
536 get currentTarget(): EventTarget | null {
537 throw new Error('Gettter not implemented.');
539 get defaultPrevented(): boolean {
540 throw new Error('Gettter not implemented.');
542 get eventPhase(): number {
543 throw new Error('Gettter not implemented.');
545 get isTrusted(): boolean {
546 throw new Error('Gettter not implemented.');
548 get returnValue(): boolean {
549 throw new Error('Gettter not implemented.');
551 get srcElement(): EventTarget | null {
552 throw new Error('Gettter not implemented.');
554 get target(): EventTarget | null {
555 throw new Error('Gettter not implemented.');
557 get timeStamp(): number {
558 throw new Error('Gettter not implemented.');
561 throw new Error('Gettter not implemented.');
563 composedPath(): EventTarget[] {
564 throw new Error('Method not implemented.');
568 bubbles?: boolean | undefined,
569 cancelable?: boolean | undefined,
571 throw new Error('Method not implemented.');
573 stopImmediatePropagation(): void {
576 stopPropagation(): void {
580 CAPTURING_PHASE = 1 as const;
581 AT_TARGET = 2 as const;
582 BUBBLING_PHASE = 3 as const;
588 export class KeyboardEventMock extends EventMock implements KeyboardEvent {
590 get charCode(): number {
591 throw new Error('Getter not implemented.');
594 throw new Error('Getter not implemented.');
597 get isComposing(): boolean {
598 throw new Error('Getter not implemented.');
601 throw new Error('Getter not implemented.');
603 get keyCode(): number {
604 throw new Error('Getter not implemented.');
606 get location(): number {
607 throw new Error('Getter not implemented.');
610 get repeat(): boolean {
611 throw new Error('Getter not implemented.');
614 constructor(type: void | string) {
617 getModifierState(keyArg: string): boolean {
618 throw new Error('Method not implemented.');
622 bubblesArg?: boolean | undefined,
623 cancelableArg?: boolean | undefined,
624 viewArg?: Window | null | undefined,
625 keyArg?: string | undefined,
626 locationArg?: number | undefined,
627 ctrlKey?: boolean | undefined,
628 altKey?: boolean | undefined,
629 shiftKey?: boolean | undefined,
630 metaKey?: boolean | undefined,
632 throw new Error('Method not implemented.');
634 DOM_KEY_LOCATION_STANDARD = 0 as const;
635 DOM_KEY_LOCATION_LEFT = 1 as const;
636 DOM_KEY_LOCATION_RIGHT = 2 as const;
637 DOM_KEY_LOCATION_NUMPAD = 3 as const;
638 get detail(): number {
639 throw new Error('Getter not implemented.');
641 get view(): Window | null {
642 throw new Error('Getter not implemented.');
644 get which(): number {
645 throw new Error('Getter not implemented.');
649 bubblesArg?: boolean | undefined,
650 cancelableArg?: boolean | undefined,
651 viewArg?: Window | null | undefined,
652 detailArg?: number | undefined,
654 throw new Error('Method not implemented.');
658 export function tabKeyboardEvent() {
659 return new KeyboardEventMock('keydown');
662 export function shiftTabKeyboardEvent() {
663 const keyboardEvent = new KeyboardEventMock('keydown');
664 keyboardEvent.shiftKey = true;
665 return keyboardEvent;
668 export function generatePermutations<T>(
670 maxLength = values.length,
672 if (maxLength > values.length) {
673 throw new Error('maxLength over values.length');
675 const result: T[][] = [];
676 const current: T[] = [];
677 const seen = new Set();
678 (function permutationsImpl() {
679 if (current.length > maxLength) {
682 result.push(current.slice());
683 for (let i = 0; i < values.length; i++) {
684 const key = values[i];
698 // This tag function is just used to trigger prettier auto-formatting.
699 // (https://prettier.io/blog/2020/08/24/2.1.0.html#api)
700 export function html(
701 partials: TemplateStringsArray,
705 for (let i = 0; i < partials.length; i++) {
706 output += partials[i];
707 if (i < partials.length - 1) {
714 export function expectHtmlToBeEqual(expected: string, actual: string): void {
715 expect(formatHtml(expected)).toBe(formatHtml(actual));
718 function formatHtml(s: string): string {
719 return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();