Skip to content

Commit 3f1b322

Browse files
Harden Playwright CDP client initialization flow
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent dc6607d commit 3f1b322

3 files changed

Lines changed: 149 additions & 6 deletions

File tree

currentState.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
153153
- Expanded settle trace regressions with cleaner log-capture coverage to ensure recording-video trace diagnostics remain validated without noisy test output.
154154
- Hardened settle debug-option lookup against trap-prone `getDebugOptions()` reads, with deterministic fallback trace defaults and sanitized warning diagnostics.
155155
- Refined settle context probing to avoid noisy warnings when `page.context` is unavailable while still surfacing sanitized diagnostics for trap-prone context method/getter failures.
156+
- Hardened Playwright CDP session initialization flow:
157+
- `createSession` now guards context acquisition, `newCDPSession` method reads, and session creation with bounded/sanitized diagnostics.
158+
- `getCDPClientForPage` now always clears pending init promises (including failed init paths) and tolerates trap-prone `page.once` close-listener attachment with sanitized warnings.
156159
- Hardened A11y DOM option ingestion (`useCache`, `onFrameChunk`, `filterAdTrackingFrames`) with trap-safe reads, so malformed option objects no longer break extraction setup.
157160
- Hardened A11y DOM debug-option lookup (`getDebugOptions`) with trap-safe fallback defaults and sanitized warning diagnostics.
158161
- Hardened OpenAI/Anthropic structured-schema debug-option reads so trap-prone debug-option access no longer interrupts structured invocation paths.

src/cdp/playwright-adapter.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,81 @@ describe("playwright adapter error formatting", () => {
109109
warnSpy.mockRestore();
110110
}
111111
});
112+
113+
it("surfaces sanitized diagnostics when page context traps during init", async () => {
114+
const page = {
115+
context: () => {
116+
throw new Error(`context\u0000\n${"x".repeat(2_000)}`);
117+
},
118+
once: jest.fn(),
119+
} as unknown as Page;
120+
121+
await expect(getCDPClientForPage(page)).rejects.toThrow(
122+
"[CDP][PlaywrightAdapter] Failed to create CDP session"
123+
);
124+
125+
await expect(getCDPClientForPage(page)).rejects.toThrow("[truncated");
126+
});
127+
128+
it("clears pending init promise after context-init failure", async () => {
129+
const session = {
130+
send: jest.fn().mockResolvedValue({}),
131+
on: jest.fn(),
132+
off: jest.fn(),
133+
detach: jest.fn().mockResolvedValue(undefined),
134+
} as unknown as PlaywrightSession;
135+
136+
const context = jest
137+
.fn<unknown, []>()
138+
.mockImplementationOnce(() => {
139+
throw new Error("first init failure");
140+
})
141+
.mockImplementation(() => ({
142+
newCDPSession: jest.fn().mockResolvedValue(session),
143+
}));
144+
145+
const page = {
146+
context,
147+
once: jest.fn(),
148+
} as unknown as Page;
149+
150+
await expect(getCDPClientForPage(page)).rejects.toThrow(
151+
"first init failure"
152+
);
153+
await expect(getCDPClientForPage(page)).resolves.toBeDefined();
154+
expect(context).toHaveBeenCalledTimes(2);
155+
});
156+
157+
it("warns and continues when close-listener attachment traps", async () => {
158+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
159+
const session = {
160+
send: jest.fn().mockResolvedValue({}),
161+
on: jest.fn(),
162+
off: jest.fn(),
163+
detach: jest.fn().mockResolvedValue(undefined),
164+
} as unknown as PlaywrightSession;
165+
166+
const page = {
167+
context: () => ({
168+
newCDPSession: jest.fn().mockResolvedValue(session),
169+
}),
170+
get once() {
171+
throw new Error(`close-listener\u0000\n${"x".repeat(2_000)}`);
172+
},
173+
} as unknown as Page;
174+
175+
try {
176+
await expect(getCDPClientForPage(page)).resolves.toBeDefined();
177+
const warning = String(
178+
warnSpy.mock.calls.find((call) =>
179+
String(call[0] ?? "").includes("Failed to attach page close listener")
180+
)?.[0] ?? ""
181+
);
182+
expect(warning).toContain("[truncated");
183+
expect(warning).not.toContain("\u0000");
184+
expect(warning).not.toContain("\n");
185+
} finally {
186+
warnSpy.mockRestore();
187+
}
188+
});
112189
});

