selenium-webdriver 4.51.0 → 4.52.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +15 -0
  3. data/Gemfile +14 -0
  4. data/README.md +20 -0
  5. data/bin/linux-arm64/selenium-manager +0 -0
  6. data/bin/linux-x86_64/selenium-manager +0 -0
  7. data/bin/macos/selenium-manager +0 -0
  8. data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +543 -226
  9. data/bin/selenium-manager.cdx.json +804 -722
  10. data/bin/windows/selenium-manager.exe +0 -0
  11. data/lib/selenium/webdriver/atoms/findElements.js +960 -250
  12. data/lib/selenium/webdriver/atoms/getAttribute.js +153 -127
  13. data/lib/selenium/webdriver/atoms/isDisplayed.js +433 -364
  14. data/lib/selenium/webdriver/atoms.rb +4 -1
  15. data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +6 -6
  16. data/lib/selenium/webdriver/bidi/protocol/browser.rb +21 -9
  17. data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +109 -39
  18. data/lib/selenium/webdriver/bidi/protocol/emulation.rb +89 -44
  19. data/lib/selenium/webdriver/bidi/protocol/input.rb +95 -31
  20. data/lib/selenium/webdriver/bidi/protocol/log.rb +4 -4
  21. data/lib/selenium/webdriver/bidi/protocol/moz_debugging.rb +12 -10
  22. data/lib/selenium/webdriver/bidi/protocol/moz_profiler.rb +4 -4
  23. data/lib/selenium/webdriver/bidi/protocol/network.rb +82 -39
  24. data/lib/selenium/webdriver/bidi/protocol/script.rb +38 -24
  25. data/lib/selenium/webdriver/bidi/protocol/session.rb +61 -8
  26. data/lib/selenium/webdriver/bidi/protocol/storage.rb +3 -3
  27. data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +3 -3
  28. data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +17 -9
  29. data/lib/selenium/webdriver/bidi/serialization/record.rb +66 -24
  30. data/lib/selenium/webdriver/bidi/serialization.rb +2 -2
  31. data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +228 -150
  32. data/lib/selenium/webdriver/common/client_config.rb +4 -1
  33. data/lib/selenium/webdriver/common/driver_finder.rb +5 -2
  34. data/lib/selenium/webdriver/common/websocket_connection.rb +51 -13
  35. data/lib/selenium/webdriver/remote/http/default.rb +17 -1
  36. data/lib/selenium/webdriver/version.rb +1 -1
  37. data/selenium-webdriver.gemspec +0 -12
  38. metadata +6 -163
@@ -86,12 +86,22 @@ module BiDiGenerate
86
86
  value.is_a?(String) ? "'#{value}'" : value.to_s
87
87
  end
88
88
 
89
+ # Source literal for a bound (a number, or an exclusive flag's `true`), grouping a long integer's
90
+ # digits as RuboCop's NumericLiterals expects.
91
+ def self.numeric_literal(value)
92
+ return value.to_s unless value.is_a?(Integer) && value.abs >= 10_000
93
+
94
+ grouped = value.abs.to_s.reverse.scan(/\d{1,3}/).join('_').reverse
95
+ value.negative? ? "-#{grouped}" : grouped
96
+ end
97
+
89
98
  # Renders `prefix(args)` on one line, or one argument per line when it would exceed
90
99
  # LINE_LIMIT at the given indent. open/close default to parentheses (pass {} for a
91
- # hash literal) so emitted calls and literals stay within RuboCop's length limit.
92
- def self.wrap_call(prefix, args, indent, open: '(', close: ')')
100
+ # hash literal) so emitted calls and literals stay within RuboCop's length limit. trailing
101
+ # counts what the caller appends to the last line (an entry's separating comma).
102
+ def self.wrap_call(prefix, args, indent, open: '(', close: ')', trailing: 0)
93
103
  one_line = "#{prefix}#{open}#{args.join(', ')}#{close}"
94
- return one_line if args.empty? || indent + one_line.length <= LINE_LIMIT
104
+ return one_line if args.empty? || indent + one_line.length + trailing <= LINE_LIMIT
95
105
 
96
106
  pad = ' ' * (indent + 2)
97
107
  "#{prefix}#{open}\n#{pad}#{args.join(",\n#{pad}")}\n#{' ' * indent}#{close}"
@@ -251,65 +261,14 @@ module BiDiGenerate
251
261
  end
252
262
  end
253
263
 
