class Capybara::Apparition::Page

Constants

CSS_FIND_JS
EVALUATE_ASYNC_JS
EVALUATE_WITH_ID_JS
EXECUTE_JS
XPATH_FIND_JS

Attributes

browser_context_id[R]
keyboard[R]
modal_messages[R]
mouse[R]
network_traffic[R]
perm_headers[RW]
status_code[R]
target_id[R]
temp_headers[RW]
temp_no_redirect_headers[RW]
url_blacklist[R]
url_whitelist[R]
viewport_size[R]

Public Class Methods

create(browser, session, id, browser_context_id, ignore_https_errors: false, **options) click to toggle source
# File lib/capybara/apparition/page.rb, line 18
def self.create(browser, session, id, browser_context_id,
                ignore_https_errors: false, **options)
  session.async_command 'Page.enable'

  # Provides a lot of info - but huge overhead
  # session.command 'Page.setLifecycleEventsEnabled', enabled: true

  page = Page.new(browser, session, id, browser_context_id, **options)

  session.async_commands 'Network.enable', 'Runtime.enable', 'Security.enable', 'DOM.enable'
  session.async_command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
  if Capybara.save_path
    session.async_command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
  end
  page
end
new(browser, session, target_id, browser_context_id, js_errors: false, url_blacklist: [], url_whitelist: [], extensions: []) click to toggle source
# File lib/capybara/apparition/page.rb, line 35
def initialize(browser, session, target_id, browser_context_id,
               js_errors: false, url_blacklist: [], url_whitelist: [], extensions: [])
  @target_id = target_id
  @browser_context_id = browser_context_id
  @browser = browser
  @session = session
  @keyboard = Keyboard.new(self)
  @mouse = Mouse.new(self, @keyboard)
  @modals = []
  @modal_messages = []
  @frames = Capybara::Apparition::FrameManager.new(@target_id)
  @response_headers = {}
  @status_code = 0
  @url_blacklist = url_blacklist || []
  @url_whitelist = url_whitelist || []
  @credentials = nil
  @auth_attempts = []
  @proxy_credentials = nil
  @proxy_auth_attempts = []
  @perm_headers = {}
  @temp_headers = {}
  @temp_no_redirect_headers = {}
  @viewport_size = nil
  @network_traffic = []
  @open_resource_requests = {}
  @raise_js_errors = js_errors
  @js_error = nil
  @modal_mutex = Mutex.new
  @modal_closed = ConditionVariable.new

  register_event_handlers

  register_js_error_handler # if js_errors

  extensions.each do |name|
    add_extension(name)
  end

  setup_network_interception if browser.proxy_auth
end

Public Instance Methods

add_extension(filename) click to toggle source
# File lib/capybara/apparition/page.rb, line 90
def add_extension(filename)
  command('Page.addScriptToEvaluateOnNewDocument', source: File.read(filename))
rescue Errno::ENOENT
  raise ::Capybara::Apparition::BrowserError.new('name' => "Unable to load extension: #{filename}", 'args' => nil)
end
add_modal(modal_response) click to toggle source
# File lib/capybara/apparition/page.rb, line 96
def add_modal(modal_response)
  @last_modal_message = nil
  @modals.push(modal_response)
end
async_command(name, **params) click to toggle source
# File lib/capybara/apparition/page.rb, line 368
def async_command(name, **params)
  @browser.command_for_session(@session.session_id, name, params).discard_result
end
clear_network_traffic() click to toggle source
# File lib/capybara/apparition/page.rb, line 121
def clear_network_traffic
  @network_traffic = []
end
click_at(x, y) click to toggle source
# File lib/capybara/apparition/page.rb, line 130
def click_at(x, y)
  wait_for_loaded
  @mouse.click_at(x: x, y: y)
end
command(name, **params) click to toggle source
# File lib/capybara/apparition/page.rb, line 364
def command(name, **params)
  @browser.command_for_session(@session.session_id, name, params).result
end
content() click to toggle source
# File lib/capybara/apparition/page.rb, line 270
def content
  wait_for_loaded
  _raw_evaluate("(function(){
    let val = '';
    if (document.doctype)
      val = new XMLSerializer().serializeToString(document.doctype);
    if (document.documentElement)
      val += document.documentElement.outerHTML;
    return val;
  })()")
