@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
# Network Mocking
|
|
2
|
+
|
|
3
|
+
> **When to use**: Isolating your frontend from external services, simulating error states, testing loading/empty/error UI, speeding up tests by avoiding real network calls, and testing against APIs that don't exist yet.
|
|
4
|
+
> **Prerequisites**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Intercept and return fake data
|
|
10
|
+
await page.route('**/api/users', (route) =>
|
|
11
|
+
route.fulfill({ json: [{ id: 1, name: 'Jane' }] })
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Modify a real response before it reaches the browser
|
|
15
|
+
await page.route('**/api/users', async (route) => {
|
|
16
|
+
const response = await route.fetch();
|
|
17
|
+
const json = await response.json();
|
|
18
|
+
json.push({ id: 999, name: 'Injected' });
|
|
19
|
+
await route.fulfill({ response, json });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Block third-party scripts
|
|
23
|
+
await page.route('**/*.{png,jpg,svg}', (route) => route.abort());
|
|
24
|
+
|
|
25
|
+
// Wait for a specific request/response
|
|
26
|
+
const responsePromise = page.waitForResponse('**/api/users');
|
|
27
|
+
await page.getByRole('button', { name: 'Load' }).click();
|
|
28
|
+
await responsePromise;
|
|
29
|
+
|
|
30
|
+
// HAR replay — serve recorded responses
|
|
31
|
+
await page.routeFromHAR('tests/data/api.har', { url: '**/api/**' });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Patterns
|
|
35
|
+
|
|
36
|
+
### Route Interception Basics
|
|
37
|
+
|
|
38
|
+
**Use when**: You need to intercept any HTTP request made by the page to fulfill, modify, or block it.
|
|
39
|
+
**Avoid when**: You want to test the real integration between frontend and backend (use real API calls instead).
|
|
40
|
+
|
|
41
|
+
`page.route()` registers a handler that runs for every request matching a URL pattern. Each handler must call exactly one of `route.fulfill()`, `route.continue()`, or `route.abort()`.
|
|
42
|
+
|
|
43
|
+
**TypeScript**
|
|
44
|
+
```typescript
|
|
45
|
+
import { test, expect } from '@playwright/test';
|
|
46
|
+
|
|
47
|
+
test('route interception basics', async ({ page }) => {
|
|
48
|
+
// Intercept before navigating — routes must be set up first
|
|
49
|
+
await page.route('**/api/users', (route) => {
|
|
50
|
+
route.fulfill({
|
|
51
|
+
status: 200,
|
|
52
|
+
contentType: 'application/json',
|
|
53
|
+
body: JSON.stringify([{ id: 1, name: 'Alice' }]),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await page.goto('/dashboard');
|
|
58
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
59
|
+
|
|
60
|
+
// Remove the route when done (important for cleanup)
|
|
61
|
+
await page.unroute('**/api/users');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('context-level routes apply to all pages', async ({ context, page }) => {
|
|
65
|
+
// Routes on the context apply to every page in that context
|
|
66
|
+
await context.route('**/api/config', (route) =>
|
|
67
|
+
route.fulfill({ json: { theme: 'dark', locale: 'en' } })
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await page.goto('/settings');
|
|
71
|
+
await expect(page.getByText('Dark')).toBeVisible();
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**JavaScript**
|
|
76
|
+
```javascript
|
|
77
|
+
const { test, expect } = require('@playwright/test');
|
|
78
|
+
|
|
79
|
+
test('route interception basics', async ({ page }) => {
|
|
80
|
+
await page.route('**/api/users', (route) => {
|
|
81
|
+
route.fulfill({
|
|
82
|
+
status: 200,
|
|
83
|
+
contentType: 'application/json',
|
|
84
|
+
body: JSON.stringify([{ id: 1, name: 'Alice' }]),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await page.goto('/dashboard');
|
|
89
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
90
|
+
|
|
91
|
+
await page.unroute('**/api/users');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('context-level routes apply to all pages', async ({ context, page }) => {
|
|
95
|
+
await context.route('**/api/config', (route) =>
|
|
96
|
+
route.fulfill({ json: { theme: 'dark', locale: 'en' } })
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await page.goto('/settings');
|
|
100
|
+
await expect(page.getByText('Dark')).toBeVisible();
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Mocking REST Responses
|
|
105
|
+
|
|
106
|
+
**Use when**: Your frontend depends on a REST API and you want deterministic, instant responses with controlled data.
|
|
107
|
+
**Avoid when**: You need to verify that your frontend sends the correct request body or headers to the real API (use `route.continue()` with `waitForRequest` instead).
|
|
108
|
+
|
|
109
|
+
**TypeScript**
|
|
110
|
+
```typescript
|
|
111
|
+
import { test, expect } from '@playwright/test';
|
|
112
|
+
|
|
113
|
+
const mockUsers = [
|
|
114
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
|
|
115
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
test('mock a GET endpoint with JSON', async ({ page }) => {
|
|
119
|
+
await page.route('**/api/users', (route) =>
|
|
120
|
+
route.fulfill({ json: mockUsers })
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await page.goto('/users');
|
|
124
|
+
await expect(page.getByRole('row')).toHaveCount(3); // header + 2 data rows
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('mock a POST endpoint and verify the request', async ({ page }) => {
|
|
128
|
+
await page.route('**/api/users', (route) => {
|
|
129
|
+
if (route.request().method() === 'POST') {
|
|
130
|
+
return route.fulfill({
|
|
131
|
+
status: 201,
|
|
132
|
+
json: { id: 3, name: 'Charlie', email: 'charlie@example.com' },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Let other methods through
|
|
136
|
+
return route.continue();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await page.goto('/users/new');
|
|
140
|
+
await page.getByLabel('Name').fill('Charlie');
|
|
141
|
+
await page.getByLabel('Email').fill('charlie@example.com');
|
|
142
|
+
await page.getByRole('button', { name: 'Create' }).click();
|
|
143
|
+
|
|
144
|
+
await expect(page.getByText('Charlie')).toBeVisible();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('mock with custom headers and status', async ({ page }) => {
|
|
148
|
+
await page.route('**/api/users', (route) =>
|
|
149
|
+
route.fulfill({
|
|
150
|
+
status: 200,
|
|
151
|
+
headers: {
|
|
152
|
+
'content-type': 'application/json',
|
|
153
|
+
'x-total-count': '42',
|
|
154
|
+
'x-request-id': 'test-abc-123',
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify(mockUsers),
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await page.goto('/users');
|
|
161
|
+
await expect(page.getByText('42 total')).toBeVisible();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('mock empty state', async ({ page }) => {
|
|
165
|
+
await page.route('**/api/users', (route) =>
|
|
166
|
+
route.fulfill({ json: [] })
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await page.goto('/users');
|
|
170
|
+
await expect(page.getByText('No users found')).toBeVisible();
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**JavaScript**
|
|
175
|
+
```javascript
|
|
176
|
+
const { test, expect } = require('@playwright/test');
|
|
177
|
+
|
|
178
|
+
const mockUsers = [
|
|
179
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
|
|
180
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
test('mock a GET endpoint with JSON', async ({ page }) => {
|
|
184
|
+
await page.route('**/api/users', (route) =>
|
|
185
|
+
route.fulfill({ json: mockUsers })
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
await page.goto('/users');
|
|
189
|
+
await expect(page.getByRole('row')).toHaveCount(3);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('mock a POST endpoint and verify the request', async ({ page }) => {
|
|
193
|
+
await page.route('**/api/users', (route) => {
|
|
194
|
+
if (route.request().method() === 'POST') {
|
|
195
|
+
return route.fulfill({
|
|
196
|
+
status: 201,
|
|
197
|
+
json: { id: 3, name: 'Charlie', email: 'charlie@example.com' },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return route.continue();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await page.goto('/users/new');
|
|
204
|
+
await page.getByLabel('Name').fill('Charlie');
|
|
205
|
+
await page.getByLabel('Email').fill('charlie@example.com');
|
|
206
|
+
await page.getByRole('button', { name: 'Create' }).click();
|
|
207
|
+
|
|
208
|
+
await expect(page.getByText('Charlie')).toBeVisible();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('mock empty state', async ({ page }) => {
|
|
212
|
+
await page.route('**/api/users', (route) =>
|
|
213
|
+
route.fulfill({ json: [] })
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await page.goto('/users');
|
|
217
|
+
await expect(page.getByText('No users found')).toBeVisible();
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Mocking GraphQL
|
|
222
|
+
|
|
223
|
+
**Use when**: Your frontend uses GraphQL and you want to mock specific queries or mutations by operation name.
|
|
224
|
+
**Avoid when**: The GraphQL endpoint is part of your own backend and you want full integration coverage.
|
|
225
|
+
|
|
226
|
+
**TypeScript**
|
|
227
|
+
```typescript
|
|
228
|
+
import { test, expect } from '@playwright/test';
|
|
229
|
+
|
|
230
|
+
test('mock a GraphQL query by operation name', async ({ page }) => {
|
|
231
|
+
await page.route('**/graphql', async (route) => {
|
|
232
|
+
const request = route.request();
|
|
233
|
+
const postData = request.postDataJSON();
|
|
234
|
+
|
|
235
|
+
if (postData.operationName === 'GetUsers') {
|
|
236
|
+
return route.fulfill({
|
|
237
|
+
json: {
|
|
238
|
+
data: {
|
|
239
|
+
users: [
|
|
240
|
+
{ id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
241
|
+
{ id: '2', name: 'Bob', email: 'bob@example.com' },
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (postData.operationName === 'GetUser') {
|
|
249
|
+
return route.fulfill({
|
|
250
|
+
json: {
|
|
251
|
+
data: {
|
|
252
|
+
user: { id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Let unmocked operations through to the real server
|
|
259
|
+
return route.continue();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await page.goto('/users');
|
|
263
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
264
|
+
await expect(page.getByText('Bob')).toBeVisible();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('mock a GraphQL mutation', async ({ page }) => {
|
|
268
|
+
await page.route('**/graphql', async (route) => {
|
|
269
|
+
const { operationName, variables } = route.request().postDataJSON();
|
|
270
|
+
|
|
271
|
+
if (operationName === 'CreateUser') {
|
|
272
|
+
return route.fulfill({
|
|
273
|
+
json: {
|
|
274
|
+
data: {
|
|
275
|
+
createUser: {
|
|
276
|
+
id: '99',
|
|
277
|
+
name: variables.input.name,
|
|
278
|
+
email: variables.input.email,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return route.continue();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await page.goto('/users/new');
|
|
289
|
+
await page.getByLabel('Name').fill('Charlie');
|
|
290
|
+
await page.getByLabel('Email').fill('charlie@example.com');
|
|
291
|
+
await page.getByRole('button', { name: 'Create' }).click();
|
|
292
|
+
|
|
293
|
+
await expect(page.getByText('Charlie')).toBeVisible();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('mock GraphQL errors', async ({ page }) => {
|
|
297
|
+
await page.route('**/graphql', async (route) => {
|
|
298
|
+
const { operationName } = route.request().postDataJSON();
|
|
299
|
+
|
|
300
|
+
if (operationName === 'GetUsers') {
|
|
301
|
+
return route.fulfill({
|
|
302
|
+
json: {
|
|
303
|
+
data: null,
|
|
304
|
+
errors: [
|
|
305
|
+
{
|
|
306
|
+
message: 'Not authorized',
|
|
307
|
+
extensions: { code: 'UNAUTHORIZED' },
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return route.continue();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await page.goto('/users');
|
|
318
|
+
await expect(page.getByText('Not authorized')).toBeVisible();
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**JavaScript**
|
|
323
|
+
```javascript
|
|
324
|
+
const { test, expect } = require('@playwright/test');
|
|
325
|
+
|
|
326
|
+
test('mock a GraphQL query by operation name', async ({ page }) => {
|
|
327
|
+
await page.route('**/graphql', async (route) => {
|
|
328
|
+
const postData = route.request().postDataJSON();
|
|
329
|
+
|
|
330
|
+
if (postData.operationName === 'GetUsers') {
|
|
331
|
+
return route.fulfill({
|
|
332
|
+
json: {
|
|
333
|
+
data: {
|
|
334
|
+
users: [
|
|
335
|
+
{ id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
336
|
+
{ id: '2', name: 'Bob', email: 'bob@example.com' },
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return route.continue();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await page.goto('/users');
|
|
347
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
348
|
+
await expect(page.getByText('Bob')).toBeVisible();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('mock a GraphQL mutation', async ({ page }) => {
|
|
352
|
+
await page.route('**/graphql', async (route) => {
|
|
353
|
+
const { operationName, variables } = route.request().postDataJSON();
|
|
354
|
+
|
|
355
|
+
if (operationName === 'CreateUser') {
|
|
356
|
+
return route.fulfill({
|
|
357
|
+
json: {
|
|
358
|
+
data: {
|
|
359
|
+
createUser: {
|
|
360
|
+
id: '99',
|
|
361
|
+
name: variables.input.name,
|
|
362
|
+
email: variables.input.email,
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return route.continue();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await page.goto('/users/new');
|
|
373
|
+
await page.getByLabel('Name').fill('Charlie');
|
|
374
|
+
await page.getByLabel('Email').fill('charlie@example.com');
|
|
375
|
+
await page.getByRole('button', { name: 'Create' }).click();
|
|
376
|
+
|
|
377
|
+
await expect(page.getByText('Charlie')).toBeVisible();
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Modifying Responses
|
|
382
|
+
|
|
383
|
+
**Use when**: You need the real API response but want to tweak specific fields -- inject test data, override feature flags, simulate edge cases in real data.
|
|
384
|
+
**Avoid when**: You can fully mock the response. `route.fetch()` adds a real network round-trip, so it is slower.
|
|
385
|
+
|
|
386
|
+
**TypeScript**
|
|
387
|
+
```typescript
|
|
388
|
+
import { test, expect } from '@playwright/test';
|
|
389
|
+
|
|
390
|
+
test('modify a real API response', async ({ page }) => {
|
|
391
|
+
await page.route('**/api/users', async (route) => {
|
|
392
|
+
// Fetch the real response from the server
|
|
393
|
+
const response = await route.fetch();
|
|
394
|
+
const users = await response.json();
|
|
395
|
+
|
|
396
|
+
// Inject a test user into the real data
|
|
397
|
+
users.push({ id: 999, name: 'Test User', email: 'test@example.com' });
|
|
398
|
+
|
|
399
|
+
await route.fulfill({ response, json: users });
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await page.goto('/users');
|
|
403
|
+
await expect(page.getByText('Test User')).toBeVisible();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('override feature flags from real config', async ({ page }) => {
|
|
407
|
+
await page.route('**/api/config', async (route) => {
|
|
408
|
+
const response = await route.fetch();
|
|
409
|
+
const config = await response.json();
|
|
410
|
+
|
|
411
|
+
// Enable a feature flag for testing
|
|
412
|
+
config.featureFlags = {
|
|
413
|
+
...config.featureFlags,
|
|
414
|
+
newCheckout: true,
|
|
415
|
+
darkMode: true,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
await route.fulfill({ response, json: config });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await page.goto('/settings');
|
|
422
|
+
await expect(page.getByRole('switch', { name: 'Dark mode' })).toBeVisible();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('modify response headers', async ({ page }) => {
|
|
426
|
+
await page.route('**/api/data', async (route) => {
|
|
427
|
+
const response = await route.fetch();
|
|
428
|
+
|
|
429
|
+
await route.fulfill({
|
|
430
|
+
response,
|
|
431
|
+
headers: {
|
|
432
|
+
...response.headers(),
|
|
433
|
+
'cache-control': 'no-cache',
|
|
434
|
+
'x-test-header': 'injected',
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await page.goto('/data');
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**JavaScript**
|
|
444
|
+
```javascript
|
|
445
|
+
const { test, expect } = require('@playwright/test');
|
|
446
|
+
|
|
447
|
+
test('modify a real API response', async ({ page }) => {
|
|
448
|
+
await page.route('**/api/users', async (route) => {
|
|
449
|
+
const response = await route.fetch();
|
|
450
|
+
const users = await response.json();
|
|
451
|
+
|
|
452
|
+
users.push({ id: 999, name: 'Test User', email: 'test@example.com' });
|
|
453
|
+
|
|
454
|
+
await route.fulfill({ response, json: users });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await page.goto('/users');
|
|
458
|
+
await expect(page.getByText('Test User')).toBeVisible();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('override feature flags from real config', async ({ page }) => {
|
|
462
|
+
await page.route('**/api/config', async (route) => {
|
|
463
|
+
const response = await route.fetch();
|
|
464
|
+
const config = await response.json();
|
|
465
|
+
|
|
466
|
+
config.featureFlags = {
|
|
467
|
+
...config.featureFlags,
|
|
468
|
+
newCheckout: true,
|
|
469
|
+
darkMode: true,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
await route.fulfill({ response, json: config });
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
await page.goto('/settings');
|
|
476
|
+
await expect(page.getByRole('switch', { name: 'Dark mode' })).toBeVisible();
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Request Blocking
|
|
481
|
+
|
|
482
|
+
**Use when**: Blocking analytics, ads, third-party scripts, images, or fonts to speed up tests and eliminate flakiness from external dependencies.
|
|
483
|
+
**Avoid when**: The blocked resource is required for the feature under test.
|
|
484
|
+
|
|
485
|
+
**TypeScript**
|
|
486
|
+
```typescript
|
|
487
|
+
import { test, expect } from '@playwright/test';
|
|
488
|
+
|
|
489
|
+
test('block analytics and tracking scripts', async ({ page }) => {
|
|
490
|
+
await page.route(/(google-analytics|segment|hotjar|mixpanel)/, (route) =>
|
|
491
|
+
route.abort()
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
await page.goto('/dashboard');
|
|
495
|
+
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('block images to speed up tests', async ({ page }) => {
|
|
499
|
+
await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', (route) =>
|
|
500
|
+
route.abort()
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
await page.goto('/gallery');
|
|
504
|
+
// Test interactions without waiting for image downloads
|
|
505
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('block specific third-party domains', async ({ page }) => {
|
|
509
|
+
const blockedDomains = [
|
|
510
|
+
'ads.example.com',
|
|
511
|
+
'tracker.example.com',
|
|
512
|
+
'cdn.slow-service.com',
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
await page.route('**/*', (route) => {
|
|
516
|
+
const url = new URL(route.request().url());
|
|
517
|
+
if (blockedDomains.includes(url.hostname)) {
|
|
518
|
+
return route.abort();
|
|
519
|
+
}
|
|
520
|
+
return route.continue();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await page.goto('/home');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('block by resource type', async ({ context }) => {
|
|
527
|
+
// Context-level blocking affects all pages
|
|
528
|
+
await context.route('**/*', (route) => {
|
|
529
|
+
const resourceType = route.request().resourceType();
|
|
530
|
+
if (['image', 'font', 'media'].includes(resourceType)) {
|
|
531
|
+
return route.abort();
|
|
532
|
+
}
|
|
533
|
+
return route.continue();
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**JavaScript**
|
|
539
|
+
```javascript
|
|
540
|
+
const { test, expect } = require('@playwright/test');
|
|
541
|
+
|
|
542
|
+
test('block analytics and tracking scripts', async ({ page }) => {
|
|
543
|
+
await page.route(/(google-analytics|segment|hotjar|mixpanel)/, (route) =>
|
|
544
|
+
route.abort()
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
await page.goto('/dashboard');
|
|
548
|
+
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('block images to speed up tests', async ({ page }) => {
|
|
552
|
+
await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', (route) =>
|
|
553
|
+
route.abort()
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
await page.goto('/gallery');
|
|
557
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('block by resource type', async ({ context }) => {
|
|
561
|
+
await context.route('**/*', (route) => {
|
|
562
|
+
const resourceType = route.request().resourceType();
|
|
563
|
+
if (['image', 'font', 'media'].includes(resourceType)) {
|
|
564
|
+
return route.abort();
|
|
565
|
+
}
|
|
566
|
+
return route.continue();
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### HAR Recording and Replay
|
|
572
|
+
|
|
573
|
+
**Use when**: You want to capture real network traffic once and replay it in tests for speed and determinism. Great for complex APIs with many endpoints or when API access is limited.
|
|
574
|
+
**Avoid when**: API responses change frequently and stale recordings would cause false passes. Keep HAR files in version control and update them regularly.
|
|
575
|
+
|
|
576
|
+
**TypeScript**
|
|
577
|
+
```typescript
|
|
578
|
+
import { test, expect } from '@playwright/test';
|
|
579
|
+
|
|
580
|
+
// Record a HAR file — run once to capture traffic, then replay
|
|
581
|
+
test('record HAR for later replay', async ({ page }) => {
|
|
582
|
+
// This records all matching network traffic to the HAR file.
|
|
583
|
+
// If the file already exists and responses match, it serves from HAR.
|
|
584
|
+
// If a request has no match in the HAR, it falls through to the network
|
|
585
|
+
// and the new response is appended to the HAR file.
|
|
586
|
+
await page.routeFromHAR('tests/data/users-api.har', {
|
|
587
|
+
url: '**/api/**',
|
|
588
|
+
update: true, // set to true to record, false (or omit) to replay
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await page.goto('/users');
|
|
592
|
+
await expect(page.getByRole('row')).toHaveCount(6);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Replay from a previously recorded HAR file
|
|
596
|
+
test('replay from HAR', async ({ page }) => {
|
|
597
|
+
await page.routeFromHAR('tests/data/users-api.har', {
|
|
598
|
+
url: '**/api/**',
|
|
599
|
+
// update: false is the default — serves from the HAR file
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
await page.goto('/users');
|
|
603
|
+
await expect(page.getByRole('row')).toHaveCount(6);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// HAR with notFound option — control what happens for unmatched requests
|
|
607
|
+
test('HAR replay with fallback behavior', async ({ page }) => {
|
|
608
|
+
await page.routeFromHAR('tests/data/users-api.har', {
|
|
609
|
+
url: '**/api/**',
|
|
610
|
+
notFound: 'abort', // 'abort' fails unmatched requests; 'fallback' lets them through
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
await page.goto('/users');
|
|
614
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Context-level HAR replay
|
|
618
|
+
test('HAR replay at context level', async ({ context, page }) => {
|
|
619
|
+
await context.routeFromHAR('tests/data/full-app.har', {
|
|
620
|
+
url: '**/api/**',
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
await page.goto('/dashboard');
|
|
624
|
+
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**JavaScript**
|
|
629
|
+
```javascript
|
|
630
|
+
const { test, expect } = require('@playwright/test');
|
|
631
|
+
|
|
632
|
+
test('record HAR for later replay', async ({ page }) => {
|
|
633
|
+
await page.routeFromHAR('tests/data/users-api.har', {
|
|
634
|
+
url: '**/api/**',
|
|
635
|
+
update: true,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
await page.goto('/users');
|
|
639
|
+
await expect(page.getByRole('row')).toHaveCount(6);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('replay from HAR', async ({ page }) => {
|
|
643
|
+
await page.routeFromHAR('tests/data/users-api.har', {
|
|
644
|
+
url: '**/api/**',
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
await page.goto('/users');
|
|
648
|
+
await expect(page.getByRole('row')).toHaveCount(6);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test('HAR replay with fallback behavior', async ({ page }) => {
|
|
652
|
+
await page.routeFromHAR('tests/data/users-api.har', {
|
|
653
|
+
url: '**/api/**',
|
|
654
|
+
notFound: 'abort',
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await page.goto('/users');
|
|
658
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### On-Demand HAR Recording in Tracing (Playwright 1.60+)
|
|
663
|
+
|
|
664
|
+
**Use when**: You want to capture network traffic for a *specific slice* of a test — one user flow, one step — instead of the whole session, and you want to start/stop recording from inside the test body.
|
|
665
|
+
**Avoid when**: You only need replay for mocking (use `routeFromHAR`), or you want HAR for the entire context (set `recordHar` at context creation). This API is for scoped, programmatic capture.
|
|
666
|
+
|
|
667
|
+
Playwright 1.60 promotes HAR recording to a first-class tracing API: `context.tracing.startHar()` / `context.tracing.stopHar()`. It accepts the same `content`, `mode`, and `urlFilter` options as the context-level `recordHar`, and returns a disposable so `await using` cleans up automatically even if the test throws.
|
|
668
|
+
|
|
669
|
+
**TypeScript**
|
|
670
|
+
```typescript
|
|
671
|
+
import { test, expect } from '@playwright/test';
|
|
672
|
+
|
|
673
|
+
test('capture HAR for the checkout flow only', async ({ context, page }) => {
|
|
674
|
+
await page.goto('/cart');
|
|
675
|
+
|
|
676
|
+
// Record only the network traffic during checkout
|
|
677
|
+
await context.tracing.startHar({
|
|
678
|
+
path: 'test-results/checkout.har',
|
|
679
|
+
urlFilter: '**/api/**',
|
|
680
|
+
content: 'embed', // 'omit' | 'embed' | 'attach'
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await page.getByRole('button', { name: 'Checkout' }).click();
|
|
684
|
+
await page.getByRole('button', { name: 'Place order' }).click();
|
|
685
|
+
await expect(page.getByText('Order confirmed')).toBeVisible();
|
|
686
|
+
|
|
687
|
+
await context.tracing.stopHar();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('scoped capture with automatic cleanup', async ({ context, page }) => {
|
|
691
|
+
await page.goto('/search');
|
|
692
|
+
|
|
693
|
+
// `await using` stops and flushes the HAR when the block exits — even on failure
|
|
694
|
+
await using _har = await context.tracing.startHar({
|
|
695
|
+
path: 'test-results/search.har',
|
|
696
|
+
urlFilter: '**/api/search**',
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
await page.getByLabel('Search').fill('playwright');
|
|
700
|
+
await page.getByRole('button', { name: 'Search' }).click();
|
|
701
|
+
await expect(page.getByRole('listitem')).not.toHaveCount(0);
|
|
702
|
+
});
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**JavaScript**
|
|
706
|
+
```javascript
|
|
707
|
+
const { test, expect } = require('@playwright/test');
|
|
708
|
+
|
|
709
|
+
test('capture HAR for the checkout flow only', async ({ context, page }) => {
|
|
710
|
+
await page.goto('/cart');
|
|
711
|
+
|
|
712
|
+
await context.tracing.startHar({
|
|
713
|
+
path: 'test-results/checkout.har',
|
|
714
|
+
urlFilter: '**/api/**',
|
|
715
|
+
content: 'embed',
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
await page.getByRole('button', { name: 'Checkout' }).click();
|
|
719
|
+
await page.getByRole('button', { name: 'Place order' }).click();
|
|
720
|
+
await expect(page.getByText('Order confirmed')).toBeVisible();
|
|
721
|
+
|
|
722
|
+
await context.tracing.stopHar();
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
> **`await using` note**: the disposable form requires TypeScript 5.2+ / a runtime with explicit resource management. If your toolchain doesn't support it, call `stopHar()` explicitly in a `finally` block instead.
|
|
727
|
+
|
|
728
|
+
### Conditional Mocking
|
|
729
|
+
|
|
730
|
+
**Use when**: You need different responses based on the request method, body, headers, or query parameters. Common for paginated APIs, search endpoints, and role-based access.
|
|
731
|
+
**Avoid when**: Simple static mocking suffices. Don't over-engineer route handlers.
|
|
732
|
+
|
|
733
|
+
**TypeScript**
|
|
734
|
+
```typescript
|
|
735
|
+
import { test, expect } from '@playwright/test';
|
|
736
|
+
|
|
737
|
+
test('respond based on request method', async ({ page }) => {
|
|
738
|
+
await page.route('**/api/users', (route) => {
|
|
739
|
+
const method = route.request().method();
|
|
740
|
+
|
|
741
|
+
switch (method) {
|
|
742
|
+
case 'GET':
|
|
743
|
+
return route.fulfill({
|
|
744
|
+
json: [{ id: 1, name: 'Alice' }],
|
|
745
|
+
});
|
|
746
|
+
case 'POST':
|
|
747
|
+
return route.fulfill({
|
|
748
|
+
status: 201,
|
|
749
|
+
json: { id: 2, name: 'Bob' },
|
|
750
|
+
});
|
|
751
|
+
case 'DELETE':
|
|
752
|
+
return route.fulfill({ status: 204, body: '' });
|
|
753
|
+
default:
|
|
754
|
+
return route.continue();
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
await page.goto('/users');
|
|
759
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('respond based on query parameters', async ({ page }) => {
|
|
763
|
+
await page.route('**/api/users*', (route) => {
|
|
764
|
+
const url = new URL(route.request().url());
|
|
765
|
+
const page_num = parseInt(url.searchParams.get('page') || '1');
|
|
766
|
+
const role = url.searchParams.get('role');
|
|
767
|
+
|
|
768
|
+
const allUsers = [
|
|
769
|
+
{ id: 1, name: 'Alice', role: 'admin' },
|
|
770
|
+
{ id: 2, name: 'Bob', role: 'user' },
|
|
771
|
+
{ id: 3, name: 'Charlie', role: 'user' },
|
|
772
|
+
{ id: 4, name: 'Diana', role: 'admin' },
|
|
773
|
+
];
|
|
774
|
+
|
|
775
|
+
let filtered = allUsers;
|
|
776
|
+
if (role) {
|
|
777
|
+
filtered = allUsers.filter((u) => u.role === role);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const perPage = 2;
|
|
781
|
+
const start = (page_num - 1) * perPage;
|
|
782
|
+
const paginated = filtered.slice(start, start + perPage);
|
|
783
|
+
|
|
784
|
+
return route.fulfill({
|
|
785
|
+
json: paginated,
|
|
786
|
+
headers: {
|
|
787
|
+
'content-type': 'application/json',
|
|
788
|
+
'x-total-count': String(filtered.length),
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
await page.goto('/users');
|
|
794
|
+
await expect(page.getByRole('row')).toHaveCount(3); // header + 2 rows
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test('respond based on request body', async ({ page }) => {
|
|
798
|
+
await page.route('**/api/search', (route) => {
|
|
799
|
+
const body = route.request().postDataJSON();
|
|
800
|
+
const query = body?.query?.toLowerCase() || '';
|
|
801
|
+
|
|
802
|
+
const results = {
|
|
803
|
+
playwright: [{ title: 'Playwright Docs' }, { title: 'Playwright GitHub' }],
|
|
804
|
+
cypress: [{ title: 'Cypress Docs' }],
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
return route.fulfill({
|
|
808
|
+
json: results[query] || [],
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
await page.goto('/search');
|
|
813
|
+
await page.getByLabel('Search').fill('playwright');
|
|
814
|
+
await page.getByRole('button', { name: 'Search' }).click();
|
|
815
|
+
await expect(page.getByRole('listitem')).toHaveCount(2);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test('respond based on request headers', async ({ page }) => {
|
|
819
|
+
await page.route('**/api/users', (route) => {
|
|
820
|
+
const authHeader = route.request().headers()['authorization'];
|
|
821
|
+
|
|
822
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
823
|
+
return route.fulfill({
|
|
824
|
+
status: 401,
|
|
825
|
+
json: { error: 'Unauthorized' },
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return route.fulfill({
|
|
830
|
+
json: [{ id: 1, name: 'Alice' }],
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
await page.goto('/login');
|
|
835
|
+
await expect(page.getByText('Unauthorized')).toBeVisible();
|
|
836
|
+
});
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**JavaScript**
|
|
840
|
+
```javascript
|
|
841
|
+
const { test, expect } = require('@playwright/test');
|
|
842
|
+
|
|
843
|
+
test('respond based on request method', async ({ page }) => {
|
|
844
|
+
await page.route('**/api/users', (route) => {
|
|
845
|
+
const method = route.request().method();
|
|
846
|
+
|
|
847
|
+
switch (method) {
|
|
848
|
+
case 'GET':
|
|
849
|
+
return route.fulfill({ json: [{ id: 1, name: 'Alice' }] });
|
|
850
|
+
case 'POST':
|
|
851
|
+
return route.fulfill({ status: 201, json: { id: 2, name: 'Bob' } });
|
|
852
|
+
case 'DELETE':
|
|
853
|
+
return route.fulfill({ status: 204, body: '' });
|
|
854
|
+
default:
|
|
855
|
+
return route.continue();
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
await page.goto('/users');
|
|
860
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test('respond based on query parameters', async ({ page }) => {
|
|
864
|
+
await page.route('**/api/users*', (route) => {
|
|
865
|
+
const url = new URL(route.request().url());
|
|
866
|
+
const role = url.searchParams.get('role');
|
|
867
|
+
|
|
868
|
+
const allUsers = [
|
|
869
|
+
{ id: 1, name: 'Alice', role: 'admin' },
|
|
870
|
+
{ id: 2, name: 'Bob', role: 'user' },
|
|
871
|
+
{ id: 3, name: 'Charlie', role: 'user' },
|
|
872
|
+
];
|
|
873
|
+
|
|
874
|
+
const filtered = role ? allUsers.filter((u) => u.role === role) : allUsers;
|
|
875
|
+
|
|
876
|
+
return route.fulfill({ json: filtered });
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
await page.goto('/users?role=admin');
|
|
880
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
test('respond based on request body', async ({ page }) => {
|
|
884
|
+
await page.route('**/api/search', (route) => {
|
|
885
|
+
const body = route.request().postDataJSON();
|
|
886
|
+
const query = body?.query?.toLowerCase() || '';
|
|
887
|
+
|
|
888
|
+
const results = {
|
|
889
|
+
playwright: [{ title: 'Playwright Docs' }, { title: 'Playwright GitHub' }],
|
|
890
|
+
cypress: [{ title: 'Cypress Docs' }],
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
return route.fulfill({ json: results[query] || [] });
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
await page.goto('/search');
|
|
897
|
+
await page.getByLabel('Search').fill('playwright');
|
|
898
|
+
await page.getByRole('button', { name: 'Search' }).click();
|
|
899
|
+
await expect(page.getByRole('listitem')).toHaveCount(2);
|
|
900
|
+
});
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
### Network Error Simulation
|
|
904
|
+
|
|
905
|
+
**Use when**: Testing how your UI handles server errors, timeouts, and connection failures. Essential for verifying error boundaries, retry logic, and degraded-mode UX.
|
|
906
|
+
**Avoid when**: The test is about the happy path. Only introduce errors when testing error handling.
|
|
907
|
+
|
|
908
|
+
**TypeScript**
|
|
909
|
+
```typescript
|
|
910
|
+
import { test, expect } from '@playwright/test';
|
|
911
|
+
|
|
912
|
+
test('simulate a 500 server error', async ({ page }) => {
|
|
913
|
+
await page.route('**/api/users', (route) =>
|
|
914
|
+
route.fulfill({
|
|
915
|
+
status: 500,
|
|
916
|
+
json: { error: 'Internal Server Error' },
|
|
917
|
+
})
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
await page.goto('/users');
|
|
921
|
+
await expect(page.getByText('Something went wrong')).toBeVisible();
|
|
922
|
+
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test('simulate a 403 forbidden', async ({ page }) => {
|
|
926
|
+
await page.route('**/api/admin/**', (route) =>
|
|
927
|
+
route.fulfill({
|
|
928
|
+
status: 403,
|
|
929
|
+
json: { error: 'Forbidden', message: 'Admin access required' },
|
|
930
|
+
})
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
await page.goto('/admin/settings');
|
|
934
|
+
await expect(page.getByText('Admin access required')).toBeVisible();
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
test('simulate a 404 not found', async ({ page }) => {
|
|
938
|
+
await page.route('**/api/users/999', (route) =>
|
|
939
|
+
route.fulfill({
|
|
940
|
+
status: 404,
|
|
941
|
+
json: { error: 'User not found' },
|
|
942
|
+
})
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
await page.goto('/users/999');
|
|
946
|
+
await expect(page.getByText('User not found')).toBeVisible();
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test('simulate a network error (connection refused)', async ({ page }) => {
|
|
950
|
+
await page.route('**/api/users', (route) =>
|
|
951
|
+
route.abort('connectionrefused')
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
await page.goto('/users');
|
|
955
|
+
await expect(page.getByText('Network error')).toBeVisible();
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test('simulate a timeout', async ({ page }) => {
|
|
959
|
+
await page.route('**/api/users', async (route) => {
|
|
960
|
+
// Delay longer than the app's fetch timeout to trigger a timeout error
|
|
961
|
+
await new Promise((resolve) => setTimeout(resolve, 30_000));
|
|
962
|
+
await route.fulfill({ json: [] });
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
await page.goto('/users');
|
|
966
|
+
// The app should show a timeout message before the route resolves
|
|
967
|
+
await expect(page.getByText('Request timed out')).toBeVisible({
|
|
968
|
+
timeout: 15_000,
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
test('simulate intermittent failures then recovery', async ({ page }) => {
|
|
973
|
+
let requestCount = 0;
|
|
974
|
+
|
|
975
|
+
await page.route('**/api/users', (route) => {
|
|
976
|
+
requestCount++;
|
|
977
|
+
if (requestCount <= 2) {
|
|
978
|
+
return route.fulfill({
|
|
979
|
+
status: 503,
|
|
980
|
+
json: { error: 'Service Unavailable' },
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
return route.fulfill({
|
|
984
|
+
json: [{ id: 1, name: 'Alice' }],
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
await page.goto('/users');
|
|
989
|
+
await expect(page.getByText('Something went wrong')).toBeVisible();
|
|
990
|
+
|
|
991
|
+
// Simulate user clicking retry (which makes the 3rd request)
|
|
992
|
+
await page.getByRole('button', { name: 'Retry' }).click();
|
|
993
|
+
await expect(page.getByText('Something went wrong')).toBeVisible();
|
|
994
|
+
|
|
995
|
+
// Third attempt succeeds
|
|
996
|
+
await page.getByRole('button', { name: 'Retry' }).click();
|
|
997
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
998
|
+
});
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
**JavaScript**
|
|
1002
|
+
```javascript
|
|
1003
|
+
const { test, expect } = require('@playwright/test');
|
|
1004
|
+
|
|
1005
|
+
test('simulate a 500 server error', async ({ page }) => {
|
|
1006
|
+
await page.route('**/api/users', (route) =>
|
|
1007
|
+
route.fulfill({
|
|
1008
|
+
status: 500,
|
|
1009
|
+
json: { error: 'Internal Server Error' },
|
|
1010
|
+
})
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
await page.goto('/users');
|
|
1014
|
+
await expect(page.getByText('Something went wrong')).toBeVisible();
|
|
1015
|
+
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test('simulate a network error (connection refused)', async ({ page }) => {
|
|
1019
|
+
await page.route('**/api/users', (route) =>
|
|
1020
|
+
route.abort('connectionrefused')
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
await page.goto('/users');
|
|
1024
|
+
await expect(page.getByText('Network error')).toBeVisible();
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
test('simulate intermittent failures then recovery', async ({ page }) => {
|
|
1028
|
+
let requestCount = 0;
|
|
1029
|
+
|
|
1030
|
+
await page.route('**/api/users', (route) => {
|
|
1031
|
+
requestCount++;
|
|
1032
|
+
if (requestCount <= 2) {
|
|
1033
|
+
return route.fulfill({
|
|
1034
|
+
status: 503,
|
|
1035
|
+
json: { error: 'Service Unavailable' },
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
return route.fulfill({
|
|
1039
|
+
json: [{ id: 1, name: 'Alice' }],
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
await page.goto('/users');
|
|
1044
|
+
await expect(page.getByText('Something went wrong')).toBeVisible();
|
|
1045
|
+
|
|
1046
|
+
await page.getByRole('button', { name: 'Retry' }).click();
|
|
1047
|
+
await expect(page.getByText('Something went wrong')).toBeVisible();
|
|
1048
|
+
|
|
1049
|
+
await page.getByRole('button', { name: 'Retry' }).click();
|
|
1050
|
+
await expect(page.getByText('Alice')).toBeVisible();
|
|
1051
|
+
});
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
**Abort reasons**: `'aborted'`, `'accessdenied'`, `'addressunreachable'`, `'blockedbyclient'`, `'blockedbyresponse'`, `'connectionaborted'`, `'connectionclosed'`, `'connectionfailed'`, `'connectionrefused'`, `'connectionreset'`, `'internetdisconnected'`, `'namenotresolved'`, `'timedout'`, `'failed'`.
|
|
1055
|
+
|
|
1056
|
+
### Request Waiting
|
|
1057
|
+
|
|
1058
|
+
**Use when**: Synchronizing your test with network activity -- waiting for a request to be sent or a response to arrive before asserting on the UI.
|
|
1059
|
+
**Avoid when**: A web-first assertion on a locator is sufficient. Only use request waiting when you need to inspect the request/response itself.
|
|
1060
|
+
|
|
1061
|
+
**TypeScript**
|
|
1062
|
+
```typescript
|
|
1063
|
+
import { test, expect } from '@playwright/test';
|
|
1064
|
+
|
|
1065
|
+
test('wait for response and verify UI updates', async ({ page }) => {
|
|
1066
|
+
await page.goto('/users');
|
|
1067
|
+
|
|
1068
|
+
// CRITICAL: set up the wait BEFORE triggering the action
|
|
1069
|
+
const responsePromise = page.waitForResponse('**/api/users');
|
|
1070
|
+
await page.getByRole('button', { name: 'Refresh' }).click();
|
|
1071
|
+
const response = await responsePromise;
|
|
1072
|
+
|
|
1073
|
+
expect(response.status()).toBe(200);
|
|
1074
|
+
const users = await response.json();
|
|
1075
|
+
expect(users).toHaveLength(5);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
test('wait for request and verify payload', async ({ page }) => {
|
|
1079
|
+
await page.goto('/users/new');
|
|
1080
|
+
|
|
1081
|
+
const requestPromise = page.waitForRequest('**/api/users');
|
|
1082
|
+
await page.getByLabel('Name').fill('Alice');
|
|
1083
|
+
await page.getByLabel('Email').fill('alice@example.com');
|
|
1084
|
+
await page.getByRole('button', { name: 'Create' }).click();
|
|
1085
|
+
const request = await requestPromise;
|
|
1086
|
+
|
|
1087
|
+
expect(request.method()).toBe('POST');
|
|
1088
|
+
expect(request.postDataJSON()).toMatchObject({
|
|
1089
|
+
name: 'Alice',
|
|
1090
|
+
email: 'alice@example.com',
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test('wait for response with predicate function', async ({ page }) => {
|
|
1095
|
+
await page.goto('/dashboard');
|
|
1096
|
+
|
|
1097
|
+
// Wait for a specific response matching custom criteria
|
|
1098
|
+
const responsePromise = page.waitForResponse(
|
|
1099
|
+
(response) =>
|
|
1100
|
+
response.url().includes('/api/users') &&
|
|
1101
|
+
response.status() === 200 &&
|
|
1102
|
+
response.request().method() === 'GET'
|
|
1103
|
+
);
|
|
1104
|
+
await page.getByRole('button', { name: 'Load users' }).click();
|
|
1105
|
+
const response = await responsePromise;
|
|
1106
|
+
|
|
1107
|
+
const data = await response.json();
|
|
1108
|
+
expect(data.length).toBeGreaterThan(0);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
test('wait for multiple requests in sequence', async ({ page }) => {
|
|
1112
|
+
await page.goto('/checkout');
|
|
1113
|
+
|
|
1114
|
+
// Wait for multiple API calls that happen during a flow
|
|
1115
|
+
const [validateResponse, submitResponse] = await Promise.all([
|
|
1116
|
+
page.waitForResponse('**/api/cart/validate'),
|
|
1117
|
+
page.waitForResponse('**/api/orders'),
|
|
1118
|
+
page.getByRole('button', { name: 'Place order' }).click(),
|
|
1119
|
+
]);
|
|
1120
|
+
|
|
1121
|
+
expect(validateResponse.status()).toBe(200);
|
|
1122
|
+
expect(submitResponse.status()).toBe(201);
|
|
1123
|
+
});
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
**JavaScript**
|
|
1127
|
+
```javascript
|
|
1128
|
+
const { test, expect } = require('@playwright/test');
|
|
1129
|
+
|
|
1130
|
+
test('wait for response and verify UI updates', async ({ page }) => {
|
|
1131
|
+
await page.goto('/users');
|
|
1132
|
+
|
|
1133
|
+
const responsePromise = page.waitForResponse('**/api/users');
|
|
1134
|
+
await page.getByRole('button', { name: 'Refresh' }).click();
|
|
1135
|
+
const response = await responsePromise;
|
|
1136
|
+
|
|
1137
|
+
expect(response.status()).toBe(200);
|
|
1138
|
+
const users = await response.json();
|
|
1139
|
+
expect(users).toHaveLength(5);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test('wait for request and verify payload', async ({ page }) => {
|
|
1143
|
+
await page.goto('/users/new');
|
|
1144
|
+
|
|
1145
|
+
const requestPromise = page.waitForRequest('**/api/users');
|
|
1146
|
+
await page.getByLabel('Name').fill('Alice');
|
|
1147
|
+
await page.getByLabel('Email').fill('alice@example.com');
|
|
1148
|
+
await page.getByRole('button', { name: 'Create' }).click();
|
|
1149
|
+
const request = await requestPromise;
|
|
1150
|
+
|
|
1151
|
+
expect(request.method()).toBe('POST');
|
|
1152
|
+
expect(request.postDataJSON()).toMatchObject({
|
|
1153
|
+
name: 'Alice',
|
|
1154
|
+
email: 'alice@example.com',
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
test('wait for multiple requests in sequence', async ({ page }) => {
|
|
1159
|
+
await page.goto('/checkout');
|
|
1160
|
+
|
|
1161
|
+
const [validateResponse, submitResponse] = await Promise.all([
|
|
1162
|
+
page.waitForResponse('**/api/cart/validate'),
|
|
1163
|
+
page.waitForResponse('**/api/orders'),
|
|
1164
|
+
page.getByRole('button', { name: 'Place order' }).click(),
|
|
1165
|
+
]);
|
|
1166
|
+
|
|
1167
|
+
expect(validateResponse.status()).toBe(200);
|
|
1168
|
+
expect(submitResponse.status()).toBe(201);
|
|
1169
|
+
});
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
### Glob Patterns and URL Matching
|
|
1173
|
+
|
|
1174
|
+
**Use when**: You need to match URLs with wildcards, partial paths, or regex. Every `page.route()`, `waitForRequest()`, and `waitForResponse()` accepts glob patterns, strings, or regex.
|
|
1175
|
+
**Avoid when**: The URL is known and static. Use the exact string.
|
|
1176
|
+
|
|
1177
|
+
**TypeScript**
|
|
1178
|
+
```typescript
|
|
1179
|
+
import { test, expect } from '@playwright/test';
|
|
1180
|
+
|
|
1181
|
+
test('glob pattern examples', async ({ page }) => {
|
|
1182
|
+
// ** matches any path segments (including nested)
|
|
1183
|
+
await page.route('**/api/users', (route) =>
|
|
1184
|
+
route.fulfill({ json: [] })
|
|
1185
|
+
);
|
|
1186
|
+
// Matches: https://example.com/api/users
|
|
1187
|
+
// Matches: https://example.com/v2/api/users
|
|
1188
|
+
|
|
1189
|
+
// * matches any single path segment
|
|
1190
|
+
await page.route('**/api/users/*/orders', (route) =>
|
|
1191
|
+
route.fulfill({ json: [] })
|
|
1192
|
+
);
|
|
1193
|
+
// Matches: /api/users/123/orders
|
|
1194
|
+
// Matches: /api/users/abc/orders
|
|
1195
|
+
// Does NOT match: /api/users/123/456/orders
|
|
1196
|
+
|
|
1197
|
+
// Match file extensions
|
|
1198
|
+
await page.route('**/*.{png,jpg,svg}', (route) => route.abort());
|
|
1199
|
+
// Matches: /images/logo.png, /assets/photo.jpg
|
|
1200
|
+
|
|
1201
|
+
// Match query strings with *
|
|
1202
|
+
await page.route('**/api/search?q=*', (route) =>
|
|
1203
|
+
route.fulfill({ json: [] })
|
|
1204
|
+
);
|
|
1205
|
+
// Matches: /api/search?q=anything
|
|
1206
|
+
|
|
1207
|
+
// Use regex for complex patterns
|
|
1208
|
+
await page.route(/\/api\/users\/\d+$/, (route) =>
|
|
1209
|
+
route.fulfill({ json: { id: 1, name: 'Alice' } })
|
|
1210
|
+
);
|
|
1211
|
+
// Matches: /api/users/123, /api/users/456
|
|
1212
|
+
// Does NOT match: /api/users/abc, /api/users/123/orders
|
|
1213
|
+
|
|
1214
|
+
// Regex with captured groups for dynamic responses
|
|
1215
|
+
await page.route(/\/api\/users\/(\d+)/, (route) => {
|
|
1216
|
+
const match = route.request().url().match(/\/api\/users\/(\d+)/);
|
|
1217
|
+
const userId = match ? match[1] : '0';
|
|
1218
|
+
return route.fulfill({
|
|
1219
|
+
json: { id: parseInt(userId), name: `User ${userId}` },
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
await page.goto('/users');
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
test('match all requests on a domain', async ({ page }) => {
|
|
1227
|
+
// Block everything from a specific domain
|
|
1228
|
+
await page.route('https://analytics.example.com/**', (route) =>
|
|
1229
|
+
route.abort()
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
await page.goto('/dashboard');
|
|
1233
|
+
});
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
**JavaScript**
|
|
1237
|
+
```javascript
|
|
1238
|
+
const { test, expect } = require('@playwright/test');
|
|
1239
|
+
|
|
1240
|
+
test('glob pattern examples', async ({ page }) => {
|
|
1241
|
+
// ** matches any path segments
|
|
1242
|
+
await page.route('**/api/users', (route) =>
|
|
1243
|
+
route.fulfill({ json: [] })
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
// * matches a single path segment
|
|
1247
|
+
await page.route('**/api/users/*/orders', (route) =>
|
|
1248
|
+
route.fulfill({ json: [] })
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
// Match file extensions
|
|
1252
|
+
await page.route('**/*.{png,jpg,svg}', (route) => route.abort());
|
|
1253
|
+
|
|
1254
|
+
// Regex for complex patterns
|
|
1255
|
+
await page.route(/\/api\/users\/\d+$/, (route) =>
|
|
1256
|
+
route.fulfill({ json: { id: 1, name: 'Alice' } })
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
// Regex with dynamic responses
|
|
1260
|
+
await page.route(/\/api\/users\/(\d+)/, (route) => {
|
|
1261
|
+
const match = route.request().url().match(/\/api\/users\/(\d+)/);
|
|
1262
|
+
const userId = match ? match[1] : '0';
|
|
1263
|
+
return route.fulfill({
|
|
1264
|
+
json: { id: parseInt(userId), name: `User ${userId}` },
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
await page.goto('/users');
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
test('match all requests on a domain', async ({ page }) => {
|
|
1272
|
+
await page.route('https://analytics.example.com/**', (route) =>
|
|
1273
|
+
route.abort()
|
|
1274
|
+
);
|
|
1275
|
+
|
|
1276
|
+
await page.goto('/dashboard');
|
|
1277
|
+
});
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
**Pattern reference**:
|
|
1281
|
+
|
|
1282
|
+
| Pattern | Matches | Does Not Match |
|
|
1283
|
+
|---|---|---|
|
|
1284
|
+
| `**/api/users` | `/api/users`, `/v2/api/users` | `/api/users/1` |
|
|
1285
|
+
| `**/api/users*` | `/api/users`, `/api/users?page=1` | `/api/users/1` |
|
|
1286
|
+
| `**/api/users/**` | `/api/users/1`, `/api/users/1/orders` | `/api/users` |
|
|
1287
|
+
| `**/api/users/*/orders` | `/api/users/1/orders` | `/api/users/1/2/orders` |
|
|
1288
|
+
| `**/*.{png,jpg}` | `/logo.png`, `/deep/path/img.jpg` | `/file.svg` |
|
|
1289
|
+
| `/\/api\/users\/\d+$/` (regex) | `/api/users/123` | `/api/users/abc` |
|
|
1290
|
+
|
|
1291
|
+
## Decision Guide
|
|
1292
|
+
|
|
1293
|
+
| Scenario | Use | Why |
|
|
1294
|
+
|---|---|---|
|
|
1295
|
+
| Frontend depends on an external API | `route.fulfill()` | Deterministic data, no external dependency, fast |
|
|
1296
|
+
| Need to test real API but tweak one field | `route.fetch()` + modify + `route.fulfill()` | Uses real data as baseline, only overrides what you need |
|
|
1297
|
+
| Testing happy path with real backend | `route.continue()` (or no route) | Full integration coverage |
|
|
1298
|
+
| Block analytics/ads/third-party noise | `route.abort()` | Faster tests, no flakiness from external services |
|
|
1299
|
+
| Complex API with many endpoints | `page.routeFromHAR()` with `update: true` | Record once, replay forever; minimal test code |
|
|
1300
|
+
| Testing error handling (500, timeout) | `route.fulfill({ status: 500 })` or `route.abort('timedout')` | Simulate errors deterministically |
|
|
1301
|
+
| Verify request payload sent by frontend | `page.waitForRequest()` + assertions | Confirms frontend sends correct data |
|
|
1302
|
+
| Verify response data before UI check | `page.waitForResponse()` + assertions | Confirms data arrives before asserting on DOM |
|
|
1303
|
+
| Multiple tests need the same mock | `context.route()` or fixture | Share routes across tests without repetition |
|
|
1304
|
+
| Testing loading spinners / skeleton UI | `route.fulfill()` with a delay (via `setTimeout`) | Control exact timing of response |
|
|
1305
|
+
| Paginated or search-based API | Conditional mock (check query params / body) | Dynamic responses based on request content |
|
|
1306
|
+
|
|
1307
|
+
## Anti-Patterns
|
|
1308
|
+
|
|
1309
|
+
| Don't Do This | Problem | Do This Instead |
|
|
1310
|
+
|---|---|---|
|
|
1311
|
+
| Mocking your own app's pages and static assets | You end up testing a fake app, not your real one | Only mock API/data endpoints, never HTML/JS/CSS served by your app |
|
|
1312
|
+
| Hardcoding mock data inline in every test | Duplicated data, hard to update when API changes | Extract mock data into shared fixtures (`tests/data/users.json`) |
|
|
1313
|
+
| Never updating mocks when the API changes | Tests pass against stale data; real app breaks | Use HAR recording with periodic `update: true` runs, or validate mock shapes against OpenAPI schemas |
|
|
1314
|
+
| Mocking in production-like E2E tests | You lose integration confidence | Keep a separate test suite with real backends for smoke/integration tests; mock only in component and isolated UI tests |
|
|
1315
|
+
| Forgetting to call `route.fulfill()`, `route.continue()`, or `route.abort()` | Request hangs, test times out with a confusing error | Every route handler must call exactly one of the three |
|
|
1316
|
+
| Setting up routes after `page.goto()` | Requests fire during navigation before the route is registered | Always call `page.route()` before `page.goto()` |
|
|
1317
|
+
| Using `waitForResponse` after the triggering action | Race condition: response may arrive before the wait is registered | Always set up the promise before the action: `const p = page.waitForResponse(...); await click(); await p;` |
|
|
1318
|
+
| Mocking with `route.continue()` and thinking it mocks | `route.continue()` passes the request to the real server | Use `route.fulfill()` to return fake data |
|
|
1319
|
+
| Over-broad glob patterns (`**/*`) without filtering | Catches all requests including HTML, JS, CSS; breaks the app | Be specific: `**/api/**` or filter by `resourceType()` |
|
|
1320
|
+
| Forgetting `await` on route setup or fulfillment | Route may not be active when navigation starts | Always `await page.route(...)` and `await route.fulfill(...)` |
|
|
1321
|
+
| Not calling `page.unroute()` when swapping mocks mid-test | Old route handler still fires, new one is ignored or both fire | Call `page.unroute()` before registering a new handler for the same pattern |
|
|
1322
|
+
| Using `page.on('request')` for mocking | Event listeners are read-only; they cannot modify or fulfill requests | Use `page.route()` for interception; `page.on('request')` only for logging |
|
|
1323
|
+
|
|
1324
|
+
## Troubleshooting
|
|
1325
|
+
|
|
1326
|
+
### Route handler never fires
|
|
1327
|
+
|
|
1328
|
+
**Cause**: The URL pattern does not match the actual request URL. Common when the base URL includes a port number, path prefix, or the request uses a different protocol.
|
|
1329
|
+
|
|
1330
|
+
**Fix**:
|
|
1331
|
+
- Log all requests to find the exact URL:
|
|
1332
|
+
```typescript
|
|
1333
|
+
page.on('request', (req) => console.log(req.method(), req.url()));
|
|
1334
|
+
```
|
|
1335
|
+
- Ensure glob pattern matches. `**/api/users` does not match `http://localhost:3000/api/users?page=1` -- use `**/api/users*` to include query strings.
|
|
1336
|
+
- Check that `page.route()` is called before `page.goto()`.
|
|
1337
|
+
|
|
1338
|
+
### Route handler fires but test still times out
|
|
1339
|
+
|
|
1340
|
+
**Cause**: The handler throws an error or never calls `fulfill`/`continue`/`abort`.
|
|
1341
|
+
|
|
1342
|
+
**Fix**:
|
|
1343
|
+
- Add error handling inside the route handler.
|
|
1344
|
+
- Ensure every code path in the handler ends with one of the three resolution methods.
|
|
1345
|
+
```typescript
|
|
1346
|
+
await page.route('**/api/users', async (route) => {
|
|
1347
|
+
try {
|
|
1348
|
+
const data = getTestData(); // this might throw
|
|
1349
|
+
await route.fulfill({ json: data });
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
console.error('Route handler error:', error);
|
|
1352
|
+
await route.abort();
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
### Mocked response is ignored -- app shows real data
|
|
1358
|
+
|
|
1359
|
+
**Cause**: The route is registered at the page level but the request is made by a service worker or a different browser context.
|
|
1360
|
+
|
|
1361
|
+
**Fix**:
|
|
1362
|
+
- Use `context.route()` instead of `page.route()` to cover all pages and service workers.
|
|
1363
|
+
- Disable service workers in the config if they interfere:
|
|
1364
|
+
```typescript
|
|
1365
|
+
// playwright.config.ts
|
|
1366
|
+
export default defineConfig({
|
|
1367
|
+
use: {
|
|
1368
|
+
serviceWorkers: 'block',
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
### `route.fetch()` causes infinite loop
|
|
1374
|
+
|
|
1375
|
+
**Cause**: `route.fetch()` re-issues the request, which can re-trigger the same route handler if the URL pattern matches.
|
|
1376
|
+
|
|
1377
|
+
**Fix**: Playwright handles this correctly for the same route handler -- `route.fetch()` will not re-enter the handler that called it. But if you have multiple overlapping route handlers, they can interfere. Simplify to a single handler per URL pattern, or use `route.fetch({ url: 'different-url' })` to redirect.
|
|
1378
|
+
|
|
1379
|
+
### HAR replay returns wrong responses
|
|
1380
|
+
|
|
1381
|
+
**Cause**: HAR files match requests by URL and sometimes by POST body. If the request body changes (e.g., timestamps, CSRF tokens), the match fails.
|
|
1382
|
+
|
|
1383
|
+
**Fix**:
|
|
1384
|
+
- Re-record the HAR file with `update: true`.
|
|
1385
|
+
- Use `notFound: 'fallback'` to let unmatched requests hit the real server.
|
|
1386
|
+
- For POST requests with dynamic bodies, consider using `page.route()` with manual matching instead of HAR.
|
|
1387
|
+
|
|
1388
|
+
## Related
|
|
1389
|
+
|
|
1390
|
+
- [core/when-to-mock.md](when-to-mock.md) -- decision framework for when to mock vs use real services
|
|
1391
|
+
- [core/api-testing.md](api-testing.md) -- testing REST and GraphQL APIs directly (without a browser)
|
|
1392
|
+
- [core/assertions-and-waiting.md](assertions-and-waiting.md) -- web-first assertions and `waitForResponse` patterns
|
|
1393
|
+
- [core/authentication.md](authentication.md) -- mocking auth tokens and session state
|
|
1394
|
+
- [core/error-and-edge-cases.md](error-and-edge-cases.md) -- error state testing patterns beyond network errors
|
|
1395
|
+
- [core/service-workers-and-pwa.md](service-workers-and-pwa.md) -- handling service worker caching that interferes with mocks
|