Class: Capybara::Simulated::Driver

Inherits:
Driver::Base
  • Object
show all
Defined in:
lib/capybara/simulated/driver.rb

Defined Under Namespace

Classes: FakePlaywrightLocator, FakePlaywrightPage

Constant Summary collapse

PRIMARY_HANDLE =

Per-window Browser/VM. open_aux_window creates a fresh Browser sharing the Driver's cookie + localStorage jars (origin-shared in real browsers) and visits the target URL; switch_to_window flips @active_handle so subsequent driver ops route through current_browser. sessionStorage + DOM + history + the JS VM stay per-window.

'csim-window-0'
@@live_lock =
Mutex.new
@@live =

[WeakRef] — dead refs filtered on read.

[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, js_engine: nil, viewport: nil, user_agent: nil) ⇒ Driver

viewport: [w, h] and user_agent: (typically supplied via Capybara.register_driver) force the JS-side innerWidth/innerHeight and navigator.userAgent (plus HTTP_USER_AGENT on Rack requests) before the first navigate, so matchMedia / mobile-breakpoint branches and server-side UA-based mobile detection both resolve before any document loads. The Browser tracks both as "defaults" so reset! (per-test teardown) restores them between specs.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/capybara/simulated/driver.rb', line 66

def initialize(app, js_engine: nil, viewport: nil, user_agent: nil)
  @app             = app
  @js_engine       = js_engine
  # Cookies + localStorage are origin-shared across windows
  # (real browser semantics), so we own the jars at the Driver
  # level and inject them into every per-window Browser. Each
  # Browser still has its own sessionStorage + DOM + JS VM.
  @cookies         = {}
  @local_storage   = {}
  # Capture the universal-server flag ONCE, at session construction — the WPT
  # runner sets CSIM_LOCAL_ALL_HOSTS only while building the session, then
  # restores it. Every window (incl. aux windows opened later) inherits this so
  # cross-origin iframes eager-build consistently across the whole session.
  @all_hosts_local = ENV['CSIM_LOCAL_ALL_HOSTS'] == '1'
  @browser         = build_window_browser
  @browser.window_handle = PRIMARY_HANDLE
  @aux_windows     = []  # [{handle:, browser:, name:, opener:}, …]
  @active_handle   = nil
  @next_window_seq = 0
  # Driver-level blob URL partition map: url => {browser:, site:}. A blob URL's
  # storage partition is its creating context's top-level SITE; another window
  # can resolve the blob only from the same partition (and same origin, which
  # the blob: URL embeds). Bytes aren't copied here — they're read back from the
  # creating Browser's own store, so this stays a light reference map.
  @blob_partitions      = {}
  @blob_partitions_lock = Mutex.new
  @owner_thread    = Thread.current
  @@live_lock.synchronize { @@live << WeakRef.new(self) }
  @browser.default_viewport   = viewport   if viewport
  @browser.default_user_agent = user_agent if user_agent
end

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



45
46
47
# File 'lib/capybara/simulated/driver.rb', line 45

def app
  @app
end

#browserObject (readonly)

Returns the value of attribute browser.



122
123
124
# File 'lib/capybara/simulated/driver.rb', line 122

def browser
  @browser
end

#owner_threadObject (readonly)

Returns the value of attribute owner_thread.



45
46
47
# File 'lib/capybara/simulated/driver.rb', line 45

def owner_thread
  @owner_thread
end

Class Method Details

.each_live_on_thread(thread) ⇒ Object



50
51
52
53
54
55
56
# File 'lib/capybara/simulated/driver.rb', line 50

def self.each_live_on_thread(thread)
  drivers = @@live_lock.synchronize {
    @@live.select!(&:weakref_alive?)
    @@live.filter_map {|ref| ref.__getobj__ rescue nil }
  }
  drivers.each {|d| yield d if d.owner_thread == thread }
end

Instance Method Details

#accept_modal(type, **options, &block) ⇒ Object



697
# File 'lib/capybara/simulated/driver.rb', line 697

