selenium-webdriver 4.34.0 → 4.41.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 +62 -6
- data/Gemfile +1 -1
- data/LICENSE +1 -1
- data/NOTICE +1 -1
- data/bin/linux/selenium-manager +0 -0
- data/bin/macos/selenium-manager +0 -0
- data/bin/windows/selenium-manager.exe +0 -0
- data/lib/selenium/server.rb +29 -4
- data/lib/selenium/webdriver/atoms/findElements.js +63 -49
- data/lib/selenium/webdriver/atoms/getAttribute.js +17 -6
- data/lib/selenium/webdriver/atoms/isDisplayed.js +34 -23
- 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_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/session.rb +4 -0
- data/lib/selenium/webdriver/bidi.rb +1 -1
- data/lib/selenium/webdriver/chrome/driver.rb +3 -2
- data/lib/selenium/webdriver/chrome/service.rb +10 -0
- data/lib/selenium/webdriver/common/child_process.rb +2 -1
- data/lib/selenium/webdriver/common/driver.rb +0 -5
- data/lib/selenium/webdriver/common/driver_extensions/has_file_downloads.rb +7 -1
- data/lib/selenium/webdriver/common/driver_extensions/has_session_events.rb +48 -0
- data/lib/selenium/webdriver/common/error.rb +10 -3
- data/lib/selenium/webdriver/common/local_driver.rb +11 -1
- 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 +20 -1
- data/lib/selenium/webdriver/common/platform.rb +1 -3
- data/lib/selenium/webdriver/common/service.rb +6 -0
- 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/wait.rb +5 -2
- data/lib/selenium/webdriver/common/websocket_connection.rb +73 -37
- data/lib/selenium/webdriver/common/zipper.rb +12 -2
- data/lib/selenium/webdriver/common.rb +1 -0
- data/lib/selenium/webdriver/devtools.rb +1 -1
- data/lib/selenium/webdriver/edge/driver.rb +3 -2
- data/lib/selenium/webdriver/edge/service.rb +11 -0
- data/lib/selenium/webdriver/firefox/driver.rb +3 -2
- data/lib/selenium/webdriver/firefox/service.rb +21 -2
- data/lib/selenium/webdriver/ie/driver.rb +3 -2
- data/lib/selenium/webdriver/ie/service.rb +10 -0
- data/lib/selenium/webdriver/remote/bidi_bridge.rb +4 -2
- data/lib/selenium/webdriver/remote/bridge.rb +12 -4
- data/lib/selenium/webdriver/remote/driver.rb +1 -0
- data/lib/selenium/webdriver/remote/features.rb +26 -1
- data/lib/selenium/webdriver/remote/http/common.rb +32 -0
- data/lib/selenium/webdriver/safari/driver.rb +3 -2
- 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/version.rb +1 -1
- data/lib/selenium/webdriver.rb +7 -2
- data/selenium-webdriver.gemspec +1 -1
- metadata +6 -10
- data/lib/selenium/webdriver/bidi/log/base_log_entry.rb +0 -35
- data/lib/selenium/webdriver/bidi/log/console_log_entry.rb +0 -35
- data/lib/selenium/webdriver/bidi/log/filter_by.rb +0 -40
- 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
|
@@ -39,7 +39,13 @@ module Selenium
|
|
|
39
39
|
|
|
40
40
|
begin
|
|
41
41
|
Zip::File.open("#{file_name}.zip") do |zip|
|
|
42
|
-
zip.each
|
|
42
|
+
zip.each do |entry|
|
|
43
|
+
if Zipper::RUBYZIP_V3
|
|
44
|
+
zip.extract(entry, file_name, destination_directory: target_directory)
|
|
45
|
+
else
|
|
46
|
+
zip.extract(entry, "#{target_directory}#{file_name}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
43
49
|
end
|
|
44
50
|
ensure
|
|
45
51
|
FileUtils.rm_f("#{file_name}.zip")
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
module DriverExtensions
|
|
23
|
+
module HasSessionEvents
|
|
24
|
+
#
|
|
25
|
+
# Fires a custom session event to the remote server event bus.
|
|
26
|
+
# This allows test code to trigger server-side utilities that subscribe to
|
|
27
|
+
# the event bus.
|
|
28
|
+
#
|
|
29
|
+
# @param [String] event_type The type of event (e.g., "test:failed", "log:collect")
|
|
30
|
+
# @param [Hash] payload Optional data to include with the event
|
|
31
|
+
# @return [Hash] Response data including success status, event type, and timestamp
|
|
32
|
+
#
|
|
33
|
+
# @example Fire a simple event
|
|
34
|
+
# driver.fire_session_event("test:started")
|
|
35
|
+
#
|
|
36
|
+
# @example Fire an event with payload
|
|
37
|
+
# driver.fire_session_event("test:failed", {
|
|
38
|
+
# testName: "LoginTest",
|
|
39
|
+
# error: "Element not found"
|
|
40
|
+
# })
|
|
41
|
+
#
|
|
42
|
+
def fire_session_event(event_type, payload = nil)
|
|
43
|
+
@bridge.fire_session_event(event_type, payload)
|
|
44
|
+
end
|
|
45
|
+
end # HasSessionEvents
|
|
46
|
+
end # DriverExtensions
|
|
47
|
+
end # WebDriver
|
|
48
|
+
end # Selenium
|
|
@@ -38,9 +38,9 @@ module Selenium
|
|
|
38
38
|
ERROR_URL = 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors'
|
|
39
39
|
|
|
40
40
|
URLS = {
|
|
41
|
-
NoSuchElementError: "#{ERROR_URL}#
|
|
42
|
-
StaleElementReferenceError: "#{ERROR_URL}#
|
|
43
|
-
InvalidSelectorError: "#{ERROR_URL}#
|
|
41
|
+
NoSuchElementError: "#{ERROR_URL}#nosuchelementexception",
|
|
42
|
+
StaleElementReferenceError: "#{ERROR_URL}#staleelementreferenceexception",
|
|
43
|
+
InvalidSelectorError: "#{ERROR_URL}#invalidselectorexception",
|
|
44
44
|
NoSuchDriverError: "#{ERROR_URL}/driver_location"
|
|
45
45
|
}.freeze
|
|
46
46
|
|
|
@@ -119,6 +119,13 @@ module Selenium
|
|
|
119
119
|
|
|
120
120
|
class NoSuchWindowError < WebDriverError; end
|
|
121
121
|
|
|
122
|
+
#
|
|
123
|
+
# A command to find a devtools target could not be satisfied because
|
|
124
|
+
# the target could not be found.
|
|
125
|
+
#
|
|
126
|
+
|
|
127
|
+
class NoSuchTargetError < WebDriverError; end
|
|
128
|
+
|
|
122
129
|
#
|
|
123
130
|
# The element does not have a shadow root.
|
|
124
131
|
#
|
|
@@ -27,7 +27,17 @@ module Selenium
|
|
|
27
27
|
caps = process_options(options, service)
|
|
28
28
|
url = service_url(service)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
begin
|
|
31
|
+
yield(caps, url) if block_given?
|
|
32
|
+
rescue Selenium::WebDriver::Error::WebDriverError
|
|
33
|
+
@service_manager&.stop
|
|
34
|
+
raise
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def service_url(service)
|
|
39
|
+
@service_manager = service.launch
|
|
40
|
+
@service_manager.uri
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
def process_options(options, service)
|
|
@@ -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,6 +71,8 @@ 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
|
|
76
78
|
end
|
|
@@ -91,6 +93,14 @@ module Selenium
|
|
|
91
93
|
@options[name] = value
|
|
92
94
|
end
|
|
93
95
|
|
|
96
|
+
def enable_bidi!
|
|
97
|
+
@options[:web_socket_url] = true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def bidi?
|
|
101
|
+
!!@options[:web_socket_url]
|
|
102
|
+
end
|
|
103
|
+
|
|
94
104
|
def ==(other)
|
|
95
105
|
return false unless other.is_a? self.class
|
|
96
106
|
|
|
@@ -131,11 +141,20 @@ module Selenium
|
|
|
131
141
|
|
|
132
142
|
def process_w3c_options(options)
|
|
133
143
|
w3c_options = options.select { |key, val| w3c?(key) && !val.nil? }
|
|
134
|
-
w3c_options[:unhandled_prompt_behavior] &&=
|
|
144
|
+
w3c_options[:unhandled_prompt_behavior] &&=
|
|
145
|
+
process_unhandled_prompt_behavior_value(w3c_options[:unhandled_prompt_behavior])
|
|
135
146
|
options.delete_if { |key, _val| w3c?(key) }
|
|
136
147
|
w3c_options
|
|
137
148
|
end
|
|
138
149
|
|
|
150
|
+
def process_unhandled_prompt_behavior_value(value)
|
|
151
|
+
if value.is_a?(Hash)
|
|
152
|
+
value.transform_values { |v| process_unhandled_prompt_behavior_value(v) }
|
|
153
|
+
else
|
|
154
|
+
value&.to_s&.tr('_', ' ')
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
139
158
|
def process_browser_options(_browser_options)
|
|
140
159
|
nil
|
|
141
160
|
end
|
|
@@ -104,6 +104,12 @@ module Selenium
|
|
|
104
104
|
def env_path
|
|
105
105
|
ENV.fetch(self.class::DRIVER_PATH_ENV_KEY, nil)
|
|
106
106
|
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def warn_driver_log_override
|
|
111
|
+
WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver logging settings', id: :se_debug)
|
|
112
|
+
end
|
|
107
113
|
end # Service
|
|
108
114
|
end # WebDriver
|
|
109
115
|
end # Selenium
|
|
@@ -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
|
|
@@ -29,7 +29,7 @@ module Selenium
|
|
|
29
29
|
# @param [Hash] opts Options for this instance
|
|
30
30
|
# @option opts [Numeric] :timeout (5) Seconds to wait before timing out.
|
|
31
31
|
# @option opts [Numeric] :interval (0.2) Seconds to sleep between polls.
|
|
32
|
-
# @option opts [String] :message Exception
|
|
32
|
+
# @option opts [String] :message Exception message if timed out.
|
|
33
33
|
# @option opts [Array, Exception] :ignore Exceptions to ignore while polling (default: Error::NoSuchElementError)
|
|
34
34
|
#
|
|
35
35
|
|
|
@@ -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 'zip'
|
|
21
|
+
require 'zip/version' # Not required automatically
|
|
21
22
|
require 'tempfile'
|
|
22
23
|
require 'find'
|
|
23
24
|
require 'base64'
|
|
@@ -30,6 +31,7 @@ module Selenium
|
|
|
30
31
|
|
|
31
32
|
module Zipper
|
|
32
33
|
EXTENSIONS = %w[.zip .xpi].freeze
|
|
34
|
+
RUBYZIP_V3 = Zip::VERSION >= '3.0.0'
|
|
33
35
|
|
|
34
36
|
class << self
|
|
35
37
|
def unzip(path)
|
|
@@ -42,7 +44,11 @@ module Selenium
|
|
|
42
44
|
dirname = File.dirname(to)
|
|
43
45
|
|
|
44
46
|
FileUtils.mkdir_p dirname
|
|
45
|
-
|
|
47
|
+
if RUBYZIP_V3
|
|
48
|
+
zip.extract(entry, entry.name, destination_directory: destination)
|
|
49
|
+
else
|
|
50
|
+
zip.extract(entry, to)
|
|
51
|
+
end
|
|
46
52
|
end
|
|
47
53
|
end
|
|
48
54
|
|
|
@@ -75,7 +81,11 @@ module Selenium
|
|
|
75
81
|
# Don't use Tempfile since it lacks rb_file_s_rename permission on Windows.
|
|
76
82
|
Dir.mktmpdir do |tmp_dir|
|
|
77
83
|
zip_path = File.join(tmp_dir, 'webdriver-zip')
|
|
78
|
-
|
|
84
|
+
if RUBYZIP_V3
|
|
85
|
+
Zip::File.open(zip_path, create: true, &blk)
|
|
86
|
+
else
|
|
87
|
+
Zip::File.open(zip_path, Zip::File::CREATE, &blk)
|
|
88
|
+
end
|
|
79
89
|
end
|
|
80
90
|
end
|
|
81
91
|
|
|
@@ -78,6 +78,7 @@ require 'selenium/webdriver/common/driver_extensions/has_addons'
|
|
|
78
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'
|
|
@@ -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')
|
|
@@ -31,8 +31,9 @@ module Selenium
|
|
|
31
31
|
include LocalDriver
|
|
32
32
|
|
|
33
33
|
def initialize(options: nil, service: nil, url: nil, **)
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
initialize_local_driver(options, service, url) do |caps, driver_url|
|
|
35
|
+
super(caps: caps, url: driver_url, **)
|
|
36
|
+
end
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def browser
|
|
@@ -25,6 +25,17 @@ 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
|
+
if ENV.key?('SE_DEBUG')
|
|
31
|
+
args = Array(args.dup)
|
|
32
|
+
warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
|
|
33
|
+
args << '--verbose'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
28
39
|
def log
|
|
29
40
|
return @log unless @log.is_a? String
|
|
30
41
|
|
|
@@ -37,8 +37,9 @@ module Selenium
|
|
|
37
37
|
include LocalDriver
|
|
38
38
|
|
|
39
39
|
def initialize(options: nil, service: nil, url: nil, **)
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
initialize_local_driver(options, service, url) do |caps, driver_url|
|
|
41
|
+
super(caps: caps, url: driver_url, **)
|
|
42
|
+
end
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def browser
|
|
@@ -26,14 +26,33 @@ module Selenium
|
|
|
26
26
|
SHUTDOWN_SUPPORTED = false
|
|
27
27
|
DRIVER_PATH_ENV_KEY = 'SE_GECKODRIVER'
|
|
28
28
|
|
|
29
|
-
def initialize(
|
|
30
|
-
args
|
|
29
|
+
def initialize(args: nil, **)
|
|
30
|
+
args = Array(args.dup)
|
|
31
31
|
unless args.any? { |arg| arg.include?('--connect-existing') || arg.include?('--websocket-port') }
|
|
32
32
|
args << '--websocket-port'
|
|
33
33
|
args << '0'
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
if ENV.key?('SE_DEBUG')
|
|
37
|
+
remove_log_args(args)
|
|
38
|
+
args << '-v'
|
|
39
|
+
end
|
|
40
|
+
|
|
35
41
|
super
|
|
36
42
|
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def remove_log_args(args)
|
|
47
|
+
if (index = args.index('--log'))
|
|
48
|
+
args.delete_at(index) # delete '--log'
|
|
49
|
+
args.delete_at(index) if args[index] && !args[index].start_with?('-') # delete value if present
|
|
50
|
+
warn_driver_log_override
|
|
51
|
+
elsif (index = args.index { |arg| arg.start_with?('--log=') })
|
|
52
|
+
args.delete_at(index)
|
|
53
|
+
warn_driver_log_override
|
|
54
|
+
end
|
|
55
|
+
end
|
|
37
56
|
end # Service
|
|
38
57
|
end # Firefox
|
|
39
58
|
end # WebDriver
|