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.
|
|
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
|
-
|
|
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.
|
|
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
package/release-notes.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## What's Changed
|
|
2
2
|
|
|
3
|
-
- fix:
|
|
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.
|
|
5
|
+
**Full Changelog**: https://github.com/wxhou/openspec-playwright/releases/tag/v0.1.36
|
package/templates/seed.spec.ts
CHANGED
|
@@ -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
|