Skip to content

Commit d269eaf

Browse files
committed
test: add real browser QA
1 parent 887d6ac commit d269eaf

13 files changed

Lines changed: 372 additions & 2 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,15 @@ jobs:
5555
- run: corepack enable
5656
- run: pnpm install --frozen-lockfile
5757
- run: pnpm run test:postgresql
58+
59+
browser:
60+
runs-on: ubuntu-latest
61+
steps:
62+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
63+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
64+
with:
65+
node-version: "24.15.0"
66+
- run: corepack enable
67+
- run: pnpm install --frozen-lockfile
68+
- run: pnpm exec playwright install --with-deps chromium
69+
- run: pnpm run test:browser

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ node_modules/
22
dist/
33
.test-dist/
44
coverage/
5+
playwright-report/
6+
test-results/
57
*.tgz
68
*.sqlite3
79
.DS_Store

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
per runtime and polling as the correctness fallback.
3636
- Add a typed browser component registry with keyed dependencies, replace/morph
3737
strategies, batch coalescing, cancellation, and per-target revision fencing.
38+
- Add Chromium QA for native WebSocket subscription replay, realtime fences,
39+
personalized payloads, component batching, and request cancellation.
3840

3941
## 0.1.0 - 2026-08-13
4042

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,9 @@ application's synchronous `apply` callback; the library does not assume a DOM
649649
framework. The refresh endpoint must authenticate the request and reauthorize
650650
every requested component and dependency.
651651
652+
Run `pnpm run test:browser` after installing Playwright's Chromium build to
653+
exercise the browser entry through native WebSocket and browser APIs.
654+
652655
For subscriber-specific views, declare a static payload map with a TypeScript
653656
`satisfies` check:
654657

‎docs/browser-protocol.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ target as authorization. The application-owned refresh endpoint authenticates
8181
the browser and reauthorizes every requested component before returning render
8282
results.
8383

84+
`pnpm run test:browser` builds the browser entry and runs its Playwright suite
85+
in Chromium. It covers native WebSocket subscription replay, stale revision and
86+
incarnation handling, personalized payloads, component batching, and
87+
superseded-request cancellation.
88+
8489
`runtime.realtime` delivers directly to sessions in its own Node process. For
8590
several WebSocket processes, the configured `broadcast` callback publishes the
8691
committed envelope through a shared transport and each process passes received

‎docs/parity.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Reference: Ruby `solid_objects` 0.12.0 at commit `a01b6f5`.
7474
| Turbo scalar replacement | Not applicable | The browser client exposes invalidations to application rendering code. Framework adapters can be separate packages. |
7575
| Keyed component refresh, morph/replace, and batch coalescing | Native | A typed framework-neutral registry selects explicit dependencies, coalesces batch requests, aborts superseded work, fences each target, and delegates synchronous application strategy to the host. |
7676
| Personalized payload broadcasts | Native | Static typed projections run against committed state under each fresh subscriber context, reauthorize as queries, isolate failures, and carry independent revision fences. |
77-
| Real-browser compatibility suite | Planned | Exercise reconnection, incarnation changes, stale revisions, cancellation, batching, and payload delivery in Chromium. |
77+
| Real-browser compatibility suite | Native | Playwright exercises subscription replay over native WebSocket, incarnation/revision fences, payload delivery, component batching, and cancellation in Chromium. |
7878

7979
## Rails-specific surfaces
8080

‎package.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,24 @@
4848
"format": "prettier --write .",
4949
"format:check": "prettier --check .",
5050
"test": "vitest run",
51+
"test:browser": "pnpm run build && playwright test",
5152
"test:coverage": "vitest run --coverage",
5253
"test:postgresql": "vitest run test/postgresql.test.ts",
5354
"test:watch": "vitest",
5455
"pack:check": "pnpm pack --dry-run",
5556
"prepack": "pnpm run build"
5657
},
5758
"devDependencies": {
59+
"@playwright/test": "^1.62.1",
5860
"@types/node": "^24.0.0",
5961
"@types/pg": "^8.21.0",
62+
"@types/ws": "^8.18.1",
6063
"@vitest/coverage-v8": "^4.1.10",
6164
"pg": "^8.23.0",
6265
"prettier": "^3.9.6",
6366
"typescript": "^5.9.0",
64-
"vitest": "^4.1.10"
67+
"vitest": "^4.1.10",
68+
"ws": "^8.21.3"
6569
},
6670
"peerDependencies": {
6771
"pg": "^8.23.0"

‎playwright.config.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { defineConfig } from "@playwright/test"
2+
3+
export default defineConfig({
4+
testDir: "./test/browser",
5+
testMatch: "**/*.browser.ts",
6+
fullyParallel: false,
7+
forbidOnly: Boolean(process.env.CI),
8+
retries: process.env.CI ? 2 : 0,
9+
workers: 1,
10+
reporter: "line",
11+
use: {
12+
baseURL: "http://127.0.0.1:4179",
13+
browserName: "chromium",
14+
headless: true,
15+
},
16+
webServer: {
17+
command: "node test/browser-server.mjs",
18+
url: "http://127.0.0.1:4179",
19+
reuseExistingServer: false,
20+
},
21+
})

‎pnpm-lock.yaml‎

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/browser/components.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,25 @@ export class SolidObjectsComponentRegistry<Rendered> {
249249
pending: PendingRefresh
250250
results: readonly ComponentRefreshResult<Rendered>[]
251251
}): void {
252+
if (!Array.isArray(options.results)) throw new TypeError("component results must be an array")
252253
const requested = options.pending.components
253254
const seen = new Set<string>()
255+
const validated: Array<{
256+
component: RegisteredComponent
257+
result: ComponentRefreshResult<Rendered>
258+
}> = []
254259
for (const result of options.results) {
260+
if (!isComponentRefreshResult<Rendered>(result)) {
261+
throw new TypeError("component results must contain a target and rendered value")
262+
}
255263
if (seen.has(result.target))
256264
throw new TypeError(`duplicate component result ${result.target}`)
257265
seen.add(result.target)
258266
const component = requested.get(result.target)
259267
if (!component) throw new TypeError(`unexpected component result ${result.target}`)
268+
validated.push({ component, result })
269+
}
270+
for (const { component, result } of validated) {
260271
if (this.#components.get(componentIdentity(component)) !== component) continue
261272
const current = this.#applied.get(result.target)
262273
if (
@@ -366,3 +377,12 @@ function supersedes(current: ActiveRefresh, previous: ActiveRefresh): boolean {
366377
function isAbortError(error: unknown): boolean {
367378
return error instanceof Error && error.name === "AbortError"
368379
}
380+
381+
function isComponentRefreshResult<Rendered>(
382+
value: unknown,
383+
): value is ComponentRefreshResult<Rendered> {
384+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false
385+
return (
386+
typeof (value as { target?: unknown }).target === "string" && Object.hasOwn(value, "rendered")
387+
)
388+
}

0 commit comments

Comments
 (0)