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.

In Handstage, every open browser tab is represented by a Page object. A Page maps one-to-one with a top-level CDP target — the Chrome concept for a navigable document. You create pages through a V3Context, navigate them with familiar browser-history methods, and query their current URL or title at any time.

Pages as browser tabs

Each Page instance owns the CDP session for its target, tracks all child frames (including out-of-process iframes), and exposes the high-level automation API you use day-to-day. OOPIF (out-of-process iframe) targets are adopted by the owning Page automatically — you interact with them through the same locator and frame APIs without any extra setup.

Opening a new page

Call context.newPage() to create a new tab. You can pass a URL to navigate immediately, or omit it to open about:blank.
import { V3 } from "@handstage/core"

const browser = await V3.connectLocal()
const context = browser.context

// Open a blank tab
const blankPage = await context.newPage()

// Open a tab and navigate in one call
const page = await context.newPage("https://example.com")

await browser.close()
Handstage always creates the tab at about:blank first, then navigates to the requested URL after the target attaches. This ensures init scripts registered on the context run on the first real document.

Listing open pages

context.pages() returns all top-level pages sorted from oldest to newest. OOPIF targets are not included — only top-level tabs appear in this list.
const pages = context.pages()
console.log(`${pages.length} tab(s) open`)

for (const p of pages) {
  console.log(p.url())
}

Active page

Handstage tracks which page was most recently interacted with as the “active” page. context.activePage() returns that page, or falls back to the newest tab if the recency list is empty.
const active = context.activePage()
if (active) {
  console.log("Active tab:", active.url())
}
To programmatically promote a page to active — for example after opening a popup — call context.setActivePage(page). This also brings the tab to the foreground in headful Chrome.
const popup = await context.newPage("https://docs.example.com")
context.setActivePage(popup)
Every Page provides four navigation methods that mirror the browser’s own controls.
Navigate to a URL. By default waits for "domcontentloaded" before resolving. Returns the HTTP response for the main document, or null if there was no network response (for example, about:blank).
const response = await page.goto("https://example.com", {
  waitUntil: "networkidle",
  timeoutMs: 20000,
})
console.log(response?.status())
Reload the current page. Optionally pass ignoreCache: true to bypass the browser cache.
await page.reload({ ignoreCache: true, waitUntil: "load" })
Navigate to the previous entry in the browser’s history. Returns null if there is nothing to go back to.
await page.goBack({ waitUntil: "domcontentloaded" })
Navigate to the next entry in the browser’s history. Returns null if there is nothing to go forward to.
await page.goForward({ waitUntil: "domcontentloaded" })

Load states

All navigation methods accept a waitUntil option that controls which lifecycle event Handstage waits for before resolving.
ValueDescription
"load"Waits for the load event — all resources including images and stylesheets have finished loading.
"domcontentloaded"Waits for the DOMContentLoaded event — the HTML has been parsed and the DOM is ready. This is the default for goto().
"networkidle"Waits until there have been no new network requests for at least 500ms. Useful for pages with deferred data fetching.
// Fast: resolve as soon as the DOM is parsed
await page.goto("https://example.com", { waitUntil: "domcontentloaded" })

// Thorough: resolve only after all async requests settle
await page.goto("https://example.com", { waitUntil: "networkidle" })
You can also wait for a load state independently of navigation using page.waitForLoadState():
// Trigger some client-side navigation, then wait
await page.locator("#load-more").click()
await page.waitForLoadState("networkidle")

Reading page URL and title

page.url() is synchronous and returns the cached URL from the most recent navigation event. page.title() is async and reads directly from the live document.
console.log(page.url())          // synchronous
console.log(await page.title())  // async, reads document.title

OOPIF support

Chrome renders cross-origin iframes in separate renderer processes called out-of-process iframes (OOPIFs). Handstage adopts OOPIF child sessions automatically as they attach and routes their frame events to the owning Page. You do not need to handle OOPIFs differently — locators and frame APIs work the same way whether an iframe is in-process or out-of-process.

Init scripts

Use context.addInitScript() to register a JavaScript snippet that runs at document start on every page in the context — including pages that are opened later. This is the right place to install polyfills, mock browser APIs, or seed test data before the page’s own scripts execute.
// Inject a custom value before any page script runs
await context.addInitScript(() => {
  Object.defineProperty(navigator, "language", { value: "fr-FR" })
})

// Init scripts also accept a serializable argument
await context.addInitScript(
  (seed) => { window.__testSeed = seed },
  { userId: 42 }
)

const page = await context.newPage("https://example.com")
// The mock is already in place when example.com's scripts run
Init scripts are installed on both existing pages and any new pages you open after registering them. They are not inherited by isolated contexts — register them on each context separately.

Complete example

import { V3 } from "@handstage/core"

const browser = await V3.connectLocal()
const context = browser.context

// Open two tabs
const pageA = await context.newPage("https://example.com")
const pageB = await context.newPage("https://docs.example.com")

console.log("All tabs:", context.pages().map((p) => p.url()))

// Make pageA active and navigate it
context.setActivePage(pageA)
await pageA.goto("https://example.com/about", { waitUntil: "load" })

console.log("pageA URL:", pageA.url())
console.log("pageA title:", await pageA.title())

// Go back in history
await pageA.goBack()
console.log("after goBack:", pageA.url())

await browser.close()

Build docs developers (and LLMs) love