openspec-playwright 0.1.34 → 0.1.36

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.
@@ -7,7 +7,7 @@ compatibility: Requires openspec CLI, Playwright (with browsers installed), and
7
7
  **Architecture**: Uses CLI + SKILLs (not `init-agents`). This follows Playwright's recommended approach for coding agents — CLI is more token-efficient than loading MCP tool schemas into context. MCP is used only for Healer (UI inspection on failure).
8
8
  metadata:
9
9
  author: openspec-playwright
10
- version: "2.6"
10
+ version: "2.7"
11
11
  ---
12
12
 
13
13
  ## Input
@@ -112,6 +112,54 @@ Use your file writing capability to create `tests/playwright/<name>.spec.ts`.
112
112
  - Each test: `test('描述性名称', async ({ page }) => { ... })`
113
113
  - Add `@project(user)` / `@project(admin)` on role-specific tests
114
114
 
115
+ ### Anti-Pattern Warnings (Generator)
116
+
117
+ **🚫 NEVER do this — False Pass pattern:**
118
+ ```typescript
119
+ // WRONG: If button doesn't exist, test silently passes and tests nothing
120
+ const btn = page.getByRole('button', { name: '取消' }).first();
121
+ if (await btn.isVisible().catch(() => false)) {
122
+ await btn.click();
123
+ await expect(page.getByText('成功')).toBeVisible();
124
+ }
125
+ // ✅ CORRECT: Use assertion — test fails if element is missing
126
+ await expect(page.getByRole('button', { name: '取消' })).toBeVisible();
127
+ await page.getByRole('button', { name: '取消' }).click();
128
+ await expect(page.getByText('成功')).toBeVisible();
129
+ ```
130
+
131
+ **Why it matters**: A test that passes but skipped its logic gives **false confidence**. It reports green but tests nothing. Worse — if the test modifies data, a skipped run can corrupt state for the next test.
132
+
133
+ **🚫 NEVER rely on Playwright projects for permission filtering:**
134
+ ```typescript
135
+ // WRONG: All tests run under both admin AND user projects — false "16 tests" impression
136
+ projects: [{ name: 'admin' }, { name: 'user' }]
137
+
138
+ // ✅ CORRECT: Use @tag for permission-based test filtering
139
+ test('admin only - activate subscription', { tag: '@admin' }, async ({ page }) => { ... });
140
+ test('user only - view subscription', { tag: '@user' }, async ({ page }) => { ... });
141
+ // Run with: npx playwright test --grep "@admin"
142
+ ```
143
+
144
+ **🚫 NEVER skip auth guard tests:**
145
+ The auth guard is a **critical security feature**. Skipping it leaves a gap in coverage.
146
+ ```typescript
147
+ // ✅ CORRECT: Test auth guard with a FRESH browser context (no cookies, no storage)
148
+ test('redirects unauthenticated user to login', async ({ browser }) => {
149
+ const freshContext = await browser.newContext(); // No session cookies
150
+ const freshPage = await freshContext.newPage();
151
+ await freshPage.goto(`${BASE_URL}/dashboard`);
152
+ await expect(freshPage).toHaveURL(/login|auth/);
153
+ await freshContext.close();
154
+ });
155
+ ```
156
+
157
+ **Always include error path tests** (not just happy paths):
158
+ - API returns 500 → UI error message displayed?
159
+ - API returns 404 → graceful "not found" handling?
160
+ - Network timeout → retry or error UX?
161
+ - Invalid input → validation message shown?
162
+
115
163
  **Example pattern** (from seed.spec.ts):
116
164
  ```typescript
117
165
  import { test, expect } from '@playwright/test';
@@ -173,7 +221,12 @@ If `playwright.config.ts` exists → READ it first. Extract existing `webServer`
173
221
  ```bash
174
222
  openspec-pw run <name> --project=<role>
