§8 review checkpoint (todo §8); ran the architecture + product reviewers on the whole project and addressed findings. Critical (arch): "Testing & CI" shipped no CI automation — added scripts/ci.sh, the whole gate in one command (pin-lockstep check → typecheck → units (count guard) → the 4 E2E suites, each on its own named fresh stack with guaranteed down -v + non-zero exit on first failure). The gate immediately caught a latent bug: the auth-refresh suite booted Hydra (inherited §6 web→hydra dep) but the e2e overlays don't run Hydra with --dev, so it never went healthy — dropped Hydra from the auth suite's web deps (it never needed it). Product 🔴: the README Status note claimed auth/Hydra were unbuilt (false after §4/§6/§8) — corrected it + dropped the now-false _(planned)_ markers on the Auth/MVP sections. Product 🟡: added a login-only "Forgot password?" link (the recovery flow was unreachable from /login) and a data-table empty-state row (blank list tables, recurring deferral) — both tests-first. Docs: README Layout e2e line + e2e/package.json updated for the §8 suites. Stability-reviewer APPROVE-with-nits; addressed both (per-suite compose project names; grep || true) and fixed a project-name dot bug it introduced. Corrected a reviewer error (bootstrap uses restart on-failure:5, not unless-stopped). typecheck + 306 units green; scripts/ci.sh green end-to-end (visual 9 · auth 1 · oauth 2 · full 6), all stacks torn down. Deferred to §9: the app.ts internal route-table (raised urgency), visual-parity for admin/consent screens, a key-rotation E2E; L3 (plugin-api barrel in shifts.test) → the §8 test-cleanup item.

This commit is contained in:
2026-06-19 20:08:48 +02:00
parent 9d77f6ad17
commit bd20d00714
13 changed files with 120 additions and 17 deletions

View File

@@ -82,3 +82,15 @@ test("data-table renders a minimal table (plain string cells, no select/actions)
assert.match(flat(await render()), /<table class="table"><thead><tr><\/tr><\/thead><tbody><\/tbody><\/table>/);
});
test("data-table shows an empty-state row spanning all columns when there are no rows", async () => {
// colspan covers the data columns + the select + actions columns (2 + 1 + 1 = 4).
const html = flat(await render({ actions: true, columns: [{ label: "Name" }, { label: "Email" }], rows: [], selectable: true }));
assert.match(html, /<tbody><tr><td class="table-empty" colspan="4">Nothing here yet\.<\/td><\/tr><\/tbody>/);
// a caller-supplied message overrides the default
assert.match(flat(await render({ columns: [{ label: "Shift" }], emptyText: "No shifts yet.", rows: [] })), /<td class="table-empty" colspan="1">No shifts yet\.<\/td>/);
// a populated table has no empty-state row
assert.doesNotMatch(flat(await render({ columns: [{ label: "Name" }], rows: [{ cells: ["A"] }] })), /table-empty/);
});

View File

@@ -49,6 +49,7 @@ test("maps a password login flow: csrf hidden, themed email/password fields, a s
// Chrome derived from the flow type.
assert.equal(view.title, "Sign in");
assert.equal(view.alt?.href, "/registration");
assert.equal(view.recoverHref, "/recovery"); // login offers a path to password reset
assert.equal(view.messages.length, 0);
});
@@ -99,6 +100,7 @@ test("chrome varies per flow type: registration alt, recovery back link", () =>
const reg = buildFlowView(flow([]), "registration");
assert.equal(reg.title, "Create account");
assert.equal(reg.alt?.href, "/login");
assert.equal(reg.recoverHref, undefined); // only login shows the reset link
const rec = buildFlowView(flow([]), "recovery");
assert.equal(rec.back?.href, "/login");

View File

@@ -51,6 +51,7 @@ export interface FlowView extends FlowChrome {
hidden: { name: string; value: string }[];
messages: FlowMessage[];
method: string;
recoverHref?: string; // login only: a "Forgot password?" link to the recovery flow
sso: SsoProvider[]; // one per configured oidc provider; empty ⇒ no SSO section
}
@@ -142,6 +143,7 @@ export function buildFlowView(flow: Flow, type: FlowType): FlowView {
messages: (flow.ui.messages ?? []).map((m) => ({ text: m.text, tone: tone(m.type) })),
method: flow.ui.method || "post",
sso,
...(type === "login" ? { recoverHref: "/recovery" } : {}),
...CHROME[type],
};
}