浏览代码

chore: bump `@testing-library/react` `12.1.5` -> `16.0.0` (#8322)

David Luzar 11 月之前
父节点
当前提交
f19ce30dfe
共有 52 个文件被更改,包括 1033 次插入976 次删除
  1. 5 6
      excalidraw-app/tests/collab.test.tsx
  2. 1 0
      packages/excalidraw/actions/actionElementLock.test.tsx
  3. 11 12
      packages/excalidraw/actions/actionProperties.test.tsx
  4. 30 21
      packages/excalidraw/components/App.tsx
  5. 1 1
      packages/excalidraw/components/DefaultSidebar.test.tsx
  6. 43 62
      packages/excalidraw/components/Sidebar/Sidebar.test.tsx
  7. 42 0
      packages/excalidraw/components/Sidebar/siderbar.test.helpers.tsx
  8. 40 44
      packages/excalidraw/components/Stats/stats.test.tsx
  9. 8 2
      packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx
  10. 1 0
      packages/excalidraw/components/hoc/withInternalFallback.test.tsx
  11. 1 7
      packages/excalidraw/element/routing.test.tsx
  12. 118 176
      packages/excalidraw/element/textWysiwyg.test.tsx
  13. 39 36
      packages/excalidraw/frame.test.tsx
  14. 2 1
      packages/excalidraw/package.json
  15. 1 0
      packages/excalidraw/tests/App.test.tsx
  16. 4 13
      packages/excalidraw/tests/MermaidToExcalidraw.test.tsx
  17. 10 1
      packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap
  18. 45 0
      packages/excalidraw/tests/__snapshots__/linearElementEditor.test.tsx.snap
  19. 1 0
      packages/excalidraw/tests/actionStyles.test.tsx
  20. 30 29
      packages/excalidraw/tests/align.test.tsx
  21. 4 4
      packages/excalidraw/tests/appState.test.tsx
  22. 6 5
      packages/excalidraw/tests/binding.test.tsx
  23. 5 4
      packages/excalidraw/tests/clipboard.test.tsx
  24. 3 2
      packages/excalidraw/tests/contextmenu.test.tsx
  25. 1 0
      packages/excalidraw/tests/dragCreate.test.tsx
  26. 19 18
      packages/excalidraw/tests/elementLocking.test.tsx
  27. 1 0
      packages/excalidraw/tests/excalidraw.test.tsx
  28. 6 5
      packages/excalidraw/tests/export.test.tsx
  29. 29 15
      packages/excalidraw/tests/fitToContent.test.tsx
  30. 100 87
      packages/excalidraw/tests/flip.test.tsx
  31. 51 10
      packages/excalidraw/tests/helpers/api.ts
  32. 29 12
      packages/excalidraw/tests/helpers/ui.ts
  33. 111 137
      packages/excalidraw/tests/history.test.tsx
  34. 6 3
      packages/excalidraw/tests/library.test.tsx
  35. 44 69
      packages/excalidraw/tests/linearElementEditor.test.tsx
  36. 14 11
      packages/excalidraw/tests/move.test.tsx
  37. 1 0
      packages/excalidraw/tests/multiPointCreate.test.tsx
  38. 3 1
      packages/excalidraw/tests/packages/events.test.tsx
  39. 1 1
      packages/excalidraw/tests/queries/dom.ts
  40. 4 3
      packages/excalidraw/tests/regressionTests.test.tsx
  41. 8 7
      packages/excalidraw/tests/resize.test.tsx
  42. 1 0
      packages/excalidraw/tests/rotate.test.tsx
  43. 1 1
      packages/excalidraw/tests/scene/export.test.ts
  44. 4 3
      packages/excalidraw/tests/scroll.test.tsx
  45. 7 6
      packages/excalidraw/tests/selection.test.tsx
  46. 1 0
      packages/excalidraw/tests/shortcuts.test.tsx
  47. 21 16
      packages/excalidraw/tests/test-utils.ts
  48. 11 4
      packages/excalidraw/tests/tool.test.tsx
  49. 5 4
      packages/excalidraw/tests/viewMode.test.tsx
  50. 44 39
      packages/excalidraw/tests/zindex.test.tsx
  51. 16 0
      setupTests.ts
  52. 43 98
      yarn.lock

+ 5 - 6
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,
     });

