Problem
Split off from #774 (which landed the config-level testing wins: parallel Node/Bun e2e jobs, the WebKit + Firefox browser matrix, and opt-in coverage). This issue covers the one heavy, separable item deliberately deferred from that PR because it is a large, careful refactor rather than a minimal one.
test/e2e/e2e.test.mjs (~144KB, ~119 test() blocks) drives everything through Puppeteer against the running blog, but a large share of those blocks assert only on the HTTP response, the SSR HTML string, response headers, or the importmap, none of which need a real browser. Each Puppeteer navigation is 10-50x slower than a fetch() against the same server, and the whole suite now runs twice (Node + Bun), so every demotable block is paid for four times over.
Design / approach
Add a fast fetch()-based node:test integration layer that spawns ONE blog server (reusing the e2e harness's existing spawn / freePort helpers) and asserts over plain fetch(), then move the non-browser blocks out of the Puppeteer file into it and delete them from the Puppeteer suite. Keep in Puppeteer only blocks that genuinely need a DOM: page.evaluate, navigation/hydration, client-router, slots, view transitions, streaming-into-DOM.
Clear demotion candidates already spotted in test/e2e/e2e.test.mjs (audit each, do not assume): import map includes all framework entries (~L195), modulepreload links are deduplicated (~L207), every modulepreload resolves (~L216), health endpoint responds (~L672), /api/posts GET returns an array (~L1286), POST /api/posts without auth is rejected (~L1293), 404 for unknown pathname serves text/html (~L1306), no CSRF cookie on a GET response (~L1337), dynamic route renders post title in <head> (~L1025), unknown slug hits notFound 404 (~L1043), dashboard middleware redirects (~L1055), metadata <title>/<meta> per route (~L1240).
Also fold in the cheap cleanup deferred from #774: extract the per-file hand-rolled const assert = { ok, equal, deepEqual, ... } block (duplicated across 45 browser .test.js files) into one shared test/browser-assert.js module the tests import. Superset of methods actually used: ok, equal, deepEqual, strictEqual, notStrictEqual, notEqual, match, isTrue, isFalse, isOk, isArray, doesNotThrow, throws. Match the existing inline semantics exactly (strict equality, JSON-stringify deepEqual) so it is a pure drop-in.
Implementation notes (for the implementing agent)
- Where to edit: new
test/e2e/blog-integration.test.mjs (or test/ssr/blog-http.test.mjs); test/e2e/e2e.test.mjs (delete migrated blocks); a new shared assert module + 45 browser .test.js files for the dedup.
- Landmines:
- Invariants: no
.ts in packages/; AGENTS.md testing-layer rules.
- Tests + docs: update
agent-docs/testing.md (the new integration layer + the shared assert module).
Acceptance criteria
Problem
Split off from #774 (which landed the config-level testing wins: parallel Node/Bun e2e jobs, the WebKit + Firefox browser matrix, and opt-in coverage). This issue covers the one heavy, separable item deliberately deferred from that PR because it is a large, careful refactor rather than a minimal one.
test/e2e/e2e.test.mjs(~144KB, ~119test()blocks) drives everything through Puppeteer against the running blog, but a large share of those blocks assert only on the HTTP response, the SSR HTML string, response headers, or the importmap, none of which need a real browser. Each Puppeteer navigation is 10-50x slower than afetch()against the same server, and the whole suite now runs twice (Node + Bun), so every demotable block is paid for four times over.Design / approach
Add a fast
fetch()-basednode:testintegration layer that spawns ONE blog server (reusing the e2e harness's existingspawn/freePorthelpers) and asserts over plainfetch(), then move the non-browser blocks out of the Puppeteer file into it and delete them from the Puppeteer suite. Keep in Puppeteer only blocks that genuinely need a DOM:page.evaluate, navigation/hydration, client-router, slots, view transitions, streaming-into-DOM.Clear demotion candidates already spotted in
test/e2e/e2e.test.mjs(audit each, do not assume):import map includes all framework entries(~L195),modulepreload links are deduplicated(~L207),every modulepreload resolves(~L216),health endpoint responds(~L672),/api/posts GET returns an array(~L1286),POST /api/posts without auth is rejected(~L1293),404 for unknown pathname serves text/html(~L1306),no CSRF cookie on a GET response(~L1337),dynamic route renders post title in <head>(~L1025),unknown slug hits notFound 404(~L1043),dashboard middleware redirects(~L1055),metadata <title>/<meta> per route(~L1240).Also fold in the cheap cleanup deferred from #774: extract the per-file hand-rolled
const assert = { ok, equal, deepEqual, ... }block (duplicated across 45 browser.test.jsfiles) into one sharedtest/browser-assert.jsmodule the tests import. Superset of methods actually used:ok,equal,deepEqual,strictEqual,notStrictEqual,notEqual,match,isTrue,isFalse,isOk,isArray,doesNotThrow,throws. Match the existing inline semantics exactly (strict equality, JSON-stringify deepEqual) so it is a pure drop-in.Implementation notes (for the implementing agent)
test/e2e/blog-integration.test.mjs(ortest/ssr/blog-http.test.mjs);test/e2e/e2e.test.mjs(delete migrated blocks); a new shared assert module + 45 browser.test.jsfiles for the dedup.node --testeven when the blog is Bun-served (its node:test hook lifecycle does not survivebun test; only the spawned blog process changes runtime, see the header comment and dogfood: verify and enable Prisma-on-Bun so examples/blog runs on the Bun runtime #523).!==forequaland JSON-stringify fordeepEqual; replicate exactly or a subtle semantic drift will flip tests. Run the fullnpm run test:browseron all three engines after the swap.test/.../e2e/ortest/ssr/path so the rootrun-node-tests.jsdiscovers it correctly (it excludesbrowser/ande2e/segments; pick the path deliberately so it runs in the right driver)..tsinpackages/; AGENTS.md testing-layer rules.agent-docs/testing.md(the new integration layer + the shared assert module).Acceptance criteria
test/e2e/e2e.test.mjs; the Puppeteer suite retains only genuinely DOM-needing testsconst assert = {}blocks; browser suite stays green on Chromium/Firefox/WebKitagent-docs/testing.mdupdated