1import { describe, expect, it } from "vite-plus/test";
2import {
3 delayedStatusDeadline,
4 shownDelayedStatus,
5 stepDelayedStatus,
6 type DelayedStatusState,
7} from "./delayedStatus.ts";
8
9function step(
10 state: DelayedStatusState<string> | null,
11 value: string | null,
12 now: number,
13 key = "thread-1",
14) {
15 const deadline = state === null ? null : delayedStatusDeadline(state);
16 return stepDelayedStatus(
17 state,
18 { key, value, deadlinePassed: deadline !== null && now >= deadline },
19 () => now,
20 );
21}
22
23describe("stepDelayedStatus", () => {
24 it("shows a status only after it stays set for the show delay", () => {
25 const pending = step(null, "syncing", 0);
26 expect(delayedStatusDeadline(pending)).toBe(400);
27 expect(step(pending, "syncing", 399)).toBe(pending);
28 const shown = step(pending, "syncing", 400);
29 expect(shownDelayedStatus(shown, "thread-1")).toBe("syncing");
30 const slow = stepDelayedStatus(
31 null,
32 { key: "thread-1", value: "failed", deadlinePassed: false, showDelayMs: 5_000 },
33 () => 0,
34 );
35 expect(delayedStatusDeadline(slow)).toBe(5_000);
36 expect(shownDelayedStatus(step(slow, "failed", 4_999), "thread-1")).toBeNull();
37 expect(shownDelayedStatus(step(slow, "failed", 5_000), "thread-1")).toBe("failed");
38 expect(step(slow, null, 200).phase).toBe("idle");
39 });
40
41 it("never shows a status that clears before the show delay", () => {
42 const pending = step(null, "syncing", 0);
43 const cleared = step(pending, null, 200);
44 expect(cleared.phase).toBe("idle");
45 expect(delayedStatusDeadline(step(cleared, "syncing", 300))).toBe(700);
46 });
47
48 it("shows the latest value when the show delay passes", () => {
49 const pending = step(null, "connecting", 0);
50 const changed = step(pending, "syncing", 200);
51 expect(changed).toBe(pending);
52 expect(shownDelayedStatus(step(changed, "syncing", 400), "thread-1")).toBe("syncing");
53 });
54
55 it("holds a shown status for the minimum visible time after it clears", () => {
56 const shown = step(step(null, "syncing", 0), "syncing", 400);
57 expect(delayedStatusDeadline(shown)).toBe(800);
58 expect(step(shown, null, 500)).toBe(shown);
59 expect(step(shown, null, 800).phase).toBe("idle");
60 });
61
62 it("hides immediately once the minimum visible time has passed", () => {
63 const shown = step(step(null, "syncing", 0), "syncing", 400);
64 expect(step(shown, "syncing", 900)).toBe(shown);
65 expect(step(shown, null, 1000).phase).toBe("idle");
66 });
67
68 it("restarts the minimum visible time when the shown value changes", () => {
69 const shown = step(step(null, "syncing", 0), "syncing", 400);
70 const replaced = step(shown, "replaying", 600);
71 expect(shownDelayedStatus(replaced, "thread-1")).toBe("replaying");
72 expect(delayedStatusDeadline(replaced)).toBe(1000);
73 });
74
75 it("hides the status and restarts the delay when the key changes", () => {
76 const shown = step(step(null, "syncing", 0), "syncing", 400);
77 expect(shownDelayedStatus(shown, "thread-2")).toBeNull();
78 const other = step(shown, "syncing", 500, "thread-2");
79 expect(shownDelayedStatus(other, "thread-2")).toBeNull();
80 expect(delayedStatusDeadline(other)).toBe(900);
81 const cleared = step(shown, null, 500, "cleared");
82 expect(shownDelayedStatus(cleared, "cleared")).toBeNull();
83 const retry = stepDelayedStatus(
84 cleared,
85 { key: "thread-1", value: "failed", deadlinePassed: false, showDelayMs: 5_000 },
86 () => 501,
87 );
88 expect(shownDelayedStatus(retry, "thread-1")).toBeNull();
89 expect(delayedStatusDeadline(retry)).toBe(5_501);
90 });
91});