Skip to content

Commit 24955e2

Browse files
committed
[rb] add cross-browser Driver#install_web_extension, deprecating the Firefox classic install_addon methods
1 parent 4f1e333 commit 24955e2

24 files changed

Lines changed: 491 additions & 42 deletions

File tree

rb/lib/selenium/webdriver/chromium/features.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ module Features
3131
get_log: [:post, 'session/:session_id/se/log']
3232
}.freeze
3333

34+
# TODO: remove subclass override once chrome supports base64; currently only supports an unpacked local path
35+
def install_web_extension(path)
36+
result = web_extension.install(extension_data: web_extension.extension_path(path: path))
37+
WebExtension.new(result.extension)
38+
end
39+
3440
def launch_app(id)
3541
execute :launch_app, {}, {id: id}
3642
end

rb/lib/selenium/webdriver/chromium/options.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ def process_browser_options(browser_options)
236236
options['args'] << "--user-data-dir=#{@profile.directory}"
237237
end
238238

239+
if bidi?
240+
options['args'] = options['args'].to_a | %w[--enable-unsafe-extension-debugging --remote-debugging-pipe]
241+
end
242+
239243
return if (@encoded_extensions + @extensions).empty?
240244

241245
options['extensions'] = @encoded_extensions + @extensions.map { |ext| encode_extension(ext) }

rb/lib/selenium/webdriver/common.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
require 'selenium/webdriver/common/takes_screenshot'
9595
require 'selenium/webdriver/common/driver'
9696
require 'selenium/webdriver/common/element'
97+
require 'selenium/webdriver/common/web_extension'
9798
require 'selenium/webdriver/common/shadow_root'
9899
require 'selenium/webdriver/common/websocket_connection'
99100
require 'selenium/webdriver/common/child_process'

rb/lib/selenium/webdriver/common/driver.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,34 @@ def network
277277
@network ||= WebDriver::Network.new(bridge)
278278
end
279279

280+
#
281+
# Installs a browser extension over WebDriver BiDi (+webExtension.install+).
282+
#
283+
# Firefox sends the extension base64-encoded, so it accepts an unpacked directory, a packed
284+
# extension (.xpi/.crx/.zip), or already-encoded base64 bytes, and works with remote (Grid)
285+
# sessions. Chromium browsers currently accept only an unpacked directory whose path resolves on
286+
# the browser host (local sessions), until chromium-bidi supports base64 (SeleniumHQ/selenium#16541).
287+
#
288+
# @note Requires a BiDi session (set +web_socket_url+ to true in the browser options).
289+
# @param [String] path unpacked extension directory, packed extension file, or base64-encoded bytes
290+
# @return [WebExtension] handle for the installed extension
291+
#
292+
293+
def install_web_extension(...)
294+
bridge.install_web_extension(...)
295+
end
296+
297+
#
298+
# Uninstalls a browser extension installed with {#install_web_extension}.
299+
#
300+
# @note Requires a BiDi session (set +web_socket_url+ to true in the browser options).
301+
# @param [WebExtension] extension handle returned by {#install_web_extension}
302+
#
303+
304+
def uninstall_web_extension(extension)
305+
bridge.uninstall_web_extension(extension.id)
306+
end
307+
280308
#-------------------------------- sugar --------------------------------
281309

282310
#

rb/lib/selenium/webdriver/common/driver_extensions/has_addons.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module HasAddons
3030
#
3131

3232
def install_addon(path, temporary = nil)
33+
WebDriver.logger.deprecate('#install_addon', '#install_web_extension', id: :install_addon)
3334
@bridge.install_addon(path, temporary)
3435
end
3536

@@ -40,6 +41,7 @@ def install_addon(path, temporary = nil)
4041
#
4142

4243
def uninstall_addon(id)
44+
WebDriver.logger.deprecate('#uninstall_addon', '#uninstall_web_extension', id: :uninstall_addon)
4345
@bridge.uninstall_addon(id)
4446
end
4547
end # HasAddons

rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,15 @@ module HasDevTools
2525
# Retrieves connection to DevTools.
2626
#
2727
# @return [DevTools]
28+
# @raise [Error::WebDriverError] when BiDi is enabled, as CDP shares a transport with it
2829
#
2930