254
- # A browser-specific extension to a command, kept out of the shared class so a
255
- # non-matching browser never sees it. shared_params are the base command's own
256
- # (required) params, forwarded verbatim; vendor_params are the typed extra fields,
257
- # composed into the extensible params record's passthrough bag under their exact
258
- # wire keys. params_class/result_ref/wire_name mirror the base command.
259
- VendorCommand = Struct.new(:method_name, :wire_name, :result_ref, :params_class,
260
- :shared_params, :vendor_params, :spec_href, keyword_init: true) do
261
- def def_header(indent)
262
- BiDiGenerate.wrap_call("def #{method_name}", shared_params.map(&:sig_part) + vendor_params.map(&:sig_part),
263
- indent)
264
- end
265
-
266
- # The full `def … end` method block, fully indented from `indent`. Optional vendor
267
- # fields are placed into the passthrough bag only when set (UNSET stays omitted), so
268
- # they serialize exactly like a field on the extensible record.
269
- def render_lines(indent)
270
- body = ' ' * (indent + 2)
271
- [*doc_lines(' ' * indent), "#{' ' * indent}#{def_header(indent)}", *extensions_lines(body, indent),
272
- params_line(body, indent), execute_line(body, indent), "#{' ' * indent}end"]
273
- end
274
-
275
- def doc_lines(pad)
276
- lines = ["#{pad}# @api private", "#{pad}# @see #{BiDiGenerate::BIDI_DOC_URL}"]
277
- lines << "#{pad}# @see #{spec_href}" if spec_href
278
- lines
279
- end
280
-
281
- # The extensible passthrough bag, carrying each set vendor field under its exact wire key.
282
- def extensions_lines(body, indent)
283
- inner = ' ' * (indent + 4)
284
- entries = vendor_params.map { |p| "#{inner}'#{p.wire_name}' => #{p.ruby_name}" }.join(",\n")
285
- ["#{body}extensions = {", entries, "#{body}}.reject { |_, value| Serialization::UNSET.equal?(value) }"]
286
- end
287
-
288
- def params_line(body, indent)
289
- kwargs = shared_params.map { |p| "#{p.ruby_name}: #{p.ruby_name}" } + ['extensions: extensions']
290
- "#{body}#{BiDiGenerate.wrap_call("params = #{params_class}.new", kwargs, indent + 2)}"
291
- end
292
-
293
- def execute_line(body, indent)
294
- args = ["cmd: '#{wire_name}'", 'params: params']
295
- args << "result: #{result_ref}" if result_ref
296
- "#{body}#{BiDiGenerate.wrap_call('execute', args, indent + 2)}"
297
- end
298
-
299
- def rbs_signature
300
- params = (shared_params.map(&:rbs_part) + vendor_params.map(&:rbs_part)).join(', ')
301
- ret = result_ref ? "::Selenium::WebDriver::BiDi::Protocol::#{result_ref}" : 'untyped'
302
- "(#{params}) -> #{ret}"
303
- end
304
- end
305
-
306
264
  # A namespaced group of browser-specific commands (e.g. Firefox's), emitted as a subclass of
307
265
  # the domain: it overrides the commands a vendor extends with `moz:` fields and adds the
308
266
  # commands a vendor defines in the domain (`webExtension.moz:listExtensions`).
309
267
  # A subclass (rather than a runtime-mixed module) keeps the vendor signatures statically
310
268
  # visible to type checkers, and is constructed directly (`<Name>.new(source)`) for a
311
- # matching session — no factory or runtime mix-in.
312
- VendorModule = Struct.new(:name, :namespace, :parent, :commands, keyword_init: true) do
269
+ # matching session — no factory or runtime mix-in. types are the variant's own params
270
+ # records, which shadow the shared ones so its overriding commands build them by bare name.
271
+ VendorModule = Struct.new(:name, :namespace, :parent, :types, :commands, keyword_init: true) do
313
272
  def render(indent)
314
273
  pad = ' ' * indent
315
274
  lines = [
@@ -318,6 +277,7 @@ module BiDiGenerate
318
277
  "#{pad}# Construct #{name}.new(source) for a matching session; other sessions use #{parent}.",
319
278
  "#{pad}class #{name} < #{parent}"
320
279
  ]
280
+ types.each { |type| lines.concat(type_lines(type, indent + 2)) << '' }
321
281
  commands.each_with_index do |cmd, index|
322
282
  lines << '' unless index.zero?
323
283
  lines.concat(cmd.render_lines(indent + 2))
@@ -325,6 +285,13 @@ module BiDiGenerate
325
285
  lines << "#{pad}end"
326
286
  lines.join("\n")
327
287
  end
288
+
289
+ def type_lines(type, indent)
290
+ pad = ' ' * indent
291
+ lines = ["#{pad}# @api private", "#{pad}# @see #{BiDiGenerate::BIDI_DOC_URL}"]
292
+ lines << "#{pad}# @see #{type.spec_href}" if type.spec_href
293
+ lines << "#{pad}#{type.define_assignment(type.ruby_name, indent)}"
294
+ end
328
295
  end
329
296
 
330
297
  # payload_ref is the Protocol-relative class the event's params parse into (nil when
@@ -355,16 +322,16 @@ module BiDiGenerate
355
322
  # for a scalar/opaque field); list wraps it in an array. wire_key is the exact
356
323
  # JSON payload key (the schema's `wire` name, baked verbatim).
357
324
  FieldIR = Struct.new(:ruby_name, :wire_key, :required, :nullable, :ref, :list, :enum, :primitive, :scalar, :const,
358
- :map_of, :rbs, keyword_init: true) do
325
+ :map_of, :bounds, :rbs, keyword_init: true) do
359
326
  # A `Serialization::Record.define` spec entry: `name: 'jsonKey'` shorthand, or