+ 1 - 0
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";

+ 11 - 12
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(<Excalidraw />);
@@ -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();

+ 30 - 21
packages/excalidraw/components/App.tsx

@@ -2141,16 +2141,6 @@ class App extends React.Component<AppProps, AppState> {
 
     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<AppProps, AppState> {
         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<AppProps, AppState> {
           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(), [

+ 1 - 1
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;
 

+ 43 - 62
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";
-
-export const assertSidebarDockButton = async <T extends boolean>(
-  hasDockButton: T,
-): Promise<
-  T extends false
-    ? { dockButton: null; sidebar: HTMLElement }
-    : { dockButton: HTMLElement; sidebar: HTMLElement }
-> => {
-  const sidebar =
-    GlobalTestState.renderResult.container.querySelector<HTMLElement>(
-      ".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(
-    <Excalidraw initialData={{ appState: { openSidebar: { name } } }}>
-      {sidebar}
-    </Excalidraw>,
-  );
-  await withExcalidrawDimensions({ width: 1920, height: 1080 }, test);
+import {
+  assertExcalidrawWithSidebar,
+  assertSidebarDockButton,
+} from "./siderbar.test.helpers";
+
+const toggleSidebar = (
+  ...args: Parameters<typeof window.h.app.toggleSidebar>
+): Promise<boolean> => {
+  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(
         <Excalidraw
           initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
@@ -341,6 +320,8 @@ describe("Sidebar", () => {
           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<HTMLElement>(
               "[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<HTMLElement>(
               "[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<HTMLElement>(
               "[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<HTMLElement>(
               "[role=tabpanel][data-testid=comments]",

+ 42 - 0
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 <T extends boolean>(
+  hasDockButton: T,
+): Promise<
+  T extends false
+    ? { dockButton: null; sidebar: HTMLElement }
+    : { dockButton: HTMLElement; sidebar: HTMLElement }
+> => {
+  const sidebar =
+    GlobalTestState.renderResult.container.querySelector<HTMLElement>(
+      ".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(
+    <Excalidraw initialData={{ appState: { openSidebar: { name } } }}>
+      {sidebar}
+    </Excalidraw>,
+  );
+  await withExcalidrawDimensions({ width: 1920, height: 1080 }, test);
+};

+ 40 - 44
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(<Excalidraw handleKeyboardGlobally={true} />);
 
-    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(<Excalidraw handleKeyboardGlobally={true} />);
 
-    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(<Excalidraw handleKeyboardGlobally={true} />);
 
-    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(<Excalidraw handleKeyboardGlobally={true} />);
 
-    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;

+ 8 - 2
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 <DropdownMenu/>", () => {
   it("should", async () => {
@@ -9,7 +15,7 @@ describe("Test <DropdownMenu/>", () => {
 
     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(() => {

+ 1 - 0
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";
 

+ 1 - 7
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],

+ 118 - 176
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(<Excalidraw handleKeyboardGlobally={true} />);
-      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);
+      fireEvent.keyDown(textarea, {
+        key: KEYS.TAB,
+        shiftKey: true,
+      });
 
       // 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", {
-        key: KEYS.TAB,
-        shiftKey: true,
-      });
       // 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(<Excalidraw handleKeyboardGlobally={true} />);
-      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([

+ 39 - 36
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]);
 

+ 2 - 1
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",

+ 1 - 0
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";

+ 4 - 13
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 <MermaidToExcalidraw/>", () => {
 
     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");

+ 10 - 1
packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap

@@ -1,10 +1,19 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
-"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" data-prevent-outside-click="true"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1200px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style="animation-duration: 0s;"><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Mermaid Syntax</label></div><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
+"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" data-prevent-outside-click="true"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1200px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style=""><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Mermaid Syntax</label></div><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
  A[Christmas] --&gt;|Get money| B(Go shopping)
  B --&gt; C{Let me think}
  C --&gt;|One| D[Laptop]
  C --&gt;|Two| E[iPhone]
  C --&gt;|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="89" height="158" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
 `;
+
+exports[`Test <MermaidToExcalidraw/> > 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]"
+`;

+ 45 - 0
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`] = `
 <textarea
   class="excalidraw-wysiwyg"
@@ -10,3 +22,36 @@ exports[`Test Linear Elements > 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"
+`;

+ 1 - 0
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";

+ 30 - 29
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(<Excalidraw handleKeyboardGlobally={true} />);
   });
 
@@ -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);

+ 4 - 4
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");

+ 6 - 5
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)!;

+ 5 - 4
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],

+ 3 - 2
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);

+ 1 - 0
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";

+ 19 - 18
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(<Excalidraw handleKeyboardGlobally={true} />);
-    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);

+ 1 - 0
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";

+ 6 - 5
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: "😀" }),

+ 29 - 15
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(<Excalidraw />);
@@ -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();
 

+ 100 - 87
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(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
-  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(

+ 51 - 10
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<typeof App>["updateScene"] = (...args) => {
+    act(() => {
+      h.app.updateScene(...args);
+    });
+  };
+  static setAppState: React.Component<any, AppState>["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<ExcalidrawElement["id"], true>),
+    act(() => {
+      h.setState({
+        selectedElementIds: elements.reduce((acc, element) => {
+          acc[element.id] = true;
+          return acc;
+        }, {} as Record<ExcalidrawElement["id"], true>),
+      });
+    });
+  };
+
+  static updateElement = (
+    ...[element, updates]: Parameters<typeof mutateElement>
+  ) => {
+    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);
+    });
   };
 }

+ 29 - 12
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,

+ 111 - 137
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(<Excalidraw handleKeyboardGlobally={true} />);
       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<ExcalidrawImperativeAPI>();
-      await render(
-        <Excalidraw
-          excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
-          handleKeyboardGlobally={true}
-        />,
-      );
-      const excalidrawAPI = await excalidrawAPIPromise;
+      await render(<Excalidraw handleKeyboardGlobally={true} />);
 
       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<ExcalidrawImperativeAPI>();
       await render(
         <Excalidraw
-          excalidrawAPI={(api) => 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<ExcalidrawImperativeAPI>();
-
-        await render(
-          <Excalidraw
-            excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
-            handleKeyboardGlobally={true}
-          />,
-        );
-        excalidrawAPI = await excalidrawAPIPromise;
+        await render(<Excalidraw handleKeyboardGlobally={true} />);
 
         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<ExcalidrawImperativeAPI>();
       await render(
-        <Excalidraw
-          excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
-          handleKeyboardGlobally={true}
-          isCollaborating={true}
-        />,
+        <Excalidraw 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], {

+ 6 - 3
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(<Excalidraw />);
-    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);
 

+ 44 - 69
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);
 

+ 14 - 11
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<ExcalidrawLinearElement>,
-      rectA.get() as ExcalidrawRectangleElement,
-      rectB.get() as ExcalidrawRectangleElement,
-      elementsMap,
-      {} as Scene,
-    );
+    act(() => {
+      // bind line to two rectangles
+      bindOrUnbindLinearElement(
+        arrow.get() as NonDeleted<ExcalidrawLinearElement>,
+        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();

+ 1 - 0
packages/excalidraw/tests/multiPointCreate.test.tsx

@@ -1,3 +1,4 @@
+import React from "react";
 import ReactDOM from "react-dom";
 import {
   render,

+ 3 - 1
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,
     });

+ 1 - 1
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);
 };

+ 4 - 3
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(<Excalidraw handleKeyboardGlobally={true} />);
-  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);
 

+ 8 - 7
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,

+ 1 - 0
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";

+ 1 - 1
packages/excalidraw/tests/scene/export.test.ts

@@ -88,7 +88,7 @@ describe("exportToSvg", () => {
     );
 
     expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
-      '"_themeFilter_1883f3"',
+      `"_themeFilter_1883f3"`,
     );
   });
 

+ 4 - 3
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();
   });

+ 7 - 6
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;
 

+ 1 - 0
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";

+ 21 - 16
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 = () => {

+ 11 - 4
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");
   });

+ 5 - 4
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,
       );

+ 44 - 39
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",

+ 16 - 0
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);
+  }
+};

+ 43 - 98
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/[email protected]":
   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/[email protected]", "@types/react@^17":
+"@types/react@*", "@types/[email protected]":
   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"
 
[email protected]:
-  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:
[email protected], 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==