Class: Capybara::Dommy::Driver

Inherits:
Capybara::Driver::Base
  • Object
show all
Defined in:
lib/capybara/dommy/driver.rb

Overview

A Capybara driver backed by Dommy::Rack::Session. Implements the navigation / query / reset! parts of the Capybara::Driver::Base contract; element interaction lives in Capybara::Dommy::Node. JavaScript, screenshot, window, and modal methods are left to Driver::Base (which raises Capybara::NotSupportedByDriverError).

Constant Summary collapse

VISIBILITY_MODES =
%i[all html none].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, default_host: nil, follow_redirects: nil, max_redirects: nil, visibility: nil, javascript: nil) ⇒ Driver

Returns a new instance of Driver.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/capybara/dommy/driver.rb', line 27

def initialize(app,
               default_host: nil,
               follow_redirects: nil,
               max_redirects: nil,
               visibility: nil,
               javascript: nil)
  super()
  config = Capybara::Dommy.configuration
  @app = app
  @javascript = javascript.nil? ? config.javascript : javascript
  @visibility = visibility || config.visibility
  unless VISIBILITY_MODES.include?(@visibility)
    raise ArgumentError,
          "unknown visibility mode #{@visibility.inspect} (expected one of #{VISIBILITY_MODES.join(", ")})"
  end
  @raise_on_unsupported_js = config.raise_on_unsupported_js
  @session_options = {
    default_host: default_host || config.default_host,
    follow_redirects: follow_redirects.nil? ? config.follow_redirects : follow_redirects,
    max_redirects: max_redirects || config.max_redirects,
    # Capybara drives a trusted app and legitimately visits multiple
    # hosts (e.g. app_host / multi-server specs), so don't enforce origin.
    enforce_same_origin: false
  }
  @session_options[:javascript] = true if @javascript
  # A JS session needs the virtual clock pumped inside Capybara's
  # synchronize loop, so waiting expectations converge on timer/fetch
  # driven updates. A host-installed pump (the documented seam) wins.
  @time_pump ||= -> { @rack_session&.advance_time(16) } if @javascript
end

Instance Attribute Details

#appObject (readonly)

Returns the value of attribute app.



13
14
15
# File 'lib/capybara/dommy/driver.rb', line 13

def app
  @app
end

#time_pumpObject

--- Deterministic-time seam (used by JS runtimes) ---

A JS runtime assigns a callable here; the driver invokes it before each DOM read Capybara polls in its synchronize loop (find_css / find_xpath / html / title). The pump is expected to advance Dommy's virtual scheduler a small slice and drain microtasks, so "content appears after a timeout" specs converge without wall-clock sleeps. Installing a pump also flips wait? to true, making Capybara retry failed expectations instead of raising immediately. Survives reset! (it belongs to the runtime, not to one page session).



25
26
27
# File 'lib/capybara/dommy/driver.rb', line 25

def time_pump
  @time_pump
end

#visibilityObject (readonly)

Returns the value of attribute visibility.



13
14
15
# File 'lib/capybara/dommy/driver.rb', line 13

def visibility
  @visibility
end

Instance Method Details

#active_elementObject

--- Focus / keyboard ---



181
182
183
# File 'lib/capybara/dommy/driver.rb', line 181

def active_element
  Node.new(self, document.active_element)
end

#current_urlObject



96
97
98
# File 'lib/capybara/dommy/driver.rb', line 96

def current_url
  rack_session.current_url.to_s
end

#documentObject

The document queries run against: the innermost switched-to frame's document, or the top-level page when no frame is active.



150
151
152
# File 'lib/capybara/dommy/driver.rb', line 150

def document
  frame_stack.empty? ? rack_session.document : frame_stack.last[:document]
end

#drain_jsObject

Drain the JS runtime after an interaction's events (promise reactions settle before the next Capybara step). No-op without JavaScript.



66
67
68
69
# File 'lib/capybara/dommy/driver.rb', line 66

def drain_js
  rack_session.after_interaction if @javascript
  nil
end

#evaluate_async_script(_script, *_args) ⇒ Object



239
240
241
# File 'lib/capybara/dommy/driver.rb', line 239

def evaluate_async_script(_script, *_args)
  unsupported_js!("evaluate_async_script")
end

#evaluate_script(script, *args) ⇒ Object

Raises:

  • (ArgumentError)


232
233
234
235
236
237
# File 'lib/capybara/dommy/driver.rb', line 232

def evaluate_script(script, *args)
  return unsupported_js!("evaluate_script") unless @javascript
  raise ArgumentError, "script arguments are not supported" unless args.empty?

  rack_session.evaluate_script(script)
end

#execute_script(script, *args) ⇒ Object

--- JavaScript (unsupported) --- When raise_on_unsupported_js is false these become no-ops, so tests that incidentally call them don't fail.

Raises:

  • (ArgumentError)


225
226
227
228
229
230
# File 'lib/capybara/dommy/driver.rb', line 225

def execute_script(script, *args)
  return unsupported_js!("execute_script") unless @javascript
  raise ArgumentError, "script arguments are not supported" unless args.empty?

  rack_session.execute_script(script)
end

#find_css(query, **_options) ⇒ Object

--- Query (returns Capybara::Dommy::Node arrays) ---



136
137
138
139
# File 'lib/capybara/dommy/driver.rb', line 136

def find_css(query, **_options)
  pump!
  wrap(document&.query_selector_all(query))
end

#find_xpath(query, **_options) ⇒ Object



141
142
143
144
# File 'lib/capybara/dommy/driver.rb', line 141

