fix: stop cdp load polling after timeout (#4236)

This commit is contained in:
Neil 2026-05-31 09:16:31 -07:00 committed by GitHub
parent 853fa454ef
commit b3cffddf82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 81 additions and 9 deletions

View File

@ -72,7 +72,12 @@ const SEARCH_PAGE_TREE: AXNode[] = [
// ── Mock WebContents factory ──
function createMockGuest(id: number, url: string, title: string) {
function createMockGuest(
id: number,
url: string,
title: string,
options?: { readyState?: string | (() => string) }
) {
let currentUrl = url
let currentTitle = title
let currentTree = EXAMPLE_COM_TREE
@ -111,7 +116,14 @@ function createMockGuest(id: number, url: string, title: string) {
case 'Runtime.evaluate': {
const expr = (params as { expression: string }).expression
if (expr === 'document.readyState') {
return { result: { value: 'complete' } }
return {
result: {
value:
typeof options?.readyState === 'function'
? options.readyState()
: (options?.readyState ?? 'complete')
}
}
}
if (expr === 'location.origin') {
return { result: { value: new URL(currentUrl).origin } }
@ -413,6 +425,43 @@ describe('Browser automation pipeline (integration)', () => {
expect((res.error as { code: string }).code).toBe('browser_navigation_failed')
})
it('clears readyState polling timers when navigation times out', async () => {
vi.useFakeTimers()
try {
const slowGuestHarness = createMockGuest(6001, 'https://slow.example.com', 'Slow Page', {
readyState: 'loading'
})
webContentsFromIdMock.mockImplementation((id: number) => {
if (id === 6001) {
return slowGuestHarness.guest
}
return null
})
const browserManager = new BrowserManager()
browserManager.attachGuestPolicies(slowGuestHarness.guest as never)
browserManager.registerGuest({
browserPageId: 'slow-page',
webContentsId: 6001,
rendererWebContentsId: RENDERER_WC_ID
})
const bridge = new CdpBridge(browserManager)
bridge.setActiveTab(6001)
const gotoResult = bridge.goto('https://slow.example.com/still-loading').then(
() => null,
(error: unknown) => error
)
await vi.advanceTimersByTimeAsync(25_000)
await expect(gotoResult).resolves.toMatchObject({ code: 'browser_timeout' })
expect(vi.getTimerCount()).toBe(0)
} finally {
vi.useRealTimers()
}
})
// ── Fill ──
it('fills an input by ref', async () => {

View File

@ -1634,28 +1634,51 @@ export class CdpBridge {
// Phase 1: wait for readyState=complete
await new Promise<void>((resolve, reject) => {
let settled = false
let pollTimer: ReturnType<typeof setTimeout> | null = null
const finish = (callback: () => void): void => {
if (settled) {
return
}
settled = true
clearTimeout(timeout)
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
callback()
}
const timeout = setTimeout(() => {
reject(new BrowserError('browser_timeout', 'Page load timed out.'))
finish(() => reject(new BrowserError('browser_timeout', 'Page load timed out.')))
}, TIMEOUT_MS)
const check = async (): Promise<void> => {
if (settled) {
return
}
try {
const { result } = (await sender('Runtime.evaluate', {
expression: 'document.readyState',
returnByValue: true
})) as { result: { value: string } }
if (settled) {
return
}
if (result.value === 'complete') {
clearTimeout(timeout)
resolve()
finish(resolve)
} else {
setTimeout(check, 100)
pollTimer = setTimeout(() => {
pollTimer = null
void check()
}, 100)
}
} catch {
clearTimeout(timeout)
resolve()
finish(resolve)
}
}
check()
void check()
})
// Phase 2: wait for network idle