Skip to content

STG-2196 Shadow DOM fix - #2220

Merged
monadoid merged 3 commits into
mainfrom
STG-2196
Jun 10, 2026
Merged

monadoid merged 3 commits into
mainfrom
STG-2196

Conversation

@monadoid

@monadoid monadoid commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

why

Before this PR, if we passed a Stagehand selector like /html[1]/body[1]/shadow-host[1]//div[1]/div[1]/select[1], it could return no match when the shadow host also had a regular DOM <div>. In that case, countXPathMatches returned 0 and resolveXPathAtIndex returned null, even though the target <select> existed inside the host's shadow root.

This matters because Stagehand-generated selectors use non-leading // to represent entering a shadow root. When resolution fails there, deterministic actions like selectOptionFromDropdown can fail with element-not-found.

what changed

Added a regression test for that selector shape. Then changed the XPath resolver so it keeps the existing descendant-axis behavior when that resolves, but if that path finds nothing, it resolves the non-leading // as a Stagehand shadow-root hop. With the same selector now, countXPathMatches returns 1 and resolveXPathAtIndex returns #target.

test plan

  • pnpm --filter @browserbasehq/stagehand run lint
  • pnpm --filter @browserbasehq/stagehand run build-dom-scripts && pnpm --filter @browserbasehq/stagehand run build:esm
  • pnpm --filter @browserbasehq/stagehand exec vitest run --config /Users/samfinton/code/browserbase/stagehand/packages/core/vitest.esm.config.mjs /Users/samfinton/code/browserbase/stagehand/packages/core/dist/esm/tests/unit/xpath-resolver.test.js

@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e65b851

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@browserbasehq/stagehand Patch
@browserbasehq/stagehand-evals Patch
@browserbasehq/stagehand-server-v3 Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@monadoid
monadoid marked this pull request as ready for review June 10, 2026 16:35

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Confidence score: 3/5

  • In packages/core/lib/v3/dom/locatorScripts/xpathResolver.ts, resolveXPathAtIndex appears to treat out-of-range indices as a shadow-hop fallback path, which can return behavior that disagrees with countXPathMatches; merging as-is risks inconsistent selector results and hard-to-debug locator failures in consumers—align out-of-range handling between these two functions (and add/adjust tests for index bounds) before merging.
Architecture diagram
sequenceDiagram
    participant Caller as Test / Stagehand
    participant Resolver as XPathResolver (countXPathMatches / resolveXPathAtIndex)
    participant Composed as resolveXPathComposedMatches
    participant ShadowHop as resolveStagehandShadowHopMatches (NEW)
    participant Parser as parseXPathSteps
    participant DOM as Browser DOM

    Note over Caller,Resolver: Entry
    Caller->>Resolver: countXPathMatches(selector) or resolveXPathAtIndex(selector, index)

    Resolver->>Composed: resolveXPathComposedMatches(xp, shadowCtx)
    Composed->>DOM: traverse composed tree (descendant axis)
    DOM-->>Composed: Element[]
    Composed-->>Resolver: composedMatches

    alt composedMatches has results AND index within range
        Resolver-->>Caller: return count / Element
    else composedMatches empty or index out of range (NEW fallback)
        Resolver->>ShadowHop: resolveStagehandShadowHopMatches(xp, shadowCtx)
        Note over ShadowHop: Only triggered when composed traversal found nothing

        ShadowHop->>Parser: parseXPathSteps(xp)
        Parser-->>ShadowHop: steps[]

        loop for each step i
            alt axis = "child"
                ShadowHop->>DOM: domChildren(currentRoot) (NEW)
                Note over DOM: Returns only direct DOM children (no shadow root)
                DOM-->>ShadowHop: child elements
            else axis = "desc" and i > 0 (shadow‑hop)
                ShadowHop->>DOM: shadowRootChildren(currentRoot, getClosedRoot) (NEW)
                Note over DOM: Returns only shadow root children (open/closed)
                DOM-->>ShadowHop: shadow root elements
            else i = 0 (initial descendant)
                ShadowHop->>DOM: composedDescendants(document)
                DOM-->>ShadowHop: initial elements
            end
        end

        alt shadowHopElements[targetIndex] exists
            ShadowHop-->>Resolver: matched Element
        else
            ShadowHop-->>Resolver: null / empty
        end

        alt resolved via shadow‑hop
            Resolver-->>Caller: return count / Element
        else still no match
            Resolver-->>Caller: return 0 / null
        end
    end
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/core/lib/v3/dom/locatorScripts/xpathResolver.ts Outdated
@monadoid monadoid changed the title STG-2196 STG-2196 Shadow DOM fix Jun 10, 2026
@monadoid
monadoid merged commit a64c6b7 into main Jun 10, 2026
222 of 223 checks passed
seanmcguire12 pushed a commit that referenced this pull request Jun 19, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @browserbasehq/stagehand@3.6.0

