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
|
@@ -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
|
|
@@ -422,6 +426,12 @@ module Capybara
|
|
|
422
426
|
# through its own navigation.
|
|
423
427
|
def window_realm_meta = (@window_realm_meta ||= {})
|
|
424
428
|
|
|
429
|
+
# Is this realm a TOP-LEVEL browsing context? The main realm is, and so is an auxiliary
|
|
430
|
+
# window realm — `frame_realm_parents` records 0 for one because it has no parent FRAME,
|
|
431
|
+
# which is not the same as being nested in the main window. An ancestor walk (the focus
|
|
432
|
+
# chain, most of all) has to stop here rather than step into the opener.
|
|
433
|
+
def top_level_realm?(realm_id) = realm_id.to_i.zero? || window_realm_meta.key?(realm_id.to_i)
|
|
434
|
+
|
|
425
435
|
def dispose_frame_realms
|
|
426
436
|
@realm_module_handles&.clear
|
|
427
437
|
@frame_realm_depths&.clear
|
|
@@ -440,10 +450,22 @@ module Capybara
|
|
|
440
450
|
# keeps the new realm's `parent`/`top` wired to the owning realm. The
|
|
441
451
|
# Browser then re-points the iframe element at the new id (`__csimRebindFrameRealm`).
|
|
442
452
|
def reload_frame_realm(old_id, parent_id, url, body, content_type)
|
|
443
|
-
|
|
453
|
+
# A re-navigated document discards its child browsing contexts, so dispose the old realm's
|
|
454
|
+
# DESCENDANT frame realms too — not just old_id. The JS src-reassignment path gets this for
|
|
455
|
+
# free (the old document's iframe elements go away → DOM-unregister disposes their realms);
|
|
456
|
+
# the Ruby reload path (navigate_realm_self_get/_post) rebuilds without that DOM teardown, so
|
|
457
|
+
# a descendant frame's realm would otherwise linger and its contentWindow stay live.
|
|
458
|
+
dispose_frame_realm_tree(old_id)
|
|
444
459
|
create_frame_realm(ctx, url, body, content_type, parent_id)
|
|
445
460
|
end
|
|
446
461
|
|
|
462
|
+
# Dispose a frame realm and every descendant frame realm (transitively), deepest first so a
|
|
463
|
+
# parent's child-realm set is consistent as each level is torn down.
|
|
464
|
+
def dispose_frame_realm_tree(id)
|
|
465
|
+
frame_realm_parents.select {|_child, parent| parent == id }.each_key {|child| dispose_frame_realm_tree(child) }
|
|
466
|
+
dispose_frame_realm(id)
|
|
467
|
+
end
|
|
468
|
+
|
|
447
469
|
# Navigate a window realm (`win.location = …`): a FRESH realm for the new
|
|
448
470
|
# document, like reload_frame_realm — an in-place `__csimLoadDocument` on an
|
|
449
471
|
# already-loaded realm does NOT re-run inline scripts, but a fresh realm does.
|
|
@@ -452,8 +474,14 @@ module Capybara
|
|
|
452
474
|
# the window after navigating it; a stable WindowProxy is future work.)
|
|
453
475
|
def reload_window_realm(old_id, url, body, content_type)
|
|
454
476
|
meta = window_realm_meta[old_id] || {}
|
|
455
|
-
|
|
456
|
-
|
|
477
|
+
# Like reload_frame_realm, a re-navigated window discards its child browsing contexts — dispose
|
|
478
|
+
# the descendant frame realms too, not just old_id, so a popup's iframes don't linger.
|
|
479
|
+
dispose_frame_realm_tree(old_id)
|
|
480
|
+
create_window_realm(
|
|
481
|
+
url, body, content_type,
|
|
482
|
+
opener_id: meta[:opener_id], window_name: meta[:window_name],
|
|
483
|
+
about_base: meta[:about_base], about_origin: meta[:about_origin]
|
|
484
|
+
)
|
|
457
485
|
end
|
|
458
486
|
|
|
459
487
|
# Tear down a single frame realm (e.g. a descendant frame destroyed when an
|
|
@@ -463,6 +491,13 @@ module Capybara
|
|
|
463
491
|
# The frame's browsing context is going away — revoke the blob URLs it
|
|
464
492
|
# created (url-lifetime "Removing an iframe").
|
|
465
493
|
@browser.revoke_realm_blobs(id) rescue nil
|
|
494
|
+
# Drop it from the SW Client registry so matchAll stops returning a dead client.
|
|
495
|
+
@browser.sw_unregister_client(id) rescue nil
|
|
496
|
+
# A dedicated worker is owned by the context that created it — discarding the context
|
|
497
|
+
# terminates it (and unregisters its own client record).
|
|
498
|
+
@browser.terminate_realm_workers(id) rescue nil
|
|
499
|
+
# If it held the focus chain, focus returns to the top-level browsing context.
|
|
500
|
+
@browser.note_realm_discarded(id) rescue nil
|
|
466
501
|
@realm_module_handles&.delete(id)
|
|
467
502
|
@frame_realm_depths&.delete(id)
|
|
468
503
|
@frame_realm_parents&.delete(id)
|
|
@@ -733,8 +768,8 @@ module Capybara
|
|
|
733
768
|
# id (or nil on failure — then the bridge keeps its same-realm fallback).
|
|
734
769
|
# The bridge maps `iframe.contentWindow` to `RustyRacer.contextGlobal(id)`.
|
|
735
770
|
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) }
|
|
771
|
+
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, frame_viewport = nil) {
|
|
772
|
+
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, frame_viewport) }
|
|
738
773
|
})
|
|
739
774
|
# Re-navigating an iframe (src/srcdoc reassigned) builds a fresh realm;
|
|
740
775
|
# the bridge calls this to tear down the superseded one so it doesn't
|
|
@@ -760,6 +795,15 @@ module Capybara
|
|
|
760
795
|
|
|
761
796
|
def frame_realm_depths = (@frame_realm_depths ||= {})
|
|
762
797
|
|
|
798
|
+
# A frame whose document URL is opaque — about:blank (empty src), about:srcdoc,
|
|
799
|
+
# or empty — has no scope of its own; its controller is inherited from the
|
|
800
|
+
# creator. (create_frame_realm receives 'about:blank'/'about:srcdoc' as the url
|
|
801
|
+
# for these, seeded by __csimFrameWindow.)
|
|
802
|
+
def opaque_frame_url?(url)
|
|
803
|
+
u = url.to_s
|
|
804
|
+
u.empty? || u.start_with?('about:')
|
|
805
|
+
end
|
|
806
|
+
|
|
763
807
|
# Bring a freshly-created realm context up to a runnable bridge, shared by the
|
|
764
808
|
# frame and window realm constructors. Re-evaling the snapshot source would
|
|
765
809
|
# redefine snapshot globals (e.g. the `scrollX` accessor) and throw, so only
|
|
@@ -777,7 +821,7 @@ module Capybara
|
|
|
777
821
|
realm
|
|
778
822
|
end
|
|
779
823
|
|
|
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)
|
|
824
|
+
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, frame_viewport = nil)
|
|
781
825
|
depth = (frame_realm_depths[parent_id] || 0) + 1
|
|
782
826
|
if depth > MAX_FRAME_DEPTH
|
|
783
827
|
@browser.log_console('warn', "iframe nesting depth #{depth} exceeds #{MAX_FRAME_DEPTH}; not building #{url}")
|
|
@@ -829,6 +873,11 @@ module Capybara
|
|
|
829
873
|
# loads, so a frame whose load handler reads window.name to identify itself
|
|
830
874
|
# (declarative-shadow declarative-child-frame) sees it.
|
|
831
875
|
realm.call('__csimSetWindowName', frame_name.to_s) unless frame_name.nil?
|
|
876
|
+
# The frame's viewport is its container's content box, measured by the parent realm. Seed it
|
|
877
|
+
# BEFORE the document loads so load-time `innerWidth` reads and `@media` evaluation see the
|
|
878
|
+
# frame's own size rather than the top window's. `nil` = an unrendered container, which is a
|
|
879
|
+
# 0x0 window — pass it through rather than skipping, or the frame keeps the top-level size.
|
|
880
|
+
realm.call('__csimFrameViewportChanged', frame_viewport)
|
|
832
881
|
# Seed the frame's document origin (opaque/inherited) BEFORE the document
|
|
833
882
|
# loads, so its load-time scripts read the right self.origin. nil → a
|
|
834
883
|
# real-URL frame whose origin is its own location origin.
|
|
@@ -836,7 +885,47 @@ module Capybara
|
|
|
836
885
|
# The frame's location.origin (opaque "null" for about:blank / srcdoc /
|
|
837
886
|
# javascript:); decoupled from the location string so navigation is intact.
|
|
838
887
|
realm.call('__csimSetLocationOrigin', frame_location_origin.to_s) unless frame_location_origin.nil?
|
|
888
|
+
# An about:blank / about:srcdoc frame's URL is opaque, but its base URL (for
|
|
889
|
+
# relative-URL resolution) is INHERITED from the creator. Seed that inherited
|
|
890
|
+
# base BEFORE the document loads so its load-time scripts resolve relative URLs
|
|
891
|
+
# against the parent, even though location.href reports about:blank/srcdoc.
|
|
892
|
+
realm.call('__csimSetAboutBaseURL', frame_about_base.to_s) unless frame_about_base.to_s.empty?
|
|
893
|
+
# If a service worker's scope covers this frame's URL, the frame is a CONTROLLED client:
|
|
894
|
+
# wire its `navigator.serviceWorker.controller` BEFORE its document loads, so the frame's
|
|
895
|
+
# load-time scripts see the controller and route their subresource fetch() / XHR / EventSource
|
|
896
|
+
# through the SW's fetch event (fetch-event-network-error's controllee fires XHRs during load).
|
|
897
|
+
# The frame never called register(), so its per-realm registration Map is empty — set the
|
|
898
|
+
# controller directly from Ruby's scope→handle mirror. An OPAQUE child (about:blank / srcdoc)
|
|
899
|
+
# has no scope of its own, so it INHERITS its parent frame's controller (HTML: an about:blank
|
|
900
|
+
# document is controlled by its creator). Inheritance chains through controlled FRAME realms
|
|
901
|
+
# (sw_note_realm_controller); recording it BEFORE the load also lets a NESTED frame built during
|
|
902
|
+
# this frame's load inherit the controller.
|
|
903
|
+
opaque = opaque_frame_url?(url)
|
|
904
|
+
# A sandboxed frame WITHOUT allow-same-origin has an OPAQUE origin (doc origin 'null'), which
|
|
905
|
+
# is cross-origin to everything — so it is NOT CONTROLLED AT ALL. It inherits no controller
|
|
906
|
+
# from its creator (srcdoc-iframe: "sandboxed srcdoc should not inherit"), its subresource
|
|
907
|
+
# fetches never reach the SW, and it is absent from `clients.matchAll()`. Excluding it from
|
|
908
|
+
# only the client registry would not hold: a controlled frame's first subresource fetch
|
|
909
|
+
# re-registers it lazily from the fetch event (js/src/workers.js clientFor). The SW does still
|
|
910
|
+
# answer its NAVIGATION request, because a CSP-header sandbox is knowable only FROM that
|
|
911
|
+
# response (sandboxed-iframe-fetch-event, "Service worker should NOT control the sandboxed
|
|
912
|
+
# page"). allow-same-origin restores the parent origin, and a real-URL frame carries no
|
|
913
|
+
# explicit origin at all (nil = its own location origin) — both are controlled normally.
|
|
914
|
+
if frame_doc_origin.to_s != 'null'
|
|
915
|
+
ctrl = @browser.sw_client_controller_for(url.to_s)
|
|
916
|
+
ctrl ||= @browser.sw_inherited_controller_for(parent_id) if opaque
|
|
917
|
+
if ctrl
|
|
918
|
+
# Wiring the controller is what registers this frame as a client — the realm reports
|
|
919
|
+
# itself from installController (js/src/sw-client.js), before the document below loads.
|
|
920
|
+
realm.call('__csim_swSetControllerDirect', *ctrl)
|
|
921
|
+
@browser.sw_note_realm_controller(realm.id, ctrl)
|
|
922
|
+
end
|
|
923
|
+
end
|
|
839
924
|
realm.call('__csimLoadDocument', body.to_s, content_type.to_s)
|
|
925
|
+
# This document now has a URL and any host-wired controller, so it can announce itself
|
|
926
|
+
# as a service-worker client — an UNCONTROLLED context is still a client of its origin
|
|
927
|
+
# and must show up in `matchAll({includeUncontrolled: true})`.
|
|
928
|
+
realm.call('__csim_swReportClient') rescue nil
|
|
840
929
|
# A `javascript:` URL frame: the initial empty document is now loaded and
|
|
841
930
|
# parent/top are wired, so evaluate the URL's script in the realm (global
|
|
842
931
|
# scope). Per HTML, only a STRING result navigates the frame to a new
|
|
@@ -885,7 +974,7 @@ module Capybara
|
|
|
885
974
|
# a native `__csimFrameWindowProxyFor`, so `popup.document` is a real
|
|
886
975
|
# same-isolate Document (cross-window adoptNode works). The single isolate also
|
|
887
976
|
# 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)
|
|
977
|
+
def create_window_realm(url, body, content_type, opener_id: nil, window_name: nil, doc_origin: nil, location_origin: nil, about_base: nil, about_origin: nil)
|
|
889
978
|
realm = seed_realm_bridge(ctx.create_context)
|
|
890
979
|
# Mark it a top-level window realm so its location setter routes to
|
|
891
980
|
# __csimWindowRealmNavigate (reload THIS realm) rather than the frame-nav or
|
|
@@ -917,7 +1006,21 @@ module Capybara
|
|
|
917
1006
|
}
|
|
918
1007
|
JS
|
|
919
1008
|
end
|
|
920
|
-
|
|
1009
|
+
# `window.open()` with no URL opens about:blank — that IS the new document's URL, and a
|
|
1010
|
+
# realm built from the snapshot would otherwise keep reporting the OPENER's (making the
|
|
1011
|
+
# popup a phantom duplicate of its opener everywhere a document URL is read, the service-
|
|
1012
|
+
# worker client set included). Its ORIGIN and its BASE url are inherited from the opener,
|
|
1013
|
+
# exactly as an empty <iframe>'s are (create_frame_realm's frame_about_base): the origin
|
|
1014
|
+
# so the popup isn't cross-origin to the window that opened it, the base so relative
|
|
1015
|
+
# resolution inside it still targets the opener's document.
|
|
1016
|
+
if url.to_s.empty?
|
|
1017
|
+
realm.call('__csimUpdateLocation', 'about:blank')
|
|
1018
|
+
realm.call('__csimSetAboutBaseURL', about_base.to_s) unless about_base.to_s.empty?
|
|
1019
|
+
doc_origin ||= about_origin unless about_origin.to_s.empty?
|
|
1020
|
+
location_origin ||= about_origin unless about_origin.to_s.empty?
|
|
1021
|
+
else
|
|
1022
|
+
realm.call('__csimUpdateLocation', url.to_s)
|
|
1023
|
+
end
|
|
921
1024
|
realm.call('__csimSetWindowName', window_name.to_s) unless window_name.nil?
|
|
922
1025
|
realm.call('__csimSetDocumentOrigin', doc_origin.to_s) unless doc_origin.nil?
|
|
923
1026
|
realm.call('__csimSetLocationOrigin', location_origin.to_s) unless location_origin.nil?
|
|
@@ -936,8 +1039,10 @@ module Capybara
|
|
|
936
1039
|
# Remember the window's opener / name so a self-navigation (reload_window_realm
|
|
937
1040
|
# builds a FRESH realm) can carry them across — a real popup keeps window.opener
|
|
938
1041
|
# and window.name through its own navigation.
|
|
939
|
-
window_realm_meta[realm.id] = {opener_id: opener_id, window_name: window_name}
|
|
1042
|
+
window_realm_meta[realm.id] = {opener_id: opener_id, window_name: window_name, about_base: about_base, about_origin: about_origin}
|
|
940
1043
|
realm.call('__csimLoadDocument', body.to_s, content_type.to_s)
|
|
1044
|
+
# As in create_frame_realm: an auxiliary window is a client of its origin too.
|
|
1045
|
+
realm.call('__csim_swReportClient') rescue nil
|
|
941
1046
|
realm.call('__csimFireWindowLoad') rescue nil
|
|
942
1047
|
realm.id
|
|
943
1048
|
rescue StandardError => e
|
|
@@ -1308,10 +1413,29 @@ module Capybara
|
|
|
1308
1413
|
# the per-worker postMessage host fn closed over `post_back`.
|
|
1309
1414
|
# Returns a uniform `WorkerRuntime` adapter that
|
|
1310
1415
|
# `Browser#run_worker` drives.
|
|
1311
|
-
def self.build_worker(browser, post_back)
|
|
1416
|
+
def self.build_worker(browser, post_back, broadcast_out = nil, sw_hooks = {})
|
|
1312
1417
|
c = Ctx.new(snapshot: snapshot)
|
|
1313
1418
|
attach_host_fns(c, browser)
|
|
1314
1419
|
c.attach('__csim_workerPostMessage', ->(data) { post_back.call(data); nil })
|
|
1420
|
+
# Service-worker → main-thread signals route through the thread-safe outbox (delivered by
|
|
1421
|
+
# deliver_worker_messages): client.postMessage, clients.claim (set the client's controller),
|
|
1422
|
+
# and a controlled fetch's respondWith result. See run_worker for the closures.
|
|
1423
|
+
c.attach('__csim_swPostToClient', ->(client_id, data) { sw_hooks[:post_to_client]&.call(client_id, data); nil }) if sw_hooks[:post_to_client]
|
|
1424
|
+
c.attach('__csim_swFocusClient', ->(client_id) { sw_hooks[:focus_client]&.call(client_id); nil }) if sw_hooks[:focus_client]
|
|
1425
|
+
c.attach('__csim_swNavigateClient', ->(client_id, url, nav_id) { sw_hooks[:navigate_client]&.call(client_id, url, nav_id); nil }) if sw_hooks[:navigate_client]
|
|
1426
|
+
c.attach('__csim_swClaim', -> { sw_hooks[:claim]&.call; nil }) if sw_hooks[:claim]
|
|
1427
|
+
c.attach('__csim_swSkipWaitingRequest', -> { sw_hooks[:skip_waiting]&.call; nil }) if sw_hooks[:skip_waiting]
|
|
1428
|
+
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]
|
|
1429
|
+
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]
|
|
1430
|
+
# Cross-isolate MessagePort channel signals (a worker/SW port endpoint + its outbound messages).
|
|
1431
|
+
c.attach('__csim_workerPortPost', ->(channel, data) { sw_hooks[:port_post]&.call(channel, data); nil }) if sw_hooks[:port_post]
|
|
1432
|
+
c.attach('__csim_workerPortEndpoint', ->(channel) { sw_hooks[:port_endpoint]&.call(channel); nil }) if sw_hooks[:port_endpoint]
|
|
1433
|
+
# A worker is a SEPARATE isolate: its BroadcastChannel fan-out to the main + frame realms +
|
|
1434
|
+
# other workers can't run `browser.broadcast_to_windows` inline (that would mutate the main
|
|
1435
|
+
# browser's inbox from the worker thread). Route it through the thread-safe outbox instead
|
|
1436
|
+
# (the shared BROWSER_HOST_FNS `__csimBroadcast` above, bound to `browser`, is overridden
|
|
1437
|
+
# here). Same-worker-context delivery already happened in-VM via `_bcChannels`.
|
|
1438
|
+
c.attach('__csimBroadcast', ->(name, data, _rid, origin) { broadcast_out&.call(name, data, origin); nil }) if broadcast_out
|
|
1315
1439
|
# Worker's timer table is independent from main's; routing the
|
|
1316
1440
|
# worker's `setTimersActive` through `browser.timers_active=`
|
|
1317
1441
|
# 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.9.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: []
|