#2486·stagehand

v4: locator.click() returns without committing a selectable row

Author: shrey150Created Jul 28, 2026Updated Sep 14, 2026
Labelsbugv4-pre-release

Before submitting an issue, please:

  • Checked the documentation for relevant information
  • Searched existing issues to avoid duplicates

Environment Information

Stagehand:

  • Language/SDK: TypeScript
  • Stagehand version: v4 pre-release

AI Provider:

  • Provider: N/A
  • Model: N/A

This uses the deterministic locator API; no model or agent call is involved.

Issue Description

On a private React application, an option row in a side panel is rendered as an <a role="button">. The row is visible and a v4 locator matches exactly one element.

await option.click() resolves successfully, but the application does not commit the selection: the side panel remains open and the step counter is unchanged. A Playwright locator click on the same element immediately closes the panel and increments the local step counter.

I repeated this on three fresh instances of the flow. The v4 click was a no-op each time, while the Playwright control committed the local selection each time. There is no thrown error or timeout from locator.click().

Steps to Reproduce

  1. Open a React side panel containing a selectable <a role="button">Option A</a> row.
  2. Resolve the row with a v4 locator and confirm count() === 1 and isVisible() === true.
  3. Call await option.click().
  4. Observe that the panel remains open and the application state is unchanged.
  5. Reset the UI and click the same row with a Playwright locator.
  6. Observe that the panel closes and the application state changes.

Minimal Reproduction Code

The labels and selectors below are synthetic because the original application is private.

typescript
// Stagehand imported from the v4 pre-release TypeScript SDK.

const stagehand = new Stagehand({
  browser: {
    type: "cdp",
    cdpUrl: process.env.CDP_URL!,
  },
});

await stagehand.init();
const page = await stagehand.context.activePage();
if (!page) throw new Error("No active page");

const option = page.locator(
  "xpath=//*[@role='button'][normalize-space(.)='Option A']",
);

console.log({
  count: await option.count(),       // 1
  visible: await option.isVisible(), // true
});

await option.click(); // resolves, but the application does not change

Control against the same open page and element:

typescript
import { chromium } from "playwright";

const browser = await chromium.connectOverCDP(process.env.CDP_URL!);
const page = browser.contexts()[0].pages()[0];

await page.getByRole("button", { name: "Option A" }).click();
// The panel closes and the application state changes.

Potential Implementation Lead

This may be related to the v4 Understudy locator click implementation, which queues mouseMoved, mousePressed, and mouseReleased requests together and then awaits them with Promise.all:

https://github.com/browserbase/stagehand/blob/v4-spike/packages/server/understudy/locator.ts#L325-L357

That is only a hypothesis, but it seems worth testing whether the pipelined burst can race a component's hover/pointer state. The Playwright control serializes the interaction and succeeds.

Error Messages / Log trace

No error. locator.click() resolves normally.

Screenshots / Videos

Not attached because the source application contains private data.

Related Issues

  • Related to #795
  • The fix in #819 used a Playwright click with a fallback for the older path.
  • This report is specifically for the v4 deterministic locator path.