### Minor Changes

- [#2178](#2178)
[`c49a3fc`](c49a3fc)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support
for WebMCP

### Patch Changes

- [#2217](#2217)
[`147e310`](147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- [#2231](#2231)
[`cf3603d`](cf3603d)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add
claude-fable-5 support: native structured outputs via the
@ai-sdk/anthropic bump, adaptive thinking (including the new "xhigh"
effort) on the agent path, the API's built-in server-side refusal
fallback to claude-opus-4-8, and auto tool choice for the final done
call on models that reject forced tool use.

- [#2233](#2233)
[`8d7d414`](8d7d414)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Normalize
URLs in `ActCache` key derivation by sorting query parameters before
hashing. Semantically equivalent URLs that differ only in parameter
order (e.g. `?utm_source=email&id=42` vs `?id=42&utm_source=email`) now
hit the cache instead of silently missing. Fragments and duplicate keys
are preserved.

- [#2229](#2229)
[`fd42e65`](fd42e65)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - launch
local browser with --enable-features=WebMCPTesting,DevToolsWebMCPSupport
by default

- [#2220](#2220)
[`a64c6b7`](a64c6b7)
Thanks [@monadoid](https://github.com/monadoid)! - Fix
Stagehand-generated shadow-root XPath resolution so deterministic
actions can target elements inside web components.

- [#2132](#2132)
[`ed3e566`](ed3e566)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add canonical
verifier evidence normalization for screenshots and text signals without
requiring image dependencies in core installs.

- [#2133](#2133)
[`840aac8`](840aac8)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add the
rubric-based verifier engine with normalized public rubric output and
bounded failure-step parsing.

## @browserbasehq/stagehand-evals@2.0.3

### Patch Changes

- Updated dependencies
\[[`147e310`](147e310),
[`cf3603d`](cf3603d),
[`8d7d414`](8d7d414),
[`fd42e65`](fd42e65),
[`a64c6b7`](a64c6b7),
[`c49a3fc`](c49a3fc),
[`ed3e566`](ed3e566),
[`840aac8`](840aac8)]:
    -   @browserbasehq/stagehand@3.6.0

## @browserbasehq/stagehand-server-v3@3.7.1

### Patch Changes

- [#2217](#2217)
[`147e310`](147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- Updated dependencies
\[[`147e310`](147e310),
[`cf3603d`](cf3603d),
[`8d7d414`](8d7d414),
[`fd42e65`](fd42e65),
[`a64c6b7`](a64c6b7),
[`c49a3fc`](c49a3fc),
[`ed3e566`](ed3e566),
[`840aac8`](840aac8)]:
    -   @browserbasehq/stagehand@3.6.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
# why

Before this PR, if we passed a Stagehand selector like
`/html[1]/body[1]/shadow-host[1]//div[1]/div[1]/select[1]`, it could
return no match when the shadow host also had a regular DOM `<div>`. In
that case, `countXPathMatches` returned `0` and `resolveXPathAtIndex`
returned `null`, even though the target `<select>` existed inside the
host's shadow root.

This matters because Stagehand-generated selectors use non-leading `//`
to represent entering a shadow root. When resolution fails there,
deterministic actions like `selectOptionFromDropdown` can fail with
element-not-found.

# what changed

Added a regression test for that selector shape. Then changed the XPath
resolver so it keeps the existing descendant-axis behavior when that
resolves, but if that path finds nothing, it resolves the non-leading
`//` as a Stagehand shadow-root hop. With the same selector now,
`countXPathMatches` returns `1` and `resolveXPathAtIndex` returns
`#target`.

# test plan

- `pnpm --filter @browserbasehq/stagehand run lint`
- `pnpm --filter @browserbasehq/stagehand run build-dom-scripts && pnpm
--filter @browserbasehq/stagehand run build:esm`
- `pnpm --filter @browserbasehq/stagehand exec vitest run --config
/Users/samfinton/code/browserbase/stagehand/packages/core/vitest.esm.config.mjs
/Users/samfinton/code/browserbase/stagehand/packages/core/dist/esm/tests/unit/xpath-resolver.test.js`
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @browserbasehq/stagehand@3.6.0

### Minor Changes

- [browserbase#2178](browserbase#2178)
[`c49a3fc`](browserbase@c49a3fc)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support
for WebMCP

### Patch Changes

- [browserbase#2217](browserbase#2217)
[`147e310`](browserbase@147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- [browserbase#2231](browserbase#2231)
[`cf3603d`](browserbase@cf3603d)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add
claude-fable-5 support: native structured outputs via the
@ai-sdk/anthropic bump, adaptive thinking (including the new "xhigh"
effort) on the agent path, the API's built-in server-side refusal
fallback to claude-opus-4-8, and auto tool choice for the final done
call on models that reject forced tool use.

- [browserbase#2233](browserbase#2233)
[`8d7d414`](browserbase@8d7d414)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Normalize
URLs in `ActCache` key derivation by sorting query parameters before
hashing. Semantically equivalent URLs that differ only in parameter
order (e.g. `?utm_source=email&id=42` vs `?id=42&utm_source=email`) now
hit the cache instead of silently missing. Fragments and duplicate keys
are preserved.

- [browserbase#2229](browserbase#2229)
[`fd42e65`](browserbase@fd42e65)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - launch
local browser with --enable-features=WebMCPTesting,DevToolsWebMCPSupport
by default

- [browserbase#2220](browserbase#2220)
[`a64c6b7`](browserbase@a64c6b7)
Thanks [@monadoid](https://github.com/monadoid)! - Fix
Stagehand-generated shadow-root XPath resolution so deterministic
actions can target elements inside web components.

- [browserbase#2132](browserbase#2132)
[`ed3e566`](browserbase@ed3e566)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add canonical
verifier evidence normalization for screenshots and text signals without
requiring image dependencies in core installs.

- [browserbase#2133](browserbase#2133)
[`840aac8`](browserbase@840aac8)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add the
rubric-based verifier engine with normalized public rubric output and
bounded failure-step parsing.

## @browserbasehq/stagehand-evals@2.0.3

### Patch Changes

- Updated dependencies
\[[`147e310`](browserbase@147e310),
[`cf3603d`](browserbase@cf3603d),
[`8d7d414`](browserbase@8d7d414),
[`fd42e65`](browserbase@fd42e65),
[`a64c6b7`](browserbase@a64c6b7),
[`c49a3fc`](browserbase@c49a3fc),
[`ed3e566`](browserbase@ed3e566),
[`840aac8`](browserbase@840aac8)]:
    -   @browserbasehq/stagehand@3.6.0

## @browserbasehq/stagehand-server-v3@3.7.1

### Patch Changes

- [browserbase#2217](browserbase#2217)
[`147e310`](browserbase@147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- Updated dependencies
\[[`147e310`](browserbase@147e310),
[`cf3603d`](browserbase@cf3603d),
[`8d7d414`](browserbase@8d7d414),
[`fd42e65`](browserbase@fd42e65),
[`a64c6b7`](browserbase@a64c6b7),
[`c49a3fc`](browserbase@c49a3fc),
[`ed3e566`](browserbase@ed3e566),
[`840aac8`](browserbase@840aac8)]:
    -   @browserbasehq/stagehand@3.6.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants