Skip to content

[fix]: extend support for xpaths containing predicates#1683

Open
seanmcguire12 wants to merge 3 commits intomainfrom
seanmcguire/stg-1373-add-shared-xpath-resolver-policy
Open

[fix]: extend support for xpaths containing predicates#1683
seanmcguire12 wants to merge 3 commits intomainfrom
seanmcguire/stg-1373-add-shared-xpath-resolver-policy

Conversation

@seanmcguire12
Copy link
Member

@seanmcguire12 seanmcguire12 commented Feb 13, 2026

why

  • to make sure .count()/.nth() include shadow DOM matches
  • make sure common predicates work in shadow DOM selectors (beyond just attribute predicates)
  • also to unify xpath resolution into one shared resolver, building off of this PR

what changed

  • added shared resolver policy in packages/core/lib/v3/dom/locatorScripts/xpathResolver.ts
  • expanded composed predicate parsing/evaluation in packages/core/lib/v3/dom/locatorScripts/xpathParser.ts (attr existence, contains/starts-with, text(), normalize-space, and/or/not)
  • routed xpath entrypoints through the resolver:
    • packages/core/lib/v3/dom/locatorScripts/selectors.ts (resolveXPathMainWorld)
    • packages/core/lib/v3/dom/locatorScripts/counts.ts (countXPathMatchesMainWorld)
    • packages/core/lib/v3/dom/locatorScripts/waitForSelector.ts (xpath path now uses resolver)
  • also removed unused injected helper resolveSimpleXPath from packages/core/lib/v3/dom/piercer.runtime.ts & updated packages/core/lib/v3/dom/global.d.ts

test plan

  • packages/core/tests/xpath-parser.test.ts (expanded predicate parsing + evaluation coverage)
  • packages/core/tests/xpath-resolver.test.ts (shadow-inclusive count/nth ordering across composed tree)

Summary by cubic

Extended XPath support to handle predicates and shadow DOM, and unified resolution behind a shared resolver. Fixes .count() and .nth() to include shadow DOM with consistent document-order DFS, aligning with Linear STG-1373.

  • New Features

    • Supports predicates: attribute exists/equals/contains/starts-with; text() and normalize-space; and/or/not; positional [n].
    • Shadow-piercing resolution for selectors, count, and waitForSelector with native evaluate fallback when possible and DFS ordering for indexing.
  • Refactors

    • Routed all XPath entry points (selectors, counts, waitForSelector) to the shared resolver; removed legacy traversal code.
    • Removed resolveSimpleXPath from the piercer backdoor and global types; expanded parser and resolver tests; added jsdom for DOM-based tests.

Written for commit 2aa61e5. Summary will update on new commits. Review in cubic

@changeset-bot
Copy link

changeset-bot bot commented Feb 13, 2026

🦋 Changeset detected

Latest commit: 2aa61e5

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

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 13, 2026

Greptile Overview

Greptile Summary

Unifies XPath resolution into a shared resolver module (xpathResolver.ts) that properly handles shadow DOM traversal for .count() and .nth() operations. Previously, XPath counting/indexing only searched the light DOM, causing shadow DOM elements to be missed.

Key improvements:

  • New resolveXPathComposedMatches function traverses the composed tree (light + shadow DOM) in document-order depth-first search
  • Expanded predicate parser to support @attr existence, contains(), starts-with(), text() equality/contains, normalize-space(), and boolean operators (and/or/not)
  • Removed ~200 lines of duplicated shadow DOM traversal code from piercer.runtime.ts and counts.ts
  • Fallback to native document.evaluate() when shadow DOM is not present for optimal performance
  • All XPath entrypoints (resolveXPathMainWorld, countXPathMatchesMainWorld, waitForSelector) now use the unified resolver

Testing:

  • Comprehensive predicate parsing tests covering new syntax
  • Shadow DOM traversal order tests validating composed tree enumeration

Confidence Score: 5/5

  • Safe to merge - well-structured refactoring with comprehensive test coverage
  • This is a solid refactoring that consolidates duplicate code into a shared module. The implementation correctly handles shadow DOM traversal, includes extensive test coverage for both parsing and resolution, and maintains backward compatibility by falling back to native XPath when shadow DOM is absent. No security issues or logical errors detected.
  • No files require special attention

Important Files Changed

