From 9fb94132ea792ea2bdea322c8888c6a6d5671b1a Mon Sep 17 00:00:00 2001 From: Sean McGuire <75873287+seanmcguire12@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:40:01 -0800 Subject: [PATCH] More playwright tests (#330) * add docs, move scoring functions to scoring.ts, move experiment naming to utils.ts * add initStagehand.ts * break up index.evals.ts and utils into smaller files * export LogLineEval * typing * follow StagehandConfig pattern * choose api key based on model name * Use CI on v2 branch * branch * stagehand.page tests * dont run on BB * prettier * pls dont fail * headless --------- Co-authored-by: Anirudh Kamath --- evals/deterministic/stagehand.config.ts | 4 +- .../deterministic/tests/addInitScript.test.ts | 40 +++++ .../tests/addRemoveLocatorHandler.test.ts | 93 ++++++++++ evals/deterministic/tests/addTags.test.ts | 79 +++++++++ .../deterministic/tests/bringToFront.test.ts | 37 ++++ evals/deterministic/tests/content.test.ts | 18 ++ evals/deterministic/tests/evaluate.test.ts | 31 ++++ evals/deterministic/tests/expose.test.ts | 63 +++++++ evals/deterministic/tests/frames.test.ts | 66 +++++++ evals/deterministic/tests/getBy.test.ts | 50 ++++++ evals/deterministic/tests/navigation.test.ts | 26 +++ evals/deterministic/tests/pageContext.test.ts | 59 +++++++ evals/deterministic/tests/reload.test.ts | 40 +++++ evals/deterministic/tests/waitFor.test.ts | 165 ++++++++++++++++++ 14 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 evals/deterministic/tests/addInitScript.test.ts create mode 100644 evals/deterministic/tests/addRemoveLocatorHandler.test.ts create mode 100644 evals/deterministic/tests/addTags.test.ts create mode 100644 evals/deterministic/tests/bringToFront.test.ts create mode 100644 evals/deterministic/tests/content.test.ts create mode 100644 evals/deterministic/tests/evaluate.test.ts create mode 100644 evals/deterministic/tests/expose.test.ts create mode 100644 evals/deterministic/tests/frames.test.ts create mode 100644 evals/deterministic/tests/getBy.test.ts create mode 100644 evals/deterministic/tests/navigation.test.ts create mode 100644 evals/deterministic/tests/pageContext.test.ts create mode 100644 evals/deterministic/tests/reload.test.ts create mode 100644 evals/deterministic/tests/waitFor.test.ts diff --git a/evals/deterministic/stagehand.config.ts b/evals/deterministic/stagehand.config.ts index 87adf9c4..94d4ecb7 100644 --- a/evals/deterministic/stagehand.config.ts +++ b/evals/deterministic/stagehand.config.ts @@ -1,12 +1,12 @@ import type { ConstructorParams, LogLine } from "../../lib"; const StagehandConfig: ConstructorParams = { - env: "BROWSERBASE" /* Environment to run Stagehand in */, + env: "LOCAL" /* Environment to run Stagehand in */, apiKey: process.env.BROWSERBASE_API_KEY! /* API key for authentication */, projectId: process.env.BROWSERBASE_PROJECT_ID! /* Project identifier */, verbose: 1 /* Logging verbosity level (0=quiet, 1=normal, 2=verbose) */, debugDom: true /* Enable DOM debugging features */, - headless: false /* Run browser in headless mode */, + headless: true /* Run browser in headless mode */, logger: (message: LogLine) => console.log( `[stagehand::${message.category}] ${message.message}`, diff --git a/evals/deterministic/tests/addInitScript.test.ts b/evals/deterministic/tests/addInitScript.test.ts new file mode 100644 index 00000000..2b80f601 --- /dev/null +++ b/evals/deterministic/tests/addInitScript.test.ts @@ -0,0 +1,40 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - addInitScript", () => { + test("should inject a script before the page loads", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + + await page.addInitScript(() => { + const w = window as typeof window & { + __testInitScriptVar?: string; + }; + w.__testInitScriptVar = "Hello from init script!"; + }); + + await page.goto("https://example.com"); + + const result = await page.evaluate(() => { + const w = window as typeof window & { + __testInitScriptVar?: string; + }; + return w.__testInitScriptVar; + }); + expect(result).toBe("Hello from init script!"); + + await page.goto("https://www.browserbase.com/"); + const resultAfterNavigation = await page.evaluate(() => { + const w = window as typeof window & { + __testInitScriptVar?: string; + }; + return w.__testInitScriptVar; + }); + expect(resultAfterNavigation).toBe("Hello from init script!"); + + await stagehand.close(); + }); +}); diff --git a/evals/deterministic/tests/addRemoveLocatorHandler.test.ts b/evals/deterministic/tests/addRemoveLocatorHandler.test.ts new file mode 100644 index 00000000..cfbbe8bb --- /dev/null +++ b/evals/deterministic/tests/addRemoveLocatorHandler.test.ts @@ -0,0 +1,93 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - addLocatorHandler and removeLocatorHandler", () => { + // This HTML snippet is reused by both tests. + // The "Sign up to the newsletter" overlay appears after 2 seconds. + // The "No thanks" button hides it. + const overlayHTML = ` + + + + + + + + `; + + test("should use a custom locator handler to dismiss the overlay", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const { page } = stagehand; + + await page.addLocatorHandler( + page.getByText("Sign up to the newsletter"), + async () => { + console.log("Overlay detected. Clicking 'No thanks' to remove it..."); + await page.getByRole("button", { name: "No thanks" }).click(); + }, + ); + + await page.goto("https://example.com"); + await page.setContent(overlayHTML); + + await page.waitForTimeout(5000); + + await page.getByRole("button", { name: "Start here" }).click(); + + const isOverlayVisible = await page + .getByText("Sign up to the newsletter") + .isVisible() + .catch(() => false); + + await stagehand.close(); + + expect(isOverlayVisible).toBeFalsy(); + }); + + test("should remove a custom locator handler so overlay stays visible", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const { page } = stagehand; + + const locator = page.getByText("Sign up to the newsletter"); + await page.addLocatorHandler(locator, async () => { + console.log("Overlay detected. Clicking 'No thanks' to remove it..."); + await page.getByRole("button", { name: "No thanks" }).click(); + }); + + await page.removeLocatorHandler(locator); + console.log("Locator handler removed — overlay will not be dismissed now."); + + await page.goto("https://example.com"); + await page.setContent(overlayHTML); + + await page.waitForTimeout(5000); + + await page.getByRole("button", { name: "Start here" }).click(); + + const isOverlayVisible = await page + .getByText("Sign up to the newsletter") + .isVisible() + .catch(() => false); + + await stagehand.close(); + expect(isOverlayVisible).toBe(true); + }); +}); diff --git a/evals/deterministic/tests/addTags.test.ts b/evals/deterministic/tests/addTags.test.ts new file mode 100644 index 00000000..b8317ecf --- /dev/null +++ b/evals/deterministic/tests/addTags.test.ts @@ -0,0 +1,79 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - addScriptTag and addStyleTag", () => { + let stagehand: Stagehand; + + test.beforeAll(async () => { + stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + }); + + test.afterAll(async () => { + await stagehand.close(); + }); + + test("should inject a script tag and have access to the defined function", async () => { + const { page } = stagehand; + + await page.setContent(` + + +

Hello, world!

+ + + `); + + await page.addScriptTag({ + content: ` + window.sayHello = function() { + document.getElementById("greeting").textContent = "Hello from injected script!"; + } + `, + }); + + await page.evaluate(() => { + const w = window as typeof window & { + sayHello?: () => void; + }; + w.sayHello?.(); + }); + + const text = await page.locator("#greeting").textContent(); + expect(text).toBe("Hello from injected script!"); + }); + + test("should inject a style tag and apply styles", async () => { + const { page } = stagehand; + + await page.setContent(` + + +
Some text
+ + + `); + + await page.addStyleTag({ + content: ` + #styledDiv { + color: red; + font-weight: bold; + } + `, + }); + + const color = await page.evaluate(() => { + const el = document.getElementById("styledDiv"); + return window.getComputedStyle(el!).color; + }); + expect(color).toBe("rgb(255, 0, 0)"); + + const fontWeight = await page.evaluate(() => { + const el = document.getElementById("styledDiv"); + return window.getComputedStyle(el!).fontWeight; + }); + expect(["bold", "700"]).toContain(fontWeight); + }); +}); diff --git a/evals/deterministic/tests/bringToFront.test.ts b/evals/deterministic/tests/bringToFront.test.ts new file mode 100644 index 00000000..2c4c51ea --- /dev/null +++ b/evals/deterministic/tests/bringToFront.test.ts @@ -0,0 +1,37 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - bringToFront", () => { + test("should bring a background page to the front and allow further actions", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const { page: page1 } = stagehand; + + const page2 = await stagehand.context.newPage(); + await page2.goto("https://example.com"); + const page2Title = await page2.title(); + console.log("Page2 Title:", page2Title); + + await page1.goto("https://www.google.com"); + const page1TitleBefore = await page1.title(); + console.log("Page1 Title before:", page1TitleBefore); + + await page1.bringToFront(); + + await page1.goto("https://www.browserbase.com"); + const page1TitleAfter = await page1.title(); + console.log("Page1 Title after:", page1TitleAfter); + + await page2.bringToFront(); + const page2URLBefore = page2.url(); + console.log("Page2 URL before navigation:", page2URLBefore); + + await stagehand.close(); + + expect(page1TitleBefore).toContain("Google"); + expect(page1TitleAfter).toContain("Browserbase"); + expect(page2Title).toContain("Example Domain"); + }); +}); diff --git a/evals/deterministic/tests/content.test.ts b/evals/deterministic/tests/content.test.ts new file mode 100644 index 00000000..53318903 --- /dev/null +++ b/evals/deterministic/tests/content.test.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - content", () => { + test("should retrieve the full HTML content of the page", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://example.com"); + const html = await page.content(); + expect(html).toContain("Example Domain"); + expect(html).toContain("

Example Domain

"); + + await stagehand.close(); + }); +}); diff --git a/evals/deterministic/tests/evaluate.test.ts b/evals/deterministic/tests/evaluate.test.ts new file mode 100644 index 00000000..213edf48 --- /dev/null +++ b/evals/deterministic/tests/evaluate.test.ts @@ -0,0 +1,31 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - JavaScript Evaluation", () => { + test("can evaluate JavaScript in the page context", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + + await page.goto("https://example.com"); + + const sum = await page.evaluate(() => 2 + 2); + expect(sum).toBe(4); + + const pageTitle = await page.evaluate(() => document.title); + expect(pageTitle).toMatch(/example/i); + + const obj = await page.evaluate(() => { + return { + message: "Hello from the browser", + userAgent: navigator.userAgent, + }; + }); + expect(obj).toHaveProperty("message", "Hello from the browser"); + expect(obj.userAgent).toBeDefined(); + + await stagehand.close(); + }); +}); diff --git a/evals/deterministic/tests/expose.test.ts b/evals/deterministic/tests/expose.test.ts new file mode 100644 index 00000000..44af4831 --- /dev/null +++ b/evals/deterministic/tests/expose.test.ts @@ -0,0 +1,63 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - evaluateHandle, exposeBinding, exposeFunction", () => { + let stagehand: Stagehand; + + test.beforeAll(async () => { + stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + }); + + test.afterAll(async () => { + await stagehand.close(); + }); + + test("demonstrates evaluateHandle, exposeBinding, and exposeFunction", async () => { + const { page } = stagehand; + + await page.setContent(` + + +
Initial Text
+ + + `); + + const divHandle = await page.evaluateHandle(() => { + return document.getElementById("myDiv"); + }); + await divHandle.evaluate((div, newText) => { + div.textContent = newText; + }, "Text updated via evaluateHandle"); + + const text = await page.locator("#myDiv").textContent(); + expect(text).toBe("Text updated via evaluateHandle"); + + await page.exposeBinding("myBinding", async (source, arg: string) => { + console.log("myBinding called from page with arg:", arg); + return `Node responded with: I got your message: "${arg}"`; + }); + + const responseFromBinding = await page.evaluate(async () => { + const w = window as typeof window & { + myBinding?: (arg: string) => Promise; + }; + return w.myBinding?.("Hello from the browser"); + }); + expect(responseFromBinding).toMatch(/I got your message/); + + await page.exposeFunction("addNumbers", (a: number, b: number) => { + return a + b; + }); + + const sum = await page.evaluate(async () => { + const w = window as typeof window & { + addNumbers?: (a: number, b: number) => number; + }; + return w.addNumbers?.(3, 7); + }); + expect(sum).toBe(10); + }); +}); diff --git a/evals/deterministic/tests/frames.test.ts b/evals/deterministic/tests/frames.test.ts new file mode 100644 index 00000000..8fb8618b --- /dev/null +++ b/evals/deterministic/tests/frames.test.ts @@ -0,0 +1,66 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - frame operations", () => { + let stagehand: Stagehand; + + test.beforeAll(async () => { + stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + }); + + test.afterAll(async () => { + await stagehand.close(); + }); + + test("should use page.mainFrame(), page.frames(), page.frame(), and page.frameLocator()", async () => { + const { page } = stagehand; + + await page.setContent(` + + + + + + + + `); + + await page.waitForSelector('iframe[name="frame-one"]'); + await page.waitForSelector('iframe[name="frame-two"]'); + + const frames = page.frames(); + console.log( + "All frames found:", + frames.map((f) => f.name()), + ); + expect(frames).toHaveLength(3); + + const mainFrame = page.mainFrame(); + console.log("Main frame name:", mainFrame.name()); + expect(mainFrame.name()).toBe(""); + + const frameOne = page.frame({ name: "frame-one" }); + expect(frameOne).not.toBeNull(); + + const frameOneText = await frameOne?.locator("h1").textContent(); + expect(frameOneText).toBe("Hello from Frame 1"); + + const frameTwoLocator = page.frameLocator("iframe[name='frame-two']"); + const frameTwoText = await frameTwoLocator.locator("h1").textContent(); + expect(frameTwoText).toBe("Hello from Frame 2"); + + const frameTwo = page.frame({ name: "frame-two" }); + expect(frameTwo).not.toBeNull(); + + const frameTwoTextAgain = await frameTwo?.locator("h1").textContent(); + expect(frameTwoTextAgain).toBe("Hello from Frame 2"); + }); +}); diff --git a/evals/deterministic/tests/getBy.test.ts b/evals/deterministic/tests/getBy.test.ts new file mode 100644 index 00000000..3a66d38d --- /dev/null +++ b/evals/deterministic/tests/getBy.test.ts @@ -0,0 +1,50 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - Built-in locators", () => { + let stagehand: Stagehand; + + test.beforeAll(async () => { + stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + }); + + test.afterAll(async () => { + await stagehand.close(); + }); + + test("demonstrates getByAltText, getByLabel, getByPlaceholder, getByRole, getByTestId, getByText, getByTitle", async () => { + const { page } = stagehand; + await page.setContent(` + + + Profile picture + + + + +
Hello World!
+

