diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87f1834..fdd474d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: node-version: "22" - run: npm ci - run: npm test + - run: npx playwright install --with-deps chromium + - run: npm run test:browser compatibility: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a69b5..c68491f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Add a browser test suite running the refresh modules against real Chromium and + a real Turbo build, covering `component_refresh.js`, which previously had no + tests at all. Every batching defect that reached production passed the jsdom + suite, because jsdom cannot model Turbo applying a morph, task boundaries + between socket deliveries, or abort semantics. - Verify the database server. Each adapter reports its version against the oldest one Solid Objects is exercised against, PostgreSQL 13, MySQL 8.0, and SQLite 3.35, and MySQL additionally confirms that Solid Objects tables use diff --git a/docs/roadmap.md b/docs/roadmap.md index 270a9bf..e839512 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -47,9 +47,10 @@ - Compatibility CI across the supported span: Ruby 3.3 and 3.4 against Rails 8.0 and 8.1, pinned through `RAILS_VERSION` so the advertised range is verified rather than assumed -- A JavaScript suite covering the state payload and batched refresh browser - modules, run in CI with Node's test runner and jsdom, with every GitHub - Actions reference pinned to a commit SHA +- A JavaScript suite covering every browser module, run in CI with Node's test + runner and jsdom, plus a browser suite running the same modules against real + Chromium and a real Turbo build, with every GitHub Actions reference pinned to + a commit SHA ## Partially implemented @@ -75,9 +76,6 @@ distributed per-actor rate limits and global admission control do not. - Administration: actor and dead-letter views plus policy hooks exist; richer filtering, audit records, and bulk-safe tools do not. -- Browser module coverage: the state payload and batched refresh modules have - JavaScript tests; `component_refresh.js`, which drives individual morph - refreshes, does not. - Outboxes use portable status rows with polling indexes; future versions may introduce narrow ready/claimed membership tables for very large outboxes. diff --git a/package-lock.json b/package-lock.json index 22420f0..59f4b3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,9 @@ "": { "name": "solid_objects", "devDependencies": { - "jsdom": "^26.1.0" + "@hotwired/turbo": "^8.0.23", + "jsdom": "^26.1.0", + "playwright": "^1.62.1" } }, "node_modules/@asamuzakjp/css-color": { @@ -138,6 +140,16 @@ "node": ">=18" } }, + "node_modules/@hotwired/turbo": { + "version": "8.0.23", + "resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.23.tgz", + "integrity": "sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -214,6 +226,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -349,6 +376,38 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index f159960..a9abc2e 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,12 @@ "type": "module", "description": "Browser modules for the Solid Objects Rails engine", "scripts": { - "test": "node --test test/javascript/*.test.mjs" + "test": "node --test test/javascript/*.test.mjs", + "test:browser": "node --test test/browser/*.test.mjs" }, "devDependencies": { - "jsdom": "^26.1.0" + "@hotwired/turbo": "^8.0.23", + "jsdom": "^26.1.0", + "playwright": "^1.62.1" } } diff --git a/test/browser/browser_test_helper.mjs b/test/browser/browser_test_helper.mjs new file mode 100644 index 0000000..1d3380d --- /dev/null +++ b/test/browser/browser_test_helper.mjs @@ -0,0 +1,70 @@ +import http from "node:http" +import { readFile } from "node:fs/promises" +import { chromium } from "playwright" + +const ROOT = new URL("../../", import.meta.url) + +const FILES = { + "/turbo.js": "node_modules/@hotwired/turbo/dist/turbo.es2017-esm.js", + "/component_refresh.js": "app/assets/javascripts/solid_objects/component_refresh.js", + "/component_batch_refresh.js": "app/assets/javascripts/solid_objects/component_batch_refresh.js", + "/state_payload.js": "app/assets/javascripts/solid_objects/state_payload.js" +} + +// Serves the real browser modules alongside a real Turbo build, so tests +// exercise Turbo's own stream processing rather than a stand-in for it. +export async function startServer({ routes = {} } = {}) { + const server = http.createServer(async (request, response) => { + const url = new URL(request.url, "http://localhost") + const route = routes[url.pathname] + if (route) return route(request, response, url) + + const file = FILES[url.pathname] + if (file) { + const body = await readFile(new URL(file, ROOT), "utf8") + response.writeHead(200, { "Content-Type": "text/javascript" }) + return response.end(body) + } + + if (url.pathname === "/") { + response.writeHead(200, { "Content-Type": "text/html" }) + return response.end(page()) + } + + response.writeHead(404) + response.end("not found") + }) + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)) + const { port } = server.address() + return { server, origin: `http://127.0.0.1:${port}` } +} + +function page() { + return ` + +
+