Class: Dommy::Rack::Session

Inherits:
Object
  • Object
show all
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 =

Returns:

  • (String)
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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.

Parameters:

  • app (Object)
  • default_host: (String) (defaults to: "http://example.org")
  • follow_redirects: (Boolean) (defaults to: true)
  • max_redirects: (Integer) (defaults to: 5)
  • respect_method_override: (Boolean) (defaults to: true)
  • method_override_param: (String) (defaults to: "_method")
  • user_agent: (String) (defaults to: "DommyRack")
  • accept: (String) (defaults to: DEFAULT_ACCEPT)
  • enforce_same_origin: (Boolean) (defaults to: true)
  • follow_meta_refresh: (Boolean) (defaults to: true)


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: 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_factoryObject

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

#historyHistory (readonly)

Returns the value of attribute history.

Returns:



52
53
54
# File 'lib/dommy/rack/session.rb', line 52

def history
  @history
end

#last_requestObject (readonly)

Returns the value of attribute last_request.

Returns:

  • (Object)


52
53
54
# File 'lib/dommy/rack/session.rb', line 52

def last_request
  @last_request
end

#last_responseResponse? (readonly)

Returns the value of attribute last_response.

Returns:



52
53
54
# File 'lib/dommy/rack/session.rb', line 52

def last_response
  @last_response
end

#subresource_host_blockerObject

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

