@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,921 @@
1
+ # Page Object Model
2
+
3
+ > **When to use**: Any page or component is touched by more than one test file, or a single test file interacts with more than two distinct UI regions.
4
+
5
+ Page objects encapsulate **actions**, not locators. A test should read like a user story: `await loginPage.login('admin', 'secret')`, never `await loginPage.usernameInput.fill('admin')`. The POM is a boundary between "what the user does" and "how the UI is structured." Assertions stay in tests, never inside page objects.
6
+
7
+ ## Quick Reference
8
+
9
+ | Concept | Rule |
10
+ |---|---|
11
+ | Locators | Defined as `readonly` properties in the constructor or as getters. Never exposed raw to tests. |
12
+ | Actions | Public methods that perform a user-visible behavior. Return `Promise<void>` or the next page object. |
13
+ | Assertions | **Never** inside page objects. Tests own all `expect()` calls. |
14
+ | Navigation | Methods that navigate return the destination page object, not `void`. |
15
+ | State | Page objects are stateless. No caching locator text, no tracking "current step." |
16
+ | Constructor | Takes `Page` (or `Locator` for components). Nothing else. No URLs, no test data. |
17
+ | Naming | `LoginPage`, `DashboardPage`, `NavbarComponent`. File: `login.page.ts`, `navbar.component.ts`. |
18
+
19
+ ## Patterns
20
+
21
+ ### 1. Basic POM Class
22
+
23
+ **Use when**: A page has 3+ interactions across multiple tests.
24
+ **Avoid when**: A page is used in a single test file with trivial interactions -- use a helper function instead.
25
+
26
+ **TypeScript**
27
+
28
+ ```typescript
29
+ // tests/pages/login.page.ts
30
+ import { type Page, type Locator } from '@playwright/test';
31
+
32
+ export class LoginPage {
33
+ readonly usernameInput: Locator;
34
+ readonly passwordInput: Locator;
35
+ readonly submitButton: Locator;
36
+ readonly errorMessage: Locator;
37
+
38
+ constructor(private readonly page: Page) {
39
+ this.usernameInput = page.getByLabel('Username');
40
+ this.passwordInput = page.getByLabel('Password');
41
+ this.submitButton = page.getByRole('button', { name: 'Sign in' });
42
+ this.errorMessage = page.getByRole('alert');
43
+ }
44
+
45
+ async goto() {
46
+ await this.page.goto('/login');
47
+ }
48
+
49
+ async login(username: string, password: string) {
50
+ await this.usernameInput.fill(username);
51
+ await this.passwordInput.fill(password);
52
+ await this.submitButton.click();
53
+ }
54
+ }
55
+ ```
56
+
57
+ **JavaScript**
58
+
59
+ ```javascript
60
+ // tests/pages/login.page.js
61
+ // @ts-check
62
+
63
+ export class LoginPage {
64
+ /** @param {import('@playwright/test').Page} page */
65
+ constructor(page) {
66
+ this.page = page;
67
+ this.usernameInput = page.getByLabel('Username');
68
+ this.passwordInput = page.getByLabel('Password');
69
+ this.submitButton = page.getByRole('button', { name: 'Sign in' });
70
+ this.errorMessage = page.getByRole('alert');
71
+ }
72
+
73
+ async goto() {
74
+ await this.page.goto('/login');
75
+ }
76
+
77
+ async login(username, password) {
78
+ await this.usernameInput.fill(username);
79
+ await this.passwordInput.fill(password);
80
+ await this.submitButton.click();
81
+ }
82
+ }
83
+ ```
84
+
85
+ **Test usage**
86
+
87
+ ```typescript
88
+ // tests/auth.spec.ts
89
+ import { test, expect } from '@playwright/test';
90
+ import { LoginPage } from './pages/login.page';
91
+
92
+ test('successful login redirects to dashboard', async ({ page }) => {
93
+ const loginPage = new LoginPage(page);
94
+ await loginPage.goto();
95
+ await loginPage.login('admin', 'password123');
96
+
97
+ await expect(page).toHaveURL('/dashboard');
98
+ });
99
+
100
+ test('invalid credentials show error', async ({ page }) => {
101
+ const loginPage = new LoginPage(page);
102
+ await loginPage.goto();
103
+ await loginPage.login('admin', 'wrong');
104
+
105
+ await expect(loginPage.errorMessage).toBeVisible();
106
+ await expect(loginPage.errorMessage).toHaveText('Invalid credentials');
107
+ });
108
+ ```
109
+
110
+ ### 2. Component Objects
111
+
112
+ **Use when**: A UI element (navbar, sidebar, modal, form, table) appears on multiple pages.
113
+ **Avoid when**: The component is page-specific and never reused.
114
+
115
+ Components take a `Locator` (their root container), not a `Page`. This scopes all queries to the component's DOM subtree and allows composing components into page objects.
116
+
117
+ **TypeScript**
118
+
119
+ ```typescript
120
+ // tests/components/navbar.component.ts
121
+ import { type Locator } from '@playwright/test';
122
+
123
+ export class NavbarComponent {
124
+ readonly profileMenu: Locator;
125
+ readonly searchInput: Locator;
126
+ readonly notificationBell: Locator;
127
+
128
+ constructor(private readonly root: Locator) {
129
+ this.profileMenu = root.getByRole('button', { name: 'Profile' });
130
+ this.searchInput = root.getByRole('searchbox');
131
+ this.notificationBell = root.getByRole('button', { name: 'Notifications' });
132
+ }
133
+
134
+ async search(query: string) {
135
+ await this.searchInput.fill(query);
136
+ await this.searchInput.press('Enter');
137
+ }
138
+
139
+ async openProfile() {
140
+ await this.profileMenu.click();
141
+ }
142
+
143
+ async openNotifications() {
144
+ await this.notificationBell.click();
145
+ }
146
+ }
147
+
148
+ // tests/components/modal.component.ts
149
+ import { type Locator } from '@playwright/test';
150
+
151
+ export class ModalComponent {
152
+ readonly title: Locator;
153
+ readonly closeButton: Locator;
154
+ readonly confirmButton: Locator;
155
+ readonly cancelButton: Locator;
156
+
157
+ constructor(private readonly root: Locator) {
158
+ this.title = root.getByRole('heading');
159
+ this.closeButton = root.getByRole('button', { name: 'Close' });
160
+ this.confirmButton = root.getByRole('button', { name: 'Confirm' });
161
+ this.cancelButton = root.getByRole('button', { name: 'Cancel' });
162
+ }
163
+
164
+ async confirm() {
165
+ await this.confirmButton.click();
166
+ }
167
+
168
+ async cancel() {
169
+ await this.cancelButton.click();
170
+ }
171
+
172
+ async close() {
173
+ await this.closeButton.click();
174
+ }
175
+ }
176
+
177
+ // tests/pages/dashboard.page.ts
178
+ import { type Page } from '@playwright/test';
179
+ import { NavbarComponent } from '../components/navbar.component';
180
+ import { ModalComponent } from '../components/modal.component';
181
+
182
+ export class DashboardPage {
183
+ readonly navbar: NavbarComponent;
184
+ readonly deleteModal: ModalComponent;
185
+
186
+ constructor(private readonly page: Page) {
187
+ this.navbar = new NavbarComponent(page.getByRole('navigation'));
188
+ this.deleteModal = new ModalComponent(page.getByRole('dialog'));
189
+ }
190
+
191
+ async goto() {
192
+ await this.page.goto('/dashboard');
193
+ }
194
+
195
+ async deleteItem(name: string) {
196
+ await this.page.getByRole('row', { name }).getByRole('button', { name: 'Delete' }).click();
197
+ await this.deleteModal.confirm();
198
+ }
199
+ }
200
+ ```
201
+
202
+ **JavaScript**
203
+
204
+ ```javascript
205
+ // tests/components/navbar.component.js
206
+ // @ts-check
207
+
208
+ export class NavbarComponent {
209
+ /** @param {import('@playwright/test').Locator} root */
210
+ constructor(root) {
211
+ this.root = root;
212
+ this.profileMenu = root.getByRole('button', { name: 'Profile' });
213
+ this.searchInput = root.getByRole('searchbox');
214
+ this.notificationBell = root.getByRole('button', { name: 'Notifications' });
215
+ }
216
+
217
+ async search(query) {
218
+ await this.searchInput.fill(query);
219
+ await this.searchInput.press('Enter');
220
+ }
221
+
222
+ async openProfile() {
223
+ await this.profileMenu.click();
224
+ }
225
+
226
+ async openNotifications() {
227
+ await this.notificationBell.click();
228
+ }
229
+ }
230
+
231
+ // tests/pages/dashboard.page.js
232
+ // @ts-check
233
+ import { NavbarComponent } from '../components/navbar.component.js';
234
+ import { ModalComponent } from '../components/modal.component.js';
235
+
236
+ export class DashboardPage {
237
+ /** @param {import('@playwright/test').Page} page */
238
+ constructor(page) {
239
+ this.page = page;
240
+ this.navbar = new NavbarComponent(page.getByRole('navigation'));
241
+ this.deleteModal = new ModalComponent(page.getByRole('dialog'));
242
+ }
243
+
244
+ async goto() {
245
+ await this.page.goto('/dashboard');
246
+ }
247
+
248
+ async deleteItem(name) {
249
+ await this.page.getByRole('row', { name }).getByRole('button', { name: 'Delete' }).click();
250
+ await this.deleteModal.confirm();
251
+ }
252
+ }
253
+ ```
254
+
255
+ ### 3. Page Object with Navigation
256
+
257
+ **Use when**: An action on one page navigates to a different page (form submission, link click, redirect).
258
+ **Avoid when**: Navigation is uncertain (might fail validation and stay on the same page).
259
+
260
+ When a method causes navigation, return the destination page object. This creates a typed chain that mirrors the user flow and catches stale page object usage at the call site.
261
+
262
+ **TypeScript**
263
+
264
+ ```typescript
265
+ // tests/pages/login.page.ts
266
+ import { type Page } from '@playwright/test';
267
+ import { DashboardPage } from './dashboard.page';
268
+
269
+ export class LoginPage {
270
+ constructor(private readonly page: Page) {}
271
+
272
+ async goto() {
273
+ await this.page.goto('/login');
274
+ }
275
+
276
+ /** Returns DashboardPage on success. Call only when credentials are valid. */
277
+ async loginAs(username: string, password: string): Promise<DashboardPage> {
278
+ await this.page.getByLabel('Username').fill(username);
279
+ await this.page.getByLabel('Password').fill(password);
280
+ await this.page.getByRole('button', { name: 'Sign in' }).click();
281
+ await this.page.waitForURL('/dashboard');
282
+ return new DashboardPage(this.page);
283
+ }
284
+
285
+ /** Use for invalid credential tests -- stays on login page. */
286
+ async loginExpectingError(username: string, password: string) {
287
+ await this.page.getByLabel('Username').fill(username);
288
+ await this.page.getByLabel('Password').fill(password);
289
+ await this.page.getByRole('button', { name: 'Sign in' }).click();
290
+ }
291
+ }
292
+
293
+ // tests/pages/dashboard.page.ts
294
+ import { type Page } from '@playwright/test';
295
+ import { SettingsPage } from './settings.page';
296
+
297
+ export class DashboardPage {
298
+ constructor(private readonly page: Page) {}
299
+
300
+ async gotoSettings(): Promise<SettingsPage> {
301
+ await this.page.getByRole('link', { name: 'Settings' }).click();
302
+ await this.page.waitForURL('/settings');
303
+ return new SettingsPage(this.page);
304
+ }
305
+ }
306
+ ```
307
+
308
+ **JavaScript**
309
+
310
+ ```javascript
311
+ // tests/pages/login.page.js
312
+ // @ts-check
313
+ import { DashboardPage } from './dashboard.page.js';
314
+
315
+ export class LoginPage {
316
+ /** @param {import('@playwright/test').Page} page */
317
+ constructor(page) {
318
+ this.page = page;
319
+ }
320
+
321
+ async goto() {
322
+ await this.page.goto('/login');
323
+ }
324
+
325
+ /** @returns {Promise<DashboardPage>} */
326
+ async loginAs(username, password) {
327
+ await this.page.getByLabel('Username').fill(username);
328
+ await this.page.getByLabel('Password').fill(password);
329
+ await this.page.getByRole('button', { name: 'Sign in' }).click();
330
+ await this.page.waitForURL('/dashboard');
331
+ return new DashboardPage(this.page);
332
+ }
333
+
334
+ async loginExpectingError(username, password) {
335
+ await this.page.getByLabel('Username').fill(username);
336
+ await this.page.getByLabel('Password').fill(password);
337
+ await this.page.getByRole('button', { name: 'Sign in' }).click();
338
+ }
339
+ }
340
+ ```
341
+
342
+ **Test usage showing the navigation chain**
343
+
344
+ ```typescript
345
+ // tests/settings.spec.ts
346
+ import { test, expect } from '@playwright/test';
347
+ import { LoginPage } from './pages/login.page';
348
+
349
+ test('user can navigate from login to settings', async ({ page }) => {
350
+ const loginPage = new LoginPage(page);
351
+ await loginPage.goto();
352
+
353
+ const dashboard = await loginPage.loginAs('admin', 'password123');
354
+ const settings = await dashboard.gotoSettings();
355
+
356
+ await expect(page).toHaveURL('/settings');
357
+ });
358
+ ```
359
+
360
+ ### 4. Page Object as Fixture
361
+
362
+ **Use when**: Page objects are used in 3+ test files. This is the recommended approach for mature test suites.
363
+ **Avoid when**: Early prototyping with rapidly changing pages.
364
+
365
+ Fixtures eliminate boilerplate instantiation, provide automatic cleanup, and make POMs available via destructuring -- identical to built-in `page` and `request` fixtures.
366
+
367
+ **TypeScript**
368
+
369
+ ```typescript
370
+ // tests/fixtures.ts
371
+ import { test as base } from '@playwright/test';
372
+ import { LoginPage } from './pages/login.page';
373
+ import { DashboardPage } from './pages/dashboard.page';
374
+ import { SettingsPage } from './pages/settings.page';
375
+
376
+ type PageObjects = {
377
+ loginPage: LoginPage;
378
+ dashboardPage: DashboardPage;
379
+ settingsPage: SettingsPage;
380
+ };
381
+
382
+ export const test = base.extend<PageObjects>({
383
+ loginPage: async ({ page }, use) => {
384
+ await use(new LoginPage(page));
385
+ },
386
+ dashboardPage: async ({ page }, use) => {
387
+ await use(new DashboardPage(page));
388
+ },
389
+ settingsPage: async ({ page }, use) => {
390
+ await use(new SettingsPage(page));
391
+ },
392
+ });
393
+
394
+ export { expect } from '@playwright/test';
395
+ ```
396
+
397
+ **JavaScript**
398
+
399
+ ```javascript
400
+ // tests/fixtures.js
401
+ // @ts-check
402
+ import { test as base } from '@playwright/test';
403
+ import { LoginPage } from './pages/login.page.js';
404
+ import { DashboardPage } from './pages/dashboard.page.js';
405
+ import { SettingsPage } from './pages/settings.page.js';
406
+
407
+ export const test = base.extend({
408
+ loginPage: async ({ page }, use) => {
409
+ await use(new LoginPage(page));
410
+ },
411
+ dashboardPage: async ({ page }, use) => {
412
+ await use(new DashboardPage(page));
413
+ },
414
+ settingsPage: async ({ page }, use) => {
415
+ await use(new SettingsPage(page));
416
+ },
417
+ });
418
+
419
+ export { expect } from '@playwright/test';
420
+ ```
421
+
422
+ **Test usage**
423
+
424
+ ```typescript
425
+ // tests/auth.spec.ts
426
+ import { test, expect } from './fixtures';
427
+
428
+ test('successful login redirects to dashboard', async ({ loginPage, page }) => {
429
+ await loginPage.goto();
430
+ await loginPage.login('admin', 'password123');
431
+
432
+ await expect(page).toHaveURL('/dashboard');
433
+ });
434
+ ```
435
+
436
+ **Advanced: fixture that performs setup**
437
+
438
+ ```typescript
439
+ // tests/fixtures.ts
440
+ import { test as base } from '@playwright/test';
441
+ import { DashboardPage } from './pages/dashboard.page';
442
+
443
+ export const test = base.extend<{ authenticatedDashboard: DashboardPage }>({
444
+ authenticatedDashboard: async ({ page }, use) => {
445
+ // Setup: navigate and authenticate
446
+ await page.goto('/login');
447
+ await page.getByLabel('Username').fill('admin');
448
+ await page.getByLabel('Password').fill('password123');
449
+ await page.getByRole('button', { name: 'Sign in' }).click();
450
+ await page.waitForURL('/dashboard');
451
+
452
+ await use(new DashboardPage(page));
453
+
454
+ // Teardown (optional): logout, clean up test data
455
+ await page.goto('/logout');
456
+ },
457
+ });
458
+
459
+ export { expect } from '@playwright/test';
460
+ ```
461
+
462
+ ### 5. Factory Functions
463
+
464
+ **Use when**: A page has few actions and no shared state. Simpler than a class, still encapsulates actions.
465
+ **Avoid when**: The page has 5+ methods or needs component composition.
466
+
467
+ Factory functions are a lightweight alternative for simple pages. They return an object with action methods, avoiding class ceremony.
468
+
469
+ **TypeScript**
470
+
471
+ ```typescript
472
+ // tests/pages/error.page.ts
473
+ import { type Page } from '@playwright/test';
474
+
475
+ export function createErrorPage(page: Page) {
476
+ return {
477
+ errorHeading: page.getByRole('heading', { name: 'Something went wrong' }),
478
+ retryButton: page.getByRole('button', { name: 'Retry' }),
479
+
480
+ async goto() {
481
+ await page.goto('/error');
482
+ },
483
+
484
+ async retry() {
485
+ await page.getByRole('button', { name: 'Retry' }).click();
486
+ },
487
+
488
+ async goHome() {
489
+ await page.getByRole('link', { name: 'Go home' }).click();
490
+ await page.waitForURL('/');
491
+ },
492
+ };
493
+ }
494
+ ```
495
+
496
+ **JavaScript**
497
+
498
+ ```javascript
499
+ // tests/pages/error.page.js
500
+ // @ts-check
501
+
502
+ /** @param {import('@playwright/test').Page} page */
503
+ export function createErrorPage(page) {
504
+ return {
505
+ errorHeading: page.getByRole('heading', { name: 'Something went wrong' }),
506
+ retryButton: page.getByRole('button', { name: 'Retry' }),
507
+
508
+ async goto() {
509
+ await page.goto('/error');
510
+ },
511
+
512
+ async retry() {
513
+ await page.getByRole('button', { name: 'Retry' }).click();
514
+ },
515
+
516
+ async goHome() {
517
+ await page.getByRole('link', { name: 'Go home' }).click();
518
+ await page.waitForURL('/');
519
+ },
520
+ };
521
+ }
522
+ ```
523
+
524
+ **Test usage**
525
+
526
+ ```typescript
527
+ // tests/error.spec.ts
528
+ import { test, expect } from '@playwright/test';
529
+ import { createErrorPage } from './pages/error.page';
530
+
531
+ test('retry button reloads the page', async ({ page }) => {
532
+ const errorPage = createErrorPage(page);
533
+ await errorPage.goto();
534
+ await errorPage.retry();
535
+
536
+ await expect(page).not.toHaveURL('/error');
537
+ });
538
+ ```
539
+
540
+ ### 6. Getter Pattern for Dynamic Locators
541
+
542
+ **Use when**: Locators depend on dynamic content (parameterized rows, nth items, content that changes after page load).
543
+ **Avoid when**: All locators are static and known at construction time -- use constructor assignment instead.
544
+
545
+ Getters create the locator fresh on each access. This avoids stale references when the DOM changes after the page object is constructed.
546
+
547
+ **TypeScript**
548
+
549
+ ```typescript
550
+ // tests/pages/users.page.ts
551
+ import { type Page, type Locator } from '@playwright/test';
552
+
553
+ export class UsersPage {
554
+ constructor(private readonly page: Page) {}
555
+
556
+ /** Static locators -- fine in constructor or as getters, either works. */
557
+ get addUserButton(): Locator {
558
+ return this.page.getByRole('button', { name: 'Add user' });
559
+ }
560
+
561
+ get userRows(): Locator {
562
+ return this.page.getByRole('row').filter({ hasNot: this.page.getByRole('columnheader') });
563
+ }
564
+
565
+ /** Dynamic locator -- must be a method since it takes a parameter. */
566
+ userRow(name: string): Locator {
567
+ return this.page.getByRole('row', { name });
568
+ }
569
+
570
+ userDeleteButton(name: string): Locator {
571
+ return this.userRow(name).getByRole('button', { name: 'Delete' });
572
+ }
573
+
574
+ async goto() {
575
+ await this.page.goto('/admin/users');
576
+ }
577
+
578
+ async deleteUser(name: string) {
579
+ await this.userDeleteButton(name).click();
580
+ // Wait for row to disappear, confirming deletion
581
+ await this.userRow(name).waitFor({ state: 'hidden' });
582
+ }
583
+
584
+ async addUser() {
585
+ await this.addUserButton.click();
586
+ }
587
+ }
588
+ ```
589
+
590
+ **JavaScript**
591
+
592
+ ```javascript
593
+ // tests/pages/users.page.js
594
+ // @ts-check
595
+
596
+ export class UsersPage {
597
+ /** @param {import('@playwright/test').Page} page */
598
+ constructor(page) {
599
+ this.page = page;
600
+ }
601
+
602
+ get addUserButton() {
603
+ return this.page.getByRole('button', { name: 'Add user' });
604
+ }
605
+
606
+ get userRows() {
607
+ return this.page.getByRole('row').filter({ hasNot: this.page.getByRole('columnheader') });
608
+ }
609
+
610
+ userRow(name) {
611
+ return this.page.getByRole('row', { name });
612
+ }
613
+
614
+ userDeleteButton(name) {
615
+ return this.userRow(name).getByRole('button', { name: 'Delete' });
616
+ }
617
+
618
+ async goto() {
619
+ await this.page.goto('/admin/users');
620
+ }
621
+
622
+ async deleteUser(name) {
623
+ await this.userDeleteButton(name).click();
624
+ await this.userRow(name).waitFor({ state: 'hidden' });
625
+ }
626
+
627
+ async addUser() {
628
+ await this.addUserButton.click();
629
+ }
630
+ }
631
+ ```
632
+
633
+ ### 7. Directory Structure
634
+
635
+ Use this layout. Adapt folder names to your convention, but keep the separation between pages, components, and fixtures.
636
+
637
+ ```
638
+ tests/
639
+ fixtures.ts # Custom test with POM fixtures
640
+ auth.spec.ts # Test files at top level or in feature folders
641
+ dashboard.spec.ts
642
+ pages/ # One file per page
643
+ login.page.ts
644
+ dashboard.page.ts
645
+ settings.page.ts
646
+ users.page.ts
647
+ components/ # Reusable UI components
648
+ navbar.component.ts
649
+ modal.component.ts
650
+ sidebar.component.ts
651
+ data-table.component.ts
652
+ ```
653
+
654
+ Rules:
655
+ - **One class per file.** File name matches the class: `LoginPage` lives in `login.page.ts`.
656
+ - **Pages vs components**: Pages own a full route (`/login`, `/dashboard`). Components are embedded in pages (`NavbarComponent`, `ModalComponent`).
657
+ - **No barrel files** (`index.ts` re-exporting everything). Import directly from the file. Barrel files create circular dependency traps and slow down IDE tooling.
658
+ - **`fixtures.ts` at `tests/` root.** Every spec imports `test` and `expect` from `./fixtures` instead of `@playwright/test`.
659
+
660
+ ### 8. Handling Async Initialization
661
+
662
+ **Use when**: A page object needs async setup before it is usable (waiting for API data to load, waiting for animations to settle, waiting for a specific element to appear).
663
+ **Avoid when**: The page is ready immediately after `goto()`.
664
+
665
+ Never put `await` in a constructor. Use a static factory method instead.
666
+
667
+ **TypeScript**
668
+
669
+ ```typescript
670
+ // tests/pages/analytics.page.ts
671
+ import { type Page, type Locator } from '@playwright/test';
672
+
673
+ export class AnalyticsPage {
674
+ readonly chart: Locator;
675
+ readonly dateRange: Locator;
676
+
677
+ private constructor(private readonly page: Page) {
678
+ this.chart = page.locator('[data-testid="analytics-chart"]');
679
+ this.dateRange = page.getByLabel('Date range');
680
+ }
681
+
682
+ /** Use this instead of `new AnalyticsPage()`. Waits for chart data to load. */
683
+ static async create(page: Page): Promise<AnalyticsPage> {
684
+ const analyticsPage = new AnalyticsPage(page);
685
+ await page.goto('/analytics');
686
+ // Wait for the async content that makes this page "ready"
687
+ await analyticsPage.chart.waitFor({ state: 'visible' });
688
+ return analyticsPage;
689
+ }
690
+
691
+ async selectDateRange(range: string) {
692
+ await this.dateRange.click();
693
+ await this.page.getByRole('option', { name: range }).click();
694
+ // Wait for chart to update after date change
695
+ await this.chart.waitFor({ state: 'visible' });
696
+ }
697
+ }
698
+ ```
699
+
700
+ **JavaScript**
701
+
702
+ ```javascript
703
+ // tests/pages/analytics.page.js
704
+ // @ts-check
705
+
706
+ export class AnalyticsPage {
707
+ /** @param {import('@playwright/test').Page} page */
708
+ constructor(page) {
709
+ this.page = page;
710
+ this.chart = page.locator('[data-testid="analytics-chart"]');
711
+ this.dateRange = page.getByLabel('Date range');
712
+ }
713
+
714
+ /** @param {import('@playwright/test').Page} page */
715
+ static async create(page) {
716
+ const analyticsPage = new AnalyticsPage(page);
717
+ await page.goto('/analytics');
718
+ await analyticsPage.chart.waitFor({ state: 'visible' });
719
+ return analyticsPage;
720
+ }
721
+
722
+ async selectDateRange(range) {
723
+ await this.dateRange.click();
724
+ await this.page.getByRole('option', { name: range }).click();
725
+ await this.chart.waitFor({ state: 'visible' });
726
+ }
727
+ }
728
+ ```
729
+
730
+ **As a fixture**
731
+
732
+ ```typescript
733
+ // tests/fixtures.ts
734
+ import { test as base } from '@playwright/test';
735
+ import { AnalyticsPage } from './pages/analytics.page';
736
+
737
+ export const test = base.extend<{ analyticsPage: AnalyticsPage }>({
738
+ analyticsPage: async ({ page }, use) => {
739
+ const analyticsPage = await AnalyticsPage.create(page);
740
+ await use(analyticsPage);
741
+ },
742
+ });
743
+
744
+ export { expect } from '@playwright/test';
745
+ ```
746
+
747
+ ## Decision Guide
748
+
749
+ ```
750
+ How complex is the page?
751
+ │
752
+ ├── 1-2 interactions, single test file
753
+ │ └── No POM. Inline locators in the test are fine.
754
+ │
755
+ ├── 3-5 interactions OR 2+ test files use it
756
+ │ ├── Few methods, no composition needed
757
+ │ │ └── Factory function (Pattern 5)
758
+ │ └── Multiple methods, needs component composition
759
+ │ └── Full POM class (Pattern 1 + 2)
760
+ │
761
+ ├── Used across 3+ test files
762
+ │ └── POM class + fixture injection (Pattern 4)
763
+ │
764
+ └── Page requires async setup before usable
765
+ └── Static factory method (Pattern 8) + fixture
766
+ ```
767
+
768
+ | Factor | Simple helper | Factory function | POM class | POM + fixture |
769
+ |---|---|---|---|---|
770
+ | Page complexity | 1-2 actions | 3-5 actions | 5+ actions | 5+ actions |
771
+ | Reuse across files | 1 file | 1-2 files | 2+ files | 3+ files |
772
+ | Component composition | No | No | Yes | Yes |
773
+ | Team size | Solo | Small | Any | Any |
774
+ | Setup ceremony | None | Low | Medium | Medium (once) |
775
+ | Recommended for | Quick prototypes | Simple pages | Standard choice | Mature suites |
776
+
777
+ ## Anti-Patterns
778
+
779
+ ### God Object
780
+
781
+ One page object for the entire application or a page object with 30+ methods covering unrelated features.
782
+
783
+ ```typescript
784
+ // BAD: one class for everything
785
+ class AppPage {
786
+ async login() { /* ... */ }
787
+ async addToCart() { /* ... */ }
788
+ async checkout() { /* ... */ }
789
+ async changePassword() { /* ... */ }
790
+ async viewAnalytics() { /* ... */ }
791
+ // 40 more methods...
792
+ }
793
+ ```
794
+
795
+ Fix: Split by page or feature. One class per logical page. Compose shared UI into component objects.
796
+
797
+ ### Assertions Inside Page Objects
798
+
799
+ ```typescript
800
+ // BAD: page object owns the assertion
801
+ class LoginPage {
802
+ async loginAndVerify(username: string, password: string) {
803
+ await this.usernameInput.fill(username);
804
+ await this.passwordInput.fill(password);
805
+ await this.submitButton.click();
806
+ // This assertion belongs in the test, not here
807
+ await expect(this.page).toHaveURL('/dashboard');
808
+ }
809
+ }
810
+
811
+ // GOOD: page object performs action, test asserts
812
+ class LoginPage {
813
+ async login(username: string, password: string) {
814
+ await this.usernameInput.fill(username);
815
+ await this.passwordInput.fill(password);
816
+ await this.submitButton.click();
817
+ }
818
+ }
819
+
820
+ // In test:
821
+ await loginPage.login('admin', 'pass');
822
+ await expect(page).toHaveURL('/dashboard');
823
+ ```
824
+
825
+ Exception: `waitForURL()` or `waitFor()` calls inside POM methods that ensure navigation completed are fine -- those are synchronization, not assertion.
826
+
827
+ ### Deeply Nested Inheritance
828
+
829
+ ```typescript
830
+ // BAD: inheritance chain
831
+ class BasePage { /* ... */ }
832
+ class AuthenticatedPage extends BasePage { /* ... */ }
833
+ class AdminPage extends AuthenticatedPage { /* ... */ }
834
+ class SuperAdminPage extends AdminPage { /* ... */ }
835
+ ```
836
+
837
+ Fix: Use composition. Create component objects for shared UI (navbar, sidebar) and compose them into page objects. Prefer `has-a` over `is-a`.
838
+
839
+ ```typescript
840
+ // GOOD: composition
841
+ class AdminPage {
842
+ readonly navbar: NavbarComponent;
843
+ readonly sidebar: AdminSidebarComponent;
844
+
845
+ constructor(private readonly page: Page) {
846
+ this.navbar = new NavbarComponent(page.getByRole('navigation'));
847
+ this.sidebar = new AdminSidebarComponent(page.getByRole('complementary'));
848
+ }
849
+ }
850
+ ```
851
+
852
+ ### Storing State in Page Objects
853
+
854
+ ```typescript
855
+ // BAD: tracking state
856
+ class CartPage {
857
+ private itemCount = 0;
858
+
859
+ async addItem(name: string) {
860
+ await this.page.getByRole('button', { name: `Add ${name}` }).click();
861
+ this.itemCount++; // State goes stale if the UI changes independently
862
+ }
863
+
864
+ getCount() {
865
+ return this.itemCount; // Lies if another tab or API call modified the cart
866
+ }
867
+ }
868
+ ```
869
+
870
+ Fix: Read state from the DOM when needed. The page is the source of truth, not your JavaScript variable.
871
+
872
+ ```typescript
873
+ // GOOD: read from DOM
874
+ class CartPage {
875
+ async addItem(name: string) {
876
+ await this.page.getByRole('button', { name: `Add ${name}` }).click();
877
+ }
878
+
879
+ get itemCount(): Locator {
880
+ return this.page.getByTestId('cart-count');
881
+ }
882
+ }
883
+
884
+ // In test:
885
+ await cartPage.addItem('Widget');
886
+ await expect(cartPage.itemCount).toHaveText('1');
887
+ ```
888
+
889
+ ### Exposing Raw Locators as the Primary API
890
+
891
+ ```typescript
892
+ // BAD: test manipulates locators directly
893
+ const loginPage = new LoginPage(page);
894
+ await loginPage.usernameInput.fill('admin');
895
+ await loginPage.passwordInput.fill('secret');
896
+ await loginPage.submitButton.click();
897
+
898
+ // GOOD: test calls an action
899
+ const loginPage = new LoginPage(page);
900
+ await loginPage.login('admin', 'secret');
901
+ ```
902
+
903
+ Exposing locators as `readonly` properties for assertions is acceptable (`expect(loginPage.errorMessage).toBeVisible()`). The anti-pattern is requiring tests to orchestrate multi-step interactions through raw locators.
904
+
905
+ ## Troubleshooting
906
+
907
+ | Problem | Cause | Fix |
908
+ |---|---|---|
909
+ | Locator times out but element exists | POM constructed before navigation; locator attached to wrong page state | Move `goto()` before POM construction, or use getter pattern (Pattern 6) |
910
+ | Circular import between page objects | `LoginPage` imports `DashboardPage` which imports `LoginPage` | Break the cycle: use lazy `import()` in the navigation method, or have the test create the destination POM |
911
+ | Fixture not available in test | Test imports `test` from `@playwright/test` instead of `./fixtures` | Always import `test` and `expect` from your custom fixtures file |
912
+ | TypeScript error: property does not exist on fixture | Fixture type not declared in `test.extend<>()` generic | Add the POM type to the generic parameter: `test.extend<{ loginPage: LoginPage }>()` |
913
+ | POM methods feel duplicated across similar pages | Multiple pages share the same form structure | Extract a component object for the shared form, compose into each page |
914
+ | `page.goto()` in constructor causes "cannot use await" | Constructors cannot be async | Use static factory method (Pattern 8) |
915
+
916
+ ## Related
917
+
918
+ - [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md) -- how `test.extend()` works in depth, fixture scoping, teardown
919
+ - [core/locators.md](../core/locators.md) -- choosing the right locator strategy for POM properties
920
+ - [core/pom-vs-fixtures-vs-helpers.md](../core/pom-vs-fixtures-vs-helpers.md) -- expanded decision framework for when to use each approach
921
+ - [core/test-organization.md](../core/test-organization.md) -- file and folder conventions for the broader test suite