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
|
@@ -225,11 +225,19 @@ module Capybara
|
|
|
225
225
|
# introduce a NoMethodError window for any stray post-close call (eval/call
|
|
226
226
|
# don't all nil-guard `@vm`) with no leak benefit.
|
|
227
227
|
|
|
228
|
-
# bridge.js patches `Intl.DateTimeFormat`; rusty_racer ships ICU
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
228
|
+
# bridge.js patches `Intl.DateTimeFormat`; rusty_racer ships ICU built-in but
|
|
229
|
+
# QuickJS gates it behind a polyfill flag (other surfaces bridge.js touches —
|
|
230
|
+
# URL / TextEncoder / atob/btoa / crypto — already route through Ruby host fns,
|
|
231
|
+
# so POLYFILL_INTL is the only one we strictly need).
|
|
232
|
+
#
|
|
233
|
+
# PERF (rule 3): quickjs is pinned to `~> 0.18.0` in the Gemfile. quickjs 0.19
|
|
234
|
+
# both split the Intl polyfills into a separate quickjs-polyfill-intl gem (which
|
|
235
|
+
# eval's them per VM at ~226 ms — vs this single bundled flag's ~140 ms) AND
|
|
236
|
+
# regressed interpreter execution ~2.8× (measured: the QuickJS spec suite ran
|
|
237
|
+
# 5.6 min on 0.18 vs 15.5 min on 0.19 with an equivalent Intl set). The 0.19
|
|
238
|
+
# migration is recorded in the cross-window / quickjs-CI memory; re-migrate to
|
|
239
|
+
# 0.19 + quickjs-polyfill-intl once that upstream perf regression is fixed.
|
|
240
|
+
INTL_FEATURES = [Quickjs::POLYFILL_INTL].freeze
|
|
233
241
|
#
|
|
234
242
|
# `max_stack_size: 0` — `JS_SetMaxStackSize` measures C stack
|
|
235
243
|
# delta from runtime construction; Ruby callers reach QuickJS
|
|
@@ -245,7 +253,7 @@ module Capybara
|
|
|
245
253
|
# handler returns `elapsed >= limit_ms`, so 0 fires on the first
|
|
246
254
|
# check), so practical no-limit.
|
|
247
255
|
VM_OPTIONS = {
|
|
248
|
-
features:
|
|
256
|
+
features: INTL_FEATURES,
|
|
249
257
|
max_stack_size: 0,
|
|
250
258
|
# quickjs.rb's 128 MB default trips "out of memory in regexp
|
|
251
259
|
# execution" on class-attribute-heavy polls and the heaviest
|
|
@@ -353,6 +361,9 @@ module Capybara
|
|
|
353
361
|
# flip doesn't race main's `polling?` gate. See v8_runtime's
|
|
354
362
|
# build_worker for the long-form rationale.
|
|
355
363
|
vm.define_function('__setTimersActive') {|_flag| nil }
|
|
364
|
+
# importScripts runs a classic script at top-level scope so its top-level
|
|
365
|
+
# const/let/class share the realm's global lexical env (see v8_runtime).
|
|
366
|
+
vm.define_function('__csim_workerImportEval') {|src| vm.eval_code(src.to_s); nil }
|
|
356
367
|
vm.eval_code('__csim_installWorkerScope();')
|
|
357
368
|
vm.drain_jobs!
|
|
358
369
|
WorkerRuntime.new(
|
|
@@ -49,6 +49,10 @@ module Capybara
|
|
|
49
49
|
# realm id). Deferred + applied by re-navigating the owning iframe, so a
|
|
50
50
|
# frame's `location.href = …` navigates the frame, not the top page.
|
|
51
51
|
'__csimFrameNavigate' => ->(b, *a) { b.frame_navigate_self(a[0], a[1].to_i); nil },
|
|
52
|
+
# A same-origin window realm (window.open in this isolate) navigating itself
|
|
53
|
+
# (a[0] = url, a[1] = the window's realm id). Deferred + applied by reloading
|
|
54
|
+
# that realm's document in place — see Browser#window_realm_navigate_self.
|
|
55
|
+
'__csimWindowRealmNavigate' => ->(b, *a) { b.window_realm_navigate_self(a[0], a[1].to_i); nil },
|
|
52
56
|
# A nested browsing context reloading its OWN location (a[0] = the frame's
|
|
53
57
|
# realm id). Deferred + applied by re-navigating the owning iframe to its
|
|
54
58
|
# current document — see Browser#frame_reload_self.
|
|
@@ -57,6 +61,7 @@ module Capybara
|
|
|
57
61
|
# initiating frame's realm id). Deferred + applied against that realm,
|
|
58
62
|
# like __csimFrameNavigate — see Browser#frame_submit_self.
|
|
59
63
|
'__csimFrameSubmit' => ->(b, *a) { b.frame_submit_self(a[0].to_i); nil },
|
|
64
|
+
'__csimFrameHistoryGo' => ->(b, *a) { b.frame_history_go(a[0].to_i, a[1].to_i); nil },
|
|
60
65
|
'__setTimersActive' => ->(b, *a) { b.timers_active = !!a[0]; nil },
|
|
61
66
|
'__setCurrentUrl' => ->(b, *a) { b.history_state(a[0], a[1]); nil },
|
|
62
67
|
'__pushHistoryEntry' => ->(b, *a) { b.history_push(a[0], a[1]); nil },
|
|
@@ -83,20 +88,37 @@ module Capybara
|
|
|
83
88
|
'__csim_wsClose' => ->(b, *a) { b.ws_close(a[0], a[1], a[2]); nil },
|
|
84
89
|
'__csim_rackFetchAsync' => ->(b, *a) { b.rack_fetch_async(a[0], a[1], a[2], a[3]) },
|
|
85
90
|
'__csim_rackFetchAsyncAbort' => ->(b, *a) { b.rack_fetch_async_abort(a[0]); nil },
|
|
86
|
-
# Cross-window references (window.open / opener / postMessage).
|
|
87
|
-
# window
|
|
88
|
-
# the
|
|
89
|
-
'__csimWindowOpen' => ->(b, *a) { b.open_child_window(a[0], a[1]) },
|
|
91
|
+
# Cross-window references (window.open / opener / postMessage). A separate-VM
|
|
92
|
+
# aux window forwards to the Driver; a same-origin window realm lives in this
|
|
93
|
+
# isolate. a[2] is the opener's realm id (for wiring window.opener).
|
|
94
|
+
'__csimWindowOpen' => ->(b, *a) { b.open_child_window(a[0], a[1], a[2]) },
|
|
95
|
+
# A `target=_blank`/named link/area activation from a frame or window realm:
|
|
96
|
+
# open a new auxiliary window (the realm's VM isn't rebuilt — a fresh window
|
|
97
|
+
# is). `opener` = rel=opener (target=_blank defaults to noopener); the Driver
|
|
98
|
+
# forces noopener for a cross-partition blob: target.
|
|
99
|
+
'__csimOpenAuxFromRealm' => ->(b, *a) { b.open_aux_from_realm(a[0], a[1], a[2]); nil },
|
|
90
100
|
'__csimWindowPostMessage' => ->(b, *a) { b.post_message_to_window(a[0], a[1], a[2]); nil },
|
|
91
|
-
'__csimBroadcast' => ->(b, *a) { b.broadcast_to_windows(a[0], a[1]); nil },
|
|
101
|
+
'__csimBroadcast' => ->(b, *a) { b.broadcast_to_windows(a[0], a[1], a[2].to_i); nil },
|
|
92
102
|
'__csimWindowGet' => ->(b, *a) { b.window_get(a[0], a[1]) },
|
|
93
103
|
'__csimWindowDocGet' => ->(b, *a) { b.window_doc_get(a[0], a[1]) },
|
|
104
|
+
# Cross-window remote-ref RPC: route an opener's node/object proxy op to
|
|
105
|
+
# the target window's VM (a[0]=window handle, a[1]=ref id, a[2]=prop/method).
|
|
106
|
+
'__csimWindowRefGet' => ->(b, *a) { b.window_ref_get(a[0], a[1], a[2]) },
|
|
107
|
+
'__csimWindowRefSet' => ->(b, *a) { b.window_ref_set(a[0], a[1], a[2], a[3]); nil },
|
|
108
|
+
'__csimWindowRefCall' => ->(b, *a) { b.window_ref_call(a[0], a[1], a[2], a[3]) },
|
|
94
109
|
'__csimWindowLocation' => ->(b, *a) { b.window_location_of(a[0]) },
|
|
95
110
|
'__csimWindowSetLocation' => ->(b, *a) { b.set_window_location(a[0], a[1]); nil },
|
|
111
|
+
'__csimWindowHistoryGo' => ->(b, *a) { b.window_history_go(a[0], a[1]) },
|
|
96
112
|
'__csimWindowClosed' => ->(b, *a) { b.window_closed?(a[0]) },
|
|
97
113
|
'__csimWindowClose' => ->(b, *a) { b.close_child_window(a[0]); nil },
|
|
98
114
|
'__csimWindowOpener' => ->(b, *_) { b.opener_handle },
|
|
115
|
+
# Fire an aux window's OWN `load` event (in its VM) — deferred by the
|
|
116
|
+
# opener so a child `window.onload` runs after the opener's current task.
|
|
117
|
+
'__csimFireAuxWindowLoad' => ->(b, *a) { b.fire_aux_window_load(a[0]); nil },
|
|
99
118
|
'__csim_workerSpawn' => ->(b, *a) { b.worker_spawn(a[0], shared: !!a[1]) },
|
|
119
|
+
# navigator.serviceWorker.register (universal-server only) — spawn a worker
|
|
120
|
+
# running the SW script as an executor context. Returns its handle.
|
|
121
|
+
'__csim_serviceWorkerRegister' => ->(b, *a) { b.worker_spawn(a[0], service: true) },
|
|
100
122
|
'__csim_workerPostToWorker' => ->(b, *a) { b.worker_post_to_worker(a[0], a[1]); nil },
|
|
101
123
|
'__csim_workerTerminate' => ->(b, *a) { b.worker_terminate(a[0]); nil },
|
|
102
124
|
'__csim_decodeImage' => ->(b, *a) { b.decode_image(a[0], a[1], a[2]) },
|
|
@@ -116,6 +138,11 @@ module Capybara
|
|
|
116
138
|
'__csim_transferFetch' => ->(b, *a) { b.transfer_buffer_fetch_for_js(a[0]) },
|
|
117
139
|
# Zero-copy postMessage transfer-token bookkeeping (see Browser#drop_pending_transfers).
|
|
118
140
|
'__csim_transferIssued' => ->(b, *a) { b.transfer_token_issued(a[0]); nil },
|
|
141
|
+
# Universal-server context (WPT runner)? Gates cross-origin eager frame
|
|
142
|
+
# building: only there is a cross-origin iframe's content served locally,
|
|
143
|
+
# so an ordinary app leaves cross-origin frames lazy (= baseline) and never
|
|
144
|
+
# eager-@app.calls a foreign URL (side effects: extra visit / log row).
|
|
145
|
+
'__csim_allHostsLocal' => ->(b, *a) { b.send(:all_hosts_local?) },
|
|
119
146
|
'__csim_decodeVideoFrame' => ->(b, *a) { b.decode_video_frame(a[0]) },
|
|
120
147
|
'__csim_encodeImage' => ->(b, *a) { b.encode_image(a[0], a[1], a[2], a[3], a[4]) },
|
|
121
148
|
# WebAuthn create / get raise `WebauthnState::Error` carrying
|
|
@@ -106,18 +106,8 @@ module Capybara
|
|
|
106
106
|
@ctx = @iso.context
|
|
107
107
|
@attached = []
|
|
108
108
|
@generation = 0
|
|
109
|
-
# rusty's heap-accounting API (heap_statistics / low_memory_notification)
|
|
110
|
-
# landed in 0.1.9. Probe the real Isolate once here — the Ctx wrapper
|
|
111
|
-
# delegates these unconditionally, so a `respond_to?` on the wrapper
|
|
112
|
-
# would always be true and never gate the version. Cached so the
|
|
113
|
-
# per-visit pressure check (rule 3) is an ivar read, not a dispatch.
|
|
114
|
-
@heap_accounting = @iso.respond_to?(:heap_statistics)
|
|
115
109
|
end
|
|
116
110
|
|
|
117
|
-
# True iff the underlying rusty Isolate exposes the 0.1.9 heap-accounting
|
|
118
|
-
# API. Drives `relieve_heap_pressure`'s version gate.
|
|
119
|
-
def heap_accounting? = @heap_accounting
|
|
120
|
-
|
|
121
111
|
# ── Context surface ─────────────────────────────────────────
|
|
122
112
|
# rusty drains microtasks at call-depth zero (V8's default kAuto
|
|
123
113
|
# policy), so a returned eval/call has already run its end-of-script
|
|
@@ -162,8 +152,8 @@ module Capybara
|
|
|
162
152
|
def terminate = @iso.terminate
|
|
163
153
|
def dispose = @iso.dispose
|
|
164
154
|
def perform_microtask_checkpoint = @iso.perform_microtask_checkpoint
|
|
165
|
-
#
|
|
166
|
-
# per-visit heap-pressure relief in `rebuild_ctx
|
|
155
|
+
# V8 heap accounting + a forced full GC (rusty >= 0.1.9, the gem's
|
|
156
|
+
# floor). Used by the per-visit heap-pressure relief in `rebuild_ctx`.
|
|
167
157
|
def heap_statistics = @iso.heap_statistics
|
|
168
158
|
def low_memory_notification = @iso.low_memory_notification
|
|
169
159
|
|
|
@@ -374,6 +364,10 @@ module Capybara
|
|
|
374
364
|
# Browser's `@current_realm_id` may still point at it; the Browser uses
|
|
375
365
|
# this to raise a stale-element instead of running a frame handle op
|
|
376
366
|
# against the main registry.
|
|
367
|
+
# Ids of every live frame / window realm in this isolate (excludes the main
|
|
368
|
+
# realm, id 0). Used to fan a BroadcastChannel post out to sibling realms.
|
|
369
|
+
def frame_realm_ids = frame_realms.keys
|
|
370
|
+
|
|
377
371
|
def frame_realm_alive?(realm_id)
|
|
378
372
|
!(realm_id.nil? || realm_id.zero?) && frame_realms.key?(realm_id)
|
|
379
373
|
end
|
|
@@ -410,9 +404,29 @@ module Capybara
|
|
|
410
404
|
# nothing else would ever free them.
|
|
411
405
|
def frame_realms = (@frame_realms ||= {})
|
|
412
406
|
|
|
407
|
+
# Parent realm id per frame realm, captured at `create_frame_realm` time
|
|
408
|
+
# (parallel to `@frame_realm_depths`). Lets a form/navigation that reaches a
|
|
409
|
+
# frame via `contentWindow` (so it never entered `within_frame` and has no
|
|
410
|
+
# `@frame_stack` entry) recover the realm that OWNS the iframe element, to
|
|
411
|
+
# rebuild + rebind it. 0/nil = the main realm.
|
|
412
|
+
def frame_realm_parents = (@frame_realm_parents ||= {})
|
|
413
|
+
|
|
414
|
+
def frame_realm_parent(realm_id)
|
|
415
|
+
return 0 if realm_id.nil? || realm_id.zero?
|
|
416
|
+
frame_realm_parents[realm_id] || 0
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Per-window-realm metadata (opener id + window.name) captured at create time so a
|
|
420
|
+
# window's self-navigation (which builds a fresh realm via `reload_window_realm`)
|
|
421
|
+
# can carry them across, the way a real popup keeps `window.opener` / `window.name`
|
|
422
|
+
# through its own navigation.
|
|
423
|
+
def window_realm_meta = (@window_realm_meta ||= {})
|
|
424
|
+
|
|
413
425
|
def dispose_frame_realms
|
|
414
426
|
@realm_module_handles&.clear
|
|
415
427
|
@frame_realm_depths&.clear
|
|
428
|
+
@frame_realm_parents&.clear
|
|
429
|
+
@window_realm_meta&.clear
|
|
416
430
|
return if @frame_realms.nil?
|
|
417
431
|
@frame_realms.each_value {|fr| fr.dispose rescue nil }
|
|
418
432
|
@frame_realms.clear
|
|
@@ -430,6 +444,18 @@ module Capybara
|
|
|
430
444
|
create_frame_realm(ctx, url, body, content_type, parent_id)
|
|
431
445
|
end
|
|
432
446
|
|
|
447
|
+
# Navigate a window realm (`win.location = …`): a FRESH realm for the new
|
|
448
|
+
# document, like reload_frame_realm — an in-place `__csimLoadDocument` on an
|
|
449
|
+
# already-loaded realm does NOT re-run inline scripts, but a fresh realm does.
|
|
450
|
+
# Returns the new realm's context id. (The opener's WindowProxy, keyed by the
|
|
451
|
+
# old id, goes stale across this — acceptable while no window.open test scripts
|
|
452
|
+
# the window after navigating it; a stable WindowProxy is future work.)
|
|
453
|
+
def reload_window_realm(old_id, url, body, content_type)
|
|
454
|
+
meta = window_realm_meta[old_id] || {}
|
|
455
|
+
dispose_frame_realm(old_id)
|
|
456
|
+
create_window_realm(url, body, content_type, opener_id: meta[:opener_id], window_name: meta[:window_name])
|
|
457
|
+
end
|
|
458
|
+
|
|
433
459
|
# Tear down a single frame realm (e.g. a descendant frame destroyed when an
|
|
434
460
|
# ancestor frame re-navigates). No-op for nil/0/unknown ids.
|
|
435
461
|
def dispose_frame_realm(id)
|
|
@@ -439,6 +465,14 @@ module Capybara
|
|
|
439
465
|
@browser.revoke_realm_blobs(id) rescue nil
|
|
440
466
|
@realm_module_handles&.delete(id)
|
|
441
467
|
@frame_realm_depths&.delete(id)
|
|
468
|
+
@frame_realm_parents&.delete(id)
|
|
469
|
+
@window_realm_meta&.delete(id)
|
|
470
|
+
# Evict from the main realm's child-realm set so `drainChildRealms` stops
|
|
471
|
+
# stepping a disposed context. A FRAME realm is also evicted via the DOM
|
|
472
|
+
# unregister path (its iframe element going away), but a WINDOW realm has no
|
|
473
|
+
# element — this is its only eviction, and it covers the reload (dispose +
|
|
474
|
+
# recreate) path too, where the old id would otherwise leak in the set.
|
|
475
|
+
ctx.eval("globalThis.__csimChildRealmIds && globalThis.__csimChildRealmIds.delete(#{id.to_i});") rescue nil
|
|
442
476
|
fr = frame_realms.delete(id)
|
|
443
477
|
fr.dispose rescue nil if fr
|
|
444
478
|
nil
|
|
@@ -577,10 +611,10 @@ module Capybara
|
|
|
577
611
|
# the GC itself only fires once a multi-visit spec has actually piled up
|
|
578
612
|
# dead realms — measured at ~once per 25-50 iframe-heavy visits, reclaiming
|
|
579
613
|
# native contexts back to 1 and the heap to baseline — so the amortized
|
|
580
|
-
# cost is negligible while memory stays bounded. No-op
|
|
581
|
-
# (
|
|
614
|
+
# cost is negligible while memory stays bounded. No-op when disabled
|
|
615
|
+
# (GC_PRESSURE_MB <= 0).
|
|
582
616
|
def relieve_heap_pressure
|
|
583
|
-
return unless GC_PRESSURE_MB.positive?
|
|
617
|
+
return unless GC_PRESSURE_MB.positive?
|
|
584
618
|
s = @ctx.heap_statistics
|
|
585
619
|
over = (s[:used_heap_size].to_i + s[:external_memory].to_i) > GC_PRESSURE_MB * 1_048_576
|
|
586
620
|
if HEAP_DIAG
|
|
@@ -596,8 +630,8 @@ module Capybara
|
|
|
596
630
|
a[:used_heap_size].to_i >> 20, a[:number_of_native_contexts].to_i))
|
|
597
631
|
end
|
|
598
632
|
rescue StandardError
|
|
599
|
-
#
|
|
600
|
-
#
|
|
633
|
+
# A transient read failure — heap relief is best-effort, never fail a
|
|
634
|
+
# visit over it.
|
|
601
635
|
end
|
|
602
636
|
|
|
603
637
|
# Built lazily on first use, on the calling (main) thread. There is no
|
|
@@ -726,6 +760,23 @@ module Capybara
|
|
|
726
760
|
|
|
727
761
|
def frame_realm_depths = (@frame_realm_depths ||= {})
|
|
728
762
|
|
|
763
|
+
# Bring a freshly-created realm context up to a runnable bridge, shared by the
|
|
764
|
+
# frame and window realm constructors. Re-evaling the snapshot source would
|
|
765
|
+
# redefine snapshot globals (e.g. the `scrollX` accessor) and throw, so only
|
|
766
|
+
# eval it on a bare no-snapshot dev ctx where the realm boots empty. The
|
|
767
|
+
# replayed `__csim_runScriptCached` / `__csim_evalEsmEntry` close over the MAIN
|
|
768
|
+
# ctx they were first attached to, so a realm script routing through them
|
|
769
|
+
# (leading-lexical, ≥64KB, or `type=module`) would run against the main
|
|
770
|
+
# document — rebind realm-executing variants on top, then reseed per-realm JS.
|
|
771
|
+
def seed_realm_bridge(realm)
|
|
772
|
+
has_bridge = realm.eval("typeof __csimLoadDocument === 'function'")
|
|
773
|
+
realm.eval(RuntimeShared.snapshot_src) unless has_bridge
|
|
774
|
+
attach_run_script_with_cache(realm)
|
|
775
|
+
attach_realm_esm_entry(realm)
|
|
776
|
+
reseed_realm_js(realm)
|
|
777
|
+
realm
|
|
778
|
+
end
|
|
779
|
+
|
|
729
780
|
def create_frame_realm(parent_ctx, url, body, content_type, parent_id = 0, frame_name = nil, frame_doc_origin = nil, frame_location_origin = nil, js_url_source = nil)
|
|
730
781
|
depth = (frame_realm_depths[parent_id] || 0) + 1
|
|
731
782
|
if depth > MAX_FRAME_DEPTH
|
|
@@ -738,21 +789,9 @@ module Capybara
|
|
|
738
789
|
# (their create_frame_realm looks up this realm's depth as their parent's),
|
|
739
790
|
# so it must already be set or the nested depth undercounts and the cap
|
|
740
791
|
# never trips.
|
|
741
|
-
frame_realm_depths[realm.id]
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
# source on a bare no-snapshot dev ctx, where the realm boots empty.
|
|
745
|
-
# Host fns are replayed onto the realm by `Ctx#create_context`.
|
|
746
|
-
has_bridge = realm.eval("typeof __csimLoadDocument === 'function'")
|
|
747
|
-
realm.eval(RuntimeShared.snapshot_src) unless has_bridge
|
|
748
|
-
# The replayed `__csim_runScriptCached` / `__csim_runScriptEval` /
|
|
749
|
-
# `__csim_evalEsmEntry` close over the context they EXECUTE in (the
|
|
750
|
-
# main ctx) — left as-is, a frame script that routes through them
|
|
751
|
-
# (leading-lexical, ≥64KB, or `type=module`) would run against the
|
|
752
|
-
# PARENT realm's document. Rebind realm-executing variants on top.
|
|
753
|
-
attach_run_script_with_cache(realm)
|
|
754
|
-
attach_realm_esm_entry(realm)
|
|
755
|
-
reseed_realm_js(realm)
|
|
792
|
+
frame_realm_depths[realm.id] = depth
|
|
793
|
+
frame_realm_parents[realm.id] = parent_id.to_i # owning realm, for contentWindow-reached rebuilds
|
|
794
|
+
seed_realm_bridge(realm)
|
|
756
795
|
# Wire `parent` / `top` to the realm that owns this iframe (its context
|
|
757
796
|
# id passed from `__csimFrameWindow`), BEFORE the frame's scripts run —
|
|
758
797
|
# `top` propagates up the chain (the main realm's `top` is itself). A
|
|
@@ -836,6 +875,86 @@ module Capybara
|
|
|
836
875
|
nil
|
|
837
876
|
end
|
|
838
877
|
|
|
878
|
+
# Build a same-origin auxiliary WINDOW (window.open / open_new_window) as a
|
|
879
|
+
# realm in THIS isolate — like an iframe's frame realm, but top-level: `parent`
|
|
880
|
+
# === `top` === the window itself (the bridge default, so unlike
|
|
881
|
+
# create_frame_realm we don't wire them to a container), and `window.opener`
|
|
882
|
+
# points at the opener realm's WindowProxy. Tracked in `frame_realms` so
|
|
883
|
+
# realm_call / drainChildRealms / dispose_frame_realms cover it for free.
|
|
884
|
+
# Returns the new realm's context id; the opener-side `window.open` wraps it in
|
|
885
|
+
# a native `__csimFrameWindowProxyFor`, so `popup.document` is a real
|
|
886
|
+
# same-isolate Document (cross-window adoptNode works). The single isolate also
|
|
887
|
+
# makes a same-origin window far cheaper than today's isolate-per-window.
|
|
888
|
+
def create_window_realm(url, body, content_type, opener_id: nil, window_name: nil, doc_origin: nil, location_origin: nil)
|
|
889
|
+
realm = seed_realm_bridge(ctx.create_context)
|
|
890
|
+
# Mark it a top-level window realm so its location setter routes to
|
|
891
|
+
# __csimWindowRealmNavigate (reload THIS realm) rather than the frame-nav or
|
|
892
|
+
# top-page path — top === self here, so neither default branch fits. Also give
|
|
893
|
+
# it the window-lifecycle surface a popup needs but a frame realm doesn't:
|
|
894
|
+
# `window.closed` (flag-backed) and `window.close()` (marks closed; the realm
|
|
895
|
+
# lingers inert until the Browser tears the isolate down — matching a real
|
|
896
|
+
# closed window whose proxy stays valid and reports closed === true).
|
|
897
|
+
realm.eval(<<~JS)
|
|
898
|
+
globalThis.__csimIsWindowRealm = true;
|
|
899
|
+
globalThis.__csimWindowClosedFlag = false;
|
|
900
|
+
try {
|
|
901
|
+
Object.defineProperty(globalThis, 'closed', {
|
|
902
|
+
configurable: true,
|
|
903
|
+
get() { return !!globalThis.__csimWindowClosedFlag; }
|
|
904
|
+
});
|
|
905
|
+
} catch (_) {}
|
|
906
|
+
globalThis.close = function () { globalThis.__csimWindowClosedFlag = true; };
|
|
907
|
+
JS
|
|
908
|
+
# window.opener → a WindowProxy for the opener realm. opener_id is the opener's
|
|
909
|
+
# context id (0 = the main realm, a VALID opener); nil means "no opener", so the
|
|
910
|
+
# guard is on nil, not on 0 (0 is falsy but real here). Assigning globalThis.opener
|
|
911
|
+
# routes through the bridge's opener setter (stores the override the getter returns).
|
|
912
|
+
unless opener_id.nil?
|
|
913
|
+
realm.eval(<<~JS)
|
|
914
|
+
if (typeof globalThis.__csimFrameWindowProxyFor === 'function') {
|
|
915
|
+
var __op = globalThis.__csimFrameWindowProxyFor(#{opener_id.to_i});
|
|
916
|
+
if (__op) globalThis.opener = __op;
|
|
917
|
+
}
|
|
918
|
+
JS
|
|
919
|
+
end
|
|
920
|
+
realm.call('__csimUpdateLocation', url.to_s) unless url.to_s.empty?
|
|
921
|
+
realm.call('__csimSetWindowName', window_name.to_s) unless window_name.nil?
|
|
922
|
+
realm.call('__csimSetDocumentOrigin', doc_origin.to_s) unless doc_origin.nil?
|
|
923
|
+
realm.call('__csimSetLocationOrigin', location_origin.to_s) unless location_origin.nil?
|
|
924
|
+
# Register the realm as alive BEFORE its document loads: its inline scripts run
|
|
925
|
+
# synchronously inside __csimLoadDocument and may post to a BroadcastChannel the
|
|
926
|
+
# opener listens on (a blob popup that posts then self.close()s). The delivery
|
|
927
|
+
# path gates on `frame_realm_alive?`, so the realm must already be tracked or its
|
|
928
|
+
# own load-time post is dropped. Mirrors create_frame_realm seeding its depth /
|
|
929
|
+
# parent maps before the load for the same reason.
|
|
930
|
+
frame_realms[realm.id] = realm
|
|
931
|
+
frame_realm_parents[realm.id] = 0 # top-level (no parent frame)
|
|
932
|
+
# Register with the opener (main) realm's child-realm set so `drainChildRealms`
|
|
933
|
+
# steps THIS realm's event loop too — otherwise its queued tasks (e.g. a
|
|
934
|
+
# BroadcastChannel delivery from a blob document) never fire.
|
|
935
|
+
ctx.eval("(globalThis.__csimChildRealmIds || (globalThis.__csimChildRealmIds = new Set())).add(#{realm.id});")
|
|
936
|
+
# Remember the window's opener / name so a self-navigation (reload_window_realm
|
|
937
|
+
# builds a FRESH realm) can carry them across — a real popup keeps window.opener
|
|
938
|
+
# and window.name through its own navigation.
|
|
939
|
+
window_realm_meta[realm.id] = {opener_id: opener_id, window_name: window_name}
|
|
940
|
+
realm.call('__csimLoadDocument', body.to_s, content_type.to_s)
|
|
941
|
+
realm.call('__csimFireWindowLoad') rescue nil
|
|
942
|
+
realm.id
|
|
943
|
+
rescue StandardError => e
|
|
944
|
+
@browser.log_console('warn', "window realm load failed: #{e.message}")
|
|
945
|
+
if realm
|
|
946
|
+
# The realm is registered (frame_realms / parents / __csimChildRealmIds)
|
|
947
|
+
# BEFORE the document loads, so a load-time throw must roll all of that back
|
|
948
|
+
# — otherwise frame_realm_alive? and drainChildRealms keep treating a
|
|
949
|
+
# disposed context as live. dispose_frame_realm unwinds the registries (and
|
|
950
|
+
# disposes if registered); the explicit dispose is the backstop for a throw
|
|
951
|
+
# that happened before registration.
|
|
952
|
+
dispose_frame_realm(realm.id)
|
|
953
|
+
realm.dispose rescue nil
|
|
954
|
+
end
|
|
955
|
+
nil
|
|
956
|
+
end
|
|
957
|
+
|
|
839
958
|
# `target` is the context the module graph compiles + evaluates in,
|
|
840
959
|
# `handles` its module-handle cache — the main ctx + `native_module_handles`
|
|
841
960
|
# by default, or a frame realm + its realm-local cache (Module handles are
|
|
@@ -1199,6 +1318,12 @@ module Capybara
|
|
|
1199
1318
|
# pending XHRs the moment the worker's queue empties. The settle
|
|
1200
1319
|
# loop already polls `worker_pending?` for worker thread activity.
|
|
1201
1320
|
c.attach('__setTimersActive', ->(_flag) { nil })
|
|
1321
|
+
# `importScripts` runs a classic script at the worker's TOP-LEVEL script scope,
|
|
1322
|
+
# so its top-level `const`/`let`/`class` (dispatcher.js's `const send`/`receive`)
|
|
1323
|
+
# join the realm's shared global lexical environment where later code sees them.
|
|
1324
|
+
# `(0, eval)` would block-scope them to the eval and they'd vanish. `c.eval` is
|
|
1325
|
+
# the top-level-script path (same as the worker's own body eval).
|
|
1326
|
+
c.attach('__csim_workerImportEval', ->(src) { c.eval(src.to_s); nil })
|
|
1202
1327
|
c.eval('__csim_installWorkerScope();')
|
|
1203
1328
|
WorkerRuntime.new(
|
|
1204
1329
|
eval_fn: ->(s) { c.eval(s.to_s) },
|