# browser-automation-protocol

```
⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠇⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠧⡇⠀⠀⠒⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⡤⡆⠦⠆⢀⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠧⣷⣆⠅⢦⠀⠀⠀⠀⠀⠀⠀⠀⠠⠀⠈⠀⠀⠀⠀⠀⢤⣤⣆⢇⣶⣤⡤⡯⣦⣌⡡⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠷⣿⣷⣆⣐⡆⠀⠀⠀⠀⢀⠤⠊⠀⠀⢀⣠⣾⢯⣦⣴⣜⣺⣾⣿⣤⠟⠋⣷⢛⡣⠭⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠯⣿⣷⢫⡯⠄⠀⠀⢀⠐⠁⠀⠀⠀⠠⣤⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣙⣷⡗⢤⡤⠀⠈⣰⠶⡤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣩⣿⡏⠉⠉⠀⢠⡔⠁⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠑⣏⠶⡉⠖⣡⠂⣈⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣮⣿⣧⣤⣤⠖⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⢉⡻⣿⣿⣿⣿⣿⣿⣿⣿⠟⠓⠈⠅⠈⠀⠀⠘⢒⣽⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⡿⠛⠉⠀⠀⠀⣀⠔⢀⡴⣃⠀⠀⢀⠷⠲⡄⠸⠟⢋⣿⣿⣿⣿⣿⡇⠀⠀⠀⠐⠁⠀⠀⠂⠀⠀⠰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⡆⣷⣆⡐⠶⠤⢤⣷⣀⣀⣩⢐⣟⣥⠜⣤⣀⣠⣤⠀⠈⠉⢀⣹⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢃⣿⣞⣫⡔⢆⡸⡿⣿⣿⣄⣰⣿⠁⢀⣛⠿⣻⣿⣿⣧⣬⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⢀
⢼⣿⣟⢿⣧⣾⣵⣷⣿⣿⣟⡿⢿⣶⣞⣍⡴⢿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⣠⠈⠀⢀⣀⣼
⠋⣿⣟⡛⢿⣿⣿⣿⣿⣿⣭⣿⣿⣿⣿⣯⣽⣿⣿⣿⣿⠟⠛⠿⢽⣿⣿⣆⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⣀⢀⡠⣤⣤⣰⣿⠟⠁⠀⠀⡼⢾⣿
⣻⣿⣟⣇⠈⣉⣯⠿browserless⣿⠿⠃⠀⠀⠀⠀⠀⠻⣿⣿⣿⣿⣴⣶⣤⣤⣤⣤⣴⣴⣴⣶⣦⣦⣤⣦⣀⣦⣤⣶⣿⣿⣿⣿⣿⣿⣿⠿⠁⠀⠀⡀⣤⣬⣾⣿
⡝⣿⣿⣇⣤⣶⣿⣷⣾⣭⡿⠻⢿⣿⣿⣿⣿⠿⠃⠀⠀⠀⠀⡄⠀⠀⠀⢊⡻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠋⢻⣿bap⣟⢿⠟⢉⠀⡀⢤⣴⣿⣿⣿⠿⠻
⡁⣻⣿⣿⣿⣿⣷⣿⣿⣿⣿⠾⣿⡿⠞⠁⠀⠀⠀⠀⠀⠔⠫⡅⠀⠀⠀⠀⠁⣀⠀⠈⠻⣿⣿⣿⣿⣻⢟⣁⣄⡄⣀⠙⠻⣿⣿⡿⠿⠛⡋⠕⠂⢀⣀⣄⣓⣳⢿⠟⢛⣩⠴⠈⠀
⠂⡁⠈⠛⠛⠛⠛⠋⠁⠀⠈⠈⡀⠀⠀⠀⠀⢀⠘⠀⠀⠀⠆⠀⡀⡢⣀⣆⠄⠈⠨⢦⡀⣈⠙⠛⠿⢿⣿⣿⣿⣿⣿⡿⡿⠿⠟⠆⠒⠁⠀⢶⣾⠿⠟⠛⢉⣀⣠⡶⠚⠁⠀⠀⣠
⠀⡇⡄⣀⡀⠀⠀⠀⠀⠀⠀⠀⢬⠠⠀⡀⠀⠋⠁⠀⡀⠀⠀⡀⠆⢱⣿⣿⣧⣧⣄⠛⣿⣞⣵⣤⣷⣄⠀⠀⠀⠐⠀⠀⠀⠀⠀⠈⠉⠁⠁⠀⠠⢤⣶⣾⣿⡿⠋⢀⣀⣰⣶⣾⣿
⡀⡆⠀⡉⡁⢿⣉⢀⠀⣰⣷⣿⣟⠠⡽⢂⡀⡄⠀⠰⣖⢱⢖⢂⡆⠈⣿⣿⣿⣿⣿⣶⣄⡙⠻⢿⣿⣿⣷⣦⣀⠀⠠⣤⣀⡀⢈⣓⣶⣶⣿⣿⣿⣿⣿⠟⠉⠀⠀⠀⣉⣭⣽⣿⣿
⡇⣯⣿⣿⣿⣾⣿⣿⣿⠿⠟⡡⢞⣹⠾⢻⣚⣛⢺⠞⢋⣭⣾⣧⡃⢄⡈⢿⣿⣿⣿⣿⣿⣿⣯⣿⣮⣽⣿⣿⣿⣿⣷⣬⣽⣿⣿⣿⣽⡿⣿⡿⠟⠋⢀⣀⣐⣺⣿⣿⣟⣫⣭⣿⣿
⢳⣿⣿⣿⣿⣿⣿⣿⣿⣤⣿⣿⣿⣿⣿⣦⠒⠉⢁⡀⠀⣙⣛⢿⣷⣶⣅⠀⠙⠻⣿⣿⣿⣿⣟⡚⠛⠻⠞⠿⠿⡿⡿⠯⠁⠟⣊⠾⠝⢋⣁⣀⣤⣤⣿⣿⣿⡿⠿⠿⠻⠛⠻⠻⠿
⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣐⣾⡿⡟⢶⠾⢋⢹⠿⢿⣿⣿⣷⣦⡈⠙⠛⠿⠿⢿⣶⣶⣶⣶⣶⢶⠟⠚⠀⠁⠀⠀⠙⠛⠛⠛⠛⠛⠋⠉⠁⠀⠀⠀⠀⠀⢀⠀⠀
```

