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.
@@ -16,7 +16,9 @@ metadata:
16
16
 
17
17
  ## Output
18
18
 
19
- - **Test file**: `tests/playwright/<name>.spec.ts` (e.g. `app-all.spec.ts` for "all")
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 | `app-all.spec.ts` |
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** (Playwright Test Agents terminology):
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 go directly to Step 6.**
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** `tests/playwright/app-all.spec.ts` (smoke regression):
398
+ **"all" mode**: Build and expand Page Objects for future Change tests.
257
399
 
258
- - For each discovered route: navigateassert HTTP 200 assert ready signal visible
259
- - No detailed assertions — just "this page loads without crashing"
260
- - This is a regression baseline — catches when existing pages break
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 — 通过 UI 触发错误
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 仅在 UI 无法触发时使用
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
- ```typescript
306
- // 🚫 False Pass 元素不存在时静默跳过
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 navigation
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 — UI 操作
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 | `browser_console_messages` → `test.skip()` |
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 (see Step 6 Anti-Pattern Warnings for fixes):
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 | Behavior |
464
- | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
465
- | No specs (change mode) | Stop — E2E requires specs. Use "all" mode instead. |
466
- | Sitemap discovery fails ("all" mode) | Continue use homepage links + common paths fallback |
467
- | App has JS errors or HTTP 5xx during exploration | **STOP** see app-knowledge.md Architecture for restart instructions |
468
- | app-all.spec.ts exists | Read and use (never regenerate — regression baseline) |
469
- | app-exploration.md missing (change mode) | **STOP** — Step 4 exploration is mandatory. Explore before generating tests. |
470
- | app-exploration.md exists | Read and use (verify routes still match specs — re-explore if page structure changed) |
471
- | app-knowledge.md exists | Read and use (append new patterns only) |
472
- | test-plan.md exists (change mode) | Read and use (never regenerate) |
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
 
@@ -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(" 5. In Claude Code, run: /opsx:e2e <change-name>"));
131
+ console.log(chalk.gray(" 6. In Claude Code, run: /opsx:e2e <change-name>"));
126
132
  }
127
- console.log(chalk.gray(` ${hasClaude ? "6." : "5."} Or: openspec-pw run <change-name>`));
128
- console.log(chalk.gray(` ${hasClaude ? "6." : "5."} Or: openspec-pw doctor to verify setup\n`));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openspec-playwright",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "description": "OpenSpec + Playwright E2E verification setup tool for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 |
@@ -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 - add selectors for your app's pages
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
- constructor(private page: Page) {}
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
+ }
@@ -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 - customize these selectors for your app
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
- constructor(private page: Page) {}
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
  /**
@@ -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.