@oalacea/demon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +23 -0
- package/README.md +103 -0
- package/agents/deps-analyzer.js +366 -0
- package/agents/detector.js +570 -0
- package/agents/fix-engine.js +305 -0
- package/agents/perf-analyzer.js +294 -0
- package/agents/test-generator.js +387 -0
- package/agents/test-runner.js +318 -0
- package/bin/Dockerfile +65 -0
- package/bin/cli.js +455 -0
- package/lib/config.js +237 -0
- package/lib/docker.js +207 -0
- package/lib/reporter.js +297 -0
- package/package.json +34 -0
- package/prompts/DEPS_EFFICIENCY.md +558 -0
- package/prompts/E2E.md +491 -0
- package/prompts/EXECUTE.md +782 -0
- package/prompts/INTEGRATION_API.md +484 -0
- package/prompts/INTEGRATION_DB.md +425 -0
- package/prompts/PERF_API.md +433 -0
- package/prompts/PERF_DB.md +430 -0
- package/prompts/REMEDIATION.md +482 -0
- package/prompts/UNIT.md +260 -0
- package/scripts/dev.js +106 -0
- package/templates/README.md +22 -0
- package/templates/k6/load-test.js +54 -0
- package/templates/playwright/e2e.spec.ts +61 -0
- package/templates/vitest/api.test.ts +51 -0
- package/templates/vitest/component.test.ts +27 -0
- package/templates/vitest/hook.test.ts +36 -0
package/prompts/E2E.md
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
# E2E Test Guide (Playwright)
|
|
2
|
+
|
|
3
|
+
This prompt is included by EXECUTE.md. It provides detailed guidance for E2E testing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Playwright Setup
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// tests/e2e/setup.ts
|
|
11
|
+
import { test as base } from '@playwright/test';
|
|
12
|
+
|
|
13
|
+
type AuthFixtures = {
|
|
14
|
+
authenticatedPage: Page;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const test = base.extend<AuthFixtures>({
|
|
18
|
+
authenticatedPage: async ({ page }, use) => {
|
|
19
|
+
// Login before test
|
|
20
|
+
await page.goto('/login');
|
|
21
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
22
|
+
await page.fill('input[name="password"]', 'password123');
|
|
23
|
+
await page.click('button[type="submit"]');
|
|
24
|
+
await page.waitForURL('/dashboard');
|
|
25
|
+
await use(page);
|
|
26
|
+
// Logout after test
|
|
27
|
+
await page.click('[data-testid="logout"]');
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export { expect } from '@playwright/test';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Authentication Flows
|
|
37
|
+
|
|
38
|
+
### Login Flow
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { test, expect } from '@playwright/test';
|
|
42
|
+
|
|
43
|
+
test.describe('Authentication', () => {
|
|
44
|
+
test.beforeEach(async ({ page }) => {
|
|
45
|
+
await page.goto('/login');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should login with valid credentials', async ({ page }) => {
|
|
49
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
50
|
+
await page.fill('input[name="password"]', 'password123');
|
|
51
|
+
await page.click('button[type="submit"]');
|
|
52
|
+
|
|
53
|
+
// Should redirect
|
|
54
|
+
await expect(page).toHaveURL('/dashboard');
|
|
55
|
+
|
|
56
|
+
// Should show user info
|
|
57
|
+
await expect(page.locator('text=Welcome, Test')).toBeVisible();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should show error with invalid credentials', async ({ page }) => {
|
|
61
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
62
|
+
await page.fill('input[name="password"]', 'wrong-password');
|
|
63
|
+
await page.click('button[type="submit"]');
|
|
64
|
+
|
|
65
|
+
// Should show error
|
|
66
|
+
await expect(page.locator('.error')).toContainText('Invalid credentials');
|
|
67
|
+
|
|
68
|
+
// Should stay on login page
|
|
69
|
+
await expect(page).toHaveURL('/login');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('should validate email format', async ({ page }) => {
|
|
73
|
+
await page.fill('input[name="email"]', 'not-an-email');
|
|
74
|
+
await page.fill('input[name="password"]', 'password123');
|
|
75
|
+
await page.click('button[type="submit"]');
|
|
76
|
+
|
|
77
|
+
// Should show inline validation error
|
|
78
|
+
await expect(page.locator('input[name="email"]'))
|
|
79
|
+
.toHaveAttribute('aria-invalid', 'true');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should toggle password visibility', async ({ page }) => {
|
|
83
|
+
await page.fill('input[name="password"]', 'password123');
|
|
84
|
+
const passwordInput = page.locator('input[name="password"]');
|
|
85
|
+
|
|
86
|
+
// Initially masked
|
|
87
|
+
await expect(passwordInput).toHaveAttribute('type', 'password');
|
|
88
|
+
|
|
89
|
+
// Click toggle
|
|
90
|
+
await page.click('button[aria-label="Show password"]');
|
|
91
|
+
await expect(passwordInput).toHaveAttribute('type', 'text');
|
|
92
|
+
|
|
93
|
+
// Click again to hide
|
|
94
|
+
await page.click('button[aria-label="Hide password"]');
|
|
95
|
+
await expect(passwordInput).toHaveAttribute('type', 'password');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Registration Flow
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
test.describe('Registration', () => {
|
|
104
|
+
test('should complete full registration', async ({ page }) => {
|
|
105
|
+
await page.goto('/register');
|
|
106
|
+
|
|
107
|
+
// Step 1: Account details
|
|
108
|
+
await page.fill('input[name="email"]', 'newuser@example.com');
|
|
109
|
+
await page.fill('input[name="password"]', 'SecurePass123!');
|
|
110
|
+
await page.fill('input[name="confirmPassword"]', 'SecurePass123!');
|
|
111
|
+
await page.click('button:has-text("Continue")');
|
|
112
|
+
|
|
113
|
+
// Step 2: Profile details
|
|
114
|
+
await page.fill('input[name="name"]', 'New User');
|
|
115
|
+
await page.selectOption('select[name="country"]', 'US');
|
|
116
|
+
await page.click('button:has-text("Complete")');
|
|
117
|
+
|
|
118
|
+
// Should redirect to onboarding
|
|
119
|
+
await expect(page).toHaveURL(/\/onboarding/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should validate password strength', async ({ page }) => {
|
|
123
|
+
await page.goto('/register');
|
|
124
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
125
|
+
await page.fill('input[name="password"]', 'weak');
|
|
126
|
+
await page.fill('input[name="confirmPassword"]', 'weak');
|
|
127
|
+
|
|
128
|
+
await page.click('button[type="submit"]');
|
|
129
|
+
|
|
130
|
+
// Should show password strength error
|
|
131
|
+
await expect(page.locator('.password-strength')).toContainText('too weak');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Password Reset Flow
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
test.describe('Password Reset', () => {
|
|
140
|
+
test('should request password reset', async ({ page }) => {
|
|
141
|
+
await page.goto('/forgot-password');
|
|
142
|
+
await page.fill('input[name="email"]', 'test@example.com');
|
|
143
|
+
await page.click('button[type="submit"]');
|
|
144
|
+
|
|
145
|
+
await expect(page.locator('.success'))
|
|
146
|
+
.toContainText('Check your email for reset link');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('should reset password with valid token', async ({ page, context }) => {
|
|
150
|
+
// Simulate clicking email link with reset token
|
|
151
|
+
const resetToken = 'valid-reset-token';
|
|
152
|
+
await page.goto(`/reset-password?token=${resetToken}`);
|
|
153
|
+
|
|
154
|
+
await page.fill('input[name="password"]', 'NewPass123!');
|
|
155
|
+
await page.fill('input[name="confirmPassword"]', 'NewPass123!');
|
|
156
|
+
await page.click('button[type="submit"]');
|
|
157
|
+
|
|
158
|
+
await expect(page).toHaveURL('/login');
|
|
159
|
+
await expect(page.locator('.success'))
|
|
160
|
+
.toContainText('Password updated successfully');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should reject invalid reset token', async ({ page }) => {
|
|
164
|
+
await page.goto('/reset-password?token=invalid-token');
|
|
165
|
+
await page.fill('input[name="password"]', 'NewPass123!');
|
|
166
|
+
await page.fill('input[name="confirmPassword"]', 'NewPass123!');
|
|
167
|
+
await page.click('button[type="submit"]');
|
|
168
|
+
|
|
169
|
+
await expect(page.locator('.error')).toContainText('Invalid or expired token');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## CRUD Operations
|
|
177
|
+
|
|
178
|
+
### Create Flow
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
test.describe('Create Post', () => {
|
|
182
|
+
test('should create new post', async ({ page }) => {
|
|
183
|
+
// Login first
|
|
184
|
+
await login(page);
|
|
185
|
+
|
|
186
|
+
await page.click('text=New Post');
|
|
187
|
+
await expect(page).toHaveURL('/posts/new');
|
|
188
|
+
|
|
189
|
+
await page.fill('input[name="title"]', 'Test Post');
|
|
190
|
+
await page.fill('textarea[name="content"]', 'This is test content');
|
|
191
|
+
await page.selectOption('select[name="category"]', 'technology');
|
|
192
|
+
await page.click('button:has-text("Publish")');
|
|
193
|
+
|
|
194
|
+
// Should redirect to post detail
|
|
195
|
+
await expect(page).toHaveURL(/\/posts\/[a-z0-9]+/);
|
|
196
|
+
await expect(page.locator('h1')).toContainText('Test Post');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('should validate required fields', async ({ page }) => {
|
|
200
|
+
await login(page);
|
|
201
|
+
await page.click('text=New Post');
|
|
202
|
+
await page.click('button:has-text("Publish")');
|
|
203
|
+
|
|
204
|
+
// Should show validation errors
|
|
205
|
+
await expect(page.locator('input[name="title"]'))
|
|
206
|
+
.toHaveAttribute('aria-invalid', 'true');
|
|
207
|
+
await expect(page.locator('textarea[name="content"]'))
|
|
208
|
+
.toHaveAttribute('aria-invalid', 'true');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('should save draft', async ({ page }) => {
|
|
212
|
+
await login(page);
|
|
213
|
+
await page.click('text=New Post');
|
|
214
|
+
await page.fill('input[name="title"]', 'Draft Post');
|
|
215
|
+
await page.click('button:has-text("Save Draft")');
|
|
216
|
+
|
|
217
|
+
// Should show success message
|
|
218
|
+
await expect(page.locator('.toast')).toContainText('Draft saved');
|
|
219
|
+
|
|
220
|
+
// Should appear in drafts list
|
|
221
|
+
await page.goto('/posts?status=draft');
|
|
222
|
+
await expect(page.locator('text=Draft Post')).toBeVisible();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Read/View Flow
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
test.describe('View Posts', () => {
|
|
231
|
+
test('should show posts list', async ({ page }) => {
|
|
232
|
+
await page.goto('/posts');
|
|
233
|
+
|
|
234
|
+
// Should show posts
|
|
235
|
+
await expect(page.locator('.post-card')).toHaveCount(10);
|
|
236
|
+
|
|
237
|
+
// Should have pagination
|
|
238
|
+
await expect(page.locator('.pagination')).toBeVisible();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('should filter posts by category', async ({ page }) => {
|
|
242
|
+
await page.goto('/posts');
|
|
243
|
+
await page.click('button:has-text("Categories")');
|
|
244
|
+
await page.click('a:has-text("Technology")');
|
|
245
|
+
|
|
246
|
+
await expect(page).toHaveURL('/posts?category=technology');
|
|
247
|
+
await expect(page.locator('.post-card').first()).toContainText('Technology');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('should search posts', async ({ page }) => {
|
|
251
|
+
await page.goto('/posts');
|
|
252
|
+
await page.fill('input[name="search"]', 'test query');
|
|
253
|
+
await page.press('input[name="search"]', 'Enter');
|
|
254
|
+
|
|
255
|
+
await expect(page).toHaveURL(/search=test/);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('should show post detail', async ({ page }) => {
|
|
259
|
+
await page.goto('/posts');
|
|
260
|
+
await page.click('.post-card:first-child');
|
|
261
|
+
|
|
262
|
+
await expect(page.locator('h1')).toBeVisible();
|
|
263
|
+
await expect(page.locator('.post-content')).toBeVisible();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Update Flow
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
test.describe('Edit Post', () => {
|
|
272
|
+
test('should update existing post', async ({ page }) => {
|
|
273
|
+
await login(page);
|
|
274
|
+
await page.goto('/posts/test-post-id');
|
|
275
|
+
await page.click('button:has-text("Edit")');
|
|
276
|
+
|
|
277
|
+
await expect(page).toHaveURL(/\/posts\/test-post-id\/edit/);
|
|
278
|
+
|
|
279
|
+
await page.fill('input[name="title"]', 'Updated Title');
|
|
280
|
+
await page.click('button:has-text("Save")');
|
|
281
|
+
|
|
282
|
+
await expect(page.locator('h1')).toContainText('Updated Title');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('should show preview', async ({ page }) => {
|
|
286
|
+
await login(page);
|
|
287
|
+
await page.goto('/posts/test-post-id/edit');
|
|
288
|
+
await page.click('button:has-text("Preview")');
|
|
289
|
+
|
|
290
|
+
await expect(page.locator('.preview-mode')).toBeVisible();
|
|
291
|
+
await expect(page.locator('.preview-content')).toContainText('Updated Title');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Delete Flow
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
test.describe('Delete Post', () => {
|
|
300
|
+
test('should delete post with confirmation', async ({ page }) => {
|
|
301
|
+
await login(page);
|
|
302
|
+
await page.goto('/posts/test-post-id');
|
|
303
|
+
await page.click('button:has-text("Delete")');
|
|
304
|
+
|
|
305
|
+
// Should show confirmation dialog
|
|
306
|
+
await expect(page.locator('.dialog')).toContainText(
|
|
307
|
+
'Are you sure you want to delete this post?'
|
|
308
|
+
);
|
|
309
|
+
await page.click('.dialog button:has-text("Delete")');
|
|
310
|
+
|
|
311
|
+
// Should redirect to posts list
|
|
312
|
+
await expect(page).toHaveURL('/posts');
|
|
313
|
+
await expect(page.locator('.toast')).toContainText('Post deleted');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('should cancel delete', async ({ page }) => {
|
|
317
|
+
await login(page);
|
|
318
|
+
await page.goto('/posts/test-post-id');
|
|
319
|
+
await page.click('button:has-text("Delete")');
|
|
320
|
+
await page.click('.dialog button:has-text("Cancel")');
|
|
321
|
+
|
|
322
|
+
// Should stay on page
|
|
323
|
+
await expect(page).toHaveURL(/\/posts\/test-post-id/);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Navigation & Routing
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
test.describe('Navigation', () => {
|
|
334
|
+
test('should navigate via menu', async ({ page }) => {
|
|
335
|
+
await page.goto('/');
|
|
336
|
+
await page.click('a:has-text("About")');
|
|
337
|
+
await expect(page).toHaveURL('/about');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('should use browser back/forward', async ({ page }) => {
|
|
341
|
+
await page.goto('/');
|
|
342
|
+
await page.click('a:has-text("Posts")');
|
|
343
|
+
await page.click('a:has-text("About")');
|
|
344
|
+
|
|
345
|
+
await page.goBack();
|
|
346
|
+
await expect(page).toHaveURL('/posts');
|
|
347
|
+
|
|
348
|
+
await page.goForward();
|
|
349
|
+
await expect(page).toHaveURL('/about');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('should handle direct URL access', async ({ page }) => {
|
|
353
|
+
await page.goto('/posts/test-post');
|
|
354
|
+
await expect(page.locator('h1')).toBeVisible();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('should show 404 for invalid routes', async ({ page }) => {
|
|
358
|
+
await page.goto('/this-page-does-not-exist');
|
|
359
|
+
await expect(page.locator('h1')).toContainText('404');
|
|
360
|
+
await expect(page.locator('text=Page not found')).toBeVisible();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Form Interactions
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
test.describe('Multi-step Form', () => {
|
|
371
|
+
test('should complete wizard', async ({ page }) => {
|
|
372
|
+
await page.goto('/wizard');
|
|
373
|
+
|
|
374
|
+
// Step 1
|
|
375
|
+
await page.fill('input[name="field1"]', 'value1');
|
|
376
|
+
await page.click('button:has-text("Next")');
|
|
377
|
+
await expect(page.locator('.wizard')).toHaveClass(/step-2/);
|
|
378
|
+
|
|
379
|
+
// Step 2
|
|
380
|
+
await page.fill('input[name="field2"]', 'value2');
|
|
381
|
+
await page.click('button:has-text("Next")');
|
|
382
|
+
await expect(page.locator('.wizard')).toHaveClass(/step-3/);
|
|
383
|
+
|
|
384
|
+
// Step 3
|
|
385
|
+
await page.click('button:has-text("Complete")');
|
|
386
|
+
await expect(page).toHaveURL('/success');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('should navigate back in wizard', async ({ page }) => {
|
|
390
|
+
await page.goto('/wizard');
|
|
391
|
+
await page.fill('input[name="field1"]', 'value1');
|
|
392
|
+
await page.click('button:has-text("Next")');
|
|
393
|
+
await page.click('button:has-text("Back")');
|
|
394
|
+
|
|
395
|
+
// Values should be preserved
|
|
396
|
+
await expect(page.locator('input[name="field1"]')).toHaveValue('value1');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Web Vitals Checks
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
test.describe('Performance', () => {
|
|
407
|
+
test('should have good LCP', async ({ page }) => {
|
|
408
|
+
await page.goto('/');
|
|
409
|
+
const metrics = await page.evaluate(() =>
|
|
410
|
+
JSON.stringify(performance.getEntriesByType('navigation'))
|
|
411
|
+
);
|
|
412
|
+
const nav = JSON.parse(metrics)[0];
|
|
413
|
+
|
|
414
|
+
// LCP should be under 2.5s
|
|
415
|
+
expect(nav.loadEventEnd - nav.fetchStart).toBeLessThan(2500);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('should not have layout shifts', async ({ page }) => {
|
|
419
|
+
await page.goto('/');
|
|
420
|
+
|
|
421
|
+
// Wait for page to settle
|
|
422
|
+
await page.waitForLoadState('networkidle');
|
|
423
|
+
|
|
424
|
+
const cls = await page.evaluate(() => {
|
|
425
|
+
return new Promise((resolve) => {
|
|
426
|
+
new PerformanceObserver((list) => {
|
|
427
|
+
const entries = list.getEntries();
|
|
428
|
+
let clsValue = 0;
|
|
429
|
+
for (const entry of entries) {
|
|
430
|
+
if (!entry.hadRecentInput) {
|
|
431
|
+
clsValue += entry.value;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
resolve(clsValue);
|
|
435
|
+
}).observe({ entryTypes: ['layout-shift'] });
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(cls).toBeLessThan(0.1);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Accessibility Tests
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
test.describe('Accessibility', () => {
|
|
450
|
+
test('should have proper heading hierarchy', async ({ page }) => {
|
|
451
|
+
await page.goto('/');
|
|
452
|
+
|
|
453
|
+
const headings = await page.$$eval('h1, h2, h3, h4, h5, h6', (nodes) =>
|
|
454
|
+
nodes.map((n) => n.tagName)
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Should start with h1
|
|
458
|
+
expect(headings[0]).toBe('H1');
|
|
459
|
+
|
|
460
|
+
// Should not skip levels
|
|
461
|
+
for (let i = 1; i < headings.length; i++) {
|
|
462
|
+
const current = parseInt(headings[i][1]);
|
|
463
|
+
const previous = parseInt(headings[i - 1][1]);
|
|
464
|
+
expect(current).toBeLessThanOrEqual(previous + 1);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('should have alt text for images', async ({ page }) => {
|
|
469
|
+
await page.goto('/');
|
|
470
|
+
const images = await page.$$('img');
|
|
471
|
+
for (const img of images) {
|
|
472
|
+
const alt = await img.getAttribute('alt');
|
|
473
|
+
expect(alt).toBeTruthy();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('should be keyboard navigable', async ({ page }) => {
|
|
478
|
+
await page.goto('/');
|
|
479
|
+
|
|
480
|
+
// Tab through interactive elements
|
|
481
|
+
const focusable = await page.$$(
|
|
482
|
+
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
for (const element of focusable) {
|
|
486
|
+
await element.focus();
|
|
487
|
+
await expect(element).toBeFocused();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
```
|