Skip to content

Commit 4e9302d

Browse files
committed
...
1 parent 25da91e commit 4e9302d

6 files changed

Lines changed: 85 additions & 32 deletions

File tree

javascript/selenium-webdriver/bidi/index.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,15 @@ class Index extends EventEmitter {
7979
}
8080
return
8181
}
82-
// Messages without a numeric id are BiDi events, not command
83-
// responses; they are routed via subscribe/EventEmitter elsewhere
84-
// and intentionally ignored by this dispatcher.
82+
// Messages without a numeric id are BiDi events, not command responses.
83+
// Emit them by method name so that generated domain classes and other
84+
// consumers can use bidi.on(methodName, callback) rather than attaching
85+
// their own raw ws.on('message', ...) listeners (which accumulate and
86+
// trigger MaxListeners warnings).
8587
if (payload == null || typeof payload.id !== 'number') {
88+
if (payload != null && typeof payload.method === 'string') {
89+
this.emit(payload.method, payload.params)
90+
}
8691
return
8792
}
8893
const entry = this._pending.get(payload.id)

javascript/selenium-webdriver/generate_bidi.mjs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,13 @@ const DOMAIN_CLASSES = {
138138
function resolveInputPath(p) {
139139
if (!p) return null
140140
if (!process.env.BAZEL_BINDIR) return resolve(p)
141-
const prefix = process.env.BAZEL_BINDIR + '/'
142-
return resolve(p.startsWith(prefix) ? p.slice(prefix.length) : p)
141+
// Normalize both strings to forward slashes before prefix-stripping so that
142+
// mixed separators on Windows (BAZEL_BINDIR uses '\', $(location) uses '/')
143+
// do not cause the startsWith check to silently fail.
144+
const normalizedP = p.replaceAll('\\', '/')
145+
const normalizedBindir = process.env.BAZEL_BINDIR.replaceAll('\\', '/')
146+
const prefix = normalizedBindir + '/'
147+
return resolve(normalizedP.startsWith(prefix) ? normalizedP.slice(prefix.length) : normalizedP)
143148
}
144149

145150
// ============================================================
@@ -169,6 +174,10 @@ async function main() {
169174
// The --output-dir arg is passed as <pkg>/<rel_path>, which is already
170175
// relative to BAZEL_BINDIR (= CWD), so it needs no special handling.
171176
const cddlPath = resolveInputPath(args.cddl)
177+
if (!existsSync(cddlPath)) {
178+
console.error(`Error: CDDL file not found: ${cddlPath}`)
179+
process.exit(1)
180+
}
172181
const outputDir = resolve(args['output-dir'])
173182
const specVersion = args['spec-version']
174183

@@ -241,7 +250,16 @@ function loadEnhancements(manifestPath) {
241250
console.warn(`Warning: enhancements manifest not found: ${fullPath}`)
242251
return {}
243252
}
244-
return JSON.parse(readFileSync(fullPath, 'utf8'))
253+
let parsed
254+
try {
255+
parsed = JSON.parse(readFileSync(fullPath, 'utf8'))
256+
} catch (err) {
257+
throw new Error(`Failed to parse enhancements manifest at ${fullPath}: ${err.message}`)
258+
}
259+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
260+
throw new Error(`Enhancements manifest at ${fullPath} must be a JSON object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`)
261+
}
262+
return parsed
245263
}
246264

