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.
@@ -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
- dispose_frame_realm(old_id)
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
- dispose_frame_realm(old_id)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Capybara
4
4
  module Simulated
5
- VERSION = '0.7.0'
5
+ VERSION = '0.8.0'
6
6
  end
7
7
  end
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.7.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.10
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: []