175
223
  ```
176
- (Add `--project=user` or `--project=admin` for role-specific tests.)
224
+
225
+ **For role-based tests using `@tag`** (recommended over `--project` filtering):
226
+ ```bash
227
+ npx playwright test tests/playwright/<name>.spec.ts --grep "@<role>"
228
+ ```
229
+ The `--project` approach runs ALL tests under each project's credentials — use `@tag` with `--grep` for precise filtering.
177
230
 
178
231
  The CLI handles:
179
232
  - Server lifecycle (start → wait for HTTP → test → stop)
@@ -207,6 +260,7 @@ If tests fail → analyze failures, use **Playwright MCP tools** to inspect UI s
207
260
  3. **Attempt heal** (up to 3 times):
208
261
  - Apply fix using `browser_snapshot` (prefer `getByRole`, `getByLabel`, `getByText`)
209
262
  - Re-run: `openspec-pw run <name> --project=<role>`
263
+
210
264
  4. **After 3 failed attempts**, collect evidence:
211
265
 
212
266
  **Evidence checklist** (in order, stop at first match):
@@ -220,7 +274,31 @@ If tests fail → analyze failures, use **Playwright MCP tools** to inspect UI s
220
274
  - **Test bug**: report with "likely selector change, verify manually at file:line"
221
275
  - Do NOT retry after evidence checklist — evidence is conclusive
222
276
 
223
- ### 9. Report results
277
+ ### 9. False Pass Detection
278
+
279
+ Run **after** the test suite completes (even if all tests pass). Scan for silent skips that give false confidence:
280
+
281
+ **Indicator A — Conditional test logic:**
282
+ Look for patterns in the test file:
283
+ ```typescript
284
+ if (await locator.isVisible().catch(() => false)) { ... }
285
+ ```
286
+ → If test passes, the locator might not exist → check with `browser_snapshot`
287
+ → Report: "Test passed but may have skipped — conditional visibility check detected"
288
+
289
+ **Indicator B — Test ran too fast:**
290
+ A test covering a complex flow that completes in < 200ms is suspicious.
291
+ → Inspect with `browser_snapshot` to confirm page state
292
+ → Report: "Test duration suspiciously short — verify test logic was executed"
293
+
294
+ **Indicator C — Auth guard not tested:**
295
+ If specs mention "protected route" or "redirect to login" but no test uses a fresh browser context:
296
+ → Report: "Auth guard not verified — test uses authenticated context (cookies/storage inherited)"
297
+ → Recommendation: Add a test with `browser.newContext()` (no storageState) to verify the guard
298
+
299
+ If any false-pass indicator is found → add a **⚠️ Coverage Gap** section to the report.
300
+
301
+ ### 10. Report results
224
302
 
225
303
  Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
226
304
 
@@ -265,7 +343,15 @@ Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
265
343
  ## Coverage
266
344
  - [x] Requirement 1
267
345
  - [ ] Requirement 2 (unverified)
268
- ```
346
+
347
+ ## ⚠️ Coverage Gaps
348
+ > Tests passed but coverage gaps were detected. Review carefully.
349
+
350
+ | Test | Gap | Recommendation |
351
+ |------|-----|----------------|
352
+ | ... | Conditional visibility check — test may have skipped | file:line — use `expect().toBeVisible()` |
353
+ | ... | Auth guard uses inherited session | Add fresh context test: `browser.newContext()` |
354
+ | ... | Suspiciously fast execution (<200ms) | Verify test logic was actually executed |
269
355
 
270
356
  ### Updated tasks.md
