class Ferrum::Page

Constants

GOTO_WAIT

Attributes

browser[R]
cookies[R]
document_id[R]
event[R]
headers[R]
keyboard[R]
mouse[R]
network[R]
referrer[RW]
target_id[R]

Public Class Methods

new(target_id, browser) click to toggle source
# File lib/ferrum/page.rb, line 46
def initialize(target_id, browser)
  @frames = {}
  @main_frame = Frame.new(nil, self)
  @target_id, @browser = target_id, browser
  @event = Event.new.tap(&:set)

  host = @browser.process.host
  port = @browser.process.port
  ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
  @client = Browser::Client.new(browser, ws_url, id_starts_with: 1000)

  @mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
  @headers, @cookies = Headers.new(self), Cookies.new(self)
  @network = Network.new(self)

  subscribe
  prepare_page
end

Public Instance Methods

back() click to toggle source
# File lib/ferrum/page.rb, line 129
def back
  history_navigate(delta: -1)
end
bypass_csp(value = true) click to toggle source
# File lib/ferrum/page.rb, line 143
def bypass_csp(value = true)
  enabled = !!value
  command("Page.setBypassCSP", enabled: enabled)
  enabled
end
close() click to toggle source
# File lib/ferrum/page.rb, line 89
def close
  @headers.clear
  @browser.command("Target.closeTarget", targetId: @target_id)
  @client.close
end
command(method, wait: 0, slowmoable: false, **params) click to toggle source
# File lib/ferrum/page.rb, line 157
def command(method, wait: 0, slowmoable: false, **params)
  iteration = @event.reset if wait > 0
  sleep(@browser.slowmo) if slowmoable && @browser.slowmo > 0
  result = @client.command(method, params)

  if wait > 0
    @event.wait(wait) # Wait a bit after command and check if iteration has
                      # changed which means there was some network event for
                      # the main frame and it started to load new content.
    if iteration != @event.iteration
      set = @event.wait(@browser.timeout)
      raise TimeoutError unless set
    end
  end
  result
end
forward() click to toggle source
# File lib/ferrum/page.rb, line 133
def forward
  history_navigate(delta: 1)
