90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
/**
|
|
* tests/unit/ui/compression-replay-reducer.test.ts
|
|
*
|
|
* F5.1 coverage gap (F3.2): the `useCompressionReplay` state machine was untested
|
|
* (only buildReplayFrames was covered). This pins the pure reducer + timing.
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
replayReducer,
|
|
INITIAL_STATE,
|
|
stepMs,
|
|
type ReplayState,
|
|
} from "../../../src/app/(dashboard)/dashboard/compression/studio/useCompressionReplay.ts";
|
|
|
|
describe("replayReducer — compression replay state machine (F3.2)", () => {
|
|
it("starts idle: frameIndex -1, not playing, speed 1", () => {
|
|
assert.deepEqual(INITIAL_STATE, { frameIndex: -1, isPlaying: false, speed: 1 });
|
|
});
|
|
|
|
it("PLAY without frameIndex starts playing and keeps the current frame", () => {
|
|
const s = replayReducer({ frameIndex: 2, isPlaying: false, speed: 1 }, { type: "PLAY" });
|
|
assert.equal(s.isPlaying, true);
|
|
assert.equal(s.frameIndex, 2);
|
|
});
|
|
|
|
it("PLAY with frameIndex restarts from that frame (used for replay-from-start)", () => {
|
|
const s = replayReducer(INITIAL_STATE, { type: "PLAY", frameIndex: 0 });
|
|
assert.equal(s.isPlaying, true);
|
|
assert.equal(s.frameIndex, 0);
|
|
});
|
|
|
|
it("PAUSE stops playing but keeps the frame", () => {
|
|
const s = replayReducer({ frameIndex: 1, isPlaying: true, speed: 1 }, { type: "PAUSE" });
|
|
assert.equal(s.isPlaying, false);
|
|
assert.equal(s.frameIndex, 1);
|
|
});
|
|
|
|
it("TICK advances one frame while below the last", () => {
|
|
const start: ReplayState = { frameIndex: -1, isPlaying: true, speed: 1 };
|
|
const a = replayReducer(start, { type: "TICK", totalFrames: 3 });
|
|
assert.equal(a.frameIndex, 0);
|
|
assert.equal(a.isPlaying, true);
|
|
const b = replayReducer(a, { type: "TICK", totalFrames: 3 });
|
|
assert.equal(b.frameIndex, 1);
|
|
assert.equal(b.isPlaying, true);
|
|
});
|
|
|
|
it("TICK stops at the last frame and clears isPlaying (auto-stop at end)", () => {
|
|
const atSecondLast: ReplayState = { frameIndex: 1, isPlaying: true, speed: 1 };
|
|
const done = replayReducer(atSecondLast, { type: "TICK", totalFrames: 3 });
|
|
assert.equal(done.frameIndex, 2, "lands on the last frame index (totalFrames-1)");
|
|
assert.equal(done.isPlaying, false, "playback stops once the last frame is reached");
|
|
});
|
|
|
|
it("RESET returns to idle (-1, not playing) but preserves speed", () => {
|
|
const s = replayReducer({ frameIndex: 2, isPlaying: true, speed: 3 }, { type: "RESET" });
|
|
assert.equal(s.frameIndex, -1);
|
|
assert.equal(s.isPlaying, false);
|
|
assert.equal(s.speed, 3, "speed must survive a reset");
|
|
});
|
|
|
|
it("SET_SPEED changes speed without touching frame/play state", () => {
|
|
const s = replayReducer({ frameIndex: 1, isPlaying: true, speed: 1 }, {
|
|
type: "SET_SPEED",
|
|
speed: 3,
|
|
});
|
|
assert.equal(s.speed, 3);
|
|
assert.equal(s.frameIndex, 1);
|
|
assert.equal(s.isPlaying, true);
|
|
});
|
|
|
|
it("unknown action is a no-op (returns the same state reference)", () => {
|
|
const state: ReplayState = { frameIndex: 0, isPlaying: false, speed: 1 };
|
|
// @ts-expect-error — exercising the default branch with an invalid action
|
|
assert.equal(replayReducer(state, { type: "NOPE" }), state);
|
|
});
|
|
});
|
|
|
|
describe("stepMs — frame pacing by speed (F3.2)", () => {
|
|
it("maps speed to a shorter interval as speed rises", () => {
|
|
assert.equal(stepMs(1), 400);
|
|
assert.equal(stepMs(3), 133); // round(400/3)
|
|
assert.equal(stepMs(0.3), 1333); // round(400/0.3)
|
|
assert.ok(stepMs(3) < stepMs(1) && stepMs(1) < stepMs(0.3), "monotonic: faster = shorter");
|
|
});
|
|
});
|