import { describe, expect, test } from "vitest"; import { attachDashboardRefreshLifecycle, collectVisibleDashboardModelIds, createDashboardPerformanceMarks, createDashboardRefreshDelay, dashboardPerformanceMarks, createDashboardTileSnapshotStore, createDashboardTileBackoff, createDashboardRequestAborter, runDashboardHydrationQueue, runDashboardHydrationBatchQueue, runViewportAwareDashboardHydrationQueue, shouldPauseDashboardRefresh, splitDashboardHydrationItemsByVisibility, waitForDashboardHydrationIdle, type DashboardIntersectionEntry, } 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("hydrates queued work in fixed-size batches", async () => { const batches: number[][] = []; await runDashboardHydrationBatchQueue({ batchSize: 3, hydrateBatch: (items) => { batches.push(items); }, items: [1, 2, 3, 4, 5, 6, 7], signal: new AbortController().signal, }); expect(batches).toEqual([ [1, 2, 3], [4, 5, 6], [7], ]); }); test("hydrates visible items before deferred items and waits for idle", async () => { const calls: string[] = []; const idleReleases: Array<() => void> = []; const hydration = runViewportAwareDashboardHydrationQueue({ collectVisibleModelIds: async () => new Set(["b"]), concurrency: 1, getModelId: (item) => item, hydrate: async (item) => { calls.push(item); }, items: ["a", "b", "c"], onAllItemsSettled: () => calls.push("all-settled"), onVisibleItemsSettled: () => calls.push("visible-settled"), signal: new AbortController().signal, waitForIdle: async () => { calls.push("idle"); await new Promise((resolve) => idleReleases.push(resolve)); }, }); await waitFor(() => calls.includes("idle")); expect(calls).toEqual(["b", "visible-settled", "idle"]); idleReleases.shift()?.(); await hydration; expect(calls).toEqual([ "b", "visible-settled", "idle", "a", "c", "all-settled", ]); }); test("splits visible hydration items while preserving document order", () => { expect(splitDashboardHydrationItemsByVisibility({ getModelId: (item) => item.id, items: [{ id: "status" }, { id: "telemetry" }, { id: "service" }], visibleModelIds: new Set(["service", "status"]), })).toEqual({ visible: [{ id: "status" }, { id: "service" }], deferred: [{ id: "telemetry" }], }); }); test("collects visible data-model-id elements with IntersectionObserver", async () => { const elements = [ modelElement("status"), modelElement("telemetry"), modelElement("unrelated"), ]; let callback: | ((entries: DashboardIntersectionEntry[]) => void) | undefined; let finishObservation: (() => void) | undefined; let disconnected = false; const observed: string[] = []; const visible = collectVisibleDashboardModelIds({ createObserver: (observerCallback) => { callback = observerCallback; return { disconnect() { disconnected = true; }, observe(element) { observed.push(element.getAttribute("data-model-id") || ""); }, }; }, documentTarget: { querySelectorAll: () => elements, }, modelIds: ["status", "telemetry"], setTimeout: (handler) => { finishObservation = handler; return 1 as unknown as ReturnType; }, clearTimeout: () => undefined, signal: new AbortController().signal, }); callback?.([ { isIntersecting: false, intersectionRatio: 0, target: elements[0], }, { isIntersecting: true, target: elements[1], }, ]); finishObservation?.(); expect(await visible).toEqual(new Set(["telemetry"])); expect(observed).toEqual(["status", "telemetry"]); expect(disconnected).toBe(true); }); test("waits for requestIdleCallback when available", async () => { let idleCallback: (() => void) | undefined; let cancelledIdle: number | undefined; const wait = waitForDashboardHydrationIdle({ cancelIdleCallback: (handle) => { cancelledIdle = handle; }, requestIdleCallback: (callback) => { idleCallback = callback; return 7; }, signal: new AbortController().signal, }); expect(cancelledIdle).toBeUndefined(); idleCallback?.(); await wait; expect(cancelledIdle).toBe(7); }); test("jitters refresh delays and slows repeated failures", () => { const delay = createDashboardRefreshDelay({ jitterRatio: 0.1, random: () => 1, }); expect(delay.nextDelayMs(1_000)).toBe(1_100); delay.recordFailure(); expect(delay.nextDelayMs(1_000)).toBe(2_200); delay.recordFailure(); expect(delay.nextDelayMs(1_000)).toBe(4_400); delay.recordSuccess(); expect(delay.nextDelayMs(1_000)).toBe(1_100); }); test("marks dashboard performance milestones once per shell run", () => { const marks: string[] = []; const performanceMarks = createDashboardPerformanceMarks({ mark: (name) => marks.push(name), }); performanceMarks.markShellLoad(); performanceMarks.markFirstTileReady(); performanceMarks.markFirstTileReady(); performanceMarks.markVisibleTilesReady(); performanceMarks.markAllTilesSettled(); performanceMarks.markShellLoad(); performanceMarks.markFirstTileReady(); expect(marks).toEqual([ dashboardPerformanceMarks.shellLoad, dashboardPerformanceMarks.firstTileReady, dashboardPerformanceMarks.visibleTilesReady, dashboardPerformanceMarks.allTilesSettled, dashboardPerformanceMarks.shellLoad, dashboardPerformanceMarks.firstTileReady, ]); }); 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); }, }; } function modelElement(modelId: string) { return { getAttribute(name: string) { return name === "data-model-id" ? modelId : null; }, }; }