From f19ce30dfeeae268d579ed7b03483b78cfab3adb Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:17:42 +0200 Subject: [PATCH] chore: bump `@testing-library/react` `12.1.5` -> `16.0.0` (#8322) --- excalidraw-app/tests/collab.test.tsx | 11 +- .../actions/actionElementLock.test.tsx | 1 + .../actions/actionProperties.test.tsx | 23 +- packages/excalidraw/components/App.tsx | 51 +-- .../components/DefaultSidebar.test.tsx | 2 +- .../components/Sidebar/Sidebar.test.tsx | 103 +++--- .../Sidebar/siderbar.test.helpers.tsx | 42 +++ .../components/Stats/stats.test.tsx | 84 +++-- .../dropdownMenu/DropdownMenu.test.tsx | 10 +- .../hoc/withInternalFallback.test.tsx | 1 + packages/excalidraw/element/routing.test.tsx | 8 +- .../excalidraw/element/textWysiwyg.test.tsx | 300 +++++++----------- packages/excalidraw/frame.test.tsx | 75 ++--- packages/excalidraw/package.json | 3 +- packages/excalidraw/tests/App.test.tsx | 1 + .../tests/MermaidToExcalidraw.test.tsx | 17 +- .../MermaidToExcalidraw.test.tsx.snap | 11 +- .../linearElementEditor.test.tsx.snap | 45 +++ .../excalidraw/tests/actionStyles.test.tsx | 1 + packages/excalidraw/tests/align.test.tsx | 59 ++-- packages/excalidraw/tests/appState.test.tsx | 8 +- packages/excalidraw/tests/binding.test.tsx | 11 +- packages/excalidraw/tests/clipboard.test.tsx | 9 +- .../excalidraw/tests/contextmenu.test.tsx | 5 +- packages/excalidraw/tests/dragCreate.test.tsx | 1 + .../excalidraw/tests/elementLocking.test.tsx | 37 +-- packages/excalidraw/tests/excalidraw.test.tsx | 1 + packages/excalidraw/tests/export.test.tsx | 11 +- .../excalidraw/tests/fitToContent.test.tsx | 44 ++- packages/excalidraw/tests/flip.test.tsx | 187 ++++++----- packages/excalidraw/tests/helpers/api.ts | 61 +++- packages/excalidraw/tests/helpers/ui.ts | 41 ++- packages/excalidraw/tests/history.test.tsx | 248 +++++++-------- packages/excalidraw/tests/library.test.tsx | 9 +- .../tests/linearElementEditor.test.tsx | 113 +++---- packages/excalidraw/tests/move.test.tsx | 25 +- .../tests/multiPointCreate.test.tsx | 1 + .../excalidraw/tests/packages/events.test.tsx | 4 +- packages/excalidraw/tests/queries/dom.ts | 2 +- .../excalidraw/tests/regressionTests.test.tsx | 7 +- packages/excalidraw/tests/resize.test.tsx | 15 +- packages/excalidraw/tests/rotate.test.tsx | 1 + .../excalidraw/tests/scene/export.test.ts | 2 +- packages/excalidraw/tests/scroll.test.tsx | 7 +- packages/excalidraw/tests/selection.test.tsx | 13 +- packages/excalidraw/tests/shortcuts.test.tsx | 1 + packages/excalidraw/tests/test-utils.ts | 37 ++- packages/excalidraw/tests/tool.test.tsx | 15 +- packages/excalidraw/tests/viewMode.test.tsx | 9 +- packages/excalidraw/tests/zindex.test.tsx | 83 ++--- setupTests.ts | 16 + yarn.lock | 141 +++----- 52 files changed, 1035 insertions(+), 978 deletions(-) create mode 100644 packages/excalidraw/components/Sidebar/siderbar.test.helpers.tsx diff --git a/excalidraw-app/tests/collab.test.tsx b/excalidraw-app/tests/collab.test.tsx index f12e944e25..4a712f784a 100644 --- a/excalidraw-app/tests/collab.test.tsx +++ b/excalidraw-app/tests/collab.test.tsx @@ -2,7 +2,6 @@ import { vi } from "vitest"; import { act, render, - updateSceneData, waitFor, } from "../../packages/excalidraw/tests/test-utils"; import ExcalidrawApp from "../App"; @@ -88,12 +87,12 @@ describe("collaboration", () => { const rect1 = API.createElement({ ...rect1Props }); const rect2 = API.createElement({ ...rect2Props }); - updateSceneData({ + API.updateScene({ elements: syncInvalidIndices([rect1, rect2]), storeAction: StoreAction.CAPTURE, }); - updateSceneData({ + API.updateScene({ elements: syncInvalidIndices([ rect1, newElementWith(h.elements[1], { isDeleted: true }), @@ -143,7 +142,7 @@ describe("collaboration", () => { }); // simulate force deleting the element remotely - updateSceneData({ + API.updateScene({ elements: syncInvalidIndices([rect1]), storeAction: StoreAction.UPDATE, }); @@ -178,7 +177,7 @@ describe("collaboration", () => { act(() => h.app.actionManager.executeAction(undoAction)); // simulate local update - updateSceneData({ + API.updateScene({ elements: syncInvalidIndices([ h.elements[0], newElementWith(h.elements[1], { x: 100 }), @@ -216,7 +215,7 @@ describe("collaboration", () => { }); // simulate force deleting the element remotely - updateSceneData({ + API.updateScene({ elements: syncInvalidIndices([rect1]), storeAction: StoreAction.UPDATE, }); diff --git a/packages/excalidraw/actions/actionElementLock.test.tsx b/packages/excalidraw/actions/actionElementLock.test.tsx index 244ccd08b7..d6dd3f7acb 100644 --- a/packages/excalidraw/actions/actionElementLock.test.tsx +++ b/packages/excalidraw/actions/actionElementLock.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { Excalidraw } from "../index"; import { queryByTestId, fireEvent } from "@testing-library/react"; import { render } from "../tests/test-utils"; diff --git a/packages/excalidraw/actions/actionProperties.test.tsx b/packages/excalidraw/actions/actionProperties.test.tsx index a7c90e303c..80d2ca257f 100644 --- a/packages/excalidraw/actions/actionProperties.test.tsx +++ b/packages/excalidraw/actions/actionProperties.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { Excalidraw } from "../index"; import { queryByTestId } from "@testing-library/react"; import { render } from "../tests/test-utils"; @@ -6,8 +7,6 @@ import { API } from "../tests/helpers/api"; import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors"; import { FONT_FAMILY, STROKE_WIDTH } from "../constants"; -const { h } = window; - describe("element locking", () => { beforeEach(async () => { await render(); @@ -22,7 +21,7 @@ describe("element locking", () => { // just in case we change it in the future expect(color).not.toBe(COLOR_PALETTE.transparent); - h.setState({ + API.setAppState({ currentItemBackgroundColor: color, }); const activeColor = queryByTestId( @@ -40,14 +39,14 @@ describe("element locking", () => { // just in case we change it in the future expect(color).not.toBe(COLOR_PALETTE.transparent); - h.setState({ + API.setAppState({ currentItemBackgroundColor: color, currentItemFillStyle: "hachure", }); const hachureFillButton = queryByTestId(document.body, `fill-hachure`); expect(hachureFillButton).toHaveClass("active"); - h.setState({ + API.setAppState({ currentItemFillStyle: "solid", }); const solidFillStyle = queryByTestId(document.body, `fill-solid`); @@ -57,7 +56,7 @@ describe("element locking", () => { it("should not show fill style when background transparent", () => { UI.clickTool("rectangle"); - h.setState({ + API.setAppState({ currentItemBackgroundColor: COLOR_PALETTE.transparent, currentItemFillStyle: "hachure", }); @@ -69,7 +68,7 @@ describe("element locking", () => { it("should show horizontal text align for text tool", () => { UI.clickTool("text"); - h.setState({ + API.setAppState({ currentItemTextAlign: "right", }); @@ -85,7 +84,7 @@ describe("element locking", () => { backgroundColor: "red", fillStyle: "cross-hatch", }); - h.elements = [rect]; + API.setElements([rect]); API.setSelectedElements([rect]); const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`); @@ -98,7 +97,7 @@ describe("element locking", () => { backgroundColor: COLOR_PALETTE.transparent, fillStyle: "cross-hatch", }); - h.elements = [rect]; + API.setElements([rect]); API.setSelectedElements([rect]); const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`); @@ -114,7 +113,7 @@ describe("element locking", () => { type: "rectangle", strokeWidth: STROKE_WIDTH.thin, }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); API.setSelectedElements([rect1, rect2]); const thinStrokeWidthButton = queryByTestId( @@ -133,7 +132,7 @@ describe("element locking", () => { type: "rectangle", strokeWidth: STROKE_WIDTH.bold, }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); API.setSelectedElements([rect1, rect2]); expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null); @@ -157,7 +156,7 @@ describe("element locking", () => { type: "text", fontFamily: FONT_FAMILY["Comic Shanns"], }); - h.elements = [rect, text]; + API.setElements([rect, text]); API.setSelectedElements([rect, text]); expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked(); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 81bac95664..3b1b1fc77e 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -2141,16 +2141,6 @@ class App extends React.Component { let editingElement: AppState["editingElement"] | null = null; if (actionResult.elements) { - actionResult.elements.forEach((element) => { - if ( - this.state.editingElement?.id === element.id && - this.state.editingElement !== element && - isNonDeletedElement(element) - ) { - editingElement = element; - } - }); - this.scene.replaceAllElements(actionResult.elements); didUpdate = true; } @@ -2183,8 +2173,20 @@ class App extends React.Component { gridSize = this.props.gridModeEnabled ? GRID_SIZE : null; } - editingElement = - editingElement || actionResult.appState?.editingElement || null; + editingElement = actionResult.appState?.editingElement || null; + + // make sure editingElement points to latest element reference + if (actionResult.elements && editingElement) { + actionResult.elements.forEach((element) => { + if ( + editingElement?.id === element.id && + editingElement !== element && + isNonDeletedElement(element) + ) { + editingElement = element; + } + }); + } if (editingElement?.isDeleted) { editingElement = null; @@ -4479,15 +4481,22 @@ class App extends React.Component { const elementIdToSelect = element.containerId ? element.containerId : element.id; - this.setState((prevState) => ({ - selectedElementIds: makeNextSelectedElementIds( - { - ...prevState.selectedElementIds, - [elementIdToSelect]: true, - }, - prevState, - ), - })); + + // needed to ensure state is updated before "finalize" action + // that's invoked on keyboard-submit as well + // TODO either move this into finalize as well, or handle all state + // updates in one place, skipping finalize action + flushSync(() => { + this.setState((prevState) => ({ + selectedElementIds: makeNextSelectedElementIds( + { + ...prevState.selectedElementIds, + [elementIdToSelect]: true, + }, + prevState, + ), + })); + }); } if (isDeleted) { fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [ diff --git a/packages/excalidraw/components/DefaultSidebar.test.tsx b/packages/excalidraw/components/DefaultSidebar.test.tsx index 717b6f9fc3..ac1e2961ca 100644 --- a/packages/excalidraw/components/DefaultSidebar.test.tsx +++ b/packages/excalidraw/components/DefaultSidebar.test.tsx @@ -9,7 +9,7 @@ import { import { assertExcalidrawWithSidebar, assertSidebarDockButton, -} from "./Sidebar/Sidebar.test"; +} from "./Sidebar/siderbar.test.helpers"; const { h } = window; diff --git a/packages/excalidraw/components/Sidebar/Sidebar.test.tsx b/packages/excalidraw/components/Sidebar/Sidebar.test.tsx index 6b60418b5f..b61529d9e6 100644 --- a/packages/excalidraw/components/Sidebar/Sidebar.test.tsx +++ b/packages/excalidraw/components/Sidebar/Sidebar.test.tsx @@ -2,8 +2,8 @@ import React from "react"; import { DEFAULT_SIDEBAR } from "../../constants"; import { Excalidraw, Sidebar } from "../../index"; import { + act, fireEvent, - GlobalTestState, queryAllByTestId, queryByTestId, render, @@ -11,39 +11,17 @@ import { withExcalidrawDimensions, } from "../../tests/test-utils"; import { vi } from "vitest"; +import { + assertExcalidrawWithSidebar, + assertSidebarDockButton, +} from "./siderbar.test.helpers"; -export const assertSidebarDockButton = async ( - hasDockButton: T, -): Promise< - T extends false - ? { dockButton: null; sidebar: HTMLElement } - : { dockButton: HTMLElement; sidebar: HTMLElement } -> => { - const sidebar = - GlobalTestState.renderResult.container.querySelector( - ".sidebar", - ); - expect(sidebar).not.toBe(null); - const dockButton = queryByTestId(sidebar!, "sidebar-dock"); - if (hasDockButton) { - expect(dockButton).not.toBe(null); - return { dockButton: dockButton!, sidebar: sidebar! } as any; - } - expect(dockButton).toBe(null); - return { dockButton: null, sidebar: sidebar! } as any; -}; - -export const assertExcalidrawWithSidebar = async ( - sidebar: React.ReactNode, - name: string, - test: () => void, -) => { - await render( - - {sidebar} - , - ); - await withExcalidrawDimensions({ width: 1920, height: 1080 }, test); +const toggleSidebar = ( + ...args: Parameters +): Promise => { + return act(() => { + return window.h.app.toggleSidebar(...args); + }); }; describe("Sidebar", () => { @@ -103,7 +81,7 @@ describe("Sidebar", () => { // toggle sidebar on // ------------------------------------------------------------------------- - expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true); + expect(await toggleSidebar({ name: "customSidebar" })).toBe(true); await waitFor(() => { const node = container.querySelector("#test-sidebar-content"); @@ -112,7 +90,7 @@ describe("Sidebar", () => { // toggle sidebar off // ------------------------------------------------------------------------- - expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(false); + expect(await toggleSidebar({ name: "customSidebar" })).toBe(false); await waitFor(() => { const node = container.querySelector("#test-sidebar-content"); @@ -121,9 +99,9 @@ describe("Sidebar", () => { // force-toggle sidebar off (=> still hidden) // ------------------------------------------------------------------------- - expect( - window.h.app.toggleSidebar({ name: "customSidebar", force: false }), - ).toBe(false); + expect(await toggleSidebar({ name: "customSidebar", force: false })).toBe( + false, + ); await waitFor(() => { const node = container.querySelector("#test-sidebar-content"); @@ -132,12 +110,12 @@ describe("Sidebar", () => { // force-toggle sidebar on // ------------------------------------------------------------------------- - expect( - window.h.app.toggleSidebar({ name: "customSidebar", force: true }), - ).toBe(true); - expect( - window.h.app.toggleSidebar({ name: "customSidebar", force: true }), - ).toBe(true); + expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe( + true, + ); + expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe( + true, + ); await waitFor(() => { const node = container.querySelector("#test-sidebar-content"); @@ -146,9 +124,7 @@ describe("Sidebar", () => { // toggle library (= hide custom sidebar) // ------------------------------------------------------------------------- - expect(window.h.app.toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe( - true, - ); + expect(await toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(true); await waitFor(() => { const node = container.querySelector("#test-sidebar-content"); @@ -161,13 +137,13 @@ describe("Sidebar", () => { // closing sidebar using `{ name: null }` // ------------------------------------------------------------------------- - expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true); + expect(await toggleSidebar({ name: "customSidebar" })).toBe(true); await waitFor(() => { const node = container.querySelector("#test-sidebar-content"); expect(node).not.toBe(null); }); - expect(window.h.app.toggleSidebar({ name: null })).toBe(false); + expect(await toggleSidebar({ name: null })).toBe(false); await waitFor(() => { const node = container.querySelector("#test-sidebar-content"); expect(node).toBe(null); @@ -321,6 +297,9 @@ describe("Sidebar", () => { }); it("shouldn't be user-dockable when only `onDock` supplied w/o `docked`", async () => { + // we expect warnings in this test and don't want to pollute stdout + const mock = jest.spyOn(console, "warn").mockImplementation(() => {}); + await render( { await assertSidebarDockButton(false); }, ); + + mock.mockRestore(); }); }); @@ -367,9 +348,9 @@ describe("Sidebar", () => { ).toBeNull(); // open library sidebar - expect( - window.h.app.toggleSidebar({ name: "custom", tab: "library" }), - ).toBe(true); + expect(await toggleSidebar({ name: "custom", tab: "library" })).toBe( + true, + ); expect( container.querySelector( "[role=tabpanel][data-testid=library]", @@ -377,9 +358,9 @@ describe("Sidebar", () => { ).not.toBeNull(); // switch to comments tab - expect( - window.h.app.toggleSidebar({ name: "custom", tab: "comments" }), - ).toBe(true); + expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe( + true, + ); expect( container.querySelector( "[role=tabpanel][data-testid=comments]", @@ -387,9 +368,9 @@ describe("Sidebar", () => { ).not.toBeNull(); // toggle sidebar closed - expect( - window.h.app.toggleSidebar({ name: "custom", tab: "comments" }), - ).toBe(false); + expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe( + false, + ); expect( container.querySelector( "[role=tabpanel][data-testid=comments]", @@ -397,9 +378,9 @@ describe("Sidebar", () => { ).toBeNull(); // toggle sidebar open - expect( - window.h.app.toggleSidebar({ name: "custom", tab: "comments" }), - ).toBe(true); + expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe( + true, + ); expect( container.querySelector( "[role=tabpanel][data-testid=comments]", diff --git a/packages/excalidraw/components/Sidebar/siderbar.test.helpers.tsx b/packages/excalidraw/components/Sidebar/siderbar.test.helpers.tsx new file mode 100644 index 0000000000..c2a37431bc --- /dev/null +++ b/packages/excalidraw/components/Sidebar/siderbar.test.helpers.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Excalidraw } from "../.."; +import { + GlobalTestState, + queryByTestId, + render, + withExcalidrawDimensions, +} from "../../tests/test-utils"; + +export const assertSidebarDockButton = async ( + hasDockButton: T, +): Promise< + T extends false + ? { dockButton: null; sidebar: HTMLElement } + : { dockButton: HTMLElement; sidebar: HTMLElement } +> => { + const sidebar = + GlobalTestState.renderResult.container.querySelector( + ".sidebar", + ); + expect(sidebar).not.toBe(null); + const dockButton = queryByTestId(sidebar!, "sidebar-dock"); + if (hasDockButton) { + expect(dockButton).not.toBe(null); + return { dockButton: dockButton!, sidebar: sidebar! } as any; + } + expect(dockButton).toBe(null); + return { dockButton: null, sidebar: sidebar! } as any; +}; + +export const assertExcalidrawWithSidebar = async ( + sidebar: React.ReactNode, + name: string, + test: () => void, +) => { + await render( + + {sidebar} + , + ); + await withExcalidrawDimensions({ width: 1920, height: 1080 }, test); +}; diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index 365abf7758..34f66a4d1c 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -1,4 +1,5 @@ -import { fireEvent, queryByTestId } from "@testing-library/react"; +import React from "react"; +import { act, fireEvent, queryByTestId } from "@testing-library/react"; import { Keyboard, Pointer, UI } from "../../tests/helpers/ui"; import { getStepSizedValue } from "./utils"; import { @@ -24,7 +25,6 @@ import { getCommonBounds, isTextElement } from "../../element"; import { API } from "../../tests/helpers/api"; import { actionGroup } from "../../actions"; import { isInGroup } from "../../groups"; -import React from "react"; const { h } = window; const mouse = new Pointer("mouse"); @@ -32,12 +32,6 @@ const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene"); let stats: HTMLElement | null = null; let elementStats: HTMLElement | null | undefined = null; -const editInput = (input: HTMLInputElement, value: string) => { - input.focus(); - fireEvent.change(input, { target: { value } }); - input.blur(); -}; - const getStatsProperty = (label: string) => { const elementStats = UI.queryStats()?.querySelector("#elementStats"); @@ -65,7 +59,7 @@ const testInputProperty = ( ) as HTMLInputElement; expect(input).toBeDefined(); expect(input.value).toBe(initialValue.toString()); - editInput(input, String(nextValue)); + UI.updateInput(input, String(nextValue)); if (property === "angle") { expect(element[property]).toBe(degreeToRadian(Number(nextValue))); } else if (property === "fontSize" && isTextElement(element)) { @@ -110,7 +104,7 @@ describe("binding with linear elements", () => { await render(); - h.elements = []; + API.setElements([]); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -148,7 +142,7 @@ describe("binding with linear elements", () => { expect(linear.startBinding).not.toBe(null); expect(inputX).not.toBeNull(); - editInput(inputX, String("204")); + UI.updateInput(inputX, String("204")); expect(linear.startBinding).not.toBe(null); }); @@ -159,7 +153,7 @@ describe("binding with linear elements", () => { ) as HTMLInputElement; expect(linear.startBinding).not.toBe(null); - editInput(inputAngle, String("1")); + UI.updateInput(inputAngle, String("1")); expect(linear.startBinding).not.toBe(null); }); @@ -171,7 +165,7 @@ describe("binding with linear elements", () => { expect(linear.startBinding).not.toBe(null); expect(inputX).not.toBeNull(); - editInput(inputX, String("254")); + UI.updateInput(inputX, String("254")); expect(linear.startBinding).toBe(null); }); @@ -182,7 +176,7 @@ describe("binding with linear elements", () => { ) as HTMLInputElement; expect(linear.startBinding).not.toBe(null); - editInput(inputAngle, String("45")); + UI.updateInput(inputAngle, String("45")); expect(linear.startBinding).toBe(null); }); }); @@ -197,7 +191,7 @@ describe("stats for a generic element", () => { await render(); - h.elements = []; + API.setElements([]); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -268,13 +262,13 @@ describe("stats for a generic element", () => { ) as HTMLInputElement; expect(input).toBeDefined(); expect(input.value).toBe(rectangle.width.toString()); - editInput(input, "123.123"); + UI.updateInput(input, "123.123"); expect(h.elements.length).toBe(1); expect(rectangle.id).toBe(rectangleId); expect(input.value).toBe("123.12"); expect(rectangle.width).toBe(123.12); - editInput(input, "88.98766"); + UI.updateInput(input, "88.98766"); expect(input.value).toBe("88.99"); expect(rectangle.width).toBe(88.99); }); @@ -387,7 +381,7 @@ describe("stats for a non-generic element", () => { await render(); - h.elements = []; + API.setElements([]); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -412,9 +406,10 @@ describe("stats for a non-generic element", () => { mouse.clickAt(20, 30); const textEditorSelector = ".excalidraw-textEditorContainer > textarea"; const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello!"); - editor.blur(); + act(() => { + editor.blur(); + }); const text = h.elements[0] as ExcalidrawTextElement; mouse.clickOn(text); @@ -427,7 +422,7 @@ describe("stats for a non-generic element", () => { ) as HTMLInputElement; expect(input).toBeDefined(); expect(input.value).toBe(text.fontSize.toString()); - editInput(input, "36"); + UI.updateInput(input, "36"); expect(text.fontSize).toBe(36); // cannot change width or height @@ -437,7 +432,7 @@ describe("stats for a non-generic element", () => { expect(height).toBeUndefined(); // min font size is 4 - editInput(input, "0"); + UI.updateInput(input, "0"); expect(text.fontSize).not.toBe(0); expect(text.fontSize).toBe(4); }); @@ -449,8 +444,8 @@ describe("stats for a non-generic element", () => { x: 150, width: 150, }); - h.elements = [frame]; - h.setState({ + API.setElements([frame]); + API.setAppState({ selectedElementIds: { [frame.id]: true, }, @@ -471,9 +466,9 @@ describe("stats for a non-generic element", () => { it("image element", () => { const image = API.createElement({ type: "image", width: 200, height: 100 }); - h.elements = [image]; + API.setElements([image]); mouse.clickOn(image); - h.setState({ + API.setAppState({ selectedElementIds: { [image.id]: true, }, @@ -508,7 +503,7 @@ describe("stats for a non-generic element", () => { mutateElement(container, { boundElements: [{ type: "text", id: text.id }], }); - h.elements = [container, text]; + API.setElements([container, text]); API.setSelectedElements([container]); const fontSize = getStatsProperty("F")?.querySelector( @@ -516,7 +511,7 @@ describe("stats for a non-generic element", () => { ) as HTMLInputElement; expect(fontSize).toBeDefined(); - editInput(fontSize, "40"); + UI.updateInput(fontSize, "40"); expect(text.fontSize).toBe(40); }); @@ -533,7 +528,7 @@ describe("stats for multiple elements", () => { await render(); - h.elements = []; + API.setElements([]); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -566,7 +561,7 @@ describe("stats for multiple elements", () => { mouse.down(-100, -100); mouse.up(125, 145); - h.setState({ + API.setAppState({ selectedElementIds: h.elements.reduce((acc, el) => { acc[el.id] = true; return acc; @@ -588,12 +583,12 @@ describe("stats for multiple elements", () => { ) as HTMLInputElement; expect(angle.value).toBe("0"); - editInput(width, "250"); + UI.updateInput(width, "250"); h.elements.forEach((el) => { expect(el.width).toBe(250); }); - editInput(height, "450"); + UI.updateInput(height, "450"); h.elements.forEach((el) => { expect(el.height).toBe(450); }); @@ -605,9 +600,10 @@ describe("stats for multiple elements", () => { mouse.clickAt(20, 30); const textEditorSelector = ".excalidraw-textEditorContainer > textarea"; const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello!"); - editor.blur(); + act(() => { + editor.blur(); + }); UI.clickTool("rectangle"); mouse.down(); @@ -619,12 +615,12 @@ describe("stats for multiple elements", () => { width: 150, }); - h.elements = [...h.elements, frame]; + API.setElements([...h.elements, frame]); const text = h.elements.find((el) => el.type === "text"); const rectangle = h.elements.find((el) => el.type === "rectangle"); - h.setState({ + API.setAppState({ selectedElementIds: h.elements.reduce((acc, el) => { acc[el.id] = true; return acc; @@ -657,13 +653,13 @@ describe("stats for multiple elements", () => { expect(fontSize).toBeDefined(); // changing width does not affect text - editInput(width, "200"); + UI.updateInput(width, "200"); expect(rectangle?.width).toBe(200); expect(frame.width).toBe(200); expect(text?.width).not.toBe(200); - editInput(angle, "40"); + UI.updateInput(angle, "40"); const angleInRadian = degreeToRadian(40); expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4); @@ -686,7 +682,7 @@ describe("stats for multiple elements", () => { mouse.click(); }); - h.app.actionManager.executeAction(actionGroup); + API.executeAction(actionGroup); }; createAndSelectGroup(); @@ -703,7 +699,7 @@ describe("stats for multiple elements", () => { expect(x).toBeDefined(); expect(Number(x.value)).toBe(x1); - editInput(x, "300"); + UI.updateInput(x, "300"); expect(h.elements[0].x).toBe(300); expect(h.elements[1].x).toBe(400); @@ -716,7 +712,7 @@ describe("stats for multiple elements", () => { expect(y).toBeDefined(); expect(Number(y.value)).toBe(y1); - editInput(y, "200"); + UI.updateInput(y, "200"); expect(h.elements[0].y).toBe(200); expect(h.elements[1].y).toBe(300); @@ -734,20 +730,20 @@ describe("stats for multiple elements", () => { expect(height).toBeDefined(); expect(Number(height.value)).toBe(200); - editInput(width, "400"); + UI.updateInput(width, "400"); [x1, y1, x2, y2] = getCommonBounds(elementsInGroup); let newGroupWidth = x2 - x1; expect(newGroupWidth).toBeCloseTo(400, 4); - editInput(width, "300"); + UI.updateInput(width, "300"); [x1, y1, x2, y2] = getCommonBounds(elementsInGroup); newGroupWidth = x2 - x1; expect(newGroupWidth).toBeCloseTo(300, 4); - editInput(height, "500"); + UI.updateInput(height, "500"); [x1, y1, x2, y2] = getCommonBounds(elementsInGroup); const newGroupHeight = y2 - y1; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx index 3aae1d0c7e..0c8deec87a 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx @@ -1,7 +1,13 @@ +import React from "react"; import { Excalidraw } from "../../index"; import { KEYS } from "../../keys"; import { Keyboard } from "../../tests/helpers/ui"; -import { render, waitFor, getByTestId } from "../../tests/test-utils"; +import { + render, + waitFor, + getByTestId, + fireEvent, +} from "../../tests/test-utils"; describe("Test ", () => { it("should", async () => { @@ -9,7 +15,7 @@ describe("Test ", () => { expect(window.h.state.openMenu).toBe(null); - getByTestId(container, "main-menu-trigger").click(); + fireEvent.click(getByTestId(container, "main-menu-trigger")); expect(window.h.state.openMenu).toBe("canvas"); await waitFor(() => { diff --git a/packages/excalidraw/components/hoc/withInternalFallback.test.tsx b/packages/excalidraw/components/hoc/withInternalFallback.test.tsx index 68cc56d348..5543133771 100644 --- a/packages/excalidraw/components/hoc/withInternalFallback.test.tsx +++ b/packages/excalidraw/components/hoc/withInternalFallback.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { render, queryAllByTestId } from "../../tests/test-utils"; import { Excalidraw, MainMenu } from "../../index"; diff --git a/packages/excalidraw/element/routing.test.tsx b/packages/excalidraw/element/routing.test.tsx index d159deeaa3..8d95c3489a 100644 --- a/packages/excalidraw/element/routing.test.tsx +++ b/packages/excalidraw/element/routing.test.tsx @@ -22,12 +22,6 @@ const { h } = window; const mouse = new Pointer("mouse"); -const editInput = (input: HTMLInputElement, value: string) => { - input.focus(); - fireEvent.change(input, { target: { value } }); - input.blur(); -}; - const getStatsProperty = (label: string) => { const elementStats = UI.queryStats()?.querySelector("#elementStats"); @@ -202,7 +196,7 @@ describe("elbow arrow ui", () => { const inputAngle = getStatsProperty("A")?.querySelector( ".drag-input", ) as HTMLInputElement; - editInput(inputAngle, String("40")); + UI.updateInput(inputAngle, String("40")); expect(arrow.points.map((point) => point.map(Math.round))).toEqual([ [0, 0], diff --git a/packages/excalidraw/element/textWysiwyg.test.tsx b/packages/excalidraw/element/textWysiwyg.test.tsx index 32fa7bffee..7b78f56677 100644 --- a/packages/excalidraw/element/textWysiwyg.test.tsx +++ b/packages/excalidraw/element/textWysiwyg.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { Excalidraw } from "../index"; import { GlobalTestState, render, screen } from "../tests/test-utils"; @@ -16,7 +17,6 @@ import type { ExcalidrawTextElementWithContainer, } from "./types"; import { API } from "../tests/helpers/api"; -import { mutateElement } from "./mutateElement"; import { getOriginalContainerHeightFromCache } from "./containerCache"; import { getTextEditor, updateTextEditor } from "../tests/queries/dom"; @@ -33,7 +33,7 @@ describe("textWysiwyg", () => { const { h } = window; beforeEach(async () => { await render(); - h.elements = []; + API.setElements([]); }); it("should prefer editing selected text element (non-bindable container present)", async () => { @@ -55,7 +55,7 @@ describe("textWysiwyg", () => { width: textSize, height: textSize, }); - h.elements = [text, line]; + API.setElements([text, line]); API.setSelectedElements([text]); @@ -95,9 +95,9 @@ describe("textWysiwyg", () => { containerId: container.id, }); - h.elements = [container, boundText, boundText2]; + API.setElements([container, boundText, boundText2]); - mutateElement(container, { + API.updateElement(container, { boundElements: [{ type: "text", id: boundText.id }], }); @@ -123,11 +123,11 @@ describe("textWysiwyg", () => { height: textSize, containerId: container.id, }); - mutateElement(container, { + API.updateElement(container, { boundElements: [{ type: "text", id: text.id }], }); - h.elements = [container, text]; + API.setElements([container, text]); API.setSelectedElements([container]); @@ -164,9 +164,9 @@ describe("textWysiwyg", () => { containerId: container.id, }); - h.elements = [container, boundText, boundText2]; + API.setElements([container, boundText, boundText2]); - mutateElement(container, { + API.updateElement(container, { boundElements: [{ type: "text", id: boundText.id }], }); @@ -187,7 +187,7 @@ describe("textWysiwyg", () => { height: 100, }); - h.elements = [text]; + API.setElements([text]); UI.clickTool("text"); mouse.clickAt(text.x + 50, text.y + 50); @@ -209,7 +209,7 @@ describe("textWysiwyg", () => { height: 100, }); - h.elements = [text]; + API.setElements([text]); UI.clickTool("selection"); mouse.doubleClickAt(text.x + 50, text.y + 50); @@ -251,7 +251,7 @@ describe("textWysiwyg", () => { // @ts-ignore h.app.refreshEditorBreakpoints(); - h.elements = []; + API.setElements([]); }); afterAll(() => { @@ -264,7 +264,7 @@ describe("textWysiwyg", () => { text: "Excalidraw\nEditor", }); - h.elements = [text]; + API.setElements([text]); const prevWidth = text.width; const prevHeight = text.height; @@ -291,18 +291,15 @@ describe("textWysiwyg", () => { const nextText = `${wrappedText} is great!`; updateTextEditor(editor, nextText); - await new Promise((cb) => setTimeout(cb, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(h.elements[0].width).toEqual(wrappedWidth); expect(h.elements[0].height).toBeGreaterThan(wrappedHeight); // remove all texts and then add it back editing updateTextEditor(editor, ""); - await new Promise((cb) => setTimeout(cb, 0)); updateTextEditor(editor, nextText); - await new Promise((cb) => setTimeout(cb, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(h.elements[0].width).toEqual(wrappedWidth); }); @@ -313,7 +310,7 @@ describe("textWysiwyg", () => { type: "text", text: originalText, }); - h.elements = [text]; + API.setElements([text]); // wrap UI.resize(text, "e", [-40, 0]); @@ -321,7 +318,7 @@ describe("textWysiwyg", () => { UI.clickTool("selection"); mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2); const editor = await getTextEditor(textEditorSelector); - editor.blur(); + Keyboard.exitTextEditor(editor); // restore after unwrapping UI.resize(text, "e", [40, 0]); expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText); @@ -332,14 +329,12 @@ describe("textWysiwyg", () => { UI.clickTool("selection"); mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2); updateTextEditor(editor, `${wrappedText}\nA new line!`); - await new Promise((cb) => setTimeout(cb, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); // remove the newly added line UI.clickTool("selection"); mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2); updateTextEditor(editor, wrappedText); - await new Promise((cb) => setTimeout(cb, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); // unwrap UI.resize(text, "e", [30, 0]); // expect the text to be restored the same @@ -376,12 +371,11 @@ describe("textWysiwyg", () => { }); it("should add a tab at the start of the first line", () => { - const event = new KeyboardEvent("keydown", { key: KEYS.TAB }); textarea.value = "Line#1\nLine#2"; // cursor: "|Line#1\nLine#2" textarea.selectionStart = 0; textarea.selectionEnd = 0; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { key: KEYS.TAB }); expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`); // cursor: " |Line#1\nLine#2" @@ -390,13 +384,12 @@ describe("textWysiwyg", () => { }); it("should add a tab at the start of the second line", () => { - const event = new KeyboardEvent("keydown", { key: KEYS.TAB }); textarea.value = "Line#1\nLine#2"; // cursor: "Line#1\nLin|e#2" textarea.selectionStart = 10; textarea.selectionEnd = 10; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { key: KEYS.TAB }); expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`); @@ -406,13 +399,12 @@ describe("textWysiwyg", () => { }); it("should add a tab at the start of the first and second line", () => { - const event = new KeyboardEvent("keydown", { key: KEYS.TAB }); textarea.value = "Line#1\nLine#2\nLine#3"; // cursor: "Li|ne#1\nLi|ne#2\nLine#3" textarea.selectionStart = 2; textarea.selectionEnd = 9; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { key: KEYS.TAB }); expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`); @@ -422,16 +414,15 @@ describe("textWysiwyg", () => { }); it("should remove a tab at the start of the first line", () => { - const event = new KeyboardEvent("keydown", { - key: KEYS.TAB, - shiftKey: true, - }); textarea.value = `${tab}Line#1\nLine#2`; // cursor: "| Line#1\nLine#2" textarea.selectionStart = 0; textarea.selectionEnd = 0; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { + key: KEYS.TAB, + shiftKey: true, + }); expect(textarea.value).toEqual(`Line#1\nLine#2`); @@ -441,16 +432,15 @@ describe("textWysiwyg", () => { }); it("should remove a tab at the start of the second line", () => { - const event = new KeyboardEvent("keydown", { - key: KEYS.TAB, - shiftKey: true, - }); // cursor: "Line#1\n Lin|e#2" textarea.value = `Line#1\n${tab}Line#2`; textarea.selectionStart = 15; textarea.selectionEnd = 15; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { + key: KEYS.TAB, + shiftKey: true, + }); expect(textarea.value).toEqual(`Line#1\nLine#2`); // cursor: "Line#1\nLin|e#2" @@ -459,16 +449,15 @@ describe("textWysiwyg", () => { }); it("should remove a tab at the start of the first and second line", () => { - const event = new KeyboardEvent("keydown", { - key: KEYS.TAB, - shiftKey: true, - }); // cursor: " Li|ne#1\n Li|ne#2\nLine#3" textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`; textarea.selectionStart = 6; textarea.selectionEnd = 17; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { + key: KEYS.TAB, + shiftKey: true, + }); expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`); // cursor: "Li|ne#1\nLi|ne#2\nLine#3" @@ -477,45 +466,41 @@ describe("textWysiwyg", () => { }); it("should remove a tab at the start of the second line and cursor stay on this line", () => { - const event = new KeyboardEvent("keydown", { - key: KEYS.TAB, - shiftKey: true, - }); // cursor: "Line#1\n | Line#2" textarea.value = `Line#1\n${tab}Line#2`; textarea.selectionStart = 9; textarea.selectionEnd = 9; - textarea.dispatchEvent(event); - - // cursor: "Line#1\n|Line#2" - expect(textarea.selectionStart).toEqual(7); - // expect(textarea.selectionEnd).toEqual(7); - }); - - it("should remove partial tabs", () => { - const event = new KeyboardEvent("keydown", { + fireEvent.keyDown(textarea, { key: KEYS.TAB, shiftKey: true, }); + + // cursor: "Line#1\n|Line#2" + expect(textarea.selectionStart).toEqual(7); + }); + + it("should remove partial tabs", () => { // cursor: "Line#1\n Line#|2" textarea.value = `Line#1\n Line#2`; textarea.selectionStart = 15; textarea.selectionEnd = 15; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { + key: KEYS.TAB, + shiftKey: true, + }); expect(textarea.value).toEqual(`Line#1\nLine#2`); }); it("should remove nothing", () => { - const event = new KeyboardEvent("keydown", { - key: KEYS.TAB, - shiftKey: true, - }); // cursor: "Line#1\n Li|ne#2" textarea.value = `Line#1\nLine#2`; textarea.selectionStart = 9; textarea.selectionEnd = 9; - textarea.dispatchEvent(event); + fireEvent.keyDown(textarea, { + key: KEYS.TAB, + shiftKey: true, + }); expect(textarea.value).toEqual(`Line#1\nLine#2`); }); @@ -523,54 +508,42 @@ describe("textWysiwyg", () => { it("should resize text via shortcuts while in wysiwyg", () => { textarea.value = "abc def"; const origFontSize = textElement.fontSize; - textarea.dispatchEvent( - new KeyboardEvent("keydown", { - key: KEYS.CHEVRON_RIGHT, - ctrlKey: true, - shiftKey: true, - }), - ); + fireEvent.keyDown(textarea, { + key: KEYS.CHEVRON_RIGHT, + ctrlKey: true, + shiftKey: true, + }); expect(textElement.fontSize).toBe(origFontSize * 1.1); - textarea.dispatchEvent( - new KeyboardEvent("keydown", { - key: KEYS.CHEVRON_LEFT, - ctrlKey: true, - shiftKey: true, - }), - ); + fireEvent.keyDown(textarea, { + key: KEYS.CHEVRON_LEFT, + ctrlKey: true, + shiftKey: true, + }); expect(textElement.fontSize).toBe(origFontSize); }); it("zooming via keyboard should zoom canvas", () => { expect(h.state.zoom.value).toBe(1); - textarea.dispatchEvent( - new KeyboardEvent("keydown", { - code: CODES.MINUS, - ctrlKey: true, - }), - ); + fireEvent.keyDown(textarea, { + code: CODES.MINUS, + ctrlKey: true, + }); expect(h.state.zoom.value).toBe(0.9); - textarea.dispatchEvent( - new KeyboardEvent("keydown", { - code: CODES.NUM_SUBTRACT, - ctrlKey: true, - }), - ); + fireEvent.keyDown(textarea, { + code: CODES.NUM_SUBTRACT, + ctrlKey: true, + }); expect(h.state.zoom.value).toBe(0.8); - textarea.dispatchEvent( - new KeyboardEvent("keydown", { - code: CODES.NUM_ADD, - ctrlKey: true, - }), - ); + fireEvent.keyDown(textarea, { + code: CODES.NUM_ADD, + ctrlKey: true, + }); expect(h.state.zoom.value).toBe(0.9); - textarea.dispatchEvent( - new KeyboardEvent("keydown", { - code: CODES.EQUAL, - ctrlKey: true, - }), - ); + fireEvent.keyDown(textarea, { + code: CODES.EQUAL, + ctrlKey: true, + }); expect(h.state.zoom.value).toBe(1); }); @@ -583,8 +556,8 @@ describe("textWysiwyg", () => { textarea, "Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!", ); - await new Promise((cb) => setTimeout(cb, 0)); - textarea.blur(); + Keyboard.exitTextEditor(textarea); + expect(textarea.style.width).toBe("792px"); expect(h.elements[0].width).toBe(1000); }); @@ -596,7 +569,7 @@ describe("textWysiwyg", () => { beforeEach(async () => { await render(); - h.elements = []; + API.setElements([]); rectangle = UI.createElement("rectangle", { x: 10, @@ -615,7 +588,7 @@ describe("textWysiwyg", () => { height: 75, backgroundColor: "red", }); - h.elements = [rectangle]; + API.setElements([rectangle]); expect(h.elements.length).toBe(1); expect(h.elements[0].id).toBe(rectangle.id); @@ -634,8 +607,7 @@ describe("textWysiwyg", () => { updateTextEditor(editor, "Hello World!"); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); @@ -648,7 +620,7 @@ describe("textWysiwyg", () => { height: 75, angle: 45, }); - h.elements = [rectangle]; + API.setElements([rectangle]); mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10); const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.type).toBe("text"); @@ -662,8 +634,7 @@ describe("textWysiwyg", () => { updateTextEditor(editor, "Hello World!"); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); @@ -677,7 +648,7 @@ describe("textWysiwyg", () => { width: 90, height: 75, }); - h.elements = [diamond]; + API.setElements([diamond]); expect(h.elements.length).toBe(1); expect(h.elements[0].id).toBe(diamond.id); @@ -687,7 +658,6 @@ describe("textWysiwyg", () => { const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); const value = new Array(1000).fill("1").join("\n"); // Pasting large text to simulate height increase @@ -712,7 +682,7 @@ describe("textWysiwyg", () => { height: 75, backgroundColor: "transparent", }); - h.elements = [rectangle]; + API.setElements([rectangle]); mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10); expect(h.elements.length).toBe(2); @@ -721,8 +691,7 @@ describe("textWysiwyg", () => { expect(text.containerId).toBe(null); mouse.down(); let editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); mouse.doubleClickAt( rectangle.x + rectangle.width / 2, @@ -738,8 +707,7 @@ describe("textWysiwyg", () => { editor = await getTextEditor(textEditorSelector, true); updateTextEditor(editor, "Hello World!"); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, @@ -759,10 +727,8 @@ describe("textWysiwyg", () => { expect(text.containerId).toBe(rectangle.id); const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); - updateTextEditor(editor, "Hello World!"); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); @@ -777,7 +743,7 @@ describe("textWysiwyg", () => { height: 75, strokeWidth: 4, }); - h.elements = [rectangle]; + API.setElements([rectangle]); expect(h.elements.length).toBe(1); expect(h.elements[0].id).toBe(rectangle.id); @@ -795,8 +761,7 @@ describe("textWysiwyg", () => { const editor = await getTextEditor(textEditorSelector, true); updateTextEditor(editor, "Hello World!"); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); @@ -808,7 +773,7 @@ describe("textWysiwyg", () => { width: 100, height: 0, }); - h.elements = [freedraw]; + API.setElements([freedraw]); UI.clickTool("text"); @@ -819,7 +784,7 @@ describe("textWysiwyg", () => { const editor = await getTextEditor(textEditorSelector, true); updateTextEditor(editor, "Hello World!"); - fireEvent.keyDown(editor, { key: KEYS.ESCAPE }); + Keyboard.exitTextEditor(editor); expect(freedraw.boundElements).toBe(null); expect(h.elements[1].type).toBe("text"); @@ -828,7 +793,7 @@ describe("textWysiwyg", () => { ["freedraw", "line"].forEach((type: any) => { it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => { - h.elements = []; + API.setElements([]); const element = UI.createElement(type, { width: 100, height: 50, @@ -855,8 +820,7 @@ describe("textWysiwyg", () => { updateTextEditor(editor, "Hello World!"); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toBe(null); }); @@ -872,7 +836,6 @@ describe("textWysiwyg", () => { editor, "Excalidraw is an opensource virtual collaborative whiteboard", ); - await new Promise((cb) => setTimeout(cb, 0)); expect(h.elements.length).toBe(2); expect(h.elements[1].type).toBe("text"); @@ -908,14 +871,18 @@ describe("textWysiwyg", () => { rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2, ); - mouse.down(); const text = h.elements[1] as ExcalidrawTextElementWithContainer; const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello World!"); - editor.blur(); + + Keyboard.exitTextEditor(editor); + + expect(await getTextEditor(textEditorSelector, false)).toBe(null); + + expect(h.state.editingElement).toBe(null); + expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont); fireEvent.click(screen.getByTitle(/code/i)); @@ -950,8 +917,7 @@ describe("textWysiwyg", () => { updateTextEditor(editor, "Hello World!"); - await new Promise((cb) => setTimeout(cb, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.text).toBe("Hello \nWorld!"); expect(text.originalText).toBe("Hello World!"); @@ -970,9 +936,7 @@ describe("textWysiwyg", () => { editor = await getTextEditor(textEditorSelector, true); updateTextEditor(editor, "Hello"); - await new Promise((r) => setTimeout(r, 0)); - - editor.blur(); + Keyboard.exitTextEditor(editor); text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.text).toBe("Hello"); @@ -998,10 +962,8 @@ describe("textWysiwyg", () => { const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); - updateTextEditor(editor, "Hello World!"); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); @@ -1034,9 +996,8 @@ describe("textWysiwyg", () => { const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.containerId).toBe(rectangle.id); const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello World!"); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); @@ -1055,9 +1016,8 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); let editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello"); - editor.blur(); + Keyboard.exitTextEditor(editor); // should center align horizontally and vertically by default UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]); @@ -1076,12 +1036,8 @@ describe("textWysiwyg", () => { editor.select(); fireEvent.click(screen.getByTitle("Left")); - await new Promise((r) => setTimeout(r, 0)); - fireEvent.click(screen.getByTitle("Align bottom")); - await new Promise((r) => setTimeout(r, 0)); - - editor.blur(); + Keyboard.exitTextEditor(editor); // should left align horizontally and bottom vertically after resize UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]); @@ -1101,9 +1057,7 @@ describe("textWysiwyg", () => { fireEvent.click(screen.getByTitle("Right")); fireEvent.click(screen.getByTitle("Align top")); - await new Promise((r) => setTimeout(r, 0)); - - editor.blur(); + Keyboard.exitTextEditor(editor); // should right align horizontally and top vertically after resize UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]); @@ -1136,8 +1090,7 @@ describe("textWysiwyg", () => { updateTextEditor(editor, "Hello World!"); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle2.boundElements).toBeNull(); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, @@ -1148,9 +1101,8 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello"); - editor.blur(); + Keyboard.exitTextEditor(editor); const textElement = h.elements[1] as ExcalidrawTextElement; expect(rectangle.width).toBe(90); expect(rectangle.height).toBe(75); @@ -1168,9 +1120,8 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello"); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(h.elements.length).toBe(2); mouse.select(rectangle); @@ -1200,9 +1151,8 @@ describe("textWysiwyg", () => { it("undo should work", async () => { Keyboard.keyPress(KEYS.ENTER); const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello"); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: h.elements[1].id, type: "text" }, ]); @@ -1237,10 +1187,9 @@ describe("textWysiwyg", () => { it("should not allow bound text with only whitespaces", async () => { Keyboard.keyPress(KEYS.ENTER); const editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, " "); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([]); expect(h.elements[1].isDeleted).toBe(true); }); @@ -1259,7 +1208,7 @@ describe("textWysiwyg", () => { text: "Online whiteboard collaboration made easy", }); - h.elements = [container, text]; + API.setElements([container, text]); API.setSelectedElements([container, text]); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -1292,9 +1241,8 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); let editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello"); - editor.blur(); + Keyboard.exitTextEditor(editor); UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]); expect(rectangle.height).toBeCloseTo(155, 8); @@ -1305,8 +1253,7 @@ describe("textWysiwyg", () => { editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.height).toBeCloseTo(155, 8); // cache updated again expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo( @@ -1321,7 +1268,7 @@ describe("textWysiwyg", () => { const editor = await getTextEditor(textEditorSelector, true); updateTextEditor(editor, "Hello World!"); - editor.blur(); + Keyboard.exitTextEditor(editor); mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); @@ -1346,7 +1293,7 @@ describe("textWysiwyg", () => { const editor = await getTextEditor(textEditorSelector, true); updateTextEditor(editor, "Hello World!"); - editor.blur(); + Keyboard.exitTextEditor(editor); expect( (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight, ).toEqual(1.25); @@ -1378,7 +1325,7 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); editor = await getTextEditor(textEditorSelector, true); updateTextEditor(editor, "Hello"); - editor.blur(); + Keyboard.exitTextEditor(editor); mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); editor = await getTextEditor(textEditorSelector, true); @@ -1498,13 +1445,11 @@ describe("textWysiwyg", () => { editor, "Excalidraw is an opensource virtual collaborative whiteboard", ); - await new Promise((cb) => setTimeout(cb, 0)); editor.select(); fireEvent.click(screen.getByTitle("Left")); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); const textElement = h.elements[1] as ExcalidrawTextElement; expect(textElement.width).toBe(600); @@ -1581,16 +1526,14 @@ describe("textWysiwyg", () => { let text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.containerId).toBe(rectangle.id); let editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Hello!"); expect( (h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign, ).toBe(VERTICAL_ALIGN.MIDDLE); fireEvent.click(screen.getByTitle("Align bottom")); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, @@ -1606,9 +1549,8 @@ describe("textWysiwyg", () => { rectangle.y + rectangle.height / 2, ); editor = await getTextEditor(textEditorSelector, true); - await new Promise((r) => setTimeout(r, 0)); updateTextEditor(editor, "Excalidraw"); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(h.elements.length).toBe(3); expect(rectangle.boundElements).toStrictEqual([ diff --git a/packages/excalidraw/frame.test.tsx b/packages/excalidraw/frame.test.tsx index d9e0c5e3e9..010ed93434 100644 --- a/packages/excalidraw/frame.test.tsx +++ b/packages/excalidraw/frame.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import type { ExcalidrawElement } from "./element/types"; import { convertToExcalidrawElements, Excalidraw } from "./index"; import { API } from "./tests/helpers/api"; @@ -122,7 +123,7 @@ describe("adding elements to frames", () => { ) => { describe.skip("when frame is in a layer below", async () => { it("should add an element", async () => { - h.elements = [frame, rect2]; + API.setElements([frame, rect2]); func(frame, rect2); @@ -131,7 +132,7 @@ describe("adding elements to frames", () => { }); it("should add elements", async () => { - h.elements = [frame, rect2, rect3]; + API.setElements([frame, rect2, rect3]); func(frame, rect2); func(frame, rect3); @@ -142,7 +143,7 @@ describe("adding elements to frames", () => { }); it("should add elements when there are other other elements in between", async () => { - h.elements = [frame, rect1, rect2, rect4, rect3]; + API.setElements([frame, rect1, rect2, rect4, rect3]); func(frame, rect2); func(frame, rect3); @@ -153,7 +154,7 @@ describe("adding elements to frames", () => { }); it("should add elements when there are other elements in between and the order is reversed", async () => { - h.elements = [frame, rect3, rect4, rect2, rect1]; + API.setElements([frame, rect3, rect4, rect2, rect1]); func(frame, rect2); func(frame, rect3); @@ -166,7 +167,7 @@ describe("adding elements to frames", () => { describe.skip("when frame is in a layer above", async () => { it("should add an element", async () => { - h.elements = [rect2, frame]; + API.setElements([rect2, frame]); func(frame, rect2); @@ -175,7 +176,7 @@ describe("adding elements to frames", () => { }); it("should add elements", async () => { - h.elements = [rect2, rect3, frame]; + API.setElements([rect2, rect3, frame]); func(frame, rect2); func(frame, rect3); @@ -186,7 +187,7 @@ describe("adding elements to frames", () => { }); it("should add elements when there are other other elements in between", async () => { - h.elements = [rect1, rect2, rect4, rect3, frame]; + API.setElements([rect1, rect2, rect4, rect3, frame]); func(frame, rect2); func(frame, rect3); @@ -197,7 +198,7 @@ describe("adding elements to frames", () => { }); it("should add elements when there are other elements in between and the order is reversed", async () => { - h.elements = [rect3, rect4, rect2, rect1, frame]; + API.setElements([rect3, rect4, rect2, rect1, frame]); func(frame, rect2); func(frame, rect3); @@ -210,7 +211,7 @@ describe("adding elements to frames", () => { describe("when frame is in an inner layer", async () => { it.skip("should add elements", async () => { - h.elements = [rect2, frame, rect3]; + API.setElements([rect2, frame, rect3]); func(frame, rect2); func(frame, rect3); @@ -221,7 +222,7 @@ describe("adding elements to frames", () => { }); it.skip("should add elements when there are other other elements in between", async () => { - h.elements = [rect2, rect1, frame, rect4, rect3]; + API.setElements([rect2, rect1, frame, rect4, rect3]); func(frame, rect2); func(frame, rect3); @@ -232,7 +233,7 @@ describe("adding elements to frames", () => { }); it.skip("should add elements when there are other elements in between and the order is reversed", async () => { - h.elements = [rect3, rect4, frame, rect2, rect1]; + API.setElements([rect3, rect4, frame, rect2, rect1]); func(frame, rect2); func(frame, rect3); @@ -253,20 +254,22 @@ describe("adding elements to frames", () => { const frame = API.createElement({ type: "frame", x: 0, y: 0 }); - h.elements = reorderElements( - [ - frame, - ...convertToExcalidrawElements([ - { - type: containerType, - x: 100, - y: 100, - height: 10, - label: { text: "xx" }, - }, - ]), - ], - initialOrder, + API.setElements( + reorderElements( + [ + frame, + ...convertToExcalidrawElements([ + { + type: containerType, + x: 100, + y: 100, + height: 10, + label: { text: "xx" }, + }, + ]), + ], + initialOrder, + ), ); assertOrder(h.elements, initialOrder); @@ -337,7 +340,7 @@ describe("adding elements to frames", () => { }); it.skip("should add arrow bound with text when frame is in a layer below", async () => { - h.elements = [frame, arrow, text]; + API.setElements([frame, arrow, text]); resizeFrameOverElement(frame, arrow); @@ -347,7 +350,7 @@ describe("adding elements to frames", () => { }); it("should add arrow bound with text when frame is in a layer above", async () => { - h.elements = [arrow, text, frame]; + API.setElements([arrow, text, frame]); resizeFrameOverElement(frame, arrow); @@ -357,7 +360,7 @@ describe("adding elements to frames", () => { }); it.skip("should add arrow bound with text when frame is in an inner layer", async () => { - h.elements = [arrow, frame, text]; + API.setElements([arrow, frame, text]); resizeFrameOverElement(frame, arrow); @@ -369,7 +372,7 @@ describe("adding elements to frames", () => { describe("resizing frame over elements but downwards", async () => { it.skip("should add elements when frame is in a layer below", async () => { - h.elements = [frame, rect1, rect2, rect3, rect4]; + API.setElements([frame, rect1, rect2, rect3, rect4]); resizeFrameOverElement(frame, rect4); resizeFrameOverElement(frame, rect3); @@ -380,7 +383,7 @@ describe("adding elements to frames", () => { }); it.skip("should add elements when frame is in a layer above", async () => { - h.elements = [rect1, rect2, rect3, rect4, frame]; + API.setElements([rect1, rect2, rect3, rect4, frame]); resizeFrameOverElement(frame, rect4); resizeFrameOverElement(frame, rect3); @@ -391,7 +394,7 @@ describe("adding elements to frames", () => { }); it.skip("should add elements when frame is in an inner layer", async () => { - h.elements = [rect1, rect2, frame, rect3, rect4]; + API.setElements([rect1, rect2, frame, rect3, rect4]); resizeFrameOverElement(frame, rect4); resizeFrameOverElement(frame, rect3); @@ -406,7 +409,7 @@ describe("adding elements to frames", () => { await commonTestCases(dragElementIntoFrame); it.skip("should drag element inside, duplicate it and keep it in frame", () => { - h.elements = [frame, rect2]; + API.setElements([frame, rect2]); dragElementIntoFrame(frame, rect2); @@ -420,7 +423,7 @@ describe("adding elements to frames", () => { }); it.skip("should drag element inside, duplicate it and remove it from frame", () => { - h.elements = [frame, rect2]; + API.setElements([frame, rect2]); dragElementIntoFrame(frame, rect2); @@ -490,7 +493,7 @@ describe("adding elements to frames", () => { frameId: frame3.id, }); - h.elements = [ + API.setElements([ frame1, rectangle4, rectangle1, @@ -498,7 +501,7 @@ describe("adding elements to frames", () => { frame3, rectangle2, frame2, - ]; + ]); API.setSelectedElements([rectangle2]); @@ -541,7 +544,7 @@ describe("adding elements to frames", () => { frameId: frame2.id, }); - h.elements = [rectangle1, rectangle2, frame1, frame2]; + API.setElements([rectangle1, rectangle2, frame1, frame2]); API.setSelectedElements([rectangle2]); diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index 7742fd1913..498a679b3d 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -96,8 +96,9 @@ "@babel/preset-react": "7.24.1", "@babel/preset-typescript": "7.24.1", "@size-limit/preset-big-lib": "9.0.0", + "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "5.16.2", - "@testing-library/react": "12.1.5", + "@testing-library/react": "16.0.0", "@types/pako": "1.0.3", "@types/pica": "5.1.3", "@types/resize-observer-browser": "0.1.7", diff --git a/packages/excalidraw/tests/App.test.tsx b/packages/excalidraw/tests/App.test.tsx index 9fb055453a..df8ee15825 100644 --- a/packages/excalidraw/tests/App.test.tsx +++ b/packages/excalidraw/tests/App.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import * as StaticScene from "../renderer/staticScene"; import { reseed } from "../random"; diff --git a/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx index 8c5a2acd73..101c26e5bf 100644 --- a/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx +++ b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx @@ -1,4 +1,5 @@ -import { act, render, waitFor } from "./test-utils"; +import React from "react"; +import { render, waitFor } from "./test-utils"; import { Excalidraw } from "../index"; import { expect } from "vitest"; import { getTextEditor, updateTextEditor } from "./queries/dom"; @@ -103,19 +104,9 @@ describe("Test ", () => { expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull(); - expect(editor.textContent).toMatchInlineSnapshot(` - "flowchart TD - A[Christmas] -->|Get money| B(Go shopping) - B --> C{Let me think} - C -->|One| D[Laptop] - C -->|Two| E[iPhone] - C -->|Three| F[Car]" - `); + expect(editor.textContent).toMatchSnapshot(); - await act(async () => { - updateTextEditor(editor, "flowchart TD1"); - await new Promise((cb) => setTimeout(cb, 0)); - }); + updateTextEditor(editor, "flowchart TD1"); editor = await getTextEditor(selector, false); expect(editor.textContent).toBe("flowchart TD1"); diff --git a/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap index 1850f074f0..2943aeee71 100644 --- a/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap @@ -1,10 +1,19 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Test > should open mermaid popup when active tool is mermaid 1`] = ` -"" `; + +exports[`Test > should show error in preview when mermaid library throws error 1`] = ` +"flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[Car]" +`; diff --git a/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap index 7eb8e727f0..8ba8195197 100644 --- a/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap @@ -1,5 +1,17 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Test Linear Elements > Test bound text element > should bind text to arrow when clicked on arrow and enter pressed 1`] = ` +"Online whiteboard +collaboration made +easy" +`; + +exports[`Test Linear Elements > Test bound text element > should bind text to arrow when double clicked 1`] = ` +"Online whiteboard +collaboration made +easy" +`; + exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = `