From 2bbada7d2c5c00c5f844b6916c08bc4a9ea8cb25 Mon Sep 17 00:00:00 2001 From: cashmere Date: Mon, 27 Jul 2026 01:21:02 +0200 Subject: feat: freeze-duration prompt Freeze frame now asks for the duration (decimal seconds, 0 < d <= 600) before capture, via a promise-based zustand store mounted as an editor-global dialog. Last-used duration persists and prefills. Cancel aborts before capture with no commands. --- .../actions/__tests__/freeze-frame-store.test.ts | 98 ++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 apps/web/src/actions/__tests__/freeze-frame-store.test.ts (limited to 'apps/web/src/actions/__tests__') diff --git a/apps/web/src/actions/__tests__/freeze-frame-store.test.ts b/apps/web/src/actions/__tests__/freeze-frame-store.test.ts new file mode 100644 index 00000000..94e1566e --- /dev/null +++ b/apps/web/src/actions/__tests__/freeze-frame-store.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import { + DEFAULT_FREEZE_DURATION_SECONDS, + isValidFreezeDuration, + MAX_FREEZE_DURATION_SECONDS, + useFreezeFrameStore, +} from "../freeze-frame-store"; + +function resetStore() { + useFreezeFrameStore.setState({ + isOpen: false, + lastDurationSeconds: DEFAULT_FREEZE_DURATION_SECONDS, + resolver: null, + }); +} + +describe("freeze-frame store", () => { + beforeEach(() => { + resetStore(); + }); + + describe("isValidFreezeDuration", () => { + test("accepts positive durations up to the max, including decimals", () => { + expect(isValidFreezeDuration({ seconds: 3 })).toBe(true); + expect(isValidFreezeDuration({ seconds: 2.5 })).toBe(true); + expect(isValidFreezeDuration({ seconds: 0.1 })).toBe(true); + expect( + isValidFreezeDuration({ seconds: MAX_FREEZE_DURATION_SECONDS }), + ).toBe(true); + }); + + test("rejects non-positive, over-max, and non-finite durations", () => { + expect(isValidFreezeDuration({ seconds: 0 })).toBe(false); + expect(isValidFreezeDuration({ seconds: -1 })).toBe(false); + expect(isValidFreezeDuration({ seconds: 600.1 })).toBe(false); + expect(isValidFreezeDuration({ seconds: Number.NaN })).toBe(false); + expect(isValidFreezeDuration({ seconds: Number.POSITIVE_INFINITY })).toBe( + false, + ); + }); + }); + + describe("requestDuration", () => { + test("opens the dialog and resolves with the confirmed seconds", async () => { + const promise = useFreezeFrameStore.getState().requestDuration(); + expect(useFreezeFrameStore.getState().isOpen).toBe(true); + + useFreezeFrameStore.getState().confirmDuration({ seconds: 5 }); + + await expect(promise).resolves.toBe(5); + expect(useFreezeFrameStore.getState().isOpen).toBe(false); + }); + + test("remembers the confirmed duration as the last-used value", async () => { + const promise = useFreezeFrameStore.getState().requestDuration(); + useFreezeFrameStore.getState().confirmDuration({ seconds: 5 }); + await promise; + + expect(useFreezeFrameStore.getState().lastDurationSeconds).toBe(5); + }); + + test("resolves null on cancel and keeps the last-used duration", async () => { + useFreezeFrameStore.setState({ lastDurationSeconds: 7 }); + + const promise = useFreezeFrameStore.getState().requestDuration(); + expect(useFreezeFrameStore.getState().isOpen).toBe(true); + + useFreezeFrameStore.getState().cancelDuration(); + + await expect(promise).resolves.toBeNull(); + expect(useFreezeFrameStore.getState().isOpen).toBe(false); + expect(useFreezeFrameStore.getState().lastDurationSeconds).toBe(7); + }); + + test("ignores an invalid confirm and leaves the dialog open", () => { + useFreezeFrameStore.getState().requestDuration(); + + useFreezeFrameStore.getState().confirmDuration({ seconds: 0 }); + useFreezeFrameStore.getState().confirmDuration({ seconds: 601 }); + + expect(useFreezeFrameStore.getState().isOpen).toBe(true); + expect(useFreezeFrameStore.getState().lastDurationSeconds).toBe( + DEFAULT_FREEZE_DURATION_SECONDS, + ); + }); + + test("a new request supersedes a pending one by cancelling it", async () => { + const first = useFreezeFrameStore.getState().requestDuration(); + const second = useFreezeFrameStore.getState().requestDuration(); + + await expect(first).resolves.toBeNull(); + expect(useFreezeFrameStore.getState().isOpen).toBe(true); + + useFreezeFrameStore.getState().confirmDuration({ seconds: 2 }); + await expect(second).resolves.toBe(2); + }); + }); +}); -- cgit v1.3.1