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



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

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

#active_elementObject



408
409
410
411
# File 'lib/capybara/simulated/driver.rb', line 408

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

#close_window(h) ⇒ Object



346
347
348
349
350
351
352
353
354
# File 'lib/capybara/simulated/driver.rb', line 346

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



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

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

#evaluate_async_script(script, *args) ⇒ Object



385
386
387
# File 'lib/capybara/simulated/driver.rb', line 385

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

#evaluate_script(script, *args) ⇒ Object



370
371
372
# File 'lib/capybara/simulated/driver.rb', line 370

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.



380
381
382
383
# File 'lib/capybara/simulated/driver.rb', line 380

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



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

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



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

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



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

def no_such_window_error   = Capybara::WindowError

#open_aux_window(url = nil, name: nil, opener_handle: 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
# File 'lib/capybara/simulated/driver.rb', line 267

def open_aux_window(url = nil, name: nil, opener_handle: nil)
  name = name.to_s
  if !name.empty? && (existing = @aux_windows.find {|w| w[:name] == name })
    navigate_window(existing[:browser], url)
    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}
  aux.visit(url) if url && !url.empty?
  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).



342
343
344
# File 'lib/capybara/simulated/driver.rb', line 342

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.



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

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))
end

#opener_handle_of(browser) ⇒ Object



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

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



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

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

#resize_window_to(_, w, h) ⇒ Object



364
365
366
# File 'lib/capybara/simulated/driver.rb', line 364

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



403
404
405
406
# File 'lib/capybara/simulated/driver.rb', line 403

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

#send_keys(*keys) ⇒ Object



422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/capybara/simulated/driver.rb', line 422

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


418
419
420
# File 'lib/capybara/simulated/driver.rb', line 418

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



355
356
357
358
359
360
361
362
363
# File 'lib/capybara/simulated/driver.rb', line 355

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)


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

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



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

def window_location(handle)        = (window_browser(handle)&.current_url).to_s

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



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

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_set_location(handle, url) ⇒ Object



312
313
314
315
# File 'lib/capybara/simulated/driver.rb', line 312

def window_set_location(handle, url)
  b = window_browser(handle) or return
  navigate_window(b, b.resolve_document_url(url))
end

#window_size(_) ⇒ Object



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

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