@assertrx/dino 1.0.0

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