openspec-playwright 0.1.57 → 0.1.59
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.
|
@@ -10,58 +10,86 @@ metadata:
|
|
|
10
10
|
|
|
11
11
|
## Input
|
|
12
12
|
|
|
13
|
-
- **Change name**: `/opsx:e2e <name>` or
|
|
14
|
-
- **Specs**: `openspec/changes/<name>/specs/*.md`
|
|
13
|
+
- **Change name**: `/opsx:e2e <name>` or `/opsx:e2e all` (full app exploration, no OpenSpec needed)
|
|
14
|
+
- **Specs**: `openspec/changes/<name>/specs/*.md` (if change mode)
|
|
15
15
|
- **Credentials**: `E2E_USERNAME` + `E2E_PASSWORD` env vars
|
|
16
16
|
|
|
17
17
|
## Output
|
|
18
18
|
|
|
19
|
-
- **Test file**: `tests/playwright/<name>.spec.ts`
|
|
19
|
+
- **Test file**: `tests/playwright/<name>.spec.ts` (e.g. `app-all.spec.ts` for "all")
|
|
20
20
|
- **Auth setup**: `tests/playwright/auth.setup.ts` (if auth required)
|
|
21
21
|
- **Report**: `openspec/reports/playwright-e2e-<name>-<timestamp>.md`
|
|
22
|
-
- **Test plan**: `openspec/changes/<name>/specs/playwright/test-plan.md`
|
|
22
|
+
- **Test plan**: `openspec/changes/<name>/specs/playwright/test-plan.md` (change mode only)
|
|
23
23
|
|
|
24
24
|
## Architecture
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Two modes, same pipeline:
|
|
27
27
|
|
|
28
|
-
|
|
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` |
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
Both modes update `app-knowledge.md` and `app-exploration.md`. All `.spec.ts` files run together as regression suite.
|
|
31
34
|
|
|
32
|
-
##
|
|
35
|
+
## Testing principles
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
**UI first** — Test every user flow through the browser UI. E2E validates that users can accomplish tasks in the real interface, not just that the backend responds correctly.
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- If ambiguous, run `openspec list --json` and use the **AskUserQuestion tool** to let the user select
|
|
39
|
+
```
|
|
40
|
+
用户操作 → 浏览器 UI → 后端 → 数据库 → UI 反馈
|
|
41
|
+
```
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
**API only as fallback** — Use `page.request` only when UI genuinely cannot cover the scenario:
|
|
44
|
+
- Triggering HTTP 5xx/4xx error responses (hard to reach via UI)
|
|
45
|
+
- Edge cases requiring pre-condition data that UI cannot set up
|
|
46
|
+
- Cases where Step 4 exploration confirmed no UI element exists
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
|
|
48
|
+
**Decision rule**:
|
|
49
|
+
```
|
|
50
|
+
Can this be tested through the UI?
|
|
51
|
+
→ Yes → page.getByRole/ByLabel/ByText + click/fill/type + assert UI
|
|
52
|
+
→ No → record reason → use page.request
|
|
46
53
|
```
|
|
47
|
-
If `openspec/changes/<name>/specs/` is empty, inform the user and stop. E2E requires specs.
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
**Never use API calls to replace routine UI flows.** If a test completes in < 200ms, it is almost certainly using `page.request` instead of real UI interactions.
|
|
56
|
+
|
|
57
|
+
## Steps
|
|
58
|
+
|
|
59
|
+
### 1. Select the change or mode
|
|
60
|
+
|
|
61
|
+
**Change mode** (`/opsx:e2e <name>`):
|
|
62
|
+
- Use provided name, or infer from context, or auto-select if only one exists
|
|
63
|
+
- If ambiguous → `openspec list --json` + AskUserQuestion
|
|
64
|
+
- Verify specs exist: `openspec status --change "<name>" --json`
|
|
65
|
+
- If specs empty → **STOP: E2E requires specs.** Use "all" mode instead.
|
|
66
|
+
|
|
67
|
+
**"all" mode** (`/opsx:e2e all` — no OpenSpec needed):
|
|
68
|
+
- Announce: "Mode: full app exploration"
|
|
69
|
+
- Discover routes via:
|
|
70
|
+
1. Navigate to `${BASE_URL}/sitemap.xml` (if exists)
|
|
71
|
+
2. Navigate to `${BASE_URL}/` → extract all links from snapshot
|
|
72
|
+
3. Fallback common paths: `/`, `/login`, `/dashboard`, `/admin`, `/profile`, `/api/`
|
|
73
|
+
- Group routes: Guest vs Protected (by attempting direct access)
|
|
74
|
+
|
|
75
|
+
### 2. Detect auth
|
|
76
|
+
|
|
77
|
+
**Change mode**: Read specs and extract functional requirements. Detect auth from keywords.
|
|
50
78
|
|
|
51
|
-
|
|
79
|
+
**"all" mode**: Detect auth by attempting to access known protected paths (e.g. `/dashboard`, `/profile`). If redirected to `/login` → auth required.
|
|
52
80
|
|
|
53
|
-
|
|
81
|
+
**Auth detection — both modes** (BOTH conditions required):
|
|
54
82
|
|
|
55
83
|
**Condition A — Explicit markers**: "login", "signin", "logout", "authenticate", "protected", "authenticated", "session", "unauthorized", "jwt", "token", "refresh", "middleware"
|
|
56
84
|
|
|
57
85
|
**Condition B — Context indicators**: Protected routes ("/dashboard", "/profile", "/admin"), role mentions ("admin", "user"), redirect flows
|
|
58
86
|
|
|
59
|
-
**Exclude false positives
|
|
87
|
+
**Exclude false positives**: HTTP header examples (`Authorization: Bearer ...`) and code snippets do not count.
|
|
60
88
|
|
|
61
|
-
**Confidence
|
|
62
|
-
- High (auto-proceed): Multiple
|
|
63
|
-
- Medium (proceed with note): Single
|
|
64
|
-
- Low (skip auth): No
|
|
89
|
+
**Confidence**:
|
|
90
|
+
- High (auto-proceed): Multiple markers AND context indicators
|
|
91
|
+
- Medium (proceed with note): Single marker, context unclear
|
|
92
|
+
- Low (skip auth): No markers found
|
|
65
93
|
|
|
66
94
|
### 3. Validate environment
|
|
67
95
|
|
|
@@ -80,11 +108,11 @@ Explore to collect real DOM data before writing test plan. This eliminates blind
|
|
|
80
108
|
|
|
81
109
|
**Prerequisites**: seed test pass. If auth is required, ensure `auth.setup.ts` has been run (Step 7). BASE_URL must be verified reachable (see 4.1).
|
|
82
110
|
|
|
83
|
-
#### 4.1. Verify BASE_URL + Read app-knowledge.md
|
|
111
|
+
#### 4.1. Verify BASE_URL + Read app-knowledge.md
|
|
84
112
|
|
|
85
|
-
1. **Verify BASE_URL**: `browser_navigate(BASE_URL)` →
|
|
86
|
-
2. **Read app-knowledge.md**:
|
|
87
|
-
3. **
|
|
113
|
+
1. **Verify BASE_URL**: `browser_navigate(BASE_URL)` → if HTTP 5xx → **STOP: backend error. Fix app first.**
|
|
114
|
+
2. **Read app-knowledge.md**: known risks, project conventions
|
|
115
|
+
3. **Routes** (from Step 1): use already-discovered routes — no need to re-extract
|
|
88
116
|
|
|
89
117
|
#### 4.2. Explore each route via Playwright MCP
|
|
90
118
|
|
|
@@ -188,139 +216,94 @@ Read `tests/playwright/app-knowledge.md` as context for cross-change patterns.
|
|
|
188
216
|
|
|
189
217
|
### 5. Generate test plan
|
|
190
218
|
|
|
191
|
-
|
|
219
|
+
> **"all" mode: skip this step — go directly to Step 6.**
|
|
192
220
|
|
|
193
|
-
**
|
|
194
|
-
- `openspec/changes/<name>/specs/*.md` — functional requirements
|
|
195
|
-
- `openspec/changes/<name>/specs/playwright/app-exploration.md` — **real routes and verified selectors**
|
|
196
|
-
- `tests/playwright/app-knowledge.md` — project-level shared knowledge
|
|
221
|
+
**Change mode**: Create `openspec/changes/<name>/specs/playwright/test-plan.md`.
|
|
197
222
|
|
|
198
|
-
|
|
199
|
-
- List each functional requirement as a test case
|
|
200
|
-
- Mark with `@role(user|admin|guest|none)` and `@auth(required|none)`
|
|
201
|
-
- Include happy path AND error paths
|
|
202
|
-
- Reference the **real route URL** and **verified selectors** from app-exploration.md
|
|
223
|
+
**Read inputs**: specs, app-exploration.md, app-knowledge.md
|
|
203
224
|
|
|
204
|
-
|
|
225
|
+
**Create test cases**: functional requirement → test case, with `@role` and `@auth` tags. Reference verified selectors from app-exploration.md.
|
|
205
226
|
|
|
206
|
-
|
|
227
|
+
Template: `openspec/schemas/playwright-e2e/templates/test-plan.md`
|
|
207
228
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
Create `tests/playwright/<name>.spec.ts`:
|
|
211
|
-
|
|
212
|
-
**Read inputs first**:
|
|
213
|
-
- `openspec/changes/<name>/specs/playwright/test-plan.md` — test cases
|
|
214
|
-
- `openspec/changes/<name>/specs/playwright/app-exploration.md` — **verified routes and selectors**
|
|
215
|
-
- `tests/playwright/app-knowledge.md` — project-level patterns and conventions
|
|
216
|
-
- `tests/playwright/seed.spec.ts` — code pattern and page object structure
|
|
229
|
+
**Idempotency**: If test-plan.md exists → read and use, do NOT regenerate.
|
|
217
230
|
|
|
218
|
-
|
|
231
|
+
### 6. Generate test file
|
|
219
232
|
|
|
220
|
-
**
|
|
233
|
+
**"all" mode** → `tests/playwright/app-all.spec.ts` (smoke regression):
|
|
234
|
+
- For each discovered route: navigate → assert HTTP 200 → assert ready signal visible
|
|
235
|
+
- No detailed assertions — just "this page loads without crashing"
|
|
236
|
+
- This is a regression baseline — catches when existing pages break
|
|
221
237
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
1. Determine target route (from app-exploration.md)
|
|
226
|
-
2. Determine auth state (load storageState if @auth(required))
|
|
227
|
-
3. Navigate: browser_navigate to the route
|
|
228
|
-
4. Wait for page ready: browser_wait_for with a key element
|
|
229
|
-
5. Verify: browser_snapshot to confirm page loaded
|
|
230
|
-
6. For each selector in the test case:
|
|
231
|
-
a. Check if selector exists in app-exploration.md
|
|
232
|
-
b. If yes → verify it's still valid via browser_snapshot
|
|
233
|
-
c. If no → find equivalent from current snapshot
|
|
234
|
-
d. Selector priority: see 4.3 table
|
|
235
|
-
7. Write test code with verified selectors
|
|
236
|
-
8. If selector cannot be verified → note it for Healer (Step 9)
|
|
237
|
-
```
|
|
238
|
+
**Change mode** → `tests/playwright/<name>.spec.ts` (functional):
|
|
239
|
+
- Read: test-plan.md, app-exploration.md, app-knowledge.md, seed.spec.ts
|
|
240
|
+
- For each test case: verify selectors in real browser, then write Playwright code
|
|
238
241
|
|
|
239
|
-
|
|
242
|
+
**Selector verification (change mode)**:
|
|
243
|
+
1. Navigate to route with correct auth state
|
|
244
|
+
2. browser_snapshot to confirm page loaded
|
|
245
|
+
3. For each selector: verify from current snapshot (see 4.3 table for priority)
|
|
246
|
+
4. Write test code with verified selectors
|
|
247
|
+
5. If selector unverifiable → note for Healer (Step 9)
|
|
240
248
|
|
|
241
|
-
**
|
|
249
|
+
**Output format**:
|
|
242
250
|
- Follow `seed.spec.ts` structure
|
|
243
|
-
- Prefer `data-testid` selectors (see 4.3 table for priority)
|
|
244
|
-
- Include happy path AND error/edge cases
|
|
245
251
|
- Use `test.describe(...)` for grouping
|
|
246
252
|
- Each test: `test('描述性名称', async ({ page }) => { ... })`
|
|
253
|
+
- Prefer `data-testid` selectors (see 4.3 table)
|
|
247
254
|
|
|
248
|
-
|
|
255
|
+
**Code examples — UI first:**
|
|
249
256
|
|
|
250
|
-
**🚫 False Pass** — never silently skip when element is missing:
|
|
251
257
|
```typescript
|
|
252
|
-
//
|
|
258
|
+
// ✅ UI 测试 — 用户在界面上的真实操作
|
|
259
|
+
await page.goto(`${BASE_URL}/orders`);
|
|
260
|
+
await page.getByRole('button', { name: '新建订单' }).click();
|
|
261
|
+
await page.getByLabel('订单名称').fill('Test Order');
|
|
262
|
+
await page.getByRole('button', { name: '提交' }).click();
|
|
263
|
+
await expect(page.getByText('订单创建成功')).toBeVisible();
|
|
264
|
+
|
|
265
|
+
// ✅ Error path — 通过 UI 触发错误
|
|
266
|
+
await page.goto(`${BASE_URL}/orders`);
|
|
267
|
+
await page.getByRole('button', { name: '新建订单' }).click();
|
|
268
|
+
await page.getByRole('button', { name: '提交' }).click();
|
|
269
|
+
await expect(page.getByRole('alert')).toContainText('名称不能为空');
|
|
270
|
+
|
|
271
|
+
// ✅ API fallback — 仅在 UI 无法触发时使用
|
|
272
|
+
const res = await page.request.get(`${BASE_URL}/api/orders/99999`);
|
|
273
|
+
expect(res.status()).toBe(404);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// 🚫 False Pass — 元素不存在时静默跳过
|
|
253
278
|
if (await btn.isVisible().catch(() => false)) { ... }
|
|
279
|
+
|
|
254
280
|
// ✅ CORRECT
|
|
255
281
|
await expect(page.getByRole('button', { name: '取消' })).toBeVisible();
|
|
256
|
-
```
|
|
257
282
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
283
|
+
// 🚫 用 API 替代 UI — 失去了端到端的意义
|
|
284
|
+
const res = await page.request.post(`${BASE_URL}/api/login`, { data: credentials });
|
|
285
|
+
|
|
286
|
+
// ✅ CORRECT — 通过 UI 登录
|
|
287
|
+
await page.goto(`${BASE_URL}/login`);
|
|
288
|
+
await page.getByLabel('邮箱').fill(process.env.E2E_USERNAME);
|
|
289
|
+
await page.getByLabel('密码').fill(process.env.E2E_PASSWORD);
|
|
290
|
+
await page.getByRole('button', { name: '登录' }).click();
|
|
291
|
+
await expect(page).toHaveURL(/dashboard/);
|
|
262
292
|
```
|
|
263
293
|
|
|
264
|
-
**🚫 Auth guard** — test with FRESH browser context (no inherited cookies):
|
|
265
294
|
```typescript
|
|
266
|
-
|
|
295
|
+
// ✅ Fresh browser context for auth guard
|
|
296
|
+
test('unauthenticated user redirected to login', async ({ browser }) => {
|
|
267
297
|
const freshPage = await browser.newContext().newPage();
|
|
268
298
|
await freshPage.goto(`${BASE_URL}/dashboard`);
|
|
269
299
|
await expect(freshPage).toHaveURL(/login|auth/);
|
|
270
300
|
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
#### Playwright API Guardrails
|
|
274
|
-
|
|
275
|
-
These are the most common mistakes that cause test failures. **Always follow these rules:**
|
|
276
|
-
|
|
277
|
-
**API calls — use `page.request` directly:**
|
|
278
|
-
```typescript
|
|
279
|
-
// ❌ WRONG — page.evaluate with fetch has timeout, CORS, and context issues
|
|
280
|
-
const result = await page.evaluate(async () => {
|
|
281
|
-
const res = await fetch('/api/data');
|
|
282
|
-
return res.json();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// ✅ CORRECT — page.request is already an APIRequestContext, use directly
|
|
286
|
-
const res = await page.request.get(`${BASE_URL}/api/data`);
|
|
287
|
-
expect(res.status()).toBe(200);
|
|
288
|
-
const data = await res.json();
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
**Browser context — use `close()` for BrowserContext, no cleanup for APIRequestContext:**
|
|
292
|
-
```typescript
|
|
293
|
-
// ✅ BrowserContext — close it when done
|
|
294
|
-
const context = await browser.newContext();
|
|
295
|
-
await context.close(); // ← correct
|
|
296
|
-
|
|
297
|
-
// ✅ APIRequestContext — page.request is already one, no cleanup needed
|
|
298
|
-
const res = await page.request.get(`${BASE_URL}/api/data`);
|
|
299
|
-
// No dispose() or close() needed
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
**File uploads — use `setInputFiles()`, NOT `page.evaluate()` + fetch:**
|
|
303
|
-
```typescript
|
|
304
|
-
// ❌ WRONG
|
|
305
|
-
await page.evaluate(() => {
|
|
306
|
-
const input = document.querySelector('input[type=file]');
|
|
307
|
-
// ...
|
|
308
|
-
});
|
|
309
301
|
|
|
310
|
-
// ✅
|
|
302
|
+
// ✅ File uploads — UI 操作
|
|
311
303
|
await page.locator('input[type="file"]').setInputFiles('/path/to/file.pdf');
|
|
312
304
|
```
|
|
313
305
|
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
// ❌ WRONG — bypasses Playwright's actionability checks
|
|
317
|
-
await page.evaluate(() => document.querySelector('button[type=submit]').click());
|
|
318
|
-
|
|
319
|
-
// ✅ CORRECT — respects visibility, enabled, stable
|
|
320
|
-
await page.getByRole('button', { name: 'Submit' }).click();
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
**Always include error path tests** — API 500, 404, network timeout, invalid input.
|
|
306
|
+
Always include error path tests: UI validation messages, network failure, invalid input. Use `page.request` only for scenarios confirmed unreachable via UI.
|
|
324
307
|
|
|
325
308
|
If the file exists → diff against test-plan, add only missing test cases.
|
|
326
309
|
|
|
@@ -429,13 +412,13 @@ Reference: `openspec/schemas/playwright-e2e/templates/report.md`
|
|
|
429
412
|
|
|
430
413
|
| Scenario | Behavior |
|
|
431
414
|
|----------|----------|
|
|
432
|
-
| No specs | Stop — E2E requires specs |
|
|
433
|
-
|
|
|
434
|
-
| App has JS errors or HTTP 5xx during exploration | **STOP** — see app-knowledge.md → Architecture
|
|
435
|
-
|
|
|
415
|
+
| No specs (change mode) | Stop — E2E requires specs. Use "all" mode instead. |
|
|
416
|
+
| Sitemap discovery fails ("all" mode) | Continue — use homepage links + common paths fallback |
|
|
417
|
+
| App has JS errors or HTTP 5xx during exploration | **STOP** — see app-knowledge.md → Architecture for restart instructions |
|
|
418
|
+
| app-all.spec.ts exists | Read and use (never regenerate — regression baseline) |
|
|
436
419
|
| app-exploration.md exists | Read and use (never regenerate) |
|
|
437
420
|
| app-knowledge.md exists | Read and use (append new patterns only) |
|
|
438
|
-
| test-plan.md exists | Read and use (never regenerate) |
|
|
421
|
+
| test-plan.md exists (change mode) | Read and use (never regenerate) |
|
|
439
422
|
| auth.setup.ts exists | Verify format (update only if stale) |
|
|
440
423
|
| playwright.config.ts exists | Preserve all fields (add only missing) |
|
|
441
424
|
| Test fails (backend) | `test.skip()` + report |
|
|
Binary file
|
package/package.json
CHANGED
package/release-notes.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## What's Changed
|
|
2
2
|
|
|
3
|
-
-
|
|
3
|
+
- chore: bump version to 0.1.59
|
|
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.59
|
|
Binary file
|