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.
@@ -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
- drivers.each {|d| yield d if d.owner_thread == thread }
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
- def window_size(_) = [current_browser.viewport_width, current_browser.viewport_height]
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
- if h == PRIMARY_HANDLE
618
- @active_handle = nil
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(_, w, h) = current_browser.set_viewport(w, h)
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
- def maximize_window(_) ; nil ; end
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))