Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
50 changes: 50 additions & 0 deletions javascript/selenium-webdriver/bidi/domain.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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(). */
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 (only if nothing else on this connection already has), then
* attaches `handler` as a local listener for it. Remote subscription is
* ref-counted against the connection's own listener count, so it's shared
* correctly across every Domain instance on that connection, not just this one.
* @param descriptor An event descriptor from event().
* @param handler Invoked with the event's parsed params each time it fires.
* @returns A handle for this subscription; call `unsubscribe()` to stop
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
* receiving the event (and, if it was the last listener for it on this
* connection, to tell the remote end to stop sending it).
*/
addCallback<T>(
descriptor: EventDescriptor<T>,
handler: (params: T) => void,
): Promise<{ unsubscribe(): Promise<void> }>
}
103 changes: 103 additions & 0 deletions javascript/selenium-webdriver/bidi/domain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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.
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. Two distinct things happen: remote
* subscription (telling the browser to start sending this event at all,
* via the underlying connection's `subscribe()`) and local listening
* (attaching `handler` to the connection so it runs when the event
* arrives) — a descriptor's event only ever reaches `handler` once both
* are in place. Remote subscription/unsubscription is ref-counted against
* the connection's own listener count — shared truth across every Domain
* instance on the same connection — so a second caller subscribing to the
* same event doesn't re-subscribe remotely, and unsubscribing doesn't cut
* off another caller still listening for it.
* @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.
Resolved
const dispatch = descriptor.type === undefined ? handler : (params) => handler(descriptor.type.fromWire(params))
if (this.#bidi.listenerCount(descriptor.method) === 0) {
await this.#bidi.subscribe(descriptor.method)
}
this.#bidi.on(descriptor.method, dispatch)
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
return {
unsubscribe: async () => {
this.#bidi.off(descriptor.method, dispatch)
if (this.#bidi.listenerCount(descriptor.method) === 0) {
await this.#bidi.unsubscribe(descriptor.method)
}
},
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
}
}
}

module.exports = { Domain, event, DOMAIN_TOKEN }
24 changes: 24 additions & 0 deletions javascript/selenium-webdriver/bidi/serialization/enum.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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. */
export function defineEnum<T extends string>(name: string, values: readonly T[]): EnumEntry<T>
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
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.
Resolved
const allowed = new Set(values)
const entry = { kind: 'enum', values, includes: (value) => allowed.has(value) }
register(name, entry)
return entry
}

module.exports = { defineEnum }
65 changes: 65 additions & 0 deletions javascript/selenium-webdriver/bidi/serialization/record.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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()).
*/
export function defineRecord<T>(name: string, fields: FieldSpec[], options?: RecordOptions): RecordClass<T>

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