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
# 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
  @aux_windows     = []  # [{handle:, browser:}, …]
  @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.



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

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



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

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

#active_elementObject



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

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

#close_window(h) ⇒ Object



267
268
269
270
271
272
273
274
275
# File 'lib/capybara/simulated/driver.rb', line 267

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.



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

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

#current_traceObject



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

def current_trace = browser.trace || browser.pending_trace

#current_urlObject



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

def current_url          = current_browser.current_url || ''

#current_window_handleObject



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

def current_window_handle    = @active_handle || PRIMARY_HANDLE

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



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

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

#evaluate_async_script(script, *args) ⇒ Object



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

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

#evaluate_script(script, *args) ⇒ Object



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

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.



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

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

#find_css(query, **_) ⇒ Object



226
227
228
# File 'lib/capybara/simulated/driver.rb', line 226

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

#find_xpath(query, **_) ⇒ Object



222
223
224
# File 'lib/capybara/simulated/driver.rb', line 222

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

#go_backObject



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

def go_back              = current_browser.go_back

#go_forwardObject



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

def go_forward           = current_browser.go_forward

#header(name, value) ⇒ Object



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

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

#htmlObject



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

def html                 = current_browser.html

#invalid_element_errorsObject



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

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

#javascript_enabled?Boolean

Returns:

  • (Boolean)


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

def javascript_enabled? = true

#maximize_window(_) ⇒ Object



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

def maximize_window(_)       ; nil ; end

#needs_server?Boolean

Returns:

  • (Boolean)


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

def needs_server?       = false

#no_such_window_errorObject



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

def no_such_window_error   = Capybara::WindowError

#open_aux_window(url = nil) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/capybara/simulated/driver.rb', line 241

def open_aux_window(url = nil)
  @next_window_seq += 1
  handle = "csim-window-#{@next_window_seq}"
  aux = build_window_browser
  aux.visit(url) if url && !url.empty?
  @aux_windows << {handle: handle, browser: aux}
  handle
rescue StandardError => e
  # Aux window URL-load failure (binary content, network error,
  # …) shouldn't tear down the test — record the handle 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]}"
  @aux_windows << {handle: handle, browser: aux}
  handle
end

#open_new_window(_kind = :tab) ⇒ Object

Capybara ‘Session#open_new_window(:tab)` entry point — visits `about:blank` so the test can `switch_to_window` then `visit` the real URL. We don’t distinguish ‘:tab` from `:window` (no window-chrome semantics in this driver).



263
264
265
# File 'lib/capybara/simulated/driver.rb', line 263

def open_new_window(_kind = :tab)
  open_aux_window
end

#refreshObject



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

def refresh              = current_browser.refresh

#reset!Object



207
208
209
210
211
212
# File 'lib/capybara/simulated/driver.rb', line 207

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



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

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

#resize_window_to(_, w, h) ⇒ Object



285
286
287
# File 'lib/capybara/simulated/driver.rb', line 285

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



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

def response_headers     = current_browser.response_headers

#save_screenshot(path, **_opts) ⇒ Object



324
325
326
327
# File 'lib/capybara/simulated/driver.rb', line 324

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

#send_keys(*keys) ⇒ Object



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

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


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

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.



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

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

#status_codeObject



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

def status_code          = current_browser.status_code

#stop_tracing(path: nil) ⇒ Object



98
99
100
101
102
103
# File 'lib/capybara/simulated/driver.rb', line 98

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_window(h) ⇒ Object



276
277
278
279
280
281
282
283
284
# File 'lib/capybara/simulated/driver.rb', line 276

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



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

def title                = current_browser.title

#tracing?Boolean

Returns:

  • (Boolean)


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

def tracing?      = !current_trace.nil?

#visit(path) ⇒ Object



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

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)


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

def wait?               = current_browser.polling?

#window_handlesObject



238
239
240
# File 'lib/capybara/simulated/driver.rb', line 238

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

#window_size(_) ⇒ Object



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

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.



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

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