Skip to navigation

Test across multiple tabs, windows, and browsers

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

{
"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.

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

window and tab share one selector grammar:

FormMeaning
(omitted)The active window / active tab
"cart"The window/tab with that assigned name
0, 1, …By index in creation order
-1The 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 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.
{
"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:

{
"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.

Close tabs and windows

{ "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.

{
"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, 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 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.

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.