Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/l-xiaoshen/handstage/llms.txt

Use this file to discover all available pages before exploring further.

A Locator is the primary way to interact with elements on a page. You obtain one by calling page.locator(selector), and from there you can click, fill, hover, and more. Locators are intentionally lazy — the selector is not evaluated until you call an action, and it is re-evaluated on every action. This means you never hold a stale element reference across navigations.

Creating a locator

Pass a CSS selector or an XPath expression to page.locator(). The locator is bound to the page’s main frame.
import { V3 } from "@handstage/core"

const browser = await V3.connectLocal()
const context = browser.context
const page = await context.newPage("https://example.com")

// CSS selector
const button = page.locator("button.submit")

// XPath expression
const heading = page.locator("//h1[@class='title']")

await browser.close()

Supported selector syntax

Handstage supports three selector types, detected automatically from the string prefix:
  • CSS selectors — standard CSS like #id, .class, input[type="text"], div > span
  • XPath expressions — strings that start with / or (, such as //button[@id='submit'], /html/body/div[1], or (//td)[3]. You can also use an explicit xpath= prefix.
  • Text selectors — strings prefixed with text=, such as text=Submit. Matches elements containing the given text.
You can also use explicit prefixes: css=, xpath=, text= to override automatic detection.

Interacting with elements

Locators expose a set of action methods. Each one resolves the element at call time, performs the action, and releases the remote object.
// Left-click at the element's visual center
await page.locator("button#submit").click()

// Right-click
await page.locator(".context-menu-trigger").click({ button: "right" })

// Double-click
await page.locator(".item").click({ clickCount: 2 })

Targeting the nth match

By default, a locator acts on the first element that matches the selector. Use nth(index) to target a specific match by zero-based index.
const items = page.locator("ul li")

// First item (explicit)
await items.nth(0).click()

// Third item
await items.nth(2).click()

// Shorthand for the first item
await items.first().click()
You can also check how many elements match a selector with count():
const count = await page.locator("ul li").count()
console.log(`${count} list items found`)

Shadow DOM piercing

Handstage can pierce shadow DOM boundaries automatically. Pass { deep: true } in the locator options to search inside shadow roots when evaluating CSS selectors.
// Find a button inside a shadow root
const shadowButton = page.locator("my-custom-element::shadow button", { deep: true })
await shadowButton.click()
The deep option applies to CSS selectors. XPath expressions already traverse shadow roots natively in Handstage’s resolver.

Locators are lazy

Locators do not resolve the DOM element when you call page.locator(). The element is looked up fresh on every action call. This has two practical implications:
  1. You can create a locator before the element exists in the DOM and call actions on it later.
  2. You never need to worry about stale element references across navigations — the locator always finds the current live element.
// Create the locator before the element exists
const successBanner = page.locator(".success-banner")

// Trigger some action that causes the banner to appear
await page.locator("form button[type='submit']").click()
await page.waitForSelector(".success-banner")

// Now resolve and interact with it
await successBanner.click()

Frame locators for iframes

To interact with elements inside an iframe, use page.frameLocator(selector) to scope subsequent locators to that frame. You can chain frame locators for nested iframes.
// Target an element inside an iframe
const frame = page.frameLocator("iframe#payment-form")
await frame.locator("input[name='card-number']").fill("4111111111111111")
await frame.locator("button.pay").click()
frameLocator accepts the same CSS and XPath selector syntax as locator. It resolves the iframe element and scopes all chained locator calls to its document.

Error handling

When a locator cannot find a matching element, Handstage throws HandstagesElementNotFoundError. When the element exists but is not visible (for example, its bounding box has zero area), actions like click() and hover() throw ElementNotVisibleError.
import {
  HandstagesElementNotFoundError,
  ElementNotVisibleError,
} from "@handstage/core"

try {
  await page.locator("#missing-button").click()
} catch (err) {
  if (err instanceof HandstagesElementNotFoundError) {
    console.log("Element not found in the DOM")
  } else if (err instanceof ElementNotVisibleError) {
    console.log("Element found but not visible")
  } else {
    throw err
  }
}

Common patterns

Use page.waitForSelector() when an element appears asynchronously. Once it resolves, you can interact with it through a locator.
await page.waitForSelector(".results-list", { state: "visible" })
const firstResult = page.locator(".results-list .result-item").nth(0)
await firstResult.click()
Call isVisible() to guard against interacting with hidden elements.
const modal = page.locator(".modal")
if (await modal.isVisible()) {
  await modal.locator(".close-button").click()
}
Use inputValue() to read the current value of an input, textarea, select, or contenteditable element.
const value = await page.locator("input#username").inputValue()
console.log("Current value:", value)
Use selectOption() on a <select> element. Pass one or more option values.
await page.locator("select#country").selectOption("US")

// Multiple selections
await page.locator("select#tags").selectOption(["typescript", "testing"])

Build docs developers (and LLMs) love