capybara-simulated 0.5.0 → 0.6.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 +58 -218
- data/lib/capybara/simulated/browser.rb +966 -148
- data/lib/capybara/simulated/driver.rb +220 -17
- data/lib/capybara/simulated/js/bridge.bundle.js +5384 -921
- data/lib/capybara/simulated/quickjs_runtime.rb +17 -6
- data/lib/capybara/simulated/runtime_shared.rb +32 -5
- data/lib/capybara/simulated/v8_runtime.rb +157 -32
- data/lib/capybara/simulated/version.rb +1 -1
- data/vendor/js/vendor.bundle.js +12 -9
- 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: 7b82726ef99cc26d3aaba51a010103e2712615464a13d5513400877cbbbe686e
|
|
4
|
+
data.tar.gz: ce5d823da6b55a169d76db983a18919893172fb7b88e1c96b71d2f9b6ee85bc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbdaecdbc0b555e43842c7569f8b3b144efd1c0fe1e2bd0f5f48c577056cd1d356154c4d15a8910b36b5a88f9d069f1dfc7bf98961fbf2431b81e6b64350b3c0
|
|
7
|
+
data.tar.gz: 8ea39123d1b9c5d13b5e8289e06c21d369a6d886a0e87ba9da07fa6071f71db10aa2a208dfde6ec713a34e7df4b76d3cbe0d5c15fd3cc05eb3ee9503499072d6
|
data/README.md
CHANGED
|
@@ -1,65 +1,30 @@
|
|
|
1
1
|
# capybara-simulated
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
[
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
exactly the tree the app sees.
|
|
3
|
+
*A simulated browser environment with DOM, JavaScript, and CSS cascade—without a rendering engine.*
|
|
4
|
+
|
|
5
|
+
capybara-simulated is an in-process Capybara driver. You shouldn't have to drop a system test down to a lower layer just because a real browser is expensive — a test written from the user's point of view should describe what the user actually does, not the HTTP requests your test code assembles.
|
|
6
|
+
|
|
7
|
+
capybara-simulated lets you keep those user-facing system tests without paying the cost of a real browser: nothing to download or boot, no WebDriver to set up. Everything runs in-process, and the JavaScript your app actually loads — Turbo, Stimulus, React, … — runs as-is, so you're verifying real behavior rather than a mock.
|
|
8
|
+
|
|
9
|
+
Its correctness is held continuously to the same
|
|
10
|
+
[web-platform-tests](https://github.com/web-platform-tests/wpt) that
|
|
11
|
+
Chromium and Firefox use.
|
|
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 visual layout.
|
|
15
14
|
|
|
16
15
|
## Is it a fit?
|
|
17
16
|
|
|
18
|
-
**A good fit when** your tests are JavaScript-driven but don't depend on
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- **
|
|
29
|
-
MutationObserver, custom elements, `<template>`, Shadow DOM, ES modules
|
|
30
|
-
+ importmap, **Hotwire (Stimulus + Turbo)**, Trix.
|
|
31
|
-
- **Drop-in**: the Capybara DSL is unchanged — register `:simulated` and
|
|
32
|
-
go. Just this gem plus one JS-engine gem.
|
|
33
|
-
- **Held to spec**: a vendored
|
|
34
|
-
[web-platform-tests](https://github.com/web-platform-tests/wpt)
|
|
35
|
-
conformance gate plus five real app suites (see [Status](#status)).
|
|
36
|
-
|
|
37
|
-
**Reach for a real browser** (Selenium / Cuprite) **when** your tests need
|
|
38
|
-
what this driver doesn't simulate **by design** — there's no rendering
|
|
39
|
-
engine: **pixel layout** (`getBoundingClientRect()` returns zeros,
|
|
40
|
-
`elementFromPoint()` isn't implemented, so visual hit-testing, coordinate
|
|
41
|
-
drag-and-drop, and sticky-scroll math don't work) and **screenshots**.
|
|
42
|
-
|
|
43
|
-
Most of the rest runs in-process — including the things that usually mean
|
|
44
|
-
"you need a real browser": **`within_frame`**, **multiple windows / tabs**,
|
|
45
|
-
**WebSocket + Action Cable**, **EventSource**, and **Web Workers** all work.
|
|
46
|
-
Each has constraints (JS engine, settle-timing, no layout); see
|
|
47
|
-
[Capabilities & limits](#capabilities--limits).
|
|
48
|
-
|
|
49
|
-
## Status
|
|
50
|
-
|
|
51
|
-
The architecture and behaviour are stable. Correctness is held to two
|
|
52
|
-
bars. A vendored subset of
|
|
53
|
-
[web-platform-tests](https://github.com/web-platform-tests/wpt) — the
|
|
54
|
-
same DOM / HTML tests Chromium and Firefox hold themselves to — runs as
|
|
55
|
-
a conformance gate. And each target app (Redmine / Forem / Avo /
|
|
56
|
-
Mastodon / Discourse) runs its full system suite against the driver in
|
|
57
|
-
[capybara-simulated-vs-world](https://github.com/ursm/capybara-simulated-vs-world)
|
|
58
|
-
as an integration check.
|
|
59
|
-
|
|
60
|
-
The remaining gaps are the layout / pixel-geometry features the driver
|
|
61
|
-
deliberately doesn't simulate — the same set Selenium escapes via
|
|
62
|
-
screenshots (see [Capabilities & limits](#capabilities--limits)).
|
|
17
|
+
**A good fit when** your tests are JavaScript-driven but don't depend on visual layout:
|
|
18
|
+
|
|
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
|
+
- **Deterministic** — a virtual clock and synchronous in-process execution remove the wall-clock timing, network, and rendering races that make headless-browser suites flaky.
|
|
21
|
+
- **Real front-end JS runs**: inline `<script>` + event handlers, MutationObserver, custom elements, `<template>`, Shadow DOM, ES modules importmap, **Hotwire (Stimulus + Turbo)**, Trix.
|
|
22
|
+
- **Drop-in**: the Capybara DSL is unchanged — register `:simulated` and go. Just this gem plus one JS-engine gem.
|
|
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
|
+
|
|
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: **pixel layout** (`getBoundingClientRect()` returns zeros, `elementFromPoint()` isn't implemented, so visual hit-testing, coordinate drag-and-drop, and sticky-scroll math don't work) and **screenshots**.
|
|
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, no layout); see [Capabilities & limits](#capabilities--limits).
|
|
63
28
|
|
|
64
29
|
## Install
|
|
65
30
|
|
|
@@ -68,29 +33,22 @@ gem 'capybara-simulated', group: :test
|
|
|
68
33
|
gem 'rusty_racer', group: :test # JS engine — pick one
|
|
69
34
|
```
|
|
70
35
|
|
|
71
|
-
`bundle install`. Requires Ruby ≥ 3.3. The gem ships its JS bridge
|
|
72
|
-
under `lib/capybara/simulated/js/` and the vendored JS deps under
|
|
73
|
-
`vendor/js/`, so there's no Node toolchain at consume time.
|
|
36
|
+
`bundle install`. Requires Ruby ≥ 3.3. The gem ships its JS bridge under `lib/capybara/simulated/js/` and the vendored JS deps under `vendor/js/`, so there's no Node toolchain at consume time.
|
|
74
37
|
|
|
75
38
|
### JS engine
|
|
76
39
|
|
|
77
40
|
The gem treats the JS engine as a soft dependency. Pick one of:
|
|
78
41
|
|
|
79
42
|
```ruby
|
|
80
|
-
gem 'rusty_racer'
|
|
81
|
-
gem 'quickjs', '>= 0.18'
|
|
82
|
-
|
|
83
|
-
|
|
43
|
+
gem 'rusty_racer', '>= 0.1.9' # V8 (JIT, fastest per spec) — default
|
|
44
|
+
gem 'quickjs', '>= 0.18' # QuickJS (interpreter, smaller per-VM RAM —
|
|
45
|
+
# wins when scaling parallel workers under
|
|
46
|
+
# a fixed memory budget)
|
|
84
47
|
```
|
|
85
48
|
|
|
86
|
-
The V8 engine comes from [rusty_racer](https://github.com/ursm/rusty_racer),
|
|
87
|
-
a rusty_v8-based Ruby binding with the native ES Module API,
|
|
88
|
-
`ScriptCompiler::CachedData` snapshots, and per-frame realm contexts the
|
|
89
|
-
driver builds on.
|
|
49
|
+
The V8 engine comes from [rusty_racer](https://github.com/ursm/rusty_racer), a rusty_v8-based Ruby binding with the native ES Module API, `ScriptCompiler::CachedData` snapshots, and per-frame realm contexts the driver builds on.
|
|
90
50
|
|
|
91
|
-
The engine is auto-detected at boot; if both gems are present V8 wins.
|
|
92
|
-
Override explicitly with `CSIM_JS_ENGINE=v8|quickjs`
|
|
93
|
-
or `Capybara::Simulated::Driver.new(app, js_engine: :quickjs)`.
|
|
51
|
+
The engine is auto-detected at boot; if both gems are present V8 wins. Override explicitly with `CSIM_JS_ENGINE=v8|quickjs` or `Capybara::Simulated::Driver.new(app, js_engine: :quickjs)`.
|
|
94
52
|
|
|
95
53
|
## Use
|
|
96
54
|
|
|
@@ -108,8 +66,7 @@ Capybara.javascript_driver = :simulated
|
|
|
108
66
|
# Capybara.default_driver = :simulated
|
|
109
67
|
```
|
|
110
68
|
|
|
111
|
-
Tests tagged `js: true` (or `type: :system, js: true` in Rails) run
|
|
112
|
-
in the driver:
|
|
69
|
+
Tests tagged `js: true` (or `type: :system, js: true` in Rails) run in the driver:
|
|
113
70
|
|
|
114
71
|
```ruby
|
|
115
72
|
RSpec.describe 'sign-in', type: :system, js: true do
|
|
@@ -134,8 +91,7 @@ end
|
|
|
134
91
|
|
|
135
92
|
### Minitest
|
|
136
93
|
|
|
137
|
-
`Capybara.javascript_driver` is RSpec-only — `ActionDispatch::SystemTestCase`
|
|
138
|
-
ignores it. Set the driver explicitly:
|
|
94
|
+
`Capybara.javascript_driver` is RSpec-only — `ActionDispatch::SystemTestCase` ignores it. Set the driver explicitly:
|
|
139
95
|
|
|
140
96
|
```ruby
|
|
141
97
|
# test/application_system_test_case.rb
|
|
@@ -165,16 +121,9 @@ puts page.text
|
|
|
165
121
|
|
|
166
122
|
## Trace
|
|
167
123
|
|
|
168
|
-
Each Capybara action (`visit`, `click`, `set`, …) is recorded as a step
|
|
169
|
-
in a per-test trace: URL before / after, console output and network
|
|
170
|
-
requests during the step, plus elapsed and per-step durations. On
|
|
171
|
-
action failure (and only then, by default) the post-action DOM is
|
|
172
|
-
captured too.
|
|
124
|
+
Each Capybara action (`visit`, `click`, `set`, …) is recorded as a step in a per-test trace: URL before / after, console output and network requests during the step, plus elapsed and per-step durations. On action failure (and only then, by default) the post-action DOM is captured too.
|
|
173
125
|
|
|
174
|
-
Recording is **on by default** — fully in-memory, no files written
|
|
175
|
-
unless you opt in via `CSIM_TRACE_DIR`. Wall-time overhead is
|
|
176
|
-
run-to-run-variance equivalent because the expensive part — DOM
|
|
177
|
-
serialization — only fires on action error.
|
|
126
|
+
Recording is **on by default** — fully in-memory, no files written unless you opt in via `CSIM_TRACE_DIR`. Wall-time overhead is run-to-run-variance equivalent because the expensive part — DOM serialization — only fires on action error.
|
|
178
127
|
|
|
179
128
|
### Modes (`CSIM_TRACE=…`)
|
|
180
129
|
|
|
@@ -209,48 +158,30 @@ require 'capybara/simulated/rspec' # RSpec
|
|
|
209
158
|
require 'capybara/simulated/minitest' # Minitest / Rails system tests
|
|
210
159
|
```
|
|
211
160
|
|
|
212
|
-
With `CSIM_TRACE_DIR=/path/to/dir` set, each example that recorded a
|
|
213
|
-
trace is written to `<dir>/<slug>.json` after it runs; both integrations
|
|
214
|
-
are inert when the env var is unset.
|
|
161
|
+
With `CSIM_TRACE_DIR=/path/to/dir` set, each example that recorded a trace is written to `<dir>/<slug>.json` after it runs; both integrations are inert when the env var is unset.
|
|
215
162
|
|
|
216
163
|
```sh
|
|
217
164
|
CSIM_TRACE_DIR=tmp/csim-traces bundle exec rspec spec/system
|
|
218
165
|
```
|
|
219
166
|
|
|
220
|
-
The metadata block on each trace includes `title`, `file`, `outcome`
|
|
221
|
-
(`passed` / `failed`), and the exception message — enough to index a
|
|
222
|
-
CI artifact directory by failure.
|
|
167
|
+
The metadata block on each trace includes `title`, `file`, `outcome` (`passed` / `failed`), and the exception message — enough to index a CI artifact directory by failure.
|
|
223
168
|
|
|
224
169
|
### Viewing traces
|
|
225
170
|
|
|
226
|
-
The recorded JSON stays plain data; to look at one, render it into a
|
|
227
|
-
self-contained HTML viewer with the bundled CLI:
|
|
171
|
+
The recorded JSON stays plain data; to look at one, render it into a self-contained HTML viewer with the bundled CLI:
|
|
228
172
|
|
|
229
173
|
```sh
|
|
230
174
|
capybara-simulated trace tmp/csim-traces/checkout_flow.json
|
|
231
175
|
# wrote /tmp/checkout_flow.html (then opens it in your browser)
|
|
232
176
|
```
|
|
233
177
|
|
|
234
|
-
By default the HTML is written to a temp file and opened in your
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
timeline of actions, and per step the URL before/after, console output,
|
|
238
|
-
network requests, the error, and a rendered preview of the post-action
|
|
239
|
-
DOM snapshot. Its **Load JSON…** button / drag-and-drop swaps in any
|
|
240
|
-
other trace file.
|
|
241
|
-
|
|
242
|
-
`-o PATH` writes the HTML somewhere specific (`-o -` to stdout);
|
|
243
|
-
`--no-open` skips launching the browser. Browser launching uses
|
|
244
|
-
[launchy](https://rubygems.org/gems/launchy) when it's installed
|
|
245
|
-
(`gem 'launchy'`, recommended for reliable cross-platform / WSL opening)
|
|
246
|
-
and falls back to the platform opener (`xdg-open` / `open` / `start`)
|
|
247
|
-
otherwise.
|
|
178
|
+
By default the HTML is written to a temp file and opened in your browser. The viewer works straight from `file://` — the trace JSON is embedded inline, so no server is needed — and shows a step-by-step UI: a timeline of actions, and per step the URL before/after, console output, network requests, the error, and a rendered preview of the post-action DOM snapshot. Its **Load JSON…** button / drag-and-drop swaps in any other trace file.
|
|
179
|
+
|
|
180
|
+
`-o PATH` writes the HTML somewhere specific (`-o -` to stdout); `--no-open` skips launching the browser. Browser launching uses [launchy](https://rubygems.org/gems/launchy) when it's installed (`gem 'launchy'`, recommended for reliable cross-platform / WSL opening) and falls back to the platform opener (`xdg-open` / `open` / `start`) otherwise.
|
|
248
181
|
|
|
249
182
|
### Programmatic
|
|
250
183
|
|
|
251
|
-
For finer control, call `driver.start_tracing(...)` /
|
|
252
|
-
`driver.stop_tracing(path: ...)`. The shape mirrors
|
|
253
|
-
`capybara-playwright-driver`:
|
|
184
|
+
For finer control, call `driver.start_tracing(...)` / `driver.stop_tracing(path: ...)`. The shape mirrors `capybara-playwright-driver`:
|
|
254
185
|
|
|
255
186
|
```ruby
|
|
256
187
|
RSpec.describe 'flaky payment flow', type: :system, js: true do
|
|
@@ -292,134 +223,43 @@ end
|
|
|
292
223
|
|
|
293
224
|
## Performance characteristics
|
|
294
225
|
|
|
295
|
-
The driver builds a base snapshot once per process — the bundled
|
|
296
|
-
bridge plus the vendored JS deps, as a V8 `Snapshot` for rusty_racer or
|
|
297
|
-
bytecode for QuickJS. On V8 that snapshot warms a single long-lived
|
|
298
|
-
isolate whose context is reset to a clean realm per navigation
|
|
299
|
-
(`Context#reset`); on QuickJS each navigation checks a freshly
|
|
300
|
-
snapshot-loaded VM out of a small pre-warmed pool. Either way, every
|
|
301
|
-
navigation lands on a clean, warm JS context near-instantly.
|
|
226
|
+
The driver builds a base snapshot once per process — the bundled bridge plus the vendored JS deps, as a V8 `Snapshot` for rusty_racer or bytecode for QuickJS. On V8 that snapshot warms a single long-lived isolate whose context is reset to a clean realm per navigation (`Context#reset`); on QuickJS each navigation checks a freshly snapshot-loaded VM out of a small pre-warmed pool. Either way, every navigation lands on a clean, warm JS context near-instantly.
|
|
302
227
|
|
|
303
228
|
### Library snapshot policy
|
|
304
229
|
|
|
305
|
-
Per visit, `<script src>`-referenced libraries (jQuery, Stimulus,
|
|
306
|
-
…) re-evaluate fresh against the new page. They are **not** baked
|
|
307
|
-
into a per-app snapshot — preserving library state across page
|
|
308
|
-
navigations is what real browsers don't do, and trying to do it
|
|
309
|
-
broke `$.ready` Callbacks queues whose user-app callbacks
|
|
310
|
-
referenced page-specific DOM.
|
|
230
|
+
Per visit, `<script src>`-referenced libraries (jQuery, Stimulus, …) re-evaluate fresh against the new page. They are **not** baked into a per-app snapshot — preserving library state across page navigations is what real browsers don't do, and trying to do it broke `$.ready` Callbacks queues whose user-app callbacks referenced page-specific DOM.
|
|
311
231
|
|
|
312
232
|
### Other factors
|
|
313
233
|
|
|
314
|
-
- **`<script src>` parsing** dominates `visit` on JS-heavy pages.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
- **CSS cascade resolution**: stylesheets are parsed once per distinct
|
|
319
|
-
set of sources and cached content-addressably, so repeat visits and
|
|
320
|
-
subsequent finds on the same page reuse the resolved cascade instead
|
|
321
|
-
of re-parsing.
|
|
322
|
-
- **DOM ops stay inside the JS engine** — find / has_? / event
|
|
323
|
-
dispatch never cross the Ruby ↔ JS boundary for the actual tree
|
|
324
|
-
walk; only the resulting handle ids do. Modify-heavy tests
|
|
325
|
-
(SortableJS dragging thousands of items) run at JS-engine speed,
|
|
326
|
-
not at host-call-IPC speed.
|
|
327
|
-
- **Polling** (Capybara `default_max_wait_time`) advances a *virtual*
|
|
328
|
-
JS clock — timers fire as polling steps the clock forward, not in
|
|
329
|
-
real time. A page that schedules `setTimeout(2000, x)` doesn't block
|
|
330
|
-
for 2 s; the callback fires once polling has advanced the clock past
|
|
331
|
-
it.
|
|
234
|
+
- **`<script src>` parsing** dominates `visit` on JS-heavy pages. Each external script is fetched through the in-process Rack app, compiled, and run in the JS engine with bytecode cache hits from the base snapshot warmup.
|
|
235
|
+
- **CSS cascade resolution**: stylesheets are parsed once per distinct set of sources and cached content-addressably, so repeat visits and subsequent finds on the same page reuse the resolved cascade instead of re-parsing.
|
|
236
|
+
- **DOM ops stay inside the JS engine** — find / has_? / event dispatch never cross the Ruby ↔ JS boundary for the actual tree walk; only the resulting handle ids do. Modify-heavy tests (SortableJS dragging thousands of items) run at JS-engine speed, not at host-call-IPC speed.
|
|
237
|
+
- **Polling** (Capybara `default_max_wait_time`) advances a *virtual* JS clock — timers fire as polling steps the clock forward, not in real time. A page that schedules `setTimeout(2000, x)` doesn't block for 2 s; the callback fires once polling has advanced the clock past it.
|
|
332
238
|
|
|
333
239
|
## Capabilities & limits
|
|
334
240
|
|
|
335
|
-
Most features run in-process; the notes below are mostly "works, but…",
|
|
336
|
-
followed by the short list of things that need a real browser **by design**.
|
|
241
|
+
Most features run in-process; the notes below are mostly "works, but…", followed by the short list of things that need a real browser **by design**.
|
|
337
242
|
|
|
338
243
|
### Works, with constraints
|
|
339
244
|
|
|
340
|
-
- **`within_frame` / `switch_to_frame`** (V8 engine) — each `<iframe>` runs
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
context, so `within_frame` raises there.
|
|
346
|
-
- **Multiple windows / tabs** (both engines) — each window is its own
|
|
347
|
-
Browser + JS VM (own DOM, sessionStorage, history; cookies + localStorage
|
|
348
|
-
shared). `open_new_window` / `within_window` / `switch_to_window` /
|
|
349
|
-
`window_opened_by` drive them; JS `window.open` opens a real window,
|
|
350
|
-
`window.opener` links back, and `postMessage` crosses windows. Only the
|
|
351
|
-
active window's event loop runs, so a message is delivered when you switch
|
|
352
|
-
to its window. `target="_blank"` opens with no opener (modern-browser
|
|
353
|
-
default). `postMessage` carries real structured data (not a lossy JSON
|
|
354
|
-
hop) — `Map` / `Set` / `Date` / `BigInt` / typed arrays / cyclic graphs all
|
|
355
|
-
round-trip on V8 — and a `transfer`-list buffer moves **zero-copy** (backing
|
|
356
|
-
store by token, source detached); only bare `undefined` collapses to `null`
|
|
357
|
-
(Ruby has no distinct `undefined`). Window viewport APIs (`maximize` /
|
|
358
|
-
`fullscreen` / pixel-exact `resize_to`) are no-ops (no layout engine).
|
|
359
|
-
- **WebSocket + Action Cable** — `new WebSocket(url)` works in-process over
|
|
360
|
-
the `rack.hijack` socket the Rack app hijacks (hand-rolled RFC6455, including
|
|
361
|
-
subprotocol negotiation). The real `@rails/actioncable` consumer connects,
|
|
362
|
-
subscribes, and receives broadcasts, so `turbo_stream_from` live updates
|
|
363
|
-
work. Constraints: server
|
|
364
|
-
pushes land at settle (not instant); the Cable app must use the **async /
|
|
365
|
-
in-process** adapter (a real Redis adapter needs real Redis); binary frames
|
|
366
|
-
are V8-only (QuickJS corrupts raw bytes across the host boundary — text,
|
|
367
|
-
hence Action Cable, works on both engines). `EventSource` and Web Workers
|
|
368
|
-
are likewise real (background reader threads draining at settle).
|
|
369
|
-
- **`fetch` / XHR** — synchronous through Rack: HTML / JSON round-trips work,
|
|
370
|
-
but there's no streaming, no `Request#body` ReadableStream, and no
|
|
371
|
-
concurrent requests.
|
|
372
|
-
- **`:hover` / `:focus-within`-gated content** — reachable two ways: call
|
|
373
|
-
`element.hover` explicitly (we track the most-recently-hovered element and
|
|
374
|
-
propagate `:hover` up its chain), or rely on the candidate-chain fallback
|
|
375
|
-
(when the stateless cascade reports `display: none`, we re-evaluate with the
|
|
376
|
-
candidate itself in the `:hover` set). Symmetric peers — N rows each with
|
|
377
|
-
`tr:hover .icon` revealing `.icon`, queried as a bare `find('.icon')` —
|
|
378
|
-
reveal all and Capybara raises `Capybara::Ambiguous`; scope the test
|
|
379
|
-
(`find('tr', text: 'foo').hover` then `find('.icon')`), which is also more
|
|
380
|
-
robust against real-browser flake.
|
|
245
|
+
- **`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.
|
|
246
|
+
- **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`). Window viewport APIs (`maximize` / `fullscreen` / pixel-exact `resize_to`) are no-ops (no layout engine).
|
|
247
|
+
- **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).
|
|
248
|
+
- **`fetch` / XHR** — synchronous through Rack: HTML / JSON round-trips work, but there's no streaming, no `Request#body` ReadableStream, and no concurrent requests.
|
|
249
|
+
- **`: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.
|
|
381
250
|
|
|
382
251
|
### Out of scope (by design — use Selenium / Cuprite)
|
|
383
252
|
|
|
384
|
-
- **Layout / pixel geometry.** `visible?` and `Node#style` consult the CSS
|
|
385
|
-
cascade and the inline `style` attribute, but `getBoundingClientRect()`
|
|
386
|
-
returns zeros and `elementFromPoint()` isn't implemented. Click offsets work
|
|
387
|
-
for fixture-style absolute / relative positioning (ancestor-summed
|
|
388
|
-
`top`/`left`); position-via-layout (Dragula drops, sticky-header scroll math,
|
|
389
|
-
viewport-clip visibility) needs a real browser.
|
|
253
|
+
- **Layout / pixel geometry.** `visible?` and `Node#style` consult the CSS cascade and the inline `style` attribute, but `getBoundingClientRect()` returns zeros and `elementFromPoint()` isn't implemented. Click offsets work for fixture-style absolute / relative positioning (ancestor-summed `top`/`left`); position-via-layout (Dragula drops, sticky-header scroll math, viewport-clip visibility) needs a real browser.
|
|
390
254
|
- **Screenshots.**
|
|
391
255
|
|
|
392
256
|
## Architecture
|
|
393
257
|
|
|
394
|
-
- `lib/capybara/simulated/js/src/` — the entire DOM lives here, split
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
`dispatchEvent(target, event)`); a virtual `setTimeout` /
|
|
400
|
-
`setInterval` / `requestAnimationFrame` clock; MutationObserver;
|
|
401
|
-
custom-element registry; `Range` / `Selection`; and the cascade
|
|
402
|
-
resolver for `display` / `visibility` / `text-transform`. Capybara's
|
|
403
|
-
finds run through the vendored css-select (with css-what / css-tree)
|
|
404
|
-
for CSS and xpathway for XPath — both true third parties under
|
|
405
|
-
`vendor/js/`, executing in the same context as the page's JS.
|
|
406
|
-
- `lib/capybara/simulated/browser.rb` — Rack client, history stack,
|
|
407
|
-
modal handler queue, virtual-clock anchor, trace recorder. Owns
|
|
408
|
-
the JS runtime via `V8Runtime` or `QuickJSRuntime`. The hot
|
|
409
|
-
operations (`find_css` / `find_xpath` / DOM ops / event dispatch)
|
|
410
|
-
are single-`Context#call` round-trips returning handle id arrays;
|
|
411
|
-
per-result iteration stays Ruby-side.
|
|
412
|
-
- `lib/capybara/simulated/v8_runtime.rb` / `quickjs_runtime.rb` —
|
|
413
|
-
per-engine wrappers, common bits in `runtime_shared.rb`. The V8
|
|
414
|
-
base-snapshot (and the QuickJS bytecode equivalent) bakes in the
|
|
415
|
-
bundled bridge + vendored deps, so a per-navigation context reset
|
|
416
|
-
(V8) or pooled VM checkout (QuickJS) is sub-millisecond.
|
|
417
|
-
- `lib/capybara/simulated/driver.rb` — Capybara `Driver::Base`
|
|
418
|
-
surface (visit / find / execute_script / window handling / modal /
|
|
419
|
-
tracing API).
|
|
420
|
-
- `lib/capybara/simulated/node.rb` — `Driver::Node` over a
|
|
421
|
-
`(handle_id, context_gen)` pair so a handle from a pre-rebuild
|
|
422
|
-
Context can't ghost into the next one.
|
|
258
|
+
- `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`; and the cascade resolver for `display` / `visibility` / `text-transform`. 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.
|
|
259
|
+
- `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.
|
|
260
|
+
- `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.
|
|
261
|
+
- `lib/capybara/simulated/driver.rb` — Capybara `Driver::Base` surface (visit / find / execute_script / window handling / modal / tracing API).
|
|
262
|
+
- `lib/capybara/simulated/node.rb` — `Driver::Node` over a `(handle_id, context_gen)` pair so a handle from a pre-rebuild Context can't ghost into the next one.
|
|
423
263
|
|
|
424
264
|
## License
|
|
425
265
|
|