diff --git a/dashboard/biome.json b/dashboard/biome.json index 429508f..acff4fc 100644 --- a/dashboard/biome.json +++ b/dashboard/biome.json @@ -5,8 +5,8 @@ "rules": { "recommended": true, "a11y": { - "noStaticElementInteractions": "off", - "useKeyWithClickEvents": "off" + "noStaticElementInteractions": "error", + "useKeyWithClickEvents": "error" }, "complexity": { "noForEach": "off" diff --git a/dashboard/src/providers/refresh-interval-provider.test.ts b/dashboard/src/providers/refresh-interval-provider.test.ts new file mode 100644 index 0000000..6ef5ab1 --- /dev/null +++ b/dashboard/src/providers/refresh-interval-provider.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { parseRefreshOption, refreshIntervalMs } from "./refresh-interval-provider"; + +describe("parseRefreshOption", () => { + it("returns each known option verbatim", () => { + expect(parseRefreshOption("2s")).toBe("2s"); + expect(parseRefreshOption("5s")).toBe("5s"); + expect(parseRefreshOption("10s")).toBe("10s"); + expect(parseRefreshOption("off")).toBe("off"); + }); + + it("falls back to 5s when storage is empty", () => { + expect(parseRefreshOption(null)).toBe("5s"); + expect(parseRefreshOption("")).toBe("5s"); + }); + + it("falls back to 5s on unknown values", () => { + expect(parseRefreshOption("3s")).toBe("5s"); + expect(parseRefreshOption("nonsense")).toBe("5s"); + expect(parseRefreshOption("0")).toBe("5s"); + }); + + it("is case-sensitive — does not normalise casing or whitespace", () => { + expect(parseRefreshOption("2S")).toBe("5s"); + expect(parseRefreshOption("OFF")).toBe("5s"); + expect(parseRefreshOption(" 2s")).toBe("5s"); + expect(parseRefreshOption("2s ")).toBe("5s"); + }); +}); + +describe("refreshIntervalMs", () => { + it("maps each option to its polling interval", () => { + expect(refreshIntervalMs("2s")).toBe(2_000); + expect(refreshIntervalMs("5s")).toBe(5_000); + expect(refreshIntervalMs("10s")).toBe(10_000); + }); + + it("returns false for off so consumers can disable polling", () => { + expect(refreshIntervalMs("off")).toBe(false); + }); +}); diff --git a/dashboard/src/providers/refresh-interval-provider.tsx b/dashboard/src/providers/refresh-interval-provider.tsx index 073b2ab..cdbe2e4 100644 --- a/dashboard/src/providers/refresh-interval-provider.tsx +++ b/dashboard/src/providers/refresh-interval-provider.tsx @@ -10,6 +10,7 @@ const REFRESH_MS: Record = { }; const STORAGE_KEY = "taskito.refresh"; +const DEFAULT_OPTION: RefreshOption = "5s"; interface RefreshContextValue { option: RefreshOption; @@ -19,10 +20,17 @@ interface RefreshContextValue { const RefreshContext = createContext(null); -function readStored(): RefreshOption { - const stored = localStorage.getItem(STORAGE_KEY); +export function parseRefreshOption(stored: string | null): RefreshOption { if (stored === "2s" || stored === "5s" || stored === "10s" || stored === "off") return stored; - return "5s"; + return DEFAULT_OPTION; +} + +export function refreshIntervalMs(option: RefreshOption): number | false { + return REFRESH_MS[option]; +} + +function readStored(): RefreshOption { + return parseRefreshOption(localStorage.getItem(STORAGE_KEY)); } export function RefreshIntervalProvider({ children }: { children: ReactNode }) { @@ -31,7 +39,7 @@ export function RefreshIntervalProvider({ children }: { children: ReactNode }) { const value = useMemo( () => ({ option, - intervalMs: REFRESH_MS[option], + intervalMs: refreshIntervalMs(option), setOption: (next) => { setOptionState(next); localStorage.setItem(STORAGE_KEY, next);