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.
+---
+
+
+
+
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' && }