end
credentials=(creds) click to toggle source
# File lib/capybara/apparition/page.rb, line 106
def credentials=(creds)
  @credentials = creds
  setup_network_interception
end
current_frame_offset() click to toggle source
# File lib/capybara/apparition/page.rb, line 139
def current_frame_offset
  frame_offset(current_frame)
end
current_state() click to toggle source
# File lib/capybara/apparition/page.rb, line 135
def current_state
  main_frame.state
end
current_url() click to toggle source
# File lib/capybara/apparition/page.rb, line 296
def current_url
  wait_for_loaded
  _raw_evaluate('window.location.href', context_id: main_frame.context_id)
end
element_from_point(x:, y:) click to toggle source
# File lib/capybara/apparition/page.rb, line 301
def element_from_point(x:, y:)
  r_o = _raw_evaluate("document.elementFromPoint(#{x}, #{y})", context_id: main_frame.context_id)
  while r_o&.[]('description')&.start_with?('iframe')
    frame_node = command('DOM.describeNode', objectId: r_o['objectId'])
    frame = @frames.get(frame_node.dig('node', 'frameId'))
    fo = frame_offset(frame)
    r_o = _raw_evaluate("document.elementFromPoint(#{x - fo[:x]}, #{y - fo[:y]})", context_id: frame.context_id)
  end
  r_o
end
evaluate(script, *args) click to toggle source
# File lib/capybara/apparition/page.rb, line 212
def evaluate(script, *args)
  eval_wrapped_script(EVALUATE_WITH_ID_JS, script, args)
end
evaluate_async(script, _wait_time, *args) click to toggle source
# File lib/capybara/apparition/page.rb, line 216
def evaluate_async(script, _wait_time, *args)
  eval_wrapped_script(EVALUATE_ASYNC_JS, script, args)
end
execute(script, *args) click to toggle source
# File lib/capybara/apparition/page.rb, line 207
def execute(script, *args)
  eval_wrapped_script(EXECUTE_JS, script, args)
  nil
end
extra_headers() click to toggle source
# File lib/capybara/apparition/page.rb, line 372
def extra_headers
  temp_headers.merge(perm_headers).merge(temp_no_redirect_headers)
end
find(method, selector) click to toggle source
# File lib/capybara/apparition/page.rb, line 195
def find(method, selector)
  wait_for_loaded
  js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
  query = method == :css ? CSS_FIND_JS : XPATH_FIND_JS
  result = _raw_evaluate(format(query, selector: js_escaped_selector))
  (result || []).map { |r_o| [self, r_o['objectId'], tag_name: r_o['description'].split(/[.#]/, 2)[0]] }
rescue ::Capybara::Apparition::BrowserError => e
  raise unless /is not a valid (XPath expression|selector)/.match? e.name

  raise Capybara::Apparition::InvalidSelector, 'args' => [method, selector]
end
frame_title() click to toggle source
# File lib/capybara/apparition/page.rb, line 359
def frame_title
  wait_for_loaded
  _raw_evaluate('document.title')
end
frame_url() click to toggle source
# File lib/capybara/apparition/page.rb, line 312
def frame_url
  wait_for_loaded
  _raw_evaluate('window.location.href')
end
fullscreen() click to toggle source
# File lib/capybara/apparition/page.rb, line 341
def fullscreen
  result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
  @browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'fullscreen' })
end
go_back() click to toggle source
# File lib/capybara/apparition/page.rb, line 227
def go_back
  wait_for_loaded
  go_history(-1)
end
go_forward() click to toggle source
# File lib/capybara/apparition/page.rb, line 232
def go_forward
  wait_for_loaded
  go_history(+1)
end
inherit(page) click to toggle source
# File lib/capybara/apparition/page.rb, line 383
def inherit(page)
  if page
    self.url_whitelist = page.url_whitelist.dup
    self.url_blacklist = page.url_blacklist.dup
    set_viewport(**page.viewport_size) if page.viewport_size
  end
  self
end
js_error() click to toggle source
# File lib/capybara/apparition/page.rb, line 392
def js_error
  res = @js_error
  @js_error = nil
  res
end
maximize() click to toggle source
# File lib/capybara/apparition/page.rb, line 346
def maximize
  screen_width, screen_height = *evaluate('[window.screen.width, window.screen.height]')
  set_viewport(width: screen_width, height: screen_height)

  result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
  @browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'maximized' })
