> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.doc-detective.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.doc-detective.com/_mcp/server.

# Test across multiple tabs, windows, and browsers

> Open, address, and close browser tabs, windows, and whole browsers in one test with goTo, the surface field, and closeSurface.

Documented procedures often span more than one tab. Think of "click the link, which opens the invoice in a new tab." Or "keep the admin console open in a second window while you work." Some span more than one browser. Consider "log in as the shopper in one browser and approve the order from the admin account in another." Doc Detective tests follow those procedures exactly. Use any number of browsers, each with any number of tabs and windows. All are addressable by name, position, or page content.

Three pieces work together:

* **[`goTo`](/docs/actions/goto) opens tabs and windows** with `newTab` / `newWindow`. It's the only action that opens tabs and windows you can address, so name what you open.
* **The `surface` field addresses them.** Every browser action (`find`, `click`, `type`, `dragAndDrop`, `runBrowserScript`, `screenshot`, `record`, `goTo`) accepts a `surface` that says *which* window/tab to act on. Omit it to act on the active tab, exactly as before.
* **[`closeSurface`](/docs/actions/closesurface) closes them** at tab or window granularity. Closing something that's already closed passes, so cleanup steps are safe to repeat.

## Open and name tabs and windows

```json
{
  "tests": [
    {
      "steps": [
        { "goTo": "https://my-app.com" },
        {
          "description": "Open the checkout in a new tab named 'cart'.",
          "goTo": { "url": "/checkout", "newTab": "cart" }
        },
        {
          "description": "Open the admin console in a new window; name its first tab too.",
          "goTo": {
            "url": "/admin",
            "newWindow": { "name": "admin", "tab": "overview" }
          }
        }
      ]
    }
  ]
}
```

Both openers focus what they open, take `true` (anonymous), a name string, or an object form, and are mutually exclusive on one step. Names must be unique within the browser and survive later navigation.

## Address a tab or window

`surface` is the same shape on every action: the engine plus optional `window` and `tab` selectors.

```json
{ "find": { "elementText": "Order #1234", "surface": { "browser": "chrome", "tab": "cart" } } }
```

`window` and `tab` share one selector grammar:

| Form                                              | Meaning                                    |
| ------------------------------------------------- | ------------------------------------------ |
| *(omitted)*                                       | The active window / active tab             |
| `"cart"`                                          | The window/tab with that assigned name     |
| `0`, `1`, …                                       | By index in creation order                 |
| `-1`                                              | The newest (negative counts from the end)  |
| `{ "title": "/Invoice/" }`                        | By page title, as a substring or `/regex/` |
| `{ "url": "checkout" }`                           | By page URL, as a substring or `/regex/`   |
| `{ "name": …, "index": …, "title": …, "url": … }` | Combined criteria that must all match      |

Two behaviors to know:

* **Focus follows the surface.** A step that names a `surface` leaves that tab focused, so following surface-less steps act there too. That's also how you point surface-less concerns at a tab: focus it with any surfaced step first.
* **Tabs the page opens are addressable too.** A `target="_blank"` link or `window.open` call creates a tab Doc Detective didn't open. It has no name, but you can reach it by `title`, `url`, or index (`-1` is handy right after the click). You don't need a manual [`wait`](/docs/actions/wait) step first. A selector might not match yet, because the tab doesn't exist or its title and URL haven't finished loading. Doc Detective retries for up to 2 seconds before failing, so addressing it in the very next step works.

```json
{
  "steps": [
    { "click": { "elementText": "View invoice" } },
    {
      "find": {
        "elementText": "Invoice total",
        "surface": { "browser": "chrome", "tab": { "title": "/Invoice/" } }
      }
    }
  ]
}
```

The tab you're targeting might genuinely not exist, from a typo in the name or a step that never opens what you expect. This 2-second retry is then how long that step takes to fail. It's the same trade-off as any other Doc Detective timeout: bounded and predictable, not indefinite.

## Type with readiness conditions

`type` into a browser surface can wait for the page to react to the keystrokes. These are the same conditions `goTo` uses after navigation, but nothing applies by default:

