@testsmith/testblocks 0.6.0 → 0.8.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/dist/cli/executor.d.ts +4 -1
- package/dist/cli/executor.js +101 -5
- package/dist/cli/index.js +148 -4
- package/dist/cli/reporters/ConsoleReporter.d.ts +12 -0
- package/dist/cli/reporters/ConsoleReporter.js +39 -0
- package/dist/cli/reporters/HTMLReporter.d.ts +19 -0
- package/dist/cli/reporters/HTMLReporter.js +506 -0
- package/dist/cli/reporters/JSONReporter.d.ts +15 -0
- package/dist/cli/reporters/JSONReporter.js +80 -0
- package/dist/cli/reporters/JUnitReporter.d.ts +19 -0
- package/dist/cli/reporters/JUnitReporter.js +105 -0
- package/dist/cli/reporters/index.d.ts +17 -0
- package/dist/cli/reporters/index.js +31 -0
- package/dist/cli/reporters/types.d.ts +28 -0
- package/dist/cli/reporters/types.js +2 -0
- package/dist/cli/reporters/utils.d.ts +31 -0
- package/dist/cli/reporters/utils.js +136 -0
- package/dist/cli/reporters.d.ts +13 -62
- package/dist/cli/reporters.js +16 -719
- package/dist/client/assets/index-Boo8ZrY_.js +2195 -0
- package/dist/client/assets/{index-dXniUrbi.js.map → index-Boo8ZrY_.js.map} +1 -1
- package/dist/client/assets/index-OxNH9dW-.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/core/blocks/api.js +3 -6
- package/dist/core/blocks/assertions.d.ts +31 -0
- package/dist/core/blocks/assertions.js +72 -0
- package/dist/core/blocks/index.d.ts +1 -0
- package/dist/core/blocks/index.js +6 -1
- package/dist/core/blocks/lifecycle.js +5 -3
- package/dist/core/blocks/logic.js +2 -3
- package/dist/core/blocks/playwright/assertions.d.ts +5 -0
- package/dist/core/blocks/playwright/assertions.js +321 -0
- package/dist/core/blocks/playwright/index.d.ts +17 -0
- package/dist/core/blocks/playwright/index.js +49 -0
- package/dist/core/blocks/playwright/interactions.d.ts +5 -0
- package/dist/core/blocks/playwright/interactions.js +191 -0
- package/dist/core/blocks/playwright/navigation.d.ts +5 -0
- package/dist/core/blocks/playwright/navigation.js +133 -0
- package/dist/core/blocks/playwright/retrieval.d.ts +5 -0
- package/dist/core/blocks/playwright/retrieval.js +144 -0
- package/dist/core/blocks/playwright/types.d.ts +65 -0
- package/dist/core/blocks/playwright/types.js +5 -0
- package/dist/core/blocks/playwright/utils.d.ts +26 -0
- package/dist/core/blocks/playwright/utils.js +137 -0
- package/dist/core/blocks/playwright.d.ts +13 -2
- package/dist/core/blocks/playwright.js +14 -761
- package/dist/core/executor/BaseTestExecutor.d.ts +60 -0
- package/dist/core/executor/BaseTestExecutor.js +297 -0
- package/dist/core/executor/index.d.ts +1 -0
- package/dist/core/executor/index.js +5 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/types.d.ts +12 -0
- package/dist/core/utils/blocklyParser.d.ts +18 -0
- package/dist/core/utils/blocklyParser.js +84 -0
- package/dist/core/utils/dataLoader.d.ts +9 -0
- package/dist/core/utils/dataLoader.js +117 -0
- package/dist/core/utils/index.d.ts +2 -0
- package/dist/core/utils/index.js +12 -0
- package/dist/core/utils/logger.d.ts +14 -0
- package/dist/core/utils/logger.js +48 -0
- package/dist/core/utils/variableResolver.d.ts +24 -0
- package/dist/core/utils/variableResolver.js +92 -0
- package/dist/server/executor.d.ts +6 -0
- package/dist/server/executor.js +207 -47
- package/dist/server/globals.d.ts +6 -1
- package/dist/server/globals.js +7 -0
- package/dist/server/startServer.js +15 -0
- package/package.json +1 -1
- package/dist/client/assets/index-dXniUrbi.js +0 -2193
- package/dist/client/assets/index-oTTttNKd.css +0 -1
package/dist/cli/reporters.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reporters - Re-export from modular structure
|
|
4
|
+
*
|
|
5
|
+
* This file re-exports all reporters from the reporters/ directory
|
|
6
|
+
* for backwards compatibility. The reporters are now organized in:
|
|
7
|
+
* - reporters/ConsoleReporter.ts - Terminal output
|
|
8
|
+
* - reporters/JSONReporter.ts - JSON file output
|
|
9
|
+
* - reporters/JUnitReporter.ts - JUnit XML output
|
|
10
|
+
* - reporters/HTMLReporter.ts - Styled HTML report
|
|
11
|
+
* - reporters/types.ts - Shared interfaces
|
|
12
|
+
* - reporters/utils.ts - Utility functions
|
|
13
|
+
*/
|
|
2
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
15
|
if (k2 === undefined) k2 = k;
|
|
4
16
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -10,723 +22,8 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
|
|
|
10
22
|
if (k2 === undefined) k2 = k;
|
|
11
23
|
o[k2] = m[k];
|
|
12
24
|
}));
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
25
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
26
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
|
+
};
|
|
35
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
|
|
37
|
-
exports.getTimestamp = getTimestamp;
|
|
38
|
-
exports.generateHTMLReport = generateHTMLReport;
|
|
39
|
-
exports.generateJUnitXML = generateJUnitXML;
|
|
40
|
-
const fs = __importStar(require("fs"));
|
|
41
|
-
const path = __importStar(require("path"));
|
|
42
|
-
// Generate timestamp string for filenames (e.g., 2024-01-15T14-30-45)
|
|
43
|
-
function getTimestamp() {
|
|
44
|
-
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
45
|
-
}
|
|
46
|
-
class ConsoleReporter {
|
|
47
|
-
onTestFileComplete(file, testFile, results) {
|
|
48
|
-
console.log('');
|
|
49
|
-
for (const result of results) {
|
|
50
|
-
const icon = result.status === 'passed' ? '✓' : '✗';
|
|
51
|
-
const color = result.status === 'passed' ? '\x1b[32m' : '\x1b[31m';
|
|
52
|
-
const reset = '\x1b[0m';
|
|
53
|
-
console.log(`${color} ${icon} ${result.testName}${reset} (${result.duration}ms)`);
|
|
54
|
-
if (result.error) {
|
|
55
|
-
console.log(` ${result.error.message}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
console.log('');
|
|
59
|
-
}
|
|
60
|
-
onComplete(allResults) {
|
|
61
|
-
const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
|
|
62
|
-
const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
|
|
63
|
-
const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
|
|
64
|
-
const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
|
|
65
|
-
console.log('─'.repeat(50));
|
|
66
|
-
console.log(`Tests: ${passed} passed, ${failed} failed, ${totalTests} total`);
|
|
67
|
-
console.log(`Duration: ${(totalDuration / 1000).toFixed(2)}s`);
|
|
68
|
-
console.log(`Test Files: ${allResults.length}`);
|
|
69
|
-
console.log('─'.repeat(50));
|
|
70
|
-
if (failed > 0) {
|
|
71
|
-
console.log('\n\x1b[31mTest run failed\x1b[0m\n');
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
console.log('\n\x1b[32mAll tests passed!\x1b[0m\n');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
exports.ConsoleReporter = ConsoleReporter;
|
|
79
|
-
class JSONReporter {
|
|
80
|
-
constructor(outputDir) {
|
|
81
|
-
this.allResults = [];
|
|
82
|
-
this.outputDir = outputDir;
|
|
83
|
-
}
|
|
84
|
-
onTestFileComplete(file, testFile, results) {
|
|
85
|
-
this.allResults.push({ file, testFile, results });
|
|
86
|
-
const passed = results.filter(r => r.status === 'passed').length;
|
|
87
|
-
const failed = results.filter(r => r.status !== 'passed').length;
|
|
88
|
-
console.log(` ${passed} passed, ${failed} failed\n`);
|
|
89
|
-
}
|
|
90
|
-
onComplete(allResults) {
|
|
91
|
-
// Ensure output directory exists
|
|
92
|
-
if (!fs.existsSync(this.outputDir)) {
|
|
93
|
-
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
94
|
-
}
|
|
95
|
-
const timestamp = getTimestamp();
|
|
96
|
-
const outputPath = path.join(this.outputDir, `results-${timestamp}.json`);
|
|
97
|
-
const report = {
|
|
98
|
-
timestamp: new Date().toISOString(),
|
|
99
|
-
summary: {
|
|
100
|
-
totalFiles: allResults.length,
|
|
101
|
-
totalTests: allResults.reduce((sum, r) => sum + r.results.length, 0),
|
|
102
|
-
passed: allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0),
|
|
103
|
-
failed: allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0),
|
|
104
|
-
duration: allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0),
|
|
105
|
-
},
|
|
106
|
-
testFiles: this.allResults.map(({ file, testFile, results }) => ({
|
|
107
|
-
file,
|
|
108
|
-
name: testFile.name,
|
|
109
|
-
results,
|
|
110
|
-
})),
|
|
111
|
-
};
|
|
112
|
-
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
|
|
113
|
-
console.log(`JSON report saved to: ${outputPath}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
exports.JSONReporter = JSONReporter;
|
|
117
|
-
class JUnitReporter {
|
|
118
|
-
constructor(outputDir) {
|
|
119
|
-
this.allResults = [];
|
|
120
|
-
this.outputDir = outputDir;
|
|
121
|
-
}
|
|
122
|
-
onTestFileComplete(file, testFile, results) {
|
|
123
|
-
this.allResults.push({ file, testFile, results });
|
|
124
|
-
const passed = results.filter(r => r.status === 'passed').length;
|
|
125
|
-
const failed = results.filter(r => r.status !== 'passed').length;
|
|
126
|
-
console.log(` ${passed} passed, ${failed} failed\n`);
|
|
127
|
-
}
|
|
128
|
-
onComplete(allResults) {
|
|
129
|
-
if (!fs.existsSync(this.outputDir)) {
|
|
130
|
-
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
131
|
-
}
|
|
132
|
-
const timestamp = getTimestamp();
|
|
133
|
-
const outputPath = path.join(this.outputDir, `junit-${timestamp}.xml`);
|
|
134
|
-
const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
|
|
135
|
-
const failures = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
|
|
136
|
-
const totalTime = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0) / 1000;
|
|
137
|
-
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
138
|
-
xml += `<testsuites tests="${totalTests}" failures="${failures}" time="${totalTime.toFixed(3)}">\n`;
|
|
139
|
-
for (const { file, testFile, results } of this.allResults) {
|
|
140
|
-
const suiteTests = results.length;
|
|
141
|
-
const suiteFailures = results.filter(r => r.status !== 'passed').length;
|
|
142
|
-
const suiteTime = results.reduce((s, t) => s + t.duration, 0) / 1000;
|
|
143
|
-
xml += ` <testsuite name="${escapeXml(testFile.name)}" tests="${suiteTests}" failures="${suiteFailures}" time="${suiteTime.toFixed(3)}" file="${escapeXml(file)}">\n`;
|
|
144
|
-
for (const result of results) {
|
|
145
|
-
const testTime = result.duration / 1000;
|
|
146
|
-
xml += ` <testcase name="${escapeXml(result.testName)}" classname="${escapeXml(testFile.name)}" time="${testTime.toFixed(3)}">\n`;
|
|
147
|
-
if (result.status !== 'passed' && result.error) {
|
|
148
|
-
xml += ` <failure message="${escapeXml(result.error.message)}">\n`;
|
|
149
|
-
xml += `${escapeXml(result.error.stack || result.error.message)}\n`;
|
|
150
|
-
xml += ` </failure>\n`;
|
|
151
|
-
}
|
|
152
|
-
xml += ` </testcase>\n`;
|
|
153
|
-
}
|
|
154
|
-
xml += ` </testsuite>\n`;
|
|
155
|
-
}
|
|
156
|
-
xml += '</testsuites>\n';
|
|
157
|
-
fs.writeFileSync(outputPath, xml);
|
|
158
|
-
console.log(`JUnit report saved to: ${outputPath}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
exports.JUnitReporter = JUnitReporter;
|
|
162
|
-
function escapeXml(str) {
|
|
163
|
-
return str
|
|
164
|
-
.replace(/&/g, '&')
|
|
165
|
-
.replace(/</g, '<')
|
|
166
|
-
.replace(/>/g, '>')
|
|
167
|
-
.replace(/"/g, '"')
|
|
168
|
-
.replace(/'/g, ''');
|
|
169
|
-
}
|
|
170
|
-
function escapeHtml(str) {
|
|
171
|
-
return str
|
|
172
|
-
.replace(/&/g, '&')
|
|
173
|
-
.replace(/</g, '<')
|
|
174
|
-
.replace(/>/g, '>')
|
|
175
|
-
.replace(/"/g, '"');
|
|
176
|
-
}
|
|
177
|
-
class HTMLReporter {
|
|
178
|
-
constructor(outputDir) {
|
|
179
|
-
this.allResults = [];
|
|
180
|
-
this.outputDir = outputDir;
|
|
181
|
-
}
|
|
182
|
-
onTestFileComplete(file, testFile, results) {
|
|
183
|
-
this.allResults.push({ file, testFile, results });
|
|
184
|
-
const passed = results.filter(r => r.status === 'passed').length;
|
|
185
|
-
const failed = results.filter(r => r.status !== 'passed').length;
|
|
186
|
-
console.log(` ${passed} passed, ${failed} failed\n`);
|
|
187
|
-
}
|
|
188
|
-
onComplete(allResults) {
|
|
189
|
-
if (!fs.existsSync(this.outputDir)) {
|
|
190
|
-
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
191
|
-
}
|
|
192
|
-
const timestamp = getTimestamp();
|
|
193
|
-
const outputPath = path.join(this.outputDir, `report-${timestamp}.html`);
|
|
194
|
-
const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
|
|
195
|
-
const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
|
|
196
|
-
const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
|
|
197
|
-
const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
|
|
198
|
-
const html = generateHTMLReport({
|
|
199
|
-
timestamp: new Date().toISOString(),
|
|
200
|
-
summary: { totalTests, passed, failed, duration: totalDuration },
|
|
201
|
-
testFiles: this.allResults,
|
|
202
|
-
});
|
|
203
|
-
fs.writeFileSync(outputPath, html);
|
|
204
|
-
console.log(`HTML report saved to: ${outputPath}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
exports.HTMLReporter = HTMLReporter;
|
|
208
|
-
// Format step type for display (same as StepResultItem.tsx)
|
|
209
|
-
function formatStepType(type) {
|
|
210
|
-
const displayNames = {
|
|
211
|
-
'logic_log': 'Log',
|
|
212
|
-
'logic_set_variable': 'Set Variable',
|
|
213
|
-
'logic_get_variable': 'Get Variable',
|
|
214
|
-
'api_set_header': 'Set Header',
|
|
215
|
-
'api_set_headers': 'Set Headers',
|
|
216
|
-
'api_clear_headers': 'Clear Headers',
|
|
217
|
-
'api_assert_status': 'Assert Status',
|
|
218
|
-
'api_assert_body_contains': 'Assert Body',
|
|
219
|
-
'api_extract': 'Extract',
|
|
220
|
-
'api_extract_jsonpath': 'Extract (JSONPath)',
|
|
221
|
-
'api_extract_xpath': 'Extract (XPath)',
|
|
222
|
-
'web_navigate': 'Navigate',
|
|
223
|
-
'web_click': 'Click',
|
|
224
|
-
'web_fill': 'Fill',
|
|
225
|
-
'web_type': 'Type',
|
|
226
|
-
'web_press_key': 'Press Key',
|
|
227
|
-
'web_select': 'Select',
|
|
228
|
-
'web_checkbox': 'Checkbox',
|
|
229
|
-
'web_hover': 'Hover',
|
|
230
|
-
'web_wait_for_element': 'Wait for Element',
|
|
231
|
-
'web_wait_for_url': 'Wait for URL',
|
|
232
|
-
'web_wait': 'Wait',
|
|
233
|
-
'web_screenshot': 'Screenshot',
|
|
234
|
-
'web_get_text': 'Get Text',
|
|
235
|
-
'web_get_attribute': 'Get Attribute',
|
|
236
|
-
'web_get_input_value': 'Get Input Value',
|
|
237
|
-
'web_get_title': 'Get Title',
|
|
238
|
-
'web_get_url': 'Get URL',
|
|
239
|
-
'web_assert_visible': 'Assert Visible',
|
|
240
|
-
'web_assert_not_visible': 'Assert Not Visible',
|
|
241
|
-
'web_assert_text_contains': 'Assert Text Contains',
|
|
242
|
-
'web_assert_text_equals': 'Assert Text Equals',
|
|
243
|
-
'web_assert_url_contains': 'Assert URL Contains',
|
|
244
|
-
'web_assert_title_contains': 'Assert Title Contains',
|
|
245
|
-
'web_assert_enabled': 'Assert Enabled',
|
|
246
|
-
'web_assert_checked': 'Assert Checked',
|
|
247
|
-
'totp_generate': 'Generate TOTP',
|
|
248
|
-
'totp_generate_from_numeric': 'Generate TOTP (Numeric)',
|
|
249
|
-
'totp_setup': 'Setup TOTP',
|
|
250
|
-
'totp_code': 'TOTP Code',
|
|
251
|
-
'totp_time_remaining': 'TOTP Time Remaining',
|
|
252
|
-
'totp_wait_for_new_code': 'Wait for New TOTP',
|
|
253
|
-
};
|
|
254
|
-
if (displayNames[type]) {
|
|
255
|
-
return displayNames[type];
|
|
256
|
-
}
|
|
257
|
-
return type
|
|
258
|
-
.split('_')
|
|
259
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
260
|
-
.join(' ');
|
|
261
|
-
}
|
|
262
|
-
// Get step summary from output
|
|
263
|
-
function getStepSummary(stepType, output) {
|
|
264
|
-
if (!output || typeof output !== 'object')
|
|
265
|
-
return null;
|
|
266
|
-
const out = output;
|
|
267
|
-
return out._summary || null;
|
|
268
|
-
}
|
|
269
|
-
// Check if step is an API request
|
|
270
|
-
function isApiRequestStep(stepType) {
|
|
271
|
-
return ['api_get', 'api_post', 'api_put', 'api_patch', 'api_delete'].includes(stepType);
|
|
272
|
-
}
|
|
273
|
-
// Format step output for display
|
|
274
|
-
function formatStepOutput(output, stepType) {
|
|
275
|
-
if (output === undefined || output === null)
|
|
276
|
-
return '';
|
|
277
|
-
if (stepType === 'logic_log' && typeof output === 'object' && output !== null) {
|
|
278
|
-
const logOutput = output;
|
|
279
|
-
if (logOutput._message) {
|
|
280
|
-
return String(logOutput._message);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (typeof output === 'string')
|
|
284
|
-
return output;
|
|
285
|
-
if (typeof output === 'number' || typeof output === 'boolean')
|
|
286
|
-
return String(output);
|
|
287
|
-
if (typeof output === 'object') {
|
|
288
|
-
const filtered = Object.fromEntries(Object.entries(output)
|
|
289
|
-
.filter(([key]) => !key.startsWith('_')));
|
|
290
|
-
if (Object.keys(filtered).length === 0) {
|
|
291
|
-
return '';
|
|
292
|
-
}
|
|
293
|
-
return JSON.stringify(filtered, null, 2);
|
|
294
|
-
}
|
|
295
|
-
return JSON.stringify(output, null, 2);
|
|
296
|
-
}
|
|
297
|
-
function generateHTMLReport(data) {
|
|
298
|
-
const { timestamp, summary, testFiles } = data;
|
|
299
|
-
// Separate lifecycle hooks from actual tests for counting
|
|
300
|
-
let actualTests = 0;
|
|
301
|
-
let actualPassed = 0;
|
|
302
|
-
let actualFailed = 0;
|
|
303
|
-
for (const { results } of testFiles) {
|
|
304
|
-
for (const result of results) {
|
|
305
|
-
if (!result.isLifecycle) {
|
|
306
|
-
actualTests++;
|
|
307
|
-
if (result.status === 'passed')
|
|
308
|
-
actualPassed++;
|
|
309
|
-
else
|
|
310
|
-
actualFailed++;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const passRate = actualTests > 0
|
|
315
|
-
? ((actualPassed / actualTests) * 100).toFixed(1)
|
|
316
|
-
: '0';
|
|
317
|
-
let html = `<!DOCTYPE html>
|
|
318
|
-
<html lang="en">
|
|
319
|
-
<head>
|
|
320
|
-
<meta charset="UTF-8">
|
|
321
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
322
|
-
<title>TestBlocks Report - ${timestamp}</title>
|
|
323
|
-
<style>
|
|
324
|
-
:root {
|
|
325
|
-
--color-passed: #22c55e;
|
|
326
|
-
--color-failed: #ef4444;
|
|
327
|
-
--color-skipped: #f59e0b;
|
|
328
|
-
--color-bg: #f8fafc;
|
|
329
|
-
--color-surface: #ffffff;
|
|
330
|
-
--color-border: #e2e8f0;
|
|
331
|
-
--color-text: #334155;
|
|
332
|
-
--color-text-secondary: #64748b;
|
|
333
|
-
}
|
|
334
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
335
|
-
body {
|
|
336
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
337
|
-
background: var(--color-bg);
|
|
338
|
-
color: var(--color-text);
|
|
339
|
-
line-height: 1.5;
|
|
340
|
-
padding: 24px;
|
|
341
|
-
}
|
|
342
|
-
.container { max-width: 1200px; margin: 0 auto; }
|
|
343
|
-
h1 { font-size: 24px; font-weight: 600; margin-bottom: 8px; }
|
|
344
|
-
.timestamp { color: var(--color-text-secondary); font-size: 14px; margin-bottom: 24px; }
|
|
345
|
-
.summary {
|
|
346
|
-
display: grid;
|
|
347
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
348
|
-
gap: 16px;
|
|
349
|
-
margin-bottom: 32px;
|
|
350
|
-
}
|
|
351
|
-
.summary-card {
|
|
352
|
-
background: var(--color-surface);
|
|
353
|
-
border: 1px solid var(--color-border);
|
|
354
|
-
border-radius: 8px;
|
|
355
|
-
padding: 16px;
|
|
356
|
-
text-align: center;
|
|
357
|
-
}
|
|
358
|
-
.summary-card .value { font-size: 32px; font-weight: 700; }
|
|
359
|
-
.summary-card .label { font-size: 14px; color: var(--color-text-secondary); }
|
|
360
|
-
.summary-card.passed .value { color: var(--color-passed); }
|
|
361
|
-
.summary-card.failed .value { color: var(--color-failed); }
|
|
362
|
-
.test-file {
|
|
363
|
-
background: var(--color-surface);
|
|
364
|
-
border: 1px solid var(--color-border);
|
|
365
|
-
border-radius: 8px;
|
|
366
|
-
margin-bottom: 16px;
|
|
367
|
-
overflow: hidden;
|
|
368
|
-
}
|
|
369
|
-
.test-file-header {
|
|
370
|
-
padding: 16px;
|
|
371
|
-
border-bottom: 1px solid var(--color-border);
|
|
372
|
-
font-weight: 600;
|
|
373
|
-
display: flex;
|
|
374
|
-
justify-content: space-between;
|
|
375
|
-
align-items: center;
|
|
376
|
-
}
|
|
377
|
-
.test-file-path { font-size: 12px; color: var(--color-text-secondary); font-weight: normal; }
|
|
378
|
-
.lifecycle-section {
|
|
379
|
-
background: #f1f5f9;
|
|
380
|
-
padding: 8px 16px;
|
|
381
|
-
border-bottom: 1px solid var(--color-border);
|
|
382
|
-
}
|
|
383
|
-
.lifecycle-header {
|
|
384
|
-
font-size: 12px;
|
|
385
|
-
font-weight: 600;
|
|
386
|
-
color: var(--color-text-secondary);
|
|
387
|
-
text-transform: uppercase;
|
|
388
|
-
letter-spacing: 0.5px;
|
|
389
|
-
margin-bottom: 4px;
|
|
390
|
-
}
|
|
391
|
-
.lifecycle-item {
|
|
392
|
-
display: flex;
|
|
393
|
-
align-items: center;
|
|
394
|
-
gap: 8px;
|
|
395
|
-
padding: 4px 0;
|
|
396
|
-
font-size: 13px;
|
|
397
|
-
}
|
|
398
|
-
.test-case {
|
|
399
|
-
padding: 12px 16px;
|
|
400
|
-
border-bottom: 1px solid var(--color-border);
|
|
401
|
-
}
|
|
402
|
-
.test-case:last-child { border-bottom: none; }
|
|
403
|
-
.test-case.passed { border-left: 3px solid var(--color-passed); }
|
|
404
|
-
.test-case.failed { border-left: 3px solid var(--color-failed); }
|
|
405
|
-
.test-case.skipped { border-left: 3px solid var(--color-skipped); }
|
|
406
|
-
.test-case-header {
|
|
407
|
-
display: flex;
|
|
408
|
-
align-items: center;
|
|
409
|
-
gap: 12px;
|
|
410
|
-
}
|
|
411
|
-
.status-icon {
|
|
412
|
-
width: 20px;
|
|
413
|
-
height: 20px;
|
|
414
|
-
border-radius: 50%;
|
|
415
|
-
display: flex;
|
|
416
|
-
align-items: center;
|
|
417
|
-
justify-content: center;
|
|
418
|
-
font-size: 12px;
|
|
419
|
-
color: white;
|
|
420
|
-
flex-shrink: 0;
|
|
421
|
-
}
|
|
422
|
-
.status-icon.passed { background: var(--color-passed); }
|
|
423
|
-
.status-icon.failed { background: var(--color-failed); }
|
|
424
|
-
.status-icon.skipped { background: var(--color-skipped); }
|
|
425
|
-
.test-name { flex: 1; font-weight: 500; }
|
|
426
|
-
.test-duration { color: var(--color-text-secondary); font-size: 14px; }
|
|
427
|
-
.error-message {
|
|
428
|
-
background: #fef2f2;
|
|
429
|
-
border: 1px solid #fecaca;
|
|
430
|
-
border-radius: 4px;
|
|
431
|
-
padding: 8px 12px;
|
|
432
|
-
margin: 8px 0 0 32px;
|
|
433
|
-
font-size: 13px;
|
|
434
|
-
color: #991b1b;
|
|
435
|
-
}
|
|
436
|
-
.steps-toggle {
|
|
437
|
-
background: none;
|
|
438
|
-
border: none;
|
|
439
|
-
color: var(--color-text-secondary);
|
|
440
|
-
cursor: pointer;
|
|
441
|
-
font-size: 12px;
|
|
442
|
-
padding: 4px 8px;
|
|
443
|
-
}
|
|
444
|
-
.steps-toggle:hover { text-decoration: underline; }
|
|
445
|
-
.steps-list {
|
|
446
|
-
display: none;
|
|
447
|
-
margin: 12px 0 0 32px;
|
|
448
|
-
border-left: 2px solid var(--color-border);
|
|
449
|
-
padding-left: 16px;
|
|
450
|
-
}
|
|
451
|
-
.steps-list.open { display: block; }
|
|
452
|
-
.step {
|
|
453
|
-
padding: 8px 0;
|
|
454
|
-
border-bottom: 1px solid #f1f5f9;
|
|
455
|
-
}
|
|
456
|
-
.step:last-child { border-bottom: none; }
|
|
457
|
-
.step-header {
|
|
458
|
-
display: flex;
|
|
459
|
-
align-items: center;
|
|
460
|
-
gap: 8px;
|
|
461
|
-
font-size: 13px;
|
|
462
|
-
}
|
|
463
|
-
.step-dot {
|
|
464
|
-
width: 8px;
|
|
465
|
-
height: 8px;
|
|
466
|
-
border-radius: 50%;
|
|
467
|
-
flex-shrink: 0;
|
|
468
|
-
}
|
|
469
|
-
.step-dot.passed { background: var(--color-passed); }
|
|
470
|
-
.step-dot.failed { background: var(--color-failed); }
|
|
471
|
-
.step-type { font-weight: 500; }
|
|
472
|
-
.step-summary { color: var(--color-text-secondary); }
|
|
473
|
-
.step-duration { color: var(--color-text-secondary); font-size: 12px; margin-left: auto; }
|
|
474
|
-
.step-details {
|
|
475
|
-
margin-top: 8px;
|
|
476
|
-
padding: 8px;
|
|
477
|
-
background: #f8fafc;
|
|
478
|
-
border-radius: 4px;
|
|
479
|
-
font-size: 12px;
|
|
480
|
-
}
|
|
481
|
-
.step-error {
|
|
482
|
-
color: #991b1b;
|
|
483
|
-
margin-top: 4px;
|
|
484
|
-
}
|
|
485
|
-
.response-status {
|
|
486
|
-
display: flex;
|
|
487
|
-
align-items: center;
|
|
488
|
-
gap: 8px;
|
|
489
|
-
margin-bottom: 8px;
|
|
490
|
-
}
|
|
491
|
-
.response-label { font-weight: 500; }
|
|
492
|
-
.status-code { font-family: monospace; padding: 2px 6px; border-radius: 4px; }
|
|
493
|
-
.status-code.success { background: #dcfce7; color: #166534; }
|
|
494
|
-
.status-code.client-error { background: #fef3c7; color: #92400e; }
|
|
495
|
-
.status-code.server-error { background: #fef2f2; color: #991b1b; }
|
|
496
|
-
.response-section {
|
|
497
|
-
margin-top: 8px;
|
|
498
|
-
}
|
|
499
|
-
.response-section summary {
|
|
500
|
-
cursor: pointer;
|
|
501
|
-
font-weight: 500;
|
|
502
|
-
font-size: 12px;
|
|
503
|
-
color: var(--color-text-secondary);
|
|
504
|
-
}
|
|
505
|
-
.response-pre {
|
|
506
|
-
background: #1e293b;
|
|
507
|
-
color: #e2e8f0;
|
|
508
|
-
padding: 12px;
|
|
509
|
-
border-radius: 4px;
|
|
510
|
-
overflow-x: auto;
|
|
511
|
-
font-family: monospace;
|
|
512
|
-
font-size: 12px;
|
|
513
|
-
margin-top: 4px;
|
|
514
|
-
white-space: pre-wrap;
|
|
515
|
-
word-break: break-word;
|
|
516
|
-
}
|
|
517
|
-
.stack-trace {
|
|
518
|
-
background: #fef2f2;
|
|
519
|
-
color: #991b1b;
|
|
520
|
-
}
|
|
521
|
-
.screenshot {
|
|
522
|
-
max-width: 100%;
|
|
523
|
-
max-height: 300px;
|
|
524
|
-
border: 1px solid var(--color-border);
|
|
525
|
-
border-radius: 4px;
|
|
526
|
-
margin-top: 8px;
|
|
527
|
-
cursor: pointer;
|
|
528
|
-
}
|
|
529
|
-
</style>
|
|
530
|
-
</head>
|
|
531
|
-
<body>
|
|
532
|
-
<div class="container">
|
|
533
|
-
<h1>TestBlocks Test Report</h1>
|
|
534
|
-
<div class="timestamp">Generated: ${new Date(timestamp).toLocaleString()}</div>
|
|
535
|
-
|
|
536
|
-
<div class="summary">
|
|
537
|
-
<div class="summary-card">
|
|
538
|
-
<div class="value">${actualTests}</div>
|
|
539
|
-
<div class="label">Total Tests</div>
|
|
540
|
-
</div>
|
|
541
|
-
<div class="summary-card passed">
|
|
542
|
-
<div class="value">${actualPassed}</div>
|
|
543
|
-
<div class="label">Passed</div>
|
|
544
|
-
</div>
|
|
545
|
-
<div class="summary-card failed">
|
|
546
|
-
<div class="value">${actualFailed}</div>
|
|
547
|
-
<div class="label">Failed</div>
|
|
548
|
-
</div>
|
|
549
|
-
<div class="summary-card">
|
|
550
|
-
<div class="value">${passRate}%</div>
|
|
551
|
-
<div class="label">Pass Rate</div>
|
|
552
|
-
</div>
|
|
553
|
-
<div class="summary-card">
|
|
554
|
-
<div class="value">${(summary.duration / 1000).toFixed(1)}s</div>
|
|
555
|
-
<div class="label">Duration</div>
|
|
556
|
-
</div>
|
|
557
|
-
</div>
|
|
558
|
-
`;
|
|
559
|
-
for (const { file, testFile, results } of testFiles) {
|
|
560
|
-
// Separate lifecycle hooks from tests
|
|
561
|
-
const lifecycleResults = results.filter(r => r.isLifecycle);
|
|
562
|
-
const testResults = results.filter(r => !r.isLifecycle);
|
|
563
|
-
const filePassed = testResults.filter(r => r.status === 'passed').length;
|
|
564
|
-
const fileFailed = testResults.filter(r => r.status !== 'passed').length;
|
|
565
|
-
html += `
|
|
566
|
-
<div class="test-file">
|
|
567
|
-
<div class="test-file-header">
|
|
568
|
-
<span>${escapeHtml(testFile.name)}</span>
|
|
569
|
-
<span class="test-file-path">${escapeHtml(file)} • ${filePassed} passed, ${fileFailed} failed</span>
|
|
570
|
-
</div>
|
|
571
|
-
`;
|
|
572
|
-
// Render lifecycle hooks (beforeAll)
|
|
573
|
-
const beforeAllHooks = lifecycleResults.filter(r => r.lifecycleType === 'beforeAll');
|
|
574
|
-
if (beforeAllHooks.length > 0) {
|
|
575
|
-
html += ` <div class="lifecycle-section">\n`;
|
|
576
|
-
html += ` <div class="lifecycle-header">Before All</div>\n`;
|
|
577
|
-
for (const hook of beforeAllHooks) {
|
|
578
|
-
const icon = hook.status === 'passed' ? '✓' : '✗';
|
|
579
|
-
const iconClass = hook.status === 'passed' ? 'passed' : 'failed';
|
|
580
|
-
html += ` <div class="lifecycle-item">
|
|
581
|
-
<span class="step-dot ${iconClass}"></span>
|
|
582
|
-
<span>${hook.steps.length} steps</span>
|
|
583
|
-
<span style="color: var(--color-text-secondary); font-size: 12px;">${hook.duration}ms</span>
|
|
584
|
-
</div>\n`;
|
|
585
|
-
if (hook.error) {
|
|
586
|
-
html += ` <div class="error-message" style="margin: 4px 0 0 16px;">${escapeHtml(hook.error.message)}</div>\n`;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
html += ` </div>\n`;
|
|
590
|
-
}
|
|
591
|
-
// Render actual tests
|
|
592
|
-
for (const result of testResults) {
|
|
593
|
-
const statusIcon = result.status === 'passed' ? '✓' : result.status === 'failed' ? '✗' : '○';
|
|
594
|
-
const testId = `test-${Math.random().toString(36).substr(2, 9)}`;
|
|
595
|
-
html += `
|
|
596
|
-
<div class="test-case ${result.status}">
|
|
597
|
-
<div class="test-case-header">
|
|
598
|
-
<div class="status-icon ${result.status}">${statusIcon}</div>
|
|
599
|
-
<div class="test-name">${escapeHtml(result.testName)}</div>
|
|
600
|
-
<div class="test-duration">${result.duration}ms</div>
|
|
601
|
-
<button class="steps-toggle" onclick="document.getElementById('${testId}').classList.toggle('open'); this.textContent = this.textContent.includes('▶') ? '▼ ${result.steps.length} steps' : '▶ ${result.steps.length} steps'">
|
|
602
|
-
▶ ${result.steps.length} steps
|
|
603
|
-
</button>
|
|
604
|
-
</div>
|
|
605
|
-
`;
|
|
606
|
-
if (result.error) {
|
|
607
|
-
html += ` <div class="error-message">${escapeHtml(result.error.message)}</div>\n`;
|
|
608
|
-
}
|
|
609
|
-
html += ` <div class="steps-list" id="${testId}">\n`;
|
|
610
|
-
for (const step of result.steps) {
|
|
611
|
-
const summary = getStepSummary(step.stepType, step.output);
|
|
612
|
-
const isApiRequest = isApiRequestStep(step.stepType);
|
|
613
|
-
const response = step.output;
|
|
614
|
-
html += ` <div class="step">
|
|
615
|
-
<div class="step-header">
|
|
616
|
-
<span class="step-dot ${step.status}"></span>
|
|
617
|
-
<span class="step-type">${escapeHtml(formatStepType(step.stepType))}</span>
|
|
618
|
-
${summary ? `<span class="step-summary">${escapeHtml(summary)}</span>` : ''}
|
|
619
|
-
<span class="step-duration">${step.duration}ms</span>
|
|
620
|
-
</div>
|
|
621
|
-
`;
|
|
622
|
-
// Show step error
|
|
623
|
-
if (step.error) {
|
|
624
|
-
html += ` <div class="step-error">${escapeHtml(step.error.message)}</div>\n`;
|
|
625
|
-
}
|
|
626
|
-
// Show API response details
|
|
627
|
-
if (isApiRequest && response) {
|
|
628
|
-
html += ` <div class="step-details">\n`;
|
|
629
|
-
if (response.status) {
|
|
630
|
-
const statusClass = response.status >= 200 && response.status < 300 ? 'success'
|
|
631
|
-
: response.status >= 400 && response.status < 500 ? 'client-error'
|
|
632
|
-
: response.status >= 500 ? 'server-error' : '';
|
|
633
|
-
html += ` <div class="response-status">
|
|
634
|
-
<span class="response-label">Status:</span>
|
|
635
|
-
<span class="status-code ${statusClass}">${response.status}</span>
|
|
636
|
-
</div>\n`;
|
|
637
|
-
}
|
|
638
|
-
if (response.headers && Object.keys(response.headers).length > 0) {
|
|
639
|
-
html += ` <details class="response-section">
|
|
640
|
-
<summary>Headers</summary>
|
|
641
|
-
<pre class="response-pre">${escapeHtml(JSON.stringify(response.headers, null, 2))}</pre>
|
|
642
|
-
</details>\n`;
|
|
643
|
-
}
|
|
644
|
-
if (response.body !== undefined) {
|
|
645
|
-
const bodyStr = typeof response.body === 'string'
|
|
646
|
-
? response.body
|
|
647
|
-
: JSON.stringify(response.body, null, 2);
|
|
648
|
-
html += ` <details class="response-section">
|
|
649
|
-
<summary>Body</summary>
|
|
650
|
-
<pre class="response-pre">${escapeHtml(bodyStr)}</pre>
|
|
651
|
-
</details>\n`;
|
|
652
|
-
}
|
|
653
|
-
html += ` </div>\n`;
|
|
654
|
-
}
|
|
655
|
-
else if (step.output !== undefined && step.output !== null && !step.screenshot) {
|
|
656
|
-
// Show generic output for non-API steps
|
|
657
|
-
const outputStr = formatStepOutput(step.output, step.stepType);
|
|
658
|
-
if (outputStr) {
|
|
659
|
-
html += ` <div class="step-details">
|
|
660
|
-
<pre class="response-pre">${escapeHtml(outputStr)}</pre>
|
|
661
|
-
</div>\n`;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// Show stack trace for errors
|
|
665
|
-
if (step.error?.stack) {
|
|
666
|
-
html += ` <details class="response-section">
|
|
667
|
-
<summary>Stack Trace</summary>
|
|
668
|
-
<pre class="response-pre stack-trace">${escapeHtml(step.error.stack)}</pre>
|
|
669
|
-
</details>\n`;
|
|
670
|
-
}
|
|
671
|
-
// Show screenshot
|
|
672
|
-
if (step.screenshot) {
|
|
673
|
-
html += ` <img class="screenshot" src="${step.screenshot}" alt="Screenshot at failure" onclick="window.open(this.src, '_blank')">\n`;
|
|
674
|
-
}
|
|
675
|
-
html += ` </div>\n`;
|
|
676
|
-
}
|
|
677
|
-
html += ` </div>\n`;
|
|
678
|
-
html += ` </div>\n`;
|
|
679
|
-
}
|
|
680
|
-
// Render lifecycle hooks (afterAll)
|
|
681
|
-
const afterAllHooks = lifecycleResults.filter(r => r.lifecycleType === 'afterAll');
|
|
682
|
-
if (afterAllHooks.length > 0) {
|
|
683
|
-
html += ` <div class="lifecycle-section">\n`;
|
|
684
|
-
html += ` <div class="lifecycle-header">After All</div>\n`;
|
|
685
|
-
for (const hook of afterAllHooks) {
|
|
686
|
-
const iconClass = hook.status === 'passed' ? 'passed' : 'failed';
|
|
687
|
-
html += ` <div class="lifecycle-item">
|
|
688
|
-
<span class="step-dot ${iconClass}"></span>
|
|
689
|
-
<span>${hook.steps.length} steps</span>
|
|
690
|
-
<span style="color: var(--color-text-secondary); font-size: 12px;">${hook.duration}ms</span>
|
|
691
|
-
</div>\n`;
|
|
692
|
-
if (hook.error) {
|
|
693
|
-
html += ` <div class="error-message" style="margin: 4px 0 0 16px;">${escapeHtml(hook.error.message)}</div>\n`;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
html += ` </div>\n`;
|
|
697
|
-
}
|
|
698
|
-
html += ` </div>\n`;
|
|
699
|
-
}
|
|
700
|
-
html += `
|
|
701
|
-
</div>
|
|
702
|
-
</body>
|
|
703
|
-
</html>`;
|
|
704
|
-
return html;
|
|
705
|
-
}
|
|
706
|
-
function generateJUnitXML(data) {
|
|
707
|
-
const { testFiles } = data;
|
|
708
|
-
const totalTests = testFiles.reduce((sum, f) => sum + f.results.length, 0);
|
|
709
|
-
const failures = testFiles.reduce((sum, f) => sum + f.results.filter(t => t.status !== 'passed').length, 0);
|
|
710
|
-
const totalTime = testFiles.reduce((sum, f) => sum + f.results.reduce((s, t) => s + t.duration, 0), 0) / 1000;
|
|
711
|
-
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
712
|
-
xml += `<testsuites tests="${totalTests}" failures="${failures}" time="${totalTime.toFixed(3)}">\n`;
|
|
713
|
-
for (const { file, testFile, results } of testFiles) {
|
|
714
|
-
const suiteTests = results.length;
|
|
715
|
-
const suiteFailures = results.filter(r => r.status !== 'passed').length;
|
|
716
|
-
const suiteTime = results.reduce((s, t) => s + t.duration, 0) / 1000;
|
|
717
|
-
xml += ` <testsuite name="${escapeXml(testFile.name)}" tests="${suiteTests}" failures="${suiteFailures}" time="${suiteTime.toFixed(3)}" file="${escapeXml(file)}">\n`;
|
|
718
|
-
for (const result of results) {
|
|
719
|
-
const testTime = result.duration / 1000;
|
|
720
|
-
xml += ` <testcase name="${escapeXml(result.testName)}" classname="${escapeXml(testFile.name)}" time="${testTime.toFixed(3)}">\n`;
|
|
721
|
-
if (result.status !== 'passed' && result.error) {
|
|
722
|
-
xml += ` <failure message="${escapeXml(result.error.message)}">\n`;
|
|
723
|
-
xml += `${escapeXml(result.error.stack || result.error.message)}\n`;
|
|
724
|
-
xml += ` </failure>\n`;
|
|
725
|
-
}
|
|
726
|
-
xml += ` </testcase>\n`;
|
|
727
|
-
}
|
|
728
|
-
xml += ` </testsuite>\n`;
|
|
729
|
-
}
|
|
730
|
-
xml += '</testsuites>\n';
|
|
731
|
-
return xml;
|
|
732
|
-
}
|
|
29
|
+
__exportStar(require("./reporters/index"), exports);
|