def accept_modal(type, **options, &block) = run_modal(type, accept: true, **options, &block)

#active_elementObject



669
670
671
672
# File 'lib/capybara/simulated/driver.rb', line 669

def active_element
  handle = current_browser.active_element_handle
  handle ? Node.new(self, handle) : nil
end

#blob_bytes_for(url, accessor) ⇒ Object

Resolve a blob: URL's bytes from whichever Browser created it (the bytes live in the creator's isolate, not necessarily the navigator's). Used to load a blob document into a TOP-LEVEL window (window.open / a window navigation): that new context is the blob's own partition, so no partition gate applies here — the cross-partition rule for windows is the noopener severing above, and for nested frames it is enforced at the frame-navigation site. Falls back to the accessor's own store for an unpartitioned URL (worker / legacy path).



451
452
453
454
455
# File 'lib/capybara/simulated/driver.rb', line 451

def blob_bytes_for(url, accessor)
  entry = @blob_partitions_lock.synchronize { @blob_partitions[url.to_s] }
  creator = entry ? entry[:browser] : accessor
  creator.respond_to?(:read_blob_for_window) ? creator.read_blob_for_window(url) : nil
end

#blob_partition_site_of(url) ⇒ Object

The storage-partition site a blob: URL was created in (its creator's top-level site), or nil for an unknown / revoked / unpartitioned URL.



431
432
433
434
# File 'lib/capybara/simulated/driver.rb', line 431

def blob_partition_site_of(url)
  e = @blob_partitions_lock.synchronize { @blob_partitions[url.to_s] }
  e && e[:site]
end

#broadcast_channel(source_browser, name, data) ⇒ Object

BroadcastChannel.postMessage — deliver to every OTHER window's channels with the same name (same-window delivery is handled in-VM by the sender).



509
510
511
512
513
514
# File 'lib/capybara/simulated/driver.rb', line 509

def broadcast_channel(source_browser, name, data)
  window_entries.each do |w|
    next if w[:browser].equal?(source_browser)
    w[:browser].enqueue_broadcast(name, data)
  end
end

#close_window(h) ⇒ Object



597
598
599
600
601
602
603
604
605
606
# File 'lib/capybara/simulated/driver.rb', line 597

def close_window(h)
  return if h == PRIMARY_HANDLE
  @aux_windows.reject! {|w|
    next false unless w[:handle] == h
    drop_blob_partitions_for(w[:browser])   # don't leave entries pointing at a disposed VM
    w[:browser].dispose rescue nil
    true
  }
  @active_handle = nil if @active_handle == h
end

#cross_partition_blob?(url, accessor) ⇒ Boolean

Is this blob: URL in a different storage partition than accessor's top-level site? Unknown blobs (no entry) are treated as same-partition (no extra gating beyond the existing same-origin behaviour).

Returns:

  • (Boolean)


439
440
441
442
# File 'lib/capybara/simulated/driver.rb', line 439

def cross_partition_blob?(url, accessor)
  site = blob_partition_site_of(url)
  !site.nil? && site != accessor.blob_partition_site
end

#current_browserObject

Active window's Browser. Primary by default; switches when the test calls switch_to_window(aux_handle). Every DOM / URL / JS-touching driver method routes through here so per-window state (DOM, sessionStorage, history) stays window-scoped.



128
129
130
131
132
# File 'lib/capybara/simulated/driver.rb', line 128

def current_browser
  return @browser unless @active_handle
  w = @aux_windows.find {|win| win[:handle] == @active_handle }
  w ? w[:browser] : @browser
end

#current_traceObject



120
# File 'lib/capybara/simulated/driver.rb', line 120

def current_trace = browser.trace || browser.pending_trace

#current_urlObject



298
# File 'lib/capybara/simulated/driver.rb', line 298

def current_url          = current_browser.current_url || ''

#current_window_handleObject



329
# File 'lib/capybara/simulated/driver.rb', line 329

def current_window_handle    = @active_handle || PRIMARY_HANDLE

#dismiss_modal(type, **options, &block) ⇒ Object



698
# File 'lib/capybara/simulated/driver.rb', line 698

def dismiss_modal(type, **options, &block) = run_modal(type, accept: false, **options, &block)

#evaluate_async_script(script, *args) ⇒ Object



646
647
648
# File 'lib/capybara/simulated/driver.rb', line 646

def evaluate_async_script(script, *args)
  unwrap(current_browser.evaluate_async_script(script, args))
end

#evaluate_script(script, *args) ⇒ Object



631
632
633
# File 'lib/capybara/simulated/driver.rb', line 631

def evaluate_script(script, *args)
  unwrap(current_browser.evaluate_script(script, args))
end

#execute_script(script, *args) ⇒ Object

Capybara's execute_script contract is "run it, discard the return". Route through a no-return JS path so a script that returns a non-marshallable value (jQuery $('…').text('…') returns a chainable jQuery object that the engine's value filter recurses into until it stack-overflows) doesn't blow up on the way back.



