@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.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. 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