diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d1735f..7078b57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,3 +18,14 @@ jobs: node-version: ${{ matrix.node }} - run: npm install - run: npm test + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm install + - run: npx playwright install --with-deps chromium-headless-shell + - run: npm run test:e2e diff --git a/examples/counter/app.js b/examples/counter/app.js index b3bd1fc..d86d53e 100644 --- a/examples/counter/app.js +++ b/examples/counter/app.js @@ -10,6 +10,9 @@ export default function createApp () { const app = choo() app.use(countStore) app.route('/', mainView) + // the demo gets served from arbitrary subpaths (npx serve ., test + // servers); a wildcard fallback makes it mount anywhere + app.route('*', mainView) return app } diff --git a/examples/counter/index.html b/examples/counter/index.html index dc62a3e..4ddccf2 100644 --- a/examples/counter/index.html +++ b/examples/counter/index.html @@ -17,7 +17,8 @@ "@choojs/core/timing": "../../packages/core/lib/timing.js", "@choojs/html": "../../packages/html/browser.js", "@choojs/html/raw": "../../packages/html/raw.js", - "@choojs/html/morph": "../../packages/html/morph.js" + "@choojs/html/morph": "../../packages/html/morph.js", + "@choojs/html/hydrate": "../../packages/html/hydrate.js" } } diff --git a/package.json b/package.json index 522d36d..fa77d2e 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,14 @@ "node": ">=24" }, "scripts": { - "test": "node --test packages/core/test/ packages/html/test/ packages/component/test/" + "test": "node --test packages/core/test/ packages/html/test/ packages/component/test/", + "test:e2e": "node --test test/e2e/", + "bench": "node bench/render.js" }, "repository": "choojs/choo", "license": "MIT", "devDependencies": { - "happy-dom": "^20.14.0" + "happy-dom": "^20.14.0", + "playwright": "^1.63.0" } } diff --git a/packages/core/lib/raf.js b/packages/core/lib/raf.js index 15dac42..f3d8da8 100644 --- a/packages/core/lib/raf.js +++ b/packages/core/lib/raf.js @@ -7,7 +7,10 @@ export default function nanoraf (render, raf) { equal(typeof render, 'function', 'nanoraf: render should be a function') ok(typeof raf === 'function' || typeof raf === 'undefined', 'nanoraf: raf should be a function or undefined') - if (!raf) raf = globalThis.requestAnimationFrame + // Wrap rather than alias: calling an extracted requestAnimationFrame + // with no receiver throws 'Illegal invocation' in strict-mode ESM + // (the CJS original survived only thanks to sloppy-mode this-patching). + if (!raf) raf = (cb) => globalThis.requestAnimationFrame(cb) let redrawScheduled = false let args = null diff --git a/packages/html/hydrate.js b/packages/html/hydrate.js index 5c433e2..8656dbf 100644 --- a/packages/html/hydrate.js +++ b/packages/html/hydrate.js @@ -42,7 +42,10 @@ function firstDifference (a, b, path) { } if (a.nodeType === 3 || a.nodeType === 8) { // text, comment - if (a.nodeValue !== b.nodeValue) { + // whitespace-insensitive: the parser shuffles insignificant + // whitespace (e.g. text after
, scripts belong in : type="module" is + // deferred by definition, and body must contain only what the view + // renders or hydration would (rightly) flag the extra nodes. + return ` + + + +