@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/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
+ ```