musubi-sdd 2.2.0 ā 3.0.0
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/README.ja.md +1 -1
- package/README.md +1 -1
- package/bin/musubi-browser.js +457 -0
- package/bin/musubi-convert.js +179 -0
- package/bin/musubi-gui.js +270 -0
- package/package.json +13 -3
- package/src/agents/browser/action-executor.js +255 -0
- package/src/agents/browser/ai-comparator.js +255 -0
- package/src/agents/browser/context-manager.js +207 -0
- package/src/agents/browser/index.js +265 -0
- package/src/agents/browser/nl-parser.js +408 -0
- package/src/agents/browser/screenshot.js +174 -0
- package/src/agents/browser/test-generator.js +271 -0
- package/src/converters/index.js +285 -0
- package/src/converters/ir/types.js +508 -0
- package/src/converters/parsers/musubi-parser.js +759 -0
- package/src/converters/parsers/speckit-parser.js +1001 -0
- package/src/converters/writers/musubi-writer.js +808 -0
- package/src/converters/writers/speckit-writer.js +718 -0
- package/src/gui/public/index.html +856 -0
- package/src/gui/server.js +352 -0
- package/src/gui/services/file-watcher.js +119 -0
- package/src/gui/services/index.js +16 -0
- package/src/gui/services/project-scanner.js +547 -0
- package/src/gui/services/traceability-service.js +372 -0
- package/src/gui/services/workflow-service.js +242 -0
- package/src/templates/skills/browser-agent.md +164 -0
- package/src/templates/skills/web-gui.md +188 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview MUSUBI Web GUI CLI
|
|
5
|
+
* @description Command-line interface for the Web GUI Dashboard
|
|
6
|
+
* @module bin/musubi-gui
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { Command } = require('commander');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('musubi-gui')
|
|
17
|
+
.description('MUSUBI Web GUI Dashboard - Visual interface for SDD workflow')
|
|
18
|
+
.version('0.1.0');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start command - Launch the GUI server
|
|
22
|
+
*/
|
|
23
|
+
program
|
|
24
|
+
.command('start')
|
|
25
|
+
.description('Start the Web GUI server')
|
|
26
|
+
.option('-p, --port <port>', 'Server port', '3000')
|
|
27
|
+
.option('-d, --dir <directory>', 'Project directory', process.cwd())
|
|
28
|
+
.option('--no-open', 'Do not open browser automatically')
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
try {
|
|
31
|
+
const Server = require('../src/gui/server');
|
|
32
|
+
const projectPath = path.resolve(options.dir);
|
|
33
|
+
|
|
34
|
+
console.log(chalk.blue('š® MUSUBI Web GUI'));
|
|
35
|
+
console.log(chalk.gray(`Project: ${projectPath}`));
|
|
36
|
+
|
|
37
|
+
const server = new Server(projectPath, {
|
|
38
|
+
port: parseInt(options.port, 10),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await server.start();
|
|
42
|
+
|
|
43
|
+
console.log(chalk.green(`\nā Server running at http://localhost:${options.port}`));
|
|
44
|
+
console.log(chalk.gray('Press Ctrl+C to stop\n'));
|
|
45
|
+
|
|
46
|
+
// Open browser if requested
|
|
47
|
+
if (options.open !== false) {
|
|
48
|
+
const open = await import('open');
|
|
49
|
+
await open.default(`http://localhost:${options.port}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle shutdown
|
|
53
|
+
process.on('SIGINT', async () => {
|
|
54
|
+
console.log(chalk.yellow('\n\nShutting down...'));
|
|
55
|
+
await server.stop();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build command - Build the frontend
|
|
67
|
+
*/
|
|
68
|
+
program
|
|
69
|
+
.command('build')
|
|
70
|
+
.description('Build the frontend for production')
|
|
71
|
+
.option('-o, --outdir <directory>', 'Output directory', 'dist')
|
|
72
|
+
.action(async (options) => {
|
|
73
|
+
try {
|
|
74
|
+
const { execSync } = require('child_process');
|
|
75
|
+
const guiDir = path.join(__dirname, '..', 'src', 'gui', 'client');
|
|
76
|
+
|
|
77
|
+
console.log(chalk.blue('Building frontend...'));
|
|
78
|
+
|
|
79
|
+
// Check if client directory exists
|
|
80
|
+
const fs = require('fs');
|
|
81
|
+
if (!fs.existsSync(guiDir)) {
|
|
82
|
+
console.log(chalk.yellow('Frontend client not found. Creating minimal build...'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
execSync('npm run build', {
|
|
87
|
+
cwd: guiDir,
|
|
88
|
+
stdio: 'inherit',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log(chalk.green(`ā Frontend built to ${options.outdir}`));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(chalk.red(`Build failed: ${error.message}`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Dev command - Start development mode with hot reload
|
|
100
|
+
*/
|
|
101
|
+
program
|
|
102
|
+
.command('dev')
|
|
103
|
+
.description('Start in development mode with hot reload')
|
|
104
|
+
.option('-p, --port <port>', 'Server port', '3000')
|
|
105
|
+
.option('-d, --dir <directory>', 'Project directory', process.cwd())
|
|
106
|
+
.action(async (options) => {
|
|
107
|
+
try {
|
|
108
|
+
const Server = require('../src/gui/server');
|
|
109
|
+
const projectPath = path.resolve(options.dir);
|
|
110
|
+
|
|
111
|
+
console.log(chalk.blue('š® MUSUBI Web GUI (Development Mode)'));
|
|
112
|
+
console.log(chalk.gray(`Project: ${projectPath}`));
|
|
113
|
+
|
|
114
|
+
const server = new Server(projectPath, {
|
|
115
|
+
port: parseInt(options.port, 10),
|
|
116
|
+
dev: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await server.start();
|
|
120
|
+
|
|
121
|
+
console.log(chalk.green(`\nā Server running at http://localhost:${options.port}`));
|
|
122
|
+
console.log(chalk.cyan('Hot reload enabled'));
|
|
123
|
+
console.log(chalk.gray('Press Ctrl+C to stop\n'));
|
|
124
|
+
|
|
125
|
+
// Handle shutdown
|
|
126
|
+
process.on('SIGINT', async () => {
|
|
127
|
+
console.log(chalk.yellow('\n\nShutting down...'));
|
|
128
|
+
await server.stop();
|
|
129
|
+
process.exit(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Status command - Show project status
|
|
140
|
+
*/
|
|
141
|
+
program
|
|
142
|
+
.command('status')
|
|
143
|
+
.description('Show project status summary')
|
|
144
|
+
.option('-d, --dir <directory>', 'Project directory', process.cwd())
|
|
145
|
+
.option('--json', 'Output as JSON')
|
|
146
|
+
.action(async (options) => {
|
|
147
|
+
try {
|
|
148
|
+
const ProjectScanner = require('../src/gui/services/project-scanner');
|
|
149
|
+
const projectPath = path.resolve(options.dir);
|
|
150
|
+
|
|
151
|
+
const scanner = new ProjectScanner(projectPath);
|
|
152
|
+
const project = await scanner.scan();
|
|
153
|
+
|
|
154
|
+
if (options.json) {
|
|
155
|
+
console.log(JSON.stringify(project, null, 2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(chalk.blue('\nš® MUSUBI Project Status\n'));
|
|
160
|
+
console.log(chalk.white(`Project: ${chalk.bold(project.name)}`));
|
|
161
|
+
console.log(chalk.white(`Path: ${project.path}`));
|
|
162
|
+
console.log('');
|
|
163
|
+
|
|
164
|
+
if (project.hasSteering) {
|
|
165
|
+
console.log(chalk.green('ā Steering directory found'));
|
|
166
|
+
if (project.constitution) {
|
|
167
|
+
console.log(chalk.green(` ā Constitution: ${project.constitution.articles.length} articles`));
|
|
168
|
+
}
|
|
169
|
+
if (project.steering) {
|
|
170
|
+
const docs = ['product', 'structure', 'tech'].filter(d => project.steering[d]);
|
|
171
|
+
console.log(chalk.green(` ā Documents: ${docs.join(', ')}`));
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
console.log(chalk.yellow('ā No steering directory (run musubi init)'));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (project.hasSpecs) {
|
|
178
|
+
console.log(chalk.green(`ā Specs: ${project.specs.length} specification files`));
|
|
179
|
+
|
|
180
|
+
let totalReqs = 0;
|
|
181
|
+
let totalTasks = 0;
|
|
182
|
+
let completedTasks = 0;
|
|
183
|
+
|
|
184
|
+
for (const spec of project.specs) {
|
|
185
|
+
totalReqs += spec.requirements.length;
|
|
186
|
+
totalTasks += spec.tasks.length;
|
|
187
|
+
completedTasks += spec.tasks.filter(t => t.completed).length;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(chalk.white(` ⢠Requirements: ${totalReqs}`));
|
|
191
|
+
console.log(chalk.white(` ⢠Tasks: ${completedTasks}/${totalTasks} completed`));
|
|
192
|
+
} else {
|
|
193
|
+
console.log(chalk.gray('ā No specs yet'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log('');
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Matrix command - Show traceability matrix
|
|
205
|
+
*/
|
|
206
|
+
program
|
|
207
|
+
.command('matrix')
|
|
208
|
+
.description('Display traceability matrix')
|
|
209
|
+
.option('-d, --dir <directory>', 'Project directory', process.cwd())
|
|
210
|
+
.option('--json', 'Output as JSON')
|
|
211
|
+
.action(async (options) => {
|
|
212
|
+
try {
|
|
213
|
+
const TraceabilityService = require('../src/gui/services/traceability-service');
|
|
214
|
+
const projectPath = path.resolve(options.dir);
|
|
215
|
+
|
|
216
|
+
const service = new TraceabilityService(projectPath);
|
|
217
|
+
const matrix = await service.buildMatrix();
|
|
218
|
+
|
|
219
|
+
if (options.json) {
|
|
220
|
+
console.log(JSON.stringify(matrix, null, 2));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(chalk.blue('\nš® Traceability Matrix\n'));
|
|
225
|
+
|
|
226
|
+
const coverage = await service.getCoverage();
|
|
227
|
+
console.log(chalk.white('Coverage:'));
|
|
228
|
+
console.log(chalk.white(` Total Requirements: ${coverage.total}`));
|
|
229
|
+
console.log(chalk.white(` Linked: ${coverage.linked} (${coverage.linkedPercent}%)`));
|
|
230
|
+
console.log(chalk.white(` Implemented: ${coverage.implemented} (${coverage.implementedPercent}%)`));
|
|
231
|
+
console.log('');
|
|
232
|
+
|
|
233
|
+
if (matrix.requirements.length === 0) {
|
|
234
|
+
console.log(chalk.gray('No requirements found.'));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(chalk.white('Requirements:'));
|
|
239
|
+
for (const req of matrix.requirements) {
|
|
240
|
+
const statusColor = req.status === 'implemented' ? 'green' :
|
|
241
|
+
req.status === 'tasked' ? 'yellow' :
|
|
242
|
+
req.status === 'designed' ? 'cyan' : 'gray';
|
|
243
|
+
console.log(chalk[statusColor](` ${req.id}: ${req.title} [${req.status}]`));
|
|
244
|
+
|
|
245
|
+
if (req.links.designs.length > 0) {
|
|
246
|
+
console.log(chalk.gray(` ā Designs: ${req.links.designs.map(d => d.id).join(', ')}`));
|
|
247
|
+
}
|
|
248
|
+
if (req.links.tasks.length > 0) {
|
|
249
|
+
console.log(chalk.gray(` ā Tasks: ${req.links.tasks.map(t => t.id).join(', ')}`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log('');
|
|
254
|
+
|
|
255
|
+
const gaps = await service.findGaps();
|
|
256
|
+
if (gaps.length > 0) {
|
|
257
|
+
console.log(chalk.yellow('Gaps:'));
|
|
258
|
+
for (const gap of gaps) {
|
|
259
|
+
console.log(chalk.yellow(` ā ${gap.message}`));
|
|
260
|
+
}
|
|
261
|
+
console.log('');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Ultimate Specification Driven Development Tool with 27 Agents for 7 AI Coding Platforms + MCP Integration (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"musubi-change": "bin/musubi-change.js",
|
|
21
21
|
"musubi-workflow": "bin/musubi-workflow.js",
|
|
22
22
|
"musubi-remember": "bin/musubi-remember.js",
|
|
23
|
-
"musubi-resolve": "bin/musubi-resolve.js"
|
|
23
|
+
"musubi-resolve": "bin/musubi-resolve.js",
|
|
24
|
+
"musubi-convert": "bin/musubi-convert.js",
|
|
25
|
+
"musubi-browser": "bin/musubi-browser.js",
|
|
26
|
+
"musubi-gui": "bin/musubi-gui.js"
|
|
24
27
|
},
|
|
25
28
|
"scripts": {
|
|
26
29
|
"test": "jest",
|
|
@@ -54,11 +57,18 @@
|
|
|
54
57
|
"dependencies": {
|
|
55
58
|
"@octokit/rest": "^22.0.1",
|
|
56
59
|
"chalk": "^4.1.2",
|
|
60
|
+
"chokidar": "^3.5.3",
|
|
57
61
|
"commander": "^11.0.0",
|
|
62
|
+
"cors": "^2.8.5",
|
|
63
|
+
"express": "^4.18.2",
|
|
58
64
|
"fs-extra": "^11.3.2",
|
|
59
65
|
"glob": "^10.5.0",
|
|
66
|
+
"gray-matter": "^4.0.3",
|
|
60
67
|
"inquirer": "^9.0.0",
|
|
61
|
-
"js-yaml": "^4.1.0"
|
|
68
|
+
"js-yaml": "^4.1.0",
|
|
69
|
+
"open": "^10.1.0",
|
|
70
|
+
"playwright": "^1.40.0",
|
|
71
|
+
"ws": "^8.14.2"
|
|
62
72
|
},
|
|
63
73
|
"devDependencies": {
|
|
64
74
|
"eslint": "^8.50.0",
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Action Executor for Browser Automation
|
|
3
|
+
* @module agents/browser/action-executor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} ActionResult
|
|
8
|
+
* @property {boolean} success - Whether the action succeeded
|
|
9
|
+
* @property {string} type - Action type
|
|
10
|
+
* @property {string} [error] - Error message if failed
|
|
11
|
+
* @property {*} [data] - Result data
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} ExecutionContext
|
|
16
|
+
* @property {import('playwright').Page} page - Playwright page instance
|
|
17
|
+
* @property {import('./screenshot')} screenshot - Screenshot capture instance
|
|
18
|
+
* @property {number} timeout - Default timeout
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Action Executor - Executes parsed actions using Playwright
|
|
23
|
+
*/
|
|
24
|
+
class ActionExecutor {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.defaultTimeout = 30000;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Execute a single action
|
|
31
|
+
* @param {import('./nl-parser').Action} action - Action to execute
|
|
32
|
+
* @param {ExecutionContext} context - Execution context
|
|
33
|
+
* @returns {Promise<ActionResult>}
|
|
34
|
+
*/
|
|
35
|
+
async execute(action, context) {
|
|
36
|
+
const { page, timeout = this.defaultTimeout } = context;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
switch (action.type) {
|
|
40
|
+
case 'navigate':
|
|
41
|
+
return await this.executeNavigate(action, page, timeout);
|
|
42
|
+
case 'click':
|
|
43
|
+
return await this.executeClick(action, page, timeout);
|
|
44
|
+
case 'fill':
|
|
45
|
+
return await this.executeFill(action, page, timeout);
|
|
46
|
+
case 'select':
|
|
47
|
+
return await this.executeSelect(action, page, timeout);
|
|
48
|
+
case 'wait':
|
|
49
|
+
return await this.executeWait(action);
|
|
50
|
+
case 'screenshot':
|
|
51
|
+
return await this.executeScreenshot(action, context);
|
|
52
|
+
case 'assert':
|
|
53
|
+
return await this.executeAssert(action, page, timeout);
|
|
54
|
+
default:
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
type: action.type,
|
|
58
|
+
error: `Unknown action type: ${action.type}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
type: action.type,
|
|
65
|
+
error: error.message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute navigate action
|
|
72
|
+
* @param {Object} action
|
|
73
|
+
* @param {import('playwright').Page} page
|
|
74
|
+
* @param {number} timeout
|
|
75
|
+
* @returns {Promise<ActionResult>}
|
|
76
|
+
*/
|
|
77
|
+
async executeNavigate(action, page, timeout) {
|
|
78
|
+
await page.goto(action.url, { timeout, waitUntil: 'domcontentloaded' });
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
type: 'navigate',
|
|
83
|
+
data: { url: action.url, currentUrl: page.url() },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Execute click action
|
|
89
|
+
* @param {Object} action
|
|
90
|
+
* @param {import('playwright').Page} page
|
|
91
|
+
* @param {number} timeout
|
|
92
|
+
* @returns {Promise<ActionResult>}
|
|
93
|
+
*/
|
|
94
|
+
async executeClick(action, page, timeout) {
|
|
95
|
+
const selectors = action.selector.split(',').map(s => s.trim());
|
|
96
|
+
|
|
97
|
+
// Try each selector until one works
|
|
98
|
+
for (const selector of selectors) {
|
|
99
|
+
try {
|
|
100
|
+
await page.click(selector, { timeout });
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
type: 'click',
|
|
104
|
+
data: { selector },
|
|
105
|
+
};
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// Try next selector
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If none worked, try with the original selector and let it fail
|
|
112
|
+
await page.click(action.selector, { timeout });
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
type: 'click',
|
|
117
|
+
data: { selector: action.selector },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Execute fill action
|
|
123
|
+
* @param {Object} action
|
|
124
|
+
* @param {import('playwright').Page} page
|
|
125
|
+
* @param {number} timeout
|
|
126
|
+
* @returns {Promise<ActionResult>}
|
|
127
|
+
*/
|
|
128
|
+
async executeFill(action, page, timeout) {
|
|
129
|
+
const selectors = action.selector.split(',').map(s => s.trim());
|
|
130
|
+
|
|
131
|
+
// Try each selector until one works
|
|
132
|
+
for (const selector of selectors) {
|
|
133
|
+
try {
|
|
134
|
+
await page.fill(selector, action.value, { timeout });
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
type: 'fill',
|
|
138
|
+
data: { selector, value: action.value },
|
|
139
|
+
};
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// Try next selector
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If none worked, try with the original selector
|
|
146
|
+
await page.fill(action.selector, action.value, { timeout });
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
type: 'fill',
|
|
151
|
+
data: { selector: action.selector, value: action.value },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute select action
|
|
157
|
+
* @param {Object} action
|
|
158
|
+
* @param {import('playwright').Page} page
|
|
159
|
+
* @param {number} timeout
|
|
160
|
+
* @returns {Promise<ActionResult>}
|
|
161
|
+
*/
|
|
162
|
+
async executeSelect(action, page, timeout) {
|
|
163
|
+
const selectors = action.selector.split(',').map(s => s.trim());
|
|
164
|
+
|
|
165
|
+
for (const selector of selectors) {
|
|
166
|
+
try {
|
|
167
|
+
await page.selectOption(selector, action.value, { timeout });
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
type: 'select',
|
|
171
|
+
data: { selector, value: action.value },
|
|
172
|
+
};
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Try next selector
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await page.selectOption(action.selector, action.value, { timeout });
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
type: 'select',
|
|
183
|
+
data: { selector: action.selector, value: action.value },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Execute wait action
|
|
189
|
+
* @param {Object} action
|
|
190
|
+
* @returns {Promise<ActionResult>}
|
|
191
|
+
*/
|
|
192
|
+
async executeWait(action) {
|
|
193
|
+
await new Promise(resolve => setTimeout(resolve, action.delay));
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
type: 'wait',
|
|
198
|
+
data: { delay: action.delay },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Execute screenshot action
|
|
204
|
+
* @param {Object} action
|
|
205
|
+
* @param {ExecutionContext} context
|
|
206
|
+
* @returns {Promise<ActionResult>}
|
|
207
|
+
*/
|
|
208
|
+
async executeScreenshot(action, context) {
|
|
209
|
+
const { page, screenshot } = context;
|
|
210
|
+
|
|
211
|
+
const path = await screenshot.capture(page, {
|
|
212
|
+
name: action.name,
|
|
213
|
+
fullPage: action.fullPage,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
success: true,
|
|
218
|
+
type: 'screenshot',
|
|
219
|
+
data: { path, fullPage: action.fullPage },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Execute assert action
|
|
225
|
+
* @param {Object} action
|
|
226
|
+
* @param {import('playwright').Page} page
|
|
227
|
+
* @param {number} timeout
|
|
228
|
+
* @returns {Promise<ActionResult>}
|
|
229
|
+
*/
|
|
230
|
+
async executeAssert(action, page, timeout) {
|
|
231
|
+
const locator = page.locator(action.selector);
|
|
232
|
+
|
|
233
|
+
await locator.waitFor({ state: 'visible', timeout });
|
|
234
|
+
|
|
235
|
+
let text = null;
|
|
236
|
+
if (action.expectedText) {
|
|
237
|
+
text = await locator.textContent();
|
|
238
|
+
if (!text.includes(action.expectedText)) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
type: 'assert',
|
|
242
|
+
error: `Expected "${action.expectedText}" but found "${text}"`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
type: 'assert',
|
|
250
|
+
data: { selector: action.selector, visible: true, text },
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = ActionExecutor;
|