diff --git a/javascript/selenium-webdriver/normalize_bidi_ast.mjs b/javascript/selenium-webdriver/normalize_bidi_ast.mjs index a388d62e1ecb8..6812d750fb826 100644 --- a/javascript/selenium-webdriver/normalize_bidi_ast.mjs +++ b/javascript/selenium-webdriver/normalize_bidi_ast.mjs @@ -97,6 +97,16 @@ function groupRef(value) { return { Type: 'group', Value: value, Unwrapped: false } } +/** True when `entry` is a string/number/bool literal (`{Type:'literal', Value}`). */ +function isLiteral(entry) { + return entry && typeof entry === 'object' && entry.Type === 'literal' +} + +/** True when `entry` is the CDDL null keyword (bare `'null'`) or a `nil`/`null` prelude ref. */ +function isNullArm(entry) { + return entry === 'null' || (isGroupRef(entry) && (entry.Value === 'null' || entry.Value === 'nil')) +} + /** * Drop the leading run of `label` that restates `ownerLocal`, backing off to a * camelCase boundary, so `ContinueWithAuthParameters` + `ContinueWithAuthCredentials` @@ -151,9 +161,11 @@ function eachPropertyDeep(properties, fn) { } /** - * Rewrite fields whose type is a union of >= 2 string literals into a reference - * to a synthetic enum def, and append those enum defs. Single-literal fields - * (discriminators) are left untouched. Returns a new AST array. + * Rewrite fields whose type is a choice of >= 2 string literals (optionally with a + * null alternative) into a reference to a synthetic enum def, and append those enum + * defs. A null alternative is kept on the field so the enum stays nullable; the enum + * def itself holds only the literals. Single-literal fields (discriminators) are left + * untouched. Returns a new AST array. * @param {object[]} ast The AST to transform. * @returns {object[]} A new AST array with inline enums hoisted to named defs. */ @@ -167,9 +179,12 @@ export function hoistInlineEnums(ast) { const owner = splitName(def.Name ?? '') eachPropertyDeep(def.Properties, (prop) => { const entries = typeList(prop.Type) - const allLiterals = - entries.length >= 2 && entries.every((e) => e && typeof e === 'object' && e.Type === 'literal') - if (!allLiterals) return + const literals = entries.filter(isLiteral) + const nullArms = entries.filter(isNullArm) + // Hoist a choice of >= 2 string literals, tolerating a null alternative so a nullable inline + // enum (`("a" / "b") / null`) is still named. The null stays on the field (below), never in the + // enum def; anything else in the choice (a ref, a single literal discriminator) is left untouched. + if (literals.length < 2 || literals.length + nullArms.length !== entries.length) return const base = pascal(prop.Name) || `Value${created.length}` const localName = `${owner.local}${base}` @@ -179,13 +194,13 @@ export function hoistInlineEnums(ast) { Type: 'variable', Name: synthName, IsChoiceAddition: false, - PropertyType: entries.map((e) => structuredClone(e)), + PropertyType: literals.map((e) => structuredClone(e)), Comments: prop.Comments ?? [], 'x-selenium-synthetic': true, 'x-selenium-owner': def.Name, 'x-selenium-label': base, }) - prop.Type = [groupRef(synthName)] + prop.Type = [groupRef(synthName), ...nullArms.map((e) => structuredClone(e))] }) } diff --git a/javascript/selenium-webdriver/normalize_bidi_ast_test.mjs b/javascript/selenium-webdriver/normalize_bidi_ast_test.mjs index e90329db1166b..6a6c1262c4fc5 100644 --- a/javascript/selenium-webdriver/normalize_bidi_ast_test.mjs +++ b/javascript/selenium-webdriver/normalize_bidi_ast_test.mjs @@ -56,6 +56,18 @@ describe('hoistInlineEnums', () => { ) }) + it('hoists a nullable literal choice, keeping the null on the field and out of the enum', () => { + const ast = [def('x.T', [field('scrollbarType', [lit('classic'), lit('overlay'), 'null'])])] + const out = hoistInlineEnums(ast) + + const enumName = 'x.TScrollbarType' + assert.deepEqual(byName(out, 'x.T').Properties[0].Type, [ref(enumName), 'null']) + assert.deepEqual( + byName(out, enumName).PropertyType.map((e) => e.Value), + ['classic', 'overlay'], + ) + }) + it('does NOT hoist a single-literal (discriminator) field', () => { const ast = [def('x.T', [field('type', [lit('password')])])] const out = hoistInlineEnums(ast) diff --git a/javascript/selenium-webdriver/package.json b/javascript/selenium-webdriver/package.json index b4b3b05c5ef3e..9689ea5e9facb 100644 --- a/javascript/selenium-webdriver/package.json +++ b/javascript/selenium-webdriver/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "cddl": "^0.21.0", + "cddl": "^0.21.1", "cddl2ts": "^0.10.0", "clean-jsdoc-theme": "^4.3.3", "eslint": "^10.7.0", diff --git a/javascript/selenium-webdriver/project_bidi_schema.mjs b/javascript/selenium-webdriver/project_bidi_schema.mjs index 53b99dd205776..1128108bf6412 100644 --- a/javascript/selenium-webdriver/project_bidi_schema.mjs +++ b/javascript/selenium-webdriver/project_bidi_schema.mjs @@ -195,8 +195,12 @@ function projectEntry(e) { if (e.Type === 'array') return { list: projectRef(e.Values?.[0]?.Type) } if (e.Type === 'map') return { map: projectRef(e.ValueType ?? e.Values?.[0]?.Type), extensible: true } if (e.Type === 'range') { - const intRange = Number.isInteger(e.Value?.Min?.Value) && Number.isInteger(e.Value?.Max?.Value) - return { primitive: intRange ? 'integer' : 'number' } // e.g. js-uint (0..MAX) vs scale (0.1..2) + // A bound written as a float (`1.0`) parses to an integer `Value` carrying an `IsFloat` + // marker; consult it so `(0.0..1.0)` is a number range, not — as its integral bounds alone + // would read — an integer one. A bound with no marker falls back to its value's integralness. + const intBound = (b) => b && !b.IsFloat && Number.isInteger(b.Value) + const intRange = intBound(e.Value?.Min) && intBound(e.Value?.Max) + return { primitive: intRange ? 'integer' : 'number' } // e.g. js-uint (0..MAX) vs latitude (-90.0..90.0) } return { primitive: PRIMITIVES[e.Type] ?? 'unknown' } } diff --git a/javascript/selenium-webdriver/project_bidi_schema_test.mjs b/javascript/selenium-webdriver/project_bidi_schema_test.mjs index 6420031d65fa8..ed0d0d235414c 100644 --- a/javascript/selenium-webdriver/project_bidi_schema_test.mjs +++ b/javascript/selenium-webdriver/project_bidi_schema_test.mjs @@ -182,11 +182,20 @@ describe('projectType (list / union / alias defs)', () => { Name: 'x.F', PropertyType: [{ Type: 'range', Value: { Min: { Value: 0.1 }, Max: { Value: 2 } } }], }, + { + // `(0.0..1.0)` — integral bounds, but the `IsFloat` marker makes it a number range. + Type: 'variable', + Name: 'x.W', + PropertyType: [ + { Type: 'range', Value: { Min: { Value: 0, IsFloat: true }, Max: { Value: 1, IsFloat: true } } }, + ], + }, ], {}, ) assert.deepEqual(s.types['x.U'], { kind: 'alias', type: { primitive: 'integer' } }) assert.deepEqual(s.types['x.F'], { kind: 'alias', type: { primitive: 'number' } }) + assert.deepEqual(s.types['x.W'], { kind: 'alias', type: { primitive: 'number' } }) }) it('unwraps a control-operator (.default / .ge) wrapped field type to its inner type', () => { @@ -446,15 +455,14 @@ describe('schema signals (objectOnly / preserveExtras / enum primitive)', () => assert.deepEqual(checkSchema(s), []) }) - it('types an inline (non-hoisted) literal choice with the primitive its literals share', () => { - // A nullable literal choice (`("classic" / "overlay") / null`) the normalizer leaves - // inline — carry `primitive: string` so the scalar is typed rather than opaque. + it('hoists a nullable literal choice to a named enum, referenced with the null preserved', () => { + // A nullable literal choice (`("classic" / "overlay") / null`) is hoisted (normalize_bidi_ast) + // to a named enum and referenced with the null kept on the field — a nullable enum ref, not an + // inline enum carrying a primitive. const s = projectSchema([group('x.R', [field('kind', [lit('classic'), lit('overlay'), 'null'])])], {}) - assert.deepEqual(s.types['x.R'].fields[0].type, { - enum: ['classic', 'overlay'], - primitive: 'string', - nullable: true, - }) + assert.deepEqual(s.types['x.R'].fields[0].type, { ref: 'x.RKind', nullable: true }) + assert.equal(s.types['x.RKind'].kind, 'enum') + assert.deepEqual(s.types['x.RKind'].values, ['classic', 'overlay']) assert.deepEqual(checkSchema(s), []) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b5b2701edea3..4c75f3f00a2fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,8 +138,8 @@ importers: specifier: ^10.0.1 version: 10.0.1(eslint@10.7.0(supports-color@10.2.2)) cddl: - specifier: ^0.21.0 - version: 0.21.0 + specifier: ^0.21.1 + version: 0.21.1 cddl2ts: specifier: ^0.10.0 version: 0.10.0 @@ -1435,6 +1435,10 @@ packages: resolution: {integrity: sha512-/2lnDcCA/7DRDChH2szAW4tKzZeqYFQKhw2nGqi6WqBr7N9mx2yObATLnc01pqG9pXJrw3auT1X5O58azNBTMQ==} hasBin: true + cddl@0.21.1: + resolution: {integrity: sha512-Sv4ZR4ZDODrcCaOjedZ2dxOIcVBcahQm/z/jbVonBO2hRbUvTpJiCq1syM5h4ZTWst0Ld2wOZP26voYXeZr4Dg==} + hasBin: true + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -5642,6 +5646,11 @@ snapshots: camelcase: 9.0.0 yargs: 18.0.0 + cddl@0.21.1: + dependencies: + camelcase: 9.0.0 + yargs: 18.0.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 diff --git a/rb/lib/selenium/webdriver/bidi/protocol/browsing_context.rb b/rb/lib/selenium/webdriver/bidi/protocol/browsing_context.rb index eaf27d8a96acc..859a8d6c71b12 100644 --- a/rb/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +++ b/rb/lib/selenium/webdriver/bidi/protocol/browsing_context.rb @@ -211,7 +211,7 @@ class Locator < Serialization::Union # @see https://w3c.github.io/webdriver-bidi/#cddl-type-browsingcontextimageformat ImageFormat = Serialization::Record.define( type: {wire_key: 'type', primitive: 'string'}, - quality: {wire_key: 'quality', required: false, primitive: 'integer'} + quality: {wire_key: 'quality', required: false, primitive: 'number'} ) # @api private diff --git a/rb/lib/selenium/webdriver/bidi/protocol/emulation.rb b/rb/lib/selenium/webdriver/bidi/protocol/emulation.rb index 3126a4f10b19d..b1680174313f0 100644 --- a/rb/lib/selenium/webdriver/bidi/protocol/emulation.rb +++ b/rb/lib/selenium/webdriver/bidi/protocol/emulation.rb @@ -48,6 +48,11 @@ class Emulation < Domain landscape_secondary: 'landscape-secondary' }.freeze + SET_SCROLLBAR_TYPE_OVERRIDE_PARAMETERS_SCROLLBAR_TYPE = { + classic: 'classic', + overlay: 'overlay' + }.freeze + # @api private # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-emulationsetforcedcolorsmodethemeoverrideparameters @@ -88,12 +93,12 @@ class SetGeolocationOverrideParameters < Serialization::Union # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-emulationgeolocationcoordinates GeolocationCoordinates = Serialization::Record.define( - latitude: {wire_key: 'latitude', primitive: 'integer'}, - longitude: {wire_key: 'longitude', primitive: 'integer'}, + latitude: {wire_key: 'latitude', primitive: 'number'}, + longitude: {wire_key: 'longitude', primitive: 'number'}, accuracy: {wire_key: 'accuracy', required: false, primitive: 'number'}, altitude: {wire_key: 'altitude', required: false, nullable: true, primitive: 'number'}, altitude_accuracy: {wire_key: 'altitudeAccuracy', required: false, nullable: true, primitive: 'number'}, - heading: {wire_key: 'heading', required: false, nullable: true, primitive: 'integer'}, + heading: {wire_key: 'heading', required: false, nullable: true, primitive: 'number'}, speed: {wire_key: 'speed', required: false, nullable: true, primitive: 'number'} ) @@ -185,7 +190,11 @@ class SetGeolocationOverrideParameters < Serialization::Union # @see https://www.selenium.dev/documentation/warnings/bidi-implementation/ # @see https://w3c.github.io/webdriver-bidi/#cddl-type-emulationsetscrollbartypeoverrideparameters SetScrollbarTypeOverrideParameters = Serialization::Record.define( - scrollbar_type: {wire_key: 'scrollbarType', nullable: true, primitive: 'string'}, + scrollbar_type: { + wire_key: 'scrollbarType', + nullable: true, + enum: 'Emulation::SET_SCROLLBAR_TYPE_OVERRIDE_PARAMETERS_SCROLLBAR_TYPE' + }, contexts: {wire_key: 'contexts', required: false, list: true}, user_contexts: {wire_key: 'userContexts', required: false, list: true} ) @@ -319,6 +328,11 @@ def set_scrollbar_type_override( contexts: Serialization::UNSET, user_contexts: Serialization::UNSET ) + Serialization.validate!( + 'scrollbarType', + scrollbar_type, + Emulation::SET_SCROLLBAR_TYPE_OVERRIDE_PARAMETERS_SCROLLBAR_TYPE + ) params = SetScrollbarTypeOverrideParameters.new( scrollbar_type: scrollbar_type, contexts: contexts, diff --git a/rb/lib/selenium/webdriver/bidi/protocol/input.rb b/rb/lib/selenium/webdriver/bidi/protocol/input.rb index 42db9c9c51068..4e3e0c1d69bf4 100644 --- a/rb/lib/selenium/webdriver/bidi/protocol/input.rb +++ b/rb/lib/selenium/webdriver/bidi/protocol/input.rb @@ -197,8 +197,8 @@ class WheelSourceAction < Serialization::Union button: {wire_key: 'button', primitive: 'integer'}, width: {wire_key: 'width', required: false, primitive: 'integer'}, height: {wire_key: 'height', required: false, primitive: 'integer'}, - pressure: {wire_key: 'pressure', required: false, primitive: 'integer'}, - tangential_pressure: {wire_key: 'tangentialPressure', required: false, primitive: 'integer'}, + pressure: {wire_key: 'pressure', required: false, primitive: 'number'}, + tangential_pressure: {wire_key: 'tangentialPressure', required: false, primitive: 'number'}, twist: {wire_key: 'twist', required: false, primitive: 'integer'}, altitude_angle: {wire_key: 'altitudeAngle', required: false, primitive: 'number'}, azimuth_angle: {wire_key: 'azimuthAngle', required: false, primitive: 'number'} @@ -215,8 +215,8 @@ class WheelSourceAction < Serialization::Union origin: {wire_key: 'origin', required: false, ref: 'Input::Origin'}, width: {wire_key: 'width', required: false, primitive: 'integer'}, height: {wire_key: 'height', required: false, primitive: 'integer'}, - pressure: {wire_key: 'pressure', required: false, primitive: 'integer'}, - tangential_pressure: {wire_key: 'tangentialPressure', required: false, primitive: 'integer'}, + pressure: {wire_key: 'pressure', required: false, primitive: 'number'}, + tangential_pressure: {wire_key: 'tangentialPressure', required: false, primitive: 'number'}, twist: {wire_key: 'twist', required: false, primitive: 'integer'}, altitude_angle: {wire_key: 'altitudeAngle', required: false, primitive: 'number'}, azimuth_angle: {wire_key: 'azimuthAngle', required: false, primitive: 'number'} @@ -241,8 +241,8 @@ class WheelSourceAction < Serialization::Union PointerCommonProperties = Serialization::Record.define( width: {wire_key: 'width', required: false, primitive: 'integer'}, height: {wire_key: 'height', required: false, primitive: 'integer'}, - pressure: {wire_key: 'pressure', required: false, primitive: 'integer'}, - tangential_pressure: {wire_key: 'tangentialPressure', required: false, primitive: 'integer'}, + pressure: {wire_key: 'pressure', required: false, primitive: 'number'}, + tangential_pressure: {wire_key: 'tangentialPressure', required: false, primitive: 'number'}, twist: {wire_key: 'twist', required: false, primitive: 'integer'}, altitude_angle: {wire_key: 'altitudeAngle', required: false, primitive: 'number'}, azimuth_angle: {wire_key: 'azimuthAngle', required: false, primitive: 'number'} diff --git a/rb/lib/selenium/webdriver/bidi/serialization/record.rb b/rb/lib/selenium/webdriver/bidi/serialization/record.rb index 7285c8b5c44a9..086b4bc51d15e 100644 --- a/rb/lib/selenium/webdriver/bidi/serialization/record.rb +++ b/rb/lib/selenium/webdriver/bidi/serialization/record.rb @@ -102,9 +102,10 @@ def from_json(json_payload) # Checks each field's value: a required field cannot be omitted (UNSET), a non-nullable # field cannot be nil (nil is neither a value nor the UNSET omit-sentinel, so it would be # silently dropped on the wire), a nullable-const field must carry its literal (not some - # other value), and an enum field must be in its allowed set. The enum constant is resolved - # lazily so a cross-domain enum need not be loaded first. Outbound only (from +new+); - # inbound presence/enum are checked separately in +wire_value+/+read+. + # other value), a primitive field must be the matching Ruby type, and an enum field must be + # in its allowed set. The enum constant is resolved lazily so a cross-domain enum need not be + # loaded first. Outbound only (from +new+); inbound presence/primitive/enum are checked + # separately in +wire_value+/+read+. def validate_values(attributes) fields.each do |f| value = attributes[f.name] @@ -112,12 +113,20 @@ def validate_values(attributes) raise ::ArgumentError, "#{name}##{f.name} cannot be nil" if value.nil? && !f.nullable next if value.nil? || UNSET.equal?(value) - validate_const(f, value) - check_outbound_shape(f, value) - Serialization.validate!("#{name}##{f.name}", value, Protocol.const_get(f.enum)) if f.enum + validate_present(f, value) end end + # Checks a field that carries an actual value (neither omitted nor nil): a nullable-const + # field against its literal, list/scalar shape, primitive type (lists excepted, as inbound + # does), and enum membership (resolved lazily so a cross-domain enum need not load first). + def validate_present(field, value) + validate_const(field, value) + check_outbound_shape(field, value) + check_outbound_primitive(field, value) unless field.list + Serialization.validate!("#{name}##{field.name}", value, Protocol.const_get(field.enum)) if field.enum + end + # A nullable constant (`literal / null`) is caller-settable but its only non-null value is # the literal, so a value that is neither the literal nor nil (nil is handled above) is a # local error rather than a wire round-trip. A non-const field carries UNSET here and passes. @@ -137,6 +146,17 @@ def check_outbound_shape(field, value) raise ::ArgumentError, "#{name}##{field.name} expected #{kind}, got #{value.inspect}" end + # Outbound mirror of check_primitive: a primitive-typed arg (`string`/`integer`/…) must be + # the matching Ruby type, so a caller mistake (a string width, a float count) is a local + # ArgumentError here rather than a rejection the browser reports a round-trip later. A field + # with no primitive descriptor (enum, ref, opaque) passes; lists are skipped, as inbound does. + def check_outbound_primitive(field, value) + expected = PRIMITIVE_TYPES[field.primitive] + return if expected.nil? || expected.any? { |type| value.is_a?(type) } + + raise ::ArgumentError, "#{name}##{field.name} expected #{field.primitive}, got #{value.inspect}" + end + def fixed?(field) !UNSET.equal?(field.fixed) end diff --git a/rb/sig/lib/selenium/webdriver/bidi/protocol/browsing_context.rbs b/rb/sig/lib/selenium/webdriver/bidi/protocol/browsing_context.rbs index 64ea159abaf1c..13213ce399014 100644 --- a/rb/sig/lib/selenium/webdriver/bidi/protocol/browsing_context.rbs +++ b/rb/sig/lib/selenium/webdriver/bidi/protocol/browsing_context.rbs @@ -129,7 +129,7 @@ module Selenium class ImageFormat < ::Selenium::WebDriver::BiDi::Serialization::Record attr_reader type: String attr_reader quality: untyped - def self.new: (type: String, ?quality: Integer) -> instance + def self.new: (type: String, ?quality: Numeric) -> instance end class ClipRectangle < ::Selenium::WebDriver::BiDi::Serialization::Union diff --git a/rb/sig/lib/selenium/webdriver/bidi/protocol/emulation.rbs b/rb/sig/lib/selenium/webdriver/bidi/protocol/emulation.rbs index 56a5bff727130..d6b01bb0bc8b1 100644 --- a/rb/sig/lib/selenium/webdriver/bidi/protocol/emulation.rbs +++ b/rb/sig/lib/selenium/webdriver/bidi/protocol/emulation.rbs @@ -29,6 +29,8 @@ module Selenium SCREEN_ORIENTATION_TYPE: Hash[Symbol, String] + SET_SCROLLBAR_TYPE_OVERRIDE_PARAMETERS_SCROLLBAR_TYPE: Hash[Symbol, String] + class SetForcedColorsModeThemeOverrideParameters < ::Selenium::WebDriver::BiDi::Serialization::Record attr_reader theme: Symbol? attr_reader contexts: untyped @@ -52,14 +54,14 @@ module Selenium end class GeolocationCoordinates < ::Selenium::WebDriver::BiDi::Serialization::Record - attr_reader latitude: Integer - attr_reader longitude: Integer + attr_reader latitude: Numeric + attr_reader longitude: Numeric attr_reader accuracy: untyped attr_reader altitude: untyped attr_reader altitude_accuracy: untyped attr_reader heading: untyped attr_reader speed: untyped - def self.new: (latitude: Integer, longitude: Integer, ?accuracy: Numeric, ?altitude: Numeric?, ?altitude_accuracy: Numeric?, ?heading: Integer?, ?speed: Numeric?) -> instance + def self.new: (latitude: Numeric, longitude: Numeric, ?accuracy: Numeric, ?altitude: Numeric?, ?altitude_accuracy: Numeric?, ?heading: Numeric?, ?speed: Numeric?) -> instance end class GeolocationPositionError < ::Selenium::WebDriver::BiDi::Serialization::Record @@ -127,10 +129,10 @@ module Selenium end class SetScrollbarTypeOverrideParameters < ::Selenium::WebDriver::BiDi::Serialization::Record - attr_reader scrollbar_type: String? + attr_reader scrollbar_type: Symbol? attr_reader contexts: untyped attr_reader user_contexts: untyped - def self.new: (scrollbar_type: String?, ?contexts: Array[String], ?user_contexts: Array[String]) -> instance + def self.new: (scrollbar_type: Symbol?, ?contexts: Array[String], ?user_contexts: Array[String]) -> instance end class SetTimezoneOverrideParameters < ::Selenium::WebDriver::BiDi::Serialization::Record @@ -154,7 +156,7 @@ module Selenium def set_screen_orientation_override: (screen_orientation: ::Selenium::WebDriver::BiDi::Protocol::Emulation::ScreenOrientation?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped def set_screen_settings_override: (screen_area: ::Selenium::WebDriver::BiDi::Protocol::Emulation::ScreenArea?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped def set_scripting_enabled: (enabled: bool?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped - def set_scrollbar_type_override: (scrollbar_type: String?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped + def set_scrollbar_type_override: (scrollbar_type: Symbol?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped def set_timezone_override: (timezone: String?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped def set_touch_override: (max_touch_points: Integer?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped def set_user_agent_override: (user_agent: String?, ?contexts: Array[String], ?user_contexts: Array[String]) -> untyped diff --git a/rb/sig/lib/selenium/webdriver/bidi/protocol/input.rbs b/rb/sig/lib/selenium/webdriver/bidi/protocol/input.rbs index 720e921bef469..57e242b4ebbe8 100644 --- a/rb/sig/lib/selenium/webdriver/bidi/protocol/input.rbs +++ b/rb/sig/lib/selenium/webdriver/bidi/protocol/input.rbs @@ -119,7 +119,7 @@ module Selenium attr_reader twist: untyped attr_reader altitude_angle: untyped attr_reader azimuth_angle: untyped - def self.new: (?type: String, button: Integer, ?width: Integer, ?height: Integer, ?pressure: Integer, ?tangential_pressure: Integer, ?twist: Integer, ?altitude_angle: Numeric, ?azimuth_angle: Numeric) -> instance + def self.new: (?type: String, button: Integer, ?width: Integer, ?height: Integer, ?pressure: Numeric, ?tangential_pressure: Numeric, ?twist: Integer, ?altitude_angle: Numeric, ?azimuth_angle: Numeric) -> instance end class PointerMoveAction < ::Selenium::WebDriver::BiDi::Serialization::Record @@ -135,7 +135,7 @@ module Selenium attr_reader twist: untyped attr_reader altitude_angle: untyped attr_reader azimuth_angle: untyped - def self.new: (?type: String, x: Numeric, y: Numeric, ?duration: Integer, ?origin: ::Selenium::WebDriver::BiDi::Protocol::Input::Origin, ?width: Integer, ?height: Integer, ?pressure: Integer, ?tangential_pressure: Integer, ?twist: Integer, ?altitude_angle: Numeric, ?azimuth_angle: Numeric) -> instance + def self.new: (?type: String, x: Numeric, y: Numeric, ?duration: Integer, ?origin: ::Selenium::WebDriver::BiDi::Protocol::Input::Origin, ?width: Integer, ?height: Integer, ?pressure: Numeric, ?tangential_pressure: Numeric, ?twist: Integer, ?altitude_angle: Numeric, ?azimuth_angle: Numeric) -> instance end class WheelScrollAction < ::Selenium::WebDriver::BiDi::Serialization::Record @@ -157,7 +157,7 @@ module Selenium attr_reader twist: untyped attr_reader altitude_angle: untyped attr_reader azimuth_angle: untyped - def self.new: (?width: Integer, ?height: Integer, ?pressure: Integer, ?tangential_pressure: Integer, ?twist: Integer, ?altitude_angle: Numeric, ?azimuth_angle: Numeric) -> instance + def self.new: (?width: Integer, ?height: Integer, ?pressure: Numeric, ?tangential_pressure: Numeric, ?twist: Integer, ?altitude_angle: Numeric, ?azimuth_angle: Numeric) -> instance end class Origin < ::Selenium::WebDriver::BiDi::Serialization::Union diff --git a/rb/sig/lib/selenium/webdriver/bidi/serialization.rbs b/rb/sig/lib/selenium/webdriver/bidi/serialization.rbs index 40837e6740134..b21395d5ee219 100644 --- a/rb/sig/lib/selenium/webdriver/bidi/serialization.rbs +++ b/rb/sig/lib/selenium/webdriver/bidi/serialization.rbs @@ -62,10 +62,14 @@ module Selenium def validate_values: (Hash[Symbol, untyped] attributes) -> void + def validate_present: (untyped field, untyped value) -> void + def validate_const: (untyped field, untyped value) -> void def check_outbound_shape: (untyped field, untyped value) -> void + def check_outbound_primitive: (untyped field, untyped value) -> void + def fixed?: (untyped field) -> bool def wire_value: (untyped field, Hash[untyped, untyped] json_payload) -> untyped diff --git a/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb b/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb index 2b0525b33ec7e..cd3c5e60e1451 100644 --- a/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb +++ b/rb/spec/unit/selenium/webdriver/bidi/serialization_spec.rb @@ -378,6 +378,25 @@ def moz_install(**kwargs) end end + describe 'outbound primitive validation' do + it 'rejects a wrong-typed primitive at construction, so an invalid object cannot exist' do + expect { BrowsingContext::NavigateParameters.new(context: 'c', url: 123) } + .to raise_error(ArgumentError, /NavigateParameters#url expected string/) + end + + it 'rejects a float for an integer field, mirroring the wire integer/number split' do + expect { Emulation::ScreenArea.new(width: 5.0, height: 5) } + .to raise_error(ArgumentError, /ScreenArea#width expected integer/) + end + + it 'accepts either an integer or a float for a number field' do + klass = Emulation::GeolocationCoordinates + + expect(klass.new(latitude: 0, longitude: 0, accuracy: 1.5).accuracy).to eq(1.5) + expect(klass.new(latitude: 0, longitude: 0, accuracy: 2).accuracy).to eq(2) + end + end + describe 'enum symbol coercion' do it 'takes an idiomatic symbol and serializes the wire token (kebab included)' do params = Bluetooth::SimulateAdapterParameters.new(context: 'c', state: :powered_off) @@ -404,6 +423,19 @@ def moz_install(**kwargs) expect(Network::AddInterceptParameters.from_json(params.as_json).phases) .to eq(%i[before_request_sent auth_required]) end + + # A nullable inline literal choice (scrollbarType = "classic" / "overlay" / null) is + # hoisted to a named enum, so it validates as a closed vocabulary in both directions + # (and still admits null) rather than passing any string through as it did when opaque. + it 'validates a hoisted nullable inline enum, still admitting null' do + klass = Emulation::SetScrollbarTypeOverrideParameters + + expect(klass.new(scrollbar_type: :overlay).as_json).to eq('scrollbarType' => 'overlay') + expect(klass.new(scrollbar_type: nil).as_json).to eq('scrollbarType' => nil) + expect { klass.new(scrollbar_type: :banana) }.to raise_error(ArgumentError, /must be one of/) + expect { klass.from_json('scrollbarType' => 'banana') } + .to raise_error(Error::WebDriverError, /received an unknown value/) + end end describe 'inbound shape validation' do @@ -457,13 +489,6 @@ def moz_install(**kwargs) expect(parsed.key).to eq(5) end - # Signal 3: an inline literal choice the projector now types as `string` - # (scrollbarType = "classic" / "overlay" / null), previously opaque. - it 'raises when an inline-enum scalar field arrives as the wrong primitive' do - expect { Emulation::SetScrollbarTypeOverrideParameters.from_json('scrollbarType' => 123) } - .to raise_error(Error::WebDriverError, /scrollbar_type expected string/) - end - # Signal 3: a scalar hidden behind an alias (size -> js-uint -> integer) now carries # its leaf primitive, so a wrong-typed value is rejected instead of passing opaque. it 'raises when an alias-typed integer field (js-uint) arrives as a string' do