diff --git a/rb/lib/selenium/webdriver/chromium/features.rb b/rb/lib/selenium/webdriver/chromium/features.rb index 5333c6e21a16b..2ad519b98d30f 100644 --- a/rb/lib/selenium/webdriver/chromium/features.rb +++ b/rb/lib/selenium/webdriver/chromium/features.rb @@ -31,6 +31,12 @@ module Features get_log: [:post, 'session/:session_id/se/log'] }.freeze + # TODO: remove subclass override once chrome supports base64; currently only supports an unpacked local path + def install_web_extension(path) + result = web_extension.install(extension_data: web_extension.extension_path(path: path)) + WebExtension.new(result.extension) + end + def launch_app(id) execute :launch_app, {}, {id: id} end diff --git a/rb/lib/selenium/webdriver/chromium/options.rb b/rb/lib/selenium/webdriver/chromium/options.rb index 4a2a3b72dca05..4dd6dbb26dbbd 100644 --- a/rb/lib/selenium/webdriver/chromium/options.rb +++ b/rb/lib/selenium/webdriver/chromium/options.rb @@ -236,6 +236,10 @@ def process_browser_options(browser_options) options['args'] << "--user-data-dir=#{@profile.directory}" end + if bidi? + options['args'] = options['args'].to_a | %w[--enable-unsafe-extension-debugging --remote-debugging-pipe] + end + return if (@encoded_extensions + @extensions).empty? options['extensions'] = @encoded_extensions + @extensions.map { |ext| encode_extension(ext) } diff --git a/rb/lib/selenium/webdriver/common.rb b/rb/lib/selenium/webdriver/common.rb index 0412195a74266..609dcc8788bac 100644 --- a/rb/lib/selenium/webdriver/common.rb +++ b/rb/lib/selenium/webdriver/common.rb @@ -94,6 +94,7 @@ require 'selenium/webdriver/common/takes_screenshot' require 'selenium/webdriver/common/driver' require 'selenium/webdriver/common/element' +require 'selenium/webdriver/common/web_extension' require 'selenium/webdriver/common/shadow_root' require 'selenium/webdriver/common/websocket_connection' require 'selenium/webdriver/common/child_process' diff --git a/rb/lib/selenium/webdriver/common/driver.rb b/rb/lib/selenium/webdriver/common/driver.rb index 3a9de28b24805..9b7f608fa8775 100644 --- a/rb/lib/selenium/webdriver/common/driver.rb +++ b/rb/lib/selenium/webdriver/common/driver.rb @@ -277,6 +277,34 @@ def network @network ||= WebDriver::Network.new(bridge) end + # + # Installs a browser extension over WebDriver BiDi (+webExtension.install+). + # + # Firefox sends the extension base64-encoded, so it accepts an unpacked directory, a packed + # extension (.xpi/.crx/.zip), or already-encoded base64 bytes, and works with remote (Grid) + # sessions. Chromium browsers currently accept only an unpacked directory whose path resolves on + # the browser host (local sessions), until chromium-bidi supports base64 (SeleniumHQ/selenium#16541). + # + # @note Requires a BiDi session (set +web_socket_url+ to true in the browser options). + # @param [String] path unpacked extension directory, packed extension file, or base64-encoded bytes + # @return [WebExtension] handle for the installed extension + # + + def install_web_extension(...) + bridge.install_web_extension(...) + end + + # + # Uninstalls a browser extension installed with {#install_web_extension}. + # + # @note Requires a BiDi session (set +web_socket_url+ to true in the browser options). + # @param [WebExtension] extension handle returned by {#install_web_extension} + # + + def uninstall_web_extension(extension) + bridge.uninstall_web_extension(extension.id) + end + #-------------------------------- sugar -------------------------------- # diff --git a/rb/lib/selenium/webdriver/common/driver_extensions/has_addons.rb b/rb/lib/selenium/webdriver/common/driver_extensions/has_addons.rb index dbe6def2672a1..c3fcc04a642a2 100644 --- a/rb/lib/selenium/webdriver/common/driver_extensions/has_addons.rb +++ b/rb/lib/selenium/webdriver/common/driver_extensions/has_addons.rb @@ -30,6 +30,7 @@ module HasAddons # def install_addon(path, temporary = nil) + WebDriver.logger.deprecate('#install_addon', '#install_web_extension', id: :install_addon) @bridge.install_addon(path, temporary) end @@ -40,6 +41,7 @@ def install_addon(path, temporary = nil) # def uninstall_addon(id) + WebDriver.logger.deprecate('#uninstall_addon', '#uninstall_web_extension', id: :uninstall_addon) @bridge.uninstall_addon(id) end end # HasAddons diff --git a/rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb b/rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb index 74fc6e80f6759..6c68bf31b76bc 100644 --- a/rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb +++ b/rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb @@ -25,9 +25,15 @@ module HasDevTools # Retrieves connection to DevTools. # # @return [DevTools] + # @raise [Error::WebDriverError] when BiDi is enabled, as CDP shares a transport with it # def devtools(target_type: 'page') + if @bridge.bidi? + raise Error::WebDriverError, + 'CDP (DevTools) is disabled when BiDi is enabled; use the WebDriver BiDi APIs instead' + end + @devtools ||= {} @devtools[target_type] ||= begin require 'selenium/devtools' diff --git a/rb/lib/selenium/webdriver/common/web_extension.rb b/rb/lib/selenium/webdriver/common/web_extension.rb new file mode 100644 index 0000000000000..ad57c765ff7fd --- /dev/null +++ b/rb/lib/selenium/webdriver/common/web_extension.rb @@ -0,0 +1,42 @@ +# 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 + # + # Handle for a browser extension installed via Driver#install_web_extension. + # Wraps the identifier the browser assigned; pass it to Driver#uninstall_web_extension. + # + class WebExtension + # + # @return [String] identifier assigned to the extension by the browser + # + + attr_reader :id + + # + # @api private + # + + def initialize(id) + @id = id + end + end # WebExtension + end # WebDriver +end # Selenium diff --git a/rb/lib/selenium/webdriver/firefox/features.rb b/rb/lib/selenium/webdriver/firefox/features.rb index e20138ef7aa41..e5ae3a29d4792 100644 --- a/rb/lib/selenium/webdriver/firefox/features.rb +++ b/rb/lib/selenium/webdriver/firefox/features.rb @@ -53,6 +53,21 @@ def uninstall_addon(id) execute :uninstall_addon, {}, {id: id} end + def install_web_extension(path, allow_private_browsing: nil, permanent: nil) + data = encode_extension(path) + return classic_install_web_extension(data, permanent, allow_private_browsing) unless bidi? + + moz = web_extension.moz + options = {allow_private_browsing:, permanent:}.compact + result = moz.install(extension_data: moz.extension_base64_encoded(value: data), **options) + WebDriver::WebExtension.new(result.extension) + end + + def uninstall_web_extension(extension_id) + bidi? ? web_extension.uninstall(extension: extension_id) : uninstall_addon(extension_id) + nil + end + def full_screenshot execute :full_page_screenshot end @@ -64,6 +79,20 @@ def context=(context) def context execute :get_context end + + private + + def classic_install_web_extension(data, permanent, allow_private_browsing) + if allow_private_browsing == false + raise Error::WebDriverError, + 'allow_private_browsing: false requires a BiDi session; the classic install always grants ' \ + 'private-browsing access' + end + + temporary = !permanent unless permanent.nil? + options = {temporary: temporary, allowPrivateBrowsing: allow_private_browsing}.compact + WebDriver::WebExtension.new(execute(:install_addon, {}, {addon: data, **options})) + end end # Bridge end # Firefox end # WebDriver diff --git a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb index 70efdeece164f..70c04a9a6691d 100644 --- a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb +++ b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb @@ -45,6 +45,17 @@ def create_session(capabilities) end end + def install_web_extension(path) + data = web_extension.extension_base64_encoded(value: encode_extension(path)) + result = web_extension.install(extension_data: data) + WebExtension.new(result.extension) + end + + def uninstall_web_extension(id) + web_extension.uninstall(extension: id) + nil + end + def get(url) browsing_context.navigate(context: window_handle, url: url, wait: readiness_state) nil @@ -91,6 +102,10 @@ def browsing_context @browsing_context ||= BiDi::Protocol::BrowsingContext.new(connection) end + def web_extension + @web_extension ||= BiDi::Protocol::WebExtension.new(connection) + end + def readiness_state READINESS_STATE.fetch(capabilities[:page_load_strategy] || 'normal') end diff --git a/rb/lib/selenium/webdriver/remote/bridge.rb b/rb/lib/selenium/webdriver/remote/bridge.rb index f45fde9e7a558..1f17cf0893ce5 100644 --- a/rb/lib/selenium/webdriver/remote/bridge.rb +++ b/rb/lib/selenium/webdriver/remote/bridge.rb @@ -593,14 +593,18 @@ def click_fedcm_dialog_button execute :click_fedcm_dialog_button, {}, {dialogButton: 'ConfirmIdpLoginContinue'} end - def bidi - msg = 'BiDi must be enabled by setting #web_socket_url to true in options class' - raise(WebDriver::Error::WebDriverError, msg) + def bidi(*) + raise WebDriver::Error::WebDriverError, + 'BiDi must be enabled by setting #web_socket_url to true in options class' end + alias connection bidi + alias web_extension bidi + alias install_web_extension bidi + alias uninstall_web_extension bidi + private :web_extension - def connection - msg = 'BiDi must be enabled by setting #web_socket_url to true in options class' - raise(WebDriver::Error::WebDriverError, msg) + def bidi? + !@bidi.nil? end def command_list @@ -609,6 +613,16 @@ def command_list private + def encode_extension(path) + if File.directory?(path) + Zipper.zip(path) + elsif File.file?(path) + File.open(path, 'rb') { |file| Base64.strict_encode64(file.read) } + else + path # already base64-encoded bytes + end + end + # # executes a command on the remote server. # diff --git a/rb/sig/interfaces/bridge.rbs b/rb/sig/interfaces/bridge.rbs index bba953138841c..4c03a41fbb51a 100644 --- a/rb/sig/interfaces/bridge.rbs +++ b/rb/sig/interfaces/bridge.rbs @@ -18,4 +18,10 @@ interface _Bridge def execute: (untyped command, ?Hash[untyped, untyped] opts, ?untyped? command_hash) -> untyped + + def bidi?: () -> bool + + def web_extension: () -> Selenium::WebDriver::BiDi::Protocol::WebExtension + + def encode_extension: (String path) -> String end diff --git a/rb/sig/lib/selenium/webdriver/chromium/features.rbs b/rb/sig/lib/selenium/webdriver/chromium/features.rbs index 1a336f2f36836..edd5bf5e727ae 100644 --- a/rb/sig/lib/selenium/webdriver/chromium/features.rbs +++ b/rb/sig/lib/selenium/webdriver/chromium/features.rbs @@ -26,6 +26,8 @@ module Selenium def commands: (Symbol command) -> Array[Symbol | String] + def install_web_extension: (String path) -> Selenium::WebDriver::WebExtension + def launch_app: (String id) -> String def cast_sinks: () -> Array[String] diff --git a/rb/sig/lib/selenium/webdriver/common/driver.rbs b/rb/sig/lib/selenium/webdriver/common/driver.rbs index f7294f63ed787..202af58ee23d2 100644 --- a/rb/sig/lib/selenium/webdriver/common/driver.rbs +++ b/rb/sig/lib/selenium/webdriver/common/driver.rbs @@ -71,6 +71,10 @@ module Selenium def add_virtual_authenticator: (untyped options) -> VirtualAuthenticator + def install_web_extension: (String path, **untyped options) -> WebExtension + + def uninstall_web_extension: (WebExtension extension) -> void + alias first find_element alias all find_elements diff --git a/rb/sig/lib/selenium/webdriver/common/web_extension.rbs b/rb/sig/lib/selenium/webdriver/common/web_extension.rbs new file mode 100644 index 0000000000000..822eaf995ecfa --- /dev/null +++ b/rb/sig/lib/selenium/webdriver/common/web_extension.rbs @@ -0,0 +1,29 @@ +# 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 + class WebExtension + @id: String + + attr_reader id: String + + def initialize: (String id) -> void + end + end +end diff --git a/rb/sig/lib/selenium/webdriver/firefox/features.rbs b/rb/sig/lib/selenium/webdriver/firefox/features.rbs index 50f679d1ebb14..3358034116075 100644 --- a/rb/sig/lib/selenium/webdriver/firefox/features.rbs +++ b/rb/sig/lib/selenium/webdriver/firefox/features.rbs @@ -32,11 +32,19 @@ module Selenium def uninstall_addon: (untyped id) -> untyped + def install_web_extension: (String path, ?allow_private_browsing: bool?, ?permanent: bool?) -> Selenium::WebDriver::WebExtension + + def uninstall_web_extension: (String extension_id) -> void + def full_screenshot: () -> untyped def context=: (untyped context) -> untyped def context: () -> untyped + + private + + def classic_install_web_extension: (String data, bool? permanent, bool? allow_private_browsing) -> Selenium::WebDriver::WebExtension end end end diff --git a/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs b/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs index 6c7ec99d4c1b0..0b5a52e229fc2 100644 --- a/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs +++ b/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs @@ -26,12 +26,18 @@ module Selenium @connection: untyped + @web_extension: BiDi::Protocol::WebExtension + attr_reader bidi: BiDi attr_reader connection: untyped def create_session: (untyped capabilities) -> void + def install_web_extension: (String path) -> WebExtension + + def uninstall_web_extension: (String id) -> void + def get: (String url) -> void def go_back: () -> void @@ -50,6 +56,8 @@ module Selenium def browsing_context: () -> BiDi::Protocol::BrowsingContext + def web_extension: () -> BiDi::Protocol::WebExtension + def readiness_state: () -> Symbol end end diff --git a/rb/sig/lib/selenium/webdriver/remote/bridge.rbs b/rb/sig/lib/selenium/webdriver/remote/bridge.rbs index 7c6f83190f8fa..a145336e83043 100644 --- a/rb/sig/lib/selenium/webdriver/remote/bridge.rbs +++ b/rb/sig/lib/selenium/webdriver/remote/bridge.rbs @@ -52,8 +52,14 @@ module Selenium def bidi: -> BiDi + def bidi?: () -> bool + def connection: -> untyped + def install_web_extension: (String path) -> WebExtension + + def uninstall_web_extension: (String id) -> void + def cancel_fedcm_dialog: -> nil def click_fedcm_dialog_button: -> nil @@ -252,6 +258,10 @@ module Selenium private + def web_extension: () -> WebDriver::BiDi::Protocol::WebExtension + + def encode_extension: (String path) -> String + def execute: (untyped command, ?::Hash[untyped, untyped] opts, ?untyped? command_hash) -> String def escaper: () -> untyped diff --git a/rb/spec/integration/selenium/webdriver/BUILD.bazel b/rb/spec/integration/selenium/webdriver/BUILD.bazel index a06ee3e9e4cdc..6db4cdbcc2c18 100644 --- a/rb/spec/integration/selenium/webdriver/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/BUILD.bazel @@ -43,6 +43,7 @@ _OS_SENSITIVE = [ # specs whose classes have both bidi implementations _BIDI_IMPLEMENTATIONS = [ + "driver_spec.rb", "navigation_spec.rb", ] @@ -124,6 +125,7 @@ _NO_GRID = ["driver_finder_spec.rb"] rb_integration_test( name = f[:-8], srcs = [f], + bidi = True, tags = ["exclusive-if-local"], deps = [ "//rb/lib/selenium/devtools", diff --git a/rb/spec/integration/selenium/webdriver/devtools_spec.rb b/rb/spec/integration/selenium/webdriver/devtools_spec.rb index dee5b5b1833ce..4bc12c2de35b0 100644 --- a/rb/spec/integration/selenium/webdriver/devtools_spec.rb +++ b/rb/spec/integration/selenium/webdriver/devtools_spec.rb @@ -249,5 +249,13 @@ module WebDriver end end end + + describe DevTools, skip_unless: {browser_family: :chromium} do + context 'when BiDi is enabled', skip_unless: {bidi: true, reason: 'CDP shares a transport with BiDi'} do + it 'disables the CDP API' do + expect { driver.devtools }.to raise_error(Error::WebDriverError, /BiDi/) + end + end + end end end diff --git a/rb/spec/integration/selenium/webdriver/driver_spec.rb b/rb/spec/integration/selenium/webdriver/driver_spec.rb index 5e2562d708c7e..763bd70416146 100644 --- a/rb/spec/integration/selenium/webdriver/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/driver_spec.rb @@ -361,5 +361,100 @@ module WebDriver end end end + + describe Driver do + context 'when BiDi is enabled', + skip_unless: {bidi: true, reason: 'extensions install over the webExtension BiDi command'} do + let(:extensions) { '../../../../../common/extensions/' } + + after { |example| reset_driver!(example: example) } + + describe '#install_web_extension' do + context 'with an unpacked directory' do + it 'installs and removes the extension on any browser', + pending_if: {browser_family: :chromium, driver: :remote, + reason: 'path must resolve on the browser host (SeleniumHQ/selenium#16541)'} do + ext = File.expand_path("#{extensions}/webextensions-selenium-example-signed", __dir__) + extension = driver.install_web_extension(ext) + expect(extension.id).not_to be_empty + + driver.navigate.to url_for('blank.html') + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + + driver.uninstall_web_extension(extension) + driver.navigate.refresh + expect(driver.find_elements(id: 'webextensions-selenium-example')).to be_empty + end + end + + context 'with a packed archive', + pending_if: {browser_family: :chromium, + reason: 'chromium-bidi installs only unpacked directories ' \ + '(SeleniumHQ/selenium#16541)'} do + it 'installs and removes an xpi file' do + ext = File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) + extension = driver.install_web_extension(ext) + expect(extension.id).to eq 'webextensions-selenium-example-v3@example.com' + + driver.navigate.to url_for('blank.html') + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + + driver.uninstall_web_extension(extension) + end + end + + context 'when the browser is Firefox', skip_unless: {browser: :firefox} do + it 'installs an unsigned directory with permanent: false' do + ext = File.expand_path("#{extensions}/webextensions-selenium-example", __dir__) + extension = driver.install_web_extension(ext, permanent: false) + expect(extension.id).to eq 'webextensions-selenium-example-v3@example.com' + + driver.navigate.to url_for('blank.html') + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + + driver.uninstall_web_extension(extension) + end + + it 'installs from base64-encoded bytes' do + xpi = File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) + extension = driver.install_web_extension(Base64.strict_encode64(File.binread(xpi))) + expect(extension.id).to eq 'webextensions-selenium-example-v3@example.com' + + driver.navigate.to url_for('blank.html') + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + + driver.uninstall_web_extension(extension) + end + + context 'with allow_private_browsing enabled' do + let(:ext) { File.expand_path("#{extensions}/webextensions-selenium-example-signed", __dir__) } + + it 'runs in a private window when allowed' do + reset_driver!(prefs: {'browser.privatebrowsing.autostart': true}) do |driver| + driver.install_web_extension(ext, allow_private_browsing: true) + driver.navigate.to url_for('blank.html') + + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + end + end + + it 'does not run in a private window by default' do + reset_driver!(prefs: {'browser.privatebrowsing.autostart': true}) do |driver| + driver.install_web_extension(ext) + driver.navigate.to url_for('blank.html') + + expect(driver.find_elements(id: 'webextensions-selenium-example')).to be_empty + end + end + end + end + end + end + end end # WebDriver end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb b/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb index a92f81ebd6806..6e0e2ecdc283b 100644 --- a/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb @@ -22,48 +22,66 @@ module Selenium module WebDriver module Firefox - describe Driver, skip_unless: [{bidi: false, reason: 'Not yet implemented with BiDi'}, {browser: :firefox}] do + describe Driver, skip_unless: {browser: :firefox} do let(:extensions) { '../../../../../../common/extensions/' } - describe '#print_options' do - let(:magic_number) { 'JVBER' } + context 'when BiDi is not enabled', skip_unless: {bidi: false, reason: 'Not yet implemented with BiDi'} do + describe '#print_options' do + let(:magic_number) { 'JVBER' } - before { driver.navigate.to url_for('printPage.html') } + before { driver.navigate.to url_for('printPage.html') } - it 'returns base64 for print command' do - expect(driver.print_page).to include(magic_number) - end + it 'returns base64 for print command' do + expect(driver.print_page).to include(magic_number) + end - it 'prints with orientation' do - expect(driver.print_page(orientation: 'landscape')).to include(magic_number) - end + it 'prints with orientation' do + expect(driver.print_page(orientation: 'landscape')).to include(magic_number) + end + + it 'prints with valid params' do + expect(driver.print_page(orientation: 'landscape', + page_ranges: ['1-2'], + page: {width: 30})).to include(magic_number) + end - it 'prints with valid params' do - expect(driver.print_page(orientation: 'landscape', - page_ranges: ['1-2'], - page: {width: 30})).to include(magic_number) + it 'prints full page', pending_if: [{platform: :macosx, + reason: 'showing half resolution of what expected'}] do + viewport_width = driver.execute_script('return window.innerWidth;') + viewport_height = driver.execute_script('return window.innerHeight;') + + path = "#{Dir.tmpdir}/test#{SecureRandom.urlsafe_base64}.png" + screenshot = driver.save_full_page_screenshot(path) + width, height = png_size(screenshot) + + expect(width).to be >= viewport_width + expect(height).to be > viewport_height + ensure + FileUtils.rm_rf(path) + end end - it 'prints full page', pending_if: [{platform: :macosx, - reason: 'showing half resolution of what expected'}] do - viewport_width = driver.execute_script('return window.innerWidth;') - viewport_height = driver.execute_script('return window.innerHeight;') + it 'can get and set context', + skip_if: {driver: :remote, reason: 'system access cannot be granted per-session on Grid'} do + service = WebDriver::Service.firefox(args: ['--allow-system-access']) + reset_driver!(service: service, prefs: {'browser.download.dir': 'foo/bar'}) do |driver| + expect(driver.context).to eq 'content' - path = "#{Dir.tmpdir}/test#{SecureRandom.urlsafe_base64}.png" - screenshot = driver.save_full_page_screenshot(path) - width, height = png_size(screenshot) + driver.context = 'chrome' + expect(driver.context).to eq 'chrome' - expect(width).to be >= viewport_width - expect(height).to be > viewport_height - ensure - FileUtils.rm_rf(path) + # This call can not be made when context is set to 'content' + dir = driver.execute_script("return Services.prefs.getStringPref('browser.download.dir')") + expect(dir).to eq 'foo/bar' + end end end describe '#install_addon' do it 'install and uninstall xpi file' do ext = File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) - id = driver.install_addon(ext) + id = nil + expect { id = driver.install_addon(ext) }.to have_deprecated(:install_addon) expect(id).to eq 'webextensions-selenium-example-v3@example.com' driver.navigate.to url_for('blank.html') @@ -140,18 +158,50 @@ module Firefox end end - it 'can get and set context', - skip_if: {driver: :remote, reason: 'system access cannot be granted per-session on Grid'} do - service = WebDriver::Service.firefox(args: ['--allow-system-access']) - reset_driver!(service: service, prefs: {'browser.download.dir': 'foo/bar'}) do |driver| - expect(driver.context).to eq 'content' + describe '#install_web_extension' do + it 'installs and uninstalls without BiDi enabled' do + ext = File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) + extension = driver.install_web_extension(ext) + expect(extension.id).to eq 'webextensions-selenium-example-v3@example.com' + + driver.navigate.to url_for('blank.html') + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + + driver.uninstall_web_extension(extension) + driver.navigate.refresh + expect(driver.find_elements(id: 'webextensions-selenium-example')).to be_empty + end + + it 'installs an unsigned directory with permanent: false' do + ext = File.expand_path("#{extensions}/webextensions-selenium-example", __dir__) + extension = driver.install_web_extension(ext, permanent: false) + expect(extension.id).to eq 'webextensions-selenium-example-v3@example.com' + + driver.navigate.to url_for('blank.html') + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + + driver.uninstall_web_extension(extension) + end + + context 'with private browsing' do + let(:ext) { File.expand_path("#{extensions}/webextensions-selenium-example-signed", __dir__) } + + it 'runs in a private window when allowed' do + reset_driver!(prefs: {'browser.privatebrowsing.autostart': true}) do |driver| + driver.install_web_extension(ext, allow_private_browsing: true) + driver.navigate.to url_for('blank.html') - driver.context = 'chrome' - expect(driver.context).to eq 'chrome' + injected = driver.find_element(id: 'webextensions-selenium-example') + expect(injected.text).to eq 'Content injected by webextensions-selenium-example' + end + end - # This call can not be made when context is set to 'content' - dir = driver.execute_script("return Services.prefs.getStringPref('browser.download.dir')") - expect(dir).to eq 'foo/bar' + it 'raises when private browsing is explicitly disabled without BiDi' do + expect { driver.install_web_extension(ext, allow_private_browsing: false) } + .to raise_error(Error::WebDriverError, /BiDi/) + end end end end diff --git a/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb b/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb index 54496aa1a0d15..eb70a331f6115 100644 --- a/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb +++ b/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb @@ -283,6 +283,24 @@ module Chrome expect(options.as_json).to eq('browserName' => 'chrome', 'goog:chromeOptions' => {}) end + it 'adds the web extension debugging arguments when BiDi is enabled' do + bidi_options = described_class.new(web_socket_url: true) + + args = bidi_options.as_json['goog:chromeOptions']['args'] + expect(args).to include('--enable-unsafe-extension-debugging', '--remote-debugging-pipe') + end + + it 'does not add the web extension debugging arguments without BiDi' do + expect(options.as_json['goog:chromeOptions']).not_to have_key('args') + end + + it 'does not duplicate web extension debugging arguments already present' do + bidi_options = described_class.new(web_socket_url: true, args: ['--remote-debugging-pipe']) + + args = bidi_options.as_json['goog:chromeOptions']['args'] + expect(args.count('--remote-debugging-pipe')).to eq(1) + end + it 'errors when unrecognized capability is passed' do options.add_option(:foo, 'bar') diff --git a/rb/spec/unit/selenium/webdriver/common/web_extension_spec.rb b/rb/spec/unit/selenium/webdriver/common/web_extension_spec.rb new file mode 100644 index 0000000000000..2b291ffd45fa4 --- /dev/null +++ b/rb/spec/unit/selenium/webdriver/common/web_extension_spec.rb @@ -0,0 +1,32 @@ +# 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 File.expand_path('../spec_helper', __dir__) + +module Selenium + module WebDriver + describe WebExtension do + let(:extension) { described_class.new('installed-extension-id') } + + it 'exposes the identifier assigned by the browser' do + expect(extension.id).to eq 'installed-extension-id' + end + end + end +end diff --git a/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb b/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb index 3dc58f2984e39..33c9a9b750fea 100644 --- a/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb +++ b/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb @@ -134,6 +134,36 @@ module Remote end end + describe '#install_web_extension' do + context 'when BiDi is not enabled' do + it 'raises a helpful error telling the user to enable BiDi' do + expect { bridge.install_web_extension('/tmp/ext') } + .to raise_error(Error::WebDriverError, /must be enabled/) + end + + it 'raises for a Chromium session, which has no classic install path' do + bridge.extend(WebDriver::Chrome::Features) + expect { bridge.install_web_extension('/tmp/ext') } + .to raise_error(Error::WebDriverError, /must be enabled/) + end + + it 'raises for Firefox when private browsing is explicitly disabled' do + bridge.extend(WebDriver::Firefox::Features) + expect { bridge.install_web_extension('/tmp/ext', allow_private_browsing: false) } + .to raise_error(Error::WebDriverError, /BiDi/) + end + end + end + + describe '#uninstall_web_extension' do + context 'when BiDi is not enabled' do + it 'raises a helpful error telling the user to enable BiDi' do + expect { bridge.uninstall_web_extension('an-id') } + .to raise_error(Error::WebDriverError, /must be enabled/) + end + end + end + describe '#quit' do it 'respects quit_errors' do allow(bridge).to receive(:execute).with(:delete_session).and_raise(IOError)