641
642
643
644
# File 'lib/capybara/simulated/driver.rb', line 641

def execute_script(script, *args)
  current_browser.execute_script(script, args)
  nil
end

#find_css(query, **_) ⇒ Object



309
310
311
# File 'lib/capybara/simulated/driver.rb', line 309

def find_css(query, **_)
  current_browser.find_css(query).map {|id| Node.new(self, id) }
end

#find_xpath(query, **_) ⇒ Object



305
306
307
# File 'lib/capybara/simulated/driver.rb', line 305

def find_xpath(query, **_)
  current_browser.find_xpath(query).map {|id| Node.new(self, id) }
end

#fire_aux_window_load(handle) ⇒ Object

Cross-window remote-ref RPC: route a node/object proxy op to the window that owns the ref (handle), executing in that window's VM.



525
# File 'lib/capybara/simulated/driver.rb', line 525

def fire_aux_window_load(handle)             = ((b = window_browser(handle)) && b.fire_own_window_load)

#go_backObject



295
# File 'lib/capybara/simulated/driver.rb', line 295

def go_back              = current_browser.go_back

#go_forwardObject



296
# File 'lib/capybara/simulated/driver.rb', line 296

def go_forward           = current_browser.go_forward

#header(name, value) ⇒ Object



303
# File 'lib/capybara/simulated/driver.rb', line 303

def header(name, value)  = current_browser.set_header(name, value)

#htmlObject



299
# File 'lib/capybara/simulated/driver.rb', line 299

def html                 = current_browser.html

#invalid_element_errorsObject



661
# File 'lib/capybara/simulated/driver.rb', line 661

def invalid_element_errors = [Capybara::Simulated::StaleElement]

#javascript_enabled?Boolean

Returns:

  • (Boolean)


135
# File 'lib/capybara/simulated/driver.rb', line 135

def javascript_enabled? = true

#maximize_window(_) ⇒ Object



629
# File 'lib/capybara/simulated/driver.rb', line 629

def maximize_window(_)       ; nil ; end

#needs_server?Boolean

Returns:

  • (Boolean)


134
# File 'lib/capybara/simulated/driver.rb', line 134

def needs_server?       = false

#no_such_window_errorObject



662
# File 'lib/capybara/simulated/driver.rb', line 662

def no_such_window_error   = Capybara::WindowError

#open_aux_window(url = nil, name: nil, opener_handle: nil, source: nil, blob_snapshot: nil, post: nil, opener: false, referrer: nil) ⇒ Object

Open (or, by name, reuse) an auxiliary window. target="_blank" clicks and window.open both land here. A non-empty name that matches an existing window navigates that window instead of opening a new one (HTML window-name targeting); opener_handle records the opener so the new window's window.opener resolves back to it.



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/capybara/simulated/driver.rb', line 349

