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.
- package/LICENSE.md +209 -0
- package/README.md +474 -0
- package/bin/endorphin.js +256 -0
- package/examples/endorphin.config.js +22 -0
- package/examples/tests/QE-001-basic-login-test.js +18 -0
- package/examples/tests/sample-test.js +9 -0
- package/examples/tools/.gitkeep +0 -0
- package/framework/config/agent-config.js +53 -0
- package/framework/config/browser-config.js +53 -0
- package/framework/config/paths.js +40 -0
- package/framework/core/agent-setup.js +75 -0
- package/framework/core/browser-framework.js +766 -0
- package/framework/core/config-loader.js +309 -0
- package/framework/core/test-discovery.js +402 -0
- package/framework/core/test-manager.js +343 -0
- package/framework/core/test-recorder.js +302 -0
- package/framework/core/test-runner.js +133 -0
- package/framework/core/test-session.js +98 -0
- package/framework/index.js +44 -0
- package/framework/interactive/enhanced-interactive-recorder.js +223 -0
- package/framework/interactive/index.js +18 -0
- package/framework/interactive/interactive-test-clean.js +33 -0
- package/framework/interactive/interactive-test.js +33 -0
- package/framework/test-framework.js +29 -0
- package/framework/testing/index.js +15 -0
- package/framework/testing/test-interactive-recorder.js +83 -0
- package/framework/testing/test-modular-framework.js +47 -0
- package/framework/testing/verify-test-format.js +58 -0
- package/framework/tools/content.js +67 -0
- package/framework/tools/index.js +52 -0
- package/framework/tools/interaction.js +180 -0
- package/framework/tools/navigation.js +43 -0
- package/framework/tools/utilities.js +99 -0
- package/framework/tools/verification.js +84 -0
- 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
|
+
}
|