end
pop_frame(top: false) click to toggle source
# File lib/capybara/apparition/page.rb, line 191
def pop_frame(top: false)
  @frames.pop_frame(top: top)
end
proxy_credentials=(creds) click to toggle source
# File lib/capybara/apparition/page.rb, line 101
def proxy_credentials=(creds)
  @proxy_credentials = creds
  setup_network_interception
end
push_frame(frame_el) click to toggle source
# File lib/capybara/apparition/page.rb, line 172
def push_frame(frame_el)
  node = command('DOM.describeNode', objectId: frame_el.base.id)
  frame_id = node['node']['frameId']

  timer = Capybara::Helpers.timer(expire_in: 10)
  while (frame = @frames[frame_id]).nil? || frame.loading?
    # Wait for the frame creation messages to be processed
    if timer.expired?
      puts 'Timed out waiting for frame to be ready'
      raise TimeoutError.new('push_frame')
    end
    sleep 0.1
  end

  frame.element_id = frame_el.base.id
  @frames.push_frame(frame.id)
  frame
end
refresh() click to toggle source
# File lib/capybara/apparition/page.rb, line 220
def refresh
  wait_for_loaded
  main_frame.reloading!
  command('Page.reload', ignoreCache: true)
  wait_for_loaded
end
render(options) click to toggle source
# File lib/capybara/apparition/page.rb, line 143
    def render(options)
      wait_for_loaded
      pixel_ratio = evaluate('window.devicePixelRatio')
      scale = (@browser.zoom_factor || 1).to_f / pixel_ratio
      if options[:format].to_s == 'pdf'
        params = { scale: scale }
        if @browser.paper_size
          params[:paperWidth] = @browser.paper_size[:width].to_f
          params[:paperHeight] = @browser.paper_size[:height].to_f
        end
        command('Page.printToPDF', **params)
      else
        clip_options = if options[:selector]
          pos = evaluate("document.querySelector('#{options.delete(:selector)}').getBoundingClientRect().toJSON();")
          %w[x y width height].each_with_object({}) { |key, hash| hash[key] = pos[key] }
        elsif options[:full]
          evaluate <<~JS
            { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight}
          JS
        else
          evaluate <<~JS
            { width: window.innerWidth, height: window.innerHeight }
          JS
        end
        options[:clip] = { x: 0, y: 0, scale: scale }.merge(clip_options)
        command('Page.captureScreenshot', **options)
      end['data']
    end
reset() click to toggle source
# File lib/capybara/apparition/page.rb, line 80
def reset
  @modals.clear
  @modal_messages.clear
  @response_headers = {}
  @status_code = 0
  @auth_attempts = []
  @proxy_auth_attempts = []
  @perm_headers = {}
end
response_headers() click to toggle source
# File lib/capybara/apparition/page.rb, line 237
def response_headers
  @response_headers[current_frame.id] || {}
end
scroll_to(left, top) click to toggle source
# File lib/capybara/apparition/page.rb, line 125
def scroll_to(left, top)
  wait_for_loaded
  execute('window.scrollTo(arguments[0], arguments[1])', left, top)
end
set_viewport(width:, height:, screen: nil) click to toggle source
# File lib/capybara/apparition/page.rb, line 317
def set_viewport(width:, height:, screen: nil)
  # wait_for_loaded
  @viewport_size = { width: width, height: height }
  result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
  begin
    @browser.command('Browser.setWindowBounds',
                     windowId: result['windowId'],
                     bounds: { width: width, height: height })
  rescue WrongWorld # TODO: Fix Error naming here
    @browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'normal' })
    retry
  end

  metrics = {
    mobile: false,
    width: width,
    height: height,
    deviceScaleFactor: 1
  }
  metrics[:screenWidth], metrics[:screenHeight] = *screen if screen

  command('Emulation.setDeviceMetricsOverride', **metrics)
end
title() click to toggle source
# File lib/capybara/apparition/page.rb, line 354
def title
  wait_for_loaded
  _raw_evaluate('document.title', context_id: main_frame.context_id)
end
update_headers(async: false) click to toggle source
# File lib/capybara/apparition/page.rb, line 376
def update_headers(async: false)
  if (ua = extra_headers.find { |k, _v| /^User-Agent$/i.match? k })
    send(async ? :async_command : :command, 'Network.setUserAgentOverride', userAgent: ua[1])
  end
  setup_network_interception
