From 8b684a17d20a6130635e081e383b9b79085d37cc Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Tue, 8 Sep 2026 19:32:37 +0200 Subject: [PATCH] feat(examples)+test(e2e): counter service worker; bankai dev+prod in Chromium The counter example gains a sw.js exercising bankai's injected-precache build. e2e proves both bankai paths in real Chromium: the built app serves, hydrates and runs with zero console errors and zero mismatch warnings (and choo consumed window.initialState), and the dev server's Vite-transformed virtual entry hydrates the same way. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd --- examples/counter/sw.js | 28 ++++++++++++++++ test/e2e/bankai.test.js | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 examples/counter/sw.js create mode 100644 test/e2e/bankai.test.js diff --git a/examples/counter/sw.js b/examples/counter/sw.js new file mode 100644 index 0000000..b1f138d --- /dev/null +++ b/examples/counter/sw.js @@ -0,0 +1,28 @@ +/* global __BANKAI_ASSETS__ */ +// Service worker built by `bankai build`: the precache list is injected +// at build time from the asset manifest (the choo-service-worker +// convention, manifest edition). Register it from your app with +// navigator.serviceWorker.register('/sw.js'). + +const CACHE = 'counter-v1' +const ASSETS = __BANKAI_ASSETS__ + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE).then((cache) => cache.addAll(ASSETS)) + ) +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))) + ) + ) +}) + +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request).then((hit) => hit || fetch(event.request)) + ) +}) diff --git a/test/e2e/bankai.test.js b/test/e2e/bankai.test.js new file mode 100644 index 0000000..7d5e4cb --- /dev/null +++ b/test/e2e/bankai.test.js @@ -0,0 +1,72 @@ +// bankai v10 end-to-end in real Chromium: the production pipeline +// (build → serve → SSR page hydrates and runs) and the dev server +// (Vite middleware + virtual entry + streaming SSR). + +import { test, before, after } from 'node:test' +import assert from 'node:assert' +import { mkdtemp, rm } from 'node:fs/promises' +import { join, dirname } from 'node:path' +import { tmpdir } from 'node:os' +import { fileURLToPath } from 'node:url' +import { chromium } from 'playwright' + +import build from '../../packages/bankai/lib/build.js' +import serve from '../../packages/bankai/lib/serve.js' +import dev from '../../packages/bankai/lib/dev.js' + +const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..') +const entry = join(repo, 'examples', 'counter', 'app.js') + +let browser, outDir, prod, devSrv + +before(async () => { + browser = await chromium.launch() + outDir = await mkdtemp(join(tmpdir(), 'bankai-e2e-')) + await build({ entry, outDir, title: 'counter' }) + prod = await serve({ outDir, port: 0 }) + devSrv = await dev({ entry, port: 0, title: 'counter dev' }) +}) + +after(async () => { + await browser?.close() + await prod?.close() + await devSrv?.close() + if (outDir) await rm(outDir, { recursive: true, force: true }) +}) + +async function openAndCount (origin) { + const page = await browser.newPage() + const errors = [] + const warnings = [] + page.on('console', (msg) => { + if (msg.type() === 'error') errors.push(msg.text()) + if (msg.type() === 'warning') warnings.push(msg.text()) + }) + page.on('pageerror', (err) => errors.push(String(err))) + + await page.goto(origin + '/') + await page.waitForFunction(() => document.querySelector('h1')?.textContent === 'count is 0') + + await page.click('button') + await page.waitForFunction(() => document.querySelector('h1')?.textContent === 'count is 1') + + return { page, errors, warnings } +} + +test('production: built app serves, hydrates and runs with no errors', async () => { + const { page, errors, warnings } = await openAndCount(prod.origin) + + assert.deepStrictEqual(errors, [], 'no console errors') + const mismatches = warnings.filter((w) => w.includes('markup differ')) + assert.deepStrictEqual(mismatches, [], 'no hydration mismatch warnings') + + const state = await page.evaluate(() => window.choo ? 'devtools' : typeof window.initialState) + assert.strictEqual(state, 'undefined', 'choo consumed initialState (deleted after merge)') + await page.close() +}) + +test('dev server: Vite-transformed entry hydrates and runs', async () => { + const { page, errors } = await openAndCount(devSrv.origin) + assert.deepStrictEqual(errors, [], 'no console errors') + await page.close() +})