def open_aux_window(url = nil, name: nil, opener_handle: nil, source: nil, blob_snapshot: nil, post: nil, opener: false, referrer: nil)
  name = name.to_s
  # A blob: URL opened from a different storage partition is forced noopener
  # (cross-partition-navigation), overriding an explicit rel=opener — the new
  # top-level window is the blob's own partition (so the blob still loads), but
  # the opener relationship is severed.
  opener = false if opener && url.to_s.start_with?('blob:') && source && cross_partition_blob?(url, source)
  # A `<form target>` keeps its opener by default (unlike a `target=_blank`
  # LINK, which is noopener) — resolve the opener handle from the source.
  opener_handle ||= handle_for(source) if opener && source
  if !name.empty? && (existing = @aux_windows.find {|w| w[:name] == name })
    if post
      existing[:browser].navigate_post(url, post[:body], post[:content_type], referer: referrer)
    else
      navigate_window(existing[:browser], url, source: source)
    end
    return existing[:handle]
  end
  @next_window_seq += 1
  handle = "csim-window-#{@next_window_seq}"
  aux = build_window_browser
  aux.window_handle = handle
  # Register BEFORE visiting: the opened document's own boot scripts read
  # `window.opener`, which resolves through this entry — so the entry
  # (with its opener) must exist before `visit` runs those scripts.
  @aux_windows << {handle: handle, browser: aux, name: name, opener: opener_handle}
  if url && !url.empty?
    if post
      # A `<form target=_blank method=post>` loads the new window via POST,
      # carrying the opener's URL as referrer (unless rel=noreferrer → '').
      aux.navigate_post(url, post[:body], post[:content_type], referer: referrer)
    # A blob: URL isn't rack-navigable and its bytes live in the OPENER's
    # isolate — load the document directly from a click-time snapshot (a
    # deferred target=_blank nav may revoke the URL first) or, failing that,
    # the opener's blob store.
    elsif !(url.to_s.start_with?('blob:') && load_blob_into_window(aux, url, source, snapshot: blob_snapshot))
      # A form submission carries a referrer (the opener's URL) unless the
      # form opted out via rel=noreferrer (referrer: '').
      aux.visit(url, referer: referrer)
    end
  end
  handle
rescue StandardError => e
  # Aux window URL-load failure (binary content, network error, …)
  # shouldn't tear down the test — the handle is already recorded so
  # `window_opened_by` succeeds; within_window assertions on
  # `current_url` may still pass through whatever `visit` managed to set
  # before raising.
  warn "[csim] open_aux_window(#{url.inspect}) raised: #{e.class}: #{e.message[0, 200]}"
  handle
end

#open_new_window(_kind = :tab) ⇒ Object

Capybara Session#open_new_window(:tab) entry point — opens at about:blank (so current_url/title match a real new tab) and the test then switch_to_window + visits the real URL. We don't distinguish :tab from :window (no window-chrome semantics here).



593
594
595
# File 'lib/capybara/simulated/driver.rb', line 593

def open_new_window(_kind = :tab)
  open_aux_window('about:blank')
end

#open_window_from_js(opener_browser, url, name, opener_realm_id = 0) ⇒ Object

window.open(url, name) from the opener window's JS. Resolves the URL against the opener's document and records the opener relationship.



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/capybara/simulated/driver.rb', line 407

def open_window_from_js(opener_browser, url, name, opener_realm_id = 0)
  resolved = url.to_s.empty? ? nil : opener_browser.resolve_document_url(url)
  # Opening a blob: URL whose storage partition differs from the opener's
  # top-level site is forced NOOPENER (cross-partition-navigation): the new
  # auxiliary window is its own top-level context in the blob's partition, so the
  # blob still loads — but there is no opener relationship and `window.open`
  # returns null. Same-partition keeps the normal opener.
  if resolved.to_s.start_with?('blob:') && cross_partition_blob?(resolved, opener_browser)
    open_aux_window(resolved, name: name, source: opener_browser)   # no opener_handle ⇒ window.opener null
    return nil                                                      # window.open(...) === null
  end
  # Same-origin window → a realm in the opener's isolate (shared heap); the
  # returned realm-id context becomes a native WindowProxy on the JS side, so
  # cross-window scripting/adoption need no cross-isolate RPC. The opener's realm
  # id wires the popup's window.opener. Falls through to the separate-VM aux path
  # (cross-origin, or a URL we don't yet realm-load).
  if (rid = opener_browser.open_window_realm(resolved, name: name, opener_realm_id: opener_realm_id))
    return rid
  end
  open_aux_window(resolved, name: name, opener_handle: handle_for(opener_browser), source: opener_browser)
