Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion javascript/selenium-webdriver/bidi/serialization/record.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ const { register, resolve } = require('./registry')

class ValidationError extends Error {}

function checkBounds(typeNode, value, path) {
if (typeof value !== 'number') return
if (typeNode.min !== undefined) {
const ok = typeNode.exclusiveMin ? value > typeNode.min : value >= typeNode.min
if (!ok) {
const op = typeNode.exclusiveMin ? '>' : '>='
throw new ValidationError(`${path}: expected ${op} ${typeNode.min}, got ${value}`)
}
}
if (typeNode.max !== undefined) {
const ok = typeNode.exclusiveMax ? value < typeNode.max : value <= typeNode.max
if (!ok) {
const op = typeNode.exclusiveMax ? '<' : '<='
throw new ValidationError(`${path}: expected ${op} ${typeNode.max}, got ${value}`)
}
}
}

// Validates a *present* value against a resolved type node, and returns the value to
// assign for it — usually the same value, but for a nested ref-to-record/union (the
// freshly constructed/parsed instance — fromWire()/build() inbound, `new RecordClass(value)`/
Expand Down Expand Up @@ -60,6 +78,7 @@ function validateValue(typeNode, value, path, direction) {
if (typeNode.primitive === 'integer' && !Number.isInteger(value)) {
throw new ValidationError(`${path}: expected an integer, got ${value}`)
}
checkBounds(typeNode, value, path)
// An inline literal choice (project_bidi_schema.mjs's enumNode()) carries both
// `primitive` and `enum` — the primitive check above narrows the type, but the
// closed vocabulary below still needs to run, so only return early when there
Expand Down Expand Up @@ -146,7 +165,9 @@ function validateValue(typeNode, value, path, direction) {
}

if (referenced.kind === 'alias') {
return validateValue(referenced.type, value, path, direction)
const result = validateValue(referenced.type, value, path, direction)
checkBounds(typeNode, result, path)
return result
}

return value
Expand Down
114 changes: 114 additions & 0 deletions javascript/selenium-webdriver/test/bidi/serialization/record_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,4 +493,118 @@ describe('serialization/record', function () {
assert.strictEqual(parsed.choice, false)
})
})

describe('numeric range and bounds validation', function () {
defineAlias('test.record.JsUint', {
primitive: 'integer',
min: 0,
max: 9007199254740991,
})

const LocateNodesParameters = defineRecord('test.record.LocateNodesParameters', [
{ name: 'maxNodeCount', wire: 'maxNodeCount', required: false, type: { ref: 'test.record.JsUint', min: 1 } },
])

const ResponseData = defineRecord('test.record.ResponseData', [
{ name: 'status', wire: 'status', required: true, type: { ref: 'test.record.JsUint' } },
])

const ViewportParameters = defineRecord('test.record.ViewportParameters', [
{
name: 'devicePixelRatio',
wire: 'devicePixelRatio',
required: false,
type: { primitive: 'number', min: 0, exclusiveMin: true, nullable: true },
},
])

const GeolocationCoordinates = defineRecord('test.record.GeolocationCoordinates', [
{
name: 'heading',
wire: 'heading',
required: false,
type: { primitive: 'number', min: 0, max: 360, exclusiveMax: true, nullable: true },
},
])

const PrintParameters = defineRecord('test.record.PrintParameters', [
{
name: 'pageRanges',
wire: 'pageRanges',
required: false,
type: { list: { union: [{ ref: 'test.record.JsUint' }, { primitive: 'string' }] } },
},
])

it('holds an alias-typed field to its declared range outbound', function () {
const resp = new ResponseData({ status: 200 })
assert.strictEqual(resp.status, 200)

assert.throws(() => new ResponseData({ status: -1 }), /expected >= 0, got -1/)
assert.throws(() => new ResponseData({ status: 9007199254740992 }), /expected <= 9007199254740991/)
})

it('rejects a value outside the range of the alias it names on receipt', function () {
const parsed = ResponseData.fromWire({ status: 200 })
assert.strictEqual(parsed.status, 200)

assert.throws(() => ResponseData.fromWire({ status: -1 }), /expected >= 0, got -1/)
assert.throws(() => ResponseData.fromWire({ status: 9007199254740992 }), /expected <= 9007199254740991/)
})

it('holds a field narrowing an alias to its tightened range (e.g. maxNodeCount 0 vs 1)', function () {
const valid = new LocateNodesParameters({ maxNodeCount: 1 })
assert.strictEqual(valid.maxNodeCount, 1)

const parsed = LocateNodesParameters.fromWire({ maxNodeCount: 1 })
assert.strictEqual(parsed.maxNodeCount, 1)

assert.throws(() => new LocateNodesParameters({ maxNodeCount: 0 }), /expected >= 1, got 0/)
assert.throws(() => LocateNodesParameters.fromWire({ maxNodeCount: 0 }), /expected >= 1, got 0/)

assert.throws(() => new LocateNodesParameters({ maxNodeCount: -1 }), /expected >= 0, got -1/)
assert.throws(() => LocateNodesParameters.fromWire({ maxNodeCount: -1 }), /expected >= 0, got -1/)
})

it('enforces exclusive min bound on floating point numbers (e.g. devicePixelRatio > 0)', function () {
const valid = new ViewportParameters({ devicePixelRatio: 0.5 })
assert.strictEqual(valid.devicePixelRatio, 0.5)

const parsed = ViewportParameters.fromWire({ devicePixelRatio: 1 })
assert.strictEqual(parsed.devicePixelRatio, 1)

const nullVal = new ViewportParameters({ devicePixelRatio: null })
assert.strictEqual(nullVal.devicePixelRatio, null)

assert.throws(() => new ViewportParameters({ devicePixelRatio: 0 }), /expected > 0, got 0/)
assert.throws(() => ViewportParameters.fromWire({ devicePixelRatio: 0 }), /expected > 0, got 0/)
assert.throws(() => new ViewportParameters({ devicePixelRatio: -0.1 }), /expected > 0, got -0.1/)
})

it('enforces exclusive max bound on floating point numbers (e.g. heading < 360)', function () {
const valid = new GeolocationCoordinates({ heading: 359.9 })
assert.strictEqual(valid.heading, 359.9)

const zero = new GeolocationCoordinates({ heading: 0 })
assert.strictEqual(zero.heading, 0)

const parsed = GeolocationCoordinates.fromWire({ heading: 180 })
assert.strictEqual(parsed.heading, 180)

assert.throws(() => new GeolocationCoordinates({ heading: 360 }), /expected < 360, got 360/)
assert.throws(() => GeolocationCoordinates.fromWire({ heading: 360 }), /expected < 360, got 360/)
assert.throws(() => new GeolocationCoordinates({ heading: -1 }), /expected >= 0, got -1/)
})

it('holds union list elements to their declared numeric ranges (e.g. pageRanges)', function () {
const valid = new PrintParameters({ pageRanges: ['1-3', 2] })
assert.deepStrictEqual(valid.pageRanges, ['1-3', 2])

const parsed = PrintParameters.fromWire({ pageRanges: ['1-3', 2] })
assert.deepStrictEqual(parsed.pageRanges, ['1-3', 2])

assert.throws(() => new PrintParameters({ pageRanges: ['1-3', -1] }), ValidationError)
assert.throws(() => PrintParameters.fromWire({ pageRanges: ['1-3', -1] }), ValidationError)
})
})
})
Loading