360
327
  # `name: {wire_key:, …}` when the field carries JSON facts beyond its name.
361
328
  # enum carries the allowed-values constant path, validated at construction.
362
- def spec_entry(indent = 0)
329
+ def spec_entry(indent = 0, trailing: 0)
363
330
  meta = value_facts
364
331
  return "#{ruby_name}: '#{wire_key}'" if meta.empty?
365
332
 
366
333
  meta.unshift("wire_key: '#{wire_key}'")
367
- BiDiGenerate.wrap_call("#{ruby_name}: ", meta, indent, open: '{', close: '}')
334
+ BiDiGenerate.wrap_call("#{ruby_name}: ", meta, indent, open: '{', close: '}', trailing: trailing)
368
335
  end
369
336
 
370
337
  # The JSON facts beyond the field's name, in the order Record.define reads them. A
@@ -379,22 +346,28 @@ module BiDiGenerate
379
346
  end
380
347
 
381
348
  # The facts describing the value's type and shape: what the runtime reads it as and checks
382
- # it against. `map_of` is the value primitive of a string-keyed map (`{*text => text}`).
349
+ # it against. For a list, ref/scalar/enum/primitive describe each element. `map_of` is the
350
+ # value primitive(s) of a string-keyed map (`{*text => text}`).
383
351
  def type_facts
384
352
  facts = []
385
353
  facts << "ref: '#{ref}'" if ref
386
354
  facts << 'list: true' if list
387
- facts << "scalar: #{scalar_literal}" if scalar
355
+ facts << "scalar: #{primitive_literal(scalar)}" if scalar
388
356
  facts << "enum: '#{enum}'" if enum
389
- facts << "primitive: '#{primitive}'" if primitive
390
- facts << "map: '#{map_of}'" if map_of
391
- facts
357
+ facts << "primitive: #{primitive_literal(primitive)}" if primitive
358
+ facts << "map: #{primitive_literal(map_of)}" if map_of
359
+ facts + bound_facts
392
360
  end
393
361
 
394
- # The `scalar` primitive(s) a bare non-object wire value must match at a scalar-tolerant
395
- # union position: a single primitive string, or an array when the union's scalar arms differ.
396
- def scalar_literal
397
- scalar.is_a?(::Array) ? "[#{scalar.map { |s| "'#{s}'" }.join(', ')}]" : "'#{scalar}'"
362
+ # The numeric bounds every value the field carries is held to (for a list or map, each numeric one).
363
+ def bound_facts
364
+ bounds.map { |key, value| "#{BiDiGenerate.camel_to_snake(key)}: #{BiDiGenerate.numeric_literal(value)}" }
365
+ end
366
+
367
+ # A single primitive string, or an array when a value may match several: a union of scalars
368
+ # (`primitive`), a scalar-tolerant union position (`scalar`), or a map's union of values (`map`).
369
+ def primitive_literal(value)
370
+ value.is_a?(::Array) ? BiDiGenerate.word_array(value) : "'#{value}'"
398
371
  end
399
372
 
400
373
  # The `self.new` keyword for this field — a user-supplied input carrying the field's
@@ -425,7 +398,8 @@ module BiDiGenerate
425
398
  def define_entries(entry_indent)
426
399
  entries = []
427
400
  entries << discriminator_entry if discriminator
428
- entries.concat(fields.map { |f| f.spec_entry(entry_indent) })
401
+ last = fields.last unless extensible
402
+ entries.concat(fields.map { |f| f.spec_entry(entry_indent, trailing: f.equal?(last) ? 0 : 1) })
429
403
  entries << 'extensible: true' if extensible
430
404
  entries
431
405
  end
@@ -586,11 +560,62 @@ module BiDiGenerate
586
560
  @events = schema['events'] + vendor_events
587
561
  @domains = schema['domains'] || {}
588
562
  promote_command_params_records!
563
+ @vendor_params = fold_vendor_extensions!
589
564
  end
590
565
 
591
566
  # A vendor module's domain keeps its wire prefix (`moz:debugging`); a spec domain has none.
592
567
  def vendor_domain?(domain) = domain.include?(':')
593
568
 