247265
// ============================================================
@@ -752,8 +770,6 @@ function generateDomainFile({
752770
parts.push(` send(command: Record<string, unknown>): Promise<unknown>`)
753771
parts.push(` subscribe(event: string | string[], contexts?: string[]): Promise<void>`)
754772
parts.push(` on(event: string, listener: (params: unknown) => void): void`)
755-
parts.push(` /** Raw WebSocket — used by event listeners to attach message handlers. */`)
756-
parts.push(` readonly socket: { on(event: 'message', listener: (data: { toString(): string }) => void): void }`)
757773
parts.push(`}`)
758774
parts.push('')
759775
}
@@ -826,6 +842,10 @@ function generateClass({ className, commands, events, enhancement, emptyResultTy
826842
lines.push(` private constructor(private readonly bidi: BidiConnection) {}`)
827843
lines.push('')
828844
lines.push(` static async create(driver: unknown): Promise<${className}> {`)
845+
lines.push(` const caps = await (driver as { getCapabilities(): Promise<{ get(key: string): unknown }> }).getCapabilities()`)
846+
lines.push(` if (!caps.get('webSocketUrl')) {`)
847+
lines.push(` throw new Error('WebDriver instance must support BiDi protocol')`)
848+
lines.push(` }`)
829849
lines.push(` const bidi = await (driver as { getBidi(): Promise<BidiConnection> }).getBidi()`)
830850
lines.push(` return new ${className}(bidi)`)
831851
lines.push(` }`)
@@ -901,19 +921,13 @@ function generateEventMethod(evt) {
901921
const lines = []
902922
lines.push(` async ${onMethodName}(callback: ${cbType}): Promise<void> {`)
903923
lines.push(` await this.bidi.subscribe('${methodStr}')`)
904-
// Use the raw WebSocket message listener — the same pattern as the hand-coded
905-
// logInspector.js / browsingContext.js classes. bidi/index.js intentionally
906-
// ignores WebSocket messages without a numeric "id" in its response dispatcher,
907-
// so event payloads must be captured directly from the socket instead.
908-
lines.push(` const ws = this.bidi.socket`)
909-
lines.push(` ws.on('message', (data: { toString(): string }) => {`)
910-
lines.push(` const msg = JSON.parse(data.toString()) as {`)
911-
lines.push(` method?: string`)
912-
lines.push(` params?: unknown`)
913-
lines.push(` }`)
914-
lines.push(` if (msg.method === '${methodStr}') {`)
915-
lines.push(` callback(${paramsTypeName ? `msg.params as ${paramsTypeName}` : 'msg.params'})`)
916-
lines.push(` }`)
924+
// bidi/index.js emits BiDi events by method name through its single shared
925+
// message dispatcher (which already handles JSON parsing and closed-state
926+
// guards). Using bidi.on() here avoids attaching a new ws.on('message', ...)
927+
// listener on every subscription call, preventing listener accumulation and
928+
// MaxListeners warnings.
929+
lines.push(` this.bidi.on('${methodStr}', (params: unknown) => {`)
930+
lines.push(` callback(${paramsTypeName ? `params as ${paramsTypeName}` : 'params'})`)
917931
lines.push(` })`)
918932
lines.push(` }`)
919933
return lines.join('\n')

javascript/selenium-webdriver/private/generate_bidi.bzl

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@ _DOMAIN_TS_FILES = [
2121
"webextension.ts",
2222
]
2323

24+
def _merge_cddl_impl(ctx):
25+
"""Merges one or more CDDL files into a single output file."""
26+
out = ctx.outputs.out
27+
args = ctx.actions.args()
28+
args.add(out)
29+
args.add_all(ctx.files.srcs)
30+
ctx.actions.run(
31+
inputs = ctx.files.srcs,
32+
outputs = [out],
33+
executable = ctx.executable.tool,
34+
arguments = [args],
35+
mnemonic = "MergeCddl",
36+
progress_message = "Merging CDDL files into %s" % out.short_path,
37+
)
38+
return [DefaultInfo(files = depset([out]))]
39+
40+
_merge_cddl = rule(
41+
implementation = _merge_cddl_impl,
42+
attrs = {
43+
"srcs": attr.label_list(allow_files = True, mandatory = True),
44+
"out": attr.output(mandatory = True),
45+
"tool": attr.label(
46+
executable = True,
47+
cfg = "exec",
48+
mandatory = True,
49+
),
50+
},
51+
doc = "Merges CDDL specification files into a single file using an external merge tool.",
52+
)
53+
2454
def _compile_bidi_ts_impl(ctx):
2555
ts_files = ctx.files.srcs
2656
output_subdir = ctx.attr.output_subdir
@@ -41,7 +71,6 @@ def _compile_bidi_ts_impl(ctx):
4171
args.add("--module", "NodeNext")
4272
args.add("--moduleResolution", "NodeNext")
4373
args.add("--declaration")
44-
args.add("--removeComments")
4574
args.add("--outDir", js_outputs[0].dirname)
4675
for f in ts_files:
4776
args.add(f.path)
@@ -108,14 +137,14 @@ def generate_bidi_library(
108137

109138
# Step 1: merge CDDL files into one.
110139
# merge_cddl signature: <output> <input1> [<input2> ...]
111-
# genrule: $@ = output, $(SRCS) = inputs in order.
140+
# Uses ctx.actions.run so arguments are passed as an argv list rather than
141+
# a shell command string, avoiding quoting/escaping issues with special chars.
112142
merged_name = name + "_merged_cddl"
113-
native.genrule(
143+
_merge_cddl(
114144
name = merged_name,
115145
srcs = [cddl_file] + extra_cddl_files,
116-
outs = [name + "_merged.cddl"],
117-
cmd = "$(location " + merge_tool + ") $@ $(SRCS)",
118-
tools = [merge_tool],
146+
out = name + "_merged.cddl",
147+
tool = merge_tool,
119148
)
120149

121150
# Step 2: run generate_bidi.mjs → 15 .ts files (one per BiDi domain).

javascript/selenium-webdriver/test/bidi/generated/browsing_context_test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ suite(
223223
})
224224

225225
await browsingContext.navigate({ context: contextId, url: Pages.emptyPage, wait: 'complete' })
226+
await driver.wait(() => navEvent !== null, 5000)
226227

227228
assert.ok(navEvent, 'navigationCommitted event should have fired')
228229
assert.strictEqual(navEvent.context, contextId)

javascript/selenium-webdriver/test/bidi/generated/log_test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ suite(
183183

184184
// Trigger a JS error to produce a javascript log entry
185185
await driver.findElement({ id: 'jsException' }).click()
186+
await driver.wait(() => entries.length > 0, 5000)
186187

187-
// onJavascriptLog covers JS-level log entries
188-
assert.ok(entries.length >= 0) // may or may not fire depending on driver impl
188+
assert.ok(entries.length > 0, 'onJavascriptLog should have received at least one entry')
189+
assert.strictEqual(entries[0].type, 'javascript')
189190
})
190191
})
191192
},

javascript/selenium-webdriver/test/bidi/generated/network_test.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ suite(
5656
assert.ok(event, 'beforeRequestSent should have fired')
5757
assert.strictEqual(event.request.method, 'GET')
5858
assert.ok(event.request.url)
59-
assert.ok(event.request.headers.length >= 0)
59+
assert.ok(event.request.headers.length > 0, 'request should have at least one header')
6060
})
6161

6262
it('receives cookies in beforeRequestSent', async function () {
@@ -110,9 +110,12 @@ suite(
110110
errorEvent = params
111111
})
112112

113-
// Navigate to a non-existent host to trigger a fetch error
113+
// Navigate to a non-existent host to trigger a fetch error.
114+
// Use wait: 'none' so the call returns immediately — waiting for
115+
// 'complete' on a non-existent host blocks for the full TCP/DNS
116+
// timeout before the fetchError event can be observed.
114117
await browsingContext
115-
.navigate({ context: contextId, url: 'http://doesnotexist.invalid/', wait: 'complete' })
118+
.navigate({ context: contextId, url: 'http://doesnotexist.invalid/', wait: 'none' })
116119
.catch(() => {})
117120
await driver.wait(() => errorEvent !== null, 5000)
118121

0 commit comments

Comments
 (0)