end

#opener_handle_of(browser) ⇒ Object



561
562
563
564
# File 'lib/capybara/simulated/driver.rb', line 561

def opener_handle_of(browser)
  handle = handle_for(browser)
  window_entries.find {|w| w[:handle] == handle }&.fetch(:opener)
end

#peek_script(expr) ⇒ Object

Clock-free read of a JS expression in the active browsing context (no virtual-time advance, unlike evaluate_script) — for polling page state between event-loop frames without perturbing the clock.



272
# File 'lib/capybara/simulated/driver.rb', line 272

def peek_script(expr) = current_browser.peek_script(expr)

#refreshObject



275
# File 'lib/capybara/simulated/driver.rb', line 275

def refresh              = current_browser.refresh

#register_blob_partition(url, browser, site) ⇒ Object

Record / drop a blob URL's storage partition (called by Browser#blob_register / #blob_unregister). site is the creating context's top-level site.



459
460
461
# File 'lib/capybara/simulated/driver.rb', line 459

def register_blob_partition(url, browser, site)
  @blob_partitions_lock.synchronize { @blob_partitions[url.to_s] = {browser: browser, site: site.to_s} }
end

#reset!Object



276
277
278
279
# File 'lib/capybara/simulated/driver.rb', line 276

def reset!
  reset_windows!
  browser.reset!
end

#reset_history!Object



297
# File 'lib/capybara/simulated/driver.rb', line 297

def reset_history!       = current_browser.reset_history!

#reset_windows!Object

Dispose every auxiliary window and return focus to the primary — a fresh browsing context has no sibling windows. Disposing each aux Browser tears down its worker / SSE / websocket threads and its V8 isolate eagerly; left alone they pile into V8Runtime's process-wide @@live set and are only reclaimed by the at_exit hook, which on a long-lived multi-file session (the WPT runner) means a slow — sometimes minutes-long — process exit. Split out of reset! so a caller can drop windows WITHOUT resetting the primary's page state (the WPT runner rebuilds the primary itself, per file, via visit).



289
290
291
292
293
294
# File 'lib/capybara/simulated/driver.rb', line 289

def reset_windows!
  @aux_windows.each {|w| w[:browser].dispose rescue nil }
  @aux_windows.clear
  @active_handle = nil
  @blob_partitions_lock.synchronize { @blob_partitions.clear }
end

#resize(w, h) ⇒ Object

Forem's ahoy-tracking spec calls driver.resize(w, h) directly rather than through current_window.resize_to.



628
# File 'lib/capybara/simulated/driver.rb', line 628

def resize(w, h) = current_browser.set_viewport(w, h)

#resize_window_to(_, w, h) ⇒ Object



625
626
627
# File 'lib/capybara/simulated/driver.rb', line 625

def resize_window_to(_, w, h) = current_browser.set_viewport(w, h)
# Forem's ahoy-tracking spec calls `driver.resize(w, h)` directly
# rather than through `current_window.resize_to`.

#response_headersObject



302
# File 'lib/capybara/simulated/driver.rb', line 302

def response_headers     = current_browser.response_headers

#revoke_blob_partitioned(url, source) ⇒ Object

A user URL.revokeObjectURL(url) from source. Storage-partitioned: a revoke from a different top-level site than the blob's is a NO-OP (cross-partition.https "shouldn't be revocable from a cross-partition iframe/worker"). A same-partition revoke drops the Driver entry AND invalidates the blob in the CREATOR's isolate (the blob may have been created in another window), so every window stops resolving it. Returns false when vetoed (caller leaves its local copy intact).



