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,987 @@
|
|
|
1
|
+
# Interaction Test Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive Playwright test code patterns organized by interaction type. Copy and adapt for generated test suites.
|
|
4
|
+
|
|
5
|
+
## Forms — Fill, Submit, Validate
|
|
6
|
+
|
|
7
|
+
### Positive — Valid Form Submission
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
test('should create user with valid data', async ({ page }) => {
|
|
11
|
+
const formPage = new UserFormPage(page);
|
|
12
|
+
await formPage.gotoCreate();
|
|
13
|
+
|
|
14
|
+
await formPage.fillAndSubmit({
|
|
15
|
+
name: 'Jane Smith',
|
|
16
|
+
email: 'jane.smith@example.com',
|
|
17
|
+
role: 'User',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await expect(page).toHaveURL(/\/users$/);
|
|
21
|
+
await expect(page.getByRole('alert')).toContainText(/created|success/i);
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Negative — Missing Required Fields (One at a Time)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
test('should show error when name is empty', async ({ page }) => {
|
|
29
|
+
const formPage = new UserFormPage(page);
|
|
30
|
+
await formPage.gotoCreate();
|
|
31
|
+
|
|
32
|
+
await formPage.fillEmail('jane@example.com');
|
|
33
|
+
await formPage.clickSubmit();
|
|
34
|
+
|
|
35
|
+
await expect(formPage.nameError).toBeVisible();
|
|
36
|
+
await expect(formPage.nameError).toContainText(/required|name/i);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should show error when email is empty', async ({ page }) => {
|
|
40
|
+
const formPage = new UserFormPage(page);
|
|
41
|
+
await formPage.gotoCreate();
|
|
42
|
+
|
|
43
|
+
await formPage.fillName('Jane Smith');
|
|
44
|
+
await formPage.clickSubmit();
|
|
45
|
+
|
|
46
|
+
await expect(formPage.emailError).toBeVisible();
|
|
47
|
+
await expect(formPage.emailError).toContainText(/required|email/i);
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Negative — Invalid Formats
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
test('should show error for invalid email format', async ({ page }) => {
|
|
55
|
+
const formPage = new UserFormPage(page);
|
|
56
|
+
await formPage.gotoCreate();
|
|
57
|
+
|
|
58
|
+
await formPage.fillName('Jane Smith');
|
|
59
|
+
await formPage.fillEmail('not-an-email');
|
|
60
|
+
await formPage.clickSubmit();
|
|
61
|
+
|
|
62
|
+
await expect(formPage.emailError).toBeVisible();
|
|
63
|
+
await expect(formPage.emailError).toContainText(/valid|format|email/i);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should show error for invalid phone format', async ({ page }) => {
|
|
67
|
+
const formPage = new UserFormPage(page);
|
|
68
|
+
await formPage.gotoCreate();
|
|
69
|
+
|
|
70
|
+
await formPage.fillName('Jane Smith');
|
|
71
|
+
await formPage.fillEmail('jane@example.com');
|
|
72
|
+
await formPage.fillPhone('abc');
|
|
73
|
+
await formPage.clickSubmit();
|
|
74
|
+
|
|
75
|
+
await expect(formPage.phoneError).toBeVisible();
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Negative — Boundary Values
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
test('should show error when name is too short', async ({ page }) => {
|
|
83
|
+
const formPage = new UserFormPage(page);
|
|
84
|
+
await formPage.gotoCreate();
|
|
85
|
+
|
|
86
|
+
await formPage.fillName('A');
|
|
87
|
+
await formPage.fillEmail('jane@example.com');
|
|
88
|
+
await formPage.clickSubmit();
|
|
89
|
+
|
|
90
|
+
await expect(formPage.nameError).toBeVisible();
|
|
91
|
+
await expect(formPage.nameError).toContainText(/minimum|at least|characters/i);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should show error when name exceeds max length', async ({ page }) => {
|
|
95
|
+
const formPage = new UserFormPage(page);
|
|
96
|
+
await formPage.gotoCreate();
|
|
97
|
+
|
|
98
|
+
await formPage.fillName('A'.repeat(256));
|
|
99
|
+
await formPage.fillEmail('jane@example.com');
|
|
100
|
+
await formPage.clickSubmit();
|
|
101
|
+
|
|
102
|
+
await expect(formPage.nameError).toBeVisible();
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Form Reset and Dirty State
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
test('should clear form on cancel', async ({ page }) => {
|
|
110
|
+
const formPage = new UserFormPage(page);
|
|
111
|
+
await formPage.gotoCreate();
|
|
112
|
+
|
|
113
|
+
await formPage.fillName('Jane Smith');
|
|
114
|
+
await formPage.fillEmail('jane@example.com');
|
|
115
|
+
await formPage.clickCancel();
|
|
116
|
+
|
|
117
|
+
await expect(page).toHaveURL(/\/users$/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should warn before leaving dirty form', async ({ page }) => {
|
|
121
|
+
const formPage = new UserFormPage(page);
|
|
122
|
+
await formPage.gotoCreate();
|
|
123
|
+
|
|
124
|
+
await formPage.fillName('Jane Smith');
|
|
125
|
+
|
|
126
|
+
// Try to navigate away
|
|
127
|
+
page.on('dialog', dialog => dialog.dismiss());
|
|
128
|
+
await page.getByRole('link', { name: /dashboard/i }).click();
|
|
129
|
+
|
|
130
|
+
// Should stay on form page
|
|
131
|
+
await expect(page).toHaveURL(/\/users\/new/);
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Inline Validation (Blur Trigger)
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
test('should show validation on field blur (Angular)', async ({ page }) => {
|
|
139
|
+
const formPage = new UserFormPage(page);
|
|
140
|
+
await formPage.gotoCreate();
|
|
141
|
+
|
|
142
|
+
await formPage.emailInput.focus();
|
|
143
|
+
await formPage.emailInput.blur();
|
|
144
|
+
|
|
145
|
+
await expect(formPage.emailError).toBeVisible();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should clear validation when valid input entered', async ({ page }) => {
|
|
149
|
+
const formPage = new UserFormPage(page);
|
|
150
|
+
await formPage.gotoCreate();
|
|
151
|
+
|
|
152
|
+
await formPage.emailInput.focus();
|
|
153
|
+
await formPage.emailInput.blur();
|
|
154
|
+
await expect(formPage.emailError).toBeVisible();
|
|
155
|
+
|
|
156
|
+
await formPage.fillEmail('jane@example.com');
|
|
157
|
+
await expect(formPage.emailError).not.toBeVisible();
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Click Events
|
|
162
|
+
|
|
163
|
+
### Button Click
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
test('should navigate to create form on Add button click', async ({ page }) => {
|
|
167
|
+
const listPage = new UserListPage(page);
|
|
168
|
+
await listPage.goto();
|
|
169
|
+
|
|
170
|
+
await listPage.clickAdd();
|
|
171
|
+
|
|
172
|
+
await expect(page).toHaveURL(/\/users\/new/);
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Toggle Switch
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
test('should toggle active status', async ({ page }) => {
|
|
180
|
+
const formPage = new UserFormPage(page);
|
|
181
|
+
await formPage.gotoEdit('123');
|
|
182
|
+
|
|
183
|
+
const toggle = page.getByRole('switch', { name: /active/i });
|
|
184
|
+
await expect(toggle).toBeChecked();
|
|
185
|
+
|
|
186
|
+
await toggle.click();
|
|
187
|
+
await expect(toggle).not.toBeChecked();
|
|
188
|
+
|
|
189
|
+
await toggle.click();
|
|
190
|
+
await expect(toggle).toBeChecked();
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Double-Click Prevention
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
test('should not submit form twice on rapid double-click', async ({ page }) => {
|
|
198
|
+
const formPage = new UserFormPage(page);
|
|
199
|
+
await formPage.gotoCreate();
|
|
200
|
+
|
|
201
|
+
await formPage.fillName('Jane Smith');
|
|
202
|
+
await formPage.fillEmail('jane@example.com');
|
|
203
|
+
|
|
204
|
+
await formPage.submitButton.dblclick();
|
|
205
|
+
|
|
206
|
+
// Should navigate once, not show duplicate error
|
|
207
|
+
await expect(page).toHaveURL(/\/users$/);
|
|
208
|
+
|
|
209
|
+
// Verify submit button was disabled after first click
|
|
210
|
+
// or only one success toast appeared
|
|
211
|
+
const toasts = page.getByRole('alert');
|
|
212
|
+
await expect(toasts).toHaveCount(1);
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Icon Button Actions
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
test('should open edit form on row edit icon click', async ({ page }) => {
|
|
220
|
+
const listPage = new UserListPage(page);
|
|
221
|
+
await listPage.goto();
|
|
222
|
+
|
|
223
|
+
await listPage.clickEditRow(0);
|
|
224
|
+
|
|
225
|
+
await expect(page).toHaveURL(/\/users\/[^/]+\/edit/);
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Navigation
|
|
230
|
+
|
|
231
|
+
### Route Transitions
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
test('should navigate between pages via sidebar', async ({ page }) => {
|
|
235
|
+
await page.goto('/dashboard');
|
|
236
|
+
const nav = new NavigationComponent(page);
|
|
237
|
+
|
|
238
|
+
await nav.navigateTo('Users');
|
|
239
|
+
await expect(page).toHaveURL(/\/users/);
|
|
240
|
+
await expect(page.getByRole('heading', { name: /users/i })).toBeVisible();
|
|
241
|
+
|
|
242
|
+
await nav.navigateTo('Settings');
|
|
243
|
+
await expect(page).toHaveURL(/\/settings/);
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Browser Back/Forward
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
test('should support browser back and forward navigation', async ({ page }) => {
|
|
251
|
+
await page.goto('/dashboard');
|
|
252
|
+
await page.getByRole('link', { name: /users/i }).click();
|
|
253
|
+
await expect(page).toHaveURL(/\/users/);
|
|
254
|
+
|
|
255
|
+
await page.goBack();
|
|
256
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
257
|
+
|
|
258
|
+
await page.goForward();
|
|
259
|
+
await expect(page).toHaveURL(/\/users/);
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Deep Linking
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
test('should load correct page from direct URL', async ({ page }) => {
|
|
267
|
+
await page.goto('/users');
|
|
268
|
+
await expect(page.getByRole('heading', { name: /users/i })).toBeVisible();
|
|
269
|
+
await expect(page.locator('mat-table, table')).toBeVisible();
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Protected Routes
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
test('should redirect to login when accessing protected route without auth', async ({ browser }) => {
|
|
277
|
+
const context = await browser.newContext();
|
|
278
|
+
const page = await context.newPage();
|
|
279
|
+
|
|
280
|
+
await page.goto('/dashboard');
|
|
281
|
+
|
|
282
|
+
await expect(page).toHaveURL(/\/login/);
|
|
283
|
+
await context.close();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('should redirect back to intended page after login', async ({ browser }) => {
|
|
287
|
+
const context = await browser.newContext();
|
|
288
|
+
const page = await context.newPage();
|
|
289
|
+
|
|
290
|
+
await page.goto('/users');
|
|
291
|
+
await expect(page).toHaveURL(/\/login/);
|
|
292
|
+
|
|
293
|
+
await page.getByLabel('Email').fill('admin@example.com');
|
|
294
|
+
await page.getByLabel('Password').fill('SecureP@ss123');
|
|
295
|
+
await page.getByRole('button', { name: /login/i }).click();
|
|
296
|
+
|
|
297
|
+
await expect(page).toHaveURL(/\/users/);
|
|
298
|
+
await context.close();
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 404 Page
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
test('should display 404 page for unknown routes', async ({ page }) => {
|
|
306
|
+
await page.goto('/nonexistent-route');
|
|
307
|
+
await expect(page.getByText(/404|not found|page.*exist/i)).toBeVisible();
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Modals and Dialogs
|
|
312
|
+
|
|
313
|
+
### Open, Verify Content, Close
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
test('should open delete confirmation dialog', async ({ page }) => {
|
|
317
|
+
const listPage = new UserListPage(page);
|
|
318
|
+
await listPage.goto();
|
|
319
|
+
|
|
320
|
+
await listPage.clickDeleteRow(0);
|
|
321
|
+
|
|
322
|
+
const dialog = new ConfirmDialogComponent(page);
|
|
323
|
+
await expect(dialog.dialog).toBeVisible();
|
|
324
|
+
await expect(dialog.title).toContainText(/delete|confirm/i);
|
|
325
|
+
await expect(dialog.message).toContainText(/sure|permanently/i);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('should close dialog on cancel', async ({ page }) => {
|
|
329
|
+
const listPage = new UserListPage(page);
|
|
330
|
+
await listPage.goto();
|
|
331
|
+
|
|
332
|
+
await listPage.clickDeleteRow(0);
|
|
333
|
+
const dialog = new ConfirmDialogComponent(page);
|
|
334
|
+
await dialog.cancel();
|
|
335
|
+
|
|
336
|
+
await expect(dialog.dialog).not.toBeVisible();
|
|
337
|
+
// Row should still be present
|
|
338
|
+
await expect(listPage.tableRows).toHaveCount(await listPage.getRowCount());
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('should delete item on confirm', async ({ page }) => {
|
|
342
|
+
const listPage = new UserListPage(page);
|
|
343
|
+
await listPage.goto();
|
|
344
|
+
const initialCount = await listPage.getRowCount();
|
|
345
|
+
|
|
346
|
+
await listPage.clickDeleteRow(0);
|
|
347
|
+
const dialog = new ConfirmDialogComponent(page);
|
|
348
|
+
await dialog.confirm();
|
|
349
|
+
|
|
350
|
+
await expect(dialog.dialog).not.toBeVisible();
|
|
351
|
+
await expect(page.getByRole('alert')).toContainText(/deleted|removed|success/i);
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Close via Escape Key
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
test('should close dialog with Escape key', async ({ page }) => {
|
|
359
|
+
const listPage = new UserListPage(page);
|
|
360
|
+
await listPage.goto();
|
|
361
|
+
|
|
362
|
+
await listPage.clickDeleteRow(0);
|
|
363
|
+
const dialog = new ConfirmDialogComponent(page);
|
|
364
|
+
await expect(dialog.dialog).toBeVisible();
|
|
365
|
+
|
|
366
|
+
await dialog.closeWithEscape();
|
|
367
|
+
|
|
368
|
+
await expect(dialog.dialog).not.toBeVisible();
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Close via Overlay Click
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
test('should close dialog on backdrop click', async ({ page }) => {
|
|
376
|
+
const listPage = new UserListPage(page);
|
|
377
|
+
await listPage.goto();
|
|
378
|
+
|
|
379
|
+
await listPage.clickDeleteRow(0);
|
|
380
|
+
const dialog = new ConfirmDialogComponent(page);
|
|
381
|
+
await expect(dialog.dialog).toBeVisible();
|
|
382
|
+
|
|
383
|
+
await dialog.closeWithOverlayClick();
|
|
384
|
+
|
|
385
|
+
await expect(dialog.dialog).not.toBeVisible();
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Dropdowns and Select
|
|
390
|
+
|
|
391
|
+
### Open and Select Option
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
test('should select role from dropdown', async ({ page }) => {
|
|
395
|
+
const formPage = new UserFormPage(page);
|
|
396
|
+
await formPage.gotoCreate();
|
|
397
|
+
|
|
398
|
+
await formPage.selectRole('Admin');
|
|
399
|
+
|
|
400
|
+
// Verify selection (Angular Material)
|
|
401
|
+
await expect(formPage.roleSelect).toContainText('Admin');
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Multi-Select
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
test('should select multiple tags', async ({ page }) => {
|
|
409
|
+
await page.goto('/products/new');
|
|
410
|
+
|
|
411
|
+
const tagSelect = page.getByLabel('Tags');
|
|
412
|
+
await tagSelect.click();
|
|
413
|
+
await page.getByRole('option', { name: 'Electronics' }).click();
|
|
414
|
+
await page.getByRole('option', { name: 'Featured' }).click();
|
|
415
|
+
await page.keyboard.press('Escape');
|
|
416
|
+
|
|
417
|
+
await expect(page.getByText('Electronics')).toBeVisible();
|
|
418
|
+
await expect(page.getByText('Featured')).toBeVisible();
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Searchable Select / Autocomplete
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
test('should filter and select from autocomplete', async ({ page }) => {
|
|
426
|
+
await page.goto('/users/new');
|
|
427
|
+
|
|
428
|
+
const cityInput = page.getByLabel('City');
|
|
429
|
+
await cityInput.fill('New');
|
|
430
|
+
|
|
431
|
+
const options = page.getByRole('option');
|
|
432
|
+
await expect(options).toHaveCount(3); // New York, New Delhi, New Orleans
|
|
433
|
+
await page.getByRole('option', { name: 'New York' }).click();
|
|
434
|
+
|
|
435
|
+
await expect(cityInput).toHaveValue('New York');
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Tables
|
|
440
|
+
|
|
441
|
+
### Sort Column
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
test('should sort table by name column', async ({ page }) => {
|
|
445
|
+
const listPage = new UserListPage(page);
|
|
446
|
+
await listPage.goto();
|
|
447
|
+
|
|
448
|
+
await listPage.sortByColumn('Name');
|
|
449
|
+
|
|
450
|
+
const firstCell = listPage.cellInRow(0, 'name');
|
|
451
|
+
const firstName = await firstCell.textContent();
|
|
452
|
+
|
|
453
|
+
await listPage.sortByColumn('Name'); // Sort descending
|
|
454
|
+
|
|
455
|
+
const firstCellDesc = listPage.cellInRow(0, 'name');
|
|
456
|
+
const firstNameDesc = await firstCellDesc.textContent();
|
|
457
|
+
|
|
458
|
+
expect(firstName).not.toEqual(firstNameDesc);
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Pagination
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
test('should navigate between pages', async ({ page }) => {
|
|
466
|
+
const listPage = new UserListPage(page);
|
|
467
|
+
await listPage.goto();
|
|
468
|
+
|
|
469
|
+
const firstPageLabel = await listPage.paginatorLabel.textContent();
|
|
470
|
+
await listPage.goToNextPage();
|
|
471
|
+
|
|
472
|
+
const secondPageLabel = await listPage.paginatorLabel.textContent();
|
|
473
|
+
expect(firstPageLabel).not.toEqual(secondPageLabel);
|
|
474
|
+
|
|
475
|
+
await listPage.goToPreviousPage();
|
|
476
|
+
const backToFirstLabel = await listPage.paginatorLabel.textContent();
|
|
477
|
+
expect(backToFirstLabel).toEqual(firstPageLabel);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('should change page size', async ({ page }) => {
|
|
481
|
+
const listPage = new UserListPage(page);
|
|
482
|
+
await listPage.goto();
|
|
483
|
+
|
|
484
|
+
await listPage.changePageSize('50');
|
|
485
|
+
const rowCount = await listPage.getRowCount();
|
|
486
|
+
expect(rowCount).toBeLessThanOrEqual(50);
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Empty State
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
test('should display empty state when no results', async ({ page }) => {
|
|
494
|
+
const listPage = new UserListPage(page);
|
|
495
|
+
await listPage.goto();
|
|
496
|
+
|
|
497
|
+
await listPage.search('xyznonexistentzyx');
|
|
498
|
+
|
|
499
|
+
await expect(listPage.emptyState).toBeVisible();
|
|
500
|
+
await expect(listPage.tableRows).toHaveCount(0);
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Loading State
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
test('should show loading indicator during data fetch', async ({ page }) => {
|
|
508
|
+
const listPage = new UserListPage(page);
|
|
509
|
+
|
|
510
|
+
// Slow down API response to catch loading state
|
|
511
|
+
await page.route('**/api/users*', async route => {
|
|
512
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
513
|
+
await route.continue();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
await listPage.goto();
|
|
517
|
+
await expect(listPage.loadingSpinner).toBeVisible();
|
|
518
|
+
await expect(listPage.loadingSpinner).not.toBeVisible({ timeout: 10000 });
|
|
519
|
+
await expect(listPage.tableRows.first()).toBeVisible();
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Row Click
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
test('should navigate to detail on row click', async ({ page }) => {
|
|
527
|
+
const listPage = new UserListPage(page);
|
|
528
|
+
await listPage.goto();
|
|
529
|
+
|
|
530
|
+
await listPage.rowByIndex(0).click();
|
|
531
|
+
|
|
532
|
+
await expect(page).toHaveURL(/\/users\/[^/]+$/);
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## Responsive
|
|
537
|
+
|
|
538
|
+
### Mobile Layout
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
import { test, expect, devices } from '@playwright/test';
|
|
542
|
+
|
|
543
|
+
test.describe('Mobile Layout', () => {
|
|
544
|
+
test.use({ ...devices['iPhone 13'] });
|
|
545
|
+
|
|
546
|
+
test('should show hamburger menu on mobile', async ({ page }) => {
|
|
547
|
+
await page.goto('/dashboard');
|
|
548
|
+
|
|
549
|
+
const nav = new NavigationComponent(page);
|
|
550
|
+
await expect(nav.hamburgerMenu).toBeVisible();
|
|
551
|
+
await expect(nav.sidebar).not.toBeVisible();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('should open navigation drawer on hamburger click', async ({ page }) => {
|
|
555
|
+
await page.goto('/dashboard');
|
|
556
|
+
|
|
557
|
+
const nav = new NavigationComponent(page);
|
|
558
|
+
await nav.toggleSidebar();
|
|
559
|
+
await expect(nav.sidebar).toBeVisible();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test('should close drawer after navigation', async ({ page }) => {
|
|
563
|
+
await page.goto('/dashboard');
|
|
564
|
+
|
|
565
|
+
const nav = new NavigationComponent(page);
|
|
566
|
+
await nav.toggleSidebar();
|
|
567
|
+
await nav.navigateTo('Users');
|
|
568
|
+
|
|
569
|
+
await expect(page).toHaveURL(/\/users/);
|
|
570
|
+
await expect(nav.sidebar).not.toBeVisible();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test('should stack form fields vertically on mobile', async ({ page }) => {
|
|
574
|
+
await page.goto('/users/new');
|
|
575
|
+
|
|
576
|
+
const nameInput = page.getByLabel('Name');
|
|
577
|
+
const emailInput = page.getByLabel('Email');
|
|
578
|
+
|
|
579
|
+
const nameBox = await nameInput.boundingBox();
|
|
580
|
+
const emailBox = await emailInput.boundingBox();
|
|
581
|
+
|
|
582
|
+
// Fields should be stacked (email below name)
|
|
583
|
+
expect(emailBox!.y).toBeGreaterThan(nameBox!.y);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Tablet Layout
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
test.describe('Tablet Layout', () => {
|
|
592
|
+
test.use({ viewport: { width: 768, height: 1024 } });
|
|
593
|
+
|
|
594
|
+
test('should show collapsed sidebar on tablet', async ({ page }) => {
|
|
595
|
+
await page.goto('/dashboard');
|
|
596
|
+
|
|
597
|
+
const nav = new NavigationComponent(page);
|
|
598
|
+
const sidebarBox = await nav.sidebar.boundingBox();
|
|
599
|
+
|
|
600
|
+
// Collapsed sidebar should be narrow (icons only)
|
|
601
|
+
expect(sidebarBox!.width).toBeLessThan(100);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## Accessibility
|
|
607
|
+
|
|
608
|
+
### Keyboard Navigation
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
test('should navigate form fields with Tab', async ({ page }) => {
|
|
612
|
+
await page.goto('/users/new');
|
|
613
|
+
|
|
614
|
+
await page.keyboard.press('Tab');
|
|
615
|
+
await expect(page.getByLabel('Name')).toBeFocused();
|
|
616
|
+
|
|
617
|
+
await page.keyboard.press('Tab');
|
|
618
|
+
await expect(page.getByLabel('Email')).toBeFocused();
|
|
619
|
+
|
|
620
|
+
await page.keyboard.press('Tab');
|
|
621
|
+
await expect(page.getByLabel('Phone')).toBeFocused();
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('should submit form with Enter key', async ({ page }) => {
|
|
625
|
+
const loginPage = new LoginPage(page);
|
|
626
|
+
await loginPage.goto();
|
|
627
|
+
|
|
628
|
+
await loginPage.fillEmail('admin@example.com');
|
|
629
|
+
await loginPage.fillPassword('SecureP@ss123');
|
|
630
|
+
await page.keyboard.press('Enter');
|
|
631
|
+
|
|
632
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
633
|
+
});
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### ARIA Labels
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
test('should have proper ARIA labels on interactive elements', async ({ page }) => {
|
|
640
|
+
await page.goto('/users');
|
|
641
|
+
|
|
642
|
+
// Buttons should have accessible names
|
|
643
|
+
const addButton = page.getByRole('button', { name: /add|create/i });
|
|
644
|
+
await expect(addButton).toBeVisible();
|
|
645
|
+
|
|
646
|
+
// Table should have proper role
|
|
647
|
+
await expect(page.getByRole('table')).toBeVisible();
|
|
648
|
+
|
|
649
|
+
// Column headers should have sort indicators
|
|
650
|
+
const sortableHeader = page.getByRole('columnheader', { name: /name/i });
|
|
651
|
+
await expect(sortableHeader).toHaveAttribute('aria-sort', /(ascending|descending|none)/);
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Axe Accessibility Audit
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
import AxeBuilder from '@axe-core/playwright';
|
|
659
|
+
|
|
660
|
+
test('should have no accessibility violations on user list page', async ({ page }) => {
|
|
661
|
+
await page.goto('/users');
|
|
662
|
+
|
|
663
|
+
const results = await new AxeBuilder({ page })
|
|
664
|
+
.withTags(['wcag2a', 'wcag2aa'])
|
|
665
|
+
.analyze();
|
|
666
|
+
|
|
667
|
+
expect(results.violations).toEqual([]);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test('should have no accessibility violations on form page', async ({ page }) => {
|
|
671
|
+
await page.goto('/users/new');
|
|
672
|
+
|
|
673
|
+
const results = await new AxeBuilder({ page })
|
|
674
|
+
.withTags(['wcag2a', 'wcag2aa'])
|
|
675
|
+
.exclude('.third-party-widget') // Exclude elements you can't control
|
|
676
|
+
.analyze();
|
|
677
|
+
|
|
678
|
+
expect(results.violations).toEqual([]);
|
|
679
|
+
});
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Focus Management
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
test('should return focus to trigger element after dialog closes', async ({ page }) => {
|
|
686
|
+
await page.goto('/users');
|
|
687
|
+
|
|
688
|
+
const deleteButton = page.getByRole('button', { name: /delete/i }).first();
|
|
689
|
+
await deleteButton.click();
|
|
690
|
+
|
|
691
|
+
const dialog = new ConfirmDialogComponent(page);
|
|
692
|
+
await dialog.cancel();
|
|
693
|
+
|
|
694
|
+
await expect(deleteButton).toBeFocused();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test('should trap focus inside open modal', async ({ page }) => {
|
|
698
|
+
await page.goto('/users');
|
|
699
|
+
|
|
700
|
+
await page.getByRole('button', { name: /delete/i }).first().click();
|
|
701
|
+
const dialog = page.getByRole('dialog');
|
|
702
|
+
await expect(dialog).toBeVisible();
|
|
703
|
+
|
|
704
|
+
// Tab through all focusable elements in dialog
|
|
705
|
+
await page.keyboard.press('Tab');
|
|
706
|
+
const focused1 = await page.evaluate(() => document.activeElement?.closest('[role="dialog"]'));
|
|
707
|
+
expect(focused1).not.toBeNull();
|
|
708
|
+
|
|
709
|
+
await page.keyboard.press('Tab');
|
|
710
|
+
const focused2 = await page.evaluate(() => document.activeElement?.closest('[role="dialog"]'));
|
|
711
|
+
expect(focused2).not.toBeNull();
|
|
712
|
+
});
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
## Auth Flows
|
|
716
|
+
|
|
717
|
+
### Login
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
test.describe('Login Flow', () => {
|
|
721
|
+
test('should login and redirect to dashboard', async ({ page }) => {
|
|
722
|
+
const loginPage = new LoginPage(page);
|
|
723
|
+
await loginPage.goto();
|
|
724
|
+
|
|
725
|
+
await loginPage.login('admin@example.com', 'SecureP@ss123');
|
|
726
|
+
|
|
727
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
728
|
+
await expect(page.getByText(/welcome|dashboard/i)).toBeVisible();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test('should show error for invalid credentials', async ({ page }) => {
|
|
732
|
+
const loginPage = new LoginPage(page);
|
|
733
|
+
await loginPage.goto();
|
|
734
|
+
|
|
735
|
+
await loginPage.login('wrong@example.com', 'WrongPass');
|
|
736
|
+
|
|
737
|
+
await expect(page).toHaveURL(/\/login/);
|
|
738
|
+
await expect(loginPage.generalError).toBeVisible();
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### Logout
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
test('should logout and redirect to login', async ({ page }) => {
|
|
747
|
+
await page.goto('/dashboard');
|
|
748
|
+
|
|
749
|
+
const nav = new NavigationComponent(page);
|
|
750
|
+
await nav.logout();
|
|
751
|
+
|
|
752
|
+
await expect(page).toHaveURL(/\/login/);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test('should not access protected routes after logout', async ({ page }) => {
|
|
756
|
+
await page.goto('/dashboard');
|
|
757
|
+
|
|
758
|
+
const nav = new NavigationComponent(page);
|
|
759
|
+
await nav.logout();
|
|
760
|
+
|
|
761
|
+
await page.goto('/users');
|
|
762
|
+
await expect(page).toHaveURL(/\/login/);
|
|
763
|
+
});
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### Session Expiry
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
test('should handle expired session gracefully', async ({ page }) => {
|
|
770
|
+
await page.goto('/dashboard');
|
|
771
|
+
|
|
772
|
+
// Clear auth tokens to simulate session expiry
|
|
773
|
+
await page.evaluate(() => {
|
|
774
|
+
localStorage.removeItem('authToken');
|
|
775
|
+
sessionStorage.removeItem('authToken');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Next API call should trigger redirect
|
|
779
|
+
await page.goto('/users');
|
|
780
|
+
await expect(page).toHaveURL(/\/login/);
|
|
781
|
+
});
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Role-Based Visibility
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
test('should hide admin-only elements for regular users', async ({ page }) => {
|
|
788
|
+
// Login as regular user
|
|
789
|
+
await page.goto('/login');
|
|
790
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
791
|
+
await page.getByLabel('Password').fill('UserP@ss123');
|
|
792
|
+
await page.getByRole('button', { name: /login/i }).click();
|
|
793
|
+
|
|
794
|
+
await page.goto('/users');
|
|
795
|
+
|
|
796
|
+
// Delete button should not be visible for regular users
|
|
797
|
+
await expect(page.getByRole('button', { name: /delete/i })).not.toBeVisible();
|
|
798
|
+
|
|
799
|
+
// Settings link should not be in nav
|
|
800
|
+
const nav = new NavigationComponent(page);
|
|
801
|
+
await expect(nav.navLink('Settings')).not.toBeVisible();
|
|
802
|
+
});
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
## Error Handling
|
|
806
|
+
|
|
807
|
+
### API Failure
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
test('should display error message when API fails', async ({ page }) => {
|
|
811
|
+
// Mock API failure
|
|
812
|
+
await page.route('**/api/users*', route => {
|
|
813
|
+
route.fulfill({
|
|
814
|
+
status: 500,
|
|
815
|
+
contentType: 'application/json',
|
|
816
|
+
body: JSON.stringify({ message: 'Internal Server Error' }),
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
await page.goto('/users');
|
|
821
|
+
|
|
822
|
+
await expect(page.getByText(/error|failed|something went wrong/i)).toBeVisible();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
test('should offer retry option on API failure', async ({ page }) => {
|
|
826
|
+
let callCount = 0;
|
|
827
|
+
|
|
828
|
+
await page.route('**/api/users*', route => {
|
|
829
|
+
callCount++;
|
|
830
|
+
if (callCount === 1) {
|
|
831
|
+
route.fulfill({ status: 500, body: '{}' });
|
|
832
|
+
} else {
|
|
833
|
+
route.continue();
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
await page.goto('/users');
|
|
838
|
+
await expect(page.getByText(/error|failed/i)).toBeVisible();
|
|
839
|
+
|
|
840
|
+
await page.getByRole('button', { name: /retry|try again/i }).click();
|
|
841
|
+
await expect(page.locator('mat-table, table')).toBeVisible();
|
|
842
|
+
});
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### Network Offline
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
test('should show offline indicator when network is down', async ({ page, context }) => {
|
|
849
|
+
await page.goto('/dashboard');
|
|
850
|
+
|
|
851
|
+
await context.setOffline(true);
|
|
852
|
+
|
|
853
|
+
// Trigger a navigation or action that needs network
|
|
854
|
+
await page.getByRole('link', { name: /users/i }).click();
|
|
855
|
+
|
|
856
|
+
await expect(page.getByText(/offline|network|connection/i)).toBeVisible();
|
|
857
|
+
|
|
858
|
+
await context.setOffline(false);
|
|
859
|
+
});
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Form Submit API Error
|
|
863
|
+
|
|
864
|
+
```typescript
|
|
865
|
+
test('should display server error on form submit failure', async ({ page }) => {
|
|
866
|
+
await page.route('**/api/users', route => {
|
|
867
|
+
if (route.request().method() === 'POST') {
|
|
868
|
+
route.fulfill({
|
|
869
|
+
status: 422,
|
|
870
|
+
contentType: 'application/json',
|
|
871
|
+
body: JSON.stringify({
|
|
872
|
+
message: 'A user with this email already exists',
|
|
873
|
+
}),
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
route.continue();
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const formPage = new UserFormPage(page);
|
|
881
|
+
await formPage.gotoCreate();
|
|
882
|
+
|
|
883
|
+
await formPage.fillAndSubmit({
|
|
884
|
+
name: 'Jane Smith',
|
|
885
|
+
email: 'existing@example.com',
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
await expect(page.getByRole('alert')).toContainText(/already exists|duplicate/i);
|
|
889
|
+
// Should stay on form page, not navigate away
|
|
890
|
+
await expect(page).toHaveURL(/\/users\/new/);
|
|
891
|
+
});
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
## File Upload
|
|
895
|
+
|
|
896
|
+
```typescript
|
|
897
|
+
test('should upload avatar image', async ({ page }) => {
|
|
898
|
+
const formPage = new UserFormPage(page);
|
|
899
|
+
await formPage.gotoEdit('123');
|
|
900
|
+
|
|
901
|
+
const fileChooserPromise = page.waitForEvent('filechooser');
|
|
902
|
+
await page.getByRole('button', { name: /upload|choose.*file/i }).click();
|
|
903
|
+
const fileChooser = await fileChooserPromise;
|
|
904
|
+
await fileChooser.setFiles('e2e/fixtures/avatar.png');
|
|
905
|
+
|
|
906
|
+
await expect(page.getByRole('img', { name: /avatar|preview/i })).toBeVisible();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test('should reject files exceeding size limit', async ({ page }) => {
|
|
910
|
+
const formPage = new UserFormPage(page);
|
|
911
|
+
await formPage.gotoEdit('123');
|
|
912
|
+
|
|
913
|
+
// Use setInputFiles directly for input[type="file"]
|
|
914
|
+
await formPage.avatarFileInput.setInputFiles('e2e/fixtures/large-file.png');
|
|
915
|
+
|
|
916
|
+
await expect(page.getByText(/too large|max.*size|exceeds/i)).toBeVisible();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test('should reject unsupported file types', async ({ page }) => {
|
|
920
|
+
const formPage = new UserFormPage(page);
|
|
921
|
+
await formPage.gotoEdit('123');
|
|
922
|
+
|
|
923
|
+
await formPage.avatarFileInput.setInputFiles('e2e/fixtures/document.exe');
|
|
924
|
+
|
|
925
|
+
await expect(page.getByText(/unsupported|invalid.*type|allowed/i)).toBeVisible();
|
|
926
|
+
});
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
## Tabs
|
|
930
|
+
|
|
931
|
+
```typescript
|
|
932
|
+
test('should switch between tabs', async ({ page }) => {
|
|
933
|
+
await page.goto('/users/123');
|
|
934
|
+
|
|
935
|
+
await page.getByRole('tab', { name: /profile/i }).click();
|
|
936
|
+
await expect(page.getByRole('tabpanel')).toContainText(/name|email/i);
|
|
937
|
+
|
|
938
|
+
await page.getByRole('tab', { name: /activity/i }).click();
|
|
939
|
+
await expect(page.getByRole('tabpanel')).toContainText(/log|history/i);
|
|
940
|
+
|
|
941
|
+
await page.getByRole('tab', { name: /settings/i }).click();
|
|
942
|
+
await expect(page.getByRole('tabpanel')).toContainText(/preferences|notification/i);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test('should navigate tabs with keyboard', async ({ page }) => {
|
|
946
|
+
await page.goto('/users/123');
|
|
947
|
+
|
|
948
|
+
await page.getByRole('tab', { name: /profile/i }).focus();
|
|
949
|
+
await page.keyboard.press('ArrowRight');
|
|
950
|
+
await expect(page.getByRole('tab', { name: /activity/i })).toBeFocused();
|
|
951
|
+
|
|
952
|
+
await page.keyboard.press('ArrowRight');
|
|
953
|
+
await expect(page.getByRole('tab', { name: /settings/i })).toBeFocused();
|
|
954
|
+
});
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
## Notifications / Toasts
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
test('should show success toast after save', async ({ page }) => {
|
|
961
|
+
const formPage = new UserFormPage(page);
|
|
962
|
+
await formPage.gotoCreate();
|
|
963
|
+
|
|
964
|
+
await formPage.fillAndSubmit({
|
|
965
|
+
name: 'Jane Smith',
|
|
966
|
+
email: 'jane@example.com',
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const toast = page.getByRole('alert');
|
|
970
|
+
await expect(toast).toBeVisible();
|
|
971
|
+
await expect(toast).toContainText(/success|created|saved/i);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test('should auto-dismiss toast after timeout', async ({ page }) => {
|
|
975
|
+
const formPage = new UserFormPage(page);
|
|
976
|
+
await formPage.gotoCreate();
|
|
977
|
+
|
|
978
|
+
await formPage.fillAndSubmit({
|
|
979
|
+
name: 'Jane Smith',
|
|
980
|
+
email: 'jane@example.com',
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const toast = page.getByRole('alert');
|
|
984
|
+
await expect(toast).toBeVisible();
|
|
985
|
+
await expect(toast).not.toBeVisible({ timeout: 10000 });
|
|
986
|
+
});
|
|
987
|
+
```
|