3031
def devtools(target_type: 'page')
32+
if @bridge.bidi?
33+
raise Error::WebDriverError,
34+
'CDP (DevTools) is disabled when BiDi is enabled; use the WebDriver BiDi APIs instead'
35+
end
36+
3137
@devtools ||= {}
3238
@devtools[target_type] ||= begin
3339
require 'selenium/devtools'
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
#
23+
# Handle for a browser extension installed via Driver#install_web_extension.
24+
# Wraps the identifier the browser assigned; pass it to Driver#uninstall_web_extension.
25+
#
26+
class WebExtension
27+
#
28+
# @return [String] identifier assigned to the extension by the browser
29+
#
30+
31+
attr_reader :id
32+
33+
#
34+
# @api private
35+
#
36+
37+
def initialize(id)
38+
@id = id
39+
end
40+
end # WebExtension
41+
end # WebDriver
42+
end # Selenium

rb/lib/selenium/webdriver/firefox/features.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ def uninstall_addon(id)
5353
execute :uninstall_addon, {}, {id: id}
5454
end
5555

56+
def install_web_extension(path, allow_private_browsing: nil, permanent: nil)
57+
data = encode_extension(path)
58+
return classic_install_web_extension(data, permanent, allow_private_browsing) unless bidi?
59+
60+
moz = web_extension.moz
61+
options = {allow_private_browsing:, permanent:}.compact
62+
result = moz.install(extension_data: moz.extension_base64_encoded(value: data), **options)
63+
WebDriver::WebExtension.new(result.extension)
64+
end
65+
66+
def uninstall_web_extension(extension_id)
67+
bidi? ? web_extension.uninstall(extension: extension_id) : uninstall_addon(extension_id)
68+
nil
69+
end
70+
5671
def full_screenshot
5772
execute :full_page_screenshot
5873
end
@@ -64,6 +79,20 @@ def context=(context)
6479
def context
6580
execute :get_context
6681
end
82+
83+
private
84+
85+
def classic_install_web_extension(data, permanent, allow_private_browsing)
86+
if allow_private_browsing == false
87+
raise Error::WebDriverError,
88+
'allow_private_browsing: false requires a BiDi session; the classic install always grants ' \
89+
'private-browsing access'
90+
end
91+
92+
temporary = !permanent unless permanent.nil?
93+
options = {temporary: temporary, allowPrivateBrowsing: allow_private_browsing}.compact
94+
WebDriver::WebExtension.new(execute(:install_addon, {}, {addon: data, **options}))
95+
end
6796
end # Bridge
6897
end # Firefox
6998
end # WebDriver

rb/lib/selenium/webdriver/remote/bidi_bridge.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ def create_session(capabilities)
4545
end
4646
end
4747

48+
def install_web_extension(path)
49+
data = web_extension.extension_base64_encoded(value: encode_extension(path))
50+
result = web_extension.install(extension_data: data)
51+
WebExtension.new(result.extension)
52+
end
53+
54+
def uninstall_web_extension(id)
55+
web_extension.uninstall(extension: id)
56+
nil
57+
end
58+
4859
def get(url)
4960
browsing_context.navigate(context: window_handle, url: url, wait: readiness_state)
5061
nil
@@ -91,6 +102,10 @@ def browsing_context
91102
@browsing_context ||= BiDi::Protocol::BrowsingContext.new(connection)
92103
end
93104

105+
def web_extension
106+
@web_extension ||= BiDi::Protocol::WebExtension.new(connection)
107+
end
108+
94109
def readiness_state
95110
READINESS_STATE.fetch(capabilities[:page_load_strategy] || 'normal')
96111
end

rb/lib/selenium/webdriver/remote/bridge.rb

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -593,14 +593,18 @@ def click_fedcm_dialog_button
593593
execute :click_fedcm_dialog_button, {}, {dialogButton: 'ConfirmIdpLoginContinue'}
594594
end
595595

596-
def bidi
597-
msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
598-
raise(WebDriver::Error::WebDriverError, msg)
596+
def bidi(*)
597+
raise WebDriver::Error::WebDriverError,
598+
'BiDi must be enabled by setting #web_socket_url to true in options class'
599599
end
600+
alias connection bidi
601+
alias web_extension bidi
602+
alias install_web_extension bidi
603+
alias uninstall_web_extension bidi
604+
private :web_extension
600605

601-
def connection
602-
msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
603-
raise(WebDriver::Error::WebDriverError, msg)
606+
def bidi?
607+
!@bidi.nil?
604608
end
605609

606610
def command_list
@@ -609,6 +613,16 @@ def command_list
609613

610614
private
611615

616+
def encode_extension(path)
617+
if File.directory?(path)
618+
Zipper.zip(path)
619+
elsif File.file?(path)
620+
File.open(path, 'rb') { |file| Base64.strict_encode64(file.read) }
621+
else
622+
path # already base64-encoded bytes
623+
end
624+
end
625+
612626
#
613627
# executes a command on the remote server.
614628
#

0 commit comments

Comments
 (0)