Skip to content

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

Description

@shrey150

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.

// 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:

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions