openspec-playwright 0.1.34 → 0.1.35
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.
|
|
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';
|
|
@@ -220,6 +268,30 @@ If tests fail → analyze failures, use **Playwright MCP tools** to inspect UI s
|
|
|
220
268
|
- **Test bug**: report with "likely selector change, verify manually at file:line"
|
|
221
269
|
- Do NOT retry after evidence checklist — evidence is conclusive
|
|
222
270
|
|
|
271
|
+
4b. **False Pass Detection (after test run — before reporting success)**
|
|
272
|
+
|
|
273
|
+
Even passing tests can give false confidence. Scan test output for silent skips:
|
|
274
|
+
|
|
275
|
+
**Indicator A — Conditional test logic:**
|
|
276
|
+
Look for patterns in the test file:
|
|
277
|
+
```typescript
|
|
278
|
+
if (await locator.isVisible().catch(() => false)) { ... }
|
|
279
|
+
```
|
|
280
|
+
→ If test passes, the locator might not exist → check with `browser_snapshot`
|
|
281
|
+
→ Report: "Test passed but may have skipped — conditional visibility check detected"
|
|
282
|
+
|
|
283
|
+
**Indicator B — Test ran too fast:**
|
|
284
|
+
A test covering a complex flow that completes in < 200ms is suspicious.
|
|
285
|
+
→ Inspect with `browser_snapshot` to confirm page state
|
|
286
|
+
→ Report: "Test duration suspiciously short — verify test logic was executed"
|
|
287
|
+
|
|
288
|
+
**Indicator C — Auth guard not tested:**
|
|
289
|
+
If specs mention "protected route" or "redirect to login" but no test uses a fresh browser context:
|
|
290
|
+
→ Report: "Auth guard not verified — test uses authenticated context (cookies/storage inherited)"
|
|
291
|
+
→ Recommendation: Add a test with `browser.newContext()` (no storageState) to verify the guard
|
|
292
|
+
|
|
293
|
+
If any false-pass indicator is found → add a **⚠️ Coverage Gap** section to the report.
|
|
294
|
+
|
|
223
295
|
### 9. Report results
|
|
224
296
|
|
|
225
297
|
Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
|
|
@@ -265,7 +337,15 @@ Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
|
|
|
265
337
|
## Coverage
|
|
266
338
|
- [x] Requirement 1
|
|
267
339
|
- [ ] Requirement 2 (unverified)
|
|
268
|
-
|
|
340
|
+
|
|
341
|
+
## ⚠️ Coverage Gaps
|
|
342
|
+
> Tests passed but coverage gaps were detected. Review carefully.
|
|
343
|
+
|
|
344
|
+
| Test | Gap | Recommendation |
|
|
345
|
+
|------|-----|----------------|
|
|
346
|
+
| ... | Conditional visibility check — test may have skipped | file:line — use `expect().toBeVisible()` |
|
|
347
|
+
| ... | Auth guard uses inherited session | Add fresh context test: `browser.newContext()` |
|
|
348
|
+
| ... | Suspiciously fast execution (<200ms) | Verify test logic was actually executed |
|
|
269
349
|
|
|
270
350
|
### Updated tasks.md
|
|
271
351
|
```
|
|
@@ -286,6 +366,7 @@ Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
|
|
|
286
366
|
| Test fails (selector) | Healer: snapshot → fix selector → re-run (≤3 attempts) |
|
|
287
367
|
| Test fails (assertion) | Healer: snapshot → fix assertion → re-run (≤3 attempts) |
|
|
288
368
|
| 3 heal attempts failed | Confirm root cause → if app bug: `test.skip()` + report; if unclear: report with recommendation |
|
|
369
|
+
| False pass detected | Report coverage gap → add to "⚠️ Coverage Gap" section in report |
|
|
289
370
|
|
|
290
371
|
## Verification Heuristics
|
|
291
372
|
|
|
@@ -293,6 +374,8 @@ Read the report at `openspec/reports/playwright-e2e-<name>-<timestamp>.md`.
|
|
|
293
374
|
- **Selector robustness**: Prefer `data-testid`, fallback to semantic selectors
|
|
294
375
|
- **False positives**: If test fails due to test bug (not app bug) → fix the test
|
|
295
376
|
- **Actionability**: Every failed test needs a specific recommendation
|
|
377
|
+
- **No false passes**: Every passing test must actually execute its test logic — verify absence of `if (isVisible())` conditional patterns
|
|
378
|
+
- **Auth guard verified**: Protected routes must have a test using a fresh browser context (no inherited cookies)
|
|
296
379
|
|
|
297
380
|
## Guardrails
|
|
298
381
|
|
|
Binary file
|
package/package.json
CHANGED
package/release-notes.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## What's Changed
|
|
2
2
|
|
|
3
|
-
-
|
|
3
|
+
- feat(skill): add anti-pattern guidance and false-pass detection (v2.7)
|
|
4
4
|
|
|
5
|
-
**Full Changelog**: https://github.com/wxhou/openspec-playwright/releases/tag/v0.1.
|
|
5
|
+
**Full Changelog**: https://github.com/wxhou/openspec-playwright/releases/tag/v0.1.35
|
package/templates/seed.spec.ts
CHANGED
|
@@ -61,3 +61,70 @@ 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
|
+
// if (await page.getByRole('button', { name: '取消' }).isVisible().catch(() => false)) {
|
|
124
|
+
// await page.getByRole('button', { name: '取消' }).click();
|
|
125
|
+
// }
|
|
126
|
+
|
|
127
|
+
// ✅ CORRECT — Use assertion: test fails if element is missing
|
|
128
|
+
// await expect(page.getByRole('button', { name: '取消' })).toBeVisible();
|
|
129
|
+
// await page.getByRole('button', { name: '取消' }).click();
|
|
130
|
+
// await expect(page.getByText('操作成功')).toBeVisible();
|
|
Binary file
|