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
5 changes: 5 additions & 0 deletions javascript/selenium-webdriver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ js_library(
"common/*.js",
"bidi/*.js",
"bidi/external/*.js",
"bidi/serialization/*.js",
]),
deps = [
":node_modules/@bazel/runfiles",
Expand Down Expand Up @@ -197,7 +198,11 @@ pkg_tar(
)

SMALL_TESTS = [
"test/bidi/domain_test.js",
"test/bidi/index_test.js",
"test/bidi/serialization/record_test.js",
"test/bidi/serialization/union_test.js",
"test/bidi/serialization/wire_contract_test.js",
"test/io/io_test.js",
"test/io/zip_test.js",
"test/lib/bidi_connection_test.js",
Expand Down
55 changes: 55 additions & 0 deletions javascript/selenium-webdriver/bidi/domain.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

export interface EventDescriptor<T> {
readonly method: string
readonly type?: { fromWire(payload: unknown): T }
}

/**
* Describes one subscribable BiDi event, for use with Domain#addCallback().
* @param {string} method The event's wire method name, e.g. 'log.entryAdded'.
* @param {{fromWire(payload: unknown): T}} [type] Runtime record/union class for the
* event's params, if the schema declares one — addCallback() parses each delivered
* payload through it before the caller's handler runs.
* @returns {EventDescriptor<T>} The event descriptor, ready to pass to Domain#addCallback().
*/
export function event<T>(method: string, type?: { fromWire(payload: unknown): T }): EventDescriptor<T>

/** Internal construction guard — only a generated `Class.create(driver)` passes this. Never use directly. */
export const DOMAIN_TOKEN: unique symbol

export declare class Domain {
protected constructor(bidi: unknown, token: typeof DOMAIN_TOKEN)
protected static connect(driver: unknown): Promise<unknown>
protected send(method: string, params: Record<string, unknown>): Promise<unknown>

/**
* Subscribes `handler` to a BiDi event — asks the remote end to start sending it,
* then attaches `handler` as a local listener for it. Placeholder for now: always
* subscribes/unsubscribes remotely on every call, with no ref-counting across
* callers — connection-wide listener bookkeeping is follow-up work.
* @param {EventDescriptor<T>} descriptor An event descriptor from event().
* @param {function(T): void} handler Invoked with the event's parsed params each time it fires.
* @returns {Promise<{unsubscribe: function(): Promise<void>}>} A handle for this
* subscription; call `unsubscribe()` to stop receiving the event.
*/
addCallback<T>(
descriptor: EventDescriptor<T>,
handler: (params: T) => void,
): Promise<{ unsubscribe(): Promise<void> }>
}
96 changes: 96 additions & 0 deletions javascript/selenium-webdriver/bidi/domain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

const { getBidiConnection } = require('../lib/bidi_connection')

// Gates Domain's constructor so `new Network(someRandomThing)` fails loudly
// instead of silently producing a broken instance. A Symbol can't be forged
// or guessed, so this is real runtime enforcement, not just a TS annotation —
// only a generated `Class.create(driver)` (and this package's own tests) may
// pass it. It's exported deliberately, not hidden: the point is to stop
// accidental misuse of the normal `new Network(x)` shape, not to defend
// against someone who deliberately imports and passes this.
const DOMAIN_TOKEN = Symbol('Domain internal construction token — obtained only via Class.create(driver)')

/**
* Describes one subscribable BiDi event, for use with Domain#addCallback().
* @param {string} method
* @param {{fromWire(payload: unknown): unknown}} [type] Runtime record/union
* class for the event's params, if the schema declares one. When present,
* addCallback() parses each delivered payload through it before the
* caller's handler runs — inbound wire payloads are validated against
* their resolved type; an event's params is such a payload just as much
* as a command's result is.
* @returns {{method: string, type: ({fromWire(payload: unknown): unknown}|undefined)}}
* The event descriptor, ready to pass to Domain#addCallback().
*/
function event(method, type) {
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
return { method, type }
}

/** Shared base for every generated BiDi domain class. See domain.d.ts for the typed surface. */
class Domain {
#bidi

constructor(bidi, token) {
if (token !== DOMAIN_TOKEN) {
throw new TypeError(`${new.target.name} must be constructed via ${new.target.name}.create(driver), not \`new\``)
}
this.#bidi = bidi
}

static async connect(driver) {
return getBidiConnection(driver)
}

async send(method, params) {
const response = await this.#bidi.send({ method, params })
if (response?.error !== undefined) {
throw new Error(`${response.error}: ${response.message}`)
}
return response?.result
}

/**
* Subscribes `handler` to a BiDi event: tells the remote end to start sending it
* (the underlying connection's `subscribe()`), then attaches `handler` as a local
* listener for it. Placeholder for now — always subscribes/unsubscribes remotely on
* every call, with no ref-counting across callers or connection-wide listener
* bookkeeping (that belongs at the connection level, shared across every Domain
* instance, mirroring Ruby's WebSocketConnection#add_callback/remove_callback or
* Java's equivalent — tracked as follow-up work, not folded into this PR).
* @param {{method: string, type: ({fromWire(payload: unknown): unknown}|undefined)}} descriptor
* An event descriptor from event().
* @param {function(unknown): void} handler Invoked with the event's params
* (parsed through descriptor.type first, if one was given) each time it fires.
* @returns {Promise<{unsubscribe: function(): Promise<void>}>}
* A handle for this subscription — call `unsubscribe()` to stop receiving the event.
*/
async addCallback(descriptor, handler) {
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
const dispatch = descriptor.type === undefined ? handler : (params) => handler(descriptor.type.fromWire(params))
await this.#bidi.subscribe(descriptor.method)
this.#bidi.on(descriptor.method, dispatch)
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
return {
unsubscribe: async () => {
this.#bidi.off(descriptor.method, dispatch)
await this.#bidi.unsubscribe(descriptor.method)
},
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}
}
}

module.exports = { Domain, event, DOMAIN_TOKEN }
30 changes: 30 additions & 0 deletions javascript/selenium-webdriver/bidi/serialization/enum.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

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

/**
* Registers a schema `enum` — a closed set of string values a field may hold.
* @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>
35 changes: 35 additions & 0 deletions javascript/selenium-webdriver/bidi/serialization/enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

const { register } = require('./registry')

/**
* Registers a schema `enum` — a closed set of string values a field may hold.
* @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}}
* 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.
*/
function defineEnum(name, values) {
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
const allowed = new Set(values)
const entry = { kind: 'enum', values, includes: (value) => allowed.has(value) }
register(name, entry)
return entry
}

module.exports = { defineEnum }
70 changes: 70 additions & 0 deletions javascript/selenium-webdriver/bidi/serialization/record.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

// Mirrors bidi_schema.json's type-ref vocabulary (see project_bidi_schema.mjs).
export interface TypeNode {
primitive?: string
const?: unknown
ref?: string
enum?: string[]
list?: TypeNode
map?: TypeNode
union?: TypeNode[]
// An inline (unnamed) record — project_bidi_schema.mjs's projectEntry() emits this
// for an anonymous CDDL group instead of hoisting it to a named, ref'able type.
record?: FieldSpec[]
nullable?: boolean
// Present on an inline union with a bare-scalar arm — the primitive(s) that
// arm accepts (see unionNode() in project_bidi_schema.mjs). Not consumed by
// validateValue yet; declared so embedding a real schema node type-checks.
scalar?: string | string[]
// The exact `const` literals a bare-scalar union arm admits (e.g.
// input.Origin's "viewport"/"pointer") — see unionNode(). Same status as
// `scalar`: not yet consumed by validateValue, declared for the embed.
scalarValues?: unknown[]
}

export interface FieldSpec {
name: string
wire: string
required: boolean
type: TypeNode
}

export interface RecordOptions {
extensible?: boolean
}

export declare class ValidationError extends Error {}

export interface RecordClass<T> {
new (data: T): Readonly<T>
fromWire(payload: unknown): Readonly<T>
}

/**
* Registers a schema `record` — a fixed set of named fields, each independently
* validated on the way out (constructor) and in (fromWire()).
* @param name Schema type name, e.g. 'network.AddInterceptParameters'.
* @param fields The record's field specs.
* @param options
* @returns The generated Record class — `new Record(data)` validates and constructs
* outbound, `Record.fromWire(payload)` validates and parses inbound.
*/
export function defineRecord<T>(name: string, fields: FieldSpec[], options?: RecordOptions): RecordClass<T>

export function defineAlias(name: string, type: TypeNode): void
Loading