Skip to content

[fix]: mask param in page.screenshot() only applied to first node#1612

Merged
seanmcguire12 merged 6 commits intomainfrom
seanmcguire/stg-1216-fix-screenshot-mask
Jan 27, 2026
Merged

[fix]: mask param in page.screenshot() only applied to first node#1612
seanmcguire12 merged 6 commits intomainfrom
seanmcguire/stg-1216-fix-screenshot-mask

Conversation

@seanmcguire12
Copy link
Member

@seanmcguire12 seanmcguire12 commented Jan 26, 2026

why

what changed

  • renamed resolveMaskRect() to resolveMaskRects(), and updated behaviour to resolve all bounding rects instead of just 1
  • added locator.resolveNodesForMask() which is called by resolveMaskRects() and resolves all nodes & object IDs so they can be iterated through, and have masks applied to them

test plan

  • extended tests in page-screenshot.spec.ts to cover the case where multiple elements match the mask locator

@changeset-bot
Copy link

changeset-bot bot commented Jan 26, 2026

🦋 Changeset detected

Latest commit: 7470fb6

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 Jan 26, 2026

Greptile Overview

Greptile Summary

This PR fixes issue #1585 where the mask parameter in page.screenshot() was only applied to the first matching element instead of all elements matched by the locator. The fix refactors the masking logic to resolve and process all matching nodes.

Key changes:

  • Changed nthIndex default from 0 to -1 to distinguish between "no explicit nth() call" (mask all elements) and locator.nth(0) (mask only first element)
  • Renamed resolveMaskRect() to resolveMaskRects() (plural) to return multiple rects instead of a single rect
  • Added locator.resolveNodesForMask() method that respects the nthIndex flag and returns all matching nodes when no explicit nth() was called
  • Properly releases all object IDs in both success and error paths using try-catch-finally blocks
  • Updated tests to verify multiple elements are masked (expects 2 rects in the test assertion)

The implementation correctly addresses the previous review concern about nth(0) behavior by using -1 as a sentinel value.

Confidence Score: 5/5

  • This PR is safe to merge with no issues found
  • The implementation is clean, well-tested, and properly addresses the reported bug. The use of -1 as a sentinel value correctly distinguishes between explicit nth(0) calls and default behavior. Object ID cleanup is handled properly in all code paths including error scenarios. The test coverage validates the fix works for multiple matching elements.
  • No files require special attention

Important Files Changed

Filename Overview
packages/core/lib/v3/understudy/locator.ts Uses -1 sentinel value to distinguish explicit nth() from all elements; adds resolveNodesForMask()
packages/core/lib/v3/understudy/frameLocator.ts LocatorDelegate updated to track nthIndex and properly delegate nth() calls
packages/core/lib/v3/understudy/screenshotUtils.ts Refactored to handle multiple mask rects; properly releases all object IDs in error paths

Sequence Diagram

sequenceDiagram
    participant User
    participant Page
    participant ScreenshotUtils
    participant Locator
    participant SelectorResolver
    participant CDP
    
    User->>Page: screenshot({mask: [locator]})
    Page->>ScreenshotUtils: applyMaskOverlays(locators)
    
    loop for each mask locator
        ScreenshotUtils->>ScreenshotUtils: resolveMaskRects(locator)
        ScreenshotUtils->>Locator: resolveNodesForMask()
        
        alt nthIndex >= 0 (explicit nth())
            Locator->>SelectorResolver: resolveAtIndex(query, nthIndex)
            SelectorResolver->>CDP: evaluate element at index
            CDP-->>SelectorResolver: {objectId, nodeId}
            SelectorResolver-->>Locator: [single resolved node]
        else nthIndex < 0 (all elements)
            Locator->>SelectorResolver: resolveAll(query)
            SelectorResolver->>CDP: evaluate all matching elements
            CDP-->>SelectorResolver: [{objectId, nodeId}, ...]
            SelectorResolver-->>Locator: [all resolved nodes]
        end
        
        Locator-->>ScreenshotUtils: resolved nodes with objectIds
        
        loop for each resolved node
            ScreenshotUtils->>ScreenshotUtils: resolveMaskRectForObject(objectId)
            ScreenshotUtils->>CDP: callFunctionOn(getBoundingClientRect)
            CDP-->>ScreenshotUtils: {x, y, width, height}
            ScreenshotUtils->>CDP: releaseObject(objectId)
            Note right of ScreenshotUtils: Always releases objectId<br/>even on failure
        end
        
        ScreenshotUtils-->>ScreenshotUtils: {frame, rects: [rect1, rect2, ...]}
    end
    
    ScreenshotUtils->>Page: inject mask overlay for all rects
    Page->>CDP: screenshot with overlays
    CDP-->>User: screenshot buffer
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.

1 file reviewed, 1 comment

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.

2 issues found across 4 files

Confidence score: 3/5

  • Some risk due to a user-facing selector behavior: .nth(0) in packages/core/lib/v3/understudy/locator.ts won’t narrow results because this.nthIndex > 0 excludes zero, which can change element targeting.
  • Potential resource leak in packages/core/lib/v3/understudy/screenshotUtils.ts if resolveMaskRectForObject throws mid-iteration, leaving unreleased objectIds and possibly degrading stability.
  • Overall impact looks moderate (mid-severity issues, no evidence of tests or guards here), so this isn’t a blocker but warrants attention before merge.
  • Pay close attention to packages/core/lib/v3/understudy/locator.ts and packages/core/lib/v3/understudy/screenshotUtils.ts - selector narrowing correctness and object release on failure.
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/understudy/locator.ts">