end
url_blacklist=(blacklist) click to toggle source
# File lib/capybara/apparition/page.rb, line 111
def url_blacklist=(blacklist)
  @url_blacklist = blacklist
  setup_network_blocking
end
url_whitelist=(whitelist) click to toggle source
# File lib/capybara/apparition/page.rb, line 116
def url_whitelist=(whitelist)
  @url_whitelist = whitelist
  setup_network_blocking
end
usable?() click to toggle source
# File lib/capybara/apparition/page.rb, line 76
def usable?
  !!current_frame&.context_id
end
visit(url) click to toggle source
# File lib/capybara/apparition/page.rb, line 282
def visit(url)
  wait_for_loaded
  @status_code = 0
  navigate_opts = { url: url, transitionType: 'reload' }
  navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
  response = command('Page.navigate', **navigate_opts)
  raise StatusFailError, 'args' => [url, response['errorText']] if response['errorText']

  main_frame.loading(response['loaderId'])
  wait_for_loaded
rescue TimeoutError
  raise StatusFailError.new('args' => [url])
end
wait_for_loaded(allow_obsolete: false) click to toggle source
# File lib/capybara/apparition/page.rb, line 243
def wait_for_loaded(allow_obsolete: false)
  # We can't reliably detect if the page is loaded, so just ensure the context
  # is usable
  timer = Capybara::Helpers.timer(expire_in: 30)
  page_function = '(function(){ return 1 == 1; })()'
  begin
    response = command('Runtime.evaluate',
                       expression: page_function,
                       contextId: current_frame.context_id,
                       returnByValue: false,
                       awaitPromise: true)
    process_response(response)
    current_frame.loaded!
  rescue # rubocop:disable Style/RescueStandardError
    return if allow_obsolete && current_frame.obsolete?

    unless timer.expired?
      sleep 0.05
      retry
    end
    puts 'Timedout waiting for page to be loaded' if ENV['DEBUG']
    raise TimeoutError.new('wait_for_loaded')
  end

  raise JavascriptError.new(js_error) if @js_error
end

Protected Instance Methods

current_frame() click to toggle source
# File lib/capybara/apparition/page.rb, line 402
def current_frame
  @frames.current
end
main_frame() click to toggle source
# File lib/capybara/apparition/page.rb, line 406
def main_frame
  @frames.main
end

Private Instance Methods

_execute_script(script, *args) click to toggle source
# File lib/capybara/apparition/page.rb, line 689
def _execute_script(script, *args)
  args = args.map do |arg|
    if arg.is_a? Capybara::Apparition::Node
      { objectId: arg.id }
    else
      { value: arg }
    end
  end
  context_id = current_frame&.context_id
  response = command('Runtime.callFunctionOn',
                     functionDeclaration: script,
                     executionContextId: context_id,
                     arguments: args,
                     returnByValue: false,
                     awaitPromise: true,
                     userGesture: true)
  process_response(response)
end
_raw_evaluate(page_function, context_id: nil) click to toggle source
# File lib/capybara/apparition/page.rb, line 708
def _raw_evaluate(page_function, context_id: nil)
  wait_for_loaded
  return unless page_function.is_a? String

  context_id ||= current_frame.context_id

  response = command('Runtime.evaluate',
                     expression: page_function,
                     contextId: context_id,
                     returnByValue: false,
                     awaitPromise: true)
  process_response(response)
end
accept_modal?(type, message:, manual:) click to toggle source
# File lib/capybara/apparition/page.rb, line 675
def accept_modal?(type, message:, manual:)
  if type == :beforeunload
    true
  else
    response = @modals.pop
    if !response&.key?(type)
      manual ? manual_unexpected_modal(type) : auto_unexpected_modal(type)
    else
      @modal_messages.push(message)
      response[type].nil? ? true : response[type]
    end
  end
end
auto_unexpected_modal(type) click to toggle source
# File lib/capybara/apparition/page.rb, line 748
def auto_unexpected_modal(type)
  case type
  when :prompt
    warn 'Unexpected prompt modal - accepting with the default value.' \
         'You should be using `accept_prompt` or `dismiss_prompt`.'
  when :confirm
    warn 'Unexpected confirm modal - accepting.' \
         'You should be using `accept_confirm` or `dismiss_confirm`.'
  else
    warn 'Unexpected alert modal - clearing.' \
         'You should be using `accept_alert`.'
  end
  true