569
+ # A vendor's fields on a spec type are declared fields (ADR 17786), so they are typed and never ride
570
+ # the extensions map. Fields on a spec command's params go to a record of the vendor variant's own (see
571
+ # vendor_modules_for), returned per namespace; fields on any other type join it as optional fields.
572
+ def fold_vendor_extensions!
573
+ @vendor_field_refs = []
574
+ @vendor.to_h do |namespace, section|
575
+ params = {}
576
+ (section['extends'] || {}).each do |type_name, entry|
577
+ fields = vendor_extension_fields(namespace, type_name, entry)
578
+ if params_command(type_name)
579
+ params[type_name] = fields
580
+ else
581
+ optional = fields.map { |field| field.merge('required' => false) }
582
+ @types[type_name] = @types[type_name].merge('fields' => @types[type_name]['fields'] + optional)
583
+ end
584
+ end
585
+ [namespace, params]
586
+ end
587
+ end
588
+
589
+ # Also records the vendor types the fields reference, so vendor_types_reached emits them.
590
+ def vendor_extension_fields(namespace, type_name, entry)
591
+ type = @types[type_name] || raise("vendor #{namespace} extends unknown type #{type_name}")
592
+ raise "vendor #{namespace} extends #{type_name}, which is not a record" unless type['kind'] == 'record'
593
+
594
+ fields = entry['fields'].map { |field| hoist_vendor_enum(type_name, field) }
595
+ @vendor_field_refs.concat(fields.flat_map { |field| refs_in_type(field['type']) })
596
+ fields
597
+ end
598
+
599
+ # The projector leaves a vendor field's enum inline (`moz:scope`), so name it after the field in the
600
+ # extended type's domain (`browsingContext.MozScope`) to emit and validate it like a spec enum.
601
+ def hoist_vendor_enum(type_name, field)
602
+ values = field['type']['enum']
603
+ return field unless values
604
+
605
+ domain = type_name.split('.', 2).first
606
+ name = "#{domain}.#{BiDiGenerate.snake_to_class_name(BiDiGenerate.camel_to_snake(field['name']))}"
607
+ enum = {'kind' => 'enum', 'values' => values, 'primitive' => field['type']['primitive']}
608
+ raise "vendor enum #{name} conflicts with an existing type" if @types.key?(name) && @types[name] != enum
609
+
610
+ @types[name] = enum
611
+ field.merge('type' => field['type'].slice('nullable').merge('ref' => name))
612
+ end
613
+
614
+ # The spec command that sends a type as its params, or nil.
615
+ def params_command(type_name)
616
+ @commands.find { |c| c.dig('params', 'ref') == type_name && !vendor_domain?(c['domain']) }
617
+ end
618
+
594
619
  def vendor_messages(kind)
595
620
  @vendor.values.flat_map { |section| section[kind] || [] }
596
621
  end
@@ -681,24 +706,23 @@ module BiDiGenerate
681
706
  ruby_path(name).sub(/\A#{Regexp.escape(prefix)}/, '')
682
707
  end
683
708
 
684
- # The vendor modules a domain carries, one per namespace (`moz` → module `Moz`). The
685
- # schema's `vendor` section names, per namespace, which shared type each vendor extends;
686
- # we map that type back to the command that sends it, so the vendor method mirrors the
709
+ # The vendor modules a domain carries, one per namespace (`moz` → module `Moz`). Each
710
+ # command whose params a vendor extends is overridden there with a closed params record of
711
+ # the variant's own (the shared fields plus the vendor's), so the vendor method mirrors the
687
712
  # base command's wire method and result while adding the typed vendor fields. Empty for
688
713
  # any domain (or schema) with no vendor extensions, so non-vendor output is unaffected.
689
714
  def vendor_modules_for(domain)
690
715
  parent = BiDiGenerate.snake_to_class_name(BiDiGenerate.camel_to_snake(domain))
691
- @vendor.filter_map do |namespace, spec|
692
- overrides = (spec['extends'] || {}).filter_map do |type_name, entry|
693
- cmd = @commands.find { |c| c.dig('params', 'ref') == type_name }
694
- build_vendor_command(cmd, type_name, entry, namespace) if cmd && cmd['domain'] == domain
716
+ @vendor_params.filter_map do |namespace, extended|
717
+ overrides = extended.filter_map do |type_name, fields|
718
+ cmd = params_command(type_name)
719
+ build_vendor_override(cmd, type_name, fields, namespace) if cmd['domain'] == domain
695
720
  end
696
721
  added = vendor_added_commands(domain, namespace).map { |cmd| build_added_command(cmd, namespace, domain) }
697
- commands = overrides + added
698
- next if commands.empty?
722
+ next if overrides.empty? && added.empty?
699
723
 
700
724
  VendorModule.new(name: BiDiGenerate.snake_to_class_name(namespace), namespace: namespace, parent: parent,
701
- commands: commands)
725
+ types: overrides.map(&:first), commands: overrides.map(&:last) + added)
702
726
  end
703
727
  end
704
728
 
@@ -716,35 +740,27 @@ module BiDiGenerate
716
740
  command
717
741
  end
718
742
 