<violation number="1" location="packages/core/lib/v3/understudy/locator.ts:875">
P2: The condition `this.nthIndex > 0` doesn't handle `.nth(0)` as expected. Since `nthIndex` defaults to `0`, calling `.nth(0)` won't narrow to just the first element - it will return all elements instead. The JSDoc comment is misleading because `.nth(0)` doesn't actually narrow the result.

Consider either:
1. Tracking whether `.nth()` was explicitly called (e.g., with a separate flag or using `undefined`/`null` as the default)
2. Update the comment to clarify that `.nth(0)` behaves the same as no `.nth()` call</violation>
</file>

<file name="packages/core/lib/v3/understudy/screenshotUtils.ts">

<violation number="1" location="packages/core/lib/v3/understudy/screenshotUtils.ts:300">
P2: Potential resource leak: if `resolveMaskRectForObject` throws mid-iteration, remaining `objectId`s in `resolved` won't be released. Consider releasing all objectIds in an outer finally block that covers the entire iteration.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Process as applyMaskOverlays
    participant Utils as resolveMaskRects (Internal)
    participant Loc as Locator
    participant CDP as CDPSession (Browser)

    Note over Process,CDP: Logic flow for calculating mask coordinates

    loop For each Locator in mask options
        Process->>Utils: CHANGED: resolveMaskRects(locator)
        
        Note over Utils,Loc: NEW: Resolves list of nodes instead of single node
        Utils->>Loc: NEW: resolveNodesForMask()
        
        alt Locator uses .nth(i)
            Loc->>CDP: Resolve single node at index
            CDP-->>Loc: Single ObjectId
        else Default (All matches)
            Loc->>CDP: CHANGED: Resolve ALL matching nodes
            CDP-->>Loc: List of ObjectIds
        end
        
        Loc-->>Utils: Return Array<{ objectId }>

        loop NEW: Iterate through every resolved node
            Utils->>Utils: resolveMaskRectForObject(objectId)
            Utils->>CDP: Runtime.callFunctionOn (getBoundingClientRect)
            
            alt Element is visible & has dimensions
                CDP-->>Utils: { x, y, width, height }
            else Hidden or empty
                CDP-->>Utils: null
            end

            Utils->>CDP: Runtime.releaseObject
        end

        Utils-->>Process: Return aggregated list of Rects
        Process->>Process: Add Rects to frame overlay list
    end
Loading

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

@seanmcguire12
Copy link
Member Author

@greptileai

@seanmcguire12
Copy link
Member Author

@cubic-dev-ai

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Jan 26, 2026

@cubic-dev-ai

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

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.

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

1 issue found across 5 files

Confidence score: 4/5

  • This PR looks safe to merge; the only noted issue is a minor, non-functional error-message inconsistency.
  • packages/core/lib/v3/understudy/frameLocator.ts has a misleading error message about .nth() which could confuse users but doesn't affect runtime behavior.
  • Pay close attention to packages/core/lib/v3/understudy/frameLocator.ts - misleading .nth() error message.
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/understudy/frameLocator.ts">

<violation number="1" location="packages/core/lib/v3/understudy/frameLocator.ts:158">
P2: Error message is misleading. `FrameLocator` doesn't have a `.nth()` method—this method is on `LocatorDelegate` which is returned by `frameLocator().locator()`. For consistency with the `Locator` class, the message should reference the locator context, e.g., `"locator().nth() expects a non-negative index"`.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant Utils as screenshotUtils
    participant Loc as Locator
    participant Res as SelectorResolver
    participant CDP as CDPSession (Browser)

    Note over Client,CDP: NEW: Multi-element Masking Flow

    Client->>Utils: page.screenshot({ mask: locator })
    
    Utils->>Loc: NEW: resolveNodesForMask()
    
    alt Locator has explicit .nth(i)
        Loc->>Res: resolveAtIndex(i)
        Res-->>Loc: [Single Node]
    else NEW: Default (Mask all matches)
        Loc->>Res: resolveAll()
        Res-->>Loc: [Multiple Nodes]
    end

    Loc-->>Utils: List of RemoteObjectIds

    loop NEW: For each ObjectId
        Utils->>CDP: Runtime.callFunctionOn(getBoundingClientRect)
        
        alt Element visible & valid
            CDP-->>Utils: Rect {x, y, w, h}
        else Hidden or Invalid
            CDP-->>Utils: null
        end
        
        Utils->>CDP: Runtime.releaseObject
    end

    Utils->>Utils: Aggregate valid rects
    Utils-->>Client: Screenshot with N masks
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 Jan 26, 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.

No issues found across 5 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant User as Test/Client
    participant Utils as ScreenshotUtils
    participant Loc as Locator
    participant CDP as Chrome Session

    User->>Utils: page.screenshot({ mask: [locator] })
    Utils->>Utils: applyMaskOverlays()

    Note over Utils,CDP: CHANGED: Logic now handles multiple nodes per locator

    Utils->>Loc: NEW: resolveNodesForMask()
    
    alt Locator has explicit .nth(index)
        Loc->>CDP: Resolve single node at index
        CDP-->>Loc: Returns single objectId
    else NEW: Default behavior (no .nth)
        Loc->>CDP: Resolve ALL matching nodes
        CDP-->>Loc: Returns list of objectIds
    end
    
    Loc-->>Utils: Array<{ objectId, nodeId }>

    loop NEW: Iterate over all resolved objects
        Utils->>CDP: Runtime.callFunctionOn(getBoundingClientRect)
        CDP-->>Utils: Rect {x, y, width, height}
        Utils->>CDP: Runtime.releaseObject(objectId)
        Utils->>Utils: Collect valid rect
    end

    Utils->>Utils: Draw masks over all collected rects
    Utils-->>User: Return Screenshot Buffer
Loading

@seanmcguire12 seanmcguire12 merged commit bdd8b4e into main Jan 27, 2026
54 of 56 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.

2 participants