test.web/presentation/timers/surface.timer.test.ts

test.web/presentation/timers/surface.timer.test.ts is a file in Codebase Testing. 93 lines of code and 0 definitions.

import { afterEach, describe, expect, it, vi } from "vitest";
import { STEP_LIMIT_MS } from "@banes-lab/web/configuration/constants/surface.constants.ts";
import { SurfaceTimer } from "@banes-lab/web/presentation/timers/surface.timer.ts";

type FrameCallback = (now: number) => void;

const frames: FrameCallback[] = [];

const runFrame = function runFrame(now: number): void {
    frames.shift()?.(now);
};

const stubFrames = function stubFrames(): ReturnType<typeof vi.fn> {
    frames.length = 0;
    const cancel = vi.fn(() => {
        frames.length = 0;
    });
    vi.stubGlobal("requestAnimationFrame", (callback: FrameCallback) => frames.push(callback));
    vi.stubGlobal("cancelAnimationFrame", cancel);
    return cancel;
};

interface Recording {
    readonly events: string[];
    readonly on: (entry: string) => () => void;
}

const recording = function recording(): Recording {
    const events: string[] = [];
    return {
        events,
        on: (entry) => () => {
            events.push(entry);
        },
    };
};

afterEach(() => {
    vi.unstubAllGlobals();
});

describe("SurfaceTimer", () => {
    it("begins each segment once, reports its progress, and carries leftover time into the next segment as it loops", () => {
        stubFrames();
        const { events, on } = recording();
        const timer = new SurfaceTimer([
            {
                advance: (progress) => {
                    events.push(`a ${String(progress)}`);
                },
                begin: on("begin a"),
                ms: 40,
            },
            { begin: on("begin b"), ms: 40 },
        ]);
        timer.run();
        runFrame(0);
        runFrame(20);
        runFrame(60);
        runFrame(100);
        expect(events).toStrictEqual(["begin a", "a 0", "a 0.5", "a 1", "begin b", "begin a", "a 0.5"]);
    });

    it("caps one frame's step, so a tab that slept does not skip the script", () => {
        stubFrames();
        const progress: number[] = [];
        const timer = new SurfaceTimer([
            {
                advance: (value) => {
                    progress.push(value);
                },
                ms: STEP_LIMIT_MS * 4,
            },
        ]);
        timer.run();
        runFrame(0);
        runFrame(STEP_LIMIT_MS * 100);
        expect(progress).toStrictEqual([0, 0.25]);
    });

    it("applies the first segments at once on settle, and stops asking for frames on halt", () => {
        const cancel = stubFrames();
        const { events, on } = recording();
        const timer = new SurfaceTimer([
            {
                advance: (value) => {
                    events.push(`a ${String(value)}`);
                },
                begin: on("begin a"),
                ms: 10,
            },
            { begin: on("begin b"), ms: 10 },
        ]);
        timer.settle(1);
        expect(events).toStrictEqual(["begin a", "a 1"]);
        timer.run();
        timer.halt();
        expect(cancel).toHaveBeenCalledTimes(1);
        expect(frames).toHaveLength(0);
        new SurfaceTimer([]).run();
        expect(frames).toHaveLength(0);
    });
});