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<Driver>

— 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
# 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.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.



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

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



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

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

#active_elementObject



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_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.



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_traceObject



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

def current_trace = browser.trace || browser.pending_trace

#current_urlObject



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

def current_url          = current_browser.current_url || ''

#current_window_handleObject



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, **options, &block) = run_modal(type, accept: false, **options, &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_backObject



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

def go_back              = current_browser.go_back

#go_forwardObject



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)

#htmlObject



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

def html                 = current_browser.html

#invalid_element_errorsObject



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

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

#javascript_enabled?Boolean

Returns:

  • (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

Returns:

  • (Boolean)


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

def needs_server?       = false

#no_such_window_errorObject



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.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` + `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

#refreshObject



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.set_viewport(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.set_viewport(w, h)
# Forem's ahoy-tracking spec calls `driver.resize(w, h)` directly
# rather than through `current_window.resize_to`.

#response_headersObject



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_codeObject



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

#titleObject



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

def title                = current_browser.title

#tracing?Boolean

Returns:

  • (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.

Returns:

  • (Boolean)


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

Returns:

  • (Boolean)


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

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

#window_handlesObject



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 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.



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.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.



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

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