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,
36 LexicalNodeReplacement,
37 } from '../../LexicalEditor';
38 import {resetRandomKey} from '../../LexicalUtils';
42 readonly container: HTMLDivElement;
43 readonly editor: LexicalEditor;
44 readonly outerHTML: string;
45 readonly innerHTML: string;
48 export function initializeUnitTest(
49 runTests: (testEnv: TestEnv) => void,
50 editorConfig: CreateEditorArgs = {namespace: 'test', theme: {}},
53 _container: null as HTMLDivElement | null,
54 _editor: null as LexicalEditor | null,
56 if (!this._container) {
57 throw new Error('testEnv.container not initialized.');
59 return this._container;
61 set container(container) {
62 this._container = container;
66 throw new Error('testEnv.editor not initialized.');
71 this._editor = editor;
74 return (this.container.firstChild as HTMLElement).innerHTML;
77 return this.container.innerHTML;
80 this._container = null;
85 beforeEach(async () => {
88 testEnv.container = document.createElement('div');
89 document.body.appendChild(testEnv.container);
91 const editorEl = document.createElement('div');
92 editorEl.setAttribute('contenteditable', 'true');
93 testEnv.container.append(editorEl);
95 const lexicalEditor = createTestEditor(editorConfig);
96 lexicalEditor.setRootElement(editorEl);
97 testEnv.editor = lexicalEditor;
101 document.body.removeChild(testEnv.container);
108 export function initializeClipboard() {
109 Object.defineProperty(window, 'DragEvent', {
110 value: class DragEvent {},
112 Object.defineProperty(window, 'ClipboardEvent', {
113 value: class ClipboardEvent {},
117 export type SerializedTestElementNode = SerializedElementNode;
119 export class TestElementNode extends ElementNode {
120 static getType(): string {
124 static clone(node: TestElementNode) {
125 return new TestElementNode(node.__key);
129 serializedNode: SerializedTestElementNode,
130 ): TestInlineElementNode {
131 const node = $createTestInlineElementNode();
132 node.setDirection(serializedNode.direction);
136 exportJSON(): SerializedTestElementNode {
138 ...super.exportJSON(),
145 return document.createElement('div');
153 export function $createTestElementNode(): TestElementNode {
154 return new TestElementNode();
157 type SerializedTestTextNode = SerializedTextNode;
159 export class TestTextNode extends TextNode {
164 static clone(node: TestTextNode): TestTextNode {
165 return new TestTextNode(node.__text, node.__key);
168 static importJSON(serializedNode: SerializedTestTextNode): TestTextNode {
169 return new TestTextNode(serializedNode.text);
172 exportJSON(): SerializedTestTextNode {
174 ...super.exportJSON(),
181 export type SerializedTestInlineElementNode = SerializedElementNode;
183 export class TestInlineElementNode extends ElementNode {
184 static getType(): string {
185 return 'test_inline_block';
188 static clone(node: TestInlineElementNode) {
189 return new TestInlineElementNode(node.__key);
193 serializedNode: SerializedTestInlineElementNode,
194 ): TestInlineElementNode {
195 const node = $createTestInlineElementNode();
196 node.setDirection(serializedNode.direction);
200 exportJSON(): SerializedTestInlineElementNode {
202 ...super.exportJSON(),
203 type: 'test_inline_block',
209 return document.createElement('a');
221 export function $createTestInlineElementNode(): TestInlineElementNode {
222 return new TestInlineElementNode();
225 export type SerializedTestShadowRootNode = SerializedElementNode;
227 export class TestShadowRootNode extends ElementNode {
228 static getType(): string {
229 return 'test_shadow_root';
232 static clone(node: TestShadowRootNode) {
233 return new TestElementNode(node.__key);
237 serializedNode: SerializedTestShadowRootNode,
238 ): TestShadowRootNode {
239 const node = $createTestShadowRootNode();
240 node.setDirection(serializedNode.direction);
244 exportJSON(): SerializedTestShadowRootNode {
246 ...super.exportJSON(),
253 return document.createElement('div');
265 export function $createTestShadowRootNode(): TestShadowRootNode {
266 return new TestShadowRootNode();
269 export type SerializedTestSegmentedNode = SerializedTextNode;
271 export class TestSegmentedNode extends TextNode {
272 static getType(): string {
273 return 'test_segmented';
276 static clone(node: TestSegmentedNode): TestSegmentedNode {
277 return new TestSegmentedNode(node.__text, node.__key);
281 serializedNode: SerializedTestSegmentedNode,
282 ): TestSegmentedNode {
283 const node = $createTestSegmentedNode(serializedNode.text);
284 node.setFormat(serializedNode.format);
285 node.setDetail(serializedNode.detail);
286 node.setMode(serializedNode.mode);
287 node.setStyle(serializedNode.style);
291 exportJSON(): SerializedTestSegmentedNode {
293 ...super.exportJSON(),
294 type: 'test_segmented',
300 export function $createTestSegmentedNode(text: string): TestSegmentedNode {
301 return new TestSegmentedNode(text).setMode('segmented');
304 export type SerializedTestExcludeFromCopyElementNode = SerializedElementNode;
306 export class TestExcludeFromCopyElementNode extends ElementNode {
307 static getType(): string {
308 return 'test_exclude_from_copy_block';
311 static clone(node: TestExcludeFromCopyElementNode) {
312 return new TestExcludeFromCopyElementNode(node.__key);
316 serializedNode: SerializedTestExcludeFromCopyElementNode,
317 ): TestExcludeFromCopyElementNode {
318 const node = $createTestExcludeFromCopyElementNode();
319 node.setDirection(serializedNode.direction);
323 exportJSON(): SerializedTestExcludeFromCopyElementNode {
325 ...super.exportJSON(),
326 type: 'test_exclude_from_copy_block',
332 return document.createElement('div');
344 export function $createTestExcludeFromCopyElementNode(): TestExcludeFromCopyElementNode {
345 return new TestExcludeFromCopyElementNode();
348 export type SerializedTestDecoratorNode = SerializedLexicalNode;
350 export class TestDecoratorNode extends DecoratorNode<HTMLElement> {
351 static getType(): string {
352 return 'test_decorator';
355 static clone(node: TestDecoratorNode) {
356 return new TestDecoratorNode(node.__key);
360 serializedNode: SerializedTestDecoratorNode,
361 ): TestDecoratorNode {
362 return $createTestDecoratorNode();
365 exportJSON(): SerializedTestDecoratorNode {
367 ...super.exportJSON(),
368 type: 'test_decorator',
375 'test-decorator': (domNode: HTMLElement) => {
377 conversion: () => ({node: $createTestDecoratorNode()}),
385 element: document.createElement('test-decorator'),
390 return 'Hello world';
394 return document.createElement('span');
402 const decorator = document.createElement('span');
403 decorator.textContent = 'Hello world';
408 export function $createTestDecoratorNode(): TestDecoratorNode {
409 return new TestDecoratorNode();
412 const DEFAULT_NODES: NonNullable<ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>> = [
424 TestExcludeFromCopyElementNode,
426 TestInlineElementNode,
431 export function createTestEditor(
434 editorState?: EditorState;
435 theme?: EditorThemeClasses;
436 parentEditor?: LexicalEditor;
437 nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;
438 onError?: (error: Error) => void;
439 disableEvents?: boolean;
444 const customNodes = config.nodes || [];
445 const editor = createEditor({
446 namespace: config.namespace,
451 nodes: DEFAULT_NODES.concat(customNodes),
456 export function createTestHeadlessEditor(
457 editorState?: EditorState,
459 return createHeadlessEditor({
461 onError: (error) => {
467 export function $assertRangeSelection(selection: unknown): RangeSelection {
468 if (!$isRangeSelection(selection)) {
469 throw new Error(`Expected RangeSelection, got ${selection}`);
474 export function invariant(cond?: boolean, message?: string): asserts cond {
478 throw new Error(`Invariant: ${message}`);
481 export class ClipboardDataMock {
482 getData: jest.Mock<string, [string]>;
483 setData: jest.Mock<void, [string, string]>;
486 this.getData = jest.fn();
487 this.setData = jest.fn();
491 export class DataTransferMock implements DataTransfer {
492 _data: Map<string, string> = new Map();
493 get dropEffect(): DataTransfer['dropEffect'] {
494 throw new Error('Getter not implemented.');
496 get effectAllowed(): DataTransfer['effectAllowed'] {
497 throw new Error('Getter not implemented.');
499 get files(): FileList {
500 throw new Error('Getter not implemented.');
502 get items(): DataTransferItemList {
503 throw new Error('Getter not implemented.');
505 get types(): ReadonlyArray<string> {
506 return Array.from(this._data.keys());
508 clearData(dataType?: string): void {
511 getData(dataType: string): string {
512 return this._data.get(dataType) || '';
514 setData(dataType: string, data: string): void {
515 this._data.set(dataType, data);
517 setDragImage(image: Element, x: number, y: number): void {
522 export class EventMock implements Event {
523 get bubbles(): boolean {
524 throw new Error('Getter not implemented.');
526 get cancelBubble(): boolean {
527 throw new Error('Gettter not implemented.');
529 get cancelable(): boolean {
530 throw new Error('Gettter not implemented.');
532 get composed(): boolean {
533 throw new Error('Gettter not implemented.');
535 get currentTarget(): EventTarget | null {
536 throw new Error('Gettter not implemented.');
538 get defaultPrevented(): boolean {
539 throw new Error('Gettter not implemented.');
541 get eventPhase(): number {
542 throw new Error('Gettter not implemented.');
544 get isTrusted(): boolean {
545 throw new Error('Gettter not implemented.');
547 get returnValue(): boolean {
548 throw new Error('Gettter not implemented.');
550 get srcElement(): EventTarget | null {
551 throw new Error('Gettter not implemented.');
553 get target(): EventTarget | null {
554 throw new Error('Gettter not implemented.');
556 get timeStamp(): number {
557 throw new Error('Gettter not implemented.');
560 throw new Error('Gettter not implemented.');
562 composedPath(): EventTarget[] {
563 throw new Error('Method not implemented.');
567 bubbles?: boolean | undefined,
568 cancelable?: boolean | undefined,
570 throw new Error('Method not implemented.');
572 stopImmediatePropagation(): void {
575 stopPropagation(): void {
579 CAPTURING_PHASE = 1 as const;
580 AT_TARGET = 2 as const;
581 BUBBLING_PHASE = 3 as const;
587 export class KeyboardEventMock extends EventMock implements KeyboardEvent {
589 get charCode(): number {
590 throw new Error('Getter not implemented.');
593 throw new Error('Getter not implemented.');
596 get isComposing(): boolean {
597 throw new Error('Getter not implemented.');
600 throw new Error('Getter not implemented.');
602 get keyCode(): number {
603 throw new Error('Getter not implemented.');
605 get location(): number {
606 throw new Error('Getter not implemented.');
609 get repeat(): boolean {
610 throw new Error('Getter not implemented.');
613 constructor(type: void | string) {
616 getModifierState(keyArg: string): boolean {
617 throw new Error('Method not implemented.');
621 bubblesArg?: boolean | undefined,
622 cancelableArg?: boolean | undefined,
623 viewArg?: Window | null | undefined,
624 keyArg?: string | undefined,
625 locationArg?: number | undefined,
626 ctrlKey?: boolean | undefined,
627 altKey?: boolean | undefined,
628 shiftKey?: boolean | undefined,
629 metaKey?: boolean | undefined,
631 throw new Error('Method not implemented.');
633 DOM_KEY_LOCATION_STANDARD = 0 as const;
634 DOM_KEY_LOCATION_LEFT = 1 as const;
635 DOM_KEY_LOCATION_RIGHT = 2 as const;
636 DOM_KEY_LOCATION_NUMPAD = 3 as const;
637 get detail(): number {
638 throw new Error('Getter not implemented.');
640 get view(): Window | null {
641 throw new Error('Getter not implemented.');
643 get which(): number {
644 throw new Error('Getter not implemented.');
648 bubblesArg?: boolean | undefined,
649 cancelableArg?: boolean | undefined,
650 viewArg?: Window | null | undefined,
651 detailArg?: number | undefined,
653 throw new Error('Method not implemented.');
657 export function tabKeyboardEvent() {
658 return new KeyboardEventMock('keydown');
661 export function shiftTabKeyboardEvent() {
662 const keyboardEvent = new KeyboardEventMock('keydown');
663 keyboardEvent.shiftKey = true;
664 return keyboardEvent;
667 export function generatePermutations<T>(
669 maxLength = values.length,
671 if (maxLength > values.length) {
672 throw new Error('maxLength over values.length');
674 const result: T[][] = [];
675 const current: T[] = [];
676 const seen = new Set();
677 (function permutationsImpl() {
678 if (current.length > maxLength) {
681 result.push(current.slice());
682 for (let i = 0; i < values.length; i++) {
683 const key = values[i];
697 // This tag function is just used to trigger prettier auto-formatting.
698 // (https://prettier.io/blog/2020/08/24/2.1.0.html#api)
699 export function html(
700 partials: TemplateStringsArray,
704 for (let i = 0; i < partials.length; i++) {
705 output += partials[i];
706 if (i < partials.length - 1) {
713 export function expectHtmlToBeEqual(expected: string, actual: string): void {
714 expect(formatHtml(expected)).toBe(formatHtml(actual));
717 function formatHtml(s: string): string {
718 return s.replace(/>\s+</g, '><').replace(/\s*\n\s*/g, ' ').trim();