@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,947 @@
|
|
|
1
|
+
# Test Organization
|
|
2
|
+
|
|
3
|
+
> **When to use**: Structuring test files, naming tests, grouping with `describe`, tagging, filtering, and deciding parallel vs serial execution.
|
|
4
|
+
> **Prerequisites**: [core/configuration.md](foundations/configuration.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
| Concept | Rule |
|
|
9
|
+
|---|---|
|
|
10
|
+
| File suffix | `.spec.ts` / `.spec.js` — always |
|
|
11
|
+
| Grouping | By feature/domain, not by page or URL |
|
|
12
|
+
| Test names | `test('should ...')` or `test('user can ...')` — describe behavior |
|
|
13
|
+
| Nesting | Max 2 levels of `test.describe()` |
|
|
14
|
+
| Default execution | `fullyParallel: true` — tests run in parallel by default |
|
|
15
|
+
| Serial tests | Almost never use `test.describe.serial()` |
|
|
16
|
+
| Test dependencies | Avoid — each test sets up its own state |
|
|
17
|
+
| Tags | `@smoke`, `@regression`, `@slow` — filter with `--grep` |
|
|
18
|
+
|
|
19
|
+
## Patterns
|
|
20
|
+
|
|
21
|
+
### Pattern 1: Feature-Based File Structure
|
|
22
|
+
|
|
23
|
+
**Use when**: Any project — this is the default layout.
|
|
24
|
+
**Avoid when**: Never. This is always correct.
|
|
25
|
+
|
|
26
|
+
Group tests by feature or domain, not by page class or test type.
|
|
27
|
+
|
|
28
|
+
**Small project (< 20 tests):**
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
tests/
|
|
32
|
+
├── auth.spec.ts
|
|
33
|
+
├── dashboard.spec.ts
|
|
34
|
+
├── settings.spec.ts
|
|
35
|
+
└── checkout.spec.ts
|
|
36
|
+
playwright.config.ts
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Medium project (20–200 tests):**
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
tests/
|
|
43
|
+
├── auth/
|
|
44
|
+
│ ├── login.spec.ts
|
|
45
|
+
│ ├── signup.spec.ts
|
|
46
|
+
│ ├── password-reset.spec.ts
|
|
47
|
+
│ └── mfa.spec.ts
|
|
48
|
+
├── dashboard/
|
|
49
|
+
│ ├── widgets.spec.ts
|
|
50
|
+
│ ├── filters.spec.ts
|
|
51
|
+
│ └── export.spec.ts
|
|
52
|
+
├── checkout/
|
|
53
|
+
│ ├── cart.spec.ts
|
|
54
|
+
│ ├── payment.spec.ts
|
|
55
|
+
│ └── confirmation.spec.ts
|
|
56
|
+
├── settings/
|
|
57
|
+
│ ├── profile.spec.ts
|
|
58
|
+
│ └── notifications.spec.ts
|
|
59
|
+
└── fixtures/
|
|
60
|
+
├── auth.fixture.ts
|
|
61
|
+
└── test-data.ts
|
|
62
|
+
playwright.config.ts
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Large project (200+ tests):**
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
tests/
|
|
69
|
+
├── e2e/
|
|
70
|
+
│ ├── auth/
|
|
71
|
+
│ │ ├── login.spec.ts
|
|
72
|
+
│ │ ├── signup.spec.ts
|
|
73
|
+
│ │ ├── password-reset.spec.ts
|
|
74
|
+
│ │ └── mfa.spec.ts
|
|
75
|
+
│ ├── checkout/
|
|
76
|
+
│ │ ├── cart.spec.ts
|
|
77
|
+
│ │ ├── payment.spec.ts
|
|
78
|
+
│ │ ├── promo-codes.spec.ts
|
|
79
|
+
│ │ └── confirmation.spec.ts
|
|
80
|
+
│ ├── admin/
|
|
81
|
+
│ │ ├── user-management.spec.ts
|
|
82
|
+
│ │ ├── reports.spec.ts
|
|
83
|
+
│ │ └── audit-log.spec.ts
|
|
84
|
+
│ └── inventory/
|
|
85
|
+
│ ├── product-list.spec.ts
|
|
86
|
+
│ ├── product-detail.spec.ts
|
|
87
|
+
│ └── stock-management.spec.ts
|
|
88
|
+
├── api/
|
|
89
|
+
│ ├── users.spec.ts
|
|
90
|
+
│ ├── orders.spec.ts
|
|
91
|
+
│ └── products.spec.ts
|
|
92
|
+
├── visual/
|
|
93
|
+
│ ├── homepage.spec.ts
|
|
94
|
+
│ └── product-page.spec.ts
|
|
95
|
+
├── fixtures/
|
|
96
|
+
│ ├── auth.fixture.ts
|
|
97
|
+
│ ├── db.fixture.ts
|
|
98
|
+
│ └── base.fixture.ts
|
|
99
|
+
├── page-objects/
|
|
100
|
+
│ ├── login.page.ts
|
|
101
|
+
│ ├── dashboard.page.ts
|
|
102
|
+
│ └── checkout.page.ts
|
|
103
|
+
└── helpers/
|
|
104
|
+
├── test-data.ts
|
|
105
|
+
└── api-client.ts
|
|
106
|
+
playwright.config.ts
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### Pattern 2: Naming Conventions
|
|
112
|
+
|
|
113
|
+
**Use when**: Writing any test or file.
|
|
114
|
+
**Avoid when**: Never.
|
|
115
|
+
|
|
116
|
+
**TypeScript:**
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// tests/checkout/cart.spec.ts
|
|
120
|
+
import { test, expect } from '@playwright/test';
|
|
121
|
+
|
|
122
|
+
// Good: group by feature, describe behavior
|
|
123
|
+
test.describe('Shopping Cart', () => {
|
|
124
|
+
test('should add item to empty cart', async ({ page }) => {
|
|
125
|
+
await page.goto('/products/widget-a');
|
|
126
|
+
await page.getByRole('button', { name: 'Add to cart' }).click();
|
|
127
|
+
await expect(page.getByTestId('cart-count')).toHaveText('1');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should update quantity when same item added twice', async ({ page }) => {
|
|
131
|
+
await page.goto('/products/widget-a');
|
|
132
|
+
await page.getByRole('button', { name: 'Add to cart' }).click();
|
|
133
|
+
await page.getByRole('button', { name: 'Add to cart' }).click();
|
|
134
|
+
await expect(page.getByTestId('cart-count')).toHaveText('2');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('user can remove item from cart', async ({ page }) => {
|
|
138
|
+
await page.goto('/cart');
|
|
139
|
+
// ... setup item in cart via API or fixture
|
|
140
|
+
await page.getByRole('button', { name: 'Remove' }).first().click();
|
|
141
|
+
await expect(page.getByText('Your cart is empty')).toBeVisible();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**JavaScript:**
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// tests/checkout/cart.spec.js
|
|
150
|
+
const { test, expect } = require('@playwright/test');
|
|
151
|
+
|
|
152
|
+
test.describe('Shopping Cart', () => {
|
|
153
|
+
test('should add item to empty cart', async ({ page }) => {
|
|
154
|
+
await page.goto('/products/widget-a');
|
|
155
|
+
await page.getByRole('button', { name: 'Add to cart' }).click();
|
|
156
|
+
await expect(page.getByTestId('cart-count')).toHaveText('1');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('should update quantity when same item added twice', async ({ page }) => {
|
|
160
|
+
await page.goto('/products/widget-a');
|
|
161
|
+
await page.getByRole('button', { name: 'Add to cart' }).click();
|
|
162
|
+
await page.getByRole('button', { name: 'Add to cart' }).click();
|
|
163
|
+
await expect(page.getByTestId('cart-count')).toHaveText('2');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('user can remove item from cart', async ({ page }) => {
|
|
167
|
+
await page.goto('/cart');
|
|
168
|
+
await page.getByRole('button', { name: 'Remove' }).first().click();
|
|
169
|
+
await expect(page.getByText('Your cart is empty')).toBeVisible();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Naming rules:**
|
|
175
|
+
|
|
176
|
+
| Element | Convention | Example |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| File name | `kebab-case.spec.ts` | `password-reset.spec.ts` |
|
|
179
|
+
| `test.describe()` | Title Case, feature name | `'Password Reset'` |
|
|
180
|
+
| `test()` | Sentence starting with `should` or `user can` | `'should send reset email'` |
|
|
181
|
+
| Page objects | `PascalCase.page.ts` | `login.page.ts` / `LoginPage` |
|
|
182
|
+
| Fixtures | `kebab-case.fixture.ts` | `auth.fixture.ts` |
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Pattern 3: `test.describe()` Grouping
|
|
187
|
+
|
|
188
|
+
**Use when**: A file has multiple related tests that share context or setup.
|
|
189
|
+
**Avoid when**: A file has only 1–3 tests with no shared setup — skip the `describe` wrapper.
|
|
190
|
+
|
|
191
|
+
**TypeScript:**
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// tests/auth/login.spec.ts
|
|
195
|
+
import { test, expect } from '@playwright/test';
|
|
196
|
+
|
|
197
|
+
test.describe('Login', () => {
|
|
198
|
+
// Shared setup for all tests in this describe block
|
|
199
|
+
test.beforeEach(async ({ page }) => {
|
|
200
|
+
await page.goto('/login');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should login with valid credentials', async ({ page }) => {
|
|
204
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
205
|
+
await page.getByLabel('Password').fill('securepass123');
|
|
206
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
207
|
+
await expect(page).toHaveURL('/dashboard');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('should show error for invalid password', async ({ page }) => {
|
|
211
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
212
|
+
await page.getByLabel('Password').fill('wrongpassword');
|
|
213
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
214
|
+
await expect(page.getByRole('alert')).toHaveText('Invalid credentials');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// One level of nesting — acceptable
|
|
218
|
+
test.describe('Rate Limiting', () => {
|
|
219
|
+
test('should lock account after 5 failed attempts', async ({ page }) => {
|
|
220
|
+
for (let i = 0; i < 5; i++) {
|
|
221
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
222
|
+
await page.getByLabel('Password').fill('wrong');
|
|
223
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
224
|
+
}
|
|
225
|
+
await expect(page.getByRole('alert')).toContainText('Account locked');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**JavaScript:**
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
// tests/auth/login.spec.js
|
|
235
|
+
const { test, expect } = require('@playwright/test');
|
|
236
|
+
|
|
237
|
+
test.describe('Login', () => {
|
|
238
|
+
test.beforeEach(async ({ page }) => {
|
|
239
|
+
await page.goto('/login');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('should login with valid credentials', async ({ page }) => {
|
|
243
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
244
|
+
await page.getByLabel('Password').fill('securepass123');
|
|
245
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
246
|
+
await expect(page).toHaveURL('/dashboard');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('should show error for invalid password', async ({ page }) => {
|
|
250
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
251
|
+
await page.getByLabel('Password').fill('wrongpassword');
|
|
252
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
253
|
+
await expect(page.getByRole('alert')).toHaveText('Invalid credentials');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test.describe('Rate Limiting', () => {
|
|
257
|
+
test('should lock account after 5 failed attempts', async ({ page }) => {
|
|
258
|
+
for (let i = 0; i < 5; i++) {
|
|
259
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
260
|
+
await page.getByLabel('Password').fill('wrong');
|
|
261
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
262
|
+
}
|
|
263
|
+
await expect(page.getByRole('alert')).toContainText('Account locked');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Nesting limit: 2 levels max.** If you need a third level, split into a separate file.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### Pattern 4: Tags and Annotations
|
|
274
|
+
|
|
275
|
+
**Use when**: You need to categorize tests for selective runs (smoke suites, CI pipelines, skip known issues).
|
|
276
|
+
**Avoid when**: All tests always run together and you have < 20 tests.
|
|
277
|
+
|
|
278
|
+
**TypeScript:**
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// tests/checkout/payment.spec.ts
|
|
282
|
+
import { test, expect } from '@playwright/test';
|
|
283
|
+
|
|
284
|
+
// Tag in the test title — simplest approach
|
|
285
|
+
test('should process credit card payment @smoke', async ({ page }) => {
|
|
286
|
+
await page.goto('/checkout');
|
|
287
|
+
await page.getByLabel('Card number').fill('4242424242424242');
|
|
288
|
+
await page.getByLabel('Expiry').fill('12/28');
|
|
289
|
+
await page.getByLabel('CVC').fill('123');
|
|
290
|
+
await page.getByRole('button', { name: 'Pay now' }).click();
|
|
291
|
+
await expect(page.getByText('Payment successful')).toBeVisible();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('should handle declined card @regression', async ({ page }) => {
|
|
295
|
+
await page.goto('/checkout');
|
|
296
|
+
await page.getByLabel('Card number').fill('4000000000000002');
|
|
297
|
+
await page.getByLabel('Expiry').fill('12/28');
|
|
298
|
+
await page.getByLabel('CVC').fill('123');
|
|
299
|
+
await page.getByRole('button', { name: 'Pay now' }).click();
|
|
300
|
+
await expect(page.getByRole('alert')).toContainText('Card declined');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Tag via test.describe for group-level tagging
|
|
304
|
+
test.describe('Payment Edge Cases @regression', () => {
|
|
305
|
+
test('should handle network timeout during payment', async ({ page }) => {
|
|
306
|
+
await page.goto('/checkout');
|
|
307
|
+
// ... test implementation
|
|
308
|
+
await expect(page.getByText('Please try again')).toBeVisible();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Annotations for test lifecycle control
|
|
313
|
+
test('should render 3D Secure iframe @slow', async ({ page }) => {
|
|
314
|
+
test.slow(); // Triples the default timeout
|
|
315
|
+
await page.goto('/checkout/3ds');
|
|
316
|
+
// ... long-running 3D Secure flow
|
|
317
|
+
await expect(page.getByText('Verified')).toBeVisible();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('should apply loyalty points discount', async ({ page }) => {
|
|
321
|
+
test.skip(process.env.CI !== 'true', 'Only run in CI — needs loyalty service');
|
|
322
|
+
await page.goto('/checkout');
|
|
323
|
+
// ...
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('should handle Apple Pay on Safari', async ({ page, browserName }) => {
|
|
327
|
+
test.skip(browserName !== 'webkit', 'Apple Pay only works in Safari');
|
|
328
|
+
await page.goto('/checkout');
|
|
329
|
+
// ...
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('should display PayPal button', async ({ page }) => {
|
|
333
|
+
test.fixme(); // Known broken — tracked in JIRA-1234
|
|
334
|
+
await page.goto('/checkout');
|
|
335
|
+
await expect(page.getByRole('button', { name: 'PayPal' })).toBeVisible();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('should fail when submitting empty card form', async ({ page }) => {
|
|
339
|
+
test.fail(); // This test is expected to fail — documents known bug
|
|
340
|
+
await page.goto('/checkout');
|
|
341
|
+
await page.getByRole('button', { name: 'Pay now' }).click();
|
|
342
|
+
// Bug: currently no validation shown — this assertion will fail
|
|
343
|
+
await expect(page.getByRole('alert')).toBeVisible();
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**JavaScript:**
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
// tests/checkout/payment.spec.js
|
|
351
|
+
const { test, expect } = require('@playwright/test');
|
|
352
|
+
|
|
353
|
+
test('should process credit card payment @smoke', async ({ page }) => {
|
|
354
|
+
await page.goto('/checkout');
|
|
355
|
+
await page.getByLabel('Card number').fill('4242424242424242');
|
|
356
|
+
await page.getByLabel('Expiry').fill('12/28');
|
|
357
|
+
await page.getByLabel('CVC').fill('123');
|
|
358
|
+
await page.getByRole('button', { name: 'Pay now' }).click();
|
|
359
|
+
await expect(page.getByText('Payment successful')).toBeVisible();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('should handle declined card @regression', async ({ page }) => {
|
|
363
|
+
await page.goto('/checkout');
|
|
364
|
+
await page.getByLabel('Card number').fill('4000000000000002');
|
|
365
|
+
await page.getByLabel('Expiry').fill('12/28');
|
|
366
|
+
await page.getByLabel('CVC').fill('123');
|
|
367
|
+
await page.getByRole('button', { name: 'Pay now' }).click();
|
|
368
|
+
await expect(page.getByRole('alert')).toContainText('Card declined');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test.describe('Payment Edge Cases @regression', () => {
|
|
372
|
+
test('should handle network timeout during payment', async ({ page }) => {
|
|
373
|
+
await page.goto('/checkout');
|
|
374
|
+
await expect(page.getByText('Please try again')).toBeVisible();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('should render 3D Secure iframe @slow', async ({ page }) => {
|
|
379
|
+
test.slow();
|
|
380
|
+
await page.goto('/checkout/3ds');
|
|
381
|
+
await expect(page.getByText('Verified')).toBeVisible();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('should apply loyalty points discount', async ({ page }) => {
|
|
385
|
+
test.skip(process.env.CI !== 'true', 'Only run in CI — needs loyalty service');
|
|
386
|
+
await page.goto('/checkout');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('should handle Apple Pay on Safari', async ({ page, browserName }) => {
|
|
390
|
+
test.skip(browserName !== 'webkit', 'Apple Pay only works in Safari');
|
|
391
|
+
await page.goto('/checkout');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('should display PayPal button', async ({ page }) => {
|
|
395
|
+
test.fixme(); // Known broken — tracked in JIRA-1234
|
|
396
|
+
await page.goto('/checkout');
|
|
397
|
+
await expect(page.getByRole('button', { name: 'PayPal' })).toBeVisible();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('should fail when submitting empty card form', async ({ page }) => {
|
|
401
|
+
test.fail();
|
|
402
|
+
await page.goto('/checkout');
|
|
403
|
+
await page.getByRole('button', { name: 'Pay now' }).click();
|
|
404
|
+
await expect(page.getByRole('alert')).toBeVisible();
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Annotation cheat sheet:**
|
|
409
|
+
|
|
410
|
+
| Annotation | Effect | When to use |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| `test.skip()` | Skips the test entirely | Feature not available in this env/browser |
|
|
413
|
+
| `test.skip(condition, reason)` | Conditional skip | Browser-specific or env-specific tests |
|
|
414
|
+
| `test.fixme()` | Skips and marks as "needs fix" | Known bug, not yet fixed |
|
|
415
|
+
| `test.slow()` | Triples the timeout | Tests with inherently slow workflows |
|
|
416
|
+
| `test.fail()` | Expects the test to fail; fails if it passes | Documenting a known bug with a regression guard |
|
|
417
|
+
| `test.info().annotations` | Add custom annotations | Custom metadata for reports |
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
### Pattern 5: Test Filtering
|
|
422
|
+
|
|
423
|
+
**Use when**: Running a subset of tests from the CLI or CI.
|
|
424
|
+
**Avoid when**: Never — know these commands.
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
# By tag (in test title)
|
|
428
|
+
npx playwright test --grep @smoke
|
|
429
|
+
npx playwright test --grep @regression
|
|
430
|
+
npx playwright test --grep-invert @slow # everything except @slow
|
|
431
|
+
npx playwright test -G @slow # same — -G is the 1.61+ shorthand
|
|
432
|
+
|
|
433
|
+
# By file
|
|
434
|
+
npx playwright test tests/auth/
|
|
435
|
+
npx playwright test tests/checkout/payment.spec.ts
|
|
436
|
+
|
|
437
|
+
# By test name
|
|
438
|
+
npx playwright test --grep "should login"
|
|
439
|
+
|
|
440
|
+
# By project (from playwright.config)
|
|
441
|
+
npx playwright test --project=chromium
|
|
442
|
+
npx playwright test --project=mobile-safari
|
|
443
|
+
|
|
444
|
+
# Combine filters
|
|
445
|
+
npx playwright test --grep @smoke --project=chromium
|
|
446
|
+
npx playwright test tests/checkout/ --grep @smoke
|
|
447
|
+
|
|
448
|
+
# By line number — run a single test
|
|
449
|
+
npx playwright test tests/auth/login.spec.ts:15
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Using tags with `test.describe.configure()`:**
|
|
453
|
+
|
|
454
|
+
**TypeScript:**
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// playwright.config.ts
|
|
458
|
+
import { defineConfig } from '@playwright/test';
|
|
459
|
+
|
|
460
|
+
export default defineConfig({
|
|
461
|
+
projects: [
|
|
462
|
+
{
|
|
463
|
+
name: 'smoke',
|
|
464
|
+
testMatch: '**/*.spec.ts',
|
|
465
|
+
grep: /@smoke/,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'regression',
|
|
469
|
+
testMatch: '**/*.spec.ts',
|
|
470
|
+
grep: /@regression/,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'all',
|
|
474
|
+
testMatch: '**/*.spec.ts',
|
|
475
|
+
grepInvert: /@slow/,
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**JavaScript:**
|
|
482
|
+
|
|
483
|
+
```javascript
|
|
484
|
+
// playwright.config.js
|
|
485
|
+
const { defineConfig } = require('@playwright/test');
|
|
486
|
+
|
|
487
|
+
module.exports = defineConfig({
|
|
488
|
+
projects: [
|
|
489
|
+
{
|
|
490
|
+
name: 'smoke',
|
|
491
|
+
testMatch: '**/*.spec.js',
|
|
492
|
+
grep: /@smoke/,
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'regression',
|
|
496
|
+
testMatch: '**/*.spec.js',
|
|
497
|
+
grep: /@regression/,
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: 'all',
|
|
501
|
+
testMatch: '**/*.spec.js',
|
|
502
|
+
grepInvert: /@slow/,
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### Pattern 6: Parallel vs Serial Execution
|
|
511
|
+
|
|
512
|
+
**Use when**: Deciding how tests run relative to each other.
|
|
513
|
+
**Avoid when**: You have < 5 tests (parallelism doesn't matter).
|
|
514
|
+
|
|
515
|
+
**Default: always parallel.** Set `fullyParallel: true` in config.
|
|
516
|
+
|
|
517
|
+
**TypeScript:**
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// playwright.config.ts
|
|
521
|
+
import { defineConfig } from '@playwright/test';
|
|
522
|
+
|
|
523
|
+
export default defineConfig({
|
|
524
|
+
fullyParallel: true, // All tests run in parallel by default
|
|
525
|
+
workers: process.env.CI ? 1 : undefined, // Use all cores locally, 1 in CI (or adjust)
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**JavaScript:**
|
|
530
|
+
|
|
531
|
+
```javascript
|
|
532
|
+
// playwright.config.js
|
|
533
|
+
const { defineConfig } = require('@playwright/test');
|
|
534
|
+
|
|
535
|
+
module.exports = defineConfig({
|
|
536
|
+
fullyParallel: true,
|
|
537
|
+
workers: process.env.CI ? 1 : undefined,
|
|
538
|
+
});
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Serial execution — use only when tests share external state you cannot isolate:**
|
|
542
|
+
|
|
543
|
+
**TypeScript:**
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// tests/onboarding/wizard.spec.ts
|
|
547
|
+
import { test, expect } from '@playwright/test';
|
|
548
|
+
|
|
549
|
+
// ONLY use serial when steps truly depend on prior state that cannot be set up independently
|
|
550
|
+
test.describe.serial('Onboarding Wizard', () => {
|
|
551
|
+
test('step 1: user enters company name', async ({ page }) => {
|
|
552
|
+
await page.goto('/onboarding');
|
|
553
|
+
await page.getByLabel('Company name').fill('Acme Corp');
|
|
554
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
555
|
+
await expect(page).toHaveURL('/onboarding/step-2');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('step 2: user selects plan', async ({ page }) => {
|
|
559
|
+
await page.goto('/onboarding/step-2');
|
|
560
|
+
await page.getByRole('radio', { name: 'Pro plan' }).check();
|
|
561
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
562
|
+
await expect(page).toHaveURL('/onboarding/step-3');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('step 3: user confirms and completes', async ({ page }) => {
|
|
566
|
+
await page.goto('/onboarding/step-3');
|
|
567
|
+
await page.getByRole('button', { name: 'Complete setup' }).click();
|
|
568
|
+
await expect(page.getByText('Welcome to Acme Corp')).toBeVisible();
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**JavaScript:**
|
|
574
|
+
|
|
575
|
+
```javascript
|
|
576
|
+
// tests/onboarding/wizard.spec.js
|
|
577
|
+
const { test, expect } = require('@playwright/test');
|
|
578
|
+
|
|
579
|
+
test.describe.serial('Onboarding Wizard', () => {
|
|
580
|
+
test('step 1: user enters company name', async ({ page }) => {
|
|
581
|
+
await page.goto('/onboarding');
|
|
582
|
+
await page.getByLabel('Company name').fill('Acme Corp');
|
|
583
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
584
|
+
await expect(page).toHaveURL('/onboarding/step-2');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('step 2: user selects plan', async ({ page }) => {
|
|
588
|
+
await page.goto('/onboarding/step-2');
|
|
589
|
+
await page.getByRole('radio', { name: 'Pro plan' }).check();
|
|
590
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
591
|
+
await expect(page).toHaveURL('/onboarding/step-3');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('step 3: user confirms and completes', async ({ page }) => {
|
|
595
|
+
await page.goto('/onboarding/step-3');
|
|
596
|
+
await page.getByRole('button', { name: 'Complete setup' }).click();
|
|
597
|
+
await expect(page.getByText('Welcome to Acme Corp')).toBeVisible();
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**Better alternative: test the full flow in one test with `test.step()`:**
|
|
603
|
+
|
|
604
|
+
**TypeScript:**
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
// tests/onboarding/wizard.spec.ts
|
|
608
|
+
import { test, expect } from '@playwright/test';
|
|
609
|
+
|
|
610
|
+
test('user completes full onboarding wizard', async ({ page }) => {
|
|
611
|
+
await test.step('enter company name', async () => {
|
|
612
|
+
await page.goto('/onboarding');
|
|
613
|
+
await page.getByLabel('Company name').fill('Acme Corp');
|
|
614
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
615
|
+
await expect(page).toHaveURL('/onboarding/step-2');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
await test.step('select plan', async () => {
|
|
619
|
+
await page.getByRole('radio', { name: 'Pro plan' }).check();
|
|
620
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
621
|
+
await expect(page).toHaveURL('/onboarding/step-3');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
await test.step('confirm and complete', async () => {
|
|
625
|
+
await page.getByRole('button', { name: 'Complete setup' }).click();
|
|
626
|
+
await expect(page.getByText('Welcome to Acme Corp')).toBeVisible();
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**JavaScript:**
|
|
632
|
+
|
|
633
|
+
```javascript
|
|
634
|
+
// tests/onboarding/wizard.spec.js
|
|
635
|
+
const { test, expect } = require('@playwright/test');
|
|
636
|
+
|
|
637
|
+
test('user completes full onboarding wizard', async ({ page }) => {
|
|
638
|
+
await test.step('enter company name', async () => {
|
|
639
|
+
await page.goto('/onboarding');
|
|
640
|
+
await page.getByLabel('Company name').fill('Acme Corp');
|
|
641
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
642
|
+
await expect(page).toHaveURL('/onboarding/step-2');
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
await test.step('select plan', async () => {
|
|
646
|
+
await page.getByRole('radio', { name: 'Pro plan' }).check();
|
|
647
|
+
await page.getByRole('button', { name: 'Next' }).click();
|
|
648
|
+
await expect(page).toHaveURL('/onboarding/step-3');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
await test.step('confirm and complete', async () => {
|
|
652
|
+
await page.getByRole('button', { name: 'Complete setup' }).click();
|
|
653
|
+
await expect(page.getByText('Welcome to Acme Corp')).toBeVisible();
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
### Pattern 7: Monorepo Testing
|
|
661
|
+
|
|
662
|
+
**Use when**: A single repository contains multiple apps or packages that each need E2E tests.
|
|
663
|
+
**Avoid when**: Single app repo.
|
|
664
|
+
|
|
665
|
+
**TypeScript:**
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// playwright.config.ts (root of monorepo)
|
|
669
|
+
import { defineConfig } from '@playwright/test';
|
|
670
|
+
|
|
671
|
+
export default defineConfig({
|
|
672
|
+
projects: [
|
|
673
|
+
{
|
|
674
|
+
name: 'web-app',
|
|
675
|
+
testDir: './apps/web/tests',
|
|
676
|
+
use: {
|
|
677
|
+
baseURL: 'http://localhost:3000',
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: 'admin-panel',
|
|
682
|
+
testDir: './apps/admin/tests',
|
|
683
|
+
use: {
|
|
684
|
+
baseURL: 'http://localhost:3001',
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'marketing-site',
|
|
689
|
+
testDir: './apps/marketing/tests',
|
|
690
|
+
use: {
|
|
691
|
+
baseURL: 'http://localhost:4000',
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**JavaScript:**
|
|
699
|
+
|
|
700
|
+
```javascript
|
|
701
|
+
// playwright.config.js (root of monorepo)
|
|
702
|
+
const { defineConfig } = require('@playwright/test');
|
|
703
|
+
|
|
704
|
+
module.exports = defineConfig({
|
|
705
|
+
projects: [
|
|
706
|
+
{
|
|
707
|
+
name: 'web-app',
|
|
708
|
+
testDir: './apps/web/tests',
|
|
709
|
+
use: {
|
|
710
|
+
baseURL: 'http://localhost:3000',
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: 'admin-panel',
|
|
715
|
+
testDir: './apps/admin/tests',
|
|
716
|
+
use: {
|
|
717
|
+
baseURL: 'http://localhost:3001',
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
name: 'marketing-site',
|
|
722
|
+
testDir: './apps/marketing/tests',
|
|
723
|
+
use: {
|
|
724
|
+
baseURL: 'http://localhost:4000',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
});
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**Monorepo directory layout:**
|
|
732
|
+
|
|
733
|
+
```
|
|
734
|
+
monorepo/
|
|
735
|
+
├── apps/
|
|
736
|
+
│ ├── web/
|
|
737
|
+
│ │ ├── src/
|
|
738
|
+
│ │ └── tests/
|
|
739
|
+
│ │ ├── auth/
|
|
740
|
+
│ │ │ └── login.spec.ts
|
|
741
|
+
│ │ └── dashboard/
|
|
742
|
+
│ │ └── widgets.spec.ts
|
|
743
|
+
│ ├── admin/
|
|
744
|
+
│ │ ├── src/
|
|
745
|
+
│ │ └── tests/
|
|
746
|
+
│ │ └── user-management.spec.ts
|
|
747
|
+
│ └── marketing/
|
|
748
|
+
│ ├── src/
|
|
749
|
+
│ └── tests/
|
|
750
|
+
│ └── landing-page.spec.ts
|
|
751
|
+
├── packages/
|
|
752
|
+
│ └── shared-fixtures/
|
|
753
|
+
│ ├── auth.fixture.ts
|
|
754
|
+
│ └── index.ts
|
|
755
|
+
└── playwright.config.ts
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
Run only one app's tests:
|
|
759
|
+
|
|
760
|
+
```bash
|
|
761
|
+
npx playwright test --project=web-app
|
|
762
|
+
npx playwright test --project=admin-panel
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
## Decision Guide
|
|
766
|
+
|
|
767
|
+
```
|
|
768
|
+
How many tests do you have?
|
|
769
|
+
│
|
|
770
|
+
├── < 20 tests (small)
|
|
771
|
+
│ ├── Flat structure: all .spec.ts files in tests/
|
|
772
|
+
│ ├── No subdirectories needed
|
|
773
|
+
│ ├── Tags optional — just run everything
|
|
774
|
+
│ └── fullyParallel: true, single config project
|
|
775
|
+
│
|
|
776
|
+
├── 20–200 tests (medium)
|
|
777
|
+
│ ├── Feature subdirectories: tests/auth/, tests/checkout/
|
|
778
|
+
│ ├── Use @smoke tag for critical-path subset (< 15 min)
|
|
779
|
+
│ ├── Shared fixtures in tests/fixtures/
|
|
780
|
+
│ ├── Page objects in tests/page-objects/ (if you use them)
|
|
781
|
+
│ └── Consider separate projects for different browsers
|
|
782
|
+
│
|
|
783
|
+
└── 200+ tests (large)
|
|
784
|
+
├── Top-level split: tests/e2e/, tests/api/, tests/visual/
|
|
785
|
+
├── Feature subdirectories under each
|
|
786
|
+
├── @smoke, @regression, @slow tags — multiple CI pipelines
|
|
787
|
+
├── Sharding in CI for faster runs
|
|
788
|
+
├── Project-based filtering: smoke project, full project
|
|
789
|
+
└── Shared fixtures as a package in monorepo
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
```
|
|
793
|
+
Should I use test.describe.serial()?
|
|
794
|
+
│
|
|
795
|
+
├── Can each test set up its own state via API/fixture?
|
|
796
|
+
│ └── YES → Do NOT use serial. Use independent parallel tests.
|
|
797
|
+
│
|
|
798
|
+
├── Is this a multi-step wizard where each step changes server state
|
|
799
|
+
│ that cannot be reset between tests?
|
|
800
|
+
│ └── Write it as ONE test with test.step() instead.
|
|
801
|
+
│
|
|
802
|
+
└── Is this genuinely stateful (e.g., database migration sequence)?
|
|
803
|
+
└── Use serial as last resort. Add a comment explaining why.
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
## Anti-Patterns
|
|
807
|
+
|
|
808
|
+
### One giant test file
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
// BAD: tests/all-tests.spec.ts with 80 tests and 2000+ lines
|
|
812
|
+
test.describe('Everything', () => {
|
|
813
|
+
test('login works', async ({ page }) => { /* ... */ });
|
|
814
|
+
test('signup works', async ({ page }) => { /* ... */ });
|
|
815
|
+
test('cart works', async ({ page }) => { /* ... */ });
|
|
816
|
+
// ... 77 more tests
|
|
817
|
+
});
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
**Fix:** Split by feature — one file per feature area, 5–15 tests per file.
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
### Meaningless test names
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
// BAD
|
|
828
|
+
test('test1', async ({ page }) => { /* ... */ });
|
|
829
|
+
test('test2', async ({ page }) => { /* ... */ });
|
|
830
|
+
test('payment test', async ({ page }) => { /* ... */ });
|
|
831
|
+
test('it works', async ({ page }) => { /* ... */ });
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
**Fix:** Describe behavior. When a test fails, the name should tell you what broke.
|
|
835
|
+
|
|
836
|
+
```typescript
|
|
837
|
+
// GOOD
|
|
838
|
+
test('should reject expired credit card', async ({ page }) => { /* ... */ });
|
|
839
|
+
test('user can update shipping address during checkout', async ({ page }) => { /* ... */ });
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
### Deep describe nesting
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
// BAD: 3+ levels deep — hard to read, hard to find tests in reports
|
|
848
|
+
test.describe('Auth', () => {
|
|
849
|
+
test.describe('Login', () => {
|
|
850
|
+
test.describe('With MFA', () => {
|
|
851
|
+
test.describe('SMS', () => {
|
|
852
|
+
test('should send code', async ({ page }) => { /* ... */ });
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
**Fix:** Max 2 levels. Split deeper nesting into separate files.
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
// GOOD: tests/auth/login-mfa-sms.spec.ts
|
|
863
|
+
test.describe('Login with SMS MFA', () => {
|
|
864
|
+
test('should send verification code', async ({ page }) => { /* ... */ });
|
|
865
|
+
test('should reject invalid code', async ({ page }) => { /* ... */ });
|
|
866
|
+
});
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
### Using `test.describe.serial()` as the default
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
// BAD: serial for no reason — kills parallelism, creates hidden dependencies
|
|
875
|
+
test.describe.serial('User Profile', () => {
|
|
876
|
+
test('should display profile page', async ({ page }) => { /* ... */ });
|
|
877
|
+
test('should update display name', async ({ page }) => { /* ... */ });
|
|
878
|
+
test('should upload avatar', async ({ page }) => { /* ... */ });
|
|
879
|
+
});
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**Fix:** Each test should be independent. Use `beforeEach` or fixtures to set up state.
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
// GOOD
|
|
886
|
+
test.describe('User Profile', () => {
|
|
887
|
+
test.beforeEach(async ({ page }) => {
|
|
888
|
+
await page.goto('/profile');
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test('should display profile page', async ({ page }) => { /* ... */ });
|
|
892
|
+
test('should update display name', async ({ page }) => { /* ... */ });
|
|
893
|
+
test('should upload avatar', async ({ page }) => { /* ... */ });
|
|
894
|
+
});
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
### Relying on test execution order
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
// BAD: test 2 depends on test 1 creating data
|
|
903
|
+
test('should create a product', async ({ page }) => {
|
|
904
|
+
// creates "Widget X" in the database
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test('should edit the product', async ({ page }) => {
|
|
908
|
+
// assumes "Widget X" exists — breaks if run alone or in parallel
|
|
909
|
+
});
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
**Fix:** Each test creates its own data via API calls or fixtures.
|
|
913
|
+
|
|
914
|
+
```typescript
|
|
915
|
+
// GOOD
|
|
916
|
+
test('should edit a product', async ({ page, request }) => {
|
|
917
|
+
// Set up test data independently
|
|
918
|
+
const response = await request.post('/api/products', {
|
|
919
|
+
data: { name: 'Widget X', price: 9.99 },
|
|
920
|
+
});
|
|
921
|
+
const product = await response.json();
|
|
922
|
+
|
|
923
|
+
await page.goto(`/products/${product.id}/edit`);
|
|
924
|
+
await page.getByLabel('Name').fill('Widget X Updated');
|
|
925
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
926
|
+
await expect(page.getByText('Widget X Updated')).toBeVisible();
|
|
927
|
+
});
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
## Troubleshooting
|
|
931
|
+
|
|
932
|
+
| Problem | Cause | Fix |
|
|
933
|
+
|---|---|---|
|
|
934
|
+
| Tests pass alone but fail together | Shared state between tests (cookies, DB rows, global variables) | Isolate each test — use fixtures for setup/teardown |
|
|
935
|
+
| `--grep @smoke` matches nothing | Tag not in test title string | Verify the tag appears literally in `test('... @smoke', ...)` |
|
|
936
|
+
| Serial tests cascade-fail | One test fails, all subsequent tests skip | Rewrite as independent tests or use `test.step()` in a single test |
|
|
937
|
+
| Tests run slower than expected | `fullyParallel` not set | Add `fullyParallel: true` in `playwright.config` |
|
|
938
|
+
| Wrong tests run in CI | `testMatch` or `testDir` misconfigured | Check `playwright.config` — print resolved config with `npx playwright test --list` |
|
|
939
|
+
| Monorepo tests pick up wrong files | Default `testDir` is `.` | Set explicit `testDir` per project |
|
|
940
|
+
|
|
941
|
+
## Related
|
|
942
|
+
|
|
943
|
+
- [core/configuration.md](foundations/configuration.md) — `testMatch`, `testDir`, `fullyParallel`, `workers`
|
|
944
|
+
- [core/fixtures-and-hooks.md](foundations/fixtures-and-hooks.md) — shared setup via fixtures instead of `beforeAll`
|
|
945
|
+
- [core/test-architecture.md](decisions/test-architecture.md) — when to write E2E vs API vs component tests
|
|
946
|
+
- [ci/parallel-and-sharding.md](infrastructure/parallel-and-sharding.md) — CI sharding for large suites
|
|
947
|
+
- [ci/projects-and-dependencies.md](infrastructure/projects-and-dependencies.md) — multi-project config
|