473
474
475
476
477
478
479
480
# File 'lib/capybara/simulated/driver.rb', line 473

def revoke_blob_partitioned(url, source)
  entry = @blob_partitions_lock.synchronize { @blob_partitions[url.to_s] }
  return false if entry && entry[:site] != source.blob_partition_site
  @blob_partitions_lock.synchronize { @blob_partitions.delete(url.to_s) }
  creator = entry && entry[:browser]
  creator.drop_local_blob(url.to_s) if creator && creator.respond_to?(:drop_local_blob) && !creator.equal?(source)
  true
end

#run_event_loop_frame(frame_ms) ⇒ Object

Run one real-cadence event-loop frame and return the loop's observable state. Drives "advance the page one frame" without the full poll tick evaluate_script would incur per read; the wpt_runner uses it to drain a page to completion at browser cadence.

EVERY live window steps, not just the active one: an auxiliary window is a separate VM, but the cross-context orchestration the dispatcher framework builds on (a popup running an executor that polls a shared queue while the opener waits) needs those background windows to make progress autonomously. The active window's state leads; each aux window folds its progress / raf / async / nearest-timer in so the caller keeps pumping while any window works.



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/capybara/simulated/driver.rb', line 230

def run_event_loop_frame(frame_ms)
  state = current_browser.run_event_loop_frame(frame_ms)
  # Iterate a SNAPSHOT: a window's drain can open or close windows mid-loop (a
  # popup spawning another, or window.close disposing one). Re-check each window
  # is still open before stepping it, and isolate a per-window failure so one
  # dead/half-torn-down VM can't abort the whole frame pump.
  @aux_windows.dup.each do |w|
    b = w[:browser]
    next if b.equal?(current_browser)
    next unless @aux_windows.include?(w)   # closed earlier this loop → skip
    begin
      state = merge_frame_state(state, b.run_event_loop_frame(frame_ms))
    rescue StandardError
      next
    end
  end
  state
end

#save_screenshot(path, **_opts) ⇒ Object



664
665
666
667
# File 'lib/capybara/simulated/driver.rb', line 664

def save_screenshot(path, **_opts)
  File.write(path, current_browser.html.to_s)
  path
end

#send_keys(*keys) ⇒ Object



683
684
685
686
687
688
689
690
691
692
693
694
695
# File 'lib/capybara/simulated/driver.rb', line 683

def send_keys(*keys)
  # Selenium contract: top-level modifier symbols (`send_keys(
  # :shift, :enter)`) press the modifier *and hold it* over the
  # following key, releasing at the end of the call. Nested
  # arrays (`send_keys([:control, "/"])`) are chords — modifiers
  # combined with the final key in one press. Pass the whole
  # batch to `Browser#send_session_keys` in one call so the
  # JS-side handler can build a `combo` atom from the held
  # modifiers + the next key. Iterating per-key would split the
  # chord across calls and drop the modifier flags.
  current_browser.send_session_keys(keys)
  nil
end

#set_geolocation(latitude: nil, longitude: nil, accuracy: 10, denied: false, **rest) ⇒ Object

CDP-ish geolocation override (Capybara driver-level API).

page.driver.set_geolocation(latitude: 35.6, longitude: 139.7)
page.driver.set_geolocation(denied: true)  # PERMISSION_DENIED
page.driver.set_geolocation                # clear -> POSITION_UNAVAILABLE


679
680
681
# File 'lib/capybara/simulated/driver.rb', line 679

def set_geolocation(latitude: nil, longitude: nil, accuracy: 10, denied: false, **rest)
  current_browser.set_geolocation(latitude: latitude, longitude: longitude, accuracy: accuracy, denied: denied, **rest)
end

#start_tracing(**metadata) ⇒ Object

