selenium-webdriver 4.35.0 → 4.47.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +111 -0
  3. data/Gemfile +1 -4
  4. data/LICENSE +1 -1
  5. data/NOTICE +1 -1
  6. data/README.md +43 -23
  7. data/bin/linux/selenium-manager +0 -0
  8. data/bin/macos/selenium-manager +0 -0
  9. data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5971 -0
  10. data/bin/selenium-manager.cdx.json +11629 -0
  11. data/bin/windows/selenium-manager.exe +0 -0
  12. data/lib/selenium/server.rb +30 -5
  13. data/lib/selenium/webdriver/atoms/findElements.js +272 -54
  14. data/lib/selenium/webdriver/atoms/getAttribute.js +152 -6
  15. data/lib/selenium/webdriver/atoms/isDisplayed.js +385 -28
  16. data/lib/selenium/webdriver/bidi/browser.rb +71 -0
  17. data/lib/selenium/webdriver/bidi/browsing_context.rb +3 -2
  18. data/lib/selenium/webdriver/bidi/{log/filter_by.rb → error.rb} +20 -16
  19. data/lib/selenium/webdriver/bidi/log_handler.rb +5 -0
  20. data/lib/selenium/webdriver/bidi/network/cookies.rb +13 -9
  21. data/lib/selenium/webdriver/bidi/network/credentials.rb +4 -0
  22. data/lib/selenium/webdriver/bidi/network/headers.rb +4 -0
  23. data/lib/selenium/webdriver/bidi/network/intercepted_auth.rb +4 -0
  24. data/lib/selenium/webdriver/bidi/network/intercepted_item.rb +4 -0
  25. data/lib/selenium/webdriver/bidi/network/intercepted_request.rb +20 -4
  26. data/lib/selenium/webdriver/bidi/network/intercepted_response.rb +24 -6
  27. data/lib/selenium/webdriver/bidi/network/url_pattern.rb +4 -0
  28. data/lib/selenium/webdriver/bidi/network.rb +18 -9
  29. data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +495 -0
  30. data/lib/selenium/webdriver/bidi/protocol/browser.rb +250 -0
  31. data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +798 -0
  32. data/lib/selenium/webdriver/bidi/{log/base_log_entry.rb → protocol/domain.rb} +22 -11
  33. data/lib/selenium/webdriver/bidi/protocol/emulation.rb +448 -0
  34. data/lib/selenium/webdriver/bidi/protocol/error_code.rb +66 -0
  35. data/lib/selenium/webdriver/bidi/protocol/input.rb +354 -0
  36. data/lib/selenium/webdriver/bidi/protocol/log.rb +115 -0
  37. data/lib/selenium/webdriver/bidi/protocol/network.rb +720 -0
  38. data/lib/selenium/webdriver/bidi/protocol/permissions.rb +75 -0
  39. data/lib/selenium/webdriver/bidi/protocol/script.rb +1052 -0
  40. data/lib/selenium/webdriver/bidi/protocol/session.rb +286 -0
  41. data/lib/selenium/webdriver/bidi/protocol/speculation.rb +58 -0
  42. data/lib/selenium/webdriver/bidi/protocol/storage.rb +183 -0
  43. data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +88 -0
  44. data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +132 -0
  45. data/lib/selenium/webdriver/bidi/protocol.rb +42 -0
  46. data/lib/selenium/webdriver/bidi/serialization/record.rb +402 -0
  47. data/lib/selenium/webdriver/bidi/serialization/union.rb +156 -0
  48. data/lib/selenium/webdriver/bidi/serialization.rb +90 -0
  49. data/lib/selenium/webdriver/bidi/session.rb +4 -0
  50. data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +1420 -0
  51. data/lib/selenium/webdriver/bidi/support/check_generated.rb +63 -0
  52. data/lib/selenium/webdriver/bidi/transport.rb +57 -0
  53. data/lib/selenium/webdriver/bidi.rb +4 -1
  54. data/lib/selenium/webdriver/chrome/driver.rb +4 -3
  55. data/lib/selenium/webdriver/chrome/service.rb +13 -0
  56. data/lib/selenium/webdriver/chromium/driver.rb +0 -1
  57. data/lib/selenium/webdriver/chromium/options.rb +23 -1
  58. data/lib/selenium/webdriver/chromium/profile.rb +6 -0
  59. data/lib/selenium/webdriver/common/child_process.rb +2 -1
  60. data/lib/selenium/webdriver/common/client_config.rb +97 -0
  61. data/lib/selenium/webdriver/common/driver.rb +13 -7
  62. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +0 -7
  63. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +0 -7
  64. data/lib/selenium/webdriver/common/driver_extensions/{has_bidi.rb → has_session_events.rb} +19 -7
  65. data/lib/selenium/webdriver/common/driver_finder.rb +35 -25
  66. data/lib/selenium/webdriver/common/error.rb +10 -3
  67. data/lib/selenium/webdriver/common/local_driver.rb +29 -12
  68. data/lib/selenium/webdriver/common/logger.rb +28 -0
  69. data/lib/selenium/webdriver/common/manager.rb +2 -0
  70. data/lib/selenium/webdriver/common/options.rb +41 -2
  71. data/lib/selenium/webdriver/common/platform.rb +1 -3
  72. data/lib/selenium/webdriver/common/proxy.rb +1 -9
  73. data/lib/selenium/webdriver/common/selenium_manager.rb +2 -1
  74. data/lib/selenium/webdriver/common/service.rb +4 -8
  75. data/lib/selenium/webdriver/common/service_manager.rb +36 -4
  76. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  77. data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
  78. data/lib/selenium/webdriver/common/virtual_authenticator/credential.rb +1 -1
  79. data/lib/selenium/webdriver/common/wait.rb +4 -1
  80. data/lib/selenium/webdriver/common/websocket_connection.rb +73 -37
  81. data/lib/selenium/webdriver/common.rb +2 -1
  82. data/lib/selenium/webdriver/devtools/response.rb +3 -3
  83. data/lib/selenium/webdriver/devtools.rb +1 -1
  84. data/lib/selenium/webdriver/edge/driver.rb +4 -3
  85. data/lib/selenium/webdriver/edge/service.rb +14 -0
  86. data/lib/selenium/webdriver/firefox/driver.rb +4 -6
  87. data/lib/selenium/webdriver/firefox/options.rb +19 -0
  88. data/lib/selenium/webdriver/firefox/profile.rb +13 -6
  89. data/lib/selenium/webdriver/firefox/service.rb +31 -2
  90. data/lib/selenium/webdriver/ie/driver.rb +4 -3
  91. data/lib/selenium/webdriver/ie/service.rb +10 -0
  92. data/lib/selenium/webdriver/remote/bidi_bridge.rb +44 -10
  93. data/lib/selenium/webdriver/remote/bridge.rb +20 -14
  94. data/lib/selenium/webdriver/remote/driver.rb +15 -4
  95. data/lib/selenium/webdriver/remote/features.rb +26 -1
  96. data/lib/selenium/webdriver/remote/http/common.rb +57 -12
  97. data/lib/selenium/webdriver/remote/http/curb.rb +6 -8
  98. data/lib/selenium/webdriver/remote/http/default.rb +56 -39
  99. data/lib/selenium/webdriver/safari/driver.rb +4 -3
  100. data/lib/selenium/webdriver/safari/options.rb +23 -4
  101. data/lib/selenium/webdriver/safari.rb +1 -6
  102. data/lib/selenium/webdriver/support/block_event_listener.rb +5 -1
  103. data/lib/selenium/webdriver/support/color.rb +14 -14
  104. data/lib/selenium/webdriver/support/event_firing_bridge.rb +5 -1
  105. data/lib/selenium/webdriver/support/guards/guard.rb +30 -14
  106. data/lib/selenium/webdriver/support/guards.rb +11 -4
  107. data/lib/selenium/webdriver/version.rb +1 -1
  108. data/lib/selenium/webdriver.rb +7 -2
  109. data/selenium-webdriver.gemspec +1 -1
  110. metadata +33 -11
  111. data/lib/selenium/webdriver/bidi/log/console_log_entry.rb +0 -35
  112. data/lib/selenium/webdriver/bidi/log/generic_log_entry.rb +0 -33
  113. data/lib/selenium/webdriver/bidi/log/javascript_log_entry.rb +0 -33
  114. data/lib/selenium/webdriver/bidi/log_inspector.rb +0 -147
