Class: Unmagic::Browser::Session
- Inherits:
-
Object
- Object
- Unmagic::Browser::Session
- Defined in:
- lib/unmagic/browser/session.rb
Overview
A live browser page you drive step by step.
The verbs read like Capybara's, and they auto-wait: click_button "Sign in" keeps looking until a button by that name is on the page and
actionable, so an explicit #wait_for is rare. Questions — has_text?
and friends — answer immediately instead, because "no" is an answer a
caller usually wants now rather than in ten seconds.
A session is a plain object. Hold it and it stays live between calls, which is what lets an agent open a page in one turn and act on it in the next — and what makes #close your job.
Constant Summary collapse
- KEYS =
Symbols that read better than Chrome's own key names.
{ enter: "Enter", return: "Enter", tab: "Tab", escape: "Escape", esc: "Escape", space: "Space", backspace: "Backspace", delete: "Delete", up: "ArrowUp", down: "ArrowDown", left: "ArrowLeft", right: "ArrowRight", home: "Home", end: "End", page_up: "PageUp", page_down: "PageDown", }.freeze
- POLL_INTERVAL_MS =
How often a waiting locator re-checks the page, in milliseconds. Slower than Chrome's animation frame on purpose: the matcher walks the DOM, and running that sixty times a second buys nothing a caller would notice.
100- PAGE_TEXT_SCRIPT =
innerTextis what a person would read — it respectsdisplay: noneand collapses the way the page renders — wheretextContentwould hand back hidden markup as if it were on screen. <<~JAVASCRIPT () => { const body = document.body; if (!body) return ""; return body.innerText === undefined ? body.textContent : body.innerText; } JAVASCRIPT
Instance Attribute Summary collapse
-
#browser ⇒ Unmagic::Browser
readonly
The browser this session belongs to.
-
#emulation ⇒ Emulation
readonly
What's currently being emulated.
Class Method Summary collapse
-
.key_name(key) ⇒ String
Translate a key symbol into the name Chrome knows.
Instance Method Summary collapse
-
#all(query = nil, scope: nil, purpose: :any, **options) ⇒ Array<Element>
Find every matching element, right now — no waiting, because "none" is a legitimate answer.
- #attach_file(query = nil, *paths, **options) ⇒ self
- #check(query = nil, **options) ⇒ self
- #choose(query = nil, **options) ⇒ self
-
#click(query = nil, **options) ⇒ self
Click whatever matches — a button, a link, a div, anything.
- #click_button(query = nil, **options) ⇒ self
- #click_link(query = nil, **options) ⇒ self
-
#close ⇒ void
Close the page and give the browser back.
-
#closed? ⇒ Boolean
Whether this session has been closed.
-
#current_url ⇒ String
The URL currently shown.
-
#driver ⇒ Puppeteer::Page
The live Puppeteer page, for everything this API doesn't wrap — network interception, tracing, the rest of the surface.
-
#emulate(config = nil, **knobs) { ... } ⇒ Emulation, Object
Change what's emulated from here on, or — with a block — just for the block, reverting afterwards.
-
#evaluate(script, *args) ⇒ Object
Run JavaScript in the page.
-
#fill_in(query = nil, with:, **options) ⇒ self
Type into a field, replacing whatever's in it.
-
#find(query = nil, scope: nil, wait: nil, purpose: :any, **options) ⇒ Element
Find one element, waiting for it to turn up.
- #go_back ⇒ self
- #go_forward ⇒ self
-
#has_button?(query = nil, **options) ⇒ Boolean
Whether such a button is on the page right now.
-
#has_element?(query = nil, **options) ⇒ Boolean
Whether anything matches right now.
-
#has_field?(query = nil, **options) ⇒ Boolean
Whether such a field is on the page right now.
-
#has_link?(query = nil, **options) ⇒ Boolean
Whether such a link is on the page right now.
-
#has_selector?(css) ⇒ Boolean
Whether anything matches it right now.
-
#has_text?(string) ⇒ Boolean
Whether it's on the page right now.
- #hover(query = nil, **options) ⇒ self
-
#html ⇒ String
The page's HTML, as it stands after JavaScript has run.
-
#initialize(browser:, connection:, page:, emulation: nil, timeout: nil, navigation_timeout: nil) ⇒ Session
constructor
A new instance of Session.
-
#inspect ⇒ String
The session and where it is, or that it's closed.
-
#lock { ... } ⇒ Object
(also: #synchronize)
Hold the session for a whole sequence, so another caller can't slip a keystroke between two of your steps.
-
#markdown ⇒ String
The page as Markdown.
-
#pdf ⇒ String
PDF bytes.
-
#press(key, on: nil, **options) ⇒ self
Press a key.
- #reload ⇒ self
-
#scope ⇒ Element?
The element locators are currently scoped to.
-
#screenshot(full_page: false, selector: nil) ⇒ String
PNG bytes.
-
#select(option, from: nil, **options) ⇒ self
Pick an option in a select, by the label a reader sees.
-
#text ⇒ String
The visible text of the page, or of the current scope.
-
#title ⇒ String
The page's title.
- #uncheck(query = nil, **options) ⇒ self
-
#value(query = nil, **options) ⇒ String?
What's currently in the field.
-
#visit(url) ⇒ self
Go to a URL and wait for it to settle.
-
#wait_for(text: nil, selector: nil, timeout: nil, **options) ⇒ self
Block until something shows up.
-
#within(query = nil, **options) {|session| ... } ⇒ Object
Narrow every locator inside the block to one part of the page.
Constructor Details
#initialize(browser:, connection:, page:, emulation: nil, timeout: nil, navigation_timeout: nil) ⇒ Session
Returns a new instance of Session.
84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/unmagic/browser/session.rb', line 84 def initialize(browser:, connection:, page:, emulation: nil, timeout: nil, navigation_timeout: nil) @browser = browser @connection = connection @page = page @emulation = Emulation.build(emulation) @timeout = timeout || Browser.configuration.timeout @navigation_timeout = || Browser.configuration. @monitor = Monitor.new @scopes = [] @closed = false guarded("setting up emulation") { @emulation.apply(@page) } end |
Instance Attribute Details
#browser ⇒ Unmagic::Browser (readonly)
Returns the browser this session belongs to.
73 74 75 |
# File 'lib/unmagic/browser/session.rb', line 73 def browser @browser end |
#emulation ⇒ Emulation (readonly)
Returns what's currently being emulated.
76 77 78 |
# File 'lib/unmagic/browser/session.rb', line 76 def emulation @emulation end |
Class Method Details
.key_name(key) ⇒ String
Translate a key symbol into the name Chrome knows.
67 68 69 |
# File 'lib/unmagic/browser/session.rb', line 67 def key_name(key) KEYS[key.to_s.downcase.to_sym] || key.to_s end |
Instance Method Details
#all(query = nil, scope: nil, purpose: :any, **options) ⇒ Array<Element>
Find every matching element, right now — no waiting, because "none" is a legitimate answer.
396 397 398 399 |
# File 'lib/unmagic/browser/session.rb', line 396 def all(query = nil, scope: nil, purpose: :any, **) locator = Locator.build(query, purpose: purpose, **) resolve_all(locator, scope: scope || self.scope) end |
#attach_file(query = nil, *paths, **options) ⇒ self
228 229 230 231 |
# File 'lib/unmagic/browser/session.rb', line 228 def attach_file(query = nil, *paths, **) find(query, purpose: :file_field, **).attach(*paths) self end |
#check(query = nil, **options) ⇒ self
203 204 205 206 |
# File 'lib/unmagic/browser/session.rb', line 203 def check(query = nil, **) find(query, purpose: :checkbox, **).check self end |
#choose(query = nil, **options) ⇒ self
219 220 221 222 |
# File 'lib/unmagic/browser/session.rb', line 219 def choose(query = nil, **) find(query, purpose: :radio, **).click self end |
#click(query = nil, **options) ⇒ self
Click whatever matches — a button, a link, a div, anything.
180 181 182 183 |
# File 'lib/unmagic/browser/session.rb', line 180 def click(query = nil, **) find(query, **).click self end |
#click_button(query = nil, **options) ⇒ self
160 161 162 163 |
# File 'lib/unmagic/browser/session.rb', line 160 def (query = nil, **) find(query, purpose: :button, **).click self end |
#click_link(query = nil, **options) ⇒ self
169 170 171 172 |
# File 'lib/unmagic/browser/session.rb', line 169 def click_link(query = nil, **) find(query, purpose: :link, **).click self end |
#close ⇒ void
This method returns an undefined value.
Close the page and give the browser back. On Cloudflare that releases one of your concurrent slots, so a kept session that's finished with should always be closed.
460 461 462 463 464 465 466 467 468 469 470 471 472 |
# File 'lib/unmagic/browser/session.rb', line 460 def close @monitor.synchronize do return if @closed @closed = true begin @page.close rescue StandardError nil # The page may have gone with its browser; the connection close is what matters. end @connection.close end end |
#closed? ⇒ Boolean
Returns whether this session has been closed.
475 476 477 |
# File 'lib/unmagic/browser/session.rb', line 475 def closed? @closed end |
#current_url ⇒ String
Returns the URL currently shown.
133 134 135 |
# File 'lib/unmagic/browser/session.rb', line 133 def current_url guarded("reading the URL") { @page.url } end |
#driver ⇒ Puppeteer::Page
The live Puppeteer page, for everything this API doesn't wrap — network interception, tracing, the rest of the surface. Wrap multi-step native work in #lock when it has to be atomic against other callers.
103 104 105 |
# File 'lib/unmagic/browser/session.rb', line 103 def driver @page end |
#emulate(config = nil, **knobs) { ... } ⇒ Emulation, Object
Change what's emulated from here on, or — with a block — just for the block, reverting afterwards.
432 433 434 435 436 437 438 439 440 441 442 |
# File 'lib/unmagic/browser/session.rb', line 432 def emulate(config = nil, **knobs) previous = @emulation apply_emulation(previous.merge(config || knobs)) return @emulation unless block_given? begin yield ensure apply_emulation(previous) end end |
#evaluate(script, *args) ⇒ Object
Run JavaScript in the page.
307 308 309 |
# File 'lib/unmagic/browser/session.rb', line 307 def evaluate(script, *args) guarded("evaluating JavaScript") { @page.evaluate(script, *args) } end |
#fill_in(query = nil, with:, **options) ⇒ self
Type into a field, replacing whatever's in it.
151 152 153 154 |
# File 'lib/unmagic/browser/session.rb', line 151 def fill_in(query = nil, with:, **) find(query, purpose: :field, **).fill_in(with) self end |
#find(query = nil, scope: nil, wait: nil, purpose: :any, **options) ⇒ Element
Find one element, waiting for it to turn up.
380 381 382 383 384 385 386 |
# File 'lib/unmagic/browser/session.rb', line 380 def find(query = nil, scope: nil, wait: nil, purpose: :any, **) locator = Locator.build(query, purpose: purpose, **) found = resolve(locator, scope: scope || self.scope, wait: wait || @timeout) raise Error::NotFound, "Couldn't find a #{locator} on #{current_url}." unless found found end |
#go_back ⇒ self
123 124 125 |
# File 'lib/unmagic/browser/session.rb', line 123 def go_back navigating("going back") { @page.go_back(wait_until: Driver::Base::WAIT_UNTIL, timeout: ) } end |
#go_forward ⇒ self
128 129 130 |
# File 'lib/unmagic/browser/session.rb', line 128 def go_forward navigating("going forward") { @page.go_forward(wait_until: Driver::Base::WAIT_UNTIL, timeout: ) } end |
#has_button?(query = nil, **options) ⇒ Boolean
Returns whether such a button is on the page right now.
338 339 340 |
# File 'lib/unmagic/browser/session.rb', line 338 def (query = nil, **) exists?(query, purpose: :button, **) end |
#has_element?(query = nil, **options) ⇒ Boolean
Returns whether anything matches right now.
365 366 367 |
# File 'lib/unmagic/browser/session.rb', line 365 def has_element?(query = nil, **) exists?(query, **) end |
#has_field?(query = nil, **options) ⇒ Boolean
Returns whether such a field is on the page right now.
352 353 354 |
# File 'lib/unmagic/browser/session.rb', line 352 def has_field?(query = nil, **) exists?(query, purpose: :field, **) end |
#has_link?(query = nil, **options) ⇒ Boolean
Returns whether such a link is on the page right now.
345 346 347 |
# File 'lib/unmagic/browser/session.rb', line 345 def has_link?(query = nil, **) exists?(query, purpose: :link, **) end |
#has_selector?(css) ⇒ Boolean
Returns whether anything matches it right now.
358 359 360 |
# File 'lib/unmagic/browser/session.rb', line 358 def has_selector?(css) exists?(css: css) end |
#has_text?(string) ⇒ Boolean
Returns whether it's on the page right now.
331 332 333 |
# File 'lib/unmagic/browser/session.rb', line 331 def has_text?(string) text.downcase.include?(string.to_s.downcase.gsub(/\s+/, " ").strip) end |
#hover(query = nil, **options) ⇒ self
251 252 253 254 |
# File 'lib/unmagic/browser/session.rb', line 251 def hover(query = nil, **) find(query, **).hover self end |
#html ⇒ String
Returns the page's HTML, as it stands after JavaScript has run.
269 270 271 272 273 |
# File 'lib/unmagic/browser/session.rb', line 269 def html return scope.html if scope guarded("reading the page") { @page.content } end |
#inspect ⇒ String
Returns the session and where it is, or that it's closed.
480 481 482 483 |
# File 'lib/unmagic/browser/session.rb', line 480 def inspect state = @closed ? "closed" : current_url "#<#{self.class.name} #{state}>" end |
#lock { ... } ⇒ Object Also known as: synchronize
Hold the session for a whole sequence, so another caller can't slip a keystroke between two of your steps. Individual operations are already serialised; this is for when a sequence has to be atomic.
450 451 452 |
# File 'lib/unmagic/browser/session.rb', line 450 def lock(&block) @monitor.synchronize(&block) end |
#markdown ⇒ String
Returns the page as Markdown.
276 277 278 |
# File 'lib/unmagic/browser/session.rb', line 276 def markdown Markdown.from_html(html) end |
#pdf ⇒ String
Returns PDF bytes.
298 299 300 |
# File 'lib/unmagic/browser/session.rb', line 298 def pdf guarded("printing to PDF") { @page.pdf } end |
#press(key, on: nil, **options) ⇒ self
Press a key. With no locator it goes to whatever has focus.
239 240 241 242 243 244 245 246 |
# File 'lib/unmagic/browser/session.rb', line 239 def press(key, on: nil, **) if on element(on, **).press(key) else guarded("pressing #{key}") { @page.keyboard.press(self.class.key_name(key)) } end self end |
#reload ⇒ self
118 119 120 |
# File 'lib/unmagic/browser/session.rb', line 118 def reload navigating("reloading") { @page.reload(wait_until: Driver::Base::WAIT_UNTIL, timeout: ) } end |
#scope ⇒ Element?
Returns the element locators are currently scoped to.
419 420 421 |
# File 'lib/unmagic/browser/session.rb', line 419 def scope @scopes.last end |
#screenshot(full_page: false, selector: nil) ⇒ String
Returns PNG bytes.
290 291 292 293 294 295 |
# File 'lib/unmagic/browser/session.rb', line 290 def screenshot(full_page: false, selector: nil) target = selector || scope return element(target).screenshot if target guarded("taking a screenshot") { @page.screenshot(type: "png", full_page: full_page) } end |
#select(option, from: nil, **options) ⇒ self
Pick an option in a select, by the label a reader sees.
192 193 194 195 196 197 198 |
# File 'lib/unmagic/browser/session.rb', line 192 def select(option, from: nil, **) # With nothing naming the select, there's only one sensible reading: # the page has one, and this is it. = { css: "select" } if from.nil? && .empty? find(from, purpose: :select, **).select_option(option) self end |
#text ⇒ String
Returns the visible text of the page, or of the current scope.
259 260 261 262 263 264 265 266 |
# File 'lib/unmagic/browser/session.rb', line 259 def text raw = if scope scope.evaluate("(root) => (root.innerText === undefined ? root.textContent : root.innerText)") else guarded("reading the page") { @page.evaluate(PAGE_TEXT_SCRIPT) } end raw.to_s.gsub(/[ \t]+/, " ").strip end |
#title ⇒ String
Returns the page's title.
138 139 140 |
# File 'lib/unmagic/browser/session.rb', line 138 def title guarded("reading the title") { @page.title } end |
#uncheck(query = nil, **options) ⇒ self
211 212 213 214 |
# File 'lib/unmagic/browser/session.rb', line 211 def uncheck(query = nil, **) find(query, purpose: :checkbox, **).uncheck self end |
#value(query = nil, **options) ⇒ String?
Returns what's currently in the field.
283 284 285 |
# File 'lib/unmagic/browser/session.rb', line 283 def value(query = nil, **) find(query, purpose: :field, **).value end |
#visit(url) ⇒ self
Go to a URL and wait for it to settle.
113 114 115 |
# File 'lib/unmagic/browser/session.rb', line 113 def visit(url) navigating("visiting #{url}") { @page.goto(url, wait_until: Driver::Base::WAIT_UNTIL, timeout: ) } end |
#wait_for(text: nil, selector: nil, timeout: nil, **options) ⇒ self
Block until something shows up.
321 322 323 324 325 326 327 |
# File 'lib/unmagic/browser/session.rb', line 321 def wait_for(text: nil, selector: nil, timeout: nil, **) raise ArgumentError, "wait_for needs text: or selector:" if text.nil? && selector.nil? wait_for_text(text, timeout) if text find(selector, wait: timeout || @timeout, **) if selector self end |
#within(query = nil, **options) {|session| ... } ⇒ Object
Narrow every locator inside the block to one part of the page.
408 409 410 411 412 413 414 415 416 |
# File 'lib/unmagic/browser/session.rb', line 408 def within(query = nil, **) inner = element(query, **) @scopes.push(inner) begin yield(self) ensure @scopes.pop end end |