feat(packages/excalidraw): export throttleRAF (#10912)

This commit is contained in:
David Luzar
2026-03-07 12:05:33 +01:00
committed by GitHub
parent 3d8c12fba4
commit fa1f7d9f22
6 changed files with 123 additions and 5 deletions
+89
View File
@@ -3,6 +3,12 @@ import {
mapFind,
reduceToCommonValue,
} from "@excalidraw/common";
import { vi } from "vitest";
// Import directly to avoid the @excalidraw/common throttleRAF mock from setupTests.ts.
import { throttleRAF } from "./utils";
type RafCallback = FrameRequestCallback;
describe("@excalidraw/common/utils", () => {
describe("isTransparent()", () => {
@@ -79,4 +85,87 @@ describe("@excalidraw/common/utils", () => {
expect(mapFind([1, 2], () => null)).toBe(undefined);
});
});
describe("throttleRAF()", () => {
let frameCallbacks: Map<number, RafCallback>;
let nextFrameId: number;
const runScheduledFrame = (timestamp = 16) => {
const callbacks = [...frameCallbacks.values()];
frameCallbacks.clear();
callbacks.forEach((callback) => callback(timestamp));
};
beforeEach(() => {
frameCallbacks = new Map();
nextFrameId = 0;
vi.spyOn(window, "requestAnimationFrame").mockImplementation(
(callback) => {
const frameId = ++nextFrameId;
frameCallbacks.set(frameId, callback);
return frameId;
},
);
vi.spyOn(window, "cancelAnimationFrame").mockImplementation((frameId) => {
frameCallbacks.delete(frameId);
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should invoke the callback with the last args from the same frame", () => {
const fn = vi.fn();
const throttled = throttleRAF(fn);
throttled("first", 1);
throttled("second", 2);
throttled("last", 3);
expect(fn).not.toHaveBeenCalled();
expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1);
runScheduledFrame();
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("last", 3);
});
it("should flush the pending callback immediately", () => {
const fn = vi.fn();
const throttled = throttleRAF(fn);
throttled("first");
throttled("last");
throttled.flush();
expect(window.cancelAnimationFrame).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("last");
runScheduledFrame();
expect(fn).toHaveBeenCalledTimes(1);
});
it("should cancel the pending callback", () => {
const fn = vi.fn();
const throttled = throttleRAF(fn);
throttled("first");
throttled("last");
throttled.cancel();
expect(window.cancelAnimationFrame).toHaveBeenCalledTimes(1);
runScheduledFrame();
expect(fn).not.toHaveBeenCalled();
});
});
});
-4
View File
@@ -169,10 +169,6 @@ export const throttleRAF = <T extends any[]>(fn: (...args: T) => void) => {
};
const ret = (...args: T) => {
if (isTestEnv()) {
fn(...args);
return;
}
lastArgs = args;
if (timerId === null) {
scheduleFunc();
+1
View File
@@ -267,6 +267,7 @@ export {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
getFormFactor,
throttleRAF,
} from "@excalidraw/common";
export {
+4 -1
View File
@@ -28,7 +28,9 @@ const { h } = window;
const mouse = new Pointer("mouse");
vi.mock("@excalidraw/common", async (importOriginal) => {
const module: any = await importOriginal();
const module = await importOriginal<typeof import("@excalidraw/common")>();
const { mockThrottleRAF } = await import("./helpers/mocks");
return {
__esmodule: true,
...module,
@@ -37,6 +39,7 @@ vi.mock("@excalidraw/common", async (importOriginal) => {
...module.KEYS,
CTRL_OR_CMD: "ctrlKey",
},
throttleRAF: mockThrottleRAF,
};
});
@@ -3,6 +3,25 @@ import React from "react";
import { vi } from "vitest";
import type { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
import type { throttleRAF as throttleRAFType } from "@excalidraw/common";
type ThrottledFn<T extends unknown[]> = ((...args: T) => void) & {
flush: () => void;
cancel: () => void;
};
export const mockThrottleRAF: typeof throttleRAFType = <T extends unknown[]>(
fn: (...args: T) => void,
) => {
const ret = ((...args: T) => {
fn(...args);
}) as ThrottledFn<T>;
ret.flush = () => {};
ret.cancel = () => {};
return ret;
};
export const mockMermaidToExcalidraw = (opts: {
parseMermaidToExcalidraw: typeof parseMermaidToExcalidraw;
+10
View File
@@ -6,9 +6,19 @@ import "@testing-library/jest-dom";
import { vi } from "vitest";
import polyfill from "./packages/excalidraw/polyfill";
import { mockThrottleRAF } from "./packages/excalidraw/tests/helpers/mocks";
import { yellow } from "./packages/excalidraw/tests/helpers/colorize";
import { testPolyfills } from "./packages/excalidraw/tests/helpers/polyfills";
vi.mock("@excalidraw/common", async (importOriginal) => {
const module = await importOriginal<typeof import("@excalidraw/common")>();
return {
...module,
throttleRAF: mockThrottleRAF,
};
});
// mock for pep.js not working with setPointerCapture()
HTMLElement.prototype.setPointerCapture = vi.fn();