diff --git a/packages/core/src/browser/playwrightBrowser.ts b/packages/core/src/browser/playwrightBrowser.ts index daffab40..8bb49cca 100644 --- a/packages/core/src/browser/playwrightBrowser.ts +++ b/packages/core/src/browser/playwrightBrowser.ts @@ -590,8 +590,13 @@ export class PlaywrightBrowser implements AriaBrowser { if (!this.page) throw new Error("Browser not started"); try { - // 1. Wait for DOM to be ready - this is critical for interactivity - await this.page.waitForLoadState("domcontentloaded"); + // 1. Wait for DOM to be ready - this is critical for interactivity. + // Bounded by actionTimeoutMs: a page (or SPA soft-nav) that never reaches + // domcontentloaded must not hang the agent. On timeout we continue, since the + // page may still be interactive and the load wait + settle below still run. + await this.page.waitForLoadState("domcontentloaded", { + timeout: this.actionTimeoutMs, + }); } catch (error) { // Still continue since we might be able to interact with what's loaded } diff --git a/packages/core/test/playwrightBrowser.test.ts b/packages/core/test/playwrightBrowser.test.ts index 08ba15eb..97444db1 100644 --- a/packages/core/test/playwrightBrowser.test.ts +++ b/packages/core/test/playwrightBrowser.test.ts @@ -1340,4 +1340,20 @@ describe("PlaywrightBrowser", () => { expect(error).not.toBeInstanceOf(BrowserDisconnectedError); }); }); + + describe("ensureOptimizedPageLoad", () => { + it("bounds the domcontentloaded wait with actionTimeoutMs so it can't hang indefinitely", async () => { + const browser = new PlaywrightBrowser({ browser: "chromium", actionTimeoutMs: 1234 }); + const waitForLoadState = vi.fn().mockResolvedValue(undefined); + (browser as any).page = { + waitForLoadState, + waitForTimeout: vi.fn().mockResolvedValue(undefined), + }; + + await (browser as any).ensureOptimizedPageLoad(); + + // The domcontentloaded wait must carry a timeout (previously unbounded). + expect(waitForLoadState).toHaveBeenCalledWith("domcontentloaded", { timeout: 1234 }); + }); + }); });