def find_xpath(query, **_options)
  pump!
  wrap(document&.xpath(query))
end


192
193
194
# File 'lib/capybara/dommy/driver.rb', line 192

def follow_link(element)
  rack_session.click_link_element(element)
end

#frame_titleObject



175
176
177
# File 'lib/capybara/dommy/driver.rb', line 175

def frame_title
  document&.title
end

#frame_urlObject



171
172
173
# File 'lib/capybara/dommy/driver.rb', line 171

def frame_url
  frame_stack.empty? ? rack_session.current_url.to_s : frame_stack.last[:url]
end

#go_backObject



104
105
106
# File 'lib/capybara/dommy/driver.rb', line 104

def go_back
  rack_session.back
end

#go_forwardObject



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

def go_forward
  rack_session.forward
end

#htmlObject

--- Page state ---



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

def html
  pump!
  rack_session.html
end

#invalid_element_errorsObject

Lets Capybara reload a node when it goes stale (after navigation).



217
218
219
# File 'lib/capybara/dommy/driver.rb', line 217

def invalid_element_errors
  [Capybara::Dommy::StaleElementReferenceError]
end

#javascript?Boolean

Whether this driver runs page JavaScript (javascript: true, backed by a Dommy::Rack::Session.new(app, javascript: true)). Node interactions then dispatch real DOM events (Turbo/Stimulus handlers run) instead of the HTML-only fast paths.

Returns:

  • (Boolean)


62
# File 'lib/capybara/dommy/driver.rb', line 62

def javascript? = @javascript

#needs_server?Boolean

Returns:

  • (Boolean)


212
213
214
# File 'lib/capybara/dommy/driver.rb', line 212

def needs_server?
  false
end

#rack_sessionObject

The dommy-rack session. Named rack_session to avoid colliding with Capybara::Driver::Base#session (the owning Capybara::Session). Rebuilt when the effective host (Capybara app_host / default_host) changes so current_url reflects it and same-origin checks pass.



75
76
77
78
79
80
81
82
83
# File 'lib/capybara/dommy/driver.rb', line 75

def rack_session
  host = effective_host
  if @rack_session.nil? || @rack_session_host != host
    @rack_session&.dispose
    @rack_session = ::Dommy::Rack::Session.new(@app, **@session_options.merge(default_host: host))
    @rack_session_host = host
  end
  @rack_session
end

#refreshObject



100
101
102
# File 'lib/capybara/dommy/driver.rb', line 100

def refresh
  rack_session.reload
end

#reset!Object

--- Lifecycle ---



202
203
204
205
206
# File 'lib/capybara/dommy/driver.rb', line 202

def reset!
  @rack_session&.dispose
  @rack_session = nil
  @frame_stack = []
end

#response_headersObject



130
131
132
# File 'lib/capybara/dommy/driver.rb', line 130

def response_headers
  rack_session.headers || {}
end

#send_keys(*keys) ⇒ Object

Session-level send_keys. Without JavaScript only focus navigation is meaningful, so :tab (the key Capybara's focused: specs use) moves focus through the tab order; other keys are ignored.



188
189
190
# File 'lib/capybara/dommy/driver.rb', line 188

def send_keys(*keys)
  keys.each { |key| focus_next_tabbable if key == :tab }
end

#status_codeObject



126
127
128
# File 'lib/capybara/dommy/driver.rb', line 126

def status_code
  rack_session.status
end

#submit_form(form, submitter:) ⇒ Object



196
197
198
# File 'lib/capybara/dommy/driver.rb', line 196

def submit_form(form, submitter:)
  rack_session.submit_form(form, submitter: submitter)
end

#switch_to_frame(frame) ⇒ Object

--- Frames --- Capybara::Session#switch_to_frame drives these with an iframe element node, :parent, or :top. Frame documents are fetched through the dommy-rack session (sharing cookies); nothing here touches the top-level page state, so current_url / title stay top-level.



160
161
162
163
164
165
166
167
168
169
# File 'lib/capybara/dommy/driver.rb', line 160

def switch_to_frame(frame)
  case frame
  when :top
    @frame_stack = []
  when :parent
    frame_stack.pop
  else
    frame_stack.push(load_frame(frame.native))
  end
end

#titleObject

The title of the top-level browsing context, even inside a frame (Capybara's #title contract); the current frame's title is #frame_title.



121
122
123
124
# File 'lib/capybara/dommy/driver.rb', line 121

def title
  pump!
  rack_session.document&.title
end

#visible?(element) ⇒ Boolean

Visibility decision used by Node#visible?. :all / :none treat every element as visible; :html defers to dommy-rack's HTML-level check.

Returns:

  • (Boolean)


245
246
247
248
249
# File 'lib/capybara/dommy/driver.rb', line 245

def visible?(element)
  return true if @visibility == :all || @visibility == :none

  ::Dommy::Rack.visible?(element)
end

#visit(path) ⇒ Object

--- Navigation ---



87
88
89
90
91
92
93
94
# File 'lib/capybara/dommy/driver.rb', line 87

def visit(path)
  @frame_stack = []
  # A fresh visit resolves a relative path against the host root (not the
  # current page's directory), matching browser address-bar semantics.
  rack_session.visit(::URI.join("#{effective_host}/", path.to_s).to_s)
rescue URI::InvalidURIError
  rack_session.visit(path)
end

#wait?Boolean

Returns:

  • (Boolean)


208
209
210
# File 'lib/capybara/dommy/driver.rb', line 208

def wait?
  !@time_pump.nil?
end