endorphin-ai 0.1.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.
Files changed (35) hide show
  1. package/LICENSE.md +209 -0
  2. package/README.md +474 -0
  3. package/bin/endorphin.js +256 -0
  4. package/examples/endorphin.config.js +22 -0
  5. package/examples/tests/QE-001-basic-login-test.js +18 -0
  6. package/examples/tests/sample-test.js +9 -0
  7. package/examples/tools/.gitkeep +0 -0
  8. package/framework/config/agent-config.js +53 -0
  9. package/framework/config/browser-config.js +53 -0
  10. package/framework/config/paths.js +40 -0
  11. package/framework/core/agent-setup.js +75 -0
  12. package/framework/core/browser-framework.js +766 -0
  13. package/framework/core/config-loader.js +309 -0
  14. package/framework/core/test-discovery.js +402 -0
  15. package/framework/core/test-manager.js +343 -0
  16. package/framework/core/test-recorder.js +302 -0
  17. package/framework/core/test-runner.js +133 -0
  18. package/framework/core/test-session.js +98 -0
  19. package/framework/index.js +44 -0
  20. package/framework/interactive/enhanced-interactive-recorder.js +223 -0
  21. package/framework/interactive/index.js +18 -0
  22. package/framework/interactive/interactive-test-clean.js +33 -0
  23. package/framework/interactive/interactive-test.js +33 -0
  24. package/framework/test-framework.js +29 -0
  25. package/framework/testing/index.js +15 -0
  26. package/framework/testing/test-interactive-recorder.js +83 -0
  27. package/framework/testing/test-modular-framework.js +47 -0
  28. package/framework/testing/verify-test-format.js +58 -0
  29. package/framework/tools/content.js +67 -0
  30. package/framework/tools/index.js +52 -0
  31. package/framework/tools/interaction.js +180 -0
  32. package/framework/tools/navigation.js +43 -0
  33. package/framework/tools/utilities.js +99 -0
  34. package/framework/tools/verification.js +84 -0
  35. package/package.json +84 -0