@@ -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,402 @@
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 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.
86
+ def from_json(json_payload)
87
+ unless json_payload.is_a?(::Hash)
88
+ raise Error::WebDriverError, "#{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
+ expected = Array(field.scalar).flat_map { |primitive| PRIMITIVE_TYPES[primitive] || [] }
177
+ return if expected.empty? || expected.any? { |type| value.is_a?(type) }
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
+ expected = PRIMITIVE_TYPES[field.primitive]
208
+ return if expected.nil? || expected.any? { |type| value.is_a?(type) }
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
+ def wire_value(field, json_payload)
218
+ return field.fixed if fixed?(field)
219
+ return read(field, json_payload[field.wire_key]) if json_payload.key?(field.wire_key)
220
+ return UNSET unless field.required
221
+
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
235
+ end
236
+
237
+ def read(field, raw)
238
+ if raw.nil?
239
+ return raw if field.nullable
240
+
241
+ raise Error::WebDriverError, "#{name}##{field.name} received null but is not nullable"
242
+ end
243
+ check_shape(field, raw)
244
+ return Serialization.to_symbol("#{name}##{field.name}", raw, enum_hash(field)) if field.enum
245
+
246
+ if field.ref.nil?
247
+ check_primitive(field, raw) unless field.list
248
+ return raw
249
+ end
250
+
251
+ read_ref(field, raw)
252
+ end
253
+
254
+ # Reads a ref-typed value into its class. A `scalar` position is an inline union with
255
+ # a scalar arm collapsed onto its union ref (a map's string keys): a non-object leaf
256
+ # passes through instead of being handed to the object_only union, but only when it
257
+ # matches the arm's primitive (+scalar+ carries it). A list recurses per element.
258
+ def read_ref(field, raw)
259
+ klass = (@refs ||= {})[field.name] ||= Protocol.const_get(field.ref)
260
+ return read_list(field, raw, klass) if field.list
261
+
262
+ field.scalar && !raw.is_a?(::Hash) ? scalar_value(field, raw) : klass.from_json(raw)
263
+ end
264
+
265
+ # A declared list must arrive as an array; a scalar-shaped field (enum or ref, not a
266
+ # list) must not. An opaque field carries no shape descriptor, so it passes through.
267
+ def check_shape(field, raw)
268
+ return if field.list == raw.is_a?(::Array)
269
+ return unless field.list || field.enum || field.ref
270
+
271
+ raise Error::WebDriverError,
272
+ "#{name}##{field.name} expected #{field.list ? 'a list' : 'a single value'}, got #{raw.inspect}"
273
+ end
274
+
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]
282
+ }.freeze
283
+
284
+ def check_primitive(field, raw)
285
+ expected = PRIMITIVE_TYPES[field.primitive]
286
+ return if expected.nil? || expected.any? { |type| raw.is_a?(type) }
287
+
288
+ raise Error::WebDriverError, "#{name}##{field.name} expected #{field.primitive}, got #{raw.inspect}"
289
+ end
290
+
291
+ def enum_hash(field)
292
+ (@enums ||= {})[field.name] ||= Protocol.const_get(field.enum)
293
+ end
294
+
295
+ # Parses each element. A `scalar` field is a map encoded as `[key, value]` pairs, so
296
+ # every element must be a 2-item pair — each is read as one, and a malformed entry is
297
+ # rejected. Non-scalar lists recurse into nested lists; other elements deserialize.
298
+ def read_list(field, raw, klass)
299
+ raw.map do |element|
300
+ if field.scalar
301
+ read_map_entry(field, element, klass)
302
+ elsif element.is_a?(::Array)
303
+ read_list(field, element, klass)
304
+ else
305
+ klass.from_json(element)
306
+ end
307
+ end
308
+ end
309
+
310
+ # A map entry is a `[key, value]` pair. The key is `Ref / text` — an object key
311
+ # deserializes, a bare-string key passes through once validated against the arm's
312
+ # primitive. The value is the object-only Ref and always deserializes, so a bare scalar
313
+ # there is rejected (object_only holds at the value position). A non-pair element is a
314
+ # malformed entry and is rejected outright.
315
+ def read_map_entry(field, element, klass)
316
+ unless element.is_a?(::Array) && element.size == 2
317
+ raise Error::WebDriverError,
318
+ "#{name}##{field.name} expected a [key, value] pair, got #{element.inspect}"
319
+ end
320
+
321
+ key, value = element
322
+ key = key.is_a?(::Hash) ? klass.from_json(key) : scalar_value(field, key)
323
+ [key, klass.from_json(value)]
324
+ end
325
+
326
+ # A bare scalar at a scalar-tolerant union position must match one of the union's
327
+ # scalar-arm primitives (+scalar+ is a primitive name or an array of them); a
328
+ # 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
330
+ # left unchecked, matching the lenient default elsewhere.
331
+ 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) }
334
+
335
+ raise Error::WebDriverError,
336
+ "#{name}##{field.name} expected #{Array(field.scalar).join(' or ')}, got #{value.inspect}"
337
+ end
338
+
339
+ def extra(json_payload)
340
+ known = (@wire_keys ||= fields.map(&:wire_key))
341
+ json_payload.except(*known)
342
+ end
343
+
344
+ # Forward-compat signal: a property a closed type does not model is dropped and warned so
345
+ # schema drift is visible (an extensible type keeps its extras silently — the spec sanctions
346
+ # them). Tagged +:bidi_undeclared_property+ so a caller can silence it via +logger.ignore+.
347
+ def warn_undeclared(undeclared)
348
+ undeclared.each_key do |key|
349
+ WebDriver.logger.warn("#{name} received an undeclared property: #{key.inspect}",
350
+ id: :bidi_undeclared_property)
351
+ end
352
+ end
353
+ end
354
+
355
+ # @api private
356
+ module Serializable
357
+ def self.as_json(value)
358
+ case value
359
+ when Serializable then value.as_json
360
+ when ::Array then value.map { |element| as_json(element) }
361
+ when ::Hash then value.transform_values { |element| as_json(element) }
362
+ else value
363
+ end
364
+ end
365
+
366
+ # Omit UNSET fields; emit null only for nullable ones.
367
+ def as_json(*)
368
+ payload = {}
369
+ self.class.fields.each do |f|
370
+ value = public_send(f.name)
371
+ next if UNSET.equal?(value)
372
+ next if value.nil? && !f.nullable
373
+
374
+ value = Serialization.to_wire(value, Protocol.const_get(f.enum)) if f.enum
375
+ payload[f.wire_key] = Serializable.as_json(value)
376
+ end
377
+ merge_extensions!(payload) if self.class.extensible? && !extensions.empty?
378
+ payload
379
+ end
380
+
381
+ private
382
+
383
+ # Merge the passthrough extras onto the wire, erroring rather than letting an extra whose key
384
+ # is a declared field's wire key silently clobber that typed value; an extra is by definition
385
+ # a field the spec does not declare. Keys are stringified first so a symbol key (e.g. `name:`)
386
+ # cannot slip past the guard and then reappear as a duplicate wire key once serialized. The
387
+ # single gate every outbound path funnels through: +new+, +with+, and in-place mutation.
388
+ def merge_extensions!(payload)
389
+ extras = extensions.transform_keys(&:to_s)
390
+ collisions = extras.keys & self.class.fields.map(&:wire_key)
391
+ unless collisions.empty?
392
+ raise ::ArgumentError, "#{self.class.name} extensions shadow declared fields: #{collisions.join(', ')}"
393
+ end
394
+
395
+ payload.merge!(extras)
396
+ end
397
+ end
398
+ end
399
+ end
400
+ end # BiDi
401
+ end # WebDriver
402
+ end # Selenium