openspec-playwright 0.1.16

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.
Files changed (38) hide show
  1. package/.claude/commands/opsx/e2e.md +8 -0
  2. package/.claude/skills/openspec-e2e/SKILL.md +138 -0
  3. package/.github/workflows/release.yml +41 -0
  4. package/README.md +133 -0
  5. package/README.zh-CN.md +118 -0
  6. package/bin/openspec-pw +4 -0
  7. package/bin/openspec-pw.js +2 -0
  8. package/dist/commands/doctor.d.ts +1 -0
  9. package/dist/commands/doctor.js +110 -0
  10. package/dist/commands/doctor.js.map +1 -0
  11. package/dist/commands/init.d.ts +6 -0
  12. package/dist/commands/init.js +174 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/run.d.ts +5 -0
  15. package/dist/commands/run.js +135 -0
  16. package/dist/commands/run.js.map +1 -0
  17. package/dist/commands/update.d.ts +5 -0
  18. package/dist/commands/update.js +91 -0
  19. package/dist/commands/update.js.map +1 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +40 -0
  22. package/dist/index.js.map +1 -0
  23. package/docs/plans/2026-03-26-openspec-playwright-design.md +180 -0
  24. package/package.json +39 -0
  25. package/schemas/playwright-e2e/schema.yaml +56 -0
  26. package/schemas/playwright-e2e/templates/e2e-test.ts +55 -0
  27. package/schemas/playwright-e2e/templates/playwright.config.ts +52 -0
  28. package/schemas/playwright-e2e/templates/report.md +27 -0
  29. package/schemas/playwright-e2e/templates/test-plan.md +24 -0
  30. package/src/commands/doctor.ts +114 -0
  31. package/src/commands/init.ts +209 -0
  32. package/src/commands/run.ts +172 -0
  33. package/src/commands/update.ts +130 -0
  34. package/src/index.ts +47 -0
  35. package/templates/auth.setup.ts +77 -0
  36. package/templates/credentials.yaml +33 -0
  37. package/templates/seed.spec.ts +63 -0
  38. package/tsconfig.json +18 -0