@@ -0,0 +1,343 @@
1
+ // Endorphin e2e AI test framework>
2
+ // Copyright (C) 2025 Redstudio Agency
3
+
4
+ // This program is free software: you can redistribute it and/or modify
5
+ // it under the terms of the GNU Affero General Public License as
6
+ // published by the Free Software Foundation, either version 3 of the
7
+ // License, or (at your option) any later version.
8
+
9
+ // This program is distributed in the hope that it will be useful,
10
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ // GNU Affero General Public License for more details.
13
+
14
+ // You should have received a copy of the GNU Affero General Public License
15
+ // along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+
18
+
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import * as dotenv from "dotenv";
23
+
24
+ dotenv.config();
25
+
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = path.dirname(__filename);
28
+
29
+ // ๐ŸŽฏ **TEST MANAGER**
30
+ // Manages individual test files in the tests/ folder
31
+
32
+ class TestManager {
33
+ constructor() {
34
+ this.framework = null; // Don't initialize browser framework in test manager
35
+ this.testsDir = path.join(__dirname, '../../tests');
36
+ this.testFiles = [];
37
+ this.loadedTests = new Map();
38
+ }
39
+
40
+ async loadTests() {
41
+ try {
42
+ await this.loadAllTests();
43
+ return this.loadedTests;
44
+ } catch (error) {
45
+ console.error('โŒ Error loading tests:', error);
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ // ๐Ÿ“ **LOAD ALL TEST FILES**
51
+ async loadAllTests() {
52
+ try {
53
+ const files = fs.readdirSync(this.testsDir);
54
+ this.testFiles = files.filter(file => file.endsWith('.js') && file.startsWith('QE-'));
55
+
56
+ // console.log(`๐Ÿ“ Found ${this.testFiles.length} test files in tests/ directory`);
57
+
58
+ for (const file of this.testFiles) {
59
+ await this.loadTestFile(file);
60
+ }
61
+
62
+ // console.log(`โœ… Loaded ${this.loadedTests.size} test cases`);
63
+ } catch (error) {
64
+ console.error('โŒ Error loading test files:', error);
65
+ }
66
+ }
67
+
68
+ // ๐Ÿ“„ **LOAD INDIVIDUAL TEST FILE**
69
+ async loadTestFile(filename) {
70
+ try {
71
+ const filePath = path.join(this.testsDir, filename);
72
+ const testModule = await import(`file://${filePath}`);
73
+
74
+ // Handle both export default and export const formats
75
+ let testCase = testModule.default;
76
+
77
+ // If no default export, look for named exports starting with QE
78
+ if (!testCase) {
79
+ const exports = Object.keys(testModule);
80
+ const qeExport = exports.find(key => key.startsWith('QE'));
81
+ if (qeExport) {
82
+ testCase = testModule[qeExport];
83
+ }
84
+ }
85
+
86
+ if (testCase && testCase.id) {
87
+ this.loadedTests.set(testCase.id, {
88
+ ...testCase,
89
+ filename: filename,
90
+ filePath: filePath
91
+ });
92
+ console.log(`๐Ÿ“ Loaded test: ${testCase.id} - ${testCase.name}`);
93
+ } else {
94
+ console.warn(`โš ๏ธ Invalid test file format: ${filename}`);
95
+ }
96
+ } catch (error) {
97
+ console.error(`โŒ Error loading test file ${filename}:`, error);
98
+ }
99
+ }
100
+
101
+ // ๐Ÿ“‹ **LIST ALL TESTS**
102
+ listAllTests() {
103
+ console.log("\n๐Ÿ“‹ **AVAILABLE TEST CASES**");
104
+ console.log("โ•".repeat(50));
105
+
106
+ const sortedTests = Array.from(this.loadedTests.values()).sort((a, b) => a.id.localeCompare(b.id));
107
+
108
+ sortedTests.forEach(test => {
109
+ console.log(`\n๐Ÿ”น ${test.id}: ${test.name}`);
110
+ console.log(` ๐Ÿ“ ${test.description}`);
111
+ console.log(` ๐ŸŽฏ Priority: ${test.priority}`);
112
+ console.log(` ๐Ÿท๏ธ Tags: ${test.tags.join(', ')}`);
113
+ if (test.prerequisites) {
114
+ console.log(` ๐Ÿ“‹ Prerequisites: ${test.prerequisites.join(', ')}`);
115
+ }
116
+ });
117
+ console.log();
118
+ }
119
+
120
+ // ๐Ÿ” **FIND TESTS BY CRITERIA**
121
+ findTests(criteria = {}) {
122
+ const allTests = Array.from(this.loadedTests.values());
123
+
124
+ return allTests.filter(test => {
125
+ // Filter by tag
126
+ if (criteria.tag && !test.tags.includes(criteria.tag)) {
127
+ return false;
128
+ }
129
+
130
+ // Filter by priority
131
+ if (criteria.priority && test.priority !== criteria.priority) {
132
+ return false;
133
+ }
134
+
135
+ // Filter by ID pattern
136
+ if (criteria.idPattern && !test.id.match(criteria.idPattern)) {
137
+ return false;
138
+ }
139
+
140
+ // Filter by name search
141
+ if (criteria.nameSearch && !test.name.toLowerCase().includes(criteria.nameSearch.toLowerCase())) {
142
+ return false;
143
+ }
144
+
145
+ return true;
146
+ });
147
+ }
148
+
149
+ // ๐Ÿƒ **RUN SINGLE TEST BY ID**
150
+ async runTestById(testId) {
151
+ const test = this.loadedTests.get(testId);
152
+ if (!test) {
153
+ console.log(`โŒ Test ${testId} not found`);
154
+ return null;
155
+ }
156
+
157
+ console.log(`\n๐ŸŽฏ Running Test: ${test.id} - ${test.name}`);
158
+ console.log(`๐Ÿ“ Description: ${test.description}`);
159
+ console.log(`๐ŸŽฏ Priority: ${test.priority}`);
160
+ console.log(`๐Ÿท๏ธ Tags: ${test.tags.join(', ')}`);
161
+
162
+ // Check prerequisites
163
+ if (test.prerequisites) {
164
+ console.log(`๐Ÿ“‹ Prerequisites: ${test.prerequisites.join(', ')}`);
165
+ // Note: In a full implementation, you might want to check if prerequisites passed
166
+ }
167
+
168
+ return await this.framework.runTask(test.task, test.name);
169
+ }
170
+
171
+ // ๐Ÿƒ **RUN MULTIPLE TESTS**
172
+ async runTests(testIds) {
173
+ const results = [];
174
+
175
+ for (const testId of testIds) {
176
+ const result = await this.runTestById(testId);
177
+ if (result) {
178
+ results.push(result);
179
+ }
180
+
181
+ // Add delay between tests
182
+ if (testIds.indexOf(testId) < testIds.length - 1) {
183
+ console.log("โฑ๏ธ Waiting 2 seconds before next test...\n");
184
+ await new Promise(resolve => setTimeout(resolve, 2000));
185
+ }
186
+ }
187
+
188
+ return results;
189
+ }
190
+
191
+ // ๐Ÿท๏ธ **RUN TESTS BY TAG**
192
+ async runTestsByTag(tag) {
193
+ const tests = this.findTests({ tag });
194
+ const testIds = tests.map(test => test.id);
195
+
196
+ console.log(`\n๐Ÿท๏ธ Running ${tests.length} tests with tag: ${tag}`);
197
+ console.log(`Tests: ${testIds.join(', ')}`);
198
+
199
+ return await this.runTests(testIds);
200
+ }
201
+
202
+ // ๐ŸŽฏ **RUN TESTS BY PRIORITY**
203
+ async runTestsByPriority(priority) {
204
+ const tests = this.findTests({ priority });
205
+ const testIds = tests.map(test => test.id);
206
+
207
+ console.log(`\n๐ŸŽฏ Running ${tests.length} tests with priority: ${priority}`);
208
+ console.log(`Tests: ${testIds.join(', ')}`);
209
+
210
+ return await this.runTests(testIds);
211
+ }
212
+
213
+ // ๐Ÿ”ฅ **RUN SMOKE TESTS**
214
+ async runSmokeTests() {
215
+ return await this.runTestsByTag('smoke');
216
+ }
217
+
218
+ // ๐Ÿ” **RUN AUTHENTICATION TESTS**
219
+ async runAuthTests() {
220
+ return await this.runTestsByTag('authentication');
221
+ }
222
+
223
+ // ๐Ÿƒ **RUN ALL TESTS**
224
+ async runAllTests() {
225
+ const allTestIds = Array.from(this.loadedTests.keys()).sort();
226
+
227
+ console.log(`\n๐Ÿƒ Running ALL ${allTestIds.length} tests`);
228
+ console.log(`Tests: ${allTestIds.join(', ')}`);
229
+
230
+ return await this.runTests(allTestIds);
231
+ }
232
+
233
+ // ๐Ÿ“Š **GENERATE TEST SUMMARY**
234
+ getTestSummary() {
235
+ const allTests = Array.from(this.loadedTests.values());
236
+
237
+ const summary = {
238
+ total: allTests.length,
239
+ byPriority: {},
240
+ byTag: {},
241
+ files: this.testFiles.length
242
+ };
243
+
244
+ // Count by priority
245
+ allTests.forEach(test => {
246
+ summary.byPriority[test.priority] = (summary.byPriority[test.priority] || 0) + 1;
247
+ });
248
+
249
+ // Count by tags
250
+ allTests.forEach(test => {
251
+ test.tags.forEach(tag => {
252
+ summary.byTag[tag] = (summary.byTag[tag] || 0) + 1;
253
+ });
254
+ });
255
+
256
+ return summary;
257
+ }
258
+
259
+ // ๐Ÿ“„ **CREATE NEW TEST FILE**
260
+ async createNewTest(testData) {
261
+ // Generate next QE number
262
+ const existingNumbers = Array.from(this.loadedTests.keys())
263
+ .map(id => parseInt(id.split('-')[1]))
264
+ .sort((a, b) => a - b);
265
+
266
+ const nextNumber = existingNumbers.length > 0
267
+ ? Math.max(...existingNumbers) + 1
268
+ : 1;
269
+
270
+ const testId = `QE-${nextNumber.toString().padStart(3, '0')}`;
271
+ const filename = `${testId}-${testData.name.toLowerCase().replace(/\s+/g, '-')}.js`;
272
+ const filePath = path.join(this.testsDir, filename);
273
+
274
+ const testContent = `// ${testId}: ${testData.name}
275
+ // Description: ${testData.description}
276
+ // Priority: ${testData.priority || 'Medium'}
277
+ // Tags: ${testData.tags ? testData.tags.join(', ') : 'general'}
278
+
279
+ export default {
280
+ id: "${testId}",
281
+ name: "${testData.name}",
282
+ description: "${testData.description}",
283
+ priority: "${testData.priority || 'Medium'}",
284
+ tags: ${JSON.stringify(testData.tags || ['general'])},
285
+ site: "${testData.site || 'https://qafromla.herokuapp.com/'}",
286
+ ${testData.testData ? `testData: ${JSON.stringify(testData.testData, null, 2)},` : ''}
287
+ ${testData.prerequisites ? `prerequisites: ${JSON.stringify(testData.prerequisites)},` : ''}
288
+ task: \`${testData.task}\`
289
+ };`;
290
+
291
+ fs.writeFileSync(filePath, testContent);
292
+ console.log(`โœ… Created new test file: ${filename}`);
293
+
294
+ // Reload the test
295
+ await this.loadTestFile(filename);
296
+
297
+ return testId;
298
+ }
299
+
300
+ // ๐Ÿ” **GET METHODS**
301
+ getTestById(testId) {
302
+ return this.loadedTests.get(testId);
303
+ }
304
+
305
+ getTestsByTag(tag) {
306
+ return Array.from(this.loadedTests.values()).filter(test =>
307
+ test.tags && test.tags.includes(tag)
308
+ );
309
+ }
310
+
311
+ getTestsByPriority(priority) {
312
+ return Array.from(this.loadedTests.values()).filter(test =>
313
+ test.priority && test.priority === priority
314
+ );
315
+ }
316
+
317
+ getAllTests() {
318
+ return Array.from(this.loadedTests.values());
319
+ }
320
+
321
+ listTests() {
322
+ console.log('\n๐Ÿ“‹ Available Test Cases:');
323
+ console.log('========================');
324
+ for (const test of this.getAllTests()) {
325
+ console.log(`${test.id}: ${test.name}`);
326
+ console.log(` ๐Ÿ“ ${test.description}`);
327
+ console.log(` ๐ŸŽฏ Priority: ${test.priority}`);
328
+ console.log(` ๐Ÿท๏ธ Tags: ${test.tags.join(', ')}`);
329
+ console.log('');
330
+ }
331
+ }
332
+
333
+ async close() {
334
+ // No browser framework to close in test manager
335
+ }
336
+
337
+ generateReport() {
338
+ // No framework to generate report from in test manager
339
+ return null;
340
+ }
341
+ }
342
+
343
+ export { TestManager };
@@ -0,0 +1,302 @@
1
+ // Endorphin e2e AI test framework>
2
+ // Copyright (C) 2025 Redstudio Agency
3
+
4
+ // This program is free software: you can redistribute it and/or modify
5
+ // it under the terms of the GNU Affero General Public License as
6
+ // published by the Free Software Foundation, either version 3 of the
7
+ // License, or (at your option) any later version.
8
+
9
+ // This program is distributed in the hope that it will be useful,
10
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ // GNU Affero General Public License for more details.
13
+
14
+ // You should have received a copy of the GNU Affero General Public License
15
+ // along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+
18
+ import fs from 'fs/promises';
19
+ import path from 'path';
20
+ import { fileURLToPath } from 'url';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+
25
+ /**
26
+ * Interactive Test Recorder
27
+ * Records user interactions and generates test files
28
+ */
29
+ export class TestRecorder {
30
+ constructor(framework, testData = {}) {
31
+ this.framework = framework;
32
+ this.testData = testData;
33
+ this.steps = [];
34
+ this.stepCounter = 0;
35
+ this.recordingId = null;
36
+ this.recordingPath = null;
37
+ this.screenshotsPath = null;
38
+ this.isRecording = false;
39
+ }
40
+
41
+ /**
42
+ * Start recording session
43
+ */
44
+ async startRecording() {
45
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) + 'Z';
46
+ this.recordingId = `${this.testData.id || 'INTERACTIVE-TEST'}_${timestamp}`;
47
+
48
+ // Create recording directories
49
+ const rootPath = path.resolve(__dirname, '../../');
50
+ this.recordingPath = path.join(rootPath, 'test-recorder', this.recordingId);
51
+ this.screenshotsPath = path.join(this.recordingPath, 'screenshots');
52
+
53
+ await fs.mkdir(this.recordingPath, { recursive: true });
54
+ await fs.mkdir(this.screenshotsPath, { recursive: true });
55
+
56
+ this.isRecording = true;
57
+ this.stepCounter = 0;
58
+ this.steps = [];
59
+
60
+ console.log(`๐ŸŽฌ Recording started: ${this.recordingId}`);
61
+ console.log(`๐Ÿ“ Recording path: ${this.recordingPath}`);
62
+
63
+ return this.recordingId;
64
+ }
65
+
66
+ /**
67
+ * Record a step with tool call and screenshot
68
+ */
69
+ async recordStep(prompt, toolName, toolParams, result) {
70
+ if (!this.isRecording) return;
71
+
72
+ this.stepCounter++;
73
+ const stepId = `step-${this.stepCounter}`;
74
+
75
+ // Take screenshot before and after action
76
+ const screenshotBefore = await this.takeScreenshot(`${stepId}-before`);
77
+
78
+ const step = {
79
+ id: stepId,
80
+ stepNumber: this.stepCounter,
81
+ timestamp: new Date().toISOString(),
82
+ prompt: prompt,
83
+ tool: {
84
+ name: toolName,
85
+ params: toolParams
86
+ },
87
+ result: result,
88
+ screenshots: {
89
+ before: screenshotBefore
90
+ }
91
+ };
92
+
93
+ // Take screenshot after action (with small delay)
94
+ await this.framework.page.waitForTimeout(1000);
95
+ const screenshotAfter = await this.takeScreenshot(`${stepId}-after`);
96
+ step.screenshots.after = screenshotAfter;
97
+
98
+ this.steps.push(step);
99
+
100
+ // Log to console
101
+ console.log(`\n๐Ÿ“ Step ${this.stepCounter}: ${prompt}`);
102
+ console.log(`๐Ÿ”ง Tool: ${toolName}`);
103
+ console.log(`๐Ÿ“ธ Screenshots: ${screenshotBefore}, ${screenshotAfter}`);
104
+
105
+ // Show visual feedback in browser
106
+ await this.showBrowserFeedback(stepId, prompt, toolName);
107
+
108
+ return step;
109
+ }
110
+
111
+ /**
112
+ * Take screenshot and save to recording folder
113
+ */
114
+ async takeScreenshot(filename) {
115
+ try {
116
+ const screenshotPath = path.join(this.screenshotsPath, `${filename}.png`);
117
+ await this.framework.page.screenshot({
118
+ path: screenshotPath,
119
+ fullPage: true
120
+ });
121
+ return `${filename}.png`;
122
+ } catch (error) {
123
+ console.log(`โš ๏ธ Screenshot failed: ${error.message}`);
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Show visual feedback in browser
130
+ */
131
+ async showBrowserFeedback(stepId, prompt, toolName) {
132
+ try {
133
+ await this.framework.page.evaluate((data) => {
134
+ // Remove previous feedback
135
+ const existing = document.querySelector('#test-recorder-feedback');
136
+ if (existing) existing.remove();
137
+
138
+ // Create feedback overlay
139
+ const overlay = document.createElement('div');
140
+ overlay.id = 'test-recorder-feedback';
141
+ overlay.style.cssText = `
142
+ position: fixed;
143
+ top: 10px;
144
+ right: 10px;
145
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
146
+ color: white;
147
+ padding: 15px;
148
+ border-radius: 8px;
149
+ font-family: Arial, sans-serif;
150
+ font-size: 14px;
151
+ z-index: 10000;
152
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
153
+ max-width: 300px;
154
+ animation: slideIn 0.3s ease-out;
155
+ `;
156
+
157
+ overlay.innerHTML = `
158
+ <div style="font-weight: bold; margin-bottom: 8px;">๐ŸŽฌ Recording Step ${data.stepId}</div>
159
+ <div style="margin-bottom: 5px;"><strong>Action:</strong> ${data.prompt}</div>
160
+ <div><strong>Tool:</strong> ${data.toolName}</div>
161
+ `;
162
+
163
+ // Add animation keyframes if not exists
164
+ if (!document.querySelector('#recorder-styles')) {
165
+ const style = document.createElement('style');
166
+ style.id = 'recorder-styles';
167
+ style.textContent = `
168
+ @keyframes slideIn {
169
+ from { transform: translateX(100%); opacity: 0; }
170
+ to { transform: translateX(0); opacity: 1; }
171
+ }
172
+ `;
173
+ document.head.appendChild(style);
174
+ }
175
+
176
+ document.body.appendChild(overlay);
177
+
178
+ // Auto-remove after 3 seconds
179
+ setTimeout(() => {
180
+ if (overlay.parentNode) {
181
+ overlay.style.animation = 'slideIn 0.3s ease-out reverse';
182
+ setTimeout(() => overlay.remove(), 300);
183
+ }
184
+ }, 3000);
185
+
186
+ }, { stepId, prompt, toolName });
187
+ } catch (error) {
188
+ // Browser feedback is optional, don't fail if it doesn't work
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Stop recording and generate test file
194
+ */
195
+ async stopRecording() {
196
+ if (!this.isRecording) return;
197
+
198
+ this.isRecording = false;
199
+
200
+ // Save session data
201
+ const sessionData = {
202
+ recordingId: this.recordingId,
203
+ testData: this.testData,
204
+ steps: this.steps,
205
+ timestamp: new Date().toISOString(),
206
+ totalSteps: this.stepCounter
207
+ };
208
+
209
+ // Save test session
210
+ await fs.writeFile(
211
+ path.join(this.recordingPath, 'test-session.json'),
212
+ JSON.stringify(sessionData, null, 2)
213
+ );
214
+
215
+ // Generate summary
216
+ const summary = {
217
+ id: this.testData.id,
218
+ name: this.testData.name,
219
+ description: this.testData.description,
220
+ site: this.testData.site,
221
+ totalSteps: this.stepCounter,
222
+ duration: new Date().toISOString(),
223
+ recordingPath: this.recordingPath
224
+ };
225
+
226
+ await fs.writeFile(
227
+ path.join(this.recordingPath, 'summary.json'),
228
+ JSON.stringify(summary, null, 2)
229
+ );
230
+
231
+ // Generate test file
232
+ await this.generateTestFile();
233
+
234
+ console.log(`\nโœ… Recording completed: ${this.recordingId}`);
235
+ console.log(`๐Ÿ“ Artifacts saved to: ${this.recordingPath}`);
236
+ console.log(`๐Ÿงช Test file generated in tests/ folder`);
237
+
238
+ return {
239
+ recordingId: this.recordingId,
240
+ recordingPath: this.recordingPath,
241
+ steps: this.stepCounter,
242
+ testData: this.testData
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Generate test file in tests/ folder
248
+ */
249
+ async generateTestFile() {
250
+ const testId = this.testData.id || 'QE-NEW';
251
+ const filename = `${testId.toLowerCase()}-recorded-test.js`;
252
+ const testPath = path.resolve(__dirname, '../../tests', filename);
253
+
254
+ // Build task from recorded steps
255
+ const taskSteps = this.steps.map(step => {
256
+ const toolName = step.tool.name;
257
+ const params = step.tool.params;
258
+
259
+ switch (toolName) {
260
+ case 'navigate':
261
+ return `Navigate to ${params.url}.`;
262
+ case 'click':
263
+ return `Click on "${params.selector || 'element'}".`;
264
+ case 'fill':
265
+ return `Fill "${params.selector || 'field'}" with "${params.value}".`;
266
+ case 'clearField':
267
+ return `Clear field "${params.selector}".`;
268
+ case 'wait':
269
+ return `Wait ${params.time || 1000}ms.`;
270
+ case 'screenshot':
271
+ return `Take screenshot.`;
272
+ default:
273
+ return step.prompt;
274
+ }
275
+ }).join(' ');
276
+
277
+ const testContent = `// ${testId}: ${this.testData.name}
278
+ // Description: ${this.testData.description}
279
+ // Priority: ${this.testData.priority || 'Medium'}
280
+ // Tags: ${(this.testData.tags || []).join(', ')}
281
+ // Generated by Test Recorder: ${this.recordingId}
282
+
283
+ export const ${testId.replace(/-/g, '')} = {
284
+ "id": "${testId}",
285
+ "name": "${this.testData.name}",
286
+ "description": "${this.testData.description}",
287
+ "priority": "${this.testData.priority || 'Medium'}",
288
+ "tags": ${JSON.stringify(this.testData.tags || [], null, 4)},
289
+ "site": "${this.testData.site}",
290
+ "testData": ${JSON.stringify(this.testData.testData || {}, null, 4)},
291
+ "task": "${taskSteps} STOP - test completed.",
292
+ "recordingId": "${this.recordingId}",
293
+ "recordedSteps": ${this.stepCounter}
294
+ };
295
+ `;
296
+
297
+ await fs.writeFile(testPath, testContent);
298
+ console.log(`๐Ÿ“ Test file created: ${filename}`);
299
+
300
+ return testPath;
301
+ }
302
+ }