143 lines
4.6 KiB
TypeScript
143 lines
4.6 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import {
|
|
attachDashboardRefreshLifecycle,
|
|
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<number, () => 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<void>((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);
|
|
});
|
|
});
|
|
|
|
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");
|
|
}
|