selenium-webdriver 4.47.0 → 4.49.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.
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)
@@ -209,7 +209,8 @@ module Selenium
209
209
  context: {wire_key: 'context', primitive: 'string'},
210
210
  origin: {wire_key: 'origin', required: false, enum: 'BrowsingContext::CAPTURE_SCREENSHOT_PARAMETERS_ORIGIN'},
211
211
  format: {wire_key: 'format', required: false, ref: 'BrowsingContext::ImageFormat'},
212
- clip: {wire_key: 'clip', required: false, ref: 'BrowsingContext::ClipRectangle'}
212
+ clip: {wire_key: 'clip', required: false, ref: 'BrowsingContext::ClipRectangle'},
213
+ image_size: {wire_key: 'imageSize', required: false, ref: 'BrowsingContext::ImageSize'}
213
214
  )
214
215
 
215
216
  # @api private
@@ -220,6 +221,14 @@ module Selenium
220
221
  quality: {wire_key: 'quality', required: false, primitive: 'number'}
221
222
  )
222
223
 
224
+ # @api private
225
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
226
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-browsingcontextimagesize
227
+ ImageSize = Serialization::Record.define(
228
+ max_width: {wire_key: 'maxWidth', required: false, primitive: 'integer'},
229
+ max_height: {wire_key: 'maxHeight', required: false, primitive: 'integer'}
230
+ )
231
+
223
232
  # @api private
224
233
  # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
225
234
  # @see https://w3c.github.io/webdriver-bidi/#cddl-type-browsingcontextcliprectangle
@@ -585,6 +594,7 @@ module Selenium
585
594
  def inner_text_locator(**) = InnerTextLocator.new(**)
586
595
  def x_path_locator(**) = XPathLocator.new(**)
587
596
  def image_format(**) = ImageFormat.new(**)
597
+ def image_size(**) = ImageSize.new(**)
588
598
  def clip_rectangle = ClipRectangle
589
599
  def element_clip_rectangle(**) = ElementClipRectangle.new(**)
590
600
  def box_clip_rectangle(**) = BoxClipRectangle.new(**)
@@ -610,10 +620,17 @@ module Selenium
610
620
  context:,
611
621
  origin: Serialization::UNSET,
612
622
  format: Serialization::UNSET,
613
- clip: Serialization::UNSET
623
+ clip: Serialization::UNSET,
624
+ image_size: Serialization::UNSET
614
625
  )
615
626
  Serialization.validate!('origin', origin, BrowsingContext::CAPTURE_SCREENSHOT_PARAMETERS_ORIGIN)
616
- params = CaptureScreenshotParameters.new(context: context, origin: origin, format: format, clip: clip)
627
+ params = CaptureScreenshotParameters.new(
628
+ context: context,
629
+ origin: origin,
630
+ format: format,
631
+ clip: clip,
632
+ image_size: image_size
633
+ )
617
634
  execute(
618
635
  cmd: 'browsingContext.captureScreenshot',
619
636
  params: params,
@@ -48,6 +48,140 @@ module Selenium
48
48
  landscape_secondary: 'landscape-secondary'
49
49
  }.freeze
50
50
 
