Skip to content

Commit dc6607d

Browse files
Refine settle context probing warning behavior
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 00eee9a commit dc6607d

3 files changed

Lines changed: 131 additions & 5 deletions

File tree

‎currentState.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
152152
- Aligned settle network-tracing behavior so recording-video sessions now propagate trace mode into network-idle diagnostics (including stalled-request warnings) even when debug flags are off.
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.
155+
- 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.
155156
- Hardened A11y DOM option ingestion (`useCache`, `onFrameChunk`, `filterAdTrackingFrames`) with trap-safe reads, so malformed option objects no longer break extraction setup.
156157
- Hardened A11y DOM debug-option lookup (`getDebugOptions`) with trap-safe fallback defaults and sanitized warning diagnostics.
157158
- Hardened OpenAI/Anthropic structured-schema debug-option reads so trap-prone debug-option access no longer interrupts structured invocation paths.

‎src/utils/waitForSettledDOM.test.ts‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,89 @@ describe("waitForSettledDOM diagnostics", () => {
260260
}
261261
});
262262

263+
it("continues when page.context() getter throws", async () => {
264+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
265+
const { session } = createSessionWithEvents();
266+
const cdpClient: CDPClient = {
267+
rootSession: session,
268+
createSession: async () => session,
269+
acquireSession: async () => session,
270+
dispose: async () => undefined,
271+
};
272+
getCDPClient.mockResolvedValue(cdpClient);
273+
getOrCreateFrameContextManager.mockReturnValue({
274+
setDebug: jest.fn(),
275+
});
276+
getDebugOptions.mockReturnValue({
277+
enabled: false,
278+
traceWait: false,
279+
});
280+
281+
const page = {
282+
context: () => {
283+
throw new Error(`context\u0000\n${"x".repeat(2_000)}`);
284+
},
285+
} as never;
286+
287+
try {
288+
const waitPromise = waitForSettledDOM(page, 600);
289+
await Promise.resolve();
290+
await Promise.resolve();
291+
await jest.advanceTimersByTimeAsync(700);
292+
const stats = await waitPromise;
293+
294+
expect(stats.resolvedByTimeout).toBe(false);
295+
const warning = String(
296+
warnSpy.mock.calls.find((call) =>
297+
String(call[0] ?? "").includes("Failed to read page context")
298+
)?.[0] ?? ""
299+
);
300+
expect(warning).toContain("[truncated");
301+
expect(warning).not.toContain("\u0000");
302+
expect(warning).not.toContain("\n");
303+
} finally {
304+
warnSpy.mockRestore();
305+
}
306+
});
307+
308+
it("does not warn when page.context method is unavailable", async () => {
309+
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
310+
const { session } = createSessionWithEvents();
311+
const cdpClient: CDPClient = {
312+
rootSession: session,
313+
createSession: async () => session,
314+
acquireSession: async () => session,
315+
dispose: async () => undefined,
316+
};
317+
getCDPClient.mockResolvedValue(cdpClient);
318+
getOrCreateFrameContextManager.mockReturnValue({
319+
setDebug: jest.fn(),
320+
});
321+
getDebugOptions.mockReturnValue({
322+
enabled: false,
323+
traceWait: false,
324+
});
325+
326+
const page = {} as never;
327+
328+
try {
329+
const waitPromise = waitForSettledDOM(page, 600);
330+
await Promise.resolve();
331+
await Promise.resolve();
332+
await jest.advanceTimersByTimeAsync(700);
333+
const stats = await waitPromise;
334+
335+
expect(stats.resolvedByTimeout).toBe(false);
336+
expect(
337+
warnSpy.mock.calls.some((call) =>
338+
String(call[0] ?? "").includes("Failed to read page context")
339+
)
340+
).toBe(false);
341+
} finally {
342+
warnSpy.mockRestore();
343+
}
344+
});
345+
263346
it("applies frame filtering option when provided", async () => {
264347
const { session } = createSessionWithEvents();
265348
const cdpClient: CDPClient = {

‎src/utils/waitForSettledDOM.ts‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* 6. Global timeout ensures we don't wait forever
1515
*/
1616

17-
import type { BrowserContext, Page } from "playwright-core";
17+
import type { Page } from "playwright-core";
1818
import { getCDPClient, getOrCreateFrameContextManager } from "@/cdp";
1919
import type { CDPSession } from "@/cdp";
2020
import { Protocol } from "devtools-protocol";
@@ -207,6 +207,51 @@ function readWaitDebugOptions(): { enabled: boolean; traceWait: boolean } {
207207
}
208208
}
209209

210+
function hasRecordingVideoContext(page: Page): boolean {
211+
let contextMethod: unknown;
212+
try {
213+
contextMethod = (
214+
page as unknown as { context?: unknown }
215+
).context;
216+
} catch (error) {
217+
console.warn(
218+
`[waitForSettledDOM] Failed to read page context method: ${formatWaitDiagnostic(
219+
error
220+
)}`
221+
);
222+
return false;
223+
}
224+
if (typeof contextMethod !== "function") {
225+
return false;
226+
}
227+
228+
let context: unknown;
229+
try {
230+
context = (contextMethod as (this: Page) => unknown).call(page);
231+
} catch (error) {
232+
console.warn(
233+
`[waitForSettledDOM] Failed to read page context: ${formatWaitDiagnostic(
234+
error
235+
)}`
236+
);
237+
return false;
238+
}
239+
if (!context || typeof context !== "object") {
240+
return false;
241+
}
242+
try {
243+
const options = (context as { _options?: { recordVideo?: unknown } })._options;
244+
return !!options?.recordVideo;
245+
} catch (error) {
246+
console.warn(
247+
`[waitForSettledDOM] Failed to read context options: ${formatWaitDiagnostic(
248+
error
249+
)}`
250+
);
251+
return false;
252+
}
253+
}
254+
210255
function safeReadWaitOptionField(
211256
options: unknown,
212257
field: keyof WaitForSettledOptions
@@ -235,13 +280,10 @@ export async function waitForSettledDOM(
235280
typeof filterAdTrackingFramesOption === "boolean"
236281
? filterAdTrackingFramesOption
237282
: undefined;
238-
const ctx = page.context() as BrowserContext & {
239-
_options?: { recordVideo?: unknown };
240-
};
241283
const debugOptions = readWaitDebugOptions();
242284
const traceWaitFlag =
243285
(debugOptions.enabled && debugOptions.traceWait) || ENV_TRACE_WAIT;
244-
const traceWait = traceWaitFlag || !!ctx._options?.recordVideo;
286+
const traceWait = traceWaitFlag || hasRecordingVideoContext(page);
245287
const totalStart = performance.now();
246288

247289
// Currently we only wait for network idle (historical behavior). Hook exists if we add DOM states later.

0 commit comments

Comments
 (0)