Skip to content

Conversation

@seanmcguire12
Copy link
Member

@seanmcguire12 seanmcguire12 commented Jan 7, 2026

why

  • adds support for a new function: page.waitForSelector(), which works with cross-iframe selectors & cross-shadow root selectors

what changed

  • added page.waitForSelector() which returns true when the element that the selector points to resolves, and times out with an error when it does not resolve

test plan

  • added a bunch of e2e test cases, covering various CSS selector types, & cross iframe/shadow root cases

Summary by cubic

Adds Page.waitForSelector() to wait for CSS or XPath targets to reach a state, piercing shadow DOM and traversing iframes. This makes waits reliable across complex trees and uses MutationObserver for efficiency.

  • New Features

    • Page.waitForSelector(selector, { state, timeout, pierceShadow }) with states: attached, detached, visible, hidden (defaults: visible, 30s, pierceShadow=true).
    • Supports CSS and XPath (xpath= or /), including open and closed shadow DOM via the V3 piercer.
    • Works across iframes and with '>>' hop notation; resolves the correct frame automatically.
    • Clear timeout errors; no polling (MutationObserver-based).
  • Refactors

    • Added resolveLocatorTarget to unify hop/XPath frame resolution and reuse in waitForSelector.
    • Exported waitForSelector locator script under dom/locatorScripts.

Written for commit a238850. Summary will update on new commits.

@changeset-bot
Copy link

changeset-bot bot commented Jan 7, 2026

🦋 Changeset detected

Latest commit: a238850

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

@seanmcguire12
Copy link
Member Author

@greptileai

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 7, 2026

Greptile Summary

Implements Page.waitForSelector() to wait for CSS or XPath selectors to reach a specific state (attached, detached, visible, hidden). The implementation uses MutationObserver for efficient DOM monitoring, pierces both open and closed shadow DOM via the V3 piercer, and works across iframes using >> hop notation.

Key changes:

  • New waitForSelector.ts locator script runs in-page with comprehensive selector support (CSS, XPath, shadow DOM, iframe traversal)
  • Page API integration with iframe hop resolution via resolveLocatorTarget (refactored from deepLocator.ts)
  • Comprehensive test suite (50+ tests) covering edge cases, visibility states, shadow DOM, and dynamic mutations
  • Previous race condition issues addressed with settled flag to prevent double resolve/reject

Implementation highlights:

  • No polling - uses MutationObserver on main document and all shadow roots
  • Handles XPath via native document.evaluate with fallback to composed tree traversal for shadow DOM
  • State checking includes CSS visibility, dimensions, opacity checks
  • Timeout management with proper cleanup of observers and event handlers