Filename Overview
packages/core/lib/v3/dom/locatorScripts/xpathResolver.ts New shared XPath resolver with shadow DOM traversal. Implements composed tree matching via resolveXPathComposedMatches, with fallback to native XPath when shadow DOM is absent.
packages/core/lib/v3/dom/locatorScripts/xpathParser.ts Expanded predicate support for XPath expressions. Added attrExists, attrContains, attrStartsWith, text predicates, and boolean operators (and/or/not).
packages/core/lib/v3/dom/locatorScripts/selectors.ts Updated resolveXPathMainWorld to delegate to new unified resolver (resolveXPathAtIndex), removing duplicate traversal logic.
packages/core/lib/v3/dom/locatorScripts/counts.ts Refactored countXPathMatchesMainWorld to use shared countXPathMatches resolver, removing ~100 lines of duplicate shadow DOM traversal code.
packages/core/lib/v3/dom/locatorScripts/waitForSelector.ts Updated deepXPathQuery to use new resolveXPathFirst from unified resolver, aligning with refactored architecture.
packages/core/lib/v3/dom/piercer.runtime.ts Removed resolveSimpleXPath helper (moved to dedicated resolver module). Eliminates ~100 lines of code duplication.

Sequence Diagram

sequenceDiagram
    participant Client as Locator API
    participant Selector as selectors.ts
    participant Count as counts.ts
    participant Wait as waitForSelector.ts
    participant Resolver as xpathResolver.ts
    participant Parser as xpathParser.ts
    participant DOM as Shadow DOM

    Client->>Selector: resolveXPathMainWorld(xpath, index)
    Selector->>Resolver: resolveXPathAtIndex(xpath, index)
    Resolver->>Parser: parseXPathSteps(xpath)
    Parser-->>Resolver: steps with predicates
    Resolver->>Resolver: getShadowContext()
    Resolver->>Resolver: resolveXPathComposedMatches(xpath)
    Resolver->>DOM: composedChildren/composedDescendants
    DOM-->>Resolver: elements from light and shadow
    Resolver->>Parser: applyPredicates(elements, predicates)
    Parser-->>Resolver: filtered elements
    Resolver-->>Selector: element at index
    Selector-->>Client: Element

    Client->>Count: countXPathMatchesMainWorld(xpath)
    Count->>Resolver: countXPathMatches(xpath)
    Resolver->>Parser: parseXPathSteps(xpath)
    Parser-->>Resolver: steps
    Resolver->>Resolver: resolveXPathComposedMatches(xpath)
    Resolver->>DOM: traverse composed tree
    DOM-->>Resolver: all matches
    Resolver-->>Count: count
    Count-->>Client: number

    Client->>Wait: waitForSelector(xpath)
    Wait->>Resolver: resolveXPathFirst(xpath)
    Resolver->>Resolver: resolveXPathAtIndex(xpath, 0)
    Resolver-->>Wait: first element or null
Loading

Last reviewed commit: 955f87b

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

12 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

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

Choose a reason for hiding this comment

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

cubic analysis

1 issue found across 12 files

Confidence score: 4/5

  • This PR looks generally safe to merge, with one moderate parser edge case noted as the main risk driver.
  • In packages/core/lib/v3/dom/locatorScripts/xpathParser.ts, isBoundary only treats whitespace as a word boundary, so XPath keywords like and/or adjacent to parentheses may be misparsed and could affect certain selector expressions.
  • Pay close attention to packages/core/lib/v3/dom/locatorScripts/xpathParser.ts - keyword boundary handling around parentheses may break valid XPath operators without spaces.
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/lib/v3/dom/locatorScripts/xpathParser.ts">

<violation number="1" location="packages/core/lib/v3/dom/locatorScripts/xpathParser.ts:349">
P2: `isBoundary` only considers whitespace as a word boundary, so `and`/`or` keywords adjacent to parentheses (e.g., `not(@x)and @y='z'`) won't be split. XPath allows operators without surrounding spaces. Consider treating any non-word character as a boundary instead.</violation>
</file>

Linked issue analysis

Linked issue: STG-1373: add shared xpath resolver + policy

Status Acceptance criteria Notes
Add shared resolver (e.g. xpathResolver.ts) Added xpathResolver.ts with exported resolver functions
Resolver owns native document.evaluate resolution vs composed-tree resolution Resolver tries native evaluate then falls back to composed traversal
Resolver centralizes nth (.nth) vs count (.count) semantics Provides resolveXPathAtIndex(index) and countXPathMatches() APIs
Ordering: use DFS (document-order) instead of BFS for composed traversal composedDescendants uses stack + reversed push to produce DFS order
Make all call sites use the shared resolver (selectors, counts, waitForSelector) Selectors/counts/waitForSelector now call resolver functions
Architecture diagram
sequenceDiagram
    participant Script as Entry Point (selectors/counts)
    participant Resolver as NEW: xpathResolver.ts
    participant Parser as CHANGED: xpathParser.ts
    participant DOM as Browser DOM (Light + Shadow)
    participant Backdoor as window.__stagehandV3__

    Note over Script,Backdoor: Unified XPath Resolution Flow

    Script->>Resolver: NEW: resolveXPathAtIndex / countXPathMatches
    Resolver->>Resolver: normalizeXPath()

    Resolver->>Parser: NEW: parseXPathSteps(xpath)
    Note right of Parser: Parses complex predicates:<br/>and/or/not, contains(),<br/>normalize-space(), text()
    Parser-->>Resolver: Array of XPathStep objects

    alt Pierce Shadow (Default)
        Resolver->>Backdoor: getShadowContext()
        Backdoor-->>Resolver: stats & getClosedRoot helper
        
        loop For each XPath Step
            Resolver->>DOM: Get composed children/descendants
            Note right of Resolver: DFS traversal across<br/>ShadowRoot boundaries
            
            loop For each candidate Element
                Resolver->>Parser: NEW: applyPredicates(element, predicates)
                Parser->>DOM: CHANGED: Check attributes/text
                Parser-->>Resolver: boolean (matches)
            end
        end
    else Native Fallback (Light DOM only)
        Resolver->>DOM: document.evaluate(xpath, ...)
        DOM-->>Resolver: XPathResult
    end

    Resolver-->>Script: Final Element(s) or Match Count
    Script-->>Script: Return result to Main World/Playwright
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@seanmcguire12
Copy link
Member Author

