import { describe, expect, test } from "vitest"; import { attachDashboardRefreshLifecycle, createDashboardTileSnapshotStore, createDashboardTileBackoff, createDashboardRequestAborter, runDashboardHydrationQueue, shouldPauseDashboardRefresh, } from "./dashboard-refresh"; describe("dashboard refresh lifecycle", () => { test("pauses refreshes when the document is hidden or the browser is offline", () => { expect( shouldPauseDashboardRefresh({ visibilityState: "visible", online: true }), ).toBe(false); expect( shouldPauseDashboardRefresh({ visibilityState: "hidden", online: true }), ).toBe(true); expect( shouldPauseDashboardRefresh({ visibilityState: "visible", online: false }), ).toBe(true); }); test("aborts shell and tile requests when a new shell run starts", () => { const aborter = createDashboardRequestAborter(); const shellSignal = aborter.beginShellRun(); const tileSignal = aborter.beginTileRun(); const nextShellSignal = aborter.beginShellRun(); expect(shellSignal.aborted).toBe(true); expect(tileSignal.aborted).toBe(true); expect(nextShellSignal.aborted).toBe(false); }); test("aborts active requests when refreshes are paused", () => { const aborter = createDashboardRequestAborter(); const shellSignal = aborter.beginShellRun(); const tileSignal = aborter.beginTileRun(); aborter.abortActiveRequests(); expect(shellSignal.aborted).toBe(true); expect(tileSignal.aborted).toBe(true); }); test("pauses on hidden, offline, and pagehide events then resumes immediately when visible", () => { const documentTarget = new EventTarget(); const windowTarget = new EventTarget(); const calls: string[] = []; let paused = true; const detach = attachDashboardRefreshLifecycle({ documentTarget, windowTarget, loadDashboard: () => { calls.push("load"); }, pauseRefreshes: () => { calls.push("pause"); }, refreshPaused: () => paused, }); documentTarget.dispatchEvent(new Event("visibilitychange")); windowTarget.dispatchEvent(new Event("offline")); windowTarget.dispatchEvent(new Event("pagehide")); paused = false; documentTarget.dispatchEvent(new Event("visibilitychange")); windowTarget.dispatchEvent(new Event("online")); detach(); documentTarget.dispatchEvent(new Event("visibilitychange")); windowTarget.dispatchEvent(new Event("online")); expect(calls).toEqual(["pause", "pause", "pause", "load", "load"]); }); test("limits tile hydration concurrency", async () => { let active = 0; let maxActive = 0; const started: number[] = []; const releases = new Map void>(); const queue = runDashboardHydrationQueue({ concurrency: 2, items: [1, 2, 3, 4], signal: new AbortController().signal, hydrate: async (item) => { active += 1; maxActive = Math.max(maxActive, active); started.push(item); await new Promise((resolve) => releases.set(item, resolve)); active -= 1; }, }); await waitFor(() => started.length === 2); expect(started).toEqual([1, 2]); expect(maxActive).toBe(2); releases.get(1)?.(); await waitFor(() => started.length === 3); expect(started).toEqual([1, 2, 3]); expect(maxActive).toBe(2); releases.get(2)?.(); releases.get(3)?.(); await waitFor(() => started.length === 4); releases.get(4)?.(); await queue; expect(maxActive).toBe(2); }); test("backs off failed tile keys and resets after success", () => { const backoff = createDashboardTileBackoff(); backoff.recordFailure("telemetry:infra-ram", 1_000); expect(backoff.canAttempt("telemetry:infra-ram", 15_999)).toBe(false); expect(backoff.canAttempt("telemetry:infra-ram", 16_000)).toBe(true); backoff.recordFailure("telemetry:infra-ram", 16_000); expect(backoff.canAttempt("telemetry:infra-ram", 45_999)).toBe(false); expect(backoff.canAttempt("telemetry:infra-ram", 46_000)).toBe(true); backoff.recordFailure("telemetry:infra-ram", 46_000); backoff.recordFailure("telemetry:infra-ram", 106_000); expect(backoff.canAttempt("telemetry:infra-ram", 225_999)).toBe(false); expect(backoff.canAttempt("telemetry:infra-ram", 226_000)).toBe(true); backoff.recordSuccess("telemetry:infra-ram"); expect(backoff.canAttempt("telemetry:infra-ram", 107_000)).toBe(true); }); test("stores only ready tile snapshots and restores them for matching revisions", () => { const storage = createMemoryStorage(); let now = 1_000; const store = createDashboardTileSnapshotStore(storage, { now: () => now, }); store.saveReadyTile({ currentRevisionId: "revision-a", response: { state: "ready", tile: { kind: "telemetry", id: "infra-ram" }, item: { id: "infra-ram", label: "Infra RAM", value: { kind: "percent", value: 42 }, detail: "live", severity: "ok", }, }, schemaVersion: "dashboard.v1", }); store.saveTile({ currentRevisionId: "revision-a", response: { state: "not_found", tile: { kind: "telemetry", id: "missing" }, message: "Missing", }, schemaVersion: "dashboard.v1", }); store.saveTile({ currentRevisionId: "revision-a", response: { state: "disabled", tile: { kind: "module", id: "disabled-module" }, message: "Disabled", }, schemaVersion: "dashboard.v1", }); now = 61_000; const restored = store.restore({ currentRevisionId: "revision-a", schemaVersion: "dashboard.v1", }); expect(restored).toEqual([ { ageMs: 60_000, response: { state: "ready", tile: { kind: "telemetry", id: "infra-ram" }, item: { id: "infra-ram", label: "Infra RAM", value: { kind: "percent", value: 42 }, detail: "live - stale 60s", severity: "stale", }, }, }, ]); expect(store.restore({ currentRevisionId: "revision-b", schemaVersion: "dashboard.v1", })).toEqual([]); }); test("ignores malformed stored tile snapshots", () => { const storage = createMemoryStorage(); storage.setItem( "dimensionlab.dashboard.tiles.v1", JSON.stringify({ currentRevisionId: "revision-a", schemaVersion: "dashboard.v1", tiles: [ {}, { item: { id: "incomplete", detail: "live", }, savedAt: 1_000, tile: { kind: "telemetry", id: "incomplete" }, }, { item: { id: "infra-ram", label: "Infra RAM", value: { kind: "percent", value: 42 }, detail: "live", severity: "ok", }, savedAt: 1_000, tile: { kind: "telemetry", id: "infra-ram" }, }, { item: { id: "broken-detail", detail: 42 }, savedAt: 1_000, tile: { kind: "telemetry", id: "broken-detail" }, }, ], version: 1, }), ); const store = createDashboardTileSnapshotStore(storage, { now: () => 16_000, }); expect(store.restore({ currentRevisionId: "revision-a", schemaVersion: "dashboard.v1", })).toEqual([ { ageMs: 15_000, response: { state: "ready", tile: { kind: "telemetry", id: "infra-ram" }, item: { id: "infra-ram", label: "Infra RAM", value: { kind: "percent", value: 42 }, detail: "live - stale 15s", severity: "stale", }, }, }, ]); }); }); async function waitFor(predicate: () => boolean) { for (let attempt = 0; attempt < 20; attempt += 1) { if (predicate()) return; await Promise.resolve(); } throw new Error("condition was not met"); } function createMemoryStorage(): Storage { const values = new Map(); return { get length() { return values.size; }, clear() { values.clear(); }, getItem(key) { return values.get(key) ?? null; }, key(index) { return [...values.keys()][index] ?? null; }, removeItem(key) { values.delete(key); }, setItem(key, value) { values.set(key, value); }, }; }