Network Interception
Why Network Interception Matters
Production E2E test suites require network interception to mock external API responses, simulate error conditions, and eliminate test flakiness from third-party service dependencies. Without network mocking, tests fail randomly when external services experience downtime, rate-limit test requests, or return non-deterministic data. Network interception transforms brittle integration tests into reliable, fast-executing test suites that validate application behavior under controlled conditions.
Playwright route interception enables request mocking, response modification, and network simulation at the browser network layer. This capability prevents production test failures from external API changes (provider updates schema without notice), reduces test execution time by 70-90% (no real network calls), and enables comprehensive error scenario testing (500 errors, timeouts, malformed responses) impossible with live services.
Production teams use network interception because:
- Test reliability: Eliminate flakiness from external service downtime or non-deterministic responses
- Speed: Mock responses return in <10ms instead of 200-500ms network roundtrip
- Error scenarios: Test 500 errors, timeouts, network failures impossible with live APIs
- Cost reduction: Avoid charges from third-party API usage in test environments
- Deterministic data: Control exact response data for predictable test assertions
Standard Library Approach: Basic Route Interception
Playwright core API provides route interception through page.route() for request mocking without requiring HAR file frameworks.
Basic API mocking with route.fulfill:
import { test, expect } from "@playwright/test";
// => Import Playwright test framework
// => No mocking framework required
// => Built-in route interception
test("mock API response for user data", async ({ page }) => {
// => Test receives page fixture
// => Fresh browser context per test
// => No shared route handlers
await page.route("**/api/users/123", async (route) => {
// => Intercept all requests matching URL pattern
// => route parameter provides request/response control
// => Executes before request sent to server
// => ** matches any subdomain or path prefix
await route.fulfill({
// => Respond with mock data
// => Browser receives this instead of server response
// => No actual network request sent
status: 200,
// => HTTP status code
// => 200 indicates success
// => Client sees successful response
contentType: "application/json",
// => Sets Content-Type header
// => Browser parses response as JSON
// => Required for JSON parsing
body: JSON.stringify({
id: 123,
username: "testuser",
email: "test@example.com",
}),
// => Response body as JSON string
// => JSON.stringify converts object to string
// => Must match API contract structure
});
});
// => Route handler registered
// => Applied to all matching requests
// => Persists for entire test
await page.goto("https://example.com/users/123");
// => Navigate to page that calls /api/users/123
// => Browser makes request to /api/users/123
// => Request intercepted by route handler
// => Mock response returned instead of server
await expect(page.locator("h1")).toHaveText("testuser");
// => Assert UI displays mocked username
// => Verifies application renders mock data
// => No dependency on live API
});Conditional route handling based on request data:
test("mock POST request based on body content", async ({ page }) => {
// => Test form submission with API mock
// => Validates request body before mocking
// => Conditional response based on data
await page.route("**/api/users", async (route) => {
// => Intercept POST requests to /api/users
// => route.request() provides request details
// => Access method, headers, body
const request = route.request();
// => Get Request object from route
// => Contains method, URL, headers, body
// => Inspect before deciding response
if (request.method() === "POST") {
// => Check if request is POST
// => Only mock POST, allow GET through
// => Method-specific handling
const postData = request.postDataJSON();
// => Parse JSON request body
// => Returns typed object
// => Assumes Content-Type: application/json
if (postData.username === "validuser") {
// => Conditional response based on data
// => Validates request content
// => Different responses for different inputs
await route.fulfill({
status: 201,
// => 201 Created for successful user creation
// => Matches REST API conventions
// => Client interprets as success
contentType: "application/json",
body: JSON.stringify({
id: 456,
username: postData.username,
email: postData.email,
}),
// => Return created user with generated ID
// => Simulates server-generated ID
// => Client receives confirmation
});
} else {
// => Invalid username case
// => Simulates server validation failure
// => Tests error handling path
await route.fulfill({
status: 400,
// => 400 Bad Request for validation error
// => Indicates client error
// => Triggers error handling in app
contentType: "application/json",
body: JSON.stringify({
error: "Invalid username",
}),
});
}
} else {
// => Not a POST request
// => Allow through to server
// => No mocking for other methods
await route.continue();
// => Continue request to actual server
// => No interception for GET/PUT/DELETE
// => Selective mocking strategy
}
});
await page.goto("https://example.com/signup");
// => Navigate to signup page
// => Page will POST to /api/users
// => Route handler ready to intercept
await page.fill('input[name="username"]', "validuser");
await page.fill('input[name="email"]', "valid@example.com");
await page.click('button[type="submit"]');
// => Fill form and submit
// => Triggers POST request
// => Request intercepted and mocked
await expect(page.locator(".success")).toHaveText("User created successfully");
// => Assert success message appears
// => Validates app handles mock response correctly
// => End-to-end verification
});Simulating network errors:
test("handle API timeout error", async ({ page }) => {
// => Test network failure scenario
// => Simulates timeout condition
// => Validates error handling
await page.route("**/api/users/123", async (route) => {
// => Intercept user API request
// => Abort instead of fulfill
// => Simulates network failure
await route.abort("timedout");
// => Abort request with timeout error
// => Browser sees ERR_CONNECTION_TIMED_OUT
// => No response returned
});
await page.goto("https://example.com/users/123");
// => Navigate to page requiring API call
// => API request aborted with timeout
// => Application must handle error
await expect(page.locator(".error")).toHaveText("Failed to load user");
// => Assert error message displayed
// => Verifies graceful error handling
// => Prevents blank page on error
});Limitations for production network testing:
- No response reuse: Mock data hardcoded in each test (duplication, maintenance burden)
- Manual request matching: URL patterns in every test (brittle, error-prone)
- No HAR replay: Cannot capture and replay real API traffic (complex mocks difficult to create)
- Limited request inspection: Basic method/body checks only (no header validation, no timing simulation)
- No request history: Cannot assert on intercepted requests (no verification API was called)
- Performance simulation gaps: Cannot simulate slow networks, high latency, or bandwidth limits
Production Framework: HAR Replay and Request Fixtures
Production network testing uses HAR (HTTP Archive) files to capture real API traffic, dedicated fixtures for route configuration, and mock services for deterministic responses.
Recording HAR file from real traffic:
// tests/setup/recordHAR.ts
import { chromium } from "@playwright/test";
// => Import chromium browser from Playwright
// => Used for recording session
// => Runs outside test context
async function recordHAR() {
// => Script to record real API traffic
// => Run once to capture responses
// => Generates HAR file for replay
const browser = await chromium.launch({ headless: false });
// => Launch visible browser for recording
// => headless: false shows browser window
// => Allows manual interaction
const context = await browser.newContext({
// => Create browser context for recording
// => Isolated session
// => Configures HAR recording
recordHar: { path: "tests/fixtures/users.har" },
// => Enable HAR recording to file
// => Captures all network traffic
// => Saved to tests/fixtures/users.har
});
const page = await context.newPage();
// => Create new page in recording context
// => All network activity captured
// => HAR includes requests and responses
await page.goto("https://example.com");
// => Navigate to application
// => Interact with real APIs
// => All traffic recorded to HAR file
// Manually interact with application to record API calls
await page.click('a[href="/users"]');
// => Click users link
// => API calls to fetch users recorded
// => Wait for navigation and API responses
await page.waitForLoadState("networkidle");
// => Wait for all network activity to finish
// => Ensures all API responses captured
// => HAR contains complete traffic
await context.close();
// => Close context to finish recording
// => HAR file written to disk
// => Browser saves captured traffic
await browser.close();
// => Close browser after recording
// => Recording session complete
}
recordHAR();
// => Execute recording function
// => Run this script manually when APIs change
// => Update HAR file with latest responses
Using HAR replay in tests:
import { test, expect } from "@playwright/test";
// => Import Playwright test framework
// => HAR replay built into Playwright
// => No additional framework needed
test("replay HAR file for deterministic responses", async ({ browser }) => {
// => Test receives browser fixture
// => Create custom context with HAR replay
// => Isolates HAR replay to this test
const context = await browser.newContext({
// => Create new browser context
// => Context-level HAR configuration
// => All pages in context use HAR replay
recordHar: {
// => Configure HAR recording/replay
// => Can record or replay existing file
// => Controls network behavior
path: "tests/fixtures/users.har",
// => Path to HAR file for replay
// => Contains captured API responses
// => Matches requests to recorded responses
mode: "replay",
// => Replay mode: serve responses from HAR
// => No actual network requests sent
// => All responses from file
urlFilter: "**/api/**",
// => Only replay API requests
// => Other requests pass through
// => Selective HAR replay
},
});
const page = await context.newPage();
// => Create page in HAR replay context
// => All API calls served from HAR
// => Deterministic responses
await page.goto("https://example.com/users");
// => Navigate to users page
// => Page calls /api/users
// => Response served from HAR file
// => No network dependency
await expect(page.locator(".user-card")).toHaveCount(5);
// => Assert exact user count from HAR
// => HAR file contains deterministic data
// => Test expectations match HAR content
await context.close();
// => Clean up context after test
// => Releases resources
// => HAR replay session ends
});Production network fixture with request logging:
// fixtures/networkFixture.ts
import { test as base, expect } from "@playwright/test";
// => Import base test from Playwright
// => Extend with network fixtures
// => Dependency injection pattern
type NetworkFixtures = {
interceptedRequests: Request[];
// => Array to store intercepted requests
// => Tests can assert on API calls made
// => Enables request verification
};
export const test = base.extend<NetworkFixtures>({
// => Extend base test with network fixtures
// => Type-safe fixture definitions
// => Automatic setup and teardown
interceptedRequests: async ({ page }, use) => {
// => Define interceptedRequests fixture
// => Receives page fixture as dependency
// => use() callback provides fixture value
const requests: Request[] = [];
// => Array to collect intercepted requests
// => Mutable state shared across test
// => Populated by route handler
await page.route("**/api/**", async (route) => {
// => Intercept all API requests
// => route parameter provides request control
// => Logs request before continuing
const request = route.request();
// => Get Request object from route
// => Contains method, URL, headers, body
// => Store for later assertion
requests.push(request);
// => Add request to collection
// => Preserves request order
// => Test can verify requests made
await route.continue();
// => Continue request to actual server
// => Non-blocking interception
// => Request proceeds normally
});
await use(requests);
// => Provide requests array to test
// => Test receives collected requests
// => Can assert on request count, URLs, data
},
});Using network fixture to verify requests:
import { test, expect } from "./fixtures/networkFixture";
// => Import custom test with network fixture
// => Automatically provides interceptedRequests
// => Type-safe fixture access
test("verify user search makes correct API call", async ({ page, interceptedRequests }) => {
// => Test receives page and interceptedRequests
// => interceptedRequests populated by fixture
// => All API requests logged
await page.goto("https://example.com/users");
// => Navigate to users page
// => Initial API calls logged
// => interceptedRequests contains requests
await page.fill('input[name="search"]', "john");
// => Fill search input
// => Triggers search API call
// => Request intercepted and logged
await page.click('button[type="submit"]');
// => Submit search form
// => API request sent
// => Added to interceptedRequests
await page.waitForLoadState("networkidle");
// => Wait for all API requests to complete
// => Ensures all requests logged
// => Ready to assert on requests
const searchRequest = interceptedRequests.find((req) => req.url().includes("/api/users/search"));
// => Find search API request
// => Filter by URL pattern
// => Returns Request object or undefined
expect(searchRequest).toBeDefined();
// => Assert search request was made
// => Verifies API called
// => Confirms application behavior
const searchParams = new URL(searchRequest!.url()).searchParams;
// => Parse URL query parameters
// => Extract search query
// => ! asserts non-null (defined check above)
expect(searchParams.get("q")).toBe("john");
// => Assert query parameter matches input
// => Verifies correct data sent to API
// => End-to-end request validation
});Network Interception Flow
flowchart TB
subgraph "Network Interception Architecture"
A[Browser Test] -->|1. Navigate/Interact| B[Playwright Page]
B -->|2. Request Triggered| C{Route Handler}
C -->|3a. route.fulfill| D[Mock Response]
C -->|3b. route.continue| E[Real API]
C -->|3c. route.abort| F[Network Error]
D -->|4a. Return Mock| B
E -->|4b. Return Real Response| B
F -->|4c. Trigger Error| B
B -->|5. Render Response| G[Test Assertions]
end
subgraph "HAR Replay Flow"
H[Test Setup] -->|1. Load HAR| I[HAR File]
I -->|2. Configure| J[Browser Context]
J -->|3. Create Page| K[Page with HAR]
K -->|4. Make Request| L{Request Match}
L -->|5a. Match Found| M[Serve from HAR]
L -->|5b. No Match| N[Pass Through/Error]
M -->|6. Return Cached| K
end
style D fill:#0173B2,color:#fff
style E fill:#029E73,color:#fff
style F fill:#DE8F05,color:#000
style M fill:#CC78BC,color:#fff
style N fill:#CA9161,color:#000
Production Patterns and Best Practices
Pattern 1: Centralized Mock Data Repository
Problem: Mock data duplicated across tests, difficult to maintain consistency
Solution: Centralize mock responses in dedicated files with typed schemas
// fixtures/mockData/users.ts
export const mockUsers = {
// => Centralized mock data repository
// => Single source of truth for user mocks
// => Shared across all tests
validUser: {
id: 123,
username: "testuser",
email: "test@example.com",
role: "admin",
},
// => Valid user fixture
// => Reused in multiple tests
// => Consistent data structure
guestUser: {
id: 456,
username: "guest",
email: "guest@example.com",
role: "guest",
},
};
export const mockUserResponses = {
// => Pre-configured API responses
// => Ready for route.fulfill
// => Reduces boilerplate
success: (user: typeof mockUsers.validUser) => ({
status: 200,
contentType: "application/json",
body: JSON.stringify(user),
}),
// => Success response factory
// => Accepts user object
// => Returns route.fulfill config
notFound: {
status: 404,
contentType: "application/json",
body: JSON.stringify({ error: "User not found" }),
},
// => 404 error response
// => Consistent error format
// => Reusable across tests
serverError: {
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Internal server error" }),
},
};Using centralized mocks:
import { test, expect } from "@playwright/test";
import { mockUsers, mockUserResponses } from "./fixtures/mockData/users";
// => Import centralized mock data
// => Type-safe mock objects
// => Consistent across tests
test("display user profile with centralized mock", async ({ page }) => {
// => Test using shared mock data
// => No duplication of user structure
// => Maintains consistency
await page.route("**/api/users/123", async (route) => {
// => Intercept user API request
// => Use pre-configured response
// => Minimal boilerplate
await route.fulfill(mockUserResponses.success(mockUsers.validUser));
// => Fulfill with centralized response
// => mockUserResponses.success returns config
// => mockUsers.validUser provides data
});
await page.goto("https://example.com/users/123");
// => Navigate to user profile
// => API mocked with consistent data
// => UI renders mock user
await expect(page.locator("h1")).toHaveText(mockUsers.validUser.username);
// => Assert using same mock data
// => Type-safe property access
// => Consistent expectations
});Pattern 2: API Error Scenario Testing
Problem: Applications crash on unexpected API errors (500, timeout, malformed JSON)
Solution: Systematically test error scenarios using route interception
import { test, expect } from "@playwright/test";
// => Import Playwright test framework
// => Error scenario testing
// => Network failure simulation
const errorScenarios = [
// => Array of error scenarios to test
// => Each scenario defines network condition
// => Tests loop through all scenarios
{
name: "500 Internal Server Error",
// => Human-readable scenario name
// => Used in test description
// => Appears in test reports
handler: async (route) => {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Internal server error" }),
});
// => Simulate 500 error from API
// => Tests server error handling
// => Application should show graceful error
},
},
{
name: "Network Timeout",
handler: async (route) => {
await route.abort("timedout");
// => Abort request with timeout error
// => Browser sees ERR_CONNECTION_TIMED_OUT
// => Tests timeout handling
},
},
{
name: "Malformed JSON Response",
handler: async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: "{ invalid json",
// => Invalid JSON syntax
// => Browser JSON parsing fails
// => Tests malformed response handling
});
},
},
{
name: "Connection Refused",
handler: async (route) => {
await route.abort("connectionrefused");
// => Abort with connection refused error
// => Simulates server down
// => Tests network failure handling
},
},
];
for (const scenario of errorScenarios) {
// => Loop through all error scenarios
// => Generate test for each scenario
// => Comprehensive error coverage
test(`handle ${scenario.name} gracefully`, async ({ page }) => {
// => Dynamic test name from scenario
// => Test report shows specific error type
// => Easy to identify failing scenarios
await page.route("**/api/users/123", scenario.handler);
// => Apply scenario error handler
// => Intercepts API request
// => Triggers specific error condition
await page.goto("https://example.com/users/123");
// => Navigate to page requiring API
// => API call intercepted
// => Error handler triggered
await expect(page.locator(".error-message")).toBeVisible();
// => Assert error message displayed
// => Verifies graceful degradation
// => Application doesn't crash
await expect(page.locator(".retry-button")).toBeVisible();
// => Assert retry option available
// => Provides user recovery path
// => Good UX for error scenarios
});
}Pattern 3: HAR File Update Strategy
Problem: HAR files become stale when APIs change, tests break with outdated mocks
Solution: Implement HAR refresh workflow with version control and validation
// tests/setup/harManagement.ts
import { chromium, expect } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
// => Import filesystem modules
// => Manage HAR file updates
// => Version control integration
export async function updateHARFile(
url: string,
harPath: string,
options?: {
interactions?: (page: Page) => Promise<void>;
validateHAR?: (harContent: any) => void;
},
) {
// => Function to update HAR file
// => Captures latest API responses
// => Validates HAR content before saving
const browser = await chromium.launch({ headless: false });
// => Launch visible browser for manual verification
// => Developer can observe recording
// => Ensures correct interactions captured
const context = await browser.newContext({
recordHar: { path: harPath, mode: "minimal" },
// => Record HAR to specified path
// => mode: minimal reduces file size
// => Excludes unnecessary headers
});
const page = await context.newPage();
// => Create page for recording
// => All network traffic captured
// => HAR includes requests/responses
await page.goto(url);
// => Navigate to application
// => Triggers initial API calls
// => Captured to HAR
if (options?.interactions) {
// => Execute custom interactions if provided
// => Trigger specific API endpoints
// => Record complex workflows
await options.interactions(page);
// => Run interaction callback
// => Developer-defined actions
// => Captures relevant API calls
}
await page.waitForLoadState("networkidle");
// => Wait for all network activity
// => Ensures complete HAR capture
// => No truncated responses
await context.close();
// => Close context to finalize HAR
// => HAR written to disk
// => Recording complete
await browser.close();
if (options?.validateHAR) {
// => Validate HAR content if validator provided
// => Ensures HAR contains expected requests
// => Catches incomplete recordings
const harContent = JSON.parse(fs.readFileSync(harPath, "utf8"));
// => Read HAR file content
// => Parse JSON structure
// => Validate against schema
options.validateHAR(harContent);
// => Execute validation callback
// => Throws if HAR invalid
// => Prevents committing bad HAR files
}
console.log(`HAR file updated: ${harPath}`);
// => Log successful update
// => Confirms file written
// => Developer feedback
}HAR validation helper:
export function validateUserHAR(harContent: any) {
// => Validate user API HAR file
// => Ensures required endpoints present
// => Throws if validation fails
const entries = harContent.log.entries;
// => Extract HAR entries array
// => Each entry is request/response pair
// => Search for required endpoints
const hasUsersList = entries.some((e) => e.request.url.includes("/api/users") && e.request.method === "GET");
// => Check for GET /api/users endpoint
// => Validates users list captured
// => Required for user tests
const hasUserDetail = entries.some((e) => e.request.url.match(/\/api\/users\/\d+/));
// => Check for GET /api/users/:id endpoint
// => Validates user detail captured
// => Required for profile tests
if (!hasUsersList || !hasUserDetail) {
// => Validation failed
// => Required endpoints missing
// => HAR file incomplete
throw new Error(
`HAR validation failed: Missing required user endpoints (list: ${hasUsersList}, detail: ${hasUserDetail})`,
);
// => Throw descriptive error
// => Indicates which endpoints missing
// => Developer can re-record
}
}Using HAR update script:
// tests/setup/updateUserHAR.ts
import { updateHARFile, validateUserHAR } from "./harManagement";
// => Import HAR management utilities
// => Reusable update workflow
// => Consistent HAR updates
updateHARFile("https://example.com/users", "tests/fixtures/users.har", {
// => Update users HAR file
// => Captures API traffic from users page
// => Validates before saving
interactions: async (page) => {
// => Custom interactions to trigger APIs
// => Captures all required endpoints
// => Ensures comprehensive HAR
await page.click('a[href="/users/123"]');
// => Navigate to user detail
// => Triggers GET /api/users/123
// => Captured in HAR
await page.waitForLoadState("networkidle");
// => Wait for API responses
// => Ensures complete capture
// => No partial responses
await page.click('button[data-action="search"]');
// => Trigger search feature
// => Captures search API endpoint
// => Additional HAR coverage
await page.waitForLoadState("networkidle");
},
validateHAR: validateUserHAR,
// => Validate HAR after recording
// => Ensures required endpoints present
// => Prevents incomplete HAR commits
});
// Run: npx ts-node tests/setup/updateUserHAR.ts
// => Execute HAR update script
// => Developer runs when API changes
// => Updates HAR file with latest responses
Trade-offs and When to Use
Standard Route Interception
Use when:
- Simple API mocking for individual tests
- Testing specific error scenarios (500, timeout)
- Request validation without recording infrastructure
Benefits:
- No HAR file management overhead
- Full control over response data and timing
- Quick setup for one-off test scenarios
- Easy to understand for simple mocks
Costs:
- Mock data duplicated across tests
- Manual maintenance when API contracts change
- Difficult to capture complex API sequences
- No replay of real traffic patterns
HAR Replay
Use when:
- Testing against complex API workflows
- Validating application with real API responses
- Need deterministic responses for flaky tests
- Multiple tests share same API responses
Benefits:
- Captures real API traffic automatically
- Single HAR file serves multiple tests
- Realistic response timing and structure
- Easy to update when APIs change (re-record)
Costs:
- HAR files require version control management
- Stale HAR files cause test failures
- Cannot easily modify specific responses
- Larger file sizes for complex workflows
Production Recommendation
For small test suites (<50 tests): Use standard route interception with centralized mock data repository. Simplicity outweighs HAR overhead.
For large test suites (>50 tests): Implement HAR replay with automated update workflow. Reduces mock maintenance burden and ensures realistic API responses.
Hybrid approach (recommended for most teams):
- HAR replay for happy path scenarios (common API workflows)
- Route interception for error scenarios (500, timeout, malformed JSON)
- Centralized mock repository for edge cases (specific data conditions)
Security Considerations
Sensitive Data in HAR Files:
- HAR files capture all request/response data including authentication tokens, API keys, personally identifiable information
- Risk: Committed HAR files expose production credentials to anyone with repository access
- Mitigation: Sanitize HAR files before committing (remove Authorization headers, mask PII)
- Tool: Use
har-sanitizerto automatically redact sensitive fields
import { sanitizeHAR } from "har-sanitizer";
const harContent = JSON.parse(fs.readFileSync("users.har", "utf8"));
const sanitized = sanitizeHAR(harContent, {
// => Sanitize HAR before committing
// => Remove sensitive data
// => Safe for version control
redactHeaders: ["Authorization", "Cookie", "X-API-Key"],
// => Headers to remove completely
// => Prevents token exposure
// => Security best practice
maskFields: ["email", "phone", "ssn"],
// => Fields to mask with ***
// => Protects PII
// => Maintains structure for tests
});
fs.writeFileSync("users.har", JSON.stringify(sanitized, null, 2));Mock Data Validation:
- Production applications trust HAR replay responses as if from real API
- Risk: Malicious HAR modifications bypass security validations
- Mitigation: Validate HAR schema on CI/CD, review HAR changes in pull requests
Request Leakage Prevention:
- Misconfigured route patterns (
*instead of**/api/**) intercept all requests - Risk: Tests accidentally bypass authentication, CORS, or security headers
- Mitigation: Use specific URL patterns, verify non-intercepted requests reach server
Common Pitfalls
Route Handler Race Conditions: Registering route handlers after page navigation causes requests to miss interception. Always register route handlers BEFORE
page.goto().Overlapping Route Patterns: Multiple route handlers matching same URL create non-deterministic behavior. Playwright uses first registered handler, but order matters. Use specific patterns (
**/api/users/**not**/**).Ignoring Request Continuation: Using
route.fulfill()for non-API requests (images, CSS) breaks page rendering. Only intercept API endpoints (**/api/**), continue all other requests withroute.continue().HAR File Size Bloat: Recording entire application session creates multi-MB HAR files. Use
urlFilterto capture only API endpoints,mode: "minimal"to reduce headers.Hardcoded Response Delays: Adding
await page.waitForTimeout(1000)in route handlers slows tests unnecessarily. Playwright automatically waits for responses, no manual delays needed.Missing HAR Validation: Committing incomplete HAR files (network dropped during recording) causes random test failures. Always validate HAR contains expected endpoints before committing.
Forgetting to Update HAR After API Changes: Tests pass with stale HAR data while production uses new API contract. Implement CI check comparing HAR version to API schema version.