168 lines
4.7 KiB
TypeScript
168 lines
4.7 KiB
TypeScript
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);
|
||
});
|
||
});
|