This is some descriptive text on the page.

+

Site Title

+ + + `); + const image = page.getByAltText("Profile picture"); + await expect(image).toBeVisible(); + const usernameInput = page.getByLabel("Username"); + await expect(usernameInput).toBeVisible(); + const emailInput = page.getByPlaceholder("Enter your email"); + await expect(emailInput).toBeVisible(); + const signInButton = page.getByRole("button", { name: "Sign in" }); + await expect(signInButton).toBeVisible(); + const greetingDiv = page.getByTestId("greeting"); + await expect(greetingDiv).toHaveText("Hello World!"); + const descriptiveText = page.getByText( + "This is some descriptive text on the page.", + ); + await expect(descriptiveText).toBeVisible(); + const heading = page.getByTitle("A heading for the page"); + await expect(heading).toHaveText("Site Title"); + }); +}); diff --git a/evals/deterministic/tests/navigation.test.ts b/evals/deterministic/tests/navigation.test.ts new file mode 100644 index 00000000..0583ca9d --- /dev/null +++ b/evals/deterministic/tests/navigation.test.ts @@ -0,0 +1,26 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - Navigation", () => { + test("should navigate back and forward between pages", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + + await page.goto("https://example.com"); + expect(page.url()).toBe("https://example.com/"); + + await page.goto("https://www.browserbase.com/"); + expect(page.url()).toBe("https://www.browserbase.com/"); + + await page.goBack(); + expect(page.url()).toBe("https://example.com/"); + + await page.goForward(); + expect(page.url()).toBe("https://www.browserbase.com/"); + + await stagehand.close(); + }); +}); diff --git a/evals/deterministic/tests/pageContext.test.ts b/evals/deterministic/tests/pageContext.test.ts new file mode 100644 index 00000000..c966a070 --- /dev/null +++ b/evals/deterministic/tests/pageContext.test.ts @@ -0,0 +1,59 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - page.context()", () => { + let stagehand: Stagehand; + + test.beforeEach(async () => { + stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + }); + + test.afterEach(async () => { + if (stagehand) { + try { + await stagehand.close(); + } catch (error) { + console.error("[afterEach] Error during stagehand.close():", error); + } + } else { + console.log("[afterEach] Stagehand was not defined, skipping close()."); + } + }); + + test("should confirm page.context() and stagehand.context share state", async () => { + const page = stagehand.page; + const stagehandContext = stagehand.context; + const pageContext = page.context(); + + await pageContext.addCookies([ + { + name: "stagehandTestCookie", + value: "hello-stagehand", + domain: "example.com", + path: "/", + expires: Math.floor(Date.now() / 1000) + 3600, // 1 hour + httpOnly: false, + secure: false, + sameSite: "Lax", + }, + ]); + + const cookies = await stagehandContext.cookies("https://example.com"); + + const testCookie = cookies.find((c) => c.name === "stagehandTestCookie"); + expect(testCookie).toBeDefined(); + expect(testCookie?.value).toBe("hello-stagehand"); + + const extraPage = await pageContext.newPage(); + await extraPage.goto("https://example.com"); + const contextPages = stagehandContext.pages(); + + // The newly created page should be recognized by stagehandContext as well. + const foundExtraPage = contextPages.find( + (p) => p.url() === "https://example.com/", + ); + expect(foundExtraPage).toBeDefined(); + }); +}); diff --git a/evals/deterministic/tests/reload.test.ts b/evals/deterministic/tests/reload.test.ts new file mode 100644 index 00000000..6d728afe --- /dev/null +++ b/evals/deterministic/tests/reload.test.ts @@ -0,0 +1,40 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - Reload", () => { + test("should reload the page and reset page state", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://www.browserbase.com/"); + + await page.evaluate(() => { + const w = window as typeof window & { + __testReloadMarker?: string; + }; + w.__testReloadMarker = "Hello Reload!"; + }); + + const markerBeforeReload = await page.evaluate(() => { + const w = window as typeof window & { + __testReloadMarker?: string; + }; + return w.__testReloadMarker; + }); + expect(markerBeforeReload).toBe("Hello Reload!"); + + await page.reload(); + + const markerAfterReload = await page.evaluate(() => { + const w = window as typeof window & { + __testReloadMarker?: string; + }; + return w.__testReloadMarker; + }); + expect(markerAfterReload).toBeUndefined(); + + await stagehand.close(); + }); +}); diff --git a/evals/deterministic/tests/waitFor.test.ts b/evals/deterministic/tests/waitFor.test.ts new file mode 100644 index 00000000..8e5020b3 --- /dev/null +++ b/evals/deterministic/tests/waitFor.test.ts @@ -0,0 +1,165 @@ +import { test, expect } from "@playwright/test"; +import { Stagehand } from "../../../lib"; +import StagehandConfig from "../stagehand.config"; + +test.describe("StagehandPage - waitFor", () => { + test("should wait for an element to become visible", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://docs.browserbase.com/introduction"); + const dynamicElement = page.locator( + "div.grid:nth-child(1) > a:nth-child(1) > div:nth-child(1)", + ); + + const isVisibleBefore = await dynamicElement.isVisible(); + expect(isVisibleBefore).toBe(false); + + const clickableElement = page.locator( + "div.mt-12:nth-child(3) > ul:nth-child(2) > li:nth-child(2) > div:nth-child(1)", + ); + await clickableElement.click(); + + await dynamicElement.waitFor({ state: "visible" }); + + const isVisibleAfter = await dynamicElement.isVisible(); + expect(isVisibleAfter).toBe(true); + + await stagehand.close(); + }); + + test("should wait for an element to be detached", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://docs.browserbase.com/introduction"); + + const disappearingElement = page.locator( + "div.not-prose:nth-child(2) > a:nth-child(1) > div:nth-child(1)", + ); + + await disappearingElement.click(); + await disappearingElement.waitFor({ state: "detached" }); + + const isAttachedAfter = await disappearingElement.isVisible(); + expect(isAttachedAfter).toBe(false); + + await stagehand.close(); + }); + + test("should wait for a specific event (waitForEvent)", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://docs.browserbase.com/introduction"); + + const consolePromise = page.waitForEvent("console"); + await page.evaluate(() => { + console.log("Hello from the browser console!"); + }); + const consoleMessage = await consolePromise; + expect(consoleMessage.text()).toBe("Hello from the browser console!"); + + await stagehand.close(); + }); + + test("should wait for a function to return true (waitForFunction)", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://docs.browserbase.com/introduction"); + + await page.evaluate(() => { + setTimeout(() => { + const w = window as typeof window & { + __stagehandFlag?: boolean; + }; + w.__stagehandFlag = true; + }, 1000); + }); + + await page.waitForFunction(() => { + const w = window as typeof window & { + __stagehandFlag?: boolean; + }; + return w.__stagehandFlag === true; + }); + + const value = await page.evaluate(() => { + const w = window as typeof window & { + __stagehandFlag?: boolean; + }; + return w.__stagehandFlag; + }); + expect(value).toBe(true); + + await stagehand.close(); + }); + + test("should wait for the load state (waitForLoadState)", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://docs.browserbase.com/introduction"); + await page.waitForLoadState("networkidle"); + const heroTitle = page.locator("h1"); + await expect(heroTitle).toHaveText(/Documentation/i); + + await stagehand.close(); + }); + + test("should wait for a specific request (waitForRequest)", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + const requestPromise = page.waitForRequest((req) => + req.url().includes("mintlify"), + ); + + await page.goto("https://docs.browserbase.com/introduction"); + const matchingRequest = await requestPromise; + expect(matchingRequest.url()).toContain("mintlify"); + + await stagehand.close(); + }); + + test("should wait for a specific response (waitForResponse)", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + const responsePromise = page.waitForResponse( + (res) => res.url().includes("introduction") && res.status() === 200, + ); + + await page.goto("https://docs.browserbase.com/introduction"); + const matchingResponse = await responsePromise; + expect(await matchingResponse.text()).toContain("Browserbase"); + + await stagehand.close(); + }); + + test("should wait for a URL (waitForURL)", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto("https://docs.browserbase.com"); + + const quickstartLink = page.locator( + "div.mt-12:nth-child(3) > ul:nth-child(2) > li:nth-child(2) > div:nth-child(1) > div:nth-child(1)", + ); + await quickstartLink.click(); + + await page.waitForURL(/.*quickstart.*/); + expect(page.url()).toContain("/quickstart"); + + await stagehand.close(); + }); +});