A Puppeteer-like TypeScript SDK for [Browserless](https://browserless.io). Browser Automation Protocol wraps the BrowserQL GraphQL-over-WebSocket API with a familiar, strongly-typed interface. Enjoy a puppeteer-like experience, with the best-in-class browser automation engine.

## Table of Contents

- [Install](#install)
- [Quick Start](#quick-start)
- [Browser & Node](#browser--node)
- [Examples](#examples)
  - [Take a Screenshot](#take-a-screenshot)
  - [Generate a PDF](#generate-a-pdf)
  - [Fill Out a Form](#fill-out-a-form)
  - [Query the DOM](#query-the-dom)
  - [Wait for Elements and Navigation](#wait-for-elements-and-navigation)
  - [Listen to Page Events](#listen-to-page-events)
  - [Run JavaScript in the Page](#run-javascript-in-the-page)
  - [Set Viewport, Headers, and Cookies](#set-viewport-headers-and-cookies)
  - [Block Requests](#block-requests)
  - [Use a Proxy](#use-a-proxy)
  - [Solve CAPTCHAs](#solve-captchas)
  - [Get a Live URL for Debugging](#get-a-live-url-for-debugging)
  - [Reconnect to a Session](#reconnect-to-a-session)
  - [Bring Your Own Transport](#bring-your-own-transport)
  - [Subscribe to Raw Frames](#subscribe-to-raw-frames)
  - [Error Handling](#error-handling)
- [API Reference](#api-reference)
  - [`Browserless.connect(options)`](#browserlessconnectoptions)
  - [`Browser`](#browser)
  - [`Page`](#page)
  - [`ElementHandle`](#elementhandle)
- [Browser Automation Protocol vs Puppeteer](#browser-automation-protocol-vs-puppeteer)
  - [Architecture](#architecture)
  - [Shared Methods](#shared-methods)
  - [Behavioral Differences](#behavioral-differences)
  - [Not in Browser Automation Protocol](#not-in-browser-automation-protocol)
  - [Browser Automation Protocol-Only Features](#browser-automation-protocol-only-features)
- [How It Works](#how-it-works)

## Install

```bash
npm install @browserless.io/bap-ts
```

## Updating the Generated Types

`src/mutations.graphql` is the local schema snapshot used by the TypeScript generator. The source of truth lives in the sibling `enterprise` repo at `../enterprise/mutations.graphql`.

Run this whenever the BrowserQL schema changes:

```bash
npm run update:schema
```

That command copies the enterprise schema into `src/mutations.graphql` and regenerates:

- `src/types/enums.ts`
- `src/types/inputs.ts`
- `src/types/responses.ts`
- `src/types/options.ts`
- `src/graphql/mutations.ts`

Use this check in CI or before publishing:

```bash
npm run check:schema
```

If the enterprise repo is not checked out next to this repo, pass a schema path:

```bash
npm run sync:schema -- --schema /path/to/mutations.graphql
npm run codegen
```

## Generating Public SDK Docs

BAP uses TypeDoc to generate public SDK reference docs for `browserless-docs`. Run this after code or generated type changes:

```bash
npm run docs:sdk
```

The docs are written to `../browserless-docs/docs/sdk/bap/reference`. TypeDoc reads only the public exports from `src/index.ts`, so low-level GraphQL transport and mutation-builder internals stay out of the published docs.

## Quick Start

```typescript
import Browserless from "@browserless.io/bap-ts";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: "your-api-token",
});

const page = await browser.newPage();
await page.goto("https://example.com");

const title = await page.title();
console.log(title); // "Example Domain"

await browser.close();
```

## Browser & Node

The library is isomorphic. In Node it uses the [`ws`](https://www.npmjs.com/package/ws) package; bundlers targeting the browser (Vite, webpack, esbuild, Rollup) automatically pick the native `WebSocket` build via the `browser` export condition — no `ws` and no Node built-ins end up in the bundle.

Two platform differences:

- `screenshot()` / `pdf()` resolve to a `Uint8Array` (in Node the value is a `Buffer`, which is a `Uint8Array`).
- The `path` option on `screenshot()` / `pdf()` writes to disk in Node only; in the browser it rejects — use the returned bytes instead.

In the browser, take the `Uint8Array` and turn it into something renderable or downloadable with a `Blob`:

```typescript
import { Browserless } from "@browserless.io/bap-ts";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: import.meta.env.VITE_BROWSERLESS_TOKEN,
});

const page = await browser.newPage();
await page.goto("https://example.com");

// No `path` — use the returned bytes directly
const bytes = await page.screenshot({ type: "png" });

// Show it in an <img>
const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));
document.querySelector("img")!.src = url;

// …or trigger a download
const pdf = await page.pdf({ format: "a4" });
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([pdf], { type: "application/pdf" }));
a.download = "page.pdf";
a.click();

await browser.close();
```

> The browser must be able to reach the Browserless WebSocket endpoint directly — make sure your endpoint allows the token and origin you connect from.

## Examples

### Take a Screenshot

```typescript
const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: "your-api-token",
});

const page = await browser.newPage();
await page.goto("https://example.com");

// Returns a Uint8Array, optionally writes to disk (Node only)
await page.screenshot({ path: "screenshot.png" });

// Full-page screenshot as WebP
await page.screenshot({
  path: "full.webp",
  type: "webp",
  fullPage: true,
  quality: 80,
});

await browser.close();
```

### Generate a PDF

```typescript
const page = await browser.newPage();
await page.goto("https://example.com");

await page.pdf({
  path: "page.pdf",
  format: "a4",
  printBackground: true,
  landscape: true,
});

await browser.close();
```

### Fill Out a Form

```typescript
const page = await browser.newPage();
await page.goto("https://example.com/login");

await page.type("#username", "user@example.com");
await page.type("#password", "secret", { delay: [100, 200] });
await page.click("#submit");

await page.waitForNavigation({ waitUntil: "networkIdle" });
console.log(await page.url());

await browser.close();
```

### Query the DOM

```typescript
const page = await browser.newPage();
await page.goto("https://example.com");

// Single element
const heading = await page.$("h1");
console.log(heading?.innerText);

// Multiple elements
const links = await page.$$("a");
for (const link of links) {
  console.log(link.innerHTML);
}

// Extract text from a selector
const text = await page.$eval(".content");
console.log(text);

// Map over multiple elements
const items = await page.$$eval("ul li");
for (const item of items) {
  console.log(item.innerText);
}
```

### Wait for Elements and Navigation

```typescript
const page = await browser.newPage();
await page.goto("https://example.com/spa");

// Wait for an element to appear
await page.waitForSelector(".loaded-content", {
  visible: true,
  timeout: 10000,
});

// Wait for a specific network request
await page.waitForRequest("https://api.example.com/data");

// Wait for a specific response status
await page.waitForResponse({
  url: "https://api.example.com/data",
  statuses: [200],
});
```

### Listen to Page Events

Streaming events use a puppeteer-style event emitter. Attaching the first
listener for `console`, `request`, or `response` lazily opens a GraphQL
subscription (over the `graphql-transport-ws` subprotocol); removing the last
listener closes it. Use `on`, `once`, and `off` just like puppeteer.

```typescript
const page = await browser.newPage();

// Console output as the page emits it
page.on("console", (msg) => {
  console.log(`[${msg.type}] ${msg.text}`);
});

// Every network request as it is issued
page.on("request", (req) => {
  console.log(`${req.method} ${req.url}`);
});

// Every network response as it is received. The response body is NOT fetched
// by default (streaming many bodies is expensive).
page.on("response", (res) => {
  console.log(`${res.status} ${res.url}`);
});

// One-shot listener, then auto-removed
page.once("request", (req) => console.log("first request:", req.url));

// Non-fatal streaming/handshake failures surface as an `error` event
page.on("error", (err) => console.error("event stream error:", err.message));

await page.goto("https://example.com");
```

### Run JavaScript in the Page

```typescript
const page = await browser.newPage();
await page.goto("https://example.com");

// Pass a string
const result = await page.evaluate("document.title");
console.log(result);

// Pass a function
const dims = await page.evaluate(() => {
  return JSON.stringify({
    width: window.innerWidth,
    height: window.innerHeight,
  });
});
console.log(dims);
```

### Set Viewport, Headers, and Cookies

```typescript
const page = await browser.newPage();

// Mobile viewport
await page.setViewport({
  width: 375,
  height: 812,
  mobile: true,
  deviceScaleFactor: 3,
});

// Custom headers
await page.setExtraHTTPHeaders({
  "Accept-Language": "en-US",
  "X-Custom": "value",
});

// Set cookies before navigating
await page.setCookie(
  { name: "session", value: "abc123", domain: ".example.com" },
  { name: "prefs", value: "dark", domain: ".example.com" },
);

await page.goto("https://example.com");

// Read cookies back
const cookies = await page.cookies();
console.log(cookies);
```

### Block Requests

```typescript
const page = await browser.newPage();

// Block images and stylesheets
await page.reject({
  type: ["image", "stylesheet"],
  operator: "or",
});

await page.goto("https://example.com");
```

### Use a Proxy

```typescript
const page = await browser.newPage();

await page.proxy({
  country: "US",
  state: "California",
  sticky: true,
});

await page.goto("https://example.com");
```

### Solve CAPTCHAs

```typescript
const page = await browser.newPage();
await page.goto("https://example.com/protected");

const result = await page.solve({
  type: "cloudflare",
  timeout: 30000,
});

console.log(result.solved); // true
```

### Get a Live URL for Debugging

```typescript
const page = await browser.newPage();
await page.goto("https://example.com");

const { liveURL } = await page.liveURL({
  interactable: true,
  type: "png",
});

console.log(`Watch live: ${liveURL}`);
```

### Reconnect to a Session

```typescript
const page = await browser.newPage();
await page.goto("https://example.com");

const session = await page.reconnect({ timeout: 60000 });
console.log(session.browserWSEndpoint);
// Use this endpoint to connect a new browser to the same session
```

### Bring Your Own Transport

By default, connecting to a WebSocket URL runs through the library's built-in
`WebSocketTransport` (native `WebSocket` in the browser, `ws` in Node). You can
replace it with your own — the same way Puppeteer lets you pass a custom
`transport` — to tunnel BrowserQL frames over a different channel, add logging,
inject auth, or reuse an existing connection.

Pass a `transport` factory to `Browserless.connect`. It's called once per
`newPage()` and must resolve a `ConnectionTransport` that is **already open**.

`browserWSEndpoint` and `transport` are **not two separate connections** — the
endpoint is simply the argument handed to your factory. There are two ways to
use them, and you pick exactly one:

**1. Let the library supply the endpoint.** Set `browserWSEndpoint` (with a
`token`, `?token=…` is appended) and read it as the factory's `url` argument.
Use this to wrap or decorate the default connection — logging, metrics, auth:

```typescript
import Browserless, {
  type ConnectionTransport,
} from "@browserless.io/bap-ts";

// A minimal transport, modeled on puppeteer's ConnectionTransport.
class LoggingTransport implements ConnectionTransport {
  onmessage?: (message: string) => void;
  onclose?: () => void;
  onerror?: (error: unknown) => void;

  private constructor(private readonly ws: WebSocket) {
    ws.addEventListener("message", (e) => this.onmessage?.(String(e.data)));
    ws.addEventListener("close", () => this.onclose?.());
    ws.addEventListener("error", (e) => this.onerror?.(e));
  }

  static create(url: string): Promise<LoggingTransport> {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(url);
      ws.addEventListener("open", () => resolve(new LoggingTransport(ws)), {
        once: true,
      });
      ws.addEventListener("error", reject, { once: true });
    });
  }

  send(message: string): void {
    console.log("→", message);
    this.ws.send(message);
  }

  close(): void {
    this.ws.close();
  }
}

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: "your-api-token",
  transport: (url) => LoggingTransport.create(url), // `url` is the endpoint above
});
```

**2. Let the transport own the connection.** Omit `browserWSEndpoint` — the
factory's `url` is then `undefined`, and your transport connects wherever it
likes (a tunnel, a reused socket, a mock):

```typescript
const browser = Browserless.connect({
  transport: () => MyTransport.create(), // endpoint baked into the transport
});
```

**What happens on each `newPage()`.** The connection is opened lazily — nothing
connects at `Browserless.connect()` time. When you call `browser.newPage()`:

1. The endpoint URL is resolved: `browserWSEndpoint` with `?token=…` appended
   when a `token` is set, or `undefined` if you omitted `browserWSEndpoint`.
2. Your `transport` factory is invoked **once** with that URL (per pattern 1 or
   2 above).
3. The library `await`s the factory, so **it must resolve only once the
   connection is open** — frames are sent the moment it resolves.
4. The library assigns `onmessage`/`onclose`/`onerror` on the returned transport,
   then hands it to the new `Page`. Every BrowserQL operation on that page is
   serialized to a frame and written via your transport's `send`; you deliver
   replies back by calling `onmessage`.

A **fresh transport is created per `newPage()`** — the factory runs again for
each page, so one page maps to one transport (and typically one connection).
Return a **new instance on every call**: the library assigns
`onmessage`/`onclose`/`onerror` on whatever the factory returns, so handing back
the same instance across pages overwrites the earlier pages' handlers and
cross-wires their frames. Share an underlying connection across pages only if
the transport multiplexes frames per page itself.

The built-in `WebSocketTransport` is exported too, so pattern 1 can wrap it
instead of hand-rolling a socket:

```typescript
import { WebSocketTransport } from "@browserless.io/bap-ts";

Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  transport: (url) => WebSocketTransport.create(url),
});
```

### Subscribe to Raw Frames

Every frame the transport receives can be observed with `page.on('message')` —
including server-pushed frames that don't correspond to a request. Pair it with
`page.send()` to drive raw BrowserQL:

```typescript
const page = await browser.newPage();

// Listen for every incoming raw frame
page.on("message", (frame) => {
  console.log("frame:", frame);
});

// `subscribe` is sugar that returns an unsubscribe function
const unsubscribe = page.subscribe((frame) => console.log(frame));

// Send a raw query yourself
await page.send("mutation { goto(url: \"https://example.com\") { status } }");

unsubscribe();
// page.off("message", listener) also removes a listener
```

> BrowserQL is a serial request/response protocol: a pushed frame that arrives
> **while a request is in flight** is consumed as that request's response.
> `on('message')` sees all frames; correlating subscription traffic is up to
> your custom transport.

### Error Handling

```typescript
import Browserless, {
  BrowserQLError,
  ConnectionError,
  TimeoutError,
} from "@browserless.io/bap-ts";

try {
  const browser = Browserless.connect({
    browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
    token: "your-api-token",
  });
  const page = await browser.newPage();
  await page.goto("https://example.com", { timeout: 5000 });
} catch (error) {
  if (error instanceof TimeoutError) {
    console.error("Operation timed out");
  } else if (error instanceof ConnectionError) {
    console.error("WebSocket connection failed");
  } else if (error instanceof BrowserQLError) {
    console.error("GraphQL errors:", error.errors);
  }
}
```

## API Reference

### `Browserless.connect(options)`

Creates a `Browser` instance. The WebSocket connection opens when you call `newPage()`.

| Option              | Type                | Description                                                                              |
| ------------------- | ------------------- | --------------------------------------------------------------------------------------- |
| `browserWSEndpoint` | `string?`           | WebSocket URL (e.g. `wss://production-sfo.browserless.io/chromium/bql`). Optional when a custom `transport` is supplied |
| `token`             | `string?`           | API token                                                                               |
| `timeout`           | `number?`           | Default timeout in ms (default: 30000)                                                   |
| `transport`         | `TransportFactory?` | Override the built-in WebSocket with your own transport — see [Bring Your Own Transport](#bring-your-own-transport) |

### `Browser`

| Method       | Returns         | Description                              |
| ------------ | --------------- | ---------------------------------------- |
| `newPage()`  | `Promise<Page>` | Opens a WebSocket and returns a new Page |
| `getPages()` | `Page[]`        | Returns all non-closed pages             |
| `close()`    | `Promise<void>` | Closes all pages                         |

### `Page`

#### Navigation

| Method                        | Returns                         |
| ----------------------------- | ------------------------------- |
| `goto(url, options?)`         | `Promise<HTTPResponse \| null>` |
| `goBack(options?)`            | `Promise<HTTPResponse \| null>` |
| `goForward(options?)`         | `Promise<HTTPResponse \| null>` |
| `reload(options?)`            | `Promise<HTTPResponse \| null>` |
| `setContent(html, options?)`  | `Promise<HTTPResponse \| null>` |
| `waitForNavigation(options?)` | `Promise<HTTPResponse>`         |

#### Interaction

| Method                           | Returns                   |
| -------------------------------- | ------------------------- |
| `click(selector, options?)`      | `Promise<ClickResponse>`  |
| `hover(options?)`                | `Promise<HoverResponse>`  |
| `scroll(options?)`               | `Promise<ScrollResponse>` |
| `type(selector, text, options?)` | `Promise<TypeResponse>`   |
| `check(selector, options?)`      | `Promise<ClickResponse>`  |
| `uncheck(selector, options?)`    | `Promise<ClickResponse>`  |
| `select(selector, ...values)`    | `Promise<SelectResponse>` |

#### Content

| Method                 | Returns                 |
| ---------------------- | ----------------------- |
| `content()`            | `Promise<string>`       |
| `html(options?)`       | `Promise<HTMLResponse>` |
| `text(options?)`       | `Promise<TextResponse>` |
| `title()`              | `Promise<string>`       |
| `url()`                | `Promise<string>`       |
| `screenshot(options?)` | `Promise<Uint8Array>`       |
| `pdf(options?)`        | `Promise<Uint8Array>`       |

#### Scripts & Styles

| Method                   | Returns                         |
| ------------------------ | ------------------------------- |
| `addScriptTag(options?)` | `Promise<AddScriptTagResponse>` |
| `addStyleTag(options?)`  | `Promise<AddStyleTagResponse>`  |

Pass `url` to load from a URL, or `content` for inline source.

> **Note:** inline `content` must be a **single line**. The BrowserQL server
> rejects multi-line `content` with `SyntaxError: Invalid or unexpected token`.
> For multi-line scripts, host them and use `url`, or collapse the source to one
> line first (e.g. bundle with minification) when it has no newline-sensitive
> syntax such as `//` comments or multi-line template literals.

#### Selectors

| Method                                | Returns                            |
| ------------------------------------- | ---------------------------------- |
| `$(selector, options?)`               | `Promise<ElementHandle \| null>`   |
| `$$(selector, options?)`              | `Promise<ElementHandle[]>`         |
| `$eval(selector)`                     | `Promise<string>`                  |
| `$$eval(selector, options?)`          | `Promise<MapSelectorResponse[]>`   |
| `mapSelector(selector, options?)`     | `Promise<MapSelectorResponse[]>`   |
| `waitForSelector(selector, options?)` | `Promise<WaitForSelectorResponse>` |

#### Wait

| Method                           | Returns                            |
| -------------------------------- | ---------------------------------- |
| `waitForTimeout(ms)`             | `Promise<void>`                    |
| `waitForRequest(urlOrOptions?)`  | `Promise<WaitForRequestResponse>`  |
| `waitForResponse(urlOrOptions?)` | `Promise<WaitForResponseResponse>` |

#### Settings

| Method                          | Returns                        |
| ------------------------------- | ------------------------------ |
| `setUserAgent(ua)`              | `Promise<UserAgentResponse>`   |
| `setViewport(options)`          | `Promise<ViewportResponse>`    |
| `setCookie(...cookies)`         | `Promise<CookieResponse>`      |
| `cookies()`                     | `Promise<StandardCookie[]>`    |
| `setExtraHTTPHeaders(headers)`  | `Promise<HTTPHeadersResponse>` |
| `setJavaScriptEnabled(enabled)` | `Promise<JavaScriptResponse>`  |
| `evaluate(content, options?)`   | `Promise<string \| null>`      |

#### Network

| Method               | Returns                       |
| -------------------- | ----------------------------- |
| `request(options?)`  | `Promise<RequestResponse[]>`  |
| `response(options?)` | `Promise<ResponseResponse[]>` |
| `reject(options?)`   | `Promise<RejectResponse>`     |
| `proxy(options)`     | `Promise<ProxyResponse>`      |

#### Events

Puppeteer-style emitter. Subscription-backed events open a stream lazily on the
first listener and close it on the last removal. See
[Listen to Page Events](#listen-to-page-events).

| Method                     | Returns   | Event payloads                                                                     |
| -------------------------- | --------- | ---------------------------------------------------------------------------------- |
| `on(event, listener)`      | `this`    | `console` → `ConsoleMessage`, `request` → `RequestResponse`, `response` → `ResponseResponse`, `error` → `Error` |
| `once(event, listener)`    | `this`    | one-shot variant of `on`                                                           |
| `off(event, listener)`     | `this`    | remove a listener                                                                  |
| `removeAllListeners(event?)` | `this`  | remove all listeners for an event, or all events                                   |

#### Session

| Method                       | Returns                                 |
| ---------------------------- | --------------------------------------- |
| `reconnect(options?)`        | `Promise<ReconnectionResponse>`         |
| `liveURL(options?)`          | `Promise<LiveURLResponse>`              |
| `stopSessionRecording()`     | `Promise<StopSessionRecordingResponse>` |
| `switchToWindow(options?)`   | `Promise<SwitchWindowResponse>`         |
| `solve(options?)`            | `Promise<CaptchaResponse>`              |
| `solveImageCaptcha(options)` | `Promise<CaptchaResponse>`              |
| `close()`                    | `Promise<void>`                         |

#### Events

Raw-frame listeners over the underlying transport (see [Subscribe to Raw Frames](#subscribe-to-raw-frames)).

| Method                       | Returns      | Description                                       |
| ---------------------------- | ------------ | ------------------------------------------------- |
| `on('message', listener)`    | `this`       | Listen for every raw frame the transport receives |
| `off('message', listener)`   | `this`       | Remove a listener added with `on`                 |
| `subscribe(listener)`        | `() => void` | Like `on('message')`, returns an unsubscribe fn   |
| `send(query, options?)`      | `Promise<T>` | Send a raw BrowserQL query and await the response |

### `ElementHandle`

| Property / Method      | Type                     |
| ---------------------- | ------------------------ |
| `innerHTML`            | `string \| null`         |
| `innerText`            | `string \| null`         |
| `id`                   | `string \| null`         |
| `className`            | `string \| null`         |
| `localName`            | `string \| null`         |
| `outerHTML`            | `string \| null`         |
| `childElementCount`    | `number \| null`         |
| `click(options?)`      | `Promise<ClickResponse>` |
| `hover()`              | `Promise<void>`          |
| `type(text, options?)` | `Promise<TypeResponse>`  |
| `screenshot(options?)` | `Promise<Uint8Array>`        |
| `textContent()`        | `Promise<string>`        |

## Browser Automation Protocol vs Puppeteer

Browser Automation Protocol's `Page` class borrows Puppeteer's naming conventions so the API feels familiar, but there are important differences in architecture, scope, and behavior.

### Architecture

Puppeteer controls a local Chrome instance over the Chrome DevTools Protocol (CDP) via a WebSocket. Browser Automation Protocol is a **GraphQL client** — every method builds a mutation, sends it over a single WebSocket to the Browserless BQL server, and waits for the response. There is no direct CDP connection. This means a massive reduction in the amount and size of messages being sent over the network, making scripting performance much faster.

Because this client builds on top of our BrowserQL service, it also means all the languages we support are (mostly) compiled from the BrowserQL mutation specification. This means all clients are treated equally, operate the same, and will perform similarly.

### Shared Methods

These methods exist in both libraries with similar signatures:

| Category    | Methods                                                                                              |
| ----------- | ---------------------------------------------------------------------------------------------------- |
| Navigation  | `goto`, `goBack`, `goForward`, `reload`, `setContent`, `waitForNavigation`                           |
| Interaction | `click`, `type`, `select`, `hover`                                                                   |
| Content     | `content`, `title`, `url`, `screenshot`, `pdf`                                                       |
| Selectors   | `$`, `$$`, `waitForSelector`                                                                         |
| Evaluate    | `evaluate`                                                                                           |
| Settings    | `setUserAgent`, `setViewport`, `setCookie`, `cookies`, `setExtraHTTPHeaders`, `setJavaScriptEnabled` |
| Wait        | `waitForTimeout`, `waitForRequest`, `waitForResponse`                                                |
| Lifecycle   | `close`                                                                                              |

### Complete Method Reference

Every `Page` method and its source BrowserQL mutation. This table is generated from `src/mutations.graphql` by `npm run codegen`, so it stays in sync with the schema:

<!-- AUTOGEN:method-table START -->

| Method | Source mutation | Returns | Puppeteer-named |
| ------ | --------------- | ------- | --------------- |
| `$` | `querySelector` | `ElementHandle \| null` | ✅ |
| `$$` | `querySelectorAll` | `ElementHandle[]` | ✅ |
| `$$eval` | `mapSelector` | `MapSelectorResponse[]` | ✅ |
| `$eval` | `text` | `string` | ✅ |
| `addScriptTag` | `addScriptTag` | `AddScriptTagResponse` | ✅ |
| `addStyleTag` | `addStyleTag` | `AddStyleTagResponse` | ✅ |
| `authenticate` | `authenticate` | `HTTPResponse \| null` | — |
| `check` | `checkbox` | `ClickResponse` | — |
| `click` | `click` | `ClickResponse` | ✅ |
| `close` | `—` | `void` | ✅ |
| `content` | `html` | `string` | ✅ |
| `cookies` | `cookies` | `StandardCookie[]` | ✅ |
| `emulateMediaType` | `emulateMediaType` | `EmulateMediaTypeResponse` | ✅ |
| `evaluate` | `evaluate` | `string \| null` | ✅ |
| `fulfill` | `fulfill` | `FulfillResponse` | — |
| `goBack` | `back` | `HTTPResponse \| null` | ✅ |
| `goForward` | `forward` | `HTTPResponse \| null` | ✅ |
| `goto` | `goto` | `HTTPResponse \| null` | ✅ |
| `hover` | `hover` | `HoverResponse` | ✅ |
| `html` | `html` | `HTMLResponse` | — |
| `liveURL` | `liveURL` | `LiveURLResponse` | — |
| `loadSecret` | `loadSecret` | `LoadSecretResponse` | — |
| `mapSelector` | `mapSelector` | `MapSelectorResponse[]` | — |
| `markdown` | `markdown` | `MarkdownResponse` | — |
| `pdf` | `pdf` | `Uint8Array` | ✅ |
| `preferences` | `preferences` | `DefaultResponse` | — |
| `proxy` | `proxy` | `ProxyResponse` | — |
| `reconnect` | `reconnect` | `ReconnectionResponse` | — |
| `reject` | `reject` | `RejectResponse` | — |
| `reload` | `reload` | `HTTPResponse \| null` | ✅ |
| `request` | `request` | `RequestResponse[]` | — |
| `response` | `response` | `ResponseResponse[]` | — |
| `screenshot` | `screenshot` | `Uint8Array` | ✅ |
| `scroll` | `scroll` | `ScrollResponse` | — |
| `select` | `select` | `SelectResponse` | ✅ |
| `send` | `—` | `T` | — |
| `setContent` | `content` | `HTTPResponse \| null` | ✅ |
| `setCookie` | `cookies` | `CookieResponse` | ✅ |
| `setExtraHTTPHeaders` | `setExtraHTTPHeaders` | `HTTPHeadersResponse` | ✅ |
| `setJavaScriptEnabled` | `javaScriptEnabled` | `JavaScriptResponse` | ✅ |
| `setUserAgent` | `userAgent` | `UserAgentResponse` | ✅ |
| `setViewport` | `viewport` | `ViewportResponse` | ✅ |
| `solve` | `solve` | `CaptchaResponse` | — |
| `solveImageCaptcha` | `solveImageCaptcha` | `CaptchaResponse` | — |
| `stopSessionRecording` | `stopSessionRecording` | `StopSessionRecordingResponse` | — |
| `switchToWindow` | `switchToWindow` | `SwitchWindowResponse` | — |
| `text` | `text` | `TextResponse` | — |
| `title` | `title` | `string` | ✅ |
| `type` | `type` | `TypeResponse` | ✅ |
| `uncheck` | `checkbox` | `ClickResponse` | — |
| `url` | `url` | `string` | ✅ |
| `waitForEvent` | `waitForEvent` | `WaitForEvent` | — |
| `waitForFunction` | `waitForFunction` | `WaitForFunction` | ✅ |
| `waitForNavigation` | `waitForNavigation` | `HTTPResponse` | ✅ |
| `waitForNetworkIdle` | `waitForNetworkIdle` | `WaitForTimeoutResponse` | — |
| `waitForRequest` | `waitForRequest` | `WaitForRequestResponse` | ✅ |
| `waitForResponse` | `waitForResponse` | `WaitForResponseResponse` | ✅ |
| `waitForSelector` | `waitForSelector` | `WaitForSelectorResponse` | ✅ |
| `waitForTimeout` | `waitForTimeout` | `void` | ✅ |
| `watchEvent` | `watchEvent` | `WaitForEvent` | — |

<!-- AUTOGEN:method-table END -->

### Behavioral Differences

| Method                               | Puppeteer                                                                                      | Browser Automation Protocol                                                                                        |
| ------------------------------------ | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `$eval(selector, fn)`                | Runs a function in the browser with the matched element as the argument and returns the result | Returns the text content of the matched selector (no function argument)                         |
| `$$eval(selector, fn)`               | Runs a function in the browser with all matched elements as an array argument                  | Delegates to `mapSelector` — returns structured `MapSelectorResponse[]` with element properties |
| `scroll()`                           | Not available on Page — you use `mouse.wheel()` or `evaluate`                                  | First-class method with selector targeting and coordinate support                               |
| `evaluate(fn, ...args)`              | Passes serialized arguments to the function and returns deserialized results                   | Accepts a string or function but always returns `string \| null` — no argument passing          |
| `waitForRequest` / `waitForResponse` | Accept a URL string or a predicate function                                                    | Accept a URL string or an options object (no predicate functions)                               |

### Not in Browser Automation Protocol

Browser Automation Protocol does not include Puppeteer's event system or lower-level primitives. These might be added in a later release:

- **Events** — no per-domain event listeners (`page.on('request' | 'response' | 'console' | 'dialog' | ...)`). There _is_ a raw-frame listener — `page.on('message', …)` / `page.subscribe(…)` — for observing the underlying transport (see [Subscribe to Raw Frames](#subscribe-to-raw-frames))
- **Input devices** — no `page.keyboard`, `page.mouse`, `page.touchscreen`
- **Frames** — no `page.frames()`, `page.mainFrame()`, or frame targeting
- **Workers** — no `page.workers()`
- **Function exposure** — no `exposeFunction()`
- **Emulation** — no `emulate()` or `emulateCPUThrottling()` (`emulateMediaType()` _is_ supported)
- **Security/Cache** — no `setBypassCSP()`, `setCacheEnabled()`, `setOfflineMode()`
- **Coverage/Tracing** — no `page.coverage`, `page.tracing`
- **Accessibility** — no `page.accessibility`

### Browser Automation Protocol-Only Features

These methods have no equivalent in Puppeteer:

| Method                                     | Description                                                   |
| ------------------------------------------ | ------------------------------------------------------------- |
| `html(options?)`                           | Extract HTML with optional selector targeting and cleaning    |
| `text(options?)`                           | Extract text with optional selector targeting and cleaning    |
| `check(selector)` / `uncheck(selector)`    | Checkbox helpers                                              |
| `mapSelector(selector, options?)`          | Map over matched elements and return structured properties    |
| `reject(options?)`                         | Block network requests by type, URL, or method                |
| `proxy(options)`                           | Route traffic through a proxy with geo-targeting              |
| `solve(options?)`                          | Solve CAPTCHAs (Cloudflare, reCAPTCHA, etc.)                  |
| `solveImageCaptcha(options)`               | Solve image-based CAPTCHAs with selector targeting            |
| `liveURL(options?)`                        | Get a shareable live-view URL for debugging                   |
| `reconnect(options?)`                      | Get a new WebSocket endpoint to reconnect to the same session |
| `switchToWindow(options?)`                 | Switch between browser tabs/windows                           |
| `stopSessionRecording()`                   | Stop recording the current session                            |
| `preferences(options?)`                    | Dismiss cookie banners and preference dialogs                 |
| `request(options?)` / `response(options?)` | Query captured network traffic with filters                   |

## How It Works

Browser Automation Protocol communicates with Browserless over a single WebSocket connection per page. Each method call constructs a GraphQL mutation, sends it as a JSON message, and waits for the response. Operations are queued and executed serially, matching BrowserQL's server-side concurrency model. Browser and page state persists across calls on the same connection.
