Class: Capybara::Simulated::Browser
- Inherits:
-
Object
- Object
- Capybara::Simulated::Browser
- Defined in:
- lib/capybara/simulated/browser.rb
Constant Summary collapse
- VENDOR_DIR =
File.('../../../../vendor/js', __FILE__)
- DEFAULT_HOST =
'http://www.example.com'.freeze
- EXTERNAL_SCRIPT_DENYLIST =
The V8 isolate is shared across the lifetime of a Browser instance —only the happy-dom Window is recreated per visit. Reset between specs is via ‘reset!` on the Driver, which closes the Window and clears tracked DOM handles, but keeps the (expensive) isolate alive. On mini_racer + happy-dom the previously-denylisted scripts (notably jQuery UI) load without hanging the runtime, and ready callbacks depend on them succeeding so we no longer skip anything by default.
/\A\z/.freeze
- ASYNC_POLL_STEP_MS =
Capybara’s async-script contract: the user script receives a callback as its final ‘arguments` and must invoke it (possibly after a setTimeout) with the result. We have no real event loop, so after kicking the script off we drive the virtual clock in small slices until the callback fires or the wait budget is up.
50
Instance Attribute Summary collapse
-
#app ⇒ Object
readonly
Returns the value of attribute app.
-
#driver_for_results ⇒ Object
Returns the value of attribute driver_for_results.
-
#response_headers ⇒ Object
readonly
Returns the value of attribute response_headers.
-
#status_code ⇒ Object
readonly
Returns the value of attribute status_code.
Instance Method Summary collapse
- #active_element ⇒ Object
-
#add_modal_handler(type:, text: nil, response: true) ⇒ Object
Append a text-matched handler used by ‘accept_modal` / `dismiss_modal`.
-
#advance_virtual_clock_step(ms) ⇒ Object
Forced advance for callers (e.g. accept_modal) that need to age out async setTimeout-driven side effects without waiting on the wall-clock heuristic in advance_virtual_clock.
- #all_text(id) ⇒ Object
- #attr(id, name) ⇒ Object
- #blur(id) ⇒ Object
-
#capture_pending_modals ⇒ Object
Snapshot the JS-side modalQueue before a navigate wipes it, so a subsequent ‘drain_modal_queue` still sees alerts that fired synchronously from inside the click handler.
- #checked?(id) ⇒ Boolean
- #click(id, modifiers = {}) ⇒ Object
-
#current_url ⇒ Object
window.history.pushState/replaceState only updates the JS-side location; mirror it onto the Ruby @current_url so Capybara reads the new path.
- #disabled?(id) ⇒ Boolean
- #double_click(id, modifiers = {}) ⇒ Object
- #drain_modal_queue ⇒ Object
- #drop(id, items) ⇒ Object
- #encode_modal_handler(type, text, response) ⇒ Object
- #encode_modal_text(text) ⇒ Object
- #evaluate_async_script(code, args = []) ⇒ Object
- #evaluate_script(code, args = []) ⇒ Object
- #execute_script(code, args = []) ⇒ Object
- #find_css(css, context_id = nil) ⇒ Object
- #find_xpath(xpath, context_id = nil) ⇒ Object
- #focus(id) ⇒ Object
-
#format_set_value(value) ⇒ Object
Capybara hands Date/DateTime/Time objects through to the driver as-is but mini_racer can’t serialise them.
- #go_back ⇒ Object
- #go_forward ⇒ Object
- #hover(id) ⇒ Object
- #html ⇒ Object
-
#initialize(app) ⇒ Browser
constructor
A new instance of Browser.
- #inner_html(id) ⇒ Object
-
#modal_inbox ⇒ Object
Long-lived inbox shared by nested ‘accept_modal` / `dismiss_modal` blocks.
- #multiple?(id) ⇒ Boolean
- #outer_html(id) ⇒ Object
- #path(id) ⇒ Object
- #prop(id, name) ⇒ Object
- #readonly?(id) ⇒ Boolean
- #rect(id) ⇒ Object
- #refresh ⇒ Object
- #remove_modal_handler(type:, text: nil) ⇒ Object
-
#reset_state! ⇒ Object
Tear down all per-page state without throwing away the V8 isolate.
- #resolve_visit_url(url) ⇒ Object
- #right_click(id, modifiers = {}) ⇒ Object
- #select_option(id) ⇒ Object
- #selected?(id) ⇒ Boolean
- #send_keys(id, keys) ⇒ Object
- #set_modal_responses(alerts: [], confirms: [], prompts: []) ⇒ Object
- #set_value(id, value) ⇒ Object
- #shadow_root(id) ⇒ Object
- #submit(id) ⇒ Object
- #tag_name(id) ⇒ Object
- #title ⇒ Object
- #trigger(id, evt) ⇒ Object
- #unselect_option(id) ⇒ Object
- #value(id) ⇒ Object
- #visible?(id) ⇒ Boolean
- #visible_text(id) ⇒ Object
-
#visit(url) ⇒ Object
Explicit ‘visit` is a new navigation, not a follow-up — drop the referer the way browsers’ address-bar navigation does, and resolve the URL against the configured app host rather than the current page (which may have a stale ‘<base href>`).
Constructor Details
#initialize(app) ⇒ Browser
Returns a new instance of Browser.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/capybara/simulated/browser.rb', line 46 def initialize(app) @app = app @ctx = MiniRacer::Context.new @ctx.eval(File.read(File.join(VENDOR_DIR, 'prelude.js'))) @ctx.eval(File.read(File.join(VENDOR_DIR, 'csim.bundle.js'))) @ctx.eval(File.read(File.join(VENDOR_DIR, 'runtime.js'))) @ctx.attach('__csim_fetch', method(:js_fetch)) @history = [] @history_index = -1 @current_url = nil @status_code = nil @response_headers = {} @modal_responses = {alert: [], confirm: [], prompt: []} @cookie_jar = [] end |
Instance Attribute Details
#app ⇒ Object (readonly)
Returns the value of attribute app.
26 27 28 |
# File 'lib/capybara/simulated/browser.rb', line 26 def app @app end |
#driver_for_results ⇒ Object
Returns the value of attribute driver_for_results.
44 45 46 |
# File 'lib/capybara/simulated/browser.rb', line 44 def driver_for_results @driver_for_results end |
#response_headers ⇒ Object (readonly)
Returns the value of attribute response_headers.
26 27 28 |
# File 'lib/capybara/simulated/browser.rb', line 26 def response_headers @response_headers end |
#status_code ⇒ Object (readonly)
Returns the value of attribute status_code.
26 27 28 |
# File 'lib/capybara/simulated/browser.rb', line 26 def status_code @status_code end |
Instance Method Details
#active_element ⇒ Object
204 |
# File 'lib/capybara/simulated/browser.rb', line 204 def active_element = call_runtime('activeElement') |
#add_modal_handler(type:, text: nil, response: true) ⇒ Object
Append a text-matched handler used by ‘accept_modal` / `dismiss_modal`. JS-side modal stubs scan the handler stack in registration order and pick the first whose text predicate matches the firing message — which is what enables nested `dismiss_confirm { accept_confirm { … } }`.
263 264 265 266 |
# File 'lib/capybara/simulated/browser.rb', line 263 def add_modal_handler(type:, text: nil, response: true) encoded = encode_modal_handler(type, text, response) call_runtime('pushModalHandler', encoded) end |
#advance_virtual_clock_step(ms) ⇒ Object
Forced advance for callers (e.g. accept_modal) that need to age out async setTimeout-driven side effects without waiting on the wall-clock heuristic in advance_virtual_clock.
315 316 317 |
# File 'lib/capybara/simulated/browser.rb', line 315 def advance_virtual_clock_step(ms) @ctx.call('__csim.drainTimers', ms.to_i) rescue nil end |
#all_text(id) ⇒ Object
128 |
# File 'lib/capybara/simulated/browser.rb', line 128 def all_text(id) = call_runtime('allText', id).to_s |
#attr(id, name) ⇒ Object
133 |
# File 'lib/capybara/simulated/browser.rb', line 133 def attr(id, name) = call_runtime('attr', id, name.to_s) |
#blur(id) ⇒ Object
203 |
# File 'lib/capybara/simulated/browser.rb', line 203 def blur(id) = call_runtime('blur', id) |
#capture_pending_modals ⇒ Object
Snapshot the JS-side modalQueue before a navigate wipes it, so a subsequent ‘drain_modal_queue` still sees alerts that fired synchronously from inside the click handler.
305 306 307 308 309 310 |
# File 'lib/capybara/simulated/browser.rb', line 305 def capture_pending_modals pending = Array(@ctx.call('__csim.drainModalQueue')) rescue [] return if pending.empty? @captured_modals ||= [] @captured_modals.concat(pending) end |
#checked?(id) ⇒ Boolean
137 |
# File 'lib/capybara/simulated/browser.rb', line 137 def checked?(id) = !!call_runtime('checked', id) |
#click(id, modifiers = {}) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/capybara/simulated/browser.rb', line 168 def click(id, modifiers = {}) delay = modifiers.delete('delay') if delay&.positive? call_runtime('mouseDown', id, 0, modifiers) sleep(delay) result = follow(call_runtime('click', id, 0, modifiers, true)) else result = follow(call_runtime('click', id, 0, modifiers)) end drain_async_timers result end |
#current_url ⇒ Object
window.history.pushState/replaceState only updates the JS-side location; mirror it onto the Ruby @current_url so Capybara reads the new path. Before any real visit (or after reset_session!), we have no @current_url, in which case we report ‘nil` rather than leaking the synthetic happy-dom Window URL.
33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/capybara/simulated/browser.rb', line 33 def current_url return '' unless @current_url # Capybara's synchronize loop calls current_path/current_url while # waiting for navigation. Drive the virtual clock so timer-based # location updates (window.location.pathname = '...') eventually # fire under the wait budget. advance_virtual_clock check_location_change unless @in_navigate loc = (@ctx.eval('window && window.location ? window.location.href : null') rescue nil) loc && !loc.empty? ? loc.to_s : @current_url end |
#disabled?(id) ⇒ Boolean
139 |
# File 'lib/capybara/simulated/browser.rb', line 139 def disabled?(id) = !!call_runtime('disabled', id) |
#double_click(id, modifiers = {}) ⇒ Object
191 192 193 194 195 |
# File 'lib/capybara/simulated/browser.rb', line 191 def double_click(id, modifiers = {}) result = call_runtime('doubleClick', id, modifiers) drain_async_timers result end |
#drain_modal_queue ⇒ Object
287 288 289 290 291 292 |
# File 'lib/capybara/simulated/browser.rb', line 287 def drain_modal_queue captured = @captured_modals @captured_modals = nil out = Array(call_runtime('drainModalQueue')) captured ? captured + out : out end |
#drop(id, items) ⇒ Object
206 207 208 |
# File 'lib/capybara/simulated/browser.rb', line 206 def drop(id, items) call_runtime('drop', id, items).tap { drain_async_timers } end |
#encode_modal_handler(type, text, response) ⇒ Object
272 273 274 275 276 277 278 |
# File 'lib/capybara/simulated/browser.rb', line 272 def encode_modal_handler(type, text, response) { 'type' => type.to_s, 'text' => encode_modal_text(text), 'response' => response.is_a?(Symbol) ? response.to_s : response } end |
#encode_modal_text(text) ⇒ Object
280 281 282 283 284 285 286 |
# File 'lib/capybara/simulated/browser.rb', line 280 def encode_modal_text(text) case text when nil then nil when Regexp then {'regexp' => text.source, 'flags' => text.} else text.to_s end end |
#evaluate_async_script(code, args = []) ⇒ Object
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/capybara/simulated/browser.rb', line 237 def evaluate_async_script(code, args = []) call_runtime('startAsync', code.to_s, encode_script_args(args)) budget = (Capybara.default_max_wait_time || 2).to_f deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + budget loop do status = @ctx.call('__csim.pollAsync') if status['done'] raise MiniRacer::RuntimeError, status['error'] if status['error'] return decode_script_result(status['value']) end break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline @ctx.call('__csim.drainTimers', ASYNC_POLL_STEP_MS) rescue nil end raise Capybara::ScriptTimeoutError, 'evaluate_async_script timed out' end |
#evaluate_script(code, args = []) ⇒ Object
227 228 229 |
# File 'lib/capybara/simulated/browser.rb', line 227 def evaluate_script(code, args = []) decode_script_result(call_runtime('evaluate', code.to_s, encode_script_args(args))) end |
#execute_script(code, args = []) ⇒ Object
223 224 225 226 |
# File 'lib/capybara/simulated/browser.rb', line 223 def execute_script(code, args = []) call_runtime('executeScript', code.to_s, encode_script_args(args)) nil end |
#find_css(css, context_id = nil) ⇒ Object
126 |
# File 'lib/capybara/simulated/browser.rb', line 126 def find_css(css, context_id = nil) = Array(call_runtime('findCSS', css, context_id)) |
#find_xpath(xpath, context_id = nil) ⇒ Object
125 |
# File 'lib/capybara/simulated/browser.rb', line 125 def find_xpath(xpath, context_id = nil) = Array(call_runtime('findXPath', xpath, context_id)) |
#focus(id) ⇒ Object
202 |
# File 'lib/capybara/simulated/browser.rb', line 202 def focus(id) = call_runtime('focus', id) |
#format_set_value(value) ⇒ Object
Capybara hands Date/DateTime/Time objects through to the driver as-is but mini_racer can’t serialise them. Pre-format them to the strings an HTML5 input expects so they round-trip via JS.
156 157 158 159 160 161 162 163 164 |
# File 'lib/capybara/simulated/browser.rb', line 156 def format_set_value(value) case value when Array then value.map {|v| format_set_value(v) } when Date then value.strftime('%Y-%m-%d') when DateTime then value.strftime('%Y-%m-%dT%H:%M') when Time then value.strftime('%Y-%m-%dT%H:%M') else value end end |
#go_back ⇒ Object
111 112 113 114 115 |
# File 'lib/capybara/simulated/browser.rb', line 111 def go_back return if @history_index <= 0 @history_index -= 1 navigate(:get, @history[@history_index], [], history_move: true) end |
#go_forward ⇒ Object
116 117 118 119 120 |
# File 'lib/capybara/simulated/browser.rb', line 116 def go_forward return if @history_index >= @history.size - 1 @history_index += 1 navigate(:get, @history[@history_index], [], history_move: true) end |
#hover(id) ⇒ Object
196 197 198 |
# File 'lib/capybara/simulated/browser.rb', line 196 def hover(id) call_runtime('hover', id).tap { drain_async_timers } end |
#html ⇒ Object
122 |
# File 'lib/capybara/simulated/browser.rb', line 122 def html = call_runtime('html') |
#inner_html(id) ⇒ Object
130 |
# File 'lib/capybara/simulated/browser.rb', line 130 def inner_html(id) = call_runtime('innerHTML', id).to_s |
#modal_inbox ⇒ Object
Long-lived inbox shared by nested ‘accept_modal` / `dismiss_modal` blocks. Each driver call drains the JS-side queue once and stashes unmatched modals here so the OUTER block can still find its message after the inner one consumes its own.
298 299 300 |
# File 'lib/capybara/simulated/browser.rb', line 298 def modal_inbox @modal_inbox ||= [] end |
#multiple?(id) ⇒ Boolean
141 |
# File 'lib/capybara/simulated/browser.rb', line 141 def multiple?(id) = !!call_runtime('multiple', id) |
#outer_html(id) ⇒ Object
131 |
# File 'lib/capybara/simulated/browser.rb', line 131 def outer_html(id) = call_runtime('outerHTML', id).to_s |
#path(id) ⇒ Object
142 |
# File 'lib/capybara/simulated/browser.rb', line 142 def path(id) = call_runtime('path', id).to_s |
#prop(id, name) ⇒ Object
134 |
# File 'lib/capybara/simulated/browser.rb', line 134 def prop(id, name) = call_runtime('prop', id, name.to_s) |
#readonly?(id) ⇒ Boolean
140 |
# File 'lib/capybara/simulated/browser.rb', line 140 def readonly?(id) = !!call_runtime('readonly', id) |
#rect(id) ⇒ Object
143 |
# File 'lib/capybara/simulated/browser.rb', line 143 def rect(id) = call_runtime('rect', id) || {} |
#refresh ⇒ Object
102 103 104 105 106 107 108 109 110 |
# File 'lib/capybara/simulated/browser.rb', line 102 def refresh return unless @current_url # Browsers re-issue the previous request as-is on F5 (after a # confirmation prompt for non-GETs, which our driver skips). method = @last_method || :get navigate(method, @current_url, @last_fields || [], enctype: @last_enctype || 'application/x-www-form-urlencoded', replace_history: true) end |
#remove_modal_handler(type:, text: nil) ⇒ Object
268 269 270 |
# File 'lib/capybara/simulated/browser.rb', line 268 def remove_modal_handler(type:, text: nil) call_runtime('popModalHandler', type.to_s, encode_modal_text(text)) end |
#reset_state! ⇒ Object
Tear down all per-page state without throwing away the V8 isolate. Capybara calls this between specs via Driver#reset!.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/capybara/simulated/browser.rb', line 65 def reset_state! @history = [] @history_index = -1 @current_url = nil @status_code = nil @response_headers = {} @modal_responses = {alert: [], confirm: [], prompt: []} @last_call_at = nil @cookie_jar = [] # `loadHTML` clears all tracked node handles and recreates the # happy-dom Window for an empty document. call_runtime('loadHTML', '<!doctype html><html><body></body></html>', DEFAULT_HOST) call_runtime('setModalResponses', stringify_keys(@modal_responses)) end |
#resolve_visit_url(url) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/capybara/simulated/browser.rb', line 88 def resolve_visit_url(url) return @current_url if !url.nil? && !url.empty? && @current_url && url == @current_url url = '/' if url.nil? || url.empty? return url if url.start_with?('http://', 'https://') # Resolve relative paths against the app host's root (not the # currently displayed page), matching Capybara's `visit` semantics # for rack_test-style drivers. host = Capybara.app_host if host.nil? uri = URI.parse(@current_url || DEFAULT_HOST) host = "#{uri.scheme}://#{uri.host}#{uri.port ? ":#{uri.port}" : ''}/" end URI.join(host, url).to_s end |
#right_click(id, modifiers = {}) ⇒ Object
180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/capybara/simulated/browser.rb', line 180 def right_click(id, modifiers = {}) delay = modifiers.delete('delay') if delay&.positive? call_runtime('mouseDown', id, 2, modifiers) sleep(delay) call_runtime('rightClick', id, modifiers, true) else call_runtime('rightClick', id, modifiers) end drain_async_timers end |
#select_option(id) ⇒ Object
165 |
# File 'lib/capybara/simulated/browser.rb', line 165 def select_option(id) = call_runtime('selectOption', id) |
#selected?(id) ⇒ Boolean
138 |
# File 'lib/capybara/simulated/browser.rb', line 138 def selected?(id) = !!call_runtime('selected', id) |
#send_keys(id, keys) ⇒ Object
218 219 220 221 |
# File 'lib/capybara/simulated/browser.rb', line 218 def send_keys(id, keys) directive = call_runtime('sendKeys', id, encode_keys(keys)) follow(directive) end |
#set_modal_responses(alerts: [], confirms: [], prompts: []) ⇒ Object
253 254 255 256 |
# File 'lib/capybara/simulated/browser.rb', line 253 def set_modal_responses(alerts: [], confirms: [], prompts: []) @modal_responses = {alert: Array(alerts), confirm: Array(confirms), prompt: Array(prompts)} call_runtime('setModalResponses', stringify_keys(@modal_responses)) end |
#set_value(id, value) ⇒ Object
145 146 147 148 149 150 151 |
# File 'lib/capybara/simulated/browser.rb', line 145 def set_value(id, value) # setValue may return a submit-descriptor when the user typed `\n` # into the only text input of a form (HTML implicit submission). result = call_runtime('setValue', id, format_set_value(value)) follow(result) if result.is_a?(Hash) && result['action'] result end |
#shadow_root(id) ⇒ Object
216 |
# File 'lib/capybara/simulated/browser.rb', line 216 def shadow_root(id) = call_runtime('shadowRoot', id) |
#submit(id) ⇒ Object
210 211 212 213 214 |
# File 'lib/capybara/simulated/browser.rb', line 210 def submit(id) result = follow(call_runtime('submit', id)) drain_async_timers result end |
#tag_name(id) ⇒ Object
132 |
# File 'lib/capybara/simulated/browser.rb', line 132 def tag_name(id) = call_runtime('tagName', id).to_s |
#title ⇒ Object
123 |
# File 'lib/capybara/simulated/browser.rb', line 123 def title = call_runtime('title') |
#trigger(id, evt) ⇒ Object
199 200 201 |
# File 'lib/capybara/simulated/browser.rb', line 199 def trigger(id, evt) call_runtime('trigger', id, evt.to_s).tap { drain_async_timers } end |
#unselect_option(id) ⇒ Object
166 |
# File 'lib/capybara/simulated/browser.rb', line 166 def unselect_option(id) = call_runtime('unselectOption', id) |
#value(id) ⇒ Object
135 |
# File 'lib/capybara/simulated/browser.rb', line 135 def value(id) = call_runtime('value', id) |
#visible?(id) ⇒ Boolean
136 |
# File 'lib/capybara/simulated/browser.rb', line 136 def visible?(id) = !!call_runtime('visible', id) |
#visible_text(id) ⇒ Object
129 |
# File 'lib/capybara/simulated/browser.rb', line 129 def visible_text(id) = call_runtime('visibleText', id).to_s |
#visit(url) ⇒ Object
Explicit ‘visit` is a new navigation, not a follow-up — drop the referer the way browsers’ address-bar navigation does, and resolve the URL against the configured app host rather than the current page (which may have a stale ‘<base href>`).
84 85 86 |
# File 'lib/capybara/simulated/browser.rb', line 84 def visit(url) navigate(:get, resolve_visit_url(url), [], referer: false) end |