teampoltergeist--poltergeist/lib/capybara/poltergeist/driver.rb

475 lines
12 KiB
Ruby

# frozen_string_literal: true
require 'uri'
module Capybara::Poltergeist
class Driver < Capybara::Driver::Base
DEFAULT_TIMEOUT = 30
attr_reader :app, :options
def initialize(app, options = {})
@app = app
@options = options
@browser = nil
@inspector = nil
@server = nil
@client = nil
@started = false
end
def needs_server?
true
end
def browser
@browser ||= begin
browser = Browser.new(server, client, logger)
browser.js_errors = options[:js_errors] if options.key?(:js_errors)
browser.extensions = options.fetch(:extensions, [])
browser.debug = true if options[:debug]
browser.url_blacklist = options[:url_blacklist] if options.key?(:url_blacklist)
browser.url_whitelist = options[:url_whitelist] if options.key?(:url_whitelist)
browser.page_settings = options[:page_settings] if options.key?(:page_settings)
browser
end
end
def inspector
@inspector ||= options[:inspector] && Inspector.new(options[:inspector])
end
def server
@server ||= Server.new(options[:port], options.fetch(:timeout) { DEFAULT_TIMEOUT }, options[:host])
end
def client
@client ||= Client.start(server,
path: options[:phantomjs],
window_size: options[:window_size],
phantomjs_options: phantomjs_options,
phantomjs_logger: phantomjs_logger)
end
def phantomjs_options
list = options[:phantomjs_options] || []
# PhantomJS defaults to only using SSLv3, which since POODLE (Oct 2014)
# many sites have dropped from their supported protocols (eg PayPal,
# Braintree).
list += ['--ignore-ssl-errors=yes'] unless list.grep(/ignore-ssl-errors/).any?
list += ['--ssl-protocol=TLSv1'] unless list.grep(/ssl-protocol/).any?
list += ["--remote-debugger-port=#{inspector.port}", '--remote-debugger-autorun=yes'] if inspector
list
end
def client_pid
client.pid
end
def timeout
server.timeout
end
def timeout=(sec)
server.timeout = sec
end
def restart
browser.restart
end
def quit
server.stop
client.stop
end
# logger should be an object that responds to puts, or nil
def logger
options[:logger] || (options[:debug] && STDERR)
end
# logger should be an object that behaves like IO or nil
def phantomjs_logger
options.fetch(:phantomjs_logger, nil)
end
def visit(url)
@started = true
browser.visit(url)
end
def current_url
if Capybara::VERSION.to_f < 3.0
frame_url
else
browser.current_url.gsub(' ', '%20') # PhantomJS < 2.1 doesn't escape spaces
end
end
def frame_url
browser.frame_url.gsub(' ', '%20') # PhantomJS < 2.1 doesn't escape spaces
end
def status_code
browser.status_code
end
def html
browser.body
end
alias_method :body, :html
def source
browser.source.to_s
end
def title
if Capybara::VERSION.to_f < 3.0
frame_title
else
browser.title
end
end
def frame_title
browser.frame_title
end
def find(method, selector)
browser.find(method, selector).map { |page_id, id| Capybara::Poltergeist::Node.new(self, page_id, id) }
end
def find_xpath(selector)
find :xpath, selector
end
def find_css(selector)
find :css, selector
end
def click(x, y)
browser.click_coordinates(x, y)
end
def evaluate_script(script, *args)
result = browser.evaluate(script, *native_args(args))
unwrap_script_result(result)
end
def evaluate_async_script(script, *args)
result = browser.evaluate_async(script, session_wait_time, *native_args(args))
unwrap_script_result(result)
end
def execute_script(script, *args)
browser.execute(script, *native_args(args))
nil
end
def within_frame(name, &block)
browser.within_frame(name, &block)
end
def switch_to_frame(locator)
browser.switch_to_frame(locator)
end
def current_window_handle
browser.window_handle
end
def window_handles
browser.window_handles
end
def close_window(handle)
browser.close_window(handle)
end
def open_new_window
browser.open_new_window
end
def switch_to_window(handle)
browser.switch_to_window(handle)
end
def within_window(name, &block)
browser.within_window(name, &block)
end
def no_such_window_error
NoSuchWindowError
end
def reset!
browser.reset
browser.url_blacklist = options[:url_blacklist] if options.key?(:url_blacklist)
browser.url_whitelist = options[:url_whitelist] if options.key?(:url_whitelist)
@started = false
end
def save_screenshot(path, options = {})
browser.render(path, options)
end
alias_method :render, :save_screenshot
def render_base64(format = :png, options = {})
browser.render_base64(format, options)
end
def paper_size=(size = {})
browser.set_paper_size(size)
end
def zoom_factor=(zoom_factor)
browser.set_zoom_factor(zoom_factor)
end
def resize(width, height)
browser.resize(width, height)
end
alias_method :resize_window, :resize
def resize_window_to(handle, width, height)
within_window(handle) do
resize(width, height)
end
end
def maximize_window(handle)
resize_window_to(handle, *screen_size)
end
def window_size(handle)
within_window(handle) do
evaluate_script('[window.innerWidth, window.innerHeight]')
end
end
def scroll_to(left, top)
browser.scroll_to(left, top)
end
def network_traffic(type = nil)
browser.network_traffic(type)
end
def clear_network_traffic
browser.clear_network_traffic
end
def set_proxy(ip, port, type = 'http', user = nil, password = nil)
browser.set_proxy(ip, port, type, user, password)
end
def headers
browser.get_headers
end
def headers=(headers)
browser.set_headers(headers)
end
def add_headers(headers)
browser.add_headers(headers)
end
def add_header(name, value, options = {})
browser.add_header({ name => value }, { permanent: true }.merge(options))
end
def response_headers
browser.response_headers
end
def cookies
browser.cookies
end
def set_cookie(name, value, options = {})
options[:name] ||= name
options[:value] ||= value
options[:domain] ||= begin
if @started
URI.parse(browser.current_url).host
else
URI.parse(default_cookie_host).host || '127.0.0.1'
end
end
browser.set_cookie(options)
end
def remove_cookie(name)
browser.remove_cookie(name)
end
def clear_cookies
browser.clear_cookies
end
def cookies_enabled=(flag)
browser.cookies_enabled = flag
end
def clear_memory_cache
browser.clear_memory_cache
end
# * PhantomJS with set settings doesn't send `Authorize` on POST request
# * With manually set header PhantomJS makes next request with
# `Authorization: Basic Og==` header when settings are empty and the
# response was `401 Unauthorized` (which means Base64.encode64(':')).
# Combining both methods to reach proper behavior.
def basic_authorize(user, password)
browser.set_http_auth(user, password)
credentials = ["#{user}:#{password}"].pack('m*').strip
add_header('Authorization', "Basic #{credentials}")
end
def debug
if @options[:inspector]
# Fall back to default scheme
scheme = URI.parse(browser.current_url).scheme rescue nil
scheme = 'http' if scheme != 'https'
inspector.open(scheme)
pause
else
raise Error, 'To use the remote debugging, you have to launch the driver ' \
'with `:inspector => true` configuration option'
end
end
def pause
# STDIN is not necessarily connected to a keyboard. It might even be closed.
# So we need a method other than keypress to continue.
# In jRuby - STDIN returns immediately from select
# see https://github.com/jruby/jruby/issues/1783
read, write = IO.pipe
Thread.new { IO.copy_stream(STDIN, write); write.close }
STDERR.puts "Poltergeist execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
signal = false
old_trap = trap('SIGCONT') { signal = true; STDERR.puts "\nSignal SIGCONT received" }
keyboard = IO.select([read], nil, nil, 1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received
unless signal
begin
input = read.read_nonblock(80) # clear out the read buffer
puts unless input&.end_with?("\n")
rescue EOFError, IO::WaitReadable # Ignore problems reading from STDIN.
end
end
ensure
trap('SIGCONT', old_trap) # Restore the previous signal handler, if there was one.
STDERR.puts 'Continuing'
end
def wait?
true
end
def invalid_element_errors
[Capybara::Poltergeist::ObsoleteNode, Capybara::Poltergeist::MouseEventFailed]
end
def go_back
browser.go_back
end
def go_forward
browser.go_forward
end
def refresh
browser.refresh
end
def accept_modal(type, options = {})
case type
when :confirm
browser.accept_confirm
when :prompt
browser.accept_prompt options[:with]
end
yield if block_given?
find_modal(options)
end
def dismiss_modal(type, options = {})
case type
when :confirm
browser.dismiss_confirm
when :prompt
browser.dismiss_prompt
end
yield if block_given?
find_modal(options)
end
private
def native_args(args)
args.map { |arg| arg.is_a?(Capybara::Poltergeist::Node) ? arg.native : arg }
end
def screen_size
options[:screen_size] || [1366, 768]
end
def find_modal(options)
start_time = Time.now
timeout_sec = options.fetch(:wait) { session_wait_time }
expect_text = options[:text]
expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
not_found_msg = 'Unable to find modal dialog'
not_found_msg += " with #{expect_text}" if expect_text
begin
modal_text = browser.modal_message
raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
rescue Capybara::ModalNotFound => e
raise e, not_found_msg if (Time.now - start_time) >= timeout_sec
sleep(0.05)
retry
end
modal_text
end
def session_wait_time
if respond_to?(:session_options)
session_options.default_max_wait_time
else
begin Capybara.default_max_wait_time rescue Capybara.default_wait_time end
end
end
def default_cookie_host
if respond_to?(:session_options)
session_options.app_host
else
Capybara.app_host
end || ''
end
def unwrap_script_result(arg)
case arg
when Array
arg.map { |e| unwrap_script_result(e) }
when Hash
return Capybara::Poltergeist::Node.new(self, arg['ELEMENT']['page_id'], arg['ELEMENT']['id']) if arg['ELEMENT']
arg.each { |k, v| arg[k] = unwrap_script_result(v) }
else
arg
end
end
end
end