From 5533ab92581fb7b8dfb96dab8199668df7ae676b Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Wed, 4 Feb 2026 12:26:42 -0800 Subject: [PATCH 1/5] give locator.fill access to globals --- .changeset/yellow-mails-deny.md | 5 +++++ packages/core/lib/v3/understudy/locator.ts | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .changeset/yellow-mails-deny.md 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/understudy/locator.ts b/packages/core/lib/v3/understudy/locator.ts index b5c32cb29..8bd2d151f 100644 --- a/packages/core/lib/v3/understudy/locator.ts +++ b/packages/core/lib/v3/understudy/locator.ts @@ -3,7 +3,11 @@ 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 { @@ -510,6 +514,8 @@ export class Locator { */ async fill(value: string): Promise { const session = this.frame.session; + // Use the bundled locator globals; the raw fill snippet depends on helper symbols. + const fillDeclaration = `function(value) { ${locatorScriptBootstrap}; return ${locatorScriptGlobalRefs.fillElementValue}.call(this, value); }`; const { objectId } = await this.resolveNode(); let releaseNeeded = true; @@ -519,7 +525,7 @@ export class Locator { "Runtime.callFunctionOn", { objectId, - functionDeclaration: locatorScriptSources.fillElementValue, + functionDeclaration: fillDeclaration, arguments: [{ value }], returnByValue: true, }, From 627bcd7fa2946770af6bec641dcdfa0bdac93d3e Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Wed, 4 Feb 2026 12:26:48 -0800 Subject: [PATCH 2/5] add test --- .../core/lib/v3/tests/locator-fill.spec.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/core/lib/v3/tests/locator-fill.spec.ts 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..f41f81564 --- /dev/null +++ b/packages/core/lib/v3/tests/locator-fill.spec.ts @@ -0,0 +1,43 @@ +import { expect, test } from "@playwright/test"; +import { V3 } from "../v3"; +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("#date"); + await dateInput.fill("2026-01-01"); + + const value = await dateInput.inputValue(); + expect(value).toBe("2026-01-01"); + }); +}); From 06d2802d9a4ed5548e65d4677ce6f69b535d5122 Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Wed, 4 Feb 2026 14:05:21 -0800 Subject: [PATCH 3/5] throw StagehandLocatorError on runtime exception --- packages/core/lib/v3/types/public/sdkErrors.ts | 8 ++++++++ packages/core/lib/v3/understudy/locator.ts | 9 +++++++++ 2 files changed, 17 insertions(+) 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 8bd2d151f..16c317b26 100644 --- a/packages/core/lib/v3/understudy/locator.ts +++ b/packages/core/lib/v3/understudy/locator.ts @@ -13,6 +13,7 @@ import { FrameSelectorResolver, type SelectorQuery } from "./selectorResolver"; import { StagehandElementNotFoundError, StagehandInvalidArgumentError, + StagehandLocatorError, ElementNotVisibleError, } from "../types/public/sdkErrors"; import { normalizeInputFiles } from "./fileUploadUtils"; @@ -530,6 +531,14 @@ export class Locator { returnByValue: true, }, ); + if (res.exceptionDetails) { + // prefer exception.description over text (eg "Uncaught") + const message = + res.exceptionDetails.exception?.description ?? + res.exceptionDetails.text ?? + "Unknown exception during locator().fill()"; + throw new StagehandLocatorError("Filling", this.selector, message); + } const result = res.result.value as | { status?: string; reason?: string; value?: string } From 7f278ad723260b379135b4d75b6b96b38961e29a Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Wed, 4 Feb 2026 14:05:33 -0800 Subject: [PATCH 4/5] add locator error tests --- .../core/lib/v3/tests/locator-fill.spec.ts | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/core/lib/v3/tests/locator-fill.spec.ts b/packages/core/lib/v3/tests/locator-fill.spec.ts index f41f81564..8f19db7e6 100644 --- a/packages/core/lib/v3/tests/locator-fill.spec.ts +++ b/packages/core/lib/v3/tests/locator-fill.spec.ts @@ -1,5 +1,6 @@ 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()", () => { @@ -34,10 +35,113 @@ test.describe("Locator.fill()", () => { ), ); - const dateInput = page.mainFrame().locator("#date"); + 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"); + } + }); }); From 218a4ec72c2144793c3a7adc7fd65f7897b9afb4 Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Wed, 4 Feb 2026 14:33:59 -0800 Subject: [PATCH 5/5] update api export test --- packages/core/tests/public-api/public-error-types.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/tests/public-api/public-error-types.test.ts b/packages/core/tests/public-api/public-error-types.test.ts index 2c110b2d2..5b2849921 100644 --- a/packages/core/tests/public-api/public-error-types.test.ts +++ b/packages/core/tests/public-api/public-error-types.test.ts @@ -37,6 +37,7 @@ export const publicErrorTypes = { StagehandIframeError: Stagehand.StagehandIframeError, StagehandInitError: Stagehand.StagehandInitError, StagehandInvalidArgumentError: Stagehand.StagehandInvalidArgumentError, + StagehandLocatorError: Stagehand.StagehandLocatorError, StagehandMissingArgumentError: Stagehand.StagehandMissingArgumentError, StagehandNotInitializedError: Stagehand.StagehandNotInitializedError, StagehandResponseBodyError: Stagehand.StagehandResponseBodyError,