719
- def build_vendor_command(cmd, type_name, entry, namespace)
720
- shared = record_params(@types[type_name]['fields'], extensible_record?(type_name))
721
- taken = shared.map(&:ruby_name)
722
- VendorCommand.new(
723
- method_name: BiDiGenerate.safe_method_name(BiDiGenerate.camel_to_snake(cmd['name'])),
724
- wire_name: cmd['method'],
725
- result_ref: cmd['result'] && structured_ref(cmd['result']['ref']),
726
- params_class: BiDiGenerate.type_class_name(type_name),
727
- shared_params: shared,
728
- vendor_params: entry['fields'].map { |field| vendor_param(field, namespace, taken) },
729
- spec_href: cmd['specHref']
730
- )
743
+ # The vendor variant's params record and the command that builds it, as [record, command].
744
+ def build_vendor_override(cmd, type_name, fields, namespace)
745
+ shared = @types[type_name]['fields']
746
+ taken = shared.map { |field| field_name(field, false) }
747
+ type = @types[type_name].merge('fields' => shared + fields.map { |f| vendor_field(f, namespace, taken) })
748
+ record = record_class(type_name, type)
749
+ command = Command.new(wire_name: cmd['method'],
750
+ method_name: BiDiGenerate.safe_method_name(BiDiGenerate.camel_to_snake(cmd['name'])),
751
+ params: record_params(type['fields'], false),
752
+ result_ref: cmd['result'] && structured_ref(cmd['result']['ref']),
753
+ params_class: record.ruby_name, union_params: false, spec_href: cmd['specHref'])
754
+ [record, command]
731
755
  end
732
756
 
733
757
  # A vendor field's ruby name drops its namespace prefix (`moz:permanent` → permanent): the
734
758
  # module already scopes it, so re-encoding the namespace in every identifier is redundant. The
735
759
  # wire key is untouched. Falls back to the prefixed name only if stripping would collide with a
736
- # shared param on the same command.
737
- def vendor_param(field, namespace, taken)
738
- stripped = field['name'].sub(/\A#{Regexp.escape(namespace)}:/, '')
739
- ruby_name = BiDiGenerate.safe_field_name(BiDiGenerate.camel_to_snake(stripped))
740
- ruby_name = BiDiGenerate.safe_field_name(BiDiGenerate.camel_to_snake(field['name'])) if taken.include?(ruby_name)
741
- Param.new(
742
- ruby_name: ruby_name,
743
- wire_name: field['wire'],
744
- required: field['required'],
745
- enum: enum_const(field['type']),
746
- rbs: rbs_type(field['type'])
747
- )
760
+ # shared field on the same record.
761
+ def vendor_field(field, namespace, taken)
762
+ stripped = field.merge('name' => field['name'].delete_prefix("#{namespace}:"))
763
+ taken.include?(field_name(stripped, false)) ? field : stripped
748
764
  end
749
765
 
750
766
  def type_kind(ref)
@@ -798,10 +814,11 @@ module BiDiGenerate
798
814
  end
799
815
 
800
816
  # The vendor types a vendor-added command in this spec domain reaches through its params and
801
- # result, so they are emitted in the domain's module beside the command that returns them.
817
+ # result, or a vendor field folded onto a spec type references, so they are emitted in the
818
+ # domain's module beside what uses them.
802
819
  def vendor_types_reached(domain)
803
820
  commands = @vendor.keys.flat_map { |namespace| vendor_added_commands(domain, namespace) }
804
- pending = commands.flat_map { |c| [c.dig('params', 'ref'), c.dig('result', 'ref')] }.compact
821
+ pending = commands.flat_map { |c| [c.dig('params', 'ref'), c.dig('result', 'ref')] }.compact + @vendor_field_refs
805
822
  reached = Set.new
806
823
  until pending.empty?
807
824
  name = pending.pop
@@ -921,33 +938,61 @@ module BiDiGenerate
921
938
  # Frozen because it is shared across callers.
922
939
  OPAQUE = {ref: nil, list: false, rbs: 'untyped'}.freeze
923
940
 
924
- # Projects a schema type node to {ref:, list:, nullable:, rbs:}. Deriving the
941
+ # Projects a schema type node to {ref:, list:, nullable:, primitive:, bounds:, rbs:}. Deriving the
925
942
  # serialization facts and the RBS signature from one walk keeps them from drifting
926
943
  # apart when the schema shape changes.
927
944
  def resolve(node)
928
945
  nullable = node['nullable'] ? true : false
929
946
  if node.key?('list')
930
- element = resolve(node['list'])
931
- return {ref: element[:ref], list: true, nullable: nullable, scalar: element[:scalar],
932
- rbs: nilable("Array[#{element[:rbs]}]", nullable)}
947
+ list = resolve_list(node['list'])
948
+ return list.merge(nullable: nullable, rbs: nilable(list[:rbs], nullable))
933
949
  end
934
950
  if node.key?('ref')
935
- named = resolve_named(node['ref'])
951
+ named = narrowed(resolve_named(node['ref']), node)
936
952
  return {ref: named[:ref], list: named[:list], nullable: nullable, scalar: named[:scalar],
937
- map: named[:map], rbs: nilable(named[:rbs], nullable)}
953
+ map: named[:map], primitive: named[:primitive], bounds: named[:bounds],
954
+ rbs: nilable(named[:rbs], nullable)}
938
955
  end
939
956
  return resolve_union(node, nullable) if node.key?('union')
940
957
 
