475 lines
12 KiB
Ruby
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
|