selenium-webdriver 4.39.0 → 4.40.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 +13 -0
- 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 +62 -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 +7 -0
- data/lib/selenium/webdriver/bidi/browsing_context.rb +2 -1
- data/lib/selenium/webdriver/bidi/log_handler.rb +5 -0
- data/lib/selenium/webdriver/bidi/network/cookies.rb +4 -0
- 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 +4 -0
- data/lib/selenium/webdriver/bidi/network/intercepted_response.rb +4 -0
- data/lib/selenium/webdriver/bidi/network/url_pattern.rb +4 -0
- data/lib/selenium/webdriver/bidi/network.rb +6 -0
- data/lib/selenium/webdriver/bidi/session.rb +4 -0
- data/lib/selenium/webdriver/chrome/driver.rb +3 -2
- data/lib/selenium/webdriver/common/driver.rb +0 -5
- data/lib/selenium/webdriver/common/local_driver.rb +11 -1
- data/lib/selenium/webdriver/common/options.rb +10 -0
- data/lib/selenium/webdriver/common/service_manager.rb +29 -3
- data/lib/selenium/webdriver/common/socket_poller.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/edge/driver.rb +3 -2
- data/lib/selenium/webdriver/firefox/driver.rb +3 -2
- data/lib/selenium/webdriver/ie/driver.rb +3 -2
- data/lib/selenium/webdriver/remote/bidi_bridge.rb +4 -2
- data/lib/selenium/webdriver/remote/bridge.rb +12 -4
- data/lib/selenium/webdriver/remote/http/common.rb +32 -0
- data/lib/selenium/webdriver/safari/driver.rb +3 -2
- data/lib/selenium/webdriver/version.rb +1 -1
- data/lib/selenium/webdriver.rb +1 -1
- metadata +2 -2
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -32,8 +32,9 @@ module Selenium
|
|
|
32
32
|
include LocalDriver
|
|
33
33
|
|
|
34
34
|
def initialize(options: nil, service: nil, url: nil, **)
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
initialize_local_driver(options, service, url) do |caps, driver_url|
|
|
36
|
+
super(caps: caps, url: driver_url, **)
|
|
37
|
+
end
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def browser
|
|
@@ -206,12 +206,20 @@ module Selenium
|
|
|
206
206
|
switch_to_frame nil
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
-
QUIT_ERRORS = [IOError].freeze
|
|
209
|
+
QUIT_ERRORS = [IOError, EOFError, WebSocket::Error].freeze
|
|
210
210
|
|
|
211
211
|
def quit
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
begin
|
|
213
|
+
execute :delete_session
|
|
214
|
+
rescue *QUIT_ERRORS => e
|
|
215
|
+
WebDriver.logger.debug "delete_session failed during quit: #{e.class}: #{e.message}", id: :quit
|
|
216
|
+
ensure
|
|
217
|
+
begin
|
|
218
|
+
http.close
|
|
219
|
+
rescue *QUIT_ERRORS => e
|
|
220
|
+
WebDriver.logger.debug "http.close failed during quit: #{e.class}: #{e.message}", id: :quit
|
|
221
|
+
end
|
|
222
|
+
end
|
|
215
223
|
nil
|
|
216
224
|
end
|
|
217
225
|
|
|
@@ -28,6 +28,7 @@ module Selenium
|
|
|
28
28
|
'Accept' => CONTENT_TYPE,
|
|
29
29
|
'Content-Type' => "#{CONTENT_TYPE}; charset=UTF-8"
|
|
30
30
|
}.freeze
|
|
31
|
+
BINARY_ENCODINGS = [Encoding::BINARY, Encoding::ASCII_8BIT].freeze
|
|
31
32
|
|
|
32
33
|
class << self
|
|
33
34
|
attr_accessor :extra_headers
|
|
@@ -55,6 +56,7 @@ module Selenium
|
|
|
55
56
|
headers['Cache-Control'] = 'no-cache' if verb == :get
|
|
56
57
|
|
|
57
58
|
if command_hash
|
|
59
|
+
command_hash = ensure_utf8_encoding(command_hash)
|
|
58
60
|
payload = JSON.generate(command_hash)
|
|
59
61
|
headers['Content-Length'] = payload.bytesize.to_s if %i[post put].include?(verb)
|
|
60
62
|
|
|
@@ -91,6 +93,36 @@ module Selenium
|
|
|
91
93
|
raise NotImplementedError, 'subclass responsibility'
|
|
92
94
|
end
|
|
93
95
|
|
|
96
|
+
def ensure_utf8_encoding(obj)
|
|
97
|
+
case obj
|
|
98
|
+
when String
|
|
99
|
+
encode_string_to_utf8(obj)
|
|
100
|
+
when Array
|
|
101
|
+
obj.map { |item| ensure_utf8_encoding(item) }
|
|
102
|
+
when Hash
|
|
103
|
+
obj.each_with_object({}) do |(key, value), result|
|
|
104
|
+
result[ensure_utf8_encoding(key)] = ensure_utf8_encoding(value)
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
obj
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def encode_string_to_utf8(str)
|
|
112
|
+
return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
|
|
113
|
+
|
|
114
|
+
if BINARY_ENCODINGS.include?(str.encoding)
|
|
115
|
+
result = str.dup.force_encoding(Encoding::UTF_8)
|
|
116
|
+
return result if result.valid_encoding?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
str.encode(Encoding::UTF_8)
|
|
120
|
+
rescue EncodingError => e
|
|
121
|
+
raise Error::WebDriverError,
|
|
122
|
+
"Unable to encode string to UTF-8: #{e.message}. " \
|
|
123
|
+
"String encoding: #{str.encoding}, content: #{str.inspect}"
|
|
124
|
+
end
|
|
125
|
+
|
|
94
126
|
def create_response(code, body, content_type)
|
|
95
127
|
code = code.to_i
|
|
96
128
|
body = body.to_s.strip
|
|
@@ -32,8 +32,9 @@ module Selenium
|
|
|
32
32
|
include LocalDriver
|
|
33
33
|
|
|
34
34
|
def initialize(options: nil, service: nil, url: nil, **)
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
initialize_local_driver(options, service, url) do |caps, driver_url|
|
|
36
|
+
super(caps: caps, url: driver_url, **)
|
|
37
|
+
end
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def browser
|
data/lib/selenium/webdriver.rb
CHANGED
|
@@ -95,7 +95,7 @@ module Selenium
|
|
|
95
95
|
#
|
|
96
96
|
|
|
97
97
|
def self.logger(**)
|
|
98
|
-
level = $DEBUG || ENV.key?('DEBUG') ? :debug : :info
|
|
98
|
+
level = $DEBUG || ENV.key?('DEBUG') || ENV.key?('SE_DEBUG') ? :debug : :info
|
|
99
99
|
@logger ||= WebDriver::Logger.new('Selenium', default_level: level, **)
|
|
100
100
|
end
|
|
101
101
|
end # WebDriver
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: selenium-webdriver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Rodionov
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date:
|
|
13
|
+
date: 2026-01-18 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: base64
|