@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,1209 @@
1
+ # Test Data Management
2
+
3
+ > **When to use**: Every test that interacts with data — user accounts, form inputs, entities, or any state that must exist before assertions run.
4
+
5
+ ## Quick Reference
6
+
7
+ | Strategy | Speed | Isolation | Complexity | Best For |
8
+ |---|---|---|---|---|
9
+ | Inline data | Instant | Perfect | None | Simple value checks, form inputs |
10
+ | Factory functions | Instant | Perfect | Low | Unique identifiers, consistent shapes |
11
+ | Faker/random data | Instant | Perfect | Low | Realistic fields, edge-case discovery |
12
+ | Builder pattern | Instant | Perfect | Medium | Complex objects with many optional fields |
13
+ | API seeding | Fast | Perfect | Medium | Creating entities the app depends on |
14
+ | Database seeding | Fast | Good | High | Complex relational data, bulk setup |
15
+ | Storage state | Fast | Good | Low | Reusing authenticated sessions |
16
+ | Fixture-based setup | Fast | Perfect | Medium | Encapsulating setup + guaranteed teardown |
17
+
18
+ **Core principle**: Every test creates its own data and cleans up after itself. No test should depend on data left behind by another test or pre-existing in the environment.
19
+
20
+ ## Patterns
21
+
22
+ ### Inline Test Data
23
+
24
+ **Use when**: The data is simple, unique to one test, and essential for understanding the assertion.
25
+
26
+ **Avoid when**: The same data shape repeats across many tests — extract a factory instead.
27
+
28
+ Inline data makes tests self-documenting. The reader sees exactly what matters without chasing imports.
29
+
30
+ **TypeScript**
31
+ ```typescript
32
+ // tests/contact-form.spec.ts
33
+ import { test, expect } from '@playwright/test';
34
+
35
+ test('submits contact form with valid data', async ({ page }) => {
36
+ const name = 'Ada Lovelace';
37
+ const email = `ada-${Date.now()}@example.com`;
38
+ const message = 'Inquiry about analytics engine';
39
+
40
+ await page.goto('/contact');
41
+ await page.getByLabel('Name').fill(name);
42
+ await page.getByLabel('Email').fill(email);
43
+ await page.getByLabel('Message').fill(message);
44
+ await page.getByRole('button', { name: 'Send' }).click();
45
+
46
+ await expect(page.getByText('Thank you, Ada Lovelace')).toBeVisible();
47
+ });
48
+ ```
49
+
50
+ **JavaScript**
51
+ ```javascript
52
+ // tests/contact-form.spec.js
53
+ const { test, expect } = require('@playwright/test');
54
+
55
+ test('submits contact form with valid data', async ({ page }) => {
56
+ const name = 'Ada Lovelace';
57
+ const email = `ada-${Date.now()}@example.com`;
58
+ const message = 'Inquiry about analytics engine';
59
+
60
+ await page.goto('/contact');
61
+ await page.getByLabel('Name').fill(name);
62
+ await page.getByLabel('Email').fill(email);
63
+ await page.getByLabel('Message').fill(message);
64
+ await page.getByRole('button', { name: 'Send' }).click();
65
+
66
+ await expect(page.getByText('Thank you, Ada Lovelace')).toBeVisible();
67
+ });
68
+ ```
69
+
70
+ Use `Date.now()` or `crypto.randomUUID()` for fields that must be unique (emails, usernames). This prevents collisions during parallel execution.
71
+
72
+ ---
73
+
74
+ ### Factory Functions
75
+
76
+ **Use when**: Multiple tests need the same data shape with different values, or you need guaranteed uniqueness.
77
+
78
+ **Avoid when**: The data is trivial and only used once — inline it instead.
79
+
80
+ Factories centralize data creation logic. When the data shape changes, you update one function, not dozens of tests.
81
+
82
+ **TypeScript**
83
+ ```typescript
84
+ // tests/factories/user.factory.ts
85
+ export interface UserData {
86
+ firstName: string;
87
+ lastName: string;
88
+ email: string;
89
+ password: string;
90
+ }
91
+
92
+ let counter = 0;
93
+
94
+ export function createUserData(overrides: Partial<UserData> = {}): UserData {
95
+ counter++;
96
+ const id = `${Date.now()}-${counter}`;
97
+ return {
98
+ firstName: `Test`,
99
+ lastName: `User${id}`,
100
+ email: `testuser-${id}@example.com`,
101
+ password: 'SecureP@ss123!',
102
+ ...overrides,
103
+ };
104
+ }
105
+ ```
106
+
107
+ ```typescript
108
+ // tests/registration.spec.ts
109
+ import { test, expect } from '@playwright/test';
110
+ import { createUserData } from './factories/user.factory';
111
+
112
+ test('registers a new user', async ({ page }) => {
113
+ const user = createUserData();
114
+
115
+ await page.goto('/register');
116
+ await page.getByLabel('First name').fill(user.firstName);
117
+ await page.getByLabel('Last name').fill(user.lastName);
118
+ await page.getByLabel('Email').fill(user.email);
119
+ await page.getByLabel('Password').fill(user.password);
120
+ await page.getByRole('button', { name: 'Create account' }).click();
121
+
122
+ await expect(page.getByText(`Welcome, ${user.firstName}`)).toBeVisible();
123
+ });
124
+
125
+ test('rejects duplicate email', async ({ page }) => {
126
+ const user = createUserData({ email: 'duplicate@example.com' });
127
+
128
+ await page.goto('/register');
129
+ await page.getByLabel('Email').fill(user.email);
130
+ await page.getByLabel('Password').fill(user.password);
131
+ await page.getByRole('button', { name: 'Create account' }).click();
132
+
133
+ await expect(page.getByText('Email already registered')).toBeVisible();
134
+ });
135
+ ```
136
+
137
+ **JavaScript**
138
+ ```javascript
139
+ // tests/factories/user.factory.js
140
+ let counter = 0;
141
+
142
+ function createUserData(overrides = {}) {
143
+ counter++;
144
+ const id = `${Date.now()}-${counter}`;
145
+ return {
146
+ firstName: 'Test',
147
+ lastName: `User${id}`,
148
+ email: `testuser-${id}@example.com`,
149
+ password: 'SecureP@ss123!',
150
+ ...overrides,
151
+ };
152
+ }
153
+
154
+ module.exports = { createUserData };
155
+ ```
156
+
157
+ ```javascript
158
+ // tests/registration.spec.js
159
+ const { test, expect } = require('@playwright/test');
160
+ const { createUserData } = require('./factories/user.factory');
161
+
162
+ test('registers a new user', async ({ page }) => {
163
+ const user = createUserData();
164
+
165
+ await page.goto('/register');
166
+ await page.getByLabel('First name').fill(user.firstName);
167
+ await page.getByLabel('Last name').fill(user.lastName);
168
+ await page.getByLabel('Email').fill(user.email);
169
+ await page.getByLabel('Password').fill(user.password);
170
+ await page.getByRole('button', { name: 'Create account' }).click();
171
+
172
+ await expect(page.getByText(`Welcome, ${user.firstName}`)).toBeVisible();
173
+ });
174
+ ```
175
+
176
+ ---
177
+
178
+ ### Faker / Random Data
179
+
180
+ **Use when**: You need realistic-looking data (names, addresses, phone numbers) or want to discover edge cases through randomness.
181
+
182
+ **Avoid when**: Debugging a failure — use a fixed seed to make it reproducible.
183
+
184
+ Always seed faker so failures are reproducible. Use `testInfo.testId` or a fixed seed per test file.
185
+
186
+ **TypeScript**
187
+ ```typescript
188
+ // tests/factories/faker-user.factory.ts
189
+ import { faker } from '@faker-js/faker';
190
+
191
+ export function createFakerUser(seed?: number) {
192
+ if (seed !== undefined) {
193
+ faker.seed(seed);
194
+ }
195
+ return {
196
+ firstName: faker.person.firstName(),
197
+ lastName: faker.person.lastName(),
198
+ email: faker.internet.email({ provider: 'testmail.example.com' }),
199
+ phone: faker.phone.number(),
200
+ address: {
201
+ street: faker.location.streetAddress(),
202
+ city: faker.location.city(),
203
+ state: faker.location.state({ abbreviated: true }),
204
+ zip: faker.location.zipCode(),
205
+ },
206
+ };
207
+ }
208
+ ```
209
+
210
+ ```typescript
211
+ // tests/checkout.spec.ts
212
+ import { test, expect } from '@playwright/test';
213
+ import { createFakerUser } from './factories/faker-user.factory';
214
+
215
+ test('completes checkout with shipping address', async ({ page }, testInfo) => {
216
+ // Seed with a stable value so re-runs produce the same data
217
+ const user = createFakerUser(testInfo.workerIndex);
218
+
219
+ await page.goto('/checkout');
220
+ await page.getByLabel('Street address').fill(user.address.street);
221
+ await page.getByLabel('City').fill(user.address.city);
222
+ await page.getByLabel('State').fill(user.address.state);
223
+ await page.getByLabel('ZIP code').fill(user.address.zip);
224
+ await page.getByRole('button', { name: 'Place order' }).click();
225
+
226
+ await expect(page.getByText('Order confirmed')).toBeVisible();
227
+ });
228
+ ```
229
+
230
+ **JavaScript**
231
+ ```javascript
232
+ // tests/factories/faker-user.factory.js
233
+ const { faker } = require('@faker-js/faker');
234
+
235
+ function createFakerUser(seed) {
236
+ if (seed !== undefined) {
237
+ faker.seed(seed);
238
+ }
239
+ return {
240
+ firstName: faker.person.firstName(),
241
+ lastName: faker.person.lastName(),
242
+ email: faker.internet.email({ provider: 'testmail.example.com' }),
243
+ phone: faker.phone.number(),
244
+ address: {
245
+ street: faker.location.streetAddress(),
246
+ city: faker.location.city(),
247
+ state: faker.location.state({ abbreviated: true }),
248
+ zip: faker.location.zipCode(),
249
+ },
250
+ };
251
+ }
252
+
253
+ module.exports = { createFakerUser };
254
+ ```
255
+
256
+ ```javascript
257
+ // tests/checkout.spec.js
258
+ const { test, expect } = require('@playwright/test');
259
+ const { createFakerUser } = require('./factories/faker-user.factory');
260
+
261
+ test('completes checkout with shipping address', async ({ page }, testInfo) => {
262
+ const user = createFakerUser(testInfo.workerIndex);
263
+
264
+ await page.goto('/checkout');
265
+ await page.getByLabel('Street address').fill(user.address.street);
266
+ await page.getByLabel('City').fill(user.address.city);
267
+ await page.getByLabel('State').fill(user.address.state);
268
+ await page.getByLabel('ZIP code').fill(user.address.zip);
269
+ await page.getByRole('button', { name: 'Place order' }).click();
270
+
271
+ await expect(page.getByText('Order confirmed')).toBeVisible();
272
+ });
273
+ ```
274
+
275
+ Always use a test-specific email domain (e.g., `testmail.example.com`) so faker-generated emails never hit real inboxes. The `example.com` domain is reserved by RFC 2606 and is safe for testing.
276
+
277
+ ---
278
+
279
+ ### Builder Pattern
280
+
281
+ **Use when**: Objects have many optional fields, conditional logic, or multiple valid configurations. Common for product listings, user profiles, or form payloads with nested data.
282
+
283
+ **Avoid when**: The object has fewer than 5 fields — a factory with overrides is simpler.
284
+
285
+ **TypeScript**
286
+ ```typescript
287
+ // tests/builders/product.builder.ts
288
+ export interface Product {
289
+ name: string;
290
+ price: number;
291
+ currency: string;
292
+ category: string;
293
+ description: string;
294
+ inStock: boolean;
295
+ tags: string[];
296
+ variants: { size: string; color: string }[];
297
+ }
298
+
299
+ export class ProductBuilder {
300
+ private product: Product = {
301
+ name: `Product-${Date.now()}`,
302
+ price: 29.99,
303
+ currency: 'USD',
304
+ category: 'Electronics',
305
+ description: 'A test product',
306
+ inStock: true,
307
+ tags: [],
308
+ variants: [],
309
+ };
310
+
311
+ withName(name: string): this {
312
+ this.product.name = name;
313
+ return this;
314
+ }
315
+
316
+ withPrice(price: number, currency = 'USD'): this {
317
+ this.product.price = price;
318
+ this.product.currency = currency;
319
+ return this;
320
+ }
321
+
322
+ withCategory(category: string): this {
323
+ this.product.category = category;
324
+ return this;
325
+ }
326
+
327
+ outOfStock(): this {
328
+ this.product.inStock = false;
329
+ return this;
330
+ }
331
+
332
+ withTags(...tags: string[]): this {
333
+ this.product.tags = tags;
334
+ return this;
335
+ }
336
+
337
+ withVariant(size: string, color: string): this {
338
+ this.product.variants.push({ size, color });
339
+ return this;
340
+ }
341
+
342
+ build(): Product {
343
+ return { ...this.product };
344
+ }
345
+ }
346
+ ```
347
+
348
+ ```typescript
349
+ // tests/product-catalog.spec.ts
350
+ import { test, expect } from '@playwright/test';
351
+ import { ProductBuilder } from './builders/product.builder';
352
+
353
+ test('displays out-of-stock badge for unavailable products', async ({ page, request }) => {
354
+ const product = new ProductBuilder()
355
+ .withName('Wireless Keyboard')
356
+ .withCategory('Accessories')
357
+ .outOfStock()
358
+ .build();
359
+
360
+ // Seed via API (see API Seeding pattern below)
361
+ await request.post('/api/products', { data: product });
362
+
363
+ await page.goto('/products');
364
+ const card = page.getByRole('listitem').filter({ hasText: product.name });
365
+ await expect(card.getByText('Out of Stock')).toBeVisible();
366
+ });
367
+ ```
368
+
369
+ **JavaScript**
370
+ ```javascript
371
+ // tests/builders/product.builder.js
372
+ class ProductBuilder {
373
+ constructor() {
374
+ this.product = {
375
+ name: `Product-${Date.now()}`,
376
+ price: 29.99,
377
+ currency: 'USD',
378
+ category: 'Electronics',
379
+ description: 'A test product',
380
+ inStock: true,
381
+ tags: [],
382
+ variants: [],
383
+ };
384
+ }
385
+
386
+ withName(name) {
387
+ this.product.name = name;
388
+ return this;
389
+ }
390
+
391
+ withPrice(price, currency = 'USD') {
392
+ this.product.price = price;
393
+ this.product.currency = currency;
394
+ return this;
395
+ }
396
+
397
+ withCategory(category) {
398
+ this.product.category = category;
399
+ return this;
400
+ }
401
+
402
+ outOfStock() {
403
+ this.product.inStock = false;
404
+ return this;
405
+ }
406
+
407
+ withTags(...tags) {
408
+ this.product.tags = tags;
409
+ return this;
410
+ }
411
+
412
+ withVariant(size, color) {
413
+ this.product.variants.push({ size, color });
414
+ return this;
415
+ }
416
+
417
+ build() {
418
+ return { ...this.product };
419
+ }
420
+ }
421
+
422
+ module.exports = { ProductBuilder };
423
+ ```
424
+
425
+ ```javascript
426
+ // tests/product-catalog.spec.js
427
+ const { test, expect } = require('@playwright/test');
428
+ const { ProductBuilder } = require('./builders/product.builder');
429
+
430
+ test('displays out-of-stock badge for unavailable products', async ({ page, request }) => {
431
+ const product = new ProductBuilder()
432
+ .withName('Wireless Keyboard')
433
+ .withCategory('Accessories')
434
+ .outOfStock()
435
+ .build();
436
+
437
+ await request.post('/api/products', { data: product });
438
+
439
+ await page.goto('/products');
440
+ const card = page.getByRole('listitem').filter({ hasText: product.name });
441
+ await expect(card.getByText('Out of Stock')).toBeVisible();
442
+ });
443
+ ```
444
+
445
+ ---
446
+
447
+ ### API Seeding
448
+
449
+ **Use when**: Tests need entities to already exist (users, products, orders) and the app exposes APIs to create them. This is the default strategy for test data setup.
450
+
451
+ **Avoid when**: No API exists for the entity, or the API itself is what you are testing (use the UI or database instead).
452
+
453
+ API seeding is faster than UI-based setup, more maintainable than database seeding, and exercises real application logic. Prefer it over all other approaches when an API is available.
454
+
455
+ **TypeScript**
456
+ ```typescript
457
+ // tests/fixtures/api-data.fixture.ts
458
+ import { test as base, APIRequestContext } from '@playwright/test';
459
+ import { createUserData, UserData } from '../factories/user.factory';
460
+
461
+ type ApiDataFixtures = {
462
+ apiUser: UserData & { id: string };
463
+ };
464
+
465
+ export const test = base.extend<ApiDataFixtures>({
466
+ apiUser: async ({ request }, use) => {
467
+ const userData = createUserData();
468
+
469
+ // Create
470
+ const response = await request.post('/api/users', { data: userData });
471
+ const created = await response.json();
472
+ const user = { ...userData, id: created.id };
473
+
474
+ // Provide to test
475
+ await use(user);
476
+
477
+ // Cleanup — runs even if test fails
478
+ await request.delete(`/api/users/${user.id}`);
479
+ },
480
+ });
481
+
482
+ export { expect } from '@playwright/test';
483
+ ```
484
+
485
+ ```typescript
486
+ // tests/user-profile.spec.ts
487
+ import { test, expect } from './fixtures/api-data.fixture';
488
+
489
+ test('edits user profile name', async ({ page, apiUser }) => {
490
+ await page.goto(`/users/${apiUser.id}/profile`);
491
+ await page.getByLabel('First name').fill('Updated');
492
+ await page.getByRole('button', { name: 'Save' }).click();
493
+
494
+ await expect(page.getByText('Profile updated')).toBeVisible();
495
+ });
496
+ ```
497
+
498
+ **JavaScript**
499
+ ```javascript
500
+ // tests/fixtures/api-data.fixture.js
501
+ const { test: base } = require('@playwright/test');
502
+ const { createUserData } = require('../factories/user.factory');
503
+
504
+ const test = base.extend({
505
+ apiUser: async ({ request }, use) => {
506
+ const userData = createUserData();
507
+
508
+ const response = await request.post('/api/users', { data: userData });
509
+ const created = await response.json();
510
+ const user = { ...userData, id: created.id };
511
+
512
+ await use(user);
513
+
514
+ await request.delete(`/api/users/${user.id}`);
515
+ },
516
+ });
517
+
518
+ module.exports = { test, expect: require('@playwright/test').expect };
519
+ ```
520
+
521
+ ```javascript
522
+ // tests/user-profile.spec.js
523
+ const { test, expect } = require('./fixtures/api-data.fixture');
524
+
525
+ test('edits user profile name', async ({ page, apiUser }) => {
526
+ await page.goto(`/users/${apiUser.id}/profile`);
527
+ await page.getByLabel('First name').fill('Updated');
528
+ await page.getByRole('button', { name: 'Save' }).click();
529
+
530
+ await expect(page.getByText('Profile updated')).toBeVisible();
531
+ });
532
+ ```
533
+
534
+ For multi-entity seeding, compose fixtures:
535
+
536
+ **TypeScript**
537
+ ```typescript
538
+ // tests/fixtures/order-data.fixture.ts
539
+ import { test as base } from './api-data.fixture';
540
+
541
+ export const test = base.extend({
542
+ apiOrder: async ({ request, apiUser }, use) => {
543
+ const orderResponse = await request.post('/api/orders', {
544
+ data: { userId: apiUser.id, items: [{ sku: 'WIDGET-001', qty: 2 }] },
545
+ });
546
+ const order = await orderResponse.json();
547
+
548
+ await use(order);
549
+
550
+ await request.delete(`/api/orders/${order.id}`);
551
+ },
552
+ });
553
+ ```
554
+
555
+ ---
556
+
557
+ ### Database Seeding
558
+
559
+ **Use when**: No API exists for the data you need, you need bulk data, or you need to set up complex relational state that is cumbersome via API calls.
560
+
561
+ **Avoid when**: An API exists — API seeding is more maintainable and exercises real application logic. Database seeding couples tests to schema details.
562
+
563
+ **TypeScript**
564
+ ```typescript
565
+ // tests/fixtures/db.fixture.ts
566
+ import { test as base } from '@playwright/test';
567
+ import { Pool } from 'pg';
568
+
569
+ type DbFixtures = {
570
+ db: Pool;
571
+ seededOrg: { id: string; name: string };
572
+ };
573
+
574
+ export const test = base.extend<DbFixtures>({
575
+ db: async ({}, use) => {
576
+ const pool = new Pool({
577
+ connectionString: process.env.TEST_DATABASE_URL,
578
+ });
579
+ await use(pool);
580
+ await pool.end();
581
+ },
582
+
583
+ seededOrg: async ({ db }, use) => {
584
+ const orgName = `TestOrg-${Date.now()}`;
585
+ const result = await db.query(
586
+ 'INSERT INTO organizations (name, plan) VALUES ($1, $2) RETURNING id',
587
+ [orgName, 'enterprise']
588
+ );
589
+ const orgId = result.rows[0].id;
590
+
591
+ await use({ id: orgId, name: orgName });
592
+
593
+ // Cascade delete cleans up related data
594
+ await db.query('DELETE FROM organizations WHERE id = $1', [orgId]);
595
+ },
596
+ });
597
+
598
+ export { expect } from '@playwright/test';
599
+ ```
600
+
601
+ **JavaScript**
602
+ ```javascript
603
+ // tests/fixtures/db.fixture.js
604
+ const { test: base } = require('@playwright/test');
605
+ const { Pool } = require('pg');
606
+
607
+ const test = base.extend({
608
+ db: async ({}, use) => {
609
+ const pool = new Pool({
610
+ connectionString: process.env.TEST_DATABASE_URL,
611
+ });
612
+ await use(pool);
613
+ await pool.end();
614
+ },
615
+
616
+ seededOrg: async ({ db }, use) => {
617
+ const orgName = `TestOrg-${Date.now()}`;
618
+ const result = await db.query(
619
+ 'INSERT INTO organizations (name, plan) VALUES ($1, $2) RETURNING id',
620
+ [orgName, 'enterprise']
621
+ );
622
+ const orgId = result.rows[0].id;
623
+
624
+ await use({ id: orgId, name: orgName });
625
+
626
+ await db.query('DELETE FROM organizations WHERE id = $1', [orgId]);
627
+ },
628
+ });
629
+
630
+ module.exports = { test, expect: require('@playwright/test').expect };
631
+ ```
632
+
633
+ Always use parameterized queries (`$1`, `$2`) to prevent SQL injection — even in tests. It is a good habit and prevents breakage from special characters in generated data.
634
+
635
+ ---
636
+
637
+ ### Storage State
638
+
639
+ **Use when**: Multiple tests need an authenticated session and you want to avoid logging in via the UI in every test.
640
+
641
+ **Avoid when**: The test is specifically testing the login flow itself.
642
+
643
+ Generate storage state once in a setup project, then reuse it across all tests that need authentication.
644
+
645
+ **TypeScript**
646
+ ```typescript
647
+ // playwright.config.ts
648
+ import { defineConfig } from '@playwright/test';
649
+
650
+ export default defineConfig({
651
+ projects: [
652
+ {
653
+ name: 'auth-setup',
654
+ testMatch: /auth\.setup\.ts/,
655
+ },
656
+ {
657
+ name: 'authenticated-tests',
658
+ dependencies: ['auth-setup'],
659
+ use: {
660
+ storageState: '.auth/user.json',
661
+ },
662
+ },
663
+ ],
664
+ });
665
+ ```
666
+
667
+ ```typescript
668
+ // tests/auth.setup.ts
669
+ import { test as setup, expect } from '@playwright/test';
670
+ import path from 'node:path';
671
+
672
+ const authFile = path.join(__dirname, '..', '.auth', 'user.json');
673
+
674
+ setup('authenticate as standard user', async ({ page }) => {
675
+ await page.goto('/login');
676
+ await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
677
+ await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
678
+ await page.getByRole('button', { name: 'Sign in' }).click();
679
+
680
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
681
+
682
+ await page.context().storageState({ path: authFile });
683
+ });
684
+ ```
685
+
686
+ **JavaScript**
687
+ ```javascript
688
+ // playwright.config.js
689
+ const { defineConfig } = require('@playwright/test');
690
+
691
+ module.exports = defineConfig({
692
+ projects: [
693
+ {
694
+ name: 'auth-setup',
695
+ testMatch: /auth\.setup\.js/,
696
+ },
697
+ {
698
+ name: 'authenticated-tests',
699
+ dependencies: ['auth-setup'],
700
+ use: {
701
+ storageState: '.auth/user.json',
702
+ },
703
+ },
704
+ ],
705
+ });
706
+ ```
707
+
708
+ ```javascript
709
+ // tests/auth.setup.js
710
+ const { test: setup, expect } = require('@playwright/test');
711
+ const path = require('node:path');
712
+
713
+ const authFile = path.join(__dirname, '..', '.auth', 'user.json');
714
+
715
+ setup('authenticate as standard user', async ({ page }) => {
716
+ await page.goto('/login');
717
+ await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL);
718
+ await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD);
719
+ await page.getByRole('button', { name: 'Sign in' }).click();
720
+
721
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
722
+
723
+ await page.context().storageState({ path: authFile });
724
+ });
725
+ ```
726
+
727
+ For multiple roles, create separate setup files and storage state files:
728
+
729
+ ```typescript
730
+ // tests/admin-auth.setup.ts
731
+ import { test as setup } from '@playwright/test';
732
+ import path from 'node:path';
733
+
734
+ const adminAuthFile = path.join(__dirname, '..', '.auth', 'admin.json');
735
+
736
+ setup('authenticate as admin', async ({ page }) => {
737
+ await page.goto('/login');
738
+ await page.getByLabel('Email').fill(process.env.ADMIN_EMAIL!);
739
+ await page.getByLabel('Password').fill(process.env.ADMIN_PASSWORD!);
740
+ await page.getByRole('button', { name: 'Sign in' }).click();
741
+ await page.context().storageState({ path: adminAuthFile });
742
+ });
743
+ ```
744
+
745
+ Add `.auth/` to `.gitignore` — storage state files contain session tokens.
746
+
747
+ ---
748
+
749
+ ### Test Data Cleanup
750
+
751
+ **Use when**: Always. Every test that creates data must clean it up.
752
+
753
+ **Avoid when**: Never. Skipping cleanup causes cascading failures in subsequent runs.
754
+
755
+ **Strategy 1: Fixture teardown (preferred)**
756
+
757
+ Put cleanup in the fixture's teardown block. Runs even if the test throws.
758
+
759
+ ```typescript
760
+ // Already shown in API Seeding — the cleanup runs after `use()`
761
+ apiUser: async ({ request }, use) => {
762
+ const user = await createViaApi(request);
763
+ await use(user);
764
+ // This ALWAYS runs — even on test failure
765
+ await request.delete(`/api/users/${user.id}`);
766
+ },
767
+ ```
768
+
769
+ **Strategy 2: Batch cleanup by timestamp prefix**
770
+
771
+ Tag all test-created data with a recognizable prefix, then sweep it in global teardown.
772
+
773
+ **TypeScript**
774
+ ```typescript
775
+ // global-teardown.ts
776
+ import { request } from '@playwright/test';
777
+
778
+ export default async function globalTeardown() {
779
+ const context = await request.newContext({
780
+ baseURL: process.env.BASE_URL,
781
+ });
782
+
783
+ // Delete all test entities created in this run
784
+ // Assumes entities created with the "test-" prefix
785
+ const response = await context.delete('/api/test-data/cleanup', {
786
+ data: { prefix: 'test-', olderThanMinutes: 60 },
787
+ });
788
+
789
+ if (!response.ok()) {
790
+ console.warn(`Cleanup returned ${response.status()}`);
791
+ }
792
+
793
+ await context.dispose();
794
+ }
795
+ ```
796
+
797
+ **JavaScript**
798
+ ```javascript
799
+ // global-teardown.js
800
+ const { request } = require('@playwright/test');
801
+
802
+ module.exports = async function globalTeardown() {
803
+ const context = await request.newContext({
804
+ baseURL: process.env.BASE_URL,
805
+ });
806
+
807
+ const response = await context.delete('/api/test-data/cleanup', {
808
+ data: { prefix: 'test-', olderThanMinutes: 60 },
809
+ });
810
+
811
+ if (!response.ok()) {
812
+ console.warn(`Cleanup returned ${response.status()}`);
813
+ }
814
+
815
+ await context.dispose();
816
+ };
817
+ ```
818
+
819
+ **Strategy 3: Isolated tenant per worker**
820
+
821
+ Each Playwright worker gets its own tenant/organization. All data is scoped to that tenant. Teardown deletes the entire tenant.
822
+
823
+ ```typescript
824
+ // tests/fixtures/tenant.fixture.ts
825
+ import { test as base } from '@playwright/test';
826
+
827
+ export const test = base.extend<{}, { workerTenant: { id: string; apiKey: string } }>({
828
+ workerTenant: [async ({ request }, use) => {
829
+ const res = await request.post('/api/tenants', {
830
+ data: { name: `test-worker-${Date.now()}` },
831
+ });
832
+ const tenant = await res.json();
833
+
834
+ await use(tenant);
835
+
836
+ await request.delete(`/api/tenants/${tenant.id}`);
837
+ }, { scope: 'worker' }],
838
+ });
839
+ ```
840
+
841
+ ---
842
+
843
+ ### Environment-Specific Data
844
+
845
+ **Use when**: Tests run against multiple environments (dev, staging, production-mirror) with different base URLs, credentials, or data constraints.
846
+
847
+ **Avoid when**: You only have one test environment.
848
+
849
+ Never hardcode environment-specific values in test files. Use `.env` files and `playwright.config` to inject them.
850
+
851
+ **TypeScript**
852
+ ```typescript
853
+ // playwright.config.ts
854
+ import { defineConfig } from '@playwright/test';
855
+ import dotenv from 'dotenv';
856
+ import path from 'node:path';
857
+
858
+ // Load environment-specific .env file
859
+ const envFile = process.env.TEST_ENV || 'local';
860
+ dotenv.config({ path: path.resolve(__dirname, `.env.${envFile}`) });
861
+
862
+ export default defineConfig({
863
+ use: {
864
+ baseURL: process.env.BASE_URL,
865
+ },
866
+ });
867
+ ```
868
+
869
+ ```
870
+ # .env.local
871
+ BASE_URL=http://localhost:3000
872
+ TEST_USER_EMAIL=testuser@localhost.test
873
+ TEST_USER_PASSWORD=localpassword123
874
+
875
+ # .env.staging
876
+ BASE_URL=https://staging.example.com
877
+ TEST_USER_EMAIL=e2e-bot@staging.example.com
878
+ TEST_USER_PASSWORD=staging-secret-from-vault
879
+ ```
880
+
881
+ ```typescript
882
+ // tests/fixtures/env-data.fixture.ts
883
+ import { test as base } from '@playwright/test';
884
+
885
+ type EnvConfig = {
886
+ testCredentials: { email: string; password: string };
887
+ };
888
+
889
+ export const test = base.extend<EnvConfig>({
890
+ testCredentials: async ({}, use) => {
891
+ const email = process.env.TEST_USER_EMAIL;
892
+ const password = process.env.TEST_USER_PASSWORD;
893
+ if (!email || !password) {
894
+ throw new Error('TEST_USER_EMAIL and TEST_USER_PASSWORD must be set');
895
+ }
896
+ await use({ email, password });
897
+ },
898
+ });
899
+
900
+ export { expect } from '@playwright/test';
901
+ ```
902
+
903
+ **JavaScript**
904
+ ```javascript
905
+ // playwright.config.js
906
+ const { defineConfig } = require('@playwright/test');
907
+ const dotenv = require('dotenv');
908
+ const path = require('node:path');
909
+
910
+ const envFile = process.env.TEST_ENV || 'local';
911
+ dotenv.config({ path: path.resolve(__dirname, `.env.${envFile}`) });
912
+
913
+ module.exports = defineConfig({
914
+ use: {
915
+ baseURL: process.env.BASE_URL,
916
+ },
917
+ });
918
+ ```
919
+
920
+ Run against a specific environment:
921
+
922
+ ```bash
923
+ TEST_ENV=staging npx playwright test
924
+ ```
925
+
926
+ ---
927
+
928
+ ### Fixtures for Test Data
929
+
930
+ **Use when**: Test data setup and teardown should be encapsulated, reusable, and guaranteed to clean up. This is the recommended pattern for all non-trivial test data.
931
+
932
+ **Avoid when**: The data is a simple inline value that does not require cleanup.
933
+
934
+ Fixtures are the backbone of reliable test data management in Playwright. They compose, they guarantee teardown, and they make tests declarative.
935
+
936
+ **TypeScript**
937
+ ```typescript
938
+ // tests/fixtures/index.ts
939
+ import { test as base, expect } from '@playwright/test';
940
+ import { createUserData, UserData } from '../factories/user.factory';
941
+
942
+ type TestFixtures = {
943
+ seedUser: UserData & { id: string };
944
+ seedProduct: { id: string; name: string; price: number };
945
+ };
946
+
947
+ export const test = base.extend<TestFixtures>({
948
+ seedUser: async ({ request }, use) => {
949
+ const data = createUserData();
950
+ const res = await request.post('/api/users', { data });
951
+ expect(res.ok()).toBeTruthy();
952
+ const user = { ...data, id: (await res.json()).id };
953
+
954
+ await use(user);
955
+
956
+ await request.delete(`/api/users/${user.id}`);
957
+ },
958
+
959
+ seedProduct: async ({ request }, use) => {
960
+ const data = {
961
+ name: `Product-${Date.now()}`,
962
+ price: 49.99,
963
+ category: 'Testing',
964
+ };
965
+ const res = await request.post('/api/products', { data });
966
+ expect(res.ok()).toBeTruthy();
967
+ const product = { ...data, id: (await res.json()).id };
968
+
969
+ await use(product);
970
+
971
+ await request.delete(`/api/products/${product.id}`);
972
+ },
973
+ });
974
+
975
+ export { expect };
976
+ ```
977
+
978
+ ```typescript
979
+ // tests/shopping-cart.spec.ts
980
+ import { test, expect } from './fixtures';
981
+
982
+ test('adds product to cart', async ({ page, seedUser, seedProduct }) => {
983
+ // seedUser and seedProduct are already created and will be cleaned up automatically
984
+ await page.goto(`/products/${seedProduct.id}`);
985
+ await page.getByRole('button', { name: 'Add to cart' }).click();
986
+
987
+ await page.goto('/cart');
988
+ await expect(page.getByText(seedProduct.name)).toBeVisible();
989
+ await expect(page.getByText('$49.99')).toBeVisible();
990
+ });
991
+ ```
992
+
993
+ **JavaScript**
994
+ ```javascript
995
+ // tests/fixtures/index.js
996
+ const { test: base, expect } = require('@playwright/test');
997
+ const { createUserData } = require('../factories/user.factory');
998
+
999
+ const test = base.extend({
1000
+ seedUser: async ({ request }, use) => {
1001
+ const data = createUserData();
1002
+ const res = await request.post('/api/users', { data });
1003
+ expect(res.ok()).toBeTruthy();
1004
+ const user = { ...data, id: (await res.json()).id };
1005
+
1006
+ await use(user);
1007
+
1008
+ await request.delete(`/api/users/${user.id}`);
1009
+ },
1010
+
1011
+ seedProduct: async ({ request }, use) => {
1012
+ const data = {
1013
+ name: `Product-${Date.now()}`,
1014
+ price: 49.99,
1015
+ category: 'Testing',
1016
+ };
1017
+ const res = await request.post('/api/products', { data });
1018
+ expect(res.ok()).toBeTruthy();
1019
+ const product = { ...data, id: (await res.json()).id };
1020
+
1021
+ await use(product);
1022
+
1023
+ await request.delete(`/api/products/${product.id}`);
1024
+ },
1025
+ });
1026
+
1027
+ module.exports = { test, expect };
1028
+ ```
1029
+
1030
+ ```javascript
1031
+ // tests/shopping-cart.spec.js
1032
+ const { test, expect } = require('./fixtures');
1033
+
1034
+ test('adds product to cart', async ({ page, seedUser, seedProduct }) => {
1035
+ await page.goto(`/products/${seedProduct.id}`);
1036
+ await page.getByRole('button', { name: 'Add to cart' }).click();
1037
+
1038
+ await page.goto('/cart');
1039
+ await expect(page.getByText(seedProduct.name)).toBeVisible();
1040
+ await expect(page.getByText('$49.99')).toBeVisible();
1041
+ });
1042
+ ```
1043
+
1044
+ Fixtures only run when a test requests them by name. If a test does not use `seedProduct`, the product is never created — no wasted setup, no unnecessary cleanup.
1045
+
1046
+ ## Decision Guide
1047
+
1048
+ ```
1049
+ What data does your test need?
1050
+ │
1051
+ ├── Simple values (strings, numbers for form fields)?
1052
+ │ └── Use INLINE DATA — keep it in the test
1053
+ │
1054
+ ├── Same shape used across many tests?
1055
+ │ ├── < 5 fields? → Use a FACTORY FUNCTION
1056
+ │ └── >= 5 fields with optional/conditional fields? → Use a BUILDER
1057
+ │
1058
+ ├── Need realistic-looking data (names, addresses)?
1059
+ │ └── Use FAKER with a fixed seed
1060
+ │
1061
+ ├── Entities that must exist before the test starts?
1062
+ │ ├── App has an API for this entity?
1063
+ │ │ └── Use API SEEDING in a fixture (preferred)
1064
+ │ ├── No API, but have database access?
1065
+ │ │ └── Use DATABASE SEEDING in a fixture
1066
+ │ └── No API, no DB access?
1067
+ │ └── Use UI setup in a beforeEach (slowest — avoid if possible)
1068
+ │
1069
+ ├── Need an authenticated session?
1070
+ │ └── Use STORAGE STATE via setup project
1071
+ │
1072
+ └── Need data isolated per worker?
1073
+ └── Use WORKER-SCOPED FIXTURES with tenant isolation
1074
+ ```
1075
+
1076
+ ### Speed ranking (fastest to slowest)
1077
+
1078
+ 1. Inline data / factories / faker — no I/O, instant
1079
+ 2. API seeding — one HTTP call per entity
1080
+ 3. Database seeding — direct DB call, skips app logic
1081
+ 4. Storage state — one-time login, reused across tests
1082
+ 5. UI-based setup — full browser interaction per test (avoid)
1083
+
1084
+ ### Isolation ranking (most to least isolated)
1085
+
1086
+ 1. Test-scoped fixtures with API teardown — each test gets fresh data, cleaned up after
1087
+ 2. Worker-scoped tenant isolation — tests in a worker share a tenant, cleaned up when worker exits
1088
+ 3. Timestamp-prefixed batch cleanup — catches orphaned data, runs in global teardown
1089
+ 4. Shared database state — fragile, prone to ordering bugs (avoid)
1090
+
1091
+ ## Anti-Patterns
1092
+
1093
+ ### Shared mutable data across tests
1094
+
1095
+ ```typescript
1096
+ // BAD — tests share the same user object and depend on execution order
1097
+ let sharedUser: UserData;
1098
+
1099
+ test.beforeAll(async ({ request }) => {
1100
+ sharedUser = await createUser(request);
1101
+ });
1102
+
1103
+ test('updates user name', async ({ page }) => {
1104
+ // Mutates sharedUser — other tests see the changed state
1105
+ });
1106
+
1107
+ test('checks original name', async ({ page }) => {
1108
+ // Fails because the previous test changed the name
1109
+ });
1110
+ ```
1111
+
1112
+ Fix: Use test-scoped fixtures. Each test gets its own instance.
1113
+
1114
+ ### Hardcoded IDs
1115
+
1116
+ ```typescript
1117
+ // BAD — this ID only exists in your local database
1118
+ test('edits product', async ({ page }) => {
1119
+ await page.goto('/products/507f1f77bcf86cd799439011/edit');
1120
+ });
1121
+ ```
1122
+
1123
+ Fix: Create the product in a fixture and use the returned ID.
1124
+
1125
+ ### No cleanup
1126
+
1127
+ ```typescript
1128
+ // BAD — creates data but never deletes it
1129
+ test.beforeEach(async ({ request }) => {
1130
+ await request.post('/api/products', { data: { name: 'Leaked Product' } });
1131
+ });
1132
+ // Over time, the database fills with test debris causing slowdowns and false positives
1133
+ ```
1134
+
1135
+ Fix: Always pair creation with deletion in a fixture teardown.
1136
+
1137
+ ### Relying on pre-existing database state
1138
+
1139
+ ```typescript
1140
+ // BAD — assumes "Premium Plan" already exists in the database
1141
+ test('subscribes to premium', async ({ page }) => {
1142
+ await page.goto('/pricing');
1143
+ await page.getByRole('button', { name: 'Premium Plan' }).click();
1144
+ // Breaks on a fresh database or different environment
1145
+ });
1146
+ ```
1147
+
1148
+ Fix: Seed the plan via API or database fixture before the test.
1149
+
1150
+ ### Using production data in tests
1151
+
1152
+ ```typescript
1153
+ // BAD — tests against real customer data
1154
+ test('views customer orders', async ({ page }) => {
1155
+ await page.goto('/admin/customers/real-customer-id/orders');
1156
+ });
1157
+ ```
1158
+
1159
+ Fix: Create synthetic test data. Never point test suites at production databases or use real customer identifiers.
1160
+
1161
+ ### Over-engineering data setup
1162
+
1163
+ ```typescript
1164
+ // BAD — abstraction for its own sake
1165
+ const data = new TestDataOrchestrator()
1166
+ .withStrategy('api')
1167
+ .withRetry(3)
1168
+ .withCleanupPolicy('deferred')
1169
+ .withEnvironment('staging')
1170
+ .build();
1171
+ ```
1172
+
1173
+ Fix: A factory function and a fixture cover 95% of cases. Add complexity only when you have a proven need.
1174
+
1175
+ ## Troubleshooting
1176
+
1177
+ | Problem | Cause | Fix |
1178
+ |---|---|---|
1179
+ | Tests fail with "duplicate key" errors | Data from previous runs was not cleaned up | Add fixture teardown; run batch cleanup in `globalTeardown` |
1180
+ | Tests pass alone but fail in parallel | Tests share mutable state or collide on unique fields | Use `Date.now()` or `crypto.randomUUID()` in factory output; use test-scoped fixtures |
1181
+ | Faker produces different data on retry | Faker was not seeded, or seed changes between runs | Seed with `testInfo.workerIndex` or a fixed value |
1182
+ | Storage state expired / session invalid | Auth tokens have a short TTL | Re-run auth setup before each suite; set `fullyParallel: false` on auth-dependent project if needed |
1183
+ | Cleanup fails and blocks other tests | Teardown makes network calls that can timeout | Wrap cleanup in `try/catch`; log failures but do not throw; rely on batch cleanup as a safety net |
1184
+ | Database seeding is slow | Too many individual INSERT statements | Batch inserts; use transactions; consider API seeding instead |
1185
+ | Tests break when run against staging | Hardcoded values that only exist locally | Use environment variables for all environment-specific data; validate in fixture with clear error messages |
1186
+
1187
+ ### Making cleanup resilient
1188
+
1189
+ ```typescript
1190
+ // Wrap fixture teardown in try/catch so a cleanup failure does not mask the real test failure
1191
+ seedUser: async ({ request }, use) => {
1192
+ const user = await createViaApi(request);
1193
+ await use(user);
1194
+
1195
+ try {
1196
+ await request.delete(`/api/users/${user.id}`);
1197
+ } catch (error) {
1198
+ console.warn(`Failed to clean up user ${user.id}:`, error);
1199
+ }
1200
+ },
1201
+ ```
1202
+
1203
+ ## Related
1204
+
1205
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) — fixture mechanics, scoping, composition
1206
+ - [core/authentication.md](authentication.md) — storage state setup, multi-role auth patterns
1207
+ - [core/api-testing.md](api-testing.md) — API request context, response validation
1208
+ - [ci/global-setup-teardown.md](../ci/global-setup-teardown.md) — global setup/teardown for batch operations
1209
+ - [pom/pom-vs-fixtures-vs-helpers.md](../pom/pom-vs-fixtures-vs-helpers.md) — when to use fixtures vs page objects vs helpers