capybara-simulated 0.7.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 +12 -10
- data/lib/capybara/simulated/browser.rb +3077 -245
- data/lib/capybara/simulated/driver.rb +85 -15
- data/lib/capybara/simulated/js/bridge.bundle.js +33716 -19156
- data/lib/capybara/simulated/node.rb +33 -7
- data/lib/capybara/simulated/quickjs_runtime.rb +74 -17
- data/lib/capybara/simulated/runtime_shared.rb +453 -34
- data/lib/capybara/simulated/v8_runtime.rb +134 -10
- data/lib/capybara/simulated/version.rb +1 -1
- metadata +2 -2
|
@@ -52,7 +52,10 @@ module Capybara
|
|
|
52
52
|
@@live.select!(&:weakref_alive?)
|
|
53
53
|
@@live.filter_map {|ref| ref.__getobj__ rescue nil }
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
# A DISPOSED driver is not live: its runtime context is gone, so calling into it raises
|
|
56
|
+
# (`undefined method 'call' for nil` out of `run_loop_step`). `dispose` deregisters, but the
|
|
57
|
+
# predicate is the belt — a WeakRef stays in the list until GC actually collects.
|
|
58
|
+
drivers.each {|d| yield d if d.owner_thread == thread && !d.disposed? }
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
# `viewport: [w, h]` and `user_agent:` (typically supplied via
|
|
@@ -71,7 +74,12 @@ module Capybara
|
|
|
71
74
|
# level and inject them into every per-window Browser. Each
|
|
72
75
|
# Browser still has its own sessionStorage + DOM + JS VM.
|
|
73
76
|
@cookies = {}
|
|
77
|
+
@auth_cache = {}
|
|
74
78
|
@local_storage = {}
|
|
79
|
+
# Cache Storage (caches/Cache) is origin-shared like localStorage — owned at the
|
|
80
|
+
# Driver level and injected, so a service worker and every same-origin window see
|
|
81
|
+
# the same caches (partitioned by origin key within the store).
|
|
82
|
+
@cache_storage = {}
|
|
75
83
|
# Capture the universal-server flag ONCE, at session construction — the WPT
|
|
76
84
|
# runner sets CSIM_LOCAL_ALL_HOSTS only while building the session, then
|
|
77
85
|
# restores it. Every window (incl. aux windows opened later) inherits this so
|
|
@@ -100,7 +108,9 @@ module Capybara
|
|
|
100
108
|
driver: self,
|
|
101
109
|
js_engine: @js_engine,
|
|
102
110
|
cookies: @cookies,
|
|
111
|
+
auth_cache: @auth_cache,
|
|
103
112
|
local_storage: @local_storage,
|
|
113
|
+
cache_storage: @cache_storage,
|
|
104
114
|
all_hosts_local: @all_hosts_local)
|
|
105
115
|
end
|
|
106
116
|
|
|
@@ -292,6 +302,29 @@ module Capybara
|
|
|
292
302
|
@active_handle = nil
|
|
293
303
|
@blob_partitions_lock.synchronize { @blob_partitions.clear }
|
|
294
304
|
end
|
|
305
|
+
|
|
306
|
+
# Full teardown of the whole driver: aux windows AND the primary browser's
|
|
307
|
+
# V8 isolate. `reset_windows!` deliberately keeps the primary alive (the
|
|
308
|
+
# per-test reset path rebuilds only its page); this is for permanently
|
|
309
|
+
# DROPPING a session. A caller that nils its session without this leaks the
|
|
310
|
+
# primary isolate — with its heap, canvas pixel buffers, and worker threads —
|
|
311
|
+
# into V8Runtime's process-wide `@@live` until at_exit. The WPT runner recycles
|
|
312
|
+
# the cross-origin session per `.sub.`/`.https.` file, so that leak is ~one
|
|
313
|
+
# isolate per cross-origin file (hundreds over the suite); disposing here
|
|
314
|
+
# incrementally is what reset_windows! already does for aux windows.
|
|
315
|
+
def dispose
|
|
316
|
+
return if @disposed
|
|
317
|
+
@disposed = true
|
|
318
|
+
# Drop out of the live registry FIRST: everything below tears down the runtime this driver
|
|
319
|
+
# would be asked to step if `each_live_on_thread` still yielded it.
|
|
320
|
+
@@live_lock.synchronize { @@live.reject! {|ref| (ref.__getobj__ rescue nil).equal?(self) } }
|
|
321
|
+
reset_windows!
|
|
322
|
+
@browser.dispose rescue nil
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Has this driver been permanently dropped? (A `reset!` between examples does NOT set this —
|
|
326
|
+
# that rebuilds the page on a live runtime.)
|
|
327
|
+
def disposed? = @disposed == true
|
|
295
328
|
def go_back = current_browser.go_back
|
|
296
329
|
def go_forward = current_browser.go_forward
|
|
297
330
|
def reset_history! = current_browser.reset_history!
|
|
@@ -341,6 +374,12 @@ module Capybara
|
|
|
341
374
|
window_entries.find {|w| w[:handle] == handle }&.fetch(:browser)
|
|
342
375
|
end
|
|
343
376
|
|
|
377
|
+
# Same, but for the operations that ADDRESS a window rather than probe for one: a closed or
|
|
378
|
+
# unknown handle is Capybara's `WindowError`, never a silent fall back to the current window.
|
|
379
|
+
def window_browser!(handle)
|
|
380
|
+
window_browser(handle) or raise Capybara::WindowError, "Unknown window handle: #{handle}"
|
|
381
|
+
end
|
|
382
|
+
|
|
344
383
|
# Open (or, by `name`, reuse) an auxiliary window. `target="_blank"`
|
|
345
384
|
# clicks and `window.open` both land here. A non-empty `name` that
|
|
346
385
|
# matches an existing window navigates that window instead of opening a
|
|
@@ -404,7 +443,7 @@ module Capybara
|
|
|
404
443
|
|
|
405
444
|
# `window.open(url, name)` from the `opener` window's JS. Resolves the URL
|
|
406
445
|
# against the opener's document and records the opener relationship.
|
|
407
|
-
def open_window_from_js(opener_browser, url, name, opener_realm_id = 0)
|
|
446
|
+
def open_window_from_js(opener_browser, url, name, opener_realm_id = 0, about_base = nil, about_origin = nil)
|
|
408
447
|
resolved = url.to_s.empty? ? nil : opener_browser.resolve_document_url(url)
|
|
409
448
|
# Opening a blob: URL whose storage partition differs from the opener's
|
|
410
449
|
# top-level site is forced NOOPENER (cross-partition-navigation): the new
|
|
@@ -420,7 +459,7 @@ module Capybara
|
|
|
420
459
|
# cross-window scripting/adoption need no cross-isolate RPC. The opener's realm
|
|
421
460
|
# id wires the popup's window.opener. Falls through to the separate-VM aux path
|
|
422
461
|
# (cross-origin, or a URL we don't yet realm-load).
|
|
423
|
-
if (rid = opener_browser.open_window_realm(resolved, name: name, opener_realm_id: opener_realm_id))
|
|
462
|
+
if (rid = opener_browser.open_window_realm(resolved, name: name, opener_realm_id: opener_realm_id, about_base: about_base, about_origin: about_origin))
|
|
424
463
|
return rid
|
|
425
464
|
end
|
|
426
465
|
open_aux_window(resolved, name: name, opener_handle: handle_for(opener_browser), source: opener_browser)
|
|
@@ -506,10 +545,30 @@ module Capybara
|
|
|
506
545
|
|
|
507
546
|
# `BroadcastChannel.postMessage` — deliver to every OTHER window's channels
|
|
508
547
|
# with the same name (same-window delivery is handled in-VM by the sender).
|
|
509
|
-
def broadcast_channel(source_browser, name, data)
|
|
548
|
+
def broadcast_channel(source_browser, name, data, origin = nil)
|
|
549
|
+
# An opaque origin is unique to its own agent cluster; its key is a token ('opaque:…') minted
|
|
550
|
+
# per-realm and therefore only unique WITHIN one isolate — two unrelated opaque contexts in
|
|
551
|
+
# DIFFERENT windows could mint the same token. A BroadcastChannel never bridges two distinct
|
|
552
|
+
# opaque origins, and no opaque origin spans separate top-level windows here, so a cross-
|
|
553
|
+
# WINDOW post from an opaque origin reaches no one: drop it rather than risk a cross-isolate
|
|
554
|
+
# token collision. (Same-isolate opaque peers are reached in-VM / via enqueue_broadcast; an
|
|
555
|
+
# inherited-origin blob worker via its own inbox — neither goes through this cross-window path.)
|
|
556
|
+
return if origin.to_s.start_with?('opaque:')
|
|
510
557
|
window_entries.each do |w|
|
|
511
558
|
next if w[:browser].equal?(source_browser)
|
|
512
|
-
w[:browser].enqueue_broadcast(name, data)
|
|
559
|
+
w[:browser].enqueue_broadcast(name, data, nil, origin)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# A localStorage change fans out to every OTHER window (localStorage spans same-origin
|
|
564
|
+
# browsing contexts). Every window shares the Driver's one `@local_storage` jar, so all
|
|
565
|
+
# windows are same-origin peers here (cross-origin storage partitioning is a separate
|
|
566
|
+
# backlog item); a nil source realm reaches each target's every realm. sessionStorage is
|
|
567
|
+
# per-context and never reaches this path.
|
|
568
|
+
def storage_broadcast(source_browser, kind, key, old, new, url)
|
|
569
|
+
window_entries.each do |w|
|
|
570
|
+
next if w[:browser].equal?(source_browser)
|
|
571
|
+
w[:browser].enqueue_storage_event(kind, key, old, new, url, nil)
|
|
513
572
|
end
|
|
514
573
|
end
|
|
515
574
|
|
|
@@ -593,7 +652,14 @@ module Capybara
|
|
|
593
652
|
def open_new_window(_kind = :tab)
|
|
594
653
|
open_aux_window('about:blank')
|
|
595
654
|
end
|
|
596
|
-
|
|
655
|
+
# Every window has its own viewport, so these address the window the
|
|
656
|
+
# handle names — not the active one. `Capybara::Window#resize_to` on a
|
|
657
|
+
# background window must resize THAT window and leave the current one
|
|
658
|
+
# (and Capybara's idea of which window is current) alone.
|
|
659
|
+
def window_size(handle)
|
|
660
|
+
b = window_browser!(handle)
|
|
661
|
+
[b.viewport_width, b.viewport_height]
|
|
662
|
+
end
|
|
597
663
|
def close_window(h)
|
|
598
664
|
return if h == PRIMARY_HANDLE
|
|
599
665
|
@aux_windows.reject! {|w|
|
|
@@ -614,19 +680,23 @@ module Capybara
|
|
|
614
680
|
end
|
|
615
681
|
end
|
|
616
682
|
def switch_to_window(h)
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
elsif @aux_windows.any? {|w| w[:handle] == h }
|
|
620
|
-
@active_handle = h
|
|
621
|
-
else
|
|
622
|
-
raise Capybara::WindowError, "Unknown window handle: #{h}"
|
|
623
|
-
end
|
|
683
|
+
window_browser!(h) # unknown / already-closed handle → WindowError
|
|
684
|
+
@active_handle = (h == PRIMARY_HANDLE ? nil : h)
|
|
624
685
|
end
|
|
625
|
-
def resize_window_to(
|
|
686
|
+
def resize_window_to(handle, w, h) = window_browser!(handle).set_viewport(w, h)
|
|
626
687
|
# Forem's ahoy-tracking spec calls `driver.resize(w, h)` directly
|
|
627
688
|
# rather than through `current_window.resize_to`.
|
|
628
689
|
def resize(w, h) = current_browser.set_viewport(w, h)
|
|
629
|
-
|
|
690
|
+
# Both restore the window to the display it lives on (`Browser#screen_size`), which is where
|
|
691
|
+
# it started — so they undo a `resize_to` rather than doing nothing. Coarse: we model no
|
|
692
|
+
# window chrome, so a maximized window and a fullscreen one end up the same size (a real
|
|
693
|
+
# browser's fullscreen is taller by the chrome it hides).
|
|
694
|
+
def maximize_window(handle) = restore_window_size(handle)
|
|
695
|
+
def fullscreen_window(handle) = restore_window_size(handle)
|
|
696
|
+
private def restore_window_size(handle)
|
|
697
|
+
b = window_browser!(handle)
|
|
698
|
+
b.set_viewport(*b.screen_size)
|
|
699
|
+
end
|
|
630
700
|
|
|
631
701
|
def evaluate_script(script, *args)
|
|
632
702
|
unwrap(current_browser.evaluate_script(script, args))
|