941
- {ref: nil, list: false, nullable: nullable, rbs: nilable(scalar_rbs(node), nullable)}
958
+ {ref: nil, list: false, nullable: nullable, primitive: checkable_primitive(node),
959
+ bounds: node.slice(*BOUND_KEYS), rbs: nilable(scalar_rbs(node), nullable)}
960
+ end
961
+
962
+ # A ref's own bounds narrow those of the alias it names (`js-uint .ge 1` is 1..2**53-1).
963
+ def narrowed(named, node)
964
+ own = node.slice(*BOUND_KEYS)
965
+ own.empty? ? named : named.merge(bounds: narrow_bounds(named[:bounds] || {}, own))
966
+ end
967
+
968
+ # A list's facts describe its element, so `primitive` (like `ref`) is the element's. A nested
969
+ # list's elements are lists, so no primitive check applies to them.
970
+ def resolve_list(element_node)
971
+ element = resolve(element_node)
972
+ # Record checks every element as non-null, so a nullable one would reject valid wire data.
973
+ raise "nullable list element #{element_node.inspect} is not supported by Record" if element[:nullable]
974
+
975
+ primitive, bounds = element.values_at(:primitive, :bounds) unless element[:list]
976
+ {ref: element[:ref], list: true, scalar: element[:scalar], primitive: primitive, bounds: bounds,
977
+ rbs: "Array[#{element[:rbs]}]"}
978
+ end
979
+
980
+ # Reached through alias chains too (resolve_named), so js-uint is checked as an integer.
981
+ def checkable_primitive(node)
982
+ node['primitive'] if CHECKABLE_PRIMITIVES.include?(node['primitive'])
942
983
  end
943
984
 
944
985
  # An inline union of one union-typed arm plus scalars (e.g. a MappingRemoteValue entry,
945
986
  # RemoteValue / string) is carried as that union ref so nested entries are typed. Because
946
987
  # the union is object_only, a bare-scalar sibling would raise there — so the projector's
947
988
  # `scalar` signal (a bare-scalar arm is present) is forwarded, and the runtime passes a
948
- # non-object leaf through instead (the map's string keys). Any other shape (a record arm,
949
- # multiple structured arms, all scalars) stays opaque.
989
+ # non-object leaf through instead (the map's string keys). A union of scalars carries their
990
+ # primitives; any other shape (a record arm, multiple structured arms, a scalar beside an
991
+ # enum) stays opaque.
950
992
  def resolve_union(node, nullable)
993
+ primitives = union_primitives(node['union'])
994
+ return scalar_union(primitives, nullable).merge(bounds: union_bounds(node['union'])) if primitives
995
+
951
996
  refs = node['union'].select { |arm| arm.key?('ref') }
952
997
  opaque = {ref: nil, list: false, nullable: nullable, rbs: nilable('untyped', nullable)}
953
998
  return opaque unless refs.one? && union_ref?(refs.first['ref'])
@@ -957,6 +1002,33 @@ module BiDiGenerate
957
1002
  rbs: nilable('untyped', nullable)}
958
1003
  end
959
1004
 
1005
+ # The primitives a union of scalars admits (pageRanges' `js-uint / text`), or nil when any arm
1006
+ # is something else (an enum, a const, a ref), which no primitive check can admit.
1007
+ def union_primitives(arms)
1008
+ primitives = arms.map do |arm|
1009
+ resolved = resolve(arm)
1010
+ resolved[:primitive] unless arm.key?('const') || resolved[:list] || resolved[:nullable]
1011
+ end
1012
+ primitives.flatten.uniq unless primitives.include?(nil)
1013
+ end
1014
+
1015
+ # The runtime holds every numeric value a field carries to one set of bounds, so numeric arms
1016
+ # that disagree (`js-uint / float`) would be checked wrongly; fail generation instead.
1017
+ def union_bounds(arms)
1018
+ numeric = arms.map { |arm| resolve(arm) }.select { |arm| NUMERIC_PRIMITIVES.include?(arm[:primitive]) }
1019
+ bounds = numeric.map { |arm| arm[:bounds] || {} }.uniq
1020
+ raise "numeric union arms #{arms} have different bounds: #{bounds}" if bounds.size > 1
1021
+
1022
+ bounds.first
1023
+ end
1024
+
1025
+ def scalar_union(primitives, nullable)
1026
+ rbs = primitives.map { |primitive| PRIMITIVE_RBS.fetch(primitive) }.join(' | ')
1027
+ rbs = "(#{rbs})" if nullable && primitives.size > 1
1028
+ {ref: nil, list: false, nullable: nullable, primitive: primitives.one? ? primitives.first : primitives,
1029
+ rbs: nilable(rbs, nullable)}
1030
+ end
1031
+
960
1032
  # True when a ref (following aliases) is a union — the only arm whose from_json tolerates a
961
1033
  # scalar sibling. A record arm would raise on one, so it is not carried.
962
1034
  def union_ref?(name)
@@ -991,13 +1063,20 @@ module BiDiGenerate
991
1063
  # A field-less record is either a genuinely empty one (EmptyParams, Extensible — opaque)
992
1064
  # or the projector's stand-in for an inline map (`{*text => text}` is hoisted to a record
993
1065
  # with no fields and the value type under `map`, e.g. script.NodePropertiesAttributes).
