Checkpoint: current working state
Some checks failed
ci / check (push) Has been cancelled

This commit is contained in:
Lumen Stage1 2026-09-29 21:33:33 -05:00
commit e83946c207
57 changed files with 6257 additions and 775 deletions

177
tests/unit/status.test.ts Normal file
View file

@ -0,0 +1,177 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/**
* @vitest-environment jsdom
* Stage 9 — Status screen rendering and header-chip boot wiring.
* Trace: IMPLEMENTATION-CONTRACT.md §20.5, SPIKE-05 §3.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// @ts-expect-error fake-indexeddb types via exports fallback
import FDBFactory from "fake-indexeddb/lib/FDBFactory";
// @ts-expect-error fake-indexeddb types via exports fallback
import FDBKeyRange from "fake-indexeddb/lib/FDBKeyRange";
import { mount as boot } from "../../src/app/main.js";
import { createPrepGuidanceView, createStatusView } from "../../src/ui/views/status/status.js";
import type { BootReadinessReport } from "../../src/domain/readiness/types.js";
// jsdom implements <dialog> but not showModal — stub it like a real browser.
function stubDialog(): void {
const proto = window.HTMLDialogElement.prototype as unknown as Record<string, unknown>;
if (
typeof proto.showModal !== "function" ||
proto.showModal.toString().includes("not implemented")
) {
proto.showModal = function (this: Element): void {
this.setAttribute("open", "");
};
}
}
function report(patch: Partial<BootReadinessReport> = {}): BootReadinessReport {
return {
state: "READY",
reason: null,
missing: [],
failedOp: null,
level: "L2",
edition: "lumen-2026",
packageVersion: 1,
generatedAt: "2026-09-01T00:00:00.000Z",
fetchedAt: 1_750_000_000_000,
sections: [
{ id: "emergency", required: true, present: true, parseable: true },
{ id: "schedule", required: true, present: true, parseable: true },
],
assets: { total: 2, present: 2, missing: [] },
floorVersion: "1.0.0+1",
storage: { usage: 1024, quota: 2048 },
shellCache: "lumen-shell-test",
canRestore: false,
activationPending: false,
...patch,
};
}
function actions() {
return {
onCheckData: vi.fn(),
onGetData: vi.fn(),
onRestore: null as (() => void) | null,
onClose: vi.fn(),
};
}
describe("Status view", () => {
it("READY shows checklist, versions, storage, floor — no recovery actions", () => {
const view = createStatusView(report(), actions());
expect(view.querySelector("h1")?.textContent).toBe("Offline status");
expect(view.textContent).toContain("State: READY");
expect(view.textContent).toContain("v1");
expect(view.textContent).toContain("1.0 KB used of 2.0 KB");
expect(view.textContent).toContain("Emergency floor");
expect(view.textContent).not.toContain("Restore previous version");
expect(view.querySelectorAll(".status-check--ok")).toHaveLength(2);
});
it("PARTIAL enumerates missing and offers Continue setup", () => {
const a = actions();
const view = createStatusView(
report({ state: "PARTIAL", level: "L1", missing: ["map", "assets"] }),
a,
);
expect(view.textContent).toContain("Missing: map, assets");
const buttons = [...view.querySelectorAll("button")].map((b) => b.textContent);
expect(buttons).toContain("Continue setup");
view.querySelectorAll("button")[0]?.click();
expect(a.onGetData).toHaveBeenCalledTimes(1);
});
it("RECOVERY with retained slot offers both restore paths", () => {
const onRestore = vi.fn();
const view = createStatusView(report({ state: "RECOVERY", reason: "missing", level: "L2" }), {
...actions(),
onRestore,
});
const labels = [...view.querySelectorAll("button")].map((b) => b.textContent);
expect(labels).toContain("Restore festival data");
expect(labels).toContain("Restore previous version");
[...view.querySelectorAll("button")]
.find((b) => b.textContent === "Restore previous version")
?.click();
expect(onRestore).toHaveBeenCalledTimes(1);
});
it("FAILED offers Retry and keeps diagnostics on-device", () => {
const a = actions();
const view = createStatusView(report({ state: "FAILED", failedOp: "activation" }), a);
expect(view.textContent).toContain("Failed operation: activation");
[...view.querySelectorAll("button").values()].find((b) => b.textContent === "Retry")?.click();
expect(a.onCheckData).toHaveBeenCalledTimes(1);
});
it("BASELINE_ONLY explains itself with install guidance, no dead actions", () => {
const view = createStatusView(report({ state: "BASELINE_ONLY", level: "L0" }), actions());
expect(view.textContent).toContain("cannot save offline data");
expect(view.querySelector("button")?.textContent).toBe("Check my data");
});
it("renders no raw HTML — all content is text", () => {
const view = createStatusView(
report({ edition: "<img src=x>", missing: ["<b>map</b>"] }),
actions(),
);
expect(view.innerHTML).not.toContain("<img src=x>");
expect(view.innerHTML).not.toContain("<b>map</b>");
});
it("prep guidance has Back and Close", () => {
const onBack = vi.fn();
const onClose = vi.fn();
const view = createPrepGuidanceView(onBack, onClose);
expect(view.querySelector("h1")?.textContent).toBe("Get festival data");
[...view.querySelectorAll("button").values()]
.find((b) => b.textContent === "Back to status")
?.click();
[...view.querySelectorAll("button").values()].find((b) => b.textContent === "Close")?.click();
expect(onBack).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledTimes(1);
});
});
describe("header chip boot wiring", () => {
beforeEach(() => {
stubDialog();
history.replaceState(null, "", "/");
document.body.innerHTML = '<div id="app" aria-busy="true"></div>';
});
afterEach(() => {
document.body.innerHTML = "";
// Remove the polyfill for the no-IDB case.
delete (globalThis as Record<string, unknown>).indexedDB;
});
it("without storage the chip reports BASELINE_ONLY, never blank", async () => {
boot();
const chip = document.querySelector(".app-header__status");
await vi.waitFor(() => {
expect(chip?.textContent).toBe("SAVING UNAVAILABLE");
});
expect(chip?.getAttribute("aria-label")).toContain("cannot save offline data");
});
it("clicking the chip re-evaluates and opens the Status dialog, not a stale report", async () => {
const g = globalThis as unknown as Record<string, unknown>;
g.indexedDB = new FDBFactory() as unknown;
g.IDBKeyRange = FDBKeyRange as unknown;
boot();
const chip = document.querySelector<HTMLElement>(".app-header__status");
await vi.waitFor(() => {
expect(chip?.textContent).toBe("Get festival data");
});
// Fresh boot: NOT_READY. Clicking must re-evaluate (not reuse the
// boot-time report) — the dialog proves which report was used.
chip?.click();
await vi.waitFor(() => {
expect(document.querySelector("dialog[open]")?.textContent).toContain("State: NOT_READY");
});
});
});