51
+ MEDIA_FEATURES_ANY_HOVER = {
52
+ none: 'none',
53
+ hover: 'hover'
54
+ }.freeze
55
+
56
+ MEDIA_FEATURES_ANY_POINTER = {
57
+ none: 'none',
58
+ coarse: 'coarse',
59
+ fine: 'fine'
60
+ }.freeze
61
+
62
+ MEDIA_FEATURES_COLOR_GAMUT = {
63
+ srgb: 'srgb',
64
+ p3: 'p3',
65
+ rec2020: 'rec2020'
66
+ }.freeze
67
+
68
+ MEDIA_FEATURES_DISPLAY_MODE = {
69
+ fullscreen: 'fullscreen',
70
+ standalone: 'standalone',
71
+ minimal_ui: 'minimal-ui',
72
+ browser: 'browser',
73
+ picture_in_picture: 'picture-in-picture'
74
+ }.freeze
75
+
76
+ MEDIA_FEATURES_DYNAMIC_RANGE = {
77
+ standard: 'standard',
78
+ high: 'high'
79
+ }.freeze
80
+
81
+ MEDIA_FEATURES_ENVIRONMENT_BLENDING = {
82
+ opaque: 'opaque',
83
+ additive: 'additive',
84
+ subtractive: 'subtractive'
85
+ }.freeze
86
+
87
+ MEDIA_FEATURES_FORCED_COLORS = {
88
+ none: 'none',
89
+ active: 'active'
90
+ }.freeze
91
+
92
+ MEDIA_FEATURES_GRID = {
93
+ _0: 0,
94
+ _1: 1
95
+ }.freeze
96
+
97
+ MEDIA_FEATURES_HOVER = {
98
+ none: 'none',
99
+ hover: 'hover'
100
+ }.freeze
101
+
102
+ MEDIA_FEATURES_INVERTED_COLORS = {
103
+ none: 'none',
104
+ inverted: 'inverted'
105
+ }.freeze
106
+
107
+ MEDIA_FEATURES_NAV_CONTROLS = {
108
+ none: 'none',
109
+ back: 'back'
110
+ }.freeze
111
+
112
+ MEDIA_FEATURES_OVERFLOW_BLOCK = {
113
+ none: 'none',
114
+ scroll: 'scroll',
115
+ optional_paged: 'optional-paged',
116
+ paged: 'paged'
117
+ }.freeze
118
+
119
+ MEDIA_FEATURES_OVERFLOW_INLINE = {
120
+ none: 'none',
121
+ scroll: 'scroll'
122
+ }.freeze
123
+
124
+ MEDIA_FEATURES_POINTER = {
125
+ none: 'none',
126
+ coarse: 'coarse',
127
+ fine: 'fine'
128
+ }.freeze
129
+
130
+ MEDIA_FEATURES_PREFERS_COLOR_SCHEME = {
131
+ light: 'light',
132
+ dark: 'dark'
133
+ }.freeze
134
+
135
+ MEDIA_FEATURES_PREFERS_CONTRAST = {
136
+ no_preference: 'no-preference',
137
+ more: 'more',
138
+ less: 'less',
139
+ custom: 'custom'
140
+ }.freeze
141
+
142
+ MEDIA_FEATURES_PREFERS_REDUCED_DATA = {
143
+ no_preference: 'no-preference',
144
+ reduce: 'reduce'
145
+ }.freeze
146
+
147
+ MEDIA_FEATURES_PREFERS_REDUCED_MOTION = {
148
+ no_preference: 'no-preference',
149
+ reduce: 'reduce'
150
+ }.freeze
151
+
152
+ MEDIA_FEATURES_PREFERS_REDUCED_TRANSPARENCY = {
153
+ no_preference: 'no-preference',
154
+ reduce: 'reduce'
155
+ }.freeze
156
+
157
+ MEDIA_FEATURES_SCAN = {
158
+ interlace: 'interlace',
159
+ progressive: 'progressive'
160
+ }.freeze
161
+
162
+ MEDIA_FEATURES_SCRIPTING = {
163
+ none: 'none',
164
+ initial_only: 'initial-only',
165
+ enabled: 'enabled'
166
+ }.freeze
167
+
168
+ MEDIA_FEATURES_UPDATE = {
169
+ none: 'none',
170
+ slow: 'slow',
171
+ fast: 'fast'
172
+ }.freeze
173
+
174
+ MEDIA_FEATURES_VIDEO_COLOR_GAMUT = {
175
+ srgb: 'srgb',
176
+ p3: 'p3',
177
+ rec2020: 'rec2020'
178
+ }.freeze
179
+
180
+ MEDIA_FEATURES_VIDEO_DYNAMIC_RANGE = {
181
+ standard: 'standard',
182
+ high: 'high'
183
+ }.freeze
184
+
51
185
  SET_SCROLLBAR_TYPE_OVERRIDE_PARAMETERS_SCROLLBAR_TYPE = {
52
186
  classic: 'classic',
53
187
  overlay: 'overlay'
@@ -120,17 +254,149 @@ module Selenium
120
254
  # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
121
255
  # @see https://w3c.github.io/webdriver-bidi/#cddl-type-emulationsetmediafeaturesoverrideparameters
122
256
  SetMediaFeaturesOverrideParameters = Serialization::Record.define(
123
- features: {wire_key: 'features', nullable: true, ref: 'Emulation::MediaFeature', list: true},
257
+ features: {wire_key: 'features', nullable: true, ref: 'Emulation::MediaFeatures'},
124
258
  contexts: {wire_key: 'contexts', required: false, list: true},
125
259
  user_contexts: {wire_key: 'userContexts', required: false, list: true}
126
260
  )
127
261
 
128
262
  # @api private
129
263
  # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
130
- # @see https://w3c.github.io/webdriver-bidi/#cddl-type-emulationmediafeature
131
- MediaFeature = Serialization::Record.define(
132
- name: {wire_key: 'name', primitive: 'string'},
133
- value: {wire_key: 'value', primitive: 'string'}
264
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-emulationmediafeatures
265
+ MediaFeatures = Serialization::Record.define(
266
+ any_hover: {
267
+ wire_key: 'any-hover',
268
+ required: false,
269
+ nullable: true,
270
+ enum: 'Emulation::MEDIA_FEATURES_ANY_HOVER'
271
+ },
272
+ any_pointer: {
273
+ wire_key: 'any-pointer',
274
+ required: false,
275
+ nullable: true,
276
+ enum: 'Emulation::MEDIA_FEATURES_ANY_POINTER'
277
+ },
278
+ color: {wire_key: 'color', required: false, nullable: true, primitive: 'integer'},
279
+ color_gamut: {
280
+ wire_key: 'color-gamut',
281
+ required: false,
282
+ nullable: true,
283
+ enum: 'Emulation::MEDIA_FEATURES_COLOR_GAMUT'
284
+ },
285
+ color_index: {wire_key: 'color-index', required: false, nullable: true, primitive: 'integer'},
286
+ display_mode: {
287
+ wire_key: 'display-mode',
288
+ required: false,
289
+ nullable: true,
290
+ enum: 'Emulation::MEDIA_FEATURES_DISPLAY_MODE'
291
+ },
292
+ dynamic_range: {
293
+ wire_key: 'dynamic-range',
294
+ required: false,
295
+ nullable: true,
296
+ enum: 'Emulation::MEDIA_FEATURES_DYNAMIC_RANGE'
297
+ },
298
+ environment_blending: {
299
+ wire_key: 'environment-blending',
300
+ required: false,
301
+ nullable: true,
302
+ enum: 'Emulation::MEDIA_FEATURES_ENVIRONMENT_BLENDING'
303
+ },
304
+ forced_colors: {
305
+ wire_key: 'forced-colors',
306
+ required: false,
307
+ nullable: true,
308
+ enum: 'Emulation::MEDIA_FEATURES_FORCED_COLORS'
309
+ },
310
+ grid: {wire_key: 'grid', required: false, nullable: true, enum: 'Emulation::MEDIA_FEATURES_GRID'},
311
+ horizontal_viewport_segments: {
312
+ wire_key: 'horizontal-viewport-segments',
313
+ required: false,
314
+ nullable: true,
315
+ primitive: 'integer'
316
+ },
317
+ hover: {wire_key: 'hover', required: false, nullable: true, enum: 'Emulation::MEDIA_FEATURES_HOVER'},
318
+ inverted_colors: {
319
+ wire_key: 'inverted-colors',
320
+ required: false,
321
+ nullable: true,
322
+ enum: 'Emulation::MEDIA_FEATURES_INVERTED_COLORS'
323
+ },
324
+ monochrome: {wire_key: 'monochrome', required: false, nullable: true, primitive: 'integer'},
325
+ nav_controls: {
326
+ wire_key: 'nav-controls',
327
+ required: false,
328
+ nullable: true,
329
+ enum: 'Emulation::MEDIA_FEATURES_NAV_CONTROLS'
330
+ },
331
+ overflow_block: {
332
+ wire_key: 'overflow-block',
333
+ required: false,
334
+ nullable: true,
335
+ enum: 'Emulation::MEDIA_FEATURES_OVERFLOW_BLOCK'
336
+ },
337
+ overflow_inline: {
338
+ wire_key: 'overflow-inline',
339
+ required: false,
340
+ nullable: true,
341
+ enum: 'Emulation::MEDIA_FEATURES_OVERFLOW_INLINE'
342
+ },
343
+ pointer: {wire_key: 'pointer', required: false, nullable: true, enum: 'Emulation::MEDIA_FEATURES_POINTER'},
344
+ prefers_color_scheme: {
345
+ wire_key: 'prefers-color-scheme',
346
+ required: false,
347
+ nullable: true,
348
+ enum: 'Emulation::MEDIA_FEATURES_PREFERS_COLOR_SCHEME'
349
+ },
350
+ prefers_contrast: {
351
+ wire_key: 'prefers-contrast',
352
+ required: false,
353
+ nullable: true,
354
+ enum: 'Emulation::MEDIA_FEATURES_PREFERS_CONTRAST'
355
+ },
356
+ prefers_reduced_data: {
357
+ wire_key: 'prefers-reduced-data',
358
+ required: false,
359
+ nullable: true,
360
+ enum: 'Emulation::MEDIA_FEATURES_PREFERS_REDUCED_DATA'
361
+ },
362
+ prefers_reduced_motion: {
363
+ wire_key: 'prefers-reduced-motion',
364
+ required: false,
365
+ nullable: true,
366
+ enum: 'Emulation::MEDIA_FEATURES_PREFERS_REDUCED_MOTION'
367
+ },
368
+ prefers_reduced_transparency: {
369
+ wire_key: 'prefers-reduced-transparency',
370
+ required: false,
371
+ nullable: true,
372
+ enum: 'Emulation::MEDIA_FEATURES_PREFERS_REDUCED_TRANSPARENCY'
373
+ },
374
+ scan: {wire_key: 'scan', required: false, nullable: true, enum: 'Emulation::MEDIA_FEATURES_SCAN'},
375
+ scripting: {
376
+ wire_key: 'scripting',
377
+ required: false,
378
+ nullable: true,
379
+ enum: 'Emulation::MEDIA_FEATURES_SCRIPTING'
380
+ },
381
+ update: {wire_key: 'update', required: false, nullable: true, enum: 'Emulation::MEDIA_FEATURES_UPDATE'},
382
+ vertical_viewport_segments: {
383
+ wire_key: 'vertical-viewport-segments',
384
+ required: false,
385
+ nullable: true,
386
+ primitive: 'integer'
387
+ },
388
+ video_color_gamut: {
389
+ wire_key: 'video-color-gamut',
390
+ required: false,
391
+ nullable: true,
392
+ enum: 'Emulation::MEDIA_FEATURES_VIDEO_COLOR_GAMUT'
393
+ },
394
+ video_dynamic_range: {
395
+ wire_key: 'video-dynamic-range',
396
+ required: false,
397
+ nullable: true,
398
+ enum: 'Emulation::MEDIA_FEATURES_VIDEO_DYNAMIC_RANGE'
399
+ }
134
400
  )
135
401
 
136
402
  # @api private
@@ -245,7 +511,7 @@ module Selenium
245
511
 
246
512
  def geolocation_coordinates(**) = GeolocationCoordinates.new(**)
247
513
  def geolocation_position_error(**) = GeolocationPositionError.new(**)
248
- def media_feature(**) = MediaFeature.new(**)
514
+ def media_features(**) = MediaFeatures.new(**)
249
515
  def network_conditions_offline(**) = NetworkConditionsOffline.new(**)
250
516
  def screen_area(**) = ScreenArea.new(**)
251
517
  def screen_orientation(**) = ScreenOrientation.new(**)
@@ -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 is omitted and warned (or
83
- # raised in strict mode, in +wire_value+); enum tokens are mapped back to symbols and an
84
- # unrecognized one raises (in +read+); an undeclared property is captured silently
85
- # (extensible) or warned and dropped (closed) — strict on shape, lenient on extras.
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::WebDriverError, "#{name} expected an object on the wire, got #{json_payload.inspect}"
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
- expected = Array(field.scalar).flat_map { |primitive| PRIMITIVE_TYPES[primitive] || [] }
177
- return if expected.empty? || expected.any? { |type| value.is_a?(type) }
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
- expected = PRIMITIVE_TYPES[field.primitive]
208
- return if expected.nil? || expected.any? { |type| value.is_a?(type) }
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
- missing_required(field)
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::WebDriverError, "#{name}##{field.name} received null but is not nullable"
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
- check_primitive(field, raw) unless field.list
248
- return raw
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::WebDriverError,
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
- # Ruby classes a checkable primitive admits. `number` is any Numeric (JSON has one
276
- # number type); `integer` requires an Integer — a browser emits `5`, not `5.0`, for an
277
- # integer (JS has no int/float split), so this rarely false-positives yet still rejects
278
- # a genuine non-integer like 1.5. A field with no primitive descriptor is left unchecked.
279
- PRIMITIVE_TYPES = {
280
- 'string' => [::String], 'boolean' => [::TrueClass, ::FalseClass],
281
- 'number' => [::Numeric], 'integer' => [::Integer]
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
- expected = PRIMITIVE_TYPES[field.primitive]
286
- return if expected.nil? || expected.any? { |type| raw.is_a?(type) }
282
+ check = PRIMITIVE_CHECKS[field.primitive]
283
+ return if check.nil? || check.call(raw)
287
284
 
288
- raise Error::WebDriverError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}"
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::WebDriverError,
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 PRIMITIVE_TYPES) is
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
- expected = Array(field.scalar).flat_map { |primitive| PRIMITIVE_TYPES[primitive] || [] }
333
- return value if expected.empty? || expected.any? { |type| value.is_a?(type) }
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::WebDriverError,
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") with no
57
- # object to dispatch on, so it is returned unchanged unless every arm is an object
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
- return json_payload unless @object_only
62
-
63
- raise Error::WebDriverError, "#{name} expected an object on the wire, got #{json_payload.inspect}"
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::WebDriverError,
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, and the
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::WebDriverError, "#{name} received an unknown value: #{value.inspect}")
70
+ enum.key(value) || raise(Error::SerializationError, "#{name} received an unknown value: #{value.inspect}")
83
71
  end
84
72
  end
85
73
  end # BiDi
@@ -137,14 +137,16 @@ module BiDiGenerate
137
137
  # snake_case hash key for an enum value (only a label for the wire value it maps to).
138
138
  # Preserves camelCase word boundaries (beforeRequestSent → before_request_sent), maps a
139
139
  # leading minus to "neg" (-0 → neg0, -Infinity → neg_infinity; no underscore before a
140
- # digit, so the key stays normalcase), and collapses other punctuation
141
- # (dedicated-worker → dedicated_worker).
140
+ # digit, so the key stays normalcase), collapses other punctuation
141
+ # (dedicated-worker → dedicated_worker), and prefixes a numeric value so the key
142
+ # is still a valid symbol (0 → _0).
142
143
  def self.enum_key(value)
143
- camel_to_snake(value.to_s)
144
- .sub(/\A-(?=\d)/, 'neg')
145
- .sub(/\A-/, 'neg_')
146
- .gsub(/[^a-z0-9]+/, '_')
147
- .gsub(/\A_+|_+\z/, '')
144
+ token = camel_to_snake(value.to_s)
145
+ .sub(/\A-(?=\d)/, 'neg')
146
+ .sub(/\A-/, 'neg_')
147
+ .gsub(/[^a-z0-9]+/, '_')
148
+ .gsub(/\A_+|_+\z/, '')
149
+ token.empty? || token.match?(/\A\d/) ? "_#{token}" : token
148
150
  end
149
151
 
150
152
  # ruby_name is the snake_case keyword argument; wire_name is the exact key the
@@ -308,9 +310,16 @@ module BiDiGenerate
308
310
  def type_entry = "'#{wire_name}' => #{payload_ref || 'nil'}"
309
311
  end
310
312
 
311
- # constant_name is the SCREAMING_SNAKE hash name; pairs are [symbol_key, wire_value] tuples.
312
- # spec_href links to the type's definition in the live spec (nil when the schema has none).
313
- Enum = Struct.new(:constant_name, :pairs, :spec_href, keyword_init: true)
313
+ # The RBS type a schema value primitive maps to; an unrecognized one is untyped.
314
+ RBS_VALUE_TYPES = {'string' => 'String', 'integer' => 'Integer', 'number' => 'Float',
315
+ 'boolean' => 'bool'}.freeze
316
+
317
+ # constant_name is the SCREAMING_SNAKE hash name; pairs are [symbol_key, wire_value] tuples;
318
+ # primitive is the schema's declared value type. spec_href links to the type's definition in
319
+ # the live spec (nil when the schema has none).
320
+ Enum = Struct.new(:constant_name, :pairs, :primitive, :spec_href, keyword_init: true) do
321
+ def rbs_value_type = RBS_VALUE_TYPES.fetch(primitive, 'untyped')
322
+ end
314
323
 
315
324
  # The generated Protocol::ErrorCode module (filename 'error_code'): `codes` is the [wire, class_name]
316
325
  # pairs in schema order (the full map); `new_classes` is the subset of class names the classic
@@ -694,9 +703,9 @@ module BiDiGenerate
694
703
  next unless type['kind'] == 'enum'
695
704
  next unless name.start_with?("#{domain}.")
696
705
 
697
- pairs = type['values'].map { |v| [BiDiGenerate.enum_key(v), v.to_s] }
706
+ pairs = type['values'].map { |v| [BiDiGenerate.enum_key(v), v] }
698
707
  Enum.new(constant_name: BiDiGenerate.screaming_snake(name.sub("#{domain}.", '')), pairs: pairs,
699
- spec_href: type['specHref'])
708
+ primitive: type['primitive'], spec_href: type['specHref'])
700
709
  end
701
710
  end
702
711