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.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @fileoverview Screenshot Capture for Browser Automation
3
+ * @module agents/browser/screenshot
4
+ */
5
+
6
+ const path = require('path');
7
+ const fs = require('fs-extra');
8
+
9
+ /**
10
+ * Screenshot Capture - Handles screenshot taking and management
11
+ */
12
+ class ScreenshotCapture {
13
+ /**
14
+ * Create a new ScreenshotCapture instance
15
+ * @param {string} outputDir - Output directory for screenshots
16
+ */
17
+ constructor(outputDir) {
18
+ this.outputDir = outputDir;
19
+ this.screenshots = [];
20
+ this.counter = 0;
21
+ }
22
+
23
+ /**
24
+ * Ensure the output directory exists
25
+ * @returns {Promise<void>}
26
+ */
27
+ async ensureDir() {
28
+ await fs.ensureDir(this.outputDir);
29
+ }
30
+
31
+ /**
32
+ * Capture a screenshot
33
+ * @param {import('playwright').Page} page - Playwright page
34
+ * @param {Object} options - Capture options
35
+ * @param {string} [options.name] - Screenshot name
36
+ * @param {boolean} [options.fullPage=false] - Capture full page
37
+ * @param {string} [options.type='png'] - Image type (png/jpeg)
38
+ * @param {number} [options.quality] - JPEG quality (0-100)
39
+ * @returns {Promise<string>} Screenshot file path
40
+ */
41
+ async capture(page, options = {}) {
42
+ await this.ensureDir();
43
+
44
+ const timestamp = Date.now();
45
+ this.counter++;
46
+
47
+ const name = options.name || `screenshot-${this.counter}`;
48
+ const extension = options.type || 'png';
49
+ const filename = `${timestamp}-${name}.${extension}`;
50
+ const filePath = path.join(this.outputDir, filename);
51
+
52
+ const screenshotOptions = {
53
+ path: filePath,
54
+ fullPage: options.fullPage || false,
55
+ type: extension,
56
+ };
57
+
58
+ if (extension === 'jpeg' && options.quality) {
59
+ screenshotOptions.quality = options.quality;
60
+ }
61
+
62
+ await page.screenshot(screenshotOptions);
63
+
64
+ const metadata = {
65
+ path: filePath,
66
+ filename,
67
+ name,
68
+ timestamp,
69
+ fullPage: options.fullPage || false,
70
+ url: page.url(),
71
+ title: await page.title(),
72
+ };
73
+
74
+ this.screenshots.push(metadata);
75
+
76
+ return filePath;
77
+ }
78
+
79
+ /**
80
+ * Capture an element screenshot
81
+ * @param {import('playwright').Page} page - Playwright page
82
+ * @param {string} selector - Element selector
83
+ * @param {Object} options - Capture options
84
+ * @returns {Promise<string>} Screenshot file path
85
+ */
86
+ async captureElement(page, selector, options = {}) {
87
+ await this.ensureDir();
88
+
89
+ const timestamp = Date.now();
90
+ this.counter++;
91
+
92
+ const name = options.name || `element-${this.counter}`;
93
+ const extension = options.type || 'png';
94
+ const filename = `${timestamp}-${name}.${extension}`;
95
+ const filePath = path.join(this.outputDir, filename);
96
+
97
+ const element = page.locator(selector);
98
+ await element.screenshot({ path: filePath });
99
+
100
+ const metadata = {
101
+ path: filePath,
102
+ filename,
103
+ name,
104
+ timestamp,
105
+ selector,
106
+ url: page.url(),
107
+ };
108
+
109
+ this.screenshots.push(metadata);
110
+
111
+ return filePath;
112
+ }
113
+
114
+ /**
115
+ * Get all captured screenshots
116
+ * @returns {Array<Object>}
117
+ */
118
+ getAll() {
119
+ return [...this.screenshots];
120
+ }
121
+
122
+ /**
123
+ * Get the latest screenshot
124
+ * @returns {Object|null}
125
+ */
126
+ getLatest() {
127
+ if (this.screenshots.length === 0) {
128
+ return null;
129
+ }
130
+ return this.screenshots[this.screenshots.length - 1];
131
+ }
132
+
133
+ /**
134
+ * Get a screenshot by name
135
+ * @param {string} name
136
+ * @returns {Object|undefined}
137
+ */
138
+ getByName(name) {
139
+ return this.screenshots.find(s => s.name === name);
140
+ }
141
+
142
+ /**
143
+ * Clear screenshot history
144
+ */
145
+ clearHistory() {
146
+ this.screenshots = [];
147
+ this.counter = 0;
148
+ }
149
+
150
+ /**
151
+ * Delete all screenshots from disk
152
+ * @returns {Promise<void>}
153
+ */
154
+ async cleanup() {
155
+ for (const screenshot of this.screenshots) {
156
+ try {
157
+ await fs.remove(screenshot.path);
158
+ } catch (e) {
159
+ // Ignore errors
160
+ }
161
+ }
162
+ this.clearHistory();
163
+ }
164
+
165
+ /**
166
+ * Get screenshot count
167
+ * @returns {number}
168
+ */
169
+ get count() {
170
+ return this.screenshots.length;
171
+ }
172
+ }
173
+
174
+ module.exports = ScreenshotCapture;
@@ -0,0 +1,271 @@
1
+ /**
2
+ * @fileoverview Test Code Generator for Browser Automation
3
+ * @module agents/browser/test-generator
4
+ */
5
+
6
+ const path = require('path');
7
+ const fs = require('fs-extra');
8
+
9
+ /**
10
+ * Test Code Generator - Generates Playwright test code from action history
11
+ */
12
+ class TestGenerator {
13
+ constructor() {
14
+ this.templates = {
15
+ playwright: this.generatePlaywrightTest.bind(this),
16
+ jest: this.generateJestTest.bind(this),
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Generate test code from action history
22
+ * @param {Array} history - Action history
23
+ * @param {Object} options - Generation options
24
+ * @param {string} [options.name='Generated Test'] - Test name
25
+ * @param {string} [options.format='playwright'] - Test format
26
+ * @param {string} [options.output] - Output file path
27
+ * @returns {Promise<string>} Generated test code
28
+ */
29
+ async generateTest(history, options = {}) {
30
+ const format = options.format || 'playwright';
31
+ const generator = this.templates[format];
32
+
33
+ if (!generator) {
34
+ throw new Error(`Unknown test format: ${format}`);
35
+ }
36
+
37
+ const code = generator(history, options);
38
+
39
+ if (options.output) {
40
+ await fs.ensureDir(path.dirname(options.output));
41
+ await fs.writeFile(options.output, code, 'utf-8');
42
+ }
43
+
44
+ return code;
45
+ }
46
+
47
+ /**
48
+ * Generate Playwright test
49
+ * @param {Array} history
50
+ * @param {Object} options
51
+ * @returns {string}
52
+ */
53
+ generatePlaywrightTest(history, options = {}) {
54
+ const testName = options.name || 'Generated Test';
55
+ const lines = [
56
+ `import { test, expect } from '@playwright/test';`,
57
+ ``,
58
+ `test('${this.escapeString(testName)}', async ({ page }) => {`,
59
+ ];
60
+
61
+ for (const item of history) {
62
+ const action = item.action;
63
+ const code = this.actionToPlaywrightCode(action);
64
+ if (code) {
65
+ lines.push(` ${code}`);
66
+ }
67
+ }
68
+
69
+ lines.push(`});`);
70
+ lines.push(``);
71
+
72
+ return lines.join('\n');
73
+ }
74
+
75
+ /**
76
+ * Convert action to Playwright code
77
+ * @param {Object} action
78
+ * @returns {string}
79
+ */
80
+ actionToPlaywrightCode(action) {
81
+ switch (action.type) {
82
+ case 'navigate':
83
+ return `await page.goto('${this.escapeString(action.url)}');`;
84
+
85
+ case 'click':
86
+ return `await page.click('${this.escapeSelector(action.selector)}');`;
87
+
88
+ case 'fill':
89
+ return `await page.fill('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
90
+
91
+ case 'select':
92
+ return `await page.selectOption('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
93
+
94
+ case 'wait':
95
+ return `await page.waitForTimeout(${action.delay});`;
96
+
97
+ case 'screenshot':
98
+ const name = action.name || 'screenshot';
99
+ return `await page.screenshot({ path: 'screenshots/${name}.png'${action.fullPage ? ', fullPage: true' : ''} });`;
100
+
101
+ case 'assert':
102
+ if (action.expectedText) {
103
+ return `await expect(page.locator('text="${this.escapeString(action.expectedText)}"')).toBeVisible();`;
104
+ }
105
+ return `await expect(page.locator('${this.escapeSelector(action.selector)}')).toBeVisible();`;
106
+
107
+ default:
108
+ return `// Unknown action: ${action.type}`;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Generate Jest + Puppeteer style test
114
+ * @param {Array} history
115
+ * @param {Object} options
116
+ * @returns {string}
117
+ */
118
+ generateJestTest(history, options = {}) {
119
+ const testName = options.name || 'Generated Test';
120
+ const lines = [
121
+ `const puppeteer = require('puppeteer');`,
122
+ ``,
123
+ `describe('${this.escapeString(testName)}', () => {`,
124
+ ` let browser;`,
125
+ ` let page;`,
126
+ ``,
127
+ ` beforeAll(async () => {`,
128
+ ` browser = await puppeteer.launch();`,
129
+ ` page = await browser.newPage();`,
130
+ ` });`,
131
+ ``,
132
+ ` afterAll(async () => {`,
133
+ ` await browser.close();`,
134
+ ` });`,
135
+ ``,
136
+ ` test('should complete flow', async () => {`,
137
+ ];
138
+
139
+ for (const item of history) {
140
+ const action = item.action;
141
+ const code = this.actionToPuppeteerCode(action);
142
+ if (code) {
143
+ lines.push(` ${code}`);
144
+ }
145
+ }
146
+
147
+ lines.push(` });`);
148
+ lines.push(`});`);
149
+ lines.push(``);
150
+
151
+ return lines.join('\n');
152
+ }
153
+
154
+ /**
155
+ * Convert action to Puppeteer code
156
+ * @param {Object} action
157
+ * @returns {string}
158
+ */
159
+ actionToPuppeteerCode(action) {
160
+ switch (action.type) {
161
+ case 'navigate':
162
+ return `await page.goto('${this.escapeString(action.url)}');`;
163
+
164
+ case 'click':
165
+ return `await page.click('${this.escapeSelector(action.selector)}');`;
166
+
167
+ case 'fill':
168
+ return `await page.type('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
169
+
170
+ case 'select':
171
+ return `await page.select('${this.escapeSelector(action.selector)}', '${this.escapeString(action.value)}');`;
172
+
173
+ case 'wait':
174
+ return `await new Promise(r => setTimeout(r, ${action.delay}));`;
175
+
176
+ case 'screenshot':
177
+ const name = action.name || 'screenshot';
178
+ return `await page.screenshot({ path: 'screenshots/${name}.png'${action.fullPage ? ', fullPage: true' : ''} });`;
179
+
180
+ case 'assert':
181
+ return `await page.waitForSelector('${this.escapeSelector(action.selector)}');`;
182
+
183
+ default:
184
+ return `// Unknown action: ${action.type}`;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Generate test from MUSUBI specification
190
+ * @param {Object} specification - MUSUBI specification
191
+ * @param {Object} options
192
+ * @returns {string}
193
+ */
194
+ generateFromSpec(specification, options = {}) {
195
+ const lines = [
196
+ `import { test, expect } from '@playwright/test';`,
197
+ ``,
198
+ `/**`,
199
+ ` * Tests generated from MUSUBI specification: ${specification.title || 'Unknown'}`,
200
+ ` */`,
201
+ ``,
202
+ ];
203
+
204
+ for (const req of specification.requirements || []) {
205
+ const testCode = this.requirementToTest(req);
206
+ lines.push(testCode);
207
+ lines.push(``);
208
+ }
209
+
210
+ return lines.join('\n');
211
+ }
212
+
213
+ /**
214
+ * Convert a requirement to a test
215
+ * @param {Object} requirement
216
+ * @returns {string}
217
+ */
218
+ requirementToTest(requirement) {
219
+ const testName = `${requirement.id}: ${requirement.title || requirement.action || 'Requirement'}`;
220
+
221
+ const lines = [
222
+ `test('${this.escapeString(testName)}', async ({ page }) => {`,
223
+ ` // Pattern: ${requirement.pattern}`,
224
+ ` // Statement: ${requirement.statement || ''}`,
225
+ ` `,
226
+ ` // TODO: Implement test based on requirement`,
227
+ ];
228
+
229
+ // Add hints based on pattern
230
+ if (requirement.pattern === 'event-driven' && requirement.trigger) {
231
+ lines.push(` // Trigger: ${requirement.trigger}`);
232
+ lines.push(` // Action: ${requirement.action}`);
233
+ } else if (requirement.pattern === 'state-driven' && requirement.condition) {
234
+ lines.push(` // Condition: ${requirement.condition}`);
235
+ lines.push(` // Action: ${requirement.action}`);
236
+ }
237
+
238
+ lines.push(` `);
239
+ lines.push(` throw new Error('Test not implemented');`);
240
+ lines.push(`});`);
241
+
242
+ return lines.join('\n');
243
+ }
244
+
245
+ /**
246
+ * Escape a string for JavaScript
247
+ * @param {string} str
248
+ * @returns {string}
249
+ */
250
+ escapeString(str) {
251
+ if (!str) return '';
252
+ return str
253
+ .replace(/\\/g, '\\\\')
254
+ .replace(/'/g, "\\'")
255
+ .replace(/"/g, '\\"')
256
+ .replace(/\n/g, '\\n')
257
+ .replace(/\r/g, '\\r');
258
+ }
259
+
260
+ /**
261
+ * Escape a CSS selector for JavaScript
262
+ * @param {string} selector
263
+ * @returns {string}
264
+ */
265
+ escapeSelector(selector) {
266
+ if (!selector) return '*';
267
+ return selector.replace(/'/g, "\\'");
268
+ }
269
+ }
270
+
271
+ module.exports = TestGenerator;