digiqagent 1.2.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/.cursor-plugin/plugin.json +29 -0
- package/README.md +197 -0
- package/agents/code-reviewer.md +48 -0
- package/bin/cli.js +210 -0
- package/package.json +41 -0
- package/skills/playwright-test-generator/SKILL.md +563 -0
- package/skills/playwright-test-generator/interaction-test-patterns.md +987 -0
- package/skills/playwright-test-generator/page-object-patterns.md +833 -0
- package/skills/postman-collection-generator/SKILL.md +310 -0
- package/skills/postman-collection-generator/collection-patterns.md +493 -0
- package/skills/postman-test-suite-generator/SKILL.md +653 -0
- package/skills/postman-test-suite-generator/test-scenario-patterns.md +612 -0
- package/skills/receiving-code-review/SKILL.md +213 -0
- package/skills/requesting-code-review/SKILL.md +105 -0
- package/skills/requesting-code-review/code-reviewer.md +146 -0
- package/skills/review-prompts/code-quality-reviewer-prompt.md +26 -0
- package/skills/review-prompts/spec-reviewer-prompt.md +61 -0
- package/skills/swagger-generator/SKILL.md +238 -0
- package/skills/swagger-generator/openapi-patterns.md +667 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/test-driven-development/SKILL.md +371 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/verification-before-completion/SKILL.md +139 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
# Page Object Model Patterns
|
|
2
|
+
|
|
3
|
+
Reference patterns for Playwright Page Object Model classes. Adapt these to the detected framework and component library.
|
|
4
|
+
|
|
5
|
+
## Base Page
|
|
6
|
+
|
|
7
|
+
All page objects extend this base class:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Page, Locator } from '@playwright/test';
|
|
11
|
+
|
|
12
|
+
export class BasePage {
|
|
13
|
+
constructor(protected page: Page) {}
|
|
14
|
+
|
|
15
|
+
async goto(path: string) {
|
|
16
|
+
await this.page.goto(path);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getTitle(): Promise<string> {
|
|
20
|
+
return this.page.title();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get loadingSpinner(): Locator {
|
|
24
|
+
return this.page.getByRole('progressbar');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async waitForLoad() {
|
|
28
|
+
await this.loadingSpinner.waitFor({ state: 'hidden', timeout: 10000 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get toastMessage(): Locator {
|
|
32
|
+
return this.page.getByRole('alert');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getToastText(): Promise<string> {
|
|
36
|
+
await this.toastMessage.waitFor({ state: 'visible' });
|
|
37
|
+
return (await this.toastMessage.textContent()) ?? '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get confirmDialog(): Locator {
|
|
41
|
+
return this.page.getByRole('dialog');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async confirmDialogAction() {
|
|
45
|
+
await this.confirmDialog.getByRole('button', { name: /confirm|yes|ok/i }).click();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async cancelDialogAction() {
|
|
49
|
+
await this.confirmDialog.getByRole('button', { name: /cancel|no/i }).click();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Login Page — Complete Example
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { Page, Locator } from '@playwright/test';
|
|
58
|
+
import { BasePage } from './base.page';
|
|
59
|
+
|
|
60
|
+
export class LoginPage extends BasePage {
|
|
61
|
+
readonly path = '/login';
|
|
62
|
+
|
|
63
|
+
constructor(page: Page) {
|
|
64
|
+
super(page);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async goto() {
|
|
68
|
+
await super.goto(this.path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Locators ---
|
|
72
|
+
|
|
73
|
+
get emailInput(): Locator {
|
|
74
|
+
return this.page.getByLabel('Email');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get passwordInput(): Locator {
|
|
78
|
+
return this.page.getByLabel('Password');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get submitButton(): Locator {
|
|
82
|
+
return this.page.getByRole('button', { name: /login|sign in|submit/i });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get emailError(): Locator {
|
|
86
|
+
return this.page.getByText(/email.*required|enter.*email|valid.*email/i);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get passwordError(): Locator {
|
|
90
|
+
return this.page.getByText(/password.*required|enter.*password/i);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get generalError(): Locator {
|
|
94
|
+
return this.page.getByRole('alert');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get forgotPasswordLink(): Locator {
|
|
98
|
+
return this.page.getByRole('link', { name: /forgot.*password/i });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get registerLink(): Locator {
|
|
102
|
+
return this.page.getByRole('link', { name: /register|sign up|create.*account/i });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get rememberMeCheckbox(): Locator {
|
|
106
|
+
return this.page.getByRole('checkbox', { name: /remember/i });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get passwordToggle(): Locator {
|
|
110
|
+
return this.page.getByRole('button', { name: /show.*password|toggle.*password/i });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Actions ---
|
|
114
|
+
|
|
115
|
+
async fillEmail(email: string) {
|
|
116
|
+
await this.emailInput.clear();
|
|
117
|
+
await this.emailInput.fill(email);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async fillPassword(password: string) {
|
|
121
|
+
await this.passwordInput.clear();
|
|
122
|
+
await this.passwordInput.fill(password);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async clickSubmit() {
|
|
126
|
+
await this.submitButton.click();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async login(email: string, password: string) {
|
|
130
|
+
await this.fillEmail(email);
|
|
131
|
+
await this.fillPassword(password);
|
|
132
|
+
await this.clickSubmit();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async toggleRememberMe() {
|
|
136
|
+
await this.rememberMeCheckbox.click();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async clickForgotPassword() {
|
|
140
|
+
await this.forgotPasswordLink.click();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async clickRegister() {
|
|
144
|
+
await this.registerLink.click();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Form Page — Angular Reactive Forms
|
|
150
|
+
|
|
151
|
+
POM for a form page using Angular Material and Reactive Forms:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { Page, Locator } from '@playwright/test';
|
|
155
|
+
import { BasePage } from './base.page';
|
|
156
|
+
|
|
157
|
+
export class UserFormPage extends BasePage {
|
|
158
|
+
readonly createPath = '/users/new';
|
|
159
|
+
|
|
160
|
+
constructor(page: Page) {
|
|
161
|
+
super(page);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async gotoCreate() {
|
|
165
|
+
await super.goto(this.createPath);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async gotoEdit(userId: string) {
|
|
169
|
+
await super.goto(`/users/${userId}/edit`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- Form field locators ---
|
|
173
|
+
|
|
174
|
+
get nameInput(): Locator {
|
|
175
|
+
return this.page.getByLabel('Name');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get emailInput(): Locator {
|
|
179
|
+
return this.page.getByLabel('Email');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get phoneInput(): Locator {
|
|
183
|
+
return this.page.getByLabel('Phone');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get roleSelect(): Locator {
|
|
187
|
+
// Angular Material mat-select
|
|
188
|
+
return this.page.locator('mat-select[formcontrolname="role"]');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get activeToggle(): Locator {
|
|
192
|
+
return this.page.getByRole('switch', { name: /active/i });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get dateOfBirthInput(): Locator {
|
|
196
|
+
return this.page.getByLabel(/date of birth|dob/i);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get avatarFileInput(): Locator {
|
|
200
|
+
return this.page.locator('input[type="file"]');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
get submitButton(): Locator {
|
|
204
|
+
return this.page.getByRole('button', { name: /save|submit|create/i });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get cancelButton(): Locator {
|
|
208
|
+
return this.page.getByRole('button', { name: /cancel/i });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get formTitle(): Locator {
|
|
212
|
+
return this.page.getByRole('heading', { level: 1 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Validation error locators ---
|
|
216
|
+
|
|
217
|
+
get nameError(): Locator {
|
|
218
|
+
return this.page.locator('mat-error').filter({ hasText: /name/i });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get emailError(): Locator {
|
|
222
|
+
return this.page.locator('mat-error').filter({ hasText: /email/i });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get phoneError(): Locator {
|
|
226
|
+
return this.page.locator('mat-error').filter({ hasText: /phone/i });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Actions ---
|
|
230
|
+
|
|
231
|
+
async fillName(name: string) {
|
|
232
|
+
await this.nameInput.clear();
|
|
233
|
+
await this.nameInput.fill(name);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async fillEmail(email: string) {
|
|
237
|
+
await this.emailInput.clear();
|
|
238
|
+
await this.emailInput.fill(email);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async fillPhone(phone: string) {
|
|
242
|
+
await this.phoneInput.clear();
|
|
243
|
+
await this.phoneInput.fill(phone);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async selectRole(role: string) {
|
|
247
|
+
await this.roleSelect.click();
|
|
248
|
+
await this.page.getByRole('option', { name: role }).click();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async toggleActive() {
|
|
252
|
+
await this.activeToggle.click();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async fillDateOfBirth(date: string) {
|
|
256
|
+
await this.dateOfBirthInput.clear();
|
|
257
|
+
await this.dateOfBirthInput.fill(date);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async uploadAvatar(filePath: string) {
|
|
261
|
+
await this.avatarFileInput.setInputFiles(filePath);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async clickSubmit() {
|
|
265
|
+
await this.submitButton.click();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async clickCancel() {
|
|
269
|
+
await this.cancelButton.click();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async fillAndSubmit(data: {
|
|
273
|
+
name: string;
|
|
274
|
+
email: string;
|
|
275
|
+
phone?: string;
|
|
276
|
+
role?: string;
|
|
277
|
+
}) {
|
|
278
|
+
await this.fillName(data.name);
|
|
279
|
+
await this.fillEmail(data.email);
|
|
280
|
+
if (data.phone) await this.fillPhone(data.phone);
|
|
281
|
+
if (data.role) await this.selectRole(data.role);
|
|
282
|
+
await this.clickSubmit();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async triggerValidation(fieldLocator: Locator) {
|
|
286
|
+
await fieldLocator.focus();
|
|
287
|
+
await fieldLocator.blur();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Form Page — React with MUI
|
|
293
|
+
|
|
294
|
+
POM for a form page using React MUI components:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Page, Locator } from '@playwright/test';
|
|
298
|
+
import { BasePage } from './base.page';
|
|
299
|
+
|
|
300
|
+
export class UserFormPage extends BasePage {
|
|
301
|
+
constructor(page: Page) {
|
|
302
|
+
super(page);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async gotoCreate() {
|
|
306
|
+
await super.goto('/users/new');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// --- Form field locators ---
|
|
310
|
+
|
|
311
|
+
get nameInput(): Locator {
|
|
312
|
+
return this.page.getByLabel('Name');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
get emailInput(): Locator {
|
|
316
|
+
return this.page.getByLabel('Email');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
get roleSelect(): Locator {
|
|
320
|
+
return this.page.getByLabel('Role');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
get activeCheckbox(): Locator {
|
|
324
|
+
return this.page.getByRole('checkbox', { name: /active/i });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get submitButton(): Locator {
|
|
328
|
+
return this.page.getByRole('button', { name: /save|submit|create/i });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
get cancelButton(): Locator {
|
|
332
|
+
return this.page.getByRole('button', { name: /cancel/i });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- MUI-specific validation error locators ---
|
|
336
|
+
|
|
337
|
+
getFieldError(fieldLabel: string): Locator {
|
|
338
|
+
return this.page.locator(`[aria-label="${fieldLabel}"]`)
|
|
339
|
+
.locator('xpath=ancestor::div[contains(@class, "MuiFormControl")]')
|
|
340
|
+
.locator('.MuiFormHelperText-root.Mui-error');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
get nameError(): Locator {
|
|
344
|
+
return this.page.getByText(/name.*required|enter.*name/i);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
get emailError(): Locator {
|
|
348
|
+
return this.page.getByText(/email.*required|valid.*email/i);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Actions ---
|
|
352
|
+
|
|
353
|
+
async fillName(name: string) {
|
|
354
|
+
await this.nameInput.clear();
|
|
355
|
+
await this.nameInput.fill(name);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async fillEmail(email: string) {
|
|
359
|
+
await this.emailInput.clear();
|
|
360
|
+
await this.emailInput.fill(email);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async selectRole(role: string) {
|
|
364
|
+
await this.roleSelect.click();
|
|
365
|
+
await this.page.getByRole('option', { name: role }).click();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async toggleActive() {
|
|
369
|
+
await this.activeCheckbox.click();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async clickSubmit() {
|
|
373
|
+
await this.submitButton.click();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async clickCancel() {
|
|
377
|
+
await this.cancelButton.click();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async fillAndSubmit(data: { name: string; email: string; role?: string }) {
|
|
381
|
+
await this.fillName(data.name);
|
|
382
|
+
await this.fillEmail(data.email);
|
|
383
|
+
if (data.role) await this.selectRole(data.role);
|
|
384
|
+
await this.clickSubmit();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Table / List Page — Angular Material
|
|
390
|
+
|
|
391
|
+
POM for a data table using `mat-table` and `mat-paginator`:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { Page, Locator } from '@playwright/test';
|
|
395
|
+
import { BasePage } from './base.page';
|
|
396
|
+
|
|
397
|
+
export class UserListPage extends BasePage {
|
|
398
|
+
readonly path = '/users';
|
|
399
|
+
|
|
400
|
+
constructor(page: Page) {
|
|
401
|
+
super(page);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async goto() {
|
|
405
|
+
await super.goto(this.path);
|
|
406
|
+
await this.waitForLoad();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// --- Locators ---
|
|
410
|
+
|
|
411
|
+
get pageTitle(): Locator {
|
|
412
|
+
return this.page.getByRole('heading', { name: /users/i });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
get addButton(): Locator {
|
|
416
|
+
return this.page.getByRole('button', { name: /add|create|new/i });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
get searchInput(): Locator {
|
|
420
|
+
return this.page.getByPlaceholder(/search|filter/i);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
get table(): Locator {
|
|
424
|
+
return this.page.locator('mat-table, table');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
get tableRows(): Locator {
|
|
428
|
+
return this.page.locator('mat-row, tbody tr');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
get emptyState(): Locator {
|
|
432
|
+
return this.page.getByText(/no users|no data|no results|empty/i);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// --- Column header locators for sorting ---
|
|
436
|
+
|
|
437
|
+
columnHeader(name: string): Locator {
|
|
438
|
+
return this.page.getByRole('columnheader', { name: new RegExp(name, 'i') });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
get sortIndicator(): Locator {
|
|
442
|
+
return this.page.locator('.mat-sort-header-arrow, [aria-sort]');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// --- Pagination ---
|
|
446
|
+
|
|
447
|
+
get paginator(): Locator {
|
|
448
|
+
return this.page.locator('mat-paginator');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
get nextPageButton(): Locator {
|
|
452
|
+
return this.page.getByRole('button', { name: /next page/i });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
get previousPageButton(): Locator {
|
|
456
|
+
return this.page.getByRole('button', { name: /previous page/i });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
get pageSizeSelect(): Locator {
|
|
460
|
+
return this.paginator.locator('mat-select');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
get paginatorLabel(): Locator {
|
|
464
|
+
return this.paginator.locator('.mat-mdc-paginator-range-label');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// --- Row actions ---
|
|
468
|
+
|
|
469
|
+
rowByIndex(index: number): Locator {
|
|
470
|
+
return this.tableRows.nth(index);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
editButtonInRow(index: number): Locator {
|
|
474
|
+
return this.rowByIndex(index).getByRole('button', { name: /edit/i });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
deleteButtonInRow(index: number): Locator {
|
|
478
|
+
return this.rowByIndex(index).getByRole('button', { name: /delete/i });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
cellInRow(rowIndex: number, columnName: string): Locator {
|
|
482
|
+
return this.rowByIndex(rowIndex).locator(`mat-cell.mat-column-${columnName}, td.mat-column-${columnName}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// --- Actions ---
|
|
486
|
+
|
|
487
|
+
async clickAdd() {
|
|
488
|
+
await this.addButton.click();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async search(query: string) {
|
|
492
|
+
await this.searchInput.clear();
|
|
493
|
+
await this.searchInput.fill(query);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async clearSearch() {
|
|
497
|
+
await this.searchInput.clear();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async sortByColumn(columnName: string) {
|
|
501
|
+
await this.columnHeader(columnName).click();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async goToNextPage() {
|
|
505
|
+
await this.nextPageButton.click();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async goToPreviousPage() {
|
|
509
|
+
await this.previousPageButton.click();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async changePageSize(size: string) {
|
|
513
|
+
await this.pageSizeSelect.click();
|
|
514
|
+
await this.page.getByRole('option', { name: size }).click();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async getRowCount(): Promise<number> {
|
|
518
|
+
return this.tableRows.count();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async clickEditRow(index: number) {
|
|
522
|
+
await this.editButtonInRow(index).click();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async clickDeleteRow(index: number) {
|
|
526
|
+
await this.deleteButtonInRow(index).click();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async confirmDelete() {
|
|
530
|
+
await this.confirmDialogAction();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async cancelDelete() {
|
|
534
|
+
await this.cancelDialogAction();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Table / List Page — React MUI DataGrid
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import { Page, Locator } from '@playwright/test';
|
|
543
|
+
import { BasePage } from './base.page';
|
|
544
|
+
|
|
545
|
+
export class UserListPage extends BasePage {
|
|
546
|
+
constructor(page: Page) {
|
|
547
|
+
super(page);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async goto() {
|
|
551
|
+
await super.goto('/users');
|
|
552
|
+
await this.waitForLoad();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// --- Locators ---
|
|
556
|
+
|
|
557
|
+
get addButton(): Locator {
|
|
558
|
+
return this.page.getByRole('button', { name: /add|create|new/i });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
get searchInput(): Locator {
|
|
562
|
+
return this.page.getByPlaceholder(/search|filter/i);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
get dataGrid(): Locator {
|
|
566
|
+
return this.page.locator('.MuiDataGrid-root');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
get rows(): Locator {
|
|
570
|
+
return this.page.locator('.MuiDataGrid-row');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
get emptyState(): Locator {
|
|
574
|
+
return this.page.getByText(/no rows|no data|no results/i);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// --- Column headers ---
|
|
578
|
+
|
|
579
|
+
columnHeader(name: string): Locator {
|
|
580
|
+
return this.page.getByRole('columnheader', { name: new RegExp(name, 'i') });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// --- Pagination ---
|
|
584
|
+
|
|
585
|
+
get nextPageButton(): Locator {
|
|
586
|
+
return this.page.getByRole('button', { name: /next page/i });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
get previousPageButton(): Locator {
|
|
590
|
+
return this.page.getByRole('button', { name: /previous page/i });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
get rowCountLabel(): Locator {
|
|
594
|
+
return this.page.locator('.MuiTablePagination-displayedRows');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// --- Row actions ---
|
|
598
|
+
|
|
599
|
+
rowByIndex(index: number): Locator {
|
|
600
|
+
return this.rows.nth(index);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
editButtonInRow(index: number): Locator {
|
|
604
|
+
return this.rowByIndex(index).getByRole('button', { name: /edit/i });
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
deleteButtonInRow(index: number): Locator {
|
|
608
|
+
return this.rowByIndex(index).getByRole('button', { name: /delete/i });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// --- Actions ---
|
|
612
|
+
|
|
613
|
+
async clickAdd() {
|
|
614
|
+
await this.addButton.click();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async search(query: string) {
|
|
618
|
+
await this.searchInput.clear();
|
|
619
|
+
await this.searchInput.fill(query);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async sortByColumn(columnName: string) {
|
|
623
|
+
await this.columnHeader(columnName).click();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async goToNextPage() {
|
|
627
|
+
await this.nextPageButton.click();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async getRowCount(): Promise<number> {
|
|
631
|
+
return this.rows.count();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async clickEditRow(index: number) {
|
|
635
|
+
await this.editButtonInRow(index).click();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async clickDeleteRow(index: number) {
|
|
639
|
+
await this.deleteButtonInRow(index).click();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
## Modal / Dialog Page Object
|
|
645
|
+
|
|
646
|
+
Reusable POM for modals that appear across pages:
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { Page, Locator } from '@playwright/test';
|
|
650
|
+
|
|
651
|
+
export class ConfirmDialogComponent {
|
|
652
|
+
constructor(private page: Page) {}
|
|
653
|
+
|
|
654
|
+
get dialog(): Locator {
|
|
655
|
+
return this.page.getByRole('dialog');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
get title(): Locator {
|
|
659
|
+
return this.dialog.getByRole('heading');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
get message(): Locator {
|
|
663
|
+
return this.dialog.locator('.dialog-content, .mat-mdc-dialog-content, .MuiDialogContent-root');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
get confirmButton(): Locator {
|
|
667
|
+
return this.dialog.getByRole('button', { name: /confirm|yes|ok|delete|proceed/i });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
get cancelButton(): Locator {
|
|
671
|
+
return this.dialog.getByRole('button', { name: /cancel|no|close/i });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
get closeIconButton(): Locator {
|
|
675
|
+
return this.dialog.getByRole('button', { name: /close/i }).first();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async confirm() {
|
|
679
|
+
await this.confirmButton.click();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async cancel() {
|
|
683
|
+
await this.cancelButton.click();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async closeWithIcon() {
|
|
687
|
+
await this.closeIconButton.click();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async closeWithEscape() {
|
|
691
|
+
await this.page.keyboard.press('Escape');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async closeWithOverlayClick() {
|
|
695
|
+
const backdrop = this.page.locator('.cdk-overlay-backdrop, .MuiBackdrop-root');
|
|
696
|
+
await backdrop.click({ force: true, position: { x: 0, y: 0 } });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
## Navigation / Sidebar Page Object
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
import { Page, Locator } from '@playwright/test';
|
|
705
|
+
|
|
706
|
+
export class NavigationComponent {
|
|
707
|
+
constructor(private page: Page) {}
|
|
708
|
+
|
|
709
|
+
get sidebar(): Locator {
|
|
710
|
+
return this.page.getByRole('navigation');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
get hamburgerMenu(): Locator {
|
|
714
|
+
return this.page.getByRole('button', { name: /menu|toggle/i });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
navLink(name: string): Locator {
|
|
718
|
+
return this.sidebar.getByRole('link', { name: new RegExp(name, 'i') });
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
get activeNavItem(): Locator {
|
|
722
|
+
return this.sidebar.locator('[aria-current="page"], .active, .mat-mdc-list-item-active');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
get userMenu(): Locator {
|
|
726
|
+
return this.page.getByRole('button', { name: /profile|account|avatar/i });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
get logoutButton(): Locator {
|
|
730
|
+
return this.page.getByRole('menuitem', { name: /logout|sign out/i });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async navigateTo(linkName: string) {
|
|
734
|
+
await this.navLink(linkName).click();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async toggleSidebar() {
|
|
738
|
+
await this.hamburgerMenu.click();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async openUserMenu() {
|
|
742
|
+
await this.userMenu.click();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async logout() {
|
|
746
|
+
await this.openUserMenu();
|
|
747
|
+
await this.logoutButton.click();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
## Auth Setup Fixture
|
|
753
|
+
|
|
754
|
+
Pattern for authenticating before tests that need auth state:
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
import { test as setup, expect } from '@playwright/test';
|
|
758
|
+
|
|
759
|
+
const authFile = 'e2e/.auth/user.json';
|
|
760
|
+
|
|
761
|
+
setup('authenticate', async ({ page }) => {
|
|
762
|
+
await page.goto('/login');
|
|
763
|
+
await page.getByLabel('Email').fill('admin@example.com');
|
|
764
|
+
await page.getByLabel('Password').fill('SecureP@ss123');
|
|
765
|
+
await page.getByRole('button', { name: /login|sign in/i }).click();
|
|
766
|
+
|
|
767
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
768
|
+
|
|
769
|
+
await page.context().storageState({ path: authFile });
|
|
770
|
+
});
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
Usage in `playwright.config.ts`:
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
projects: [
|
|
777
|
+
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
|
778
|
+
{
|
|
779
|
+
name: 'chromium',
|
|
780
|
+
use: {
|
|
781
|
+
...devices['Desktop Chrome'],
|
|
782
|
+
storageState: 'e2e/.auth/user.json',
|
|
783
|
+
},
|
|
784
|
+
dependencies: ['setup'],
|
|
785
|
+
},
|
|
786
|
+
]
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
## Test Data Fixture
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
export const testUsers = {
|
|
793
|
+
valid: {
|
|
794
|
+
name: 'Jane Smith',
|
|
795
|
+
email: 'jane.smith@example.com',
|
|
796
|
+
password: 'SecureP@ss123',
|
|
797
|
+
phone: '+1-555-123-4567',
|
|
798
|
+
role: 'User',
|
|
799
|
+
},
|
|
800
|
+
admin: {
|
|
801
|
+
name: 'Alex Johnson',
|
|
802
|
+
email: 'alex.johnson@example.com',
|
|
803
|
+
password: 'AdminP@ss456',
|
|
804
|
+
phone: '+1-555-987-6543',
|
|
805
|
+
role: 'Admin',
|
|
806
|
+
},
|
|
807
|
+
invalidEmail: {
|
|
808
|
+
name: 'Test User',
|
|
809
|
+
email: 'not-an-email',
|
|
810
|
+
password: 'SecureP@ss123',
|
|
811
|
+
},
|
|
812
|
+
shortPassword: {
|
|
813
|
+
name: 'Test User',
|
|
814
|
+
email: 'test@example.com',
|
|
815
|
+
password: 'ab',
|
|
816
|
+
},
|
|
817
|
+
emptyFields: {
|
|
818
|
+
name: '',
|
|
819
|
+
email: '',
|
|
820
|
+
password: '',
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
export const testProducts = {
|
|
825
|
+
valid: {
|
|
826
|
+
name: 'Wireless Bluetooth Headphones',
|
|
827
|
+
description: 'Premium noise-cancelling headphones with 30-hour battery',
|
|
828
|
+
price: '149.99',
|
|
829
|
+
category: 'Electronics',
|
|
830
|
+
sku: 'WBH-2025-001',
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
```
|