```json
{
  "type": {
    "keys": ["doc detective", "$ENTER$"],
    "selector": "#search",
    "surface": { "browser": "chrome", "tab": "cart" },
    "waitUntil": { "find": { "selector": ".results" } },
    "timeout": 10000
  }
}
```

`waitUntil` on a browser surface accepts `networkIdleTime`, `domIdleTime`, and `find`. See the [`type`](/reference/schemas/type) reference.

## Close tabs and windows

```json
{ "closeSurface": { "browser": "chrome", "tab": "cart" } }
{ "closeSurface": { "browser": "chrome", "window": "admin" } }
{ "closeSurface": { "browser": "chrome", "tab": -1 } }
```

A `tab` reference closes that tab. A `window` reference closes the window and its tabs. Closing something already closed is a PASS no-op, as is closing something that never matched after the brief retry described above. Cleanup steps are therefore safe to repeat. Doc Detective refuses to close the last open tab, because that would end the browser session the test run owns.

## Drive multiple browsers at once

A test can hold several browsers open at the same time. That might be a second window of the same engine for a different account, or a different engine entirely. Like tabs and windows, **only `goTo` opens browsers**. A `goTo` whose `surface` names an unopened browser launches and navigates it. It uses the same headless setting as the test's default browser. Every other action requires the browser to already be open, and fails pointing you at `goTo`.

```json
{
  "steps": [
    { "goTo": "https://my-app.com" },
    {
      "description": "Open a SECOND browser session, named 'admin'.",
      "goTo": {
        "url": "https://my-app.com/admin",
        "surface": { "browser": "firefox", "name": "admin" }
      }
    },
    {
      "description": "A bare engine keyword opens (or selects) a browser of that engine.",
      "goTo": { "url": "https://my-app.com/checkout", "surface": "chrome" }
    },
    {
      "description": "Act in a specific browser — session, window, and tab selectors compose.",
      "find": {
        "elementText": "Pending approval",
        "surface": { "browser": "firefox", "name": "admin", "tab": 0 }
      }
    },
    {
      "description": "Close a whole browser by name (or engine keyword).",
      "closeSurface": "admin"
    }
  ]
}
```

How references resolve:

* The test's **default browser registers under its engine name** (`"chrome"`, `"firefox"`, …), so `surface: "firefox"` selects it like any named surface. A browser opened with `name` is addressed by that name; without one, it defaults to its engine name.
* **Focus follows the surface across browsers too.** A step that names a browser leaves it active, so following surface-less steps act there. The *active surface* is always the most recently opened or focused browser.
* **`closeSurface` with no `window` or `tab` selector closes the whole browser.** Use a bare name, an engine keyword, or `{ "browser": …, "name": … }`. Closing an unopened browser is a PASS no-op, and focus falls back to the most recently focused surviving browser. A browser with an active recording refuses to close until you `stopRecord`.

Two caveats:

* **Pin a browser OR fan out, but not both.** Cross-browser coverage for browser-*agnostic* tests still comes from the [`runOn` matrix](/docs/test-docs/platforms-and-browsers), which runs the whole test once per engine. A test that pins `surface: { "browser": "firefox" }` inside its steps should pin a single `runOn` browser too. Otherwise it opens Firefox alongside every matrix engine.
* **Safari allows one automation session at a time** (a `safaridriver` limit), so a second concurrent Safari isn't available; pair Safari with other engines instead.

## Current limits

Native app surfaces launch through [`startSurface`](/docs/actions/startsurface) on Windows and macOS. Opening several surfaces at once with the array form of `startSurface` lands in a later phase of the [multi-surface design](https://github.com/doc-detective/doc-detective/blob/main/docs/design/multi-surface-targeting.md).

Indexes reflect **creation order** (the order Doc Detective first saw each tab), not on-screen tab order.

Recording with the default (ffmpeg) engine captures by window title, which can't tell two same-engine browsers apart. Start the recording before opening a same-engine twin, or record with the browser engine, which is always per-session.