From bcae35b63e6bec3d3a05594893bd12e04f8d3eaf Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Tue, 30 Jun 2026 07:48:22 -0700 Subject: [PATCH] =?UTF-8?q?test(static):=20E2E=20=E2=80=94=20static=20boot?= =?UTF-8?q?=20with=20no=20API=20+=20dream=20shader=20survives=20cross-orig?= =?UTF-8?q?in=20CORS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/e2e/README.md | 21 ++++++++++ simulator/e2e/playwright.static.config.ts | 11 ++++++ simulator/e2e/serve-static.mjs | 48 +++++++++++++++++++++++ simulator/e2e/tests/static-build.spec.ts | 30 ++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 simulator/e2e/playwright.static.config.ts create mode 100644 simulator/e2e/serve-static.mjs create mode 100644 simulator/e2e/tests/static-build.spec.ts diff --git a/simulator/e2e/README.md b/simulator/e2e/README.md index b1d43d7..0cdc3f2 100644 --- a/simulator/e2e/README.md +++ b/simulator/e2e/README.md @@ -32,3 +32,24 @@ npm test `playwright.config.ts` starts uvicorn on port 8099 automatically (`reuseExistingServer` locally) and points the tests at it. + +## Static-build E2E (Cloudflare publish) + +`tests/static-build.spec.ts` verifies the **built static site** (Cloudflare publish +— see `deploy/cloudflare/README.md`): it boots with NO `/api/*` server (baked JSON), +and the WebGL dream shader runs on **cross-origin** media without tainting the GL +texture (the CORS path R2 will use in production). + +It uses `playwright.static.config.ts` (no uvicorn `webServer`) and a dual static +server, `serve-static.mjs`, that serves the built `dist/` (app, :8077) and +`dist-media/` (media with CORS + Range, :8078): + +```bash +# from the repo root: build with the media base pointed at the local CORS server +.venv/bin/python -m tools.build_static --media-base http://localhost:8078/ + +# from this directory (simulator/e2e): start the dual server, then run the spec +DIST_DIR="$PWD/../../dist" MEDIA_DIR_E2E="$PWD/../../dist-media" node serve-static.mjs & +npx playwright test --config playwright.static.config.ts +``` + diff --git a/simulator/e2e/playwright.static.config.ts b/simulator/e2e/playwright.static.config.ts new file mode 100644 index 0000000..7c3705e --- /dev/null +++ b/simulator/e2e/playwright.static.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "@playwright/test"; + +// Static-build E2E: NO webServer — the built dist/ + dist-media/ are served by +// serve-static.mjs (started manually, see README.md), so there is no uvicorn/API. +// Run: npx playwright test --config playwright.static.config.ts +export default defineConfig({ + testDir: "./tests", + testMatch: /static-build\.spec\.ts/, + timeout: 60_000, + use: { actionTimeout: 10_000 }, +}); diff --git a/simulator/e2e/serve-static.mjs b/simulator/e2e/serve-static.mjs new file mode 100644 index 0000000..388a513 --- /dev/null +++ b/simulator/e2e/serve-static.mjs @@ -0,0 +1,48 @@ +// Serves the built dist/ (app) and dist-media/ (media, with CORS + Range) on two +// ports — a local stand-in for Pages + R2, so the static-build E2E exercises the +// cross-origin media path exactly as production will (CORS for the WebGL dream +// shader, Range for video scrubbing). +// +// Env: DIST_DIR (app root), MEDIA_DIR_E2E (media root). Ports: app 8077, media 8078. +import http from "node:http"; +import { createReadStream, statSync } from "node:fs"; +import { join, extname, normalize } from "node:path"; + +const TYPES = { + ".html": "text/html", ".js": "text/javascript", ".json": "application/json", + ".css": "text/css", ".mp4": "video/mp4", ".mp3": "audio/mpeg", +}; + +function serve(root, port, cors, indexFallback) { + http.createServer((req, res) => { + if (cors) { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Accept-Ranges", "bytes"); } + let p = decodeURIComponent(req.url.split("?")[0]); + if (p.endsWith("/")) p += "index.html"; + // contain to root (no path traversal) + const file = normalize(join(root, p)); + if (!file.startsWith(normalize(root))) { res.writeHead(403); return res.end("forbidden"); } + try { + const st = statSync(file); + res.setHeader("Content-Type", TYPES[extname(file)] || "application/octet-stream"); + const range = req.headers.range; // honor Range for video scrub + if (range && /^bytes=/.test(range)) { + const [s, e] = range.replace("bytes=", "").split("-"); + const start = +s, end = e ? +e : st.size - 1; + res.writeHead(206, { + "Content-Range": `bytes ${start}-${end}/${st.size}`, + "Content-Length": end - start + 1, + }); + createReadStream(file, { start, end }).pipe(res); + } else { + res.writeHead(200, { "Content-Length": st.size }); + createReadStream(file).pipe(res); + } + } catch { + res.writeHead(404); res.end("not found"); + } + }).listen(port); +} + +serve(process.env.DIST_DIR, 8077, false); +serve(process.env.MEDIA_DIR_E2E, 8078, true); +console.log("static app :8077 media(cors) :8078"); diff --git a/simulator/e2e/tests/static-build.spec.ts b/simulator/e2e/tests/static-build.spec.ts new file mode 100644 index 0000000..662b2e3 --- /dev/null +++ b/simulator/e2e/tests/static-build.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from "@playwright/test"; + +// Runs against the BUILT static site: dist/ served on :8077, media (CORS) on :8078 +// by serve-static.mjs. Build with `--media-base http://localhost:8078/` so config.js +// points media cross-origin — the production CORS path, locally. See README.md. +const APP = "http://localhost:8077/human-experience-simulator/"; + +test("boots fully static — no /api calls, ring + clips load from baked JSON", async ({ page }) => { + const apiCalls: string[] = []; + page.on("request", (r) => { if (r.url().includes("/api/")) apiCalls.push(r.url()); }); + await page.goto(APP); + await expect(page.locator("#vid")).toBeVisible({ timeout: 30_000 }); + expect(apiCalls, "static build must not call /api/*").toEqual([]); +}); + +test("dream shader runs on cross-origin media without tainting the GL texture", async ({ page }) => { + const errors: string[] = []; + page.on("pageerror", (e) => errors.push(String(e))); + await page.goto(APP); + await expect(page.locator("#vid")).toBeVisible({ timeout: 30_000 }); + // crank the Feel (Right) knob to engage the dream, let a few frames render + await page.evaluate(() => { + const r = document.getElementById("right") as HTMLInputElement; + r.value = "4"; + r.dispatchEvent(new Event("input", { bubbles: true })); + }); + await page.waitForTimeout(2000); + // a tainted GL texture throws SecurityError on draw; assert none surfaced + expect(errors.filter((e) => /SecurityError|tainted|cross-origin/i.test(e))).toEqual([]); +});