Per-test trace recording. Mirrors capybara-playwright-driver's start_tracing / stop_tracing shape so suites can swap drivers without rewriting hooks.



110
# File 'lib/capybara/simulated/driver.rb', line 110

def start_tracing(**) = browser.start_trace()

#status_codeObject



301
# File 'lib/capybara/simulated/driver.rb', line 301

def status_code          = current_browser.status_code

#stop_tracing(path: nil) ⇒ Object



112
113
114
115
116
117
# File 'lib/capybara/simulated/driver.rb', line 112

def stop_tracing(path: nil)
  active = current_trace or return nil
  result = path ? browser.finish_trace_to(path, active) : active
  browser.clear_trace!
  result
end

#switch_to_frame(frame) ⇒ Object

Capybara within_frame / switch_to_frame. frame is the iframe Capybara::Node::Element (its .native is our driver Node), or the :parent / :top symbols. The block's finds + actions then route into the frame's own V8 realm via the Browser's @current_realm_id.



317
318
319
320
# File 'lib/capybara/simulated/driver.rb', line 317

def switch_to_frame(frame)
  target = frame.is_a?(Symbol) ? frame : frame.native.handle_id
  current_browser.switch_to_frame(target)
end

#switch_to_window(h) ⇒ Object



616
617
618
619
620
621
622
623
624
# File 'lib/capybara/simulated/driver.rb', line 616

def switch_to_window(h)
  if h == PRIMARY_HANDLE
    @active_handle = nil
  elsif @aux_windows.any? {|w| w[:handle] == h }
    @active_handle = h
  else
    raise Capybara::WindowError, "Unknown window handle: #{h}"
  end
end

#titleObject



300
# File 'lib/capybara/simulated/driver.rb', line 300

def title                = current_browser.title

#tracing?Boolean

Returns:

  • (Boolean)


119
# File 'lib/capybara/simulated/driver.rb', line 119

def tracing?      = !current_trace.nil?

#unregister_blob_partition(url) ⇒ Object



463
464
465
# File 'lib/capybara/simulated/driver.rb', line 463

def unregister_blob_partition(url)
  @blob_partitions_lock.synchronize { @blob_partitions.delete(url.to_s) }
end

#visit(path) ⇒ Object



274
# File 'lib/capybara/simulated/driver.rb', line 274

def visit(path)          = current_browser.visit(path)

#wait?Boolean

Dynamic wait?: only poll when there's pending timer work that real-time advancement could resolve. With no timers queued, polling can't change anything, so we fail fast via the wait? = false synchronize path.

Returns:

  • (Boolean)


217
# File 'lib/capybara/simulated/driver.rb', line 217

def wait?               = current_browser.polling?

#window_browser(handle) ⇒ Object

The Browser backing a handle, or nil if the window is closed/unknown.



340
341
342
# File 'lib/capybara/simulated/driver.rb', line 340

def window_browser(handle)
  window_entries.find {|w| w[:handle] == handle }&.fetch(:browser)
end

#window_closed?(handle) ⇒ Boolean

Returns:

  • (Boolean)


560
# File 'lib/capybara/simulated/driver.rb', line 560

def window_closed?(handle)         = window_browser(handle).nil?

#window_handlesObject



330
331
332
# File 'lib/capybara/simulated/driver.rb', line 330

def window_handles
  [PRIMARY_HANDLE] + @aux_windows.map {|w| w[:handle] }
end

#window_history_go(handle, delta) ⇒ Object

A cross-window w.history.back()/forward()/go(n): traverse the target window's history. The opener's VM is the one executing, so a non-active target can rebuild eagerly (like navigate_window); an active target (e.g. opener.history.back() from an aux) defers to avoid tearing down the running VM mid-call. Returns true when the traversal crossed a document boundary — the JS proxy then fires the target's deferred load (the same deferral as navAux) so the restored page's window.onload runs after the opener's current task. False for a same-document (pushState) traversal — popstate already fired — or a no-op.



547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/capybara/simulated/driver.rb', line 547

