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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 692a6a8f67870e2078ba815a0698831f3cec4ada4dc3d79d9af795156107e37b
4
- data.tar.gz: 734a4cfe94f4cd93367b95082619e10729cb4ebf1f83a890b4d87c78ce5fd3cf
3
+ metadata.gz: 3b2b947ec8b6945ec441d61c70a2bf0155283696c7d8188a135c990850149f68
4
+ data.tar.gz: c7530db87c7b019f25db0370213c6965a4c3f44172284ef469f84e8617319291
5
5
  SHA512:
6
- metadata.gz: 9c9bfde944aad7339e4d7153e2d49b3b6834662fc5bcaf945a63899ca580493d1c67bfbd625ce7e06683bdbfacab83fa8192af7f02a73d5228f080dc22fc1dda
7
- data.tar.gz: da6971ee07edd867e00baa4e6d382ff291d5d0c8c437084f6f4d7dba50ae5c0596e4726db9be58f1d7a6847340fccff847d06d0056fdb8de3847a703887a4c28
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
- MutationObserver / custom elements / `<template>` / Shadow DOM /
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 or real network stack, the same ground Selenium covers via a real
41
- browser:
42
-
43
- - **Pixel layout** — `getBoundingClientRect()` returns zeros and
44
- `elementFromPoint()` isn't implemented, so visual hit-testing,
45
- coordinate drag-and-drop, and sticky-scroll math don't work.
46
- - **Real networking** `fetch` / XHR are synchronous through Rack: no
47
- streaming, no concurrency, no WebSocket.
48
- - **Screenshots**.
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 need a real layout engine (`elementFromPoint`,
80
- truthy `getBoundingClientRect`, viewport-clip visibility, `display:
81
- contents` table edge cases) — the same set Selenium escapes via
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
- ## Known limits
337
-
338
- - **No layout engine.** `visible?` and `Node#style` consult the CSS
339
- cascade and the inline `style` attribute, but
340
- `getBoundingClientRect()` returns zeros and `elementFromPoint()`
341
- isn't implemented. Click offsets work for fixture-style absolute /
342
- relative positioning (ancestor-summed `top`/`left`); position-via-
343
- layout (Dragula drops, sticky-header scroll math) needs a real
344
- browser.
345
- - **`:hover` / `:focus-within`-gated content** is reachable two ways:
346
- call `element.hover` explicitly (we track the most-recently-hovered
347
- element and propagate `:hover` up its chain), or rely on the
348
- candidate-chain fallback (when stateless cascade reports
349
- `display: none`, we re-evaluate with the candidate itself in the
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 across windows). `open_new_window` / `within_window` /
374
- `switch_to_window` / `window_opened_by` drive them; JS `window.open` opens
375
- a real window, `window.opener` links back, and `postMessage` is routed
376
- across windows (delivered as a `message` event when the target window next
377
- settles). Caveats: `target="_blank"` opens with no opener (modern-browser
378
- no-opener default); cross-window `postMessage` data is JSON-shaped, not a
379
- full structured clone (no `DataCloneError`, `undefined`→`null`); and only
380
- the active window's event loop runs, so a message is delivered when you
381
- switch to its window. Window viewport APIs (`maximize` / `fullscreen` /
382
- pixel-exact `resize_to`) are no-ops no layout engine.
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).