openspec-playwright 0.1.61 ā 0.1.63
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/LICENSE +21 -0
- package/README.md +4 -3
- package/README.zh-CN.md +4 -3
- package/bin/CLAUDE.md +11 -0
- package/dist/CLAUDE.md +17 -0
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +110 -73
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/editors.js +149 -95
- package/dist/commands/editors.js.map +1 -1
- package/dist/commands/init.js +105 -97
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcpSync.js +46 -31
- package/dist/commands/mcpSync.js.map +1 -1
- package/dist/commands/run.d.ts +13 -0
- package/dist/commands/run.js +74 -51
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/uninstall.d.ts +1 -0
- package/dist/commands/uninstall.js +133 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +92 -55
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +33 -26
- package/dist/index.js.map +1 -1
- package/package.json +19 -1
- package/schemas/playwright-e2e/templates/playwright.config.ts +22 -22
- package/templates/CLAUDE.md +15 -0
- package/templates/seed.spec.ts +5 -3
- package/.claude/commands/opsx/e2e-body.md +0 -39
- package/.claude/commands/opsx/e2e.md +0 -47
- package/.claude/skills/openspec-e2e/SKILL.md +0 -444
- package/.github/workflows/release.yml +0 -81
- package/docs/plans/2026-03-26-openspec-playwright-design.md +0 -180
- package/employee-standards.md +0 -42
- package/openspec/schemas/playwright-e2e/schema.yaml +0 -56
- package/openspec/schemas/playwright-e2e/templates/e2e-test.ts +0 -55
- package/openspec/schemas/playwright-e2e/templates/playwright.config.ts +0 -52
- package/openspec/schemas/playwright-e2e/templates/report.md +0 -27
- package/openspec/schemas/playwright-e2e/templates/test-plan.md +0 -24
- package/openspec-playwright-0.1.61.tgz +0 -0
- package/release-notes.md +0 -5
- package/src/commands/doctor.ts +0 -115
- package/src/commands/editors.ts +0 -606
- package/src/commands/init.ts +0 -252
- package/src/commands/mcpSync.ts +0 -160
- package/src/commands/run.ts +0 -172
- package/src/commands/update.ts +0 -165
- package/src/index.ts +0 -47
- package/tests/editors.test.ts +0 -180
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -9
package/src/commands/init.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
writeFileSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
} from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { homedir } from 'os';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import chalk from 'chalk';
|
|
12
|
-
import { readFile } from 'fs/promises';
|
|
13
|
-
import { syncMcpTools } from './mcpSync.js';
|
|
14
|
-
import { detectEditors, detectCodex, installForAllEditors, installSkill, installProjectClaudeMd, readEmployeeStandards, claudeAdapter } from './editors.js';
|
|
15
|
-
|
|
16
|
-
const TEMPLATE_DIR = fileURLToPath(new URL('../../templates', import.meta.url));
|
|
17
|
-
const SCHEMA_DIR = fileURLToPath(new URL('../../schemas', import.meta.url));
|
|
18
|
-
const SKILL_SRC = fileURLToPath(new URL('../../.claude/skills/openspec-e2e/SKILL.md', import.meta.url));
|
|
19
|
-
const CMD_BODY_SRC = fileURLToPath(new URL('../../.claude/commands/opsx/e2e-body.md', import.meta.url));
|
|
20
|
-
const EMPLOYEE_STANDARDS_SRC = fileURLToPath(new URL('../../employee-standards.md', import.meta.url));
|
|
21
|
-
|
|
22
|
-
export interface InitOptions {
|
|
23
|
-
change?: string;
|
|
24
|
-
mcp?: boolean;
|
|
25
|
-
seed?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function init(options: InitOptions) {
|
|
29
|
-
console.log(chalk.blue('\nš§ OpenSpec + Playwright E2E Setup\n'));
|
|
30
|
-
|
|
31
|
-
const projectRoot = process.cwd();
|
|
32
|
-
|
|
33
|
-
// 1. Check prerequisites
|
|
34
|
-
console.log(chalk.blue('āāā Prerequisites āāā'));
|
|
35
|
-
|
|
36
|
-
const hasNode = execCmd('node --version', 'Node.js', true);
|
|
37
|
-
const hasNpm = execCmd('npm --version', 'npm', true);
|
|
38
|
-
const hasOpenspec = execCmd('npx openspec --version 2>/dev/null || echo "not found"', 'OpenSpec', true);
|
|
39
|
-
|
|
40
|
-
if (!hasNode || !hasNpm) {
|
|
41
|
-
console.log(chalk.red(' ā Node.js/npm is required'));
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
console.log(chalk.green(' ā Node.js and npm found'));
|
|
45
|
-
|
|
46
|
-
// 2. Check OpenSpec
|
|
47
|
-
if (!existsSync(join(projectRoot, 'openspec'))) {
|
|
48
|
-
console.log(chalk.yellow('\nā OpenSpec not initialized. Run these commands first:'));
|
|
49
|
-
console.log(chalk.gray(' npm install -g @fission-ai/openspec'));
|
|
50
|
-
console.log(chalk.gray(' openspec init'));
|
|
51
|
-
console.log(chalk.gray(' openspec config profile core'));
|
|
52
|
-
console.log(chalk.gray(' openspec update\n'));
|
|
53
|
-
console.log(chalk.gray(' Then run openspec-pw init again.\n'));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
console.log(chalk.green(' ā OpenSpec initialized'));
|
|
57
|
-
|
|
58
|
-
// 3. Install Playwright MCP (global)
|
|
59
|
-
if (options.mcp !== false) {
|
|
60
|
-
console.log(chalk.blue('\nāāā Installing Playwright MCP āāā'));
|
|
61
|
-
|
|
62
|
-
// Check if playwright MCP already exists in global config
|
|
63
|
-
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
64
|
-
const claudeJson = existsSync(claudeJsonPath) ? JSON.parse(readFileSync(claudeJsonPath, 'utf-8')) : {};
|
|
65
|
-
const globalMcp = claudeJson?.mcpServers ?? {};
|
|
66
|
-
const localMcp = claudeJson?.projects?.[projectRoot]?.mcpServers ?? {};
|
|
67
|
-
|
|
68
|
-
if (globalMcp['playwright'] || localMcp['playwright']) {
|
|
69
|
-
console.log(chalk.green(' ā Playwright MCP already installed'));
|
|
70
|
-
} else {
|
|
71
|
-
try {
|
|
72
|
-
execSync('claude mcp add playwright npx @playwright/mcp@latest', {
|
|
73
|
-
cwd: projectRoot,
|
|
74
|
-
stdio: 'inherit',
|
|
75
|
-
});
|
|
76
|
-
console.log(chalk.green(' ā Playwright MCP installed globally'));
|
|
77
|
-
console.log(chalk.gray(' (Restart Claude Code to activate)'));
|
|
78
|
-
} catch {
|
|
79
|
-
console.log(chalk.yellow(' ā Failed to run claude mcp add'));
|
|
80
|
-
console.log(chalk.gray(' Run manually: claude mcp add playwright npx @playwright/mcp@latest'));
|
|
81
|
-
console.log(chalk.gray(' (Restart Claude Code to activate the MCP server)'));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 4. Install E2E commands for detected editors
|
|
87
|
-
console.log(chalk.blue('\nāāā Installing E2E Commands āāā'));
|
|
88
|
-
const detected = detectEditors(projectRoot);
|
|
89
|
-
const codex = detectCodex();
|
|
90
|
-
const adapters = codex ? [...detected, codex] : detected;
|
|
91
|
-
if (adapters.length > 0) {
|
|
92
|
-
const body = await readFile(CMD_BODY_SRC, 'utf-8');
|
|
93
|
-
installForAllEditors(body, adapters, projectRoot);
|
|
94
|
-
} else {
|
|
95
|
-
const body = await readFile(CMD_BODY_SRC, 'utf-8');
|
|
96
|
-
installForAllEditors(body, [claudeAdapter], projectRoot);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Claude Code also gets the SKILL.md
|
|
100
|
-
if (existsSync(join(projectRoot, '.claude'))) {
|
|
101
|
-
const skillContent = await readFile(SKILL_SRC, 'utf-8');
|
|
102
|
-
installSkill(projectRoot, skillContent);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 5. Sync Healer tools with latest @playwright/mcp (Claude Code only)
|
|
106
|
-
if (existsSync(join(projectRoot, '.claude'))) {
|
|
107
|
-
console.log(chalk.blue('\nāāā Syncing Healer Tools āāā'));
|
|
108
|
-
const skillDest = join(projectRoot, '.claude', 'skills', 'openspec-e2e', 'SKILL.md');
|
|
109
|
-
await syncMcpTools(skillDest, true);
|
|
110
|
-
} else {
|
|
111
|
-
console.log(chalk.blue('\nāāā Syncing Healer Tools āāā'));
|
|
112
|
-
console.log(chalk.gray(' - Claude Code not detected, skipping MCP sync'));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 6. Install OpenSpec schema
|
|
116
|
-
console.log(chalk.blue('\nāāā Installing OpenSpec Schema āāā'));
|
|
117
|
-
await installSchema(projectRoot);
|
|
118
|
-
|
|
119
|
-
// 7. Generate seed test
|
|
120
|
-
if (options.seed !== false) {
|
|
121
|
-
console.log(chalk.blue('\nāāā Generating Seed Test āāā'));
|
|
122
|
-
await generateSeedTest(projectRoot);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 8. Generate app-knowledge.md
|
|
126
|
-
console.log(chalk.blue('\nāāā Generating App Knowledge āāā'));
|
|
127
|
-
await generateAppKnowledge(projectRoot);
|
|
128
|
-
|
|
129
|
-
// 9. Install employee-grade CLAUDE.md
|
|
130
|
-
console.log(chalk.blue('\nāāā Installing Employee Standards āāā'));
|
|
131
|
-
const standards = readEmployeeStandards(EMPLOYEE_STANDARDS_SRC);
|
|
132
|
-
if (standards) {
|
|
133
|
-
installProjectClaudeMd(projectRoot, standards);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 10. Summary
|
|
137
|
-
console.log(chalk.blue('\nāāā Summary āāā'));
|
|
138
|
-
console.log(chalk.green(' ā Setup complete!\n'));
|
|
139
|
-
|
|
140
|
-
console.log(chalk.bold('Next steps:'));
|
|
141
|
-
console.log(chalk.gray(' 1. Install Playwright browsers: npx playwright install --with-deps'));
|
|
142
|
-
console.log(chalk.gray(' 2. Customize tests/playwright/credentials.yaml with your test user'));
|
|
143
|
-
console.log(chalk.gray(' 3. Set credentials: export E2E_USERNAME=xxx E2E_PASSWORD=yyy'));
|
|
144
|
-
console.log(chalk.gray(' 4. Run auth setup: npx playwright test --project=setup'));
|
|
145
|
-
const hasClaude = existsSync(join(projectRoot, '.claude'));
|
|
146
|
-
if (hasClaude) {
|
|
147
|
-
console.log(chalk.gray(' 5. In Claude Code, run: /opsx:e2e <change-name>'));
|
|
148
|
-
}
|
|
149
|
-
console.log(chalk.gray(` ${hasClaude ? '6.' : '5.'} Or: openspec-pw run <change-name>`));
|
|
150
|
-
console.log(chalk.gray(` ${hasClaude ? '7.' : '6.'} Or: openspec-pw doctor to verify setup\n`));
|
|
151
|
-
|
|
152
|
-
console.log(chalk.bold('How it works:'));
|
|
153
|
-
console.log(chalk.gray(' /opsx:e2e reads your OpenSpec specs and runs Playwright'));
|
|
154
|
-
console.log(chalk.gray(' E2E tests through a three-agent pipeline:'));
|
|
155
|
-
console.log(chalk.gray(' Planner ā Generator ā Healer\n'));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function generateSeedTest(projectRoot: string) {
|
|
159
|
-
const testsDir = join(projectRoot, 'tests', 'playwright');
|
|
160
|
-
mkdirSync(testsDir, { recursive: true });
|
|
161
|
-
|
|
162
|
-
const seedPath = join(testsDir, 'seed.spec.ts');
|
|
163
|
-
if (existsSync(seedPath)) {
|
|
164
|
-
console.log(chalk.gray(' - seed.spec.ts already exists, skipping'));
|
|
165
|
-
} else {
|
|
166
|
-
const seedContent = await readFile(TEMPLATE_DIR + '/seed.spec.ts', 'utf-8');
|
|
167
|
-
writeFileSync(seedPath, seedContent);
|
|
168
|
-
console.log(chalk.green(' ā Generated: tests/playwright/seed.spec.ts'));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Generate auth.setup.ts
|
|
172
|
-
const authSetupPath = join(testsDir, 'auth.setup.ts');
|
|
173
|
-
if (existsSync(authSetupPath)) {
|
|
174
|
-
console.log(chalk.gray(' - auth.setup.ts already exists, skipping'));
|
|
175
|
-
} else {
|
|
176
|
-
const authContent = await readFile(TEMPLATE_DIR + '/auth.setup.ts', 'utf-8');
|
|
177
|
-
writeFileSync(authSetupPath, authContent);
|
|
178
|
-
console.log(chalk.green(' ā Generated: tests/playwright/auth.setup.ts'));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Generate credentials.yaml
|
|
182
|
-
const credsPath = join(testsDir, 'credentials.yaml');
|
|
183
|
-
if (existsSync(credsPath)) {
|
|
184
|
-
console.log(chalk.gray(' - credentials.yaml already exists, skipping'));
|
|
185
|
-
} else {
|
|
186
|
-
const credsContent = await readFile(TEMPLATE_DIR + '/credentials.yaml', 'utf-8');
|
|
187
|
-
writeFileSync(credsPath, credsContent);
|
|
188
|
-
console.log(chalk.green(' ā Generated: tests/playwright/credentials.yaml'));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
console.log(chalk.gray(' (Customize BASE_URL and credentials for your app)'));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function generateAppKnowledge(projectRoot: string) {
|
|
195
|
-
const src = join(SCHEMA_DIR, 'playwright-e2e', 'templates', 'app-knowledge.md');
|
|
196
|
-
const dest = join(projectRoot, 'tests', 'playwright', 'app-knowledge.md');
|
|
197
|
-
|
|
198
|
-
if (existsSync(dest)) {
|
|
199
|
-
console.log(chalk.gray(' - app-knowledge.md already exists, skipping'));
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (existsSync(src)) {
|
|
204
|
-
writeFileSync(dest, readFileSync(src));
|
|
205
|
-
console.log(chalk.green(' ā Generated: tests/playwright/app-knowledge.md'));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function installSchema(projectRoot: string) {
|
|
210
|
-
const schemaSrc = SCHEMA_DIR + '/playwright-e2e';
|
|
211
|
-
const schemaDest = join(projectRoot, 'openspec', 'schemas', 'playwright-e2e');
|
|
212
|
-
const schemaFiles = ['schema.yaml'];
|
|
213
|
-
|
|
214
|
-
mkdirSync(schemaDest, { recursive: true });
|
|
215
|
-
for (const file of schemaFiles) {
|
|
216
|
-
const src = join(schemaSrc, file);
|
|
217
|
-
const dest = join(schemaDest, file);
|
|
218
|
-
if (existsSync(src)) {
|
|
219
|
-
writeFileSync(dest, readFileSync(src));
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Copy templates
|
|
224
|
-
const templatesSrc = join(schemaSrc, 'templates');
|
|
225
|
-
const templatesDest = join(schemaDest, 'templates');
|
|
226
|
-
mkdirSync(templatesDest, { recursive: true });
|
|
227
|
-
const templateFiles = ['test-plan.md', 'report.md', 'e2e-test.ts', 'playwright.config.ts', 'app-knowledge.md'];
|
|
228
|
-
for (const file of templateFiles) {
|
|
229
|
-
const src = join(templatesSrc, file);
|
|
230
|
-
const dest = join(templatesDest, file);
|
|
231
|
-
if (existsSync(src)) {
|
|
232
|
-
writeFileSync(dest, readFileSync(src));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
console.log(chalk.green(' ā Schema installed: openspec/schemas/playwright-e2e/'));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function execCmd(
|
|
240
|
-
cmd: string,
|
|
241
|
-
name: string,
|
|
242
|
-
silent = false
|
|
243
|
-
): boolean {
|
|
244
|
-
try {
|
|
245
|
-
execSync(cmd, { stdio: 'pipe' });
|
|
246
|
-
if (!silent) console.log(chalk.green(` ā ${name} found`));
|
|
247
|
-
return true;
|
|
248
|
-
} catch {
|
|
249
|
-
if (!silent) console.log(chalk.yellow(` ā ${name} not found`));
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
package/src/commands/mcpSync.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
import { promisify } from 'util';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import * as tar from 'tar';
|
|
8
|
-
|
|
9
|
-
export const MCP_VERSION_MARKER = '<!-- MCP_VERSION:';
|
|
10
|
-
|
|
11
|
-
export const DEFAULT_HEALER_TOOLS = [
|
|
12
|
-
{ name: 'browser_navigate', purpose: "Go to the failing test's page" },
|
|
13
|
-
{ name: 'browser_snapshot', purpose: 'Get page structure to find equivalent selectors' },
|
|
14
|
-
{ name: 'browser_console_messages', purpose: 'Diagnose JS errors that may cause failures' },
|
|
15
|
-
{ name: 'browser_take_screenshot', purpose: 'Visually compare before/after fixes' },
|
|
16
|
-
{ name: 'browser_run_code', purpose: 'Execute custom fix logic (optional)' },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
/** Extract MCP version from SKILL.md marker */
|
|
20
|
-
export function getStoredMcpVersion(skillContent: string): string | null {
|
|
21
|
-
const idx = skillContent.indexOf(MCP_VERSION_MARKER);
|
|
22
|
-
if (idx === -1) return null;
|
|
23
|
-
const end = skillContent.indexOf(' -->', idx);
|
|
24
|
-
return skillContent.slice(idx + MCP_VERSION_MARKER.length, end).trim();
|
|
25
|
-
}
|
|
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
|
-
|
|
35
|
-
/** Build the Healer tools table markdown */
|
|
36
|
-
function buildHealerTable(version: string, tools: Array<{ name: string; purpose: string }>): string {
|
|
37
|
-
const rows = tools.map(t => `| \`${t.name}\` | ${t.purpose} |`).join('\n');
|
|
38
|
-
return `${MCP_VERSION_MARKER} ${version} -->\n\n| Tool | Purpose |\n|------|---------|\n${rows}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Replace the Healer tools table in SKILL.md */
|
|
42
|
-
export function updateHealerTable(
|
|
43
|
-
skillContent: string,
|
|
44
|
-
version: string,
|
|
45
|
-
tools: Array<{ name: string; purpose: string }>
|
|
46
|
-
): string {
|
|
47
|
-
const noMarkers = removeMcpVersionMarkers(skillContent);
|
|
48
|
-
const start = noMarkers.indexOf('| Tool | Purpose |');
|
|
49
|
-
if (start === -1) return skillContent;
|
|
50
|
-
let end = noMarkers.indexOf('\n\n', start);
|
|
51
|
-
if (end === -1) end = noMarkers.length;
|
|
52
|
-
|
|
53
|
-
const before = noMarkers.slice(0, start);
|
|
54
|
-
const after = noMarkers.slice(end);
|
|
55
|
-
return before + buildHealerTable(version, tools) + after;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Fetch latest @playwright/mcp version from npm registry */
|
|
59
|
-
export function getLatestMcpVersion(): Promise<string | null> {
|
|
60
|
-
return new Promise((resolve) => {
|
|
61
|
-
exec('npm show @playwright/mcp version --json', { timeout: 15000 }, (err, stdout) => {
|
|
62
|
-
if (err) { resolve(null); return; }
|
|
63
|
-
try { resolve(JSON.parse(stdout.trim())); } catch { resolve(null); }
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const execAsync = promisify(exec);
|
|
69
|
-
|
|
70
|
-
/** Extract a .tgz tarball to a destination directory (cross-platform) */
|
|
71
|
-
async function extractTarball(tarballPath: string, destDir: string): Promise<void> {
|
|
72
|
-
rmSync(destDir, { recursive: true, force: true });
|
|
73
|
-
mkdirSync(destDir, { recursive: true });
|
|
74
|
-
await tar.extract({ file: tarballPath, cwd: destDir, strip: 1 });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Parse README markdown to extract browser_* tool entries */
|
|
78
|
-
function parseMcpReadme(content: string): Array<{ name: string; purpose: string }> {
|
|
79
|
-
const tools: Array<{ name: string; purpose: string }> = [];
|
|
80
|
-
const re = /-\s+\*\*`?([^`*\n]+)`?\*\*\s*-\s*Title:\s*([^\n]+)/g;
|
|
81
|
-
let m;
|
|
82
|
-
while ((m = re.exec(content)) !== null) {
|
|
83
|
-
const name = m[1].trim();
|
|
84
|
-
if (name.startsWith('browser_')) {
|
|
85
|
-
const purpose = m[2].trim().replace(/\.$/, '');
|
|
86
|
-
tools.push({ name, purpose });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return tools;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Fetch @playwright/mcp tools from npm package.
|
|
94
|
-
* Downloads the tarball, extracts README, parses tool names.
|
|
95
|
-
*/
|
|
96
|
-
export async function fetchMcpTools(version: string): Promise<Array<{ name: string; purpose: string }>> {
|
|
97
|
-
const tmpDir = join(tmpdir(), `openspec-pw-mcp-${version}`);
|
|
98
|
-
try {
|
|
99
|
-
await execAsync(
|
|
100
|
-
`npm pack @playwright/mcp@${version} --pack-destination ${tmpDir}`,
|
|
101
|
-
{ timeout: 30000 }
|
|
102
|
-
);
|
|
103
|
-
const tgzFiles = readdirSync(tmpDir).filter(f => f.startsWith('playwright-mcp-') && f.endsWith('.tgz'));
|
|
104
|
-
if (tgzFiles.length === 0) return [];
|
|
105
|
-
const tarballPath = join(tmpDir, tgzFiles[0]);
|
|
106
|
-
const extractDir = join(tmpDir, 'pkg');
|
|
107
|
-
await extractTarball(tarballPath, extractDir);
|
|
108
|
-
const readmePath = join(extractDir, 'README.md');
|
|
109
|
-
const content = existsSync(readmePath) ? readFileSync(readmePath, 'utf-8') : '';
|
|
110
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
111
|
-
return parseMcpReadme(content);
|
|
112
|
-
} catch {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Sync Healer tools table in SKILL.md with latest @playwright/mcp.
|
|
119
|
-
* Returns true if updated, false if already current or failed.
|
|
120
|
-
*/
|
|
121
|
-
export async function syncMcpTools(
|
|
122
|
-
skillDest: string,
|
|
123
|
-
verbose = false
|
|
124
|
-
): Promise<boolean> {
|
|
125
|
-
const latestVersion = await getLatestMcpVersion();
|
|
126
|
-
if (!latestVersion) {
|
|
127
|
-
if (verbose) console.log(chalk.yellow(' ā Could not fetch latest @playwright/mcp version'));
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!existsSync(skillDest)) {
|
|
132
|
-
if (verbose) console.log(chalk.gray(' - SKILL.md not found, skipping MCP sync'));
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const skillContent = readFileSync(skillDest, 'utf-8');
|
|
137
|
-
const storedVersion = getStoredMcpVersion(skillContent);
|
|
138
|
-
|
|
139
|
-
if (storedVersion === latestVersion) {
|
|
140
|
-
if (verbose) console.log(chalk.gray(` - Healer tools current (${latestVersion})`));
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (verbose) console.log(chalk.blue(` - Updating from ${storedVersion ?? 'unknown'} ā ${latestVersion}`));
|
|
145
|
-
|
|
146
|
-
const tools = await fetchMcpTools(latestVersion);
|
|
147
|
-
const toolSet = tools.length > 0 ? tools : DEFAULT_HEALER_TOOLS;
|
|
148
|
-
|
|
149
|
-
const updated = updateHealerTable(skillContent, latestVersion, toolSet);
|
|
150
|
-
writeFileSync(skillDest, updated);
|
|
151
|
-
|
|
152
|
-
if (verbose) {
|
|
153
|
-
if (tools.length > 0) {
|
|
154
|
-
console.log(chalk.green(` ā Healer tools synced to ${latestVersion} (${tools.length} tools)`));
|
|
155
|
-
} else {
|
|
156
|
-
console.log(chalk.green(` ā Healer tools synced to ${latestVersion} (default set)`));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return true;
|
|
160
|
-
}
|
package/src/commands/run.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
|
|
6
|
-
export interface RunOptions {
|
|
7
|
-
project?: string;
|
|
8
|
-
timeout?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const REPORTS_DIR = 'openspec/reports';
|
|
12
|
-
|
|
13
|
-
export async function run(changeName: string, options: RunOptions) {
|
|
14
|
-
console.log(chalk.blue(`\nš OpenSpec Playwright E2E: ${changeName}\n`));
|
|
15
|
-
|
|
16
|
-
const projectRoot = process.cwd();
|
|
17
|
-
|
|
18
|
-
// 1. Verify test file exists
|
|
19
|
-
const testFile = join(projectRoot, 'tests', 'playwright', `${changeName}.spec.ts`);
|
|
20
|
-
if (!existsSync(testFile)) {
|
|
21
|
-
console.log(chalk.red(` ā Test file not found: tests/playwright/${changeName}.spec.ts`));
|
|
22
|
-
console.log(chalk.gray(' Run /opsx:e2e first to generate tests.\n'));
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 2. Setup reports dir
|
|
27
|
-
mkdirSync(join(projectRoot, REPORTS_DIR), { recursive: true });
|
|
28
|
-
|
|
29
|
-
// 3. Detect auth credentials
|
|
30
|
-
const credsPath = join(projectRoot, 'tests', 'playwright', 'credentials.yaml');
|
|
31
|
-
const hasCredentials = existsSync(credsPath);
|
|
32
|
-
if (!hasCredentials) {
|
|
33
|
-
console.log(chalk.yellow(' ā No credentials.yaml found ā tests may fail if auth required'));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 4. Run Playwright tests with output capture
|
|
37
|
-
console.log(chalk.blue('āāā Running Tests āāā'));
|
|
38
|
-
|
|
39
|
-
const args = ['npx', 'playwright', 'test', testFile, '--reporter=list'];
|
|
40
|
-
if (options.project) {
|
|
41
|
-
args.push('--project=' + options.project);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let testOutput = '';
|
|
45
|
-
let exitCode = 0;
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
// Capture stdout to detect port mismatch
|
|
49
|
-
const result = execSync(args.join(' '), {
|
|
50
|
-
cwd: projectRoot,
|
|
51
|
-
encoding: 'utf-8',
|
|
52
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
-
timeout: (options.timeout ?? 300) * 1000,
|
|
54
|
-
});
|
|
55
|
-
testOutput = result;
|
|
56
|
-
} catch (err: unknown) {
|
|
57
|
-
exitCode = 1;
|
|
58
|
-
const error = err as { stdout?: string; stderr?: string; status?: number };
|
|
59
|
-
testOutput = (error.stdout ?? '') + (error.stderr ?? '');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 5. Parse results from Playwright output
|
|
63
|
-
const results = parsePlaywrightOutput(testOutput);
|
|
64
|
-
|
|
65
|
-
// 6. Detect port mismatch
|
|
66
|
-
if (testOutput.includes('net::ERR_CONNECTION_REFUSED') ||
|
|
67
|
-
testOutput.includes('listen EADDRINUSE') ||
|
|
68
|
-
testOutput.includes('0.0.0.0:')) {
|
|
69
|
-
console.log(chalk.yellow(' ā Port mismatch detected. Check BASE_URL and webServer port.'));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 7. Generate markdown report
|
|
73
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
74
|
-
const reportPath = join(projectRoot, REPORTS_DIR, `playwright-e2e-${changeName}-${timestamp}.md`);
|
|
75
|
-
|
|
76
|
-
const reportContent = generateReport(changeName, timestamp, results);
|
|
77
|
-
writeFileSync(reportPath, reportContent);
|
|
78
|
-
|
|
79
|
-
// 8. Summary
|
|
80
|
-
console.log(chalk.blue('\nāāā Results āāā'));
|
|
81
|
-
console.log(` Tests: ${results.total} ` +
|
|
82
|
-
chalk.green(`ā ${results.passed}`) + ' ' +
|
|
83
|
-
(results.failed > 0 ? chalk.red(`ā ${results.failed}`) : chalk.gray(`ā ${results.failed}`)) +
|
|
84
|
-
` Duration: ${results.duration}`);
|
|
85
|
-
|
|
86
|
-
console.log(chalk.blue('\nāāā Report āāā'));
|
|
87
|
-
console.log(chalk.green(` ā ${reportPath}\n`));
|
|
88
|
-
|
|
89
|
-
if (results.failed > 0) {
|
|
90
|
-
console.log(chalk.red(`ā E2E verification FAILED (${results.failed} tests)`));
|
|
91
|
-
process.exit(1);
|
|
92
|
-
} else {
|
|
93
|
-
console.log(chalk.green('ā E2E verification PASSED'));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface TestResults {
|
|
98
|
-
total: number;
|
|
99
|
-
passed: number;
|
|
100
|
-
failed: number;
|
|
101
|
-
duration: string;
|
|
102
|
-
tests: Array<{ name: string; status: 'passed' | 'failed' }>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function parsePlaywrightOutput(output: string): TestResults {
|
|
106
|
-
const results: TestResults = { total: 0, passed: 0, failed: 0, duration: '0s', tests: [] };
|
|
107
|
-
|
|
108
|
-
// Parse: "ā my-test (1.2s)" or "ā my-test (0.5s)"
|
|
109
|
-
const testLineRegex = /([āāx]) (.+?) \((\d+(?:\.\d+)?[a-z]+)\)/g;
|
|
110
|
-
let match;
|
|
111
|
-
while ((match = testLineRegex.exec(output)) !== null) {
|
|
112
|
-
const status = match[1] === 'ā' ? 'passed' : 'failed';
|
|
113
|
-
results.tests.push({ name: match[2], status });
|
|
114
|
-
results.total++;
|
|
115
|
-
if (status === 'passed') results.passed++;
|
|
116
|
-
else results.failed++;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Parse duration: "N tests ran (1m 30s)" or "1 test ran (5s)"
|
|
120
|
-
const durationMatch = output.match(/\d+ tests? ran \((\d+(?:m\s*\d+)?s?)\)/);
|
|
121
|
-
if (durationMatch) results.duration = durationMatch[1];
|
|
122
|
-
|
|
123
|
-
return results;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function generateReport(changeName: string, timestamp: string, results: TestResults): string {
|
|
127
|
-
const lines: string[] = [
|
|
128
|
-
`# E2E Verify Report: ${changeName}`,
|
|
129
|
-
'',
|
|
130
|
-
`**Change**: \`${changeName}\``,
|
|
131
|
-
`**Generated**: ${timestamp.replace('T', ' ').slice(0, 16)} UTC`,
|
|
132
|
-
'',
|
|
133
|
-
'## Summary',
|
|
134
|
-
'',
|
|
135
|
-
'| Check | Status |',
|
|
136
|
-
'|-------|--------|',
|
|
137
|
-
`| Tests Run | ${results.total} |`,
|
|
138
|
-
`| Passed | ${results.passed} |`,
|
|
139
|
-
`| Failed | ${results.failed} |`,
|
|
140
|
-
`| Duration | ${results.duration} |`,
|
|
141
|
-
`| Final Status | ${results.failed === 0 ? 'ā
PASS' : 'ā FAIL'} |`,
|
|
142
|
-
'',
|
|
143
|
-
'## Test Results',
|
|
144
|
-
'',
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
if (results.tests.length === 0) {
|
|
148
|
-
lines.push('_(No test output captured ā check Playwright configuration)_', '');
|
|
149
|
-
} else {
|
|
150
|
-
for (const test of results.tests) {
|
|
151
|
-
const icon = test.status === 'passed' ? 'ā
' : 'ā';
|
|
152
|
-
lines.push(`- ${test.name}: ${icon} ${test.status}`);
|
|
153
|
-
}
|
|
154
|
-
lines.push('');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
lines.push('## Recommendations', '');
|
|
158
|
-
if (results.failed > 0) {
|
|
159
|
-
lines.push(
|
|
160
|
-
'Review failed tests above. Common fixes:',
|
|
161
|
-
'- Update selectors if UI changed (use `data-testid` attributes)',
|
|
162
|
-
'- Adjust BASE_URL in seed.spec.ts if port differs',
|
|
163
|
-
'- Set E2E_USERNAME/E2E_PASSWORD if auth is required',
|
|
164
|
-
'- Check `npx playwright show-report` for screenshots',
|
|
165
|
-
''
|
|
166
|
-
);
|
|
167
|
-
} else {
|
|
168
|
-
lines.push('All tests passed. No action needed.', '');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return lines.join('\n');
|
|
172
|
-
}
|