@testsmith/testblocks 0.6.0 → 0.7.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 (71) hide show
  1. package/dist/cli/executor.d.ts +4 -1
  2. package/dist/cli/executor.js +101 -5
  3. package/dist/cli/index.js +78 -3
  4. package/dist/cli/reporters/ConsoleReporter.d.ts +12 -0
  5. package/dist/cli/reporters/ConsoleReporter.js +39 -0
  6. package/dist/cli/reporters/HTMLReporter.d.ts +19 -0
  7. package/dist/cli/reporters/HTMLReporter.js +506 -0
  8. package/dist/cli/reporters/JSONReporter.d.ts +15 -0
  9. package/dist/cli/reporters/JSONReporter.js +80 -0
  10. package/dist/cli/reporters/JUnitReporter.d.ts +19 -0
  11. package/dist/cli/reporters/JUnitReporter.js +105 -0
  12. package/dist/cli/reporters/index.d.ts +17 -0
  13. package/dist/cli/reporters/index.js +31 -0
  14. package/dist/cli/reporters/types.d.ts +28 -0
  15. package/dist/cli/reporters/types.js +2 -0
  16. package/dist/cli/reporters/utils.d.ts +31 -0
  17. package/dist/cli/reporters/utils.js +136 -0
  18. package/dist/cli/reporters.d.ts +13 -62
  19. package/dist/cli/reporters.js +16 -719
  20. package/dist/client/assets/index-Boo8ZrY_.js +2195 -0
  21. package/dist/client/assets/{index-dXniUrbi.js.map → index-Boo8ZrY_.js.map} +1 -1
  22. package/dist/client/assets/index-OxNH9dW-.css +1 -0
  23. package/dist/client/index.html +2 -2
  24. package/dist/core/blocks/api.js +3 -6
  25. package/dist/core/blocks/assertions.d.ts +31 -0
  26. package/dist/core/blocks/assertions.js +72 -0
  27. package/dist/core/blocks/index.d.ts +1 -0
  28. package/dist/core/blocks/index.js +6 -1
  29. package/dist/core/blocks/lifecycle.js +5 -3
  30. package/dist/core/blocks/logic.js +2 -3
  31. package/dist/core/blocks/playwright/assertions.d.ts +5 -0
  32. package/dist/core/blocks/playwright/assertions.js +321 -0
  33. package/dist/core/blocks/playwright/index.d.ts +17 -0
  34. package/dist/core/blocks/playwright/index.js +49 -0
  35. package/dist/core/blocks/playwright/interactions.d.ts +5 -0
  36. package/dist/core/blocks/playwright/interactions.js +191 -0
  37. package/dist/core/blocks/playwright/navigation.d.ts +5 -0
  38. package/dist/core/blocks/playwright/navigation.js +133 -0
  39. package/dist/core/blocks/playwright/retrieval.d.ts +5 -0
  40. package/dist/core/blocks/playwright/retrieval.js +144 -0
  41. package/dist/core/blocks/playwright/types.d.ts +65 -0
  42. package/dist/core/blocks/playwright/types.js +5 -0
  43. package/dist/core/blocks/playwright/utils.d.ts +26 -0
  44. package/dist/core/blocks/playwright/utils.js +137 -0
  45. package/dist/core/blocks/playwright.d.ts +13 -2
  46. package/dist/core/blocks/playwright.js +14 -761
  47. package/dist/core/executor/BaseTestExecutor.d.ts +60 -0
  48. package/dist/core/executor/BaseTestExecutor.js +297 -0
  49. package/dist/core/executor/index.d.ts +1 -0
  50. package/dist/core/executor/index.js +5 -0
  51. package/dist/core/index.d.ts +1 -0
  52. package/dist/core/index.js +4 -0
  53. package/dist/core/types.d.ts +12 -0
  54. package/dist/core/utils/blocklyParser.d.ts +18 -0
  55. package/dist/core/utils/blocklyParser.js +84 -0
  56. package/dist/core/utils/dataLoader.d.ts +9 -0
  57. package/dist/core/utils/dataLoader.js +117 -0
  58. package/dist/core/utils/index.d.ts +2 -0
  59. package/dist/core/utils/index.js +12 -0
  60. package/dist/core/utils/logger.d.ts +14 -0
  61. package/dist/core/utils/logger.js +48 -0
  62. package/dist/core/utils/variableResolver.d.ts +24 -0
  63. package/dist/core/utils/variableResolver.js +92 -0
  64. package/dist/server/executor.d.ts +6 -0
  65. package/dist/server/executor.js +207 -47
  66. package/dist/server/globals.d.ts +6 -1
  67. package/dist/server/globals.js +7 -0
  68. package/dist/server/startServer.js +15 -0
  69. package/package.json +1 -1
  70. package/dist/client/assets/index-dXniUrbi.js +0 -2193
  71. package/dist/client/assets/index-oTTttNKd.css +0 -1
@@ -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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
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
- exports.HTMLReporter = exports.JUnitReporter = exports.JSONReporter = exports.ConsoleReporter = void 0;
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, '&amp;')
165
- .replace(/</g, '&lt;')
166
- .replace(/>/g, '&gt;')
167
- .replace(/"/g, '&quot;')
168
- .replace(/'/g, '&apos;');
169
- }
170
- function escapeHtml(str) {
171
- return str
172
- .replace(/&/g, '&amp;')
173
- .replace(/</g, '&lt;')
174
- .replace(/>/g, '&gt;')
175
- .replace(/"/g, '&quot;');
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);