src/cdp/playwright-adapter.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,52 @@ class PlaywrightCDPClient implements CDPClient {
161161

162162
async createSession(descriptor?: CDPTargetDescriptor): Promise<CDPSession> {
163163
const target = this.resolveTarget(descriptor);
164-
const session = await this.page.context().newCDPSession(target);
164+
let pageContext: unknown;
165+
try {
166+
pageContext = this.page.context();
167+
} catch (error) {
168+
throw new Error(
169+
`[CDP][PlaywrightAdapter] Failed to create CDP session: ${formatPlaywrightAdapterDiagnostic(
170+
error
171+
)}`
172+
);
173+
}
174+
if (!pageContext || typeof pageContext !== "object") {
175+
throw new Error(
176+
"[CDP][PlaywrightAdapter] Failed to create CDP session: page context unavailable"
177+
);
178+
}
179+
180+
let newCDPSessionMethod: unknown;
181+
try {
182+
newCDPSessionMethod = (
183+
pageContext as { newCDPSession?: unknown }
184+
).newCDPSession;
185+
} catch (error) {
186+
throw new Error(
187+
`[CDP][PlaywrightAdapter] Failed to create CDP session: ${formatPlaywrightAdapterDiagnostic(
188+
error
189+
)}`
190+
);
191+
}
192+
if (typeof newCDPSessionMethod !== "function") {
193+
throw new Error(
194+
"[CDP][PlaywrightAdapter] Failed to create CDP session: newCDPSession() unavailable"
195+
);
196+
}
197+
198+
let session: PlaywrightSession;
199+
try {
200+
session = (await (
201+
newCDPSessionMethod as (targetArg: Page | Frame) => Promise<PlaywrightSession>
202+
)(target)) as PlaywrightSession;
203+
} catch (error) {
204+
throw new Error(
205+
`[CDP][PlaywrightAdapter] Failed to create CDP session: ${formatPlaywrightAdapterDiagnostic(
206+
error
207+
)}`
208+
);
209+
}
165210
const wrapped = new PlaywrightSessionAdapter(session, (adapter) =>
166211
this.trackedSessions.delete(adapter)
167212
);
@@ -298,12 +343,30 @@ export async function getCDPClientForPage(page: Page): Promise<CDPClient> {
298343
const client = new PlaywrightCDPClient(page);
299344
await client.init();
300345
clientCache.set(page, client);
301-
pendingClients.delete(page);
302-
page.once("close", () => {
303-
disposeCDPClientForPage(page).catch(() => {});
304-
});
346+
try {
347+
const once = (page as Page & { once?: unknown }).once;
348+
if (typeof once === "function") {
349+
(
350+
once as (
351+
this: Page,
352+
event: "close",
353+
listener: () => void
354+
) => void
355+
).call(page, "close", () => {
356+
disposeCDPClientForPage(page).catch(() => {});
357+
});
358+
}
359+
} catch (error) {
360+
console.warn(
361+
`[CDP][PlaywrightAdapter] Failed to attach page close listener: ${formatPlaywrightAdapterDiagnostic(
362+
error
363+
)}`
364+
);
365+
}
305366
return client;
306-
})();
367+
})().finally(() => {
368+
pendingClients.delete(page);
369+
});
307370

308371
pendingClients.set(page, initPromise);
309372
return initPromise;

0 commit comments

Comments
 (0)