From dd47fb2f74c521dcb18890ba7b8da26640628f22 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Mon, 24 Aug 2026 12:59:53 -0500 Subject: [PATCH 1/7] [rb] add cross-browser Driver#install_web_extension, deprecating the Firefox classic install_addon methods --- rb/lib/selenium/webdriver/common.rb | 1 + rb/lib/selenium/webdriver/common/driver.rb | 24 ++++ .../common/driver_extensions/has_addons.rb | 2 + .../webdriver/common/web_extension.rb | 42 ++++++ rb/lib/selenium/webdriver/firefox/features.rb | 25 +++- .../selenium/webdriver/remote/bidi_bridge.rb | 21 +++ rb/lib/selenium/webdriver/remote/bridge.rb | 26 +++- rb/sig/interfaces/bridge.rbs | 8 ++ .../lib/selenium/webdriver/common/driver.rbs | 4 + .../webdriver/common/web_extension.rbs | 29 ++++ .../selenium/webdriver/firefox/features.rbs | 4 + .../selenium/webdriver/remote/bidi_bridge.rbs | 10 ++ .../lib/selenium/webdriver/remote/bridge.rbs | 10 ++ .../selenium/webdriver/BUILD.bazel | 2 + .../selenium/webdriver/driver_spec.rb | 111 ++++++++++++++++ .../selenium/webdriver/firefox/driver_spec.rb | 125 +++++++++++++----- .../selenium/webdriver/chrome/options_spec.rb | 5 + .../webdriver/common/web_extension_spec.rb | 32 +++++ .../selenium/webdriver/remote/bridge_spec.rb | 24 ++++ 19 files changed, 456 insertions(+), 49 deletions(-) create mode 100644 rb/lib/selenium/webdriver/common/web_extension.rb create mode 100644 rb/sig/lib/selenium/webdriver/common/web_extension.rbs create mode 100644 rb/spec/unit/selenium/webdriver/common/web_extension_spec.rb 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..210a04b20df4b 100644 --- a/rb/lib/selenium/webdriver/common/driver.rb +++ b/rb/lib/selenium/webdriver/common/driver.rb @@ -277,6 +277,30 @@ def network @network ||= WebDriver::Network.new(bridge) end + # + # Installs a browser extension from an unpacked directory, a packed extension (.xpi/.crx/.zip), + # or base64-encoded bytes. + # + # @note Chromium requires a BiDi session and installs only unpacked directories + # (SeleniumHQ/selenium#16541); Firefox falls back to the classic endpoint without BiDi. + # @param [String] path directory, packed extension, or base64-encoded bytes + # @return [WebExtension] the installed extension + # + + def install_web_extension(...) + bridge.install_web_extension(...) + end + + # + # Uninstalls a browser extension installed with {#install_web_extension}. + # + # @param [WebExtension] extension the extension 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/web_extension.rb b/rb/lib/selenium/webdriver/common/web_extension.rb new file mode 100644 index 0000000000000..c7dca09349936 --- /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 + # + # 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..4a655e222e3ad 100644 --- a/rb/lib/selenium/webdriver/firefox/features.rb +++ b/rb/lib/selenium/webdriver/firefox/features.rb @@ -38,13 +38,7 @@ def commands(command) end def install_addon(path, temporary) - addon = if File.directory?(path) - Zipper.zip(path) - else - File.open(path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read } - end - - payload = {addon: addon} + payload = {addon: encode_extension(path)} payload[:temporary] = temporary unless temporary.nil? execute :install_addon, {}, payload end @@ -53,6 +47,23 @@ def uninstall_addon(id) execute :uninstall_addon, {}, {id: id} end + def install_web_extension(path, allow_private_browsing: nil, permanent: nil) + unless bidi? + temporary = !permanent unless permanent.nil? + options = {temporary: temporary, allowPrivateBrowsing: allow_private_browsing}.compact + return WebDriver::WebExtension.new(execute(:install_addon, {}, {addon: encode_extension(path), **options})) + end + + options = {allow_private_browsing:, permanent:}.compact + result = web_extension.moz.install(extension_data: web_extension_data(path), **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 diff --git a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb index 70efdeece164f..f979f036be8e5 100644 --- a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb +++ b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb @@ -45,6 +45,16 @@ def create_session(capabilities) end end + def install_web_extension(path) + result = web_extension.install(extension_data: web_extension_data(path)) + 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 +101,17 @@ def browsing_context @browsing_context ||= BiDi::Protocol::BrowsingContext.new(connection) end + def web_extension + @web_extension ||= BiDi::Protocol::WebExtension.new(connection) + end + + # A directory is referenced by its path; archives and base64 bytes travel inline. + def web_extension_data(path) + return web_extension.extension_base64_encoded(value: encode_extension(path)) unless File.directory?(path) + + web_extension.extension_path(path: path) + 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..0a4092474e3fe 100644 --- a/rb/sig/interfaces/bridge.rbs +++ b/rb/sig/interfaces/bridge.rbs @@ -18,4 +18,12 @@ 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 web_extension_data: (String path) -> untyped + + def encode_extension: (String path) -> String end 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..7c407c9a78078 100644 --- a/rb/sig/lib/selenium/webdriver/firefox/features.rbs +++ b/rb/sig/lib/selenium/webdriver/firefox/features.rbs @@ -32,6 +32,10 @@ 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 diff --git a/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs b/rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs index 6c7ec99d4c1b0..624e5045b10e9 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,10 @@ module Selenium def browsing_context: () -> BiDi::Protocol::BrowsingContext + def web_extension: () -> BiDi::Protocol::WebExtension + + def web_extension_data: (String path) -> untyped + 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/driver_spec.rb b/rb/spec/integration/selenium/webdriver/driver_spec.rb index 5e2562d708c7e..b9f7e1f0c837a 100644 --- a/rb/spec/integration/selenium/webdriver/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/driver_spec.rb @@ -361,5 +361,116 @@ 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: {driver: :remote, + reason: 'directory path must resolve on the browser host; Grid support added separately'} 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' do + let(:archive) { File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) } + + it 'installs and removes an xpi file', skip_unless: {browser: :firefox} do + extension = driver.install_web_extension(archive) + 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 'raises on Chromium, which installs only unpacked directories (SeleniumHQ/selenium#16541)', + skip_unless: {browser_family: :chromium} do + expect { driver.install_web_extension(archive) } + .to raise_error(Error::UnsupportedOperationError, /not supported/) + end + end + + context 'with base64-encoded bytes' do + let(:encoded) do + xpi = File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) + Base64.strict_encode64(File.binread(xpi)) + end + + it 'installs and removes on Firefox', skip_unless: {browser: :firefox} do + extension = driver.install_web_extension(encoded) + 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 'raises on Chromium, which installs only unpacked directories (SeleniumHQ/selenium#16541)', + skip_unless: {browser_family: :chromium} do + expect { driver.install_web_extension(encoded) } + .to raise_error(Error::UnsupportedOperationError, /not supported/) + 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 + + 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..cd7dfebb77ecd 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,53 @@ 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.context = 'chrome' - expect(driver.context).to eq 'chrome' + 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 - # 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' + 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') + + 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 when disabled' do + reset_driver!(prefs: {'browser.privatebrowsing.autostart': true}) do |driver| + driver.install_web_extension(ext, allow_private_browsing: false) + driver.navigate.to url_for('blank.html') + expect(driver.find_elements(id: 'webextensions-selenium-example')).to be_empty + end + 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..81a152cb2c49b 100644 --- a/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb +++ b/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb @@ -283,6 +283,11 @@ module Chrome expect(options.as_json).to eq('browserName' => 'chrome', 'goog:chromeOptions' => {}) end + it 'does not inject debugging arguments when BiDi is enabled' do + bidi_options = described_class.new(web_socket_url: true) + expect(bidi_options.as_json['goog:chromeOptions']).not_to have_key('args') + 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..f90b17bd7089c 100644 --- a/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb +++ b/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb @@ -134,6 +134,30 @@ 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 + 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) From ac20228c0cd156250fe8bc5364e06332aa32e9d5 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Mon, 24 Aug 2026 12:59:53 -0500 Subject: [PATCH 2/7] [rb] install web extensions on a Grid node by uploading the unpacked directory via se/file --- rb/lib/selenium/webdriver/common/driver.rb | 2 +- rb/lib/selenium/webdriver/common/zipper.rb | 23 ++++++------- .../selenium/webdriver/remote/bidi_bridge.rb | 4 ++- rb/lib/selenium/webdriver/remote/features.rb | 17 +++++----- .../lib/selenium/webdriver/common/zipper.rbs | 4 ++- .../selenium/webdriver/driver_spec.rb | 4 +-- .../selenium/webdriver/remote/bridge_spec.rb | 15 +++++---- .../unit/selenium/webdriver/zipper_spec.rb | 33 ++++++++++++++----- 8 files changed, 62 insertions(+), 40 deletions(-) diff --git a/rb/lib/selenium/webdriver/common/driver.rb b/rb/lib/selenium/webdriver/common/driver.rb index 210a04b20df4b..21ed1a240f7d1 100644 --- a/rb/lib/selenium/webdriver/common/driver.rb +++ b/rb/lib/selenium/webdriver/common/driver.rb @@ -279,7 +279,7 @@ def network # # Installs a browser extension from an unpacked directory, a packed extension (.xpi/.crx/.zip), - # or base64-encoded bytes. + # or base64-encoded bytes; works with remote (Grid) sessions. # # @note Chromium requires a BiDi session and installs only unpacked directories # (SeleniumHQ/selenium#16541); Firefox falls back to the classic endpoint without BiDi. diff --git a/rb/lib/selenium/webdriver/common/zipper.rb b/rb/lib/selenium/webdriver/common/zipper.rb index 2197068effcf0..6dcbe6962470f 100644 --- a/rb/lib/selenium/webdriver/common/zipper.rb +++ b/rb/lib/selenium/webdriver/common/zipper.rb @@ -56,27 +56,28 @@ def unzip(path) end def zip(path) - with_tmp_zip do |zip| - ::Find.find(path) do |file| - add_zip_entry zip, file, file.sub("#{path}/", '') unless File.directory?(file) - end + encode_zip(path, path) + end - zip.commit - File.open(zip.name, 'rb') { |io| Base64.strict_encode64 io.read } - end + # Keeps +path+ (a file or directory) as the archive's single top-level entry, unlike #zip + # which flattens a directory's contents; the Grid upload endpoint returns that one entry's path. + def zip_root(path) + encode_zip(path, File.dirname(path)) end - def zip_file(path) + private + + def encode_zip(path, base) with_tmp_zip do |zip| - add_zip_entry zip, path, File.basename(path) + ::Find.find(path) do |file| + add_zip_entry zip, file, file.sub("#{base}/", '') unless File.directory?(file) + end zip.commit File.open(zip.name, 'rb') { |io| Base64.strict_encode64 io.read } end end - private - def with_tmp_zip(&blk) # Don't use Tempfile since it lacks rb_file_s_rename permission on Windows. Dir.mktmpdir do |tmp_dir| diff --git a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb index f979f036be8e5..339213af40d1b 100644 --- a/rb/lib/selenium/webdriver/remote/bidi_bridge.rb +++ b/rb/lib/selenium/webdriver/remote/bidi_bridge.rb @@ -105,10 +105,12 @@ def web_extension @web_extension ||= BiDi::Protocol::WebExtension.new(connection) end - # A directory is referenced by its path; archives and base64 bytes travel inline. + # A directory only resolves on the machine running the browser, so upload it to the remote + # end and reference the returned path; archives and base64 bytes travel inline. def web_extension_data(path) return web_extension.extension_base64_encoded(value: encode_extension(path)) unless File.directory?(path) + path = upload(path) if respond_to?(:upload) web_extension.extension_path(path: path) end diff --git a/rb/lib/selenium/webdriver/remote/features.rb b/rb/lib/selenium/webdriver/remote/features.rb index 8951f4a239365..e3af68575a55f 100644 --- a/rb/lib/selenium/webdriver/remote/features.rb +++ b/rb/lib/selenium/webdriver/remote/features.rb @@ -42,20 +42,21 @@ def commands(command) end def upload(local_file) - unless File.file?(local_file) - WebDriver.logger.error("File detector only works with files. #{local_file.inspect} isn`t a file!", - id: :file_detector) - raise Error::WebDriverError, "You are trying to upload something that isn't a file." - end - - execute :upload_file, {}, {file: Zipper.zip_file(local_file)} + execute :upload_file, {}, {file: Zipper.zip_root(local_file)} end def upload_if_necessary(keys) local_files = keys.first&.split("\n")&.filter_map { |key| @file_detector.call(Array(key)) } return keys unless local_files&.any? - keys = local_files.map { |local_file| upload(local_file) } + keys = local_files.map do |local_file| + unless File.file?(local_file) + WebDriver.logger.error("File detector only works with files. #{local_file.inspect} isn`t a file!", + id: :file_detector) + raise Error::WebDriverError, "You are trying to upload something that isn't a file." + end + upload(local_file) + end Array(keys.join("\n")) end diff --git a/rb/sig/lib/selenium/webdriver/common/zipper.rbs b/rb/sig/lib/selenium/webdriver/common/zipper.rbs index d01306ecd81d5..629bfef259eb6 100644 --- a/rb/sig/lib/selenium/webdriver/common/zipper.rbs +++ b/rb/sig/lib/selenium/webdriver/common/zipper.rbs @@ -27,10 +27,12 @@ module Selenium def self.zip: (untyped path) -> untyped - def self.zip_file: (untyped path) -> untyped + def self.zip_root: (untyped path) -> untyped private + def self.encode_zip: (untyped path, untyped base) -> untyped + def self.with_tmp_zip: () { () -> untyped } -> untyped def self.add_zip_entry: (untyped zip, untyped file, untyped entry_name) -> untyped diff --git a/rb/spec/integration/selenium/webdriver/driver_spec.rb b/rb/spec/integration/selenium/webdriver/driver_spec.rb index b9f7e1f0c837a..7db346bc67ce1 100644 --- a/rb/spec/integration/selenium/webdriver/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/driver_spec.rb @@ -371,9 +371,7 @@ module WebDriver describe '#install_web_extension' do context 'with an unpacked directory' do - it 'installs and removes the extension on any browser', - pending_if: {driver: :remote, - reason: 'directory path must resolve on the browser host; Grid support added separately'} do + it 'installs and removes the extension on any browser' do ext = File.expand_path("#{extensions}/webextensions-selenium-example-signed", __dir__) extension = driver.install_web_extension(ext) expect(extension.id).not_to be_empty diff --git a/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb b/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb index f90b17bd7089c..c29b9740d738f 100644 --- a/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb +++ b/rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb @@ -125,12 +125,15 @@ module Remote end end - describe '#upload' do - it 'raises WebDriverError if uploading non-files' do - expect { - bridge.extend(WebDriver::Remote::Features) - bridge.upload('NotAFile') - }.to raise_error(Error::WebDriverError) + describe '#upload_if_necessary' do + before do + bridge.extend(WebDriver::Remote::Features) + bridge.file_detector = ->((file)) { file } + end + + it 'raises WebDriverError when the detected path is not a file' do + expect { bridge.upload_if_necessary(['NotAFile']) } + .to raise_error(Error::WebDriverError, /isn't a file/) end end diff --git a/rb/spec/unit/selenium/webdriver/zipper_spec.rb b/rb/spec/unit/selenium/webdriver/zipper_spec.rb index 5623886ddbeb6..536f04bc1de54 100644 --- a/rb/spec/unit/selenium/webdriver/zipper_spec.rb +++ b/rb/spec/unit/selenium/webdriver/zipper_spec.rb @@ -38,14 +38,6 @@ def create_file after { FileUtils.rm_rf tmp_dir } describe '#zip' do - it 'a file' do - File.open(zip_file, 'wb') do |io| - io << Base64.decode64(described_class.zip_file(create_file)) - end - - expect(File).to exist(zip_file) - end - it 'a folder' do create_file @@ -69,10 +61,33 @@ def create_file end end + describe '#zip_root' do + it 'wraps a file as a single top-level entry' do + File.open(zip_file, 'wb') do |io| + io << Base64.decode64(described_class.zip_root(create_file)) + end + + unzipped = described_class.unzip(zip_file) + expect(Dir.children(unzipped)).to eq([base_file_name]) + end + + it 'wraps a directory as a single top-level folder named for it' do + create_file + + File.open(zip_file, 'wb') do |io| + io << Base64.decode64(described_class.zip_root(dir_to_zip)) + end + + unzipped = described_class.unzip(zip_file) + expect(Dir.children(unzipped)).to eq([File.basename(dir_to_zip)]) + expect(File.read(File.join(unzipped, File.basename(dir_to_zip), base_file_name))).to eq(file_content) + end + end + describe '#unzip' do it 'a file' do File.open(zip_file, 'wb') do |io| - io << Base64.decode64(described_class.zip_file(create_file)) + io << Base64.decode64(described_class.zip_root(create_file)) end unzipped = described_class.unzip(zip_file) From 9e869f942413126169dea904a59aaee559e01a70 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Mon, 24 Aug 2026 12:59:53 -0500 Subject: [PATCH 3/7] [rb] cover web extension install over BiDi on a Grid, asserting Chromium's unsupported modes --- .../selenium/webdriver/BUILD.bazel | 21 ++++++++++-- .../selenium/webdriver/driver_spec.rb | 33 +++++++------------ rb/spec/tests.bzl | 32 +++++++++++++++++- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/BUILD.bazel b/rb/spec/integration/selenium/webdriver/BUILD.bazel index 6db4cdbcc2c18..74cd645dd6c6f 100644 --- a/rb/spec/integration/selenium/webdriver/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/BUILD.bazel @@ -43,10 +43,14 @@ _OS_SENSITIVE = [ # specs whose classes have both bidi implementations _BIDI_IMPLEMENTATIONS = [ - "driver_spec.rb", "navigation_spec.rb", ] +# bidi implementations that must also run over a Grid (e.g. se/file upload of an unpacked extension) +_GRID_BIDI = [ + "driver_spec.rb", +] + # tests that require bidi enabled _BIDI_ONLY = [ "bidi_spec.rb", @@ -67,7 +71,7 @@ _NO_GRID = ["driver_finder_spec.rb"] ) for file in glob( ["*_spec.rb"], - exclude = _OS_SENSITIVE + _BIDI_IMPLEMENTATIONS + _BIDI_ONLY + _DEVTOOLS + _NO_GRID, + exclude = _OS_SENSITIVE + _BIDI_IMPLEMENTATIONS + _GRID_BIDI + _BIDI_ONLY + _DEVTOOLS + _NO_GRID, ) ] @@ -108,6 +112,19 @@ _NO_GRID = ["driver_finder_spec.rb"] for f in _BIDI_IMPLEMENTATIONS ] +[ + rb_integration_test( + name = f[:-8], + srcs = [f], + bidi = True, + grid_bidi = True, + deps = [ + "//rb/lib/selenium/webdriver:bidi", + ], + ) + for f in _GRID_BIDI +] + [ rb_integration_test( name = f[:-8], diff --git a/rb/spec/integration/selenium/webdriver/driver_spec.rb b/rb/spec/integration/selenium/webdriver/driver_spec.rb index 7db346bc67ce1..2e9c92e295f19 100644 --- a/rb/spec/integration/selenium/webdriver/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/driver_spec.rb @@ -387,10 +387,12 @@ module WebDriver end context 'with a packed archive' do - let(:archive) { File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) } - - it 'installs and removes an xpi file', skip_unless: {browser: :firefox} do - extension = driver.install_web_extension(archive) + it 'installs and removes an xpi file', + pending_if: {browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} 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') @@ -399,22 +401,15 @@ module WebDriver driver.uninstall_web_extension(extension) end - - it 'raises on Chromium, which installs only unpacked directories (SeleniumHQ/selenium#16541)', - skip_unless: {browser_family: :chromium} do - expect { driver.install_web_extension(archive) } - .to raise_error(Error::UnsupportedOperationError, /not supported/) - end end context 'with base64-encoded bytes' do - let(:encoded) do + it 'installs and removes base64-encoded bytes', + pending_if: {browser_family: :chromium, + exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do xpi = File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) - Base64.strict_encode64(File.binread(xpi)) - end - - it 'installs and removes on Firefox', skip_unless: {browser: :firefox} do - extension = driver.install_web_extension(encoded) + 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') @@ -423,12 +418,6 @@ module WebDriver driver.uninstall_web_extension(extension) end - - it 'raises on Chromium, which installs only unpacked directories (SeleniumHQ/selenium#16541)', - skip_unless: {browser_family: :chromium} do - expect { driver.install_web_extension(encoded) } - .to raise_error(Error::UnsupportedOperationError, /not supported/) - end end context 'when the browser is Firefox', skip_unless: {browser: :firefox} do diff --git a/rb/spec/tests.bzl b/rb/spec/tests.bzl index 33c9c69991aef..ab92f758da794 100644 --- a/rb/spec/tests.bzl +++ b/rb/spec/tests.bzl @@ -189,7 +189,8 @@ def rb_integration_test( tags = [], bidi = False, classic = True, - grid = True): + grid = True, + grid_bidi = False): for browser in browsers: generate_classic = BROWSERS[browser].get("classic", True) generate_bidi = BROWSERS[browser].get("bidi", False) @@ -259,3 +260,32 @@ def rb_integration_test( visibility = ["//rb:__subpackages__"], target_compatible_with = BROWSERS[browser]["target_compatible_with"], ) + + # Bidi over a Grid, for specs that must exercise remote-end behavior (e.g. se/file uploads). + if grid_bidi: + rb_test( + name = "{}-{}-remote-bidi".format(name, browser), + size = "large", + srcs = srcs, + args = ["rb/spec/"], + data = BROWSERS[browser]["data"] + data + [ + "//common/src/web", + "//java/src/org/openqa/selenium/grid:selenium_server_deploy.jar", + "//rb/spec:java-location", + "@bazel_tools//tools/jdk:current_java_runtime", + ], + env = BROWSERS[browser]["env"] | { + "WD_BAZEL_JAVA_LOCATION": "$(rootpath //rb/spec:java-location)", + "WD_SPEC_DRIVER": "remote", + "WEBDRIVER_BIDI": "true", + }, + main = "@bundle//bin:rspec", + tags = COMMON_TAGS + BROWSERS[browser]["tags"] + universal_tags + ["bidi", "{}-remote".format(browser)] + family_tags, + deps = {d: True for d in ( + ["//rb/spec/integration/selenium/webdriver:spec_helper", "//rb/lib/selenium/webdriver:bidi"] + + BROWSERS[browser]["deps"] + + deps + )}.keys(), + visibility = ["//rb:__subpackages__"], + target_compatible_with = BROWSERS[browser]["target_compatible_with"], + ) From 4e525adee8eb1bdb74a30c19b1580058b2d4675c Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Mon, 24 Aug 2026 13:53:53 -0500 Subject: [PATCH 4/7] [rb] test zip_root keeps the folder name with a trailing path separator --- rb/spec/unit/selenium/webdriver/zipper_spec.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rb/spec/unit/selenium/webdriver/zipper_spec.rb b/rb/spec/unit/selenium/webdriver/zipper_spec.rb index 536f04bc1de54..eea1afb055d74 100644 --- a/rb/spec/unit/selenium/webdriver/zipper_spec.rb +++ b/rb/spec/unit/selenium/webdriver/zipper_spec.rb @@ -82,6 +82,17 @@ def create_file expect(Dir.children(unzipped)).to eq([File.basename(dir_to_zip)]) expect(File.read(File.join(unzipped, File.basename(dir_to_zip), base_file_name))).to eq(file_content) end + + it 'keeps the folder name when the path has a trailing separator' do + create_file + + File.open(zip_file, 'wb') do |io| + io << Base64.decode64(described_class.zip_root("#{dir_to_zip}/")) + end + + unzipped = described_class.unzip(zip_file) + expect(Dir.children(unzipped)).to eq([File.basename(dir_to_zip)]) + end end describe '#unzip' do From 7e229b97eecbb8bad862380133ba19eef9fa09a7 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 25 Aug 2026 09:54:28 -0500 Subject: [PATCH 5/7] [rb] move browser-specific web extension tests into per-browser driver specs --- .../selenium/webdriver/chrome/BUILD.bazel | 16 ++- .../selenium/webdriver/chrome/driver_spec.rb | 32 ++++++ .../selenium/webdriver/driver_spec.rb | 97 +++---------------- .../selenium/webdriver/edge/BUILD.bazel | 13 ++- .../selenium/webdriver/edge/driver_spec.rb | 32 ++++++ .../selenium/webdriver/firefox/BUILD.bazel | 16 ++- .../selenium/webdriver/firefox/driver_spec.rb | 67 ++++++++++++- 7 files changed, 183 insertions(+), 90 deletions(-) diff --git a/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel b/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel index 96ce1333d3c8e..1be330d24b5bf 100644 --- a/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel @@ -19,10 +19,24 @@ filegroup( ) for file in glob( ["*_spec.rb"], - exclude = ["service_spec.rb"], + exclude = [ + "driver_spec.rb", + "service_spec.rb", + ], ) ] +rb_integration_test( + name = "driver", + srcs = ["driver_spec.rb"], + bidi = True, + browsers = [ + "chrome", + "chrome-beta", + ], + data = ["//common/extensions"], +) + rb_integration_test( name = "service", srcs = ["service_spec.rb"], diff --git a/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb b/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb index 4e7c354444a7f..f3ed2e490657f 100644 --- a/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb @@ -17,6 +17,8 @@ # specific language governing permissions and limitations # under the License. +require 'tmpdir' + require_relative '../spec_helper' module Selenium @@ -184,6 +186,36 @@ def get_permission(name) expect(get_permission('clipboard-write')).to eq('prompt') end end + + describe Driver, skip_unless: [{bidi: true, reason: 'web extensions install over the webExtension BiDi command'}, + {browser: :chrome}] do + let(:directory) do + File.expand_path('../../../../../../common/extensions/webextensions-selenium-example', __dir__) + end + + describe '#install_web_extension' do + it 'installs a packed archive', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + Dir.mktmpdir do |dir| + archive = File.join(dir, 'extension.zip') + File.binwrite(archive, Base64.decode64(Zipper.zip_root(directory))) + + extension = driver.install_web_extension(archive) + expect(extension.id).not_to be_empty + driver.uninstall_web_extension(extension) + end + end + + it 'installs base64-encoded bytes', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + extension = driver.install_web_extension(Zipper.zip_root(directory)) + expect(extension.id).not_to be_empty + driver.uninstall_web_extension(extension) + end + end + end end # Chrome end # WebDriver end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/driver_spec.rb b/rb/spec/integration/selenium/webdriver/driver_spec.rb index 2e9c92e295f19..286aca2490ce6 100644 --- a/rb/spec/integration/selenium/webdriver/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/driver_spec.rb @@ -370,91 +370,18 @@ module WebDriver 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' 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' do - it 'installs and removes an xpi file', - pending_if: {browser_family: :chromium, - exception: {class: Error::UnsupportedOperationError}, - reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} 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 'with base64-encoded bytes' do - it 'installs and removes base64-encoded bytes', - pending_if: {browser_family: :chromium, - exception: {class: Error::UnsupportedOperationError}, - reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} 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 - 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 - - 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 + it 'installs and removes an unpacked directory on any browser' 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 end diff --git a/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel b/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel index 59b72ae83ec78..e3c565764bb7b 100644 --- a/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel @@ -16,10 +16,21 @@ filegroup( ) for file in glob( ["*_spec.rb"], - exclude = ["service_spec.rb"], + exclude = [ + "driver_spec.rb", + "service_spec.rb", + ], ) ] +rb_integration_test( + name = "driver", + srcs = ["driver_spec.rb"], + bidi = True, + browsers = ["edge"], + data = ["//common/extensions"], +) + rb_integration_test( name = "service", srcs = ["service_spec.rb"], diff --git a/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb b/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb index e868fc31d12cc..711999c318a52 100644 --- a/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb @@ -17,6 +17,8 @@ # specific language governing permissions and limitations # under the License. +require 'tmpdir' + require_relative '../spec_helper' module Selenium @@ -114,6 +116,36 @@ module Edge end end end + + describe Driver, skip_unless: [{bidi: true, reason: 'web extensions install over the webExtension BiDi command'}, + {browser: :edge}] do + let(:directory) do + File.expand_path('../../../../../../common/extensions/webextensions-selenium-example', __dir__) + end + + describe '#install_web_extension' do + it 'installs a packed archive', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + Dir.mktmpdir do |dir| + archive = File.join(dir, 'extension.zip') + File.binwrite(archive, Base64.decode64(Zipper.zip_root(directory))) + + extension = driver.install_web_extension(archive) + expect(extension.id).not_to be_empty + driver.uninstall_web_extension(extension) + end + end + + it 'installs base64-encoded bytes', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + extension = driver.install_web_extension(Zipper.zip_root(directory)) + expect(extension.id).not_to be_empty + driver.uninstall_web_extension(extension) + end + end + end end # Edge end # WebDriver end # Selenium diff --git a/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel b/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel index f5782d96949f9..3cfcc1d6a047a 100644 --- a/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel @@ -19,10 +19,24 @@ filegroup( ) for file in glob( ["*_spec.rb"], - exclude = ["service_spec.rb"], + exclude = [ + "driver_spec.rb", + "service_spec.rb", + ], ) ] +rb_integration_test( + name = "driver", + srcs = ["driver_spec.rb"], + bidi = True, + browsers = [ + "firefox", + "firefox-beta", + ], + data = ["//common/extensions"], +) + rb_integration_test( name = "service", srcs = ["service_spec.rb"], diff --git a/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb b/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb index cd7dfebb77ecd..6188874c67650 100644 --- a/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb @@ -77,7 +77,7 @@ module Firefox end end - describe '#install_addon' do + describe '#install_addon', skip_unless: {bidi: false, reason: 'classic moz/addon endpoint'} do it 'install and uninstall xpi file' do ext = File.expand_path("#{extensions}/webextensions-selenium-example.xpi", __dir__) id = nil @@ -158,7 +158,7 @@ module Firefox end end - describe '#install_web_extension' do + describe '#install_web_extension', skip_unless: {bidi: false, reason: 'classic moz/addon fallback'} 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) @@ -207,6 +207,69 @@ module Firefox end end end + + describe '#install_web_extension', skip_unless: {bidi: true, reason: 'moz webExtension BiDi command'} do + after { |example| reset_driver!(example: example) } + + 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 + + it 'installs and removes 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 + + 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 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 # Firefox end # WebDriver From 3b7ab15b7915bd76417a3c2663db51aa844caa58 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Tue, 25 Aug 2026 12:09:16 -0500 Subject: [PATCH 6/7] [rb] ship the web extension fixture with the shared driver_spec bidi targets --- rb/spec/integration/selenium/webdriver/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/rb/spec/integration/selenium/webdriver/BUILD.bazel b/rb/spec/integration/selenium/webdriver/BUILD.bazel index 74cd645dd6c6f..b8fbb81e0e07c 100644 --- a/rb/spec/integration/selenium/webdriver/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/BUILD.bazel @@ -117,6 +117,7 @@ _NO_GRID = ["driver_finder_spec.rb"] name = f[:-8], srcs = [f], bidi = True, + data = ["//common/extensions"], grid_bidi = True, deps = [ "//rb/lib/selenium/webdriver:bidi", From bbbfaffdfcf18453b3e354803a35c4ce6a5070f2 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Wed, 26 Aug 2026 11:07:41 -0500 Subject: [PATCH 7/7] [rb] apply review feedback: BiDi contexts, DRY browsers, uninstall example, zip_file alias --- rb/lib/selenium/webdriver/common/zipper.rb | 3 + .../lib/selenium/webdriver/common/zipper.rbs | 2 + .../selenium/webdriver/BUILD.bazel | 2 +- .../selenium/webdriver/chrome/BUILD.bazel | 20 +- .../selenium/webdriver/chrome/driver_spec.rb | 290 +++++++++--------- .../selenium/webdriver/driver_spec.rb | 13 +- .../selenium/webdriver/edge/BUILD.bazel | 8 +- .../selenium/webdriver/edge/driver_spec.rb | 194 ++++++------ .../selenium/webdriver/firefox/BUILD.bazel | 20 +- rb/spec/tests.bzl | 2 +- .../selenium/webdriver/chrome/options_spec.rb | 5 - .../unit/selenium/webdriver/zipper_spec.rb | 6 + 12 files changed, 290 insertions(+), 275 deletions(-) diff --git a/rb/lib/selenium/webdriver/common/zipper.rb b/rb/lib/selenium/webdriver/common/zipper.rb index 6dcbe6962470f..c328d0665eb34 100644 --- a/rb/lib/selenium/webdriver/common/zipper.rb +++ b/rb/lib/selenium/webdriver/common/zipper.rb @@ -65,6 +65,9 @@ def zip_root(path) encode_zip(path, File.dirname(path)) end + # Backwards-compatible name for the file-only behavior #zip_root now subsumes. + alias zip_file zip_root + private def encode_zip(path, base) diff --git a/rb/sig/lib/selenium/webdriver/common/zipper.rbs b/rb/sig/lib/selenium/webdriver/common/zipper.rbs index 629bfef259eb6..2e80f11a747bd 100644 --- a/rb/sig/lib/selenium/webdriver/common/zipper.rbs +++ b/rb/sig/lib/selenium/webdriver/common/zipper.rbs @@ -29,6 +29,8 @@ module Selenium def self.zip_root: (untyped path) -> untyped + alias self.zip_file self.zip_root + private def self.encode_zip: (untyped path, untyped base) -> untyped diff --git a/rb/spec/integration/selenium/webdriver/BUILD.bazel b/rb/spec/integration/selenium/webdriver/BUILD.bazel index b8fbb81e0e07c..d9bdedf62584f 100644 --- a/rb/spec/integration/selenium/webdriver/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/BUILD.bazel @@ -46,7 +46,7 @@ _BIDI_IMPLEMENTATIONS = [ "navigation_spec.rb", ] -# bidi implementations that must also run over a Grid (e.g. se/file upload of an unpacked extension) +# bidi implementations that must also run over a Grid _GRID_BIDI = [ "driver_spec.rb", ] diff --git a/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel b/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel index 1be330d24b5bf..2d09b46d1f2f5 100644 --- a/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/chrome/BUILD.bazel @@ -1,5 +1,10 @@ load("//rb/spec:tests.bzl", "rb_integration_test") +_BROWSERS = [ + "chrome", + "chrome-beta", +] + filegroup( name = "all_srcs", testonly = True, @@ -11,10 +16,7 @@ filegroup( rb_integration_test( name = file[:-8], srcs = [file], - browsers = [ - "chrome", - "chrome-beta", - ], + browsers = _BROWSERS, data = ["//common/extensions"], ) for file in glob( @@ -30,19 +32,13 @@ rb_integration_test( name = "driver", srcs = ["driver_spec.rb"], bidi = True, - browsers = [ - "chrome", - "chrome-beta", - ], + browsers = _BROWSERS, data = ["//common/extensions"], ) rb_integration_test( name = "service", srcs = ["service_spec.rb"], - browsers = [ - "chrome", - "chrome-beta", - ], + browsers = _BROWSERS, grid = False, ) diff --git a/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb b/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb index f3ed2e490657f..5762752a41ece 100644 --- a/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/chrome/driver_spec.rb @@ -24,196 +24,198 @@ module Selenium module WebDriver module Chrome - describe Driver, skip_unless: [{bidi: false, reason: 'Not yet implemented with BiDi'}, {browser: :chrome}] do - it 'gets and sets network conditions' do - driver.network_conditions = {offline: false, latency: 56, throughput: 789} - expect(driver.network_conditions).to eq( - 'offline' => false, - 'latency' => 56, - 'download_throughput' => 789, - 'upload_throughput' => 789 - ) - end + describe Driver, skip_unless: {browser: :chrome} do + context 'when BiDi is not enabled', skip_unless: {bidi: false, reason: 'Not yet implemented with BiDi'} do + it 'gets and sets network conditions' do + driver.network_conditions = {offline: false, latency: 56, throughput: 789} + expect(driver.network_conditions).to eq( + 'offline' => false, + 'latency' => 56, + 'download_throughput' => 789, + 'upload_throughput' => 789 + ) + end - it 'sets download path' do - expect { driver.download_path = File.expand_path(__dir__) }.not_to raise_exception - end + it 'sets download path' do + expect { driver.download_path = File.expand_path(__dir__) }.not_to raise_exception + end - it 'can execute CDP commands' do - res = driver.execute_cdp('Page.addScriptToEvaluateOnNewDocument', source: 'window.was_here="TW";') - expect(res).to have_key('identifier') + it 'can execute CDP commands' do + res = driver.execute_cdp('Page.addScriptToEvaluateOnNewDocument', source: 'window.was_here="TW";') + expect(res).to have_key('identifier') - begin - driver.navigate.to url_for('formPage.html') + begin + driver.navigate.to url_for('formPage.html') - tw = driver.execute_script('return window.was_here') - expect(tw).to eq('TW') - ensure - driver.execute_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier: res['identifier']) + tw = driver.execute_script('return window.was_here') + expect(tw).to eq('TW') + ensure + driver.execute_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier: res['identifier']) + end end - end - describe 'PrintsPage' do - before(:all) { @headless = ENV.delete('HEADLESS') } - before { reset_driver!(args: ['--headless']) } + describe 'PrintsPage' do + before(:all) { @headless = ENV.delete('HEADLESS') } + before { reset_driver!(args: ['--headless']) } - after(:all) do - quit_driver - ENV['HEADLESS'] = @headless - end + after(:all) do + quit_driver + ENV['HEADLESS'] = @headless + end - let(:magic_number) { 'JVBER' } + let(:magic_number) { 'JVBER' } - it 'returns base64 for print command' do - driver.navigate.to url_for('printPage.html') - expect(driver.print_page).to include(magic_number) - end + it 'returns base64 for print command' do + driver.navigate.to url_for('printPage.html') + expect(driver.print_page).to include(magic_number) + end - it 'prints with valid params' do - driver.navigate.to url_for('printPage.html') - expect(driver.print_page(orientation: 'landscape', - page_ranges: ['1-2'], - page: {width: 30})).to include(magic_number) - end + it 'prints with valid params' do + driver.navigate.to url_for('printPage.html') + expect(driver.print_page(orientation: 'landscape', + page_ranges: ['1-2'], + page: {width: 30})).to include(magic_number) + end - it 'saves pdf' do - driver.navigate.to url_for('printPage.html') + it 'saves pdf' do + driver.navigate.to url_for('printPage.html') - path = "#{Dir.tmpdir}/test#{SecureRandom.urlsafe_base64}.pdf" + path = "#{Dir.tmpdir}/test#{SecureRandom.urlsafe_base64}.pdf" - driver.save_print_page path + driver.save_print_page path - expect(File.exist?(path)).to be true - expect(File.size(path)).to be_positive - ensure - FileUtils.rm_rf(path) + expect(File.exist?(path)).to be true + expect(File.size(path)).to be_positive + ensure + FileUtils.rm_rf(path) + end end - end - describe '#logs' do - before do - reset_driver!(logging_prefs: {browser: 'ALL', - driver: 'ALL', - performance: 'ALL'}) - driver.navigate.to url_for('errors.html') - end + describe '#logs' do + before do + reset_driver!(logging_prefs: {browser: 'ALL', + driver: 'ALL', + performance: 'ALL'}) + driver.navigate.to url_for('errors.html') + end - after(:all) { reset_driver! } + after(:all) { reset_driver! } - it 'can fetch available log types' do - expect(driver.logs.available_types).to include(:performance, :browser, :driver) - end + it 'can fetch available log types' do + expect(driver.logs.available_types).to include(:performance, :browser, :driver) + end - it 'can get the browser log' do - driver.find_element(tag_name: 'input').click + it 'can get the browser log' do + driver.find_element(tag_name: 'input').click - entries = driver.logs.get(:browser) - expect(entries).not_to be_empty - expect(entries.first).to be_a(LogEntry) - end + entries = driver.logs.get(:browser) + expect(entries).not_to be_empty + expect(entries.first).to be_a(LogEntry) + end - it 'can get the driver log' do - entries = driver.logs.get(:driver) - expect(entries).not_to be_empty - expect(entries.first).to be_a(LogEntry) - end + it 'can get the driver log' do + entries = driver.logs.get(:driver) + expect(entries).not_to be_empty + expect(entries.first).to be_a(LogEntry) + end - it 'can get the performance log' do - entries = driver.logs.get(:performance) - expect(entries).not_to be_empty - expect(entries.first).to be_a(LogEntry) + it 'can get the performance log' do + entries = driver.logs.get(:performance) + expect(entries).not_to be_empty + expect(entries.first).to be_a(LogEntry) + end end - end - it 'manages network features' do - driver.network_conditions = {offline: false, latency: 56, download_throughput: 789, upload_throughput: 600} - conditions = driver.network_conditions - expect(conditions['offline']).to be false - expect(conditions['latency']).to eq 56 - expect(conditions['download_throughput']).to eq 789 - expect(conditions['upload_throughput']).to eq 600 - driver.delete_network_conditions + it 'manages network features' do + driver.network_conditions = {offline: false, latency: 56, download_throughput: 789, upload_throughput: 600} + conditions = driver.network_conditions + expect(conditions['offline']).to be false + expect(conditions['latency']).to eq 56 + expect(conditions['download_throughput']).to eq 789 + expect(conditions['upload_throughput']).to eq 600 + driver.delete_network_conditions - error = /network conditions must be set before it can be retrieved/ - expect { driver.network_conditions }.to raise_error(Error::UnknownError, error) + error = /network conditions must be set before it can be retrieved/ + expect { driver.network_conditions }.to raise_error(Error::UnknownError, error) - # Need to reset because https://bugs.chromium.org/p/chromedriver/issues/detail?id=4790 - reset_driver! - end + # Need to reset because https://bugs.chromium.org/p/chromedriver/issues/detail?id=4790 + reset_driver! + end - # This requires cast sinks to run - it 'casts' do - # Does not get list correctly the first time for some reason - driver.cast_sinks - sleep 2 - sinks = driver.cast_sinks - unless sinks.empty? - device_name = sinks.first['name'] - driver.start_cast_tab_mirroring(device_name) - expect { driver.stop_casting(device_name) }.not_to raise_exception + # This requires cast sinks to run + it 'casts' do + # Does not get list correctly the first time for some reason + driver.cast_sinks + sleep 2 + sinks = driver.cast_sinks + unless sinks.empty? + device_name = sinks.first['name'] + driver.start_cast_tab_mirroring(device_name) + expect { driver.stop_casting(device_name) }.not_to raise_exception + end end - end - def get_permission(name) - driver.execute_async_script('callback = arguments[arguments.length - 1];' \ - 'callback(navigator.permissions.query({name: arguments[0]}));', name)['state'] - end + def get_permission(name) + driver.execute_async_script('callback = arguments[arguments.length - 1];' \ + 'callback(navigator.permissions.query({name: arguments[0]}));', name)['state'] + end - it 'can set single permissions' do - driver.navigate.to url_for('xhtmlTest.html') + it 'can set single permissions' do + driver.navigate.to url_for('xhtmlTest.html') - expect(get_permission('clipboard-read')).to eq('prompt') - expect(get_permission('clipboard-write')).to eq('granted') + expect(get_permission('clipboard-read')).to eq('prompt') + expect(get_permission('clipboard-write')).to eq('granted') - driver.add_permission('clipboard-read', 'denied') - driver.add_permission('clipboard-write', 'prompt') + driver.add_permission('clipboard-read', 'denied') + driver.add_permission('clipboard-write', 'prompt') - expect(get_permission('clipboard-read')).to eq('denied') - expect(get_permission('clipboard-write')).to eq('prompt') + expect(get_permission('clipboard-read')).to eq('denied') + expect(get_permission('clipboard-write')).to eq('prompt') - reset_driver! - end + reset_driver! + end - it 'can set multiple permissions' do - driver.navigate.to url_for('xhtmlTest.html') + it 'can set multiple permissions' do + driver.navigate.to url_for('xhtmlTest.html') - expect(get_permission('clipboard-read')).to eq('prompt') - expect(get_permission('clipboard-write')).to eq('granted') + expect(get_permission('clipboard-read')).to eq('prompt') + expect(get_permission('clipboard-write')).to eq('granted') - driver.add_permissions('clipboard-read' => 'denied', 'clipboard-write' => 'prompt') + driver.add_permissions('clipboard-read' => 'denied', 'clipboard-write' => 'prompt') - expect(get_permission('clipboard-read')).to eq('denied') - expect(get_permission('clipboard-write')).to eq('prompt') + expect(get_permission('clipboard-read')).to eq('denied') + expect(get_permission('clipboard-write')).to eq('prompt') + end end - end - describe Driver, skip_unless: [{bidi: true, reason: 'web extensions install over the webExtension BiDi command'}, - {browser: :chrome}] do - let(:directory) do - File.expand_path('../../../../../../common/extensions/webextensions-selenium-example', __dir__) - end + context 'when BiDi is enabled', + skip_unless: {bidi: true, reason: 'web extensions install over the webExtension BiDi command'} do + let(:directory) do + File.expand_path('../../../../../../common/extensions/webextensions-selenium-example', __dir__) + end - describe '#install_web_extension' do - it 'installs a packed archive', - pending_if: {exception: {class: Error::UnsupportedOperationError}, - reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do - Dir.mktmpdir do |dir| - archive = File.join(dir, 'extension.zip') - File.binwrite(archive, Base64.decode64(Zipper.zip_root(directory))) + describe '#install_web_extension' do + it 'installs a packed archive', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + Dir.mktmpdir do |dir| + archive = File.join(dir, 'extension.zip') + File.binwrite(archive, Base64.decode64(Zipper.zip_root(directory))) + + extension = driver.install_web_extension(archive) + expect(extension.id).not_to be_empty + driver.uninstall_web_extension(extension) + end + end - extension = driver.install_web_extension(archive) + it 'installs base64-encoded bytes', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + extension = driver.install_web_extension(Zipper.zip_root(directory)) expect(extension.id).not_to be_empty driver.uninstall_web_extension(extension) end end - - it 'installs base64-encoded bytes', - pending_if: {exception: {class: Error::UnsupportedOperationError}, - reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do - extension = driver.install_web_extension(Zipper.zip_root(directory)) - expect(extension.id).not_to be_empty - driver.uninstall_web_extension(extension) - end end end end # Chrome diff --git a/rb/spec/integration/selenium/webdriver/driver_spec.rb b/rb/spec/integration/selenium/webdriver/driver_spec.rb index 286aca2490ce6..e7c26b45829cf 100644 --- a/rb/spec/integration/selenium/webdriver/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/driver_spec.rb @@ -370,7 +370,7 @@ module WebDriver after { |example| reset_driver!(example: example) } describe '#install_web_extension' do - it 'installs and removes an unpacked directory on any browser' do + it 'installs an unpacked directory on any browser' do ext = File.expand_path("#{extensions}/webextensions-selenium-example-signed", __dir__) extension = driver.install_web_extension(ext) expect(extension.id).not_to be_empty @@ -378,6 +378,17 @@ module WebDriver 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 + + describe '#uninstall_web_extension' do + it 'removes an installed extension on any browser' do + ext = File.expand_path("#{extensions}/webextensions-selenium-example-signed", __dir__) + extension = driver.install_web_extension(ext) + + 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 diff --git a/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel b/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel index e3c565764bb7b..106bdae903cab 100644 --- a/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/edge/BUILD.bazel @@ -1,5 +1,7 @@ load("//rb/spec:tests.bzl", "rb_integration_test") +_BROWSERS = ["edge"] + filegroup( name = "all_srcs", testonly = True, @@ -11,7 +13,7 @@ filegroup( rb_integration_test( name = file[:-8], srcs = [file], - browsers = ["edge"], + browsers = _BROWSERS, data = ["//common/extensions"], ) for file in glob( @@ -27,13 +29,13 @@ rb_integration_test( name = "driver", srcs = ["driver_spec.rb"], bidi = True, - browsers = ["edge"], + browsers = _BROWSERS, data = ["//common/extensions"], ) rb_integration_test( name = "service", srcs = ["service_spec.rb"], - browsers = ["edge"], + browsers = _BROWSERS, grid = False, ) diff --git a/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb b/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb index 711999c318a52..039e40184c03b 100644 --- a/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb +++ b/rb/spec/integration/selenium/webdriver/edge/driver_spec.rb @@ -24,126 +24,128 @@ module Selenium module WebDriver module Edge - describe Driver, skip_unless: [{bidi: false, reason: 'Not yet implemented with BiDi'}, {browser: :edge}] do - it 'gets and sets network conditions' do - driver.network_conditions = {offline: false, latency: 56, throughput: 789} - expect(driver.network_conditions).to eq( - 'offline' => false, - 'latency' => 56, - 'download_throughput' => 789, - 'upload_throughput' => 789 - ) - driver.delete_network_conditions - end + describe Driver, skip_unless: {browser: :edge} do + context 'when BiDi is not enabled', skip_unless: {bidi: false, reason: 'Not yet implemented with BiDi'} do + it 'gets and sets network conditions' do + driver.network_conditions = {offline: false, latency: 56, throughput: 789} + expect(driver.network_conditions).to eq( + 'offline' => false, + 'latency' => 56, + 'download_throughput' => 789, + 'upload_throughput' => 789 + ) + driver.delete_network_conditions + end - it 'supports default network conditions' do - driver.network_conditions = {latency: 56} - expect(driver.network_conditions).to eq( - 'offline' => false, - 'latency' => 56, - 'download_throughput' => -1, - 'upload_throughput' => -1 - ) - driver.delete_network_conditions - - # Need to reset because https://bugs.chromium.org/p/chromedriver/issues/detail?id=4790 - reset_driver! - end + it 'supports default network conditions' do + driver.network_conditions = {latency: 56} + expect(driver.network_conditions).to eq( + 'offline' => false, + 'latency' => 56, + 'download_throughput' => -1, + 'upload_throughput' => -1 + ) + driver.delete_network_conditions + + # Need to reset because https://bugs.chromium.org/p/chromedriver/issues/detail?id=4790 + reset_driver! + end - it 'sets download path' do - expect { driver.download_path = File.expand_path(__dir__) }.not_to raise_exception - end + it 'sets download path' do + expect { driver.download_path = File.expand_path(__dir__) }.not_to raise_exception + end - it 'can execute CDP commands' do - res = driver.execute_cdp('Page.addScriptToEvaluateOnNewDocument', source: 'window.was_here="TW";') - expect(res).to have_key('identifier') + it 'can execute CDP commands' do + res = driver.execute_cdp('Page.addScriptToEvaluateOnNewDocument', source: 'window.was_here="TW";') + expect(res).to have_key('identifier') - begin - driver.navigate.to url_for('formPage.html') + begin + driver.navigate.to url_for('formPage.html') - tw = driver.execute_script('return window.was_here') - expect(tw).to eq('TW') - ensure - driver.execute_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier: res['identifier']) + tw = driver.execute_script('return window.was_here') + expect(tw).to eq('TW') + ensure + driver.execute_cdp('Page.removeScriptToEvaluateOnNewDocument', identifier: res['identifier']) + end end - end - describe '#logs' do - before do - reset_driver!(logging_prefs: {browser: 'ALL', - driver: 'ALL', - performance: 'ALL'}) - driver.navigate.to url_for('errors.html') - end + describe '#logs' do + before do + reset_driver!(logging_prefs: {browser: 'ALL', + driver: 'ALL', + performance: 'ALL'}) + driver.navigate.to url_for('errors.html') + end - after(:all) { reset_driver! } + after(:all) { reset_driver! } - it 'can fetch available log types' do - expect(driver.logs.available_types).to include(:performance, :browser, :driver) - end + it 'can fetch available log types' do + expect(driver.logs.available_types).to include(:performance, :browser, :driver) + end - it 'can get the browser log' do - driver.find_element(tag_name: 'input').click + it 'can get the browser log' do + driver.find_element(tag_name: 'input').click - entries = driver.logs.get(:browser) - expect(entries).not_to be_empty - expect(entries.first).to be_a(LogEntry) - end + entries = driver.logs.get(:browser) + expect(entries).not_to be_empty + expect(entries.first).to be_a(LogEntry) + end - it 'can get the driver log' do - entries = driver.logs.get(:driver) - expect(entries).not_to be_empty - expect(entries.first).to be_a(LogEntry) - end + it 'can get the driver log' do + entries = driver.logs.get(:driver) + expect(entries).not_to be_empty + expect(entries.first).to be_a(LogEntry) + end - it 'can get the performance log' do - entries = driver.logs.get(:performance) - expect(entries).not_to be_empty - expect(entries.first).to be_a(LogEntry) + it 'can get the performance log' do + entries = driver.logs.get(:performance) + expect(entries).not_to be_empty + expect(entries.first).to be_a(LogEntry) + end end - end - # This requires cast sinks to run - it 'casts' do - # Does not get list correctly the first time for some reason - driver.cast_sinks - sleep 2 - sinks = driver.cast_sinks - unless sinks.empty? - device_name = sinks.first['name'] - driver.start_cast_tab_mirroring(device_name) - expect { driver.stop_casting(device_name) }.not_to raise_exception + # This requires cast sinks to run + it 'casts' do + # Does not get list correctly the first time for some reason + driver.cast_sinks + sleep 2 + sinks = driver.cast_sinks + unless sinks.empty? + device_name = sinks.first['name'] + driver.start_cast_tab_mirroring(device_name) + expect { driver.stop_casting(device_name) }.not_to raise_exception + end end end - end - describe Driver, skip_unless: [{bidi: true, reason: 'web extensions install over the webExtension BiDi command'}, - {browser: :edge}] do - let(:directory) do - File.expand_path('../../../../../../common/extensions/webextensions-selenium-example', __dir__) - end + context 'when BiDi is enabled', + skip_unless: {bidi: true, reason: 'web extensions install over the webExtension BiDi command'} do + let(:directory) do + File.expand_path('../../../../../../common/extensions/webextensions-selenium-example', __dir__) + end - describe '#install_web_extension' do - it 'installs a packed archive', - pending_if: {exception: {class: Error::UnsupportedOperationError}, - reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do - Dir.mktmpdir do |dir| - archive = File.join(dir, 'extension.zip') - File.binwrite(archive, Base64.decode64(Zipper.zip_root(directory))) + describe '#install_web_extension' do + it 'installs a packed archive', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + Dir.mktmpdir do |dir| + archive = File.join(dir, 'extension.zip') + File.binwrite(archive, Base64.decode64(Zipper.zip_root(directory))) + + extension = driver.install_web_extension(archive) + expect(extension.id).not_to be_empty + driver.uninstall_web_extension(extension) + end + end - extension = driver.install_web_extension(archive) + it 'installs base64-encoded bytes', + pending_if: {exception: {class: Error::UnsupportedOperationError}, + reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do + extension = driver.install_web_extension(Zipper.zip_root(directory)) expect(extension.id).not_to be_empty driver.uninstall_web_extension(extension) end end - - it 'installs base64-encoded bytes', - pending_if: {exception: {class: Error::UnsupportedOperationError}, - reason: 'chromium-bidi installs only unpacked directories (SeleniumHQ/selenium#16541)'} do - extension = driver.install_web_extension(Zipper.zip_root(directory)) - expect(extension.id).not_to be_empty - driver.uninstall_web_extension(extension) - end end end end # Edge diff --git a/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel b/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel index 3cfcc1d6a047a..6cf668dcf6ce3 100644 --- a/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel +++ b/rb/spec/integration/selenium/webdriver/firefox/BUILD.bazel @@ -1,5 +1,10 @@ load("//rb/spec:tests.bzl", "rb_integration_test") +_BROWSERS = [ + "firefox", + "firefox-beta", +] + filegroup( name = "all_srcs", testonly = True, @@ -11,10 +16,7 @@ filegroup( rb_integration_test( name = file[:-8], srcs = [file], - browsers = [ - "firefox", - "firefox-beta", - ], + browsers = _BROWSERS, data = ["//common/extensions"], ) for file in glob( @@ -30,19 +32,13 @@ rb_integration_test( name = "driver", srcs = ["driver_spec.rb"], bidi = True, - browsers = [ - "firefox", - "firefox-beta", - ], + browsers = _BROWSERS, data = ["//common/extensions"], ) rb_integration_test( name = "service", srcs = ["service_spec.rb"], - browsers = [ - "firefox", - "firefox-beta", - ], + browsers = _BROWSERS, grid = False, ) diff --git a/rb/spec/tests.bzl b/rb/spec/tests.bzl index ab92f758da794..265e8a3225e49 100644 --- a/rb/spec/tests.bzl +++ b/rb/spec/tests.bzl @@ -261,7 +261,7 @@ def rb_integration_test( target_compatible_with = BROWSERS[browser]["target_compatible_with"], ) - # Bidi over a Grid, for specs that must exercise remote-end behavior (e.g. se/file uploads). + # Bidi over a Grid, for specs that must exercise remote-end behavior. if grid_bidi: rb_test( name = "{}-{}-remote-bidi".format(name, browser), diff --git a/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb b/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb index 81a152cb2c49b..54496aa1a0d15 100644 --- a/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb +++ b/rb/spec/unit/selenium/webdriver/chrome/options_spec.rb @@ -283,11 +283,6 @@ module Chrome expect(options.as_json).to eq('browserName' => 'chrome', 'goog:chromeOptions' => {}) end - it 'does not inject debugging arguments when BiDi is enabled' do - bidi_options = described_class.new(web_socket_url: true) - expect(bidi_options.as_json['goog:chromeOptions']).not_to have_key('args') - end - it 'errors when unrecognized capability is passed' do options.add_option(:foo, 'bar') diff --git a/rb/spec/unit/selenium/webdriver/zipper_spec.rb b/rb/spec/unit/selenium/webdriver/zipper_spec.rb index eea1afb055d74..27f5809bc6e44 100644 --- a/rb/spec/unit/selenium/webdriver/zipper_spec.rb +++ b/rb/spec/unit/selenium/webdriver/zipper_spec.rb @@ -95,6 +95,12 @@ def create_file end end + describe '#zip_file' do + it 'is a backwards-compatible alias for #zip_root' do + expect(described_class.method(:zip_file)).to eq(described_class.method(:zip_root)) + end + end + describe '#unzip' do it 'a file' do File.open(zip_file, 'wb') do |io|