Skip to content

Commit 8e818f5

Browse files
ursmclaude
andcommitted
docs: refresh README for release
Bring the README in line with the current architecture ahead of the first release: - Status: drop "pre-release / unstable / unpublished"; describe the two correctness bars (vendored web-platform-tests conformance gate + the five app system suites as the integration check). - Performance + Architecture: the V8 context pool is gone — V8 now warms one long-lived isolate and resets its context per navigation (`Context#reset`); QuickJS keeps the pre-warmed VM pool. The DOM is no longer a monolithic `bridge.js` but ~50 ES modules bundled into `bridge.bundle.js`. Capybara's CSS finds run through the vendored css-select (the hand-rolled tokeniser is gone); XPath through xpathway. - Drop stale internal names (`__layoutRules`/`__hideRules`) for the current content-addressed cascade cache; reword the virtual-clock note. - Known limits: `within_frame` (not "frames") is what's unsupported — an iframe's own scripts run in a per-frame realm. - Add the Ruby ≥ 3.3 requirement and a License section (MIT). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d1881d8 commit 8e818f5

1 file changed

Lines changed: 55 additions & 40 deletions

File tree

README.md

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,25 @@ The DOM lives entirely inside the JS engine — V8 via
1010
[rusty_racer](https://github.com/ursm/rusty_racer) or QuickJS via
1111
[quickjs.rb](https://github.com/hmsk/quickjs.rb), whichever is
1212
installed — with no Nokogiri tree on the Ruby side. Capybara finds
13-
resolve through xpathway / CSS-selector code running in the same
14-
context as the page's JS, so `find` / `has_css?` / `within` see
13+
resolve through css-select (CSS) and xpathway (XPath) running in the
14+
same context as the page's JS, so `find` / `has_css?` / `within` see
1515
exactly the tree the app sees.
1616

1717
## Status
1818

19-
Pre-release — the API is unstable and the gem isn't published to
20-
RubyGems yet. The numbers and benchmarks live in
19+
The architecture and behaviour are stable. Correctness is held to two
20+
bars. A vendored subset of
21+
[web-platform-tests](https://github.com/web-platform-tests/wpt) — the
22+
same DOM / HTML tests Chromium and Firefox hold themselves to — runs as
23+
a conformance gate. And each target app (Redmine / Forem / Avo /
24+
Mastodon / Discourse) runs its full system suite against the driver in
2125
[capybara-simulated-vs-world](https://github.com/ursm/capybara-simulated-vs-world)
22-
where each target app (Redmine / Forem / Avo / Mastodon / Discourse)
23-
runs its system suite against this driver.
26+
as an integration check.
2427

25-
The pending shared-spec tests all need a real layout engine
26-
(`elementFromPoint`, real `getBoundingClientRect`, viewport-clip
27-
visibility, `display: contents` table edge cases) — same set Selenium
28-
escapes via screenshots and we don't try to simulate.
28+
The remaining gaps need a real layout engine (`elementFromPoint`,
29+
truthy `getBoundingClientRect`, viewport-clip visibility, `display:
30+
contents` table edge cases) — the same set Selenium escapes via
31+
screenshots and this driver deliberately doesn't simulate.
2932

3033
## Install
3134

@@ -34,10 +37,9 @@ gem 'capybara-simulated', group: :test
3437
gem 'rusty_racer', group: :test # JS engine — pick one
3538
```
3639

37-
`bundle install`. The gem ships its JS bridge under
38-
`lib/capybara/simulated/js/`, with the vendored JS deps under
39-
`vendor/js/`, so
40-
there's no Node toolchain at consume time.
40+
`bundle install`. Requires Ruby ≥ 3.3. The gem ships its JS bridge
41+
under `lib/capybara/simulated/js/` and the vendored JS deps under
42+
`vendor/js/`, so there's no Node toolchain at consume time.
4143

4244
### JS engine
4345

@@ -227,11 +229,13 @@ end
227229

228230
## Performance characteristics
229231

230-
The driver builds a base snapshot once per process (bridge.js +
231-
the vendored JS deps — a V8 `Snapshot` for rusty_racer, bytecode for
232-
QuickJS) and
233-
checks Contexts out of a small process-wide pool of pre-warmed
234-
clones, so each navigation lands on a fresh JS context instantly.
232+
The driver builds a base snapshot once per process — the bundled
233+
bridge plus the vendored JS deps, as a V8 `Snapshot` for rusty_racer or
234+
bytecode for QuickJS. On V8 that snapshot warms a single long-lived
235+
isolate whose context is reset to a clean realm per navigation
236+
(`Context#reset`); on QuickJS each navigation checks a freshly
237+
snapshot-loaded VM out of a small pre-warmed pool. Either way, every
238+
navigation lands on a clean, warm JS context near-instantly.
235239

236240
**Wall time is sensitive to whether the app uses Turbo Drive**,
237241
because navigation simulates real-browser semantics:
@@ -263,18 +267,20 @@ referenced page-specific DOM.
263267
Each external script is fetched through the in-process Rack app,
264268
compiled, and run in the JS engine with bytecode cache hits from
265269
the base snapshot warmup.
266-
- **CSS cascade resolution**: rules are parsed once on first encounter
267-
per stylesheet set; subsequent finds on the same page hit the
268-
cached `__layoutRules` / `__hideRules` arrays in JS-side memory.
270+
- **CSS cascade resolution**: stylesheets are parsed once per distinct
271+
set of sources and cached content-addressably, so repeat visits and
272+
subsequent finds on the same page reuse the resolved cascade instead
273+
of re-parsing.
269274
- **DOM ops stay inside the JS engine** — find / has_? / event
270275
dispatch never cross the Ruby ↔ JS boundary for the actual tree
271276
walk; only the resulting handle ids do. Modify-heavy tests
272277
(SortableJS dragging thousands of items) run at JS-engine speed,
273278
not at host-call-IPC speed.
274279
- **Polling** (Capybara `default_max_wait_time`) advances a *virtual*
275-
JS clock — `setTimeout(N)` fires after `N` ms of accumulated wall
276-
time, not real time. A page that schedules `setTimeout(2000, x)`
277-
doesn't block for 2 s; it fires once polling has waited that long.
280+
JS clock — timers fire as polling steps the clock forward, not in
281+
real time. A page that schedules `setTimeout(2000, x)` doesn't block
282+
for 2 s; the callback fires once polling has advanced the clock past
283+
it.
278284

279285
## Known limits
280286

@@ -303,21 +309,26 @@ referenced page-specific DOM.
303309
open a window-handle and `current_window` / `switch_to_window`
304310
work, but each aux window only records its URL (no per-window JS
305311
context or cross-window `postMessage`).
306-
- **Frames, WebSocket, screenshots, and drag pixel coordinates** are
307-
out of scope — use Selenium / Cuprite. (EventSource and Web Workers
308-
*are* implemented.)
312+
- **`within_frame`, WebSocket, screenshots, and drag pixel
313+
coordinates** are out of scope — use Selenium / Cuprite. There's no
314+
frame-switching DSL to drive a test into an `<iframe>`, though an
315+
iframe's own scripts do run, in a per-frame JS realm. (EventSource
316+
and Web Workers *are* implemented.)
309317

310318
## Architecture
311319

312-
- `lib/capybara/simulated/js/bridge.js` — the entire DOM lives here.
313-
`Document` / `Element` / `Text` / `DocumentFragment` / `ShadowRoot`
314-
classes; CSS selector tokeniser + matcher; event dispatch
315-
(capture / target / bubble phases with `dispatchEvent(target,
316-
event)`); virtual `setTimeout` / `setInterval` /
317-
`requestAnimationFrame` clock; MutationObserver; custom-element
318-
registry; `Range` / `Selection`; cascade resolver for `display` /
319-
`visibility` / `text-transform` / layout primitives. xpathway (true
320-
third-party, under `vendor/js/`) sits on top for XPath.
320+
- `lib/capybara/simulated/js/src/` — the entire DOM lives here, split
321+
across ~50 ES modules bundled into `bridge.bundle.js` (esbuild; no
322+
Node toolchain at consume time). `Document` / `Element` / `Text` /
323+
`DocumentFragment` / `ShadowRoot` classes; event dispatch
324+
(capture / target / bubble with shadow retargeting, via
325+
`dispatchEvent(target, event)`); a virtual `setTimeout` /
326+
`setInterval` / `requestAnimationFrame` clock; MutationObserver;
327+
custom-element registry; `Range` / `Selection`; and the cascade
328+
resolver for `display` / `visibility` / `text-transform`. Capybara's
329+
finds run through the vendored css-select (with css-what / css-tree)
330+
for CSS and xpathway for XPath — both true third parties under
331+
`vendor/js/`, executing in the same context as the page's JS.
321332
- `lib/capybara/simulated/browser.rb` — Rack client, history stack,
322333
modal handler queue, virtual-clock anchor, trace recorder. Owns
323334
the JS runtime via `V8Runtime` or `QuickJSRuntime`. The hot
@@ -326,9 +337,9 @@ referenced page-specific DOM.
326337
per-result iteration stays Ruby-side.
327338
- `lib/capybara/simulated/v8_runtime.rb` / `quickjs_runtime.rb`
328339
per-engine wrappers, common bits in `runtime_shared.rb`. The V8
329-
base-snapshot (and the QuickJS bytecode equivalent) caches
330-
bridge.js + the vendored deps so each Context spawn is
331-
sub-millisecond.
340+
base-snapshot (and the QuickJS bytecode equivalent) bakes in the
341+
bundled bridge + vendored deps, so a per-navigation context reset
342+
(V8) or pooled VM checkout (QuickJS) is sub-millisecond.
332343
- `lib/capybara/simulated/driver.rb` — Capybara `Driver::Base`
333344
surface (visit / find / execute_script / window handling / modal /
334345
tracing API).
@@ -368,3 +379,7 @@ pin '@hotwired/turbo'
368379

369380
`window.fetch` routes through Rack, so Turbo's frame fetch and
370381
link-action POSTs round-trip the test app.
382+
383+
## License
384+
385+
[MIT](LICENSE).

0 commit comments

Comments
 (0)