openspec-playwright 0.1.72 → 0.1.74
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/.claude/skills/openspec-e2e/SKILL.md +385 -72
- package/dist/commands/init.js +23 -3
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/templates/app-exploration.md +13 -0
- package/templates/e2e-test.ts +18 -17
- package/templates/pages/BasePage.ts +171 -0
- package/templates/seed.spec.ts +7 -13
- package/templates/test-plan.md +229 -0
|
@@ -16,7 +16,9 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
## Output
|
|
18
18
|
|
|
19
|
-
- **Test file**: `tests/playwright/<name>.spec.ts`
|
|
19
|
+
- **Test file**: `tests/playwright/<name>.spec.ts`
|
|
20
|
+
- **Page Objects** (all mode): `tests/playwright/pages/<Route>Page.ts`
|
|
21
|
+
- **Auth setup**: `tests/playwright/auth.setup.ts` (if auth required)
|
|
20
22
|
- **Auth setup**: `tests/playwright/auth.setup.ts` (if auth required)
|
|
21
23
|
- **Report**: `openspec/reports/playwright-e2e-<name>-<timestamp>.md`
|
|
22
24
|
- **Test plan**: `openspec/changes/<name>/specs/playwright/test-plan.md` (change mode only)
|
|
@@ -25,20 +27,14 @@ metadata:
|
|
|
25
27
|
|
|
26
28
|
Two modes, same pipeline:
|
|
27
29
|
|
|
28
|
-
| Mode | Command | Route source | Output
|
|
29
|
-
| ------ | ------------------ | ------------------------ |
|
|
30
|
-
| Change | `/opsx:e2e <name>` | OpenSpec specs | `<name>.spec.ts`
|
|
31
|
-
| All | `/opsx:e2e all` | sitemap + homepage crawl | `
|
|
30
|
+
| Mode | Command | Route source | Output |
|
|
31
|
+
| ------ | ------------------ | ------------------------ | ------------------------------- |
|
|
32
|
+
| Change | `/opsx:e2e <name>` | OpenSpec specs | `<name>.spec.ts` |
|
|
33
|
+
| All | `/opsx:e2e all` | sitemap + homepage crawl | `pages/*.ts` (Page Objects) |
|
|
32
34
|
|
|
33
35
|
Both modes update `app-knowledge.md` and `app-exploration.md`. All `.spec.ts` files run together as regression suite.
|
|
34
36
|
|
|
35
|
-
**Role mapping
|
|
36
|
-
|
|
37
|
-
| Role | This SKILL | What it does |
|
|
38
|
-
| --------- | ---------- | ------------------------------------------------------------ |
|
|
39
|
-
| Planner | Step 4–5 | Explores app via Playwright MCP → produces test-plan.md |
|
|
40
|
-
| Generator | Step 6 | Transforms test-plan.md → `.spec.ts` with verified selectors |
|
|
41
|
-
| Healer | Step 9 | Executes tests, repairs failures via Playwright MCP |
|
|
37
|
+
> **Role mapping**: Planner (Step 4–5) → test-plan.md; Generator (Step 6) → `.spec.ts` + Page Objects; Healer (Step 9) → repairs failures via MCP.
|
|
42
38
|
|
|
43
39
|
## Testing principles
|
|
44
40
|
|
|
@@ -77,7 +73,8 @@ Can this be tested through the UI?
|
|
|
77
73
|
|
|
78
74
|
**"all" mode** (`/opsx:e2e all` — no OpenSpec needed):
|
|
79
75
|
|
|
80
|
-
- Announce: "Mode: full app exploration"
|
|
76
|
+
- Announce: "Mode: full app exploration + Page Object discovery"
|
|
77
|
+
- **Goal**: Discover new routes, extract selectors, and build `pages/*.ts` Page Objects — accumulated asset for future Change tests
|
|
81
78
|
- Discover routes via:
|
|
82
79
|
1. Navigate to `${BASE_URL}/sitemap.xml` (if exists)
|
|
83
80
|
2. Navigate to `${BASE_URL}/` → extract all links from snapshot
|
|
@@ -144,6 +141,9 @@ browser_navigate → browser_console_messages → browser_snapshot → browser_t
|
|
|
144
141
|
| JS error in console | App runtime error | **STOP** — tell user: "Page has JS errors. Fix them, then re-run /opsx:e2e." |
|
|
145
142
|
| HTTP 404 | Route not in app (metadata issue) | Continue — mark `⚠️ route not found` in app-exploration.md |
|
|
146
143
|
| Auth required, no credentials | Missing auth setup | Continue — skip protected routes, explore login page |
|
|
144
|
+
| Suspicious network request | API returned 4xx/5xx | Continue — mark `⚠️ API error: <endpoint> returned <code>` in app-exploration.md |
|
|
145
|
+
|
|
146
|
+
**Network monitoring**: After navigating, use `browser_network_requests` to check for failed API calls. Failed requests (status ≥ 400) on a route indicate an API/backend issue — record in `app-exploration.md` for reference.
|
|
147
147
|
|
|
148
148
|
**For guest routes** (no auth):
|
|
149
149
|
|
|
@@ -184,6 +184,98 @@ From `browser_snapshot` output, extract **interactive elements** for each route:
|
|
|
184
184
|
| **Headings** | text content, selector | for assertions |
|
|
185
185
|
| **Error messages** | text patterns, selector | for error path testing |
|
|
186
186
|
| **Dynamic content** | structure — row counts, card layouts | for data-driven tests |
|
|
187
|
+
| **Special elements** | type, selector, dimensions | for canvas/iframe/Shadow DOM test strategies |
|
|
188
|
+
|
|
189
|
+
#### 4.3.1. Detect special elements
|
|
190
|
+
|
|
191
|
+
From `browser_snapshot` + `browser_evaluate`, identify these special elements per route:
|
|
192
|
+
|
|
193
|
+
**Special element detection matrix:**
|
|
194
|
+
|
|
195
|
+
| Element | Snapshot signal | Evaluate supplement | Exploration priority |
|
|
196
|
+
| ------- | --------------- | ---------------------------------------------- | ------------------- |
|
|
197
|
+
| `<canvas>` | `role="img"`, `tagName="CANVAS"` | `canvas.getContext('2d'/'webgl')`, `width`, `height` | High |
|
|
198
|
+
| `<iframe>` | `role="iframe"`, `src` attribute | `frameLocator` available | High |
|
|
199
|
+
| CAPTCHA | `.g-recaptcha`, `.h-captcha`, `[data-sitekey]`, canvas+slider | recaptcha score via API (if configured) | High |
|
|
200
|
+
| OTP / SMS | 6-digit input, countdown timer | Check if dev bypass exists | High |
|
|
201
|
+
| Shadow DOM | `role="generic"` with no children | Check `shadowRoot` via evaluate | Medium |
|
|
202
|
+
| Rich text editor | `[contenteditable]`, `role="textbox"` | `innerHTML`, `getContent()` | Medium |
|
|
203
|
+
| Video / Audio | `role="application"` or name contains "video"/"audio" | `evaluate` checks both `<video>` and `<audio>` tags | Medium |
|
|
204
|
+
| File upload | `<input type="file">` | `accept` attribute, `multiple` flag | Medium |
|
|
205
|
+
| Drag-and-drop | drag events in JS | Simulate DnD via coordinate clicks | Low |
|
|
206
|
+
| Date picker | specific `data-testid` or class patterns | Click triggers → evaluate value | Low (skip unless specs mention) |
|
|
207
|
+
| Infinite scroll | Dynamic row insertion | Count elements before/after scroll | Low |
|
|
208
|
+
| WebSocket / SSE | No DOM signal | Check `browser_console_messages` for WS events | Low |
|
|
209
|
+
|
|
210
|
+
**For each detected special element, capture:**
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
// Canvas — get metadata (check WebGL first to avoid consuming 2D context)
|
|
214
|
+
const canvasData = await browser_evaluate(() => {
|
|
215
|
+
const c = document.querySelector('canvas');
|
|
216
|
+
if (!c) return null;
|
|
217
|
+
// getContext consumes the context — check WebGL2 first, then WebGL1, then 2D
|
|
218
|
+
let context = 'unknown';
|
|
219
|
+
if (c.getContext('webgl2')) context = 'webgl2';
|
|
220
|
+
else if (c.getContext('webgl')) context = 'webgl';
|
|
221
|
+
else if (c.getContext('2d')) context = '2d';
|
|
222
|
+
return {
|
|
223
|
+
id: c.id || '',
|
|
224
|
+
context,
|
|
225
|
+
width: c.width,
|
|
226
|
+
height: c.height,
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Iframe — record frameLocator
|
|
231
|
+
// Note: iframe has src or name attribute
|
|
232
|
+
|
|
233
|
+
// Rich text editor — get content
|
|
234
|
+
const editorContent = await browser_evaluate(() => {
|
|
235
|
+
const el = document.querySelector('[contenteditable]');
|
|
236
|
+
return el ? { tag: el.tagName, content: el.innerHTML, length: el.textContent.length } : null;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Video / Audio — get state via evaluate (snapshot doesn't expose tagName)
|
|
240
|
+
const mediaState = await browser_evaluate(() => {
|
|
241
|
+
const v = document.querySelector('video');
|
|
242
|
+
if (v) return { type: 'video', paused: v.paused, duration: v.duration };
|
|
243
|
+
const a = document.querySelector('audio');
|
|
244
|
+
if (a) return { type: 'audio', paused: a.paused, duration: a.duration };
|
|
245
|
+
return null;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// contenteditable — detect via evaluate
|
|
249
|
+
const isContentEditable = await browser_evaluate(() => {
|
|
250
|
+
const el = document.querySelector('[contenteditable]');
|
|
251
|
+
return !!el;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// CAPTCHA — detect type
|
|
255
|
+
const captchaInfo = await browser_evaluate(() => {
|
|
256
|
+
const recaptcha = document.querySelector('.g-recaptcha, [data-sitekey]');
|
|
257
|
+
if (recaptcha) return { type: 'recaptcha', sitekey: recaptcha.getAttribute('data-sitekey') };
|
|
258
|
+
const hcaptcha = document.querySelector('.h-captcha');
|
|
259
|
+
if (hcaptcha) return { type: 'hcaptcha', sitekey: hcaptcha.getAttribute('data-sitekey') };
|
|
260
|
+
const turnstile = document.querySelector('[data-sitekey*="cloudflare"]');
|
|
261
|
+
if (turnstile) return { type: 'turnstile' };
|
|
262
|
+
const canvas = document.querySelector('canvas[class*="captcha"]');
|
|
263
|
+
if (canvas) return { type: 'canvas-captcha' };
|
|
264
|
+
const slider = document.querySelector('[class*="slider"], [class*="drag"]');
|
|
265
|
+
if (slider) return { type: 'slider-captcha' };
|
|
266
|
+
return null;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// OTP input — detect
|
|
270
|
+
const otpInfo = await browser_evaluate(() => {
|
|
271
|
+
const inputs = document.querySelectorAll('input');
|
|
272
|
+
const otpInputs = Array.from(inputs).filter(i => i.maxLength === 1 && i.type === 'text' || i.type === 'tel');
|
|
273
|
+
if (otpInputs.length >= 4) return { type: 'otp-sms', digits: otpInputs.length };
|
|
274
|
+
return null;
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Record findings in `app-exploration.md` → **Special Elements Detected** table.
|
|
187
279
|
|
|
188
280
|
#### 4.4. Write app-exploration.md
|
|
189
281
|
|
|
@@ -237,7 +329,16 @@ Read `tests/playwright/app-knowledge.md` as context for cross-change patterns.
|
|
|
237
329
|
|
|
238
330
|
### 5. Generate test plan
|
|
239
331
|
|
|
240
|
-
> **"all" mode: skip this step
|
|
332
|
+
> **"all" mode: skip this step.** No OpenSpec specs → no test-plan to generate. All mode skips test-plan verification — Page Objects are discovered incrementally from exploration, not from structured specs.
|
|
333
|
+
|
|
334
|
+
**All mode — brief confirmation before Step 6:**
|
|
335
|
+
```
|
|
336
|
+
## All Mode: Page Object Discovery
|
|
337
|
+
Discovered <N> routes (<M> guest, <K> protected)
|
|
338
|
+
Special elements: <element summary>
|
|
339
|
+
Ready to generate Page Objects for: <page-name>Page.ts, <page-name>Page.ts, ...
|
|
340
|
+
Reply **yes** to proceed, or tell me to exclude routes or adjust strategies.
|
|
341
|
+
```
|
|
241
342
|
|
|
242
343
|
**Change mode — prerequisite**: If `openspec/changes/<name>/specs/playwright/app-exploration.md` does not exist → **STOP**. Run Step 4 (explore application) before generating tests. Without real DOM data from exploration, selectors are guesses and tests will be fragile.
|
|
243
344
|
|
|
@@ -251,13 +352,62 @@ Template: `templates/test-plan.md`
|
|
|
251
352
|
|
|
252
353
|
**Idempotency**: If test-plan.md exists → read and use, do NOT regenerate.
|
|
253
354
|
|
|
355
|
+
**⚠️ Human verification — STOP before generating code.**
|
|
356
|
+
|
|
357
|
+
After creating (or reading existing) test-plan.md, **stop and display the test plan summary** for user confirmation:
|
|
358
|
+
|
|
359
|
+
**Output format** — show the test plan in markdown directly in the conversation:
|
|
360
|
+
|
|
361
|
+
````markdown
|
|
362
|
+
## Test Plan Summary: `<change-name>`
|
|
363
|
+
|
|
364
|
+
**Auth**: required / not required | Roles: ...
|
|
365
|
+
|
|
366
|
+
### Test Cases
|
|
367
|
+
- ✅ `<test-name>` — `<route>`, happy path
|
|
368
|
+
- ✅ `<test-name>` — `<route>`, error path: `<error condition>`
|
|
369
|
+
|
|
370
|
+
### Special Elements
|
|
371
|
+
- ⚠️ **CAPTCHA** at `<route>` — strategy: `auth.setup bypass / skip / api-only`
|
|
372
|
+
- ⚠️ **Canvas/WebGL** at `<route>` — strategy: screenshot + dimensions
|
|
373
|
+
- ⚠️ **OTP** at `<route>` — strategy: test credentials / dev bypass
|
|
374
|
+
- ⚠️ **Iframe** at `<route>` — strategy: frameLocator + assert inner content
|
|
375
|
+
- ⚠️ **Video/Audio** at `<route>` — strategy: play() + assert !paused
|
|
376
|
+
- ⚠️ **File Upload** at `<route>` — strategy: setInputFiles + assert upload
|
|
377
|
+
- ⚠️ **Drag-and-Drop** at `<route>` — strategy: dragAndDrop or evaluate events
|
|
378
|
+
- ⚠️ **WebSocket/SSE** at `<route>` — strategy: waitForResponse + waitForFunction
|
|
379
|
+
|
|
380
|
+
### Not Covered
|
|
381
|
+
- `<element or scenario not testable>`
|
|
382
|
+
````
|
|
383
|
+
|
|
384
|
+
Then ask: "Does this coverage match your intent? Reply **yes** to proceed, or tell me what to add/change."
|
|
385
|
+
|
|
386
|
+
**Why this matters**: Step 5 is the last human-reviewable checkpoint before code generation. Once test code is written, fixes address *how* tests run, not *what* they verify. Reviewing the test plan takes seconds and catches logic errors that Healer cannot fix.
|
|
387
|
+
|
|
388
|
+
**Confirmation criteria**:
|
|
389
|
+
- All scenarios from OpenSpec specs are covered
|
|
390
|
+
- Special elements (Canvas, Iframe, Video, Audio, CAPTCHA, OTP, File Upload, Drag-drop, WebSocket) have correct automation strategy
|
|
391
|
+
- Auth states and roles are accurate
|
|
392
|
+
- Nothing important is missing
|
|
393
|
+
|
|
394
|
+
If the user requests changes → update test-plan.md → re-display summary → re-confirm → proceed.
|
|
395
|
+
|
|
254
396
|
### 6. Generate test file
|
|
255
397
|
|
|
256
|
-
**"all" mode
|
|
398
|
+
**"all" mode**: Build and expand Page Objects for future Change tests.
|
|
257
399
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
400
|
+
**Prerequisite**: If `app-exploration.md` does not exist → **STOP**. Run Step 4 first. All mode explores routes via browser MCP to build exploration data.
|
|
401
|
+
|
|
402
|
+
For each discovered route:
|
|
403
|
+
|
|
404
|
+
1. Read existing `pages/<Route>Page.ts` (if any — incremental, not overwrite)
|
|
405
|
+
2. Navigate to route with correct auth state
|
|
406
|
+
3. browser_snapshot to extract interactive elements (see 4.3 table)
|
|
407
|
+
4. Write or update `pages/<Route>Page.ts` — extend with newly discovered elements
|
|
408
|
+
5. Also write `tests/playwright/app-all.spec.ts` — smoke test (route loads without crash)
|
|
409
|
+
|
|
410
|
+
**Output priority**: Page Objects (`pages/*.ts`) are the primary asset. Smoke test is secondary. Existing Page Objects are never overwritten — only extended.
|
|
261
411
|
|
|
262
412
|
**Change mode** → `tests/playwright/<name>.spec.ts` (functional):
|
|
263
413
|
|
|
@@ -274,6 +424,124 @@ Template: `templates/test-plan.md`
|
|
|
274
424
|
|
|
275
425
|
**Test coverage — empty states**: For list/detail pages, explore the empty state. If the app shows a "no data" UI when the list is empty, generate a test to verify it. Empty states are often missing from specs but are real user paths.
|
|
276
426
|
|
|
427
|
+
**Test coverage — special elements**: Check `app-exploration.md` → **Special Elements Detected** table. For each special element:
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// Canvas — screenshot + dimensions
|
|
431
|
+
test('canvas renders with correct dimensions', async ({ page }) => {
|
|
432
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
433
|
+
const canvas = page.locator('canvas');
|
|
434
|
+
await expect(canvas).toBeVisible();
|
|
435
|
+
const box = await canvas.boundingBox();
|
|
436
|
+
expect(box.width).toBeGreaterThan(0);
|
|
437
|
+
await canvas.screenshot({ path: '__screenshots__/canvas.png' });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Canvas — 2D pixel verification
|
|
441
|
+
test('canvas 2D content is not blank', async ({ page }) => {
|
|
442
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
443
|
+
const hasContent = await page.evaluate(() => {
|
|
444
|
+
const c = document.querySelector('canvas');
|
|
445
|
+
if (!c) return false;
|
|
446
|
+
const ctx = c.getContext('2d');
|
|
447
|
+
if (!ctx) return false;
|
|
448
|
+
const data = ctx.getImageData(0, 0, c.width, c.height).data;
|
|
449
|
+
return data.some((v, i) => i % 4 !== 3 && v !== 0); // non-transparent non-black pixel
|
|
450
|
+
});
|
|
451
|
+
expect(hasContent).toBe(true);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Canvas — WebGL screenshot
|
|
455
|
+
test('canvas WebGL renders', async ({ page }) => {
|
|
456
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
457
|
+
const canvas = page.locator('canvas');
|
|
458
|
+
await expect(canvas).toBeVisible();
|
|
459
|
+
await canvas.screenshot({ path: '__screenshots__/webgl.png' });
|
|
460
|
+
// No pixel comparison — WebGL rendering may vary
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Iframe — switch context
|
|
464
|
+
test('iframe content is accessible', async ({ page }) => {
|
|
465
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
466
|
+
const frame = page.frameLocator('iframe[name="<name>"]');
|
|
467
|
+
await expect(frame.locator('<selector-inside-frame>')).toBeVisible();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Rich text editor — evaluate content
|
|
471
|
+
test('editor content persists', async ({ page }) => {
|
|
472
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
473
|
+
const editor = page.locator('[contenteditable]');
|
|
474
|
+
await editor.click();
|
|
475
|
+
await page.keyboard.type('Hello E2E');
|
|
476
|
+
const content = await page.evaluate(() => {
|
|
477
|
+
const el = document.querySelector('[contenteditable]');
|
|
478
|
+
return el?.textContent;
|
|
479
|
+
});
|
|
480
|
+
expect(content).toContain('Hello E2E');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Video — playback state
|
|
484
|
+
test('video can be played', async ({ page }) => {
|
|
485
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
486
|
+
const video = page.locator('video');
|
|
487
|
+
await expect(video).toBeVisible();
|
|
488
|
+
await video.evaluate((v: HTMLVideoElement) => { v.play(); });
|
|
489
|
+
const isPlaying = await video.evaluate((v: HTMLVideoElement) => !v.paused);
|
|
490
|
+
expect(isPlaying).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Audio — playback state
|
|
494
|
+
test('audio can be played', async ({ page }) => {
|
|
495
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
496
|
+
const audio = page.locator('audio');
|
|
497
|
+
await expect(audio).toBeVisible();
|
|
498
|
+
await audio.evaluate((a: HTMLAudioElement) => { a.play(); });
|
|
499
|
+
const isPlaying = await audio.evaluate((a: HTMLAudioElement) => !a.paused);
|
|
500
|
+
expect(isPlaying).toBe(true);
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
See `templates/test-plan.md` → **Special Element Test Cases** for full templates including Canvas, Video, Audio, Iframe, and Rich Text Editor.
|
|
505
|
+
|
|
506
|
+
**Test coverage — AI-opaque elements**: For CAPTCHA, OTP, slider CAPTCHA, file upload, and drag-drop — elements that Playwright cannot reliably automate:
|
|
507
|
+
|
|
508
|
+
1. Mark the element in `app-exploration.md` → **Special Elements Detected** table with type and automation strategy
|
|
509
|
+
2. Generate the test using the appropriate strategy from `templates/test-plan.md` → **AI-Opaque Elements** section:
|
|
510
|
+
- **CAPTCHA**: Bypass via `auth.setup.ts` storageState, or skip with `test.skip()`, or verify via API
|
|
511
|
+
- **OTP**: Use pre-verified test credentials (`E2E_OTP_CODE` env var), or development bypass flag
|
|
512
|
+
- **File upload**: Use `page.setInputFiles()` with fixture files
|
|
513
|
+
- **Drag-drop**: Use `page.dragAndDrop()` or `page.evaluate()` with custom event dispatching
|
|
514
|
+
3. If the element is truly non-automatable, write `test.skip()` with a comment explaining why, and mark with `/handoff` for manual testing
|
|
515
|
+
|
|
516
|
+
**Test coverage — performance**: Verify Core Web Vitals metrics. If the app specifies performance targets, generate a test:
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// Performance — Core Web Vitals
|
|
520
|
+
test('page loads within performance budget', async ({ page }) => {
|
|
521
|
+
await page.goto(`${BASE_URL}/<route>`);
|
|
522
|
+
await expect(page.getByRole('heading')).toBeVisible();
|
|
523
|
+
const timings = await page.evaluate(() => {
|
|
524
|
+
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
525
|
+
return {
|
|
526
|
+
ttfb: nav.responseStart - nav.requestStart,
|
|
527
|
+
lcp: nav.loadEventEnd - nav.requestStart,
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
expect(timings.ttfb).toBeLessThan(500);
|
|
531
|
+
expect(timings.lcp).toBeLessThan(2500);
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
// 🚫 Avoid for special elements:
|
|
537
|
+
await canvas.screenshot() // screenshot alone — no dimension/size assertion
|
|
538
|
+
await expect(canvas).toHaveScreenshot() // pixel-to-pixel comparison for WebGL
|
|
539
|
+
|
|
540
|
+
// ✅ Always:
|
|
541
|
+
const box = await canvas.boundingBox();
|
|
542
|
+
expect(box.width).toBeGreaterThan(0);
|
|
543
|
+
```
|
|
544
|
+
|
|
277
545
|
**Output format**:
|
|
278
546
|
|
|
279
547
|
- Follow `seed.spec.ts` structure
|
|
@@ -281,52 +549,110 @@ Template: `templates/test-plan.md`
|
|
|
281
549
|
- Each test: `test('描述性名称', async ({ page }) => { ... })`
|
|
282
550
|
- Prefer `data-testid` selectors (see 4.3 table)
|
|
283
551
|
|
|
552
|
+
#### 6.1. Use BasePage for shared navigation and selectors
|
|
553
|
+
|
|
554
|
+
Read `tests/playwright/pages/BasePage.ts` for shared utilities:
|
|
555
|
+
- `goto(path)` — navigation with configurable `waitUntil`
|
|
556
|
+
- `byTestId(id)`, `byRole(role, opts)`, `byLabel(label)`, `byText(text)`, `byPlaceholder(text)` — selector helpers in priority order
|
|
557
|
+
- `click(locator)`, `fill(locator, value)` — safe interactions with built-in `scrollIntoViewIfNeeded`
|
|
558
|
+
- `waitForToast(text?)`, `waitForLoad(spinnerSelector?)` — wait utilities
|
|
559
|
+
- `reload()` — page reload with hydration
|
|
560
|
+
|
|
561
|
+
**AppPage pattern** — extend BasePage for page-specific selectors:
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
// tests/playwright/pages/LoginPage.ts
|
|
565
|
+
import { BasePage } from './BasePage';
|
|
566
|
+
import type { Page } from '@playwright/test';
|
|
567
|
+
|
|
568
|
+
export class LoginPage extends BasePage {
|
|
569
|
+
get usernameInput() { return this.byLabel('用户名'); }
|
|
570
|
+
get passwordInput() { return this.byLabel('密码'); }
|
|
571
|
+
get submitBtn() { return this.byRole('button', { name: '登录' }); }
|
|
572
|
+
|
|
573
|
+
constructor(page: Page) { super(page); }
|
|
574
|
+
|
|
575
|
+
async login(user: string, pass: string) {
|
|
576
|
+
await this.goto('/login');
|
|
577
|
+
await this.usernameInput.fill(user);
|
|
578
|
+
await this.passwordInput.fill(pass);
|
|
579
|
+
await this.submitBtn.click();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
// tests/playwright/<name>.spec.ts
|
|
586
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
587
|
+
|
|
588
|
+
test('user can login', async ({ page }) => {
|
|
589
|
+
const loginPage = new LoginPage(page);
|
|
590
|
+
await loginPage.login('user@example.com', 'password123');
|
|
591
|
+
await loginPage.expectURL(/dashboard/);
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**If a shared page object doesn't exist yet**: define it inline in the spec AND write it to `tests/playwright/pages/<PageName>.ts` so future tests can reuse it.
|
|
596
|
+
|
|
597
|
+
#### 6.2. Selector anti-patterns
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
// 🚫 Fragile — CSS class selectors break on style refactors
|
|
601
|
+
page.locator('.notification-bell')
|
|
602
|
+
page.locator('.header-bar')
|
|
603
|
+
page.locator('.skeleton-overlay')
|
|
604
|
+
|
|
605
|
+
// ✅ Robust — semantic selectors survive style changes
|
|
606
|
+
page.getByRole('button', { name: '通知' })
|
|
607
|
+
page.getByTestId('header-bar')
|
|
608
|
+
page.getByText('加载中')
|
|
609
|
+
|
|
610
|
+
// 🚫 Fragile — CSS ID selectors can duplicate in React HMR
|
|
611
|
+
page.locator('#avatarBtn')
|
|
612
|
+
page.locator('#userAvatarBtn')
|
|
613
|
+
|
|
614
|
+
// ✅ Robust — prefer role/label/testid over CSS ID
|
|
615
|
+
page.getByTestId('user-avatar')
|
|
616
|
+
page.getByRole('button', { name: '用户菜单' })
|
|
617
|
+
|
|
618
|
+
// 🚫 Missing wait — leads to random CI failures
|
|
619
|
+
await page.locator('.submit-btn').click();
|
|
620
|
+
|
|
621
|
+
// ✅ Safe — scroll into view first
|
|
622
|
+
await page.locator('.submit-btn').scrollIntoViewIfNeeded();
|
|
623
|
+
await page.locator('.submit-btn').click();
|
|
624
|
+
|
|
625
|
+
// ✅ Better — use BasePage click with built-in wait
|
|
626
|
+
const app = new AppPage(page);
|
|
627
|
+
await app.click(app.byRole('button', { name: '提交' }));
|
|
628
|
+
```
|
|
629
|
+
|
|
284
630
|
**Code examples — UI first:**
|
|
285
631
|
|
|
286
632
|
```typescript
|
|
287
|
-
// ✅ UI 测试
|
|
633
|
+
// ✅ UI 测试
|
|
288
634
|
await page.goto(`${BASE_URL}/orders`);
|
|
289
635
|
await page.getByRole("button", { name: "新建订单" }).click();
|
|
290
636
|
await page.getByLabel("订单名称").fill("Test Order");
|
|
291
637
|
await page.getByRole("button", { name: "提交" }).click();
|
|
292
638
|
await expect(page.getByText("订单创建成功")).toBeVisible();
|
|
293
639
|
|
|
294
|
-
// ✅ Error path
|
|
640
|
+
// ✅ Error path
|
|
295
641
|
await page.goto(`${BASE_URL}/orders`);
|
|
296
|
-
await page.getByRole("button", { name: "新建订单" }).click();
|
|
297
642
|
await page.getByRole("button", { name: "提交" }).click();
|
|
298
643
|
await expect(page.getByRole("alert")).toContainText("名称不能为空");
|
|
299
644
|
|
|
300
|
-
// ✅ API fallback
|
|
645
|
+
// ✅ API fallback (only when UI cannot reach the scenario)
|
|
301
646
|
const res = await page.request.get(`${BASE_URL}/api/orders/99999`);
|
|
302
647
|
expect(res.status()).toBe(404);
|
|
303
|
-
```
|
|
304
648
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (await btn.isVisible().catch(() => false)) { ... }
|
|
308
|
-
|
|
309
|
-
// ✅ CORRECT
|
|
310
|
-
await expect(page.getByRole('button', { name: '取消' })).toBeVisible();
|
|
311
|
-
|
|
312
|
-
// 🚫 用 API 替代 UI — 失去了端到端的意义
|
|
313
|
-
const res = await page.request.post(`${BASE_URL}/api/login`, { data: credentials });
|
|
314
|
-
|
|
315
|
-
// ✅ CORRECT — 通过 UI 登录
|
|
316
|
-
await page.goto(`${BASE_URL}/login`);
|
|
317
|
-
await page.getByLabel('邮箱').fill(process.env.E2E_USERNAME);
|
|
318
|
-
await page.getByLabel('密码').fill(process.env.E2E_PASSWORD);
|
|
319
|
-
await page.getByRole('button', { name: '登录' }).click();
|
|
320
|
-
await expect(page).toHaveURL(/dashboard/);
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
// ✅ Fresh browser context for auth guard
|
|
325
|
-
test("unauthenticated user redirected to login", async ({ browser }) => {
|
|
649
|
+
// ✅ Auth guard — fresh browser context (no cookies)
|
|
650
|
+
test("redirects to login when unauthenticated", async ({ browser }) => {
|
|
326
651
|
const freshPage = await browser.newContext().newPage();
|
|
327
652
|
await freshPage.goto(`${BASE_URL}/dashboard`);
|
|
328
653
|
await expect(freshPage).toHaveURL(/login|auth/);
|
|
329
654
|
});
|
|
655
|
+
|
|
330
656
|
// ✅ Session — logout clears protected state
|
|
331
657
|
await page.getByRole("button", { name: "退出登录" }).click();
|
|
332
658
|
await expect(page).toHaveURL(/login|auth/);
|
|
@@ -334,21 +660,17 @@ const freshPage2 = await browser.newContext().newPage();
|
|
|
334
660
|
await freshPage2.goto(`${BASE_URL}/dashboard`);
|
|
335
661
|
await expect(freshPage2).toHaveURL(/login|auth/); // session revoked
|
|
336
662
|
|
|
337
|
-
// ✅ Browser history — SPA back/forward
|
|
663
|
+
// ✅ Browser history — SPA back/forward
|
|
338
664
|
await page.goto(`${BASE_URL}/list`);
|
|
339
665
|
await page.getByRole("link", { name: "详情" }).first().click();
|
|
340
666
|
await expect(page).toHaveURL(/detail/);
|
|
341
667
|
await page.goBack();
|
|
342
668
|
await expect(page).toHaveURL(/list/);
|
|
343
|
-
await page.goForward();
|
|
344
|
-
await expect(page).toHaveURL(/detail/);
|
|
345
669
|
|
|
346
|
-
// ✅ File uploads
|
|
670
|
+
// ✅ File uploads
|
|
347
671
|
await page.locator('input[type="file"]').setInputFiles("/path/to/file.pdf");
|
|
348
672
|
```
|
|
349
673
|
|
|
350
|
-
Always include error path tests: UI validation messages, network failure, invalid input. Use `page.request` only for scenarios confirmed unreachable via UI.
|
|
351
|
-
|
|
352
674
|
If the file exists → diff against test-plan, add only missing test cases.
|
|
353
675
|
|
|
354
676
|
### 7. Configure auth (if required)
|
|
@@ -406,13 +728,12 @@ If tests fail → use Playwright MCP tools to inspect UI, fix selectors, re-run.
|
|
|
406
728
|
|
|
407
729
|
**Healer MCP tools** (in order of use):
|
|
408
730
|
|
|
409
|
-
<!-- MCP_VERSION: 0.0.70 -->
|
|
410
|
-
|
|
411
731
|
| Tool | Purpose |
|
|
412
732
|
| -------------------------- | ----------------------------------------------- |
|
|
413
733
|
| `browser_navigate` | Go to the failing test's page |
|
|
414
734
|
| `browser_snapshot` | Get page structure to find equivalent selectors |
|
|
415
735
|
| `browser_console_messages` | Diagnose JS errors that may cause failures |
|
|
736
|
+
| `browser_network_requests` | Diagnose backend/API failures (4xx/5xx) |
|
|
416
737
|
| `browser_take_screenshot` | Visually compare before/after fixes |
|
|
417
738
|
| `browser_run_code` | Execute custom fix logic (optional) |
|
|
418
739
|
|
|
@@ -423,7 +744,7 @@ If tests fail → use Playwright MCP tools to inspect UI, fix selectors, re-run.
|
|
|
423
744
|
|
|
424
745
|
| Failure type | Signal | Action |
|
|
425
746
|
| ---------------------------- | --------------------------------------- | ----------------------------------------------------- |
|
|
426
|
-
| **Network/backend** | `fetch failed`, `net::ERR`, 5xx
|
|
747
|
+
| **Network/backend** | `fetch failed`, `net::ERR`, 4xx/5xx | `browser_network_requests` → `browser_console_messages` → `test.skip()` |
|
|
427
748
|
| **Selector changed** | Element not found | `browser_snapshot` → fix selector → re-run |
|
|
428
749
|
| **Assertion mismatch** | Wrong content/value | `browser_snapshot` → compare → fix assertion → re-run |
|
|
429
750
|
| **Timing issue** | `waitFor`/`page.evaluate` timeout | Switch to `request` API or add `waitFor` → re-run |
|
|
@@ -434,14 +755,12 @@ If tests fail → use Playwright MCP tools to inspect UI, fix selectors, re-run.
|
|
|
434
755
|
|
|
435
756
|
### 10. False Pass Detection
|
|
436
757
|
|
|
437
|
-
Run after test suite completes (even if all pass). Common patterns
|
|
758
|
+
Run after test suite completes (even if all pass). Common patterns:
|
|
438
759
|
|
|
439
760
|
- **Conditional visibility**: `if (locator.isVisible().catch(() => false))` — if test passes, locator may not exist
|
|
440
761
|
- **Too fast**: < 200ms for a complex flow is suspicious
|
|
441
762
|
- **No fresh auth context**: Protected routes without `browser.newContext()`
|
|
442
763
|
|
|
443
|
-
Report any gaps in a **⚠️ Coverage Gap** section.
|
|
444
|
-
|
|
445
764
|
### 11. Report results
|
|
446
765
|
|
|
447
766
|
Read report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`. Present:
|
|
@@ -460,22 +779,16 @@ Reference: `templates/report.md`
|
|
|
460
779
|
|
|
461
780
|
## Graceful Degradation
|
|
462
781
|
|
|
463
|
-
| Scenario
|
|
464
|
-
|
|
|
465
|
-
| No specs (change mode)
|
|
466
|
-
|
|
|
467
|
-
|
|
|
468
|
-
| app-all.spec.ts
|
|
469
|
-
|
|
|
470
|
-
|
|
|
471
|
-
|
|
|
472
|
-
|
|
|
473
|
-
| auth.setup.ts exists | Verify format (update only if stale) |
|
|
474
|
-
| playwright.config.ts exists | Preserve all fields (add only missing) |
|
|
475
|
-
| Test fails (backend) | `test.skip()` + report |
|
|
476
|
-
| Test fails (selector/assertion) | Healer: snapshot → fix → re-run (≤3) |
|
|
477
|
-
| 3 heals failed | Evidence checklist → app bug: `test.skip()`; unclear: report |
|
|
478
|
-
| False pass detected | Add "⚠️ Coverage Gap" to report |
|
|
782
|
+
| Scenario | Behavior |
|
|
783
|
+
| ------- | ------- |
|
|
784
|
+
| No specs / app-exploration.md missing (change mode) | **STOP** |
|
|
785
|
+
| JS errors or HTTP 5xx during exploration | **STOP** |
|
|
786
|
+
| Sitemap fails ("all" mode) | Continue with homepage links fallback |
|
|
787
|
+
| File already exists (app-exploration, test-plan, app-all.spec.ts, Page Objects) | Read and use — never regenerate |
|
|
788
|
+
| Test fails (backend) | `test.skip()` + report |
|
|
789
|
+
| Test fails (selector/assertion) | Healer: snapshot → fix → re-run (≤3) |
|
|
790
|
+
| 3 heals failed | `test.skip()` if app bug; report if unclear |
|
|
791
|
+
| False pass detected | Add "⚠️ Coverage Gap" to report |
|
|
479
792
|
|
|
480
793
|
## Guardrails
|
|
481
794
|
|
package/dist/commands/init.js
CHANGED
|
@@ -103,6 +103,11 @@ export async function init(options) {
|
|
|
103
103
|
console.log(chalk.blue("\n─── Generating Seed Test ───"));
|
|
104
104
|
await generateSeedTest(projectRoot);
|
|
105
105
|
}
|
|
106
|
+
// 6b. Generate shared pages directory
|
|
107
|
+
if (options.seed !== false) {
|
|
108
|
+
console.log(chalk.blue("\n─── Generating Shared Pages ───"));
|
|
109
|
+
await generateSharedPages(projectRoot);
|
|
110
|
+
}
|
|
106
111
|
// 7. Generate app-knowledge.md
|
|
107
112
|
console.log(chalk.blue("\n─── Generating App Knowledge ───"));
|
|
108
113
|
await generateAppKnowledge(projectRoot);
|
|
@@ -120,12 +125,13 @@ export async function init(options) {
|
|
|
120
125
|
console.log(chalk.gray(" 2. Customize tests/playwright/credentials.yaml with your test user"));
|
|
121
126
|
console.log(chalk.gray(" 3. Set credentials: export E2E_USERNAME=xxx E2E_PASSWORD=yyy"));
|
|
122
127
|
console.log(chalk.gray(" 4. Run auth setup: npx playwright test --project=setup"));
|
|
128
|
+
console.log(chalk.gray(" 5. Page objects: extend tests/playwright/pages/BasePage.ts for shared selectors"));
|
|
123
129
|
const hasClaude = existsSync(join(projectRoot, ".claude"));
|
|
124
130
|
if (hasClaude) {
|
|
125
|
-
console.log(chalk.gray("
|
|
131
|
+
console.log(chalk.gray(" 6. In Claude Code, run: /opsx:e2e <change-name>"));
|
|
126
132
|
}
|
|
127
|
-
console.log(chalk.gray(` ${hasClaude ? "
|
|
128
|
-
console.log(chalk.gray(` ${hasClaude ? "
|
|
133
|
+
console.log(chalk.gray(` ${hasClaude ? "7." : "6."} Or: openspec-pw run <change-name>`));
|
|
134
|
+
console.log(chalk.gray(` ${hasClaude ? "8." : "7."} Or: openspec-pw doctor to verify setup\n`));
|
|
129
135
|
console.log(chalk.bold("How it works:"));
|
|
130
136
|
console.log(chalk.gray(" /opsx:e2e reads your OpenSpec specs and runs Playwright"));
|
|
131
137
|
console.log(chalk.gray(" E2E tests through a three-agent pipeline:"));
|
|
@@ -177,6 +183,20 @@ async function generateAppKnowledge(projectRoot) {
|
|
|
177
183
|
console.log(chalk.green(" ✓ Generated: tests/playwright/app-knowledge.md"));
|
|
178
184
|
}
|
|
179
185
|
}
|
|
186
|
+
async function generateSharedPages(projectRoot) {
|
|
187
|
+
const pagesDir = join(projectRoot, "tests", "playwright", "pages");
|
|
188
|
+
mkdirSync(pagesDir, { recursive: true });
|
|
189
|
+
const basePageSrc = join(TEMPLATE_DIR, "pages", "BasePage.ts");
|
|
190
|
+
const basePageDest = join(pagesDir, "BasePage.ts");
|
|
191
|
+
if (existsSync(basePageDest)) {
|
|
192
|
+
console.log(chalk.gray(" - pages/BasePage.ts already exists, skipping"));
|
|
193
|
+
}
|
|
194
|
+
else if (existsSync(basePageSrc)) {
|
|
195
|
+
writeFileSync(basePageDest, readFileSync(basePageSrc));
|
|
196
|
+
console.log(chalk.green(" ✓ Generated: tests/playwright/pages/BasePage.ts"));
|
|
197
|
+
console.log(chalk.gray(" (Extend BasePage to create page objects: pages/LoginPage.ts, etc.)"));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
180
200
|
// Install SKILL reference templates (format guides for LLM to read)
|
|
181
201
|
function installSkillTemplates(projectRoot) {
|
|
182
202
|
const SKILL_DIR = join(projectRoot, ".claude", "skills", "openspec-e2e");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -39,6 +39,19 @@ BASE_URL: <from env or seed.spec.ts>
|
|
|
39
39
|
- <any dynamic content that was observed>
|
|
40
40
|
- <test assertions should use toContainText, not toHaveText for user-specific data>
|
|
41
41
|
|
|
42
|
+
### Special Elements Detected
|
|
43
|
+
|
|
44
|
+
| Element | Type | Context | Dimensions | Test Strategy |
|
|
45
|
+
|---------|------|---------|------------|---------------|
|
|
46
|
+
| | | | | |
|
|
47
|
+
| | | | | |
|
|
48
|
+
|
|
49
|
+
> **Special element type legend**: `canvas-2d` | `canvas-webgl` | `iframe` | `shadow-dom` | `contenteditable` | `video` | `audio` | `datepicker` | `drag-drop` | `infinite-scroll` | `file-upload` | `captcha-image` | `captcha-slider` | `captcha-3d` | `recaptcha` | `hcaptcha` | `turnstile` | `otp-sms` | `otp-totp` | `websocket` | `sse`
|
|
50
|
+
>
|
|
51
|
+
> **AI-opaque strategy**: CAPTCHA/OTP/slider-CAPTCHA — see `templates/test-plan.md` → **AI-Opaque Elements**
|
|
52
|
+
>
|
|
53
|
+
> **Test strategy**: See `templates/test-plan.md` → **Special Element Test Cases**
|
|
54
|
+
|
|
42
55
|
## Exploration Failures
|
|
43
56
|
|
|
44
57
|
| Route | Error | Notes |
|
package/templates/e2e-test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
import { BasePage } from './pages/BasePage';
|
|
2
3
|
|
|
3
4
|
// ──────────────────────────────────────────────
|
|
4
5
|
// Test plan: <change-name>
|
|
@@ -8,24 +9,24 @@ import { test, expect, Page } from '@playwright/test';
|
|
|
8
9
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* Page Object Pattern
|
|
12
|
+
* Page Object Pattern
|
|
13
|
+
*
|
|
14
|
+
* Extend BasePage to add page-specific selectors and actions.
|
|
15
|
+
* Example:
|
|
16
|
+
* class LoginPage extends BasePage {
|
|
17
|
+
* get usernameInput() { return this.byLabel('用户名'); }
|
|
18
|
+
* get passwordInput() { return this.byLabel('密码'); }
|
|
19
|
+
* get submitBtn() { return this.byRole('button', { name: '登录' }); }
|
|
20
|
+
* async login(user: string, pass: string) {
|
|
21
|
+
* await this.usernameInput.fill(user);
|
|
22
|
+
* await this.passwordInput.fill(pass);
|
|
23
|
+
* await this.submitBtn.click();
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
12
26
|
*/
|
|
13
|
-
class AppPage {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
async goto(path: string = '/') {
|
|
17
|
-
await this.page.goto(`${BASE_URL}${path}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async getByTestId(id: string) {
|
|
21
|
-
return this.page.locator(`[data-testid="${id}"]`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async waitForToast(message?: string) {
|
|
25
|
-
if (message) {
|
|
26
|
-
await this.page.getByText(message, { state: 'visible' }).waitFor();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
27
|
+
class AppPage extends BasePage {
|
|
28
|
+
// Add page-specific selectors here
|
|
29
|
+
// Example: get heading() { return this.byRole('heading'); }
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function createPage(page: Page): AppPage {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// BasePage — shared navigation and selector utilities for all page objects
|
|
2
|
+
// Extends this class in tests/playwright/pages/<PageName>.ts to build page objects
|
|
3
|
+
// Customize: add page-specific selectors as getters or methods
|
|
4
|
+
|
|
5
|
+
import { Page, Locator, expect } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
|
8
|
+
|
|
9
|
+
export class BasePage {
|
|
10
|
+
protected page: Page;
|
|
11
|
+
|
|
12
|
+
constructor(page: Page) {
|
|
13
|
+
this.page = page;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ─── Navigation ──────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Navigate to a path. Uses 'domcontentloaded' by default for speed.
|
|
20
|
+
* Pass { waitUntil: 'networkidle' } for SPAs that fetch data on mount.
|
|
21
|
+
*/
|
|
22
|
+
async goto(
|
|
23
|
+
path: string,
|
|
24
|
+
options?: { waitUntil?: 'domcontentloaded' | 'load' | 'networkidle' | 'commit' },
|
|
25
|
+
) {
|
|
26
|
+
const url = path.startsWith('http') ? path : `${BASE_URL}${path}`;
|
|
27
|
+
await this.page.goto(url, {
|
|
28
|
+
waitUntil: options?.waitUntil ?? 'domcontentloaded',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Selector helpers — use in priority order ───────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Best: data-testid survives style changes and text refactors.
|
|
36
|
+
* Use when the dev team adds data-testid to elements.
|
|
37
|
+
*/
|
|
38
|
+
byTestId(id: string): Locator {
|
|
39
|
+
return this.page.getByTestId(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Good: semantic role selectors survive DOM restructuring.
|
|
44
|
+
* Use for buttons, links, form fields, dialogs.
|
|
45
|
+
*/
|
|
46
|
+
byRole(role: Parameters<typeof this.page.getByRole>[0], options?: { name?: string | RegExp; exact?: boolean }): Locator {
|
|
47
|
+
return this.page.getByRole(role, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Good: label selectors are stable for form fields.
|
|
52
|
+
* Use for inputs, selects, textareas with visible labels.
|
|
53
|
+
*/
|
|
54
|
+
byLabel(label: string | RegExp, options?: { exact?: boolean }): Locator {
|
|
55
|
+
return this.page.getByLabel(label, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Okay: text selectors are visible to users but may break on copy changes.
|
|
60
|
+
* Use for assertions, not interactions.
|
|
61
|
+
*/
|
|
62
|
+
byText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
|
63
|
+
return this.page.getByText(text, options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fallback: placeholder text. Stable for form inputs.
|
|
68
|
+
*/
|
|
69
|
+
byPlaceholder(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
|
70
|
+
return this.page.getByPlaceholder(text, options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Safe interactions ──────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Click with automatic scroll-into-view. Prevents "element not interactable" errors.
|
|
77
|
+
*/
|
|
78
|
+
async click(selector: Locator | string, options?: Parameters<Locator['click']>[0]) {
|
|
79
|
+
const el = typeof selector === 'string' ? this.page.locator(selector) : selector;
|
|
80
|
+
await el.scrollIntoViewIfNeeded();
|
|
81
|
+
await el.click(options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fill with automatic scroll-into-view. Clears existing value first.
|
|
86
|
+
*/
|
|
87
|
+
async fill(selector: Locator | string, value: string) {
|
|
88
|
+
const el = typeof selector === 'string' ? this.page.locator(selector) : selector;
|
|
89
|
+
await el.scrollIntoViewIfNeeded();
|
|
90
|
+
await el.fill(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Type with character-by-character input. Triggers keydown/keyup events.
|
|
95
|
+
* Use for editors and inputs that listen to keystroke events.
|
|
96
|
+
*/
|
|
97
|
+
async type(selector: Locator | string, text: string) {
|
|
98
|
+
const el = typeof selector === 'string' ? this.page.locator(selector) : selector;
|
|
99
|
+
await el.scrollIntoViewIfNeeded();
|
|
100
|
+
await el.click();
|
|
101
|
+
await this.page.keyboard.type(text);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Wait utilities ─────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wait for a toast / snackbar / alert message to appear and optionally assert its text.
|
|
108
|
+
*/
|
|
109
|
+
async waitForToast(text?: string | RegExp, timeout = 5000): Promise<Locator> {
|
|
110
|
+
const toast = text
|
|
111
|
+
? this.page.getByText(text, { exact: false }).last()
|
|
112
|
+
: this.page.locator('[role="alert"], .toast, .snackbar, [aria-live="polite"]').last();
|
|
113
|
+
await toast.waitFor({ state: 'visible', timeout });
|
|
114
|
+
return toast;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wait for a spinner / loading indicator to disappear.
|
|
119
|
+
* Call after navigation or async actions.
|
|
120
|
+
* Times out silently — callers should add their own assertion if needed.
|
|
121
|
+
*/
|
|
122
|
+
async waitForLoad(spinnerSelector = '[role="progressbar"], .spinner, .loading', timeout = 10000) {
|
|
123
|
+
const spinner = this.page.locator(spinnerSelector);
|
|
124
|
+
if (await spinner.isVisible().catch(() => false)) {
|
|
125
|
+
await spinner.waitFor({ state: 'hidden', timeout }).catch(() => {
|
|
126
|
+
// Spinner did not disappear within timeout — caller should assert if needed
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Wait for URL to match a pattern. Use after redirects or navigation clicks.
|
|
133
|
+
*/
|
|
134
|
+
async waitForURL(pattern: string | RegExp, timeout = 10000) {
|
|
135
|
+
await this.page.waitForURL(pattern, { timeout });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Assertion shortcuts ───────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Assert current URL matches pattern (after navigation or redirect).
|
|
142
|
+
*/
|
|
143
|
+
async expectURL(pattern: string | RegExp) {
|
|
144
|
+
await expect(this.page).toHaveURL(pattern);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Assert page displays specific text.
|
|
149
|
+
* Note: exact option only applies to string text; RegExp is always partial match.
|
|
150
|
+
*/
|
|
151
|
+
async expectText(text: string | RegExp) {
|
|
152
|
+
await expect(this.page.getByText(text)).toBeVisible();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Auth helpers ──────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Assert user is on a guest (login) page.
|
|
159
|
+
* Use after logout or to verify auth guard redirects correctly.
|
|
160
|
+
*/
|
|
161
|
+
async expectGuest() {
|
|
162
|
+
await expect(this.page.getByRole('button', { name: /login|sign in|登录/i })).toBeVisible();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reload page and wait for hydration (for SPAs).
|
|
167
|
+
*/
|
|
168
|
+
async reload(waitFor = 'domcontentloaded') {
|
|
169
|
+
await this.page.reload({ waitUntil: waitFor as 'domcontentloaded' });
|
|
170
|
+
}
|
|
171
|
+
}
|
package/templates/seed.spec.ts
CHANGED
|
@@ -4,25 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { test, expect, Page, ConsoleMessage } from '@playwright/test';
|
|
6
6
|
import { existsSync } from 'fs';
|
|
7
|
+
import { BasePage } from './pages/BasePage';
|
|
7
8
|
|
|
8
9
|
// Customize these for your application
|
|
9
10
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Page Object Pattern -
|
|
13
|
+
* Page Object Pattern - extends BasePage for shared utilities
|
|
14
|
+
* Add your app's common selectors here
|
|
15
|
+
* Example:
|
|
16
|
+
* get loginButton() { return this.byRole('button', { name: '登录' }); }
|
|
13
17
|
*/
|
|
14
|
-
class AppPage {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
async goto(path: string = '/') {
|
|
18
|
-
await this.page.goto(`${BASE_URL}${path}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Add your app's common selectors here
|
|
22
|
-
// Example:
|
|
23
|
-
// async getLoginButton() {
|
|
24
|
-
// return this.page.locator('button[data-testid="login"]');
|
|
25
|
-
// }
|
|
18
|
+
class AppPage extends BasePage {
|
|
19
|
+
// Add page-specific selectors here
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
/**
|
package/templates/test-plan.md
CHANGED
|
@@ -22,3 +22,232 @@ Generated from: `openspec/changes/<change-name>/specs/`
|
|
|
22
22
|
|
|
23
23
|
**Error paths:**
|
|
24
24
|
- ...
|
|
25
|
+
|
|
26
|
+
## Special Element Test Cases
|
|
27
|
+
|
|
28
|
+
### Canvas — 2D Rendering
|
|
29
|
+
|
|
30
|
+
- **Route**: `/<page>`
|
|
31
|
+
- **Role**: `@role(<role>)`
|
|
32
|
+
- **Auth**: `@auth(required|none)`
|
|
33
|
+
- **Type**: `canvas-2d`
|
|
34
|
+
- **Element**: `<canvas id="...">` or `canvas`
|
|
35
|
+
|
|
36
|
+
**Test approach:**
|
|
37
|
+
1. Navigate to page
|
|
38
|
+
2. Assert canvas visible + dimensions > 0
|
|
39
|
+
3. (Optional) Screenshot for baseline archive
|
|
40
|
+
|
|
41
|
+
**Assertions:**
|
|
42
|
+
- `canvas.boundingBox().width > 0`
|
|
43
|
+
- Screenshot archived for manual review
|
|
44
|
+
|
|
45
|
+
### Canvas — WebGL Rendering
|
|
46
|
+
|
|
47
|
+
- **Route**: `/<page>`
|
|
48
|
+
- **Type**: `canvas-webgl`
|
|
49
|
+
- **Element**: `<canvas>` with WebGL context
|
|
50
|
+
|
|
51
|
+
**Test approach:**
|
|
52
|
+
1. Navigate to page
|
|
53
|
+
2. Assert canvas visible + correct dimensions
|
|
54
|
+
3. Screenshot (no pixel comparison — rendering may vary)
|
|
55
|
+
|
|
56
|
+
### Iframe — Content Accessible
|
|
57
|
+
|
|
58
|
+
- **Route**: `/<page>`
|
|
59
|
+
- **Type**: `iframe`
|
|
60
|
+
- **Element**: `<iframe name="..." src="...">`
|
|
61
|
+
|
|
62
|
+
**Test approach:**
|
|
63
|
+
1. Navigate to page
|
|
64
|
+
2. Use `frameLocator` to switch context
|
|
65
|
+
3. Assert element inside iframe is visible
|
|
66
|
+
|
|
67
|
+
### Rich Text Editor — Content Persists
|
|
68
|
+
|
|
69
|
+
- **Route**: `/<page>`
|
|
70
|
+
- **Type**: `contenteditable`
|
|
71
|
+
- **Element**: `[contenteditable]`, CodeMirror, Monaco
|
|
72
|
+
|
|
73
|
+
**Test approach:**
|
|
74
|
+
1. Navigate to page
|
|
75
|
+
2. Click editor → type content
|
|
76
|
+
3. Evaluate `textContent` or `innerHTML`
|
|
77
|
+
4. Assert content matches input
|
|
78
|
+
|
|
79
|
+
### Video — Playback Control
|
|
80
|
+
|
|
81
|
+
- **Route**: `/<page>`
|
|
82
|
+
- **Type**: `video`
|
|
83
|
+
- **Element**: `<video>`
|
|
84
|
+
|
|
85
|
+
**Test approach:**
|
|
86
|
+
1. Navigate to page
|
|
87
|
+
2. Call `video.play()` via `page.evaluate()`
|
|
88
|
+
3. Assert `!video.paused`
|
|
89
|
+
|
|
90
|
+
### Audio — Playback Control
|
|
91
|
+
|
|
92
|
+
- **Route**: `/<page>`
|
|
93
|
+
- **Type**: `audio`
|
|
94
|
+
- **Element**: `<audio>`
|
|
95
|
+
|
|
96
|
+
**Test approach:**
|
|
97
|
+
1. Navigate to page
|
|
98
|
+
2. Call `audio.play()` via `page.evaluate()`
|
|
99
|
+
3. Assert `!audio.paused`
|
|
100
|
+
|
|
101
|
+
## AI-Opaque Elements: Out of Scope for Automation
|
|
102
|
+
|
|
103
|
+
<!-- Elements that AI cannot reliably interact with. Mark in `app-exploration.md` → Special Elements table. -->
|
|
104
|
+
|
|
105
|
+
### CAPTCHA — Human Verification Required
|
|
106
|
+
|
|
107
|
+
- **Route**: `/<page>`
|
|
108
|
+
- **Type**: `captcha-image | captcha-slider | captcha-3d | recaptcha | hcaptcha | turnstile`
|
|
109
|
+
- **Element**: `<canvas id="...">`, `.g-recaptcha`, `.h-captcha`, `[data-sitekey]`, slider track
|
|
110
|
+
|
|
111
|
+
**Automation strategy — choose one:**
|
|
112
|
+
|
|
113
|
+
#### Strategy A: Auth-Setup Bypass (Recommended)
|
|
114
|
+
Use pre-authenticated sessions from `auth.setup.ts` so CAPTCHA is bypassed before the test runs.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
1. Store authenticated storageState in auth.setup.ts
|
|
118
|
+
2. In test: use storageState to skip login
|
|
119
|
+
3. Verify post-CAPTCHA state via API instead
|
|
120
|
+
4. Assert: API response after CAPTCHA = expected state
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Example assertions:**
|
|
124
|
+
- `API POST /submit-form` returns 200 + `success: true`
|
|
125
|
+
- UI redirects to `/dashboard` after CAPTCHA pass
|
|
126
|
+
- Database record created with correct values
|
|
127
|
+
|
|
128
|
+
#### Strategy B: Skip with /handoff
|
|
129
|
+
If CAPTCHA is the primary interaction under test, mark as non-automatable:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
test.skip('<feature> requires CAPTCHA', async ({ page }) => {
|
|
133
|
+
// Human reviewer must manually pass CAPTCHA
|
|
134
|
+
// Then run: npx playwright test --grep "<feature>"
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Strategy C: API Verification Only
|
|
139
|
+
Test the result of CAPTCHA-protected actions without automating the CAPTCHA:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
1. Manually pass CAPTCHA once → capture resulting session/token
|
|
143
|
+
2. Use that session in subsequent API calls
|
|
144
|
+
3. Assert: API behavior matches CAPTCHA-passed state
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Detected CAPTCHA type** (fill in based on `app-exploration.md`):
|
|
148
|
+
- Type: `<!-- image | slider | 3d-object | recaptcha | hcaptcha | turnstile -->`
|
|
149
|
+
- Bypass method: `<!-- auth.setup | skip | api-only -->`
|
|
150
|
+
- Verified by: `<!-- human-manual | api-response | db-state -->`
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### OTP / SMS Verification — Test Account Bypass
|
|
155
|
+
|
|
156
|
+
- **Route**: `/<page>`
|
|
157
|
+
- **Type**: `otp-sms | otp-email | totp`
|
|
158
|
+
- **Element**: 6-digit input, countdown timer, resend button
|
|
159
|
+
|
|
160
|
+
**Automation strategy:**
|
|
161
|
+
|
|
162
|
+
#### Strategy A: Test Credentials (Recommended)
|
|
163
|
+
Use pre-verified test accounts with known OTP codes.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
1. Set E2E_OTP_CODE=<valid-test-code> in credentials.yaml
|
|
167
|
+
2. In test:
|
|
168
|
+
await page.getByRole('textbox').fill(process.env.E2E_OTP_CODE);
|
|
169
|
+
await page.getByRole('button', { name: 'Verify' }).click();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Strategy B: Development Bypass Flag
|
|
173
|
+
If dev mode disables OTP, use that flag.
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
1. Set BASE_URL to dev environment with OTP disabled
|
|
177
|
+
2. Proceed as normal authenticated user
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Strategy C: API-Only Verification
|
|
181
|
+
Bypass OTP UI entirely and test the protected endpoint directly.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
1. Obtain valid token via API (skip OTP step)
|
|
185
|
+
2. Use token in subsequent API calls
|
|
186
|
+
3. Assert: protected endpoint returns expected data
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### File Upload — Complex Input
|
|
192
|
+
|
|
193
|
+
- **Route**: `/<page>`
|
|
194
|
+
- **Type**: `file-upload`
|
|
195
|
+
- **Element**: `<input type="file">`, drag-and-drop zone
|
|
196
|
+
|
|
197
|
+
**Test approach:**
|
|
198
|
+
```
|
|
199
|
+
1. Create test fixture file: `test fixtures/login-hero.png`
|
|
200
|
+
2. Set `accept: '<mime-type>'` if specified
|
|
201
|
+
3. Use `page.setInputFiles()` for reliable upload
|
|
202
|
+
4. Assert: upload progress completes → success message or preview
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Assertions:**
|
|
206
|
+
- Upload progress bar completes (if shown)
|
|
207
|
+
- File preview renders correctly
|
|
208
|
+
- API response contains uploaded file reference
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### Drag-and-Drop — Custom Implementation
|
|
213
|
+
|
|
214
|
+
- **Route**: `/<page>`
|
|
215
|
+
- **Type**: `drag-drop`
|
|
216
|
+
- **Element**: `[draggable]`, `.drop-zone`, sortable list items
|
|
217
|
+
|
|
218
|
+
**Test approach:**
|
|
219
|
+
```
|
|
220
|
+
1. Identify drag handle and drop target via Playwright MCP snapshot
|
|
221
|
+
2. Use `page.dragAndDrop()` or `locator.dragTo()`
|
|
222
|
+
3. If custom implementation uses JS events, use `page.evaluate()` to dispatch events
|
|
223
|
+
4. Assert: target state reflects the drag result
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Note**: If the drag-drop uses complex physics (e.g., Kanban board, calendar), prefer `page.evaluate()` with custom event dispatching over `dragAndDrop()`.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### WebSocket / Real-Time — Timing Sensitive
|
|
231
|
+
|
|
232
|
+
- **Route**: `/<page>`
|
|
233
|
+
- **Type**: `websocket | sse | polling`
|
|
234
|
+
- **Element**: live data display, notification badge, live chart
|
|
235
|
+
|
|
236
|
+
**Test approach:**
|
|
237
|
+
```
|
|
238
|
+
1. Establish WebSocket/SSE connection via page
|
|
239
|
+
2. Wait for specific message/event: `await page.waitForResponse(/<ws-endpoint>/)`
|
|
240
|
+
3. Assert: DOM updates after message received
|
|
241
|
+
4. Use `page.waitForFunction()` to poll for expected state
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Assertions:**
|
|
245
|
+
- Live data counter increments after server push
|
|
246
|
+
- Notification badge shows correct count
|
|
247
|
+
- Chart redraws with new data points
|
|
248
|
+
|
|
249
|
+
**Note**: Increase test timeout for real-time tests. See `playwright.config.ts` → `timeout`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
> **Reference**: See `app-exploration.md` → **Special Elements Detected** table for per-route specifics.
|