Class: Obxcura::Page
- Inherits:
-
Object
- Object
- Obxcura::Page
- Extended by:
- Forwardable
- Defined in:
- lib/obxcura/page.rb
Overview
A single page/tab: a CDP target with its own attached session. Created via Browser#create_page — you rarely instantiate this directly.
Page owns the CDP session, navigation, in-page POSTs and lifecycle. Reading
the DOM and executing JS belong to its main Frame (see Frame::DOM and
Frame::Runtime), and Page delegates those (#evaluate, #html, #title,
#at_css, ...).
Obscura has no paint engine, so there is deliberately no screenshot API.
Constant Summary collapse
- TIMEOUT_HEADROOM =
Seconds of slack given to the CDP reply beyond a #post timeout, so the in-page abort is what surfaces rather than the transport giving up first.
5
Instance Attribute Summary collapse
- #client ⇒ String, ... readonly
- #frame ⇒ String, ... readonly
- #session_id ⇒ String, ... readonly
- #target_id ⇒ String, ... readonly
Instance Method Summary collapse
-
#close ⇒ Obxcura::Page?
Close this page's target and stop listening for its events.
-
#close_connection ⇒ void
Drop the underlying WebSocket connection (affects the whole browser).
-
#command(method, params = {}) ⇒ Hash
Send a CDP command scoped to this page's session.
-
#cookies ⇒ Array<Hash>
The page's cookies as CDP cookie hashes.
-
#goto(url) ⇒ self
(also: #go_to)
Navigate to
urland block until the page's load event fires. -
#initialize(browser, target_id:, session_id:) ⇒ Page
constructor
A new instance of Page.
-
#network_log ⇒ Array<Hash>
Requests this page issued, oldest first, as
{ url:, request_id:, finished: }. -
#post(url, payload, content_type, headers, timeout: nil) ⇒ Hash
(also: #xhr_post)
POST from the page context via
fetch. -
#refresh ⇒ Hash
(also: #reload)
Reload the page and block until it loads again.
Constructor Details
#initialize(browser, target_id:, session_id:) ⇒ Page
Returns a new instance of Page.
45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/obxcura/page.rb', line 45 def initialize(browser, target_id:, session_id:) @browser = browser @client = browser.client @target_id = target_id @session_id = session_id @frame = Frame.new(target_id, self) @load_queue = Queue.new @network_log = [] @network_mutex = Mutex.new @client.subscribe(@session_id) { |method, params| dispatch_event(method, params) } command("Network.enable") end |
Instance Attribute Details
#client ⇒ String, ... (readonly)
36 37 38 |
# File 'lib/obxcura/page.rb', line 36 def client @client end |
#frame ⇒ String, ... (readonly)
36 37 38 |
# File 'lib/obxcura/page.rb', line 36 def frame @frame end |
#session_id ⇒ String, ... (readonly)
36 37 38 |
# File 'lib/obxcura/page.rb', line 36 def session_id @session_id end |
#target_id ⇒ String, ... (readonly)
36 37 38 |
# File 'lib/obxcura/page.rb', line 36 def target_id @target_id end |
Instance Method Details
#close ⇒ Obxcura::Page?
Close this page's target and stop listening for its events.
89 90 91 92 93 94 95 96 97 |
# File 'lib/obxcura/page.rb', line 89 def close @client.unsubscribe(@session_id) # @client.command("Network.clearBrowserCookies", { targetId: @target_id }) @client.command("Target.closeTarget", { targetId: @target_id }) rescue ProtocolError # Target already gone — nothing to do. ensure @browser.remove_page(self) end |
#close_connection ⇒ void
This method returns an undefined value.
Drop the underlying WebSocket connection (affects the whole browser).
102 103 104 |
# File 'lib/obxcura/page.rb', line 102 def close_connection @client.close end |
#command(method, params = {}) ⇒ Hash
Send a CDP command scoped to this page's session.
125 126 127 |
# File 'lib/obxcura/page.rb', line 125 def command(method, params = {}) @client.command(method, params, session_id: @session_id) end |
#cookies ⇒ Array<Hash>
Returns the page's cookies as CDP cookie hashes.
116 117 118 |
# File 'lib/obxcura/page.rb', line 116 def command("Storage.getCookies")["cookies"] end |
#goto(url) ⇒ self Also known as: go_to
Navigate to url and block until the page's load event fires. Aliased as
go_to.
78 79 80 81 82 83 |
# File 'lib/obxcura/page.rb', line 78 def goto(url) @load_queue = Queue.new command("Page.navigate", url: url) wait_for_load self end |
#network_log ⇒ Array<Hash>
Requests this page issued, oldest first, as
{ url:, request_id:, finished: }.
Scope is deliberately narrow: Obscura emits Network events for requests the
navigation drives (the document and its subresources), but not for ones
started from script. A #post — or any in-page fetch/XMLHttpRequest —
therefore never shows up here. Verified against Obscura 0.1.11: enabling the
Network domain and issuing a scripted POST produces no events at all.
69 70 71 |
# File 'lib/obxcura/page.rb', line 69 def network_log @network_mutex.synchronize { @network_log.map(&:dup) } end |
#post(url, payload, content_type, headers, timeout: nil) ⇒ Hash Also known as: xhr_post
POST from the page context via fetch. All values cross as arguments, never
interpolated into the JS. Returns { status, ok, body } on any HTTP reply
(including 4xx/5xx). A transport failure — the request never reached the
server (blocked by CORS / private-network SSRF guard, mixed origin, or a
dead host) — raises ConnectionError instead of silently returning nil.
timeout (seconds) is enforced in the page by racing the fetch against a
timer, so a server that accepts the connection and never answers (some
anti-bot endpoints tarpit non-stealth clients) fails in roughly timeout
seconds instead of Client::DEFAULT_TIMEOUT. The CDP reply gets a little
headroom past that so the in-page result is what we observe.
The race is deliberate, and not the obvious AbortSignal.timeout. Obscura
does accept an abort signal, but when the abort actually fires, the fetch
rejection is swallowed and the call returns undefined — the same
in-page-throws-vanish behaviour that makes Frame::Runtime use { error: }
sentinels. A rejection can't carry the reason across, so a resolved value
has to. We still abort the underlying request once the timer wins, purely so
it stops occupying the connection.
Requests made here do not appear in #network_log; see that method.
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/obxcura/page.rb', line 159 def post(url, payload, content_type, headers, timeout: nil) timeout_ms = timeout && (timeout * 1000).to_i result = evaluate_func(<<~JS, url, payload, content_type, headers, timeout_ms, timeout: timeout && timeout + TIMEOUT_HEADROOM) function(url, payload, contentType, headers, timeoutMs) { const controller = timeoutMs ? new AbortController() : null; const init = { method: "POST", headers: Object.assign({ "Content-Type": contentType }, headers), body: payload }; if (controller) init.signal = controller.signal; let timer = null; const request = fetch(url, init) .then((r) => r.text().then((body) => ({ status: r.status, ok: r.ok, body: body }))) .catch((e) => ({ error: String(e) })) .then((outcome) => { if (timer) clearTimeout(timer); return outcome; }); if (!timeoutMs) return request; return Promise.race([ request, new Promise((resolve) => { timer = setTimeout(() => { if (controller) controller.abort(); resolve({ timeout: true }); }, timeoutMs); }) ]); } JS raise TimeoutError, (url) if result.is_a?(Hash) && result["timeout"] if result.nil? || result["error"] reason = result&.dig("error") || "no response (request blocked or never settled)" raise ConnectionError, "POST #{url} failed: #{reason}" end result rescue TimeoutError raise TimeoutError, (url) end |
#refresh ⇒ Hash Also known as: reload
Reload the page and block until it loads again. Aliased as reload.
109 110 111 112 |
# File 'lib/obxcura/page.rb', line 109 def refresh command("Page.reload") wait_for_load end |