diff --git a/.changeset/yellow-mails-deny.md b/.changeset/yellow-mails-deny.md new file mode 100644 index 000000000..66b03d1c5 --- /dev/null +++ b/.changeset/yellow-mails-deny.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +fix issue where locator.fill() was not working on elements that require direct value setting diff --git a/packages/core/lib/v3/tests/locator-fill.spec.ts b/packages/core/lib/v3/tests/locator-fill.spec.ts new file mode 100644 index 000000000..8f19db7e6 --- /dev/null +++ b/packages/core/lib/v3/tests/locator-fill.spec.ts @@ -0,0 +1,147 @@ +import { expect, test } from "@playwright/test"; +import { V3 } from "../v3"; +import { StagehandLocatorError } from "../types/public/sdkErrors"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Locator.fill()", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch((e) => { + void e; + }); + }); + + test("fills date inputs via value setter even when beforeinput blocks insertText", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + `
+ + + `, + ), + ); + + const dateInput = page.mainFrame().locator("xpath=/html/body/input"); + await dateInput.fill("2026-01-01"); + + const value = await dateInput.inputValue(); + expect(value).toBe("2026-01-01"); + }); + + test("xpath case: throws StagehandLocatorError when fill encounters an exception", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + await page.waitForSelector("xpath=/html/body/input"); + + await page.evaluate(() => { + const input = document.querySelector("input"); + Object.defineProperty(input, "isConnected", { + get() { + throw new Error("boom"); + }, + }); + }); + + const dateInput = page.mainFrame().locator("xpath=/html/body/input"); + let error: unknown; + try { + await dateInput.fill("2026-01-01"); + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(StagehandLocatorError); + if (error instanceof Error) { + // Log the message so it's visible in test output. + expect(error.message).toContain("Error Filling Element"); + expect(error.message).toContain("selector: xpath=/html/body/input"); + expect(error.message).toContain("boom"); + } + }); + + test("css selector case: throws StagehandLocatorError when fill encounters an exception", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + await page.waitForSelector("#date"); + + // Override in main world + await page.evaluate(() => { + const input = document.querySelector("input"); + Object.defineProperty(input, "isConnected", { + get() { + throw new Error("boom"); + }, + configurable: true, + }); + }); + + // Also override in the isolated world that CSS selectors use + const frameId = page.mainFrameId(); + const { executionContextId } = await page.sendCDP<{ + executionContextId: number; + }>("Page.createIsolatedWorld", { + frameId, + worldName: "v3-world", + }); + + await page.sendCDP("Runtime.evaluate", { + expression: `(() => { + const input = document.querySelector('input'); + if (input) { + Object.defineProperty(input, 'isConnected', { + get() { throw new Error("boom"); }, + configurable: true + }); + } + })()`, + contextId: executionContextId, + }); + + const dateInput = page.mainFrame().locator("#date"); + let error: unknown; + try { + await dateInput.fill("2026-01-01"); + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(StagehandLocatorError); + if (error instanceof Error) { + expect(error.message).toContain("Error Filling Element"); + expect(error.message).toContain("selector: #date"); + expect(error.message).toContain("boom"); + } + }); +}); diff --git a/packages/core/lib/v3/types/public/sdkErrors.ts b/packages/core/lib/v3/types/public/sdkErrors.ts index ca8443c26..b41653c59 100644 --- a/packages/core/lib/v3/types/public/sdkErrors.ts +++ b/packages/core/lib/v3/types/public/sdkErrors.ts @@ -161,6 +161,14 @@ export class StagehandDomProcessError extends StagehandError { } } +export class StagehandLocatorError extends StagehandError { + constructor(action: string, selector: string, message: string) { + super( + `Error ${action} Element with selector: ${selector} Reason: ${message}`, + ); + } +} + export class StagehandClickError extends StagehandError { constructor(message: string, selector: string) { super( diff --git a/packages/core/lib/v3/understudy/locator.ts b/packages/core/lib/v3/understudy/locator.ts index b5c32cb29..16c317b26 100644 --- a/packages/core/lib/v3/understudy/locator.ts +++ b/packages/core/lib/v3/understudy/locator.ts @@ -3,12 +3,17 @@ import { Protocol } from "devtools-protocol"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { locatorScriptSources } from "../dom/build/locatorScripts.generated"; +import { + locatorScriptBootstrap, + locatorScriptGlobalRefs, + locatorScriptSources, +} from "../dom/build/locatorScripts.generated"; import type { Frame } from "./frame"; import { FrameSelectorResolver, type SelectorQuery } from "./selectorResolver"; import { StagehandElementNotFoundError, StagehandInvalidArgumentError, + StagehandLocatorError, ElementNotVisibleError, } from "../types/public/sdkErrors"; import { normalizeInputFiles } from "./fileUploadUtils"; @@ -510,6 +515,8 @@ export class Locator { */ async fill(value: string): Promise