end
eval_wrapped_script(wrapper, script, args) click to toggle source
# File lib/capybara/apparition/page.rb, line 412
def eval_wrapped_script(wrapper, script, args)
  wait_for_loaded
  _execute_script format(wrapper, script: script), *args
end
frame_offset(frame) click to toggle source
# File lib/capybara/apparition/page.rb, line 417
def frame_offset(frame)
  return { x: 0, y: 0 } if frame.id == main_frame.id

  result = command('DOM.getBoxModel', objectId: frame.element_id)
  x, y = result['model']['content']
  { x: x, y: y }
end
go_history(delta) click to toggle source
# File lib/capybara/apparition/page.rb, line 665
def go_history(delta)
  history = command('Page.getNavigationHistory')
  entry = history['entries'][history['currentIndex'] + delta]
  return nil unless entry

  main_frame.loading(-1)
  command('Page.navigateToHistoryEntry', entryId: entry['id'])
  wait_for_loaded
end
handle_proxy_auth(interception_id) click to toggle source
# File lib/capybara/apparition/page.rb, line 763
def handle_proxy_auth(interception_id)
  credentials_response = if @proxy_auth_attempts.include?(interception_id)
    puts 'Cancelling proxy auth' if ENV['DEBUG']
    { response: 'CancelAuth' }
  else
    puts 'Replying with proxy auth credentials' if ENV['DEBUG']
    @proxy_auth_attempts.push(interception_id)
    { response: 'ProvideCredentials' }.merge(@browser.proxy_auth || {})
  end
  continue_request(interception_id, authChallengeResponse: credentials_response)
end
handle_user_auth(interception_id) click to toggle source
# File lib/capybara/apparition/page.rb, line 775
def handle_user_auth(interception_id)
  credentials_response = if @auth_attempts.include?(interception_id)
    puts 'Cancelling auth' if ENV['DEBUG']
    { response: 'CancelAuth' }
  else
    @auth_attempts.push(interception_id)
    puts 'Replying with auth credentials' if ENV['DEBUG']
    { response: 'ProvideCredentials' }.merge(@credentials || {})
  end
  continue_request(interception_id, authChallengeResponse: credentials_response)
end
manual_unexpected_modal(type) click to toggle source
# File lib/capybara/apparition/page.rb, line 740
def manual_unexpected_modal(type)
  warn "An unexpected #{type} modal has opened - please close"
  @modal_mutex.synchronize do
    @modal_closed.wait(@modal_mutex)
  end
  nil
end
process_intercepted_fetch(interception_id, request, resource_type) click to toggle source
# File lib/capybara/apparition/page.rb, line 633
def process_intercepted_fetch(interception_id, request, resource_type)
  navigation = (resource_type == 'Document')
  headers, url = request.values_at('headers', 'url')
  headers = headers.merge(extra_headers)

  unless @temp_headers.empty? || navigation # rubocop:disable Style/IfUnlessModifier
    headers.delete_if { |name, value| @temp_headers[name] == value }
  end
  unless @temp_no_redirect_headers.empty? || !navigation
    headers.delete_if { |name, value| @temp_no_redirect_headers[name] == value }
  end
  if (accept = perm_headers.keys.find { |k| /accept/i.match? k })
    headers[accept] = perm_headers[accept]
  end

  if @url_blacklist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
    async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
  elsif @url_whitelist.any?
    if @url_whitelist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
      async_command('Fetch.continueRequest',
                    requestId: interception_id,
                    headers: headers.map { |k, v| { name: k, value: v } })
    else
      async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
    end
  else
    async_command('Fetch.continueRequest',
                  requestId: interception_id,
                  headers: headers.map { |k, v| { name: k, value: v } })
  end
end
process_response(response) click to toggle source
# File lib/capybara/apparition/page.rb, line 722
def process_response(response)
  return nil if response.nil?

  exception = response['exceptionDetails']&.dig('exception')
  if exception
    case exception['className']
    when 'DOMException'
      raise ::Capybara::Apparition::BrowserError.new('name' => exception['description'], 'args' => nil)
    when 'ObsoleteException'
      raise ::Capybara::Apparition::ObsoleteNode.new(self, '') if exception['value'] == 'ObsoleteNode'
    else
      raise Capybara::Apparition::JavascriptError, [exception['description']]
    end
  end

  DevToolsProtocol::RemoteObject.new(self, response['result']).value
