diff --git a/rb/.rubocop.yml b/rb/.rubocop.yml index f0a87859523c7..4de46f2f7b77d 100644 --- a/rb/.rubocop.yml +++ b/rb/.rubocop.yml @@ -62,12 +62,14 @@ Metrics/PerceivedComplexity: Exclude: - 'lib/selenium/webdriver/chrome/driver.rb' - 'lib/selenium/webdriver/remote/capabilities.rb' + - 'lib/selenium/webdriver/chrome/options.rb' Metrics/CyclomaticComplexity: Max: 9 Exclude: - 'lib/selenium/webdriver/chrome/driver.rb' - 'lib/selenium/webdriver/remote/capabilities.rb' + - 'lib/selenium/webdriver/chrome/options.rb' - 'spec/integration/selenium/webdriver/spec_support/test_environment.rb' Metrics/ClassLength: diff --git a/rb/lib/selenium/webdriver/chrome/driver.rb b/rb/lib/selenium/webdriver/chrome/driver.rb index 964e79e17950e..0425dd2dee0b6 100644 --- a/rb/lib/selenium/webdriver/chrome/driver.rb +++ b/rb/lib/selenium/webdriver/chrome/driver.rb @@ -72,6 +72,7 @@ def create_capabilities(opts) if profile profile = profile.as_json + options.args ||= [] if options.args.none?(&/user-data-dir/.method(:match?)) options.add_argument("--user-data-dir=#{profile['directory']}") end diff --git a/rb/lib/selenium/webdriver/chrome/options.rb b/rb/lib/selenium/webdriver/chrome/options.rb index 763df03f88a96..cafb3d6e6662e 100755 --- a/rb/lib/selenium/webdriver/chrome/options.rb +++ b/rb/lib/selenium/webdriver/chrome/options.rb @@ -21,12 +21,23 @@ module Selenium module WebDriver module Chrome class Options < WebDriver::Common::Options - attr_reader :args, :prefs, :options, :emulation, :extensions, :encoded_extensions - attr_accessor :binary KEY = 'goog:chromeOptions' - # + # see: http://chromedriver.chromium.org/capabilities + CAPABILITIES = %i[args binary extensions local_state prefs detach debugger_address exclude_switches + minidump_path mobile_emulation perf_logging_prefs window_types].freeze + + (CAPABILITIES + %i[options emulation encoded_extensions]).each do |key| + define_method key do + @options[key] + end + + define_method "#{key}=" do |value| + @options[key] = value + end + end + # Create a new Options instance. # # @example @@ -40,18 +51,31 @@ class Options < WebDriver::Common::Options # @option opts [Array] :extensions A list of paths to (.crx) Chrome extensions to install on startup # @option opts [Hash] :options A hash for raw options # @option opts [Hash] :emulation A hash for raw emulation options - # - - def initialize(**opts) - @args = Set.new(opts.delete(:args) || []) - @binary = opts.delete(:binary) || Chrome.path - @prefs = opts.delete(:prefs) || {} - @extensions = opts.delete(:extensions) || [] - @options = opts.delete(:options) || {} - @emulation = opts.delete(:emulation) || {} - @encoded_extensions = [] + # @option opts [Hash] :local_state A hash for the Local State file in the user data folder + # @option opts [Boolean] :detach whether browser is closed when the driver is sent the quit command + # @option opts [String] :debugger_address address of a Chrome debugger server to connect to + # @option opts [Array] :exclude_switches command line switches to exclude + # @option opts [String] :minidump_path Directory to store Chrome minidumps (linux only) + # @option opts [Hash] :perf_logging_prefs A hash for performance logging preferences + # @option opts [Array] :window_types A list of window types to appear in the list of window handles + # + + def initialize(emulation: nil, encoded_extensions: nil, options: nil, **opts) + @options = if options + WebDriver.logger.deprecate(":options as keyword for initializing #{self.class}", + "values directly in #new constructor") + opts.merge(options) + else + opts + end + @options[:mobile_emulation] ||= emulation if emulation + @options[:encoded_extensions] = encoded_extensions if encoded_extensions + @options[:extensions]&.each(&method(:validate_extension)) end + alias_method :emulation, :mobile_emulation + alias_method :emulation=, :mobile_emulation= + # # Add an extension by local path. # @@ -63,10 +87,9 @@ def initialize(**opts) # def add_extension(path) - raise Error::WebDriverError, "could not find extension at #{path.inspect}" unless File.file?(path) - raise Error::WebDriverError, "file was not an extension #{path.inspect}" unless File.extname(path) == '.crx' - - @extensions << path + validate_extension(path) + @options[:extensions] ||= [] + @options[:extensions] << path end # @@ -80,7 +103,8 @@ def add_extension(path) # def add_encoded_extension(encoded) - @encoded_extensions << encoded + @options[:encoded_extensions] ||= [] + @options[:encoded_extensions] << encoded end # @@ -94,7 +118,8 @@ def add_encoded_extension(encoded) # def add_argument(arg) - @args << arg + @options[:args] ||= [] + @options[:args] << arg end # @@ -124,7 +149,8 @@ def add_option(name, value) # def add_preference(name, value) - prefs[name] = value + @options[:prefs] ||= {} + @options[:prefs][name] = value end # @@ -155,10 +181,8 @@ def headless! # @param [String] user_agent Full user agent # - def add_emulation(device_name: nil, device_metrics: nil, user_agent: nil) - @emulation[:deviceName] = device_name if device_name - @emulation[:deviceMetrics] = device_metrics if device_metrics - @emulation[:userAgent] = user_agent if user_agent + def add_emulation(**opt) + @options[:mobile_emulation] = opt end # @@ -166,19 +190,41 @@ def add_emulation(device_name: nil, device_metrics: nil, user_agent: nil) # def as_json(*) - extensions = @extensions.map do |crx_path| - File.open(crx_path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read } + options = @options.dup + + opts = CAPABILITIES.each_with_object({}) do |capability_name, hash| + capability_value = options.delete(capability_name) + hash[capability_name] = capability_value unless capability_value.nil? end - extensions.concat(@encoded_extensions) - opts = @options - opts[:binary] = @binary if @binary - opts[:args] = @args.to_a if @args.any? - opts[:extensions] = extensions if extensions.any? - opts[:mobileEmulation] = @emulation unless @emulation.empty? - opts[:prefs] = @prefs unless @prefs.empty? + opts[:binary] ||= Chrome.path if Chrome.path + extensions = opts[:extensions] || [] + opts[:extensions] = extensions.map(&method(:encode_extension)) + + (options.delete(:encoded_extensions) || []) + opts.delete(:extensions) if opts[:extensions].empty? + opts[:mobile_emulation] = process_emulation(opts[:mobile_emulation] || options.delete(:emulation) || {}) + opts.delete(:mobile_emulation) if opts[:mobile_emulation].empty? + + {KEY => generate_as_json(opts.merge(options))} + end + + private - {KEY => generate_as_json(opts)} + def process_emulation(device_name: nil, device_metrics: nil, user_agent: nil) + emulation = {} + emulation[:device_name] = device_name if device_name + emulation[:device_metrics] = device_metrics if device_metrics + emulation[:user_agent] = user_agent if user_agent + emulation + end + + def encode_extension(path) + File.open(path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read } + end + + def validate_extension(path) + raise Error::WebDriverError, "could not find extension at #{path.inspect}" unless File.file?(path) + raise Error::WebDriverError, "file was not an extension #{path.inspect}" unless File.extname(path) == '.crx' end end # Options end # Chrome diff --git a/rb/lib/selenium/webdriver/ie/options.rb b/rb/lib/selenium/webdriver/ie/options.rb index b536ca23e5e3b..7b292fa3be0a1 100644 --- a/rb/lib/selenium/webdriver/ie/options.rb +++ b/rb/lib/selenium/webdriver/ie/options.rb @@ -122,15 +122,15 @@ def add_option(name, value) def as_json(*) opts = {} + options = @options.dup CAPABILITIES.each do |capability_alias, capability_name| - capability_value = @options.delete(capability_alias) + capability_value = options.delete(capability_alias) opts[capability_name] = capability_value unless capability_value.nil? end opts['ie.browserCommandLineSwitches'] = @args.to_a.join(' ') if @args.any? - opts.merge!(@options) - {KEY => generate_as_json(opts)} + {KEY => generate_as_json(opts.merge(options))} end end # Options end # IE diff --git a/rb/spec/unit/selenium/webdriver/chrome/driver_spec.rb b/rb/spec/unit/selenium/webdriver/chrome/driver_spec.rb index 0dc751d15a468..be7711370b146 100644 --- a/rb/spec/unit/selenium/webdriver/chrome/driver_spec.rb +++ b/rb/spec/unit/selenium/webdriver/chrome/driver_spec.rb @@ -67,13 +67,11 @@ module Chrome profile = Profile.new profile['some_pref'] = true - profile.add_extension(__FILE__) Driver.new(http_client: http, profile: profile) profile_data = profile.as_json expect(caps['goog:chromeOptions']['args'].first).to include(profile_data['directory']) - expect(caps['goog:chromeOptions']['extensions']).to eq(profile_data['extensions']) end context 'with custom desired capabilities' do diff --git a/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb b/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb index e0cbce8c72b0c..921c780526a73 100644 --- a/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb +++ b/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb @@ -26,43 +26,51 @@ module Chrome subject(:options) { described_class.new } describe '#initialize' do - it 'sets passed args' do - opt = Options.new(args: %w[foo bar]) - expect(opt.args.to_a).to eq(%w[foo bar]) - end + it 'accepts defined parameters' do + allow(File).to receive(:file?).and_return(true) + # allow_any_instance_of(Options).to receive(:encode_file).with('foo.crx').and_return("encoded_foo") + # allow_any_instance_of(Options).to receive(:encode_file).with('bar.crx').and_return("encoded_bar") + + opt = Options.new(args: %w[foo bar], + prefs: {foo: 'bar'}, + binary: '/foo/bar', + extensions: ['foo.crx', 'bar.crx'], + encoded_extensions: ['encoded_foobar'], + foo: 'bar', + emulation: {device_name: :bar}, + local_state: {foo: 'bar'}, + detach: true, + debugger_address: '127.0.0.1:8181', + exclude_switches: %w[foobar barfoo], + minidump_path: 'linux/only', + perf_logging_prefs: {enable_network: true}, + window_types: %w[normal devtools]) - it 'sets passed prefs' do - opt = Options.new(prefs: {foo: 'bar'}) + expect(opt.args.to_a).to eq(%w[foo bar]) expect(opt.prefs[:foo]).to eq('bar') - end - - it 'sets passed binary value' do - opt = Options.new(binary: '/foo/bar') expect(opt.binary).to eq('/foo/bar') - end - - it 'sets passed extensions' do - opt = Options.new(extensions: ['foo.crx', 'bar.crx']) expect(opt.extensions).to eq(['foo.crx', 'bar.crx']) - end - - it 'sets passed options' do - opt = Options.new(options: {foo: 'bar'}) - expect(opt.options[:foo]).to eq('bar') - end - - it 'sets passed emulation options' do - opt = Options.new(emulation: {foo: 'bar'}) - expect(opt.emulation[:foo]).to eq('bar') + expect(opt.encoded_extensions).to eq(%w[encoded_foobar]) + expect(opt.instance_variable_get('@options')[:foo]).to eq('bar') + expect(opt.mobile_emulation[:device_name]).to eq(:bar) + expect(opt.local_state[:foo]).to eq('bar') + expect(opt.detach).to eq(true) + expect(opt.debugger_address).to eq('127.0.0.1:8181') + expect(opt.exclude_switches).to eq(%w[foobar barfoo]) + expect(opt.minidump_path).to eq('linux/only') + expect(opt.perf_logging_prefs[:enable_network]).to eq(true) + expect(opt.window_types).to eq(%w[normal devtools]) end end describe '#add_extension' do it 'adds an extension' do - allow(File).to receive(:file?).with('/foo/bar.crx').and_return(true) + allow(File).to receive(:file?).and_return(true) + ext = 'foo.crx' + allow_any_instance_of(Options).to receive(:encode_file).with(ext).and_return("encoded_#{ext[/([^\.]*)/]}") - options.add_extension('/foo/bar.crx') - expect(options.extensions).to include('/foo/bar.crx') + options.add_extension(ext) + expect(options.extensions).to eq([ext]) end it 'raises error when the extension file is missing' do @@ -109,7 +117,7 @@ module Chrome describe '#add_option' do it 'adds an option' do options.add_option(:foo, 'bar') - expect(options.options[:foo]).to eq('bar') + expect(options.instance_variable_get('@options')[:foo]).to eq('bar') end end @@ -123,45 +131,55 @@ module Chrome describe '#add_emulation' do it 'add an emulated device by name' do options.add_emulation(device_name: 'iPhone 6') - expect(options.emulation).to eq(deviceName: 'iPhone 6') + expect(options.emulation).to eq(device_name: 'iPhone 6') end it 'adds emulated device metrics' do options.add_emulation(device_metrics: {width: 400}) - expect(options.emulation).to eq(deviceMetrics: {width: 400}) + expect(options.emulation).to eq(device_metrics: {width: 400}) end it 'adds emulated user agent' do options.add_emulation(user_agent: 'foo') - expect(options.emulation).to eq(userAgent: 'foo') + expect(options.emulation).to eq(user_agent: 'foo') end end describe '#as_json' do - it 'encodes extensions to base64' do + it 'returns a JSON hash' do allow(File).to receive(:file?).and_return(true) - options.add_extension('/foo.crx') - - allow(File).to receive(:open).and_yield(instance_double(File, read: :foo)) - expect(Base64).to receive(:strict_encode64).with(:foo) - options.as_json - end + allow_any_instance_of(Options).to receive(:encode_extension).with('foo.crx').and_return("encoded_foo") + allow_any_instance_of(Options).to receive(:encode_extension).with('bar.crx').and_return("encoded_bar") - it 'returns a JSON hash' do - allow(File).to receive(:open).and_return('bar') - opts = Options.new(args: ['foo'], + opts = Options.new(args: %w[foo bar], + prefs: {foo: 'bar'}, binary: '/foo/bar', - prefs: {a: 1}, - extensions: ['/foo.crx'], - options: {foo: :bar}, - emulation: {device_name: 'mine'}) + extensions: ['foo.crx', 'bar.crx'], + encoded_extensions: ['encoded_foobar'], + foo: 'bar', + emulation: {device_name: :mine}, + local_state: {foo: 'bar'}, + detach: true, + debugger_address: '127.0.0.1:8181', + exclude_switches: %w[foobar barfoo], + minidump_path: 'linux/only', + perf_logging_prefs: {'enable_network': true}, + window_types: %w[normal devtools]) json = opts.as_json['goog:chromeOptions'] - expect(json).to eq('args' => ['foo'], 'binary' => '/foo/bar', - 'prefs' => {'a' => 1}, - 'extensions' => ['bar'], + expect(json).to eq('args' => %w[foo bar], + 'prefs' => {'foo' => 'bar'}, + 'binary' => '/foo/bar', + 'extensions' => %w[encoded_foo encoded_bar encoded_foobar], 'foo' => 'bar', - 'mobileEmulation' => {'deviceName' => 'mine'}) + 'mobileEmulation' => {'deviceName' => 'mine'}, + 'localState' => {'foo' => 'bar'}, + 'detach' => true, + 'debuggerAddress' => '127.0.0.1:8181', + 'excludeSwitches' => %w[foobar barfoo], + 'minidumpPath' => 'linux/only', + 'perfLoggingPrefs' => {'enableNetwork' => true}, + 'windowTypes' => %w[normal devtools]) end end end # Options