#traceObject (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 __enqueue_page_navigation__(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 __flush_page_navigation__
  nav = @pending_navigation
  return unless nav

  @pending_navigation = nil
  if nav.key?(:traverse)
    traverse_history(nav[:traverse].negative? ? :back : :forward)
  else
    perform_page_navigation(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_runtimeObject

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.cookies_for(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 __navigation_delegate_for__(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_interactionObject

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
  __flush_page_navigation__
end

#all_css(selector) ⇒ Object

Parameters:

  • selector (String)

Returns:

  • (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

Parameters:

  • xpath (String)

Returns:

  • (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.

Parameters:

  • response (Response)
  • final_url (String)
  • push_history: (Boolean) (defaults to: true)


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 apply_navigation_response(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

Parameters:

  • selector (String)

Returns:

  • (Object)


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

Parameters:

  • xpath (String)

Returns:

  • (Object)


554
555
556
# File 'lib/dommy/rack/session.rb', line 554

def at_xpath(xpath)
  document&.at_xpath(xpath)
end

#attach_fileObject

Parameters:

  • locator (Object)
  • path (Object)

Returns:

  • (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.

Parameters:

  • token (Object)

Returns:

  • (self)


455
456
457
458
# File 'lib/dommy/rack/session.rb', line 455

def authorization_bearer(token)
  @headers.bearer(token)
  self
end

#backResponse?

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.

Returns:



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.

Parameters:

  • user (Object)
  • password (Object)

Returns:

  • (self)


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_hostsObject

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

#bodyString?

Returns:

  • (String, nil)


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

#checkObject

Parameters:

  • locator (Object)

Returns:

  • (Object)


212
# File 'sig/dommy/rack.rbs', line 212

def check: (untyped locator) -> untyped

#chooseObject

Parameters:

  • locator (Object)

Returns:

  • (Object)


211
# File 'sig/dommy/rack.rbs', line 211

def choose: (untyped locator) -> untyped

#clear_cookiesvoid

This method returns an undefined value.



645
# File 'lib/dommy/rack/session.rb', line 645

def clear_cookies = @cookie_jar.clear

#click_button(locator) ⇒ Object

--- Form submission ---

Parameters:

  • locator (Object)

Returns:

  • (Object)


614
615
616
617
618
619
620
621
622
# File 'lib/dommy/rack/session.rb', line 614

def click_button(locator)
  @trace&.__internal_open_action(:click_button, locator)
  button = finder.find_button(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 button unless submit_button?(button)

  submit_form(finder.form_for(button), submitter: button)
end

links and forms

Parameters:

  • locator (Object)

Returns:

  • (Object)


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

Parameters:

  • element (Object)

Returns:

  • (Object)

Raises:



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

Returns:

  • (Boolean)


491
# File 'lib/dommy/rack/session.rb', line 491

def client_error? = @last_response&.client_error? || false

#configObject

Returns:

  • (Object)


226
# File 'lib/dommy/rack/session.rb', line 226

def config = @config

#consoleObject



181
# File 'lib/dommy/rack/session.rb', line 181

def console = @js_runtime ? @js_runtime.console : []

#cookiesArray[untyped]

cookies

Returns:

  • (Array[untyped])


637
# File 'lib/dommy/rack/session.rb', line 637

def cookies = @cookie_jar.all

#current_hostString?

Returns:

  • (String, nil)


474
475
476
# File 'lib/dommy/rack/session.rb', line 474

def current_host
  @current_url && URI.parse(@current_url).host
end

#current_pathString?

Returns:

  • (String, nil)


470
471
472
# File 'lib/dommy/rack/session.rb', line 470

def current_path
  @current_url && URI.parse(@current_url).path
end

#current_urlString?

current page state

Returns:

  • (String, nil)


468
# File 'lib/dommy/rack/session.rb', line 468

def current_url = @current_url

#default_headersheaders

persistent headers / auth

Returns:



436
# File 'lib/dommy/rack/session.rb', line 436

def default_headers = @headers.to_h

#default_hostString

config readers

Returns:

  • (String)


220
# File 'lib/dommy/rack/session.rb', line 220

def default_host = @config.default_host

#delete(path, params: nil, body: nil, headers: {}) ⇒ Response

Parameters:

  • path (Object)
  • params: (params, nil) (defaults to: nil)
  • body: (Object) (defaults to: nil)
  • headers: (headers) (defaults to: {})

Returns:



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

Parameters:

  • name (Object)

Returns:

  • (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

Parameters:

  • path (Object)
  • data (Object)
  • headers: (headers) (defaults to: {})

Returns:



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

#disposeObject

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_jsObject

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

#documentObject

Returns:

  • (Object)


481
# File 'lib/dommy/rack/session.rb', line 481

def document = @current_window&.document

#dropped_subresource_hostsObject

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

Returns:

  • (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_cookiesObject

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 export_cookies = @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.

Returns:

  • (Boolean)


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) ---

Parameters:

  • url (Object)
  • method: (Object) (defaults to: "GET")
  • headers: (headers) (defaults to: {})
  • body: (Object) (defaults to: nil)
  • params: (params, nil) (defaults to: nil)
  • redirect: (redirect_mode) (defaults to: :follow)

Returns:



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_inObject

Parameters:

  • locator (Object)
  • with: (Object)

Returns:

  • (Object)


210
# File 'sig/dommy/rack.rbs', line 210

def fill_in: (untyped locator, with: untyped) -> untyped

#follow_meta_refresh?Boolean

Returns:

  • (Boolean)


224
# File 'lib/dommy/rack/session.rb', line 224

def follow_meta_refresh? = @config.follow_meta_refresh

#follow_redirects?Boolean

Returns:

  • (Boolean)


221
# File 'lib/dommy/rack/session.rb', line 221

def follow_redirects? = @config.follow_redirects

#forwardResponse?

Returns:



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) ---

Parameters:

  • path (Object)
  • headers: (headers) (defaults to: {})

Returns:



390
391
392
# File 'lib/dommy/rack/session.rb', line 390

def get(path, headers: {})
  navigate(method: "GET", url: path, headers: headers)
end

Parameters:

  • name (Object)

Returns:

  • (String, nil)


644
# File 'lib/dommy/rack/session.rb', line 644

def get_cookie(name) = @cookie_jar.get(name)

#has_button?Boolean

Parameters:

  • locator (Object)

Returns:

  • (Boolean)


204
# File 'sig/dommy/rack.rbs', line 204

def has_button?: (untyped locator) -> bool

#has_css?Boolean

Parameters:

  • selector (String)
  • count: (Integer, nil)

Returns:

  • (Boolean)


199
# File 'sig/dommy/rack.rbs', line 199

def has_css?: (String selector, ?count: Integer?) -> bool

#has_field?Boolean

Parameters:

  • locator (Object)

Returns:

  • (Boolean)


205
# File 'sig/dommy/rack.rbs', line 205

def has_field?: (untyped locator) -> bool

#has_link?Boolean

Parameters:

  • locator (Object)

Returns:

  • (Boolean)


203
# File 'sig/dommy/rack.rbs', line 203

def has_link?: (untyped locator) -> bool

#has_no_css?Boolean

Parameters:

  • selector (String)
  • count: (Integer, nil)

Returns:

  • (Boolean)


200
# File 'sig/dommy/rack.rbs', line 200

def has_no_css?: (String selector, ?count: Integer?) -> bool

#has_no_text?Boolean

Parameters:

  • string (Object)

Returns:

  • (Boolean)


202
# File 'sig/dommy/rack.rbs', line 202

def has_no_text?: (untyped string) -> bool

#has_text?Boolean

Parameters:

  • string (Object)

Returns:

  • (Boolean)


201
# File 'sig/dommy/rack.rbs', line 201

def has_text?: (untyped string) -> bool

#headersheaders?

Returns:



479
# File 'lib/dommy/rack/session.rb', line 479

def headers = @last_response&.headers

#htmlString?

Returns:

  • (String, nil)


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 import_cookies(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.

Returns:

  • (Boolean)


146
# File 'lib/dommy/rack/session.rb', line 146

def javascript? = !@js_runtime.nil?

#js_errorsObject

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.

Parameters:

  • symbolize_names: (Boolean) (defaults to: false)

Returns:

  • (Object)


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

Returns:

  • (Boolean)


225
# File 'lib/dommy/rack/session.rb', line 225

def load_stylesheets? = @config.load_stylesheets

#max_redirectsInteger

Returns:

  • (Integer)


222
# File 'lib/dommy/rack/session.rb', line 222

def max_redirects = @config.max_redirects

Parameters:

  • method: (Object) (defaults to: "GET")
  • url: (Object)
  • params: (params, nil) (defaults to: nil)
  • body: (Object) (defaults to: nil)
  • headers: (headers) (defaults to: {})

Returns:



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_executorObject



227
# File 'lib/dommy/rack/session.rb', line 227

def network_executor = @network_executor

#not_found?Boolean

Returns:

  • (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