Confidence Score: 4/5

  • This PR is safe to merge with one minor visibility logic inconsistency noted
  • The implementation is solid with comprehensive tests and proper error handling. Previous race conditions have been fixed with the settled flag. The only concern is the inconsistency between visible and hidden state checks (offsetParent check doesn't align with inverse of visible logic), but this is a minor edge case that doesn't affect core functionality.
  • No files require special attention - the offsetParent inconsistency in waitForSelector.ts:353 is minor and doesn't impact most use cases

Important Files Changed

Filename Overview
packages/core/lib/v3/dom/locatorScripts/waitForSelector.ts Adds MutationObserver-based waitForSelector with shadow DOM piercing and XPath support. Previous race conditions addressed with settled flag.
packages/core/lib/v3/understudy/page.ts Adds Page.waitForSelector() method that integrates with resolveLocatorTarget for iframe hop support and timeout handling.
packages/core/lib/v3/understudy/deepLocator.ts Extracted resolveLocatorTarget to unify hop/XPath frame resolution, now shared by waitForSelector and deepLocator.

Sequence Diagram

sequenceDiagram
    participant User
    participant Page
    participant resolveLocatorTarget
    participant Frame
    participant waitForSelector
    participant MutationObserver
    participant piercer as V3 Piercer

    User->>Page: waitForSelector(selector, options)
    Page->>resolveLocatorTarget: resolve iframe hops/XPath
    alt has ">>" hop notation
        resolveLocatorTarget->>Frame: navigate through iframes
        Frame-->>resolveLocatorTarget: target frame
    else deep XPath with iframe steps
        resolveLocatorTarget->>Frame: parse XPath, resolve iframe steps
        Frame-->>resolveLocatorTarget: target frame
    else plain selector
        resolveLocatorTarget-->>Page: use current frame
    end
    
    Page->>Frame: evaluate(waitForSelector script)
    Frame->>waitForSelector: execute in page context
    
    waitForSelector->>waitForSelector: check immediate condition
    alt condition already met
        waitForSelector-->>Frame: resolve(true)
    else need to wait
        waitForSelector->>MutationObserver: setup observers
        waitForSelector->>piercer: setupShadowObservers (if pierceShadow)
        piercer-->>waitForSelector: observe closed shadow roots
        
        loop on mutations
            MutationObserver->>waitForSelector: trigger check()
            waitForSelector->>waitForSelector: findElement (CSS/XPath)
            alt pierceShadow=true
                waitForSelector->>piercer: query shadow DOM
                piercer-->>waitForSelector: element or null
            end
            waitForSelector->>waitForSelector: checkState (visible/hidden/etc)
            alt condition met
                waitForSelector->>waitForSelector: set settled=true
                waitForSelector->>waitForSelector: clearTimer & cleanup
                waitForSelector-->>Frame: resolve(true)
            end
        end
        
        alt timeout expires
            waitForSelector->>waitForSelector: set settled=true
            waitForSelector->>waitForSelector: cleanup observers
            waitForSelector-->>Frame: reject(timeout error)
        end
    end
    
    Frame-->>Page: return result
    Page-->>User: return boolean
Loading

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.

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@seanmcguire12
Copy link
Member Author

@greptileai

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.

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@seanmcguire12
Copy link
Member Author

@greptileai

@seanmcguire12 seanmcguire12 marked this pull request as ready for review January 9, 2026 01:54
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.

1 issue found across 5 files

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/waitForSelector.ts">

<violation number="1" location="packages/core/lib/v3/dom/locatorScripts/waitForSelector.ts:442">
P2: Invalid state values are silently accepted and treated as `"visible"`. Consider validating the state parameter and throwing an error for invalid values to help users catch mistakes early.</violation>
</file>

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

Comment on lines +442 to +443
const state =
(String(stateRaw ?? "visible") as WaitForSelectorState) || "visible";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 9, 2026

Choose a reason for hiding this comment

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

P2: Invalid state values are silently accepted and treated as "visible". Consider validating the state parameter and throwing an error for invalid values to help users catch mistakes early.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/dom/locatorScripts/waitForSelector.ts, line 442:

<comment>Invalid state values are silently accepted and treated as `"visible"`. Consider validating the state parameter and throwing an error for invalid values to help users catch mistakes early.</comment>

<file context>
@@ -0,0 +1,549 @@
+  pierceShadowRaw?: boolean,
+): Promise<boolean> {
+  const selector = String(selectorRaw ?? "").trim();
+  const state =
+    (String(stateRaw ?? "visible") as WaitForSelectorState) || "visible";
+  const timeout =
</file context>
Suggested change
const state =
(String(stateRaw ?? "visible") as WaitForSelectorState) || "visible";
const validStates = ["attached", "detached", "visible", "hidden"];
const stateStr = String(stateRaw ?? "visible").trim() || "visible";
if (!validStates.includes(stateStr)) {
throw new Error(`waitForSelector: Invalid state "${stateStr}". Must be one of: ${validStates.join(", ")}`);
}
const state = stateStr as WaitForSelectorState;
Fix with Cubic

@seanmcguire12 seanmcguire12 merged commit e56c6eb into main Jan 9, 2026
21 checks passed
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.

3 participants