@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,1072 @@
|
|
|
1
|
+
# Fixtures and Hooks
|
|
2
|
+
|
|
3
|
+
> **When to use**: Whenever tests need shared setup, teardown, reusable resources, or configurable context. Fixtures are Playwright's killer feature — prefer them over hooks in every situation where both could work.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
| Mechanism | Scope | Cleanup guaranteed? | Parallelism-safe? | Use for |
|
|
8
|
+
|---|---|---|---|---|
|
|
9
|
+
| `test.extend()` fixture | per-test | Yes (via `use()` callback) | Yes | Most setup/teardown needs |
|
|
10
|
+
| Worker-scoped fixture | per-worker | Yes | Yes (isolated per worker) | Expensive resources: DB connections, auth state |
|
|
11
|
+
| Auto fixture | per-test or per-worker | Yes | Yes | Side effects that must always run: blocking analytics, logging |
|
|
12
|
+
| `beforeEach` / `afterEach` | per-test | No (`afterEach` skipped on crash) | Yes | Simple, one-off setup that doesn't need cleanup |
|
|
13
|
+
| `beforeAll` / `afterAll` | per-worker | No (`afterAll` skipped on crash) | Dangerous if mutating shared state | One-time worker setup with no cleanup needs |
|
|
14
|
+
|
|
15
|
+
**The rule**: If it needs cleanup, use a fixture. If it doesn't need cleanup and is simple, a hook is acceptable. When in doubt, use a fixture.
|
|
16
|
+
|
|
17
|
+
## Patterns
|
|
18
|
+
|
|
19
|
+
### 1. Custom Test Fixture
|
|
20
|
+
|
|
21
|
+
**Use when**: Tests need a resource with guaranteed setup and teardown.
|
|
22
|
+
**Avoid when**: The setup is a single line with no teardown — a `beforeEach` is fine.
|
|
23
|
+
|
|
24
|
+
Fixtures use a `use()` callback. Everything before `use()` is setup; everything after is teardown. Teardown runs even if the test crashes.
|
|
25
|
+
|
|
26
|
+
**TypeScript**
|
|
27
|
+
```typescript
|
|
28
|
+
// fixtures.ts
|
|
29
|
+
import { test as base, expect } from '@playwright/test';
|
|
30
|
+
|
|
31
|
+
type TodoFixtures = {
|
|
32
|
+
todoPage: TodoPage;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class TodoPage {
|
|
36
|
+
constructor(private page: import('@playwright/test').Page) {}
|
|
37
|
+
|
|
38
|
+
async addTodo(text: string) {
|
|
39
|
+
await this.page.getByPlaceholder('What needs to be done?').fill(text);
|
|
40
|
+
await this.page.getByPlaceholder('What needs to be done?').press('Enter');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async todos() {
|
|
44
|
+
return this.page.getByTestId('todo-item');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const test = base.extend<TodoFixtures>({
|
|
49
|
+
todoPage: async ({ page }, use) => {
|
|
50
|
+
// Setup
|
|
51
|
+
await page.goto('/todos');
|
|
52
|
+
const todoPage = new TodoPage(page);
|
|
53
|
+
|
|
54
|
+
// Hand the fixture to the test
|
|
55
|
+
await use(todoPage);
|
|
56
|
+
|
|
57
|
+
// Teardown — runs even if test fails or crashes
|
|
58
|
+
await page.evaluate(() => localStorage.clear());
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export { expect };
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// todos.spec.ts
|
|
67
|
+
import { test, expect } from './fixtures';
|
|
68
|
+
|
|
69
|
+
test('add a todo item', async ({ todoPage, page }) => {
|
|
70
|
+
await todoPage.addTodo('Buy milk');
|
|
71
|
+
await expect(await todoPage.todos()).toHaveCount(1);
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**JavaScript**
|
|
76
|
+
```javascript
|
|
77
|
+
// fixtures.js
|
|
78
|
+
const { test: base, expect } = require('@playwright/test');
|
|
79
|
+
|
|
80
|
+
class TodoPage {
|
|
81
|
+
constructor(page) {
|
|
82
|
+
this.page = page;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async addTodo(text) {
|
|
86
|
+
await this.page.getByPlaceholder('What needs to be done?').fill(text);
|
|
87
|
+
await this.page.getByPlaceholder('What needs to be done?').press('Enter');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async todos() {
|
|
91
|
+
return this.page.getByTestId('todo-item');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const test = base.extend({
|
|
96
|
+
todoPage: async ({ page }, use) => {
|
|
97
|
+
await page.goto('/todos');
|
|
98
|
+
const todoPage = new TodoPage(page);
|
|
99
|
+
await use(todoPage);
|
|
100
|
+
await page.evaluate(() => localStorage.clear());
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
module.exports = { test, expect };
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 2. Worker-Scoped Fixtures
|
|
108
|
+
|
|
109
|
+
**Use when**: A resource is expensive to create and safe to share across tests in the same worker (database connections, auth tokens, compiled assets).
|
|
110
|
+
**Avoid when**: Tests mutate the resource — each test must get its own copy.
|
|
111
|
+
|
|
112
|
+
Worker-scoped fixtures are created once per worker process, not once per test. They cannot depend on test-scoped fixtures (`page`, `context`, `request`).
|
|
113
|
+
|
|
114
|
+
**TypeScript**
|
|
115
|
+
```typescript
|
|
116
|
+
// fixtures.ts
|
|
117
|
+
import { test as base } from '@playwright/test';
|
|
118
|
+
|
|
119
|
+
type WorkerFixtures = {
|
|
120
|
+
dbConnection: DatabaseClient;
|
|
121
|
+
authToken: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const test = base.extend<{}, WorkerFixtures>({
|
|
125
|
+
dbConnection: [async ({}, use) => {
|
|
126
|
+
const db = await DatabaseClient.connect(process.env.DB_URL!);
|
|
127
|
+
await use(db);
|
|
128
|
+
await db.disconnect();
|
|
129
|
+
}, { scope: 'worker' }],
|
|
130
|
+
|
|
131
|
+
authToken: [async ({}, use) => {
|
|
132
|
+
const response = await fetch(`${process.env.API_URL}/auth/token`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
username: 'test-user',
|
|
137
|
+
password: process.env.TEST_PASSWORD,
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
const { token } = await response.json();
|
|
141
|
+
await use(token);
|
|
142
|
+
// No teardown needed — token expires on its own
|
|
143
|
+
}, { scope: 'worker' }],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export { expect } from '@playwright/test';
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**JavaScript**
|
|
150
|
+
```javascript
|
|
151
|
+
// fixtures.js
|
|
152
|
+
const { test: base, expect } = require('@playwright/test');
|
|
153
|
+
|
|
154
|
+
const test = base.extend({
|
|
155
|
+
dbConnection: [async ({}, use) => {
|
|
156
|
+
const db = await DatabaseClient.connect(process.env.DB_URL);
|
|
157
|
+
await use(db);
|
|
158
|
+
await db.disconnect();
|
|
159
|
+
}, { scope: 'worker' }],
|
|
160
|
+
|
|
161
|
+
authToken: [async ({}, use) => {
|
|
162
|
+
const response = await fetch(`${process.env.API_URL}/auth/token`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
username: 'test-user',
|
|
167
|
+
password: process.env.TEST_PASSWORD,
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
const { token } = await response.json();
|
|
171
|
+
await use(token);
|
|
172
|
+
}, { scope: 'worker' }],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
module.exports = { test, expect };
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Important**: The second type parameter in `base.extend<TestFixtures, WorkerFixtures>` is for worker-scoped fixtures. Always put worker fixtures in the second generic argument.
|
|
179
|
+
|
|
180
|
+
### 3. Auto Fixtures
|
|
181
|
+
|
|
182
|
+
**Use when**: Something must run for every test without being explicitly requested — blocking analytics, capturing console errors, injecting feature flags.
|
|
183
|
+
**Avoid when**: Tests need to opt out. Auto fixtures always run; there is no per-test escape hatch.
|
|
184
|
+
|
|
185
|
+
**TypeScript**
|
|
186
|
+
```typescript
|
|
187
|
+
// fixtures.ts
|
|
188
|
+
import { test as base, expect } from '@playwright/test';
|
|
189
|
+
|
|
190
|
+
type AutoFixtures = {
|
|
191
|
+
blockAnalytics: void;
|
|
192
|
+
consoleErrors: string[];
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const test = base.extend<AutoFixtures>({
|
|
196
|
+
// Blocks all analytics/tracking requests in every test
|
|
197
|
+
blockAnalytics: [async ({ page }, use) => {
|
|
198
|
+
await page.route(/google-analytics|segment|hotjar|mixpanel/, (route) =>
|
|
199
|
+
route.abort()
|
|
200
|
+
);
|
|
201
|
+
await use();
|
|
202
|
+
}, { auto: true }],
|
|
203
|
+
|
|
204
|
+
// Captures console errors — fail test if unexpected errors appear
|
|
205
|
+
consoleErrors: [async ({ page }, use) => {
|
|
206
|
+
const errors: string[] = [];
|
|
207
|
+
page.on('console', (msg) => {
|
|
208
|
+
if (msg.type() === 'error') {
|
|
209
|
+
errors.push(msg.text());
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
await use(errors);
|
|
213
|
+
// Teardown: assert no unexpected console errors
|
|
214
|
+
const unexpected = errors.filter(
|
|
215
|
+
(e) => !e.includes('Expected warning')
|
|
216
|
+
);
|
|
217
|
+
if (unexpected.length > 0) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Unexpected console errors:\n${unexpected.join('\n')}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}, { auto: true }],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
export { expect };
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**JavaScript**
|
|
229
|
+
```javascript
|
|
230
|
+
// fixtures.js
|
|
231
|
+
const { test: base, expect } = require('@playwright/test');
|
|
232
|
+
|
|
233
|
+
const test = base.extend({
|
|
234
|
+
blockAnalytics: [async ({ page }, use) => {
|
|
235
|
+
await page.route(/google-analytics|segment|hotjar|mixpanel/, (route) =>
|
|
236
|
+
route.abort()
|
|
237
|
+
);
|
|
238
|
+
await use();
|
|
239
|
+
}, { auto: true }],
|
|
240
|
+
|
|
241
|
+
consoleErrors: [async ({ page }, use) => {
|
|
242
|
+
const errors = [];
|
|
243
|
+
page.on('console', (msg) => {
|
|
244
|
+
if (msg.type() === 'error') {
|
|
245
|
+
errors.push(msg.text());
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
await use(errors);
|
|
249
|
+
const unexpected = errors.filter(
|
|
250
|
+
(e) => !e.includes('Expected warning')
|
|
251
|
+
);
|
|
252
|
+
if (unexpected.length > 0) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Unexpected console errors:\n${unexpected.join('\n')}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}, { auto: true }],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
module.exports = { test, expect };
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Auto fixtures can also be worker-scoped with `{ auto: true, scope: 'worker' }` for things like starting a dev server once per worker.
|
|
264
|
+
|
|
265
|
+
### 4. Fixture Composition with `mergeTests()`
|
|
266
|
+
|
|
267
|
+
**Use when**: You have multiple fixture files (auth fixtures, API fixtures, UI fixtures) and tests need several of them.
|
|
268
|
+
**Avoid when**: You only have one fixture file. Don't over-engineer.
|
|
269
|
+
|
|
270
|
+
`mergeTests()` combines multiple extended `test` objects into one. Each fixture file defines its own concerns.
|
|
271
|
+
|
|
272
|
+
**TypeScript**
|
|
273
|
+
```typescript
|
|
274
|
+
// fixtures/auth.ts
|
|
275
|
+
import { test as base } from '@playwright/test';
|
|
276
|
+
|
|
277
|
+
type AuthFixtures = {
|
|
278
|
+
authenticatedPage: import('@playwright/test').Page;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const test = base.extend<AuthFixtures>({
|
|
282
|
+
authenticatedPage: async ({ page }, use) => {
|
|
283
|
+
await page.goto('/login');
|
|
284
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
285
|
+
await page.getByLabel('Password').fill('password123');
|
|
286
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
287
|
+
await page.waitForURL('/dashboard');
|
|
288
|
+
await use(page);
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// fixtures/api.ts
|
|
295
|
+
import { test as base, APIRequestContext } from '@playwright/test';
|
|
296
|
+
|
|
297
|
+
type ApiFixtures = {
|
|
298
|
+
apiClient: APIRequestContext;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const test = base.extend<ApiFixtures>({
|
|
302
|
+
apiClient: async ({ playwright }, use) => {
|
|
303
|
+
const api = await playwright.request.newContext({
|
|
304
|
+
baseURL: 'https://api.example.com',
|
|
305
|
+
extraHTTPHeaders: { Authorization: `Bearer ${process.env.API_TOKEN}` },
|
|
306
|
+
});
|
|
307
|
+
await use(api);
|
|
308
|
+
await api.dispose();
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// fixtures/index.ts
|
|
315
|
+
import { mergeTests } from '@playwright/test';
|
|
316
|
+
import { test as authTest } from './auth';
|
|
317
|
+
import { test as apiTest } from './api';
|
|
318
|
+
|
|
319
|
+
export const test = mergeTests(authTest, apiTest);
|
|
320
|
+
export { expect } from '@playwright/test';
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// dashboard.spec.ts
|
|
325
|
+
import { test, expect } from './fixtures';
|
|
326
|
+
|
|
327
|
+
test('dashboard loads user data', async ({ authenticatedPage, apiClient }) => {
|
|
328
|
+
// Both fixtures are available
|
|
329
|
+
const data = await apiClient.get('/users/me');
|
|
330
|
+
await expect(authenticatedPage.getByRole('heading')).toContainText('Dashboard');
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**JavaScript**
|
|
335
|
+
```javascript
|
|
336
|
+
// fixtures/auth.js
|
|
337
|
+
const { test: base } = require('@playwright/test');
|
|
338
|
+
|
|
339
|
+
const test = base.extend({
|
|
340
|
+
authenticatedPage: async ({ page }, use) => {
|
|
341
|
+
await page.goto('/login');
|
|
342
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
343
|
+
await page.getByLabel('Password').fill('password123');
|
|
344
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
345
|
+
await page.waitForURL('/dashboard');
|
|
346
|
+
await use(page);
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
module.exports = { test };
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
// fixtures/api.js
|
|
355
|
+
const { test: base } = require('@playwright/test');
|
|
356
|
+
|
|
357
|
+
const test = base.extend({
|
|
358
|
+
apiClient: async ({ playwright }, use) => {
|
|
359
|
+
const api = await playwright.request.newContext({
|
|
360
|
+
baseURL: 'https://api.example.com',
|
|
361
|
+
extraHTTPHeaders: { Authorization: `Bearer ${process.env.API_TOKEN}` },
|
|
362
|
+
});
|
|
363
|
+
await use(api);
|
|
364
|
+
await api.dispose();
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
module.exports = { test };
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
// fixtures/index.js
|
|
373
|
+
const { mergeTests } = require('@playwright/test');
|
|
374
|
+
const { test: authTest } = require('./auth');
|
|
375
|
+
const { test: apiTest } = require('./api');
|
|
376
|
+
|
|
377
|
+
const test = mergeTests(authTest, apiTest);
|
|
378
|
+
module.exports = { test, expect: require('@playwright/test').expect };
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 5. Parameterized Fixtures (Option Fixtures)
|
|
382
|
+
|
|
383
|
+
**Use when**: A fixture's behavior should be configurable per-project or per-describe block (locale, viewport, user role, feature flags).
|
|
384
|
+
**Avoid when**: The value never changes — just hardcode it in the fixture.
|
|
385
|
+
|
|
386
|
+
Option fixtures are declared with `{ option: true }` and can be overridden in `playwright.config` under `use` or with `test.use()` in test files.
|
|
387
|
+
|
|
388
|
+
**TypeScript**
|
|
389
|
+
```typescript
|
|
390
|
+
// fixtures.ts
|
|
391
|
+
import { test as base, expect } from '@playwright/test';
|
|
392
|
+
|
|
393
|
+
type OptionFixtures = {
|
|
394
|
+
userRole: 'admin' | 'editor' | 'viewer';
|
|
395
|
+
locale: string;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
type DerivedFixtures = {
|
|
399
|
+
authenticatedPage: import('@playwright/test').Page;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export const test = base.extend<OptionFixtures & DerivedFixtures>({
|
|
403
|
+
// Option fixtures — configurable per project or describe block
|
|
404
|
+
userRole: ['viewer', { option: true }],
|
|
405
|
+
locale: ['en-US', { option: true }],
|
|
406
|
+
|
|
407
|
+
// Derived fixture — uses the option values
|
|
408
|
+
authenticatedPage: async ({ page, userRole, locale }, use) => {
|
|
409
|
+
await page.goto(`/login?locale=${locale}`);
|
|
410
|
+
const credentials = {
|
|
411
|
+
admin: { email: 'admin@test.com', password: 'admin-pass' },
|
|
412
|
+
editor: { email: 'editor@test.com', password: 'editor-pass' },
|
|
413
|
+
viewer: { email: 'viewer@test.com', password: 'viewer-pass' },
|
|
414
|
+
};
|
|
415
|
+
const { email, password } = credentials[userRole];
|
|
416
|
+
await page.getByLabel('Email').fill(email);
|
|
417
|
+
await page.getByLabel('Password').fill(password);
|
|
418
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
419
|
+
await use(page);
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
export { expect };
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
// playwright.config.ts (override per project)
|
|
428
|
+
import { defineConfig } from '@playwright/test';
|
|
429
|
+
|
|
430
|
+
export default defineConfig({
|
|
431
|
+
projects: [
|
|
432
|
+
{
|
|
433
|
+
name: 'admin-tests',
|
|
434
|
+
testDir: './tests/admin',
|
|
435
|
+
use: { userRole: 'admin', locale: 'en-US' },
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'viewer-tests',
|
|
439
|
+
testDir: './tests/viewer',
|
|
440
|
+
use: { userRole: 'viewer', locale: 'fr-FR' },
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// admin-settings.spec.ts (override per describe block)
|
|
448
|
+
import { test, expect } from './fixtures';
|
|
449
|
+
|
|
450
|
+
test.describe('admin settings', () => {
|
|
451
|
+
test.use({ userRole: 'admin' });
|
|
452
|
+
|
|
453
|
+
test('can access settings page', async ({ authenticatedPage }) => {
|
|
454
|
+
await authenticatedPage.goto('/settings');
|
|
455
|
+
await expect(authenticatedPage.getByRole('heading')).toHaveText('Admin Settings');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**JavaScript**
|
|
461
|
+
```javascript
|
|
462
|
+
// fixtures.js
|
|
463
|
+
const { test: base, expect } = require('@playwright/test');
|
|
464
|
+
|
|
465
|
+
const test = base.extend({
|
|
466
|
+
userRole: ['viewer', { option: true }],
|
|
467
|
+
locale: ['en-US', { option: true }],
|
|
468
|
+
|
|
469
|
+
authenticatedPage: async ({ page, userRole, locale }, use) => {
|
|
470
|
+
await page.goto(`/login?locale=${locale}`);
|
|
471
|
+
const credentials = {
|
|
472
|
+
admin: { email: 'admin@test.com', password: 'admin-pass' },
|
|
473
|
+
editor: { email: 'editor@test.com', password: 'editor-pass' },
|
|
474
|
+
viewer: { email: 'viewer@test.com', password: 'viewer-pass' },
|
|
475
|
+
};
|
|
476
|
+
const { email, password } = credentials[userRole];
|
|
477
|
+
await page.getByLabel('Email').fill(email);
|
|
478
|
+
await page.getByLabel('Password').fill(password);
|
|
479
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
480
|
+
await use(page);
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
module.exports = { test, expect };
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### 6. Fixture Dependencies
|
|
488
|
+
|
|
489
|
+
**Use when**: One fixture needs the output of another fixture. Playwright automatically resolves the dependency graph.
|
|
490
|
+
**Avoid when**: You are tempted to nest fixtures more than 3 levels deep — flatten the design instead.
|
|
491
|
+
|
|
492
|
+
Request fixtures by name in the destructured first argument. Playwright handles ordering and lifecycle.
|
|
493
|
+
|
|
494
|
+
**TypeScript**
|
|
495
|
+
```typescript
|
|
496
|
+
// fixtures.ts
|
|
497
|
+
import { test as base, expect } from '@playwright/test';
|
|
498
|
+
|
|
499
|
+
type Fixtures = {
|
|
500
|
+
apiContext: import('@playwright/test').APIRequestContext;
|
|
501
|
+
testUser: { id: string; email: string };
|
|
502
|
+
userPage: import('@playwright/test').Page;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export const test = base.extend<Fixtures>({
|
|
506
|
+
apiContext: async ({ playwright }, use) => {
|
|
507
|
+
const ctx = await playwright.request.newContext({
|
|
508
|
+
baseURL: process.env.API_URL,
|
|
509
|
+
});
|
|
510
|
+
await use(ctx);
|
|
511
|
+
await ctx.dispose();
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
// Depends on apiContext — Playwright creates apiContext first
|
|
515
|
+
testUser: async ({ apiContext }, use) => {
|
|
516
|
+
const response = await apiContext.post('/test/users', {
|
|
517
|
+
data: { email: `user-${Date.now()}@test.com`, role: 'editor' },
|
|
518
|
+
});
|
|
519
|
+
const user = await response.json();
|
|
520
|
+
await use(user);
|
|
521
|
+
// Teardown: delete the test user
|
|
522
|
+
await apiContext.delete(`/test/users/${user.id}`);
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
// Depends on page AND testUser — both are ready before this runs
|
|
526
|
+
userPage: async ({ page, testUser }, use) => {
|
|
527
|
+
await page.goto('/login');
|
|
528
|
+
await page.getByLabel('Email').fill(testUser.email);
|
|
529
|
+
await page.getByLabel('Password').fill('default-test-password');
|
|
530
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
531
|
+
await page.waitForURL('/dashboard');
|
|
532
|
+
await use(page);
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
export { expect };
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**JavaScript**
|
|
540
|
+
```javascript
|
|
541
|
+
// fixtures.js
|
|
542
|
+
const { test: base, expect } = require('@playwright/test');
|
|
543
|
+
|
|
544
|
+
const test = base.extend({
|
|
545
|
+
apiContext: async ({ playwright }, use) => {
|
|
546
|
+
const ctx = await playwright.request.newContext({
|
|
547
|
+
baseURL: process.env.API_URL,
|
|
548
|
+
});
|
|
549
|
+
await use(ctx);
|
|
550
|
+
await ctx.dispose();
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
testUser: async ({ apiContext }, use) => {
|
|
554
|
+
const response = await apiContext.post('/test/users', {
|
|
555
|
+
data: { email: `user-${Date.now()}@test.com`, role: 'editor' },
|
|
556
|
+
});
|
|
557
|
+
const user = await response.json();
|
|
558
|
+
await use(user);
|
|
559
|
+
await apiContext.delete(`/test/users/${user.id}`);
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
userPage: async ({ page, testUser }, use) => {
|
|
563
|
+
await page.goto('/login');
|
|
564
|
+
await page.getByLabel('Email').fill(testUser.email);
|
|
565
|
+
await page.getByLabel('Password').fill('default-test-password');
|
|
566
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
567
|
+
await page.waitForURL('/dashboard');
|
|
568
|
+
await use(page);
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
module.exports = { test, expect };
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Teardown runs in reverse dependency order: `userPage` tears down first, then `testUser` (which deletes the user), then `apiContext`.
|
|
576
|
+
|
|
577
|
+
### 7. Overriding Built-in Fixtures
|
|
578
|
+
|
|
579
|
+
**Use when**: Every test needs the same modification to `page`, `context`, or `browser` — custom headers, viewport, locale, route blocking.
|
|
580
|
+
**Avoid when**: Only some tests need the override. Use a new named fixture instead to keep the default `page` available.
|
|
581
|
+
|
|
582
|
+
**TypeScript**
|
|
583
|
+
```typescript
|
|
584
|
+
// fixtures.ts
|
|
585
|
+
import { test as base, expect } from '@playwright/test';
|
|
586
|
+
|
|
587
|
+
export const test = base.extend({
|
|
588
|
+
// Override the built-in context fixture to add custom headers
|
|
589
|
+
context: async ({ browser }, use) => {
|
|
590
|
+
const context = await browser.newContext({
|
|
591
|
+
extraHTTPHeaders: {
|
|
592
|
+
'X-Test-ID': `test-${Date.now()}`,
|
|
593
|
+
'Accept-Language': 'en-US',
|
|
594
|
+
},
|
|
595
|
+
permissions: ['geolocation'],
|
|
596
|
+
geolocation: { latitude: 37.7749, longitude: -122.4194 },
|
|
597
|
+
});
|
|
598
|
+
await use(context);
|
|
599
|
+
await context.close();
|
|
600
|
+
},
|
|
601
|
+
|
|
602
|
+
// Override the built-in page fixture to block third-party scripts
|
|
603
|
+
page: async ({ context }, use) => {
|
|
604
|
+
const page = await context.newPage();
|
|
605
|
+
await page.route('**/*.{png,jpg,jpeg,gif,svg}', (route) => route.abort());
|
|
606
|
+
await use(page);
|
|
607
|
+
// No need to close page — context.close() handles it
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
export { expect };
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**JavaScript**
|
|
615
|
+
```javascript
|
|
616
|
+
// fixtures.js
|
|
617
|
+
const { test: base, expect } = require('@playwright/test');
|
|
618
|
+
|
|
619
|
+
const test = base.extend({
|
|
620
|
+
context: async ({ browser }, use) => {
|
|
621
|
+
const context = await browser.newContext({
|
|
622
|
+
extraHTTPHeaders: {
|
|
623
|
+
'X-Test-ID': `test-${Date.now()}`,
|
|
624
|
+
'Accept-Language': 'en-US',
|
|
625
|
+
},
|
|
626
|
+
permissions: ['geolocation'],
|
|
627
|
+
geolocation: { latitude: 37.7749, longitude: -122.4194 },
|
|
628
|
+
});
|
|
629
|
+
await use(context);
|
|
630
|
+
await context.close();
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
page: async ({ context }, use) => {
|
|
634
|
+
const page = await context.newPage();
|
|
635
|
+
await page.route('**/*.{png,jpg,jpeg,gif,svg}', (route) => route.abort());
|
|
636
|
+
await use(page);
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
module.exports = { test, expect };
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 8. beforeEach / afterEach — When Hooks Are Acceptable
|
|
644
|
+
|
|
645
|
+
**Use when**: Simple, stateless setup with no teardown needed. Navigation to a starting URL is the canonical example.
|
|
646
|
+
**Avoid when**: The setup creates something that must be cleaned up. Use a fixture instead.
|
|
647
|
+
|
|
648
|
+
**TypeScript**
|
|
649
|
+
```typescript
|
|
650
|
+
// acceptable-hooks.spec.ts
|
|
651
|
+
import { test, expect } from '@playwright/test';
|
|
652
|
+
|
|
653
|
+
// Acceptable: simple navigation, no teardown needed
|
|
654
|
+
test.beforeEach(async ({ page }) => {
|
|
655
|
+
await page.goto('/dashboard');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('shows welcome message', async ({ page }) => {
|
|
659
|
+
await expect(page.getByRole('heading')).toHaveText('Welcome');
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test('shows navigation sidebar', async ({ page }) => {
|
|
663
|
+
await expect(page.getByRole('navigation')).toBeVisible();
|
|
664
|
+
});
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**JavaScript**
|
|
668
|
+
```javascript
|
|
669
|
+
// acceptable-hooks.spec.js
|
|
670
|
+
const { test, expect } = require('@playwright/test');
|
|
671
|
+
|
|
672
|
+
test.beforeEach(async ({ page }) => {
|
|
673
|
+
await page.goto('/dashboard');
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('shows welcome message', async ({ page }) => {
|
|
677
|
+
await expect(page.getByRole('heading')).toHaveText('Welcome');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test('shows navigation sidebar', async ({ page }) => {
|
|
681
|
+
await expect(page.getByRole('navigation')).toBeVisible();
|
|
682
|
+
});
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Hooks can use fixtures in their argument list — `beforeEach(async ({ page, context })` works. But hooks cannot *define* fixtures.
|
|
686
|
+
|
|
687
|
+
### 9. beforeAll / afterAll — Worker-Level Hooks
|
|
688
|
+
|
|
689
|
+
**Use when**: One-time setup that all tests in a file share and that has no cleanup needs, such as logging a diagnostic or checking a precondition.
|
|
690
|
+
**Avoid when**: You are creating resources that need cleanup (use worker-scoped fixtures) or storing mutable state (this breaks parallelism).
|
|
691
|
+
|
|
692
|
+
**TypeScript**
|
|
693
|
+
```typescript
|
|
694
|
+
// health-check.spec.ts
|
|
695
|
+
import { test, expect } from '@playwright/test';
|
|
696
|
+
|
|
697
|
+
// Good: read-only check, no mutable state
|
|
698
|
+
test.beforeAll(async ({ request }) => {
|
|
699
|
+
const response = await request.get('/api/health');
|
|
700
|
+
expect(response.ok()).toBeTruthy();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test('homepage loads', async ({ page }) => {
|
|
704
|
+
await page.goto('/');
|
|
705
|
+
await expect(page).toHaveTitle(/My App/);
|
|
706
|
+
});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**JavaScript**
|
|
710
|
+
```javascript
|
|
711
|
+
// health-check.spec.js
|
|
712
|
+
const { test, expect } = require('@playwright/test');
|
|
713
|
+
|
|
714
|
+
test.beforeAll(async ({ request }) => {
|
|
715
|
+
const response = await request.get('/api/health');
|
|
716
|
+
expect(response.ok()).toBeTruthy();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test('homepage loads', async ({ page }) => {
|
|
720
|
+
await page.goto('/');
|
|
721
|
+
await expect(page).toHaveTitle(/My App/);
|
|
722
|
+
});
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`beforeAll` / `afterAll` receive only worker-scoped fixtures: `request`, `browser`, and any custom worker fixtures. They do **not** receive `page` or `context` (those are per-test).
|
|
726
|
+
|
|
727
|
+
### 10. Typed Fixtures in TypeScript
|
|
728
|
+
|
|
729
|
+
Always define an interface for your fixtures. This gives you autocomplete, catches typos at compile time, and documents the fixture contract.
|
|
730
|
+
|
|
731
|
+
**TypeScript**
|
|
732
|
+
```typescript
|
|
733
|
+
// fixtures.ts
|
|
734
|
+
import { test as base, expect, Page, APIRequestContext } from '@playwright/test';
|
|
735
|
+
|
|
736
|
+
// Define fixture types explicitly
|
|
737
|
+
interface TestFixtures {
|
|
738
|
+
adminPage: Page;
|
|
739
|
+
editorPage: Page;
|
|
740
|
+
apiClient: APIRequestContext;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
interface WorkerFixtures {
|
|
744
|
+
sharedToken: string;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export const test = base.extend<TestFixtures, WorkerFixtures>({
|
|
748
|
+
sharedToken: [async ({}, use) => {
|
|
749
|
+
const res = await fetch(`${process.env.API_URL}/auth/service-token`, {
|
|
750
|
+
method: 'POST',
|
|
751
|
+
headers: { 'Content-Type': 'application/json' },
|
|
752
|
+
body: JSON.stringify({ secret: process.env.SERVICE_SECRET }),
|
|
753
|
+
});
|
|
754
|
+
const { token } = await res.json();
|
|
755
|
+
await use(token);
|
|
756
|
+
}, { scope: 'worker' }],
|
|
757
|
+
|
|
758
|
+
apiClient: async ({ playwright, sharedToken }, use) => {
|
|
759
|
+
const ctx = await playwright.request.newContext({
|
|
760
|
+
baseURL: process.env.API_URL,
|
|
761
|
+
extraHTTPHeaders: { Authorization: `Bearer ${sharedToken}` },
|
|
762
|
+
});
|
|
763
|
+
await use(ctx);
|
|
764
|
+
await ctx.dispose();
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
adminPage: async ({ browser }, use) => {
|
|
768
|
+
const ctx = await browser.newContext({ storageState: 'auth/admin.json' });
|
|
769
|
+
const page = await ctx.newPage();
|
|
770
|
+
await use(page);
|
|
771
|
+
await ctx.close();
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
editorPage: async ({ browser }, use) => {
|
|
775
|
+
const ctx = await browser.newContext({ storageState: 'auth/editor.json' });
|
|
776
|
+
const page = await ctx.newPage();
|
|
777
|
+
await use(page);
|
|
778
|
+
await ctx.close();
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
export { expect };
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### 11. Aborting a Test From a Fixture or Route (`test.abort()`, Playwright 1.60+)
|
|
786
|
+
|
|
787
|
+
**Use when**: A fixture, hook, or route handler detects an unrecoverable precondition — a required backend is down, a seed step failed, or a route received an unexpected request — and continuing would only produce a confusing downstream failure.
|
|
788
|
+
**Avoid when**: The test body itself can assert the condition. Use a normal `expect()` there. `test.abort()` is for code that runs *outside* the test body where you can't `expect`.
|
|
789
|
+
|
|
790
|
+
Playwright 1.60 adds `test.abort(message?)`, which immediately stops the currently running test. Unlike `test.skip()`, it marks the test as failed/aborted with your message, and unlike throwing it works cleanly from fixtures and route handlers.
|
|
791
|
+
|
|
792
|
+
**TypeScript**
|
|
793
|
+
```typescript
|
|
794
|
+
import { test as base, expect } from '@playwright/test';
|
|
795
|
+
|
|
796
|
+
export const test = base.extend({
|
|
797
|
+
// Abort early if the environment isn't ready, instead of letting every
|
|
798
|
+
// assertion fail with a cryptic message.
|
|
799
|
+
seededDb: async ({}, use) => {
|
|
800
|
+
const res = await fetch(`${process.env.API_URL}/health/db`);
|
|
801
|
+
if (!res.ok) {
|
|
802
|
+
base.abort(`Database not reachable (${res.status}) — aborting test`);
|
|
803
|
+
}
|
|
804
|
+
await use(true);
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test('checkout works on a healthy backend', async ({ page, seededDb }) => {
|
|
809
|
+
await page.goto('/checkout');
|
|
810
|
+
await expect(page.getByRole('heading', { name: 'Checkout' })).toBeVisible();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test('abort from a route handler on an unexpected request', async ({ page }) => {
|
|
814
|
+
await page.route('**/api/**', (route) => {
|
|
815
|
+
if (route.request().url().includes('/api/forbidden')) {
|
|
816
|
+
test.abort('Unexpected call to forbidden endpoint');
|
|
817
|
+
}
|
|
818
|
+
return route.continue();
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
await page.goto('/dashboard');
|
|
822
|
+
});
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
**JavaScript**
|
|
826
|
+
```javascript
|
|
827
|
+
const { test, expect } = require('@playwright/test');
|
|
828
|
+
|
|
829
|
+
test('abort from a route handler on an unexpected request', async ({ page }) => {
|
|
830
|
+
await page.route('**/api/**', (route) => {
|
|
831
|
+
if (route.request().url().includes('/api/forbidden')) {
|
|
832
|
+
test.abort('Unexpected call to forbidden endpoint');
|
|
833
|
+
}
|
|
834
|
+
return route.continue();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
await page.goto('/dashboard');
|
|
838
|
+
});
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
> `test.abort()` ends the test now — code after it in the same handler won't run. Use it for *unrecoverable* conditions; for "this test doesn't apply here", prefer `test.skip()`.
|
|
842
|
+
|
|
843
|
+
## Decision Guide
|
|
844
|
+
|
|
845
|
+
```
|
|
846
|
+
I need to set up something before tests run.
|
|
847
|
+
│
|
|
848
|
+
├── Does it create a resource that MUST be cleaned up?
|
|
849
|
+
│ │
|
|
850
|
+
│ ├── YES → Use a fixture (setup before `use()`, teardown after)
|
|
851
|
+
│ │ │
|
|
852
|
+
│ │ ├── Is the resource expensive and safe to share across tests?
|
|
853
|
+
│ │ │ ├── YES → Worker-scoped fixture: { scope: 'worker' }
|
|
854
|
+
│ │ │ └── NO → Test-scoped fixture (default)
|
|
855
|
+
│ │ │
|
|
856
|
+
│ │ ├── Should every test get this automatically?
|
|
857
|
+
│ │ │ ├── YES → Auto fixture: { auto: true }
|
|
858
|
+
│ │ │ └── NO → Regular fixture (test declares it in args)
|
|
859
|
+
│ │ │
|
|
860
|
+
│ │ └── Should the value be configurable per project?
|
|
861
|
+
│ │ ├── YES → Option fixture: { option: true }
|
|
862
|
+
│ │ └── NO → Regular fixture with hardcoded setup
|
|
863
|
+
│ │
|
|
864
|
+
│ └── NO → Hook is acceptable
|
|
865
|
+
│ │
|
|
866
|
+
│ ├── Per-test setup?
|
|
867
|
+
│ │ └── beforeEach
|
|
868
|
+
│ │
|
|
869
|
+
│ └── One-time per worker?
|
|
870
|
+
│ └── beforeAll (but only for read-only / diagnostic checks)
|
|
871
|
+
│
|
|
872
|
+
└── Am I combining fixtures from multiple domains?
|
|
873
|
+
└── YES → mergeTests() from separate fixture files
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
## Anti-Patterns
|
|
877
|
+
|
|
878
|
+
### 1. Global Mutable State in `beforeAll`
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
// BAD: Mutable state shared across parallel tests
|
|
882
|
+
let testUser: { id: string; email: string };
|
|
883
|
+
|
|
884
|
+
test.beforeAll(async ({ request }) => {
|
|
885
|
+
const res = await request.post('/api/users', { data: { email: 'shared@test.com' } });
|
|
886
|
+
testUser = await res.json(); // Shared mutable state!
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test.afterAll(async ({ request }) => {
|
|
890
|
+
await request.delete(`/api/users/${testUser.id}`); // Cleanup may not run
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
test('test 1', async ({ page }) => {
|
|
894
|
+
// Uses testUser — but what if another worker also has a testUser?
|
|
895
|
+
});
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
```typescript
|
|
899
|
+
// GOOD: Worker-scoped fixture — isolated per worker, guaranteed cleanup
|
|
900
|
+
import { test as base } from '@playwright/test';
|
|
901
|
+
|
|
902
|
+
export const test = base.extend<{ testUser: { id: string; email: string } }>({
|
|
903
|
+
testUser: async ({ request }, use) => {
|
|
904
|
+
const res = await request.post('/api/users', {
|
|
905
|
+
data: { email: `user-${Date.now()}@test.com` },
|
|
906
|
+
});
|
|
907
|
+
const user = await res.json();
|
|
908
|
+
await use(user);
|
|
909
|
+
await request.delete(`/api/users/${user.id}`);
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### 2. Cleanup in `afterEach` Instead of Fixture Teardown
|
|
915
|
+
|
|
916
|
+
```typescript
|
|
917
|
+
// BAD: afterEach is not guaranteed to run on crash
|
|
918
|
+
test.beforeEach(async ({ page }) => {
|
|
919
|
+
await page.evaluate(() => localStorage.setItem('debug', 'true'));
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test.afterEach(async ({ page }) => {
|
|
923
|
+
// This might NOT run if the test crashes or times out
|
|
924
|
+
await page.evaluate(() => localStorage.clear());
|
|
925
|
+
});
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
```typescript
|
|
929
|
+
// GOOD: Fixture teardown always runs
|
|
930
|
+
export const test = base.extend<{ debugMode: void }>({
|
|
931
|
+
debugMode: async ({ page }, use) => {
|
|
932
|
+
await page.evaluate(() => localStorage.setItem('debug', 'true'));
|
|
933
|
+
await use();
|
|
934
|
+
await page.evaluate(() => localStorage.clear()); // Guaranteed
|
|
935
|
+
},
|
|
936
|
+
});
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### 3. Fixtures That Do Too Many Things
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
// BAD: One fixture doing setup for unrelated concerns
|
|
943
|
+
export const test = base.extend({
|
|
944
|
+
everything: async ({ page }, use) => {
|
|
945
|
+
// Logs in
|
|
946
|
+
await page.goto('/login');
|
|
947
|
+
await page.getByLabel('Email').fill('user@test.com');
|
|
948
|
+
await page.getByLabel('Password').fill('password');
|
|
949
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
950
|
+
// Creates test data
|
|
951
|
+
await page.request.post('/api/products', { data: { name: 'Widget' } });
|
|
952
|
+
// Blocks analytics
|
|
953
|
+
await page.route(/analytics/, (r) => r.abort());
|
|
954
|
+
// Sets locale
|
|
955
|
+
await page.evaluate(() => localStorage.setItem('locale', 'en'));
|
|
956
|
+
await use(page);
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
// GOOD: Separate fixtures, each with one responsibility
|
|
963
|
+
export const test = base.extend({
|
|
964
|
+
authenticatedPage: async ({ page }, use) => {
|
|
965
|
+
await page.goto('/login');
|
|
966
|
+
await page.getByLabel('Email').fill('user@test.com');
|
|
967
|
+
await page.getByLabel('Password').fill('password');
|
|
968
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
969
|
+
await use(page);
|
|
970
|
+
},
|
|
971
|
+
|
|
972
|
+
testProduct: async ({ request }, use) => {
|
|
973
|
+
const res = await request.post('/api/products', { data: { name: 'Widget' } });
|
|
974
|
+
const product = await res.json();
|
|
975
|
+
await use(product);
|
|
976
|
+
await request.delete(`/api/products/${product.id}`);
|
|
977
|
+
},
|
|
978
|
+
|
|
979
|
+
blockAnalytics: [async ({ page }, use) => {
|
|
980
|
+
await page.route(/analytics/, (r) => r.abort());
|
|
981
|
+
await use();
|
|
982
|
+
}, { auto: true }],
|
|
983
|
+
});
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
### 4. Over-Abstracting Fixtures
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
// BAD: Fixture factory with layers of indirection nobody can follow
|
|
990
|
+
const createFixture = (role, permissions, options) =>
|
|
991
|
+
base.extend({
|
|
992
|
+
[`${role}Page`]: async ({ page }, use) => {
|
|
993
|
+
await setupRole(page, role, permissions, options);
|
|
994
|
+
await use(page);
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const adminTest = createFixture('admin', ['read', 'write', 'delete'], { mfa: true });
|
|
999
|
+
const editorTest = createFixture('editor', ['read', 'write'], { mfa: false });
|
|
1000
|
+
// Good luck debugging which fixture ran
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
// GOOD: Explicit fixtures — boring but readable
|
|
1005
|
+
export const test = base.extend({
|
|
1006
|
+
adminPage: async ({ browser }, use) => {
|
|
1007
|
+
const ctx = await browser.newContext({ storageState: 'auth/admin.json' });
|
|
1008
|
+
const page = await ctx.newPage();
|
|
1009
|
+
await use(page);
|
|
1010
|
+
await ctx.close();
|
|
1011
|
+
},
|
|
1012
|
+
|
|
1013
|
+
editorPage: async ({ browser }, use) => {
|
|
1014
|
+
const ctx = await browser.newContext({ storageState: 'auth/editor.json' });
|
|
1015
|
+
const page = await ctx.newPage();
|
|
1016
|
+
await use(page);
|
|
1017
|
+
await ctx.close();
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
### 5. Not Typing Fixtures in TypeScript
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
// BAD: No type parameter — no autocomplete, no compile-time errors
|
|
1026
|
+
export const test = base.extend({
|
|
1027
|
+
myFixture: async ({ page }, use) => {
|
|
1028
|
+
await use({ count: 42 });
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
// test('example', async ({ myFixture }) => {
|
|
1032
|
+
// myFixture.cont // No autocomplete, no type safety
|
|
1033
|
+
// });
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
```typescript
|
|
1037
|
+
// GOOD: Explicit type interface
|
|
1038
|
+
interface MyFixtures {
|
|
1039
|
+
myFixture: { count: number };
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
export const test = base.extend<MyFixtures>({
|
|
1043
|
+
myFixture: async ({ page }, use) => {
|
|
1044
|
+
await use({ count: 42 });
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
// test('example', async ({ myFixture }) => {
|
|
1048
|
+
// myFixture.count // Autocomplete works, typos caught at compile time
|
|
1049
|
+
// });
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
## Troubleshooting
|
|
1053
|
+
|
|
1054
|
+
| Symptom | Cause | Fix |
|
|
1055
|
+
|---|---|---|
|
|
1056
|
+
| `Cannot use a test-scoped fixture in a worker-scoped fixture` | Worker fixture depends on `page`, `context`, or test-scoped custom fixture | Worker fixtures can only depend on other worker fixtures or built-in worker fixtures (`browser`, `playwright`) |
|
|
1057
|
+
| `Fixture "X" has already been registered` | Two fixture files define the same fixture name and are both extended | Use `mergeTests()` instead of chaining `.extend()` calls, or rename one fixture |
|
|
1058
|
+
| Fixture teardown not running | You used `afterEach` instead of the `use()` callback pattern | Move cleanup code after the `await use()` call inside the fixture |
|
|
1059
|
+
| `beforeAll` can't access `page` | `page` is test-scoped; `beforeAll` only gets worker-scoped fixtures | Use a worker-scoped fixture, or move logic to `beforeEach` |
|
|
1060
|
+
| Test hangs inside fixture | `await use()` was never called — Playwright waits indefinitely for the fixture to yield | Ensure every fixture code path calls `await use(value)` exactly once |
|
|
1061
|
+
| Fixture runs but test doesn't see the value | Fixture not declared in the test's destructured arguments | Add the fixture name to the test function signature: `test('name', async ({ myFixture }) => { ... })` |
|
|
1062
|
+
| Option fixture value ignored | `test.use()` called inside `test()` instead of `test.describe()` | `test.use()` must be at the top level of a `describe` block or file, not inside a test |
|
|
1063
|
+
| Auto fixture not running | Fixture file not imported — the custom `test` is not used | Import and use the `test` from your fixture file, not from `@playwright/test` |
|
|
1064
|
+
|
|
1065
|
+
## Related
|
|
1066
|
+
|
|
1067
|
+
- [pom/page-object-model.md](../pom/page-object-model.md) — Page objects are typically consumed via fixtures
|
|
1068
|
+
- [core/test-organization.md](test-organization.md) — Where to put fixture files in the project tree
|
|
1069
|
+
- [core/configuration.md](configuration.md) — Option fixtures are configured in `playwright.config`
|
|
1070
|
+
- [core/authentication.md](authentication.md) — Auth state is the most common worker-scoped fixture
|
|
1071
|
+
- [ci/global-setup-teardown.md](../ci/global-setup-teardown.md) — For truly global (all-workers) setup, not per-worker
|
|
1072
|
+
- [pom/pom-vs-fixtures-vs-helpers.md](../pom/pom-vs-fixtures-vs-helpers.md) — When to use each abstraction
|