994
- # A primitive-valued map becomes a `map` descriptor the runtime checks and a typed Hash;
995
- # a ref- or union-valued one (the Mozilla vendor types) stays opaque for now.
1066
+ # A map whose values are a primitive, or a union of primitives (Firefox `prefs`), becomes a
1067
+ # `map` descriptor the runtime checks and a typed Hash; a ref-valued one stays opaque for now.
996
1068
  def map_record(type)
997
- primitive = type.dig('map', 'primitive')
998
- return OPAQUE unless primitive && CHECKABLE_PRIMITIVES.include?(primitive)
1069
+ primitives = map_value_primitives(type['map'])
1070
+ return OPAQUE unless primitives
1071
+
1072
+ rbs = primitives.map { |primitive| PRIMITIVE_RBS.fetch(primitive) }.join(' | ')
1073
+ {ref: nil, list: false, map: primitives.one? ? primitives.first : primitives,
1074
+ bounds: union_bounds(type['map']['union'] || [type['map']]), rbs: "Hash[String, #{rbs}]"}
1075
+ end
999
1076
 
1000
- {ref: nil, list: false, map: primitive, rbs: "Hash[String, #{PRIMITIVE_RBS.fetch(primitive)}]"}
1077
+ # Each arm's checkable primitive, following aliases (js-int is an integer); nil if any arm has none.
1078
+ def map_value_primitives(node)
1079
+ union_primitives(node['union'] || [node]) if node
1001
1080
  end
1002
1081
 
1003
1082
  # A named structured type's serialization ref (nil for a dotless/global type, never
@@ -1025,14 +1104,11 @@ module BiDiGenerate
1025
1104
 
1026
1105
  def resolve_named_alias(name, inner, seen)
1027
1106
  return named_union(name) if inner.key?('union')
1028
- return resolve_named(inner['ref'], seen) if inner.key?('ref')
1029
-
1030
- if inner.key?('list')
1031
- element = resolve(inner['list'])
1032
- return {ref: element[:ref], list: true, scalar: element[:scalar], rbs: "Array[#{element[:rbs]}]"}
1033
- end
1107
+ return narrowed(resolve_named(inner['ref'], seen), inner) if inner.key?('ref')
1108
+ return resolve_list(inner['list']) if inner.key?('list')
1034
1109
 
1035
- {ref: nil, list: false, rbs: scalar_rbs(inner)}
1110
+ {ref: nil, list: false, primitive: checkable_primitive(inner), bounds: inner.slice(*BOUND_KEYS),
1111
+ rbs: scalar_rbs(inner)}
1036
1112
  end
1037
1113
 
1038
1114
  def nilable(type, flag)
@@ -1055,9 +1131,7 @@ module BiDiGenerate
1055
1131
  fields = type['fields'].reject { |f| baked_discriminator?(f) }.map { |f| field_ir(f, extensible) }
1056
1132
  # Every extensible type gets the extensions store: an undeclared wire key is preserved
1057
1133
  # and echoed back on any type the spec marks extensible, whether or not it is re-sendable.
1058
- # Extensibility alone is the signal; send-reachability does not enter into it. A type a
1059
- # vendor extends is open too: its vendor variant composes the vendor fields through that
1060
- # store (see VendorCommand), which a closed record would reject.
1134
+ # Extensibility alone is the signal; send-reachability does not enter into it.
1061
1135
  TypeClass.new(ruby_name: BiDiGenerate.type_class_name(name), fields: fields,
1062
1136
  discriminator: discriminator, extensible: extensible,
1063
1137
  schema_name: name, synthetic: type['synthetic'] ? true : false,
@@ -1065,12 +1139,8 @@ module BiDiGenerate
1065
1139
  **directionality(name))
1066
1140
  end
1067
1141
 
1068
- def vendor_extended?(name)
1069
- @vendor.values.any? { |spec| (spec['extends'] || {}).key?(name) }
1070
- end
1071
-
1072
1142
  def extensible_record?(name)
1073
- @types.dig(name, 'extensible') || vendor_extended?(name) ? true : false
1143
+ @types.dig(name, 'extensible') ? true : false
1074
1144
  end
1075
1145
 
1076
1146
  def field_name(field, extensible)
@@ -1091,8 +1161,9 @@ module BiDiGenerate
1091
1161
  FieldIR.new(ruby_name: ruby_name, wire_key: field['wire'],
1092
1162
  required: field['required'], nullable: resolved[:nullable],
1093
1163
  ref: resolved[:ref], list: resolved[:list], enum: enum_const(field['type']),
1094
- primitive: leaf_primitive(field['type']), scalar: resolved[:scalar],
1095
- const: leaf_const(field['type']), map_of: resolved[:map], rbs: resolved[:rbs])
1164
+ primitive: resolved[:primitive], scalar: resolved[:scalar],
1165
+ const: leaf_const(field['type']), map_of: resolved[:map], bounds: resolved[:bounds] || {},
1166
+ rbs: resolved[:rbs])
1096
1167
  end
1097
1168
 
1098
1169
  # The literal value of a const field, following alias chains, so the runtime can reject a
@@ -1111,21 +1182,24 @@ module BiDiGenerate
1111
1182
  leaf_const(type['type'], seen)
