capybara-simulated 0.7.0 → 0.8.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 +4 -3
- data/lib/capybara/simulated/browser.rb +2562 -234
- data/lib/capybara/simulated/driver.rb +43 -2
- data/lib/capybara/simulated/js/bridge.bundle.js +30437 -18595
- data/lib/capybara/simulated/node.rb +25 -7
- data/lib/capybara/simulated/quickjs_runtime.rb +52 -9
- data/lib/capybara/simulated/runtime_shared.rb +425 -7
- data/lib/capybara/simulated/v8_runtime.rb +81 -6
- data/lib/capybara/simulated/version.rb +1 -1
- metadata +2 -2
|
@@ -364,6 +364,10 @@ module Capybara
|
|
|
364
364
|
# Browser's `@current_realm_id` may still point at it; the Browser uses
|
|
365
365
|
# this to raise a stale-element instead of running a frame handle op
|
|
366
366
|
# against the main registry.
|
|
367
|
+
# Per-frame browsing contexts (nested realms for iframes / aux windows) are a V8-engine
|
|
368
|
+
# feature; QuickJS keeps a same-realm fallback. `within_frame` gates on this.
|
|
369
|
+
def supports_frames? = true
|
|
370
|
+
|
|
367
371
|
# Ids of every live frame / window realm in this isolate (excludes the main
|
|
368
372
|
# realm, id 0). Used to fan a BroadcastChannel post out to sibling realms.
|
|
369
373
|
def frame_realm_ids = frame_realms.keys
|
|
@@ -440,10 +444,22 @@ module Capybara
|
|
|
440
444
|
# keeps the new realm's `parent`/`top` wired to the owning realm. The
|
|
441
445
|
# Browser then re-points the iframe element at the new id (`__csimRebindFrameRealm`).
|
|
442
446
|
def reload_frame_realm(old_id, parent_id, url, body, content_type)
|
|
443
|
-
|
|
447
|
+
# A re-navigated document discards its child browsing contexts, so dispose the old realm's
|
|
448
|
+
# DESCENDANT frame realms too — not just old_id. The JS src-reassignment path gets this for
|
|
449
|
+
# free (the old document's iframe elements go away → DOM-unregister disposes their realms);
|
|
450
|
+
# the Ruby reload path (navigate_realm_self_get/_post) rebuilds without that DOM teardown, so
|
|
451
|
+
# a descendant frame's realm would otherwise linger and its contentWindow stay live.
|
|
452
|
+
dispose_frame_realm_tree(old_id)
|
|
444
453
|
create_frame_realm(ctx, url, body, content_type, parent_id)
|
|
445
454
|
end
|
|
446
455
|
|
|
456
|
+
# Dispose a frame realm and every descendant frame realm (transitively), deepest first so a
|
|
457
|
+
# parent's child-realm set is consistent as each level is torn down.
|
|
458
|
+
def dispose_frame_realm_tree(id)
|
|
459
|
+
frame_realm_parents.select {|_child, parent| parent == id }.each_key {|child| dispose_frame_realm_tree(child) }
|
|
460
|
+
dispose_frame_realm(id)
|
|
461
|
+
end
|
|
462
|
+
|
|
447
463
|
# Navigate a window realm (`win.location = …`): a FRESH realm for the new
|
|
448
464
|
# document, like reload_frame_realm — an in-place `__csimLoadDocument` on an
|
|
449
465
|
# already-loaded realm does NOT re-run inline scripts, but a fresh realm does.
|
|
@@ -452,7 +468,9 @@ module Capybara
|
|
|
452
468
|
# the window after navigating it; a stable WindowProxy is future work.)
|
|
453
469
|
def reload_window_realm(old_id, url, body, content_type)
|
|
454
470
|
meta = window_realm_meta[old_id] || {}
|
|
455
|
-
|
|
471
|
+
# Like reload_frame_realm, a re-navigated window discards its child browsing contexts — dispose
|
|
472
|
+
# the descendant frame realms too, not just old_id, so a popup's iframes don't linger.
|
|
473
|
+
dispose_frame_realm_tree(old_id)
|
|
456
474
|
create_window_realm(url, body, content_type, opener_id: meta[:opener_id], window_name: meta[:window_name])
|
|
457
475
|
end
|
|
458
476
|
|
|
@@ -463,6 +481,8 @@ module Capybara
|
|
|
463
481
|
# The frame's browsing context is going away — revoke the blob URLs it
|
|
464
482
|
# created (url-lifetime "Removing an iframe").
|
|
465
483
|
@browser.revoke_realm_blobs(id) rescue nil
|
|
484
|
+
# Drop it from the SW Client registry so matchAll stops returning a dead client.
|
|
485
|
+
@browser.sw_unregister_client(id) rescue nil
|
|
466
486
|
@realm_module_handles&.delete(id)
|
|
467
487
|
@frame_realm_depths&.delete(id)
|
|
468
488
|
@frame_realm_parents&.delete(id)
|
|
@@ -733,8 +753,8 @@ module Capybara
|
|
|
733
753
|
# id (or nil on failure — then the bridge keeps its same-realm fallback).
|
|
734
754
|
# The bridge maps `iframe.contentWindow` to `RustyRacer.contextGlobal(id)`.
|
|
735
755
|
def attach_frame_realm_loader(c)
|
|
736
|
-
c.attach('__csim_createFrameRealm', ->(url, body, content_type, parent_id = 0, frame_name = nil, frame_doc_origin = nil, frame_location_origin = nil, js_url_source = nil) {
|
|
737
|
-
RuntimeShared.safe_call { create_frame_realm(c, url, body, content_type, parent_id, frame_name, frame_doc_origin, frame_location_origin, js_url_source) }
|
|
756
|
+
c.attach('__csim_createFrameRealm', ->(url, body, content_type, parent_id = 0, frame_name = nil, frame_doc_origin = nil, frame_location_origin = nil, js_url_source = nil, frame_about_base = nil) {
|
|
757
|
+
RuntimeShared.safe_call { create_frame_realm(c, url, body, content_type, parent_id, frame_name, frame_doc_origin, frame_location_origin, js_url_source, frame_about_base) }
|
|
738
758
|
})
|
|
739
759
|
# Re-navigating an iframe (src/srcdoc reassigned) builds a fresh realm;
|
|
740
760
|
# the bridge calls this to tear down the superseded one so it doesn't
|
|
@@ -760,6 +780,15 @@ module Capybara
|
|
|
760
780
|
|
|
761
781
|
def frame_realm_depths = (@frame_realm_depths ||= {})
|
|
762
782
|
|
|
783
|
+
# A frame whose document URL is opaque — about:blank (empty src), about:srcdoc,
|
|
784
|
+
# or empty — has no scope of its own; its controller is inherited from the
|
|
785
|
+
# creator. (create_frame_realm receives 'about:blank'/'about:srcdoc' as the url
|
|
786
|
+
# for these, seeded by __csimFrameWindow.)
|
|
787
|
+
def opaque_frame_url?(url)
|
|
788
|
+
u = url.to_s
|
|
789
|
+
u.empty? || u.start_with?('about:')
|
|
790
|
+
end
|
|
791
|
+
|
|
763
792
|
# Bring a freshly-created realm context up to a runnable bridge, shared by the
|
|
764
793
|
# frame and window realm constructors. Re-evaling the snapshot source would
|
|
765
794
|
# redefine snapshot globals (e.g. the `scrollX` accessor) and throw, so only
|
|
@@ -777,7 +806,7 @@ module Capybara
|
|
|
777
806
|
realm
|
|
778
807
|
end
|
|
779
808
|
|
|
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)
|
|
809
|
+
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, frame_about_base = nil)
|
|
781
810
|
depth = (frame_realm_depths[parent_id] || 0) + 1
|
|
782
811
|
if depth > MAX_FRAME_DEPTH
|
|
783
812
|
@browser.log_console('warn', "iframe nesting depth #{depth} exceeds #{MAX_FRAME_DEPTH}; not building #{url}")
|
|
@@ -836,6 +865,36 @@ module Capybara
|
|
|
836
865
|
# The frame's location.origin (opaque "null" for about:blank / srcdoc /
|
|
837
866
|
# javascript:); decoupled from the location string so navigation is intact.
|
|
838
867
|
realm.call('__csimSetLocationOrigin', frame_location_origin.to_s) unless frame_location_origin.nil?
|
|
868
|
+
# An about:blank / about:srcdoc frame's URL is opaque, but its base URL (for
|
|
869
|
+
# relative-URL resolution) is INHERITED from the creator. Seed that inherited
|
|
870
|
+
# base BEFORE the document loads so its load-time scripts resolve relative URLs
|
|
871
|
+
# against the parent, even though location.href reports about:blank/srcdoc.
|
|
872
|
+
realm.call('__csimSetAboutBaseURL', frame_about_base.to_s) unless frame_about_base.to_s.empty?
|
|
873
|
+
# If a service worker's scope covers this frame's URL, the frame is a CONTROLLED client:
|
|
874
|
+
# wire its `navigator.serviceWorker.controller` BEFORE its document loads, so the frame's
|
|
875
|
+
# load-time scripts see the controller and route their subresource fetch() / XHR / EventSource
|
|
876
|
+
# through the SW's fetch event (fetch-event-network-error's controllee fires XHRs during load).
|
|
877
|
+
# The frame never called register(), so its per-realm registration Map is empty — set the
|
|
878
|
+
# controller directly from Ruby's scope→handle mirror. An OPAQUE child (about:blank / srcdoc)
|
|
879
|
+
# has no scope of its own, so it INHERITS its parent frame's controller (HTML: an about:blank
|
|
880
|
+
# document is controlled by its creator). Inheritance chains through controlled FRAME realms
|
|
881
|
+
# (sw_note_realm_controller); recording it BEFORE the load also lets a NESTED frame built during
|
|
882
|
+
# this frame's load inherit the controller.
|
|
883
|
+
opaque = opaque_frame_url?(url)
|
|
884
|
+
# A sandboxed frame WITHOUT allow-same-origin has an OPAQUE origin (doc origin 'null') and is
|
|
885
|
+
# cross-origin to its creator, so it does NOT inherit the creator's controller (srcdoc-iframe:
|
|
886
|
+
# "sandboxed srcdoc should not inherit"). allow-same-origin restores the parent origin → inherits.
|
|
887
|
+
inheritable = opaque && frame_doc_origin.to_s != 'null'
|
|
888
|
+
ctrl = @browser.sw_client_controller_for(url.to_s)
|
|
889
|
+
ctrl ||= @browser.sw_inherited_controller_for(parent_id) if inheritable
|
|
890
|
+
if ctrl
|
|
891
|
+
realm.call('__csim_swSetControllerDirect', *ctrl)
|
|
892
|
+
# Only an OPAQUE (about:blank / srcdoc) frame is mirrored into the SW's clients.matchAll():
|
|
893
|
+
# a real-URL frame's registration would fire during its SYNCHRONOUS SW nav-fetch (main thread
|
|
894
|
+
# blocked on @sw_nav_outbox), perturbing worker timing — the http-src client model is deferred.
|
|
895
|
+
@browser.sw_note_realm_controller(realm.id, ctrl)
|
|
896
|
+
@browser.sw_register_client(realm.id, url.to_s, 'window', 'nested', ctrl[0]) if opaque
|
|
897
|
+
end
|
|
839
898
|
realm.call('__csimLoadDocument', body.to_s, content_type.to_s)
|
|
840
899
|
# A `javascript:` URL frame: the initial empty document is now loaded and
|
|
841
900
|
# parent/top are wired, so evaluate the URL's script in the realm (global
|
|
@@ -1308,10 +1367,26 @@ module Capybara
|
|
|
1308
1367
|
# the per-worker postMessage host fn closed over `post_back`.
|
|
1309
1368
|
# Returns a uniform `WorkerRuntime` adapter that
|
|
1310
1369
|
# `Browser#run_worker` drives.
|
|
1311
|
-
def self.build_worker(browser, post_back)
|
|
1370
|
+
def self.build_worker(browser, post_back, broadcast_out = nil, sw_hooks = {})
|
|
1312
1371
|
c = Ctx.new(snapshot: snapshot)
|
|
1313
1372
|
attach_host_fns(c, browser)
|
|
1314
1373
|
c.attach('__csim_workerPostMessage', ->(data) { post_back.call(data); nil })
|
|
1374
|
+
# Service-worker → main-thread signals route through the thread-safe outbox (delivered by
|
|
1375
|
+
# deliver_worker_messages): client.postMessage, clients.claim (set the client's controller),
|
|
1376
|
+
# and a controlled fetch's respondWith result. See run_worker for the closures.
|
|
1377
|
+
c.attach('__csim_swPostToClient', ->(client_id, data) { sw_hooks[:post_to_client]&.call(client_id, data); nil }) if sw_hooks[:post_to_client]
|
|
1378
|
+
c.attach('__csim_swClaim', -> { sw_hooks[:claim]&.call; nil }) if sw_hooks[:claim]
|
|
1379
|
+
c.attach('__csim_swFetchRespond', ->(fetch_id, resp, realm_id) { sw_hooks[:fetch_respond]&.call(fetch_id, resp, realm_id); nil }) if sw_hooks[:fetch_respond]
|
|
1380
|
+
c.attach('__csim_swFetchStream', ->(fetch_id, kind, payload, realm_id) { sw_hooks[:fetch_stream]&.call(fetch_id, kind, payload, realm_id); nil }) if sw_hooks[:fetch_stream]
|
|
1381
|
+
# Cross-isolate MessagePort channel signals (a worker/SW port endpoint + its outbound messages).
|
|
1382
|
+
c.attach('__csim_workerPortPost', ->(channel, data) { sw_hooks[:port_post]&.call(channel, data); nil }) if sw_hooks[:port_post]
|
|
1383
|
+
c.attach('__csim_workerPortEndpoint', ->(channel) { sw_hooks[:port_endpoint]&.call(channel); nil }) if sw_hooks[:port_endpoint]
|
|
1384
|
+
# A worker is a SEPARATE isolate: its BroadcastChannel fan-out to the main + frame realms +
|
|
1385
|
+
# other workers can't run `browser.broadcast_to_windows` inline (that would mutate the main
|
|
1386
|
+
# browser's inbox from the worker thread). Route it through the thread-safe outbox instead
|
|
1387
|
+
# (the shared BROWSER_HOST_FNS `__csimBroadcast` above, bound to `browser`, is overridden
|
|
1388
|
+
# here). Same-worker-context delivery already happened in-VM via `_bcChannels`.
|
|
1389
|
+
c.attach('__csimBroadcast', ->(name, data, _rid, origin) { broadcast_out&.call(name, data, origin); nil }) if broadcast_out
|
|
1315
1390
|
# Worker's timer table is independent from main's; routing the
|
|
1316
1391
|
# worker's `setTimersActive` through `browser.timers_active=`
|
|
1317
1392
|
# races the main isolate's polling? gate, dropping main-thread
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: capybara-simulated
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
@@ -112,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
112
112
|
- !ruby/object:Gem::Version
|
|
113
113
|
version: '0'
|
|
114
114
|
requirements: []
|
|
115
|
-
rubygems_version: 4.0.
|
|
115
|
+
rubygems_version: 4.0.16
|
|
116
116
|
specification_version: 4
|
|
117
117
|
summary: Lightweight Capybara driver with an in-process JS-resident DOM, Chrome-free
|
|
118
118
|
test_files: []
|