Class: Dommy::Browser
- Inherits:
-
Object
- Object
- Dommy::Browser
- Includes:
- Interaction::Driver
- Defined in:
- lib/dommy/browser.rb
Overview
A lightweight test browser: parse HTML, build window/document, run its
classic <script> tags (inline + external via a resources adapter), fire
DOMContentLoaded/load, and collect JS errors / console output. For
standalone HTML + JS (bundled SPA, fixture HTML); the Rack/Rails entry point
is Dommy::Rack::Session (a later phase).
Dommy::Browser.open(html, resources: Dommy::Resources.static("/app.js" => "...")) do |b|
b.settle
b.evaluate('document.querySelector("h1").textContent')
end
JS errors are not swallowed: in strict mode (default) any unhandled rejection
or uncaught script error fails at the next checkpoint (after boot, after
settle, at dispose). Wrap intentional errors in allow_js_errors { … }.
Defined Under Namespace
Classes: JsError
Instance Attribute Summary collapse
-
#console ⇒ Object
readonly
Returns the value of attribute console.
-
#history ⇒ Object
readonly
The joint (tab) history of a navigable browser, or nil for a plain single-document browser.
-
#js_errors ⇒ Object
readonly
Returns the value of attribute js_errors.
-
#runtime ⇒ Object
readonly
Returns the value of attribute runtime.
-
#window ⇒ Object
readonly
Returns the value of attribute window.
Class Method Summary collapse
-
.open(html, **opts) ⇒ Object
Build a browser and (unless
execute_scripts: false) boot its scripts. -
.visit(url, resources:, **opts) ⇒ Object
Start a navigable session by fetching the initial document from
resources, rather than passing literal HTML.
Instance Method Summary collapse
-
#advance_time(ms) ⇒ Object
Advance virtual time by
ms, running timers that come due, then settle. -
#after_interaction ⇒ Object
An interaction's events have been dispatched (Ruby-side, synchronously invoking JS handlers); drain the runtime's microtasks so promise reactions land before the next line, then enforce strict mode.
-
#allow_js_errors ⇒ Object
Suppress strict-mode failure for JS errors raised inside the block (they stay collected in #js_errors for inspection).
-
#back ⇒ Object
Move back / forward one entry in the joint history.
-
#click_button(locator) ⇒ Object
Click a submit-capable button.
-
#click_link(locator) ⇒ Object
Click a link, firing its click event so SPA JS (Turbo, React Router, …) can intercept.
-
#current_url ⇒ Object
The current document's URL (the address bar).
- #dispose ⇒ Object
- #document ⇒ Object
-
#evaluate(js) ⇒ Object
Evaluate an expression / statement body and return the decoded value.
-
#execute(js) ⇒ Object
Run JS for side effects.
- #forward ⇒ Object
-
#html ⇒ Object
Current document HTML (serialized).
-
#initialize(html, url: "http://localhost/", resources: nil, execute_scripts: true, strict: true, settle: true, wasm_memory_shim: false, backend: nil, navigable: false, same_origin: false) ⇒ Browser
constructor
A new instance of Browser.
-
#navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false) ⇒ Object
A cross-document navigation intent (link / form / location).
-
#reload ⇒ Object
Reload the current document (re-fetch, replace the current history entry).
-
#settle ⇒ Object
Settle the work ready at the current virtual time: drain microtasks, run due-now timers, flush requestAnimationFrame.
-
#traverse(delta) ⇒ Object
A cross-document history traversal.
-
#visit(url, replace: false) ⇒ Object
Programmatically navigate to
url(a Ruby-initiated visit).
Methods included from Interaction::Driver
#all, #all_by_role, #attach_file, #check, #choose, #click, #debug, #field_interactor, #fill_in, #find, #find_by_role, #finder, #has_button?, #has_css?, #has_field?, #has_link?, #has_no_css?, #has_no_role?, #has_no_text?, #has_role?, #has_text?, #ime_input, #scope_root, #scope_text, #select, #send_keys, #send_keys_to, #uncheck, #unselect, #with_scope, #within
Constructor Details
#initialize(html, url: "http://localhost/", resources: nil, execute_scripts: true, strict: true, settle: true, wasm_memory_shim: false, backend: nil, navigable: false, same_origin: false) ⇒ Browser
Returns a new instance of Browser.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/dommy/browser.rb', line 71 def initialize(html, url: "http://localhost/", resources: nil, execute_scripts: true, strict: true, settle: true, wasm_memory_shim: false, backend: nil, navigable: false, same_origin: false) @resources = resources @same_origin = same_origin @strict = strict @backend = backend @execute_scripts = execute_scripts @settle_after_boot = settle @wasm_memory_shim = wasm_memory_shim @navigable = navigable @js_errors = [] @console = [] @acknowledged = 0 @allow_errors = false @disposed = false @pending_navigation = nil @runtime = nil @window = Dommy.parse(html) @window.location.__internal_set_url__(url) if url install_runtime(@window) if navigable @fetcher = Navigation::Fetcher.new(@resources, same_origin: @same_origin) @history = Navigation::JointHistory.new @window. = self @history.push(current_url, window: @window, windex: @window.history.__internal_index__) end check_js_errors! end |
Instance Attribute Details
#console ⇒ Object (readonly)
Returns the value of attribute console.
43 44 45 |
# File 'lib/dommy/browser.rb', line 43 def console @console end |
#history ⇒ Object (readonly)
The joint (tab) history of a navigable browser, or nil for a plain single-document browser.
112 113 114 |
# File 'lib/dommy/browser.rb', line 112 def history @history end |
#js_errors ⇒ Object (readonly)
Returns the value of attribute js_errors.
43 44 45 |
# File 'lib/dommy/browser.rb', line 43 def js_errors @js_errors end |
#runtime ⇒ Object (readonly)
Returns the value of attribute runtime.
43 44 45 |
# File 'lib/dommy/browser.rb', line 43 def runtime @runtime end |
#window ⇒ Object (readonly)
Returns the value of attribute window.
43 44 45 |
# File 'lib/dommy/browser.rb', line 43 def window @window end |
Class Method Details
.open(html, **opts) ⇒ Object
Build a browser and (unless execute_scripts: false) boot its scripts. In
block form the browser is yielded and disposed afterward, returning the
block value.
48 49 50 51 52 53 54 55 56 57 |
# File 'lib/dommy/browser.rb', line 48 def self.open(html, **opts) browser = new(html, **opts) return browser unless block_given? begin yield browser ensure browser.dispose end end |
.visit(url, resources:, **opts) ⇒ Object
Start a navigable session by fetching the initial document from
resources, rather than passing literal HTML. Links / forms / location
then perform real cross-document navigation (fetch → replace Window + JS
realm), with the browser handle (history, resources, error log) surviving.
Dommy::Browser.visit("http://localhost/", resources: my_resources)
64 65 66 67 68 69 |
# File 'lib/dommy/browser.rb', line 64 def self.visit(url, resources:, **opts) browser = new("<!doctype html><html><head></head><body></body></html>", url: "about:blank", resources: resources, navigable: true, **opts) browser.visit(url, replace: true) browser end |
Instance Method Details
#advance_time(ms) ⇒ Object
Advance virtual time by ms, running timers that come due, then settle.
196 197 198 199 200 201 202 |
# File 'lib/dommy/browser.rb', line 196 def advance_time(ms) @window.scheduler.advance_time(ms) @runtime.drain_microtasks check_js_errors! self end |
#after_interaction ⇒ Object
An interaction's events have been dispatched (Ruby-side, synchronously invoking JS handlers); drain the runtime's microtasks so promise reactions land before the next line, then enforce strict mode.
207 208 209 210 211 |
# File 'lib/dommy/browser.rb', line 207 def after_interaction @runtime.drain_microtasks check_js_errors! end |
#allow_js_errors ⇒ Object
Suppress strict-mode failure for JS errors raised inside the block (they stay collected in #js_errors for inspection). For tests that expect errors.
241 242 243 244 245 246 247 248 |
# File 'lib/dommy/browser.rb', line 241 def allow_js_errors prev = @allow_errors @allow_errors = true yield ensure @allow_errors = prev @acknowledged = @js_errors.length end |
#back ⇒ Object
Move back / forward one entry in the joint history. A same-document target (the entry's window is still live) traverses in place (popstate); a document-boundary target is re-fetched (no bfcache — D2).
133 |
# File 'lib/dommy/browser.rb', line 133 def back = traverse(-1) |
#click_button(locator) ⇒ Object
Click a submit-capable button. The button's click event fires (JS may handle / preventDefault it); if it is an un-prevented submit button, the owning form's submission algorithm runs (a real SubmitEvent a SPA can intercept, then the delegate navigation). In a navigable browser that follows the submit for real; otherwise the delegate just records it.
218 219 220 221 222 223 224 225 226 |
# File 'lib/dommy/browser.rb', line 218 def (locator) = finder.(locator) # An un-prevented click runs the button's activation behavior — a submit # button submits its owning form (real SubmitEvent + delegate navigation), # so a navigable browser follows the submit for real. Dommy::Interaction::EventSynthesis.click() after_interaction end |
#click_link(locator) ⇒ Object
Click a link, firing its click event so SPA JS (Turbo, React Router, …) can intercept. An un-prevented click runs the anchor's activation behavior (follow-the-hyperlink); in a navigable browser that navigates for real, otherwise the delegate records it.
232 233 234 235 236 237 |
# File 'lib/dommy/browser.rb', line 232 def click_link(locator) link = finder.find_link(locator) Dommy::Interaction::EventSynthesis.click(link) after_interaction link end |
#current_url ⇒ Object
The current document's URL (the address bar).
108 |
# File 'lib/dommy/browser.rb', line 108 def current_url = @window.location.href |
#dispose ⇒ Object
250 251 252 253 254 255 256 257 |
# File 'lib/dommy/browser.rb', line 250 def dispose return if @disposed @disposed = true pending = unacknowledged @runtime&.dispose raise JsError, pending if @strict && !pending.empty? end |
#document ⇒ Object
102 |
# File 'lib/dommy/browser.rb', line 102 def document = @window.document |
#evaluate(js) ⇒ Object
Evaluate an expression / statement body and return the decoded value.
172 173 174 175 176 |
# File 'lib/dommy/browser.rb', line 172 def evaluate(js) result = @runtime.evaluate(js) check_js_errors! result end |
#execute(js) ⇒ Object
Run JS for side effects.
179 180 181 182 183 |
# File 'lib/dommy/browser.rb', line 179 def execute(js) @runtime.execute(js) check_js_errors! nil end |
#forward ⇒ Object
134 |
# File 'lib/dommy/browser.rb', line 134 def forward = traverse(1) |
#html ⇒ Object
Current document HTML (serialized).
105 |
# File 'lib/dommy/browser.rb', line 105 def html = @window.document.document_element&.outer_html |
#navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false) ⇒ Object
A cross-document navigation intent (link / form / location). Navigation is
a task: rather than swap the Window + JS realm synchronously (which may run
while the outgoing realm's JS is still on the stack — e.g. location.href =
inside a script), record it and perform the fetch + swap at the next drain
boundary (settle / after_interaction / advance_time). Ruby-initiated visits
flush immediately since no JS is on the stack.
144 145 146 147 148 149 150 |
# File 'lib/dommy/browser.rb', line 144 def navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false) @pending_navigation = { url: url, method: method, body: body, params: params, enctype: enctype, headers: headers, replace: replace, source: source } nil end |
#reload ⇒ Object
Reload the current document (re-fetch, replace the current history entry).
126 127 128 |
# File 'lib/dommy/browser.rb', line 126 def reload visit(current_url, replace: true) end |
#settle ⇒ Object
Settle the work ready at the current virtual time: drain microtasks, run
due-now timers, flush requestAnimationFrame. Does NOT fire a future
setTimeout(300) — use advance_time(300) for debounce/throttle.
188 189 190 191 192 193 |
# File 'lib/dommy/browser.rb', line 188 def settle @runtime.settle check_js_errors! self end |
#traverse(delta) ⇒ Object
A cross-document history traversal. Ruby-initiated (back / forward), so it runs immediately: a same-document target (its window is still live) traverses in place (popstate); a document-boundary target is re-fetched.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/dommy/browser.rb', line 155 def traverse(delta) return self unless @navigable entry = delta.negative? ? @history.back : @history.forward return self unless entry if entry.window && entry.window.equal?(@window) @window.history.__internal_go_to__(entry.windex) @runtime.drain_microtasks check_js_errors! else ({url: entry.url, method: "GET", source: :traverse}, rebind: true) end self end |
#visit(url, replace: false) ⇒ Object
Programmatically navigate to url (a Ruby-initiated visit). Only
meaningful for a navigable browser; performs the fetch + document swap
immediately (there is no JS on the stack).
117 118 119 120 121 122 123 |
# File 'lib/dommy/browser.rb', line 117 def visit(url, replace: false) raise "browser is not navigable (use Browser.visit or navigable: true)" unless @navigable @pending_navigation = {url: url.to_s, method: "GET", source: :visit, replace: replace} self end |