Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ca5110c
[js] Add serialization and domain layer
pujagani Aug 18, 2026
e8868ce
[js] Addressed comments
pujagani Aug 24, 2026
9b8fb89
Address comments
pujagani Aug 24, 2026
8bec29d
Address comments
pujagani Aug 24, 2026
4670a18
Decouple event listening logic and add placeholder
pujagani Aug 24, 2026
90c486e
[js][bidi] Add BiDi connection-level event subscription
pujagani Aug 25, 2026
4210ede
[js][bidi] Add BiDi connection-level event subscription
pujagani Aug 25, 2026
681e5e1
Merge branch 'bidi-domain-js' of https://github.com/pujagani/selenium…
pujagani Aug 25, 2026
8a7b11a
[js] Address comments
pujagani Aug 26, 2026
ff45d8a
[js] Address comments
pujagani Aug 27, 2026
4d7e6dd
Merge branch 'trunk' into bidi-domain-js
pujagani Aug 27, 2026
99ec74d
[js] Update the BiDi generator to match the low-level contract
pujagani Sep 2, 2026
713adfc
[js] Update the BiDi generator to match the low-level contract
pujagani Sep 2, 2026
0c050bf
Merge branch 'bidi-domain-js' of https://github.com/pujagani/selenium…
pujagani Sep 2, 2026
50586c4
Fix build
pujagani Sep 2, 2026
4a45dbd
Address comments
pujagani Sep 2, 2026
2361c90
[js] Address comments
pujagani Sep 3, 2026
d927b68
Merge branch 'trunk' into bidi-domain-js
pujagani Sep 8, 2026
7c5daf8
Merge branch 'trunk' into bidi-domain-js
pujagani Sep 23, 2026
83659e5
[js] Fix BiDi generator and BUILD after trunk merge
pujagani Sep 23, 2026
5a5e074
Merge branch 'trunk' into bidi-domain-js
pujagani Sep 24, 2026
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
3 changes: 2 additions & 1 deletion javascript/selenium-webdriver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ js_binary(
name = "generate_bidi_script",
data = [
"generate_bidi.mjs",
"normalize_bidi_ast.mjs",
"project_bidi_schema.mjs",
":generated_note_local",
":license_header_local",
":node_modules/cddl",
":node_modules/cddl2ts",
],
entry_point = "generate_bidi.mjs",
)
Expand Down
9 changes: 6 additions & 3 deletions javascript/selenium-webdriver/bidi/serialization/enum.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@
// specific language governing permissions and limitations
// under the License.