def window_history_go(handle, delta)
  b = window_browser(handle) or return false
  if b.equal?(current_browser)
    # `opener.history.back()` targeting the active window: defer (can't
    # rebuild the running VM mid-call) and return false — the active
    # window's load fires through its own navigation path when the pending
    # traversal drains, NOT via the aux-load deferral the caller would run.
    b.history_go(delta)
    false
  else
    b.history_go(delta, force: true) == :cross_document
  end
end

#window_location(handle) ⇒ Object



516
517
518
# File 'lib/capybara/simulated/driver.rb', line 516

def window_location(handle)        = (window_browser(handle)&.current_url).to_s
# A cross-window property read (`win.foo` / `win.document.foo`) — read the
# primitive off the target window's VM.

#window_post_message(source_browser, target_handle, data, _origin) ⇒ Object

targetWindow.postMessage(data, origin) — queue on the target window's Browser, tagged with the source window's handle.



502
503
504
505
# File 'lib/capybara/simulated/driver.rb', line 502

def window_post_message(source_browser, target_handle, data, _origin)
  target = window_browser(target_handle) or return
  target.enqueue_window_message(data, _origin, handle_for(source_browser))
end

#window_read(handle, prop, doc: false) ⇒ Object

A cross-window property read (win.foo / win.document.foo) — read the primitive off the target window's VM.



519
520
521
522
# File 'lib/capybara/simulated/driver.rb', line 519

def window_read(handle, prop, doc: false)
  b = window_browser(handle) or return nil
  b.read_property(prop, doc: doc)
end

#window_ref_call(handle, id, method, args) ⇒ Object



528
# File 'lib/capybara/simulated/driver.rb', line 528

def window_ref_call(handle, id, method, args) = (b = window_browser(handle)) ? b.remote_ref_call(id, method, args) : nil

#window_ref_get(handle, id, prop) ⇒ Object



526
# File 'lib/capybara/simulated/driver.rb', line 526

def window_ref_get(handle, id, prop)         = (b = window_browser(handle)) ? b.remote_ref_get(id, prop) : nil

#window_ref_set(handle, id, prop, value) ⇒ Object



527
# File 'lib/capybara/simulated/driver.rb', line 527

def window_ref_set(handle, id, prop, value)  = ((b = window_browser(handle)) && b.remote_ref_set(id, prop, value))

#window_set_location(handle, url) ⇒ Object



529
530
531
532
533
534
535
536
537
# File 'lib/capybara/simulated/driver.rb', line 529

def window_set_location(handle, url)
  b = window_browser(handle) or return
  # Per HTML, `w.location = url` parses `url` relative to the ENTRY settings
  # object — the document of the script doing the assignment (the active
  # window) — NOT the target window's current document. So a cross-window
  # `w.location.href = 'resources/x.html'` resolves against the opener's
  # base, not the aux's (which would double a shared path segment).
  navigate_window(b, current_browser.resolve_document_url(url), source: current_browser)
end

#window_size(_) ⇒ Object



596
# File 'lib/capybara/simulated/driver.rb', line 596

def window_size(_)           = [current_browser.viewport_width, current_browser.viewport_height]

#with_playwright_page {|FakePlaywrightPage.new(current_browser)| ... } ⇒ Object

Playwright-driver compatibility shim. Discourse's system-spec before(:each) calls page.driver.with_playwright_page to install a JS-console logger, apply a CDP setTimezoneOverride, and (in dev_tools_spec) evaluate window.enableDevTools(). Yield a FakePlaywrightPage that delegates evaluate(js) to our JS engine and silently no-ops every other Playwright-only method via method_missing → self. Chained accessors like pw.context.new_cdp_session(pw).send_message("…") therefore propagate as a no-op rather than NoMethodError, while pw.evaluate("…") runs the JS where it matters.



147
148
149
# File 'lib/capybara/simulated/driver.rb', line 147

def with_playwright_page
  yield FakePlaywrightPage.new(current_browser) if block_given?
end