@cubic-dev-ai

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Feb 13, 2026

@cubic-dev-ai

@seanmcguire12 I have started the AI code review. It will take a few minutes to complete.

Copy link
Contributor

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

Choose a reason for hiding this comment

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

cubic analysis

No issues found across 12 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Linked issue analysis

Linked issue: STG-1373: add shared xpath resolver + policy

Status Acceptance criteria Notes
Add a shared resolver (xpathResolver.ts) that owns native document.evaluate vs composed tree resolution New xpathResolver.ts implements native vs composed logic
Ensure .count()/.nth() include shadow DOM matches (nth vs count semantics) count/resolve call into resolver with pierceShadow by default
Ordering: use DFS instead of BFS for composed traversal (document-order) composedDescendants uses stack (DFS) and reversed push
Make all call sites use the shared resolver selectors, counts and waitForSelector now call resolver APIs
Expand composed predicate parsing/evaluation (attr exists, contains, starts-with, text(), normalize-space, and/or/not) Parser adds predicate types and evaluator handles these cases
Route xpath entrypoints through the resolver (selectors.ts, counts.ts, waitForSelector.ts) Entrypoints now delegate to resolver functions
Remove unused injected helper resolveSimpleXPath from piercer.runtime.ts & update global.d.ts resolveSimpleXPath removed and backdoor type updated
Add tests covering parser and resolver (shadow DOM inclusion and ordering) Parser tests expanded and resolver tests added with jsdom env
Use native document.evaluate when possible and fallback to composed traversal when needed Resolver tries native evaluate and falls back on error/hasShadow
Ensure nth uses consistent document-order traversal and indexing semantics (including positional predicates) Index predicate handled and resolver returns composed[targetIndex]
Architecture diagram
sequenceDiagram
    participant UI as User / Test Script
    participant Entry as Entry Points (selectors/counts/waitFor)
    participant Res as NEW: xpathResolver
    participant Parser as CHANGED: xpathParser
    participant Backdoor as Shadow Piercer Backdoor
    participant DOM as Browser DOM (Light + Shadow)

    Note over UI,DOM: Unified XPath Resolution Flow (Shadow-Inclusive)

    UI->>Entry: resolveXPath / countXPath (rawXp)
    Entry->>Res: CHANGED: resolveXPathAtIndex / countXPathMatches
    
    Res->>Parser: parseXPathSteps(rawXp)
    Note right of Parser: NEW: Parses predicates (attr, text(),<br/>normalize-space, and/or/not)
    Parser-->>Res: XPathStep[] (structured axes + predicates)

    alt NEW: Pierce Shadow = false OR No Shadow DOM detected
        Res->>DOM: document.evaluate (native)
        DOM-->>Res: XPathResult
    else Composed Tree Traversal (Pierce Shadow = true)
        loop For each XPathStep
            Res->>DOM: Get children/descendants (DFS)
            
            opt Element is Shadow Host
                Res->>DOM: Access open shadowRoot
                Res->>Backdoor: NEW: getClosedRoot(element)
                Backdoor-->>Res: Return closed ShadowRoot
            end

            Res->>Parser: NEW: applyPredicates(elements, step.predicates)
            Note right of Parser: Evaluates attr existence/contains,<br/>text equality, and boolean logic
            Parser-->>Res: Filtered match list
        end
    end

    alt Indexing / Results
        Res->>Res: Order results by Document DFS
        Res-->>Entry: Target element(s) or count
    else Error / No Match
        Res-->>Entry: null / 0
    end

    Entry-->>UI: Return Result (piercing shadow boundaries)

    Note over Res,DOM: DFS ensures .nth(n) matches order across shadow boundaries
Loading

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.

1 participant