feat: add t.openIsolatedSession() for multi-user e2e tests via CDP by sam-yh · Pull Request #8492 · DevExpress/testcafe
Note: This feature was developed with the assistance of Claude Opus 4.6 (Anthropic) and should be considered "vibe coded." We have been running it in production for our multi-user e2e test suite and all 40 functional tests pass, but the codebase has not undergone traditional peer review beyond AI-assisted iteration. We welcome feedback on the implementation approach and are happy to iterate.
Summary
Adds t.openIsolatedSession() — the ability to open fully isolated browser sessions within a single test. Each session gets its own Chrome browser context (separate cookies, localStorage, sessionStorage, service workers) via CDP's Target.createBrowserContext().
This enables multi-user e2e tests: doctor + patient in a video call, two users editing the same document, concurrent booking scenarios — all within a single test.
Motivation
Currently, testing multi-user scenarios requires either:
- Running two separate TestCafe instances and coordinating them externally
- Using Role switching (which shares the same browser context — cookies bleed between users)
- Manual browser automation outside TestCafe
This PR provides a native solution: t.openIsolatedSession() returns a second test controller (t2) backed by a completely isolated Chrome browser context.
API
test('Two users interact', async t => { const t2 = await t.openIsolatedSession(); await t2.navigateTo('https://example.com'); // Each session has separate cookies, storage, DOM await t.eval(() => { document.cookie = 'user=alice'; }); await t2.eval(() => { document.cookie = 'user=bob'; }); // t2 supports most TestCafe commands await t2.click('#btn'); await t2.typeText('#input', 'text'); await t2.expect(true).ok(); // Selector chaining works await t2.click(Selector('.btn').withText('Save')); // t2.run() makes Selector/ClientFunction evaluate in the isolated tab await t2.run(async () => { await t.expect(Selector('#element').visible).ok(); }); // Automatic cleanup when test ends });
Supported commands
Command-based: click, rightClick, doubleClick, hover, drag, dragToElement, typeText, pressKey, selectText, scroll, scrollBy, scrollIntoView, dispatchEvent, navigateTo, wait, eval, expect, useRole, getCookies, setCookies, deleteCookies
Direct session methods: setHttpAuth, setWindowBounds, takeScreenshot, takeElementScreenshot, maximizeWindow, resizeWindow, setFilesToUpload, setPageLoadTimeout, switchToIframe, switchToMainWindow
Selector chaining: withText, withExactText, filterVisible, filterHidden, nth, find, parent, child, sibling, nextSibling, prevSibling, withAttribute
Requirements
- Native Automation mode (
--experimental-multiple-windowsornativeAutomation: true) - Chrome only (uses CDP
Target.createBrowserContext)
Architecture
Three new files, six modified files:
New files:
src/test-run/isolated-session.ts— Core runtime class. Holds CDP client, executes commands via CDPsrc/api/test-controller/isolated.js— Thet2controller. Mirrors TestController's delegatedAPI patternsrc/native-automation/isolated-window.ts— Thin NativeAutomationBase wrapper for isolated tabs
Modified files:
src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts—createIsolatedContext(),disposeIsolatedContext()src/browser/provider/built-in/dedicated/chrome/index.js—createIsolatedSession(),disposeIsolatedSession()src/api/test-controller/index.js—_openIsolatedSession$()methodsrc/test-run/index.ts— Isolated session lifecycle (create, dispose, find)src/client-functions/selectors/selector-builder.js—counterMode,getVisibleValueModepropertiessrc/test-run/commands/execute-client-function.js— Same two properties
Key design decisions:
- No TestCafe driver injection — all commands execute via CDP directly
- No hammerhead proxy — isolated tab is a raw Chrome tab
- Separate command queue — t2 has its own execution chain
- Automatic cleanup — sessions disposed in
TestRun._done()
Tests
40 functional tests in test/functional/fixtures/isolated-sessions/:
| Suite | Tests | Coverage |
|---|---|---|
| Basic Isolation | 6 | Cookie, localStorage, sessionStorage, DOM, multiple sessions, cleanup |
| Commands | 14 | click, typeText, hover, doubleClick, pressKey, navigateTo, scroll, eval, wait, expect, dispatchEvent |
| Selector Chaining | 7 | Selector object, withText, withExactText, nth, filterVisible, find, withAttribute |
| t2.run() | 7 | Selector.exists/visible/innerText, ClientFunction, session restore, sequential runs |
| Cookies | 3 | document.cookie isolation, getCookies, deleteCookies |
| Iframe | 3 | switchToIframe eval, interact via eval, switchToMainWindow |
| Screenshots | 2 | takeScreenshot, takeElementScreenshot |
| File Upload | 1 | setFilesToUpload |
| Window Management | 3 | maximizeWindow, resizeWindow, setPageLoadTimeout |
Run tests:
npx http-server test/functional -p 3000 -s & node lib/cli/index.js --experimental-multiple-windows "chrome:headless" test/functional/fixtures/isolated-sessions/testcafe-fixtures/basic-isolation-test.js
Documentation
Since TestCafe stores documentation in a private repo, here is the documentation for this feature:
t.openIsolatedSession() → IsolatedTestController
Opens a new browser window in a fully isolated Chrome browser context. Returns an IsolatedTestController (commonly named t2) with a subset of the standard TestCafe API.
Requires: Native Automation mode.
The isolated session has completely separate cookies, localStorage, sessionStorage, and service workers from the main session and from other isolated sessions.
Sessions are automatically cleaned up when the test ends. The CDP WebSocket is closed and Target.disposeBrowserContext() destroys the context.
t2.run(callback)
Executes a callback where Selector queries and ClientFunction evaluations target the isolated session's CDP tab. Inside the callback, Selector.exists, Selector.visible, Selector.innerText, and ClientFunction all evaluate in the isolated tab. Action commands (click, typeText, etc.) still require using t2 directly.
await t2.run(async () => { // Selectors evaluate in the isolated tab await t.expect(Selector('#element').visible).ok(); // Actions use t2 directly await t2.click('#element'); });