selenium-webdriver 4.35.0 → 4.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES +111 -0
- data/Gemfile +1 -4
- data/LICENSE +1 -1
- data/NOTICE +1 -1
- data/README.md +43 -23
- data/bin/linux/selenium-manager +0 -0
- data/bin/macos/selenium-manager +0 -0
- data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5971 -0
- data/bin/selenium-manager.cdx.json +11629 -0
- data/bin/windows/selenium-manager.exe +0 -0
- data/lib/selenium/server.rb +30 -5
- data/lib/selenium/webdriver/atoms/findElements.js +272 -54
- data/lib/selenium/webdriver/atoms/getAttribute.js +152 -6
- data/lib/selenium/webdriver/atoms/isDisplayed.js +385 -28
- data/lib/selenium/webdriver/bidi/browser.rb +71 -0
- data/lib/selenium/webdriver/bidi/browsing_context.rb +3 -2
- data/lib/selenium/webdriver/bidi/{log/filter_by.rb → error.rb} +20 -16
- data/lib/selenium/webdriver/bidi/log_handler.rb +5 -0
- data/lib/selenium/webdriver/bidi/network/cookies.rb +13 -9
- data/lib/selenium/webdriver/bidi/network/credentials.rb +4 -0
- data/lib/selenium/webdriver/bidi/network/headers.rb +4 -0
- data/lib/selenium/webdriver/bidi/network/intercepted_auth.rb +4 -0
- data/lib/selenium/webdriver/bidi/network/intercepted_item.rb +4 -0
- data/lib/selenium/webdriver/bidi/network/intercepted_request.rb +20 -4
- data/lib/selenium/webdriver/bidi/network/intercepted_response.rb +24 -6
- data/lib/selenium/webdriver/bidi/network/url_pattern.rb +4 -0
- data/lib/selenium/webdriver/bidi/network.rb +18 -9
- data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +495 -0
- data/lib/selenium/webdriver/bidi/protocol/browser.rb +250 -0
- data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +798 -0
- data/lib/selenium/webdriver/bidi/{log/base_log_entry.rb → protocol/domain.rb} +22 -11
- data/lib/selenium/webdriver/bidi/protocol/emulation.rb +448 -0
- data/lib/selenium/webdriver/bidi/protocol/error_code.rb +66 -0
- data/lib/selenium/webdriver/bidi/protocol/input.rb +354 -0
- data/lib/selenium/webdriver/bidi/protocol/log.rb +115 -0
- data/lib/selenium/webdriver/bidi/protocol/network.rb +720 -0
- data/lib/selenium/webdriver/bidi/protocol/permissions.rb +75 -0
- data/lib/selenium/webdriver/bidi/protocol/script.rb +1052 -0
- data/lib/selenium/webdriver/bidi/protocol/session.rb +286 -0
- data/lib/selenium/webdriver/bidi/protocol/speculation.rb +58 -0
- data/lib/selenium/webdriver/bidi/protocol/storage.rb +183 -0
- data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +88 -0
- data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +132 -0
- data/lib/selenium/webdriver/bidi/protocol.rb +42 -0
- data/lib/selenium/webdriver/bidi/serialization/record.rb +402 -0
- data/lib/selenium/webdriver/bidi/serialization/union.rb +156 -0
- data/lib/selenium/webdriver/bidi/serialization.rb +90 -0
- data/lib/selenium/webdriver/bidi/session.rb +4 -0
- data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +1420 -0
- data/lib/selenium/webdriver/bidi/support/check_generated.rb +63 -0
- data/lib/selenium/webdriver/bidi/transport.rb +57 -0
- data/lib/selenium/webdriver/bidi.rb +4 -1
- data/lib/selenium/webdriver/chrome/driver.rb +4 -3
- data/lib/selenium/webdriver/chrome/service.rb +13 -0
- data/lib/selenium/webdriver/chromium/driver.rb +0 -1
- data/lib/selenium/webdriver/chromium/options.rb +23 -1
- data/lib/selenium/webdriver/chromium/profile.rb +6 -0
- data/lib/selenium/webdriver/common/child_process.rb +2 -1
- data/lib/selenium/webdriver/common/client_config.rb +97 -0
- data/lib/selenium/webdriver/common/driver.rb +13 -7
- data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +0 -7
- data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +0 -7
- data/lib/selenium/webdriver/common/driver_extensions/{has_bidi.rb → has_session_events.rb} +19 -7
- data/lib/selenium/webdriver/common/driver_finder.rb +35 -25
- data/lib/selenium/webdriver/common/error.rb +10 -3
- data/lib/selenium/webdriver/common/local_driver.rb +29 -12
- data/lib/selenium/webdriver/common/logger.rb +28 -0
- data/lib/selenium/webdriver/common/manager.rb +2 -0
- data/lib/selenium/webdriver/common/options.rb +41 -2
- data/lib/selenium/webdriver/common/platform.rb +1 -3
- data/lib/selenium/webdriver/common/proxy.rb +1 -9
- data/lib/selenium/webdriver/common/selenium_manager.rb +2 -1
- data/lib/selenium/webdriver/common/service.rb +4 -8
- data/lib/selenium/webdriver/common/service_manager.rb +36 -4
- data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
- data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
- data/lib/selenium/webdriver/common/virtual_authenticator/credential.rb +1 -1
- data/lib/selenium/webdriver/common/wait.rb +4 -1
- data/lib/selenium/webdriver/common/websocket_connection.rb +73 -37
- data/lib/selenium/webdriver/common.rb +2 -1
- data/lib/selenium/webdriver/devtools/response.rb +3 -3
- data/lib/selenium/webdriver/devtools.rb +1 -1
- data/lib/selenium/webdriver/edge/driver.rb +4 -3
- data/lib/selenium/webdriver/edge/service.rb +14 -0
- data/lib/selenium/webdriver/firefox/driver.rb +4 -6
- data/lib/selenium/webdriver/firefox/options.rb +19 -0
- data/lib/selenium/webdriver/firefox/profile.rb +13 -6
- data/lib/selenium/webdriver/firefox/service.rb +31 -2
- data/lib/selenium/webdriver/ie/driver.rb +4 -3
- data/lib/selenium/webdriver/ie/service.rb +10 -0
- data/lib/selenium/webdriver/remote/bidi_bridge.rb +44 -10
- data/lib/selenium/webdriver/remote/bridge.rb +20 -14
- data/lib/selenium/webdriver/remote/driver.rb +15 -4
- data/lib/selenium/webdriver/remote/features.rb +26 -1
- data/lib/selenium/webdriver/remote/http/common.rb +57 -12
- data/lib/selenium/webdriver/remote/http/curb.rb +6 -8
- data/lib/selenium/webdriver/remote/http/default.rb +56 -39
- data/lib/selenium/webdriver/safari/driver.rb +4 -3
- data/lib/selenium/webdriver/safari/options.rb +23 -4
- data/lib/selenium/webdriver/safari.rb +1 -6
- data/lib/selenium/webdriver/support/block_event_listener.rb +5 -1
- data/lib/selenium/webdriver/support/color.rb +14 -14
- data/lib/selenium/webdriver/support/event_firing_bridge.rb +5 -1
- data/lib/selenium/webdriver/support/guards/guard.rb +30 -14
- data/lib/selenium/webdriver/support/guards.rb +11 -4
- data/lib/selenium/webdriver/version.rb +1 -1
- data/lib/selenium/webdriver.rb +7 -2
- data/selenium-webdriver.gemspec +1 -1
- metadata +33 -11
- data/lib/selenium/webdriver/bidi/log/console_log_entry.rb +0 -35
- data/lib/selenium/webdriver/bidi/log/generic_log_entry.rb +0 -33
- data/lib/selenium/webdriver/bidi/log/javascript_log_entry.rb +0 -33
- data/lib/selenium/webdriver/bidi/log_inspector.rb +0 -147
|
@@ -55,9 +55,32 @@ module Selenium
|
|
|
55
55
|
@ignored = Array(ignored)
|
|
56
56
|
@allowed = Array(allowed)
|
|
57
57
|
@first_warning = false
|
|
58
|
+
@level_forced = false
|
|
59
|
+
@output_forced = false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#
|
|
63
|
+
# Forces debug level and prevents it from being overridden.
|
|
64
|
+
#
|
|
65
|
+
def debug!
|
|
66
|
+
@level_forced = true
|
|
67
|
+
@logger.level = :debug
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#
|
|
71
|
+
# Forces output to stderr and prevents it from being overridden.
|
|
72
|
+
#
|
|
73
|
+
def stderr!
|
|
74
|
+
@output_forced = true
|
|
75
|
+
@logger.reopen($stderr)
|
|
58
76
|
end
|
|
59
77
|
|
|
60
78
|
def level=(level)
|
|
79
|
+
if @level_forced
|
|
80
|
+
warn('Logger level is forced; ignoring override', id: :logger)
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
61
84
|
if level == :info && @logger.level == :info
|
|
62
85
|
info(':info is now the default log level, to see additional logging, set log level to :debug')
|
|
63
86
|
end
|
|
@@ -71,6 +94,11 @@ module Selenium
|
|
|
71
94
|
# @param [String] io
|
|
72
95
|
#
|
|
73
96
|
def output=(io)
|
|
97
|
+
if @output_forced
|
|
98
|
+
warn('Logger output is forced; ignoring override', id: :logger)
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
74
102
|
@logger.reopen(io)
|
|
75
103
|
end
|
|
76
104
|
|
|
@@ -71,8 +71,12 @@ module Selenium
|
|
|
71
71
|
def initialize(**opts)
|
|
72
72
|
self.class.set_capabilities
|
|
73
73
|
|
|
74
|
+
opts[:web_socket_url] = opts.delete(:bidi) if opts.key?(:bidi)
|
|
75
|
+
|
|
74
76
|
@options = opts
|
|
75
77
|
@options[:browser_name] = self.class::BROWSER
|
|
78
|
+
|
|
79
|
+
enable_bidi! if @options[:web_socket_url]
|
|
76
80
|
end
|
|
77
81
|
|
|
78
82
|
#
|
|
@@ -91,6 +95,20 @@ module Selenium
|
|
|
91
95
|
@options[name] = value
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
#
|
|
99
|
+
# Enables WebDriver BiDi by requesting the W3C webSocketUrl capability.
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
#
|
|
103
|
+
|
|
104
|
+
def enable_bidi!
|
|
105
|
+
@options[:web_socket_url] = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def bidi?
|
|
109
|
+
!!@options[:web_socket_url]
|
|
110
|
+
end
|
|
111
|
+
|
|
94
112
|
def ==(other)
|
|
95
113
|
return false unless other.is_a? self.class
|
|
96
114
|
|
|
@@ -120,22 +138,43 @@ module Selenium
|
|
|
120
138
|
browser_options = {self.class::KEY => browser_options} if defined?(self.class::KEY)
|
|
121
139
|
|
|
122
140
|
process_browser_options(browser_options)
|
|
123
|
-
generate_as_json(w3c_options
|
|
141
|
+
generate_as_json(merge_browser_options(w3c_options, browser_options))
|
|
124
142
|
end
|
|
125
143
|
|
|
126
144
|
private
|
|
127
145
|
|
|
146
|
+
# Preserve a hand-built vendor options hash (e.g. passed through #add_option) by merging it
|
|
147
|
+
# with the binding's own, rather than letting one silently overwrite the other.
|
|
148
|
+
def merge_browser_options(w3c_options, browser_options)
|
|
149
|
+
w3c_options.merge(browser_options) do |_key, w3c_value, browser_value|
|
|
150
|
+
if w3c_value.is_a?(Hash) && browser_value.is_a?(Hash)
|
|
151
|
+
browser_value.merge(w3c_value)
|
|
152
|
+
else
|
|
153
|
+
browser_value
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
128
158
|
def w3c?(key)
|
|
129
159
|
W3C_OPTIONS.include?(key) || key.to_s.include?(':')
|
|
130
160
|
end
|
|
131
161
|
|
|
132
162
|
def process_w3c_options(options)
|
|
133
163
|
w3c_options = options.select { |key, val| w3c?(key) && !val.nil? }
|
|
134
|
-
w3c_options[:unhandled_prompt_behavior] &&=
|
|
164
|
+
w3c_options[:unhandled_prompt_behavior] &&=
|
|
165
|
+
process_unhandled_prompt_behavior_value(w3c_options[:unhandled_prompt_behavior])
|
|
135
166
|
options.delete_if { |key, _val| w3c?(key) }
|
|
136
167
|
w3c_options
|
|
137
168
|
end
|
|
138
169
|
|
|
170
|
+
def process_unhandled_prompt_behavior_value(value)
|
|
171
|
+
if value.is_a?(Hash)
|
|
172
|
+
value.transform_values { |v| process_unhandled_prompt_behavior_value(v) }
|
|
173
|
+
else
|
|
174
|
+
value&.to_s&.tr('_', ' ')
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
139
178
|
def process_browser_options(_browser_options)
|
|
140
179
|
nil
|
|
141
180
|
end
|
|
@@ -29,7 +29,6 @@ module Selenium
|
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
31
|
ALLOWED = {type: 'proxyType',
|
|
32
|
-
ftp: 'ftpProxy',
|
|
33
32
|
http: 'httpProxy',
|
|
34
33
|
no_proxy: 'noProxy',
|
|
35
34
|
pac: 'proxyAutoconfigUrl',
|
|
@@ -76,12 +75,6 @@ module Selenium
|
|
|
76
75
|
end
|
|
77
76
|
alias eql? ==
|
|
78
77
|
|
|
79
|
-
def ftp=(value)
|
|
80
|
-
WebDriver.logger.deprecate('FTP proxy support', nil, id: :ftp_proxy)
|
|
81
|
-
self.type = :manual
|
|
82
|
-
@ftp = value
|
|
83
|
-
end
|
|
84
|
-
|
|
85
78
|
def http=(value)
|
|
86
79
|
self.type = :manual
|
|
87
80
|
@http = value
|
|
@@ -143,9 +136,8 @@ module Selenium
|
|
|
143
136
|
def as_json(*)
|
|
144
137
|
json_result = {
|
|
145
138
|
'proxyType' => TYPES[type].downcase,
|
|
146
|
-
'ftpProxy' => ftp,
|
|
147
139
|
'httpProxy' => http,
|
|
148
|
-
'noProxy' => no_proxy.is_a?(String) ? no_proxy.split(',
|
|
140
|
+
'noProxy' => no_proxy.is_a?(String) ? no_proxy.split(',').map(&:strip).reject(&:empty?) : no_proxy,
|
|
149
141
|
'proxyAutoconfigUrl' => pac,
|
|
150
142
|
'sslProxy' => ssl,
|
|
151
143
|
'autodetect' => auto_detect,
|
|
@@ -50,7 +50,8 @@ module Selenium
|
|
|
50
50
|
def binary
|
|
51
51
|
@binary ||= begin
|
|
52
52
|
if (location = ENV.fetch('SE_MANAGER_PATH', nil))
|
|
53
|
-
WebDriver.logger.debug("Selenium Manager set by ENV['SE_MANAGER_PATH']: #{location}"
|
|
53
|
+
WebDriver.logger.debug("Selenium Manager set by ENV['SE_MANAGER_PATH']: #{location}",
|
|
54
|
+
id: :selenium_manager)
|
|
54
55
|
end
|
|
55
56
|
location ||= platform_location
|
|
56
57
|
|
|
@@ -69,7 +69,6 @@ module Selenium
|
|
|
69
69
|
def initialize(path: nil, port: nil, log: nil, args: nil)
|
|
70
70
|
port ||= self.class::DEFAULT_PORT
|
|
71
71
|
args ||= []
|
|
72
|
-
path ||= env_path
|
|
73
72
|
|
|
74
73
|
@executable_path = path
|
|
75
74
|
@host = Platform.localhost
|
|
@@ -88,7 +87,7 @@ module Selenium
|
|
|
88
87
|
end
|
|
89
88
|
|
|
90
89
|
def launch
|
|
91
|
-
@executable_path ||=
|
|
90
|
+
@executable_path ||= DriverFinder.new(nil, self).driver_path
|
|
92
91
|
ServiceManager.new(self).tap(&:start)
|
|
93
92
|
end
|
|
94
93
|
|
|
@@ -96,13 +95,10 @@ module Selenium
|
|
|
96
95
|
self.class::SHUTDOWN_SUPPORTED
|
|
97
96
|
end
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
default_options = WebDriver.const_get("#{self.class.name&.split('::')&.[](2)}::Options").new
|
|
101
|
-
DriverFinder.new(default_options, self).driver_path
|
|
102
|
-
end
|
|
98
|
+
private
|
|
103
99
|
|
|
104
|
-
def
|
|
105
|
-
|
|
100
|
+
def warn_driver_log_override
|
|
101
|
+
WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver logging settings', id: :se_debug)
|
|
106
102
|
end
|
|
107
103
|
end # Service
|
|
108
104
|
end # WebDriver
|
|
@@ -80,7 +80,13 @@ module Selenium
|
|
|
80
80
|
def build_process(*command)
|
|
81
81
|
WebDriver.logger.debug("Executing Process #{command}", id: :driver_service)
|
|
82
82
|
@process = ChildProcess.build(*command)
|
|
83
|
-
|
|
83
|
+
if ENV.key?('SE_DEBUG')
|
|
84
|
+
if @io && @io != WebDriver.logger.io
|
|
85
|
+
WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver log output to use stderr',
|
|
86
|
+
id: :se_debug)
|
|
87
|
+
end
|
|
88
|
+
@io = WebDriver.logger.io
|
|
89
|
+
end
|
|
84
90
|
@process.io = @io if @io
|
|
85
91
|
|
|
86
92
|
@process
|
|
@@ -127,10 +133,36 @@ module Selenium
|
|
|
127
133
|
end
|
|
128
134
|
|
|
129
135
|
def connect_until_stable
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
deadline = current_time + START_TIMEOUT
|
|
137
|
+
|
|
138
|
+
loop do
|
|
139
|
+
error = check_connection_error
|
|
140
|
+
return unless error
|
|
141
|
+
|
|
142
|
+
raise Error::WebDriverError, "#{cannot_connect_error_text}: #{error}" if current_time > deadline
|
|
143
|
+
|
|
144
|
+
sleep 0.1
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def check_connection_error
|
|
149
|
+
response = Net::HTTP.start(@host, @port, open_timeout: 0.5, read_timeout: 1) do |http|
|
|
150
|
+
http.get('/status', {'Connection' => 'close'})
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return "status returned #{response.code}\n#{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
154
|
+
|
|
155
|
+
status = JSON.parse(response.body)
|
|
156
|
+
ready = status['ready'] || status.dig('value', 'ready')
|
|
157
|
+
"driver not ready: #{response.body}" unless ready
|
|
158
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT,
|
|
159
|
+
Errno::EADDRNOTAVAIL, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout,
|
|
160
|
+
EOFError, SocketError, Net::HTTPBadResponse, JSON::ParserError => e
|
|
161
|
+
"#{e.class}: #{e.message}"
|
|
162
|
+
end
|
|
132
163
|
|
|
133
|
-
|
|
164
|
+
def current_time
|
|
165
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
134
166
|
end
|
|
135
167
|
|
|
136
168
|
def cannot_connect_error_text
|
|
@@ -93,7 +93,7 @@ module Selenium
|
|
|
93
93
|
true
|
|
94
94
|
rescue *NOT_CONNECTED_ERRORS
|
|
95
95
|
sock&.close
|
|
96
|
-
WebDriver.logger.debug("polling for socket on #{[@host, @port].inspect}", id: :
|
|
96
|
+
WebDriver.logger.debug("polling for socket on #{[@host, @port].inspect}", id: :socket_poller)
|
|
97
97
|
false
|
|
98
98
|
end
|
|
99
99
|
end
|
|
@@ -36,7 +36,7 @@ module Selenium
|
|
|
36
36
|
'It should end with .png extension',
|
|
37
37
|
id: :screenshot
|
|
38
38
|
end
|
|
39
|
-
WebDriver.logger.debug("Saving screenshot to #{Dir.pwd}/#{png_path}")
|
|
39
|
+
WebDriver.logger.debug("Saving screenshot to #{Dir.pwd}/#{png_path}", id: :screenshot)
|
|
40
40
|
File.open(png_path, 'wb') { |f| f << screenshot_as(:png, full_page: full_page) }
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -47,7 +47,7 @@ module Selenium
|
|
|
47
47
|
new(id: decode(opts['credentialId']),
|
|
48
48
|
resident_credential: opts['isResidentCredential'],
|
|
49
49
|
rp_id: opts['rpId'],
|
|
50
|
-
private_key: opts['privateKey'],
|
|
50
|
+
private_key: decode(opts['privateKey']),
|
|
51
51
|
sign_count: opts['signCount'],
|
|
52
52
|
user_handle: user_handle)
|
|
53
53
|
end
|
|
@@ -37,7 +37,8 @@ module Selenium
|
|
|
37
37
|
@timeout = opts.fetch(:timeout, DEFAULT_TIMEOUT)
|
|
38
38
|
@interval = opts.fetch(:interval, DEFAULT_INTERVAL)
|
|
39
39
|
@message = opts[:message]
|
|
40
|
-
@
|
|
40
|
+
@message_provider = opts[:message_provider]
|
|
41
|
+
@ignored = Array(opts[:ignore] || Error::NoSuchElementError)
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
#
|
|
@@ -64,6 +65,8 @@ module Selenium
|
|
|
64
65
|
|
|
65
66
|
msg = if @message
|
|
66
67
|
@message.dup
|
|
68
|
+
elsif @message_provider
|
|
69
|
+
@message_provider.call
|
|
67
70
|
else
|
|
68
71
|
"timed out after #{@timeout} seconds"
|
|
69
72
|
end
|
|
@@ -24,7 +24,10 @@ module Selenium
|
|
|
24
24
|
class WebSocketConnection
|
|
25
25
|
CONNECTION_ERRORS = [
|
|
26
26
|
Errno::ECONNRESET, # connection is aborted (browser process was killed)
|
|
27
|
-
Errno::EPIPE # broken pipe (browser process was killed)
|
|
27
|
+
Errno::EPIPE, # broken pipe (browser process was killed)
|
|
28
|
+
Errno::EBADF, # file descriptor already closed (double-close or GC)
|
|
29
|
+
IOError, # Ruby socket read/write after close
|
|
30
|
+
EOFError # socket reached EOF after remote closed cleanly
|
|
28
31
|
].freeze
|
|
29
32
|
|
|
30
33
|
RESPONSE_WAIT_TIMEOUT = 30
|
|
@@ -35,6 +38,11 @@ module Selenium
|
|
|
35
38
|
def initialize(url:)
|
|
36
39
|
@callback_threads = ThreadGroup.new
|
|
37
40
|
|
|
41
|
+
@callbacks_mtx = Mutex.new
|
|
42
|
+
@messages_mtx = Mutex.new
|
|
43
|
+
@closing_mtx = Mutex.new
|
|
44
|
+
|
|
45
|
+
@closing = false
|
|
38
46
|
@session_id = nil
|
|
39
47
|
@url = url
|
|
40
48
|
|
|
@@ -43,9 +51,26 @@ module Selenium
|
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
def close
|
|
46
|
-
@
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
@closing_mtx.synchronize do
|
|
55
|
+
return if @closing
|
|
56
|
+
|
|
57
|
+
@closing = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
socket.close
|
|
62
|
+
rescue *CONNECTION_ERRORS => e
|
|
63
|
+
WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
|
|
64
|
+
# already closed
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Let threads unwind instead of calling exit
|
|
68
|
+
@socket_thread&.join(0.5)
|
|
69
|
+
@callback_threads.list.each do |thread|
|
|
70
|
+
thread.join(0.5)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
WebDriver.logger.debug "Failed to join thread during close: #{e.class}: #{e.message}", id: :ws
|
|
73
|
+
end
|
|
49
74
|
end
|
|
50
75
|
|
|
51
76
|
def callbacks
|
|
@@ -53,62 +78,73 @@ module Selenium
|
|
|
53
78
|
end
|
|
54
79
|
|
|
55
80
|
def add_callback(event, &block)
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
@callbacks_mtx.synchronize do
|
|
82
|
+
callbacks[event] << block
|
|
83
|
+
block.object_id
|
|
84
|
+
end
|
|
58
85
|
end
|
|
59
86
|
|
|
60
87
|
def remove_callback(event, id)
|
|
61
|
-
|
|
88
|
+
@callbacks_mtx.synchronize do
|
|
89
|
+
return if @closing
|
|
90
|
+
|
|
91
|
+
callbacks_for_event = callbacks[event]
|
|
92
|
+
return if callbacks_for_event.reject! { |cb| cb.object_id == id }
|
|
62
93
|
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
ids = callbacks_for_event.map(&:object_id)
|
|
95
|
+
raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
|
|
96
|
+
end
|
|
65
97
|
end
|
|
66
98
|
|
|
67
99
|
def send_cmd(**payload)
|
|
68
100
|
id = next_id
|
|
69
101
|
data = payload.merge(id: id)
|
|
70
|
-
WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :
|
|
102
|
+
WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
|
|
71
103
|
data = JSON.generate(data)
|
|
72
104
|
out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
|
|
73
|
-
socket.write(out_frame.to_s)
|
|
74
105
|
|
|
75
|
-
|
|
106
|
+
begin
|
|
107
|
+
socket.write(out_frame.to_s)
|
|
108
|
+
rescue *CONNECTION_ERRORS => e
|
|
109
|
+
raise e, "WebSocket is closed (#{e.class}: #{e.message})"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
wait.until { @messages_mtx.synchronize { messages.delete(id) } }
|
|
76
113
|
end
|
|
77
114
|
|
|
78
115
|
private
|
|
79
116
|
|
|
80
|
-
# We should be thread-safe to use the hash without synchronization
|
|
81
|
-
# because its keys are WebSocket message identifiers and they should be
|
|
82
|
-
# unique within a devtools session.
|
|
83
117
|
def messages
|
|
84
118
|
@messages ||= {}
|
|
85
119
|
end
|
|
86
120
|
|
|
87
121
|
def process_handshake
|
|
88
122
|
socket.print(ws.to_s)
|
|
89
|
-
ws << socket.readpartial(1024)
|
|
123
|
+
ws << socket.readpartial(1024) until ws.finished?
|
|
90
124
|
end
|
|
91
125
|
|
|
92
126
|
def attach_socket_listener
|
|
93
127
|
Thread.new do
|
|
94
|
-
Thread.current.abort_on_exception = true
|
|
95
128
|
Thread.current.report_on_exception = false
|
|
96
129
|
|
|
97
|
-
|
|
130
|
+
loop do
|
|
131
|
+
break if @closing
|
|
132
|
+
|
|
98
133
|
incoming_frame << socket.readpartial(1024)
|
|
99
134
|
|
|
100
135
|
while (frame = incoming_frame.next)
|
|
136
|
+
break if @closing
|
|
137
|
+
|
|
101
138
|
message = process_frame(frame)
|
|
102
139
|
next unless message['method']
|
|
103
140
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@callback_threads.add(callback_thread(params, &callback))
|
|
141
|
+
@messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
|
|
142
|
+
@callback_threads.add(callback_thread(message['params'], &callback))
|
|
107
143
|
end
|
|
108
144
|
end
|
|
109
145
|
end
|
|
110
|
-
rescue *CONNECTION_ERRORS
|
|
111
|
-
|
|
146
|
+
rescue *CONNECTION_ERRORS, WebSocket::Error => e
|
|
147
|
+
WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
|
|
112
148
|
end
|
|
113
149
|
end
|
|
114
150
|
|
|
@@ -122,27 +158,27 @@ module Selenium
|
|
|
122
158
|
# Firefox will periodically fail on unparsable empty frame
|
|
123
159
|
return {} if message.empty?
|
|
124
160
|
|
|
125
|
-
|
|
126
|
-
messages[
|
|
127
|
-
WebDriver.logger.debug "WebSocket <- #{message}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi
|
|
161
|
+
msg = JSON.parse(message)
|
|
162
|
+
@messages_mtx.synchronize { messages[msg['id']] = msg if msg.key?('id') }
|
|
128
163
|
|
|
129
|
-
|
|
164
|
+
WebDriver.logger.debug "WebSocket <- #{msg}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
|
|
165
|
+
msg
|
|
130
166
|
end
|
|
131
167
|
|
|
132
168
|
def callback_thread(params)
|
|
133
169
|
Thread.new do
|
|
134
|
-
Thread.current.abort_on_exception =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# For example, if network interception event raises error,
|
|
138
|
-
# the browser will keep waiting for the request to be proceeded
|
|
139
|
-
# before returning back to the original thread. In this case,
|
|
140
|
-
# we should at least print the error.
|
|
141
|
-
Thread.current.report_on_exception = true
|
|
170
|
+
Thread.current.abort_on_exception = false
|
|
171
|
+
Thread.current.report_on_exception = false
|
|
172
|
+
next if @closing
|
|
142
173
|
|
|
143
174
|
yield params
|
|
144
|
-
rescue Error::WebDriverError, *CONNECTION_ERRORS
|
|
145
|
-
|
|
175
|
+
rescue Error::WebDriverError, *CONNECTION_ERRORS => e
|
|
176
|
+
WebDriver.logger.debug "Callback aborted: #{e.class}: #{e.message}", id: :ws
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
next if @closing
|
|
179
|
+
|
|
180
|
+
bt = Array(e.backtrace).first(5).join("\n")
|
|
181
|
+
WebDriver.logger.error "Callback error: #{e.class}: #{e.message}\n#{bt}", id: :ws
|
|
146
182
|
end
|
|
147
183
|
end
|
|
148
184
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# under the License.
|
|
19
19
|
|
|
20
20
|
require 'selenium/webdriver/common/error'
|
|
21
|
+
require 'selenium/webdriver/common/client_config'
|
|
21
22
|
require 'selenium/webdriver/common/local_driver'
|
|
22
23
|
require 'selenium/webdriver/common/driver_finder'
|
|
23
24
|
require 'selenium/webdriver/common/platform'
|
|
@@ -75,9 +76,9 @@ require 'selenium/webdriver/common/driver_extensions/prints_page'
|
|
|
75
76
|
require 'selenium/webdriver/common/driver_extensions/uploads_files'
|
|
76
77
|
require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
|
|
77
78
|
require 'selenium/webdriver/common/driver_extensions/has_addons'
|
|
78
|
-
require 'selenium/webdriver/common/driver_extensions/has_bidi'
|
|
79
79
|
require 'selenium/webdriver/common/driver_extensions/has_devtools'
|
|
80
80
|
require 'selenium/webdriver/common/driver_extensions/has_file_downloads'
|
|
81
|
+
require 'selenium/webdriver/common/driver_extensions/has_session_events'
|
|
81
82
|
require 'selenium/webdriver/common/driver_extensions/has_authentication'
|
|
82
83
|
require 'selenium/webdriver/common/driver_extensions/has_logs'
|
|
83
84
|
require 'selenium/webdriver/common/driver_extensions/has_log_events'
|
|
@@ -34,9 +34,9 @@ module Selenium
|
|
|
34
34
|
id: id,
|
|
35
35
|
code: params['responseStatusCode'],
|
|
36
36
|
body: (Base64.strict_decode64(encoded_body) if encoded_body),
|
|
37
|
-
headers: params.fetch('responseHeaders', []).
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
headers: params.fetch('responseHeaders', []).to_h do |header|
|
|
38
|
+
[header['name'], header['value']]
|
|
39
|
+
end
|
|
40
40
|
)
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -84,7 +84,7 @@ module Selenium
|
|
|
84
84
|
def start_session(target_type:)
|
|
85
85
|
targets = target.get_targets.dig('result', 'targetInfos')
|
|
86
86
|
found_target = targets.find { |target| target['type'] == target_type }
|
|
87
|
-
raise Error::
|
|
87
|
+
raise Error::NoSuchTargetError, "Target type '#{target_type}' not found" unless found_target
|
|
88
88
|
|
|
89
89
|
session = target.attach_to_target(target_id: found_target['targetId'], flatten: true)
|
|
90
90
|
@session_id = session.dig('result', 'sessionId')
|
|
@@ -30,9 +30,10 @@ module Selenium
|
|
|
30
30
|
class Driver < Chromium::Driver
|
|
31
31
|
include LocalDriver
|
|
32
32
|
|
|
33
|
-
def initialize(options: nil, service: nil, url: nil, **)
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
def initialize(options: nil, service: nil, url: nil, http_client: nil, client_config: nil, **)
|
|
34
|
+
initialize_local_driver(options, service, url, http_client, client_config) do |caps, client|
|
|
35
|
+
super(caps: caps, http_client: client, **)
|
|
36
|
+
end
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def browser
|
|
@@ -25,6 +25,20 @@ module Selenium
|
|
|
25
25
|
EXECUTABLE = 'msedgedriver'
|
|
26
26
|
SHUTDOWN_SUPPORTED = true
|
|
27
27
|
DRIVER_PATH_ENV_KEY = 'SE_EDGEDRIVER'
|
|
28
|
+
|
|
29
|
+
def initialize(args: nil, **)
|
|
30
|
+
args = Array(args.dup)
|
|
31
|
+
# yes, it is --enable-chrome-logs, even on msedgedriver; skip when CHROME_LOG_FILE is set
|
|
32
|
+
args << '--enable-chrome-logs' unless ENV.key?('CHROME_LOG_FILE') || args.include?('--enable-chrome-logs')
|
|
33
|
+
|
|
34
|
+
if ENV.key?('SE_DEBUG')
|
|
35
|
+
warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
|
|
36
|
+
args << '--verbose'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
28
42
|
def log
|
|
29
43
|
return @log unless @log.is_a? String
|
|
30
44
|
|
|
@@ -29,16 +29,14 @@ module Selenium
|
|
|
29
29
|
EXTENSIONS = [DriverExtensions::HasAddons,
|
|
30
30
|
DriverExtensions::FullPageScreenshot,
|
|
31
31
|
DriverExtensions::HasContext,
|
|
32
|
-
DriverExtensions::HasBiDi,
|
|
33
|
-
DriverExtensions::HasLogEvents,
|
|
34
|
-
DriverExtensions::HasNetworkInterception,
|
|
35
32
|
DriverExtensions::PrintsPage].freeze
|
|
36
33
|
|
|
37
34
|
include LocalDriver
|
|
38
35
|
|
|
39
|
-
def initialize(options: nil, service: nil, url: nil, **)
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
def initialize(options: nil, service: nil, url: nil, http_client: nil, client_config: nil, **)
|
|
37
|
+
initialize_local_driver(options, service, url, http_client, client_config) do |caps, client|
|
|
38
|
+
super(caps: caps, http_client: client, **)
|
|
39
|
+
end
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
def browser
|
|
@@ -69,6 +69,7 @@ module Selenium
|
|
|
69
69
|
@options[:prefs]['remote.active-protocols'] = 1
|
|
70
70
|
@options[:env] ||= {}
|
|
71
71
|
@options[:log] ||= {level: log_level} if log_level
|
|
72
|
+
@vendor_options = {}
|
|
72
73
|
|
|
73
74
|
process_profile(@options.delete(:profile))
|
|
74
75
|
end
|
|
@@ -102,6 +103,23 @@ module Selenium
|
|
|
102
103
|
@options[:prefs][name] = value
|
|
103
104
|
end
|
|
104
105
|
|
|
106
|
+
#
|
|
107
|
+
# Add a Firefox-specific capability nested in the `moz:firefoxOptions`
|
|
108
|
+
# object that is not yet exposed through a dedicated method. This is the
|
|
109
|
+
# escape hatch for legitimate vendor capabilities the bindings do not model.
|
|
110
|
+
#
|
|
111
|
+
# @example Set an option not handled by other methods
|
|
112
|
+
# options = Selenium::WebDriver::Firefox::Options.new
|
|
113
|
+
# options.add_firefox_option('unhandledCapability', 'value')
|
|
114
|
+
#
|
|
115
|
+
# @param [String, Symbol] name Name of the capability, as expected by geckodriver
|
|
116
|
+
# @param [Object] value Value of the capability
|
|
117
|
+
#
|
|
118
|
+
|
|
119
|
+
def add_firefox_option(name, value)
|
|
120
|
+
@vendor_options[name.to_s] = value
|
|
121
|
+
end
|
|
122
|
+
|
|
105
123
|
#
|
|
106
124
|
# Sets Firefox profile.
|
|
107
125
|
#
|
|
@@ -153,6 +171,7 @@ module Selenium
|
|
|
153
171
|
def process_browser_options(browser_options)
|
|
154
172
|
browser_options['moz:debuggerAddress'] = true if @debugger_address
|
|
155
173
|
options = browser_options[KEY]
|
|
174
|
+
options.merge!(@vendor_options)
|
|
156
175
|
options['binary'] ||= Firefox.path if Firefox.path
|
|
157
176
|
options['profile'] = @profile if @profile
|
|
158
177
|
end
|