From aadd087ecff7dbd14c19c4803c7643f21a5ba392 Mon Sep 17 00:00:00 2001 From: titusfortner Date: Tue, 13 Dec 2022 16:25:47 -0800 Subject: [PATCH] [rb] implement chromium classes for chrome and edge to inherit --- rb/lib/selenium/webdriver.rb | 1 + rb/lib/selenium/webdriver/chrome/driver.rb | 33 +-- rb/lib/selenium/webdriver/chrome/features.rb | 78 +---- rb/lib/selenium/webdriver/chrome/options.rb | 240 +--------------- rb/lib/selenium/webdriver/chrome/profile.rb | 86 +----- rb/lib/selenium/webdriver/chrome/service.rb | 20 +- rb/lib/selenium/webdriver/chromium.rb | 33 +++ rb/lib/selenium/webdriver/chromium/driver.rb | 62 ++++ .../selenium/webdriver/chromium/features.rb | 104 +++++++ rb/lib/selenium/webdriver/chromium/options.rb | 268 ++++++++++++++++++ rb/lib/selenium/webdriver/chromium/profile.rb | 113 ++++++++ rb/lib/selenium/webdriver/chromium/service.rb | 43 +++ rb/lib/selenium/webdriver/common/driver.rb | 2 +- rb/lib/selenium/webdriver/edge/driver.rb | 4 +- rb/lib/selenium/webdriver/edge/features.rb | 6 +- rb/lib/selenium/webdriver/edge/options.rb | 8 +- rb/lib/selenium/webdriver/edge/profile.rb | 4 +- rb/lib/selenium/webdriver/edge/service.rb | 4 +- 18 files changed, 655 insertions(+), 454 deletions(-) create mode 100644 rb/lib/selenium/webdriver/chromium.rb create mode 100644 rb/lib/selenium/webdriver/chromium/driver.rb create mode 100644 rb/lib/selenium/webdriver/chromium/features.rb create mode 100644 rb/lib/selenium/webdriver/chromium/options.rb create mode 100644 rb/lib/selenium/webdriver/chromium/profile.rb create mode 100644 rb/lib/selenium/webdriver/chromium/service.rb diff --git a/rb/lib/selenium/webdriver.rb b/rb/lib/selenium/webdriver.rb index 1bdf45610e21d..da9cd708066d1 100644 --- a/rb/lib/selenium/webdriver.rb +++ b/rb/lib/selenium/webdriver.rb @@ -36,6 +36,7 @@ module WebDriver Location = Struct.new(:latitude, :longitude, :altitude) autoload :BiDi, 'selenium/webdriver/bidi' + autoload :Chromium, 'selenium/webdriver/chromium' autoload :Chrome, 'selenium/webdriver/chrome' autoload :DevTools, 'selenium/webdriver/devtools' autoload :Edge, 'selenium/webdriver/edge' diff --git a/rb/lib/selenium/webdriver/chrome/driver.rb b/rb/lib/selenium/webdriver/chrome/driver.rb index 0408dc339db9b..8b4411f980f67 100644 --- a/rb/lib/selenium/webdriver/chrome/driver.rb +++ b/rb/lib/selenium/webdriver/chrome/driver.rb @@ -17,6 +17,8 @@ # specific language governing permissions and limitations # under the License. +require 'selenium/webdriver/chromium/driver' + module Selenium module WebDriver module Chrome @@ -26,45 +28,16 @@ module Chrome # @api private # - class Driver < WebDriver::Driver - EXTENSIONS = [DriverExtensions::HasCDP, - DriverExtensions::HasBiDi, - DriverExtensions::HasCasting, - DriverExtensions::HasNetworkConditions, - DriverExtensions::HasNetworkInterception, - DriverExtensions::HasWebStorage, - DriverExtensions::HasLaunching, - DriverExtensions::HasLocation, - DriverExtensions::HasPermissions, - DriverExtensions::DownloadsFiles, - DriverExtensions::HasDevTools, - DriverExtensions::HasAuthentication, - DriverExtensions::HasLogs, - DriverExtensions::HasLogEvents, - DriverExtensions::HasPinnedScripts, - DriverExtensions::PrintsPage].freeze - + class Driver < Chromium::Driver def browser :chrome end private - def devtools_url - uri = URI(devtools_address) - response = Net::HTTP.get(uri.hostname, '/json/version', uri.port) - - JSON.parse(response)['webSocketDebuggerUrl'] - end - - def devtools_version - Integer(capabilities.browser_version.split('.').first) - end - def devtools_address "http://#{capabilities['goog:chromeOptions']['debuggerAddress']}" end - end # Driver end # Chrome end # WebDriver diff --git a/rb/lib/selenium/webdriver/chrome/features.rb b/rb/lib/selenium/webdriver/chrome/features.rb index 8b19078e99377..49a5d98baeb54 100644 --- a/rb/lib/selenium/webdriver/chrome/features.rb +++ b/rb/lib/selenium/webdriver/chrome/features.rb @@ -17,93 +17,27 @@ # specific language governing permissions and limitations # under the License. +require 'selenium/webdriver/chromium/features' + module Selenium module WebDriver module Chrome module Features + include WebDriver::Chromium::Features + CHROME_COMMANDS = { - launch_app: [:post, 'session/:session_id/chromium/launch_app'], get_cast_sinks: [:get, 'session/:session_id/goog/cast/get_sinks'], set_cast_sink_to_use: [:post, 'session/:session_id/goog/cast/set_sink_to_use'], start_cast_tab_mirroring: [:post, 'session/:session_id/goog/cast/start_tab_mirroring'], start_cast_desktop_mirroring: [:post, 'session/:session_id/goog/cast/start_desktop_mirroring'], get_cast_issue_message: [:get, 'session/:session_id/goog/cast/get_issue_message'], stop_casting: [:post, 'session/:session_id/goog/cast/stop_casting'], - get_network_conditions: [:get, 'session/:session_id/chromium/network_conditions'], - set_network_conditions: [:post, 'session/:session_id/chromium/network_conditions'], - delete_network_conditions: [:delete, 'session/:session_id/chromium/network_conditions'], - set_permission: [:post, 'session/:session_id/permissions'], - send_command: [:post, 'session/:session_id/goog/cdp/execute'], - get_available_log_types: [:get, 'session/:session_id/se/log/types'], - get_log: [:post, 'session/:session_id/se/log'] + send_command: [:post, 'session/:session_id/goog/cdp/execute'] }.freeze def commands(command) - CHROME_COMMANDS[command] || self.class::COMMANDS[command] - end - - def launch_app(id) - execute :launch_app, {}, {id: id} - end - - def cast_sinks - execute :get_cast_sinks - end - - def cast_sink_to_use=(name) - execute :set_cast_sink_to_use, {}, {sinkName: name} - end - - def cast_issue_message - execute :cast_issue_message - end - - def start_cast_tab_mirroring(name) - execute :start_cast_tab_mirroring, {}, {sinkName: name} - end - - def start_cast_desktop_mirroring(name) - execute :start_cast_desktop_mirroring, {}, {sinkName: name} - end - - def stop_casting(name) - execute :stop_casting, {}, {sinkName: name} - end - - def set_permission(name, value) - execute :set_permission, {}, {descriptor: {name: name}, state: value} - end - - def network_conditions - execute :get_network_conditions - end - - def network_conditions=(conditions) - execute :set_network_conditions, {}, {network_conditions: conditions} - end - - def delete_network_conditions - execute :delete_network_conditions - end - - def send_command(command_params) - execute :send_command, {}, command_params - end - - def available_log_types - types = execute :get_available_log_types - Array(types).map(&:to_sym) - end - - def log(type) - data = execute :get_log, {}, {type: type.to_s} - - Array(data).map do |l| - LogEntry.new l.fetch('level', 'UNKNOWN'), l.fetch('timestamp'), l.fetch('message') - rescue KeyError - next - end + CHROME_COMMANDS[command] || CHROMIUM_COMMANDS[command] || self.class::COMMANDS[command] end end # Bridge end # Chrome diff --git a/rb/lib/selenium/webdriver/chrome/options.rb b/rb/lib/selenium/webdriver/chrome/options.rb index 419de53faabb3..53cf789899c0e 100644 --- a/rb/lib/selenium/webdriver/chrome/options.rb +++ b/rb/lib/selenium/webdriver/chrome/options.rb @@ -17,258 +17,24 @@ # specific language governing permissions and limitations # under the License. +require 'selenium/webdriver/chromium/options' + module Selenium module WebDriver module Chrome - class Options < WebDriver::Options - attr_accessor :profile, :logging_prefs - + class Options < Chromium::Options KEY = 'goog:chromeOptions' BROWSER = 'chrome' - # see: http://chromedriver.chromium.org/capabilities - CAPABILITIES = {args: 'args', - binary: 'binary', - local_state: 'localState', - prefs: 'prefs', - detach: 'detach', - debugger_address: 'debuggerAddress', - exclude_switches: 'excludeSwitches', - minidump_path: 'minidumpPath', - emulation: 'mobileEmulation', - perf_logging_prefs: 'perfLoggingPrefs', - window_types: 'windowTypes', - android_package: 'androidPackage', - android_activity: 'androidActivity', - android_device_serial: 'androidDeviceSerial', - android_use_running_app: 'androidUseRunningApp'}.freeze - - # NOTE: special handling of 'extensions' to validate when set instead of when used - attr_reader :extensions - - # Create a new Options instance. - # - # @example - # options = Selenium::WebDriver::Chrome::Options.new(args: ['start-maximized', 'user-data-dir=/tmp/temp_profile']) - # driver = Selenium::WebDriver.for(:chrome, capabilities: options) - # - # @param [Profile] profile An instance of a Chrome::Profile Class - # @param [Hash] opts the pre-defined options to create the Chrome::Options with - # @option opts [Array] encoded_extensions List of extensions that do not need to be Base64 encoded - # @option opts [Array] args List of command-line arguments to use when starting Chrome - # @option opts [String] binary Path to the Chrome executable to use - # @option opts [Hash] prefs A hash with each entry consisting of the name of the preference and its value - # @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 - # @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(profile: nil, **opts) - super(**opts) - - @profile = profile - - @options = {args: [], - prefs: {}, - emulation: {}, - extensions: [], - local_state: {}, - exclude_switches: [], - perf_logging_prefs: {}, - window_types: []}.merge(@options) - - @logging_prefs = options.delete(:logging_prefs) || {} - @encoded_extensions = @options.delete(:encoded_extensions) || [] - @extensions = [] - @options.delete(:extensions).each { |ext| validate_extension(ext) } - end - - # - # Add an extension by local path. - # - # @example - # options = Selenium::WebDriver::Chrome::Options.new - # options.add_extension('/path/to/extension.crx') - # - # @param [String] path The local path to the .crx file - # - - def add_extension(path) - validate_extension(path) - end - - # - # Add an extension by local path. - # - # @example - # extensions = ['/path/to/extension.crx', '/path/to/other.crx'] - # options = Selenium::WebDriver::Chrome::Options.new - # options.extensions = extensions - # - # @param [Array] extensions A list of paths to (.crx) Chrome extensions to install on startup - # - - def extensions=(extensions) - extensions.each { |ext| validate_extension(ext) } - end - - # - # Add an extension by Base64-encoded string. - # - # @example - # options = Selenium::WebDriver::Chrome::Options.new - # options.add_encoded_extension(encoded_string) - # - # @param [String] encoded The Base64-encoded string of the .crx file - # - - def add_encoded_extension(encoded) - @encoded_extensions << encoded - end - - # - # Add a command-line argument to use when starting Chrome. - # - # @example Start Chrome maximized - # options = Selenium::WebDriver::Chrome::Options.new - # options.add_argument('start-maximized') - # - # @param [String] arg The command-line argument to add - # - - def add_argument(arg) - @options[:args] << arg - end - - # - # Add a preference that is only applied to the user profile in use. - # - # @example Set the default homepage - # options = Selenium::WebDriver::Chrome::Options.new - # options.add_preference('homepage', 'http://www.seleniumhq.com/') - # - # @param [String] name Key of the preference - # @param [Boolean, String, Integer] value Value of the preference - # - - def add_preference(name, value) - @options[:prefs][name] = value - end - - # - # Run Chrome in headless mode. - # - # @example Enable headless mode - # options = Selenium::WebDriver::Chrome::Options.new - # options.headless! - # - - def headless! - add_argument '--headless' - end - - # - # Add emulation device information - # - # see: http://chromedriver.chromium.org/mobile-emulation - # - # @example Start Chrome in mobile emulation mode by device name - # options = Selenium::WebDriver::Chrome::Options.new - # options.add_emulation(device_name: 'iPhone 6') - # - # @example Start Chrome in mobile emulation mode by device metrics - # options = Selenium::WebDriver::Chrome::Options.new - # options.add_emulation(device_metrics: {width: 400, height: 800, pixelRatio: 1, touch: true}) - # - # @param [Hash] opts the pre-defined options for adding mobile emulation values - # @option opts [String] :device_name A valid device name from the Chrome DevTools Emulation panel - # @option opts [Hash] :device_metrics Hash containing width, height, pixelRatio, touch - # @option opts [String] :user_agent Full user agent - # - - def add_emulation(**opts) - @options[:emulation] = opts - end - - # - # Enables mobile browser use on Android. - # - # @see https://chromedriver.chromium.org/getting-started/getting-started---android - # - # @param [String] package The package name of the Chrome or WebView app. - # @param [String] serial_number The device serial number on which to launch the Chrome or WebView app. - # @param [String] use_running_app When true uses an already-running Chrome or WebView app, - # instead of launching the app with a clear data directory. - # @param [String] activity Name of the Activity hosting the WebView (Not available on Chrome Apps). - # - - def enable_android(package: 'com.android.chrome', serial_number: nil, use_running_app: nil, activity: nil) - @options[:android_package] = package - @options[:android_activity] = activity unless activity.nil? - @options[:android_device_serial] = serial_number unless serial_number.nil? - @options[:android_use_running_app] = use_running_app unless use_running_app.nil? - end - private def enable_logging(browser_options) browser_options['goog:loggingPrefs'] = @logging_prefs end - def process_browser_options(browser_options) - enable_logging(browser_options) unless @logging_prefs.empty? - - options = browser_options[self.class::KEY] - options['binary'] ||= binary_path if binary_path - - check_w3c(options[:w3c]) if options.key?(:w3c) - - if @profile - options['args'] ||= [] - options['args'] << "--user-data-dir=#{@profile.directory}" - end - - return if (@encoded_extensions + @extensions).empty? - - options['extensions'] = @encoded_extensions + @extensions.map { |ext| encode_extension(ext) } - end - - def check_w3c(w3c) - if w3c - WebDriver.logger.warn("Setting 'w3c: true' is redundant and will no longer be allowed", id: :w3c) - return - end - - raise Error::InvalidArgumentError, - "Setting 'w3c: false' is not allowed.\n" \ - "Please update to W3C Syntax: https://www.selenium.dev/blog/2022/legacy-protocol-support/" - end - def binary_path Chrome.path 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' - - @extensions << path - end - - def camelize?(key) - !%w[localState prefs].include?(key) - end end # Options end # Chrome end # WebDriver diff --git a/rb/lib/selenium/webdriver/chrome/profile.rb b/rb/lib/selenium/webdriver/chrome/profile.rb index 7fc6a814ff32b..1b34a895faf9d 100644 --- a/rb/lib/selenium/webdriver/chrome/profile.rb +++ b/rb/lib/selenium/webdriver/chrome/profile.rb @@ -17,6 +17,8 @@ # specific language governing permissions and limitations # under the License. +require 'selenium/webdriver/chromium/profile' + module Selenium module WebDriver module Chrome @@ -24,89 +26,7 @@ module Chrome # @private # - class Profile - include ProfileHelper - - def initialize(model = nil) - @model = verify_model(model) - @extensions = [] - @encoded_extensions = [] - @directory = nil - end - - def add_extension(path) - raise Error::WebDriverError, "could not find extension at #{path.inspect}" unless File.file?(path) - - @extensions << path - end - - def add_encoded_extension(encoded) - @encoded_extensions << encoded - end - - def directory - @directory || layout_on_disk - end - - # - # Set a preference in the profile. - # - # See https://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/pref_names.cc - # - - def []=(key, value) - parts = key.split('.') - parts[0..-2].inject(prefs) { |a, e| a[e] ||= {} }[parts.last] = value - end - - def [](key) - parts = key.split('.') - parts.inject(prefs) { |a, e| a.fetch(e) } - end - - def layout_on_disk - @directory = @model ? create_tmp_copy(@model) : Dir.mktmpdir('webdriver-chrome-profile') - FileReaper << @directory - - write_prefs_to @directory - - @directory - end - - def as_json(*) - extensions = @extensions.map do |crx_path| - File.open(crx_path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read } - end - - extensions.concat(@encoded_extensions) - - opts = {'directory' => directory || layout_on_disk} - opts['extensions'] = extensions if extensions.any? - opts - end - - private - - def write_prefs_to(dir) - prefs_file = prefs_file_for(dir) - - FileUtils.mkdir_p File.dirname(prefs_file) - File.open(prefs_file, 'w') { |file| file << JSON.generate(prefs) } - end - - def prefs - @prefs ||= read_model_prefs - end - - def read_model_prefs - return {} unless @model - - JSON.parse File.read(prefs_file_for(@model)) - end - - def prefs_file_for(dir) - File.join dir, 'Default', 'Preferences' - end + class Profile < Chromium::Profile end # Profile end # Chrome end # WebDriver diff --git a/rb/lib/selenium/webdriver/chrome/service.rb b/rb/lib/selenium/webdriver/chrome/service.rb index de3e4c3b9efb5..b49878fa90220 100644 --- a/rb/lib/selenium/webdriver/chrome/service.rb +++ b/rb/lib/selenium/webdriver/chrome/service.rb @@ -17,10 +17,12 @@ # specific language governing permissions and limitations # under the License. +require 'selenium/webdriver/chromium/service' + module Selenium module WebDriver module Chrome - class Service < WebDriver::Service + class Service < Chromium::Service DEFAULT_PORT = 9515 EXECUTABLE = 'chromedriver' MISSING_TEXT = <<~ERROR @@ -29,22 +31,6 @@ class Service < WebDriver::Service More info at https://www.selenium.dev/documentation/webdriver/getting_started/install_drivers/?language=ruby. ERROR SHUTDOWN_SUPPORTED = true - - private - - def extract_service_args(driver_opts) - driver_args = super - driver_opts = driver_opts.dup - driver_args << "--log-path=#{driver_opts.delete(:log_path)}" if driver_opts.key?(:log_path) - driver_args << "--url-base=#{driver_opts.delete(:url_base)}" if driver_opts.key?(:url_base) - driver_args << "--port-server=#{driver_opts.delete(:port_server)}" if driver_opts.key?(:port_server) - if driver_opts.key?(:whitelisted_ips) - driver_args << "--whitelisted-ips=#{driver_opts.delete(:whitelisted_ips)}" - end - driver_args << "--verbose" if driver_opts.key?(:verbose) - driver_args << "--silent" if driver_opts.key?(:silent) - driver_args - end end # Service end # Chrome end # WebDriver diff --git a/rb/lib/selenium/webdriver/chromium.rb b/rb/lib/selenium/webdriver/chromium.rb new file mode 100644 index 0000000000000..6636964dbca93 --- /dev/null +++ b/rb/lib/selenium/webdriver/chromium.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'net/http' + +module Selenium + module WebDriver + module Chromium + autoload :Features, 'selenium/webdriver/chromium/features' + autoload :Driver, 'selenium/webdriver/chromium/driver' + autoload :Profile, 'selenium/webdriver/chromium/profile' + autoload :Options, 'selenium/webdriver/chromium/options' + autoload :Service, 'selenium/webdriver/chromium/service' + + end # Chromium + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/chromium/driver.rb b/rb/lib/selenium/webdriver/chromium/driver.rb new file mode 100644 index 0000000000000..65323228aed1d --- /dev/null +++ b/rb/lib/selenium/webdriver/chromium/driver.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Selenium + module WebDriver + module Chromium + + # + # Driver implementation for Chrome. + # @api private + # + + class Driver < WebDriver::Driver + EXTENSIONS = [DriverExtensions::HasCDP, + DriverExtensions::HasBiDi, + DriverExtensions::HasCasting, + DriverExtensions::HasNetworkConditions, + DriverExtensions::HasNetworkInterception, + DriverExtensions::HasWebStorage, + DriverExtensions::HasLaunching, + DriverExtensions::HasLocation, + DriverExtensions::HasPermissions, + DriverExtensions::DownloadsFiles, + DriverExtensions::HasDevTools, + DriverExtensions::HasAuthentication, + DriverExtensions::HasLogs, + DriverExtensions::HasLogEvents, + DriverExtensions::HasPinnedScripts, + DriverExtensions::PrintsPage].freeze + + protected + + def devtools_url + uri = URI(devtools_address) + response = Net::HTTP.get(uri.hostname, '/json/version', uri.port) + + JSON.parse(response)['webSocketDebuggerUrl'] + end + + def devtools_version + Integer(capabilities.browser_version.split('.').first) + end + end # Driver + end # Chromium + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/chromium/features.rb b/rb/lib/selenium/webdriver/chromium/features.rb new file mode 100644 index 0000000000000..6ef999f052a29 --- /dev/null +++ b/rb/lib/selenium/webdriver/chromium/features.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Selenium + module WebDriver + module Chromium + module Features + + CHROMIUM_COMMANDS = { + launch_app: [:post, 'session/:session_id/chromium/launch_app'], + get_network_conditions: [:get, 'session/:session_id/chromium/network_conditions'], + set_network_conditions: [:post, 'session/:session_id/chromium/network_conditions'], + delete_network_conditions: [:delete, 'session/:session_id/chromium/network_conditions'], + set_permission: [:post, 'session/:session_id/permissions'], + get_available_log_types: [:get, 'session/:session_id/se/log/types'], + get_log: [:post, 'session/:session_id/se/log'] + }.freeze + + def commands(command) + CHROME_COMMANDS[command] || self.class::COMMANDS[command] + end + + def launch_app(id) + execute :launch_app, {}, {id: id} + end + + def cast_sinks + execute :get_cast_sinks + end + + def cast_sink_to_use=(name) + execute :set_cast_sink_to_use, {}, {sinkName: name} + end + + def cast_issue_message + execute :cast_issue_message + end + + def start_cast_tab_mirroring(name) + execute :start_cast_tab_mirroring, {}, {sinkName: name} + end + + def start_cast_desktop_mirroring(name) + execute :start_cast_desktop_mirroring, {}, {sinkName: name} + end + + def stop_casting(name) + execute :stop_casting, {}, {sinkName: name} + end + + def set_permission(name, value) + execute :set_permission, {}, {descriptor: {name: name}, state: value} + end + + def network_conditions + execute :get_network_conditions + end + + def network_conditions=(conditions) + execute :set_network_conditions, {}, {network_conditions: conditions} + end + + def delete_network_conditions + execute :delete_network_conditions + end + + def send_command(command_params) + execute :send_command, {}, command_params + end + + def available_log_types + types = execute :get_available_log_types + Array(types).map(&:to_sym) + end + + def log(type) + data = execute :get_log, {}, {type: type.to_s} + + Array(data).map do |l| + LogEntry.new l.fetch('level', 'UNKNOWN'), l.fetch('timestamp'), l.fetch('message') + rescue KeyError + next + end + end + end # Bridge + end # Chromium + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/chromium/options.rb b/rb/lib/selenium/webdriver/chromium/options.rb new file mode 100644 index 0000000000000..c7098314ba76d --- /dev/null +++ b/rb/lib/selenium/webdriver/chromium/options.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Selenium + module WebDriver + module Chromium + class Options < WebDriver::Options + attr_accessor :profile, :logging_prefs + + # see: http://chromedriver.chromium.org/capabilities + CAPABILITIES = {args: 'args', + binary: 'binary', + local_state: 'localState', + prefs: 'prefs', + detach: 'detach', + debugger_address: 'debuggerAddress', + exclude_switches: 'excludeSwitches', + minidump_path: 'minidumpPath', + emulation: 'mobileEmulation', + perf_logging_prefs: 'perfLoggingPrefs', + window_types: 'windowTypes', + android_package: 'androidPackage', + android_activity: 'androidActivity', + android_device_serial: 'androidDeviceSerial', + android_use_running_app: 'androidUseRunningApp'}.freeze + + # NOTE: special handling of 'extensions' to validate when set instead of when used + attr_reader :extensions + + # Create a new Options instance. + # + # @example + # options = Selenium::WebDriver::Chrome::Options.new(args: ['start-maximized', 'user-data-dir=/tmp/temp_profile']) + # driver = Selenium::WebDriver.for(:chrome, capabilities: options) + # + # @param [Profile] profile An instance of a Chrome::Profile Class + # @param [Hash] opts the pre-defined options to create the Chrome::Options with + # @option opts [Array] encoded_extensions List of extensions that do not need to be Base64 encoded + # @option opts [Array] args List of command-line arguments to use when starting Chrome + # @option opts [String] binary Path to the Chrome executable to use + # @option opts [Hash] prefs A hash with each entry consisting of the name of the preference and its value + # @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 + # @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(profile: nil, **opts) + super(**opts) + + @profile = profile + + @options = {args: [], + prefs: {}, + emulation: {}, + extensions: [], + local_state: {}, + exclude_switches: [], + perf_logging_prefs: {}, + window_types: []}.merge(@options) + + @logging_prefs = options.delete(:logging_prefs) || {} + @encoded_extensions = @options.delete(:encoded_extensions) || [] + @extensions = [] + @options.delete(:extensions).each { |ext| validate_extension(ext) } + end + + # + # Add an extension by local path. + # + # @example + # options = Selenium::WebDriver::Chrome::Options.new + # options.add_extension('/path/to/extension.crx') + # + # @param [String] path The local path to the .crx file + # + + def add_extension(path) + validate_extension(path) + end + + # + # Add an extension by local path. + # + # @example + # extensions = ['/path/to/extension.crx', '/path/to/other.crx'] + # options = Selenium::WebDriver::Chrome::Options.new + # options.extensions = extensions + # + # @param [Array] extensions A list of paths to (.crx) Chrome extensions to install on startup + # + + def extensions=(extensions) + extensions.each { |ext| validate_extension(ext) } + end + + # + # Add an extension by Base64-encoded string. + # + # @example + # options = Selenium::WebDriver::Chrome::Options.new + # options.add_encoded_extension(encoded_string) + # + # @param [String] encoded The Base64-encoded string of the .crx file + # + + def add_encoded_extension(encoded) + @encoded_extensions << encoded + end + + # + # Add a command-line argument to use when starting Chrome. + # + # @example Start Chrome maximized + # options = Selenium::WebDriver::Chrome::Options.new + # options.add_argument('start-maximized') + # + # @param [String] arg The command-line argument to add + # + + def add_argument(arg) + @options[:args] << arg + end + + # + # Add a preference that is only applied to the user profile in use. + # + # @example Set the default homepage + # options = Selenium::WebDriver::Chrome::Options.new + # options.add_preference('homepage', 'http://www.seleniumhq.com/') + # + # @param [String] name Key of the preference + # @param [Boolean, String, Integer] value Value of the preference + # + + def add_preference(name, value) + @options[:prefs][name] = value + end + + # + # Run Chrome in headless mode. + # + # @example Enable headless mode + # options = Selenium::WebDriver::Chrome::Options.new + # options.headless! + # + + def headless! + add_argument '--headless' + end + + # + # Add emulation device information + # + # see: http://chromedriver.chromium.org/mobile-emulation + # + # @example Start Chrome in mobile emulation mode by device name + # options = Selenium::WebDriver::Chrome::Options.new + # options.add_emulation(device_name: 'iPhone 6') + # + # @example Start Chrome in mobile emulation mode by device metrics + # options = Selenium::WebDriver::Chrome::Options.new + # options.add_emulation(device_metrics: {width: 400, height: 800, pixelRatio: 1, touch: true}) + # + # @param [Hash] opts the pre-defined options for adding mobile emulation values + # @option opts [String] :device_name A valid device name from the Chrome DevTools Emulation panel + # @option opts [Hash] :device_metrics Hash containing width, height, pixelRatio, touch + # @option opts [String] :user_agent Full user agent + # + + def add_emulation(**opts) + @options[:emulation] = opts + end + + # + # Enables mobile browser use on Android. + # + # @see https://chromedriver.chromium.org/getting-started/getting-started---android + # + # @param [String] package The package name of the Chrome or WebView app. + # @param [String] serial_number The device serial number on which to launch the Chrome or WebView app. + # @param [String] use_running_app When true uses an already-running Chrome or WebView app, + # instead of launching the app with a clear data directory. + # @param [String] activity Name of the Activity hosting the WebView (Not available on Chrome Apps). + # + + def enable_android(package: 'com.android.chrome', serial_number: nil, use_running_app: nil, activity: nil) + @options[:android_package] = package + @options[:android_activity] = activity unless activity.nil? + @options[:android_device_serial] = serial_number unless serial_number.nil? + @options[:android_use_running_app] = use_running_app unless use_running_app.nil? + end + + protected + + def process_browser_options(browser_options) + enable_logging(browser_options) unless @logging_prefs.empty? + + options = browser_options[self.class::KEY] + options['binary'] ||= binary_path if binary_path + + check_w3c(options[:w3c]) if options.key?(:w3c) + + if @profile + options['args'] ||= [] + options['args'] << "--user-data-dir=#{@profile.directory}" + end + + return if (@encoded_extensions + @extensions).empty? + + options['extensions'] = @encoded_extensions + @extensions.map { |ext| encode_extension(ext) } + end + + def check_w3c(w3c) + if w3c + WebDriver.logger.warn("Setting 'w3c: true' is redundant and will no longer be allowed", id: :w3c) + return + end + + raise Error::InvalidArgumentError, + "Setting 'w3c: false' is not allowed.\n" \ + "Please update to W3C Syntax: https://www.selenium.dev/blog/2022/legacy-protocol-support/" + end + + def binary_path + Chrome.path + 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' + + @extensions << path + end + + def camelize?(key) + !%w[localState prefs].include?(key) + end + end # Options + end # Chromium + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/chromium/profile.rb b/rb/lib/selenium/webdriver/chromium/profile.rb new file mode 100644 index 0000000000000..891df8e2a98aa --- /dev/null +++ b/rb/lib/selenium/webdriver/chromium/profile.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Selenium + module WebDriver + module Chromium + # + # @private + # + + class Profile + include ProfileHelper + + def initialize(model = nil) + @model = verify_model(model) + @extensions = [] + @encoded_extensions = [] + @directory = nil + end + + def add_extension(path) + raise Error::WebDriverError, "could not find extension at #{path.inspect}" unless File.file?(path) + + @extensions << path + end + + def add_encoded_extension(encoded) + @encoded_extensions << encoded + end + + def directory + @directory || layout_on_disk + end + + # + # Set a preference in the profile. + # + # See https://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/pref_names.cc + # + + def []=(key, value) + parts = key.split('.') + parts[0..-2].inject(prefs) { |a, e| a[e] ||= {} }[parts.last] = value + end + + def [](key) + parts = key.split('.') + parts.inject(prefs) { |a, e| a.fetch(e) } + end + + def layout_on_disk + @directory = @model ? create_tmp_copy(@model) : Dir.mktmpdir('webdriver-chrome-profile') + FileReaper << @directory + + write_prefs_to @directory + + @directory + end + + def as_json(*) + extensions = @extensions.map do |crx_path| + File.open(crx_path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read } + end + + extensions.concat(@encoded_extensions) + + opts = {'directory' => directory || layout_on_disk} + opts['extensions'] = extensions if extensions.any? + opts + end + + private + + def write_prefs_to(dir) + prefs_file = prefs_file_for(dir) + + FileUtils.mkdir_p File.dirname(prefs_file) + File.open(prefs_file, 'w') { |file| file << JSON.generate(prefs) } + end + + def prefs + @prefs ||= read_model_prefs + end + + def read_model_prefs + return {} unless @model + + JSON.parse File.read(prefs_file_for(@model)) + end + + def prefs_file_for(dir) + File.join dir, 'Default', 'Preferences' + end + end # Profile + end # Chromium + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/chromium/service.rb b/rb/lib/selenium/webdriver/chromium/service.rb new file mode 100644 index 0000000000000..fbd823f376180 --- /dev/null +++ b/rb/lib/selenium/webdriver/chromium/service.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Selenium + module WebDriver + module Chromium + class Service < WebDriver::Service + + protected + + def extract_service_args(driver_opts) + driver_args = super + driver_opts = driver_opts.dup + driver_args << "--log-path=#{driver_opts.delete(:log_path)}" if driver_opts.key?(:log_path) + driver_args << "--url-base=#{driver_opts.delete(:url_base)}" if driver_opts.key?(:url_base) + driver_args << "--port-server=#{driver_opts.delete(:port_server)}" if driver_opts.key?(:port_server) + if driver_opts.key?(:whitelisted_ips) + driver_args << "--whitelisted-ips=#{driver_opts.delete(:whitelisted_ips)}" + end + driver_args << "--verbose" if driver_opts.key?(:verbose) + driver_args << "--silent" if driver_opts.key?(:silent) + driver_args + end + end # Service + end # Chromium + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/common/driver.rb b/rb/lib/selenium/webdriver/common/driver.rb index b59ecf3c58249..08ed49ae47388 100644 --- a/rb/lib/selenium/webdriver/common/driver.rb +++ b/rb/lib/selenium/webdriver/common/driver.rb @@ -341,7 +341,7 @@ def screenshot def add_extensions(browser) extensions = case browser when :chrome, :msedge - Chrome::Driver::EXTENSIONS + Chromium::Driver::EXTENSIONS when :firefox Firefox::Driver::EXTENSIONS when :safari, :safari_technology_preview diff --git a/rb/lib/selenium/webdriver/edge/driver.rb b/rb/lib/selenium/webdriver/edge/driver.rb index 76b4d559c61b7..807ff2536961e 100644 --- a/rb/lib/selenium/webdriver/edge/driver.rb +++ b/rb/lib/selenium/webdriver/edge/driver.rb @@ -17,7 +17,7 @@ # specific language governing permissions and limitations # under the License. -require 'selenium/webdriver/chrome/driver' +require 'selenium/webdriver/chromium/driver' module Selenium module WebDriver @@ -28,7 +28,7 @@ module Edge # @api private # - class Driver < Selenium::WebDriver::Chrome::Driver + class Driver < Chromium::Driver def browser :edge end diff --git a/rb/lib/selenium/webdriver/edge/features.rb b/rb/lib/selenium/webdriver/edge/features.rb index a7d8d4d990a57..6e625604d0509 100644 --- a/rb/lib/selenium/webdriver/edge/features.rb +++ b/rb/lib/selenium/webdriver/edge/features.rb @@ -17,14 +17,14 @@ # specific language governing permissions and limitations # under the License. -require 'selenium/webdriver/chrome/features' +require 'selenium/webdriver/chromium/features' module Selenium module WebDriver module Edge module Features - include WebDriver::Chrome::Features + include WebDriver::Chromium::Features EDGE_COMMANDS = { get_cast_sinks: [:get, 'session/:session_id/ms/cast/get_sinks'], @@ -37,7 +37,7 @@ module Features }.freeze def commands(command) - EDGE_COMMANDS[command] || Chrome::Features::CHROME_COMMANDS[command] || self.class::COMMANDS[command] + EDGE_COMMANDS[command] || CHROMIUM_COMMANDS[command] || self.class::COMMANDS[command] end end # Bridge end # Edge diff --git a/rb/lib/selenium/webdriver/edge/options.rb b/rb/lib/selenium/webdriver/edge/options.rb index b67c9fc2330cd..cd55ff50b6b54 100644 --- a/rb/lib/selenium/webdriver/edge/options.rb +++ b/rb/lib/selenium/webdriver/edge/options.rb @@ -17,23 +17,21 @@ # specific language governing permissions and limitations # under the License. -require 'selenium/webdriver/chrome/options' +require 'selenium/webdriver/chromium/options' module Selenium module WebDriver module Edge - class Options < Selenium::WebDriver::Chrome::Options + class Options < Chromium::Options KEY = 'ms:edgeOptions' BROWSER = 'MicrosoftEdge' - protected + private def enable_logging(browser_options) browser_options['ms:loggingPrefs'] = @logging_prefs end - private - def binary_path Edge.path end diff --git a/rb/lib/selenium/webdriver/edge/profile.rb b/rb/lib/selenium/webdriver/edge/profile.rb index 8d8d5dbe1bc0f..691165eb4feae 100644 --- a/rb/lib/selenium/webdriver/edge/profile.rb +++ b/rb/lib/selenium/webdriver/edge/profile.rb @@ -17,7 +17,7 @@ # specific language governing permissions and limitations # under the License. -require 'selenium/webdriver/chrome/profile' +require 'selenium/webdriver/chromium/profile' module Selenium module WebDriver @@ -26,7 +26,7 @@ module Edge # @private # - class Profile < Selenium::WebDriver::Chrome::Profile + class Profile < Chromium::Profile end # Profile end # Edge end # WebDriver diff --git a/rb/lib/selenium/webdriver/edge/service.rb b/rb/lib/selenium/webdriver/edge/service.rb index 1e95d41bd74e5..0db8cb4d403d6 100644 --- a/rb/lib/selenium/webdriver/edge/service.rb +++ b/rb/lib/selenium/webdriver/edge/service.rb @@ -17,12 +17,12 @@ # specific language governing permissions and limitations # under the License. -require 'selenium/webdriver/chrome/service' +require 'selenium/webdriver/chromium/service' module Selenium module WebDriver module Edge - class Service < Selenium::WebDriver::Chrome::Service + class Service < Chromium::Service DEFAULT_PORT = 9515 EXECUTABLE = 'msedgedriver' MISSING_TEXT = <<~ERROR