selenium-webdriver 4.45.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +46 -0
  3. data/bin/linux/selenium-manager +0 -0
  4. data/bin/macos/selenium-manager +0 -0
  5. data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5940 -0
  6. data/bin/selenium-manager.cdx.json +11708 -0
  7. data/bin/windows/selenium-manager.exe +0 -0
  8. data/lib/selenium/webdriver/bidi/error.rb +48 -0
  9. data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +495 -0
  10. data/lib/selenium/webdriver/bidi/protocol/browser.rb +250 -0
  11. data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +798 -0
  12. data/lib/selenium/webdriver/{common/driver_extensions/has_bidi.rb → bidi/protocol/domain.rb} +23 -13
  13. data/lib/selenium/webdriver/bidi/protocol/emulation.rb +448 -0
  14. data/lib/selenium/webdriver/bidi/protocol/error_code.rb +66 -0
  15. data/lib/selenium/webdriver/bidi/protocol/input.rb +354 -0
  16. data/lib/selenium/webdriver/bidi/protocol/log.rb +115 -0
  17. data/lib/selenium/webdriver/bidi/protocol/network.rb +720 -0
  18. data/lib/selenium/webdriver/bidi/protocol/permissions.rb +75 -0
  19. data/lib/selenium/webdriver/bidi/protocol/script.rb +1052 -0
  20. data/lib/selenium/webdriver/bidi/protocol/session.rb +286 -0
  21. data/lib/selenium/webdriver/bidi/protocol/speculation.rb +58 -0
  22. data/lib/selenium/webdriver/bidi/protocol/storage.rb +183 -0
  23. data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +88 -0
  24. data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +132 -0
  25. data/lib/selenium/webdriver/bidi/protocol.rb +42 -0
  26. data/lib/selenium/webdriver/bidi/serialization/record.rb +399 -0
  27. data/lib/selenium/webdriver/bidi/serialization/union.rb +160 -0
  28. data/lib/selenium/webdriver/bidi/serialization.rb +78 -0
  29. data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +1420 -0
  30. data/lib/selenium/webdriver/bidi/support/check_generated.rb +63 -0
  31. data/lib/selenium/webdriver/bidi/transport.rb +57 -0
  32. data/lib/selenium/webdriver/bidi.rb +3 -0
  33. data/lib/selenium/webdriver/chrome/driver.rb +3 -3
  34. data/lib/selenium/webdriver/chrome/service.rb +4 -1
  35. data/lib/selenium/webdriver/chromium/driver.rb +0 -1
  36. data/lib/selenium/webdriver/chromium/options.rb +20 -0
  37. data/lib/selenium/webdriver/common/client_config.rb +97 -0
  38. data/lib/selenium/webdriver/common/driver.rb +13 -2
  39. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +0 -7
  40. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +0 -7
  41. data/lib/selenium/webdriver/common/driver_finder.rb +4 -3
  42. data/lib/selenium/webdriver/common/local_driver.rb +15 -4
  43. data/lib/selenium/webdriver/common/options.rb +21 -1
  44. data/lib/selenium/webdriver/common/proxy.rb +1 -9
  45. data/lib/selenium/webdriver/common/selenium_manager.rb +2 -1
  46. data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
  47. data/lib/selenium/webdriver/common/websocket_connection.rb +64 -26
  48. data/lib/selenium/webdriver/common.rb +1 -1
  49. data/lib/selenium/webdriver/edge/driver.rb +3 -3
  50. data/lib/selenium/webdriver/edge/service.rb +4 -1
  51. data/lib/selenium/webdriver/firefox/driver.rb +3 -6
  52. data/lib/selenium/webdriver/firefox/options.rb +19 -0
  53. data/lib/selenium/webdriver/firefox/profile.rb +13 -6
  54. data/lib/selenium/webdriver/ie/driver.rb +3 -3
  55. data/lib/selenium/webdriver/remote/bidi_bridge.rb +41 -9
  56. data/lib/selenium/webdriver/remote/bridge.rb +8 -10
  57. data/lib/selenium/webdriver/remote/driver.rb +14 -4
  58. data/lib/selenium/webdriver/remote/http/common.rb +25 -12
  59. data/lib/selenium/webdriver/remote/http/default.rb +56 -39
  60. data/lib/selenium/webdriver/safari/driver.rb +3 -3
  61. data/lib/selenium/webdriver/safari/options.rb +16 -1
  62. data/lib/selenium/webdriver/support/guards/guard.rb +20 -3
  63. data/lib/selenium/webdriver/support/guards.rb +9 -3
  64. data/lib/selenium/webdriver/version.rb +1 -1
  65. metadata +29 -3