@@ -0,0 +1,209 @@
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 chalk from 'chalk';
10
+ import { readFile } from 'fs/promises';
11
+
12
+ const TEMPLATE_DIR = new URL('../../templates', import.meta.url).pathname;
13
+ const SCHEMA_DIR = new URL('../../schemas', import.meta.url).pathname;
14
+ const SKILL_SRC = new URL('../../.claude/skills/openspec-e2e', import.meta.url).pathname;
15
+ const CMD_SRC = new URL('../../.claude/commands/opsx/e2e.md', import.meta.url).pathname;
16
+
17
+ export interface InitOptions {
18
+ change?: string;
19
+ mcp?: boolean;
20
+ seed?: boolean;
21
+ }
22
+
23
+ export async function init(options: InitOptions) {
24
+ console.log(chalk.blue('\nšŸ”§ OpenSpec + Playwright E2E Setup\n'));
25
+
26
+ const projectRoot = process.cwd();
27
+
28
+ // 1. Check prerequisites
29
+ console.log(chalk.blue('─── Prerequisites ───'));
30
+
31
+ const hasNode = execCmd('node --version', 'Node.js', true);
32
+ const hasNpm = execCmd('npm --version', 'npm', true);
33
+ const hasOpenspec = execCmd('npx openspec --version 2>/dev/null || echo "not found"', 'OpenSpec', true);
34
+
35
+ if (!hasNode || !hasNpm) {
36
+ console.log(chalk.red(' āœ— Node.js/npm is required'));
37
+ process.exit(1);
38
+ }
39
+ console.log(chalk.green(' āœ“ Node.js and npm found'));
40
+
41
+ // 2. Check OpenSpec
42
+ if (!existsSync(join(projectRoot, 'openspec'))) {
43
+ console.log(chalk.yellow('\n⚠ OpenSpec not initialized. Run these commands first:'));
44
+ console.log(chalk.gray(' npm install -g @fission-ai/openspec'));
45
+ console.log(chalk.gray(' openspec init'));
46
+ console.log(chalk.gray(' openspec config profile core'));
47
+ console.log(chalk.gray(' openspec update\n'));
48
+ console.log(chalk.gray(' Then run openspec-pw init again.\n'));
49
+ return;
50
+ }
51
+ console.log(chalk.green(' āœ“ OpenSpec initialized'));
52
+
53
+ // 3. Install Playwright MCP (global)
54
+ if (options.mcp !== false) {
55
+ console.log(chalk.blue('\n─── Installing Playwright MCP ───'));
56
+
57
+ // Check if playwright MCP already exists in global config
58
+ const claudeJsonPath = join(process.env.HOME ?? '', '.claude.json');
59
+ const claudeJson = existsSync(claudeJsonPath) ? JSON.parse(readFileSync(claudeJsonPath, 'utf-8')) : {};
60
+ const globalMcp = claudeJson?.mcpServers ?? {};
61
+ const localMcp = claudeJson?.projects?.[projectRoot]?.mcpServers ?? {};
62
+
63
+ if (globalMcp['playwright'] || localMcp['playwright']) {
64
+ console.log(chalk.green(' āœ“ Playwright MCP already installed'));
65
+ } else {
66
+ try {
67
+ execSync('claude mcp add playwright npx @playwright/mcp@latest', {
68
+ cwd: projectRoot,
69
+ stdio: 'inherit',
70
+ });
71
+ console.log(chalk.green(' āœ“ Playwright MCP installed globally'));
72
+ console.log(chalk.gray(' (Restart Claude Code to activate)'));
73
+ } catch {
74
+ console.log(chalk.yellow(' ⚠ Failed to run claude mcp add'));
75
+ console.log(chalk.gray(' Run manually: claude mcp add playwright npx @playwright/mcp@latest'));
76
+ console.log(chalk.gray(' (Restart Claude Code to activate the MCP server)'));
77
+ }
78
+ }
79
+ }
80
+
81
+ // 4. Copy skill files
82
+ console.log(chalk.blue('\n─── Installing Claude Code Skill ───'));
83
+ await installSkill(projectRoot);
84
+
85
+ // 5. Install OpenSpec schema
86
+ console.log(chalk.blue('\n─── Installing OpenSpec Schema ───'));
87
+ await installSchema(projectRoot);
88
+
89
+ // 6. Generate seed test
90
+ if (options.seed !== false) {
91
+ console.log(chalk.blue('\n─── Generating Seed Test ───'));
92
+ await generateSeedTest(projectRoot);
93
+ }
94
+
95
+ // 7. Summary
96
+ console.log(chalk.blue('\n─── Summary ───'));
97
+ console.log(chalk.green(' āœ“ Setup complete!\n'));
98
+
99
+ console.log(chalk.bold('Next steps:'));
100
+ console.log(chalk.gray(' 1. Install Playwright browsers: npx playwright install --with-deps'));
101
+ console.log(chalk.gray(' 2. Customize tests/playwright/credentials.yaml with your test user'));
102
+ console.log(chalk.gray(' 3. Set credentials: export E2E_USERNAME=xxx E2E_PASSWORD=yyy'));
103
+ console.log(chalk.gray(' 4. Run auth setup: npx playwright test --project=setup'));
104
+ console.log(chalk.gray(' 5. In Claude Code, run: /opsx:e2e <change-name>'));
105
+ console.log(chalk.gray(' 6. Or: openspec-pw doctor to verify setup\n'));
106
+
107
+ console.log(chalk.bold('How it works:'));
108
+ console.log(chalk.gray(' /opsx:e2e reads your OpenSpec specs and runs Playwright'));
109
+ console.log(chalk.gray(' E2E tests through a three-agent pipeline:'));
110
+ console.log(chalk.gray(' Planner → Generator → Healer\n'));
111
+ }
112
+
113
+ async function installSkill(projectRoot: string) {
114
+ const skillsDir = join(projectRoot, '.claude', 'skills');
115
+ const skillDir = join(skillsDir, 'openspec-e2e');
116
+ const cmdDir = join(projectRoot, '.claude', 'commands');
117
+
118
+ // Copy skill
119
+ mkdirSync(skillDir, { recursive: true });
120
+ const skillContent = await readFile(SKILL_SRC + '/SKILL.md', 'utf-8');
121
+ writeFileSync(join(skillDir, 'SKILL.md'), skillContent);
122
+ console.log(chalk.green(` āœ“ Skill installed: /openspec-e2e`));
123
+
124
+ // Copy command
125
+ mkdirSync(join(cmdDir, 'opsx'), { recursive: true });
126
+ const cmdContent = await readFile(CMD_SRC, 'utf-8');
127
+ writeFileSync(join(cmdDir, 'opsx', 'e2e.md'), cmdContent);
128
+ console.log(chalk.green(` āœ“ Command installed: /opsx:e2e`));
129
+ }
130
+
131
+ async function generateSeedTest(projectRoot: string) {
132
+ const testsDir = join(projectRoot, 'tests', 'playwright');
133
+ mkdirSync(testsDir, { recursive: true });
134
+
135
+ const seedPath = join(testsDir, 'seed.spec.ts');
136
+ if (existsSync(seedPath)) {
137
+ console.log(chalk.gray(' - seed.spec.ts already exists, skipping'));
138
+ } else {
139
+ const seedContent = await readFile(TEMPLATE_DIR + '/seed.spec.ts', 'utf-8');
140
+ writeFileSync(seedPath, seedContent);
141
+ console.log(chalk.green(' āœ“ Generated: tests/playwright/seed.spec.ts'));
142
+ }
143
+
144
+ // Generate auth.setup.ts
145
+ const authSetupPath = join(testsDir, 'auth.setup.ts');
146
+ if (existsSync(authSetupPath)) {
147
+ console.log(chalk.gray(' - auth.setup.ts already exists, skipping'));
148
+ } else {
149
+ const authContent = await readFile(TEMPLATE_DIR + '/auth.setup.ts', 'utf-8');
150
+ writeFileSync(authSetupPath, authContent);
151
+ console.log(chalk.green(' āœ“ Generated: tests/playwright/auth.setup.ts'));
152
+ }
153
+
154
+ // Generate credentials.yaml
155
+ const credsPath = join(testsDir, 'credentials.yaml');
156
+ if (existsSync(credsPath)) {
157
+ console.log(chalk.gray(' - credentials.yaml already exists, skipping'));
158
+ } else {
159
+ const credsContent = await readFile(TEMPLATE_DIR + '/credentials.yaml', 'utf-8');
160
+ writeFileSync(credsPath, credsContent);
161
+ console.log(chalk.green(' āœ“ Generated: tests/playwright/credentials.yaml'));
162
+ }
163
+
164
+ console.log(chalk.gray(' (Customize BASE_URL and credentials for your app)'));
165
+ }
166
+
167
+ async function installSchema(projectRoot: string) {
168
+ const schemaSrc = SCHEMA_DIR + '/playwright-e2e';
169
+ const schemaDest = join(projectRoot, 'openspec', 'schemas', 'playwright-e2e');
170
+ const schemaFiles = ['schema.yaml'];
171
+
172
+ for (const file of schemaFiles) {
173
+ const src = join(schemaSrc, file);
174
+ const dest = join(schemaDest, file);
175
+ if (existsSync(src)) {
176
+ writeFileSync(dest, readFileSync(src));
177
+ }
178
+ }
179
+
180
+ // Copy templates
181
+ const templatesSrc = join(schemaSrc, 'templates');
182
+ const templatesDest = join(schemaDest, 'templates');
183
+ mkdirSync(templatesDest, { recursive: true });
184
+ const templateFiles = ['test-plan.md', 'report.md', 'e2e-test.ts', 'playwright.config.ts'];
185
+ for (const file of templateFiles) {
186
+ const src = join(templatesSrc, file);
187
+ const dest = join(templatesDest, file);
188
+ if (existsSync(src)) {
189
+ writeFileSync(dest, readFileSync(src));
190
+ }
191
+ }
192
+
193
+ console.log(chalk.green(' āœ“ Schema installed: openspec/schemas/playwright-e2e/'));
194
+ }
195
+
196
+ function execCmd(
197
+ cmd: string,
198
+ name: string,
199
+ silent = false
200
+ ): boolean {
201
+ try {
202
+ execSync(cmd, { stdio: 'pipe' });
203
+ if (!silent) console.log(chalk.green(` āœ“ ${name} found`));
204
+ return true;
205
+ } catch {
206
+ if (!silent) console.log(chalk.yellow(` ⚠ ${name} not found`));
207
+ return false;
208
+ }
209
+ }
@@ -0,0 +1,172 @@
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
+ }
@@ -0,0 +1,130 @@
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 chalk from 'chalk';
10
+
11
+ const SKILL_SRC = new URL('../../.claude/skills/openspec-e2e', import.meta.url).pathname;
12
+ const CMD_SRC = new URL('../../.claude/commands/opsx/e2e.md', import.meta.url).pathname;
13
+ const SCHEMA_DIR = new URL('../../schemas', import.meta.url).pathname;
14
+
15
+ export interface UpdateOptions {
16
+ cli?: boolean;
17
+ skill?: boolean;
18
+ }
19
+
20
+ export async function update(options: UpdateOptions) {
21
+ console.log(chalk.blue('\nšŸ”„ Updating OpenSpec + Playwright E2E\n'));
22
+
23
+ const projectRoot = process.cwd();
24
+
25
+ // 1. Update CLI tool (from git latest, not npm)
26
+ if (options.cli !== false) {
27
+ console.log(chalk.blue('─── Updating CLI ───'));
28
+ try {
29
+ execSync(
30
+ 'npm install -g https://github.com/wxhou/openspec-playwright/archive/refs/heads/main.tar.gz',
31
+ { stdio: 'inherit' }
32
+ );
33
+ console.log(chalk.green(' āœ“ CLI updated to latest commit'));
34
+ } catch {
35
+ console.log(chalk.yellow(' ⚠ Failed to update CLI'));
36
+ console.log(
37
+ chalk.gray(' Run manually: npm install -g https://github.com/wxhou/openspec-playwright/archive/refs/heads/main.tar.gz')
38
+ );
39
+ }
40
+ }
41
+
42
+ // 2. Update skill and command from git tarball (latest commit, not npm package)
43
+ if (options.skill !== false) {
44
+ console.log(chalk.blue('\n─── Updating Skill & Command ───'));
45
+ try {
46
+ // Download tarball to temp file and extract
47
+ const tmpSkill = '/tmp/openspec-e2e-skill.tar.gz';
48
+ const tmpDir = '/tmp/openspec-e2e-update';
49
+
50
+ execSync(
51
+ `curl -sL https://github.com/wxhou/openspec-playwright/archive/refs/heads/main.tar.gz -o ${tmpSkill}`,
52
+ { stdio: 'pipe' }
53
+ );
54
+ // Clean and re-extract to avoid stale files from previous runs
55
+ execSync(`rm -rf ${tmpDir} && mkdir -p ${tmpDir} && tar -xzf ${tmpSkill} -C ${tmpDir} --strip-components=1`, { stdio: 'pipe' });
56
+
57
+ const skillSrc = `${tmpDir}/.claude/skills/openspec-e2e/SKILL.md`;
58
+ const cmdSrc = `${tmpDir}/.claude/commands/opsx/e2e.md`;
59
+ const schemaSrc = `${tmpDir}/schemas/playwright-e2e`;
60
+
61
+ installSkillFrom(skillSrc, cmdSrc, schemaSrc, projectRoot);
62
+ console.log(chalk.green(' āœ“ Skill & command updated to latest'));
63
+ } catch {
64
+ console.log(chalk.yellow(' ⚠ Failed to update skill/command from git'));
65
+ console.log(chalk.gray(' Running from npm package instead...'));
66
+ installSkill(projectRoot);
67
+ }
68
+ }
69
+
70
+ // Summary
71
+ console.log(chalk.blue('\n─── Summary ───'));
72
+ console.log(chalk.green(' āœ“ Update complete!\n'));
73
+
74
+ console.log(chalk.bold('Restart Claude Code to use the updated skill.'));
75
+ console.log(chalk.gray(' Then run /opsx:e2e <change-name> to verify.\n'));
76
+ }
77
+
78
+ function installSkill(projectRoot: string) {
79
+ installSkillFrom(
80
+ SKILL_SRC,
81
+ CMD_SRC,
82
+ SCHEMA_DIR + '/playwright-e2e',
83
+ projectRoot
84
+ );
85
+ }
86
+
87
+ function installSkillFrom(skillSrc: string, cmdSrc: string, schemaSrc: string, projectRoot: string) {
88
+ const skillDir = join(projectRoot, '.claude', 'skills', 'openspec-e2e');
89
+ const cmdDir = join(projectRoot, '.claude', 'commands');
90
+
91
+ mkdirSync(skillDir, { recursive: true });
92
+ const skillContent = readFileSync(skillSrc, 'utf-8');
93
+ writeFileSync(join(skillDir, 'SKILL.md'), skillContent);
94
+ console.log(chalk.green(` āœ“ Skill updated: /openspec-e2e`));
95
+
96
+ mkdirSync(join(cmdDir, 'opsx'), { recursive: true });
97
+ const cmdContent = readFileSync(cmdSrc, 'utf-8');
98
+ writeFileSync(join(cmdDir, 'opsx', 'e2e.md'), cmdContent);
99
+ console.log(chalk.green(` āœ“ Command updated: /opsx:e2e`));
100
+
101
+ installSchemaFrom(schemaSrc, projectRoot);
102
+ }
103
+
104
+ function installSchemaFrom(schemaSrc: string, projectRoot: string) {
105
+ const schemaDest = join(projectRoot, 'openspec', 'schemas', 'playwright-e2e');
106
+
107
+ mkdirSync(schemaDest, { recursive: true });
108
+ // Copy schema.yaml
109
+ const schemaYamlSrc = join(schemaSrc, 'schema.yaml');
110
+ if (existsSync(schemaYamlSrc)) {
111
+ writeFileSync(join(schemaDest, 'schema.yaml'), readFileSync(schemaYamlSrc));
112
+ }
113
+
114
+ // Copy templates
115
+ const templatesSrc = join(schemaSrc, 'templates');
116
+ const templatesDest = join(schemaDest, 'templates');
117
+ if (existsSync(templatesSrc)) {
118
+ mkdirSync(templatesDest, { recursive: true });
119
+ const templateFiles = ['test-plan.md', 'report.md', 'e2e-test.ts', 'playwright.config.ts'];
120
+ for (const file of templateFiles) {
121
+ const src = join(templatesSrc, file);
122
+ const dest = join(templatesDest, file);
123
+ if (existsSync(src)) {
124
+ writeFileSync(dest, readFileSync(src));
125
+ }
126
+ }
127
+ }
128
+
129
+ console.log(chalk.green(' āœ“ Schema updated: openspec/schemas/playwright-e2e/'));
130
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { Command } from 'commander';
5
+ import { init } from './commands/init.js';
6
+ import { update } from './commands/update.js';
7
+ import { doctor } from './commands/doctor.js';
8
+ import { run } from './commands/run.js';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('openspec-pw')
17
+ .description('OpenSpec + Playwright E2E verification setup tool')
18
+ .version(pkg.version);
19
+
20
+ program
21
+ .command('init')
22
+ .description('Initialize OpenSpec + Playwright E2E integration in the current project')
23
+ .option('-c, --change <name>', 'default change name', 'default')
24
+ .option('--no-mcp', 'skip Playwright MCP configuration')
25
+ .option('--no-seed', 'skip seed test generation')
26
+ .action(init);
27
+
28
+ program
29
+ .command('doctor')
30
+ .description('Check if all prerequisites are installed')
31
+ .action(doctor);
32
+
33
+ program
34
+ .command('update')
35
+ .description('Update the CLI tool and skill to the latest version')
36
+ .option('--no-cli', 'skip CLI update')
37
+ .option('--no-skill', 'skip skill/command update')
38
+ .action(update);
39
+
40
+ program
41
+ .command('run <change-name>')
42
+ .description('Run Playwright E2E tests for an OpenSpec change')
43
+ .option('-p, --project <name>', 'Playwright project to run (e.g., user, admin)')
44
+ .option('-t, --timeout <seconds>', 'Test timeout in seconds', '300')
45
+ .action(run);
46
+
47
+ program.parse();
@@ -0,0 +1,77 @@
1
+ // Auth setup for Playwright Test Agents
2
+ // This file authenticates once and saves the session state to .auth/user.json
3
+ // All subsequent tests reuse this state — no repeated logins needed.
4
+ //
5
+ // Setup:
6
+ // 1. Set credentials: export E2E_USERNAME=xxx E2E_PASSWORD=yyy
7
+ // 2. Run: npx playwright test --project=setup
8
+ // (if UI login: browser opens — log in manually once)
9
+ // 3. Credentials are saved — tests auto-use the saved session
10
+ //
11
+ // To switch users, update credentials and re-run step 2.
12
+ //
13
+ // For multi-user testing, add more setup blocks with different storageState paths.
14
+
15
+ import { test as setup, expect } from '@playwright/test';
16
+
17
+ // ─── API Login (preferred) ───────────────────────────────────────────────────
18
+ // If your app has a login API, configure it in tests/playwright/credentials.yaml:
19
+ // api: /api/auth/login
20
+ // Then this block runs automatically.
21
+
22
+ setup('authenticate via API', async ({ page }) => {
23
+ const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
24
+ const username = process.env.E2E_USERNAME;
25
+ const password = process.env.E2E_PASSWORD;
26
+
27
+ if (!username || !password) {
28
+ console.warn('⚠ E2E_USERNAME or E2E_PASSWORD not set — skipping API auth');
29
+ return;
30
+ }
31
+
32
+ const res = await page.request.post(`${baseUrl}/api/auth/login`, {
33
+ data: { username, password },
34
+ });
35
+
36
+ if (!res.ok()) {
37
+ throw new Error(`API login failed (${res.status()}). Check credentials.`);
38
+ }
39
+
40
+ await page.context().storageState({ path: './playwright/.auth/user.json' });
41
+ });
42
+
43
+ // ─── UI Login (fallback) ─────────────────────────────────────────────────────
44
+ // If no login API, use UI login. Update selectors to match your login page.
45
+
46
+ setup('authenticate via UI', async ({ page }) => {
47
+ const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
48
+ const username = process.env.E2E_USERNAME;
49
+ const password = process.env.E2E_PASSWORD;
50
+
51
+ if (!username || !password) {
52
+ console.warn('⚠ E2E_USERNAME or E2E_PASSWORD not set — skipping UI auth');
53
+ return;
54
+ }
55
+
56
+ // TODO: Update these selectors to match your login page
57
+ // Preferred: add data-testid attributes to your login form fields
58
+ await page.goto(`${baseUrl}/login`);
59
+
60
+ // Common selector patterns (uncomment the one that matches):
61
+ await page.fill('[data-testid="username"]', username);
62
+ // await page.fill('input[name="email"]', username);
63
+ // await page.fill('input[type="email"]', username);
64
+
65
+ await page.fill('[data-testid="password"]', password);
66
+ // await page.fill('input[name="password"]', password);
67
+
68
+ await page.click('[data-testid="login-button"]');
69
+ // await page.click('button[type="submit"]');
70
+ // await page.click('text=Sign In');
71
+
72
+ // Wait for redirect after login (adjust pattern to your app)
73
+ await page.waitForURL(/\/(dashboard|home|profile)/);
74
+ await expect(page).not.toHaveURL(/.*login.*|.*signin.*/);
75
+
76
+ await page.context().storageState({ path: './playwright/.auth/user.json' });
77
+ });
@@ -0,0 +1,33 @@
1
+ # Playwright E2E Test Credentials
2
+ # Customize these for your application.
3
+ #
4
+ # Setup:
5
+ # 1. Fill in your test user credentials below
6
+ # 2. Export credentials as environment variables:
7
+ # export E2E_USERNAME=your-email@example.com
8
+ # export E2E_PASSWORD=your-password
9
+ # 3. For multi-user tests, add more users with unique names
10
+ # 4. Run: npx playwright test --project=setup
11
+ #
12
+ # API login (preferred): Set `api` to your login endpoint
13
+ # The auth.setup.ts will call POST {api} with username + password
14
+ # Example: api: /api/auth/login
15
+ #
16
+ # UI login (fallback): Leave `api` empty
17
+ # The auth.setup.ts will open the browser and fill the login form
18
+ # Make sure selectors in auth.setup.ts match your login page
19
+
20
+ api: /api/auth/login # Leave empty for UI login
21
+
22
+ users:
23
+ - name: user
24
+ username: test@example.com
25
+ password: testpassword123
26
+
27
+ # Multi-user example (uncomment and configure for role-based tests):
28
+ # - name: admin
29
+ # username: admin@example.com
30
+ # password: adminpassword123
31
+ # - name: premium
32
+ # username: premium@example.com
33
+ # password: premiumpassword123