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_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<Driver>
-
— 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
-
#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
-
#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_script` contract is “run it, discard the return”.
- #find_css(query, **_) ⇒ Object
- #find_xpath(query, **_) ⇒ Object
- #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]` 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.
- #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) ⇒ Object
Open (or, by ‘name`, reuse) an auxiliary window.
-
#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` + `visit`s the real URL.
-
#open_window_from_js(opener_browser, url, name) ⇒ Object
‘window.open(url, name)` from the `opener` window’s JS.
- #opener_handle_of(browser) ⇒ Object
- #refresh ⇒ Object
- #reset! ⇒ Object
-
#resize(w, h) ⇒ Object
Forem’s ahoy-tracking spec calls ‘driver.resize(w, h)` directly rather than through `current_window.resize_to`.
- #resize_window_to(_, w, h) ⇒ Object
- #response_headers ⇒ Object
- #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
- #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_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_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 |
# 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 = {} @browser = build_window_browser @browser.window_handle = PRIMARY_HANDLE @aux_windows = [] # [{handle:, browser:, name:, opener:}, …] @active_handle = nil @next_window_seq = 0 @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.
109 110 111 |
# File 'lib/capybara/simulated/driver.rb', line 109 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
482 |
# File 'lib/capybara/simulated/driver.rb', line 482 def accept_modal(type, **, &block) = run_modal(type, accept: true, **, &block) |
#active_element ⇒ Object
454 455 456 457 |
# File 'lib/capybara/simulated/driver.rb', line 454 def active_element handle = current_browser.active_element_handle handle ? Node.new(self, handle) : nil 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).
337 338 339 340 341 342 |
# File 'lib/capybara/simulated/driver.rb', line 337 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
392 393 394 395 396 397 398 399 400 |
# File 'lib/capybara/simulated/driver.rb', line 392 def close_window(h) return if h == PRIMARY_HANDLE @aux_windows.reject! {|w| next false unless w[:handle] == h w[:browser].dispose rescue nil true } @active_handle = nil if @active_handle == h 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.
115 116 117 118 119 |
# File 'lib/capybara/simulated/driver.rb', line 115 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
107 |
# File 'lib/capybara/simulated/driver.rb', line 107 def current_trace = browser.trace || browser.pending_trace |
#current_url ⇒ Object
216 |
# File 'lib/capybara/simulated/driver.rb', line 216 def current_url = current_browser.current_url || '' |
#current_window_handle ⇒ Object
247 |
# File 'lib/capybara/simulated/driver.rb', line 247 def current_window_handle = @active_handle || PRIMARY_HANDLE |
#dismiss_modal(type, **options, &block) ⇒ Object
483 |
# File 'lib/capybara/simulated/driver.rb', line 483 def dismiss_modal(type, **, &block) = run_modal(type, accept: false, **, &block) |
#evaluate_async_script(script, *args) ⇒ Object
431 432 433 |
# File 'lib/capybara/simulated/driver.rb', line 431 def evaluate_async_script(script, *args) unwrap(current_browser.evaluate_async_script(script, args)) end |
#evaluate_script(script, *args) ⇒ Object
416 417 418 |
# File 'lib/capybara/simulated/driver.rb', line 416 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.
426 427 428 429 |
# File 'lib/capybara/simulated/driver.rb', line 426 def execute_script(script, *args) current_browser.execute_script(script, args) nil end |
#find_css(query, **_) ⇒ Object
227 228 229 |
# File 'lib/capybara/simulated/driver.rb', line 227 def find_css(query, **_) current_browser.find_css(query).map {|id| Node.new(self, id) } end |
#find_xpath(query, **_) ⇒ Object
223 224 225 |
# File 'lib/capybara/simulated/driver.rb', line 223 def find_xpath(query, **_) current_browser.find_xpath(query).map {|id| Node.new(self, id) } end |
#go_back ⇒ Object
214 |
# File 'lib/capybara/simulated/driver.rb', line 214 def go_back = current_browser.go_back |
#go_forward ⇒ Object
215 |
# File 'lib/capybara/simulated/driver.rb', line 215 def go_forward = current_browser.go_forward |
#header(name, value) ⇒ Object
221 |
# File 'lib/capybara/simulated/driver.rb', line 221 def header(name, value) = current_browser.set_header(name, value) |
#html ⇒ Object
217 |
# File 'lib/capybara/simulated/driver.rb', line 217 def html = current_browser.html |
#invalid_element_errors ⇒ Object
446 |
# File 'lib/capybara/simulated/driver.rb', line 446 def invalid_element_errors = [Capybara::Simulated::StaleElement] |
#javascript_enabled? ⇒ Boolean
122 |
# File 'lib/capybara/simulated/driver.rb', line 122 def javascript_enabled? = true |
#maximize_window(_) ⇒ Object
414 |
# File 'lib/capybara/simulated/driver.rb', line 414 def maximize_window(_) ; nil ; end |
#needs_server? ⇒ Boolean
121 |
# File 'lib/capybara/simulated/driver.rb', line 121 def needs_server? = false |
#no_such_window_error ⇒ Object
447 |
# File 'lib/capybara/simulated/driver.rb', line 447 def no_such_window_error = Capybara::WindowError |
#open_aux_window(url = nil, name: nil, opener_handle: nil, source: nil, blob_snapshot: 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.
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
# File 'lib/capybara/simulated/driver.rb', line 267 def open_aux_window(url = nil, name: nil, opener_handle: nil, source: nil, blob_snapshot: nil) name = name.to_s if !name.empty? && (existing = @aux_windows.find {|w| w[:name] == name }) navigate_window(existing[:browser], url, source: source) 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? # 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. unless url.to_s.start_with?('blob:') && load_blob_into_window(aux, url, source, snapshot: blob_snapshot) aux.visit(url) 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` + `visit`s the real URL. We don’t distinguish ‘:tab` from `:window` (no window-chrome semantics here).
388 389 390 |
# File 'lib/capybara/simulated/driver.rb', line 388 def open_new_window(_kind = :tab) open_aux_window('about:blank') end |
#open_window_from_js(opener_browser, url, name) ⇒ Object
‘window.open(url, name)` from the `opener` window’s JS. Resolves the URL against the opener’s document and records the opener relationship.
307 308 309 310 |
# File 'lib/capybara/simulated/driver.rb', line 307 def open_window_from_js(opener_browser, url, name) resolved = url.to_s.empty? ? nil : opener_browser.resolve_document_url(url) open_aux_window(resolved, name: name, opener_handle: handle_for(opener_browser), source: opener_browser) end |
#opener_handle_of(browser) ⇒ Object
356 357 358 359 |
# File 'lib/capybara/simulated/driver.rb', line 356 def opener_handle_of(browser) handle = handle_for(browser) window_entries.find {|w| w[:handle] == handle }&.fetch(:opener) end |
#refresh ⇒ Object
207 |
# File 'lib/capybara/simulated/driver.rb', line 207 def refresh = current_browser.refresh |
#reset! ⇒ Object
208 209 210 211 212 213 |
# File 'lib/capybara/simulated/driver.rb', line 208 def reset! @aux_windows.each {|w| w[:browser].dispose rescue nil } @aux_windows.clear @active_handle = nil browser.reset! end |
#resize(w, h) ⇒ Object
Forem’s ahoy-tracking spec calls ‘driver.resize(w, h)` directly rather than through `current_window.resize_to`.
413 |
# File 'lib/capybara/simulated/driver.rb', line 413 def resize(w, h) = current_browser.(w, h) |
#resize_window_to(_, w, h) ⇒ Object
410 411 412 |
# File 'lib/capybara/simulated/driver.rb', line 410 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
220 |
# File 'lib/capybara/simulated/driver.rb', line 220 def response_headers = current_browser.response_headers |
#save_screenshot(path, **_opts) ⇒ Object
449 450 451 452 |
# File 'lib/capybara/simulated/driver.rb', line 449 def save_screenshot(path, **_opts) File.write(path, current_browser.html.to_s) path end |
#send_keys(*keys) ⇒ Object
468 469 470 471 472 473 474 475 476 477 478 479 480 |
# File 'lib/capybara/simulated/driver.rb', line 468 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
464 465 466 |
# File 'lib/capybara/simulated/driver.rb', line 464 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.
97 |
# File 'lib/capybara/simulated/driver.rb', line 97 def start_tracing(**) = browser.start_trace() |
#status_code ⇒ Object
219 |
# File 'lib/capybara/simulated/driver.rb', line 219 def status_code = current_browser.status_code |
#stop_tracing(path: nil) ⇒ Object
99 100 101 102 103 104 |
# File 'lib/capybara/simulated/driver.rb', line 99 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`.
235 236 237 238 |
# File 'lib/capybara/simulated/driver.rb', line 235 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
401 402 403 404 405 406 407 408 409 |
# File 'lib/capybara/simulated/driver.rb', line 401 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
218 |
# File 'lib/capybara/simulated/driver.rb', line 218 def title = current_browser.title |
#tracing? ⇒ Boolean
106 |
# File 'lib/capybara/simulated/driver.rb', line 106 def tracing? = !current_trace.nil? |
#visit(path) ⇒ Object
206 |
# File 'lib/capybara/simulated/driver.rb', line 206 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.
204 |
# File 'lib/capybara/simulated/driver.rb', line 204 def wait? = current_browser.polling? |
#window_browser(handle) ⇒ Object
The Browser backing a handle, or nil if the window is closed/unknown.
258 259 260 |
# File 'lib/capybara/simulated/driver.rb', line 258 def window_browser(handle) window_entries.find {|w| w[:handle] == handle }&.fetch(:browser) end |
#window_closed?(handle) ⇒ Boolean
355 |
# File 'lib/capybara/simulated/driver.rb', line 355 def window_closed?(handle) = window_browser(handle).nil? |
#window_handles ⇒ Object
248 249 250 |
# File 'lib/capybara/simulated/driver.rb', line 248 def window_handles [PRIMARY_HANDLE] + @aux_windows.map {|w| w[:handle] } end |
#window_location(handle) ⇒ Object
344 345 346 |
# File 'lib/capybara/simulated/driver.rb', line 344 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.
330 331 332 333 |
# File 'lib/capybara/simulated/driver.rb', line 330 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.
347 348 349 350 |
# File 'lib/capybara/simulated/driver.rb', line 347 def window_read(handle, prop, doc: false) b = window_browser(handle) or return nil b.read_property(prop, doc: doc) end |
#window_set_location(handle, url) ⇒ Object
351 352 353 354 |
# File 'lib/capybara/simulated/driver.rb', line 351 def window_set_location(handle, url) b = window_browser(handle) or return navigate_window(b, b.resolve_document_url(url), source: current_browser) end |
#window_size(_) ⇒ Object
391 |
# File 'lib/capybara/simulated/driver.rb', line 391 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.
134 135 136 |
# File 'lib/capybara/simulated/driver.rb', line 134 def with_playwright_page yield FakePlaywrightPage.new(current_browser) if block_given? end |