Test Isolation
Why Test Isolation Matters
Production test suites require complete isolation between tests to prevent false failures, flaky tests, and unpredictable test behavior. Without proper isolation, one test’s actions contaminate other tests through shared cookies, localStorage, session state, or cached data, leading to intermittent failures that waste developer time and erode confidence in test results.
Playwright test isolation ensures each test executes in a clean environment independent of other tests, enabling reliable parallel execution, deterministic test outcomes, and confident CI/CD integration. Proper isolation prevents state leakage, allows safe concurrent test execution, and ensures test results remain consistent regardless of execution order.
Insufficient test isolation causes:
- Flaky tests: Random failures from state contamination between tests
- Serial execution: Cannot parallelize tests due to shared state dependencies
- Debug nightmares: Failures only reproduce when specific tests run in specific order
- False positives/negatives: Tests pass/fail based on previous test execution state
Standard Library Approach: Manual Context Creation
Playwright core API provides browser context creation for basic test isolation without requiring isolation frameworks.
Basic browser context per test:
import { test, expect, chromium } from "@playwright/test";
// => Import Playwright test and browser launcher
// => chromium provides Chrome/Edge browser
// => No isolation framework needed
test("first test with isolated context", async () => {
// => No page fixture provided
// => Must manually manage browser lifecycle
// => Manual cleanup required
const browser = await chromium.launch();
// => Launch browser instance
// => Browser process started
// => Shared across multiple contexts
const context = await browser.newContext();
// => Create isolated browser context
// => Separate cookie jar, localStorage, sessionStorage
// => Independent from other contexts
const page = await context.newPage();
// => Create page within context
// => Page inherits context isolation
// => Cookies/storage isolated from other pages
await page.goto("https://example.com");
// => Navigate to test URL
// => No previous navigation history
// => Clean browser state
await page.evaluate(() => {
// => Execute JavaScript in page context
// => Modify page state
localStorage.setItem("key1", "value1");
// => Set localStorage item
// => Stored in this context only
// => Other contexts don't see this value
});
const value = await page.evaluate(() => localStorage.getItem("key1"));
// => Retrieve localStorage item
// => Returns "value1"
expect(value).toBe("value1");
// => Assert value correct
// => Test passes
await browser.close();
// => Close browser instance
// => Destroys all contexts and pages
// => Cleanup required to prevent resource leak
});
test("second test with separate isolated context", async () => {
// => Second test runs independently
// => Must create browser again
// => Previous test's browser closed
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// => New browser, context, page created
// => Completely independent from first test
// => No shared state
await page.goto("https://example.com");
// => Navigate to same URL
// => Clean slate (no previous state)
// => No localStorage from first test
const value = await page.evaluate(() => localStorage.getItem("key1"));
// => Retrieve localStorage item
// => Returns null (not "value1")
// => First test's localStorage not accessible
expect(value).toBeNull();
// => Assert no value exists
// => Confirms isolation between tests
// => Tests don't share storage
await browser.close();
// => Manual cleanup
// => Must remember to close
// => Resource leak if forgotten
});Verifying cookie isolation:
test("cookie isolation between contexts", async () => {
const browser = await chromium.launch();
// => Launch browser for cookie test
// => Multiple contexts will share browser process
// => But have separate cookie jars
const context1 = await browser.newContext();
// => First isolated context
// => Empty cookie jar
// => No cookies set
const page1 = await context1.newPage();
await page1.goto("https://example.com");
// => Navigate in first context
// => May receive cookies from server
// => Cookies stored in context1 jar
await context1.addCookies([
{
name: "session",
value: "abc123",
domain: "example.com",
path: "/",
},
]);
// => Manually add cookie to context1
// => Cookie stored in context1 jar only
// => context2 won't have this cookie
const context2 = await browser.newContext();
// => Second isolated context
// => Separate cookie jar
// => Empty (no cookies from context1)
const page2 = await context2.newPage();
await page2.goto("https://example.com");
// => Navigate in second context
// => No session cookie sent to server
// => Different browser state
const cookies1 = await context1.cookies();
// => Get cookies from context1
// => Returns [{ name: "session", value: "abc123", ... }]
const cookies2 = await context2.cookies();
// => Get cookies from context2
// => Returns [] (empty array)
// => Confirms cookie isolation
expect(cookies1).toHaveLength(1);
expect(cookies2).toHaveLength(0);
// => Assert contexts have different cookies
// => Isolation verified
await browser.close();
// => Close browser and all contexts
// => Manual cleanup required
});Limitations for production test isolation:
- Manual lifecycle: Must remember to create and close browser/context (error-prone)
- Resource leaks: Forgotten cleanup leaves zombie browser processes
- No automatic cleanup: Test failures leave resources unclosed
- Verbose boilerplate: Every test repeats browser/context creation code
- No parallel safety: Browser management not thread-safe without framework
- No fixture reuse: Cannot share setup logic across tests
Production Framework: Playwright Test Fixtures with Auto-Isolation
Playwright Test framework provides automatic browser context isolation through fixtures, eliminating manual lifecycle management and ensuring cleanup.
Automatic context isolation with fixtures:
import { test, expect } from "@playwright/test";
// => Import Playwright Test framework (NOT just @playwright/test core)
// => Provides test fixture system
// => Automatic browser context management
test("first test automatically isolated", async ({ page }) => {
// => page fixture injected by framework
// => Fresh browser context created for THIS test
// => Automatic cleanup after test completes
await page.goto("https://example.com");
// => Navigate to test URL
// => Clean browser state (no previous cookies/localStorage)
// => Context created just for this test
await page.evaluate(() => {
localStorage.setItem("key1", "value1");
// => Set localStorage in this test's context
// => Stored in isolated context
// => Other tests won't see this value
});
const value = await page.evaluate(() => localStorage.getItem("key1"));
expect(value).toBe("value1");
// => Assert value set correctly
// => Test passes
// => NO manual cleanup needed
// => Framework automatically closes context after test
// => Even if test fails, cleanup happens
});
test("second test automatically isolated", async ({ page }) => {
// => NEW page fixture injected
// => DIFFERENT browser context from first test
// => Complete isolation guaranteed
await page.goto("https://example.com");
// => Navigate to same URL
// => Fresh context (no state from first test)
// => No localStorage from previous test
const value = await page.evaluate(() => localStorage.getItem("key1"));
// => Retrieve localStorage
// => Returns null (first test's value not accessible)
// => Confirms isolation between tests
expect(value).toBeNull();
// => Assert no value exists
// => Tests completely independent
// => No state leakage
// => Automatic cleanup
// => Framework closes this context too
// => No resource leaks
});
test("tests can run in parallel safely", async ({ page }) => {
// => This test can run concurrently with others
// => Each test has separate browser context
// => No shared state to cause race conditions
await page.goto("https://example.com");
// => Navigate independently
// => Other tests' actions don't affect this navigation
// => Isolated network requests
await page.evaluate(() => {
localStorage.setItem("parallel", "test");
document.cookie = "testcookie=value";
// => Modify localStorage and cookies
// => Changes isolated to this test's context
// => Concurrent tests unaffected
});
// => Test completes
// => Context closed automatically
// => Other parallel tests continue unaffected
});Context isolation verification:
import { test, expect } from "@playwright/test";
// => Playwright Test framework
// => Automatic context management
// => Isolation verified by framework
test.describe("context isolation verification", () => {
// => Test suite for isolation verification
// => All tests in suite run independently
// => Each gets fresh context
test("test A sets data", async ({ page }) => {
// => First test in suite
// => Fresh context provided
// => No previous state
await page.goto("https://example.com");
await page.evaluate(() => {
localStorage.setItem("testA", "dataA");
sessionStorage.setItem("testA", "sessionA");
document.cookie = "testA=cookieA";
// => Set all storage types
// => localStorage, sessionStorage, cookies
// => All isolated to this test's context
});
// => Test completes
// => Context closed with all data
// => Data destroyed (not accessible to other tests)
});
test("test B cannot access test A data", async ({ page }) => {
// => Second test in suite
// => NEW context (independent of test A)
// => Previous test's data not accessible
await page.goto("https://example.com");
// => Navigate to same URL
// => No cookies/storage from test A
// => Clean slate
const localValue = await page.evaluate(() => localStorage.getItem("testA"));
// => Try to retrieve test A localStorage
// => Returns null (not "dataA")
// => localStorage isolated
const sessionValue = await page.evaluate(() => sessionStorage.getItem("testA"));
// => Try to retrieve test A sessionStorage
// => Returns null (not "sessionA")
// => sessionStorage isolated
const cookieValue = await page.evaluate(() => {
return document.cookie.split(";").find((c) => c.includes("testA"));
});
// => Try to retrieve test A cookie
// => Returns undefined (cookie not set)
// => Cookies isolated
expect(localValue).toBeNull();
expect(sessionValue).toBeNull();
expect(cookieValue).toBeUndefined();
// => Assert all values null/undefined
// => Confirms complete isolation
// => No state leakage between tests
});
});Parallel execution with isolation:
import { test, expect } from "@playwright/test";
// => Playwright Test framework
// => Parallel execution enabled by default
// => Each test gets isolated context
test.describe.parallel("parallel suite", () => {
// => Mark suite for parallel execution
// => Tests run concurrently (not sequentially)
// => Safe because contexts isolated
test("parallel test 1", async ({ page }) => {
// => First parallel test
// => Runs simultaneously with test 2
// => Separate context (no interference)
await page.goto("https://example.com/page1");
await page.evaluate(() => localStorage.setItem("parallel1", "value1"));
// => Modify state in context 1
// => Test 2 unaffected by these changes
// => Contexts completely independent
await page.waitForTimeout(1000);
// => Simulate slow test
// => Test 2 may complete first (order non-deterministic)
// => Doesn't matter (no shared state)
const value = await page.evaluate(() => localStorage.getItem("parallel1"));
expect(value).toBe("value1");
// => Assert value correct
// => Not affected by test 2's actions
});
test("parallel test 2", async ({ page }) => {
// => Second parallel test
// => Runs simultaneously with test 1
// => Different context (complete isolation)
await page.goto("https://example.com/page2");
await page.evaluate(() => localStorage.setItem("parallel2", "value2"));
// => Modify state in context 2
// => Test 1 unaffected by these changes
// => Contexts isolated
const value = await page.evaluate(() => localStorage.getItem("parallel2"));
expect(value).toBe("value2");
// => Assert value correct
// => Not affected by test 1's actions
const otherValue = await page.evaluate(() => localStorage.getItem("parallel1"));
expect(otherValue).toBeNull();
// => Assert test 1's data not accessible
// => Confirms isolation even during concurrent execution
});
});Test Isolation Architecture
The following diagram illustrates how Playwright Test framework manages browser context isolation:
flowchart TB
TestRunner["Test Runner<br/>(Playwright Test)"]
Browser["Browser Process<br/>(Chromium/Firefox/WebKit)"]
Context1["Context 1<br/>(Test A)<br/>🔒 Isolated Cookies<br/>🔒 Isolated Storage"]
Context2["Context 2<br/>(Test B)<br/>🔒 Isolated Cookies<br/>🔒 Isolated Storage"]
Context3["Context 3<br/>(Test C)<br/>🔒 Isolated Cookies<br/>🔒 Isolated Storage"]
Page1A["Page 1"]
Page1B["Page 2"]
Page2["Page 1"]
Page3["Page 1"]
TestRunner -->|"Creates Context"| Context1
TestRunner -->|"Creates Context"| Context2
TestRunner -->|"Creates Context"| Context3
Browser -->|"Hosts"| Context1
Browser -->|"Hosts"| Context2
Browser -->|"Hosts"| Context3
Context1 -->|"Contains"| Page1A
Context1 -->|"Contains"| Page1B
Context2 -->|"Contains"| Page2
Context3 -->|"Contains"| Page3
style TestRunner fill:#0173B2,stroke:#000,color:#fff
style Browser fill:#DE8F05,stroke:#000,color:#fff
style Context1 fill:#029E73,stroke:#000,color:#fff
style Context2 fill:#029E73,stroke:#000,color:#fff
style Context3 fill:#029E73,stroke:#000,color:#fff
style Page1A fill:#CC78BC,stroke:#000,color:#fff
style Page1B fill:#CC78BC,stroke:#000,color:#fff
style Page2 fill:#CC78BC,stroke:#000,color:#fff
style Page3 fill:#CC78BC,stroke:#000,color:#fff
Architecture explanation:
- Test Runner: Playwright Test framework manages test lifecycle
- Browser Process: Single browser process hosts multiple contexts
- Browser Contexts: Isolated environments with separate cookies, localStorage, sessionStorage
- Pages: Web pages within contexts inherit context isolation
- Automatic Cleanup: Framework destroys contexts after test completion
Production Patterns and Best Practices
Pattern 1: Custom Fixtures for Shared Setup with Isolation
Create reusable fixtures that provide authenticated or configured contexts while maintaining isolation.
Authenticated fixture pattern:
// fixtures/authenticated.fixture.ts
import { test as base, expect } from "@playwright/test";
// => Extend base test with custom fixtures
// => Type-safe fixture definition
// => Reusable across test files
type AuthFixtures = {
authenticatedPage: Page;
// => Custom fixture providing logged-in page
// => Each test gets fresh authenticated context
// => Isolation maintained (separate contexts)
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ browser }, use) => {
// => Define custom fixture
// => browser fixture provided by Playwright
// => use() callback yields fixture value
const context = await browser.newContext();
// => Create NEW context for this fixture
// => Isolated from other test contexts
// => Fresh cookies/storage
const page = await context.newPage();
// => Create page in isolated context
// => Page inherits context isolation
// => Ready for authentication
await page.goto("https://example.com/login");
await page.fill('input[name="username"]', process.env.TEST_USERNAME!);
await page.fill('input[name="password"]', process.env.TEST_PASSWORD!);
await page.click('button[type="submit"]');
// => Perform login flow
// => Credentials from environment (secure)
// => Session established in this context
await page.waitForURL("**/dashboard");
// => Wait for successful login redirect
// => Authenticated session established
// => Cookies stored in context
await use(page);
// => Yield authenticated page to test
// => Test executes with authenticated context
// => Fixture paused until test completes
await context.close();
// => Close context after test completes
// => Cleanup authenticated session
// => Isolation maintained (session destroyed)
},
});
// test/profile.spec.ts
import { test, expect } from "../fixtures/authenticated.fixture";
// => Import test with custom fixture
// => Gets authenticatedPage fixture
// => Each test runs in isolated authenticated context
test("user views profile", async ({ authenticatedPage }) => {
// => Receive authenticated page fixture
// => Fresh login performed for THIS test
// => Isolated context (other tests unaffected)
await authenticatedPage.goto("https://example.com/profile");
// => Navigate to protected route
// => Session cookie sent automatically
// => Authenticated context maintained
await expect(authenticatedPage.locator("h1")).toHaveText("My Profile");
// => Assert authenticated content visible
// => Test passes
// => Context closed after test (automatic cleanup)
});
test("user updates profile", async ({ authenticatedPage }) => {
// => Second test using authenticated fixture
// => NEW login performed (separate context)
// => Previous test's session NOT reused
await authenticatedPage.goto("https://example.com/profile/edit");
// => Navigate to edit page
// => Fresh authenticated session
// => No state from previous test
await authenticatedPage.fill('input[name="bio"]', "Updated bio");
await authenticatedPage.click('button[type="submit"]');
// => Update profile
// => Changes isolated to this test's session
// => Previous test unaffected
// => Context closed after test
// => Updated data destroyed
// => Next test starts fresh
});Pattern 2: Storage State Reuse with Context Cloning
Reuse authentication state across tests without repeating login flow while maintaining isolation.
Storage state pattern:
// global-setup.ts
import { chromium, FullConfig } from "@playwright/test";
// => Global setup runs once before all tests
// => Performs expensive operations once
// => Saves authentication state for reuse
async function globalSetup(config: FullConfig) {
// => Setup function run before test suite
// => Executes once per test run
// => Creates reusable authentication state
const browser = await chromium.launch();
// => Launch browser for setup
// => Browser closed after setup completes
// => Not shared with test execution
const context = await browser.newContext();
// => Create context for authentication
// => Temporary context (destroyed after setup)
// => Used only to capture state
const page = await context.newPage();
await page.goto("https://example.com/login");
await page.fill('input[name="username"]', process.env.TEST_USERNAME!);
await page.fill('input[name="password"]', process.env.TEST_PASSWORD!);
await page.click('button[type="submit"]');
await page.waitForURL("**/dashboard");
// => Perform authentication ONCE
// => Login flow executed in setup
// => Not repeated for every test
await context.storageState({ path: "auth-state.json" });
// => Save authentication state to file
// => Captures cookies, localStorage, sessionStorage
// => Reusable across all tests
await browser.close();
// => Close setup browser
// => Setup complete
// => Authentication state saved to disk
}
export default globalSetup;
// playwright.config.ts
import { defineConfig } from "@playwright/test";
// => Playwright configuration
// => Defines global setup and storage state
// => Applied to all tests
export default defineConfig({
globalSetup: require.resolve("./global-setup"),
// => Run global-setup.ts before tests
// => Performs authentication once
// => Creates auth-state.json
use: {
storageState: "auth-state.json",
// => Load saved storage state for all contexts
// => Contexts start with authentication cookies
// => No login flow needed in tests
},
});
// test/profile.spec.ts
import { test, expect } from "@playwright/test";
// => Standard Playwright Test import
// => Contexts automatically load auth-state.json
// => Tests start authenticated
test("user views profile with restored session", async ({ page }) => {
// => page fixture created with auth-state.json loaded
// => Context has authentication cookies
// => No login flow required
await page.goto("https://example.com/profile");
// => Navigate directly to protected route
// => Session cookie sent from restored state
// => Authenticated immediately
await expect(page.locator("h1")).toHaveText("My Profile");
// => Assert authenticated content visible
// => Test passes without login flow
// => Fast test execution (no login overhead)
// => Context still isolated from other tests
// => Each test gets clone of auth-state.json
// => Changes to cookies/storage isolated
});
test("user logout doesn't affect other tests", async ({ page }) => {
// => Second test with restored authentication
// => Separate context (clone of auth-state.json)
// => Independent from first test
await page.goto("https://example.com/profile");
await page.click("text=Logout");
// => Perform logout
// => Clears session cookie in THIS context
// => Other test contexts unaffected
await page.goto("https://example.com/dashboard");
await expect(page).toHaveURL(/.*login.*/);
// => Assert redirected to login
// => Logout successful in this context
// => Other tests still authenticated (separate contexts)
});Pattern 3: Browser Context Configuration for Isolation Control
Configure browser contexts with specific isolation settings for different test scenarios.
Context configuration pattern:
import { test, expect } from "@playwright/test";
// => Playwright Test framework
// => Supports per-test context configuration
// => Overrides default settings
test("test with custom isolated context", async ({ browser }) => {
// => Receive browser fixture
// => Create context manually for custom config
// => Explicit control over isolation settings
const context = await browser.newContext({
// => Create context with custom configuration
// => Overrides default Playwright settings
// => Isolation controls specified
storageState: undefined,
// => No storage state loaded
// => Clean context (no cookies/localStorage)
// => Full isolation from global auth state
permissions: ["geolocation"],
// => Grant specific permissions
// => Isolated permission set
// => Other contexts don't inherit permissions
geolocation: { latitude: 59.95, longitude: 30.31667 },
// => Set geolocation for this context only
// => Other contexts use default location
// => Location isolated per context
locale: "en-US",
// => Set locale for this context
// => Isolated from other contexts' locales
// => Tests different localizations independently
timezoneId: "America/New_York",
// => Set timezone for this context
// => Isolated timezone settings
// => Test timezone-dependent behavior
viewport: { width: 1280, height: 720 },
// => Set viewport size for this context
// => Isolated viewport dimensions
// => Other contexts unaffected
});
const page = await context.newPage();
// => Create page in configured context
// => Page inherits context configuration
// => Isolated from other pages
await page.goto("https://example.com");
// => Navigate with custom context settings
// => Geolocation, locale, timezone applied
// => Other tests unaffected by this config
const location = await page.evaluate(() => navigator.language);
expect(location).toBe("en-US");
// => Assert locale applied
// => Isolated configuration verified
// => Other contexts use different locales
await context.close();
// => Close custom context
// => Cleanup isolated context
// => Settings don't leak to other tests
});
test("test with shared device configuration", async ({ browser }) => {
// => Second test with different context config
// => Independent from previous test's context
// => Different isolation settings
const iPhone = devices["iPhone 13"];
// => Load device configuration from Playwright
// => Predefined viewport, user agent, etc.
// => Simulates mobile device
const context = await browser.newContext({
...iPhone,
// => Spread iPhone device configuration
// => Viewport, user agent, touch enabled
// => Isolated mobile context
permissions: ["camera"],
// => Grant camera permission for mobile test
// => Isolated from other contexts
// => Desktop tests unaffected
});
const page = await context.newPage();
await page.goto("https://example.com");
// => Navigate with mobile context
// => Mobile viewport and user agent applied
// => Isolated from desktop tests
const userAgent = await page.evaluate(() => navigator.userAgent);
expect(userAgent).toContain("iPhone");
// => Assert mobile user agent
// => Mobile context verified
// => Desktop contexts unaffected
await context.close();
// => Close mobile context
// => Mobile configuration isolated
// => No impact on other tests
});Trade-offs and When to Use
Standard Approach (Manual Context Management):
- Use when: Learning Playwright, simple scripts, one-off tests
- Benefits:
- Explicit control over browser lifecycle
- No test framework dependency
- Understand isolation primitives
- Costs:
- Manual cleanup (error-prone, resource leaks)
- Verbose boilerplate in every test
- No parallel safety guarantees
- Forgotten cleanup leaves zombie processes
Production Framework (Playwright Test Fixtures):
- Use when: Production test suites, CI/CD integration, team projects
- Benefits:
- Automatic cleanup (no resource leaks)
- Test framework manages isolation
- Safe parallel execution
- Reusable fixtures eliminate duplication
- Storage state reuse speeds up tests
- Costs:
- Test framework dependency (@playwright/test)
- Learning curve for fixture system
- Less explicit control (framework handles lifecycle)
Production recommendation: Use Playwright Test fixtures for production test suites. Automatic isolation, cleanup, and parallel safety prevent flaky tests, resource leaks, and debugging nightmares. Manual context management only suitable for learning or simple scripts.
Security Considerations
Isolation prevents test data leakage:
- Each test context has separate cookie jar (no session hijacking between tests)
- localStorage/sessionStorage isolated (no sensitive data leakage)
- Network requests isolated (no request contamination)
Storage state security:
auth-state.jsoncontains authentication cookies and tokens- Store in
.gitignoreto prevent credential exposure - Use environment-specific storage state files (dev vs prod)
- Rotate authentication state regularly (avoid stale credentials)
Parallel execution security:
- Isolated contexts prevent race conditions in authentication tests
- Concurrent tests cannot interfere with each other’s sessions
- No shared state enables safe parallel authentication testing
Cleanup guarantees:
- Automatic context closure prevents lingering authenticated sessions
- Framework cleanup occurs even if test fails
- No zombie browser processes with leaked authentication state
Common Pitfalls
Pitfall 1: Sharing page fixture between tests:
// ❌ WRONG: Shared page between tests
let sharedPage: Page;
test.beforeAll(async ({ browser }) => {
sharedPage = await browser.newPage();
// => Creates ONE page for ALL tests
// => Tests share same context
// => State leakage between tests
});
test("test 1", async () => {
await sharedPage.goto("https://example.com");
await sharedPage.evaluate(() => localStorage.setItem("key", "value1"));
// => Modifies shared context
// => Affects subsequent tests
});
test("test 2", async () => {
await sharedPage.goto("https://example.com");
const value = await sharedPage.evaluate(() => localStorage.getItem("key"));
expect(value).toBeNull(); // FAILS! Returns "value1" from test 1
// => Test fails due to shared state
// => No isolation
});
// ✅ CORRECT: Use page fixture
test("test 1", async ({ page }) => {
// => Fresh context per test
// => Automatic isolation
});Pitfall 2: Mutating storage state file:
// ❌ WRONG: Modifying auth-state.json in tests
test("test modifies auth state", async ({ page }) => {
await page.goto("https://example.com/profile");
await page.evaluate(() => localStorage.setItem("mutation", "bad"));
// => Modifies storage in context
// => Changes NOT saved to auth-state.json
// => Next test loads original auth-state.json
await page.context().storageState({ path: "auth-state.json" });
// => Saves modified state to file
// => CONTAMINATES auth-state.json for other tests
// => Other tests load modified state
// => Flaky tests (depends on execution order)
});
// ✅ CORRECT: Don't save storage state in tests
test("test uses read-only auth state", async ({ page }) => {
// => Context loads auth-state.json (read-only)
// => Changes isolated to this context
// => auth-state.json unchanged
});Pitfall 3: Forgetting manual cleanup:
// ❌ WRONG: No cleanup on error
test("test with manual context", async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://example.com");
throw new Error("Test failed!");
// => Error thrown before cleanup
// => browser.close() never called
// => Resource leak (zombie browser process)
await browser.close(); // Never reached!
});
// ✅ CORRECT: Use try-finally or framework fixtures
test("test with automatic cleanup", async ({ page }) => {
// => Framework provides page fixture
// => Automatic cleanup even if test fails
// => No resource leaks
await page.goto("https://example.com");
throw new Error("Test failed!");
// => Error thrown
// => Framework still closes context
// => No resource leak
});Pitfall 4: Assuming sequential test execution:
// ❌ WRONG: Tests depend on execution order
let counter = 0;
test("test 1 increments counter", async ({ page }) => {
counter++;
// => Modifies shared variable
// => Not isolated (global state)
// => Tests depend on order
});
test("test 2 expects counter === 1", async ({ page }) => {
expect(counter).toBe(1);
// => Passes if test 1 runs first
// => Fails if test 2 runs first (parallel execution)
// => Flaky test
});
// ✅ CORRECT: Tests fully isolated
test("test 1 independent", async ({ page }) => {
// => No shared state
// => Test isolated
// => Order independent
});
test("test 2 independent", async ({ page }) => {
// => Separate test
// => No dependencies
// => Parallel safe
});Pitfall 5: Reusing contexts across test files:
// ❌ WRONG: Global context shared across files
// global-context.ts
export let globalContext: BrowserContext;
export async function setupGlobalContext() {
const browser = await chromium.launch();
globalContext = await browser.newContext();
// => Creates ONE context
// => Shared across all test files
// => State leakage between files
}
// test1.spec.ts
import { globalContext } from "./global-context";
test("test in file 1", async () => {
const page = await globalContext.newPage();
// => Uses shared context
// => Changes affect other test files
});
// ✅ CORRECT: Use fixtures (isolated per test)
test("test in file 1", async ({ page }) => {
// => Fresh context per test
// => Isolated from other files
// => Framework manages lifecycle
});File: /home/wkf/wkf-repos/wahidyankf/open-sharia-enterprise/apps/ayokoding-web/content/en/learn/software-engineering/automation-testing/tools/playwright/in-the-field/test-isolation.md
Key implementation details:
- Why It Matters: 3 paragraphs (state contamination, deterministic outcomes, parallel safety)
- Standard Library: 3 code blocks (basic context, cookie isolation, verification) with 1.5-2.0 annotation density
- Limitations: 6 specific limitations for production
- Production Framework: 3 comprehensive blocks (fixtures, parallel, verification) with 1.5-2.0 annotation density
- Mermaid diagram: Architecture showing Test Runner → Browser → Contexts → Pages with accessible colors
- 3 patterns: Custom fixtures (auth), storage state reuse, context configuration
- Trade-offs: Detailed comparison with production recommendation
- Security: 4 security considerations (data leakage, storage state, parallel, cleanup)
- 5 pitfalls: Shared page, mutating state, no cleanup, order dependency, global context
- Total: ~600 lines of production-ready content with real TypeScript examples