export interface EnumEntry<T extends string> {
export interface EnumEntry<T extends string | number | boolean> {
readonly values: readonly T[]
includes(value: unknown): value is T
}

/**
* Registers a schema `enum` — a closed set of string values a field may hold.
* Registers a schema `enum` — a closed set of string, number, or boolean values a
* field may hold (e.g. emulation.MediaFeaturesGrid, CSS's `grid` media feature, is
* spec'd as the integer 0 or 1, not a string; the schema projector's literalPrimitive()
* recognizes a boolean-literal choice the same way).
* @param name Schema type name, e.g. 'network.InterceptPhase'.
* @param values The enum's valid values.
* @returns The registered entry, used by validateValue() to check a ref'd value's
* membership; also returned so a generator can build a discoverable constant from it.
*/
export function defineEnum<T extends string>(name: string, values: readonly T[]): EnumEntry<T>
export function defineEnum<T extends string | number | boolean>(name: string, values: readonly T[]): EnumEntry<T>
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
9 changes: 6 additions & 3 deletions javascript/selenium-webdriver/bidi/serialization/enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
const { register } = require('./registry')

/**
* Registers a schema `enum` — a closed set of string values a field may hold.
* Registers a schema `enum` — a closed set of string, number, or boolean values a
* field may hold (e.g. emulation.MediaFeaturesGrid, CSS's `grid` media feature, is
* spec'd as the integer 0 or 1, not a string; the schema projector's literalPrimitive()
* recognizes a boolean-literal choice the same way).
* @param {string} name Schema type name, e.g. 'network.InterceptPhase'.
* @param {string[]} values The enum's valid values.
* @returns {{kind: 'enum', values: string[], includes: function(unknown): boolean}}
* @param {Array<string|number|boolean>} values The enum's valid values.
* @returns {{kind: 'enum', values: Array<string|number|boolean>, includes: function(unknown): boolean}}
* The registered entry, used by validateValue() to check a ref'd value's
* membership; also returned so a generator can build a discoverable constant from it.
*/
Expand Down
6 changes: 5 additions & 1 deletion javascript/selenium-webdriver/bidi/serialization/record.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ export interface TypeNode {
primitive?: string
const?: unknown
ref?: string
enum?: string[]
// Almost always string (e.g. network.SameSite), but the schema can also derive a
// numeric or boolean enum where the spec itself uses one — emulation.MediaFeaturesGrid
// (CSS's `grid` media feature) is spec'd as the integer 0 or 1, not a string, and
// literalPrimitive() recognizes a boolean-literal choice the same way.
enum?: (string | number | boolean)[]
list?: TypeNode
map?: TypeNode
union?: TypeNode[]
Expand Down
63 changes: 34 additions & 29 deletions javascript/selenium-webdriver/bidi/serialization/record.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ const { register, resolve } = require('./registry')
class ValidationError extends Error {}

// 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 parsed
// inbound (fromWire()/build() results, not the raw wire object) and for list/map/inline
// record values (a fresh, deeply-frozen copy, so validity can't be corrupted after the
// fact by mutating an array or object a caller still holds a reference to).
// Outbound nested refs deliberately keep the caller's own value instead of the
// newly-constructed instance — the nested `new referenced.RecordClass(value)` /
// `referenced.build(value)` call below exists only to validate, matching the
// constructor's existing outbound behavior of trusting the caller's own object shape.
// 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)`/
// build() outbound, not the caller's raw value) and for list/map/inline record values (a
// fresh, deeply-frozen copy, so validity can't be corrupted after the fact by mutating an
// array or object a caller still holds a reference to). Outbound needs the constructed
// instance, symmetric with inbound, so a typed parent's toJSON() cascades into a nested
// record's own toJSON() — converting *its* fields to their declared wire names too, not
// just the parent's own fields — when the whole instance is serialized for the wire.
// `direction` only affects how a nested ref-to-record/union is itself validated/parsed.
function validateValue(typeNode, value, path, direction) {
if (value === null) {
Expand Down Expand Up @@ -125,23 +125,24 @@ function validateValue(typeNode, value, path, direction) {
throw new ValidationError(`${path}: expected an object, got ${typeof value}`)
}
// Recurse through the same-direction path so a nested field gets the same
// tolerance (inbound) or strictness (outbound) as its parent. Inbound returns
// the parsed instance itself, so a typed parent record ends up with a typed
// nested value instead of the raw wire object; outbound only validates this
// way (the caller's own value is what gets kept, see the note above).
// tolerance (inbound) or strictness (outbound) as its parent, and — both
// directions alike — ends up holding the typed instance, not the raw value:
// inbound so a typed parent record has a typed nested value instead of the
// raw wire object; outbound so the parent's own toJSON() cascades into this
// nested instance's toJSON() too, converting its fields to their wire names.
if (direction === 'inbound') {
return referenced.RecordClass.fromWire(value)
}
new referenced.RecordClass(value)
return value
return new referenced.RecordClass(value)
}

if (referenced.kind === 'union') {
// build()/fromWire() already return the resolved variant's own instance —
// same reasoning as the record branch above applies to it symmetrically.
if (direction === 'inbound') {
return referenced.fromWire(value)
}
referenced.build(value)
return value
return referenced.build(value)
}

if (referenced.kind === 'alias') {
Expand Down Expand Up @@ -220,33 +221,37 @@ function defineRecord(name, fields, options = {}) {

class Record {
// Outbound: strict. Any value that doesn't match its declared shape is an error here.
// Reads `data` by JS-facing field name (field.name), not the wire key (field.wire) —
// a caller builds params using the same property names the generated TS interface
// declares (e.g. `prefersColorScheme`), never the raw spec key (`prefers-color-scheme`);
// toJSON() below is what maps back to field.wire for the actual wire send.
constructor(data) {
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
throw new ValidationError(`${name}: expected an object`)
}

for (const field of fields) {
if (!Object.hasOwn(data, field.wire)) {
if (!Object.hasOwn(data, field.name)) {
if (field.required) {
throw new ValidationError(`${name}.${field.wire}: required field is missing`)
throw new ValidationError(`${name}.${field.name}: required field is missing`)
}
continue
}
const value = data[field.wire]
this[field.name] = validateValue(field.type, value, `${name}.${field.wire}`, 'outbound')
const value = data[field.name]
this[field.name] = validateValue(field.type, value, `${name}.${field.name}`, 'outbound')
}

for (const wireKey of Object.keys(data)) {
if (byWire.has(wireKey)) continue
for (const key of Object.keys(data)) {
if (byName.has(key)) continue
if (!extensible) {
throw new ValidationError(`${name}: unknown property "${wireKey}"`)
throw new ValidationError(`${name}: unknown property "${key}"`)
}
// Object.defineProperty, not `this[wireKey] = ...`: wireKey is caller-supplied
// and a literal "__proto__" key assigned via bracket notation hijacks this
// instance's actual prototype instead of becoming a field (CWE-1321).
// defineProperty always creates a genuine own data property, regardless of name.
Object.defineProperty(this, wireKey, {
value: data[wireKey], // vendor extras reach the wire on an extensible type
// Object.defineProperty, not `this[key] = ...`: key is caller-supplied and a
// literal "__proto__" key assigned via bracket notation hijacks this instance's
// actual prototype instead of becoming a field (CWE-1321). defineProperty always
// creates a genuine own data property, regardless of name.
Object.defineProperty(this, key, {
value: data[key], // vendor extras have no separate wire name — this key is both
enumerable: true,
writable: true,
configurable: true,
Expand Down
Loading
Loading