openspec-playwright 0.1.58 → 0.1.60
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.
- package/.claude/skills/openspec-e2e/SKILL.md +63 -28
- package/dist/commands/mcpSync.js +13 -5
- package/dist/commands/mcpSync.js.map +1 -1
- package/dist/utils/mcp-tools.d.ts +23 -0
- package/dist/utils/mcp-tools.js +130 -0
- package/dist/utils/mcp-tools.js.map +1 -0
- package/openspec/schemas/playwright-e2e/schema.yaml +56 -0
- package/openspec/schemas/playwright-e2e/templates/e2e-test.ts +55 -0
- package/openspec/schemas/playwright-e2e/templates/playwright.config.ts +52 -0
- package/openspec/schemas/playwright-e2e/templates/report.md +27 -0
- package/openspec/schemas/playwright-e2e/templates/test-plan.md +24 -0
- package/openspec-playwright-0.1.60.tgz +0 -0
- package/package.json +3 -2
- package/release-notes.md +2 -2
- package/schemas/playwright-e2e/templates/app-knowledge.md +11 -1
- package/src/commands/mcpSync.ts +14 -5
- package/templates/seed.spec.ts +22 -0
- package/openspec-playwright-0.1.58.tgz +0 -0
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: Requires openspec CLI, Playwright (with browsers installed), and @playwright/mcp (globally installed via `claude mcp add playwright npx @playwright/mcp@latest`).
|
|
6
6
|
metadata:
|
|
7
7
|
author: openspec-playwright
|
|
8
|
-
version: "2.
|
|
8
|
+
version: "2.10"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
## Input
|
|
@@ -32,6 +32,28 @@ Two modes, same pipeline:
|
|
|
32
32
|
|
|
33
33
|
Both modes update `app-knowledge.md` and `app-exploration.md`. All `.spec.ts` files run together as regression suite.
|
|
34
34
|
|
|
35
|
+
## Testing principles
|
|
36
|
+
|
|
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.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
用户操作 → 浏览器 UI → 后端 → 数据库 → UI 反馈
|
|
41
|
+
```
|
|
42
|
+
|
|
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
|
|
47
|
+
|
|
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
|
|
53
|
+
```
|
|
54
|
+
|
|
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
|
+
|
|
35
57
|
## Steps
|
|
36
58
|
|
|
37
59
|
### 1. Select the change or mode
|
|
@@ -181,6 +203,7 @@ After writing `app-exploration.md`, extract **project-level shared knowledge** a
|
|
|
181
203
|
| Common Selector Patterns | New patterns discovered that apply across routes |
|
|
182
204
|
| SPA Routing | SPA framework, routing behavior |
|
|
183
205
|
| Project Conventions | BASE_URL, auth method, multi-user roles |
|
|
206
|
+
| Selector Fixes | Healed selectors (see Step 9) — route, old selector, new selector, reason |
|
|
184
207
|
|
|
185
208
|
Append only new/changed items — preserve existing content.
|
|
186
209
|
|
|
@@ -230,44 +253,58 @@ Template: `openspec/schemas/playwright-e2e/templates/test-plan.md`
|
|
|
230
253
|
- Each test: `test('描述性名称', async ({ page }) => { ... })`
|
|
231
254
|
- Prefer `data-testid` selectors (see 4.3 table)
|
|
232
255
|
|
|
233
|
-
**Code examples —
|
|
256
|
+
**Code examples — UI first:**
|
|
234
257
|
|
|
235
258
|
```typescript
|
|
236
|
-
//
|
|
259
|
+
// ✅ UI 测试 — 用户在界面上的真实操作
|
|
260
|
+
await page.goto(`${BASE_URL}/orders`);
|
|
261
|
+
await page.getByRole('button', { name: '新建订单' }).click();
|
|
262
|
+
await page.getByLabel('订单名称').fill('Test Order');
|
|
263
|
+
await page.getByRole('button', { name: '提交' }).click();
|
|
264
|
+
await expect(page.getByText('订单创建成功')).toBeVisible();
|
|
265
|
+
|
|
266
|
+
// ✅ Error path — 通过 UI 触发错误
|
|
267
|
+
await page.goto(`${BASE_URL}/orders`);
|
|
268
|
+
await page.getByRole('button', { name: '新建订单' }).click();
|
|
269
|
+
await page.getByRole('button', { name: '提交' }).click();
|
|
270
|
+
await expect(page.getByRole('alert')).toContainText('名称不能为空');
|
|
271
|
+
|
|
272
|
+
// ✅ API fallback — 仅在 UI 无法触发时使用
|
|
273
|
+
const res = await page.request.get(`${BASE_URL}/api/orders/99999`);
|
|
274
|
+
expect(res.status()).toBe(404);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// 🚫 False Pass — 元素不存在时静默跳过
|
|
237
279
|
if (await btn.isVisible().catch(() => false)) { ... }
|
|
280
|
+
|
|
238
281
|
// ✅ CORRECT
|
|
239
282
|
await expect(page.getByRole('button', { name: '取消' })).toBeVisible();
|
|
240
283
|
|
|
241
|
-
// 🚫
|
|
242
|
-
|
|
243
|
-
const freshPage = await browser.newContext().newPage();
|
|
244
|
-
await freshPage.goto(`${BASE_URL}/dashboard`);
|
|
245
|
-
await expect(freshPage).toHaveURL(/login|auth/);
|
|
246
|
-
});
|
|
284
|
+
// 🚫 用 API 替代 UI — 失去了端到端的意义
|
|
285
|
+
const res = await page.request.post(`${BASE_URL}/api/login`, { data: credentials });
|
|
247
286
|
|
|
248
|
-
// ✅
|
|
249
|
-
|
|
250
|
-
|
|
287
|
+
// ✅ CORRECT — 通过 UI 登录
|
|
288
|
+
await page.goto(`${BASE_URL}/login`);
|
|
289
|
+
await page.getByLabel('邮箱').fill(process.env.E2E_USERNAME);
|
|
290
|
+
await page.getByLabel('密码').fill(process.env.E2E_PASSWORD);
|
|
291
|
+
await page.getByRole('button', { name: '登录' }).click();
|
|
292
|
+
await expect(page).toHaveURL(/dashboard/);
|
|
251
293
|
```
|
|
252
294
|
|
|
253
295
|
```typescript
|
|
254
|
-
// ✅
|
|
255
|
-
|
|
256
|
-
await
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
```
|
|
296
|
+
// ✅ Fresh browser context for auth guard
|
|
297
|
+
test('unauthenticated user redirected to login', async ({ browser }) => {
|
|
298
|
+
const freshPage = await browser.newContext().newPage();
|
|
299
|
+
await freshPage.goto(`${BASE_URL}/dashboard`);
|
|
300
|
+
await expect(freshPage).toHaveURL(/login|auth/);
|
|
301
|
+
});
|
|
261
302
|
|
|
262
|
-
|
|
263
|
-
// ✅ File uploads — use setInputFiles()
|
|
303
|
+
// ✅ File uploads — UI 操作
|
|
264
304
|
await page.locator('input[type="file"]').setInputFiles('/path/to/file.pdf');
|
|
265
|
-
|
|
266
|
-
// ✅ Form submissions — prefer Playwright locators
|
|
267
|
-
await page.getByRole('button', { name: 'Submit' }).click();
|
|
268
305
|
```
|
|
269
306
|
|
|
270
|
-
Always include error path tests:
|
|
307
|
+
Always include error path tests: UI validation messages, network failure, invalid input. Use `page.request` only for scenarios confirmed unreachable via UI.
|
|
271
308
|
|
|
272
309
|
If the file exists → diff against test-plan, add only missing test cases.
|
|
273
310
|
|
|
@@ -321,8 +358,6 @@ The CLI handles: server lifecycle, port mismatch, report generation.
|
|
|
321
358
|
If tests fail → use Playwright MCP tools to inspect UI, fix selectors, re-run.
|
|
322
359
|
|
|
323
360
|
**Healer MCP tools** (in order of use):
|
|
324
|
-
<!-- MCP_VERSION: 0.0.68 -->
|
|
325
|
-
|
|
326
361
|
<!-- MCP_VERSION: 0.0.70 -->
|
|
327
362
|
|
|
328
363
|
| Tool | Purpose |
|
|
@@ -345,7 +380,7 @@ If tests fail → use Playwright MCP tools to inspect UI, fix selectors, re-run.
|
|
|
345
380
|
| **Timing issue** | `waitFor`/`page.evaluate` timeout | Switch to `request` API or add `waitFor` → re-run |
|
|
346
381
|
| **page.evaluate with fetch** | `fetch` in browser context, CORS errors | Switch to `page.request` API → re-run |
|
|
347
382
|
|
|
348
|
-
3. **Heal** (≤3 attempts): snapshot → fix → re-run
|
|
383
|
+
3. **Heal** (≤3 attempts): snapshot → fix → re-run. If healed successfully → append to `app-knowledge.md` → **Selector Fixes** table: route, old selector → new selector, reason.
|
|
349
384
|
4. **After 3 failures**: collect evidence checklist → `test.skip()` if app bug, report recommendation if test bug
|
|
350
385
|
|
|
351
386
|
### 10. False Pass Detection
|
package/dist/commands/mcpSync.js
CHANGED
|
@@ -21,6 +21,13 @@ export function getStoredMcpVersion(skillContent) {
|
|
|
21
21
|
const end = skillContent.indexOf(' -->', idx);
|
|
22
22
|
return skillContent.slice(idx + MCP_VERSION_MARKER.length, end).trim();
|
|
23
23
|
}
|
|
24
|
+
/** Remove all existing MCP_VERSION comment lines from content */
|
|
25
|
+
function removeMcpVersionMarkers(content) {
|
|
26
|
+
return content
|
|
27
|
+
.split('\n')
|
|
28
|
+
.filter(line => !line.trim().startsWith(MCP_VERSION_MARKER))
|
|
29
|
+
.join('\n');
|
|
30
|
+
}
|
|
24
31
|
/** Build the Healer tools table markdown */
|
|
25
32
|
function buildHealerTable(version, tools) {
|
|
26
33
|
const rows = tools.map(t => `| \`${t.name}\` | ${t.purpose} |`).join('\n');
|
|
@@ -28,14 +35,15 @@ function buildHealerTable(version, tools) {
|
|
|
28
35
|
}
|
|
29
36
|
/** Replace the Healer tools table in SKILL.md */
|
|
30
37
|
export function updateHealerTable(skillContent, version, tools) {
|
|
31
|
-
const
|
|
38
|
+
const noMarkers = removeMcpVersionMarkers(skillContent);
|
|
39
|
+
const start = noMarkers.indexOf('| Tool | Purpose |');
|
|
32
40
|
if (start === -1)
|
|
33
41
|
return skillContent;
|
|
34
|
-
let end =
|
|
42
|
+
let end = noMarkers.indexOf('\n\n', start);
|
|
35
43
|
if (end === -1)
|
|
36
|
-
end =
|
|
37
|
-
const before =
|
|
38
|
-
const after =
|
|
44
|
+
end = noMarkers.length;
|
|
45
|
+
const before = noMarkers.slice(0, start);
|
|
46
|
+
const after = noMarkers.slice(end);
|
|
39
47
|
return before + buildHealerTable(version, tools) + after;
|
|
40
48
|
}
|
|
41
49
|
/** Fetch latest @playwright/mcp version from npm registry */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpSync.js","sourceRoot":"","sources":["../../src/commands/mcpSync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC7F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAE3B,MAAM,CAAC,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAEtD,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,+BAA+B,EAAE;IACtE,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,iDAAiD,EAAE;IACxF,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,4CAA4C,EAAE;IAC3F,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,qCAAqC,EAAE;IACnF,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,qCAAqC,EAAE;CAC7E,CAAC;AAEF,+CAA+C;AAC/C,MAAM,UAAU,mBAAmB,CAAC,YAAoB;IACtD,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACrD,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9C,OAAO,YAAY,CAAC,KAAK,CAAC,GAAG,GAAG,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACzE,CAAC;AAED,4CAA4C;AAC5C,SAAS,gBAAgB,CAAC,OAAe,EAAE,KAA+C;IACxF,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3E,OAAO,GAAG,kBAAkB,IAAI,OAAO,mDAAmD,IAAI,EAAE,CAAC;AACnG,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,iBAAiB,CAC/B,YAAoB,EACpB,OAAe,EACf,KAA+C;IAE/C,MAAM,
|
|
1
|
+
{"version":3,"file":"mcpSync.js","sourceRoot":"","sources":["../../src/commands/mcpSync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Record of tool name -> purpose description */
|
|
2
|
+
export interface McpTool {
|
|
3
|
+
name: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
purpose: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Download and parse the @playwright/mcp README to extract all browser_* tools.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchMcpTools(): McpTool[];
|
|
12
|
+
/**
|
|
13
|
+
* Update the Healer MCP tools table in SKILL.md.
|
|
14
|
+
* Only syncs the 5 core Healer tools; all other MCP tools are
|
|
15
|
+
* general-purpose automation not specific to healing.
|
|
16
|
+
* Deduplicates by tool name.
|
|
17
|
+
*/
|
|
18
|
+
export declare function updateSkillHealerTable(skillPath: string, tools: McpTool[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* Main entry point: fetch tools and update SKILL.md.
|
|
21
|
+
* Call this after installing the skill in init.ts and update.ts.
|
|
22
|
+
*/
|
|
23
|
+
export declare function syncMcpTools(skillPath: string): void;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Playwright MCP tool synchronization.
|
|
3
|
+
* Parses @playwright/mcp README to extract browser_* tool names and
|
|
4
|
+
* updates the Healer MCP tools table in SKILL.md.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
/** Cached directory for npm temp files */
|
|
11
|
+
const TMP_DIR = '/tmp/openspec-pw-mcp';
|
|
12
|
+
/**
|
|
13
|
+
* Get the latest @playwright/mcp version from npm registry.
|
|
14
|
+
* Falls back to cached README if network fails.
|
|
15
|
+
*/
|
|
16
|
+
function getLatestVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const json = execSync('npm show @playwright/mcp version --json', { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
19
|
+
return JSON.parse(json);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return '0.0.68'; // fallback to known version
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Download and parse the @playwright/mcp README to extract all browser_* tools.
|
|
27
|
+
*/
|
|
28
|
+
export function fetchMcpTools() {
|
|
29
|
+
const version = getLatestVersion();
|
|
30
|
+
const tmpPkg = join(TMP_DIR, 'package');
|
|
31
|
+
// Clean and prepare temp dir
|
|
32
|
+
execSync(`rm -rf ${TMP_DIR} && mkdir -p ${TMP_DIR}`, { stdio: 'pipe' });
|
|
33
|
+
// Download tarball
|
|
34
|
+
execSync(`cd ${TMP_DIR} && npm pack @playwright/mcp@${version} --pack-destination ${TMP_DIR}`, { stdio: 'pipe' });
|
|
35
|
+
const tarball = execSync(`ls -t ${TMP_DIR}/playwright-mcp-*.tgz | head -1`, { encoding: 'utf-8' }).trim();
|
|
36
|
+
execSync(`tar -xzf "${tarball}" -C ${TMP_DIR} --strip-components=1`, { stdio: 'pipe' });
|
|
37
|
+
const readme = readFileSync(join(TMP_DIR, 'README.md'), 'utf-8');
|
|
38
|
+
return parseToolsFromReadme(readme);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse browser_* tools from the MCP README markdown.
|
|
42
|
+
* Format:
|
|
43
|
+
* - **browser_navigate**
|
|
44
|
+
* - Title: Navigate to a URL
|
|
45
|
+
* - Description: Navigate to a URL
|
|
46
|
+
*/
|
|
47
|
+
function parseToolsFromReadme(readme) {
|
|
48
|
+
const tools = [];
|
|
49
|
+
// Match tool blocks: **browser_name** followed by Title and Description
|
|
50
|
+
const blockRegex = /- \*\*([a-z_]+)\*\*[\s\S]*?- Title: ([^\n]+)\n[\s\S]*?- Description: ([^\n]+)/g;
|
|
51
|
+
let match;
|
|
52
|
+
while ((match = blockRegex.exec(readme)) !== null) {
|
|
53
|
+
const name = match[1];
|
|
54
|
+
const title = match[2].trim();
|
|
55
|
+
const description = match[3].trim();
|
|
56
|
+
// Map tool name to Healer purpose
|
|
57
|
+
const purpose = mapToolToPurpose(name, title, description);
|
|
58
|
+
tools.push({ name, title, description, purpose });
|
|
59
|
+
}
|
|
60
|
+
return tools;
|
|
61
|
+
}
|
|
62
|
+
/** Map a browser_* tool to its Healer purpose in the SKILL */
|
|
63
|
+
function mapToolToPurpose(name, title, description) {
|
|
64
|
+
const map = {
|
|
65
|
+
browser_navigate: 'Go to the failing test\'s page',
|
|
66
|
+
browser_snapshot: 'Get page structure to find equivalent selectors',
|
|
67
|
+
browser_console_messages: 'Diagnose JS errors that may cause failures',
|
|
68
|
+
browser_take_screenshot: 'Visually compare before/after fixes',
|
|
69
|
+
browser_run_code: 'Execute custom fix logic (optional)',
|
|
70
|
+
};
|
|
71
|
+
return map[name] || title;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Update the Healer MCP tools table in SKILL.md.
|
|
75
|
+
* Only syncs the 5 core Healer tools; all other MCP tools are
|
|
76
|
+
* general-purpose automation not specific to healing.
|
|
77
|
+
* Deduplicates by tool name.
|
|
78
|
+
*/
|
|
79
|
+
export function updateSkillHealerTable(skillPath, tools) {
|
|
80
|
+
if (!existsSync(skillPath))
|
|
81
|
+
return;
|
|
82
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
83
|
+
// Only the 5 core Healer tools
|
|
84
|
+
const healerToolNames = [
|
|
85
|
+
'browser_navigate',
|
|
86
|
+
'browser_snapshot',
|
|
87
|
+
'browser_console_messages',
|
|
88
|
+
'browser_take_screenshot',
|
|
89
|
+
'browser_run_code',
|
|
90
|
+
];
|
|
91
|
+
// Filter to healer tools, deduplicate by name
|
|
92
|
+
const healerTools = tools.filter(t => healerToolNames.includes(t.name));
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const unique = healerTools.filter(t => {
|
|
95
|
+
if (seen.has(t.name))
|
|
96
|
+
return false;
|
|
97
|
+
seen.add(t.name);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
// Build new table rows
|
|
101
|
+
const rows = unique.map(t => `| \`${t.name}\` | ${t.purpose} |`).join('\n');
|
|
102
|
+
// Replace the existing table
|
|
103
|
+
const tableHeader = '| Tool | Purpose |';
|
|
104
|
+
const tableSep = '|------|---------|';
|
|
105
|
+
const start = content.indexOf(tableHeader);
|
|
106
|
+
const end = content.indexOf(tableSep, start + 1);
|
|
107
|
+
if (start === -1 || end === -1)
|
|
108
|
+
return;
|
|
109
|
+
const newSection = tableHeader + '\n' + tableSep + '\n' + rows;
|
|
110
|
+
const newContent = content.substring(0, start) +
|
|
111
|
+
newSection +
|
|
112
|
+
content.substring(end + tableSep.length);
|
|
113
|
+
writeFileSync(skillPath, newContent, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Main entry point: fetch tools and update SKILL.md.
|
|
117
|
+
* Call this after installing the skill in init.ts and update.ts.
|
|
118
|
+
*/
|
|
119
|
+
export function syncMcpTools(skillPath) {
|
|
120
|
+
try {
|
|
121
|
+
const tools = fetchMcpTools();
|
|
122
|
+
updateSkillHealerTable(skillPath, tools);
|
|
123
|
+
console.log(chalk.green(` ✓ Healer tools synced from @playwright/mcp`));
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.log(chalk.yellow(` ⚠ Failed to sync MCP tools: ${err}`));
|
|
127
|
+
console.log(chalk.gray(' SKILL.md MCP tools table may be stale'));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=mcp-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-tools.js","sourceRoot":"","sources":["../../src/utils/mcp-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: playwright-e2e
|
|
2
|
+
version: 1
|
|
3
|
+
description: Playwright E2E verification for an OpenSpec change
|
|
4
|
+
|
|
5
|
+
artifacts:
|
|
6
|
+
- id: test-plan
|
|
7
|
+
generates: specs/playwright/test-plan.md
|
|
8
|
+
description: Test plan derived from change specs
|
|
9
|
+
template: test-plan.md
|
|
10
|
+
instruction: |
|
|
11
|
+
Generate a test plan from the change specs. Each functional requirement becomes
|
|
12
|
+
one or more test cases.
|
|
13
|
+
|
|
14
|
+
For each test case, specify:
|
|
15
|
+
- **Test name** (kebab-case): what it verifies
|
|
16
|
+
- **Route/Page**: which URL or UI component it targets
|
|
17
|
+
- **Prerequisites**: auth role needed (user/admin/guest/none)
|
|
18
|
+
- **Happy path**: main user flow to verify
|
|
19
|
+
- **Error paths**: key error conditions to verify
|
|
20
|
+
|
|
21
|
+
Mark tests with role tags: `@role(user)`, `@role(admin)`, `@role(guest)`, `@role(none)`.
|
|
22
|
+
Mark auth requirement: `@auth(required)` or `@auth(none)`.
|
|
23
|
+
|
|
24
|
+
- id: e2e-test
|
|
25
|
+
generates: tests/playwright/<change>.spec.ts
|
|
26
|
+
description: Playwright E2E test suite
|
|
27
|
+
template: e2e-test.ts
|
|
28
|
+
instruction: |
|
|
29
|
+
Generate Playwright tests from the test-plan.
|
|
30
|
+
|
|
31
|
+
Rules:
|
|
32
|
+
- Follow the page object pattern from seed.spec.ts
|
|
33
|
+
- Prefer `data-testid` selectors, fall back to semantic selectors
|
|
34
|
+
- Each test maps to one test case from the test-plan
|
|
35
|
+
- Use `@project(user)` / `@project(admin)` for role-specific tests
|
|
36
|
+
- Cover happy path AND key error paths
|
|
37
|
+
- Name tests descriptively: `test('shows error on invalid input')`
|
|
38
|
+
|
|
39
|
+
- id: playwright-config
|
|
40
|
+
generates: playwright.config.ts
|
|
41
|
+
description: Playwright config with webServer and auth projects
|
|
42
|
+
template: playwright.config.ts
|
|
43
|
+
instruction: |
|
|
44
|
+
Configure Playwright for E2E testing.
|
|
45
|
+
|
|
46
|
+
- Set webServer command and url from BASE_URL
|
|
47
|
+
- Set baseURL for all tests
|
|
48
|
+
- Add auth setup project if credentials are configured
|
|
49
|
+
- Preserve any existing config fields (browsers, retries, etc.)
|
|
50
|
+
- Do NOT overwrite existing projects unless they conflict
|
|
51
|
+
|
|
52
|
+
apply:
|
|
53
|
+
requires: [test-plan, e2e-test]
|
|
54
|
+
instruction: |
|
|
55
|
+
Run tests via: openspec-pw run <change-name>
|
|
56
|
+
Analyze results and generate report at openspec/reports/playwright-e2e-<change>-<timestamp>.md
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
// ──────────────────────────────────────────────
|
|
4
|
+
// Test plan: <change-name>
|
|
5
|
+
// Generated from: openspec/changes/<change-name>/specs/playwright/test-plan.md
|
|
6
|
+
// ──────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Page Object Pattern - add selectors for your app's pages
|
|
12
|
+
*/
|
|
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
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createPage(page: Page): AppPage {
|
|
32
|
+
return new AppPage(page);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ──────────────────────────────────────────────
|
|
36
|
+
// Tests - generated from test-plan.md
|
|
37
|
+
// Customize selectors and assertions to match your app
|
|
38
|
+
// ──────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
test.describe('<change-name>: E2E verification', () => {
|
|
41
|
+
|
|
42
|
+
test.beforeEach(async ({ page }) => {
|
|
43
|
+
const app = createPage(page);
|
|
44
|
+
await app.goto('/');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// TODO: Add test cases from specs/playwright/test-plan.md
|
|
48
|
+
// Example:
|
|
49
|
+
// test('shows expected content on page load', async ({ page }) => {
|
|
50
|
+
// const app = createPage(page);
|
|
51
|
+
// await app.goto('/');
|
|
52
|
+
// await expect(app.getByTestId('main-heading')).toBeVisible();
|
|
53
|
+
// });
|
|
54
|
+
|
|
55
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
// ─── BASE_URL: prefer env, then seed.spec.ts, then default ───
|
|
6
|
+
const seedSpec = join(__dirname, '../tests/playwright/seed.spec.ts');
|
|
7
|
+
let baseUrl = process.env.BASE_URL || 'http://localhost:3000';
|
|
8
|
+
if (existsSync(seedSpec)) {
|
|
9
|
+
const content = readFileSync(seedSpec, 'utf-8');
|
|
10
|
+
const m = content.match(/BASE_URL\s*=\s*process\.env\.BASE_URL\s*\|\|\s*['"]([^'"]+)['"]/);
|
|
11
|
+
if (m) baseUrl = m[1];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Dev command: detect from package.json scripts ───
|
|
15
|
+
const pkgPath = join(__dirname, '../package.json');
|
|
16
|
+
let devCmd = 'npm run dev';
|
|
17
|
+
if (existsSync(pkgPath)) {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
19
|
+
const scripts = pkg.scripts ?? {};
|
|
20
|
+
// Prefer in order: dev, start, serve, preview
|
|
21
|
+
devCmd = scripts.dev ?? scripts.start ?? scripts.serve ?? scripts.preview ?? devCmd;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
testDir: '../tests/playwright',
|
|
26
|
+
// Keep test artifacts inside tests/playwright/ instead of project root
|
|
27
|
+
outputDir: '../tests/playwright/test-results',
|
|
28
|
+
fullyParallel: true,
|
|
29
|
+
forbidOnly: !!process.env.CI,
|
|
30
|
+
retries: process.env.CI ? 2 : 0,
|
|
31
|
+
workers: process.env.CI ? 1 : undefined,
|
|
32
|
+
reporter: 'list',
|
|
33
|
+
|
|
34
|
+
use: {
|
|
35
|
+
baseURL: baseUrl,
|
|
36
|
+
trace: 'on-first-retry',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Dev server lifecycle - Playwright starts/stops automatically
|
|
40
|
+
webServer: {
|
|
41
|
+
command: devCmd,
|
|
42
|
+
url: baseUrl,
|
|
43
|
+
timeout: 120000,
|
|
44
|
+
reuseExistingServer: true,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Setup project for authentication (configured by openspec-pw run)
|
|
48
|
+
projects: [
|
|
49
|
+
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
|
50
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# E2E Verify Report: <change-name>
|
|
2
|
+
|
|
3
|
+
**Change**: `<change-name>`
|
|
4
|
+
**Generated**: `<timestamp>`
|
|
5
|
+
**Auth**: `<required|none>`
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
| Check | Status |
|
|
10
|
+
|-------|--------|
|
|
11
|
+
| Tests Run | <N> |
|
|
12
|
+
| Passed | <X> |
|
|
13
|
+
| Failed | <Y> |
|
|
14
|
+
| Auto-heals | <Z> |
|
|
15
|
+
| Final Status | <PASS|FAIL> |
|
|
16
|
+
|
|
17
|
+
## Test Results
|
|
18
|
+
|
|
19
|
+
<!-- List each test with pass/fail status -->
|
|
20
|
+
|
|
21
|
+
## Auto-Heal Log
|
|
22
|
+
|
|
23
|
+
<!-- If heals were performed, document each attempt -->
|
|
24
|
+
|
|
25
|
+
## Recommendations
|
|
26
|
+
|
|
27
|
+
<!-- For failed tests, specific file:line references and fixes -->
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Test Plan: <change-name>
|
|
2
|
+
|
|
3
|
+
Generated from: `openspec/changes/<change-name>/specs/`
|
|
4
|
+
|
|
5
|
+
## Auth Requirements
|
|
6
|
+
|
|
7
|
+
<!-- Mark auth requirements based on specs analysis: -->
|
|
8
|
+
- Auth required: **yes / no**
|
|
9
|
+
- Roles needed: none / user / admin / user+admin
|
|
10
|
+
|
|
11
|
+
## Test Cases
|
|
12
|
+
|
|
13
|
+
### <test-name>
|
|
14
|
+
|
|
15
|
+
- **Route**: `/<page>`
|
|
16
|
+
- **Role**: `@role(<role>)`
|
|
17
|
+
- **Auth**: `@auth(required|none)`
|
|
18
|
+
|
|
19
|
+
**Happy path:**
|
|
20
|
+
- Step 1: ...
|
|
21
|
+
- Step 2: ...
|
|
22
|
+
|
|
23
|
+
**Error paths:**
|
|
24
|
+
- ...
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openspec-playwright",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.60",
|
|
4
4
|
"description": "OpenSpec + Playwright E2E verification setup tool for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
"release": "npm version patch && npm run build && git push && git push --tags && npm publish"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@types/tar": "^6.1.13",
|
|
17
18
|
"chalk": "^5.3.0",
|
|
18
19
|
"commander": "^12.1.0",
|
|
19
|
-
"tar": "^7.
|
|
20
|
+
"tar": "^7.5.13"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@types/node": "^22.0.0",
|
package/release-notes.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## What's Changed
|
|
2
2
|
|
|
3
|
-
- feat
|
|
3
|
+
- feat: fix MCP_VERSION dup, expand seed, add selector memory (v2.10)
|
|
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.60
|
|
@@ -70,6 +70,16 @@ Priority: `[data-testid]` > `getByRole` > `getByLabel` > `getByText` > CSS
|
|
|
70
70
|
| auth method | API / UI | See auth.setup.ts |
|
|
71
71
|
| multi-user roles | `<roles>` | e.g. admin, user, guest |
|
|
72
72
|
|
|
73
|
+
## Selector Fixes (Healer memory)
|
|
74
|
+
|
|
75
|
+
Persists selector repairs across sessions. Prevents the same selector from being healed repeatedly.
|
|
76
|
+
|
|
77
|
+
| Date | Route | Old Selector | New Selector | Reason |
|
|
78
|
+
|------|-------|-------------|-------------|--------|
|
|
79
|
+
| | | | | |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
73
83
|
## Changelog
|
|
74
84
|
|
|
75
85
|
| Date | Change | By |
|
|
@@ -78,4 +88,4 @@ Priority: `[data-testid]` > `getByRole` > `getByLabel` > `getByText` > CSS
|
|
|
78
88
|
|
|
79
89
|
---
|
|
80
90
|
|
|
81
|
-
> **Updating this file**: After each E2E exploration (Step 4), extract new shared patterns and update this file. Generator (Step 6) reads this before writing tests.
|
|
91
|
+
> **Updating this file**: After each E2E exploration (Step 4), extract new shared patterns and update this file. Generator (Step 6) reads this before writing tests. After Healer repairs (Step 9), append the fix here.
|
package/src/commands/mcpSync.ts
CHANGED
|
@@ -24,6 +24,14 @@ export function getStoredMcpVersion(skillContent: string): string | null {
|
|
|
24
24
|
return skillContent.slice(idx + MCP_VERSION_MARKER.length, end).trim();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Remove all existing MCP_VERSION comment lines from content */
|
|
28
|
+
function removeMcpVersionMarkers(content: string): string {
|
|
29
|
+
return content
|
|
30
|
+
.split('\n')
|
|
31
|
+
.filter(line => !line.trim().startsWith(MCP_VERSION_MARKER))
|
|
32
|
+
.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
/** Build the Healer tools table markdown */
|
|
28
36
|
function buildHealerTable(version: string, tools: Array<{ name: string; purpose: string }>): string {
|
|
29
37
|
const rows = tools.map(t => `| \`${t.name}\` | ${t.purpose} |`).join('\n');
|
|
@@ -36,13 +44,14 @@ export function updateHealerTable(
|
|
|
36
44
|
version: string,
|
|
37
45
|
tools: Array<{ name: string; purpose: string }>
|
|
38
46
|
): string {
|
|
39
|
-
const
|
|
47
|
+
const noMarkers = removeMcpVersionMarkers(skillContent);
|
|
48
|
+
const start = noMarkers.indexOf('| Tool | Purpose |');
|
|
40
49
|
if (start === -1) return skillContent;
|
|
41
|
-
let end =
|
|
42
|
-
if (end === -1) end =
|
|
50
|
+
let end = noMarkers.indexOf('\n\n', start);
|
|
51
|
+
if (end === -1) end = noMarkers.length;
|
|
43
52
|
|
|
44
|
-
const before =
|
|
45
|
-
const after =
|
|
53
|
+
const before = noMarkers.slice(0, start);
|
|
54
|
+
const after = noMarkers.slice(end);
|
|
46
55
|
return before + buildHealerTable(version, tools) + after;
|
|
47
56
|
}
|
|
48
57
|
|
package/templates/seed.spec.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Customize the page object and base URL for your application
|
|
4
4
|
|
|
5
5
|
import { test, expect, Page } from '@playwright/test';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
6
7
|
|
|
7
8
|
// Customize these for your application
|
|
8
9
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
|
@@ -63,6 +64,27 @@ test.describe('Application smoke tests', () => {
|
|
|
63
64
|
});
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
// ──────────────────────────────────────────────
|
|
68
|
+
// Environment validation (runs in Step 3)
|
|
69
|
+
// ──────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
test.describe('Environment validation', () => {
|
|
72
|
+
test('BASE_URL responds 200', async ({ page }) => {
|
|
73
|
+
const res = await page.request.get(`${BASE_URL}/`);
|
|
74
|
+
expect(res.status(), `BASE_URL ${BASE_URL} returned ${res.status()}`).toBeLessThan(500);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('auth storageState exists if credentials provided', async () => {
|
|
78
|
+
if (!process.env.E2E_USERNAME) return; // skip if no credentials configured
|
|
79
|
+
const authPath = 'playwright/.auth/user.json';
|
|
80
|
+
if (!existsSync(authPath)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Auth not configured. Run:\n npx playwright test --project=setup\nThen re-run /opsx:e2e.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
66
88
|
// ──────────────────────────────────────────────
|
|
67
89
|
// Example: Role-based tests with @tag
|
|
68
90
|
// Use tags (@admin, @user) for permission filtering instead of
|
|
Binary file
|