From 520d2c928d93a9ae297d3926b0e9413de8f3df46 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 08:20:16 +0100 Subject: [PATCH] Visual E2E at five viewports, typecheck e2e-tests in the gate, drop unused icons --- CHANGELOG.md | 5 + README.md | 4 +- ci.sh | 5 + docs/reading-cost.md | 13 ++ e2e-tests/mock-oidc.ts | 10 +- e2e-tests/package-lock.json | 396 +++++++++++++++++++++++++++++++++++- e2e-tests/package.json | 7 +- e2e-tests/proxy.ts | 2 +- e2e-tests/tsconfig.json | 28 +++ e2e-tests/visual.spec.ts | 29 ++- src/ci-gate.test.ts | 7 +- src/ui/icons.ts | 3 - todo.md | 8 - views/partials/icons.ejs | 3 - 14 files changed, 488 insertions(+), 32 deletions(-) create mode 100644 e2e-tests/tsconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ee1ce..4ca4006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ at boot. Entries start at 0.3.0. ## Unreleased +### Every page carries a smaller icon sprite + +The inlined sprite no longer holds `i-chart`, `i-copy` and `i-download`, which nothing in the host or +the example plugins referenced. A plugin that referenced one renders a blank icon until the host registers it again. + ### The README defines its terms and states its intended scale A new Glossary defines host, plugin, manifest, chrome, nav fragment, permission name, system plugin diff --git a/README.md b/README.md index 7d64b51..4bd4ed7 100644 --- a/README.md +++ b/README.md @@ -1502,7 +1502,7 @@ no Node/browsers on the host. Five suites, each on its own stack: | Suite | Compose overlay | Covers | | --- | --- | --- | -| `visual.spec.ts` + `language.spec.ts` | `e2e-tests/compose.visual.yml` | Ory-free: the rendered design system (app shell, theme switch, mobile off-canvas, icon sprite, CSRF-guarded sign-out, landing, 404), plugin permission-gating, and [language switching](#languages-i18n) | +| `visual.spec.ts` + `language.spec.ts` | `e2e-tests/compose.visual.yml` | Ory-free: the rendered design system (app shell at five widths from 1920px to 320px, theme switch, mobile off-canvas, icon sprite, CSRF-guarded sign-out, landing, 404), plugin permission-gating, and [language switching](#languages-i18n) | | `auth-refresh.spec.ts` | `e2e-tests/compose.auth.yml` | The real Ory stack with the session→JWT TTL cut to 8s: a lapsed JWT is silently re-minted from the live Kratos session, and once revoked the stale cookie is cleared | | `oauth-login.spec.ts` | `e2e-tests/compose.oauth.yml` | Another app logging in through us — register a client, start an authorization flow, `/oauth2/login` accept, `/oauth2/consent` Allow → authorization code | | `full-flow.spec.ts` | `e2e-tests/compose.full.yml` | The browser UI: password + mocked-SSO login, menu filtering by permission, admin users/groups/permissions CRUD, OAuth2-clients register → one-time secret → delete, a gated plugin page, logout | @@ -1531,7 +1531,7 @@ Ory-backed ones share one backend and stay on Chromium. ### The full gate (one command) -`ci.sh` is the whole gate in one reproducible command — typecheck → unit tests → +`ci.sh` is the whole gate in one reproducible command — typecheck (the specs in `e2e-tests/` too) → unit tests → each E2E suite against its own fresh stack, with a guaranteed `down -v` after each (even on failure) and a non-zero exit on the first failure. Run it locally before a release, or wire it into your CI service: diff --git a/ci.sh b/ci.sh index 6bbbc33..fd4187f 100755 --- a/ci.sh +++ b/ci.sh @@ -53,6 +53,11 @@ docker compose build web step "Typecheck" docker compose run --rm --no-deps web npm run typecheck +# The specs import @playwright/test, which only the runner image carries, so the host's tsc can't +# resolve them: typecheck them in that image. --no-deps skips web and Ory; any suite's file builds it. +step "Typecheck e2e-tests" +docker compose -f compose.yml -f e2e-tests/compose.visual.yml run --user "$(id -u):$(id -g)" --rm --no-deps --build e2e npm run typecheck + step "Unit tests" units=$(docker compose run --rm --no-deps web npm test 2>&1) || { echo "$units"; exit 1; } echo "$units" | grep -E '^. (tests|pass|fail) ' || true diff --git a/docs/reading-cost.md b/docs/reading-cost.md index abc5218..ad8f288 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -59,3 +59,16 @@ Read most (opens and searches): - `public/css/styles.css` 4 - `CHANGELOG.md` 1 - `src/ui/stylesheet-floor.test.ts` 1 + +## Reading cost, chunk 70e46c48db80, finished 2026-10-31T07:17:56Z + +70843 new input tokens before the first commit. + +Read most (opens and searches): + +- `README.md` 18 +- `todo.md` 6 +- `public/css/styles.css` 5 +- `CHANGELOG.md` 2 +- `AGENTS.md` 1 +- `src/auth/jwt-middleware.ts` 1 diff --git a/e2e-tests/mock-oidc.ts b/e2e-tests/mock-oidc.ts index 8fe76e2..630634b 100644 --- a/e2e-tests/mock-oidc.ts +++ b/e2e-tests/mock-oidc.ts @@ -2,7 +2,7 @@ // never leaves the compose network. Auto-approves /authorize (no provider login UI), then signs an // RS256 id_token Kratos verifies against /jwks. stdlib only, in-memory, NOT app code. The single // host (mock-oidc:9000) is reachable by both the browser (/authorize) and Kratos (token/jwks). -import { createServer } from "node:http"; +import { createServer, type ServerResponse } from "node:http"; import { createSign, generateKeyPairSync, randomUUID } from "node:crypto"; const ISSUER = process.env.ISSUER ?? "http://mock-oidc:9000"; @@ -15,8 +15,8 @@ const KID = "mock-1"; const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); const jwk = { ...publicKey.export({ format: "jwk" }), alg: "RS256", kid: KID, use: "sig" }; -const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url"); -function idToken(nonce) { +const b64 = (o: unknown): string => Buffer.from(JSON.stringify(o)).toString("base64url"); +function idToken(nonce: string): string { const now = Math.floor(Date.now() / 1000); const header = b64({ alg: "RS256", kid: KID, typ: "JWT" }); const payload = b64({ @@ -27,8 +27,8 @@ function idToken(nonce) { return `${header}.${payload}.${sig}`; } -const codes = new Map(); // single-use auth code → the nonce Kratos sent (echoed into the id_token) -const json = (res, body) => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(body)); }; +const codes = new Map(); // single-use auth code → the nonce Kratos sent (echoed into the id_token) +const json = (res: ServerResponse, body: unknown): void => { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(body)); }; createServer((req, res) => { const url = new URL(req.url ?? "/", ISSUER); diff --git a/e2e-tests/package-lock.json b/e2e-tests/package-lock.json index f920776..912cafa 100644 --- a/e2e-tests/package-lock.json +++ b/e2e-tests/package-lock.json @@ -6,7 +6,9 @@ "": { "name": "plainpages-e2e", "devDependencies": { - "@playwright/test": "1.64.1" + "@playwright/test": "1.64.1", + "@types/node": "24.19.2", + "typescript": "7.0.2" } }, "node_modules/@playwright/test": { @@ -25,6 +27,356 @@ "node": ">=20" } }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/playwright": { "version": "1.64.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", @@ -53,6 +405,48 @@ "engines": { "node": ">=20" } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" } } } diff --git a/e2e-tests/package.json b/e2e-tests/package.json index 8b6314a..4853a39 100644 --- a/e2e-tests/package.json +++ b/e2e-tests/package.json @@ -4,9 +4,12 @@ "description": "Playwright E2E: design-system parity (visual), auth refresh, OAuth2 login/consent, and the full browser flow (login/menu/CRUD/plugin/logout).", "type": "module", "scripts": { - "test": "playwright test" + "test": "playwright test", + "typecheck": "tsc --noEmit" }, "devDependencies": { - "@playwright/test": "1.64.1" + "@playwright/test": "1.64.1", + "@types/node": "24.19.2", + "typescript": "7.0.2" } } diff --git a/e2e-tests/proxy.ts b/e2e-tests/proxy.ts index 0522435..211c4a0 100644 --- a/e2e-tests/proxy.ts +++ b/e2e-tests/proxy.ts @@ -11,7 +11,7 @@ const PORT = Number(process.env.PORT ?? 80); // Kratos public owns these prefixes (self-service flows, sessions, its well-known/schemas); the // browser hits them via the flow action + OIDC callbacks. Everything else is the web app. -const toKratos = (path) => ["/self-service", "/sessions", "/.well-known/ory", "/schemas"].some((p) => path === p || path.startsWith(`${p}/`)); +const toKratos = (path: string): boolean => ["/self-service", "/sessions", "/.well-known/ory", "/schemas"].some((p) => path === p || path.startsWith(`${p}/`)); createServer((req, res) => { const target = toKratos(req.url ?? "/") ? KRATOS : WEB; diff --git a/e2e-tests/tsconfig.json b/e2e-tests/tsconfig.json new file mode 100644 index 0000000..d2dc2a8 --- /dev/null +++ b/e2e-tests/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ESNext", + "types": ["node"], + + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "isolatedModules": true, + "noEmit": true, + "verbatimModuleSyntax": true, + + "strict": true, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["*.ts"] +} diff --git a/e2e-tests/visual.spec.ts b/e2e-tests/visual.spec.ts index 3ef4363..922aa9f 100644 --- a/e2e-tests/visual.spec.ts +++ b/e2e-tests/visual.spec.ts @@ -10,6 +10,15 @@ const SESSION_COOKIE = "plainpages_jwt"; // src/auth/login.ts — web verifies i const shot = (page: Page, name: string): Promise => page.screenshot({ fullPage: true, path: `artifacts/screenshots/${test.info().project.name}/${name}.png` }); +// Large desktop down to a small phone, either side of the stylesheet's 860px and 560px breakpoints. +const VIEWPORTS = [ + { name: "desktop-xl", width: 1920, height: 1080 }, + { name: "laptop", width: 1280, height: 800 }, + { name: "tablet", width: 768, height: 1024 }, + { name: "phone", width: 390, height: 844 }, + { name: "phone-small", width: 320, height: 568 }, +] as const; + // Sign a session JWT with the committed dev tokenizer key (bind-mounted at /repo/jwks.json), so the // gated dashboard renders for a "signed-in" user without standing up Ory — web verifies it // with the same key by `kid`, exactly as it verifies a real Kratos-tokenizer JWT. @@ -34,7 +43,7 @@ for (const [name, path, tail] of [ ["the starter dashboard", "/dashboard", ".form-actions .btn"], ["the public landing", "/", ".landing-actions .btn"], ] as const) { - for (const width of [1280, 390]) { + for (const { width } of VIEWPORTS) { test(`${name} scrolls to its end at ${width}px wide`, async ({ page }) => { await page.setViewportSize({ width, height: 200 }); await page.goto(path); @@ -77,13 +86,21 @@ test("captures the live pages for review", async ({ page }) => { await page.goto("/dashboard"); await page.locator("#theme-dark").check({ force: true }); // visually-hidden radio await shot(page, "live-03-dark"); - - await page.setViewportSize({ width: 390, height: 844 }); - await page.goto("/dashboard"); - await shot(page, "live-04-mobile"); - await page.setViewportSize({ width: 1280, height: 800 }); }); +// The page is a document, so a column wider than the viewport can only show as sideways scroll. +for (const { name, width, height } of VIEWPORTS) { + for (const [path, label] of [["/dashboard", "dashboard"], ["/", "landing"], ["/scheduling", "plugin"]] as const) { + test(`the ${label} page fits ${width}px wide, with no sideways scroll`, async ({ page }) => { + await page.setViewportSize({ width, height }); + await page.goto(path); + const [scrollWidth, clientWidth] = await page.evaluate(() => [document.documentElement.scrollWidth, document.documentElement.clientWidth] as const); + expect(scrollWidth).toBeLessThanOrEqual(clientWidth); + await shot(page, `live-${label}-${name}`); + }); + } +} + test("every icon resolves to a defined (no broken graphics)", async ({ page }) => { await page.goto("/dashboard"); const missing = await page.evaluate(() => { diff --git a/src/ci-gate.test.ts b/src/ci-gate.test.ts index b4956d2..ec44525 100644 --- a/src/ci-gate.test.ts +++ b/src/ci-gate.test.ts @@ -32,11 +32,16 @@ test("the commit-hash image is pushed even when the gate no-ops", () => { test("every E2E suite the gate runs writes its artifacts as the invoking user", () => { // The documented hand-run commands carry the same flag (src/compose.test.ts). const runs = gate.split("\n").filter((l) => /docker compose .*\brun\b.*\be2e\b/.test(l)); - assert.equal(runs.length, 2, "the suite helper and the devstack run"); + assert.equal(runs.length, 3, "the e2e-tests typecheck, the suite helper and the devstack run"); for (const line of runs) assert.match(line, /--user "\$\(id -u\):\$\(id -g\)"/, `runs as the caller: ${line.trim()}`); }); +test("the gate typechecks e2e-tests in the runner image, where @playwright/test resolves", () => { + assert.match(gate, /docker compose .*\brun\b.*--no-deps\b.*\be2e\b npm run typecheck/); + assert.match(read("e2e-tests/package.json"), /"typecheck": "tsc --noEmit"/); +}); + test("only *.md counts as docs; a dirty tree and a rename both count as changed", () => { assert.ok(gate.includes("\\.md$"), "the non-docs match is a *.md suffix test"); assert.match(gate, /git status --porcelain --no-renames/, "uncommitted code and a staged rename can never be skipped over"); diff --git a/src/ui/icons.ts b/src/ui/icons.ts index 3ea8ffe..8853ebf 100644 --- a/src/ui/icons.ts +++ b/src/ui/icons.ts @@ -9,12 +9,9 @@ export const ICON_NAMES: Record = { "i-bell": "bell", "i-box": "box", "i-cal": "calendar", - "i-chart": "chart-no-axes-column", "i-check-circle": "circle-check", "i-chev": "chevron-right", "i-cols": "columns-3", - "i-copy": "copy", - "i-download": "download", "i-edit": "pencil", "i-globe": "globe", "i-grid": "layout-grid", diff --git a/todo.md b/todo.md index aeba5fa..87050da 100644 --- a/todo.md +++ b/todo.md @@ -12,10 +12,7 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| -| 8 | 0.4.21 | | | **Run the visual E2E specs at viewports from a large desktop down to a small phone.** | 2 | 1 | 1 | 2 | 4 | code health by 3 | 9.0 | -| 9 | 0.4.21 | | | **Typecheck `e2e-tests/` in the gate.** | 1 | 1 | 1 | 1 | 3 | code health by 2 | 9.0 | | 24 | 0.4.21 | | | **Retire `src/ui/shell-context.ts` and render the dashboard from `ctx.chrome`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 9.0 | -| 17 | 0.4.21 | | | **Remove the icons no caller uses from `ICON_NAMES` and the sprite.** | 1 | 1 | 1 | 1 | 1 | 8 by 1, code health by 1 | 8.8 | | 21 | 0.4.21 | | | **Fail the gate on a function that exceeds a size or complexity limit.** | 3 | 1 | 1 | 3 | 5 | code health by 4 | 7.2 | | 32 | 0.4.21 | | | **Fail the gate on a stylesheet feature the browser floor lacks, unless it is guarded.** | 3 | 1 | 1 | 3 | 4 | code health by 2 | 6.0 | | 20 | 0.4.21 | | | **Redirect an anonymous hit on a session-needing flow page to sign-in, instead of a 500.** | 2 | 2 | 2 | 4 | 3 | 2 by 4 | 4.2 | @@ -42,11 +39,6 @@ `ShellModel`/`buildShellContext` has one consumer, the dashboard, and duplicates `PageChrome` on almost every field. Keep `shellUser` as the shared primitive. -### 17. Remove the icons no caller uses from `ICON_NAMES` and the sprite. - -`i-chart`, `i-copy` and `i-download` have no caller, and the sprite is inlined into every page. -AGENTS.md → "`ICON_NAMES` is a host-owned registry" already makes this routine cleanup. - ### 21. Fail the gate on a function that exceeds a size or complexity limit. Only `tsc --strict` runs today. A new tool is a new dependency, so justify it. diff --git a/views/partials/icons.ejs b/views/partials/icons.ejs index b5f2733..f53a6c2 100644 --- a/views/partials/icons.ejs +++ b/views/partials/icons.ejs @@ -5,12 +5,9 @@ - - -