diff --git a/README.md b/README.md index 20ea436..27727ef 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ you'd rather assemble pages from building blocks than fight a framework or hand- auth for the tenth time. Plainpages hands you the boring-but-hard parts (auth, authz, menu, design system, plugin host) and stays out of your domain logic. It's not a no-code tool and doesn't hide its moving parts: if "Ory is down ⇒ no logins" (see -[Auth](#auth-sessions--permissions-planned)) reads as obvious rather than a surprise, +[Auth](#auth-sessions--permissions)) reads as obvious rather than a surprise, you're the audience. ## Project goals @@ -46,16 +46,17 @@ makes **semantic, accessible markup** a priority: real landmarks, one `
| 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: [] })), / | 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/); +}); diff --git a/src/flow-view.test.ts b/src/flow-view.test.ts index 3485c3c..7b9951a 100644 --- a/src/flow-view.test.ts +++ b/src/flow-view.test.ts @@ -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"); diff --git a/src/flow-view.ts b/src/flow-view.ts index 8b9bc01..0ddf2c7 100644 --- a/src/flow-view.ts +++ b/src/flow-view.ts @@ -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], }; } diff --git a/todo.md b/todo.md index b4a8fe5..f124bd2 100644 --- a/todo.md +++ b/todo.md @@ -120,7 +120,7 @@ everything via Docker. - [x] node --test units across helpers / router / nav / auth (tests-first throughout). → Audited unit coverage across the four areas; built tests-first through §0–§7, it was already near-complete — **helpers** (`list-query`/`paginate`/`body`/`icons`/`config`/`context`/`flow-view`/`gen-jwks`/`hooks`/`shell-context`, `static` via `app.test.ts`), **router** (`router`/`view-resolver`/`plugin`/`discovery`), **nav** (`nav`/`nav-tree`/`chrome`/`menu-config`/dashboard merge), **auth** (`jwt`/`jwt-middleware`/`jwks`/`guards`/`login`/`csrf`/`cookie`/`kratos-*`/`keto-client`/`oauth-*`/`hydra-admin`) all carry direct `node --test` units. **One genuine gap closed:** `admin-nav.ts` — its pure nav helpers (`adminSection`/`adminNav`) and security-critical auth gates (`requireAdmin`/`guardedForm`, the shared gate+CSRF preamble for every admin write) were exercised only *indirectly* via the admin HTTP integration tests. Added `src/admin-nav.test.ts` (tests-first style, against the existing contract): `adminSection` (gated "Admin" header over the 4 screens, `current` marks+opens), `adminNav` (prepends Dashboard, role-filters the section — admin sees it, non-admin/anon get only Dashboard; asserts via `href` since composeNav strips `id` but keeps `current`), `requireAdmin` (anon→401→/login, non-admin→403, admin→user), `guardedForm` (valid double-submit→parsed body, missing/forged token→403, non-POST→undefined), `buildConfirmModel`. Only `server.ts` (entry-point composition root, exercised by every E2E boot) has no dedicated unit. 300 → 305 units; typecheck + tests green. Tests-only, no production code (no stability reviewer, per the §6/§7 test-cleanup precedent). - [x] **Playwright full E2E**: login (password + mocked SSO), menu filtering by role, users/groups/permissions CRUD, a plugin page, logout. → New browser-UI suite `e2e/full-flow.spec.ts` (`compose.e2e-full.yml`) — the real Playwright UI the earlier full-stack suites deferred here ("browser-UI login is owned by §8"). The themed login form posts straight to Kratos' action and cookies are host-scoped, so a tiny **stdlib reverse proxy** (`e2e/proxy.mjs`) fronts web + Kratos on **one origin** (the browser's only host), exactly like a prod reverse proxy; `ory/kratos/e2e-proxy.yml` points Kratos' base_url + every self-service URL at it, and Kratos runs `--dev` so cookies aren't marked Secure over http (Kratos marks them Secure for a non-loopback host like the gateway). Coverage (6 tests, all green): **password login** (themed form → Kratos → `/auth/complete` → dashboard); **mocked SSO** (a stdlib **mock OIDC provider** `e2e/mock-oidc.mjs` — RS256-signed id_token, nonce-bound single-use codes — wired via `SELFSERVICE_METHODS_OIDC_*` env + the committed claims jsonnet; click the provider button → auto-approve → identity created → signed in); **menu filtering by role** (the admin sees the gated Admin section + the plugin nav; anon/SSO-user don't); **users/groups/roles CRUD** (create → list → delete a user via the confirm step; create a group + role, each with a first member since a Keto set needs ≥1); the **permission-gated plugin page** (`/scheduling/shifts` renders the mock upstream's shifts in the native shell); **logout** (sign-out ends the session → back to /login, admin nav gone). **Found + fixed a real bug the E2E surfaced:** the SSO submit button sits in the same ` | |||