capybara-simulated 0.3.0 → 0.5.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 +108 -149
- data/exe/capybara-simulated +143 -0
- data/lib/capybara/simulated/browser.rb +690 -98
- data/lib/capybara/simulated/driver.rb +52 -6
- data/lib/capybara/simulated/js/bridge.bundle.js +12522 -6595
- data/lib/capybara/simulated/js/snapshot_stubs.js +34 -0
- data/lib/capybara/simulated/minitest.rb +65 -0
- data/lib/capybara/simulated/quickjs_runtime.rb +9 -0
- data/lib/capybara/simulated/rspec.rb +32 -0
- data/lib/capybara/simulated/runtime_shared.rb +27 -3
- data/lib/capybara/simulated/trace.rb +35 -2
- data/lib/capybara/simulated/trace_persistence.rb +48 -0
- data/lib/capybara/simulated/trace_viewer.html +408 -0
- data/lib/capybara/simulated/v8_runtime.rb +193 -21
- data/lib/capybara/simulated/version.rb +1 -1
- data/vendor/js/vendor.bundle.js +21 -10
- metadata +23 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67e9168ef15e413c1d85b1805ea4d68f36cae09578af5fc539bd1613d52ac04d
|
|
4
|
+
data.tar.gz: 6214303ef25079d567bc91db43460eda49a54a292ed08021b98a81ce48af0c31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8112cd3056184946f65739dfb30e76e55bee2a79f1058e3128bc8ae15f0b5790f195d0c289759518e667b69902962e70922af21f48dea6610744ab1093a10ca4
|
|
7
|
+
data.tar.gz: 53ccb66cb2bd4723d1e999ebd03b0a56bdbc83978834f82b66c128aa2c7e3ba1c4f5464c5965442104de94de2fbabe79e185a74f6e1aaa3cd2c2c2c2bfe68a26
|
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,35 +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
|
-
socket; see below.)
|
|
50
|
-
- **Screenshots**.
|
|
51
|
-
|
|
52
|
-
**`within_frame` / `switch_to_frame`** work on the V8 (rusty_racer) engine:
|
|
53
|
-
each `<iframe>` runs its own scripts in its own per-frame realm, and the DSL
|
|
54
|
-
routes finds, reads, interactions, `evaluate_script`, and self-targeted
|
|
55
|
-
navigation (a link or form submit inside the frame) into the active frame —
|
|
56
|
-
nested frames included. (QuickJS keeps a same-realm fallback, so
|
|
57
|
-
`within_frame` is V8-only.)
|
|
58
|
-
|
|
59
|
-
**Multiple windows / tabs** work on both engines: each window is its own
|
|
60
|
-
Browser + JS VM (own DOM, sessionStorage, history; cookies + localStorage
|
|
61
|
-
shared). `open_new_window` / `within_window` / `switch_to_window` /
|
|
62
|
-
`window_opened_by` drive them, and JS `window.open` opens a real window,
|
|
63
|
-
`window.opener` points back to the opener, and `postMessage` is delivered
|
|
64
|
-
across windows. (`target="_blank"` defaults to no-opener, matching modern
|
|
65
|
-
browsers; cross-window `postMessage` data is JSON-shaped, not a full
|
|
66
|
-
structured clone.)
|
|
67
|
-
|
|
68
|
-
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).
|
|
69
48
|
|
|
70
49
|
## Status
|
|
71
50
|
|
|
@@ -78,10 +57,9 @@ Mastodon / Discourse) runs its full system suite against the driver in
|
|
|
78
57
|
[capybara-simulated-vs-world](https://github.com/ursm/capybara-simulated-vs-world)
|
|
79
58
|
as an integration check.
|
|
80
59
|
|
|
81
|
-
The remaining gaps
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
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)).
|
|
85
63
|
|
|
86
64
|
## Install
|
|
87
65
|
|
|
@@ -222,11 +200,18 @@ end
|
|
|
222
200
|
|
|
223
201
|
### File output
|
|
224
202
|
|
|
225
|
-
|
|
226
|
-
RSpec
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
203
|
+
Require the test-framework integration in your `spec_helper` /
|
|
204
|
+
`rails_helper` (RSpec) or `test_helper` /
|
|
205
|
+
`application_system_test_case.rb` (Minitest):
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
require 'capybara/simulated/rspec' # RSpec
|
|
209
|
+
require 'capybara/simulated/minitest' # Minitest / Rails system tests
|
|
210
|
+
```
|
|
211
|
+
|
|
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.
|
|
230
215
|
|
|
231
216
|
```sh
|
|
232
217
|
CSIM_TRACE_DIR=tmp/csim-traces bundle exec rspec spec/system
|
|
@@ -236,6 +221,31 @@ The metadata block on each trace includes `title`, `file`, `outcome`
|
|
|
236
221
|
(`passed` / `failed`), and the exception message — enough to index a
|
|
237
222
|
CI artifact directory by failure.
|
|
238
223
|
|
|
224
|
+
### Viewing traces
|
|
225
|
+
|
|
226
|
+
The recorded JSON stays plain data; to look at one, render it into a
|
|
227
|
+
self-contained HTML viewer with the bundled CLI:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
capybara-simulated trace tmp/csim-traces/checkout_flow.json
|
|
231
|
+
# wrote /tmp/checkout_flow.html (then opens it in your browser)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
By default the HTML is written to a temp file and opened in your
|
|
235
|
+
browser. The viewer works straight from `file://` — the trace JSON is
|
|
236
|
+
embedded inline, so no server is needed — and shows a step-by-step UI: a
|
|
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.
|
|
248
|
+
|
|
239
249
|
### Programmatic
|
|
240
250
|
|
|
241
251
|
For finer control, call `driver.start_tracing(...)` /
|
|
@@ -290,21 +300,6 @@ isolate whose context is reset to a clean realm per navigation
|
|
|
290
300
|
snapshot-loaded VM out of a small pre-warmed pool. Either way, every
|
|
291
301
|
navigation lands on a clean, warm JS context near-instantly.
|
|
292
302
|
|
|
293
|
-
**Wall time is sensitive to whether the app uses Turbo Drive**,
|
|
294
|
-
because navigation simulates real-browser semantics:
|
|
295
|
-
|
|
296
|
-
| navigation source | what happens |
|
|
297
|
-
|---|---|
|
|
298
|
-
| `visit(...)`, `refresh`, programmatic `location.assign` | full reload — fresh JS Context, scripts re-evaluated |
|
|
299
|
-
| link click *with Turbo Drive loaded* | Turbo intercepts, body-swap via JS, **JS context preserved** |
|
|
300
|
-
| link click *without Turbo Drive* | full reload (anchor default action) |
|
|
301
|
-
| form submit *with Turbo Drive loaded* | Turbo intercepts (turbo-frame or page-level), body-swap |
|
|
302
|
-
| form submit *without Turbo Drive* | full reload |
|
|
303
|
-
|
|
304
|
-
So Turbo Drive apps stay fast even with click-heavy tests; non-Turbo
|
|
305
|
-
apps pay full-reload cost per click — exactly mirroring what the
|
|
306
|
-
production site does.
|
|
307
|
-
|
|
308
303
|
### Library snapshot policy
|
|
309
304
|
|
|
310
305
|
Per visit, `<script src>`-referenced libraries (jQuery, Stimulus,
|
|
@@ -335,67 +330,64 @@ referenced page-specific DOM.
|
|
|
335
330
|
for 2 s; the callback fires once polling has advanced the clock past
|
|
336
331
|
it.
|
|
337
332
|
|
|
338
|
-
##
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
`:hover` set). Symmetric peers — N rows each with `tr:hover .icon`
|
|
353
|
-
revealing `.icon`, queried as bare `find('.icon')` — reveal all and
|
|
354
|
-
Capybara raises `Capybara::Ambiguous`. Scope the test (`find('tr',
|
|
355
|
-
text: 'foo').hover` then `find('.icon')`) — also more robust
|
|
356
|
-
against real-browser flake.
|
|
357
|
-
- **`fetch` is synchronous-via-Rack** — HTML / JSON round-trips work
|
|
358
|
-
but there's no real network, no streaming, no `Request#body`
|
|
359
|
-
ReadableStream, and no concurrent requests. XHR is implemented
|
|
360
|
-
with the same Rack pass-through.
|
|
361
|
-
- **WebSocket** works in-process: `new WebSocket(url)` rides the
|
|
362
|
-
`rack.hijack` socket the Rack app hijacks, with a hand-rolled RFC6455
|
|
363
|
-
client (handshake + subprotocol negotiation, masked client frames,
|
|
364
|
-
ping/pong, close handshake). Frames deliver as `message` events when
|
|
365
|
-
the page next settles, like SSE. **Action Cable** works end-to-end on
|
|
366
|
-
this: the real `@rails/actioncable` consumer connects, subscribes, and
|
|
367
|
-
receives server broadcasts (so `turbo_stream_from` live updates are
|
|
368
|
-
reachable) — Action Cable hijacks the connection just as csim drives
|
|
369
|
-
it. Caveats: server pushes land at settle (not instant); the app must
|
|
370
|
-
use the **async / in-process** Cable adapter (a real Redis adapter
|
|
371
|
-
would need real Redis); binary frames are V8-only (QuickJS corrupts
|
|
372
|
-
raw bytes across the host boundary — text, hence Action Cable, is fine
|
|
373
|
-
on both). `EventSource` and Web Workers are likewise implemented.
|
|
374
|
-
- **Screenshots and drag pixel coordinates** are out of scope by
|
|
375
|
-
design — use Selenium / Cuprite.
|
|
376
|
-
- **`within_frame` / `switch_to_frame`** work on the V8 engine: each
|
|
377
|
-
`<iframe>` runs in its own per-frame realm and the DSL routes finds,
|
|
378
|
-
reads, interactions, `evaluate_script`, and self-targeted navigation
|
|
379
|
-
(link / form submit) into the active frame (nested frames included) — the
|
|
380
|
-
frame's realm is rebuilt from the fetched document, leaving the top page
|
|
381
|
-
untouched. `_top` navigates the main page; a `_parent` target from a
|
|
382
|
-
frame nested ≥2 levels falls back to navigating the main page, and
|
|
383
|
-
cross-origin frame locality resolves against the main origin. QuickJS has
|
|
384
|
-
no nested browsing context, so `within_frame` raises there.
|
|
385
|
-
- **Multiple windows / tabs** work on both engines: each window is its own
|
|
333
|
+
## Capabilities & limits
|
|
334
|
+
|
|
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**.
|
|
337
|
+
|
|
338
|
+
### Works, with constraints
|
|
339
|
+
|
|
340
|
+
- **`within_frame` / `switch_to_frame`** (V8 engine) — each `<iframe>` runs
|
|
341
|
+
its own scripts in its own per-frame realm; the DSL routes finds, reads,
|
|
342
|
+
interactions, `evaluate_script`, and navigation into the active frame,
|
|
343
|
+
nested frames included — the target frame's realm is rebuilt from the
|
|
344
|
+
fetched document, the top page untouched. QuickJS has no nested browsing
|
|
345
|
+
context, so `within_frame` raises there.
|
|
346
|
+
- **Multiple windows / tabs** (both engines) — each window is its own
|
|
386
347
|
Browser + JS VM (own DOM, sessionStorage, history; cookies + localStorage
|
|
387
|
-
shared
|
|
388
|
-
`
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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.
|
|
381
|
+
|
|
382
|
+
### Out of scope (by design — use Selenium / Cuprite)
|
|
383
|
+
|
|
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.
|
|
390
|
+
- **Screenshots.**
|
|
399
391
|
|
|
400
392
|
## Architecture
|
|
401
393
|
|
|
@@ -429,39 +421,6 @@ referenced page-specific DOM.
|
|
|
429
421
|
`(handle_id, context_gen)` pair so a handle from a pre-rebuild
|
|
430
422
|
Context can't ghost into the next one.
|
|
431
423
|
|
|
432
|
-
## ES modules + importmap
|
|
433
|
-
|
|
434
|
-
`<script type="module">` and `<script type="importmap">` work the
|
|
435
|
-
same way they do in a real browser: bare specifiers resolve through
|
|
436
|
-
the importmap, relative paths resolve against the importer's URL,
|
|
437
|
-
and every load (including dynamic `import(...)`) routes back through
|
|
438
|
-
the in-process Rack app. No bundling step, no Node toolchain.
|
|
439
|
-
|
|
440
|
-
The standard importmap-rails layout works as-is:
|
|
441
|
-
|
|
442
|
-
```erb
|
|
443
|
-
<%= javascript_importmap_tags %>
|
|
444
|
-
<!-- emits:
|
|
445
|
-
<script type="importmap">{ "imports": { "application": "/assets/application-...js", ... } }</script>
|
|
446
|
-
<script type="module">import "application"</script>
|
|
447
|
-
-->
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
## Hotwire (Stimulus + Turbo)
|
|
451
|
-
|
|
452
|
-
Stimulus and Turbo work both via UMD (classic `<script src>`) and via
|
|
453
|
-
the standard ESM bundles imported through importmap. For
|
|
454
|
-
importmap-rails apps, no changes are needed:
|
|
455
|
-
|
|
456
|
-
```ruby
|
|
457
|
-
# config/importmap.rb
|
|
458
|
-
pin '@hotwired/stimulus'
|
|
459
|
-
pin '@hotwired/turbo'
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
`window.fetch` routes through Rack, so Turbo's frame fetch and
|
|
463
|
-
link-action POSTs round-trip the test app.
|
|
464
|
-
|
|
465
424
|
## License
|
|
466
425
|
|
|
467
426
|
[MIT](LICENSE).
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# capybara-simulated CLI. Currently one subcommand:
|
|
5
|
+
#
|
|
6
|
+
# capybara-simulated trace TRACE.json [-o OUT.html] [--no-open]
|
|
7
|
+
#
|
|
8
|
+
# Renders a trace JSON (produced by CSIM_TRACE_DIR / driver.stop_tracing)
|
|
9
|
+
# into a self-contained HTML viewer and opens it in your browser. The
|
|
10
|
+
# viewer embeds the JSON inline so it opens straight from file:// — no
|
|
11
|
+
# server, unlike a fetch/import-based loader (CORS blocks those for
|
|
12
|
+
# file:// origins). The persisted trace stays plain JSON; this turns one
|
|
13
|
+
# into a UI on demand.
|
|
14
|
+
|
|
15
|
+
require 'optparse'
|
|
16
|
+
require 'tmpdir'
|
|
17
|
+
|
|
18
|
+
# Prefer the library co-located with this executable. In an installed
|
|
19
|
+
# gem that's the gem's own lib (correct version); in a checkout it's the
|
|
20
|
+
# working tree — so `ruby exe/capybara-simulated` doesn't accidentally
|
|
21
|
+
# load an older *installed* capybara-simulated that predates this CLI.
|
|
22
|
+
lib = File.expand_path('../lib', __dir__)
|
|
23
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
|
24
|
+
require 'capybara/simulated/trace'
|
|
25
|
+
|
|
26
|
+
PROG = 'capybara-simulated'
|
|
27
|
+
|
|
28
|
+
# `file://` URL for a local path, percent-encoding each segment so a
|
|
29
|
+
# path with spaces / special chars stays a valid URI for launchy.
|
|
30
|
+
def file_url(path)
|
|
31
|
+
require 'cgi'
|
|
32
|
+
'file://' + path.split('/').map {|seg| CGI.escapeURIComponent(seg) }.join('/')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Hand the raw path (not a URL) to the platform opener; `system`'s array
|
|
36
|
+
# form needs no shell-quoting, so spaces are fine.
|
|
37
|
+
def open_with_platform(path)
|
|
38
|
+
cmd =
|
|
39
|
+
case RbConfig::CONFIG['host_os']
|
|
40
|
+
when /darwin/ then ['open', path]
|
|
41
|
+
when /mswin|mingw/ then ['cmd', '/c', 'start', '', path]
|
|
42
|
+
else ['xdg-open', path] # Linux / *BSD
|
|
43
|
+
end
|
|
44
|
+
system(*cmd)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Open a local file in the default browser. Prefer launchy (handles
|
|
50
|
+
# macOS / Linux / Windows / WSL) when installed; on either a missing gem
|
|
51
|
+
# OR a launchy runtime failure (headless box, no opener), fall back to
|
|
52
|
+
# the platform opener.
|
|
53
|
+
def open_in_browser(path)
|
|
54
|
+
begin
|
|
55
|
+
require 'launchy'
|
|
56
|
+
Launchy.open(file_url(path))
|
|
57
|
+
return true
|
|
58
|
+
rescue LoadError
|
|
59
|
+
# launchy not installed
|
|
60
|
+
rescue StandardError
|
|
61
|
+
# launchy present but failed — try the platform opener below
|
|
62
|
+
end
|
|
63
|
+
open_with_platform(path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cmd_trace(argv)
|
|
67
|
+
out = nil
|
|
68
|
+
open = nil # nil → default (open), unless writing to stdout
|
|
69
|
+
parser = OptionParser.new do |o|
|
|
70
|
+
o.banner = "Usage: #{PROG} trace [options] TRACE.json\n\n" \
|
|
71
|
+
"Render TRACE.json into a self-contained HTML viewer and open it\n" \
|
|
72
|
+
"in your browser. By default the HTML goes to a temp file."
|
|
73
|
+
o.on('-o', '--output PATH', "write HTML to PATH instead of a temp file ('-' = stdout)") {|v| out = v }
|
|
74
|
+
o.on('--[no-]open', 'open the HTML in your browser (default: yes)') {|v| open = v }
|
|
75
|
+
o.on('-h', '--help') do
|
|
76
|
+
puts o
|
|
77
|
+
exit
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
begin
|
|
81
|
+
parser.parse!(argv)
|
|
82
|
+
rescue OptionParser::ParseError => e
|
|
83
|
+
warn e.message
|
|
84
|
+
warn parser.help
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
input = argv.shift
|
|
89
|
+
unless input
|
|
90
|
+
warn parser.help
|
|
91
|
+
exit 1
|
|
92
|
+
end
|
|
93
|
+
unless File.file?(input)
|
|
94
|
+
warn "#{PROG}: no such file: #{input}"
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
json = File.read(input)
|
|
99
|
+
begin
|
|
100
|
+
require 'json'
|
|
101
|
+
JSON.parse(json) # fail fast on a non-JSON / corrupt trace
|
|
102
|
+
rescue JSON::ParserError => e
|
|
103
|
+
warn "#{PROG}: #{input} is not valid JSON: #{e.message}"
|
|
104
|
+
exit 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
html = Capybara::Simulated::Trace.render_viewer(json)
|
|
108
|
+
|
|
109
|
+
if out == '-'
|
|
110
|
+
$stdout.write(html)
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
# Default output: a temp file named after the trace (re-runs overwrite).
|
|
114
|
+
out ||= File.join(Dir.tmpdir, File.basename(input).sub(/\.json\z/, '') + '.html')
|
|
115
|
+
File.write(out, html)
|
|
116
|
+
warn "wrote #{out}"
|
|
117
|
+
open_in_browser(File.expand_path(out)) unless open == false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def main(argv)
|
|
121
|
+
case argv.first
|
|
122
|
+
when 'trace'
|
|
123
|
+
cmd_trace(argv.drop(1))
|
|
124
|
+
when '-v', '--version'
|
|
125
|
+
require 'capybara/simulated/version'
|
|
126
|
+
puts Capybara::Simulated::VERSION
|
|
127
|
+
when nil, '-h', '--help'
|
|
128
|
+
puts <<~USAGE
|
|
129
|
+
Usage: #{PROG} <command> [options]
|
|
130
|
+
|
|
131
|
+
Commands:
|
|
132
|
+
trace TRACE.json render a trace JSON into a self-contained HTML viewer
|
|
133
|
+
|
|
134
|
+
Run `#{PROG} trace --help` for command options.
|
|
135
|
+
USAGE
|
|
136
|
+
else
|
|
137
|
+
warn "#{PROG}: unknown command: #{argv.first}"
|
|
138
|
+
warn "Run `#{PROG} --help`."
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
main(ARGV)
|