@@ -0,0 +1,132 @@
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
+ # DO NOT EDIT! This file is generated by rb/lib/selenium/webdriver/bidi/support/bidi_generate.rb.
21
+ # Regenerate with: `bazel run //rb/lib/selenium/webdriver:bidi-generate`
22
+
23
+ module Selenium
24
+ module WebDriver
25
+ class BiDi
26
+ module Protocol
27
+ # @api private
28
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
29
+ # @see https://w3c.github.io/webdriver-bidi/#module-webExtension
30
+ class WebExtension < Domain
31
+ # @api private
32
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
33
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-webextensionextensiondata
34
+ class ExtensionData < Serialization::Union
35
+ discriminator 'type', {archive_path: 'archivePath', base64: 'base64', path: 'path'}
36
+ variants(
37
+ archive_path: 'WebExtension::ExtensionArchivePath',
38
+ base64: 'WebExtension::ExtensionBase64Encoded',
39
+ path: 'WebExtension::ExtensionPath'
40
+ )
41
+ object_only
42
+
43
+ def self.archive_path(**) = WebExtension::ExtensionArchivePath.new(**)
44
+ def self.base64(**) = WebExtension::ExtensionBase64Encoded.new(**)
45
+ def self.path(**) = WebExtension::ExtensionPath.new(**)
46
+ end
47
+
48
+ # @api private
49
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
50
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-webextensionextensionpath
51
+ ExtensionPath = Serialization::Record.define(
52
+ type: {fixed: 'path'},
53
+ path: {wire_key: 'path', primitive: 'string'}
54
+ )
55
+
56
+ # @api private
57
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
58
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-webextensionextensionarchivepath
59
+ ExtensionArchivePath = Serialization::Record.define(
60
+ type: {fixed: 'archivePath'},
61
+ path: {wire_key: 'path', primitive: 'string'}
62
+ )
63
+
64
+ # @api private
65
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
66
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-webextensionextensionbase64encoded
67
+ ExtensionBase64Encoded = Serialization::Record.define(
68
+ type: {fixed: 'base64'},
69
+ value: {wire_key: 'value', primitive: 'string'}
70
+ )
71
+
72
+ # @api private
73
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
74
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-webextensioninstallresult
75
+ InstallResult = Serialization::Record.define(extension: {wire_key: 'extension', primitive: 'string'})
76
+
77
+ # @api private
78
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
79
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-webextensionuninstallparameters
80
+ UninstallParameters = Serialization::Record.define(extension: {wire_key: 'extension', primitive: 'string'})
81
+
82
+ # @api private
83
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
84
+ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-webextensioninstallparameters
85
+ InstallParameters = Serialization::Record.define(
86
+ extension_data: {wire_key: 'extensionData', ref: 'WebExtension::ExtensionData'},
87
+ extensible: true
88
+ )
89
+
90
+ def extension_data = ExtensionData
91
+ def extension_path(**) = ExtensionPath.new(**)
92
+ def extension_archive_path(**) = ExtensionArchivePath.new(**)
93
+ def extension_base64_encoded(**) = ExtensionBase64Encoded.new(**)
94
+ def moz = Moz.new(connection)
95
+
96
+ # @api private
97
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
98
+ # @see https://w3c.github.io/webdriver-bidi/#command-webExtension-install
99
+ def install(extension_data:)
100
+ params = InstallParameters.new(extension_data: extension_data)
101
+ execute(cmd: 'webExtension.install', params: params, result: WebExtension::InstallResult)
102
+ end
103
+
104
+ # @api private
105
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
106
+ # @see https://w3c.github.io/webdriver-bidi/#command-webExtension-uninstall
107
+ def uninstall(extension:)
108
+ params = UninstallParameters.new(extension: extension)
109
+ execute(cmd: 'webExtension.uninstall', params: params)
110
+ end
111
+
112
+ # @api private
113
+ # moz: vendor variant of WebExtension, overriding commands with browser-specific params.
114
+ # Construct Moz.new(source) for a matching session; other sessions use WebExtension.
115
+ class Moz < WebExtension
116
+ # @api private
117
+ # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/
118
+ # @see https://w3c.github.io/webdriver-bidi/#command-webExtension-install
119
+ def install(extension_data:, allow_private_browsing: Serialization::UNSET, permanent: Serialization::UNSET)
120
+ extensions = {
121
+ 'moz:allowPrivateBrowsing' => allow_private_browsing,
122
+ 'moz:permanent' => permanent
123
+ }.reject { |_, value| Serialization::UNSET.equal?(value) }
124
+ params = InstallParameters.new(extension_data: extension_data, extensions: extensions)
125
+ execute(cmd: 'webExtension.install', params: params, result: WebExtension::InstallResult)
126
+ end
127
+ end
128
+ end # WebExtension
129
+ end # Protocol
130
+ end # BiDi
131
+ end # WebDriver
132
+ end # Selenium
@@ -0,0 +1,42 @@
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
+ # serialization must load first (it defines the Serialization runtime the generated
21
+ # classes build on), then the Domain base the generated classes subclass. Add a require
22
+ # below when a new BiDi domain is generated.
23
+ require 'selenium/webdriver/common/error'
24
+ require 'selenium/webdriver/bidi/serialization'
25
+ require 'selenium/webdriver/bidi/transport'
26
+ require 'selenium/webdriver/bidi/protocol/domain'
27
+ require 'selenium/webdriver/bidi/protocol/error_code'
28
+ require 'selenium/webdriver/bidi/error'
29
+ require 'selenium/webdriver/bidi/protocol/bluetooth'
30
+ require 'selenium/webdriver/bidi/protocol/browser'
31
+ require 'selenium/webdriver/bidi/protocol/browsing_context'
32
+ require 'selenium/webdriver/bidi/protocol/emulation'
33
+ require 'selenium/webdriver/bidi/protocol/input'
34
+ require 'selenium/webdriver/bidi/protocol/log'
35
+ require 'selenium/webdriver/bidi/protocol/network'
36
+ require 'selenium/webdriver/bidi/protocol/permissions'
37
+ require 'selenium/webdriver/bidi/protocol/script'
38
+ require 'selenium/webdriver/bidi/protocol/session'
39
+ require 'selenium/webdriver/bidi/protocol/speculation'
40
+ require 'selenium/webdriver/bidi/protocol/storage'
41
+ require 'selenium/webdriver/bidi/protocol/user_agent_client_hints'
42
+ require 'selenium/webdriver/bidi/protocol/web_extension'
@@ -0,0 +1,399 @@
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
+ class BiDi
23
+ module Serialization
24
+ # Immutable value type for the generated protocol classes. +Record.define(spec)+
25
+ # bakes each field's wire facts and returns a +::Data+ subclass with serialization.
26
+ #
27
+ # Cookie = Record.define(name: 'name', value: {wire_key: 'value', ref: 'Network::BytesValue'})
28
+ #
29
+ # @api private
30
+ class Record < ::Data
31
+ # Named Field, not Member, to avoid colliding with +::Data#members+.
32
+ Field = ::Data.define(:name, :wire_key, :nullable, :ref, :list, :fixed, :enum, :required, :primitive,
33
+ :scalar, :const)
34
+
35
+ def self.define(**spec)
36
+ extensible = spec.delete(:extensible) || false
37
+ fields = spec.map { |name, meta| field(name, meta) }
38
+ names = fields.map(&:name)
39
+ names << :extensions if extensible
40
+
41
+ klass = super(*names)
42
+ fields.freeze
43
+ # Singleton methods are inherited by `X = Record.define(…)`; ivars would not.
44
+ klass.define_singleton_method(:fields) { fields }
45
+ klass.define_singleton_method(:extensible?) { extensible }
46
+ klass.include(Serializable)
47
+ # Capture ::Data's generated +new+, then prepend (not include) Deserializer so
48
+ # its +new+ overrides it — outbound +new+ adds validation, while inbound
49
+ # +from_json+ builds directly via the captured constructor. Bound to +self+ so a
50
+ # subclass builds itself, not the base.
51
+ data_new = klass.singleton_class.instance_method(:new)
52
+ klass.singleton_class.prepend(Deserializer)
53
+ klass.define_singleton_method(:construct) { |**attributes| data_new.bind_call(self, **attributes) }
54
+ klass.singleton_class.send(:private, :construct)
55
+ klass
56
+ end
57
+
58
+ def self.field(name, meta)
59
+ meta = {wire_key: meta} if meta.is_a?(::String)
60
+ Field.new(name: name.to_sym, wire_key: meta.fetch(:wire_key, name.to_s),
61
+ nullable: meta[:nullable] || false, ref: meta[:ref],
62
+ list: meta[:list] || false, fixed: meta.fetch(:fixed, UNSET), enum: meta[:enum],
63
+ required: meta.fetch(:required, true), primitive: meta[:primitive],
64
+ scalar: meta[:scalar], const: meta.fetch(:const, UNSET))
65
+ end
66
+ private_class_method :field
67
+
68
+ # Inbound construction: the keyword +new+ (validated) and the wire +from_json+.
69
+ #
70
+ # @api private
71
+ module Deserializer
72
+ def new(**kwargs)
73
+ # Start from what was passed so ::Data's constructor rejects an unknown key, then fill
74
+ # each field with its value or UNSET (omitted), forcing fixed discriminators.
75
+ attributes = kwargs.dup
76
+ fields.each { |f| attributes[f.name] = fixed?(f) ? f.fixed : attributes.fetch(f.name, UNSET) }
77
+ attributes[:extensions] = kwargs.fetch(:extensions, {}) if extensible?
78
+ validate_values(attributes)
79
+ construct(**attributes)
80
+ end
81
+
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
+ def from_json(json_payload)
87
+ unless json_payload.is_a?(::Hash)
88
+ raise Error::SerializationError, "#{name} expected an object on the wire, got #{json_payload.inspect}"
89
+ end
90
+
91
+ attributes = fields.to_h do |f|
92
+ [f.name, wire_value(f, json_payload)]
93
+ end
94
+ undeclared = extra(json_payload)
95
+ if extensible?
96
+ attributes[:extensions] = undeclared # the spec sanctions these extras; preserve them silently
97
+ else
98
+ warn_undeclared(undeclared) unless undeclared.empty?
99
+ end
100
+ construct(**attributes)
101
+ end
102
+
103
+ private
104
+
105
+ # Checks each field's value: a required field cannot be omitted (UNSET), a non-nullable
106
+ # field cannot be nil (nil is neither a value nor the UNSET omit-sentinel, so it would be
107
+ # silently dropped on the wire), a nullable-const field must carry its literal (not some
108
+ # other value), a primitive field must be the matching Ruby type, and an enum field must be
109
+ # in its allowed set. The enum constant is resolved lazily so a cross-domain enum need not be
110
+ # loaded first. Outbound only (from +new+); inbound presence/primitive/enum are checked
111
+ # separately in +wire_value+/+read+.
112
+ def validate_values(attributes)
113
+ fields.each do |f|
114
+ value = attributes[f.name]
115
+ raise ::ArgumentError, "#{name}##{f.name} is required" if UNSET.equal?(value) && f.required
116
+ raise ::ArgumentError, "#{name}##{f.name} cannot be nil" if value.nil? && !f.nullable
117
+ next if value.nil? || UNSET.equal?(value)
118
+
119
+ validate_present(f, value)
120
+ end
121
+ end
122
+
123
+ # Checks a field that carries an actual value (neither omitted nor nil): a nullable-const
124
+ # field against its literal, list/scalar shape, primitive type (lists excepted, as inbound
125
+ # does), ref type, and enum membership (resolved lazily so a cross-domain enum need not load first).
126
+ def validate_present(field, value)
127
+ validate_const(field, value)
128
+ check_outbound_shape(field, value)
129
+ check_outbound_primitive(field, value) unless field.list
130
+ validate_ref(field, value) if field.ref
131
+ Serialization.validate!("#{name}##{field.name}", value, Protocol.const_get(field.enum)) if field.enum
132
+ end
133
+
134
+ # Outbound mirror of read_ref: a ref-typed value must be the type it declares, so a wrong
135
+ # record or a value no union variant accepts is a caller error caught here, not a browser
136
+ # round-trip. Shape is already checked, so a list is an Array.
137
+ def validate_ref(field, value)
138
+ klass = (@refs ||= {})[field.name] ||= Protocol.const_get(field.ref)
139
+ field.list ? validate_ref_list(field, klass, value) : validate_ref_value(field, klass, value)
140
+ end
141
+
142
+ # Mirrors read_list: a scalar field is a [key, value] map, a nested list recurses, otherwise
143
+ # each element is checked against the ref.
144
+ def validate_ref_list(field, klass, list)
145
+ list.each do |element|
146
+ if field.scalar
147
+ validate_ref_entry(field, klass, element)
148
+ elsif element.is_a?(::Array)
149
+ validate_ref_list(field, klass, element)
150
+ else
151
+ validate_ref_value(field, klass, element)
152
+ end
153
+ end
154
+ end
155
+
156
+ # A [key, value] map entry: the key may be a variant or a bare scalar, the value is a variant.
157
+ def validate_ref_entry(field, klass, element)
158
+ unless element.is_a?(::Array) && element.size == 2
159
+ raise ::ArgumentError, "#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}"
160
+ end
161
+
162
+ key, value = element
163
+ key.is_a?(Serializable) ? validate_ref_value(field, klass, key) : check_outbound_scalar(field, key)
164
+ validate_ref_value(field, klass, value)
165
+ end
166
+
167
+ # A record ref must be an instance of that record; a union ref must be one the union accepts.
168
+ def validate_ref_value(field, klass, value)
169
+ return if klass < Union ? klass.valid_outbound?(value) : value.is_a?(klass)
170
+
171
+ raise ::ArgumentError, "#{name}##{field.name} expected #{field.ref}, got #{value.inspect}"
172
+ end
173
+
174
+ # Outbound mirror of scalar_value: a bare map key must match one of the arm's primitives.
175
+ def check_outbound_scalar(field, value)
176
+ checks = Array(field.scalar).filter_map { |primitive| PRIMITIVE_CHECKS[primitive] }
177
+ return if checks.empty? || checks.any? { |check| check.call(value) }
178
+
179
+ raise ::ArgumentError,
180
+ "#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
181
+ end
182
+
183
+ # A nullable constant (`literal / null`) is caller-settable but its only non-null value is
184
+ # the literal, so a value that is neither the literal nor nil (nil is handled above) is a
185
+ # local error rather than a wire round-trip. A non-const field carries UNSET here and passes.
186
+ def validate_const(field, value)
187
+ return if UNSET.equal?(field.const) || value == field.const
188
+
189
+ raise ::ArgumentError, "#{name}##{field.name} must be #{field.const.inspect}, got #{value.inspect}"
190
+ end
191
+
192
+ # Outbound mirror of check_shape: a list-typed arg must be an array, a scalar-shaped one
193
+ # (enum or ref, not a list) must not — a local ArgumentError, not a wire round-trip.
194
+ def check_outbound_shape(field, value)
195
+ return if field.list == value.is_a?(::Array)
196
+ return unless field.list || field.enum || field.ref
197
+
198
+ kind = field.list ? 'a list' : 'a single value'
199
+ raise ::ArgumentError, "#{name}##{field.name} expected #{kind}, got #{value.inspect}"
200
+ end
201
+
202
+ # Outbound mirror of check_primitive: a primitive-typed arg (`string`/`integer`/…) must be
203
+ # the matching Ruby type, so a caller mistake (a string width, a float count) is a local
204
+ # ArgumentError here rather than a rejection the browser reports a round-trip later. A field
205
+ # with no primitive descriptor (enum, ref, opaque) passes; lists are skipped, as inbound does.
206
+ def check_outbound_primitive(field, value)
207
+ check = PRIMITIVE_CHECKS[field.primitive]
208
+ return if check.nil? || check.call(value)
209
+
210
+ raise ::ArgumentError, "#{name}##{field.name} expected #{field.primitive}, got #{value.inspect}"
211
+ end
212
+
213
+ def fixed?(field)
214
+ !UNSET.equal?(field.fixed)
215
+ end
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.
220
+ def wire_value(field, json_payload)
221
+ return field.fixed if fixed?(field)
222
+ return read(field, json_payload[field.wire_key]) if json_payload.key?(field.wire_key)
223
+ return UNSET unless field.required
224
+
225
+ raise Error::SerializationError, "#{name}##{field.name} is required but was missing from the response"
226
+ end
227
+
228
+ def read(field, raw)
229
+ if raw.nil?
230
+ return raw if field.nullable
231
+
232
+ raise Error::SerializationError, "#{name}##{field.name} received null but is not nullable"
233
+ end
234
+ check_shape(field, raw)
235
+ return Serialization.to_symbol("#{name}##{field.name}", raw, enum_hash(field)) if field.enum
236
+
237
+ if field.ref.nil?
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
243
+ end
244
+
245
+ read_ref(field, raw)
246
+ end
247
+
248
+ # Reads a ref-typed value into its class. A `scalar` position is an inline union with
249
+ # a scalar arm collapsed onto its union ref (a map's string keys): a non-object leaf
250
+ # passes through instead of being handed to the object_only union, but only when it
251
+ # matches the arm's primitive (+scalar+ carries it). A list recurses per element.
252
+ def read_ref(field, raw)
253
+ klass = (@refs ||= {})[field.name] ||= Protocol.const_get(field.ref)
254
+ return read_list(field, raw, klass) if field.list
255
+
256
+ field.scalar && !raw.is_a?(::Hash) ? scalar_value(field, raw) : klass.from_json(raw)
257
+ end
258
+
259
+ # A declared list must arrive as an array; a scalar-shaped field (enum or ref, not a
260
+ # list) must not. An opaque field carries no shape descriptor, so it passes through.
261
+ def check_shape(field, raw)
262
+ return if field.list == raw.is_a?(::Array)
263
+ return unless field.list || field.enum || field.ref
264
+
265
+ raise Error::SerializationError,
266
+ "#{name}##{field.name} expected #{field.list ? 'a list' : 'a single value'}, got #{raw.inspect}"
267
+ end
268
+
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) }
279
+ }.freeze
280
+
281
+ def check_primitive(field, raw)
282
+ check = PRIMITIVE_CHECKS[field.primitive]
283
+ return if check.nil? || check.call(raw)
284
+
285
+ raise Error::SerializationError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}"
286
+ end
287
+
288
+ def enum_hash(field)
289
+ (@enums ||= {})[field.name] ||= Protocol.const_get(field.enum)
290
+ end
291
+
292
+ # Parses each element. A `scalar` field is a map encoded as `[key, value]` pairs, so
293
+ # every element must be a 2-item pair — each is read as one, and a malformed entry is
294
+ # rejected. Non-scalar lists recurse into nested lists; other elements deserialize.
295
+ def read_list(field, raw, klass)
296
+ raw.map do |element|
297
+ if field.scalar
298
+ read_map_entry(field, element, klass)
299
+ elsif element.is_a?(::Array)
300
+ read_list(field, element, klass)
301
+ else
302
+ klass.from_json(element)
303
+ end
304
+ end
305
+ end
306
+
307
+ # A map entry is a `[key, value]` pair. The key is `Ref / text` — an object key
308
+ # deserializes, a bare-string key passes through once validated against the arm's
309
+ # primitive. The value is the object-only Ref and always deserializes, so a bare scalar
310
+ # there is rejected (object_only holds at the value position). A non-pair element is a
311
+ # malformed entry and is rejected outright.
312
+ def read_map_entry(field, element, klass)
313
+ unless element.is_a?(::Array) && element.size == 2
314
+ raise Error::SerializationError,
315
+ "#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}"
316
+ end
317
+
318
+ key, value = element
319
+ key = key.is_a?(::Hash) ? klass.from_json(key) : scalar_value(field, key)
320
+ [key, klass.from_json(value)]
321
+ end
322
+
323
+ # A bare scalar at a scalar-tolerant union position must match one of the union's
324
+ # scalar-arm primitives (+scalar+ is a primitive name or an array of them); a
325
+ # wrong-typed scalar (a number where a string is expected) is a wire error, not
326
+ # something to pass through. An unrecognized primitive (none in PRIMITIVE_CHECKS) is
327
+ # left unchecked, matching the lenient default elsewhere.
328
+ def scalar_value(field, value)
329
+ checks = Array(field.scalar).filter_map { |primitive| PRIMITIVE_CHECKS[primitive] }
330
+ return value if checks.empty? || checks.any? { |check| check.call(value) }
331
+
332
+ raise Error::SerializationError,
333
+ "#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
334
+ end
335
+
336
+ def extra(json_payload)
337
+ known = (@wire_keys ||= fields.map(&:wire_key))
338
+ json_payload.except(*known)
339
+ end
340
+
341
+ # Forward-compat signal: a property a closed type does not model is dropped and warned so
342
+ # schema drift is visible (an extensible type keeps its extras silently — the spec sanctions
343
+ # them). Tagged +:bidi_undeclared_property+ so a caller can silence it via +logger.ignore+.
344
+ def warn_undeclared(undeclared)
345
+ undeclared.each_key do |key|
346
+ WebDriver.logger.warn("#{name} received an undeclared property: #{key.inspect}",
347
+ id: :bidi_undeclared_property)
348
+ end
349
+ end
350
+ end
351
+
352
+ # @api private
353
+ module Serializable
354
+ def self.as_json(value)
355
+ case value
356
+ when Serializable then value.as_json
357
+ when ::Array then value.map { |element| as_json(element) }
358
+ when ::Hash then value.transform_values { |element| as_json(element) }
359
+ else value
360
+ end
361
+ end
362
+
363
+ # Omit UNSET fields; emit null only for nullable ones.
364
+ def as_json(*)
365
+ payload = {}
366
+ self.class.fields.each do |f|
367
+ value = public_send(f.name)
368
+ next if UNSET.equal?(value)
369
+ next if value.nil? && !f.nullable
370
+
371
+ value = Serialization.to_wire(value, Protocol.const_get(f.enum)) if f.enum
372
+ payload[f.wire_key] = Serializable.as_json(value)
373
+ end
374
+ merge_extensions!(payload) if self.class.extensible? && !extensions.empty?
375
+ payload
376
+ end
377
+
378
+ private
379
+
380
+ # Merge the passthrough extras onto the wire, erroring rather than letting an extra whose key
381
+ # is a declared field's wire key silently clobber that typed value; an extra is by definition
382
+ # a field the spec does not declare. Keys are stringified first so a symbol key (e.g. `name:`)
383
+ # cannot slip past the guard and then reappear as a duplicate wire key once serialized. The
384
+ # single gate every outbound path funnels through: +new+, +with+, and in-place mutation.
385
+ def merge_extensions!(payload)
386
+ extras = extensions.transform_keys(&:to_s)
387
+ collisions = extras.keys & self.class.fields.map(&:wire_key)
388
+ unless collisions.empty?
389
+ raise ::ArgumentError, "#{self.class.name} extensions shadow declared fields: #{collisions.join(', ')}"
390
+ end
391
+
392
+ payload.merge!(extras)
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end # BiDi
398
+ end # WebDriver
399
+ end # Selenium