end
go_to(url = nil) click to toggle source
# File lib/ferrum/page.rb, line 69
def go_to(url = nil)
  options = { url: combine_url!(url) }
  options.merge!(referrer: referrer) if referrer
  response = command("Page.navigate", wait: GOTO_WAIT, **options)
  # https://cs.chromium.org/chromium/src/net/base/net_error_list.h
  if %w[net::ERR_NAME_NOT_RESOLVED
        net::ERR_NAME_RESOLUTION_FAILED
        net::ERR_INTERNET_DISCONNECTED
        net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
    raise StatusError, options[:url]
  end
  response["frameId"]
rescue TimeoutError
  if @browser.pending_connection_errors
    pendings = network.traffic.select(&:pending?).map { |e| e.request.url }
    raise PendingConnectionsError.new(options[:url], pendings) unless pendings.empty?
  end
end
Also aliased as: goto
goto(url = nil)
Alias for: go_to
on(name, &block) click to toggle source
# File lib/ferrum/page.rb, line 174
def on(name, &block)
  case name
  when :dialog
    @client.on("Page.javascriptDialogOpening") do |params, index, total|
      dialog = Dialog.new(self, params)
      block.call(dialog, index, total)
    end
  when :request
    @client.on("Fetch.requestPaused") do |params, index, total|
      request = Network::InterceptedRequest.new(self, params)
      exchange = network.select(request.network_id).last
      exchange ||= network.build_exchange(request.network_id)
      exchange.intercepted_request = request
      block.call(request, index, total)
    end
  when :auth
    @client.on("Fetch.authRequired") do |params, index, total|
      request = Network::AuthRequest.new(self, params)
      block.call(request, index, total)
    end
  else
    @client.on(name, &block)
  end
end
position() click to toggle source
# File lib/ferrum/page.rb, line 112
def position
  @browser.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds").values_at("left", "top")
end
position=(left:, top:) click to toggle source
# File lib/ferrum/page.rb, line 116
def position=(left:, top:)
  @browser.command("Browser.setWindowBounds", windowId: window_id, bounds: { left: left, top: top })
end
refresh() click to toggle source
# File lib/ferrum/page.rb, line 120
def refresh
  command("Page.reload", wait: timeout, slowmoable: true)
end
Also aliased as: reload
reload()
Alias for: refresh
resize(width: nil, height: nil, fullscreen: false) click to toggle source
# File lib/ferrum/page.rb, line 95
def resize(width: nil, height: nil, fullscreen: false)
  if fullscreen
    width, height = document_size
    set_window_bounds(windowState: "fullscreen")
  else
    set_window_bounds(windowState: "normal")
    set_window_bounds(width: width, height: height)
  end

  command("Emulation.setDeviceMetricsOverride", slowmoable: true,
                                                width: width,
                                                height: height,
                                                deviceScaleFactor: 1,
                                                mobile: false,
                                                fitWindow: false)
end
set_window_bounds(bounds = {}) click to toggle source
# File lib/ferrum/page.rb, line 153
def set_window_bounds(bounds = {})
  @browser.command("Browser.setWindowBounds", windowId: window_id, bounds: bounds)
end
stop() click to toggle source
# File lib/ferrum/page.rb, line 125
def stop
  command("Page.stopLoading", slowmoable: true)
end
subscribed?(event) click to toggle source
# File lib/ferrum/page.rb, line 199
def subscribed?(event)
  @client.subscribed?(event)
end
timeout() click to toggle source
# File lib/ferrum/page.rb, line 65
def timeout
  @browser.timeout
end
wait_for_reload(sec = 1) click to toggle source
# File lib/ferrum/page.rb, line 137
def wait_for_reload(sec = 1)
  @event.reset if @event.set?
  @event.wait(sec)
  @event.set
end
window_id() click to toggle source
# File lib/ferrum/page.rb, line 149
def window_id
  @browser.command("Browser.getWindowForTarget", targetId: @target_id)["windowId"]
end

Private Instance Methods

combine_url!(url_or_path) click to toggle source
# File lib/ferrum/page.rb, line 280
def combine_url!(url_or_path)
  url = Addressable::URI.parse(url_or_path)
  nil_or_relative = url.nil? || url.relative?

  if nil_or_relative && !@browser.base_url
    raise "Set :base_url browser's option or use absolute url in `go_to`, you passed: #{url_or_path}"
  end

  (nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
end
get_document_id() click to toggle source
# File lib/ferrum/page.rb, line 291
def get_document_id
  @document_id = command("DOM.getDocument", depth: 0).dig("root", "nodeId")
end
history_navigate(delta:) click to toggle source
# File lib/ferrum/page.rb, line 268
def history_navigate(delta:)
  history = command("Page.getNavigationHistory")
  index, entries = history.values_at("currentIndex", "entries")

  if entry = entries[index + delta]
    # Potential wait because of network event
    command("Page.navigateToHistoryEntry", wait: Mouse::CLICK_WAIT,
                                           slowmoable: true,
                                           entryId: entry["id"])
  end
end
inject_extensions() click to toggle source
# File lib/ferrum/page.rb, line 255
def inject_extensions
  @browser.extensions.each do |extension|
    # https://github.com/GoogleChrome/puppeteer/issues/1443
    # https://github.com/ChromeDevTools/devtools-protocol/issues/77
    # https://github.com/cyrus-and/chrome-remote-interface/issues/319
    # We also evaluate script just in case because
    # `Page.addScriptToEvaluateOnNewDocument` doesn't work in popups.
    command("Runtime.evaluate", expression: extension,
                                contextId: execution_id,
                                returnByValue: true)
  end
end
prepare_page() click to toggle source
# File lib/ferrum/page.rb, line 223
def prepare_page
  command("Page.enable")
  command("Runtime.enable")
  command("DOM.enable")
  command("CSS.enable")
  command("Log.enable")
  command("Network.enable")

  if @browser.options[:save_path]
    command("Page.setDownloadBehavior", behavior: "allow", downloadPath: @browser.options[:save_path])
  end

  @browser.extensions.each do |extension|
    command("Page.addScriptToEvaluateOnNewDocument", source: extension)
  end

  inject_extensions

  width, height = @browser.window_size
  resize(width: width, height: height)

  response = command("Page.getNavigationHistory")
  if response.dig("entries", 0, "transitionType") != "typed"
    # If we create page by clicking links, submiting forms and so on it
    # opens a new window for which `frameStoppedLoading` event never
    # occurs and thus search for nodes cannot be completed. Here we check
    # the history and if the transitionType for example `link` then
    # content is already loaded and we can try to get the document.
    get_document_id
  end
end
subscribe() click to toggle source
# File lib/ferrum/page.rb, line 205
def subscribe
  frames_subscribe
  network.subscribe

  if @browser.logger
    on("Runtime.consoleAPICalled") do |params|
      params["args"].each { |r| @browser.logger.puts(r["value"]) }
    end
  end

  if @browser.js_errors
    on("Runtime.exceptionThrown") do |params|
      # FIXME https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
      Thread.main.raise JavaScriptError.new(params.dig("exceptionDetails", "exception"))
    end
  end
end