🏗️ Build and Test JellyTau / Run Tests (push) Failing after 14m18s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m31s
Traceability Validation / Check Requirement Traces (push) Successful in 17s
Build & Release / Run Tests (push) Failing after 14m7s
Build & Release / Build Linux (push) Skipped
Build & Release / Build Windows (push) Skipped
Build & Release / Build Android (push) Skipped
Build & Release / Create Release (push) Skipped
The rotation interval was installed once when the banner mounted and never touched again, so a swipe, arrow or dot tap inherited whatever was left of the running countdown — swiping 5.5s into a 6s interval moved the banner on half a second later. The timer moves into heroRotation.ts as a small restartable object so it can be unit-tested, and every manual navigation path restarts it from that moment. Verified red-first: with restart() reverted to leave a running timer alone, the regression test fails. Release 0.9.1.
83 lines
2.4 KiB
TypeScript
83 lines
2.4 KiB
TypeScript
// TRACES: UR-034 | DR-038 | UT-207
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { createRotationTimer } from "./heroRotation";
|
|
|
|
describe("hero banner rotation timer", () => {
|
|
beforeEach(() => vi.useFakeTimers());
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it("advances once per interval while running", () => {
|
|
const onElapse = vi.fn();
|
|
const timer = createRotationTimer(6000, onElapse);
|
|
timer.restart();
|
|
|
|
vi.advanceTimersByTime(6000);
|
|
expect(onElapse).toHaveBeenCalledTimes(1);
|
|
vi.advanceTimersByTime(6000);
|
|
expect(onElapse).toHaveBeenCalledTimes(2);
|
|
|
|
timer.stop();
|
|
});
|
|
|
|
it("does nothing until started", () => {
|
|
const onElapse = vi.fn();
|
|
createRotationTimer(6000, onElapse);
|
|
vi.advanceTimersByTime(60_000);
|
|
expect(onElapse).not.toHaveBeenCalled();
|
|
});
|
|
|
|
// The bug: a manual swipe/click left the interval running, so the banner
|
|
// rotated again almost immediately instead of waiting a full interval.
|
|
it("restarts the countdown from now, not from the last auto-advance", () => {
|
|
const onElapse = vi.fn();
|
|
const timer = createRotationTimer(6000, onElapse);
|
|
timer.restart();
|
|
|
|
// 5.5s in the user swipes — the timer must restart from that moment.
|
|
vi.advanceTimersByTime(5500);
|
|
timer.restart();
|
|
|
|
// The remaining 500ms of the old countdown must NOT fire.
|
|
vi.advanceTimersByTime(500);
|
|
expect(onElapse).not.toHaveBeenCalled();
|
|
|
|
// A full interval after the swipe, it advances.
|
|
vi.advanceTimersByTime(5500);
|
|
expect(onElapse).toHaveBeenCalledTimes(1);
|
|
|
|
timer.stop();
|
|
});
|
|
|
|
it("does not stack timers when restarted repeatedly", () => {
|
|
const onElapse = vi.fn();
|
|
const timer = createRotationTimer(1000, onElapse);
|
|
timer.restart();
|
|
timer.restart();
|
|
timer.restart();
|
|
|
|
vi.advanceTimersByTime(1000);
|
|
expect(onElapse).toHaveBeenCalledTimes(1);
|
|
|
|
timer.stop();
|
|
});
|
|
|
|
it("stops firing after stop()", () => {
|
|
const onElapse = vi.fn();
|
|
const timer = createRotationTimer(1000, onElapse);
|
|
timer.restart();
|
|
timer.stop();
|
|
|
|
vi.advanceTimersByTime(10_000);
|
|
expect(onElapse).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports whether it is running", () => {
|
|
const timer = createRotationTimer(1000, () => {});
|
|
expect(timer.isRunning()).toBe(false);
|
|
timer.restart();
|
|
expect(timer.isRunning()).toBe(true);
|
|
timer.stop();
|
|
expect(timer.isRunning()).toBe(false);
|
|
});
|
|
});
|