snapshot of https://github.com/excalidraw/excalidraw.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Tests / test (push) Failing after 15s
Some checks failed
Tests / test (push) Failing after 15s
This commit is contained in:
167
packages/excalidraw/tests/animation.test.ts
Normal file
167
packages/excalidraw/tests/animation.test.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AnimationController } from "../renderer/animation";
|
||||
|
||||
const FIRST_KEY = "animation-test-first";
|
||||
const SECOND_KEY = "animation-test-second";
|
||||
|
||||
describe("AnimationController", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = false;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
AnimationController.cancel(FIRST_KEY);
|
||||
AnimationController.cancel(SECOND_KEY);
|
||||
window.EXCALIDRAW_THROTTLE_RENDER = undefined;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("starts a new animation after the previous last animation was cancelled", async () => {
|
||||
let firstFrames = 0;
|
||||
AnimationController.start(FIRST_KEY, () => {
|
||||
firstFrames++;
|
||||
return { keep: true };
|
||||
});
|
||||
|
||||
expect(firstFrames).toBe(1);
|
||||
|
||||
AnimationController.cancel(FIRST_KEY);
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
let secondFrames = 0;
|
||||
AnimationController.start(SECOND_KEY, () => {
|
||||
secondFrames++;
|
||||
return secondFrames === 1 ? { keep: true } : null;
|
||||
});
|
||||
|
||||
expect(secondFrames).toBe(1);
|
||||
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(secondFrames).toBe(2);
|
||||
expect(AnimationController.running(SECOND_KEY)).toBe(false);
|
||||
});
|
||||
|
||||
it("cancels a frame scheduled during a tick if no animations remain", async () => {
|
||||
let firstFrames = 0;
|
||||
let secondFrames = 0;
|
||||
|
||||
AnimationController.start(FIRST_KEY, ({ state }) => {
|
||||
if (!state) {
|
||||
return { keep: true };
|
||||
}
|
||||
|
||||
firstFrames++;
|
||||
|
||||
AnimationController.start(SECOND_KEY, () => {
|
||||
secondFrames++;
|
||||
return { keep: true };
|
||||
});
|
||||
AnimationController.cancel(SECOND_KEY);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(firstFrames).toBe(1);
|
||||
expect(secondFrames).toBe(1);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("does not resurrect an animation cancelled during its initial callback", () => {
|
||||
let frames = 0;
|
||||
|
||||
AnimationController.start(FIRST_KEY, () => {
|
||||
frames++;
|
||||
AnimationController.cancel(FIRST_KEY);
|
||||
return { keep: true };
|
||||
});
|
||||
|
||||
expect(frames).toBe(1);
|
||||
expect(AnimationController.running(FIRST_KEY)).toBe(false);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("cleans up the registration when the initial callback throws", () => {
|
||||
expect(() =>
|
||||
AnimationController.start(FIRST_KEY, () => {
|
||||
throw new Error("initial frame failed");
|
||||
}),
|
||||
).toThrow("initial frame failed");
|
||||
|
||||
expect(AnimationController.running(FIRST_KEY)).toBe(false);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves a same-key replacement started during the initial callback", async () => {
|
||||
let originalFrames = 0;
|
||||
let replacementFrames = 0;
|
||||
|
||||
const replacement = ({ state }: { state?: { keep: true } }) => {
|
||||
replacementFrames++;
|
||||
return state ? null : { keep: true as const };
|
||||
};
|
||||
|
||||
AnimationController.start(FIRST_KEY, () => {
|
||||
originalFrames++;
|
||||
AnimationController.cancel(FIRST_KEY);
|
||||
AnimationController.start(FIRST_KEY, replacement);
|
||||
return { keep: true };
|
||||
});
|
||||
|
||||
expect(originalFrames).toBe(1);
|
||||
expect(replacementFrames).toBe(1);
|
||||
expect(AnimationController.running(FIRST_KEY)).toBe(true);
|
||||
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(originalFrames).toBe(1);
|
||||
expect(replacementFrames).toBe(2);
|
||||
expect(AnimationController.running(FIRST_KEY)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not let a completed callback delete its same-key replacement", async () => {
|
||||
// tests for this unwanted case:
|
||||
//
|
||||
// 1. The original animation’s scheduled callback begins.
|
||||
// 2. Before it returns, application code cancels it and starts
|
||||
// a replacement using the same key.
|
||||
// 3. The original callback returns null.
|
||||
// 4. Previously, tick() then called delete(key), accidentally deleting
|
||||
// the replacement.
|
||||
|
||||
let originalFrames = 0;
|
||||
let replacementFrames = 0;
|
||||
|
||||
const replacement = ({ state }: { state?: { keep: true } }) => {
|
||||
replacementFrames++;
|
||||
return state ? null : { keep: true as const };
|
||||
};
|
||||
|
||||
AnimationController.start(FIRST_KEY, ({ state }) => {
|
||||
if (!state) {
|
||||
return { keep: true };
|
||||
}
|
||||
|
||||
originalFrames++;
|
||||
AnimationController.cancel(FIRST_KEY);
|
||||
AnimationController.start(FIRST_KEY, replacement);
|
||||
return null;
|
||||
});
|
||||
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(originalFrames).toBe(1);
|
||||
expect(replacementFrames).toBe(1);
|
||||
expect(AnimationController.running(FIRST_KEY)).toBe(true);
|
||||
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(originalFrames).toBe(1);
|
||||
expect(replacementFrames).toBe(2);
|
||||
expect(AnimationController.running(FIRST_KEY)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user