capybara-simulated 0.2.0 → 0.4.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 +71 -128
- data/lib/capybara/simulated/browser.rb +442 -48
- data/lib/capybara/simulated/js/bridge.bundle.js +302 -44
- data/lib/capybara/simulated/runtime_shared.rb +5 -0
- data/lib/capybara/simulated/v8_runtime.rb +11 -4
- 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: 3b2b947ec8b6945ec441d61c70a2bf0155283696c7d8188a135c990850149f68
|
|
4
|
+
data.tar.gz: c7530db87c7b019f25db0370213c6965a4c3f44172284ef469f84e8617319291
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c3e5d6358892be026316a31531ed4ced5e66f52ac0dafcda51236977f8f94f40b5465e9653cce4ce2ad84cee0ef4d29b8fbe2d850cd94efb3f1a3ab47db3eb6
|
|
7
|
+
data.tar.gz: '099d9b40e65a21667a543721f26b1df60176a2651da945f175016d5d85bbd81b0296099a17bf23b2a415101387489573d6b487aecdf8255ee0a923935d0f9b10'
|
data/README.md
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight Capybara driver that runs JavaScript against an
|
|
4
4
|
in-process JS-resident DOM, with no Chrome. Forms submit through
|
|
5
|
-
`Rack::MockRequest`, inline `<script>` and event handlers run,
|
|
6
|
-
|
|
7
|
-
Trix / Stimulus / Turbo all work, and the Capybara DSL is unchanged.
|
|
5
|
+
`Rack::MockRequest`, inline `<script>` and event handlers run, and the
|
|
6
|
+
Capybara DSL is unchanged.
|
|
8
7
|
|
|
9
8
|
The DOM lives entirely inside the JS engine — V8 via
|
|
10
9
|
[rusty_racer](https://github.com/ursm/rusty_racer) or QuickJS via
|
|
@@ -37,33 +36,15 @@ visual layout:
|
|
|
37
36
|
|
|
38
37
|
**Reach for a real browser** (Selenium / Cuprite) **when** your tests need
|
|
39
38
|
what this driver doesn't simulate **by design** — there's no rendering
|
|
40
|
-
engine
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
**`within_frame` / `switch_to_frame`** work on the V8 (rusty_racer) engine:
|
|
51
|
-
each `<iframe>` runs its own scripts in its own per-frame realm, and the DSL
|
|
52
|
-
routes finds, reads, interactions, `evaluate_script`, and self-targeted
|
|
53
|
-
navigation (a link or form submit inside the frame) into the active frame —
|
|
54
|
-
nested frames included. (QuickJS keeps a same-realm fallback, so
|
|
55
|
-
`within_frame` is V8-only.)
|
|
56
|
-
|
|
57
|
-
**Multiple windows / tabs** work on both engines: each window is its own
|
|
58
|
-
Browser + JS VM (own DOM, sessionStorage, history; cookies + localStorage
|
|
59
|
-
shared). `open_new_window` / `within_window` / `switch_to_window` /
|
|
60
|
-
`window_opened_by` drive them, and JS `window.open` opens a real window,
|
|
61
|
-
`window.opener` points back to the opener, and `postMessage` is delivered
|
|
62
|
-
across windows. (`target="_blank"` defaults to no-opener, matching modern
|
|
63
|
-
browsers; cross-window `postMessage` data is JSON-shaped, not a full
|
|
64
|
-
structured clone.)
|
|
65
|
-
|
|
66
|
-
See [Known limits](#known-limits) for the full picture.
|
|
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).
|
|
67
48
|
|
|
68
49
|
## Status
|
|
69
50
|
|
|
@@ -76,10 +57,9 @@ Mastodon / Discourse) runs its full system suite against the driver in
|
|
|
76
57
|
[capybara-simulated-vs-world](https://github.com/ursm/capybara-simulated-vs-world)
|
|
77
58
|
as an integration check.
|
|
78
59
|
|
|
79
|
-
The remaining gaps
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
screenshots and this driver deliberately doesn't simulate.
|
|
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)).
|
|
83
63
|
|
|
84
64
|
## Install
|
|
85
65
|
|
|
@@ -288,21 +268,6 @@ isolate whose context is reset to a clean realm per navigation
|
|
|
288
268
|
snapshot-loaded VM out of a small pre-warmed pool. Either way, every
|
|
289
269
|
navigation lands on a clean, warm JS context near-instantly.
|
|
290
270
|
|
|
291
|
-
**Wall time is sensitive to whether the app uses Turbo Drive**,
|
|
292
|
-
because navigation simulates real-browser semantics:
|
|
293
|
-
|
|
294
|
-
| navigation source | what happens |
|
|
295
|
-
|---|---|
|
|
296
|
-
| `visit(...)`, `refresh`, programmatic `location.assign` | full reload — fresh JS Context, scripts re-evaluated |
|
|
297
|
-
| link click *with Turbo Drive loaded* | Turbo intercepts, body-swap via JS, **JS context preserved** |
|
|
298
|
-
| link click *without Turbo Drive* | full reload (anchor default action) |
|
|
299
|
-
| form submit *with Turbo Drive loaded* | Turbo intercepts (turbo-frame or page-level), body-swap |
|
|
300
|
-
| form submit *without Turbo Drive* | full reload |
|
|
301
|
-
|
|
302
|
-
So Turbo Drive apps stay fast even with click-heavy tests; non-Turbo
|
|
303
|
-
apps pay full-reload cost per click — exactly mirroring what the
|
|
304
|
-
production site does.
|
|
305
|
-
|
|
306
271
|
### Library snapshot policy
|
|
307
272
|
|
|
308
273
|
Per visit, `<script src>`-referenced libraries (jQuery, Stimulus,
|
|
@@ -333,53 +298,64 @@ referenced page-specific DOM.
|
|
|
333
298
|
for 2 s; the callback fires once polling has advanced the clock past
|
|
334
299
|
it.
|
|
335
300
|
|
|
336
|
-
##
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
`:hover` set). Symmetric peers — N rows each with `tr:hover .icon`
|
|
351
|
-
revealing `.icon`, queried as bare `find('.icon')` — reveal all and
|
|
352
|
-
Capybara raises `Capybara::Ambiguous`. Scope the test (`find('tr',
|
|
353
|
-
text: 'foo').hover` then `find('.icon')`) — also more robust
|
|
354
|
-
against real-browser flake.
|
|
355
|
-
- **`fetch` is synchronous-via-Rack** — HTML / JSON round-trips work
|
|
356
|
-
but there's no real network, no streaming, no `Request#body`
|
|
357
|
-
ReadableStream, and no concurrent requests. XHR is implemented
|
|
358
|
-
with the same Rack pass-through.
|
|
359
|
-
- **WebSocket, screenshots, and drag pixel coordinates** are out of
|
|
360
|
-
scope by design — use Selenium / Cuprite. (EventSource and Web
|
|
361
|
-
Workers *are* implemented.)
|
|
362
|
-
- **`within_frame` / `switch_to_frame`** work on the V8 engine: each
|
|
363
|
-
`<iframe>` runs in its own per-frame realm and the DSL routes finds,
|
|
364
|
-
reads, interactions, `evaluate_script`, and self-targeted navigation
|
|
365
|
-
(link / form submit) into the active frame (nested frames included) — the
|
|
366
|
-
frame's realm is rebuilt from the fetched document, leaving the top page
|
|
367
|
-
untouched. `_top` navigates the main page; a `_parent` target from a
|
|
368
|
-
frame nested ≥2 levels falls back to navigating the main page, and
|
|
369
|
-
cross-origin frame locality resolves against the main origin. QuickJS has
|
|
370
|
-
no nested browsing context, so `within_frame` raises there.
|
|
371
|
-
- **Multiple windows / tabs** work on both engines: each window is its own
|
|
301
|
+
## Capabilities & limits
|
|
302
|
+
|
|
303
|
+
Most features run in-process; the notes below are mostly "works, but…",
|
|
304
|
+
followed by the short list of things that need a real browser **by design**.
|
|
305
|
+
|
|
306
|
+
### Works, with constraints
|
|
307
|
+
|
|
308
|
+
- **`within_frame` / `switch_to_frame`** (V8 engine) — each `<iframe>` runs
|
|
309
|
+
its own scripts in its own per-frame realm; the DSL routes finds, reads,
|
|
310
|
+
interactions, `evaluate_script`, and navigation into the active frame,
|
|
311
|
+
nested frames included — the target frame's realm is rebuilt from the
|
|
312
|
+
fetched document, the top page untouched. QuickJS has no nested browsing
|
|
313
|
+
context, so `within_frame` raises there.
|
|
314
|
+
- **Multiple windows / tabs** (both engines) — each window is its own
|
|
372
315
|
Browser + JS VM (own DOM, sessionStorage, history; cookies + localStorage
|
|
373
|
-
shared
|
|
374
|
-
`
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
316
|
+
shared). `open_new_window` / `within_window` / `switch_to_window` /
|
|
317
|
+
`window_opened_by` drive them; JS `window.open` opens a real window,
|
|
318
|
+
`window.opener` links back, and `postMessage` crosses windows. Only the
|
|
319
|
+
active window's event loop runs, so a message is delivered when you switch
|
|
320
|
+
to its window. `target="_blank"` opens with no opener (modern-browser
|
|
321
|
+
default). `postMessage` carries real structured data (not a lossy JSON
|
|
322
|
+
hop) — `Map` / `Set` / `Date` / `BigInt` / typed arrays / cyclic graphs all
|
|
323
|
+
round-trip on V8 — and a `transfer`-list buffer moves **zero-copy** (backing
|
|
324
|
+
store by token, source detached); only bare `undefined` collapses to `null`
|
|
325
|
+
(Ruby has no distinct `undefined`). Window viewport APIs (`maximize` /
|
|
326
|
+
`fullscreen` / pixel-exact `resize_to`) are no-ops (no layout engine).
|
|
327
|
+
- **WebSocket + Action Cable** — `new WebSocket(url)` works in-process over
|
|
328
|
+
the `rack.hijack` socket the Rack app hijacks (hand-rolled RFC6455, including
|
|
329
|
+
subprotocol negotiation). The real `@rails/actioncable` consumer connects,
|
|
330
|
+
subscribes, and receives broadcasts, so `turbo_stream_from` live updates
|
|
331
|
+
work. Constraints: server
|
|
332
|
+
pushes land at settle (not instant); the Cable app must use the **async /
|
|
333
|
+
in-process** adapter (a real Redis adapter needs real Redis); binary frames
|
|
334
|
+
are V8-only (QuickJS corrupts raw bytes across the host boundary — text,
|
|
335
|
+
hence Action Cable, works on both engines). `EventSource` and Web Workers
|
|
336
|
+
are likewise real (background reader threads draining at settle).
|
|
337
|
+
- **`fetch` / XHR** — synchronous through Rack: HTML / JSON round-trips work,
|
|
338
|
+
but there's no streaming, no `Request#body` ReadableStream, and no
|
|
339
|
+
concurrent requests.
|
|
340
|
+
- **`:hover` / `:focus-within`-gated content** — reachable two ways: call
|
|
341
|
+
`element.hover` explicitly (we track the most-recently-hovered element and
|
|
342
|
+
propagate `:hover` up its chain), or rely on the candidate-chain fallback
|
|
343
|
+
(when the stateless cascade reports `display: none`, we re-evaluate with the
|
|
344
|
+
candidate itself in the `:hover` set). Symmetric peers — N rows each with
|
|
345
|
+
`tr:hover .icon` revealing `.icon`, queried as a bare `find('.icon')` —
|
|
346
|
+
reveal all and Capybara raises `Capybara::Ambiguous`; scope the test
|
|
347
|
+
(`find('tr', text: 'foo').hover` then `find('.icon')`), which is also more
|
|
348
|
+
robust against real-browser flake.
|
|
349
|
+
|
|
350
|
+
### Out of scope (by design — use Selenium / Cuprite)
|
|
351
|
+
|
|
352
|
+
- **Layout / pixel geometry.** `visible?` and `Node#style` consult the CSS
|
|
353
|
+
cascade and the inline `style` attribute, but `getBoundingClientRect()`
|
|
354
|
+
returns zeros and `elementFromPoint()` isn't implemented. Click offsets work
|
|
355
|
+
for fixture-style absolute / relative positioning (ancestor-summed
|
|
356
|
+
`top`/`left`); position-via-layout (Dragula drops, sticky-header scroll math,
|
|
357
|
+
viewport-clip visibility) needs a real browser.
|
|
358
|
+
- **Screenshots.**
|
|
383
359
|
|
|
384
360
|
## Architecture
|
|
385
361
|
|
|
@@ -413,39 +389,6 @@ referenced page-specific DOM.
|
|
|
413
389
|
`(handle_id, context_gen)` pair so a handle from a pre-rebuild
|
|
414
390
|
Context can't ghost into the next one.
|
|
415
391
|
|
|
416
|
-
## ES modules + importmap
|
|
417
|
-
|
|
418
|
-
`<script type="module">` and `<script type="importmap">` work the
|
|
419
|
-
same way they do in a real browser: bare specifiers resolve through
|
|
420
|
-
the importmap, relative paths resolve against the importer's URL,
|
|
421
|
-
and every load (including dynamic `import(...)`) routes back through
|
|
422
|
-
the in-process Rack app. No bundling step, no Node toolchain.
|
|
423
|
-
|
|
424
|
-
The standard importmap-rails layout works as-is:
|
|
425
|
-
|
|
426
|
-
```erb
|
|
427
|
-
<%= javascript_importmap_tags %>
|
|
428
|
-
<!-- emits:
|
|
429
|
-
<script type="importmap">{ "imports": { "application": "/assets/application-...js", ... } }</script>
|
|
430
|
-
<script type="module">import "application"</script>
|
|
431
|
-
-->
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
## Hotwire (Stimulus + Turbo)
|
|
435
|
-
|
|
436
|
-
Stimulus and Turbo work both via UMD (classic `<script src>`) and via
|
|
437
|
-
the standard ESM bundles imported through importmap. For
|
|
438
|
-
importmap-rails apps, no changes are needed:
|
|
439
|
-
|
|
440
|
-
```ruby
|
|
441
|
-
# config/importmap.rb
|
|
442
|
-
pin '@hotwired/stimulus'
|
|
443
|
-
pin '@hotwired/turbo'
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
`window.fetch` routes through Rack, so Turbo's frame fetch and
|
|
447
|
-
link-action POSTs round-trip the test app.
|
|
448
|
-
|
|
449
392
|
## License
|
|
450
393
|
|
|
451
394
|
[MIT](LICENSE).
|