SurfSense/.cursor/skills/playwright-testing/advanced/network-advanced.md

453 lines
11 KiB
Markdown
Raw Normal View History

2026-05-10 04:19:55 +05:30
# Advanced Network Interception
## Table of Contents
1. [Request Modification](#request-modification)
2. [GraphQL Mocking](#graphql-mocking)
3. [HAR Recording & Playback](#har-recording--playback)
4. [Conditional Mocking](#conditional-mocking)
5. [Network Throttling](#network-throttling)
## Request Modification
### Modify Request Headers
```typescript
test("add auth header to requests", async ({ page }) => {
await page.route("**/api/**", (route) => {
const headers = {
...route.request().headers(),
Authorization: "Bearer test-token",
"X-Test-Header": "test-value",
};
route.continue({ headers });
});
await page.goto("/dashboard");
});
```
### Modify Request Body
```typescript
test("modify POST body", async ({ page }) => {
await page.route("**/api/orders", async (route) => {
if (route.request().method() === "POST") {
const postData = route.request().postDataJSON();
// Add test metadata
const modifiedData = {
...postData,
testMode: true,
testTimestamp: Date.now(),
};
await route.continue({
postData: JSON.stringify(modifiedData),
});
} else {
await route.continue();
}
});
await page.goto("/checkout");
await page.getByRole("button", { name: "Place Order" }).click();
});
```
### Transform Response
```typescript
test("modify API response", async ({ page }) => {
await page.route("**/api/products", async (route) => {
// Fetch real response
const response = await route.fetch();
const json = await response.json();
// Modify response
const modified = json.map((product: any) => ({
...product,
price: product.price * 0.9, // 10% discount
testMode: true,
}));
await route.fulfill({
response,
json: modified,
});
});
await page.goto("/products");
});
```
## GraphQL Mocking
### Mock by Operation Name
```typescript
test("mock GraphQL query", async ({ page }) => {
await page.route("**/graphql", async (route) => {
const postData = route.request().postDataJSON();
if (postData.operationName === "GetUser") {
return route.fulfill({
json: {
data: {
user: {
id: "1",
name: "Test User",
email: "test@example.com",
},
},
},
});
}
if (postData.operationName === "GetProducts") {
return route.fulfill({
json: {
data: {
products: [
{ id: "1", name: "Product A", price: 29.99 },
{ id: "2", name: "Product B", price: 49.99 },
],
},
},
});
}
// Pass through unmocked operations
return route.continue();
});
await page.goto("/dashboard");
});
```
### GraphQL Mock Fixture
```typescript
// fixtures/graphql.fixture.ts
type GraphQLMock = {
operation: string;
variables?: Record<string, any>;
response: { data?: any; errors?: any[] };
};
type GraphQLFixtures = {
mockGraphQL: (mocks: GraphQLMock[]) => Promise<void>;
};
export const test = base.extend<GraphQLFixtures>({
mockGraphQL: async ({ page }, use) => {
await use(async (mocks) => {
await page.route("**/graphql", async (route) => {
const postData = route.request().postDataJSON();
const mock = mocks.find((m) => {
if (m.operation !== postData.operationName) return false;
// Optionally match variables
if (m.variables) {
return (
JSON.stringify(m.variables) === JSON.stringify(postData.variables)
);
}
return true;
});
if (mock) {
return route.fulfill({ json: mock.response });
}
return route.continue();
});
});
},
});
// Usage
test("dashboard with mocked GraphQL", async ({ page, mockGraphQL }) => {
await mockGraphQL([
{
operation: "GetDashboardStats",
response: {
data: { stats: { users: 100, revenue: 50000 } },
},
},
{
operation: "GetUser",
variables: { id: "1" },
response: {
data: { user: { id: "1", name: "John" } },
},
},
]);
await page.goto("/dashboard");
await expect(page.getByText("100 users")).toBeVisible();
});
```
### Mock GraphQL Mutations
```typescript
test("mock GraphQL mutation", async ({ page }) => {
await page.route("**/graphql", async (route) => {
const postData = route.request().postDataJSON();
if (postData.operationName === "CreateOrder") {
const { input } = postData.variables;
return route.fulfill({
json: {
data: {
createOrder: {
id: "order-123",
status: "PENDING",
items: input.items,
total: input.items.reduce(
(sum: number, item: any) => sum + item.price * item.quantity,
0,
),
},
},
},
});
}
return route.continue();
});
await page.goto("/checkout");
await page.getByRole("button", { name: "Place Order" }).click();
await expect(page.getByText("Order #order-123")).toBeVisible();
});
```
## HAR Recording & Playback
### Record HAR File
```typescript
// Record network traffic
test("record HAR", async ({ page, context }) => {
// Start recording
await context.routeFromHAR("./recordings/checkout.har", {
update: true, // Create/update HAR file
url: "**/api/**",
});
await page.goto("/checkout");
await page.getByRole("button", { name: "Place Order" }).click();
// HAR file is saved automatically
});
```
### Playback HAR File
```typescript
// Use recorded HAR for offline testing
test("playback HAR", async ({ page, context }) => {
await context.routeFromHAR("./recordings/checkout.har", {
url: "**/api/**",
update: false, // Don't update, just playback
});
await page.goto("/checkout");
// All API calls served from HAR file
await expect(page.getByText("Order confirmed")).toBeVisible();
});
```
### HAR with Fallback
```typescript
test("HAR with live fallback", async ({ page, context }) => {
await context.routeFromHAR("./recordings/api.har", {
url: "**/api/**",
update: false,
notFound: "fallback", // Use real network if not in HAR
});
await page.goto("/dashboard");
});
```
## Conditional Mocking
### Mock Based on Request Body
```typescript
test("conditional mock by body", async ({ page }) => {
await page.route("**/api/search", async (route) => {
const body = route.request().postDataJSON();
if (body.query === "error") {
return route.fulfill({
status: 500,
json: { error: "Search failed" },
});
}
if (body.query === "empty") {
return route.fulfill({
json: { results: [] },
});
}
// Default response
return route.fulfill({
json: {
results: [{ id: 1, title: `Result for: ${body.query}` }],
},
});
});
await page.goto("/search");
// Test different scenarios
await page.getByLabel("Search").fill("error");
await page.getByLabel("Search").press("Enter");
await expect(page.getByText("Search failed")).toBeVisible();
});
```
### Mock Nth Request
```typescript
test("different response on retry", async ({ page }) => {
let callCount = 0;
await page.route("**/api/status", (route) => {
callCount++;
if (callCount < 3) {
return route.fulfill({
status: 503,
json: { error: "Service unavailable" },
});
}
// Succeed on 3rd attempt
return route.fulfill({
json: { status: "ok" },
});
});
await page.goto("/dashboard");
// App should retry and eventually succeed
await expect(page.getByText("Connected")).toBeVisible();
});
```
### Mock with Delay
```typescript
test("slow network simulation", async ({ page }) => {
await page.route("**/api/data", async (route) => {
// Simulate 2 second delay
await new Promise((resolve) => setTimeout(resolve, 2000));
return route.fulfill({
json: { data: "loaded" },
});
});
await page.goto("/dashboard");
// Loading state should appear
await expect(page.getByText("Loading...")).toBeVisible();
// Then data appears
await expect(page.getByText("loaded")).toBeVisible();
});
```
## Network Throttling
### Slow 3G Simulation
```typescript
test("slow network experience", async ({ page, context }) => {
// Create CDP session for network throttling
const client = await context.newCDPSession(page);
await client.send("Network.emulateNetworkConditions", {
offline: false,
downloadThroughput: (500 * 1024) / 8, // 500 Kbps
uploadThroughput: (500 * 1024) / 8,
latency: 400, // 400ms
});
await page.goto("/");
// Test loading states appear
await expect(page.getByTestId("skeleton-loader")).toBeVisible();
});
```
### Offline Mode
Use `context.setOffline(true/false)` to simulate network connectivity changes.
> **For comprehensive offline testing patterns:**
>
> - **Network failure simulation** (error recovery, graceful degradation): See [error-testing.md](error-testing.md#offline-testing)
> - **Offline-first/PWA testing** (service workers, caching, background sync): See [service-workers.md](service-workers.md#offline-testing)
### Network Throttling Fixture
```typescript
// fixtures/network.fixture.ts
type NetworkCondition = "slow3g" | "fast3g" | "offline";
const conditions = {
slow3g: { downloadThroughput: 50000, uploadThroughput: 50000, latency: 2000 },
fast3g: { downloadThroughput: 180000, uploadThroughput: 75000, latency: 150 },
};
type NetworkFixtures = {
setNetworkCondition: (condition: NetworkCondition) => Promise<void>;
};
export const test = base.extend<NetworkFixtures>({
setNetworkCondition: async ({ page, context }, use) => {
const client = await context.newCDPSession(page);
await use(async (condition) => {
if (condition === "offline") {
await context.setOffline(true);
} else {
await client.send("Network.emulateNetworkConditions", {
offline: false,
...conditions[condition],
});
}
});
// Reset
await context.setOffline(false);
},
});
```
## Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
| ------------------------ | ------------------------------ | -------------------------------- |
| Mocking all requests | Tests don't reflect reality | Mock only what's necessary |
| No cleanup of routes | Routes persist across tests | Use fixtures with cleanup |
| Ignoring request method | Mock applies to wrong requests | Check `route.request().method()` |
| Hardcoded mock responses | Brittle, hard to maintain | Use factories for mock data |
## Related References
- **Basic Mocking**: See [test-suite-structure.md](../core/test-suite-structure.md) for simple mocking
- **WebSockets**: See [websockets.md](../browser-apis/websockets.md) for real-time mocking