@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,937 @@
|
|
|
1
|
+
# Page Objects vs Fixtures vs Helpers
|
|
2
|
+
|
|
3
|
+
> **When to use**: When deciding how to organize reusable test code in a Playwright project.
|
|
4
|
+
> **Prerequisites**: [core/page-object-model.md](../core/page-object-model.md), [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md)
|
|
5
|
+
|
|
6
|
+
## Quick Answer
|
|
7
|
+
|
|
8
|
+
**Use all three.** Most projects benefit from a hybrid approach:
|
|
9
|
+
|
|
10
|
+
- **Page objects** for UI interaction (pages and components with 5+ interactions)
|
|
11
|
+
- **Custom fixtures** for test infrastructure (auth state, database, API clients, anything with lifecycle)
|
|
12
|
+
- **Helper functions** for stateless utilities (generate data, format values, simple waits)
|
|
13
|
+
|
|
14
|
+
If you are only going to use one pattern, use **custom fixtures** -- they handle setup/teardown, compose well, and Playwright is built around them. But you should use all three.
|
|
15
|
+
|
|
16
|
+
## Comparison Table
|
|
17
|
+
|
|
18
|
+
| Aspect | Page Objects | Custom Fixtures | Helper Functions |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| **Primary purpose** | Encapsulate UI interactions for a page or component | Provide shared resources with setup/teardown lifecycle | Stateless utility operations |
|
|
21
|
+
| **Setup/teardown** | Manual (constructor / methods) | Built-in (`use()` callback with automatic teardown) | None (stateless) |
|
|
22
|
+
| **Type safety** | Full class-based typing | Full typing via `test.extend<T>()` generics | Full typing via function signatures |
|
|
23
|
+
| **Composability** | Compose via constructor injection or fixture wiring | Compose by depending on other fixtures | Compose by calling other functions |
|
|
24
|
+
| **Learning curve** | Low (plain classes) | Medium (Playwright-specific `test.extend` API) | Lowest (plain functions) |
|
|
25
|
+
| **Test readability** | High -- `loginPage.signIn(user)` reads like intent | High -- `{ adminPage }` declares dependencies | Medium -- utility calls mixed with test logic |
|
|
26
|
+
| **Debugging experience** | Good -- step into class methods | Best -- fixtures appear in traces and reports | Good -- step into functions |
|
|
27
|
+
| **IDE support** | Excellent -- autocomplete on class methods | Excellent -- autocomplete on fixture parameters | Excellent -- autocomplete on function params |
|
|
28
|
+
| **State management** | Holds page reference and locators as instance state | Manages resource lifecycle (create, provide, cleanup) | No state (pure functions) |
|
|
29
|
+
| **Best for** | Pages with many interactions reused across files | Resources that need setup AND teardown | Simple, reusable logic with no side effects |
|
|
30
|
+
|
|
31
|
+
## Decision Flowchart
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
What kind of reusable code are you writing?
|
|
35
|
+
|
|
|
36
|
+
+-- Does it interact with a page or component in the browser?
|
|
37
|
+
| |
|
|
38
|
+
| +-- Does the page/component have 5+ interactions (fill, click, navigate, assert)?
|
|
39
|
+
| | |
|
|
40
|
+
| | +-- YES: Is it used in 3+ test files?
|
|
41
|
+
| | | +-- YES --> PAGE OBJECT
|
|
42
|
+
| | | +-- NO --> Inline the interactions or use a small helper function
|
|
43
|
+
| | |
|
|
44
|
+
| | +-- NO (< 5 interactions) --> HELPER FUNCTION
|
|
45
|
+
| |
|
|
46
|
+
| +-- Does it need setup before the test AND cleanup after?
|
|
47
|
+
| +-- YES --> CUSTOM FIXTURE (wraps a page object or resource)
|
|
48
|
+
| +-- NO --> PAGE OBJECT method or HELPER FUNCTION
|
|
49
|
+
|
|
|
50
|
+
+-- Does it manage a resource with lifecycle (create/destroy)?
|
|
51
|
+
| |
|
|
52
|
+
| +-- Examples: auth state, database connection, API client,
|
|
53
|
+
| | test user, browser context, temporary files
|
|
54
|
+
| |
|
|
55
|
+
| +-- YES --> CUSTOM FIXTURE (always)
|
|
56
|
+
|
|
|
57
|
+
+-- Is it a stateless utility? (no browser, no side effects)
|
|
58
|
+
| |
|
|
59
|
+
| +-- Examples: generate random email, format date, build URL,
|
|
60
|
+
| | create test data object, parse response
|
|
61
|
+
| |
|
|
62
|
+
| +-- YES --> HELPER FUNCTION
|
|
63
|
+
|
|
|
64
|
+
+-- Not sure?
|
|
65
|
+
+-- Start with a HELPER FUNCTION
|
|
66
|
+
+-- Promote to PAGE OBJECT when interactions grow
|
|
67
|
+
+-- Promote to FIXTURE when lifecycle management is needed
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## When to Use Page Objects
|
|
71
|
+
|
|
72
|
+
**Best for**: Pages or components with 5+ interactions that appear in 3+ test files. Encapsulate navigation, form filling, action sequences, and element locators into a single class that reads like user intent.
|
|
73
|
+
|
|
74
|
+
**TypeScript**
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// page-objects/checkout.page.ts
|
|
78
|
+
import { type Page, type Locator, expect } from '@playwright/test';
|
|
79
|
+
|
|
80
|
+
export class CheckoutPage {
|
|
81
|
+
readonly page: Page;
|
|
82
|
+
readonly cardNumber: Locator;
|
|
83
|
+
readonly expiry: Locator;
|
|
84
|
+
readonly cvc: Locator;
|
|
85
|
+
readonly payButton: Locator;
|
|
86
|
+
readonly promoCodeInput: Locator;
|
|
87
|
+
readonly orderTotal: Locator;
|
|
88
|
+
|
|
89
|
+
constructor(page: Page) {
|
|
90
|
+
this.page = page;
|
|
91
|
+
this.cardNumber = page.getByLabel('Card number');
|
|
92
|
+
this.expiry = page.getByLabel('Expiry');
|
|
93
|
+
this.cvc = page.getByLabel('CVC');
|
|
94
|
+
this.payButton = page.getByRole('button', { name: 'Pay now' });
|
|
95
|
+
this.promoCodeInput = page.getByLabel('Promo code');
|
|
96
|
+
this.orderTotal = page.getByTestId('order-total');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async goto() {
|
|
100
|
+
await this.page.goto('/checkout');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async fillPaymentDetails(card: { number: string; expiry: string; cvc: string }) {
|
|
104
|
+
await this.cardNumber.fill(card.number);
|
|
105
|
+
await this.expiry.fill(card.expiry);
|
|
106
|
+
await this.cvc.fill(card.cvc);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async pay() {
|
|
110
|
+
await this.payButton.click();
|
|
111
|
+
await this.page.waitForURL('**/confirmation');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async applyPromoCode(code: string) {
|
|
115
|
+
await this.promoCodeInput.fill(code);
|
|
116
|
+
await this.page.getByRole('button', { name: 'Apply' }).click();
|
|
117
|
+
await expect(this.page.getByText('Code applied')).toBeVisible();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async expectTotal(amount: string) {
|
|
121
|
+
await expect(this.orderTotal).toHaveText(amount);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// tests/checkout/payment.spec.ts
|
|
128
|
+
import { test, expect } from '@playwright/test';
|
|
129
|
+
import { CheckoutPage } from '../page-objects/checkout.page';
|
|
130
|
+
|
|
131
|
+
test('should complete checkout with valid card', async ({ page }) => {
|
|
132
|
+
const checkout = new CheckoutPage(page);
|
|
133
|
+
await checkout.goto();
|
|
134
|
+
await checkout.fillPaymentDetails({
|
|
135
|
+
number: '4242424242424242',
|
|
136
|
+
expiry: '12/28',
|
|
137
|
+
cvc: '123',
|
|
138
|
+
});
|
|
139
|
+
await checkout.pay();
|
|
140
|
+
await expect(page.getByText('Payment successful')).toBeVisible();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('should apply promo code and update total', async ({ page }) => {
|
|
144
|
+
const checkout = new CheckoutPage(page);
|
|
145
|
+
await checkout.goto();
|
|
146
|
+
await checkout.applyPromoCode('SAVE20');
|
|
147
|
+
await checkout.expectTotal('$79.99');
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**JavaScript**
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
// page-objects/checkout.page.js
|
|
155
|
+
const { expect } = require('@playwright/test');
|
|
156
|
+
|
|
157
|
+
class CheckoutPage {
|
|
158
|
+
constructor(page) {
|
|
159
|
+
this.page = page;
|
|
160
|
+
this.cardNumber = page.getByLabel('Card number');
|
|
161
|
+
this.expiry = page.getByLabel('Expiry');
|
|
162
|
+
this.cvc = page.getByLabel('CVC');
|
|
163
|
+
this.payButton = page.getByRole('button', { name: 'Pay now' });
|
|
164
|
+
this.promoCodeInput = page.getByLabel('Promo code');
|
|
165
|
+
this.orderTotal = page.getByTestId('order-total');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async goto() {
|
|
169
|
+
await this.page.goto('/checkout');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async fillPaymentDetails(card) {
|
|
173
|
+
await this.cardNumber.fill(card.number);
|
|
174
|
+
await this.expiry.fill(card.expiry);
|
|
175
|
+
await this.cvc.fill(card.cvc);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async pay() {
|
|
179
|
+
await this.payButton.click();
|
|
180
|
+
await this.page.waitForURL('**/confirmation');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async applyPromoCode(code) {
|
|
184
|
+
await this.promoCodeInput.fill(code);
|
|
185
|
+
await this.page.getByRole('button', { name: 'Apply' }).click();
|
|
186
|
+
await expect(this.page.getByText('Code applied')).toBeVisible();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async expectTotal(amount) {
|
|
190
|
+
await expect(this.orderTotal).toHaveText(amount);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { CheckoutPage };
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
// tests/checkout/payment.spec.js
|
|
199
|
+
const { test, expect } = require('@playwright/test');
|
|
200
|
+
const { CheckoutPage } = require('../page-objects/checkout.page');
|
|
201
|
+
|
|
202
|
+
test('should complete checkout with valid card', async ({ page }) => {
|
|
203
|
+
const checkout = new CheckoutPage(page);
|
|
204
|
+
await checkout.goto();
|
|
205
|
+
await checkout.fillPaymentDetails({
|
|
206
|
+
number: '4242424242424242',
|
|
207
|
+
expiry: '12/28',
|
|
208
|
+
cvc: '123',
|
|
209
|
+
});
|
|
210
|
+
await checkout.pay();
|
|
211
|
+
await expect(page.getByText('Payment successful')).toBeVisible();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('should apply promo code and update total', async ({ page }) => {
|
|
215
|
+
const checkout = new CheckoutPage(page);
|
|
216
|
+
await checkout.goto();
|
|
217
|
+
await checkout.applyPromoCode('SAVE20');
|
|
218
|
+
await checkout.expectTotal('$79.99');
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Key principles for page objects:**
|
|
223
|
+
- One class per logical page or component, not per URL
|
|
224
|
+
- Constructor takes `Page` (and optionally other page objects for composition)
|
|
225
|
+
- Locators are `readonly` properties assigned in the constructor
|
|
226
|
+
- Methods represent user-intent actions (`signIn`, `addToCart`), not low-level clicks
|
|
227
|
+
- Navigation methods (`goto`) belong on the page object
|
|
228
|
+
- Assertions can live on the page object when they verify page-specific state
|
|
229
|
+
|
|
230
|
+
## When to Use Custom Fixtures
|
|
231
|
+
|
|
232
|
+
**Best for**: Shared resources that need setup before the test and teardown after -- auth state, database connections, API clients, test users, seeded data, custom browser contexts. Anything with a lifecycle belongs in a fixture.
|
|
233
|
+
|
|
234
|
+
**TypeScript**
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// fixtures/base.fixture.ts
|
|
238
|
+
import { test as base, expect } from '@playwright/test';
|
|
239
|
+
import { CheckoutPage } from '../page-objects/checkout.page';
|
|
240
|
+
import { DashboardPage } from '../page-objects/dashboard.page';
|
|
241
|
+
|
|
242
|
+
// Define types for all custom fixtures
|
|
243
|
+
type MyFixtures = {
|
|
244
|
+
checkoutPage: CheckoutPage;
|
|
245
|
+
dashboardPage: DashboardPage;
|
|
246
|
+
authenticatedPage: ReturnType<typeof base['page']> extends Promise<infer P> ? P : never;
|
|
247
|
+
testUser: { email: string; password: string; id: string };
|
|
248
|
+
apiClient: { get: (path: string) => Promise<Response>; post: (path: string, data: unknown) => Promise<Response> };
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const test = base.extend<MyFixtures>({
|
|
252
|
+
// Fixture that wraps a page object — created fresh for each test
|
|
253
|
+
checkoutPage: async ({ page }, use) => {
|
|
254
|
+
const checkoutPage = new CheckoutPage(page);
|
|
255
|
+
await use(checkoutPage);
|
|
256
|
+
// No teardown needed — page closes automatically
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
dashboardPage: async ({ page }, use) => {
|
|
260
|
+
const dashboardPage = new DashboardPage(page);
|
|
261
|
+
await use(dashboardPage);
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
// Fixture with real setup AND teardown — creates a test user, cleans up after
|
|
265
|
+
testUser: async ({ request }, use) => {
|
|
266
|
+
// Setup: create a user via API
|
|
267
|
+
const response = await request.post('/api/test/users', {
|
|
268
|
+
data: {
|
|
269
|
+
email: `test-${Date.now()}@example.com`,
|
|
270
|
+
password: 'TestPass123!',
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
const user = await response.json();
|
|
274
|
+
|
|
275
|
+
// Provide the user to the test
|
|
276
|
+
await use(user);
|
|
277
|
+
|
|
278
|
+
// Teardown: delete the user (runs even if test fails)
|
|
279
|
+
await request.delete(`/api/test/users/${user.id}`);
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
// Fixture that depends on another fixture
|
|
283
|
+
authenticatedPage: async ({ page, testUser }, use) => {
|
|
284
|
+
await page.goto('/login');
|
|
285
|
+
await page.getByLabel('Email').fill(testUser.email);
|
|
286
|
+
await page.getByLabel('Password').fill(testUser.password);
|
|
287
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
288
|
+
await expect(page).toHaveURL('/dashboard');
|
|
289
|
+
|
|
290
|
+
await use(page);
|
|
291
|
+
// Teardown: logout (optional — browser context is destroyed anyway)
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
// API client fixture with lifecycle
|
|
295
|
+
apiClient: async ({ request }, use) => {
|
|
296
|
+
const client = {
|
|
297
|
+
get: (path: string) => request.get(`/api${path}`),
|
|
298
|
+
post: (path: string, data: unknown) => request.post(`/api${path}`, { data }),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
await use(client);
|
|
302
|
+
// Teardown runs automatically when request fixture tears down
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
export { expect } from '@playwright/test';
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// tests/dashboard/widgets.spec.ts
|
|
311
|
+
import { test, expect } from '../../fixtures/base.fixture';
|
|
312
|
+
|
|
313
|
+
// testUser and authenticatedPage are set up automatically by the fixture system
|
|
314
|
+
test('authenticated user sees dashboard widgets', async ({ authenticatedPage }) => {
|
|
315
|
+
await expect(authenticatedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
316
|
+
await expect(authenticatedPage.getByTestId('revenue-widget')).toBeVisible();
|
|
317
|
+
await expect(authenticatedPage.getByTestId('orders-widget')).toBeVisible();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('user can refresh dashboard data', async ({ authenticatedPage, dashboardPage }) => {
|
|
321
|
+
await authenticatedPage.getByRole('button', { name: 'Refresh' }).click();
|
|
322
|
+
await expect(authenticatedPage.getByText('Updated just now')).toBeVisible();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// testUser fixture creates AND cleans up the user automatically
|
|
326
|
+
test('new user sees onboarding prompt', async ({ authenticatedPage, testUser }) => {
|
|
327
|
+
await expect(authenticatedPage.getByText(`Welcome, ${testUser.email}`)).toBeVisible();
|
|
328
|
+
await expect(authenticatedPage.getByRole('dialog', { name: 'Get started' })).toBeVisible();
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**JavaScript**
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
// fixtures/base.fixture.js
|
|
336
|
+
const { test: base, expect } = require('@playwright/test');
|
|
337
|
+
const { CheckoutPage } = require('../page-objects/checkout.page');
|
|
338
|
+
const { DashboardPage } = require('../page-objects/dashboard.page');
|
|
339
|
+
|
|
340
|
+
const test = base.extend({
|
|
341
|
+
checkoutPage: async ({ page }, use) => {
|
|
342
|
+
const checkoutPage = new CheckoutPage(page);
|
|
343
|
+
await use(checkoutPage);
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
dashboardPage: async ({ page }, use) => {
|
|
347
|
+
const dashboardPage = new DashboardPage(page);
|
|
348
|
+
await use(dashboardPage);
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
testUser: async ({ request }, use) => {
|
|
352
|
+
const response = await request.post('/api/test/users', {
|
|
353
|
+
data: {
|
|
354
|
+
email: `test-${Date.now()}@example.com`,
|
|
355
|
+
password: 'TestPass123!',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
const user = await response.json();
|
|
359
|
+
|
|
360
|
+
await use(user);
|
|
361
|
+
|
|
362
|
+
await request.delete(`/api/test/users/${user.id}`);
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
authenticatedPage: async ({ page, testUser }, use) => {
|
|
366
|
+
await page.goto('/login');
|
|
367
|
+
await page.getByLabel('Email').fill(testUser.email);
|
|
368
|
+
await page.getByLabel('Password').fill(testUser.password);
|
|
369
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
370
|
+
await expect(page).toHaveURL('/dashboard');
|
|
371
|
+
|
|
372
|
+
await use(page);
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
apiClient: async ({ request }, use) => {
|
|
376
|
+
const client = {
|
|
377
|
+
get: (path) => request.get(`/api${path}`),
|
|
378
|
+
post: (path, data) => request.post(`/api${path}`, { data }),
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
await use(client);
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
module.exports = { test, expect };
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
// tests/dashboard/widgets.spec.js
|
|
390
|
+
const { test, expect } = require('../../fixtures/base.fixture');
|
|
391
|
+
|
|
392
|
+
test('authenticated user sees dashboard widgets', async ({ authenticatedPage }) => {
|
|
393
|
+
await expect(authenticatedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
394
|
+
await expect(authenticatedPage.getByTestId('revenue-widget')).toBeVisible();
|
|
395
|
+
await expect(authenticatedPage.getByTestId('orders-widget')).toBeVisible();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('new user sees onboarding prompt', async ({ authenticatedPage, testUser }) => {
|
|
399
|
+
await expect(authenticatedPage.getByText(`Welcome, ${testUser.email}`)).toBeVisible();
|
|
400
|
+
await expect(authenticatedPage.getByRole('dialog', { name: 'Get started' })).toBeVisible();
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Key principles for custom fixtures:**
|
|
405
|
+
- Use `test.extend()` to define fixtures -- never use module-level variables
|
|
406
|
+
- The `use()` callback separates setup (before) from teardown (after)
|
|
407
|
+
- Teardown runs even if the test fails or crashes -- this is the main advantage over `afterEach`
|
|
408
|
+
- Fixtures compose: one fixture can depend on another (`authenticatedPage` depends on `testUser`)
|
|
409
|
+
- Fixtures are lazy: they are only created when a test actually requests them
|
|
410
|
+
- Wrap page objects in fixtures to get automatic lifecycle management
|
|
411
|
+
- Fixtures appear in Playwright traces, making debugging easier
|
|
412
|
+
|
|
413
|
+
## When to Use Helper Functions
|
|
414
|
+
|
|
415
|
+
**Best for**: Stateless utility operations -- generating test data, formatting values, building URLs, parsing responses, creating simple wait conditions. Pure functions with no side effects and no browser interaction.
|
|
416
|
+
|
|
417
|
+
**TypeScript**
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// helpers/test-data.ts
|
|
421
|
+
import { randomUUID } from 'node:crypto';
|
|
422
|
+
|
|
423
|
+
export function generateEmail(prefix = 'test'): string {
|
|
424
|
+
return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}@example.com`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function generateUser(overrides: Partial<TestUser> = {}): TestUser {
|
|
428
|
+
return {
|
|
429
|
+
email: generateEmail(),
|
|
430
|
+
password: 'TestPass123!',
|
|
431
|
+
firstName: 'Test',
|
|
432
|
+
lastName: 'User',
|
|
433
|
+
...overrides,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
interface TestUser {
|
|
438
|
+
email: string;
|
|
439
|
+
password: string;
|
|
440
|
+
firstName: string;
|
|
441
|
+
lastName: string;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function formatCurrency(cents: number): string {
|
|
445
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function buildApiUrl(path: string, params: Record<string, string> = {}): string {
|
|
449
|
+
const url = new URL(path, 'http://localhost:3000');
|
|
450
|
+
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
|
|
451
|
+
return url.toString();
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// helpers/assertions.ts
|
|
457
|
+
import { type Page, expect } from '@playwright/test';
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Wait for all network requests to settle.
|
|
461
|
+
* Use sparingly — prefer specific assertions over blanket network waits.
|
|
462
|
+
*/
|
|
463
|
+
export async function waitForNetworkIdle(page: Page, timeout = 5000): Promise<void> {
|
|
464
|
+
await page.waitForLoadState('networkidle', { timeout });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Assert that a toast notification appears and then disappears.
|
|
469
|
+
*/
|
|
470
|
+
export async function expectToast(page: Page, message: string): Promise<void> {
|
|
471
|
+
const toast = page.getByRole('status').filter({ hasText: message });
|
|
472
|
+
await expect(toast).toBeVisible();
|
|
473
|
+
await expect(toast).toBeHidden({ timeout: 10000 });
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
// tests/settings/profile.spec.ts
|
|
479
|
+
import { test, expect } from '@playwright/test';
|
|
480
|
+
import { generateEmail, generateUser } from '../../helpers/test-data';
|
|
481
|
+
import { expectToast } from '../../helpers/assertions';
|
|
482
|
+
|
|
483
|
+
test('should update user email', async ({ page }) => {
|
|
484
|
+
const newEmail = generateEmail('updated');
|
|
485
|
+
|
|
486
|
+
await page.goto('/settings/profile');
|
|
487
|
+
await page.getByLabel('Email').fill(newEmail);
|
|
488
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
489
|
+
|
|
490
|
+
await expectToast(page, 'Profile updated');
|
|
491
|
+
await expect(page.getByLabel('Email')).toHaveValue(newEmail);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test('should display user info', async ({ page }) => {
|
|
495
|
+
const user = generateUser({ firstName: 'Jane', lastName: 'Doe' });
|
|
496
|
+
// ... use user data in test setup
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**JavaScript**
|
|
501
|
+
|
|
502
|
+
```javascript
|
|
503
|
+
// helpers/test-data.js
|
|
504
|
+
const { randomUUID } = require('node:crypto');
|
|
505
|
+
|
|
506
|
+
function generateEmail(prefix = 'test') {
|
|
507
|
+
return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}@example.com`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function generateUser(overrides = {}) {
|
|
511
|
+
return {
|
|
512
|
+
email: generateEmail(),
|
|
513
|
+
password: 'TestPass123!',
|
|
514
|
+
firstName: 'Test',
|
|
515
|
+
lastName: 'User',
|
|
516
|
+
...overrides,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function formatCurrency(cents) {
|
|
521
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function buildApiUrl(path, params = {}) {
|
|
525
|
+
const url = new URL(path, 'http://localhost:3000');
|
|
526
|
+
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
|
|
527
|
+
return url.toString();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
module.exports = { generateEmail, generateUser, formatCurrency, buildApiUrl };
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
// helpers/assertions.js
|
|
535
|
+
const { expect } = require('@playwright/test');
|
|
536
|
+
|
|
537
|
+
async function waitForNetworkIdle(page, timeout = 5000) {
|
|
538
|
+
await page.waitForLoadState('networkidle', { timeout });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function expectToast(page, message) {
|
|
542
|
+
const toast = page.getByRole('status').filter({ hasText: message });
|
|
543
|
+
await expect(toast).toBeVisible();
|
|
544
|
+
await expect(toast).toBeHidden({ timeout: 10000 });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
module.exports = { waitForNetworkIdle, expectToast };
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
```javascript
|
|
551
|
+
// tests/settings/profile.spec.js
|
|
552
|
+
const { test, expect } = require('@playwright/test');
|
|
553
|
+
const { generateEmail, generateUser } = require('../../helpers/test-data');
|
|
554
|
+
const { expectToast } = require('../../helpers/assertions');
|
|
555
|
+
|
|
556
|
+
test('should update user email', async ({ page }) => {
|
|
557
|
+
const newEmail = generateEmail('updated');
|
|
558
|
+
|
|
559
|
+
await page.goto('/settings/profile');
|
|
560
|
+
await page.getByLabel('Email').fill(newEmail);
|
|
561
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
562
|
+
|
|
563
|
+
await expectToast(page, 'Profile updated');
|
|
564
|
+
await expect(page.getByLabel('Email')).toHaveValue(newEmail);
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Key principles for helper functions:**
|
|
569
|
+
- Pure functions with no side effects -- given the same input, always the same output
|
|
570
|
+
- No browser state -- helpers that need `page` should take it as a parameter, not store it
|
|
571
|
+
- If a helper grows to need setup/teardown, promote it to a fixture
|
|
572
|
+
- If a helper grows to encapsulate many page interactions, promote it to a page object
|
|
573
|
+
- Keep helpers small and focused -- one function, one job
|
|
574
|
+
|
|
575
|
+
## Combining Approaches
|
|
576
|
+
|
|
577
|
+
The recommended hybrid approach for real projects. This is what a well-organized Playwright project looks like.
|
|
578
|
+
|
|
579
|
+
**Project structure:**
|
|
580
|
+
|
|
581
|
+
```
|
|
582
|
+
tests/
|
|
583
|
+
+-- fixtures/
|
|
584
|
+
| +-- auth.fixture.ts # Custom fixtures: authenticatedPage, testUser, adminUser
|
|
585
|
+
| +-- db.fixture.ts # Custom fixtures: database, seedData
|
|
586
|
+
| +-- base.fixture.ts # Combines all fixtures, re-exports test and expect
|
|
587
|
+
+-- page-objects/
|
|
588
|
+
| +-- login.page.ts # Page object: LoginPage
|
|
589
|
+
| +-- dashboard.page.ts # Page object: DashboardPage
|
|
590
|
+
| +-- checkout.page.ts # Page object: CheckoutPage
|
|
591
|
+
| +-- components/
|
|
592
|
+
| +-- data-table.component.ts # Reusable component object
|
|
593
|
+
| +-- modal.component.ts
|
|
594
|
+
+-- helpers/
|
|
595
|
+
| +-- test-data.ts # Helper: generateEmail, generateUser, generateOrder
|
|
596
|
+
| +-- assertions.ts # Helper: expectToast, expectTableRowCount
|
|
597
|
+
| +-- api.ts # Helper: createProductViaApi, deleteAllTestData
|
|
598
|
+
+-- e2e/
|
|
599
|
+
| +-- auth/
|
|
600
|
+
| | +-- login.spec.ts
|
|
601
|
+
| | +-- signup.spec.ts
|
|
602
|
+
| +-- checkout/
|
|
603
|
+
| | +-- cart.spec.ts
|
|
604
|
+
| | +-- payment.spec.ts
|
|
605
|
+
| +-- dashboard/
|
|
606
|
+
| +-- widgets.spec.ts
|
|
607
|
+
playwright.config.ts
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**How the three approaches work together:**
|
|
611
|
+
|
|
612
|
+
**TypeScript**
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
// fixtures/base.fixture.ts — the glue layer
|
|
616
|
+
import { test as base } from '@playwright/test';
|
|
617
|
+
import { LoginPage } from '../page-objects/login.page';
|
|
618
|
+
import { DashboardPage } from '../page-objects/dashboard.page';
|
|
619
|
+
import { CheckoutPage } from '../page-objects/checkout.page';
|
|
620
|
+
import { generateUser } from '../helpers/test-data';
|
|
621
|
+
|
|
622
|
+
type Fixtures = {
|
|
623
|
+
loginPage: LoginPage;
|
|
624
|
+
dashboardPage: DashboardPage;
|
|
625
|
+
checkoutPage: CheckoutPage;
|
|
626
|
+
testUser: { email: string; password: string; id: string };
|
|
627
|
+
authenticatedPage: import('@playwright/test').Page;
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
export const test = base.extend<Fixtures>({
|
|
631
|
+
// Page objects wrapped in fixtures for convenience
|
|
632
|
+
loginPage: async ({ page }, use) => {
|
|
633
|
+
await use(new LoginPage(page));
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
dashboardPage: async ({ page }, use) => {
|
|
637
|
+
await use(new DashboardPage(page));
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
checkoutPage: async ({ page }, use) => {
|
|
641
|
+
await use(new CheckoutPage(page));
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
// Resource fixture with lifecycle — uses helpers for data generation
|
|
645
|
+
testUser: async ({ request }, use) => {
|
|
646
|
+
const userData = generateUser();
|
|
647
|
+
const response = await request.post('/api/test/users', { data: userData });
|
|
648
|
+
const user = await response.json();
|
|
649
|
+
await use(user);
|
|
650
|
+
await request.delete(`/api/test/users/${user.id}`);
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
// Composed fixture: depends on page object + resource fixture
|
|
654
|
+
authenticatedPage: async ({ loginPage, testUser }, use) => {
|
|
655
|
+
await loginPage.goto();
|
|
656
|
+
await loginPage.signIn(testUser.email, testUser.password);
|
|
657
|
+
await use(loginPage.page);
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
export { expect } from '@playwright/test';
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
// tests/e2e/checkout/payment.spec.ts — clean test using all three
|
|
666
|
+
import { test, expect } from '../../../fixtures/base.fixture';
|
|
667
|
+
import { generateEmail } from '../../../helpers/test-data';
|
|
668
|
+
import { expectToast } from '../../../helpers/assertions';
|
|
669
|
+
|
|
670
|
+
test.describe('Checkout Payment', () => {
|
|
671
|
+
test('authenticated user completes purchase', async ({ authenticatedPage, checkoutPage }) => {
|
|
672
|
+
// authenticatedPage: fixture handled login + user creation + cleanup
|
|
673
|
+
// checkoutPage: page object encapsulates checkout UI interactions
|
|
674
|
+
await checkoutPage.goto();
|
|
675
|
+
await checkoutPage.fillPaymentDetails({
|
|
676
|
+
number: '4242424242424242',
|
|
677
|
+
expiry: '12/28',
|
|
678
|
+
cvc: '123',
|
|
679
|
+
});
|
|
680
|
+
await checkoutPage.pay();
|
|
681
|
+
await expectToast(authenticatedPage, 'Payment successful'); // helper for common assertion
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test('guest user enters email at checkout', async ({ checkoutPage }) => {
|
|
685
|
+
const email = generateEmail('guest'); // helper for test data
|
|
686
|
+
await checkoutPage.goto();
|
|
687
|
+
await checkoutPage.page.getByLabel('Guest email').fill(email);
|
|
688
|
+
await checkoutPage.fillPaymentDetails({
|
|
689
|
+
number: '4242424242424242',
|
|
690
|
+
expiry: '12/28',
|
|
691
|
+
cvc: '123',
|
|
692
|
+
});
|
|
693
|
+
await checkoutPage.pay();
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**JavaScript**
|
|
699
|
+
|
|
700
|
+
```javascript
|
|
701
|
+
// fixtures/base.fixture.js
|
|
702
|
+
const { test: base } = require('@playwright/test');
|
|
703
|
+
const { LoginPage } = require('../page-objects/login.page');
|
|
704
|
+
const { DashboardPage } = require('../page-objects/dashboard.page');
|
|
705
|
+
const { CheckoutPage } = require('../page-objects/checkout.page');
|
|
706
|
+
const { generateUser } = require('../helpers/test-data');
|
|
707
|
+
|
|
708
|
+
const test = base.extend({
|
|
709
|
+
loginPage: async ({ page }, use) => {
|
|
710
|
+
await use(new LoginPage(page));
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
dashboardPage: async ({ page }, use) => {
|
|
714
|
+
await use(new DashboardPage(page));
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
checkoutPage: async ({ page }, use) => {
|
|
718
|
+
await use(new CheckoutPage(page));
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
testUser: async ({ request }, use) => {
|
|
722
|
+
const userData = generateUser();
|
|
723
|
+
const response = await request.post('/api/test/users', { data: userData });
|
|
724
|
+
const user = await response.json();
|
|
725
|
+
await use(user);
|
|
726
|
+
await request.delete(`/api/test/users/${user.id}`);
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
authenticatedPage: async ({ loginPage, testUser }, use) => {
|
|
730
|
+
await loginPage.goto();
|
|
731
|
+
await loginPage.signIn(testUser.email, testUser.password);
|
|
732
|
+
await use(loginPage.page);
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
module.exports = { test, expect: require('@playwright/test').expect };
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
```javascript
|
|
740
|
+
// tests/e2e/checkout/payment.spec.js
|
|
741
|
+
const { test, expect } = require('../../../fixtures/base.fixture');
|
|
742
|
+
const { generateEmail } = require('../../../helpers/test-data');
|
|
743
|
+
const { expectToast } = require('../../../helpers/assertions');
|
|
744
|
+
|
|
745
|
+
test.describe('Checkout Payment', () => {
|
|
746
|
+
test('authenticated user completes purchase', async ({ authenticatedPage, checkoutPage }) => {
|
|
747
|
+
await checkoutPage.goto();
|
|
748
|
+
await checkoutPage.fillPaymentDetails({
|
|
749
|
+
number: '4242424242424242',
|
|
750
|
+
expiry: '12/28',
|
|
751
|
+
cvc: '123',
|
|
752
|
+
});
|
|
753
|
+
await checkoutPage.pay();
|
|
754
|
+
await expectToast(authenticatedPage, 'Payment successful');
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test('guest user enters email at checkout', async ({ checkoutPage }) => {
|
|
758
|
+
const email = generateEmail('guest');
|
|
759
|
+
await checkoutPage.goto();
|
|
760
|
+
await checkoutPage.page.getByLabel('Guest email').fill(email);
|
|
761
|
+
await checkoutPage.fillPaymentDetails({
|
|
762
|
+
number: '4242424242424242',
|
|
763
|
+
expiry: '12/28',
|
|
764
|
+
cvc: '123',
|
|
765
|
+
});
|
|
766
|
+
await checkoutPage.pay();
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**Summary of the hybrid approach:**
|
|
772
|
+
|
|
773
|
+
| Layer | Pattern | Responsibility |
|
|
774
|
+
|---|---|---|
|
|
775
|
+
| **Test file** | `test()` | Describes behavior, orchestrates the three layers |
|
|
776
|
+
| **Fixtures** | `test.extend()` | Resource lifecycle -- setup, provide, teardown |
|
|
777
|
+
| **Page objects** | Classes | UI interaction -- navigation, actions, locators |
|
|
778
|
+
| **Helpers** | Functions | Utilities -- data generation, formatting, common assertions |
|
|
779
|
+
|
|
780
|
+
## Anti-Patterns
|
|
781
|
+
|
|
782
|
+
### Using only page objects for everything
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
// BAD: page object managing auth state, database, AND UI
|
|
786
|
+
class LoginPage {
|
|
787
|
+
async createTestUser() { /* API call to create user */ }
|
|
788
|
+
async deleteTestUser() { /* API call to delete user */ }
|
|
789
|
+
async seedDatabase() { /* database setup */ }
|
|
790
|
+
async signIn(email: string, password: string) { /* UI interaction */ }
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
**Problem:** Page objects should only encapsulate UI interaction. Resource lifecycle (create user, seed database) belongs in fixtures where teardown is guaranteed.
|
|
795
|
+
|
|
796
|
+
**Fix:** Move `createTestUser` and `deleteTestUser` into a `testUser` fixture. Keep only `signIn` in the page object.
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
### Page objects that are just locator containers
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
// BAD: no methods, no encapsulation — just a bag of locators
|
|
804
|
+
class LoginPage {
|
|
805
|
+
emailInput = this.page.getByLabel('Email');
|
|
806
|
+
passwordInput = this.page.getByLabel('Password');
|
|
807
|
+
submitButton = this.page.getByRole('button', { name: 'Sign in' });
|
|
808
|
+
|
|
809
|
+
constructor(private page: Page) {}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Test still does all the work
|
|
813
|
+
test('login', async ({ page }) => {
|
|
814
|
+
const login = new LoginPage(page);
|
|
815
|
+
await login.emailInput.fill('user@example.com');
|
|
816
|
+
await login.passwordInput.fill('pass');
|
|
817
|
+
await login.submitButton.click();
|
|
818
|
+
});
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
**Problem:** This adds a layer of indirection with no benefit. The test is not more readable than using locators directly.
|
|
822
|
+
|
|
823
|
+
**Fix:** Add intent-revealing methods. If the page is simple enough that locators suffice, skip the page object entirely.
|
|
824
|
+
|
|
825
|
+
```typescript
|
|
826
|
+
// GOOD: methods express user intent
|
|
827
|
+
class LoginPage {
|
|
828
|
+
constructor(private page: Page) {}
|
|
829
|
+
|
|
830
|
+
async goto() {
|
|
831
|
+
await this.page.goto('/login');
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async signIn(email: string, password: string) {
|
|
835
|
+
await this.page.getByLabel('Email').fill(email);
|
|
836
|
+
await this.page.getByLabel('Password').fill(password);
|
|
837
|
+
await this.page.getByRole('button', { name: 'Sign in' }).click();
|
|
838
|
+
await this.page.waitForURL('**/dashboard');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
### Fixtures that do too much
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
// BAD: one giant fixture that sets up everything
|
|
849
|
+
test.extend({
|
|
850
|
+
everything: async ({ page, request }, use) => {
|
|
851
|
+
// Creates user, seeds products, sets up payment method, navigates to dashboard,
|
|
852
|
+
// opens settings modal, enables feature flags, creates admin user...
|
|
853
|
+
const user = await createUser(request);
|
|
854
|
+
const products = await seedProducts(request, 50);
|
|
855
|
+
await setupPayment(request, user.id);
|
|
856
|
+
await page.goto('/dashboard');
|
|
857
|
+
await enableFeatureFlags(request, ['new-ui', 'beta-checkout']);
|
|
858
|
+
const admin = await createAdminUser(request);
|
|
859
|
+
|
|
860
|
+
await use({ user, products, admin, page });
|
|
861
|
+
|
|
862
|
+
// Massive teardown
|
|
863
|
+
await deleteUser(request, user.id);
|
|
864
|
+
await deleteUser(request, admin.id);
|
|
865
|
+
await deleteProducts(request, products);
|
|
866
|
+
await disableFeatureFlags(request, ['new-ui', 'beta-checkout']);
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Problem:** Every test pays the cost of full setup even when it only needs a user. Debugging is hard because the fixture does too many things.
|
|
872
|
+
|
|
873
|
+
**Fix:** Break into small, composable fixtures. Each fixture does one thing.
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
// GOOD: small, composable fixtures
|
|
877
|
+
test.extend({
|
|
878
|
+
testUser: async ({ request }, use) => { /* create + cleanup user */ },
|
|
879
|
+
adminUser: async ({ request }, use) => { /* create + cleanup admin */ },
|
|
880
|
+
seededProducts: async ({ request }, use) => { /* seed + cleanup products */ },
|
|
881
|
+
featureFlags: async ({ request }, use) => { /* enable + disable flags */ },
|
|
882
|
+
});
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
---
|
|
886
|
+
|
|
887
|
+
### Helpers with side effects
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
// BAD: "helper" that modifies database and stores state
|
|
891
|
+
let createdUserId: string;
|
|
892
|
+
|
|
893
|
+
export async function createTestUser(request: APIRequestContext) {
|
|
894
|
+
const response = await request.post('/api/users', { data: { email: 'test@example.com' } });
|
|
895
|
+
const user = await response.json();
|
|
896
|
+
createdUserId = user.id; // Module-level state — shared across tests!
|
|
897
|
+
return user;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export async function cleanupTestUser(request: APIRequestContext) {
|
|
901
|
+
await request.delete(`/api/users/${createdUserId}`);
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
**Problem:** Module-level state leaks between parallel tests. Cleanup is not guaranteed if the test crashes. This is a fixture pretending to be a helper.
|
|
906
|
+
|
|
907
|
+
**Fix:** If it has side effects and needs cleanup, make it a fixture.
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
### Over-abstracting simple operations
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
// BAD: helper for a one-liner that adds no clarity
|
|
915
|
+
export async function clickButton(page: Page, name: string) {
|
|
916
|
+
await page.getByRole('button', { name }).click();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// BAD: page object for a page with 2 interactions
|
|
920
|
+
class ConfirmationPage {
|
|
921
|
+
constructor(private page: Page) {}
|
|
922
|
+
async expectSuccess() {
|
|
923
|
+
await expect(this.page.getByText('Success')).toBeVisible();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
**Problem:** Adds indirection without improving readability or reducing duplication. `page.getByRole('button', { name: 'Submit' }).click()` is already clear.
|
|
929
|
+
|
|
930
|
+
**Fix:** Only abstract when there is real duplication (3+ usages) or real complexity (5+ interactions). Inline simple operations.
|
|
931
|
+
|
|
932
|
+
## Related
|
|
933
|
+
|
|
934
|
+
- [core/page-object-model.md](../core/page-object-model.md) -- detailed page object patterns and examples
|
|
935
|
+
- [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md) -- complete guide to Playwright fixtures and lifecycle hooks
|
|
936
|
+
- [core/test-organization.md](../core/test-organization.md) -- file structure and naming conventions
|
|
937
|
+
- [core/test-architecture.md](../core/test-architecture.md) -- when to write E2E vs API vs component tests
|