end
register_event_handlers() click to toggle source
# File lib/capybara/apparition/page.rb, line 425
def register_event_handlers
  @session.on 'Page.javascriptDialogOpening' do |type:, message:, has_browser_handler:, **params|
    type = type.to_sym
    accept = accept_modal?(type, message: message, manual: has_browser_handler)
    next if accept.nil?

    if type == :prompt
      case accept
      when false
        async_command('Page.handleJavaScriptDialog', accept: false)
      when true
        async_command('Page.handleJavaScriptDialog', accept: true, promptText: params[:default_prompt])
      else
        async_command('Page.handleJavaScriptDialog', accept: true, promptText: accept)
      end
    else
      async_command('Page.handleJavaScriptDialog', accept: accept)
    end
  end

  @session.on 'Page.javascriptDialogClosed' do
    @modal_mutex.synchronize do
      @modal_closed.signal
    end
  end

  @session.on 'Page.windowOpen' do |**params|
    puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
    @browser.refresh_pages(opener: self)
  end

  @session.on 'Page.frameAttached' do |**params|
    puts "**** frameAttached called with #{params}" if ENV['DEBUG']
    # @frames.get(params["frameId"]) = Frame.new(params)
  end

  @session.on 'Page.frameDetached' do |frame_id:, **params|
    @frames.delete(frame_id)
    puts "**** frameDetached called with #{frame_id} : #{params}" if ENV['DEBUG']
  end

  @session.on 'Page.frameNavigated' do |frame:|
    puts "**** frameNavigated called with #{frame}" if ENV['DEBUG']
    unless @frames.exists?(frame['id'])
      puts "**** creating frame for #{frame['id']}" if ENV['DEBUG']
      @frames.add(frame['id'], frame)
    end
    @frames.get(frame['id'])&.loading(frame['loaderId'] || -1)
  end

  @session.on 'Page.frameStartedLoading' do |frame_id:|
    puts "Setting loading for #{frame_id}" if ENV['DEBUG']
    @frames.get(frame_id)&.loading(-1)
  end

  @session.on 'Page.frameStoppedLoading' do |frame_id:|
    puts "Setting loaded for #{frame_id}" if ENV['DEBUG']
    @frames.get(frame_id)&.loaded!
  end

  # @session.on 'Page.lifecycleEvent' do |params|
  #   # Provides a lot of useful info - but lots of overhead
  #   puts "Lifecycle: #{params['name']} - frame: #{params['frameId']} - loader: #{params['loaderId']}" if ENV['DEBUG']
  #   case params['name']
  #   when 'init'
  #     @frames.get(params['frameId'])&.loading(params['loaderId'])
  #   when 'firstMeaningfulPaint',
  #        'networkIdle'
  #     @frames.get(params['frameId']).tap do |frame|
  #       frame.loaded! if frame.loader_id == params['loaderId']
  #     end
  #   end
  # end

  @session.on('Page.domContentEventFired') do
    # TODO: Really need something better than this
    main_frame.loaded! if @status_code != 200
  end

  @session.on 'Page.navigatedWithinDocument' do |frame_id:, **params|
    puts "**** navigatedWithinDocument called with #{frame_id}: #{params}" if ENV['DEBUG']
    @frames.get(frame_id).loaded! if frame_id == main_frame.id
  end

  @session.on 'Runtime.executionContextCreated' do |context:|
    frame_id = context.dig('auxData', 'frameId')
    if context.dig('auxData', 'isDefault') && frame_id
      if (frame = @frames.get(frame_id))
        frame.context_id = context['id']
      elsif ENV['DEBUG']
        puts "unknown frame for context #{frame_id}"
      end
    end
  end

  @session.on 'Runtime.executionContextDestroyed' do |execution_context_id:, **params|
    puts "executionContextDestroyed: #{execution_context_id} : #{params}" if ENV['DEBUG']
    @frames.destroy_context(execution_context_id)
  end

  @session.on 'Network.requestWillBeSent' do |request_id:, request: nil, **|
    @open_resource_requests[request_id] = request&.dig('url')
  end

  @session.on 'Network.responseReceived' do |request_id:, **|
    @open_resource_requests.delete(request_id)
    temp_headers.clear
    update_headers(async: true)
  end

  @session.on 'Network.requestWillBeSent' do |**params|
    @network_traffic.push(NetworkTraffic::Request.new(params))
  end

  @session.on 'Network.responseReceived' do |request_id:, response:, **|
    req = @network_traffic.find { |request| request.request_id == request_id }
    req.response = NetworkTraffic::Response.new(response) if req
  end

  @session.on 'Network.responseReceived' do |type:, frame_id: nil, response: nil, **|
    if type == 'Document'
      @response_headers[frame_id] = response['headers']
      @status_code = response['status']
    end
  end

  @session.on 'Network.loadingFailed' do |type:, request_id:, blocked_reason: nil, error_text: nil, **params|
    req = @network_traffic.find { |request| request.request_id == request_id }
    req&.blocked_params = params if blocked_reason
    if type == 'Document'
      puts "Loading Failed - request: #{request_id} : #{error_text}" if ENV['DEBUG']
    end
  end

  @session.on 'Fetch.requestPaused' do |request:, request_id:, resource_type:, **|
    process_intercepted_fetch(request_id, request, resource_type)
  end

  @session.on 'Fetch.authRequired' do |request_id:, auth_challenge: nil, **|
    next unless auth_challenge

    credentials_response = if auth_challenge['source'] == 'Proxy'
      if @proxy_auth_attempts.include?(request_id)
        puts 'Cancelling proxy auth' if ENV['DEBUG']
        { response: 'CancelAuth' }
      else
        puts 'Replying with proxy auth credentials' if ENV['DEBUG']
        @proxy_auth_attempts.push(request_id)
        { response: 'ProvideCredentials' }.merge(@browser.proxy_auth || {})
      end
    elsif @auth_attempts.include?(request_id)
      puts 'Cancelling auth' if ENV['DEBUG']
      { response: 'CancelAuth' }
    else
      @auth_attempts.push(request_id)
      puts 'Replying with auth credentials' if ENV['DEBUG']
      { response: 'ProvideCredentials' }.merge(@credentials || {})
    end

    async_command('Fetch.continueWithAuth', requestId: request_id, authChallengeResponse: credentials_response)
  end

  @session.on 'Runtime.consoleAPICalled' do |**params|
    # {"type"=>"log", "args"=>[{"type"=>"string", "value"=>"hello"}], "executionContextId"=>2, "timestamp"=>1548722854903.285, "stackTrace"=>{"callFrames"=>[{"functionName"=>"", "scriptId"=>"15", "url"=>"http://127.0.0.1:53977/", "lineNumber"=>6, "columnNumber"=>22}]}}
    details = params.dig(:stack_trace, 'callFrames')&.first
    @browser.console.log(params[:type],
                         params[:args].map { |arg| arg['description'] || arg['value'] }.join(' ').to_s,
                         source: details['url'].empty? ? nil : details['url'],
                         line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
                         columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
  end

  # @session.on 'Security.certificateError' do |params|
  #   async_command 'Network.continueInterceptedRequest', interceptionId: id, **params
  # end

  # @session.on 'Log.entryAdded' do |params|
  #   log_entry = params['entry']
  #   if params.values_at('source', 'level') == ['javascript', 'error']
  #     @js_error ||= params['string']
  #   end
  # end
end
register_js_error_handler() click to toggle source
# File lib/capybara/apparition/page.rb, line 609
def register_js_error_handler
  @session.on 'Runtime.exceptionThrown' do |exception_details: nil, **|
    @js_error ||= exception_details&.dig('exception', 'description') if @raise_js_errors

    details = exception_details&.dig('stackTrace', 'callFrames')&.first ||
              exception_details || {}
    @browser.console.log('error',
                         exception_details&.dig('exception', 'description'),
                         source: details['url'].to_s.empty? ? nil : details['url'],
                         line_number: details['lineNumber'].to_i.zero? ? nil : details['lineNumber'],
                         columnNumber: details['columnNumber'].to_i.zero? ? nil : details['columnNumber'])
  end
end
setup_network_blocking() click to toggle source
# File lib/capybara/apparition/page.rb, line 623
def setup_network_blocking
  command 'Network.setBlockedURLs', urls: @url_blacklist
  setup_network_interception
end
setup_network_interception() click to toggle source
# File lib/capybara/apparition/page.rb, line 628
def setup_network_interception
  async_command 'Network.setCacheDisabled', cacheDisabled: true
  async_command 'Fetch.enable', handleAuthRequests: true
end