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`] = ` -"Mermaid to ExcalidrawCurrently only Flowchart, Sequence, and Class Diagrams are supported. The other types will be rendered as image in Excalidraw.Mermaid Syntaxflowchart TD +"Mermaid to ExcalidrawCurrently only Flowchart, Sequence, and Class Diagrams are supported. The other types will be rendered as image in Excalidraw.Mermaid Syntaxflowchart 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]PreviewInsertCtrlEnter" `; + +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`] = ` Test bound text element > should match styles fo wrap="off" /> `; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 2`] = ` +"Online whiteboard +collaboration made +easy" +`; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 6`] = ` +"Online whiteboard +collaboration made easy" +`; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 2`] = ` +"Online whiteboard +collaboration made +easy" +`; + +exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 5`] = ` +"Online whiteboard +collaboration made easy" +`; + +exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 1`] = ` +"Online whiteboard +collaboration made easy" +`; + +exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = ` +"Online whiteboard +collaboration made +easy" +`; diff --git a/packages/excalidraw/tests/actionStyles.test.tsx b/packages/excalidraw/tests/actionStyles.test.tsx index b514295277..c7e497a164 100644 --- a/packages/excalidraw/tests/actionStyles.test.tsx +++ b/packages/excalidraw/tests/actionStyles.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { Excalidraw } from "../index"; import { CODES } from "../keys"; import { API } from "../tests/helpers/api"; diff --git a/packages/excalidraw/tests/align.test.tsx b/packages/excalidraw/tests/align.test.tsx index 25219e9f4e..63c329d64c 100644 --- a/packages/excalidraw/tests/align.test.tsx +++ b/packages/excalidraw/tests/align.test.tsx @@ -1,5 +1,6 @@ +import React from "react"; import ReactDOM from "react-dom"; -import { render } from "./test-utils"; +import { act, render } from "./test-utils"; import { Excalidraw } from "../index"; import { defaultLang, setLanguage } from "../i18n"; import { UI, Pointer, Keyboard } from "./helpers/ui"; @@ -15,8 +16,6 @@ import { actionAlignRight, } from "../actions"; -const { h } = window; - const mouse = new Pointer("mouse"); const createAndSelectTwoRectangles = () => { @@ -59,7 +58,9 @@ describe("aligning", () => { ReactDOM.unmountComponentAtNode(document.getElementById("root")!); mouse.reset(); - await setLanguage(defaultLang); + await act(() => { + return setLanguage(defaultLang); + }); await render(); }); @@ -156,7 +157,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[0].y).toEqual(0); expect(API.getSelectedElements()[1].y).toEqual(110); - h.app.actionManager.executeAction(actionAlignVerticallyCentered); + API.executeAction(actionAlignVerticallyCentered); // Check if x position did not change expect(API.getSelectedElements()[0].x).toEqual(0); @@ -175,7 +176,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[0].y).toEqual(0); expect(API.getSelectedElements()[1].y).toEqual(110); - h.app.actionManager.executeAction(actionAlignHorizontallyCentered); + API.executeAction(actionAlignHorizontallyCentered); expect(API.getSelectedElements()[0].x).toEqual(60); expect(API.getSelectedElements()[1].x).toEqual(55); @@ -201,7 +202,7 @@ describe("aligning", () => { mouse.click(); }); - h.app.actionManager.executeAction(actionGroup); + API.executeAction(actionGroup); mouse.reset(); UI.clickTool("rectangle"); @@ -222,7 +223,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[1].y).toEqual(100); expect(API.getSelectedElements()[2].y).toEqual(200); - h.app.actionManager.executeAction(actionAlignTop); + API.executeAction(actionAlignTop); expect(API.getSelectedElements()[0].y).toEqual(0); expect(API.getSelectedElements()[1].y).toEqual(100); @@ -236,7 +237,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[1].y).toEqual(100); expect(API.getSelectedElements()[2].y).toEqual(200); - h.app.actionManager.executeAction(actionAlignBottom); + API.executeAction(actionAlignBottom); expect(API.getSelectedElements()[0].y).toEqual(100); expect(API.getSelectedElements()[1].y).toEqual(200); @@ -250,7 +251,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[1].x).toEqual(100); expect(API.getSelectedElements()[2].x).toEqual(200); - h.app.actionManager.executeAction(actionAlignLeft); + API.executeAction(actionAlignLeft); expect(API.getSelectedElements()[0].x).toEqual(0); expect(API.getSelectedElements()[1].x).toEqual(100); @@ -264,7 +265,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[1].x).toEqual(100); expect(API.getSelectedElements()[2].x).toEqual(200); - h.app.actionManager.executeAction(actionAlignRight); + API.executeAction(actionAlignRight); expect(API.getSelectedElements()[0].x).toEqual(100); expect(API.getSelectedElements()[1].x).toEqual(200); @@ -278,7 +279,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[1].y).toEqual(100); expect(API.getSelectedElements()[2].y).toEqual(200); - h.app.actionManager.executeAction(actionAlignVerticallyCentered); + API.executeAction(actionAlignVerticallyCentered); expect(API.getSelectedElements()[0].y).toEqual(50); expect(API.getSelectedElements()[1].y).toEqual(150); @@ -292,7 +293,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[1].x).toEqual(100); expect(API.getSelectedElements()[2].x).toEqual(200); - h.app.actionManager.executeAction(actionAlignHorizontallyCentered); + API.executeAction(actionAlignHorizontallyCentered); expect(API.getSelectedElements()[0].x).toEqual(50); expect(API.getSelectedElements()[1].x).toEqual(150); @@ -315,7 +316,7 @@ describe("aligning", () => { mouse.click(); }); - h.app.actionManager.executeAction(actionGroup); + API.executeAction(actionGroup); mouse.reset(); UI.clickTool("rectangle"); @@ -331,7 +332,7 @@ describe("aligning", () => { mouse.click(); }); - h.app.actionManager.executeAction(actionGroup); + API.executeAction(actionGroup); // Select the first group. // The second group is already selected because it was the last group created @@ -349,7 +350,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].y).toEqual(200); expect(API.getSelectedElements()[3].y).toEqual(300); - h.app.actionManager.executeAction(actionAlignTop); + API.executeAction(actionAlignTop); expect(API.getSelectedElements()[0].y).toEqual(0); expect(API.getSelectedElements()[1].y).toEqual(100); @@ -365,7 +366,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].y).toEqual(200); expect(API.getSelectedElements()[3].y).toEqual(300); - h.app.actionManager.executeAction(actionAlignBottom); + API.executeAction(actionAlignBottom); expect(API.getSelectedElements()[0].y).toEqual(200); expect(API.getSelectedElements()[1].y).toEqual(300); @@ -381,7 +382,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].x).toEqual(200); expect(API.getSelectedElements()[3].x).toEqual(300); - h.app.actionManager.executeAction(actionAlignLeft); + API.executeAction(actionAlignLeft); expect(API.getSelectedElements()[0].x).toEqual(0); expect(API.getSelectedElements()[1].x).toEqual(100); @@ -397,7 +398,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].x).toEqual(200); expect(API.getSelectedElements()[3].x).toEqual(300); - h.app.actionManager.executeAction(actionAlignRight); + API.executeAction(actionAlignRight); expect(API.getSelectedElements()[0].x).toEqual(200); expect(API.getSelectedElements()[1].x).toEqual(300); @@ -413,7 +414,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].y).toEqual(200); expect(API.getSelectedElements()[3].y).toEqual(300); - h.app.actionManager.executeAction(actionAlignVerticallyCentered); + API.executeAction(actionAlignVerticallyCentered); expect(API.getSelectedElements()[0].y).toEqual(100); expect(API.getSelectedElements()[1].y).toEqual(200); @@ -429,7 +430,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].x).toEqual(200); expect(API.getSelectedElements()[3].x).toEqual(300); - h.app.actionManager.executeAction(actionAlignHorizontallyCentered); + API.executeAction(actionAlignHorizontallyCentered); expect(API.getSelectedElements()[0].x).toEqual(100); expect(API.getSelectedElements()[1].x).toEqual(200); @@ -454,7 +455,7 @@ describe("aligning", () => { }); // Create first group of rectangles - h.app.actionManager.executeAction(actionGroup); + API.executeAction(actionGroup); mouse.reset(); UI.clickTool("rectangle"); @@ -468,7 +469,7 @@ describe("aligning", () => { }); // Create the nested group - h.app.actionManager.executeAction(actionGroup); + API.executeAction(actionGroup); mouse.reset(); UI.clickTool("rectangle"); @@ -490,7 +491,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].y).toEqual(200); expect(API.getSelectedElements()[3].y).toEqual(300); - h.app.actionManager.executeAction(actionAlignTop); + API.executeAction(actionAlignTop); expect(API.getSelectedElements()[0].y).toEqual(0); expect(API.getSelectedElements()[1].y).toEqual(100); @@ -506,7 +507,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].y).toEqual(200); expect(API.getSelectedElements()[3].y).toEqual(300); - h.app.actionManager.executeAction(actionAlignBottom); + API.executeAction(actionAlignBottom); expect(API.getSelectedElements()[0].y).toEqual(100); expect(API.getSelectedElements()[1].y).toEqual(200); @@ -522,7 +523,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].x).toEqual(200); expect(API.getSelectedElements()[3].x).toEqual(300); - h.app.actionManager.executeAction(actionAlignLeft); + API.executeAction(actionAlignLeft); expect(API.getSelectedElements()[0].x).toEqual(0); expect(API.getSelectedElements()[1].x).toEqual(100); @@ -538,7 +539,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].x).toEqual(200); expect(API.getSelectedElements()[3].x).toEqual(300); - h.app.actionManager.executeAction(actionAlignRight); + API.executeAction(actionAlignRight); expect(API.getSelectedElements()[0].x).toEqual(100); expect(API.getSelectedElements()[1].x).toEqual(200); @@ -554,7 +555,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].y).toEqual(200); expect(API.getSelectedElements()[3].y).toEqual(300); - h.app.actionManager.executeAction(actionAlignVerticallyCentered); + API.executeAction(actionAlignVerticallyCentered); expect(API.getSelectedElements()[0].y).toEqual(50); expect(API.getSelectedElements()[1].y).toEqual(150); @@ -570,7 +571,7 @@ describe("aligning", () => { expect(API.getSelectedElements()[2].x).toEqual(200); expect(API.getSelectedElements()[3].x).toEqual(300); - h.app.actionManager.executeAction(actionAlignHorizontallyCentered); + API.executeAction(actionAlignHorizontallyCentered); expect(API.getSelectedElements()[0].x).toEqual(50); expect(API.getSelectedElements()[1].x).toEqual(150); diff --git a/packages/excalidraw/tests/appState.test.tsx b/packages/excalidraw/tests/appState.test.tsx index 251cd3cbc0..4a937cd569 100644 --- a/packages/excalidraw/tests/appState.test.tsx +++ b/packages/excalidraw/tests/appState.test.tsx @@ -1,5 +1,5 @@ -import { queryByTestId, render, waitFor } from "./test-utils"; - +import React from "react"; +import { fireEvent, queryByTestId, render, waitFor } from "./test-utils"; import { Excalidraw } from "../index"; import { API } from "./helpers/api"; import { getDefaultAppState } from "../appState"; @@ -31,7 +31,7 @@ describe("appState", () => { expect(h.state.viewBackgroundColor).toBe("#F00"); }); - API.drop( + await API.drop( new Blob( [ JSON.stringify({ @@ -69,7 +69,7 @@ describe("appState", () => { UI.clickTool("text"); expect(h.state.currentItemFontSize).toBe(30); - queryByTestId(container, "fontSize-small")!.click(); + fireEvent.click(queryByTestId(container, "fontSize-small")!); expect(h.state.currentItemFontSize).toBe(16); const mouse = new Pointer("mouse"); diff --git a/packages/excalidraw/tests/binding.test.tsx b/packages/excalidraw/tests/binding.test.tsx index 3209043b0b..54ad8006b1 100644 --- a/packages/excalidraw/tests/binding.test.tsx +++ b/packages/excalidraw/tests/binding.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { fireEvent, render } from "./test-utils"; import { Excalidraw, isLinearElement } from "../index"; import { UI, Pointer, Keyboard } from "./helpers/ui"; @@ -37,7 +38,7 @@ describe("element binding", () => { [100, 0], ], }); - h.elements = [rect, arrow]; + API.setElements([rect, arrow]); expect(arrow.startBinding).toBe(null); // select arrow @@ -225,7 +226,7 @@ describe("element binding", () => { height: 100, }); - h.elements = [text]; + API.setElements([text]); const arrow = UI.createElement("arrow", { x: 0, @@ -267,7 +268,7 @@ describe("element binding", () => { height: 100, }); - h.elements = [text]; + API.setElements([text]); const arrow = UI.createElement("arrow", { x: 0, @@ -362,13 +363,13 @@ describe("element binding", () => { ], }); - h.elements = [rectangle1, arrow1, arrow2, text1]; + API.setElements([rectangle1, arrow1, arrow2, text1]); API.setSelectedElements([text1]); expect(h.state.selectedElementIds[text1.id]).toBe(true); - h.app.actionManager.executeAction(actionWrapTextInContainer); + API.executeAction(actionWrapTextInContainer); // new text container will be placed before the text element const container = h.elements.at(-2)!; diff --git a/packages/excalidraw/tests/clipboard.test.tsx b/packages/excalidraw/tests/clipboard.test.tsx index ca10a8528e..63e26dbc39 100644 --- a/packages/excalidraw/tests/clipboard.test.tsx +++ b/packages/excalidraw/tests/clipboard.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { vi } from "vitest"; import ReactDOM from "react-dom"; import { render, waitFor, GlobalTestState } from "./test-utils"; @@ -279,7 +280,7 @@ describe("pasting & frames", () => { }); const rect = API.createElement({ type: "rectangle" }); - h.elements = [frame]; + API.setElements([frame]); const clipboardJSON = await serializeAsClipboardJSON({ elements: [rect], @@ -318,7 +319,7 @@ describe("pasting & frames", () => { y: 100, }); - h.elements = [frame]; + API.setElements([frame]); const clipboardJSON = await serializeAsClipboardJSON({ elements: [rect, rect2], @@ -361,7 +362,7 @@ describe("pasting & frames", () => { groupIds: ["g1"], }); - h.elements = [frame]; + API.setElements([frame]); const clipboardJSON = await serializeAsClipboardJSON({ elements: [rect, rect2], @@ -412,7 +413,7 @@ describe("pasting & frames", () => { frameId: frame2.id, }); - h.elements = [frame]; + API.setElements([frame]); const clipboardJSON = await serializeAsClipboardJSON({ elements: [rect, rect2, frame2], diff --git a/packages/excalidraw/tests/contextmenu.test.tsx b/packages/excalidraw/tests/contextmenu.test.tsx index 9a9bb50afa..e37c26b9d6 100644 --- a/packages/excalidraw/tests/contextmenu.test.tsx +++ b/packages/excalidraw/tests/contextmenu.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { render, @@ -159,7 +160,7 @@ describe("contextMenu element", () => { width: 200, backgroundColor: "red", }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); API.setSelectedElements([rect1]); // lower z-index @@ -607,7 +608,7 @@ describe("contextMenu element", () => { fillStyle: "solid", groupIds: ["g1"], }); - h.elements = [rectangle1, rectangle2]; + API.setElements([rectangle1, rectangle2]); mouse.rightClickAt(50, 50); expect(API.getSelectedElements().length).toBe(2); diff --git a/packages/excalidraw/tests/dragCreate.test.tsx b/packages/excalidraw/tests/dragCreate.test.tsx index 933c96ef7c..7d0bcb2775 100644 --- a/packages/excalidraw/tests/dragCreate.test.tsx +++ b/packages/excalidraw/tests/dragCreate.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { Excalidraw } from "../index"; import * as StaticScene from "../renderer/staticScene"; diff --git a/packages/excalidraw/tests/elementLocking.test.tsx b/packages/excalidraw/tests/elementLocking.test.tsx index 908d22cb1d..ab096e53e8 100644 --- a/packages/excalidraw/tests/elementLocking.test.tsx +++ b/packages/excalidraw/tests/elementLocking.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { Excalidraw } from "../index"; import { render } from "../tests/test-utils"; @@ -16,7 +17,7 @@ const h = window.h; describe("element locking", () => { beforeEach(async () => { await render(); - h.elements = []; + API.setElements([]); }); it("click-selecting a locked element is disabled", () => { @@ -28,7 +29,7 @@ describe("element locking", () => { locked: true, }); - h.elements = [lockedRectangle]; + API.setElements([lockedRectangle]); mouse.clickAt(50, 50); expect(API.getSelectedElements().length).toBe(0); @@ -45,7 +46,7 @@ describe("element locking", () => { y: 100, }); - h.elements = [lockedRectangle]; + API.setElements([lockedRectangle]); mouse.downAt(50, 50); mouse.moveTo(250, 250); @@ -62,7 +63,7 @@ describe("element locking", () => { locked: true, }); - h.elements = [lockedRectangle]; + API.setElements([lockedRectangle]); mouse.downAt(50, 50); mouse.moveTo(100, 100); @@ -85,7 +86,7 @@ describe("element locking", () => { locked: true, }); - h.elements = [rectangle, lockedRectangle]; + API.setElements([rectangle, lockedRectangle]); mouse.downAt(50, 50); mouse.moveTo(100, 100); @@ -97,11 +98,11 @@ describe("element locking", () => { }); it("selectAll shouldn't select locked elements", () => { - h.elements = [ + API.setElements([ API.createElement({ type: "rectangle" }), API.createElement({ type: "rectangle", locked: true }), - ]; - h.app.actionManager.executeAction(actionSelectAll); + ]); + API.executeAction(actionSelectAll); expect(API.getSelectedElements().length).toBe(1); }); @@ -120,7 +121,7 @@ describe("element locking", () => { locked: true, }); - h.elements = [rectangle, lockedRectangle]; + API.setElements([rectangle, lockedRectangle]); expect(API.getSelectedElements().length).toBe(0); mouse.clickAt(50, 50); expect(API.getSelectedElements().length).toBe(1); @@ -142,7 +143,7 @@ describe("element locking", () => { locked: true, }); - h.elements = [rectangle, lockedRectangle]; + API.setElements([rectangle, lockedRectangle]); expect(API.getSelectedElements().length).toBe(0); mouse.rightClickAt(50, 50); expect(API.getSelectedElements().length).toBe(1); @@ -172,7 +173,7 @@ describe("element locking", () => { locked: true, }); - h.elements = [rectangle, lockedRectangle]; + API.setElements([rectangle, lockedRectangle]); API.setSelectedElements([rectangle]); expect(API.getSelectedElements().length).toBe(1); expect(API.getSelectedElement().id).toBe(rectangle.id); @@ -203,7 +204,7 @@ describe("element locking", () => { y: 200, }); - h.elements = [rectangle, lockedRectangle]; + API.setElements([rectangle, lockedRectangle]); mouse.clickAt(250, 250); expect(API.getSelectedElements().length).toBe(0); @@ -228,7 +229,7 @@ describe("element locking", () => { containerId: container.id, locked: true, }); - h.elements = [container, text]; + API.setElements([container, text]); API.setSelectedElements([container]); Keyboard.keyPress(KEYS.ENTER); expect(h.state.editingElement?.id).not.toBe(text.id); @@ -245,7 +246,7 @@ describe("element locking", () => { height: 100, locked: true, }); - h.elements = [text]; + API.setElements([text]); UI.clickTool("text"); mouse.clickAt(text.x + 50, text.y + 50); const editor = document.querySelector( @@ -267,7 +268,7 @@ describe("element locking", () => { height: 100, locked: true, }); - h.elements = [text]; + API.setElements([text]); UI.clickTool("selection"); mouse.doubleClickAt(text.x + 50, text.y + 50); const editor = document.querySelector( @@ -298,7 +299,7 @@ describe("element locking", () => { boundElements: [{ id: text.id, type: "text" }], }); - h.elements = [container, text]; + API.setElements([container, text]); UI.clickTool("selection"); mouse.clickAt(container.x + 10, container.y + 10); @@ -338,7 +339,7 @@ describe("element locking", () => { mutateElement(container, { boundElements: [{ id: text.id, type: "text" }], }); - h.elements = [container, text]; + API.setElements([container, text]); UI.clickTool("selection"); mouse.doubleClickAt(container.width / 2, container.height / 2); @@ -372,7 +373,7 @@ describe("element locking", () => { mutateElement(container, { boundElements: [{ id: text.id, type: "text" }], }); - h.elements = [container, text]; + API.setElements([container, text]); UI.clickTool("text"); mouse.clickAt(container.width / 2, container.height / 2); diff --git a/packages/excalidraw/tests/excalidraw.test.tsx b/packages/excalidraw/tests/excalidraw.test.tsx index be7efc914c..9de22f9f66 100644 --- a/packages/excalidraw/tests/excalidraw.test.tsx +++ b/packages/excalidraw/tests/excalidraw.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils"; import { Excalidraw, Footer, MainMenu } from "../index"; import { queryByText, queryByTestId } from "@testing-library/react"; diff --git a/packages/excalidraw/tests/export.test.tsx b/packages/excalidraw/tests/export.test.tsx index 3c299fbd1a..35429d9b4f 100644 --- a/packages/excalidraw/tests/export.test.tsx +++ b/packages/excalidraw/tests/export.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { render, waitFor } from "./test-utils"; import { Excalidraw } from "../index"; import { API } from "./helpers/api"; @@ -51,7 +52,7 @@ describe("export", () => { blob: pngBlob, metadata: serializeAsJSON(testElements, h.state, {}, "local"), }); - API.drop(pngBlobEmbedded); + await API.drop(pngBlobEmbedded); await waitFor(() => { expect(h.elements).toEqual([ @@ -71,7 +72,7 @@ describe("export", () => { }); it("import embedded png (legacy v1)", async () => { - API.drop(await API.loadFile("./fixtures/test_embedded_v1.png")); + await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png")); await waitFor(() => { expect(h.elements).toEqual([ expect.objectContaining({ type: "text", text: "test" }), @@ -80,7 +81,7 @@ describe("export", () => { }); it("import embedded png (v2)", async () => { - API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png")); + await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png")); await waitFor(() => { expect(h.elements).toEqual([ expect.objectContaining({ type: "text", text: "😀" }), @@ -89,7 +90,7 @@ describe("export", () => { }); it("import embedded svg (legacy v1)", async () => { - API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg")); + await API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg")); await waitFor(() => { expect(h.elements).toEqual([ expect.objectContaining({ type: "text", text: "test" }), @@ -98,7 +99,7 @@ describe("export", () => { }); it("import embedded svg (v2)", async () => { - API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg")); + await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg")); await waitFor(() => { expect(h.elements).toEqual([ expect.objectContaining({ type: "text", text: "😀" }), diff --git a/packages/excalidraw/tests/fitToContent.test.tsx b/packages/excalidraw/tests/fitToContent.test.tsx index c7a280f0eb..a4f03910bd 100644 --- a/packages/excalidraw/tests/fitToContent.test.tsx +++ b/packages/excalidraw/tests/fitToContent.test.tsx @@ -1,4 +1,5 @@ -import { render } from "./test-utils"; +import React from "react"; +import { act, render } from "./test-utils"; import { API } from "./helpers/api"; import { Excalidraw } from "../index"; @@ -6,6 +7,17 @@ import { vi } from "vitest"; const { h } = window; +const waitForNextAnimationFrame = () => { + return act( + () => + new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + }), + ); +}; + describe("fitToContent", () => { it("should zoom to fit the selected element", async () => { await render(); @@ -22,7 +34,9 @@ describe("fitToContent", () => { expect(h.state.zoom.value).toBe(1); - h.app.scrollToContent(rectElement, { fitToContent: true }); + act(() => { + h.app.scrollToContent(rectElement, { fitToContent: true }); + }); // element is 10x taller than the viewport size, // zoom should be at least 1/10 @@ -51,8 +65,10 @@ describe("fitToContent", () => { expect(h.state.zoom.value).toBe(1); - h.app.scrollToContent([topLeft, bottomRight], { - fitToContent: true, + act(() => { + h.app.scrollToContent([topLeft, bottomRight], { + fitToContent: true, + }); }); // elements take 100x100, which is 10x bigger than the viewport size, @@ -77,7 +93,9 @@ describe("fitToContent", () => { expect(h.state.scrollX).toBe(0); expect(h.state.scrollY).toBe(0); - h.app.scrollToContent(rectElement); + act(() => { + h.app.scrollToContent(rectElement); + }); // zoom level should stay the same expect(h.state.zoom.value).toBe(1); @@ -88,14 +106,6 @@ describe("fitToContent", () => { }); }); -const waitForNextAnimationFrame = () => { - return new Promise((resolve) => { - requestAnimationFrame(() => { - requestAnimationFrame(resolve); - }); - }); -}; - describe("fitToContent animated", () => { beforeEach(() => { vi.spyOn(window, "requestAnimationFrame"); @@ -118,7 +128,9 @@ describe("fitToContent animated", () => { y: -100, }); - h.app.scrollToContent(rectElement, { animate: true }); + act(() => { + h.app.scrollToContent(rectElement, { animate: true }); + }); expect(window.requestAnimationFrame).toHaveBeenCalled(); @@ -157,7 +169,9 @@ describe("fitToContent animated", () => { expect(h.state.scrollX).toBe(0); expect(h.state.scrollY).toBe(0); - h.app.scrollToContent(rectElement, { animate: true, fitToContent: true }); + act(() => { + h.app.scrollToContent(rectElement, { animate: true, fitToContent: true }); + }); expect(window.requestAnimationFrame).toHaveBeenCalled(); diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx index 164615f771..8aedac46ff 100644 --- a/packages/excalidraw/tests/flip.test.tsx +++ b/packages/excalidraw/tests/flip.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { fireEvent, @@ -19,7 +20,6 @@ import type { } from "../element/types"; import { newLinearElement } from "../element"; import { Excalidraw } from "../index"; -import { mutateElement } from "../element/mutateElement"; import type { NormalizedZoomValue } from "../types"; import { ROUNDNESS } from "../constants"; import { vi } from "vitest"; @@ -54,7 +54,7 @@ beforeEach(async () => { elementFromPoint: () => GlobalTestState.canvas, }); await render(); - h.setState({ + API.setAppState({ zoom: { value: 1 as NormalizedZoomValue, }, @@ -204,14 +204,14 @@ const checkElementsBoundingBox = async ( const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => { const originalElement = cloneJSON(h.elements[0]); - h.app.actionManager.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipHorizontal); const newElement = h.elements[0]; await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); }; const checkTwoPointsLineHorizontalFlip = async () => { const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement; - h.app.actionManager.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipHorizontal); const newElement = h.elements[0] as ExcalidrawLinearElement; await waitFor(() => { expect(originalElement.points[0][0]).toBeCloseTo( @@ -235,7 +235,7 @@ const checkTwoPointsLineHorizontalFlip = async () => { const checkTwoPointsLineVerticalFlip = async () => { const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement; - h.app.actionManager.executeAction(actionFlipVertical); + API.executeAction(actionFlipVertical); const newElement = h.elements[0] as ExcalidrawLinearElement; await waitFor(() => { expect(originalElement.points[0][0]).toBeCloseTo( @@ -262,7 +262,7 @@ const checkRotatedHorizontalFlip = async ( toleranceInPx: number = 0.00001, ) => { const originalElement = cloneJSON(h.elements[0]); - h.app.actionManager.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipHorizontal); const newElement = h.elements[0]; await waitFor(() => { expect(newElement.angle).toBeCloseTo(expectedAngle); @@ -275,7 +275,7 @@ const checkRotatedVerticalFlip = async ( toleranceInPx: number = 0.00001, ) => { const originalElement = cloneJSON(h.elements[0]); - h.app.actionManager.executeAction(actionFlipVertical); + API.executeAction(actionFlipVertical); const newElement = h.elements[0]; await waitFor(() => { expect(newElement.angle).toBeCloseTo(expectedAngle); @@ -286,7 +286,7 @@ const checkRotatedVerticalFlip = async ( const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => { const originalElement = cloneJSON(h.elements[0]); - h.app.actionManager.executeAction(actionFlipVertical); + API.executeAction(actionFlipVertical); const newElement = h.elements[0]; await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); @@ -295,8 +295,8 @@ const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => { const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => { const originalElement = cloneJSON(h.elements[0]); - h.app.actionManager.executeAction(actionFlipHorizontal); - h.app.actionManager.executeAction(actionFlipVertical); + API.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipVertical); const newElement = h.elements[0]; await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); @@ -309,7 +309,6 @@ const MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 20; describe("rectangle", () => { it("flips an unrotated rectangle horizontally correctly", async () => { createAndSelectOneRectangle(); - await checkHorizontalFlip(); }); @@ -408,8 +407,8 @@ describe("ellipse", () => { describe("arrow", () => { it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => { const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.elements = [arrow]; - h.app.setState({ selectedElementIds: { [arrow.id]: true } }); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); await checkHorizontalFlip( MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, ); @@ -417,8 +416,8 @@ describe("arrow", () => { it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => { const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.elements = [arrow]; - h.app.setState({ selectedElementIds: { [arrow.id]: true } }); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); await checkVerticalFlip(50); }); @@ -427,12 +426,14 @@ describe("arrow", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.elements = [line]; - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [line.id]: true, - }; - mutateElement(line, { + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { angle: originalAngle, }); @@ -446,12 +447,14 @@ describe("arrow", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); - h.elements = [line]; - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [line.id]: true, - }; - mutateElement(line, { + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { angle: originalAngle, }); @@ -464,8 +467,8 @@ describe("arrow", () => { //TODO: elements with curve outside minMax points have a wrong bounding box!!! it.skip("flips an unrotated arrow horizontally with line outside min/max points bounds", async () => { const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); - h.elements = [arrow]; - h.app.setState({ selectedElementIds: { [arrow.id]: true } }); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); await checkHorizontalFlip( MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, @@ -477,9 +480,9 @@ describe("arrow", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); - mutateElement(line, { angle: originalAngle }); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkRotatedVerticalFlip( expectedAngle, @@ -490,8 +493,8 @@ describe("arrow", () => { //TODO: elements with curve outside minMax points have a wrong bounding box!!! it.skip("flips an unrotated arrow vertically with line outside min/max points bounds", async () => { const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); - h.elements = [arrow]; - h.app.setState({ selectedElementIds: { [arrow.id]: true } }); + API.setElements([arrow]); + API.setAppState({ selectedElementIds: { [arrow.id]: true } }); await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); }); @@ -501,9 +504,9 @@ describe("arrow", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); - mutateElement(line, { angle: originalAngle }); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkRotatedVerticalFlip( expectedAngle, @@ -538,8 +541,8 @@ describe("arrow", () => { describe("line", () => { it("flips an unrotated line horizontally with line inside min/max points bounds", async () => { const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkHorizontalFlip( MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, @@ -548,8 +551,8 @@ describe("line", () => { it("flips an unrotated line vertically with line inside min/max points bounds", async () => { const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); }); @@ -563,8 +566,8 @@ describe("line", () => { //TODO: elements with curve outside minMax points have a wrong bounding box it.skip("flips an unrotated line horizontally with line outside min/max points bounds", async () => { const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkHorizontalFlip( MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, @@ -574,8 +577,8 @@ describe("line", () => { //TODO: elements with curve outside minMax points have a wrong bounding box it.skip("flips an unrotated line vertically with line outside min/max points bounds", async () => { const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); }); @@ -585,9 +588,9 @@ describe("line", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); - mutateElement(line, { angle: originalAngle }); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkRotatedHorizontalFlip( expectedAngle, @@ -600,9 +603,9 @@ describe("line", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); - mutateElement(line, { angle: originalAngle }); - h.elements = [line]; - h.app.setState({ selectedElementIds: { [line.id]: true } }); + API.updateElement(line, { angle: originalAngle }); + API.setElements([line]); + API.setAppState({ selectedElementIds: { [line.id]: true } }); await checkRotatedVerticalFlip( expectedAngle, @@ -619,12 +622,14 @@ describe("line", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.elements = [line]; - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [line.id]: true, - }; - mutateElement(line, { + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { angle: originalAngle, }); @@ -638,12 +643,14 @@ describe("line", () => { const originalAngle = Math.PI / 4; const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("line"); - h.elements = [line]; - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [line.id]: true, - }; - mutateElement(line, { + API.setElements([line]); + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [line.id]: true, + }, + }); + API.updateElement(line, { angle: originalAngle, }); @@ -669,20 +676,24 @@ describe("freedraw", () => { it("flips an unrotated drawing horizontally correctly", async () => { const draw = createAndReturnOneDraw(); // select draw, since not done automatically - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [draw.id]: true, - }; + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); await checkHorizontalFlip(); }); it("flips an unrotated drawing vertically correctly", async () => { const draw = createAndReturnOneDraw(); // select draw, since not done automatically - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [draw.id]: true, - }; + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); await checkVerticalFlip(); }); @@ -692,10 +703,12 @@ describe("freedraw", () => { const draw = createAndReturnOneDraw(originalAngle); // select draw, since not done automatically - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [draw.id]: true, - }; + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); await checkRotatedHorizontalFlip(expectedAngle); }); @@ -706,10 +719,12 @@ describe("freedraw", () => { const draw = createAndReturnOneDraw(originalAngle); // select draw, since not done automatically - h.state.selectedElementIds = { - ...h.state.selectedElementIds, - [draw.id]: true, - }; + API.setAppState({ + selectedElementIds: { + ...h.state.selectedElementIds, + [draw.id]: true, + }, + }); await checkRotatedVerticalFlip(expectedAngle); }); @@ -767,7 +782,7 @@ describe("image", () => { expect(API.getSelectedElements()[0].type).toEqual("image"); expect(h.app.files.fileId).toBeDefined(); }); - mutateElement(h.elements[0], { + API.updateElement(h.elements[0], { angle: originalAngle, }); await checkRotatedHorizontalFlip(expectedAngle); @@ -786,7 +801,7 @@ describe("image", () => { expect(API.getSelectedElements()[0].type).toEqual("image"); expect(h.app.files.fileId).toBeDefined(); }); - mutateElement(h.elements[0], { + API.updateElement(h.elements[0], { angle: originalAngle, }); @@ -827,8 +842,7 @@ describe("mutliple elements", () => { ".excalidraw-textEditorContainer > textarea", )!; fireEvent.input(editor, { target: { value: "arrow" } }); - await new Promise((resolve) => setTimeout(resolve, 0)); - Keyboard.keyPress(KEYS.ESCAPE); + Keyboard.exitTextEditor(editor); const rectangle = UI.createElement("rectangle", { x: 0, @@ -842,12 +856,11 @@ describe("mutliple elements", () => { ".excalidraw-textEditorContainer > textarea", )!; fireEvent.input(editor, { target: { value: "rect\ntext" } }); - await new Promise((resolve) => setTimeout(resolve, 0)); - Keyboard.keyPress(KEYS.ESCAPE); + Keyboard.exitTextEditor(editor); mouse.select([arrow, rectangle]); - h.app.actionManager.executeAction(actionFlipHorizontal); - h.app.actionManager.executeAction(actionFlipVertical); + API.executeAction(actionFlipHorizontal); + API.executeAction(actionFlipVertical); const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer; const arrowTextPos = getBoundTextElementPosition( diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 3b83df8464..53d66152e9 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -13,7 +13,7 @@ import type { import { newElement, newTextElement, newLinearElement } from "../../element"; import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants"; import { getDefaultAppState } from "../../appState"; -import { GlobalTestState, createEvent, fireEvent } from "../test-utils"; +import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils"; import fs from "fs"; import util from "util"; import path from "path"; @@ -27,12 +27,15 @@ import { newImageElement, newMagicFrameElement, } from "../../element/newElement"; -import type { Point } from "../../types"; +import type { AppState, Point } from "../../types"; import { getSelectedElements } from "../../scene/selection"; import { isLinearElementType } from "../../element/typeChecks"; import type { Mutable } from "../../utility-types"; import { assertNever } from "../../utils"; +import type App from "../../components/App"; import { createTestHook } from "../../components/App"; +import type { Action } from "../../actions/types"; +import { mutateElement } from "../../element/mutateElement"; const readFile = util.promisify(fs.readFile); // so that window.h is available when App.tsx is not imported as well. @@ -41,12 +44,42 @@ createTestHook(); const { h } = window; export class API { + static updateScene: InstanceType["updateScene"] = (...args) => { + act(() => { + h.app.updateScene(...args); + }); + }; + static setAppState: React.Component["setState"] = ( + state, + cb, + ) => { + act(() => { + h.setState(state, cb); + }); + }; + + static setElements = (elements: readonly ExcalidrawElement[]) => { + act(() => { + h.elements = elements; + }); + }; + static setSelectedElements = (elements: ExcalidrawElement[]) => { - h.setState({ - selectedElementIds: elements.reduce((acc, element) => { - acc[element.id] = true; - return acc; - }, {} as Record), + act(() => { + h.setState({ + selectedElementIds: elements.reduce((acc, element) => { + acc[element.id] = true; + return acc; + }, {} as Record), + }); + }); + }; + + static updateElement = ( + ...[element, updates]: Parameters + ) => { + act(() => { + mutateElement(element, updates); }); }; @@ -85,8 +118,10 @@ export class API { }; static clearSelection = () => { - // @ts-ignore - h.app.clearSelection(null); + act(() => { + // @ts-ignore + h.app.clearSelection(null); + }); expect(API.getSelectedElements().length).toBe(0); }; @@ -361,6 +396,12 @@ export class API { }, }, }); - fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent); + await fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent); + }; + + static executeAction = (action: Action) => { + act(() => { + h.app.actionManager.executeAction(action); + }); }; } diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index c5c8974216..98426eb4c4 100644 --- a/packages/excalidraw/tests/helpers/ui.ts +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -20,7 +20,7 @@ import { type TransformHandleDirection, } from "../../element/transformHandles"; import { KEYS } from "../../keys"; -import { fireEvent, GlobalTestState, screen } from "../test-utils"; +import { act, fireEvent, GlobalTestState, screen } from "../test-utils"; import { mutateElement } from "../../element/mutateElement"; import { API } from "./api"; import { @@ -125,6 +125,10 @@ export class Keyboard { Keyboard.keyPress("z"); }); }; + + static exitTextEditor = (textarea: HTMLTextAreaElement) => { + fireEvent.keyDown(textarea, { key: KEYS.ESCAPE }); + }; } const getElementPointForSelection = (element: ExcalidrawElement): Point => { @@ -299,14 +303,16 @@ const transform = ( keyboardModifiers: KeyboardModifiers = {}, ) => { const elements = Array.isArray(element) ? element : [element]; - h.setState({ - selectedElementIds: elements.reduce( - (acc, e) => ({ - ...acc, - [e.id]: true, - }), - {}, - ), + act(() => { + h.setState({ + selectedElementIds: elements.reduce( + (acc, e) => ({ + ...acc, + [e.id]: true, + }), + {}, + ), + }); }); let handleCoords: TransformHandle | undefined; if (elements.length === 1) { @@ -487,7 +493,9 @@ export class UI { const origElement = h.elements[h.elements.length - 1] as any; if (angle !== 0) { - mutateElement(origElement, { angle }); + act(() => { + mutateElement(origElement, { angle }); + }); } return proxy(origElement); @@ -511,8 +519,9 @@ export class UI { } fireEvent.input(editor, { target: { value: text } }); - await new Promise((resolve) => setTimeout(resolve, 0)); - editor.blur(); + act(() => { + editor.blur(); + }); return isTextElement(element) ? element @@ -523,6 +532,14 @@ export class UI { ); } + static updateInput = (input: HTMLInputElement, value: string | number) => { + act(() => { + input.focus(); + fireEvent.change(input, { target: { value: String(value) } }); + input.blur(); + }); + }; + static resize( element: ExcalidrawElement | ExcalidrawElement[], handle: TransformHandleDirection, diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 1abfb4d102..dcbfb34fdf 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -1,5 +1,5 @@ -import "../global.d.ts"; import React from "react"; +import "../global.d.ts"; import * as StaticScene from "../renderer/staticScene"; import { GlobalTestState, @@ -16,8 +16,8 @@ import { fireEvent, queryByTestId, waitFor } from "@testing-library/react"; import { createUndoAction, createRedoAction } from "../actions/actionHistory"; import { actionToggleViewMode } from "../actions/actionToggleViewMode"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; -import type { AppState, ExcalidrawImperativeAPI } from "../types"; -import { arrayToMap, resolvablePromise } from "../utils"; +import type { AppState } from "../types"; +import { arrayToMap } from "../utils"; import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, @@ -95,7 +95,7 @@ describe("history", () => { await render(); const rect = API.createElement({ type: "rectangle" }); - h.elements = [rect]; + API.setElements([rect]); const corrupedEntry = HistoryEntry.create( AppStateChange.empty(), @@ -158,7 +158,7 @@ describe("history", () => { const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] }); const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); mouse.select(rect1); assertSelectedElements([rect1, rect2]); expect(h.state.selectedGroupIds).toEqual({ A: true }); @@ -173,19 +173,12 @@ describe("history", () => { }); it("should not end up with history entry when there are no elements changes", async () => { - const excalidrawAPIPromise = resolvablePromise(); - await render( - excalidrawAPIPromise.resolve(api as any)} - handleKeyboardGlobally={true} - />, - ); - const excalidrawAPI = await excalidrawAPIPromise; + await render(); const rect1 = API.createElement({ type: "rectangle" }); const rect2 = API.createElement({ type: "rectangle" }); - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect1, rect2], storeAction: StoreAction.CAPTURE, }); @@ -197,7 +190,7 @@ describe("history", () => { expect.objectContaining({ id: rect2.id, isDeleted: false }), ]); - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect1, rect2], storeAction: StoreAction.CAPTURE, // even though the flag is on, same elements are passed, nothing to commit }); @@ -447,7 +440,7 @@ describe("history", () => { const undoAction = createUndoAction(h.history, h.store); const redoAction = createRedoAction(h.history, h.store); // noop - act(() => h.app.actionManager.executeAction(undoAction)); + API.executeAction(undoAction); expect(h.elements).toEqual([ expect.objectContaining({ id: "A", isDeleted: false }), ]); @@ -456,21 +449,21 @@ describe("history", () => { expect.objectContaining({ id: "A" }), expect.objectContaining({ id: rectangle.id }), ]); - act(() => h.app.actionManager.executeAction(undoAction)); + API.executeAction(undoAction); expect(h.elements).toEqual([ expect.objectContaining({ id: "A", isDeleted: false }), expect.objectContaining({ id: rectangle.id, isDeleted: true }), ]); // noop - act(() => h.app.actionManager.executeAction(undoAction)); + API.executeAction(undoAction); expect(h.elements).toEqual([ expect.objectContaining({ id: "A", isDeleted: false }), expect.objectContaining({ id: rectangle.id, isDeleted: true }), ]); expect(API.getUndoStack().length).toBe(0); - act(() => h.app.actionManager.executeAction(redoAction)); + API.executeAction(redoAction); expect(h.elements).toEqual([ expect.objectContaining({ id: "A", isDeleted: false }), expect.objectContaining({ id: rectangle.id, isDeleted: false }), @@ -495,7 +488,7 @@ describe("history", () => { expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]), ); - API.drop( + await API.drop( new Blob( [ JSON.stringify({ @@ -523,7 +516,7 @@ describe("history", () => { const undoAction = createUndoAction(h.history, h.store); const redoAction = createRedoAction(h.history, h.store); - act(() => h.app.actionManager.executeAction(undoAction)); + API.executeAction(undoAction); expect(API.getSnapshot()).toEqual([ expect.objectContaining({ id: "A", isDeleted: false }), @@ -535,7 +528,7 @@ describe("history", () => { ]); expect(h.state.viewBackgroundColor).toBe("#FFF"); - act(() => h.app.actionManager.executeAction(redoAction)); + API.executeAction(redoAction); expect(h.state.viewBackgroundColor).toBe("#000"); expect(API.getSnapshot()).toEqual([ expect.objectContaining({ id: "A", isDeleted: true }), @@ -548,10 +541,8 @@ describe("history", () => { }); it("should support appstate name or viewBackgroundColor change", async () => { - const excalidrawAPIPromise = resolvablePromise(); await render( excalidrawAPIPromise.resolve(api as any)} handleKeyboardGlobally={true} initialData={{ appState: { @@ -561,9 +552,11 @@ describe("history", () => { }} />, ); - const excalidrawAPI = await excalidrawAPIPromise; - excalidrawAPI.updateScene({ + expect(h.state.isLoading).toBe(false); + expect(h.state.name).toBe("Old name"); + + API.updateScene({ appState: { name: "New name", }, @@ -574,7 +567,7 @@ describe("history", () => { expect(API.getRedoStack().length).toBe(0); expect(h.state.name).toBe("New name"); - excalidrawAPI.updateScene({ + API.updateScene({ appState: { viewBackgroundColor: "#000", }, @@ -586,7 +579,7 @@ describe("history", () => { expect(h.state.viewBackgroundColor).toBe("#000"); // just to double check that same change is not recorded - excalidrawAPI.updateScene({ + API.updateScene({ appState: { name: "New name", viewBackgroundColor: "#000", @@ -1060,7 +1053,7 @@ describe("history", () => { x: 100, }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); mouse.select(rect1); assertSelectedElements([rect1, rect2]); expect(API.getUndoStack().length).toBe(1); @@ -1203,7 +1196,7 @@ describe("history", () => { const rect2 = UI.createElement("rectangle", { x: 20, y: 20 }); const rect3 = UI.createElement("rectangle", { x: 40, y: 40 }); - act(() => h.app.actionManager.executeAction(actionSendBackward)); + API.executeAction(actionSendBackward); expect(API.getUndoStack().length).toBe(4); expect(API.getRedoStack().length).toBe(0); @@ -1234,7 +1227,7 @@ describe("history", () => { expect(API.getRedoStack().length).toBe(0); assertSelectedElements([rect1, rect3]); - act(() => h.app.actionManager.executeAction(actionBringForward)); + API.executeAction(actionBringForward); expect(API.getUndoStack().length).toBe(7); expect(API.getRedoStack().length).toBe(0); @@ -1262,8 +1255,6 @@ describe("history", () => { }); describe("should support bidirectional bindings", async () => { - let excalidrawAPI: ExcalidrawImperativeAPI; - let rect1: ExcalidrawGenericElement; let rect2: ExcalidrawGenericElement; let text: ExcalidrawTextElement; @@ -1292,22 +1283,13 @@ describe("history", () => { } as const; beforeEach(async () => { - const excalidrawAPIPromise = - resolvablePromise(); - - await render( - excalidrawAPIPromise.resolve(api as any)} - handleKeyboardGlobally={true} - />, - ); - excalidrawAPI = await excalidrawAPIPromise; + await render(); rect1 = API.createElement({ ...rect1Props }); text = API.createElement({ ...textProps }); rect2 = API.createElement({ ...rect2Props }); - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect1, text, rect2], storeAction: StoreAction.CAPTURE, }); @@ -1758,14 +1740,14 @@ describe("history", () => { expect(undoButton).not.toBeDisabled(); expect(redoButton).toBeDisabled(); - act(() => h.app.actionManager.executeAction(undoAction)); + API.executeAction(undoAction); expect(h.history.isUndoStackEmpty).toBeTruthy(); expect(h.history.isRedoStackEmpty).toBeFalsy(); expect(undoButton).toBeDisabled(); expect(redoButton).not.toBeDisabled(); - act(() => h.app.actionManager.executeAction(redoAction)); + API.executeAction(redoAction); expect(h.history.isUndoStackEmpty).toBeFalsy(); expect(h.history.isRedoStackEmpty).toBeTruthy(); @@ -1807,13 +1789,13 @@ describe("history", () => { expect(queryByTestId(container, "button-undo")).not.toBeDisabled(); expect(queryByTestId(container, "button-redo")).toBeDisabled(); - act(() => h.app.actionManager.executeAction(actionToggleViewMode)); + API.executeAction(actionToggleViewMode); expect(h.state.viewModeEnabled).toBe(true); expect(queryByTestId(container, "button-undo")).toBeNull(); expect(queryByTestId(container, "button-redo")).toBeNull(); - act(() => h.app.actionManager.executeAction(actionToggleViewMode)); + API.executeAction(actionToggleViewMode); expect(h.state.viewModeEnabled).toBe(false); await waitFor(() => { @@ -1824,20 +1806,20 @@ describe("history", () => { // testing redo button // ----------------------------------------------------------------------- - act(() => h.app.actionManager.executeAction(undoAction)); + API.executeAction(undoAction); expect(h.history.isUndoStackEmpty).toBeTruthy(); expect(h.history.isRedoStackEmpty).toBeFalsy(); expect(queryByTestId(container, "button-undo")).toBeDisabled(); expect(queryByTestId(container, "button-redo")).not.toBeDisabled(); - act(() => h.app.actionManager.executeAction(actionToggleViewMode)); + API.executeAction(actionToggleViewMode); expect(h.state.viewModeEnabled).toBe(true); expect(queryByTestId(container, "button-undo")).toBeNull(); expect(queryByTestId(container, "button-redo")).toBeNull(); - act(() => h.app.actionManager.executeAction(actionToggleViewMode)); + API.executeAction(actionToggleViewMode); expect(h.state.viewModeEnabled).toBe(false); expect(h.history.isUndoStackEmpty).toBeTruthy(); @@ -1848,8 +1830,6 @@ describe("history", () => { }); describe("multiplayer undo/redo", () => { - let excalidrawAPI: ExcalidrawImperativeAPI; - // Util to check that we end up in the same state after series of undo / redo function runTwice(callback: () => void) { for (let i = 0; i < 2; i++) { @@ -1858,15 +1838,9 @@ describe("history", () => { } beforeEach(async () => { - const excalidrawAPIPromise = resolvablePromise(); await render( - excalidrawAPIPromise.resolve(api as any)} - handleKeyboardGlobally={true} - isCollaborating={true} - />, + , ); - excalidrawAPI = await excalidrawAPIPromise; }); it("should not override remote changes on different elements", async () => { @@ -1881,7 +1855,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ ...h.elements, API.createElement({ @@ -1921,7 +1895,7 @@ describe("history", () => { expect(API.getUndoStack().length).toBe(2); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { strokeColor: yellow, @@ -1969,7 +1943,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { backgroundColor: yellow, @@ -1985,7 +1959,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { backgroundColor: violet, @@ -2034,13 +2008,13 @@ describe("history", () => { elbowed: true, }); - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect, diamond], storeAction: StoreAction.CAPTURE, }); // Connect the arrow - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ { ...rect, @@ -2090,7 +2064,7 @@ describe("history", () => { Keyboard.undo(); - excalidrawAPI.updateScene({ + API.updateScene({ elements: h.elements.map((el) => el.id === "KPrBI4g_v9qUB1XxYLgSz" ? { @@ -2122,13 +2096,13 @@ describe("history", () => { const rect2 = API.createElement({ type: "rectangle" }); // Initialize scene - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect1, rect2], storeAction: StoreAction.UPDATE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { groupIds: ["A"] }), newElementWith(h.elements[1], { groupIds: ["A"] }), @@ -2140,7 +2114,7 @@ describe("history", () => { const rect4 = API.createElement({ type: "rectangle", groupIds: ["B"] }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { groupIds: ["A", "B"] }), newElementWith(h.elements[1], { groupIds: ["A", "B"] }), @@ -2181,7 +2155,7 @@ describe("history", () => { Keyboard.keyPress(KEYS.ENTER); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0] as ExcalidrawLinearElement, { points: [ @@ -2281,7 +2255,7 @@ describe("history", () => { ]); // Simulate remote update & restore - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { backgroundColor: yellow, @@ -2358,7 +2332,7 @@ describe("history", () => { ]); // Simulate remote update & deletion - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { backgroundColor: yellow, @@ -2417,7 +2391,7 @@ describe("history", () => { expect(API.getUndoStack().length).toBe(5); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[0], newElementWith(h.elements[1], { @@ -2482,7 +2456,7 @@ describe("history", () => { const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 }); const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 }); - h.elements = [rect1, rect2, rect3]; + API.setElements([rect1, rect2, rect3]); mouse.select(rect1); mouse.select([rect2, rect3]); @@ -2493,7 +2467,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[0], newElementWith(h.elements[1], { @@ -2532,7 +2506,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[0], newElementWith(h.elements[1], { @@ -2586,7 +2560,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect1, rect2], storeAction: StoreAction.UPDATE, }); @@ -2596,7 +2570,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [h.elements[0], h.elements[1], rect3, rect4], storeAction: StoreAction.UPDATE, }); @@ -2610,7 +2584,7 @@ describe("history", () => { expect(h.state.selectedGroupIds).toEqual({ A: true, B: true }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { isDeleted: true, @@ -2635,7 +2609,7 @@ describe("history", () => { Keyboard.undo(); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { isDeleted: false, @@ -2653,7 +2627,7 @@ describe("history", () => { expect(h.state.selectedGroupIds).toEqual({ A: true }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [h.elements[0], h.elements[1], rect3, rect4], storeAction: StoreAction.UPDATE, }); @@ -2676,7 +2650,7 @@ describe("history", () => { x: 100, }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); mouse.select(rect1); // inside the editing group @@ -2692,7 +2666,7 @@ describe("history", () => { expect(h.state.editingGroupId).toBeNull(); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { isDeleted: true, @@ -2715,7 +2689,7 @@ describe("history", () => { expect(h.state.editingGroupId).toBeNull(); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { isDeleted: false, @@ -2759,7 +2733,7 @@ describe("history", () => { expect(h.state.selectedLinearElement).toBeNull(); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { isDeleted: true, @@ -2786,11 +2760,11 @@ describe("history", () => { const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 }); // b "a1" const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 }); // c "a2" - h.elements = [rect1, rect2, rect3]; + API.setElements([rect1, rect2, rect3]); mouse.select(rect2); - act(() => h.app.actionManager.executeAction(actionSendToBack)); + API.executeAction(actionSendToBack); expect(API.getUndoStack().length).toBe(2); expect(API.getRedoStack().length).toBe(0); @@ -2802,7 +2776,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[2], { index: "Zy" as FractionalIndex }), h.elements[0], @@ -2841,7 +2815,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[2], { index: "Zx" as FractionalIndex }), h.elements[0], @@ -2876,11 +2850,11 @@ describe("history", () => { const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 }); const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 }); - h.elements = [rect1, rect2, rect3]; + API.setElements([rect1, rect2, rect3]); mouse.select(rect2); - act(() => h.app.actionManager.executeAction(actionSendToBack)); + API.executeAction(actionSendToBack); expect(API.getUndoStack().length).toBe(2); expect(API.getRedoStack().length).toBe(0); @@ -2892,7 +2866,7 @@ describe("history", () => { ]); // Simulate remote update (fixes all invalid z-indices) - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[2], // rect3 h.elements[0], // rect2 @@ -2922,7 +2896,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[1], // rect2 h.elements[0], // rect3 @@ -2956,7 +2930,7 @@ describe("history", () => { const rect = API.createElement({ ...rectProps }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [...h.elements, rect], storeAction: StoreAction.UPDATE, }); @@ -3008,7 +2982,7 @@ describe("history", () => { const rect3 = API.createElement({ ...rect3Props }); // // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [...h.elements, rect3], storeAction: StoreAction.UPDATE, }); @@ -3098,7 +3072,7 @@ describe("history", () => { const rect3 = API.createElement({ ...rect3Props }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [...h.elements, rect3], storeAction: StoreAction.UPDATE, }); @@ -3275,13 +3249,13 @@ describe("history", () => { it("should rebind bindings when both are updated through the history and there no conflicting updates in the meantime", async () => { // Initialize the scene - excalidrawAPI.updateScene({ + API.updateScene({ elements: [container, text], storeAction: StoreAction.UPDATE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: text.id, type: "text" }], @@ -3310,7 +3284,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { // no conflicting updates @@ -3362,13 +3336,13 @@ describe("history", () => { // TODO: #7348 we do rebind now, when we have bi-directional binding in history, to eliminate potential data-integrity issues, but we should consider not rebinding in the future it("should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime", async () => { // Initialize the scene - excalidrawAPI.updateScene({ + API.updateScene({ elements: [container, text], storeAction: StoreAction.UPDATE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: text.id, type: "text" }], @@ -3403,7 +3377,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: remoteText.id, type: "text" }], @@ -3465,13 +3439,13 @@ describe("history", () => { // TODO: #7348 we do rebind now, when we have bi-directional binding in history, to eliminate potential data-integrity issues, but we should consider not rebinding in the future it("should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime", async () => { // Initialize the scene - excalidrawAPI.updateScene({ + API.updateScene({ elements: [container, text], storeAction: StoreAction.UPDATE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: text.id, type: "text" }], @@ -3507,7 +3481,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[0], newElementWith(remoteContainer, { @@ -3573,13 +3547,13 @@ describe("history", () => { it("should rebind remotely added bound text when it's container is added through the history", async () => { // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [container], storeAction: StoreAction.CAPTURE, }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: text.id, type: "text" }], @@ -3634,13 +3608,13 @@ describe("history", () => { it("should rebind remotely added container when it's bound text is added through the history", async () => { // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [text], storeAction: StoreAction.CAPTURE, }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(container, { boundElements: [{ id: text.id, type: "text" }], @@ -3694,13 +3668,13 @@ describe("history", () => { it("should preserve latest remotely added binding and unbind previous one when the container is added through the history", async () => { // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [container], storeAction: StoreAction.CAPTURE, }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: text.id, type: "text" }], @@ -3736,7 +3710,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: remoteText.id, type: "text" }], @@ -3801,13 +3775,13 @@ describe("history", () => { it("should preserve latest remotely added binding and unbind previous one when the text is added through history", async () => { // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [text], storeAction: StoreAction.CAPTURE, }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(container, { boundElements: [{ id: text.id, type: "text" }], @@ -3843,7 +3817,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: remoteText.id, type: "text" }], @@ -3907,13 +3881,13 @@ describe("history", () => { it("should unbind remotely deleted bound text from container when the container is added through the history", async () => { // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [container], storeAction: StoreAction.CAPTURE, }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: text.id, type: "text" }], @@ -3964,13 +3938,13 @@ describe("history", () => { it("should unbind remotely deleted container from bound text when the text is added through the history", async () => { // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [text], storeAction: StoreAction.CAPTURE, }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(container, { boundElements: [{ id: text.id, type: "text" }], @@ -4021,13 +3995,13 @@ describe("history", () => { it("should redraw remotely added bound text when it's container is updated through the history", async () => { // Initialize the scene - excalidrawAPI.updateScene({ + API.updateScene({ elements: [container], storeAction: StoreAction.UPDATE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { x: 200, @@ -4041,7 +4015,7 @@ describe("history", () => { Keyboard.undo(); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { boundElements: [{ id: text.id, type: "text" }], @@ -4139,13 +4113,13 @@ describe("history", () => { // TODO: #7348 this leads to empty undo/redo and could be confusing - instead we might consider redrawing container based on the text dimensions it("should redraw bound text to match container dimensions when the bound text is updated through the history", async () => { // Initialize the scene - excalidrawAPI.updateScene({ + API.updateScene({ elements: [text], storeAction: StoreAction.UPDATE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { x: 205, @@ -4159,7 +4133,7 @@ describe("history", () => { Keyboard.undo(); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(container, { boundElements: [{ id: text.id, type: "text" }], @@ -4257,7 +4231,7 @@ describe("history", () => { rect2 = API.createElement({ ...rect2Props }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect1, rect2], storeAction: StoreAction.CAPTURE, }); @@ -4333,7 +4307,7 @@ describe("history", () => { ]); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { // no conflicting updates @@ -4478,7 +4452,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[0], newElementWith(h.elements[1], { boundElements: [] }), @@ -4589,7 +4563,7 @@ describe("history", () => { }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ arrow, newElementWith(h.elements[0], { @@ -4674,13 +4648,13 @@ describe("history", () => { }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [arrow], storeAction: StoreAction.CAPTURE, }); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0] as ExcalidrawLinearElement, { startBinding: { @@ -4833,7 +4807,7 @@ describe("history", () => { ); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[0], newElementWith(h.elements[1], { x: 500, y: -500 }), @@ -4909,19 +4883,19 @@ describe("history", () => { it("should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history ", async () => { // Initialize the scene - excalidrawAPI.updateScene({ + API.updateScene({ elements: [frame], storeAction: StoreAction.UPDATE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [rect, h.elements[0]], storeAction: StoreAction.CAPTURE, }); // Simulate local update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ newElementWith(h.elements[0], { frameId: frame.id, @@ -4965,7 +4939,7 @@ describe("history", () => { Keyboard.undo(); // Simulate remote update - excalidrawAPI.updateScene({ + API.updateScene({ elements: [ h.elements[0], newElementWith(h.elements[1], { diff --git a/packages/excalidraw/tests/library.test.tsx b/packages/excalidraw/tests/library.test.tsx index 449eca1ba9..51b139ce37 100644 --- a/packages/excalidraw/tests/library.test.tsx +++ b/packages/excalidraw/tests/library.test.tsx @@ -1,6 +1,7 @@ +import React from "react"; import { vi } from "vitest"; import { fireEvent, render, waitFor } from "./test-utils"; -import { queryByTestId } from "@testing-library/react"; +import { act, queryByTestId } from "@testing-library/react"; import { Excalidraw } from "../index"; import { API } from "./helpers/api"; @@ -43,7 +44,9 @@ vi.mock("../data/filesystem.ts", async (importOriginal) => { describe("library", () => { beforeEach(async () => { await render(); - h.app.library.resetLibrary(); + await act(() => { + return h.app.library.resetLibrary(); + }); }); it("import library via drag&drop", async () => { @@ -208,7 +211,7 @@ describe("library menu", () => { "dropdown-menu-button", )!, ); - queryByTestId(container, "lib-dropdown--load")!.click(); + fireEvent.click(queryByTestId(container, "lib-dropdown--load")!); const libraryItems = parseLibraryJSON(await libraryJSONPromise); diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx index 02e58d209b..2d896a1b81 100644 --- a/packages/excalidraw/tests/linearElementEditor.test.tsx +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import type { ExcalidrawElement, @@ -17,7 +18,7 @@ import { API } from "../tests/helpers/api"; import type { Point } from "../types"; import { KEYS } from "../keys"; import { LinearElementEditor } from "../element/linearElementEditor"; -import { queryByTestId, queryByText } from "@testing-library/react"; +import { act, queryByTestId, queryByText } from "@testing-library/react"; import { getBoundTextElementPosition, wrapText, @@ -27,7 +28,6 @@ import * as textElementUtils from "../element/textElement"; import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { vi } from "vitest"; import { arrayToMap } from "../utils"; -import React from "react"; const renderInteractiveScene = vi.spyOn( InteractiveCanvas, @@ -80,7 +80,7 @@ describe("Test Linear Elements", () => { ], roundness, }); - h.elements = [line]; + API.setElements([line]); mouse.clickAt(p1[0], p1[1]); return line; @@ -108,7 +108,7 @@ describe("Test Linear Elements", () => { roundness, }); mutateElement(line, { points: line.points }); - h.elements = [line]; + API.setElements([line]); mouse.clickAt(p1[0], p1[1]); return line; }; @@ -786,7 +786,7 @@ describe("Test Linear Elements", () => { it("in-editor dragging a line point covered by another element", () => { createTwoPointerLinearElement("line"); const line = h.elements[0] as ExcalidrawLinearElement; - h.elements = [ + API.setElements([ line, API.createElement({ type: "rectangle", @@ -797,7 +797,7 @@ describe("Test Linear Elements", () => { backgroundColor: "red", fillStyle: "solid", }), - ]; + ]); const dragEndPositionOffset = [100, 100] as const; API.setSelectedElements([line]); enterLineEditingMode(line, true); @@ -854,7 +854,7 @@ describe("Test Linear Elements", () => { } }); const updatedTextElement = { ...textElement, originalText: text }; - h.elements = [...elements, updatedTextElement]; + API.setElements([...elements, updatedTextElement]); return { textElement: updatedTextElement, container }; }; @@ -968,17 +968,13 @@ describe("Test Linear Elements", () => { target: { value: DEFAULT_TEXT }, }); - await new Promise((r) => setTimeout(r, 0)); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(arrow.boundElements).toStrictEqual([ { id: text.id, type: "text" }, ]); - expect((h.elements[1] as ExcalidrawTextElementWithContainer).text) - .toMatchInlineSnapshot(` - "Online whiteboard - collaboration made - easy" - `); + expect( + (h.elements[1] as ExcalidrawTextElementWithContainer).text, + ).toMatchSnapshot(); }); it("should bind text to arrow when clicked on arrow and enter pressed", async () => { @@ -998,21 +994,16 @@ describe("Test Linear Elements", () => { ".excalidraw-textEditorContainer > textarea", ) as HTMLTextAreaElement; - await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: DEFAULT_TEXT }, }); - editor.blur(); + Keyboard.exitTextEditor(editor); expect(arrow.boundElements).toStrictEqual([ { id: textElement.id, type: "text" }, ]); - expect((h.elements[1] as ExcalidrawTextElementWithContainer).text) - .toMatchInlineSnapshot(` - "Online whiteboard - collaboration made - easy" - `); + expect( + (h.elements[1] as ExcalidrawTextElementWithContainer).text, + ).toMatchSnapshot(); }); it("should not bind text to line when double clicked", async () => { @@ -1059,11 +1050,7 @@ describe("Test Linear Elements", () => { "y": 60, } `); - expect(textElement.text).toMatchInlineSnapshot(` - "Online whiteboard - collaboration made - easy" - `); + expect(textElement.text).toMatchSnapshot(); expect( LinearElementEditor.getElementAbsoluteCoords( container, @@ -1103,11 +1090,9 @@ describe("Test Linear Elements", () => { "y": 45, } `); - expect((h.elements[1] as ExcalidrawTextElementWithContainer).text) - .toMatchInlineSnapshot(` - "Online whiteboard - collaboration made easy" - `); + expect( + (h.elements[1] as ExcalidrawTextElementWithContainer).text, + ).toMatchSnapshot(); expect( LinearElementEditor.getElementAbsoluteCoords( container, @@ -1143,11 +1128,7 @@ describe("Test Linear Elements", () => { "y": 10, } `); - expect(textElement.text).toMatchInlineSnapshot(` - "Online whiteboard - collaboration made - easy" - `); + expect(textElement.text).toMatchSnapshot(); const points = LinearElementEditor.getPointsGlobalCoordinates( container, elementsMap, @@ -1171,10 +1152,7 @@ describe("Test Linear Elements", () => { "y": -5, } `); - expect(textElement.text).toMatchInlineSnapshot(` - "Online whiteboard - collaboration made easy" - `); + expect(textElement.text).toMatchSnapshot(); }); it("should not render vertical align tool when element selected", () => { @@ -1207,9 +1185,8 @@ describe("Test Linear Elements", () => { const editor = document.querySelector( ".excalidraw-textEditorContainer > textarea", ) as HTMLTextAreaElement; - await new Promise((r) => setTimeout(r, 0)); fireEvent.change(editor, { target: { value: DEFAULT_TEXT } }); - editor.blur(); + Keyboard.exitTextEditor(editor); const textElement = h.elements[2] as ExcalidrawTextElementWithContainer; @@ -1223,10 +1200,7 @@ describe("Test Linear Elements", () => { font, getBoundTextMaxWidth(arrow, null), ), - ).toMatchInlineSnapshot(` - "Online whiteboard - collaboration made easy" - `); + ).toMatchSnapshot(); const handleBindTextResizeSpy = vi.spyOn( textElementUtils, "handleBindTextResize", @@ -1252,11 +1226,7 @@ describe("Test Linear Elements", () => { font, getBoundTextMaxWidth(arrow, null), ), - ).toMatchInlineSnapshot(` - "Online whiteboard - collaboration made - easy" - `); + ).toMatchSnapshot(); }); it("should not render horizontal align tool when element selected", () => { @@ -1280,7 +1250,7 @@ describe("Test Linear Elements", () => { expect(text.x).toBe(0); expect(text.y).toBe(0); - h.elements = [h.elements[0], text]; + API.setElements([h.elements[0], text]); const container = h.elements[0]; API.setSelectedElements([container, text]); @@ -1358,20 +1328,25 @@ describe("Test Linear Elements", () => { const line = createThreePointerLinearElement("arrow"); const [origStartX, origStartY] = [line.x, line.y]; - LinearElementEditor.movePoints( - line, - [ - { index: 0, point: [line.points[0][0] + 10, line.points[0][1] + 10] }, - { - index: line.points.length - 1, - point: [ - line.points[line.points.length - 1][0] - 10, - line.points[line.points.length - 1][1] - 10, - ], - }, - ], - h.scene, - ); + act(() => { + LinearElementEditor.movePoints( + line, + [ + { + index: 0, + point: [line.points[0][0] + 10, line.points[0][1] + 10], + }, + { + index: line.points.length - 1, + point: [ + line.points[line.points.length - 1][0] - 10, + line.points[line.points.length - 1][1] - 10, + ], + }, + ], + h.scene, + ); + }); expect(line.x).toBe(origStartX + 10); expect(line.y).toBe(origStartY + 10); diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index 4951a7090b..c693c57b37 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -1,5 +1,6 @@ +import React from "react"; import ReactDOM from "react-dom"; -import { render, fireEvent } from "./test-utils"; +import { render, fireEvent, act } from "./test-utils"; import { Excalidraw } from "../index"; import * as StaticScene from "../renderer/staticScene"; import * as InteractiveCanvas from "../renderer/interactiveScene"; @@ -80,22 +81,24 @@ describe("move element", () => { const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 }); const elementsMap = h.app.scene.getNonDeletedElementsMap(); - // bind line to two rectangles - bindOrUnbindLinearElement( - arrow.get() as NonDeleted, - rectA.get() as ExcalidrawRectangleElement, - rectB.get() as ExcalidrawRectangleElement, - elementsMap, - {} as Scene, - ); + act(() => { + // bind line to two rectangles + bindOrUnbindLinearElement( + arrow.get() as NonDeleted, + rectA.get() as ExcalidrawRectangleElement, + rectB.get() as ExcalidrawRectangleElement, + elementsMap, + {} as Scene, + ); + }); // select the second rectangle new Pointer("mouse").clickOn(rectB); expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot( - `20`, + `19`, ); - expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`17`); + expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`16`); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(3); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); diff --git a/packages/excalidraw/tests/multiPointCreate.test.tsx b/packages/excalidraw/tests/multiPointCreate.test.tsx index 0bfa4f1477..ecbc8344d2 100644 --- a/packages/excalidraw/tests/multiPointCreate.test.tsx +++ b/packages/excalidraw/tests/multiPointCreate.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { render, diff --git a/packages/excalidraw/tests/packages/events.test.tsx b/packages/excalidraw/tests/packages/events.test.tsx index 09c734f280..16a938e63c 100644 --- a/packages/excalidraw/tests/packages/events.test.tsx +++ b/packages/excalidraw/tests/packages/events.test.tsx @@ -1,9 +1,11 @@ +import React from "react"; import { vi } from "vitest"; import { Excalidraw, StoreAction } from "../../index"; import type { ExcalidrawImperativeAPI } from "../../types"; import { resolvablePromise } from "../../utils"; import { render } from "../test-utils"; import { Pointer } from "../helpers/ui"; +import { API } from "../helpers/api"; describe("event callbacks", () => { const h = window.h; @@ -27,7 +29,7 @@ describe("event callbacks", () => { const origBackgroundColor = h.state.viewBackgroundColor; excalidrawAPI.onChange(onChange); - excalidrawAPI.updateScene({ + API.updateScene({ appState: { viewBackgroundColor: "red" }, storeAction: StoreAction.CAPTURE, }); diff --git a/packages/excalidraw/tests/queries/dom.ts b/packages/excalidraw/tests/queries/dom.ts index e3494edfb8..2725108fbd 100644 --- a/packages/excalidraw/tests/queries/dom.ts +++ b/packages/excalidraw/tests/queries/dom.ts @@ -15,5 +15,5 @@ export const updateTextEditor = ( value: string, ) => { fireEvent.change(editor, { target: { value } }); - editor.dispatchEvent(new Event("input")); + fireEvent.input(editor); }; diff --git a/packages/excalidraw/tests/regressionTests.test.tsx b/packages/excalidraw/tests/regressionTests.test.tsx index ec1e51af2d..c7ca332119 100644 --- a/packages/excalidraw/tests/regressionTests.test.tsx +++ b/packages/excalidraw/tests/regressionTests.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import type { ExcalidrawElement } from "../element/types"; import { CODES, KEYS } from "../keys"; @@ -55,7 +56,7 @@ beforeEach(async () => { finger2.reset(); await render(); - h.setState({ height: 768, width: 1024 }); + API.setAppState({ height: 768, width: 1024 }); }); afterEach(() => { @@ -757,7 +758,7 @@ describe("regression tests", () => { width: 500, height: 500, }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); mouse.select(rect1); @@ -793,7 +794,7 @@ describe("regression tests", () => { width: 500, height: 500, }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); mouse.select(rect1); diff --git a/packages/excalidraw/tests/resize.test.tsx b/packages/excalidraw/tests/resize.test.tsx index 67e19621d4..34e189cee0 100644 --- a/packages/excalidraw/tests/resize.test.tsx +++ b/packages/excalidraw/tests/resize.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { render } from "./test-utils"; import { reseed } from "../random"; @@ -537,7 +538,7 @@ describe("text element", () => { describe("image element", () => { it("resizes", async () => { const image = API.createElement({ type: "image", width: 100, height: 100 }); - h.elements = [image]; + API.setElements([image]); UI.resize(image, "ne", [-20, -30]); expect(image.x).toBeCloseTo(0); @@ -550,7 +551,7 @@ describe("image element", () => { it("flips while resizing", async () => { const image = API.createElement({ type: "image", width: 100, height: 100 }); - h.elements = [image]; + API.setElements([image]); UI.resize(image, "sw", [150, -150]); expect(image.x).toBeCloseTo(100); @@ -563,7 +564,7 @@ describe("image element", () => { it("resizes with locked/unlocked aspect ratio", async () => { const image = API.createElement({ type: "image", width: 100, height: 100 }); - h.elements = [image]; + API.setElements([image]); UI.resize(image, "ne", [30, -20]); expect(image.x).toBeCloseTo(0); @@ -581,7 +582,7 @@ describe("image element", () => { it("resizes from center", async () => { const image = API.createElement({ type: "image", width: 100, height: 100 }); - h.elements = [image]; + API.setElements([image]); UI.resize(image, "nw", [25, 15], { alt: true }); expect(image.x).toBeCloseTo(15); @@ -598,7 +599,7 @@ describe("image element", () => { width: 100, height: 100, }); - h.elements = [image]; + API.setElements([image]); const arrow = UI.createElement("arrow", { x: -30, y: 50, @@ -971,7 +972,7 @@ describe("multiple selection", () => { width: 120, height: 80, }); - h.elements = [topImage, bottomImage]; + API.setElements([topImage, bottomImage]); const selectionWidth = 200; const selectionHeight = 230; @@ -1043,7 +1044,7 @@ describe("multiple selection", () => { height: 100, angle: (Math.PI * 7) / 6, }); - h.elements = [image]; + API.setElements([image]); const line = UI.createElement("line", { x: 60, diff --git a/packages/excalidraw/tests/rotate.test.tsx b/packages/excalidraw/tests/rotate.test.tsx index 34752bd959..ee4576fcd2 100644 --- a/packages/excalidraw/tests/rotate.test.tsx +++ b/packages/excalidraw/tests/rotate.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { render } from "./test-utils"; import { reseed } from "../random"; diff --git a/packages/excalidraw/tests/scene/export.test.ts b/packages/excalidraw/tests/scene/export.test.ts index db017da26d..28fff1e5e7 100644 --- a/packages/excalidraw/tests/scene/export.test.ts +++ b/packages/excalidraw/tests/scene/export.test.ts @@ -88,7 +88,7 @@ describe("exportToSvg", () => { ); expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot( - '"_themeFilter_1883f3"', + `"_themeFilter_1883f3"`, ); }); diff --git a/packages/excalidraw/tests/scroll.test.tsx b/packages/excalidraw/tests/scroll.test.tsx index 9ad7757fe9..1a3f80c145 100644 --- a/packages/excalidraw/tests/scroll.test.tsx +++ b/packages/excalidraw/tests/scroll.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { mockBoundingClientRect, render, @@ -98,13 +99,13 @@ describe("appState", () => { const zoom = h.state.zoom.value; // Assert we scroll properly when zoomed in - h.setState({ zoom: { value: (zoom * 1.1) as typeof zoom } }); + API.setAppState({ zoom: { value: (zoom * 1.1) as typeof zoom } }); scrollTest(); // Assert we scroll properly when zoomed out - h.setState({ zoom: { value: (zoom * 0.9) as typeof zoom } }); + API.setAppState({ zoom: { value: (zoom * 0.9) as typeof zoom } }); scrollTest(); // Assert we scroll properly with normal zoom - h.setState({ zoom: { value: zoom } }); + API.setAppState({ zoom: { value: zoom } }); scrollTest(); restoreOriginalGetBoundingClientRect(); }); diff --git a/packages/excalidraw/tests/selection.test.tsx b/packages/excalidraw/tests/selection.test.tsx index 18e0dfe2a3..d47f3037f7 100644 --- a/packages/excalidraw/tests/selection.test.tsx +++ b/packages/excalidraw/tests/selection.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ReactDOM from "react-dom"; import { render, @@ -59,7 +60,7 @@ describe("box-selection", () => { height: 50, }); - h.elements = [rect1, rect2]; + API.setElements([rect1, rect2]); mouse.downAt(175, -20); mouse.moveTo(85, 70); @@ -87,7 +88,7 @@ describe("box-selection", () => { fillStyle: "solid", }); - h.elements = [rect1]; + API.setElements([rect1]); mouse.downAt(75, -20); mouse.moveTo(-15, 70); @@ -132,7 +133,7 @@ describe("inner box-selection", () => { width: 50, height: 50, }); - h.elements = [rect1, rect2, rect3]; + API.setElements([rect1, rect2, rect3]); Keyboard.withModifierKeys({ ctrl: true }, () => { mouse.downAt(40, 40); mouse.moveTo(290, 290); @@ -168,7 +169,7 @@ describe("inner box-selection", () => { height: 50, groupIds: ["A"], }); - h.elements = [rect1, rect2, rect3]; + API.setElements([rect1, rect2, rect3]); Keyboard.withModifierKeys({ ctrl: true }, () => { mouse.downAt(40, 40); @@ -206,7 +207,7 @@ describe("inner box-selection", () => { height: 50, groupIds: ["A"], }); - h.elements = [rect1, rect2, rect3]; + API.setElements([rect1, rect2, rect3]); Keyboard.withModifierKeys({ ctrl: true }, () => { mouse.downAt(rect2.x - 20, rect2.y - 20); mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10); @@ -506,7 +507,7 @@ describe("selectedElementIds stability", () => { height: 10, }); - h.elements = [rectangle]; + API.setElements([rectangle]); const selectedElementIds_1 = h.state.selectedElementIds; diff --git a/packages/excalidraw/tests/shortcuts.test.tsx b/packages/excalidraw/tests/shortcuts.test.tsx index 4da160fee3..e02ed01938 100644 --- a/packages/excalidraw/tests/shortcuts.test.tsx +++ b/packages/excalidraw/tests/shortcuts.test.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { KEYS } from "../keys"; import { Excalidraw } from "../index"; import { API } from "./helpers/api"; diff --git a/packages/excalidraw/tests/test-utils.ts b/packages/excalidraw/tests/test-utils.ts index 607b5c35a9..42cc7784fa 100644 --- a/packages/excalidraw/tests/test-utils.ts +++ b/packages/excalidraw/tests/test-utils.ts @@ -1,13 +1,12 @@ import "pepjs"; import type { RenderResult, RenderOptions } from "@testing-library/react"; +import { act } from "@testing-library/react"; import { render, queries, waitFor, fireEvent } from "@testing-library/react"; import * as toolQueries from "./queries/toolQueries"; import type { ImportedDataState } from "../data/types"; import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants"; - -import type { SceneData } from "../types"; import { getSelectedElements } from "../scene/selection"; import type { ExcalidrawElement } from "../element/types"; import { UI } from "./helpers/ui"; @@ -67,6 +66,12 @@ const renderApp: TestRenderFn = async (ui, options) => { if (!interactiveCanvas) { throw new Error("not initialized yet"); } + + // hack-awaiting app.initialScene() which solves some test race conditions + // (later we may switch this with proper event listener) + if (window.h.state.isLoading) { + throw new Error("still loading"); + } }); return renderResult; @@ -118,10 +123,6 @@ const initLocalStorage = (data: ImportedDataState) => { } }; -export const updateSceneData = (data: SceneData) => { - (window.collab as any).excalidrawAPI.updateScene(data); -}; - const originalGetBoundingClientRect = global.window.HTMLDivElement.prototype.getBoundingClientRect; @@ -166,20 +167,24 @@ export const withExcalidrawDimensions = async ( cb: () => void, ) => { mockBoundingClientRect(dimensions); - // @ts-ignore - h.app.refreshViewportBreakpoints(); - // @ts-ignore - h.app.refreshEditorBreakpoints(); - window.h.app.refresh(); + act(() => { + // @ts-ignore + h.app.refreshViewportBreakpoints(); + // @ts-ignore + h.app.refreshEditorBreakpoints(); + window.h.app.refresh(); + }); await cb(); restoreOriginalGetBoundingClientRect(); - // @ts-ignore - h.app.refreshViewportBreakpoints(); - // @ts-ignore - h.app.refreshEditorBreakpoints(); - window.h.app.refresh(); + act(() => { + // @ts-ignore + h.app.refreshViewportBreakpoints(); + // @ts-ignore + h.app.refreshEditorBreakpoints(); + window.h.app.refresh(); + }); }; export const restoreOriginalGetBoundingClientRect = () => { diff --git a/packages/excalidraw/tests/tool.test.tsx b/packages/excalidraw/tests/tool.test.tsx index f1c91899f3..662c61baaa 100644 --- a/packages/excalidraw/tests/tool.test.tsx +++ b/packages/excalidraw/tests/tool.test.tsx @@ -1,7 +1,8 @@ +import React from "react"; import { Excalidraw } from "../index"; import type { ExcalidrawImperativeAPI } from "../types"; import { resolvablePromise } from "../utils"; -import { render } from "./test-utils"; +import { act, render } from "./test-utils"; import { Pointer } from "./helpers/ui"; describe("setActiveTool()", () => { @@ -28,7 +29,9 @@ describe("setActiveTool()", () => { it("should set the active tool type", async () => { expect(h.state.activeTool.type).toBe("selection"); - excalidrawAPI.setActiveTool({ type: "rectangle" }); + act(() => { + excalidrawAPI.setActiveTool({ type: "rectangle" }); + }); expect(h.state.activeTool.type).toBe("rectangle"); mouse.down(10, 10); @@ -39,7 +42,9 @@ describe("setActiveTool()", () => { it("should support tool locking", async () => { expect(h.state.activeTool.type).toBe("selection"); - excalidrawAPI.setActiveTool({ type: "rectangle", locked: true }); + act(() => { + excalidrawAPI.setActiveTool({ type: "rectangle", locked: true }); + }); expect(h.state.activeTool.type).toBe("rectangle"); mouse.down(10, 10); @@ -50,7 +55,9 @@ describe("setActiveTool()", () => { it("should set custom tool", async () => { expect(h.state.activeTool.type).toBe("selection"); - excalidrawAPI.setActiveTool({ type: "custom", customType: "comment" }); + act(() => { + excalidrawAPI.setActiveTool({ type: "custom", customType: "comment" }); + }); expect(h.state.activeTool.type).toBe("custom"); expect(h.state.activeTool.customType).toBe("comment"); }); diff --git a/packages/excalidraw/tests/viewMode.test.tsx b/packages/excalidraw/tests/viewMode.test.tsx index 8a736e9396..66eca8c7f2 100644 --- a/packages/excalidraw/tests/viewMode.test.tsx +++ b/packages/excalidraw/tests/viewMode.test.tsx @@ -1,10 +1,11 @@ +import React from "react"; import { render, GlobalTestState } from "./test-utils"; import { Excalidraw } from "../index"; import { KEYS } from "../keys"; import { Keyboard, Pointer, UI } from "./helpers/ui"; import { CURSOR_TYPE } from "../constants"; +import { API } from "./helpers/api"; -const { h } = window; const mouse = new Pointer("mouse"); const touch = new Pointer("touch"); const pen = new Pointer("pen"); @@ -16,14 +17,14 @@ describe("view mode", () => { }); it("after switching to view mode – cursor type should be pointer", async () => { - h.setState({ viewModeEnabled: true }); + API.setAppState({ viewModeEnabled: true }); expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( CURSOR_TYPE.GRAB, ); }); it("after switching to view mode, moving, clicking, and pressing space key – cursor type should be pointer", async () => { - h.setState({ viewModeEnabled: true }); + API.setAppState({ viewModeEnabled: true }); pointerTypes.forEach((pointerType) => { const pointer = pointerType; @@ -58,7 +59,7 @@ describe("view mode", () => { ); } - h.setState({ viewModeEnabled: true }); + API.setAppState({ viewModeEnabled: true }); expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( CURSOR_TYPE.GRAB, ); diff --git a/packages/excalidraw/tests/zindex.test.tsx b/packages/excalidraw/tests/zindex.test.tsx index 09f47d4667..1f46d60725 100644 --- a/packages/excalidraw/tests/zindex.test.tsx +++ b/packages/excalidraw/tests/zindex.test.tsx @@ -1,5 +1,6 @@ +import React from "react"; import ReactDOM from "react-dom"; -import { render } from "./test-utils"; +import { act, render } from "./test-utils"; import { Excalidraw } from "../index"; import { reseed } from "../random"; import { @@ -86,31 +87,35 @@ const populateElements = ( ); // initialize `boundElements` on containers, if applicable - h.elements = newElements.map((element, index, elements) => { - const nextElement = elements[index + 1]; - if ( - nextElement && - "containerId" in nextElement && - element.id === nextElement.containerId - ) { - return { - ...element, - boundElements: [{ type: "text", id: nextElement.id }], - }; - } - return element; - }); + API.setElements( + newElements.map((element, index, elements) => { + const nextElement = elements[index + 1]; + if ( + nextElement && + "containerId" in nextElement && + element.id === nextElement.containerId + ) { + return { + ...element, + boundElements: [{ type: "text", id: nextElement.id }], + }; + } + return element; + }), + ); - h.setState({ - ...selectGroupsForSelectedElements( - { ...h.state, ...appState, selectedElementIds }, - h.elements, - h.state, - null, - ), - ...appState, - selectedElementIds, - } as AppState); + act(() => { + h.setState({ + ...selectGroupsForSelectedElements( + { ...h.state, ...appState, selectedElementIds }, + h.elements, + h.state, + null, + ), + ...appState, + selectedElementIds, + } as AppState); + }); return selectedElementIds; }; @@ -140,7 +145,7 @@ const assertZindex = ({ }) => { const selectedElementIds = populateElements(elements, appState); operations.forEach(([action, expected]) => { - h.app.actionManager.executeAction(action); + API.executeAction(action); expect(h.elements.map((element) => element.id)).toEqual(expected); expect(h.state.selectedElementIds).toEqual(selectedElementIds); }); @@ -894,7 +899,7 @@ describe("z-index manipulation", () => { { id: "A", isSelected: true }, { id: "B", isSelected: true }, ]); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements).toMatchObject([ { id: "A" }, { id: "A_copy" }, @@ -906,7 +911,7 @@ describe("z-index manipulation", () => { { id: "A", groupIds: ["g1"], isSelected: true }, { id: "B", groupIds: ["g1"], isSelected: true }, ]); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements).toMatchObject([ { id: "A" }, { id: "B" }, @@ -927,7 +932,7 @@ describe("z-index manipulation", () => { { id: "B", groupIds: ["g1"], isSelected: true }, { id: "C" }, ]); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements).toMatchObject([ { id: "A" }, { id: "B" }, @@ -949,7 +954,7 @@ describe("z-index manipulation", () => { { id: "B", groupIds: ["g1"], isSelected: true }, { id: "C", isSelected: true }, ]); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "B", @@ -965,7 +970,7 @@ describe("z-index manipulation", () => { { id: "C", groupIds: ["g2"], isSelected: true }, { id: "D", groupIds: ["g2"], isSelected: true }, ]); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "B", @@ -987,7 +992,7 @@ describe("z-index manipulation", () => { selectedGroupIds: { g1: true }, }, ); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "B", @@ -1007,7 +1012,7 @@ describe("z-index manipulation", () => { selectedGroupIds: { g2: true }, }, ); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "B", @@ -1030,7 +1035,7 @@ describe("z-index manipulation", () => { selectedGroupIds: { g2: true, g4: true }, }, ); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "B", @@ -1054,7 +1059,7 @@ describe("z-index manipulation", () => { ], { editingGroupId: "g1" }, ); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "A_copy", @@ -1070,7 +1075,7 @@ describe("z-index manipulation", () => { ], { editingGroupId: "g1" }, ); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "B", @@ -1086,7 +1091,7 @@ describe("z-index manipulation", () => { ], { editingGroupId: "g1" }, ); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "A_copy", @@ -1102,7 +1107,7 @@ describe("z-index manipulation", () => { { id: "B" }, { id: "C", groupIds: ["g1"], isSelected: true }, ]); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "C", @@ -1120,7 +1125,7 @@ describe("z-index manipulation", () => { { id: "D" }, ]); expect(h.state.selectedGroupIds).toEqual({ g1: true }); - h.app.actionManager.executeAction(actionDuplicateSelection); + API.executeAction(actionDuplicateSelection); expect(h.elements.map((element) => element.id)).toEqual([ "A", "B", diff --git a/setupTests.ts b/setupTests.ts index 3884fbd476..1e03e6c47b 100644 --- a/setupTests.ts +++ b/setupTests.ts @@ -97,3 +97,19 @@ vi.mock("nanoid", () => { const element = document.createElement("div"); element.id = "root"; document.body.appendChild(element); + +const logger = console.error.bind(console); +console.error = (...args) => { + // the react's act() warning usually doesn't contain any useful stack trace + // so we're catching the log and re-logging the message with the test name, + // also stripping the actual component stack trace as it's not useful + if (args[0]?.includes("act(")) { + logger( + `<<< WARNING: test "${ + expect.getState().currentTestName + }" does not wrap some state update in act() >>>`, + ); + } else { + logger(...args); + } +}; diff --git a/yarn.lock b/yarn.lock index 9eea53db22..39c3d90db2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,7 +26,7 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.2", "@babel/code-frame@^7.24.6": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.24.2", "@babel/code-frame@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.6.tgz#ab88da19344445c3d8889af2216606d3329f3ef2" integrity sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA== @@ -34,6 +34,14 @@ "@babel/highlight" "^7.24.6" picocolors "^1.0.0" +"@babel/code-frame@^7.10.4": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.4", "@babel/compat-data@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.6.tgz#b3600217688cabb26e25f8e467019e66d71b7ae2" @@ -266,6 +274,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz#08bb6612b11bdec78f3feed3db196da682454a5e" integrity sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + "@babel/helper-validator-option@^7.23.5", "@babel/helper-validator-option@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz#59d8e81c40b7d9109ab7e74457393442177f460a" @@ -298,6 +311,16 @@ js-tokens "^4.0.0" picocolors "^1.0.0" +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/parser@^7.24.5", "@babel/parser@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328" @@ -2858,15 +2881,15 @@ dependencies: tslib "^2.4.0" -"@testing-library/dom@^8.0.0": - version "8.20.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" - integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g== +"@testing-library/dom@10.4.0": + version "10.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" + integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" "@types/aria-query" "^5.0.1" - aria-query "5.1.3" + aria-query "5.3.0" chalk "^4.1.0" dom-accessibility-api "^0.5.9" lz-string "^1.5.0" @@ -2887,14 +2910,12 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@12.1.5": - version "12.1.5" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" - integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== +"@testing-library/react@16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.0.tgz#0a1e0c7a3de25841c3591b8cb7fb0cf0c0a27321" + integrity sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.0.0" - "@types/react-dom" "<18.0.0" "@tldraw/vec@1.7.1": version "1.7.1" @@ -3079,14 +3100,7 @@ dependencies: "@types/react" "*" -"@types/react-dom@<18.0.0": - version "17.0.25" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" - integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA== - dependencies: - "@types/react" "^17" - -"@types/react@*", "@types/react@18.2.0", "@types/react@^17": +"@types/react@*", "@types/react@18.2.0": version "18.2.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.0.tgz#15cda145354accfc09a18d2f2305f9fc099ada21" integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA== @@ -3789,21 +3803,14 @@ aria-hidden@^1.1.1: dependencies: tslib "^2.0.0" -aria-query@5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -aria-query@^5.0.0, aria-query@^5.3.0: +aria-query@5.3.0, aria-query@^5.0.0, aria-query@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== dependencies: dequal "^2.0.3" -array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: +array-buffer-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== @@ -5112,30 +5119,6 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" -deep-equal@^2.0.5: - version "2.2.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" - integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== - dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.5" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.2" - is-arguments "^1.1.1" - is-array-buffer "^3.0.2" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.1" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.13" - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -5476,21 +5459,6 @@ es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.17: version "1.0.19" resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" @@ -6347,7 +6315,7 @@ get-func-name@^2.0.0, get-func-name@^2.0.1, get-func-name@^2.0.2: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -6749,7 +6717,7 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -internal-slot@^1.0.4, internal-slot@^1.0.7: +internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== @@ -6780,15 +6748,7 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: +is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== @@ -6899,7 +6859,7 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-map@^2.0.2, is-map@^2.0.3: +is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== @@ -6956,7 +6916,7 @@ is-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== -is-set@^2.0.2, is-set@^2.0.3: +is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== @@ -8150,14 +8110,6 @@ object-inspect@^1.12.0, object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -8889,7 +8841,7 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: +regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== @@ -9449,13 +9401,6 @@ std-env@^3.3.3, std-env@^3.5.0: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -10612,7 +10557,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==
Mermaid to Excalidraw