Answer
Test Isolation with Workers in Playwright
The Worker Model
CODE
CI Machine
├── Worker Process 1 (Node.js process)
│ ├── Browser 1 (Chromium)
│ │ ├── Context A → test-file-1.spec.ts
│ │ └── Context B → test-file-2.spec.ts
├── Worker Process 2 (Node.js process)
│ └── Browser 2 (Chromium)
│ └── Context C → test-file-3.spec.ts
└── Worker Process 3 (Node.js process)
└── Browser 3 (Chromium)
└── Context D → test-file-4.spec.ts
How Isolation Works
- ✓Each test gets a fresh BrowserContext with:
- ✓New cookies (empty)
- ✓Clean localStorage (empty)
- ✓No auth state
- ✓No shared state from other tests
- ✓Tests in the same file run in the same worker (by default) but each test still gets a new context
- ✓Tests across files can run in different workers simultaneously
Isolation at the Test Level
TypeScript
// Each test gets fresh isolation automatically
test('test A — sets cookie "foo"', async ({ page, context }) => {
await context.addCookies([{ name: 'foo', value: 'bar', domain: 'localhost', path: '/' }]);
// This cookie ONLY exists for this test
});
test('test B — cannot see test A cookies', async ({ page, context }) => {
const cookies = await context.cookies();
// cookies is EMPTY — test A's cookies are gone
expect(cookies).toHaveLength(0);
});
Worker-Level Fixtures (Shared Within Worker)
TypeScript
// Fixtures with scope:'worker' are shared across tests in the same worker
export const test = base.extend({
sharedAuth: [async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/login');
await page.fill('#email', 'user@test.com');
await page.click('#submit');
await context.storageState({ path: 'worker-auth.json' });
await page.close();
await use('worker-auth.json');
await context.close();
}, { scope: 'worker' }],
});
Checking Worker Index
TypeScript
import { test } from '@playwright/test';
test('which worker am I?', async ({}, testInfo) => {
console.log(`Worker index: ${testInfo.workerIndex}`); // 0, 1, 2, ...
console.log(`Parallel index: ${testInfo.parallelIndex}`);
});
Avoid Shared State Between Tests
TypeScript
// BAD — global variable shared between tests
let userId = '';
test.beforeAll(async ({ request }) => {
const res = await request.post('/api/users', { data: { name: 'Test' } });
userId = (await res.json()).id; // DANGEROUS in parallel
});
// GOOD — create test-specific data per test
test('each test creates its own user', async ({ request }) => {
const res = await request.post('/api/users', {
data: { name: `user-${Date.now()}-${Math.random()}` },
});
const { id } = await res.json();
// Use id in THIS test only
});
Worker Count & Resource Impact
TypeScript
// playwright.config.ts
export default defineConfig({
workers: 4, // 4 browser instances running simultaneously
fullyParallel: true, // All tests across all files run in parallel
});
workers: 4 = 4 browser processes = 4x CPU/memory. Balance with available CI resources.
