selenium-webdriver 4.47.0 → 4.48.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 +10 -0
- data/bin/linux/selenium-manager +0 -0
- data/bin/macos/selenium-manager +0 -0
- data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +89 -120
- data/bin/selenium-manager.cdx.json +875 -796
- data/bin/windows/selenium-manager.exe +0 -0
- data/lib/selenium/webdriver/bidi/error.rb +4 -0
- data/lib/selenium/webdriver/bidi/serialization/record.rb +38 -41
- data/lib/selenium/webdriver/bidi/serialization/union.rb +11 -7
- data/lib/selenium/webdriver/bidi/serialization.rb +2 -14
- data/lib/selenium/webdriver/common/websocket_connection.rb +64 -26
- data/lib/selenium/webdriver/version.rb +1 -1
- metadata +2 -2
|
Binary file
|
|
@@ -23,6 +23,10 @@ require 'selenium/webdriver/bidi/protocol/error_code'
|
|
|
23
23
|
module Selenium
|
|
24
24
|
module WebDriver
|
|
25
25
|
module Error
|
|
26
|
+
# Raised locally when a BiDi wire payload does not match this Selenium's generated
|
|
27
|
+
# schema. It is not a protocol error code; the (de)serialization layer raises it directly.
|
|
28
|
+
class SerializationError < WebDriverError; end
|
|
29
|
+
|
|
26
30
|
# Register each BiDi-only code as a WebDriverError subclass; shared codes keep their classic class.
|
|
27
31
|
BiDi::Protocol::ErrorCode::CLASS_NAMES.each_value do |name|
|
|
28
32
|
const_set(name, Class.new(WebDriverError)) unless const_defined?(name, false)
|
|
@@ -79,13 +79,13 @@ module Selenium
|
|
|
79
79
|
construct(**attributes)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# Inbound: builds from the wire. A missing required field
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
82
|
+
# Inbound: builds from the wire. A missing required field raises (in +wire_value+); enum
|
|
83
|
+
# tokens are mapped back to symbols and an unrecognized one raises (in +read+); an
|
|
84
|
+
# undeclared property is captured silently (extensible) or warned and dropped (closed)
|
|
85
|
+
# — strict on shape, lenient on extras.
|
|
86
86
|
def from_json(json_payload)
|
|
87
87
|
unless json_payload.is_a?(::Hash)
|
|
88
|
-
raise Error::
|
|
88
|
+
raise Error::SerializationError, "#{name} expected an object on the wire, got #{json_payload.inspect}"
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
attributes = fields.to_h do |f|
|
|
@@ -173,8 +173,8 @@ module Selenium
|
|
|
173
173
|
|
|
174
174
|
# Outbound mirror of scalar_value: a bare map key must match one of the arm's primitives.
|
|
175
175
|
def check_outbound_scalar(field, value)
|
|
176
|
-
|
|
177
|
-
return if
|
|
176
|
+
checks = Array(field.scalar).filter_map { |primitive| PRIMITIVE_CHECKS[primitive] }
|
|
177
|
+
return if checks.empty? || checks.any? { |check| check.call(value) }
|
|
178
178
|
|
|
179
179
|
raise ::ArgumentError,
|
|
180
180
|
"#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
|
|
@@ -204,8 +204,8 @@ module Selenium
|
|
|
204
204
|
# ArgumentError here rather than a rejection the browser reports a round-trip later. A field
|
|
205
205
|
# with no primitive descriptor (enum, ref, opaque) passes; lists are skipped, as inbound does.
|
|
206
206
|
def check_outbound_primitive(field, value)
|
|
207
|
-
|
|
208
|
-
return if
|
|
207
|
+
check = PRIMITIVE_CHECKS[field.primitive]
|
|
208
|
+
return if check.nil? || check.call(value)
|
|
209
209
|
|
|
210
210
|
raise ::ArgumentError, "#{name}##{field.name} expected #{field.primitive}, got #{value.inspect}"
|
|
211
211
|
end
|
|
@@ -214,38 +214,32 @@ module Selenium
|
|
|
214
214
|
!UNSET.equal?(field.fixed)
|
|
215
215
|
end
|
|
216
216
|
|
|
217
|
+
# A required field absent from the response cannot yield a valid typed object, so it raises
|
|
218
|
+
# rather than substitute a placeholder or represent the field as omitted; a remote end that
|
|
219
|
+
# lags the schema is handled by a project schema override, not by runtime tolerance.
|
|
217
220
|
def wire_value(field, json_payload)
|
|
218
221
|
return field.fixed if fixed?(field)
|
|
219
222
|
return read(field, json_payload[field.wire_key]) if json_payload.key?(field.wire_key)
|
|
220
223
|
return UNSET unless field.required
|
|
221
224
|
|
|
222
|
-
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# A required field absent from the response is tolerated as omitted (UNSET) and warned, so a
|
|
226
|
-
# schema ahead of the browser does not block the caller; strict mode (SE_BIDI_STRICT) escalates
|
|
227
|
-
# to an error for callers who want it. Omitted (UNSET) stays distinct from an explicit null (nil),
|
|
228
|
-
# which matters for the required-and-nullable fields the schema flags.
|
|
229
|
-
def missing_required(field)
|
|
230
|
-
message = "#{name}##{field.name} is required but was missing from the response"
|
|
231
|
-
raise Error::WebDriverError, message if Serialization.strict?
|
|
232
|
-
|
|
233
|
-
WebDriver.logger.warn(message, id: :bidi_missing_required)
|
|
234
|
-
UNSET
|
|
225
|
+
raise Error::SerializationError, "#{name}##{field.name} is required but was missing from the response"
|
|
235
226
|
end
|
|
236
227
|
|
|
237
228
|
def read(field, raw)
|
|
238
229
|
if raw.nil?
|
|
239
230
|
return raw if field.nullable
|
|
240
231
|
|
|
241
|
-
raise Error::
|
|
232
|
+
raise Error::SerializationError, "#{name}##{field.name} received null but is not nullable"
|
|
242
233
|
end
|
|
243
234
|
check_shape(field, raw)
|
|
244
235
|
return Serialization.to_symbol("#{name}##{field.name}", raw, enum_hash(field)) if field.enum
|
|
245
236
|
|
|
246
237
|
if field.ref.nil?
|
|
247
|
-
|
|
248
|
-
|
|
238
|
+
return raw if field.list
|
|
239
|
+
|
|
240
|
+
check_primitive(field, raw)
|
|
241
|
+
# A whole number is exact in both types, so the declared type is held with nothing lost.
|
|
242
|
+
return field.primitive == 'integer' && raw.is_a?(::Float) ? raw.to_i : raw
|
|
249
243
|
end
|
|
250
244
|
|
|
251
245
|
read_ref(field, raw)
|
|
@@ -268,24 +262,27 @@ module Selenium
|
|
|
268
262
|
return if field.list == raw.is_a?(::Array)
|
|
269
263
|
return unless field.list || field.enum || field.ref
|
|
270
264
|
|
|
271
|
-
raise Error::
|
|
265
|
+
raise Error::SerializationError,
|
|
272
266
|
"#{name}##{field.name} expected #{field.list ? 'a list' : 'a single value'}, got #{raw.inspect}"
|
|
273
267
|
end
|
|
274
268
|
|
|
275
|
-
#
|
|
276
|
-
# number type)
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
'
|
|
269
|
+
# The check a schema primitive admits, by JSON kind rather than Ruby class: `number` is
|
|
270
|
+
# any Numeric (JSON has one number type), and `integer` is any whole one — a browser is
|
|
271
|
+
# free to send `5` or `5.0` (JS has no int/float split), while a fractional value like
|
|
272
|
+
# 1.5 is a real mismatch. A field with no primitive descriptor is left unchecked.
|
|
273
|
+
WHOLE_FLOAT = ->(value) { value.is_a?(::Float) && value.finite? && (value % 1).zero? }
|
|
274
|
+
PRIMITIVE_CHECKS = {
|
|
275
|
+
'string' => ->(value) { value.is_a?(::String) },
|
|
276
|
+
'boolean' => ->(value) { value.is_a?(::TrueClass) || value.is_a?(::FalseClass) },
|
|
277
|
+
'number' => ->(value) { value.is_a?(::Numeric) },
|
|
278
|
+
'integer' => ->(value) { value.is_a?(::Integer) || WHOLE_FLOAT.call(value) }
|
|
282
279
|
}.freeze
|
|
283
280
|
|
|
284
281
|
def check_primitive(field, raw)
|
|
285
|
-
|
|
286
|
-
return if
|
|
282
|
+
check = PRIMITIVE_CHECKS[field.primitive]
|
|
283
|
+
return if check.nil? || check.call(raw)
|
|
287
284
|
|
|
288
|
-
raise Error::
|
|
285
|
+
raise Error::SerializationError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}"
|
|
289
286
|
end
|
|
290
287
|
|
|
291
288
|
def enum_hash(field)
|
|
@@ -314,7 +311,7 @@ module Selenium
|
|
|
314
311
|
# malformed entry and is rejected outright.
|
|
315
312
|
def read_map_entry(field, element, klass)
|
|
316
313
|
unless element.is_a?(::Array) && element.size == 2
|
|
317
|
-
raise Error::
|
|
314
|
+
raise Error::SerializationError,
|
|
318
315
|
"#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}"
|
|
319
316
|
end
|
|
320
317
|
|
|
@@ -326,13 +323,13 @@ module Selenium
|
|
|
326
323
|
# A bare scalar at a scalar-tolerant union position must match one of the union's
|
|
327
324
|
# scalar-arm primitives (+scalar+ is a primitive name or an array of them); a
|
|
328
325
|
# wrong-typed scalar (a number where a string is expected) is a wire error, not
|
|
329
|
-
# something to pass through. An unrecognized primitive (none in
|
|
326
|
+
# something to pass through. An unrecognized primitive (none in PRIMITIVE_CHECKS) is
|
|
330
327
|
# left unchecked, matching the lenient default elsewhere.
|
|
331
328
|
def scalar_value(field, value)
|
|
332
|
-
|
|
333
|
-
return value if
|
|
329
|
+
checks = Array(field.scalar).filter_map { |primitive| PRIMITIVE_CHECKS[primitive] }
|
|
330
|
+
return value if checks.empty? || checks.any? { |check| check.call(value) }
|
|
334
331
|
|
|
335
|
-
raise Error::
|
|
332
|
+
raise Error::SerializationError,
|
|
336
333
|
"#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
|
|
337
334
|
end
|
|
338
335
|
|
|
@@ -53,19 +53,23 @@ module Selenium
|
|
|
53
53
|
# An outbound scalar outside that set matches no arm, so it is a caller error.
|
|
54
54
|
def scalar_values(*values) = @scalar_values = values
|
|
55
55
|
|
|
56
|
-
# A non-Hash payload is a bare scalar arm (e.g. input.Origin's "viewport")
|
|
57
|
-
#
|
|
58
|
-
# (object_only), where a non-Hash cannot match any variant and is a wire error.
|
|
56
|
+
# A non-Hash payload is a bare scalar arm (e.g. input.Origin's "viewport"), valid only
|
|
57
|
+
# as a literal the schema pins; under object_only it cannot match any variant at all.
|
|
59
58
|
def from_json(json_payload)
|
|
60
59
|
unless json_payload.is_a?(::Hash)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
if @object_only
|
|
61
|
+
raise Error::SerializationError,
|
|
62
|
+
"#{name} expected an object on the wire, got #{json_payload.inspect}"
|
|
63
|
+
end
|
|
64
|
+
return json_payload if scalar_arm?(json_payload)
|
|
65
|
+
|
|
66
|
+
raise Error::SerializationError,
|
|
67
|
+
"#{name} received a scalar not in this Selenium's BiDi schema: #{json_payload.inspect}"
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
variant = select(json_payload)
|
|
67
71
|
unless variant
|
|
68
|
-
raise Error::
|
|
72
|
+
raise Error::SerializationError,
|
|
69
73
|
"#{name} received a variant not in this Selenium's BiDi schema: #{json_payload.inspect}"
|
|
70
74
|
end
|
|
71
75
|
Protocol.const_get(variant).from_json(json_payload)
|
|
@@ -21,8 +21,7 @@ module Selenium
|
|
|
21
21
|
module WebDriver
|
|
22
22
|
class BiDi
|
|
23
23
|
# Wire round-trip runtime for the generated protocol layer: the value-type bases
|
|
24
|
-
# (Record, Union), the omit sentinel (UNSET), outbound enum validation
|
|
25
|
-
# strict-inbound toggle.
|
|
24
|
+
# (Record, Union), the omit sentinel (UNSET), and outbound enum validation.
|
|
26
25
|
#
|
|
27
26
|
# @api private
|
|
28
27
|
module Serialization
|
|
@@ -34,17 +33,6 @@ module Selenium
|
|
|
34
33
|
def UNSET.inspect = 'UNSET'
|
|
35
34
|
UNSET.freeze
|
|
36
35
|
|
|
37
|
-
# Strict inbound mode. Off by default: a required field missing from a response is
|
|
38
|
-
# tolerated as omitted and warned, so a schema ahead of the browser does not block the
|
|
39
|
-
# caller. When SE_BIDI_STRICT is set to anything but 0/false, that same case escalates
|
|
40
|
-
# to an error for callers who want it.
|
|
41
|
-
#
|
|
42
|
-
# @api private
|
|
43
|
-
def self.strict?
|
|
44
|
-
value = ENV.fetch('SE_BIDI_STRICT', '').strip.downcase
|
|
45
|
-
!value.empty? && value != '0' && value != 'false'
|
|
46
|
-
end
|
|
47
|
-
|
|
48
36
|
# Validates an outbound enum argument: +value+ is a symbol (or list of symbols) that
|
|
49
37
|
# must be a key of the enum hash (+{symbol => wire_token}+), so a bad value fails
|
|
50
38
|
# locally with a clear error instead of a round-trip. Outbound only; inbound wire
|
|
@@ -79,7 +67,7 @@ module Selenium
|
|
|
79
67
|
return value if value.nil?
|
|
80
68
|
return value.map { |element| to_symbol(name, element, enum) } if value.is_a?(::Array)
|
|
81
69
|
|
|
82
|
-
enum.key(value) || raise(Error::
|
|
70
|
+
enum.key(value) || raise(Error::SerializationError, "#{name} received an unknown value: #{value.inspect}")
|
|
83
71
|
end
|
|
84
72
|
end
|
|
85
73
|
end # BiDi
|
|
@@ -21,6 +21,7 @@ require 'websocket'
|
|
|
21
21
|
|
|
22
22
|
module Selenium
|
|
23
23
|
module WebDriver
|
|
24
|
+
# @api private
|
|
24
25
|
class WebSocketConnection
|
|
25
26
|
CONNECTION_ERRORS = [
|
|
26
27
|
Errno::ECONNRESET, # connection is aborted (browser process was killed)
|
|
@@ -35,6 +36,15 @@ module Selenium
|
|
|
35
36
|
|
|
36
37
|
MAX_LOG_MESSAGE_SIZE = 9999
|
|
37
38
|
|
|
39
|
+
# websocket-ruby defaults to a 20MB limit and silently drops larger
|
|
40
|
+
# frames, which can stall the listener. CDP payloads (e.g. large data:
|
|
41
|
+
# URLs) can exceed that, so raise the ceiling for our connections.
|
|
42
|
+
# The gem only exposes the limit as process-global state, so it is
|
|
43
|
+
# raised (never lowered) and not restored on close - concurrent
|
|
44
|
+
# connections share the value. Other bindings rely on their own
|
|
45
|
+
# websocket clients' limits; this constant only affects websocket-ruby.
|
|
46
|
+
MAX_FRAME_SIZE = 100 * 1024 * 1024 # 100MB
|
|
47
|
+
|
|
38
48
|
def initialize(url:)
|
|
39
49
|
@callback_threads = ThreadGroup.new
|
|
40
50
|
|
|
@@ -46,26 +56,19 @@ module Selenium
|
|
|
46
56
|
@session_id = nil
|
|
47
57
|
@url = url
|
|
48
58
|
|
|
59
|
+
apply_frame_size_limit
|
|
49
60
|
process_handshake
|
|
50
61
|
@socket_thread = attach_socket_listener
|
|
51
62
|
end
|
|
52
63
|
|
|
64
|
+
# Idempotent: the listener may already have initiated shutdown (see
|
|
65
|
+
# #frame_dropped?), so always close the socket and join threads rather
|
|
66
|
+
# than short-circuiting on @closing.
|
|
53
67
|
def close
|
|
54
|
-
|
|
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
|
|
68
|
+
close_socket
|
|
66
69
|
|
|
67
70
|
# Let threads unwind instead of calling exit
|
|
68
|
-
@socket_thread&.join(0.5)
|
|
71
|
+
@socket_thread&.join(0.5) unless @socket_thread == Thread.current
|
|
69
72
|
@callback_threads.list.each do |thread|
|
|
70
73
|
thread.join(0.5)
|
|
71
74
|
rescue StandardError => e
|
|
@@ -97,6 +100,9 @@ module Selenium
|
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
def send_cmd(**payload)
|
|
103
|
+
# IOError to match what writing to an already-closed socket raises
|
|
104
|
+
raise IOError, 'WebSocket connection is closed' if @closing
|
|
105
|
+
|
|
100
106
|
id = next_id
|
|
101
107
|
data = payload.merge(id: id)
|
|
102
108
|
WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
|
|
@@ -109,7 +115,11 @@ module Selenium
|
|
|
109
115
|
raise e, "WebSocket is closed (#{e.class}: #{e.message})"
|
|
110
116
|
end
|
|
111
117
|
|
|
112
|
-
wait.until
|
|
118
|
+
wait.until do
|
|
119
|
+
raise IOError, 'WebSocket connection closed while waiting for a response' if @closing
|
|
120
|
+
|
|
121
|
+
@messages_mtx.synchronize { messages.delete(id) }
|
|
122
|
+
end
|
|
113
123
|
end
|
|
114
124
|
|
|
115
125
|
private
|
|
@@ -132,22 +142,51 @@ module Selenium
|
|
|
132
142
|
|
|
133
143
|
incoming_frame << socket.readpartial(1024)
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
message = process_frame(frame)
|
|
139
|
-
next unless message['method']
|
|
140
|
-
|
|
141
|
-
@messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
|
|
142
|
-
@callback_threads.add(callback_thread(message['params'], &callback))
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
+
process_incoming_frames
|
|
146
|
+
break if frame_dropped?
|
|
145
147
|
end
|
|
146
148
|
rescue *CONNECTION_ERRORS, WebSocket::Error => e
|
|
147
149
|
WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
|
|
148
150
|
end
|
|
149
151
|
end
|
|
150
152
|
|
|
153
|
+
def close_socket
|
|
154
|
+
@closing_mtx.synchronize { @closing = true }
|
|
155
|
+
socket.close
|
|
156
|
+
rescue *CONNECTION_ERRORS => e
|
|
157
|
+
WebDriver.logger.debug "WebSocket socket closed: #{e.class}: #{e.message}", id: :ws
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def process_incoming_frames
|
|
161
|
+
while (frame = incoming_frame.next)
|
|
162
|
+
break if @closing
|
|
163
|
+
|
|
164
|
+
message = process_frame(frame)
|
|
165
|
+
next unless message['method']
|
|
166
|
+
|
|
167
|
+
@messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
|
|
168
|
+
@callback_threads.add(callback_thread(message['params'], &callback))
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# True when the buffered frame could not be decoded (e.g. exceeds MAX_FRAME_SIZE).
|
|
174
|
+
# websocket-ruby swallows the error and keeps returning nil, so surface it and close
|
|
175
|
+
# the connection here instead of leaving a dead listener on an open socket.
|
|
176
|
+
def frame_dropped?
|
|
177
|
+
return false unless incoming_frame.error?
|
|
178
|
+
|
|
179
|
+
WebDriver.logger.error("WebSocket frame dropped (#{incoming_frame.error}); if payloads can legitimately " \
|
|
180
|
+
"exceed #{WebSocket.max_frame_size} bytes, set WebSocket.max_frame_size " \
|
|
181
|
+
'to a higher value', id: :ws)
|
|
182
|
+
close_socket
|
|
183
|
+
true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def apply_frame_size_limit
|
|
187
|
+
WebSocket.max_frame_size = MAX_FRAME_SIZE if WebSocket.max_frame_size < MAX_FRAME_SIZE
|
|
188
|
+
end
|
|
189
|
+
|
|
151
190
|
def incoming_frame
|
|
152
191
|
@incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
|
|
153
192
|
end
|
|
@@ -204,8 +243,7 @@ module Selenium
|
|
|
204
243
|
end
|
|
205
244
|
|
|
206
245
|
def next_id
|
|
207
|
-
@id
|
|
208
|
-
@id += 1
|
|
246
|
+
@id = (@id || 0) + 1
|
|
209
247
|
end
|
|
210
248
|
end # BiDi
|
|
211
249
|
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.48.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: 2026-08-
|
|
13
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: base64
|