271
357
  ```
@@ -286,6 +372,7 @@ Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
286
372
  | Test fails (selector) | Healer: snapshot → fix selector → re-run (≤3 attempts) |
287
373
  | Test fails (assertion) | Healer: snapshot → fix assertion → re-run (≤3 attempts) |
288
374
  | 3 heal attempts failed | Confirm root cause → if app bug: `test.skip()` + report; if unclear: report with recommendation |
375
+ | False pass detected | Report coverage gap → add to "⚠️ Coverage Gap" section in report |
289
376
 
290
377
  ## Verification Heuristics
291
378
 
@@ -293,6 +380,8 @@ Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
293
380
  - **Selector robustness**: Prefer `data-testid`, fallback to semantic selectors
294
381
  - **False positives**: If test fails due to test bug (not app bug) → fix the test
295
382
  - **Actionability**: Every failed test needs a specific recommendation
383
+ - **No false passes**: Every passing test must actually execute its test logic — verify absence of `if (isVisible())` conditional patterns
384
+ - **Auth guard verified**: Protected routes must have a test using a fresh browser context (no inherited cookies)
296
385
 
297
386
  ## Guardrails
298
387
 
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openspec-playwright",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "OpenSpec + Playwright E2E verification setup tool for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
package/release-notes.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ## What's Changed
2
2
 
3
- - fix: update release script to include npm publish
3
+ - fix(skill): correct step numbering and resolve --project vs --grep contradiction
4
4
 
5
- **Full Changelog**: https://github.com/wxhou/openspec-playwright/releases/tag/v0.1.34
5
+ **Full Changelog**: https://github.com/wxhou/openspec-playwright/releases/tag/v0.1.36
@@ -61,3 +61,72 @@ test.describe('Application smoke tests', () => {
61
61
  expect(criticalErrors).toHaveLength(0);
62
62
  });
63
63
  });
64
+
65
+ // ──────────────────────────────────────────────
66
+ // Example: Role-based tests with @tag
67
+ // Use tags (@admin, @user) for permission filtering instead of
68
+ // multiple Playwright projects — prevents false "N tests" impressions
69
+ // ──────────────────────────────────────────────
70
+
71
+ // test.describe('Subscription management', () => {
72
+ // test('activate subscription', { tag: '@admin' }, async ({ page }) => {
73
+ // // Admin-only test
74
+ // await page.goto(`${BASE_URL}/admin/subscriptions`);
75
+ // await expect(page.getByRole('button', { name: '激活订阅' })).toBeVisible();
76
+ // });
77
+ //
78
+ // test('view subscription', { tag: '@user' }, async ({ page }) => {
79
+ // // User-only test
80
+ // await page.goto(`${BASE_URL}/subscription`);
81
+ // await expect(page.getByText('当前订阅')).toBeVisible();
82
+ // });
83
+ // });
84
+
85
+ // Run with: npx playwright test --grep "@admin"
86
+ // Run with: npx playwright test --grep "@user"
87
+
88
+ // ──────────────────────────────────────────────
89
+ // Example: Auth guard test with FRESH browser context
90
+ // 🚫 NEVER test auth guard with the same authenticated context!
91
+ // Use browser.newContext() to create a context with NO cookies/storage
92
+ // ──────────────────────────────────────────────
93
+
94
+ // test.describe('Auth guard', () => {
95
+ // test('redirects unauthenticated user to login', async ({ browser }) => {
96
+ // const freshContext = await browser.newContext(); // No session cookies
97
+ // const freshPage = await freshContext.newPage();
98
+ // await freshPage.goto(`${BASE_URL}/dashboard`);
99
+ // await expect(freshPage).toHaveURL(/login|auth|signin/);
100
+ // await freshContext.close();
101
+ // });
102
+ // });
103
+
104
+ // ──────────────────────────────────────────────
105
+ // Example: Error path test
106
+ // Always include error scenarios, not just happy paths
107
+ // ──────────────────────────────────────────────
108
+
109
+ // test.describe('Error handling', () => {
110
+ // test('shows error message on invalid input', async ({ page }) => {
111
+ // await page.goto(`${BASE_URL}/submit`);
112
+ // await page.getByTestId('input').fill('');
113
+ // await page.getByTestId('submit').click();
114
+ // await expect(page.getByTestId('error')).toContainText('不能为空');
115
+ // });
116
+ // });
117
+
118
+ // ──────────────────────────────────────────────
119
+ // Anti-pattern warnings
120
+ // ──────────────────────────────────────────────
121
+
122
+ // 🚫 WRONG — False Pass: test silently passes if button doesn't exist
123
+ // const cancelBtn = page.getByRole('button', { name: '取消订阅' });
124
+ // if (await cancelBtn.isVisible().catch(() => false)) {
125
+ // await cancelBtn.click();
126
+ // await expect(page.getByText('成功')).toBeVisible();
127
+ // }
128
+
129
+ // ✅ CORRECT — Use assertion: test fails if element is missing
130
+ // await expect(page.getByRole('button', { name: '取消订阅' })).toBeVisible();
131
+ // await page.getByRole('button', { name: '取消订阅' }).click();
132
+ // await expect(page.getByText('操作成功')).toBeVisible();
Binary file