From e78f223ce3a0991af329eeac0134b7f6ebeb6b20 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Thu, 20 Oct 2022 16:42:10 +0500 Subject: [PATCH] Support proxy for a page (#300) * refactor: parse options with a dedicated Options class * chore!: drop support for proxy `server: true` option * fix: support options for xvfb * style: fix rubocop warnings * fix: missing Ferrum module * feat: support :proxy option for Browser#create_page --- README.md | 14 +- lib/ferrum/browser.rb | 105 +-- lib/ferrum/browser/client.rb | 2 +- lib/ferrum/browser/command.rb | 11 +- lib/ferrum/browser/options.rb | 84 ++ lib/ferrum/browser/options/base.rb | 5 +- lib/ferrum/browser/options/chrome.rb | 23 +- lib/ferrum/browser/options/firefox.rb | 9 +- lib/ferrum/browser/process.rb | 11 +- lib/ferrum/browser/xvfb.rb | 2 +- lib/ferrum/context.rb | 5 +- lib/ferrum/contexts.rb | 4 +- lib/ferrum/page.rb | 28 +- lib/ferrum/target.rb | 10 +- spec/browser/binary_spec.rb | 194 ++-- spec/browser/version_info_spec.rb | 88 +- spec/browser/xvfb_spec.rb | 100 +- spec/browser_spec.rb | 827 +++++++++-------- spec/context_spec.rb | 60 +- spec/cookies_spec.rb | 384 ++++---- spec/dialog_spec.rb | 126 ++- spec/frame/runtime_spec.rb | 268 ++++++ spec/frame_spec.rb | 534 ++++++----- spec/headers_spec.rb | 312 ++++--- spec/keyboard_spec.rb | 38 +- spec/mouse_spec.rb | 322 ++++--- spec/network/auth_request_spec.rb | 8 +- spec/network/error_spec.rb | 24 +- spec/network/intercepted_request_spec.rb | 8 +- spec/network/request_spec.rb | 8 +- spec/network/response_spec.rb | 138 ++- spec/network_spec.rb | 596 ++++++------ spec/node_spec.rb | 1062 +++++++++++----------- spec/page/animation_spec.rb | 28 +- spec/page/screenshot_spec.rb | 642 +++++++------ spec/page/tracing_spec.rb | 244 +++-- spec/page_spec.rb | 284 +++--- spec/rbga_spec.rb | 76 +- spec/runtime_spec.rb | 270 ------ spec/spec_helper.rb | 2 +- spec/support/global_helpers.rb | 2 +- spec/unit/browser_spec.rb | 62 +- spec/unit/process_spec.rb | 50 +- 43 files changed, 3555 insertions(+), 3515 deletions(-) create mode 100644 lib/ferrum/browser/options.rb create mode 100644 spec/frame/runtime_spec.rb delete mode 100644 spec/runtime_spec.rb diff --git a/README.md b/README.md index 964d552b..441d3055 100644 --- a/README.md +++ b/README.md @@ -605,21 +605,24 @@ You can set a proxy with the `proxy` option. ```ruby browser = Ferrum::Browser.new(proxy: { host: "x.x.x.x", port: "8800" }) -browser = Ferrum::Browser.new(proxy: { host: "x.x.x.x", port: "8800", user: "user", pasword: "pa$$" }) +browser = Ferrum::Browser.new(proxy: { host: "x.x.x.x", port: "8800", user: "user", password: "pa$$" }) ``` -Chrome Devtools Protocol does not support changing proxies after the browser is launched. If you want to change proxies, you must restart your browser, which may not be convenient. There is a workaround. Ferrum provides a wrapper for a proxy server that can rotate proxies. We can run a proxy in the same process and rotate proxies inside this proxy server: +Chrome Devtools Protocol does not support changing proxies after the browser is launched. If you want to change proxies, +you must restart your browser, which may not be convenient. There is a workaround. Ferrum provides a wrapper for a proxy +server that can rotate proxies. We can run a proxy in the same process and rotate proxies inside this proxy server: ```ruby -browser = Ferrum::Browser.new(proxy: { server: true }) +proxy = Ferrum::Proxy.start(**options) +browser = Ferrum::Browser.new(proxy: { host: proxy.host, port: proxy.port }) -browser.proxy_server.rotate(host: "x.x.x.x", port: 31337, user: "user", password: "password") +proxy.rotate(host: "x.x.x.x", port: 31337, user: "user", password: "password") browser.create_page(new_context: true) do |page| page.go_to("https://api.ipify.org?format=json") page.body # => "x.x.x.x" end -browser.proxy_server.rotate(host: "y.y.y.y", port: 31337, user: "user", password: "password") +proxy.rotate(host: "y.y.y.y", port: 31337, user: "user", password: "password") browser.create_page(new_context: true) do |page| page.go_to("https://api.ipify.org?format=json") page.body # => "y.y.y.y" @@ -633,7 +636,6 @@ You can specify semi-colon-separated list of hosts for which proxy shouldn't be ```ruby browser = Ferrum::Browser.new(proxy: { host: "x.x.x.x", port: "8800", bypass: "*.google.com;*foo.com" }) -browser = Ferrum::Browser.new(proxy: { server: true, bypass: "*.google.com;*foo.com" }) ``` diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index 52823e93..7786de88 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -6,6 +6,7 @@ require "ferrum/proxy" require "ferrum/contexts" require "ferrum/browser/xvfb" +require "ferrum/browser/options" require "ferrum/browser/process" require "ferrum/browser/client" require "ferrum/browser/binary" @@ -13,10 +14,6 @@ module Ferrum class Browser - DEFAULT_TIMEOUT = ENV.fetch("FERRUM_DEFAULT_TIMEOUT", 5).to_i - WINDOW_SIZE = [1024, 768].freeze - BASE_URL_SCHEMA = %w[http https].freeze - extend Forwardable delegate %i[default_context] => :contexts delegate %i[targets create_target page pages windows] => :default_context @@ -33,10 +30,8 @@ class Browser playback_rate playback_rate=] => :page delegate %i[default_user_agent] => :process - attr_reader :client, :process, :contexts, :logger, :js_errors, :pending_connection_errors, - :slowmo, :base_url, :options, :window_size, :ws_max_receive_size, :proxy_options, - :proxy_server - attr_writer :timeout + attr_reader :client, :process, :contexts, :options, :window_size, :base_url + attr_accessor :timeout # # Initializes the browser. @@ -63,7 +58,7 @@ class Browser # # @option options [Integer, Float] :slowmo # Set a delay in seconds to wait before sending command. - # Usefull companion of headless option, so that you have time to see + # Useful companion of headless option, so that you have time to see # changes. # # @option options [Numeric] :timeout (5) @@ -126,43 +121,12 @@ class Browser # Environment variables you'd like to pass through to the process. # def initialize(options = nil) - options ||= {} - - @client = nil - @window_size = options.fetch(:window_size, WINDOW_SIZE) - @original_window_size = @window_size - - @options = Hash(options.merge(window_size: @window_size)) - @logger, @timeout, @ws_max_receive_size = - @options.values_at(:logger, :timeout, :ws_max_receive_size) - @js_errors = @options.fetch(:js_errors, false) - - if @options[:proxy] - @proxy_options = @options[:proxy] - - if @proxy_options[:server] - @proxy_server = Proxy.start(**@proxy_options.slice(:host, :port, :user, :password)) - @proxy_options.merge!(host: @proxy_server.host, port: @proxy_server.port) - end - - @options[:browser_options] ||= {} - address = "#{@proxy_options[:host]}:#{@proxy_options[:port]}" - @options[:browser_options].merge!("proxy-server" => address) - @options[:browser_options].merge!("proxy-bypass-list" => @proxy_options[:bypass]) if @proxy_options[:bypass] - end - - @pending_connection_errors = @options.fetch(:pending_connection_errors, true) - @slowmo = @options[:slowmo].to_f - - self.base_url = @options[:base_url] if @options.key?(:base_url) - - if ENV.fetch("FERRUM_DEBUG", nil) && !@logger - $stdout.sync = true - @logger = $stdout - @options[:logger] = @logger - end + @options = Options.new(options) + @client = @process = @contexts = nil - @options.freeze + @timeout = @options.timeout + @window_size = @options.window_size + @base_url = @options.base_url if @options.base_url start end @@ -173,22 +137,39 @@ def initialize(options = nil) # @param [String] value # The new base URL value. # - # @return [String] - # The base URL value. + # @raise [ArgumentError] when path is not absolute or doesn't include schema + # + # @return [Addressable::URI] + # The parsed base URI value. # def base_url=(value) - parsed = Addressable::URI.parse(value) - unless BASE_URL_SCHEMA.include?(parsed.normalized_scheme) - raise "Set `base_url` should be absolute and include schema: #{BASE_URL_SCHEMA}" - end - - @base_url = parsed + @base_url = options.parse_base_url(value) end - def create_page(new_context: false) - page = if new_context - context = contexts.create - context.create_page + # + # Creates a new page. + # + # @param [Boolean] new_context + # Whether to create a page in a new context or not. + # + # @param [Hash] proxy + # Whether to use proxy for a page. The page will be created in a new context if so. + # + # @return [Ferrum::Page] + # Created page. + # + def create_page(new_context: false, proxy: nil) + page = if new_context || proxy + params = {} + + if proxy + options.parse_proxy(proxy) + params.merge!(proxyServer: "#{proxy[:host]}:#{proxy[:port]}") + params.merge!(proxyBypassList: proxy[:bypass]) if proxy[:bypass] + end + + context = contexts.create(**params) + context.create_page(proxy: proxy) else default_context.create_page end @@ -202,7 +183,7 @@ def create_page(new_context: false) end def extensions - @extensions ||= Array(@options[:extensions]).map do |ext| + @extensions ||= Array(options.extensions).map do |ext| (ext.is_a?(Hash) && ext[:source]) || File.read(ext) end end @@ -224,10 +205,6 @@ def evaluate_on_new_document(expression) extensions << expression end - def timeout - @timeout || DEFAULT_TIMEOUT - end - def command(*args) @client.command(*args) rescue DeadBrowserError @@ -250,7 +227,7 @@ def command(*args) # browser.quit # def reset - @window_size = @original_window_size + @window_size = options.window_size contexts.reset end @@ -289,7 +266,7 @@ def version def start Utils::ElapsedTime.start - @process = Process.start(@options) + @process = Process.start(options) @client = Client.new(self, @process.ws_url) @contexts = Contexts.new(self) end diff --git a/lib/ferrum/browser/client.rb b/lib/ferrum/browser/client.rb index eb59b218..7927a050 100644 --- a/lib/ferrum/browser/client.rb +++ b/lib/ferrum/browser/client.rb @@ -12,7 +12,7 @@ def initialize(browser, ws_url, id_starts_with: 0) @browser = browser @command_id = id_starts_with @pendings = Concurrent::Hash.new - @ws = WebSocket.new(ws_url, @browser.ws_max_receive_size, @browser.logger) + @ws = WebSocket.new(ws_url, @browser.options.ws_max_receive_size, @browser.options.logger) @subscriber, @interrupter = Subscriber.build(2) @thread = Thread.new do diff --git a/lib/ferrum/browser/command.rb b/lib/ferrum/browser/command.rb index 6fa7ed26..a04c8d68 100644 --- a/lib/ferrum/browser/command.rb +++ b/lib/ferrum/browser/command.rb @@ -10,7 +10,7 @@ class Command # Currently only these browsers support CDP: # https://github.com/cyrus-and/chrome-remote-interface#implementations def self.build(options, user_data_dir) - defaults = case options[:browser_name] + defaults = case options.browser_name when :firefox Options::Firefox.options when :chrome, :opera, :edge, nil @@ -29,14 +29,14 @@ def initialize(defaults, options, user_data_dir) @defaults = defaults @options = options @user_data_dir = user_data_dir - @path = options[:browser_path] || ENV.fetch("BROWSER_PATH", nil) || defaults.detect_path + @path = options.browser_path || ENV.fetch("BROWSER_PATH", nil) || defaults.detect_path raise BinaryNotFoundError, NOT_FOUND unless @path merge_options end def xvfb? - !!options[:xvfb] + !!options.xvfb end def to_a @@ -47,9 +47,8 @@ def to_a def merge_options @flags = defaults.merge_required(@flags, options, @user_data_dir) - @flags = defaults.merge_default(@flags, options) unless options[:ignore_default_browser_options] - - @flags.merge!(options.fetch(:browser_options, {})) + @flags = defaults.merge_default(@flags, options) unless options.ignore_default_browser_options + @flags.merge!(options.browser_options) end end end diff --git a/lib/ferrum/browser/options.rb b/lib/ferrum/browser/options.rb new file mode 100644 index 00000000..5addb72c --- /dev/null +++ b/lib/ferrum/browser/options.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Ferrum + class Browser + class Options + HEADLESS = true + BROWSER_PORT = "0" + BROWSER_HOST = "127.0.0.1" + WINDOW_SIZE = [1024, 768].freeze + BASE_URL_SCHEMA = %w[http https].freeze + DEFAULT_TIMEOUT = ENV.fetch("FERRUM_DEFAULT_TIMEOUT", 5).to_i + PROCESS_TIMEOUT = ENV.fetch("FERRUM_PROCESS_TIMEOUT", 10).to_i + DEBUG_MODE = !ENV.fetch("FERRUM_DEBUG", nil).nil? + + attr_reader :window_size, :timeout, :logger, :ws_max_receive_size, + :js_errors, :base_url, :slowmo, :pending_connection_errors, + :url, :env, :process_timeout, :browser_name, :browser_path, + :save_path, :extensions, :proxy, :port, :host, :headless, + :ignore_default_browser_options, :browser_options, :xvfb + + def initialize(options = nil) + @options = Hash(options&.dup) + @port = @options.fetch(:port, BROWSER_PORT) + @host = @options.fetch(:host, BROWSER_HOST) + @timeout = @options.fetch(:timeout, DEFAULT_TIMEOUT) + @window_size = @options.fetch(:window_size, WINDOW_SIZE) + @js_errors = @options.fetch(:js_errors, false) + @headless = @options.fetch(:headless, HEADLESS) + @pending_connection_errors = @options.fetch(:pending_connection_errors, true) + @process_timeout = @options.fetch(:process_timeout, PROCESS_TIMEOUT) + @browser_options = @options.fetch(:browser_options, {}) + @slowmo = @options[:slowmo].to_f + + @ws_max_receive_size, @env, @browser_name, @browser_path, + @save_path, @extensions, @ignore_default_browser_options, @xvfb = @options.values_at( + :ws_max_receive_size, :env, :browser_name, :browser_path, :save_path, :extensions, + :ignore_default_browser_options, :xvfb + ) + + @options[:window_size] = @window_size + @proxy = parse_proxy(@options[:proxy]) + @logger = parse_logger(@options[:logger]) + @base_url = parse_base_url(@options[:base_url]) if @options[:base_url] + @url = @options[:url].to_s if @options[:url] + + @options.freeze + @browser_options.freeze + end + + def to_h + @options + end + + def parse_base_url(value) + parsed = Addressable::URI.parse(value) + unless BASE_URL_SCHEMA.include?(parsed&.normalized_scheme) + raise ArgumentError, "`base_url` should be absolute and include schema: #{BASE_URL_SCHEMA.join(' | ')}" + end + + parsed + end + + def parse_proxy(options) + return unless options + + raise ArgumentError, "proxy options must be a Hash" unless options.is_a?(Hash) + + if options[:host].nil? && options[:port].nil? + raise ArgumentError, "proxy options must be a Hash with at least :host | :port" + end + + options + end + + private + + def parse_logger(logger) + return logger if logger + + !logger && DEBUG_MODE ? $stdout.tap { |s| s.sync = true } : logger + end + end + end +end diff --git a/lib/ferrum/browser/options/base.rb b/lib/ferrum/browser/options/base.rb index 3006fee8..6ebc63ba 100644 --- a/lib/ferrum/browser/options/base.rb +++ b/lib/ferrum/browser/options/base.rb @@ -4,11 +4,8 @@ module Ferrum class Browser - module Options + class Options class Base - BROWSER_HOST = "127.0.0.1" - BROWSER_PORT = "0" - include Singleton def self.options diff --git a/lib/ferrum/browser/options/chrome.rb b/lib/ferrum/browser/options/chrome.rb index 06ceaf10..1cce92df 100644 --- a/lib/ferrum/browser/options/chrome.rb +++ b/lib/ferrum/browser/options/chrome.rb @@ -2,7 +2,7 @@ module Ferrum class Browser - module Options + class Options class Chrome < Base DEFAULT_OPTIONS = { "headless" => nil, @@ -59,17 +59,22 @@ class Chrome < Base }.freeze def merge_required(flags, options, user_data_dir) - port = options.fetch(:port, BROWSER_PORT) - host = options.fetch(:host, BROWSER_HOST) - flags.merge("remote-debugging-port" => port, - "remote-debugging-address" => host, - # Doesn't work on MacOS, so we need to set it by CDP - "window-size" => options[:window_size]&.join(","), - "user-data-dir" => user_data_dir) + flags = flags.merge("remote-debugging-port" => options.port, + "remote-debugging-address" => options.host, + # Doesn't work on MacOS, so we need to set it by CDP + "window-size" => options.window_size&.join(","), + "user-data-dir" => user_data_dir) + + if options.proxy + flags.merge!("proxy-server" => "#{options.proxy[:host]}:#{options.proxy[:port]}") + flags.merge!("proxy-bypass-list" => options.proxy[:bypass]) if options.proxy[:bypass] + end + + flags end def merge_default(flags, options) - defaults = except("headless", "disable-gpu") unless options.fetch(:headless, true) + defaults = except("headless", "disable-gpu") unless options.headless defaults ||= DEFAULT_OPTIONS defaults.merge(flags) diff --git a/lib/ferrum/browser/options/firefox.rb b/lib/ferrum/browser/options/firefox.rb index 6b22383a..e8246bfc 100644 --- a/lib/ferrum/browser/options/firefox.rb +++ b/lib/ferrum/browser/options/firefox.rb @@ -2,7 +2,7 @@ module Ferrum class Browser - module Options + class Options class Firefox < Base DEFAULT_OPTIONS = { "headless" => nil @@ -23,14 +23,11 @@ class Firefox < Base }.freeze def merge_required(flags, options, user_data_dir) - port = options.fetch(:port, BROWSER_PORT) - host = options.fetch(:host, BROWSER_HOST) - flags.merge("remote-debugger" => "#{host}:#{port}", - "profile" => user_data_dir) + flags.merge("remote-debugger" => "#{options.host}:#{options.port}", "profile" => user_data_dir) end def merge_default(flags, options) - defaults = except("headless") unless options.fetch(:headless, true) + defaults = except("headless") unless options.headless defaults ||= DEFAULT_OPTIONS defaults.merge(flags) diff --git a/lib/ferrum/browser/process.rb b/lib/ferrum/browser/process.rb index f7e73cb1..9bf1ce8c 100644 --- a/lib/ferrum/browser/process.rb +++ b/lib/ferrum/browser/process.rb @@ -15,7 +15,6 @@ class Browser class Process KILL_TIMEOUT = 2 WAIT_KILLED = 0.05 - PROCESS_TIMEOUT = ENV.fetch("FERRUM_PROCESS_TIMEOUT", 10).to_i attr_reader :host, :port, :ws_url, :pid, :command, :default_user_agent, :browser_version, :protocol_version, @@ -63,17 +62,17 @@ def self.directory_remover(path) def initialize(options) @pid = @xvfb = @user_data_dir = nil - if options[:url] - url = URI.join(options[:url].to_s, "/json/version") + if options.url + url = URI.join(options.url, "/json/version") response = JSON.parse(::Net::HTTP.get(url)) self.ws_url = response["webSocketDebuggerUrl"] parse_browser_versions return end - @logger = options[:logger] - @process_timeout = options.fetch(:process_timeout, PROCESS_TIMEOUT) - @env = Hash(options[:env]) + @logger = options.logger + @process_timeout = options.process_timeout + @env = Hash(options.env) tmpdir = Dir.mktmpdir("ferrum_user_data_dir_") ObjectSpace.define_finalizer(self, self.class.directory_remover(tmpdir)) diff --git a/lib/ferrum/browser/xvfb.rb b/lib/ferrum/browser/xvfb.rb index 76c6b8db..e146d3ad 100644 --- a/lib/ferrum/browser/xvfb.rb +++ b/lib/ferrum/browser/xvfb.rb @@ -16,7 +16,7 @@ def initialize(options) @path = Binary.find("Xvfb") raise BinaryNotFoundError, NOT_FOUND unless @path - @screen_size = "#{options.fetch(:window_size, [1024, 768]).join('x')}x24" + @screen_size = "#{options.window_size.join('x')}x24" @display_id = (Time.now.to_f * 1000).to_i % 100_000_000 end diff --git a/lib/ferrum/context.rb b/lib/ferrum/context.rb index a20405fa..875680c6 100644 --- a/lib/ferrum/context.rb +++ b/lib/ferrum/context.rb @@ -40,8 +40,9 @@ def windows(pos = nil, size = 1) windows.map(&:page) end - def create_page - create_target.page + def create_page(**options) + target = create_target + target.page = target.build_page(**options) end def create_target diff --git a/lib/ferrum/contexts.rb b/lib/ferrum/contexts.rb index fa2d8674..5e599252 100644 --- a/lib/ferrum/contexts.rb +++ b/lib/ferrum/contexts.rb @@ -23,8 +23,8 @@ def find_by(target_id:) context end - def create - response = @browser.command("Target.createBrowserContext") + def create(**options) + response = @browser.command("Target.createBrowserContext", **options) context_id = response["browserContextId"] context = Context.new(@browser, self, context_id) @contexts[context_id] = context diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 654aca3e..2fff6ee6 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -71,7 +71,7 @@ def reset # @return [Cookies] attr_reader :cookies - def initialize(target_id, browser) + def initialize(target_id, browser, proxy: nil) @frames = {} @main_frame = Frame.new(nil, self) @browser = browser @@ -83,6 +83,9 @@ def initialize(target_id, browser) ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}" @client = Browser::Client.new(browser, ws_url, id_starts_with: 1000) + @proxy_user = proxy&.[](:user) || @browser.options.proxy&.[](:user) + @proxy_password = proxy&.[](:password) || @browser.options.proxy&.[](:password) + @mouse = Mouse.new(self) @keyboard = Keyboard.new(self) @headers = Headers.new(self) @@ -127,7 +130,7 @@ def go_to(url = nil) response["frameId"] rescue TimeoutError - if @browser.pending_connection_errors + if @browser.options.pending_connection_errors pendings = network.traffic.select(&:pending?).map(&:url).compact raise PendingConnectionsError.new(options[:url], pendings) unless pendings.empty? end @@ -162,7 +165,7 @@ def resize(width: nil, height: nil, fullscreen: false) # The current position of the browser window. # # @return [(Integer, Integer)] - # The left,top coordinates of the browser window. + # The left, top coordinates of the browser window. # # @example # browser.position # => [10, 20] @@ -274,7 +277,7 @@ def set_window_bounds(bounds = {}) def command(method, wait: 0, slowmoable: false, **params) iteration = @event.reset if wait.positive? - sleep(@browser.slowmo) if slowmoable && @browser.slowmo.positive? + sleep(@browser.options.slowmo) if slowmoable && @browser.options.slowmo.positive? result = @client.command(method, params) if wait.positive? @@ -325,13 +328,13 @@ def subscribe frames_subscribe network.subscribe - if @browser.logger + if @browser.options.logger on("Runtime.consoleAPICalled") do |params| params["args"].each { |r| @browser.logger.puts(r["value"]) } end end - if @browser.js_errors + if @browser.options.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( @@ -358,21 +361,22 @@ def prepare_page command("Log.enable") command("Network.enable") - if @browser.proxy_options && @browser.proxy_options[:user] && @browser.proxy_options[:password] - auth_options = @browser.proxy_options.slice(:user, :password) - network.authorize(type: :proxy, **auth_options) do |request, _index, _total| + if @proxy_user && @proxy_password + network.authorize(user: @proxy_user, + password: @proxy_password, + type: :proxy) do |request, _index, _total| request.continue end end - if @browser.options[:save_path] - unless Pathname.new(@browser.options[:save_path]).absolute? + if @browser.options.save_path + unless Pathname.new(@browser.options.save_path).absolute? raise Error, "supply absolute path for `:save_path` option" end @browser.command("Browser.setDownloadBehavior", browserContextId: context.id, - downloadPath: @browser.options[:save_path], + downloadPath: @browser.options.save_path, behavior: "allow", eventsEnabled: true) end diff --git a/lib/ferrum/target.rb b/lib/ferrum/target.rb index de081569..23a33491 100644 --- a/lib/ferrum/target.rb +++ b/lib/ferrum/target.rb @@ -23,10 +23,12 @@ def attached? end def page - @page ||= begin - maybe_sleep_if_new_window - Page.new(id, @browser) - end + @page ||= build_page + end + + def build_page(**options) + maybe_sleep_if_new_window + Page.new(id, @browser, **options) end def id diff --git a/spec/browser/binary_spec.rb b/spec/browser/binary_spec.rb index 14afdc05..4561bb75 100644 --- a/spec/browser/binary_spec.rb +++ b/spec/browser/binary_spec.rb @@ -1,104 +1,100 @@ # frozen_string_literal: true -module Ferrum - class Browser - describe Binary do - let(:tmp_bin) { "#{PROJECT_ROOT}/spec/tmp/bin" } - let(:bin1) { File.join(tmp_bin, "bin1") } - let(:bin1_exe) { File.join(tmp_bin, "bin1.exe") } - let(:bin2_no_x) { File.join(tmp_bin, "/bin2") } - let(:bin3) { File.join(tmp_bin, "/bin3") } - let(:bin4_exe) { File.join(tmp_bin, "/bin4.exe") } - - before do - FileUtils.mkdir_p(tmp_bin) - FileUtils.touch([bin1, bin1_exe, bin2_no_x, bin3, bin4_exe]) - FileUtils.chmod("u=rwx,go=rx", bin1) - FileUtils.chmod("u=rwx,go=rx", bin1_exe) - FileUtils.chmod("u=rw,go=r", bin2_no_x) - FileUtils.chmod("u=rwx,go=rx", bin3) - FileUtils.chmod("u=rwx,go=rx", bin4_exe) - - @original_env_path = ENV.fetch("PATH", nil) - @original_env_pathext = ENV.fetch("PATHEXT", nil) - ENV["PATH"] = "#{tmp_bin}#{File::PATH_SEPARATOR}#{@original_env_path}" - end - - after do - FileUtils.rm_rf(tmp_bin) - ENV["PATH"] = @original_env_path - ENV["PATHEXT"] = @original_env_pathext - end - - describe "#find" do - it "finds one binary" do - expect(Binary.find("bin1")).to eq(bin1) - end - - it "finds first binary when list is passed" do - expect(Binary.find(%w[bin1 bin3])).to eq(bin1) - end - - it "finds binary with PATHEXT" do - ENV["PATHEXT"] = ".com;.exe" - - expect(Binary.find(%w[bin4])).to eq(bin4_exe) - end - - it "finds binary with absolute path" do - expect(Binary.find(bin4_exe)).to eq(bin4_exe) - end - - it "finds binary without ext" do - ENV["PATHEXT"] = ".com;.exe" - - expect(Binary.find("bin1")).to eq(bin1_exe) - FileUtils.rm_rf(bin1_exe) - expect(Binary.find("bin1")).to eq(bin1) - end - - it "raises an error" do - ENV["PATH"] = "" - - expect { Binary.find(%w[bin1]) }.to raise_error(Ferrum::EmptyPathError) - end - end - - describe "#all" do - it "finds one binary" do - expect(Binary.all("bin1")).to eq([bin1]) - end - - it "finds multiple binaries with ext" do - ENV["PATHEXT"] = ".com;.exe" - - expect(Binary.all("bin1")).to eq([bin1_exe, bin1]) - end - - it "finds all binary when list passed" do - expect(Binary.all(%w[bin1 bin3])).to eq([bin1, bin3]) - end - - it "finds binary with PATHEXT" do - ENV["PATHEXT"] = ".com;.exe" - - expect(Binary.all(%w[bin4])).to eq([bin4_exe]) - end - - it "raises an error" do - ENV["PATH"] = "" - - expect { Binary.all(%w[bin1]) }.to raise_error(Ferrum::EmptyPathError) - end - end - - describe "#lazy_find" do - it "works lazily" do - enum = Binary.lazy_find(%w[ls which none]) - - expect(enum.instance_of?(Enumerator::Lazy)).to be_truthy - end - end +describe Ferrum::Browser::Binary do + let(:tmp_bin) { "#{PROJECT_ROOT}/spec/tmp/bin" } + let(:bin1) { File.join(tmp_bin, "bin1") } + let(:bin1_exe) { File.join(tmp_bin, "bin1.exe") } + let(:bin2_no_x) { File.join(tmp_bin, "/bin2") } + let(:bin3) { File.join(tmp_bin, "/bin3") } + let(:bin4_exe) { File.join(tmp_bin, "/bin4.exe") } + + before do + FileUtils.mkdir_p(tmp_bin) + FileUtils.touch([bin1, bin1_exe, bin2_no_x, bin3, bin4_exe]) + FileUtils.chmod("u=rwx,go=rx", bin1) + FileUtils.chmod("u=rwx,go=rx", bin1_exe) + FileUtils.chmod("u=rw,go=r", bin2_no_x) + FileUtils.chmod("u=rwx,go=rx", bin3) + FileUtils.chmod("u=rwx,go=rx", bin4_exe) + + @original_env_path = ENV.fetch("PATH", nil) + @original_env_pathext = ENV.fetch("PATHEXT", nil) + ENV["PATH"] = "#{tmp_bin}#{File::PATH_SEPARATOR}#{@original_env_path}" + end + + after do + FileUtils.rm_rf(tmp_bin) + ENV["PATH"] = @original_env_path + ENV["PATHEXT"] = @original_env_pathext + end + + describe "#find" do + it "finds one binary" do + expect(Ferrum::Browser::Binary.find("bin1")).to eq(bin1) + end + + it "finds first binary when list is passed" do + expect(Ferrum::Browser::Binary.find(%w[bin1 bin3])).to eq(bin1) + end + + it "finds binary with PATHEXT" do + ENV["PATHEXT"] = ".com;.exe" + + expect(Ferrum::Browser::Binary.find(%w[bin4])).to eq(bin4_exe) + end + + it "finds binary with absolute path" do + expect(Ferrum::Browser::Binary.find(bin4_exe)).to eq(bin4_exe) + end + + it "finds binary without ext" do + ENV["PATHEXT"] = ".com;.exe" + + expect(Ferrum::Browser::Binary.find("bin1")).to eq(bin1_exe) + FileUtils.rm_rf(bin1_exe) + expect(Ferrum::Browser::Binary.find("bin1")).to eq(bin1) + end + + it "raises an error" do + ENV["PATH"] = "" + + expect { Ferrum::Browser::Binary.find(%w[bin1]) }.to raise_error(Ferrum::EmptyPathError) + end + end + + describe "#all" do + it "finds one binary" do + expect(Ferrum::Browser::Binary.all("bin1")).to eq([bin1]) + end + + it "finds multiple binaries with ext" do + ENV["PATHEXT"] = ".com;.exe" + + expect(Ferrum::Browser::Binary.all("bin1")).to eq([bin1_exe, bin1]) + end + + it "finds all binary when list passed" do + expect(Ferrum::Browser::Binary.all(%w[bin1 bin3])).to eq([bin1, bin3]) + end + + it "finds binary with PATHEXT" do + ENV["PATHEXT"] = ".com;.exe" + + expect(Ferrum::Browser::Binary.all(%w[bin4])).to eq([bin4_exe]) + end + + it "raises an error" do + ENV["PATH"] = "" + + expect { Ferrum::Browser::Binary.all(%w[bin1]) }.to raise_error(Ferrum::EmptyPathError) + end + end + + describe "#lazy_find" do + it "works lazily" do + enum = Ferrum::Browser::Binary.lazy_find(%w[ls which none]) + + expect(enum.instance_of?(Enumerator::Lazy)).to be_truthy end end end diff --git a/spec/browser/version_info_spec.rb b/spec/browser/version_info_spec.rb index 62b95743..9dc97b76 100644 --- a/spec/browser/version_info_spec.rb +++ b/spec/browser/version_info_spec.rb @@ -1,57 +1,53 @@ # frozen_string_literal: true -module Ferrum - class Browser - describe VersionInfo do - let(:protocol_version) { "1.3" } - let(:product) { "HeadlessChrome/106.0.5249.91" } - let(:revision) { "@fa96d5f07b1177d1bf5009f647a5b8c629762157" } - let(:user_agent) do - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \ - "(KHTML, like Gecko) HeadlessChrome/106.0.5249.91 Safari/537.36" - end - let(:js_version) { "10.6.194.17" } - let(:properties) do - { - "protocolVersion" => protocol_version, - "product" => product, - "revision" => revision, - "userAgent" => user_agent, - "jsVersion" => js_version - } - end +describe Ferrum::Browser::VersionInfo do + let(:protocol_version) { "1.3" } + let(:product) { "HeadlessChrome/106.0.5249.91" } + let(:revision) { "@fa96d5f07b1177d1bf5009f647a5b8c629762157" } + let(:user_agent) do + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \ + "(KHTML, like Gecko) HeadlessChrome/106.0.5249.91 Safari/537.36" + end + let(:js_version) { "10.6.194.17" } + let(:properties) do + { + "protocolVersion" => protocol_version, + "product" => product, + "revision" => revision, + "userAgent" => user_agent, + "jsVersion" => js_version + } + end - subject { described_class.new(properties) } + subject { described_class.new(properties) } - describe "#protocol_version" do - it "must return the protocolVersion property" do - expect(subject.protocol_version).to eq(properties["protocolVersion"]) - end - end + describe "#protocol_version" do + it "must return the protocolVersion property" do + expect(subject.protocol_version).to eq(properties["protocolVersion"]) + end + end - describe "#product" do - it "must return the product property" do - expect(subject.product).to eq(properties["product"]) - end - end + describe "#product" do + it "must return the product property" do + expect(subject.product).to eq(properties["product"]) + end + end - describe "#revision" do - it "must return the revision property" do - expect(subject.revision).to eq(properties["revision"]) - end - end + describe "#revision" do + it "must return the revision property" do + expect(subject.revision).to eq(properties["revision"]) + end + end - describe "#user_agent" do - it "must return the userAgent property" do - expect(subject.user_agent).to eq(properties["userAgent"]) - end - end + describe "#user_agent" do + it "must return the userAgent property" do + expect(subject.user_agent).to eq(properties["userAgent"]) + end + end - describe "#js_version" do - it "must return the jsVersion property" do - expect(subject.js_version).to eq(properties["jsVersion"]) - end - end + describe "#js_version" do + it "must return the jsVersion property" do + expect(subject.js_version).to eq(properties["jsVersion"]) end end end diff --git a/spec/browser/xvfb_spec.rb b/spec/browser/xvfb_spec.rb index 9e396471..14131f47 100644 --- a/spec/browser/xvfb_spec.rb +++ b/spec/browser/xvfb_spec.rb @@ -1,68 +1,64 @@ # frozen_string_literal: true -module Ferrum - class Browser - describe Xvfb, skip: !Binary.find("Xvfb") do - let(:process) { xvfb_browser.process } - let(:xvfb_browser) { Browser.new(default_options.merge(options)) } - let(:default_options) { Hash(headless: true, xvfb: true) } +describe Ferrum::Browser::Xvfb, skip: !Ferrum::Browser::Binary.find("Xvfb") do + let(:process) { xvfb_browser.process } + let(:xvfb_browser) { Ferrum::Browser.new(default_options.merge(options)) } + let(:default_options) { Hash(headless: true, xvfb: true) } - context "headless" do - context "with window_size" do - let(:options) { Hash(window_size: [1400, 1400]) } + context "headless" do + context "with window_size" do + let(:options) { Hash(window_size: [1400, 1400]) } - it "allows to run tests configured to xvfb" do - xvfb_browser.go_to(base_url) + it "allows to run tests configured to xvfb" do + xvfb_browser.go_to(base_url) - expect(xvfb_browser.body).to include("Hello world!") - expect(process_alive?(process.xvfb.pid)).to be(true) - expect(process.xvfb.screen_size).to eq("1400x1400x24") - expect(process.xvfb.to_env).to eq("DISPLAY" => ":#{process.xvfb.display_id}") - ensure - xvfb_browser&.quit - expect(process_alive?(process.xvfb.pid)).to be(false) - end - end + expect(xvfb_browser.body).to include("Hello world!") + expect(process_alive?(process.xvfb.pid)).to be(true) + expect(process.xvfb.screen_size).to eq("1400x1400x24") + expect(process.xvfb.to_env).to eq("DISPLAY" => ":#{process.xvfb.display_id}") + ensure + xvfb_browser&.quit + expect(process_alive?(process.xvfb.pid)).to be(false) + end + end - context "without window_size" do - let(:options) { {} } + context "without window_size" do + let(:options) { {} } - it "allows to run tests configured to xvfb" do - xvfb_browser.go_to(base_url) + it "allows to run tests configured to xvfb" do + xvfb_browser.go_to(base_url) - expect(xvfb_browser.body).to include("Hello world!") - expect(process_alive?(process.xvfb.pid)).to be(true) - expect(process.xvfb.screen_size).to eq("1024x768x24") - expect(process.xvfb.to_env).to eq("DISPLAY" => ":#{process.xvfb.display_id}") - ensure - xvfb_browser&.quit - expect(process_alive?(process.xvfb.pid)).to be(false) - end - end + expect(xvfb_browser.body).to include("Hello world!") + expect(process_alive?(process.xvfb.pid)).to be(true) + expect(process.xvfb.screen_size).to eq("1024x768x24") + expect(process.xvfb.to_env).to eq("DISPLAY" => ":#{process.xvfb.display_id}") + ensure + xvfb_browser&.quit + expect(process_alive?(process.xvfb.pid)).to be(false) end + end + end - context "headful" do - let(:options) { Hash(headless: false) } + context "headful" do + let(:options) { Hash(headless: false) } - it "allows to run tests configured to xvfb" do - xvfb_browser.go_to(base_url) + it "allows to run tests configured to xvfb" do + xvfb_browser.go_to(base_url) - expect(xvfb_browser.body).to include("Hello world!") - expect(process_alive?(process.xvfb.pid)).to be(true) - expect(process.xvfb.screen_size).to eq("1024x768x24") - ensure - xvfb_browser&.quit - expect(process_alive?(process.xvfb.pid)).to be(false) - end - end + expect(xvfb_browser.body).to include("Hello world!") + expect(process_alive?(process.xvfb.pid)).to be(true) + expect(process.xvfb.screen_size).to eq("1024x768x24") + ensure + xvfb_browser&.quit + expect(process_alive?(process.xvfb.pid)).to be(false) + end + end - def process_alive?(pid) - return false unless pid + def process_alive?(pid) + return false unless pid - ::Process.kill(0, pid) == 1 - rescue Errno::ESRCH - false - end - end + ::Process.kill(0, pid) == 1 + rescue Errno::ESRCH + false end end diff --git a/spec/browser_spec.rb b/spec/browser_spec.rb index 1d08844b..2d4ab27e 100644 --- a/spec/browser_spec.rb +++ b/spec/browser_spec.rb @@ -1,477 +1,479 @@ # frozen_string_literal: true -module Ferrum - describe Browser do - describe "#new" do - let(:logger) { StringIO.new } +describe Ferrum::Browser do + describe "#new" do + let(:logger) { StringIO.new } - context ":browser_path argument" do - it "includes the process output in the error" do - path = "#{PROJECT_ROOT}/spec/support/broken_chrome" + context ":browser_path argument" do + it "includes the process output in the error" do + path = "#{PROJECT_ROOT}/spec/support/broken_chrome" - expect do - Browser.new(browser_path: path) - end.to raise_error(Ferrum::ProcessTimeoutError) do |e| - expect(e.output).to include "Broken Chrome error message" - end + expect do + Ferrum::Browser.new(browser_path: path) + end.to raise_error(Ferrum::ProcessTimeoutError) do |e| + expect(e.output).to include "Broken Chrome error message" end + end - it "supports custom chrome path" do - original_path = "#{PROJECT_ROOT}/spec/support/chrome_path" - File.write(original_path, browser.process.path) - - file = "#{PROJECT_ROOT}/spec/support/custom_chrome_called" - path = "#{PROJECT_ROOT}/spec/support/custom_chrome" + it "supports custom chrome path" do + original_path = "#{PROJECT_ROOT}/spec/support/chrome_path" + File.write(original_path, browser.process.path) - browser = Browser.new(browser_path: path) + file = "#{PROJECT_ROOT}/spec/support/custom_chrome_called" + path = "#{PROJECT_ROOT}/spec/support/custom_chrome" - # If the correct custom path is called, it will touch the file. - # We allow at least 10 secs for this to happen before failing. + browser = Ferrum::Browser.new(browser_path: path) - tries = 0 - until File.exist?(file) || tries == 100 - sleep 0.1 - tries += 1 - end + # If the correct custom path is called, it will touch the file. + # We allow at least 10 secs for this to happen before failing. - expect(File.exist?(file)).to be true - ensure - FileUtils.rm_f(original_path) - FileUtils.rm_f(file) - browser&.quit + tries = 0 + until File.exist?(file) || tries == 100 + sleep 0.1 + tries += 1 end - end - it "supports :logger argument" do - browser = Browser.new(logger: logger) - browser.go_to(base_url("/ferrum/console_log")) - expect(logger.string).to include("Hello world") + expect(File.exist?(file)).to be true ensure + FileUtils.rm_f(original_path) + FileUtils.rm_f(file) browser&.quit end + end - it "supports :ignore_default_browser_options argument" do - defaults = Browser::Options::Chrome.options.except("disable-web-security") - browser = Browser.new(ignore_default_browser_options: true, browser_options: defaults) - browser.go_to(base_url("/ferrum/console_log")) - ensure - browser&.quit - end + it "supports :logger argument" do + browser = Ferrum::Browser.new(logger: logger) + browser.go_to(base_url("/ferrum/console_log")) + expect(logger.string).to include("Hello world") + ensure + browser&.quit + end - it "supports :process_timeout argument" do - path = "#{PROJECT_ROOT}/spec/support/no_chrome" + it "supports :ignore_default_browser_options argument" do + defaults = Ferrum::Browser::Options::Chrome.options.except("disable-web-security") + browser = Ferrum::Browser.new(ignore_default_browser_options: true, browser_options: defaults) + browser.go_to(base_url("/ferrum/console_log")) + ensure + browser&.quit + end - expect do - Browser.new(browser_path: path, process_timeout: 2) - end.to raise_error( - Ferrum::ProcessTimeoutError, - "Browser did not produce websocket url within 2 seconds, try to increase `:process_timeout`. See https://github.com/rubycdp/ferrum#customization" - ) - end + it "supports :process_timeout argument" do + path = "#{PROJECT_ROOT}/spec/support/no_chrome" - context ":extensions argument" do - it "extends the browser's world with files" do - browser = Browser.new(base_url: base_url, - extensions: [File.expand_path("support/geolocation.js", __dir__)]) + expect do + Ferrum::Browser.new(browser_path: path, process_timeout: 2) + end.to raise_error( + Ferrum::ProcessTimeoutError, + "Browser did not produce websocket url within 2 seconds, try to increase `:process_timeout`. See https://github.com/rubycdp/ferrum#customization" + ) + end - browser.go_to("/ferrum/requiring_custom_extension") + context ":extensions argument" do + it "extends the browser's world with files" do + browser = Ferrum::Browser.new(base_url: base_url, + extensions: [File.expand_path("support/geolocation.js", __dir__)]) - expect( - browser.body - ).to include(%(Location: 1,-1)) + browser.go_to("/ferrum/requiring_custom_extension") - expect( - browser.evaluate(%(document.getElementById("location").innerHTML)) - ).to eq("1,-1") + expect( + browser.body + ).to include(%(Location: 1,-1)) - expect( - browser.evaluate("navigator.geolocation") - ).to_not eq(nil) - ensure - browser&.quit - end + expect( + browser.evaluate(%(document.getElementById("location").innerHTML)) + ).to eq("1,-1") - it "extends the browser's world with source" do - browser = Browser.new(base_url: base_url, - extensions: [{ source: "window.secret = 'top'" }]) + expect( + browser.evaluate("navigator.geolocation") + ).to_not eq(nil) + ensure + browser&.quit + end - browser.go_to("/ferrum/requiring_custom_extension") + it "extends the browser's world with source" do + browser = Ferrum::Browser.new(base_url: base_url, + extensions: [{ source: "window.secret = 'top'" }]) - expect(browser.evaluate(%(window.secret))).to eq("top") - ensure - browser&.quit - end + browser.go_to("/ferrum/requiring_custom_extension") - it "errors when extension is unavailable" do - browser = Browser.new(extensions: [File.expand_path("../support/non_existent.js", __dir__)]) - expect { browser.go_to }.to raise_error(Errno::ENOENT) - ensure - browser&.quit - end + expect(browser.evaluate(%(window.secret))).to eq("top") + ensure + browser&.quit end - it "supports :port argument" do - browser = Browser.new(port: 12_345) - browser.go_to(base_url) + it "errors when extension is unavailable" do + browser = Ferrum::Browser.new(extensions: [File.expand_path("../support/non_existent.js", __dir__)]) + expect { browser.go_to }.to raise_error(Errno::ENOENT) + ensure + browser&.quit + end + end + + it "supports :port argument" do + browser = Ferrum::Browser.new(port: 12_345) + browser.go_to(base_url) - expect { TCPServer.new("127.0.0.1", 12_345) }.to raise_error(Errno::EADDRINUSE) + expect { TCPServer.new("127.0.0.1", 12_345) }.to raise_error(Errno::EADDRINUSE) + ensure + browser&.quit + end + + it "supports :url argument" do + with_external_browser do |url| + browser = Ferrum::Browser.new(url: url) + browser.go_to(base_url) + expect(browser.body).to include("Hello world!") ensure browser&.quit end + end + + it "supports :host argument", skip: ENV["BROWSER_TEST_HOST"].nil? do + # Use custom host "pointing" to localhost in /etc/hosts or iptables for this. + # https://superuser.com/questions/516208/how-to-change-ip-address-to-point-to-localhost + browser = Ferrum::Browser.new(host: ENV.fetch("BROWSER_TEST_HOST"), port: 12_345) + browser.go_to(base_url) + + expect do + TCPServer.new(ENV.fetch("BROWSER_TEST_HOST"), 12_345) + end.to raise_error(Errno::EADDRINUSE) + ensure + browser&.quit + end - it "supports :url argument" do - with_external_browser do |url| - browser = Browser.new(url: url) - browser.go_to(base_url) - expect(browser.body).to include("Hello world!") + context ":proxy argument" do + let(:options) { {} } + let(:proxy) { Ferrum::Proxy.start(**options) } + + after { proxy.stop } + + context "without authorization" do + it "works without authorization" do + browser = Ferrum::Browser.new( + proxy: { host: proxy.host, port: proxy.port } + ) + + browser.go_to("https://example.com") + expect(browser.network.status).to eq(200) + expect(browser.body).to include("Example Domain") ensure browser&.quit end end - it "supports :host argument", skip: ENV["BROWSER_TEST_HOST"].nil? do - # Use custom host "pointing" to localhost in /etc/hosts or iptables for this. - # https://superuser.com/questions/516208/how-to-change-ip-address-to-point-to-localhost - browser = Browser.new(host: ENV.fetch("BROWSER_TEST_HOST"), port: 12_345) - browser.go_to(base_url) + context "with authorization" do + let(:options) { Hash(user: "user", password: "pa$$") } - expect do - TCPServer.new(ENV.fetch("BROWSER_TEST_HOST"), 12_345) - end.to raise_error(Errno::EADDRINUSE) - ensure - browser&.quit - end + it "works with right password" do + browser = Ferrum::Browser.new( + proxy: { host: proxy.host, port: proxy.port, **options } + ) - context ":proxy argument" do - let(:options) { {} } - let(:proxy) { Ferrum::Proxy.start(**options) } - - context "without authorization" do - it "works without authorization" do - browser = Ferrum::Browser.new( - proxy: { host: proxy.host, port: proxy.port } - ) - - browser.go_to("https://example.com") - expect(browser.network.status).to eq(200) - expect(browser.body).to include("Example Domain") - ensure - browser&.quit - end + browser.go_to("https://example.com") + expect(browser.network.status).to eq(200) + expect(browser.body).to include("Example Domain") + ensure + browser&.quit end - context "with authorization" do - let(:options) { Hash(user: "user", password: "pa$$") } - - it "works with right password" do - browser = Ferrum::Browser.new( - proxy: { host: proxy.host, port: proxy.port, **options } - ) + it "breaks with wrong password" do + browser = Ferrum::Browser.new( + proxy: { host: proxy.host, port: proxy.port, user: "u1", password: "p1" } + ) - browser.go_to("https://example.com") - expect(browser.network.status).to eq(200) - expect(browser.body).to include("Example Domain") - ensure - browser&.quit - end + browser.go_to("https://example.com") + expect(browser.network.status).to eq(407) + ensure + browser&.quit + end + end - it "breaks with wrong password" do - browser = Ferrum::Browser.new( - proxy: { host: proxy.host, port: proxy.port, user: "u1", password: "p1" } - ) + context "with rotation", skip: "Think how to make it working on CI" do + it "works after disposing context" do + browser = Ferrum::Browser.new( + proxy: { host: proxy.host, port: proxy.port, **options } + ) - browser.go_to("https://example.com") - expect(browser.network.status).to eq(407) - ensure - browser&.quit + proxy.rotate(host: "host", port: 0, user: "user", password: "password") + browser.create_page(new_context: true) do |page| + page.go_to("https://api.ipify.org?format=json") + expect(page.network.status).to eq(200) + expect(page.body).to include("x.x.x.x") end - end - context "with rotation", skip: "Think how to make it working on CI" do - it "works after disposing context" do - browser = Ferrum::Browser.new( - proxy: { server: true } - ) - - browser.proxy_server.rotate(host: "host", port: 0, user: "user", password: "password") - browser.create_page(new_context: true) do |page| - page.go_to("https://api.ipify.org?format=json") - expect(page.network.status).to eq(200) - expect(page.body).to include("x.x.x.x") - end - - browser.proxy_server.rotate(host: "host", port: 0, user: "user", password: "password") - browser.create_page(new_context: true) do |page| - page.go_to("https://api.ipify.org?format=json") - expect(page.network.status).to eq(200) - expect(page.body).to include("x.x.x.x") - end - ensure - browser&.quit + proxy.rotate(host: "host", port: 0, user: "user", password: "password") + browser.create_page(new_context: true) do |page| + page.go_to("https://api.ipify.org?format=json") + expect(page.network.status).to eq(200) + expect(page.body).to include("x.x.x.x") end + ensure + browser&.quit end end + end - it "supports :pending_connection_errors argument" do - browser = Browser.new(base_url: base_url, pending_connection_errors: false, timeout: 0.1) + it "supports :pending_connection_errors argument" do + browser = Ferrum::Browser.new(base_url: base_url, pending_connection_errors: false, timeout: 0.1) - expect { browser.go_to("/ferrum/really_slow") }.not_to raise_error - ensure - browser&.quit - end + expect { browser.go_to("/ferrum/really_slow") }.not_to raise_error + ensure + browser&.quit + end - context ":save_path argument" do - let(:filename) { "attachment.pdf" } - let(:browser) do - Ferrum::Browser.new( - base_url: Ferrum::Server.server.base_url, - save_path: save_path - ) - end + context ":save_path argument" do + let(:filename) { "attachment.pdf" } + let(:browser) do + Ferrum::Browser.new( + base_url: Ferrum::Server.server.base_url, + save_path: save_path + ) + end - context "with absolute path" do - let(:save_path) { "/tmp/ferrum" } + context "with absolute path" do + let(:save_path) { "/tmp/ferrum" } - it "saves an attachment" do - browser.go_to("/#{filename}") + it "saves an attachment" do + browser.go_to("/#{filename}") - expect(File.exist?("#{save_path}/#{filename}")).to be true - ensure - FileUtils.rm_rf(save_path) - end + expect(File.exist?("#{save_path}/#{filename}")).to be true + ensure + FileUtils.rm_rf(save_path) end + end - context "with local path" do - let(:save_path) { "spec/tmp" } + context "with local path" do + let(:save_path) { "spec/tmp" } - it "raises an error" do - expect do - browser.go_to("/#{filename}") - end.to raise_error(Ferrum::Error, "supply absolute path for `:save_path` option") - end + it "raises an error" do + expect do + browser.go_to("/#{filename}") + end.to raise_error(Ferrum::Error, "supply absolute path for `:save_path` option") end end end + end - describe "#crash" do - it "raises an error" do - expect { browser.crash }.to raise_error(Ferrum::DeadBrowserError) - end + describe "#crash" do + it "raises an error" do + expect { browser.crash }.to raise_error(Ferrum::DeadBrowserError) + end - it "restarts the client" do - expect { browser.crash }.to raise_error(Ferrum::DeadBrowserError) + it "restarts the client" do + expect { browser.crash }.to raise_error(Ferrum::DeadBrowserError) - browser.go_to + browser.go_to - expect(browser.body).to include("Hello world") - end + expect(browser.body).to include("Hello world") end + end - describe "#version" do - it "returns browser version information" do - version_info = browser.version - - expect(version_info).to be_kind_of(Ferrum::Browser::VersionInfo) - expect(version_info.protocol_version).to_not be(nil) - expect(version_info.protocol_version).to_not be_empty - expect(version_info.product).to_not be(nil) - expect(version_info.product).to_not be_empty - expect(version_info.revision).to_not be(nil) - expect(version_info.revision).to_not be_empty - expect(version_info.user_agent).to_not be(nil) - expect(version_info.user_agent).to_not be_empty - expect(version_info.js_version).to_not be(nil) - expect(version_info.js_version).to_not be_empty - end + describe "#version" do + it "returns browser version information" do + version_info = browser.version + + expect(version_info).to be_kind_of(Ferrum::Browser::VersionInfo) + expect(version_info.protocol_version).to_not be(nil) + expect(version_info.protocol_version).to_not be_empty + expect(version_info.product).to_not be(nil) + expect(version_info.product).to_not be_empty + expect(version_info.revision).to_not be(nil) + expect(version_info.revision).to_not be_empty + expect(version_info.user_agent).to_not be(nil) + expect(version_info.user_agent).to_not be_empty + expect(version_info.js_version).to_not be(nil) + expect(version_info.js_version).to_not be_empty end + end - describe "#quit" do - it "stops silently before go_to call" do - browser = Browser.new - expect { browser.quit }.not_to raise_error - end + describe "#quit" do + it "stops silently before go_to call" do + browser = Ferrum::Browser.new + expect { browser.quit }.not_to raise_error + end - it "supports stopping the session", skip: Utils::Platform.windows? do - browser = Browser.new - pid = browser.process.pid + it "supports stopping the session", skip: Ferrum::Utils::Platform.windows? do + browser = Ferrum::Browser.new + pid = browser.process.pid - expect(Process.kill(0, pid)).to eq(1) - browser.quit + expect(Process.kill(0, pid)).to eq(1) + browser.quit - expect { Process.kill(0, pid) }.to raise_error(Errno::ESRCH) - end + expect { Process.kill(0, pid) }.to raise_error(Errno::ESRCH) end + end - describe "#resize" do - it "allows the viewport to be resized" do - browser.go_to - browser.resize(width: 200, height: 400) - expect(browser.viewport_size).to eq([200, 400]) - end + describe "#resize" do + it "allows the viewport to be resized" do + browser.go_to + browser.resize(width: 200, height: 400) + expect(browser.viewport_size).to eq([200, 400]) + end - it "inherits size for a new window" do - browser.go_to - browser.resize(width: 1200, height: 800) - page = browser.create_page - expect(page.viewport_size).to eq [1200, 800] - end + it "inherits size for a new window" do + browser.go_to + browser.resize(width: 1200, height: 800) + page = browser.create_page + expect(page.viewport_size).to eq [1200, 800] + end - it "resizes windows" do - browser.go_to + it "resizes windows" do + browser.go_to - expect(browser.targets.size).to eq(1) + expect(browser.targets.size).to eq(1) - browser.execute <<-JS + browser.execute <<-JS window.open("/ferrum/simple", "popup1") - JS + JS - sleep 0.1 + sleep 0.1 - browser.execute <<-JS + browser.execute <<-JS window.open("/ferrum/simple", "popup2") - JS - - popup1, popup2 = browser.windows(:last, 2) - popup1&.resize(width: 100, height: 200) - popup2&.resize(width: 200, height: 100) - - expect(popup1&.viewport_size).to eq([100, 200]) - expect(popup2&.viewport_size).to eq([200, 100]) - end + JS - context "fullscreen" do - shared_examples "resize viewport by fullscreen" do - it "allows the viewport to be resized by fullscreen" do - expect(browser.viewport_size).to eq([1024, 768]) - browser.go_to(path) - browser.resize(fullscreen: true) - expect(browser.viewport_size).to eq(viewport_size) - end - end - - include_examples "resize viewport by fullscreen" do - let(:path) { "/ferrum/custom_html_size" } - let(:viewport_size) { [1280, 1024] } - end + popup1, popup2 = browser.windows(:last, 2) + popup1&.resize(width: 100, height: 200) + popup2&.resize(width: 200, height: 100) - include_examples "resize viewport by fullscreen" do - let(:path) { "/ferrum/custom_html_size_100%" } - let(:viewport_size) { [1272, 1008] } - end + expect(popup1&.viewport_size).to eq([100, 200]) + expect(popup2&.viewport_size).to eq([200, 100]) + end - it "resizes to normal from fullscreen window state" do + context "fullscreen" do + shared_examples "resize viewport by fullscreen" do + it "allows the viewport to be resized by fullscreen" do + expect(browser.viewport_size).to eq([1024, 768]) browser.go_to(path) browser.resize(fullscreen: true) - browser.resize(width: 200, height: 400) - expect(browser.viewport_size).to eq([200, 400]) + expect(browser.viewport_size).to eq(viewport_size) end end - end - describe "#evaluate_on_new_document" do - it "supports evaluation of JavaScript before page loads" do - browser = Browser.new(base_url: base_url) + include_examples "resize viewport by fullscreen" do + let(:path) { "/ferrum/custom_html_size" } + let(:viewport_size) { [1280, 1024] } + end - browser.evaluate_on_new_document <<~JS - Object.defineProperty(navigator, "languages", { - get: function() { return ["tlh"]; } - }); - JS + include_examples "resize viewport by fullscreen" do + let(:path) { "/ferrum/custom_html_size_100%" } + let(:viewport_size) { [1272, 1008] } + end - browser.go_to("/ferrum/with_user_js") - language = browser.at_xpath("//*[@id='browser-languages']/text()").text - expect(language).to eq("tlh") - ensure - browser&.quit + it "resizes to normal from fullscreen window state" do + browser.go_to(path) + browser.resize(fullscreen: true) + browser.resize(width: 200, height: 400) + expect(browser.viewport_size).to eq([200, 400]) end end + end - describe "#targets" do - it "lists the open windows" do - browser.go_to - - browser.execute <<~JS - window.open("/ferrum/simple", "popup") - JS - - sleep 0.1 + describe "#evaluate_on_new_document" do + it "supports evaluation of JavaScript before page loads" do + browser = Ferrum::Browser.new(base_url: base_url) + + browser.evaluate_on_new_document <<~JS + Object.defineProperty(navigator, "languages", { + get: function() { return ["tlh"]; } + }); + JS + + browser.go_to("/ferrum/with_user_js") + language = browser.at_xpath("//*[@id='browser-languages']/text()").text + expect(language).to eq("tlh") + ensure + browser&.quit + end + end - expect(browser.targets.size).to eq(2) + describe "#targets" do + it "lists the open windows" do + browser.go_to - browser.execute <<~JS - window.open("/ferrum/simple", "popup2") - JS + browser.execute <<~JS + window.open("/ferrum/simple", "popup") + JS - sleep 0.1 + sleep 0.1 - expect(browser.targets.size).to eq(3) + expect(browser.targets.size).to eq(2) - popup2, = browser.windows(:last) - expect(popup2.body).to include("Test") - # Browser isn't dead, current page after executing JS closes connection - # and we don't have a chance to push response to the Queue. Since the - # queue and websocket are closed and response is nil the proper guess - # would be that browser is dead, but in fact the page is dead and - # browser is fully alive. - begin - popup2.execute("window.close()") - rescue StandardError - Ferrum::DeadBrowserError - end + browser.execute <<~JS + window.open("/ferrum/simple", "popup2") + JS + + sleep 0.1 + + expect(browser.targets.size).to eq(3) + + popup2, = browser.windows(:last) + expect(popup2.body).to include("Test") + # Browser isn't dead, current page after executing JS closes connection + # and we don't have a chance to push response to the Queue. Since the + # queue and websocket are closed and response is nil the proper guess + # would be that browser is dead, but in fact the page is dead and + # browser is fully alive. + begin + popup2.execute("window.close()") + rescue StandardError + Ferrum::DeadBrowserError + end - sleep 0.1 + sleep 0.1 - expect(browser.targets.size).to eq(2) - end + expect(browser.targets.size).to eq(2) end + end - describe "#reset" do - it "clears local storage" do - browser.go_to - browser.execute <<~JS - localStorage.setItem("key", "value"); - JS - value = browser.evaluate <<~JS - localStorage.getItem("key"); - JS - - expect(value).to eq("value") - - browser.reset - - browser.go_to - value = browser.evaluate <<~JS - localStorage.getItem("key"); - JS - expect(value).to be_nil - end + describe "#reset" do + it "clears local storage" do + browser.go_to + browser.execute <<~JS + localStorage.setItem("key", "value"); + JS + value = browser.evaluate <<~JS + localStorage.getItem("key"); + JS + + expect(value).to eq("value") + + browser.reset + + browser.go_to + value = browser.evaluate <<~JS + localStorage.getItem("key"); + JS + expect(value).to be_nil end + end - describe "#create_page" do - it "supports calling without block" do - expect(browser.contexts.size).to eq(0) - expect(browser.targets.size).to eq(0) - - page = browser.create_page - page.go_to("/ferrum/simple") + describe "#create_page" do + it "supports calling without block" do + expect(browser.contexts.size).to eq(0) + expect(browser.targets.size).to eq(0) - expect(browser.contexts.size).to eq(1) - expect(browser.targets.size).to eq(1) - end + page = browser.create_page + page.go_to("/ferrum/simple") - it "supports calling with block" do - expect(browser.contexts.size).to eq(0) - expect(browser.targets.size).to eq(0) + expect(browser.contexts.size).to eq(1) + expect(browser.targets.size).to eq(1) + end - browser.create_page do |page| - page.go_to("/ferrum/simple") - end + it "supports calling with block" do + expect(browser.contexts.size).to eq(0) + expect(browser.targets.size).to eq(0) - expect(browser.contexts.size).to eq(1) - expect(browser.targets.size).to eq(0) + browser.create_page do |page| + page.go_to("/ferrum/simple") end - it "supports calling with :new_context and without block" do + expect(browser.contexts.size).to eq(1) + expect(browser.targets.size).to eq(0) + end + + context "with :new_context" do + it "supports calling without block" do expect(browser.contexts.size).to eq(0) page = browser.create_page(new_context: true) @@ -486,7 +488,7 @@ module Ferrum expect(browser.contexts.size).to eq(0) end - it "supports calling with :new_context and with block" do + it "supports calling with block" do expect(browser.contexts.size).to eq(0) browser.create_page(new_context: true) do |page| @@ -497,40 +499,87 @@ module Ferrum end end - context "with pty", if: Utils::Platform.mri? && !Utils::Platform.windows? do - require "pty" - require "timeout" - - before do - Tempfile.open(%w[test rb]) do |file| - file.print(script) - file.flush - - Timeout.timeout(10) do - PTY.spawn("bundle exec ruby #{file.path}") do |read, write, pid| - sleep 0.01 until read.readline.chomp == "Please type enter" - write.puts - sleep 0.1 until (status = PTY.check(pid)) - @status = status - end - end + context "with :proxy" do + let(:options) { {} } + let(:proxy) { Ferrum::Proxy.start(**options) } + + after { proxy.stop } + + context "without authorization" do + it "succeeds" do + expect(browser.contexts.size).to eq(0) + + page = browser.create_page(proxy: { host: proxy.host, port: proxy.port }) + page.go_to("https://example.com") + + expect(browser.contexts.size).to eq(1) + expect(page.context.targets.size).to eq(1) + expect(page.network.status).to eq(200) + expect(page.body).to include("Example Domain") + + page = browser.create_page(proxy: { host: proxy.host, port: proxy.port }) + expect(browser.contexts.size).to eq(2) + page.context.dispose + expect(browser.contexts.size).to eq(1) end end - let(:script) do - <<-RUBY - require "ferrum" - browser = Ferrum::Browser.new - browser.go_to("http://example.com") - puts "Please type enter" - sleep 1 - browser.current_url - RUBY + context "with authorization" do + let(:options) { { user: "user", password: "password" } } + + it "fails with wrong password" do + page = browser.create_page(proxy: { host: proxy.host, port: proxy.port, + user: options[:user], password: "$$" }) + page.go_to("https://example.com") + + expect(page.network.status).to eq(407) + end + + it "succeeds with correct password" do + page = browser.create_page(proxy: { host: proxy.host, port: proxy.port, + user: options[:user], password: options[:password] }) + page.go_to("https://example.com") + + expect(page.network.status).to eq(200) + expect(page.body).to include("Example Domain") + end end + end + end - it do - expect(@status).to be_success + context "with pty", if: Ferrum::Utils::Platform.mri? && !Ferrum::Utils::Platform.windows? do + require "pty" + require "timeout" + + before do + Tempfile.open(%w[test rb]) do |file| + file.print(script) + file.flush + + Timeout.timeout(10) do + PTY.spawn("bundle exec ruby #{file.path}") do |read, write, pid| + sleep 0.01 until read.readline.chomp == "Please type enter" + write.puts + sleep 0.1 until (status = PTY.check(pid)) + @status = status + end + end end end + + let(:script) do + <<-RUBY + require "ferrum" + browser = Ferrum::Browser.new + browser.go_to("http://example.com") + puts "Please type enter" + sleep 1 + browser.current_url + RUBY + end + + it do + expect(@status).to be_success + end end end diff --git a/spec/context_spec.rb b/spec/context_spec.rb index 217cc4a9..c3ad38e6 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -1,43 +1,41 @@ # frozen_string_literal: true -module Ferrum - describe Context do - describe "#windows" do - it "waits for the window to load" do - browser.go_to - - browser.execute <<-JS - window.open("/ferrum/slow", "popup") - JS - - popup, = browser.windows(:last) - expect(popup.body).to include("slow page") - popup.close - end +describe Ferrum::Context do + describe "#windows" do + it "waits for the window to load" do + browser.go_to + + browser.execute <<-JS + window.open("/ferrum/slow", "popup") + JS + + popup, = browser.windows(:last) + expect(popup.body).to include("slow page") + popup.close + end - it "can access a second window of the same name" do - browser.go_to + it "can access a second window of the same name" do + browser.go_to - browser.execute <<-JS - window.open("/ferrum/simple", "popup") - JS + browser.execute <<-JS + window.open("/ferrum/simple", "popup") + JS - popup, = browser.windows(:last) - expect(popup.body).to include("Test") - popup.close + popup, = browser.windows(:last) + expect(popup.body).to include("Test") + popup.close - sleep 0.5 # https://github.com/ChromeDevTools/devtools-protocol/issues/145 + sleep 0.5 # https://github.com/ChromeDevTools/devtools-protocol/issues/145 - browser.execute <<-JS - window.open("/ferrum/simple", "popup") - JS + browser.execute <<-JS + window.open("/ferrum/simple", "popup") + JS - sleep 0.5 # https://github.com/ChromeDevTools/devtools-protocol/issues/145 + sleep 0.5 # https://github.com/ChromeDevTools/devtools-protocol/issues/145 - same, = browser.windows(:last) - expect(same.body).to include("Test") - same.close - end + same, = browser.windows(:last) + expect(same.body).to include("Test") + same.close end end end diff --git a/spec/cookies_spec.rb b/spec/cookies_spec.rb index 82c9f908..c81650b5 100644 --- a/spec/cookies_spec.rb +++ b/spec/cookies_spec.rb @@ -1,212 +1,210 @@ # frozen_string_literal: true -module Ferrum - describe Cookies do - describe "#all" do - it "returns cookie object" do - browser.go_to("/set_cookie") - - cookies = browser.cookies.all - - expect(cookies).to eq({ "stealth" => Cookies::Cookie.new("name" => "stealth", - "value" => "test_cookie", - "domain" => "127.0.0.1", - "path" => "/", - "expires" => -1, - "size" => 18, - "httpOnly" => false, - "secure" => false, - "session" => true, - "priority" => "Medium", - "sameParty" => false, - "sourceScheme" => "NonSecure", - "sourcePort" => server.port) }) - end +describe Ferrum::Cookies do + describe "#all" do + it "returns cookie object" do + browser.go_to("/set_cookie") + + cookies = browser.cookies.all + + expect(cookies).to eq({ "stealth" => Ferrum::Cookies::Cookie.new("name" => "stealth", + "value" => "test_cookie", + "domain" => "127.0.0.1", + "path" => "/", + "expires" => -1, + "size" => 18, + "httpOnly" => false, + "secure" => false, + "session" => true, + "priority" => "Medium", + "sameParty" => false, + "sourceScheme" => "NonSecure", + "sourcePort" => server.port) }) end + end + + describe "#[]" do + it "returns cookie object" do + browser.go_to("/set_cookie") + + cookie = browser.cookies["stealth"] + expect(cookie.name).to eq("stealth") + expect(cookie.value).to eq("test_cookie") + expect(cookie.domain).to eq("127.0.0.1") + expect(cookie.path).to eq("/") + expect(cookie.size).to eq(18) + expect(cookie.secure?).to be false + expect(cookie.httponly?).to be false + expect(cookie.session?).to be true + expect(cookie.expires).to be_nil + end + end + + describe "#set" do + it "sets cookies" do + browser.cookies.set(name: "stealth", value: "omg") + browser.go_to("/get_cookie") + expect(browser.body).to include("omg") + end + + it "sets cookies with custom settings" do + browser.cookies.set( + name: "stealth", + value: "omg", + path: "/ferrum", + httponly: true, + samesite: "Strict" + ) + + browser.go_to("/get_cookie") + expect(browser.body).to_not include("omg") + + browser.go_to("/ferrum/get_cookie") + expect(browser.body).to include("omg") + + expect(browser.cookies["stealth"].path).to eq("/ferrum") + expect(browser.cookies["stealth"].httponly?).to be_truthy + expect(browser.cookies["stealth"].samesite).to eq("Strict") + end + + it "sets a retrieved cookie" do + browser.cookies.set(name: "stealth", value: "omg") + browser.go_to("/get_cookie") + expect(browser.body).to include("omg") + + cookie = browser.cookies["stealth"] + browser.cookies.clear + browser.go_to("/get_cookie") + expect(browser.body).to_not include("omg") - describe "#[]" do - it "returns cookie object" do - browser.go_to("/set_cookie") - - cookie = browser.cookies["stealth"] - expect(cookie.name).to eq("stealth") - expect(cookie.value).to eq("test_cookie") - expect(cookie.domain).to eq("127.0.0.1") - expect(cookie.path).to eq("/") - expect(cookie.size).to eq(18) - expect(cookie.secure?).to be false - expect(cookie.httponly?).to be false - expect(cookie.session?).to be true - expect(cookie.expires).to be_nil - end + browser.cookies.set(cookie) + browser.go_to("/get_cookie") + expect(browser.body).to include("omg") end - describe "#set" do - it "sets cookies" do - browser.cookies.set(name: "stealth", value: "omg") - browser.go_to("/get_cookie") - expect(browser.body).to include("omg") - end - - it "sets cookies with custom settings" do - browser.cookies.set( - name: "stealth", - value: "omg", - path: "/ferrum", - httponly: true, - samesite: "Strict" - ) - - browser.go_to("/get_cookie") - expect(browser.body).to_not include("omg") - - browser.go_to("/ferrum/get_cookie") - expect(browser.body).to include("omg") - - expect(browser.cookies["stealth"].path).to eq("/ferrum") - expect(browser.cookies["stealth"].httponly?).to be_truthy - expect(browser.cookies["stealth"].samesite).to eq("Strict") - end - - it "sets a retrieved cookie" do - browser.cookies.set(name: "stealth", value: "omg") - browser.go_to("/get_cookie") - expect(browser.body).to include("omg") - - cookie = browser.cookies["stealth"] - browser.cookies.clear - browser.go_to("/get_cookie") - expect(browser.body).to_not include("omg") - - browser.cookies.set(cookie) - browser.go_to("/get_cookie") - expect(browser.body).to include("omg") - end - - it "sets a retrieved browser cookie" do - browser.go_to("/set_cookie") - cookie = browser.cookies["stealth"] - browser.go_to("/get_cookie") - expect(cookie.name).to eq("stealth") - expect(cookie.value).to eq("test_cookie") - expect(browser.body).to include("test_cookie") - - browser.cookies.clear - browser.go_to("/get_cookie") - expect(browser.body).not_to include("test_cookie") - - browser.cookies.set(cookie) - browser.go_to("/get_cookie") - expect(browser.body).to include("test_cookie") - end - - it "retains the characteristics of the reference cookie" do - browser.cookies.set(name: "stealth", value: "omg", domain: "site.com") - expect(browser.cookies["stealth"].name).to eq("stealth") - expect(browser.cookies["stealth"].value).to eq("omg") - expect(browser.cookies["stealth"].domain).to eq("site.com") - - cookie = browser.cookies["stealth"] - browser.cookies.clear - expect(browser.cookies["stealth"]).to eq(nil) - browser.cookies.set(cookie) - - expect(browser.cookies["stealth"].name).to eq("stealth") - expect(browser.cookies["stealth"].value).to eq("omg") - expect(browser.cookies["stealth"].domain).to eq("site.com") - - browser.cookies.clear - expect(browser.cookies["stealth"]).to eq(nil) - browser.cookies.set(cookie.attributes) - - expect(browser.cookies["stealth"].name).to eq("stealth") - expect(browser.cookies["stealth"].value).to eq("omg") - expect(browser.cookies["stealth"].domain).to eq("site.com") - end - - it "prevents side effects for params" do - cookie_params = { name: "stealth", value: "test_cookie" } - original_cookie_params = cookie_params.dup - - browser.cookies.set(cookie_params) - - expect(cookie_params).to eq(original_cookie_params) - end - - it "prevents side effects for cookie object" do - browser.cookies.set(name: "stealth", value: "omg") - cookie = browser.cookies["stealth"] - cookie.instance_variable_set( - :@attributes, - { "name" => "stealth", "value" => "test_cookie", "domain" => "site.com" } - ) - original_attributes = cookie.attributes.dup - - browser.cookies.set(cookie) - - expect(cookie.attributes).to eq(original_attributes) - end - - it "sets cookies with an expires time" do - time = Time.at(Time.now.to_i + 10_000) - browser.go_to - browser.cookies.set(name: "foo", value: "bar", expires: time) - expect(browser.cookies["foo"].expires).to eq(time) - end - - it "sets cookies for given domain" do - port = server.port - browser.cookies.set(name: "stealth", value: "127.0.0.1") - browser.cookies.set(name: "stealth", value: "localhost", domain: "localhost") - - browser.go_to("http://localhost:#{port}/ferrum/get_cookie") - expect(browser.body).to include("localhost") - - browser.go_to("http://127.0.0.1:#{port}/ferrum/get_cookie") - expect(browser.body).to include("127.0.0.1") - end - - it "sets cookies correctly with :domain option when base_url isn't set" do - browser = Browser.new - browser.cookies.set(name: "stealth", value: "123456", domain: "localhost") - - port = server.port - browser.go_to("http://localhost:#{port}/ferrum/get_cookie") - expect(browser.body).to include("123456") - - browser.go_to("http://127.0.0.1:#{port}/ferrum/get_cookie") - expect(browser.body).not_to include("123456") - ensure - browser&.quit - end + it "sets a retrieved browser cookie" do + browser.go_to("/set_cookie") + cookie = browser.cookies["stealth"] + browser.go_to("/get_cookie") + expect(cookie.name).to eq("stealth") + expect(cookie.value).to eq("test_cookie") + expect(browser.body).to include("test_cookie") + + browser.cookies.clear + browser.go_to("/get_cookie") + expect(browser.body).not_to include("test_cookie") + + browser.cookies.set(cookie) + browser.go_to("/get_cookie") + expect(browser.body).to include("test_cookie") end - describe "#remove" do - it "removes a cookie" do - browser.go_to("/set_cookie") + it "retains the characteristics of the reference cookie" do + browser.cookies.set(name: "stealth", value: "omg", domain: "site.com") + expect(browser.cookies["stealth"].name).to eq("stealth") + expect(browser.cookies["stealth"].value).to eq("omg") + expect(browser.cookies["stealth"].domain).to eq("site.com") + + cookie = browser.cookies["stealth"] + browser.cookies.clear + expect(browser.cookies["stealth"]).to eq(nil) + browser.cookies.set(cookie) + + expect(browser.cookies["stealth"].name).to eq("stealth") + expect(browser.cookies["stealth"].value).to eq("omg") + expect(browser.cookies["stealth"].domain).to eq("site.com") - browser.go_to("/get_cookie") - expect(browser.body).to include("test_cookie") + browser.cookies.clear + expect(browser.cookies["stealth"]).to eq(nil) + browser.cookies.set(cookie.attributes) - browser.cookies.remove(name: "stealth") + expect(browser.cookies["stealth"].name).to eq("stealth") + expect(browser.cookies["stealth"].value).to eq("omg") + expect(browser.cookies["stealth"].domain).to eq("site.com") + end + + it "prevents side effects for params" do + cookie_params = { name: "stealth", value: "test_cookie" } + original_cookie_params = cookie_params.dup + + browser.cookies.set(cookie_params) - browser.go_to("/get_cookie") - expect(browser.body).to_not include("test_cookie") - end + expect(cookie_params).to eq(original_cookie_params) end - describe "#clear" do - it "clears cookies" do - browser.go_to("/set_cookie") + it "prevents side effects for cookie object" do + browser.cookies.set(name: "stealth", value: "omg") + cookie = browser.cookies["stealth"] + cookie.instance_variable_set( + :@attributes, + { "name" => "stealth", "value" => "test_cookie", "domain" => "site.com" } + ) + original_attributes = cookie.attributes.dup + + browser.cookies.set(cookie) + + expect(cookie.attributes).to eq(original_attributes) + end + + it "sets cookies with an expires time" do + time = Time.at(Time.now.to_i + 10_000) + browser.go_to + browser.cookies.set(name: "foo", value: "bar", expires: time) + expect(browser.cookies["foo"].expires).to eq(time) + end + + it "sets cookies for given domain" do + port = server.port + browser.cookies.set(name: "stealth", value: "127.0.0.1") + browser.cookies.set(name: "stealth", value: "localhost", domain: "localhost") + + browser.go_to("http://localhost:#{port}/ferrum/get_cookie") + expect(browser.body).to include("localhost") + + browser.go_to("http://127.0.0.1:#{port}/ferrum/get_cookie") + expect(browser.body).to include("127.0.0.1") + end + + it "sets cookies correctly with :domain option when base_url isn't set" do + browser = Ferrum::Browser.new + browser.cookies.set(name: "stealth", value: "123456", domain: "localhost") + + port = server.port + browser.go_to("http://localhost:#{port}/ferrum/get_cookie") + expect(browser.body).to include("123456") + + browser.go_to("http://127.0.0.1:#{port}/ferrum/get_cookie") + expect(browser.body).not_to include("123456") + ensure + browser&.quit + end + end + + describe "#remove" do + it "removes a cookie" do + browser.go_to("/set_cookie") + + browser.go_to("/get_cookie") + expect(browser.body).to include("test_cookie") + + browser.cookies.remove(name: "stealth") + + browser.go_to("/get_cookie") + expect(browser.body).to_not include("test_cookie") + end + end + + describe "#clear" do + it "clears cookies" do + browser.go_to("/set_cookie") - browser.go_to("/get_cookie") - expect(browser.body).to include("test_cookie") + browser.go_to("/get_cookie") + expect(browser.body).to include("test_cookie") - browser.cookies.clear + browser.cookies.clear - browser.go_to("/get_cookie") - expect(browser.body).to_not include("test_cookie") - end + browser.go_to("/get_cookie") + expect(browser.body).to_not include("test_cookie") end end end diff --git a/spec/dialog_spec.rb b/spec/dialog_spec.rb index 12714564..7d199a43 100644 --- a/spec/dialog_spec.rb +++ b/spec/dialog_spec.rb @@ -1,92 +1,90 @@ # frozen_string_literal: true -module Ferrum - describe Dialog do - describe "#accept" do - it "works with nested modals" do - browser.go_to("/ferrum/with_js") - browser.on(:dialog) do |dialog, _index, _total| - if dialog.match?("Are you sure?") - dialog.accept - else - dialog.dismiss - end +describe Ferrum::Dialog do + describe "#accept" do + it "works with nested modals" do + browser.go_to("/ferrum/with_js") + browser.on(:dialog) do |dialog, _index, _total| + if dialog.match?("Are you sure?") + dialog.accept + else + dialog.dismiss end - - browser.at_css("a#open-twice").click - - expect(browser.at_xpath("//a[@id='open-twice' and @confirmed='false']")).to be end - it "works with second window" do - browser.go_to + browser.at_css("a#open-twice").click - browser.execute <<-JS - window.open("/ferrum/with_js", "popup") - JS + expect(browser.at_xpath("//a[@id='open-twice' and @confirmed='false']")).to be + end - popup, = browser.windows(:last) + it "works with second window" do + browser.go_to - popup.on(:dialog) do |dialog, _index, _total| - dialog.accept - end - popup.at_css("a#open-match").click - expect(popup.at_xpath("//a[@id='open-match' and @confirmed='true']")).to be + browser.execute <<-JS + window.open("/ferrum/with_js", "popup") + JS + + popup, = browser.windows(:last) - popup.close + popup.on(:dialog) do |dialog, _index, _total| + dialog.accept end - end + popup.at_css("a#open-match").click + expect(popup.at_xpath("//a[@id='open-match' and @confirmed='true']")).to be - describe "#dismiss" do - it "works with second window" do - browser.go_to + popup.close + end + end - browser.execute <<-JS - window.open("/ferrum/with_js", "popup") - JS + describe "#dismiss" do + it "works with second window" do + browser.go_to - popup, = browser.windows(:last) + browser.execute <<-JS + window.open("/ferrum/with_js", "popup") + JS - popup.on(:dialog) do |dialog, _index, _total| - dialog.dismiss - end - popup.at_css("a#open-match").click - expect(popup.at_xpath("//a[@id='open-match' and @confirmed='true']")).not_to be + popup, = browser.windows(:last) - popup.close + popup.on(:dialog) do |dialog, _index, _total| + dialog.dismiss end + popup.at_css("a#open-match").click + expect(popup.at_xpath("//a[@id='open-match' and @confirmed='true']")).not_to be + + popup.close end + end - describe "#match?" do - it "matches on partial strings" do - browser.go_to("/ferrum/with_js") - browser.on(:dialog) do |dialog, _index, _total| - if dialog.match?(Regexp.escape("[reg.exp] (chara©+er$)")) - dialog.accept - else - dialog.dismiss - end + describe "#match?" do + it "matches on partial strings" do + browser.go_to("/ferrum/with_js") + browser.on(:dialog) do |dialog, _index, _total| + if dialog.match?(Regexp.escape("[reg.exp] (chara©+er$)")) + dialog.accept + else + dialog.dismiss end + end - browser.at_css("a#open-match").click + browser.at_css("a#open-match").click - expect(browser.at_xpath("//a[@id='open-match' and @confirmed='true']")).to be - end + expect(browser.at_xpath("//a[@id='open-match' and @confirmed='true']")).to be + end - it "matches on regular expressions" do - browser.go_to("/ferrum/with_js") - browser.on(:dialog) do |dialog, _index, _total| - if dialog.match?(/^.t.ext.*\[\w{3}\.\w{3}\]/i) - dialog.accept - else - dialog.dismiss - end + it "matches on regular expressions" do + browser.go_to("/ferrum/with_js") + browser.on(:dialog) do |dialog, _index, _total| + if dialog.match?(/^.t.ext.*\[\w{3}\.\w{3}\]/i) + dialog.accept + else + dialog.dismiss end + end - browser.at_css("a#open-match").click + browser.at_css("a#open-match").click - expect(browser.at_xpath("//a[@id='open-match' and @confirmed='true']")).to be - end + expect(browser.at_xpath("//a[@id='open-match' and @confirmed='true']")).to be end end end diff --git a/spec/frame/runtime_spec.rb b/spec/frame/runtime_spec.rb new file mode 100644 index 00000000..bbe8bce9 --- /dev/null +++ b/spec/frame/runtime_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true + +describe Ferrum::Frame::Runtime do + describe "#execute" do + it "executes multiple lines of javascript" do + browser.execute <<-JS + var a = 1 + var b = 2 + window.result = a + b + JS + expect(browser.evaluate("window.result")).to eq(3) + end + + context "with javascript errors" do + let(:browser) { Ferrum::Browser.new(base_url: base_url, js_errors: true) } + + it "propagates a Javascript error to a ruby exception" do + expect do + browser.execute(%(throw new Error("zomg"))) + end.to raise_error(Ferrum::JavaScriptError) { |e| + expect(e.message).to include("Error: zomg") + } + end + + it "propagates an asynchronous Javascript error on the page to a ruby exception" do + expect do + browser.execute "setTimeout(function() { omg }, 0)" + sleep 0.01 + browser.execute "" + end.to raise_error(Ferrum::JavaScriptError, /ReferenceError.*omg/) + end + + it "propagates a synchronous Javascript error on the page to a ruby exception" do + expect do + browser.execute "omg" + end.to raise_error(Ferrum::JavaScriptError, /ReferenceError.*omg/) + end + + it "does not re-raise a Javascript error if it is rescued" do + expect do + browser.execute "setTimeout(function() { omg }, 0)" + sleep 0.01 + browser.execute "" + end.to raise_error(Ferrum::JavaScriptError, /ReferenceError.*omg/) + + # should not raise again + expect(browser.evaluate("1+1")).to eq(2) + end + + it "propagates a Javascript error during page load to a ruby exception" do + expect { browser.go_to("/ferrum/js_error") }.to raise_error(Ferrum::JavaScriptError) + end + + it "does not propagate a Javascript error to ruby if error raising disabled" do + browser = Ferrum::Browser.new(base_url: base_url, js_errors: false) + browser.go_to("/ferrum/js_error") + browser.execute "setTimeout(function() { omg }, 0)" + sleep 0.1 + expect(browser.body).to include("hello") + ensure + browser&.quit + end + + it "does not propagate a Javascript error to ruby if error raising disabled and client restarted" do + browser = Ferrum::Browser.new(base_url: base_url, js_errors: false) + browser.restart + browser.go_to("/ferrum/js_error") + browser.execute "setTimeout(function() { omg }, 0)" + sleep 0.1 + expect(browser.body).to include("hello") + ensure + browser&.quit + end + end + end + + describe "#evaluate" do + it "returns an element" do + browser.go_to("/ferrum/type") + element = browser.evaluate(%(document.getElementById("empty_input"))) + expect(element).to eq(browser.at_css("#empty_input")) + end + + it "returns structures with elements" do + browser.go_to("/ferrum/type") + result = browser.evaluate <<~JS + { + a: document.getElementById("empty_input"), + b: { c: document.querySelectorAll("#empty_textarea, #filled_textarea") } + } + JS + + expect(result).to eq( + "a" => browser.at_css("#empty_input"), + "b" => { + "c" => browser.css("#empty_textarea, #filled_textarea") + } + ) + end + + it "returns values properly" do + expect(browser.evaluate("null")).to be_nil + expect(browser.evaluate("false")).to be false + expect(browser.evaluate("true")).to be true + expect(browser.evaluate("undefined")).to eq(nil) + + expect(browser.evaluate("3;")).to eq(3) + expect(browser.evaluate("31337")).to eq(31_337) + expect(browser.evaluate(%("string"))).to eq("string") + expect(browser.evaluate(%({foo: "bar"}))).to eq("foo" => "bar") + + expect(browser.evaluate("new Object")).to eq({}) + expect(browser.evaluate("new Date(2012, 0).toDateString()")).to eq("Sun Jan 01 2012") + expect(browser.evaluate("new Object({a: 1})")).to eq({ "a" => 1 }) + expect(browser.evaluate("new Array")).to eq([]) + expect(browser.evaluate("new Function")).to eq({}) + + expect do + browser.evaluate(%(throw "smth")) + end.to raise_error(Ferrum::JavaScriptError) + end + + context "when cyclic structure is returned" do + context "ignores seen" do + let(:code) do + <<~JS + (function() { + var a = {}; + var b = {}; + var c = {}; + c.a = a; + a.a = a; + a.b = b; + a.c = c; + return %s; + })() + JS + end + + it "returns object" do + expect(browser.evaluate(code % "a")).to eq(Ferrum::CyclicObject.instance) + end + + it "returns array" do + expect(browser.evaluate(code % "[a]")).to eq([Ferrum::CyclicObject.instance]) + end + end + + it "backtracks what it has seen" do + expect(browser.evaluate("(function() { var a = {}; return [a, a] })()")).to eq([{}, {}]) + end + end + end + + describe "#evaluate_func" do + let(:function) do + <<~JS + function(c) { + let a = 1; + let b = 2; + return a + b + c; + } + JS + end + + it "evaluates multiple lines of javascript function" do + expect(browser.evaluate_func(function, 3)).to eq(6) + end + + it "evaluates a function on a node" do + browser.go_to("/ferrum/index") + node = browser.at_xpath(".//a") + + function = <<~JS + function(attributeName) { + return this.getAttribute(attributeName); + } + JS + + expect(browser.evaluate_func(function, "href", on: node)).to eq("js_redirect") + end + end + + describe "#evaluate_async" do + it "handles values properly" do + expect(browser.evaluate_async("arguments[0](null)", 5)).to be_nil + expect(browser.evaluate_async("arguments[0](false)", 5)).to be false + expect(browser.evaluate_async("arguments[0](true)", 5)).to be true + expect(browser.evaluate_async(%(arguments[0]({foo: "bar"})), 5)).to eq("foo" => "bar") + end + + it "times out" do + expect do + browser.evaluate_async("var callback=arguments[0]; setTimeout(function(){callback(true)}, 4000)", 1) + end.to raise_error(Ferrum::ScriptTimeoutError) + end + end + + describe "#add_script_tag" do + it "adds by url" do + browser.go_to + expect do + browser.evaluate("$('a').first().text()") + end.to raise_error(Ferrum::JavaScriptError) + + browser.add_script_tag(url: "/ferrum/jquery.min.js") + + expect(browser.evaluate("$('a').first().text()")).to eq("Relative") + end + + it "adds by path" do + browser.go_to + path = "#{Ferrum::Application::FERRUM_PUBLIC}/jquery-1.11.3.min.js" + expect do + browser.evaluate("$('a').first().text()") + end.to raise_error(Ferrum::JavaScriptError) + + browser.add_script_tag(path: path) + + expect(browser.evaluate("$('a').first().text()")).to eq("Relative") + end + + it "adds by content" do + browser.go_to + + browser.add_script_tag(content: "function yay() { return 'yay!'; }") + + expect(browser.evaluate("yay()")).to eq("yay!") + end + end + + describe "#add_style_tag" do + let(:font_size) do + <<~JS + window + .getComputedStyle(document.querySelector('a')) + .getPropertyValue('font-size') + JS + end + + it "adds by url" do + browser.go_to + expect(browser.evaluate(font_size)).to eq("16px") + + browser.add_style_tag(url: "/ferrum/add_style_tag.css") + + expect(browser.evaluate(font_size)).to eq("50px") + end + + it "adds by path" do + browser.go_to + path = "#{Ferrum::Application::FERRUM_PUBLIC}/add_style_tag.css" + expect(browser.evaluate(font_size)).to eq("16px") + + browser.add_style_tag(path: path) + + expect(browser.evaluate(font_size)).to eq("50px") + end + + it "adds by content" do + browser.go_to + + browser.add_style_tag(content: "a { font-size: 20px; }") + + expect(browser.evaluate(font_size)).to eq("20px") + end + end +end diff --git a/spec/frame_spec.rb b/spec/frame_spec.rb index 1c237d04..9bba6c96 100644 --- a/spec/frame_spec.rb +++ b/spec/frame_spec.rb @@ -1,354 +1,352 @@ # frozen_string_literal: true -module Ferrum - describe Frame do - describe "#at_xpath" do - it "works correctly when JSON is overwritten" do - browser.go_to("/ferrum/index") - browser.execute("JSON = {};") - expect { browser.at_xpath("//a[text() = 'JS redirect']") }.not_to raise_error - end - end - - it "supports selection by index" do - browser.go_to("/ferrum/frames") - frame = browser.at_xpath("//iframe").frame - expect(frame.url).to end_with("/ferrum/slow") +describe Ferrum::Frame do + describe "#at_xpath" do + it "works correctly when JSON is overwritten" do + browser.go_to("/ferrum/index") + browser.execute("JSON = {};") + expect { browser.at_xpath("//a[text() = 'JS redirect']") }.not_to raise_error end + end - it "supports selection by element" do - browser.go_to("/ferrum/frames") - frame = browser.at_css("iframe[name]").frame - expect(frame.url).to end_with("/ferrum/slow") - end + it "supports selection by index" do + browser.go_to("/ferrum/frames") + frame = browser.at_xpath("//iframe").frame + expect(frame.url).to end_with("/ferrum/slow") + end - it "supports selection by element without name or id" do - browser.go_to("/ferrum/frames") - frame = browser.at_css("iframe:not([name]):not([id])").frame - expect(frame.url).to end_with("/ferrum/headers") - end + it "supports selection by element" do + browser.go_to("/ferrum/frames") + frame = browser.at_css("iframe[name]").frame + expect(frame.url).to end_with("/ferrum/slow") + end - it "supports selection by element with id but no name" do - browser.go_to("/ferrum/frames") - frame = browser.at_css("iframe[id]:not([name])").frame - expect(frame.url).to end_with("/ferrum/get_cookie") - end + it "supports selection by element without name or id" do + browser.go_to("/ferrum/frames") + frame = browser.at_css("iframe:not([name]):not([id])").frame + expect(frame.url).to end_with("/ferrum/headers") + end - it "finds main frame properly" do - browser.go_to("/ferrum/popup_frames") + it "supports selection by element with id but no name" do + browser.go_to("/ferrum/frames") + frame = browser.at_css("iframe[id]:not([name])").frame + expect(frame.url).to end_with("/ferrum/get_cookie") + end - browser.at_xpath("//a[text()='pop up']").click + it "finds main frame properly" do + browser.go_to("/ferrum/popup_frames") - expect(browser.pages.size).to eq(2) - opened_page = browser.pages.last - expect(opened_page.main_frame.url).to end_with("/frames") - end + browser.at_xpath("//a[text()='pop up']").click - it "waits for the frame to load" do - browser.go_to - browser.execute <<-JS - document.body.innerHTML += "