| 244 | } |
| 245 | |
| 246 | export class CdpPage { |
| 247 | private nextId = 1; |
| 248 | private readonly pending = new Map<number, (value: unknown) => void>(); |
| 249 | |
| 250 | private constructor(private readonly socket: WebSocket) { |
| 251 | socket.addEventListener("message", (event) => { |
| 252 | if (typeof event.data !== "string") return; |
| 253 | const message = JSON.parse(event.data) as { id?: number; result?: unknown }; |
| 254 | if (message.id && this.pending.has(message.id)) { |
| 255 | this.pending.get(message.id)!(message.result); |
| 256 | this.pending.delete(message.id); |
| 257 | } |
| 258 | }); |
| 259 | } |
| 260 | |
| 261 | static connect = (url: string): Promise<CdpPage> => |
| 262 | new Promise((resolve, reject) => { |
| 263 | const socket = new WebSocket(url); |
| 264 | const timer = setTimeout( |
| 265 | // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter |
| 266 | () => reject(new Error(`CDP connect timeout: ${url}`)), |
| 267 | 30_000, |
| 268 | ); |
| 269 | socket.addEventListener("open", () => { |
| 270 | clearTimeout(timer); |
| 271 | resolve(new CdpPage(socket)); |
| 272 | }); |
| 273 | socket.addEventListener("error", () => { |
| 274 | clearTimeout(timer); |
| 275 | // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: WebSocket connection promise adapter |
| 276 | reject(new Error(`CDP connect failed: ${url}`)); |
| 277 | }); |
| 278 | }); |
| 279 | |
| 280 | command = <T>(method: string, params: Record<string, unknown> = {}): Promise<T> => { |
| 281 | const id = this.nextId++; |
| 282 | const result = new Promise<T>((resolve) => |
| 283 | this.pending.set(id, (value) => resolve(value as T)), |
| 284 | ); |
| 285 | this.socket.send(JSON.stringify({ id, method, params })); |
| 286 | return result; |
| 287 | }; |
| 288 | |
| 289 | waitForText = async (text: string, timeoutMs: number): Promise<void> => { |
| 290 | const deadline = Date.now() + timeoutMs; |
| 291 | const expression = `document.body?.innerText.includes(${JSON.stringify(text)}) ?? false`; |
| 292 | for (;;) { |
| 293 | const r = await this.command<{ result?: { value?: boolean } }>("Runtime.evaluate", { |
| 294 | expression, |
| 295 | returnByValue: true, |
| 296 | }); |
| 297 | if (r.result?.value) return; |
| 298 | // oxlint-disable-next-line executor/no-error-constructor -- boundary: a wait timeout is a plain failure here |
| 299 | if (Date.now() >= deadline) throw new Error(`timed out waiting for text: ${text}`); |
| 300 | await sleep(250); |
| 301 | } |
| 302 | }; |
| 303 | |