diff --git a/lib/StagehandContext.ts b/lib/StagehandContext.ts index 5d8bf4e66..f059ee505 100644 --- a/lib/StagehandContext.ts +++ b/lib/StagehandContext.ts @@ -32,7 +32,6 @@ export class StagehandContext { if (prop === "pages") { return (): Page[] => { const pwPages = target.pages(); - // Convert all pages to StagehandPages synchronously return pwPages.map((pwPage: PlaywrightPage) => { let stagehandPage = this.pageMap.get(pwPage); if (!stagehandPage) { @@ -52,14 +51,54 @@ export class StagehandContext { }); }; } + // For other properties/methods, pass through to the original context return target[prop as keyof PlaywrightContext]; }, }) as unknown as EnhancedContext; + + // Listen for new pages directly on the original Playwright context + context.on("page", async (newPage: PlaywrightPage) => { + try { + // Check if we already have this page wrapped and initialized + let stagehandPage = this.pageMap.get(newPage); + if ( + stagehandPage && + (stagehandPage as unknown as { initialized: boolean }).initialized + ) { + this.setActivePage(stagehandPage); + return; + } + + stagehandPage = await this.createStagehandPage(newPage); // This calls .init() + this.setActivePage(stagehandPage); + + this.stagehand.log({ + category: "context", + message: "New page wrapped and set as active by direct listener.", + level: 1, + }); + } catch (error) { + this.stagehand.log({ + category: "context", + message: "Error in direct 'page' event listener", + level: 0, // Error level + auxiliary: { + error: { value: error.message, type: "string" }, + trace: { value: error.stack, type: "string" }, + }, + }); + } + }); } private async createStagehandPage( page: PlaywrightPage, ): Promise { + if (this.pageMap.has(page)) { + const existingPage = this.pageMap.get(page); + return existingPage; + } + const stagehandPage = await new StagehandPage( page, this.stagehand, @@ -81,14 +120,24 @@ export class StagehandContext { // Initialize existing pages const existingPages = context.pages(); - for (const page of existingPages) { - const stagehandPage = await instance.createStagehandPage(page); - // Set the first page as active - if (!instance.activeStagehandPage) { - instance.setActivePage(stagehandPage); + if (existingPages.length === 0) { + // If no pages exist, create one (Playwright context might start empty) + stagehand.log({ + category: "context", + message: "No existing pages, creating a new one.", + level: 2, + }); + const pwPage = await context.newPage(); + const shPage = await instance.createStagehandPage(pwPage); + instance.setActivePage(shPage); + } else { + for (const page of existingPages) { + const stagehandPage = await instance.createStagehandPage(page); + if (!instance.activeStagehandPage) { + instance.setActivePage(stagehandPage); // Set first page as active + } } } - return instance; } diff --git a/lib/handlers/handlerUtils/actHandlerUtils.ts b/lib/handlers/handlerUtils/actHandlerUtils.ts index 1a343389a..42a0669e9 100644 --- a/lib/handlers/handlerUtils/actHandlerUtils.ts +++ b/lib/handlers/handlerUtils/actHandlerUtils.ts @@ -1,4 +1,4 @@ -import { Page, Locator } from "@playwright/test"; +import { Locator } from "@playwright/test"; import { PlaywrightCommandException } from "../../../types/playwright"; import { StagehandPage } from "../../StagehandPage"; import { getNodeFromXpath } from "@/lib/dom/utils"; @@ -417,72 +417,33 @@ async function handlePossiblePageNavigation( category: "action", message: `${actionDescription}, checking for page navigation`, level: 1, - auxiliary: { - xpath: { value: xpath, type: "string" }, - }, + auxiliary: { xpath: { value: xpath, type: "string" } }, }); - const newOpenedTab = await Promise.race([ - new Promise((resolve) => { - stagehandPage.context.once("page", (page) => resolve(page)); - setTimeout(() => resolve(null), 1500); - }), + // just wait a little for a possible new-tab event + await Promise.race([ + new Promise((res) => stagehandPage.context.once("page", () => res(null))), + new Promise((res) => setTimeout(res, 1500)), ]); + // Always fetch the *current* active page from the Stagehand instance + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stagehand = (stagehandPage as any).stagehand; + const pageToSettle = await stagehand.context.getActivePage(); + logger({ category: "action", - message: `${actionDescription} complete`, + message: `Settling DOM on page: ${pageToSettle.page.url()}`, level: 1, - auxiliary: { - newOpenedTab: { - value: newOpenedTab ? "opened a new tab" : "no new tabs opened", - type: "string", - }, - }, }); - if (newOpenedTab && newOpenedTab.url() !== "about:blank") { - logger({ - category: "action", - message: "new page detected (new tab) with URL", - level: 1, - auxiliary: { - url: { value: newOpenedTab.url(), type: "string" }, - }, - }); - await newOpenedTab.close(); - await stagehandPage.page.goto(newOpenedTab.url()); - await stagehandPage.page.waitForLoadState("domcontentloaded"); - } - try { - await stagehandPage._waitForSettledDom(domSettleTimeoutMs); - } catch (e) { + await pageToSettle._waitForSettledDom(domSettleTimeoutMs); + } catch { logger({ category: "action", message: "wait for settled DOM timeout hit", level: 1, - auxiliary: { - trace: { value: e.stack, type: "string" }, - message: { value: e.message, type: "string" }, - }, - }); - } - - logger({ - category: "action", - message: "finished waiting for (possible) page navigation", - level: 1, - }); - - if (stagehandPage.page.url() !== initialUrl) { - logger({ - category: "action", - message: "new page detected with URL", - level: 1, - auxiliary: { - url: { value: stagehandPage.page.url(), type: "string" }, - }, }); } }