1112
1183
  end
1113
1184
 
1114
- # The runtime-checkable scalar primitive of a field, following alias chains so a
1115
- # scalar hidden behind a named alias (js-uint -> integer, browsingContext.BrowsingContext
1116
- # -> string) is typed rather than opaque. The projector carries the primitive on the
1117
- # alias node; this surfaces it onto the field. Nil for a list (its elements are not
1118
- # scalar-checked), a record/union ref, an enum, a const, or an opaque value.
1119
- def leaf_primitive(node, seen = {})
1120
- return node['primitive'] if node.key?('primitive') && CHECKABLE_PRIMITIVES.include?(node['primitive'])
1121
- return nil unless node.key?('ref')
1185
+ # A value must satisfy both, so each side keeps the tighter bound, and at an equal value the exclusive one.
1186
+ def narrow_bounds(bounds, narrowing)
1187
+ result = bounds.dup
1188
+ {'min' => 'exclusiveMin', 'max' => 'exclusiveMax'}.each do |side, flag|
1189
+ next unless narrowing.key?(side) && tighter_bound?(side, flag, narrowing, bounds)
1122
1190
 
1123
- name = node['ref']
1124
- type = @types[name]
1125
- return nil if seen[name] || type.nil? || type['kind'] != 'alias'
1191
+ result[side] = narrowing[side]
1192
+ result.delete(flag)
1193
+ result[flag] = true if narrowing[flag]
1194
+ end
1195
+ result.slice(*BOUND_KEYS)
1196
+ end
1126
1197
 
1127
- seen[name] = true
1128
- leaf_primitive(type['type'], seen)
1198
+ def tighter_bound?(side, flag, candidate, current)
1199
+ return true unless current.key?(side)
1200
+ return candidate[flag] && !current[flag] if candidate[side] == current[side]
1201
+
1202
+ side == 'min' ? candidate[side] > current[side] : candidate[side] < current[side]
1129
1203
  end
1130
1204
 
1131
1205
  def union_class(name)
@@ -1238,6 +1312,10 @@ module BiDiGenerate
1238
1312
  # const, or opaque value) gets no descriptor and is left unchecked — lenient, so a missed
1239
1313
  # check fails open rather than a wrong strict default rejecting valid data.
1240
1314
  CHECKABLE_PRIMITIVES = %w[string number integer boolean].freeze
1315
+ NUMERIC_PRIMITIVES = %w[number integer].freeze
1316
+
1317
+ # The schema's numeric bound keys, in the order the field facts render them.
1318
+ BOUND_KEYS = %w[min max exclusiveMin exclusiveMax].freeze
1241
1319
 
1242
1320
  # The leaf of +resolve+: the bare scalar type, before any nullable wrap. An alias's
1243
1321
  # own nullable is intentionally left off — only the referencing node's is applied.
@@ -35,7 +35,7 @@ module Selenium
35
35
  end
36
36
  end
37
37
 
38
- attr_accessor :open_timeout, :read_timeout, :max_redirects, :proxy
38
+ attr_accessor :open_timeout, :read_timeout, :max_redirects, :proxy, :ignore_certificates
39
39
  attr_writer :extra_headers, :user_agent
40
40
  attr_reader :server_url
41
41
 
@@ -44,6 +44,7 @@ module Selenium
44
44
  # @param [Numeric] read_timeout Seconds to wait for a response.
45
45
  # @param [Integer] max_redirects Maximum number of redirects to follow.
46
46
  # @param [Proxy] proxy Proxy to use for the connection.
47
+ # @param [Boolean] ignore_certificates Skip verifying the server certificate (unsafe outside a trusted network).
47
48
  # @param [Hash] extra_headers Additional headers to send with each request.
48
49
  # @param [String] user_agent Value to send as the User-Agent header.
49
50
  # @param [String, URI] server_url URL of the server to connect to.
@@ -52,6 +53,7 @@ module Selenium
52
53
  read_timeout: 120,
53
54
  max_redirects: 20,
54
55
  proxy: nil,
56
+ ignore_certificates: false,
55
57
  extra_headers: nil,
56
58
  user_agent: nil,
57
59
  server_url: nil)
@@ -59,6 +61,7 @@ module Selenium
59
61
  @read_timeout = read_timeout
60
62
  @max_redirects = max_redirects
61
63
  @proxy = proxy || proxy_from_environment
64
+ @ignore_certificates = ignore_certificates
62
65
  @extra_headers = extra_headers
63
66
  @user_agent = user_agent
64
67
  self.server_url = server_url
@@ -97,8 +97,11 @@ module Selenium
97
97
  args << @options.binary.gsub('\\', '\\\\\\')
98
98
  end
99
99
  if @options.proxy
100
- args << '--proxy'
101
- args << (@options.proxy.ssl || @options.proxy.http)
100
+ proxy = @options.proxy.ssl || @options.proxy.http
101
+ if proxy
102
+ args << '--proxy'
103
+ args << proxy
104
+ end
102
105
  end
103
106
  args
104
107
  end