Class: Dommy::Rack::Session
- Inherits:
-
Object
- Object
- Dommy::Rack::Session
- Includes:
- Interaction::Driver
- Defined in:
- lib/dommy/rack/session.rb,
sig/dommy/rack.rbs
Overview
A single browser-like session over a Rack application.
Defined Under Namespace
Classes: Config
Constant Summary collapse
- DEFAULT_ACCEPT =
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
Class Attribute Summary collapse
-
.javascript_runtime_factory ⇒ Object
Returns the value of attribute javascript_runtime_factory.
Instance Attribute Summary collapse
-
#history ⇒ History
readonly
Returns the value of attribute history.
-
#last_request ⇒ Object
readonly
Returns the value of attribute last_request.
-
#last_response ⇒ Response?
readonly
Returns the value of attribute last_response.
-
#subresource_host_blocker ⇒ Object
An embedder-owned denylist predicate consulted before any subresource is fetched (even in
:openmode). -
#trace ⇒ Object
readonly
Returns the value of attribute trace.
Class Method Summary collapse
-
.resolve_load_stylesheets(load_stylesheets, javascript) ⇒ Object
Fetching external CSS (so class-based visibility matches a browser) is browser-spec behavior, not plain rack_test: an unset
load_stylesheetsfollowsjavascript(a browser spec runs page JS too).
Instance Method Summary collapse
- #__enqueue_page_navigation__(window, nav) ⇒ Object
- #__enqueue_page_traverse__(window, delta) ⇒ Object
-
#__flush_page_navigation__ ⇒ Object
Perform a recorded page navigation, if any.
-
#__internal_js_runtime ⇒ Object
The bound JS runtime (a SessionRuntime), or nil when JS is disabled.
-
#__internal_record_blocked_subresource(host) ⇒ Object
Internal: Resources records a declined cross-origin host here.
-
#__internal_record_dropped_subresource(host) ⇒ Object
Internal: Resources records a denylist-dropped host here.
-
#__internal_websocket_connector(window) ⇒ Object
Factory for the window's websocket_connector seam (installed per realm by SessionRuntime): a same-origin
new WebSocket(url)connects to the Rack app itself over rack.hijack (see WebSocketTransport), so ActionCable-backed features (Turbo Streams broadcasts, …) work in-process. -
#__navigation_delegate_for__(window) ⇒ Object
The delegate attached to each page's window (in SessionRuntime).
-
#advance_time(ms) ⇒ Object
Advance virtual time by
ms, running timers that come due, then settle. -
#after_interaction ⇒ Object
After an interaction's events are dispatched (Ruby-side, synchronously invoking JS handlers), drain the JS runtime so promise reactions settle before the next line.
- #all_css(selector) ⇒ Object
- #all_xpath(xpath) ⇒ Object
-
#allow_subresource_host(host) ⇒ Object
--- Cross-origin subresource policy ---.
-
#apply_navigation_response(response, final_url, push_history: true, replace: false) ⇒ void
Apply a final navigation response: update last_response, current_url, the document (HTML only), and the history stack.
-
#at_css(selector) ⇒ Object
DOM queries.
- #at_xpath(xpath) ⇒ Object
- #attach_file ⇒ Object
-
#authorization_bearer(token) ⇒ self
Bearer-token auth: sets a persistent Authorization header.
-
#back ⇒ Response?
Traverse the joint history like a browser's back button: a same-document target within the LIVE page (a Turbo Drive pushState entry) moves window.history and fires popstate — Turbo's restoration visit runs, no full request; a target across a document boundary re-requests the URL.
-
#basic_auth(user, password) ⇒ self
HTTP Basic auth: sets a persistent Authorization header.
-
#blocked_subresource_hosts ⇒ Object
Cross-origin hosts whose subresources were declined since the last reset, so a UI can offer to allow them.
- #body ⇒ String?
-
#build_subresource_fetch_job(target, method:, headers: {}, body: nil, redirect: :follow) ⇒ Object
Build a worker-safe thunk that fetches
target(already absolute) and returns the Response, for the async-network path. - #check ⇒ Object
- #choose ⇒ Object
- #clear_cookies ⇒ void
-
#click_button(locator) ⇒ Object
--- Form submission ---.
-
#click_link(locator) ⇒ Object
links and forms.
- #click_link_element(element) ⇒ Object
- #client_error? ⇒ Boolean
- #config ⇒ Object
- #console ⇒ Object
-
#cookies ⇒ Array[untyped]
cookies.
- #current_host ⇒ String?
- #current_path ⇒ String?
-
#current_url ⇒ String?
current page state.
-
#default_headers ⇒ headers
persistent headers / auth.
-
#default_host ⇒ String
config readers.
- #delete(path, params: nil, body: nil, headers: {}) ⇒ Response
- #delete_header(name) ⇒ self
- #delete_json(path, data, headers: {}) ⇒ Response
-
#dispose ⇒ Object
Full session teardown: the JS runtime(s) plus any live WebSocket transports.
-
#dispose_js ⇒ Object
Dispose the JS runtime(s) only.
- #document ⇒ Object
-
#dropped_subresource_hosts ⇒ Object
Hosts the denylist dropped this page.
- #enforce_same_origin? ⇒ Boolean
-
#evaluate_script(script) ⇒ Object
Evaluate JS and return the value (DOM nodes decoded to Dommy objects).
-
#execute_script(script) ⇒ Object
Run JS for side effects against the current document's realm.
-
#export_cookies ⇒ Object
Round-trip the whole jar (for a persistent cookie store): #export_cookies returns plain Hashes, #import_cookies restores them preserving host_only.
-
#external_network_pending? ⇒ Boolean
Whether an off-thread network completion is waiting to be applied on the page thread (a worker finished and posted its response to the scheduler inbox, not yet drained).
-
#fetch(url, method: "GET", headers: {}, body: nil, params: nil, redirect: :follow) ⇒ Response
--- Fetch API (returns Response; does NOT change document or history) ---.
- #fetch_stylesheet_text(href) ⇒ Object
- #fill_in ⇒ Object
- #follow_meta_refresh? ⇒ Boolean
- #follow_redirects? ⇒ Boolean
- #forward ⇒ Response?
-
#get(path, headers: {}) ⇒ Response
--- Basic request API (navigates, updating page state) ---.
- #get_cookie(name) ⇒ String?
- #has_button? ⇒ Boolean
- #has_css? ⇒ Boolean
- #has_field? ⇒ Boolean
- #has_link? ⇒ Boolean
- #has_no_css? ⇒ Boolean
- #has_no_text? ⇒ Boolean
- #has_text? ⇒ Boolean
- #headers ⇒ headers?
- #html ⇒ String?
- #import_cookies(entries) ⇒ Object
-
#initialize(app, default_host: "http://example.org", follow_redirects: true, max_redirects: 5, respect_method_override: true, method_override_param: "_method", user_agent: "DommyRack", accept: DEFAULT_ACCEPT, enforce_same_origin: true, follow_meta_refresh: true, load_stylesheets: nil, javascript: false, network_executor: nil, cross_origin_subresources: :same_origin, approximate_layout: false, trace: false, trace_level: :verbose, trace_dom: false, trace_snapshots: false) ⇒ Session
constructor
A new instance of Session.
-
#install_history_sync(window) ⇒ Object
Mirror the page's same-document history operations (Turbo Drive's pushState navigations, JS history.back()) into the session: the joint history gains/updates entries and current_url follows, so
browser.current_pathandbrowser.backsee what a browser's URL bar and back button would. -
#install_stylesheet_loading(window) ⇒ Object
Wire same-origin CSS loading for a freshly installed document: fill sheets eagerly and register an @import resolver (pulled lazily when the cascade hits an @import).
-
#javascript? ⇒ Boolean
Whether this session runs page JavaScript (created with
javascript: true). -
#js_errors ⇒ Object
Uncaught JS errors / unhandled rejections and console output collected by the JS runtime ([] when JS is disabled).
-
#json(symbolize_names: false) ⇒ Object
Parsed JSON of the most recent response, or nil if no request yet.
-
#load_document_stylesheets(window) ⇒ Object
Resolve each same-origin
<link rel=stylesheet>through the app and fill its sheet (Dommy fetches nothing on its own). - #load_stylesheets? ⇒ Boolean
- #max_redirects ⇒ Integer
- #navigate(method: "GET", url:, params: nil, body: nil, headers: {}, replace: false) ⇒ Response
- #network_executor ⇒ Object
- #not_found? ⇒ Boolean
-
#on_document_loaded(&block) ⇒ Object
Register a callback invoked with the new Window each time a navigation installs an HTML document (visit, redirects, link clicks, form submits, back/forward, reload, meta refresh).
-
#on_request {|env| ... } ⇒ self
instrumentation.
-
#on_response {|response| ... } ⇒ self
Register a callback invoked with the Response after each request.
-
#open_subresources? ⇒ Boolean
Whether cross-origin subresources load freely (browser-like), with the backend SSRF guard as the only boundary.
-
#page_exchange ⇒ Object
The HttpExchange bound to this page: it reads the live header store and routes per-request observation back onto the page thread (record last_request, fire request/response listeners).
- #patch(path, params: nil, body: nil, headers: {}) ⇒ Response
- #patch_json(path, data, headers: {}) ⇒ Response
- #post(path, params: nil, body: nil, headers: {}) ⇒ Response
-
#post_json(path, data, headers: {}) ⇒ Response
JSON.
- #put(path, params: nil, body: nil, headers: {}) ⇒ Response
- #put_json(path, data, headers: {}) ⇒ Response
-
#raw_request(method, absolute_url, params: nil, body: nil, headers: {}) ⇒ Response
collaboration API used by Navigation.
- #redirected? ⇒ Boolean
-
#redirects ⇒ Array[redirect_entry]
redirect chain.
- #reload ⇒ Response
- #request(method, path, params: nil, body: nil, headers: {}) ⇒ Response
- #reset_blocked_subresource_hosts ⇒ Object
- #reset_dropped_subresource_hosts ⇒ Object
-
#save_page(path = nil) ⇒ String
Write the current page HTML to
path(default: a timestamped file in the system temp dir) and return the path. - #select ⇒ Object
- #server_error? ⇒ Boolean
- #set_cookie(name, value, path: "/", domain: nil, **opts) ⇒ Object
- #set_header(name, value) ⇒ self
-
#settle ⇒ Object
Settle work ready at the current virtual time (microtasks + due-now timers + requestAnimationFrame).
- #status ⇒ Integer?
- #submit_form(form, submitter: nil) ⇒ Response (also: #submit)
- #subresource_host_allowed?(host) ⇒ Boolean
- #subresource_host_blocked?(host) ⇒ Boolean
-
#success? ⇒ Boolean
status predicates.
- #text ⇒ String?
- #uncheck ⇒ Object
- #unselect ⇒ Object
-
#visit(path, settle: true) ⇒ Response
navigation.
-
#within ⇒ void
scoping and matchers.
-
#within_frame(locator = nil, &block) ⇒ void
Load the iframe matched by
locator(id, name, or CSS; the sole frame if omitted) and scope finds/matchers to its document for the block.
Constructor Details
#initialize(app, default_host: "http://example.org", follow_redirects: true, max_redirects: 5, respect_method_override: true, method_override_param: "_method", user_agent: "DommyRack", accept: DEFAULT_ACCEPT, enforce_same_origin: true, follow_meta_refresh: true, load_stylesheets: nil, javascript: false, network_executor: nil, cross_origin_subresources: :same_origin, approximate_layout: false, trace: false, trace_level: :verbose, trace_dom: false, trace_snapshots: false) ⇒ Session
Returns a new instance of Session.
69 70 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/dommy/rack/session.rb', line 69 def initialize(app, default_host: "http://example.org", follow_redirects: true, max_redirects: 5, respect_method_override: true, method_override_param: "_method", user_agent: "DommyRack", accept: DEFAULT_ACCEPT, enforce_same_origin: true, follow_meta_refresh: true, load_stylesheets: nil, javascript: false, network_executor: nil, cross_origin_subresources: :same_origin, approximate_layout: false, trace: false, trace_level: :verbose, trace_dom: false, trace_snapshots: false) @app = app # An optional off-thread network executor (responds to # `submit(job) { |result| }`, e.g. dommynx's NetworkPool). When present, # subresource fetch / XHR run on a worker and resolve via a # DeferredResponse; when nil (the default), everything stays synchronous # and deterministic. See #build_subresource_fetch_job. @network_executor = network_executor # Cross-origin subresource policy. `:same_origin` (default) loads only # same-origin `<script src>` / fetch / XHR unless a host is allowlisted # (an embedding browser prompts and opts hosts in). `:open` loads any # cross-origin subresource like a real browser — the network backend's # SSRF guard is then the security boundary (it still cannot reach private # hosts). dommynx defaults to `:open` so modern third-party-bundled sites # are readable; the test/Rails front end keeps the conservative default. @cross_origin_subresources = cross_origin_subresources # Opt the page's windows into best-effort geometry (non-zero # getBoundingClientRect / client* / offset*) — a browser front end turns # this on so sites that bail on all-zero rects render; off by default # keeps the deterministic no-layout contract. Applied per window in # #apply_navigation_response before its scripts boot. @approximate_layout = approximate_layout @config = Config.new( default_host: default_host, follow_redirects: follow_redirects, max_redirects: max_redirects, respect_method_override: respect_method_override, method_override_param: method_override_param, user_agent: user_agent, accept: accept, enforce_same_origin: enforce_same_origin, follow_meta_refresh: , load_stylesheets: self.class.resolve_load_stylesheets(load_stylesheets, javascript) ).freeze @cookie_jar = CookieJar.new @headers = HeaderStore.new @navigation = Navigation.new(self, @config) @history = History.new @current_url = nil @current_window = nil @last_request = nil @last_response = nil @scope_stack = [] @request_listeners = [] @response_listeners = [] @document_loaded_listeners = [] @subresource_allowlist = [] # hosts allowed for cross-origin <script>/fetch/XHR @blocked_subresource_hosts = [] # cross-origin hosts declined since the last reset (awaiting a decision) @dropped_subresource_hosts = [] # hosts dropped by the denylist (deliberate; never prompted) @subresource_host_blocker = nil # embedder-supplied ->(host){bool} denylist (e.g. trackers) @js_runtime = build_js_runtime if javascript # Built last so it can subscribe to the (already-created) JS runtime's # console / js_error / script seams as well as the request/document seams. @trace = Trace.attach(self, level: trace_level, dom: trace_dom, snapshots: trace_snapshots) if trace end |
Class Attribute Details
.javascript_runtime_factory ⇒ Object
Returns the value of attribute javascript_runtime_factory.
58 59 60 |
# File 'lib/dommy/rack/session.rb', line 58 def javascript_runtime_factory @javascript_runtime_factory end |
Instance Attribute Details
#history ⇒ History (readonly)
Returns the value of attribute history.
52 53 54 |
# File 'lib/dommy/rack/session.rb', line 52 def history @history end |
#last_request ⇒ Object (readonly)
Returns the value of attribute last_request.
52 53 54 |
# File 'lib/dommy/rack/session.rb', line 52 def last_request @last_request end |
#last_response ⇒ Response? (readonly)
Returns the value of attribute last_response.
52 53 54 |
# File 'lib/dommy/rack/session.rb', line 52 def last_response @last_response end |
#subresource_host_blocker ⇒ Object
An embedder-owned denylist predicate consulted before any subresource is
fetched (even in :open mode). A text/headless client uses it to drop
tracker/ad hosts it never renders — the host is recorded as blocked so a
UI can still surface it. Generic mechanism only: the host set and the
matching rule (exact / domain-suffix) live in the embedder.
261 262 263 |
# File 'lib/dommy/rack/session.rb', line 261 def subresource_host_blocker @subresource_host_blocker end |
#trace ⇒ Object (readonly)
Returns the value of attribute trace.
52 53 54 |
# File 'lib/dommy/rack/session.rb', line 52 def trace @trace end |
Class Method Details
.resolve_load_stylesheets(load_stylesheets, javascript) ⇒ Object
Fetching external CSS (so class-based
visibility matches a browser) is browser-spec behavior, not plain
rack_test: an unset load_stylesheets follows javascript (a
browser spec runs page JS too). An explicit value always wins.
64 65 66 |
# File 'lib/dommy/rack/session.rb', line 64 def resolve_load_stylesheets(load_stylesheets, javascript) load_stylesheets.nil? ? javascript : load_stylesheets end |
Instance Method Details
#__enqueue_page_navigation__(window, nav) ⇒ Object
355 356 357 358 359 360 |
# File 'lib/dommy/rack/session.rb', line 355 def (window, nav) # A retained handle to a navigated-away page must not steer the session. return unless window.equal?(@current_window) @pending_navigation = nav end |
#__enqueue_page_traverse__(window, delta) ⇒ Object
362 363 364 365 366 |
# File 'lib/dommy/rack/session.rb', line 362 def __enqueue_page_traverse__(window, delta) return unless window.equal?(@current_window) @pending_navigation = {traverse: delta} end |
#__flush_page_navigation__ ⇒ Object
Perform a recorded page navigation, if any. Called after the JS runtime drains so the document/realm swap never runs with the outgoing realm's JS on the stack.
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
# File 'lib/dommy/rack/session.rb', line 371 def nav = @pending_navigation return unless nav @pending_navigation = nil if nav.key?(:traverse) traverse_history(nav[:traverse].negative? ? :back : :forward) else (nav) end rescue CrossOriginError, UnsupportedURLError, InvalidFormError, TooManyRedirectsError # A page-initiated navigation that is blocked, unsupported, or loops on # redirects is dropped (a browser blocks/abandons it); it must not crash # the drain the way a Ruby-driven `visit` (which re-raises) does. nil end |
#__internal_js_runtime ⇒ Object
The bound JS runtime (a SessionRuntime), or nil when JS is disabled. Exposed for the Trace to subscribe to the runtime's console / js_error / script seams; not part of the everyday browsing API.
151 |
# File 'lib/dommy/rack/session.rb', line 151 def __internal_js_runtime = @js_runtime |
#__internal_record_blocked_subresource(host) ⇒ Object
Internal: Resources records a declined cross-origin host here.
297 298 299 300 |
# File 'lib/dommy/rack/session.rb', line 297 def __internal_record_blocked_subresource(host) host = host.to_s @blocked_subresource_hosts << host unless host.empty? || @blocked_subresource_hosts.include?(host) end |
#__internal_record_dropped_subresource(host) ⇒ Object
Internal: Resources records a denylist-dropped host here.
282 283 284 285 |
# File 'lib/dommy/rack/session.rb', line 282 def __internal_record_dropped_subresource(host) host = host.to_s @dropped_subresource_hosts << host unless host.empty? || @dropped_subresource_hosts.include?(host) end |
#__internal_websocket_connector(window) ⇒ Object
Factory for the window's websocket_connector seam (installed per
realm by SessionRuntime): a same-origin new WebSocket(url) connects
to the Rack app itself over rack.hijack (see WebSocketTransport), so
ActionCable-backed features (Turbo Streams broadcasts, …) work
in-process. A cross-origin URL returns nil, leaving the WebSocket on
its in-memory stub.
203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/dommy/rack/session.rb', line 203 def __internal_websocket_connector(window) lambda do |ws, url, _protocols| base = @current_url || default_host target = WebSocketTransport.rack_target(url, base: base) next nil unless target transport = WebSocketTransport.new( app: @app, ws: ws, scheduler: window.scheduler, url: target, origin: Url.origin(target), cookie_string: @cookie_jar.(target.to_s) ) (@live_websocket_transports ||= []) << transport transport end end |
#__navigation_delegate_for__(window) ⇒ Object
The delegate attached to each page's window (in SessionRuntime). A
page-initiated navigation — a JS location.href= / form.submit(), a
submitted form, an activated <a> — routes here. Navigation is a task:
performing it synchronously could dispose the JS realm still on the
stack, so it is recorded and performed at the next drain (#after_interaction
/ #settle), exactly like the standalone Browser.
351 352 353 |
# File 'lib/dommy/rack/session.rb', line 351 def (window) PageNavigationDelegate.new(self, window) end |
#advance_time(ms) ⇒ Object
Advance virtual time by ms, running timers that come due, then settle.
172 173 174 175 |
# File 'lib/dommy/rack/session.rb', line 172 def advance_time(ms) require_js!.advance_time(ms) self end |
#after_interaction ⇒ Object
After an interaction's events are dispatched (Ruby-side, synchronously invoking JS handlers), drain the JS runtime so promise reactions settle before the next line. A no-op when JS is disabled (the mixin default).
827 828 829 830 |
# File 'lib/dommy/rack/session.rb', line 827 def after_interaction @js_runtime&.drain end |
#all_css(selector) ⇒ Object
550 551 552 |
# File 'lib/dommy/rack/session.rb', line 550 def all_css(selector) scope_root&.query_selector_all(selector) end |
#all_xpath(xpath) ⇒ Object
558 559 560 |
# File 'lib/dommy/rack/session.rb', line 558 def all_xpath(xpath) document ? document.xpath(xpath) : [] end |
#allow_subresource_host(host) ⇒ Object
--- Cross-origin subresource policy ---
By default only same-origin <script src> / fetch / XHR load (see
Dommy::Rack::Resources). Hosts allowed here also load — an embedding
browser prompts the user for a blocked host and reloads. The network
backend's SSRF guard still applies, so this never reaches private hosts.
248 249 250 251 252 |
# File 'lib/dommy/rack/session.rb', line 248 def allow_subresource_host(host) host = host.to_s @subresource_allowlist << host unless host.empty? || @subresource_allowlist.include?(host) self end |
#apply_navigation_response(response, final_url, push_history: true, replace: false) ⇒ void
This method returns an undefined value.
Apply a final navigation response: update last_response, current_url, the document (HTML only), and the history stack.
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 |
# File 'lib/dommy/rack/session.rb', line 726 def (response, final_url, push_history: true, replace: false) @last_response = response @current_url = final_url if response.html? @current_window = response.window # Set the geometry mode before scripts boot so the very first # getBoundingClientRect a framework calls already sees it. @current_window.approximate_layout = @approximate_layout if @approximate_layout # Fill external stylesheets before listeners (script boot / # DOMContentLoaded) run, so CSS-driven computed styles and :visible # are correct from the first observation. install_stylesheet_loading(@current_window) if load_stylesheets? # The history entry exists and the window-history sync is live # BEFORE scripts boot: Turbo's replaceState-on-start then lands on # THIS entry, and its pushState navigations append after it. windex = @current_window.history.__internal_index__ if replace # location.replace() / reload() / a redirect: overwrite the current # entry's URL and rebind it to the new document (no new entry). @history.replace_current_url(final_url) @history.rebind_current(window: @current_window, windex: windex) elsif push_history @history.push(final_url, window: @current_window, windex: windex) else # A revisit re-loaded this URL into a fresh document: re-bind the # existing entry so traversal sync matches the live window. @history.rebind_current(window: @current_window, windex: windex) end install_history_sync(@current_window) @document_loaded_listeners.each { |cb| cb.call(@current_window) } elsif replace @history.replace_current_url(final_url) elsif push_history @history.push(final_url) end end |
#at_css(selector) ⇒ Object
DOM queries
546 547 548 |
# File 'lib/dommy/rack/session.rb', line 546 def at_css(selector) scope_root&.query_selector(selector) end |
#at_xpath(xpath) ⇒ Object
554 555 556 |
# File 'lib/dommy/rack/session.rb', line 554 def at_xpath(xpath) document&.at_xpath(xpath) end |
#attach_file ⇒ Object
214 |
# File 'sig/dommy/rack.rbs', line 214
def attach_file: (untyped locator, untyped path) -> untyped
|
#authorization_bearer(token) ⇒ self
Bearer-token auth: sets a persistent Authorization header.
455 456 457 458 |
# File 'lib/dommy/rack/session.rb', line 455 def (token) @headers.bearer(token) self end |
#back ⇒ Response?
Traverse the joint history like a browser's back button: a
same-document target within the LIVE page (a Turbo Drive pushState
entry) moves window.history and fires popstate — Turbo's restoration
visit runs, no full request; a target across a document boundary
re-requests the URL. Returns the destination URL, or nil at the edge.
A JS session may need settle afterwards for the restoration fetch.
335 336 337 |
# File 'lib/dommy/rack/session.rb', line 335 def back traverse_history(:back) end |
#basic_auth(user, password) ⇒ self
HTTP Basic auth: sets a persistent Authorization header.
449 450 451 452 |
# File 'lib/dommy/rack/session.rb', line 449 def basic_auth(user, password) @headers.basic_auth(user, password) self end |
#blocked_subresource_hosts ⇒ Object
Cross-origin hosts whose subresources were declined since the last reset, so a UI can offer to allow them.
289 |
# File 'lib/dommy/rack/session.rb', line 289 def blocked_subresource_hosts = @blocked_subresource_hosts.dup |
#body ⇒ String?
480 |
# File 'lib/dommy/rack/session.rb', line 480 def body = @last_response&.body |
#build_subresource_fetch_job(target, method:, headers: {}, body: nil, redirect: :follow) ⇒ Object
Build a worker-safe thunk that fetches target (already absolute) and
returns the Response, for the async-network path. Called on the page
thread: it enforces same-origin and captures a header snapshot here, then
hands back a Proc that a network worker runs through an HttpExchange
touching only thread-safe state. Per-request observation is routed back
onto the page thread via the scheduler inbox (so the Trace still sees
subresource network); reload bookkeeping is intentionally not touched
(a subresource fetch is not a navigation).
716 717 718 719 720 721 722 |
# File 'lib/dommy/rack/session.rb', line 716 def build_subresource_fetch_job(target, method:, headers: {}, body: nil, redirect: :follow) @navigation.check_same_origin!(target) exchange = build_worker_exchange lambda do @navigation.fetch_resolved(exchange, method, target, headers: headers, body: body, redirect: redirect) end end |
#check ⇒ Object
212 |
# File 'sig/dommy/rack.rbs', line 212
def check: (untyped locator) -> untyped
|
#choose ⇒ Object
211 |
# File 'sig/dommy/rack.rbs', line 211
def choose: (untyped locator) -> untyped
|
#clear_cookies ⇒ void
This method returns an undefined value.
645 |
# File 'lib/dommy/rack/session.rb', line 645 def = @cookie_jar.clear |
#click_button(locator) ⇒ Object
--- Form submission ---
614 615 616 617 618 619 620 621 622 |
# File 'lib/dommy/rack/session.rb', line 614 def (locator) @trace&.__internal_open_action(:click_button, locator) = finder.(locator) # Only submit buttons submit a form. type=button / type=reset are # no-ops here since there is no JavaScript to handle their click. return unless () submit_form(finder.form_for(), submitter: ) end |
#click_link(locator) ⇒ Object
links and forms
588 589 590 591 |
# File 'lib/dommy/rack/session.rb', line 588 def click_link(locator) @trace&.__internal_open_action(:click_link, locator) click_link_element(finder.find_link(locator)) end |
#click_link_element(element) ⇒ Object
593 594 595 596 597 598 599 600 601 602 603 604 605 |
# File 'lib/dommy/rack/session.rb', line 593 def click_link_element(element) href = element.get_attribute("href") raise ElementNotClickableError, "link has no href" if href.nil? scheme = href.split(":", 2).first.to_s.downcase raise UnsupportedURLError, "#{scheme}: URLs are not supported" if %w[javascript mailto].include?(scheme) return document if href.start_with?("#") # in-page fragment: no request target = resolve_document_url(href) return document if same_page_fragment?(target) # url + fragment to current page: no request navigate(method: "GET", url: target, headers: referer_headers) end |
#client_error? ⇒ Boolean
491 |
# File 'lib/dommy/rack/session.rb', line 491 def client_error? = @last_response&.client_error? || false |
#config ⇒ Object
226 |
# File 'lib/dommy/rack/session.rb', line 226 def config = @config |
#console ⇒ Object
181 |
# File 'lib/dommy/rack/session.rb', line 181 def console = @js_runtime ? @js_runtime.console : [] |
#cookies ⇒ Array[untyped]
cookies
637 |
# File 'lib/dommy/rack/session.rb', line 637 def = @cookie_jar.all |
#current_host ⇒ String?
474 475 476 |
# File 'lib/dommy/rack/session.rb', line 474 def current_host @current_url && URI.parse(@current_url).host end |
#current_path ⇒ String?
470 471 472 |
# File 'lib/dommy/rack/session.rb', line 470 def current_path @current_url && URI.parse(@current_url).path end |
#current_url ⇒ String?
current page state
468 |
# File 'lib/dommy/rack/session.rb', line 468 def current_url = @current_url |
#default_headers ⇒ headers
persistent headers / auth
436 |
# File 'lib/dommy/rack/session.rb', line 436 def default_headers = @headers.to_h |
#default_host ⇒ String
config readers
220 |
# File 'lib/dommy/rack/session.rb', line 220 def default_host = @config.default_host |
#delete(path, params: nil, body: nil, headers: {}) ⇒ Response
406 407 408 |
# File 'lib/dommy/rack/session.rb', line 406 def delete(path, params: nil, body: nil, headers: {}) navigate(method: "DELETE", url: path, params: params, body: body, headers: headers) end |
#delete_header(name) ⇒ self
443 444 445 446 |
# File 'lib/dommy/rack/session.rb', line 443 def delete_header(name) @headers.delete(name) self end |
#delete_json(path, data, headers: {}) ⇒ Response
428 429 430 |
# File 'lib/dommy/rack/session.rb', line 428 def delete_json(path, data, headers: {}) request_json("DELETE", path, data, headers: headers) end |
#dispose ⇒ Object
Full session teardown: the JS runtime(s) plus any live WebSocket transports. Safe to call when JS is disabled, and repeatedly.
185 186 187 188 189 |
# File 'lib/dommy/rack/session.rb', line 185 def dispose Array(@live_websocket_transports).each(&:dispose) @live_websocket_transports = nil dispose_js end |
#dispose_js ⇒ Object
Dispose the JS runtime(s) only. Safe to call when JS is disabled.
192 193 194 195 |
# File 'lib/dommy/rack/session.rb', line 192 def dispose_js @js_runtime&.dispose @js_runtime = nil end |
#document ⇒ Object
481 |
# File 'lib/dommy/rack/session.rb', line 481 def document = @current_window&.document |
#dropped_subresource_hosts ⇒ Object
Hosts the denylist dropped this page. Distinct from blocked_subresource_hosts: a dropped host was refused on purpose (a tracker the embedder never wants), so the UI surfaces it but never offers to load it, whereas a blocked host is a cross-origin candidate awaiting a choice.
274 |
# File 'lib/dommy/rack/session.rb', line 274 def dropped_subresource_hosts = @dropped_subresource_hosts.dup |
#enforce_same_origin? ⇒ Boolean
223 |
# File 'lib/dommy/rack/session.rb', line 223 def enforce_same_origin? = @config.enforce_same_origin |
#evaluate_script(script) ⇒ Object
Evaluate JS and return the value (DOM nodes decoded to Dommy objects).
160 |
# File 'lib/dommy/rack/session.rb', line 160 def evaluate_script(script) = require_js!.evaluate(script) |
#execute_script(script) ⇒ Object
Run JS for side effects against the current document's realm.
154 155 156 157 |
# File 'lib/dommy/rack/session.rb', line 154 def execute_script(script) require_js!.execute(script) nil end |
#export_cookies ⇒ Object
Round-trip the whole jar (for a persistent cookie store): #export_cookies returns plain Hashes, #import_cookies restores them preserving host_only.
649 |
# File 'lib/dommy/rack/session.rb', line 649 def = @cookie_jar.export |
#external_network_pending? ⇒ Boolean
Whether an off-thread network completion is waiting to be applied on the page thread (a worker finished and posted its response to the scheduler inbox, not yet drained). A run loop ORs this with its executor's in-flight count to decide whether to keep ticking while async subresources load.
237 238 239 |
# File 'lib/dommy/rack/session.rb', line 237 def external_network_pending? !!@current_window&.scheduler&.external_pending? end |
#fetch(url, method: "GET", headers: {}, body: nil, params: nil, redirect: :follow) ⇒ Response
--- Fetch API (returns Response; does NOT change document or history) ---
462 463 464 |
# File 'lib/dommy/rack/session.rb', line 462 def fetch(url, method: "GET", headers: {}, body: nil, params: nil, redirect: :follow) @navigation.fetch(url, method: method, params: params, body: body, headers: headers, redirect: redirect) end |
#fetch_stylesheet_text(href) ⇒ Object
815 816 817 818 819 820 821 822 |
# File 'lib/dommy/rack/session.rb', line 815 def fetch_stylesheet_text(href) response = fetch(href) return nil unless response&.status.to_i.between?(200, 299) response.body.to_s rescue StandardError nil end |
#fill_in ⇒ Object
210 |
# File 'sig/dommy/rack.rbs', line 210
def fill_in: (untyped locator, with: untyped) -> untyped
|
#follow_meta_refresh? ⇒ Boolean
224 |
# File 'lib/dommy/rack/session.rb', line 224 def = @config. |
#follow_redirects? ⇒ Boolean
221 |
# File 'lib/dommy/rack/session.rb', line 221 def follow_redirects? = @config.follow_redirects |
#forward ⇒ Response?
339 340 341 |
# File 'lib/dommy/rack/session.rb', line 339 def forward traverse_history(:forward) end |
#get(path, headers: {}) ⇒ Response
--- Basic request API (navigates, updating page state) ---
390 391 392 |
# File 'lib/dommy/rack/session.rb', line 390 def get(path, headers: {}) navigate(method: "GET", url: path, headers: headers) end |
#get_cookie(name) ⇒ String?
644 |
# File 'lib/dommy/rack/session.rb', line 644 def (name) = @cookie_jar.get(name) |
#has_button? ⇒ Boolean
204 |
# File 'sig/dommy/rack.rbs', line 204
def has_button?: (untyped locator) -> bool
|
#has_css? ⇒ Boolean
199 |
# File 'sig/dommy/rack.rbs', line 199
def has_css?: (String selector, ?count: Integer?) -> bool
|
#has_field? ⇒ Boolean
205 |
# File 'sig/dommy/rack.rbs', line 205
def has_field?: (untyped locator) -> bool
|
#has_link? ⇒ Boolean
203 |
# File 'sig/dommy/rack.rbs', line 203
def has_link?: (untyped locator) -> bool
|
#has_no_css? ⇒ Boolean
200 |
# File 'sig/dommy/rack.rbs', line 200
def has_no_css?: (String selector, ?count: Integer?) -> bool
|
#has_no_text? ⇒ Boolean
202 |
# File 'sig/dommy/rack.rbs', line 202
def has_no_text?: (untyped string) -> bool
|
#has_text? ⇒ Boolean
201 |
# File 'sig/dommy/rack.rbs', line 201
def has_text?: (untyped string) -> bool
|
#headers ⇒ headers?
479 |
# File 'lib/dommy/rack/session.rb', line 479 def headers = @last_response&.headers |
#html ⇒ String?
525 526 527 |
# File 'lib/dommy/rack/session.rb', line 525 def html document&.to_html end |
#import_cookies(entries) ⇒ Object
650 |
# File 'lib/dommy/rack/session.rb', line 650 def (entries) = entries.each { |attrs| @cookie_jar.import!(attrs) } |
#install_history_sync(window) ⇒ Object
Mirror the page's same-document history operations (Turbo Drive's
pushState navigations, JS history.back()) into the session: the joint
history gains/updates entries and current_url follows, so
browser.current_path and browser.back see what a browser's URL
bar and back button would. Guarded against echo while the session
itself drives a traversal, and against a stale window — a retained
handle to a navigated-away page must not touch the session's state.
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 |
# File 'lib/dommy/rack/session.rb', line 770 def install_history_sync(window) window.history.__internal_on_change__ = lambda do |kind, url| next if @history_traversing next unless window.equal?(@current_window) @current_url = url case kind when :push @history.push(url, window: window, windex: window.history.__internal_index__) when :replace @history.replace_current_url(url) when :traverse @history.sync_to(window, window.history.__internal_index__) end end end |
#install_stylesheet_loading(window) ⇒ Object
Wire same-origin CSS loading for a freshly installed document: fill
sheets eagerly and register an @import resolver (pulled lazily when the cascade hits an @import).
790 791 792 793 794 |
# File 'lib/dommy/rack/session.rb', line 790 def install_stylesheet_loading(window) load_document_stylesheets(window) document = window&.document document.css_import_resolver = ->(url) { fetch_stylesheet_text(url) } if document end |
#javascript? ⇒ Boolean
Whether this session runs page JavaScript (created with javascript: true). When true, navigation boots <script> tags and the interaction
verbs drive JS handlers.
146 |
# File 'lib/dommy/rack/session.rb', line 146 def javascript? = !@js_runtime.nil? |
#js_errors ⇒ Object
Uncaught JS errors / unhandled rejections and console output collected by the JS runtime ([] when JS is disabled). A test integration fails on non-empty js_errors.
180 |
# File 'lib/dommy/rack/session.rb', line 180 def js_errors = @js_runtime ? @js_runtime.js_errors : [] |
#json(symbolize_names: false) ⇒ Object
Parsed JSON of the most recent response, or nil if no request yet.
484 485 486 |
# File 'lib/dommy/rack/session.rb', line 484 def json(symbolize_names: false) @last_response&.json(symbolize_names: symbolize_names) end |
#load_document_stylesheets(window) ⇒ Object
Resolve each same-origin <link rel=stylesheet> through the app and
fill its sheet (Dommy fetches nothing on its own). Best-effort: a
cross-origin href, a non-2xx response, or any fetch error just leaves
that link empty — its rules simply don't apply, never raising mid-load.
800 801 802 803 804 805 806 807 808 809 810 811 812 813 |
# File 'lib/dommy/rack/session.rb', line 800 def load_document_stylesheets(window) document = window&.document return unless document document.query_selector_all("link").each do |link| next unless link.respond_to?(:set_stylesheet_text) href = link.get_attribute("href").to_s next if href.empty? || link.get_attribute("rel").to_s !~ /\bstylesheet\b/i css = fetch_stylesheet_text(href) link.set_stylesheet_text(css) if css end end |
#load_stylesheets? ⇒ Boolean
225 |
# File 'lib/dommy/rack/session.rb', line 225 def load_stylesheets? = @config.load_stylesheets |
#max_redirects ⇒ Integer
222 |
# File 'lib/dommy/rack/session.rb', line 222 def max_redirects = @config.max_redirects |
#navigate(method: "GET", url:, params: nil, body: nil, headers: {}, replace: false) ⇒ Response
317 318 319 |
# File 'lib/dommy/rack/session.rb', line 317 def navigate(method: "GET", url:, params: nil, body: nil, headers: {}, replace: false) @navigation.navigate(method: method, url: url, params: params, body: body, headers: headers, replace: replace) end |
#network_executor ⇒ Object
227 |
# File 'lib/dommy/rack/session.rb', line 227 def network_executor = @network_executor |
#not_found? ⇒ Boolean
493 |
# File 'lib/dommy/rack/session.rb', line 493 def not_found? = @last_response&.not_found? || false |
#on_document_loaded(&block) ⇒ Object
Register a callback invoked with the new Window each time a navigation installs an HTML document (visit, redirects, link clicks, form submits, back/forward, reload, meta refresh). This is the page-load lifecycle seam a JavaScript runtime hooks to install window globals, execute