Class: Capybara::Simulated::Driver
- Inherits:
-
Driver::Base
- Object
- Driver::Base
- Capybara::Simulated::Driver
- Defined in:
- lib/capybara/simulated/driver.rb
Defined Under Namespace
Classes: FakePlaywrightLocator, FakePlaywrightPage
Constant Summary collapse
- PRIMARY_HANDLE =
Per-window Browser/VM.
open_aux_windowcreates a fresh Browser sharing the Driver's cookie + localStorage jars (origin-shared in real browsers) and visits the target URL;switch_to_windowflips@active_handleso subsequent driver ops route throughcurrent_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
-
#app ⇒ Object
readonly
Returns the value of attribute app.
-
#browser ⇒ Object
readonly
Returns the value of attribute browser.
-
#owner_thread ⇒ Object
readonly
Returns the value of attribute owner_thread.
Class Method Summary collapse
Instance Method Summary collapse
- #accept_modal(type, **options, &block) ⇒ Object
- #active_element ⇒ Object
-
#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).
-
#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.
-
#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). - #close_window(h) ⇒ Object
-
#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). -
#current_browser ⇒ Object
Active window's Browser.
- #current_trace ⇒ Object
- #current_url ⇒ Object
- #current_window_handle ⇒ Object
- #dismiss_modal(type, **options, &block) ⇒ Object
- #evaluate_async_script(script, *args) ⇒ Object
- #evaluate_script(script, *args) ⇒ Object
-
#execute_script(script, *args) ⇒ Object
Capybara's
execute_scriptcontract is "run it, discard the return". - #find_css(query, **_) ⇒ Object
- #find_xpath(query, **_) ⇒ Object
-
#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.
- #go_back ⇒ Object
- #go_forward ⇒ Object
- #header(name, value) ⇒ Object
- #html ⇒ Object
-
#initialize(app, js_engine: nil, viewport: nil, user_agent: nil) ⇒ Driver
constructor
viewport: [w, h]anduser_agent:(typically supplied viaCapybara.register_driver) force the JS-sideinnerWidth/innerHeightandnavigator.userAgent(plusHTTP_USER_AGENTon Rack requests) before the first navigate, somatchMedia/ mobile-breakpoint branches and server-side UA-based mobile detection both resolve before any document loads. - #invalid_element_errors ⇒ Object
- #javascript_enabled? ⇒ Boolean
- #maximize_window(_) ⇒ Object
- #needs_server? ⇒ Boolean
- #no_such_window_error ⇒ Object
-
#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. -
#open_new_window(_kind = :tab) ⇒ Object
Capybara
Session#open_new_window(:tab)entry point — opens atabout:blank(socurrent_url/title match a real new tab) and the test thenswitch_to_window+visits the real URL. -
#open_window_from_js(opener_browser, url, name, opener_realm_id = 0) ⇒ Object
window.open(url, name)from theopenerwindow's JS. - #opener_handle_of(browser) ⇒ Object
-
#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.
- #refresh ⇒ Object
-
#register_blob_partition(url, browser, site) ⇒ Object
Record / drop a blob URL's storage partition (called by Browser#blob_register / #blob_unregister).
- #reset! ⇒ Object
- #reset_history! ⇒ Object
-
#resize(w, h) ⇒ Object
Forem's ahoy-tracking spec calls
driver.resize(w, h)directly rather than throughcurrent_window.resize_to. - #resize_window_to(_, w, h) ⇒ Object
- #response_headers ⇒ Object
-
#revoke_blob_partitioned(url, source) ⇒ Object
A user
URL.revokeObjectURL(url)fromsource. -
#run_event_loop_frame(frame_ms) ⇒ Object
Run one real-cadence event-loop frame and return the loop's observable state.
- #save_screenshot(path, **_opts) ⇒ Object
- #send_keys(*keys) ⇒ Object
-
#set_geolocation(latitude: nil, longitude: nil, accuracy: 10, denied: false, **rest) ⇒ Object
CDP-ish geolocation override (Capybara driver-level API).
-
#start_tracing(**metadata) ⇒ Object
Per-test trace recording.
- #status_code ⇒ Object
- #stop_tracing(path: nil) ⇒ Object
-
#switch_to_frame(frame) ⇒ Object
Capybara
within_frame/switch_to_frame. - #switch_to_window(h) ⇒ Object
- #title ⇒ Object
- #tracing? ⇒ Boolean
- #unregister_blob_partition(url) ⇒ Object
- #visit(path) ⇒ Object
-
#wait? ⇒ Boolean
Dynamic wait?: only poll when there's pending timer work that real-time advancement could resolve.
-
#window_browser(handle) ⇒ Object
The Browser backing a handle, or nil if the window is closed/unknown.
- #window_closed?(handle) ⇒ Boolean
- #window_handles ⇒ Object
-
#window_history_go(handle, delta) ⇒ Object
A cross-window
w.history.back()/forward()/go(n): traverse the target window's history. - #window_location(handle) ⇒ Object
-
#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. -
#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. - #window_ref_call(handle, id, method, args) ⇒ Object
- #window_ref_get(handle, id, prop) ⇒ Object
- #window_ref_set(handle, id, prop, value) ⇒ Object
- #window_set_location(handle, url) ⇒ Object
- #window_size(_) ⇒ Object
-
#with_playwright_page {|FakePlaywrightPage.new(current_browser)| ... } ⇒ Object
Playwright-driver compatibility shim.
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. = if @browser.default_user_agent = user_agent if user_agent end |
Instance Attribute Details
#app ⇒ Object (readonly)
Returns the value of attribute app.
45 46 47 |
# File 'lib/capybara/simulated/driver.rb', line 45 def app @app end |
#browser ⇒ Object (readonly)
Returns the value of attribute browser.
122 123 124 |
# File 'lib/capybara/simulated/driver.rb', line 122 def browser @browser end |
#owner_thread ⇒ Object (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
685 |
# File 'lib/capybara/simulated/driver.rb', line 685 def accept_modal(type, **, &block) = run_modal(type, accept: true, **, &block) |
#active_element ⇒ Object
657 658 659 660 |
# File 'lib/capybara/simulated/driver.rb', line 657 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).
439 440 441 442 443 |
# File 'lib/capybara/simulated/driver.rb', line 439 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.
419 420 421 422 |
# File 'lib/capybara/simulated/driver.rb', line 419 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).
497 498 499 500 501 502 |
# File 'lib/capybara/simulated/driver.rb', line 497 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
585 586 587 588 589 590 591 592 593 594 |
# File 'lib/capybara/simulated/driver.rb', line 585 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).
427 428 429 430 |
# File 'lib/capybara/simulated/driver.rb', line 427 def cross_partition_blob?(url, accessor) site = blob_partition_site_of(url) !site.nil? && site != accessor.blob_partition_site end |
#current_browser ⇒ Object
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_trace ⇒ Object
120 |
# File 'lib/capybara/simulated/driver.rb', line 120 def current_trace = browser.trace || browser.pending_trace |
#current_url ⇒ Object
286 |
# File 'lib/capybara/simulated/driver.rb', line 286 def current_url = current_browser.current_url || '' |
#current_window_handle ⇒ Object
317 |
# File 'lib/capybara/simulated/driver.rb', line 317 def current_window_handle = @active_handle || PRIMARY_HANDLE |
#dismiss_modal(type, **options, &block) ⇒ Object
686 |
# File 'lib/capybara/simulated/driver.rb', line 686 def dismiss_modal(type, **, &block) = run_modal(type, accept: false, **, &block) |
#evaluate_async_script(script, *args) ⇒ Object
634 635 636 |
# File 'lib/capybara/simulated/driver.rb', line 634 def evaluate_async_script(script, *args) unwrap(current_browser.evaluate_async_script(script, args)) end |
#evaluate_script(script, *args) ⇒ Object
619 620 621 |
# File 'lib/capybara/simulated/driver.rb', line 619 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.
629 630 631 632 |
# File 'lib/capybara/simulated/driver.rb', line 629 def execute_script(script, *args) current_browser.execute_script(script, args) nil end |
#find_css(query, **_) ⇒ Object
297 298 299 |
# File 'lib/capybara/simulated/driver.rb', line 297 def find_css(query, **_) current_browser.find_css(query).map {|id| Node.new(self, id) } end |
#find_xpath(query, **_) ⇒ Object
293 294 295 |
# File 'lib/capybara/simulated/driver.rb', line 293 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.
513 |
# File 'lib/capybara/simulated/driver.rb', line 513 def fire_aux_window_load(handle) = ((b = window_browser(handle)) && b.fire_own_window_load) |
#go_back ⇒ Object
283 |
# File 'lib/capybara/simulated/driver.rb', line 283 def go_back = current_browser.go_back |
#go_forward ⇒ Object
284 |
# File 'lib/capybara/simulated/driver.rb', line 284 def go_forward = current_browser.go_forward |
#header(name, value) ⇒ Object
291 |
# File 'lib/capybara/simulated/driver.rb', line 291 def header(name, value) = current_browser.set_header(name, value) |
#html ⇒ Object
287 |
# File 'lib/capybara/simulated/driver.rb', line 287 def html = current_browser.html |
#invalid_element_errors ⇒ Object
649 |
# File 'lib/capybara/simulated/driver.rb', line 649 def invalid_element_errors = [Capybara::Simulated::StaleElement] |
#javascript_enabled? ⇒ Boolean
135 |
# File 'lib/capybara/simulated/driver.rb', line 135 def javascript_enabled? = true |
#maximize_window(_) ⇒ Object
617 |
# File 'lib/capybara/simulated/driver.rb', line 617 def maximize_window(_) ; nil ; end |
#needs_server? ⇒ Boolean
134 |
# File 'lib/capybara/simulated/driver.rb', line 134 def needs_server? = false |
#no_such_window_error ⇒ Object
650 |
# File 'lib/capybara/simulated/driver.rb', line 650 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.
337 338 339 340 341 342 343 344 345 346 347 348 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 |
# File 'lib/capybara/simulated/driver.rb', line 337 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.[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).
581 582 583 |
# File 'lib/capybara/simulated/driver.rb', line 581 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.
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/capybara/simulated/driver.rb', line 395 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
549 550 551 552 |
# File 'lib/capybara/simulated/driver.rb', line 549 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) |
#refresh ⇒ Object
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.
447 448 449 |
# File 'lib/capybara/simulated/driver.rb', line 447 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 280 281 282 |
# File 'lib/capybara/simulated/driver.rb', line 276 def reset! @aux_windows.each {|w| w[:browser].dispose rescue nil } @aux_windows.clear @active_handle = nil @blob_partitions_lock.synchronize { @blob_partitions.clear } browser.reset! end |
#reset_history! ⇒ Object
285 |
# File 'lib/capybara/simulated/driver.rb', line 285 def reset_history! = current_browser.reset_history! |
#resize(w, h) ⇒ Object
Forem's ahoy-tracking spec calls driver.resize(w, h) directly
rather than through current_window.resize_to.
616 |
# File 'lib/capybara/simulated/driver.rb', line 616 def resize(w, h) = current_browser.(w, h) |
#resize_window_to(_, w, h) ⇒ Object
613 614 615 |
# File 'lib/capybara/simulated/driver.rb', line 613 def resize_window_to(_, w, h) = current_browser.(w, h) # Forem's ahoy-tracking spec calls `driver.resize(w, h)` directly # rather than through `current_window.resize_to`. |
#response_headers ⇒ Object
290 |
# File 'lib/capybara/simulated/driver.rb', line 290 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).
461 462 463 464 465 466 467 468 |
# File 'lib/capybara/simulated/driver.rb', line 461 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
652 653 654 655 |
# File 'lib/capybara/simulated/driver.rb', line 652 def save_screenshot(path, **_opts) File.write(path, current_browser.html.to_s) path end |
#send_keys(*keys) ⇒ Object
671 672 673 674 675 676 677 678 679 680 681 682 683 |
# File 'lib/capybara/simulated/driver.rb', line 671 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
667 668 669 |
# File 'lib/capybara/simulated/driver.rb', line 667 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_code ⇒ Object
289 |
# File 'lib/capybara/simulated/driver.rb', line 289 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.
305 306 307 308 |
# File 'lib/capybara/simulated/driver.rb', line 305 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
604 605 606 607 608 609 610 611 612 |
# File 'lib/capybara/simulated/driver.rb', line 604 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 |
#title ⇒ Object
288 |
# File 'lib/capybara/simulated/driver.rb', line 288 def title = current_browser.title |
#tracing? ⇒ Boolean
119 |
# File 'lib/capybara/simulated/driver.rb', line 119 def tracing? = !current_trace.nil? |
#unregister_blob_partition(url) ⇒ Object
451 452 453 |
# File 'lib/capybara/simulated/driver.rb', line 451 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.
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.
328 329 330 |
# File 'lib/capybara/simulated/driver.rb', line 328 def window_browser(handle) window_entries.find {|w| w[:handle] == handle }&.fetch(:browser) end |
#window_closed?(handle) ⇒ Boolean
548 |
# File 'lib/capybara/simulated/driver.rb', line 548 def window_closed?(handle) = window_browser(handle).nil? |
#window_handles ⇒ Object
318 319 320 |
# File 'lib/capybara/simulated/driver.rb', line 318 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.
535 536 537 538 539 540 541 542 543 544 545 546 547 |
# File 'lib/capybara/simulated/driver.rb', line 535 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
504 505 506 |
# File 'lib/capybara/simulated/driver.rb', line 504 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.
490 491 492 493 |
# File 'lib/capybara/simulated/driver.rb', line 490 def (source_browser, target_handle, data, _origin) target = window_browser(target_handle) or return target.(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.
507 508 509 510 |
# File 'lib/capybara/simulated/driver.rb', line 507 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
516 |
# File 'lib/capybara/simulated/driver.rb', line 516 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
514 |
# File 'lib/capybara/simulated/driver.rb', line 514 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
515 |
# File 'lib/capybara/simulated/driver.rb', line 515 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
517 518 519 520 521 522 523 524 525 |
# File 'lib/capybara/simulated/driver.rb', line 517 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
584 |
# File 'lib/capybara/simulated/driver.rb', line 584 def window_size(_) = [current_browser., current_browser.] |
#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 |