diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaef92d..94cc79e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,7 @@ jobs: name: playwright-report-${{ matrix.shard }} path: | playwright-report/ + privacy-report/ test-results/ retention-days: 7 diff --git a/.gitignore b/.gitignore index 23e4177..fee139f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ pnpm-debug.log* # Local development documents (instructions and roadmap), kept on this machine only /CLAUDE.md /PLAN.md +privacy-report/ diff --git a/README.md b/README.md index 5e66819..4b1df29 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Nothing reaches `main` without a green `CI passed` check (enforced by a reposito | Lint, format, types | ESLint (typescript-eslint strict), Prettier, `astro check` | | Unit tests | Vitest | | Build-output checks | Metadata, legal pages, links and anchors, security headers, CSP, contact data protection | -| End-to-end tests | Playwright (desktop and mobile), privacy guard (same-origin only, nothing stored), axe | +| End-to-end tests | Playwright (desktop and mobile), layout guard and privacy guard (own packages), axe | | Lighthouse | Every category at least 95 on mobile, size budgets, no third-party requests | | Security | CodeQL, dependency review, `pnpm audit`, OpenSSF Scorecard, Dependabot, secret scanning | | Workflow lint | `actionlint` for the workflow files, actions pinned to commit SHAs | diff --git a/package.json b/package.json index 19edcfa..fff62f7 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "lighthouse": "^13.5.0", "lint-staged": "^17.6.0", "playwright-layout-guard": "0.3.0", + "playwright-privacy-guard": "0.2.0", "prettier": "^3.9.10", "prettier-plugin-astro": "^1.1.2", "prettier-plugin-tailwindcss": "^0.8.1", diff --git a/playwright.config.ts b/playwright.config.ts index 3884bc7..03bd209 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,8 @@ import { defineConfig, devices } from '@playwright/test'; import type { LayoutGuardOptions } from 'playwright-layout-guard'; +import type { PrivacyGuardOptions } from 'playwright-privacy-guard'; + +import { privacyGuardOptions } from './tests/e2e/privacy-options'; // The ports: `astro dev` is 4321 (WebStorm or Claude, only one of them at a time), the browser tests // start their own `astro preview` on 4322, Lighthouse uses 4323. A server that already stands on @@ -8,7 +11,10 @@ import type { LayoutGuardOptions } from 'playwright-layout-guard'; const port = 4322; const isCI = Boolean(process.env.CI); -export default defineConfig<{ layoutGuard: boolean | LayoutGuardOptions }>({ +export default defineConfig<{ + layoutGuard: boolean | LayoutGuardOptions; + privacyGuard: boolean | PrivacyGuardOptions; +}>({ testDir: './tests/e2e', fullyParallel: true, forbidOnly: isCI, @@ -23,12 +29,20 @@ export default defineConfig<{ layoutGuard: boolean | LayoutGuardOptions }>({ ...(isCI ? { workers: 4 } : {}), retries: isCI ? 1 : 0, // `list` prints every test as it finishes, so a stuck run is visible in the CI log - reporter: isCI ? [['list'], ['github'], ['html', { open: 'never' }]] : [['list']], + reporter: [ + ['list'], + ...(isCI ? ([['github'], ['html', { open: 'never' }]] as const) : []), + // One page for the whole run with everything the privacy guard found (also in the summary of a CI run) + // The tests that make a page misbehave on purpose are not part of the report + ['playwright-privacy-guard/reporter', { ignore: [/canary/] }], + ], use: { baseURL: `http://localhost:${String(port)}`, // On this site not even the height of a page may change, also not when it gets lower (the default // of the layout guard is to count only a page that gets higher) layoutGuard: { pageHeight: 'any' }, + // The promise of the privacy policy, for every test (what is watched: privacy-options.ts) + privacyGuard: privacyGuardOptions, trace: 'on-first-retry', // Fail fast instead of waiting minutes when the browser cannot start launchOptions: { timeout: 60_000 }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c75d0f3..c387ad4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -269,6 +269,9 @@ importers: playwright-layout-guard: specifier: 0.3.0 version: 0.3.0(@playwright/test@1.64.1) + playwright-privacy-guard: + specifier: 0.2.0 + version: 0.2.0(@playwright/test@1.64.1) prettier: specifier: ^3.9.10 version: 3.9.10 @@ -3269,6 +3272,12 @@ packages: peerDependencies: '@playwright/test': '>=1.45.0' + playwright-privacy-guard@0.2.0: + resolution: {integrity: sha512-JJx2UkwBvtXMzNzF5a9nGG5vEQNB809oq5Fo/blHH8Nhnhs9yJaL1W0hpy3y+aXIqxtGPYKAPeAy37+M9QRhwQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@playwright/test': '>=1.45.0' + playwright@1.64.1: resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==} engines: {node: '>=20'} @@ -7290,6 +7299,10 @@ snapshots: dependencies: '@playwright/test': 1.64.1 + playwright-privacy-guard@0.2.0(@playwright/test@1.64.1): + dependencies: + '@playwright/test': 1.64.1 + playwright@1.64.1: dependencies: playwright-core: 1.64.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 218421b..dc007ba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,11 @@ allowBuilds: esbuild: true workerd: true +# pnpm does not install a version that is younger than its minimum release age. playwright-privacy-guard +# is my own package and 0.2.0 was published on 2026-10-28: delete this entry once pnpm installs it +# without it (a few days after the release). +minimumReleaseAgeExclude: + - playwright-privacy-guard@0.2.0 # miniflare (inside wrangler) pins undici 7.29.1 exactly, and 7.29.1 has known denial of service # findings (patched in 7.30.1; found by pnpm audit on 2026-10-24). Written as a range of the bad # versions, so it does nothing once miniflare asks for a patched version itself: then delete it. diff --git a/scripts/lighthouse.mjs b/scripts/lighthouse.mjs index 127553a..ea5dd1a 100644 --- a/scripts/lighthouse.mjs +++ b/scripts/lighthouse.mjs @@ -33,10 +33,13 @@ const CATEGORIES = ['performance', 'accessibility', 'best-practices', 'seo']; * conservative. Measured on 2026-10-01 (with the contact form): scripts 4.5 KB, stylesheets 7.1 KB, total 134 KB. * The stylesheet limit was raised from 20 to 21 KB on 2026-10-18: the home page carries the pictures of * the tiles of all five games (the Backbone tile came last), it measured 20.2 KB with a smaller tile. + * It was raised from 21 to 22 KB on 2026-10-28: the project card of playwright-privacy-guard brings its + * own picture (the same kind as the one of the layout guard, a loop of ten seconds), the page measured + * 21.2 KB with it, and a card of this kind is part of the design of the page. */ const BUDGETS = { script: 10 * 1024, - stylesheet: 21 * 1024, + stylesheet: 22 * 1024, total: 200 * 1024, }; diff --git a/src/components/project-art/Privacy.astro b/src/components/project-art/Privacy.astro new file mode 100644 index 0000000..fd55183 --- /dev/null +++ b/src/components/project-art/Privacy.astro @@ -0,0 +1,452 @@ +--- +// The privacy guard in twelve seconds, in two halves. First half: a page sends requests out to another +// origin (a dashed box, "tracker.io") and a cookie lands in the browser; the box lights up amber and +// the guard's report says what it found (the test fails). Second half: the same page, but everything +// stays inside the window; the other origin stays dark and the guard says so (the test passes). +// +// Pure CSS and SVG, the same coordinates (240 by 200) and label size as the other pictures of the +// project cards. The base style of every part is the frame that tells the story of the first half +// (used with reduced motion, and before the animation starts); the keyframes play both halves and +// fade the whole scene out at the end of the loop. Times of the loop (12 s): the packets leave at 1.6 s and +// 2.0 s, the cookie lands at 3.4 s, the report at 4.0 s and stays until the switch to the second half at +// 5.6 s to 6.6 s (long enough to read it), the packet goes to and fro at 7 s, the pass at 8.6 s. +--- + + + + + privacy-guard + + + + + + + + + + + + + + + + + tracker.io + + + + + + + + + + + + + + _ga + + + + + + 2 problems: request, cookie + + + + + + + + + + + 0 problems, test passed + + + + + + A Page Calls Out + + + The Guard Fails the Test + + + Everything Stays Home + + + + + diff --git a/src/components/project-art/ProjectArt.astro b/src/components/project-art/ProjectArt.astro index b5f36a2..b1fcb58 100644 --- a/src/components/project-art/ProjectArt.astro +++ b/src/components/project-art/ProjectArt.astro @@ -9,6 +9,7 @@ import Defuze from '@/components/project-art/Defuze.astro'; import Grades from '@/components/project-art/Grades.astro'; import Guard from '@/components/project-art/Guard.astro'; import Pipeline from '@/components/project-art/Pipeline.astro'; +import Privacy from '@/components/project-art/Privacy.astro'; import Routing from '@/components/project-art/Routing.astro'; import Tetrys from '@/components/project-art/Tetrys.astro'; import { type ProjectArt } from '@/lib/projects'; @@ -30,6 +31,7 @@ const { art } = Astro.props; {art === 'grades' && } {art === 'pipeline' && } {art === 'guard' && } + {art === 'privacy' && }