capybara-simulated 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -7
- data/lib/capybara/simulated/browser.rb +537 -33
- data/lib/capybara/simulated/driver.rb +42 -13
- data/lib/capybara/simulated/js/bridge.bundle.js +6606 -3888
- data/lib/capybara/simulated/node.rb +8 -0
- data/lib/capybara/simulated/quickjs_runtime.rb +22 -8
- data/lib/capybara/simulated/runtime_shared.rb +29 -28
- data/lib/capybara/simulated/v8_runtime.rb +69 -20
- data/lib/capybara/simulated/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21b8b1545d94e237e8740cb84e4255eb7ce9b4b0743f105451176804f4fcb904
|
|
4
|
+
data.tar.gz: 5fe3aaa88743308bebfb7d6acc90366a0c2e56439afe5ae3bdfcc9eb5c107a5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f5c96e2eb7b0b73351026dc25391d17a7da6a36a00b6f4213dc9f28554164da5991b46dbcfa925a7385a5f2a6faadf01b38b16bb7ef9a6ca2569439540f5b04
|
|
7
|
+
data.tar.gz: 9e6008b539c30fe918c96f8e38f7d67fe552c1a61034890d63ebfc16accfad9748e3a09770e7d44d0d06548036cfd299ab44875e3fccbffeb491dd8a9506706e
|
data/README.md
CHANGED
|
@@ -10,11 +10,11 @@ Its correctness is held continuously to the same
|
|
|
10
10
|
[web-platform-tests](https://github.com/web-platform-tests/wpt) that
|
|
11
11
|
Chromium and Firefox use.
|
|
12
12
|
|
|
13
|
-
capybara-simulated is not a complete replacement for real-browser testing. But it sits between `rack_test` and a real browser, running the majority of system tests that don't depend on
|
|
13
|
+
capybara-simulated is not a complete replacement for real-browser testing. But it sits between `rack_test` and a real browser, running the majority of system tests that don't depend on pixel-accurate rendering.
|
|
14
14
|
|
|
15
15
|
## Is it a fit?
|
|
16
16
|
|
|
17
|
-
**A good fit when** your tests are JavaScript-driven but don't depend on
|
|
17
|
+
**A good fit when** your tests are JavaScript-driven but don't depend on pixel-accurate rendering:
|
|
18
18
|
|
|
19
19
|
- **No browser to install or boot** — no Chrome, no WebDriver, no Node toolchain; everything runs in-process. Execution is about **1.9×** faster than a headless browser on server-rendered / Hotwire apps and roughly at parity on JS-heavy SPAs (rusty_racer) — but the real win is skipping the browser's install, boot, and driver setup, not raw speed.
|
|
20
20
|
- **Deterministic** — a virtual clock and synchronous in-process execution remove the wall-clock timing, network, and rendering races that make headless-browser suites flaky.
|
|
@@ -22,9 +22,9 @@ capybara-simulated is not a complete replacement for real-browser testing. But i
|
|
|
22
22
|
- **Drop-in**: the Capybara DSL is unchanged — register `:simulated` and go. Just this gem plus one JS-engine gem.
|
|
23
23
|
- **Held to spec**: a vendored [web-platform-tests](https://github.com/web-platform-tests/wpt) conformance gate (the same DOM / HTML tests Chromium and Firefox hold themselves to), plus the full system suites of five real apps — Redmine / Forem / Avo / Mastodon / Discourse — run against the driver in [capybara-simulated-vs-world](https://github.com/ursm/capybara-simulated-vs-world).
|
|
24
24
|
|
|
25
|
-
**Reach for a real browser** (Selenium / Cuprite) **when** your tests need what this driver doesn't simulate **by design** — there's no rendering engine
|
|
25
|
+
**Reach for a real browser** (Selenium / Cuprite) **when** your tests need what this driver doesn't simulate **by design** — there's no rendering engine, so **pixel-accurate layout** (text measured by real font metrics, inline flow, flex / grid track sizing) and **screenshots** are out. There *is* a coarse box-layout engine — enough that `getBoundingClientRect()`, `elementFromPoint()`, `obscured?`, the spatial selectors, scrolling, and drag-and-drop all work against real boxes — but it answers "where is this, roughly, and what's on top", not "how would this render".
|
|
26
26
|
|
|
27
|
-
Most of the rest runs in-process — including the things that usually mean "you need a real browser": **`within_frame`**, **multiple windows / tabs**, **WebSocket + Action Cable**, **EventSource**, and **Web Workers** all work. Each has constraints (JS engine, settle-timing,
|
|
27
|
+
Most of the rest runs in-process — including the things that usually mean "you need a real browser": **`within_frame`**, **multiple windows / tabs**, **WebSocket + Action Cable**, **EventSource**, and **Web Workers** all work. Each has constraints (JS engine, settle-timing, coarse layout); see [Capabilities & limits](#capabilities--limits).
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
@@ -243,20 +243,21 @@ Most features run in-process; the notes below are mostly "works, but…", follow
|
|
|
243
243
|
|
|
244
244
|
### Works, with constraints
|
|
245
245
|
|
|
246
|
+
- **Layout-backed geometry** — a coarse box-layout engine (block flow, absolute / relative / fixed, a simple grid pass, percentage and viewport units, overflow clipping, the flat tree through shadow roots and slots, and cross-realm frames) backs the page-visible geometry, so there is *one* geometry that both the driver and the page's own JS read. That gives you `obscured?` (real occlusion, including out through nested iframes), the spatial selectors (`:above` / `:below` / `:left_of` / `:right_of` / `:near`), `scroll_to` / `scroll_by` clamped to the real scrollable range, geometry that follows `resize_to` (so a mobile-breakpoint test measures mobile boxes), and `drag_to` — which drives jQuery UI, SortableJS, Dragula and jsTree. It is laid out once per mutation generation and only when something asks for geometry. What it does *not* do: glyph metrics, inline flow, flex / grid track sizing — see [Out of scope](#out-of-scope-by-design--use-selenium--cuprite).
|
|
246
247
|
- **`within_frame` / `switch_to_frame`** (V8 engine) — each `<iframe>` runs its own scripts in its own per-frame realm; the DSL routes finds, reads, interactions, `evaluate_script`, and navigation into the active frame, nested frames included — the target frame's realm is rebuilt from the fetched document, the top page untouched. QuickJS has no nested browsing context, so `within_frame` raises there.
|
|
247
|
-
- **Multiple windows / tabs** (both engines) — each window is its own Browser + JS VM (own DOM, sessionStorage, history; cookies + localStorage shared). `open_new_window` / `within_window` / `switch_to_window` / `window_opened_by` drive them; JS `window.open` opens a real window, `window.opener` links back, and `postMessage` crosses windows. Only the active window's event loop runs, so a message is delivered when you switch to its window. `target="_blank"` opens with no opener (modern-browser default). `postMessage` carries real structured data (not a lossy JSON hop) — `Map` / `Set` / `Date` / `BigInt` / typed arrays / cyclic graphs all round-trip on V8 — and a `transfer`-list buffer moves **zero-copy** (backing store by token, source detached); only bare `undefined` collapses to `null` (Ruby has no distinct `undefined`).
|
|
248
|
+
- **Multiple windows / tabs** (both engines) — each window is its own Browser + JS VM (own DOM, sessionStorage, history; cookies + localStorage shared). `open_new_window` / `within_window` / `switch_to_window` / `window_opened_by` drive them; JS `window.open` opens a real window, `window.opener` links back, and `postMessage` crosses windows. Only the active window's event loop runs, so a message is delivered when you switch to its window. `target="_blank"` opens with no opener (modern-browser default). `postMessage` carries real structured data (not a lossy JSON hop) — `Map` / `Set` / `Date` / `BigInt` / typed arrays / cyclic graphs all round-trip on V8 — and a `transfer`-list buffer moves **zero-copy** (backing store by token, source detached); only bare `undefined` collapses to `null` (Ruby has no distinct `undefined`). `resize_to` moves the whole viewport: `innerWidth` / `innerHeight`, the `@media` cascade, `matchMedia` change + `resize` events, **and** the layout the geometry surface reports — so a mobile-breakpoint test measures mobile boxes. Each window has its own viewport. `maximize` / `fullscreen` restore the display size (a fixed 1024×768 — that part isn't configurable).
|
|
248
249
|
- **WebSocket + Action Cable** — `new WebSocket(url)` works in-process over the `rack.hijack` socket the Rack app hijacks (hand-rolled RFC6455, including subprotocol negotiation). The real `@rails/actioncable` consumer connects, subscribes, and receives broadcasts, so `turbo_stream_from` live updates work. Constraints: server pushes land at settle (not instant); the Cable app must use the **async / in-process** adapter (a real Redis adapter needs real Redis); binary frames are V8-only (QuickJS corrupts raw bytes across the host boundary — text, hence Action Cable, works on both engines). `EventSource` and Web Workers are likewise real (background reader threads draining at settle).
|
|
249
250
|
- **`fetch` / XHR** — synchronous through Rack: HTML / JSON round-trips work, but there's no streaming, no `Request#body` ReadableStream, and no concurrent requests.
|
|
250
251
|
- **`:hover` / `:focus-within`-gated content** — reachable two ways: call `element.hover` explicitly (we track the most-recently-hovered element and propagate `:hover` up its chain), or rely on the candidate-chain fallback (when the stateless cascade reports `display: none`, we re-evaluate with the candidate itself in the `:hover` set). Symmetric peers — N rows each with `tr:hover .icon` revealing `.icon`, queried as a bare `find('.icon')` — reveal all and Capybara raises `Capybara::Ambiguous`; scope the test (`find('tr', text: 'foo').hover` then `find('.icon')`), which is also more robust against real-browser flake.
|
|
251
252
|
|
|
252
253
|
### Out of scope (by design — use Selenium / Cuprite)
|
|
253
254
|
|
|
254
|
-
- **
|
|
255
|
+
- **Pixel-accurate rendering.** The layout engine (above) is coarse by design: no glyph metrics (text height is estimated from its length), no inline flow, and no flex or grid track sizing. (`resize_to` *does* move the viewport — what's fixed is the display it sits on, 1024×768.) Anything asserting *rendered appearance* — exact text wrapping, a sticky header's pixel offset, whether two boxes overlap by 3px — needs a real browser.
|
|
255
256
|
- **Screenshots.**
|
|
256
257
|
|
|
257
258
|
## Architecture
|
|
258
259
|
|
|
259
|
-
- `lib/capybara/simulated/js/src/` — the entire DOM lives here, split across ~50 ES modules bundled into `bridge.bundle.js` (esbuild; no Node toolchain at consume time). `Document` / `Element` / `Text` / `DocumentFragment` / `ShadowRoot` classes; event dispatch (capture / target / bubble with shadow retargeting, via `dispatchEvent(target, event)`); a virtual `setTimeout` / `setInterval` / `requestAnimationFrame` clock; MutationObserver; custom-element registry; `Range` / `Selection`;
|
|
260
|
+
- `lib/capybara/simulated/js/src/` — the entire DOM lives here, split across ~50 ES modules bundled into `bridge.bundle.js` (esbuild; no Node toolchain at consume time). `Document` / `Element` / `Text` / `DocumentFragment` / `ShadowRoot` classes; event dispatch (capture / target / bubble with shadow retargeting, via `dispatchEvent(target, event)`); a virtual `setTimeout` / `setInterval` / `requestAnimationFrame` clock; MutationObserver; custom-element registry; `Range` / `Selection`; the cascade resolver for `display` / `visibility` / `text-transform`; and `layout.js`, the coarse box-layout engine the geometry surface reads from. Capybara's finds run through the vendored css-select (with css-what / css-tree) for CSS and xpathway for XPath — both true third parties under `vendor/js/`, executing in the same context as the page's JS.
|
|
260
261
|
- `lib/capybara/simulated/browser.rb` — Rack client, history stack, modal handler queue, virtual-clock anchor, trace recorder. Owns the JS runtime via `V8Runtime` or `QuickJSRuntime`. The hot operations (`find_css` / `find_xpath` / DOM ops / event dispatch) are single-`Context#call` round-trips returning handle id arrays; per-result iteration stays Ruby-side.
|
|
261
262
|
- `lib/capybara/simulated/v8_runtime.rb` / `quickjs_runtime.rb` — per-engine wrappers, common bits in `runtime_shared.rb`. The V8 base-snapshot (and the QuickJS bytecode equivalent) bakes in the bundled bridge + vendored deps, so a per-navigation context reset (V8) or pooled VM checkout (QuickJS) is sub-millisecond.
|
|
262
263
|
- `lib/capybara/simulated/driver.rb` — Capybara `Driver::Base` surface (visit / find / execute_script / window handling / modal / tracing API).
|