@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,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
|