@testsmith/testblocks 0.1.0 → 0.3.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 +1 -0
- package/dist/cli/executor.js +32 -1
- package/dist/cli/index.js +25 -7
- package/dist/cli/reporters.js +324 -43
- package/dist/client/assets/index-CIvp_myM.css +1 -0
- package/dist/client/assets/index-DOJk7uTQ.js +2193 -0
- package/dist/client/assets/index-DOJk7uTQ.js.map +1 -0
- package/dist/client/index.html +2 -2
- package/dist/core/types.d.ts +7 -0
- package/dist/server/executor.d.ts +1 -0
- package/dist/server/executor.js +32 -1
- package/dist/server/index.js +74 -7
- package/package.json +1 -1
- package/dist/client/assets/index-Cq84-VIf.js +0 -2137
- package/dist/client/assets/index-Cq84-VIf.js.map +0 -1
- package/dist/client/assets/index-Dnk1ti7l.css +0 -1
package/dist/cli/executor.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare class TestExecutor {
|
|
|
16
16
|
constructor(options?: ExecutorOptions);
|
|
17
17
|
initialize(): Promise<void>;
|
|
18
18
|
cleanup(): Promise<void>;
|
|
19
|
+
private requiresBrowser;
|
|
19
20
|
runTestFile(testFile: TestFile): Promise<TestResult[]>;
|
|
20
21
|
private createBaseContext;
|
|
21
22
|
runTestWithData(test: TestCase, testFile: TestFile, dataSet: TestDataSet, dataIndex: number): Promise<TestResult>;
|
package/dist/cli/executor.js
CHANGED
|
@@ -42,9 +42,40 @@ class TestExecutor {
|
|
|
42
42
|
this.browserContext = null;
|
|
43
43
|
this.browser = null;
|
|
44
44
|
}
|
|
45
|
+
requiresBrowser(testFile) {
|
|
46
|
+
const hasWebStep = (steps) => {
|
|
47
|
+
return steps.some(step => step.type.startsWith('web_'));
|
|
48
|
+
};
|
|
49
|
+
const hasWebStepInState = (state) => {
|
|
50
|
+
const steps = this.extractStepsFromBlocklyState(state);
|
|
51
|
+
return hasWebStep(steps);
|
|
52
|
+
};
|
|
53
|
+
// Check beforeAll/afterAll hooks
|
|
54
|
+
if (testFile.beforeAll && hasWebStepInState(testFile.beforeAll))
|
|
55
|
+
return true;
|
|
56
|
+
if (testFile.afterAll && hasWebStepInState(testFile.afterAll))
|
|
57
|
+
return true;
|
|
58
|
+
if (testFile.beforeEach && hasWebStepInState(testFile.beforeEach))
|
|
59
|
+
return true;
|
|
60
|
+
if (testFile.afterEach && hasWebStepInState(testFile.afterEach))
|
|
61
|
+
return true;
|
|
62
|
+
// Check all tests
|
|
63
|
+
for (const test of testFile.tests) {
|
|
64
|
+
if (hasWebStepInState(test.steps))
|
|
65
|
+
return true;
|
|
66
|
+
if (test.beforeEach && hasWebStepInState(test.beforeEach))
|
|
67
|
+
return true;
|
|
68
|
+
if (test.afterEach && hasWebStepInState(test.afterEach))
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
45
73
|
async runTestFile(testFile) {
|
|
46
74
|
const results = [];
|
|
47
|
-
|
|
75
|
+
// Only initialize browser if test file contains web steps
|
|
76
|
+
if (this.requiresBrowser(testFile)) {
|
|
77
|
+
await this.initialize();
|
|
78
|
+
}
|
|
48
79
|
// Register procedures from the test file
|
|
49
80
|
if (testFile.procedures) {
|
|
50
81
|
for (const [name, procedure] of Object.entries(testFile.procedures)) {
|
package/dist/cli/index.js
CHANGED
|
@@ -56,6 +56,7 @@ program
|
|
|
56
56
|
.option('-o, --output <dir>', 'Output directory for reports', './testblocks-results')
|
|
57
57
|
.option('-b, --base-url <url>', 'Base URL for relative URLs')
|
|
58
58
|
.option('-v, --var <vars...>', 'Variables in key=value format')
|
|
59
|
+
.option('-g, --globals <path>', 'Path to globals.json file', './globals.json')
|
|
59
60
|
.option('--fail-fast', 'Stop on first test failure', false)
|
|
60
61
|
.option('-p, --parallel <count>', 'Number of parallel workers', '1')
|
|
61
62
|
.option('--filter <pattern>', 'Only run tests matching pattern')
|
|
@@ -72,21 +73,38 @@ program
|
|
|
72
73
|
process.exit(1);
|
|
73
74
|
}
|
|
74
75
|
console.log(`Found ${files.length} test file(s)\n`);
|
|
75
|
-
//
|
|
76
|
-
|
|
76
|
+
// Load globals.json if it exists
|
|
77
|
+
let globalVariables = {};
|
|
78
|
+
const globalsPath = path.resolve(options.globals);
|
|
79
|
+
if (fs.existsSync(globalsPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const globalsContent = fs.readFileSync(globalsPath, 'utf-8');
|
|
82
|
+
const globals = JSON.parse(globalsContent);
|
|
83
|
+
if (globals.variables && typeof globals.variables === 'object') {
|
|
84
|
+
globalVariables = globals.variables;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.warn(`Warning: Could not load globals from ${globalsPath}: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Parse CLI variables (these override globals)
|
|
92
|
+
const cliVariables = {};
|
|
77
93
|
if (options.var) {
|
|
78
94
|
for (const v of options.var) {
|
|
79
95
|
const [key, ...valueParts] = v.split('=');
|
|
80
96
|
const value = valueParts.join('=');
|
|
81
97
|
// Try to parse as JSON, otherwise use as string
|
|
82
98
|
try {
|
|
83
|
-
|
|
99
|
+
cliVariables[key] = JSON.parse(value);
|
|
84
100
|
}
|
|
85
101
|
catch {
|
|
86
|
-
|
|
102
|
+
cliVariables[key] = value;
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
106
|
+
// Merge variables: globals first, then CLI overrides
|
|
107
|
+
const variables = { ...globalVariables, ...cliVariables };
|
|
90
108
|
// Create executor options
|
|
91
109
|
const executorOptions = {
|
|
92
110
|
headless: !options.headed,
|
|
@@ -230,7 +248,7 @@ program
|
|
|
230
248
|
'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
|
|
231
249
|
},
|
|
232
250
|
devDependencies: {
|
|
233
|
-
'@testsmith/testblocks': '^0.
|
|
251
|
+
'@testsmith/testblocks': '^0.3.0',
|
|
234
252
|
},
|
|
235
253
|
};
|
|
236
254
|
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
|
|
@@ -257,8 +275,8 @@ program
|
|
|
257
275
|
},
|
|
258
276
|
{
|
|
259
277
|
id: 'step-2',
|
|
260
|
-
type: '
|
|
261
|
-
params: {
|
|
278
|
+
type: 'web_assert_title_contains',
|
|
279
|
+
params: { TEXT: 'Example Domain' },
|
|
262
280
|
},
|
|
263
281
|
],
|
|
264
282
|
tags: ['web', 'smoke'],
|
package/dist/cli/reporters.js
CHANGED
|
@@ -205,10 +205,114 @@ class HTMLReporter {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
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
|
+
}
|
|
208
297
|
function generateHTMLReport(data) {
|
|
209
298
|
const { timestamp, summary, testFiles } = data;
|
|
210
|
-
|
|
211
|
-
|
|
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)
|
|
212
316
|
: '0';
|
|
213
317
|
let html = `<!DOCTYPE html>
|
|
214
318
|
<html lang="en">
|
|
@@ -271,17 +375,39 @@ function generateHTMLReport(data) {
|
|
|
271
375
|
align-items: center;
|
|
272
376
|
}
|
|
273
377
|
.test-file-path { font-size: 12px; color: var(--color-text-secondary); font-weight: normal; }
|
|
274
|
-
.
|
|
275
|
-
|
|
378
|
+
.lifecycle-section {
|
|
379
|
+
background: #f1f5f9;
|
|
380
|
+
padding: 8px 16px;
|
|
276
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 {
|
|
277
392
|
display: flex;
|
|
278
393
|
align-items: center;
|
|
279
|
-
gap:
|
|
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);
|
|
280
401
|
}
|
|
281
402
|
.test-case:last-child { border-bottom: none; }
|
|
282
403
|
.test-case.passed { border-left: 3px solid var(--color-passed); }
|
|
283
404
|
.test-case.failed { border-left: 3px solid var(--color-failed); }
|
|
284
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
|
+
}
|
|
285
411
|
.status-icon {
|
|
286
412
|
width: 20px;
|
|
287
413
|
height: 20px;
|
|
@@ -296,18 +422,15 @@ function generateHTMLReport(data) {
|
|
|
296
422
|
.status-icon.passed { background: var(--color-passed); }
|
|
297
423
|
.status-icon.failed { background: var(--color-failed); }
|
|
298
424
|
.status-icon.skipped { background: var(--color-skipped); }
|
|
299
|
-
.test-name { flex: 1; }
|
|
425
|
+
.test-name { flex: 1; font-weight: 500; }
|
|
300
426
|
.test-duration { color: var(--color-text-secondary); font-size: 14px; }
|
|
301
|
-
.error-
|
|
427
|
+
.error-message {
|
|
302
428
|
background: #fef2f2;
|
|
303
429
|
border: 1px solid #fecaca;
|
|
304
430
|
border-radius: 4px;
|
|
305
|
-
padding: 12px;
|
|
306
|
-
margin: 8px
|
|
307
|
-
font-family: monospace;
|
|
431
|
+
padding: 8px 12px;
|
|
432
|
+
margin: 8px 0 0 32px;
|
|
308
433
|
font-size: 13px;
|
|
309
|
-
white-space: pre-wrap;
|
|
310
|
-
word-break: break-word;
|
|
311
434
|
color: #991b1b;
|
|
312
435
|
}
|
|
313
436
|
.steps-toggle {
|
|
@@ -321,27 +444,87 @@ function generateHTMLReport(data) {
|
|
|
321
444
|
.steps-toggle:hover { text-decoration: underline; }
|
|
322
445
|
.steps-list {
|
|
323
446
|
display: none;
|
|
324
|
-
|
|
447
|
+
margin: 12px 0 0 32px;
|
|
448
|
+
border-left: 2px solid var(--color-border);
|
|
449
|
+
padding-left: 16px;
|
|
325
450
|
}
|
|
326
451
|
.steps-list.open { display: block; }
|
|
327
452
|
.step {
|
|
453
|
+
padding: 8px 0;
|
|
454
|
+
border-bottom: 1px solid #f1f5f9;
|
|
455
|
+
}
|
|
456
|
+
.step:last-child { border-bottom: none; }
|
|
457
|
+
.step-header {
|
|
328
458
|
display: flex;
|
|
329
459
|
align-items: center;
|
|
330
460
|
gap: 8px;
|
|
331
|
-
padding: 4px 0;
|
|
332
461
|
font-size: 13px;
|
|
333
462
|
}
|
|
334
|
-
.step-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
}
|
|
339
521
|
.screenshot {
|
|
340
522
|
max-width: 100%;
|
|
341
523
|
max-height: 300px;
|
|
342
524
|
border: 1px solid var(--color-border);
|
|
343
525
|
border-radius: 4px;
|
|
344
526
|
margin-top: 8px;
|
|
527
|
+
cursor: pointer;
|
|
345
528
|
}
|
|
346
529
|
</style>
|
|
347
530
|
</head>
|
|
@@ -352,15 +535,15 @@ function generateHTMLReport(data) {
|
|
|
352
535
|
|
|
353
536
|
<div class="summary">
|
|
354
537
|
<div class="summary-card">
|
|
355
|
-
<div class="value">${
|
|
538
|
+
<div class="value">${actualTests}</div>
|
|
356
539
|
<div class="label">Total Tests</div>
|
|
357
540
|
</div>
|
|
358
541
|
<div class="summary-card passed">
|
|
359
|
-
<div class="value">${
|
|
542
|
+
<div class="value">${actualPassed}</div>
|
|
360
543
|
<div class="label">Passed</div>
|
|
361
544
|
</div>
|
|
362
545
|
<div class="summary-card failed">
|
|
363
|
-
<div class="value">${
|
|
546
|
+
<div class="value">${actualFailed}</div>
|
|
364
547
|
<div class="label">Failed</div>
|
|
365
548
|
</div>
|
|
366
549
|
<div class="summary-card">
|
|
@@ -374,8 +557,11 @@ function generateHTMLReport(data) {
|
|
|
374
557
|
</div>
|
|
375
558
|
`;
|
|
376
559
|
for (const { file, testFile, results } of testFiles) {
|
|
377
|
-
|
|
378
|
-
const
|
|
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;
|
|
379
565
|
html += `
|
|
380
566
|
<div class="test-file">
|
|
381
567
|
<div class="test-file-header">
|
|
@@ -383,36 +569,131 @@ function generateHTMLReport(data) {
|
|
|
383
569
|
<span class="test-file-path">${escapeHtml(file)} • ${filePassed} passed, ${fileFailed} failed</span>
|
|
384
570
|
</div>
|
|
385
571
|
`;
|
|
386
|
-
|
|
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) {
|
|
387
593
|
const statusIcon = result.status === 'passed' ? '✓' : result.status === 'failed' ? '✗' : '○';
|
|
594
|
+
const testId = `test-${Math.random().toString(36).substr(2, 9)}`;
|
|
388
595
|
html += `
|
|
389
596
|
<div class="test-case ${result.status}">
|
|
390
|
-
<div class="
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
${result.steps.length} steps
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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>
|
|
398
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`;
|
|
399
610
|
for (const step of result.steps) {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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>
|
|
407
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
|
|
408
672
|
if (step.screenshot) {
|
|
409
|
-
html += `
|
|
673
|
+
html += ` <img class="screenshot" src="${step.screenshot}" alt="Screenshot at failure" onclick="window.open(this.src, '_blank')">\n`;
|
|
410
674
|
}
|
|
675
|
+
html += ` </div>\n`;
|
|
411
676
|
}
|
|
677
|
+
html += ` </div>\n`;
|
|
412
678
|
html += ` </div>\n`;
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
}
|
|
415
695
|
}
|
|
696
|
+
html += ` </div>\n`;
|
|
416
697
|
}
|
|
417
698
|
html += ` </div>\n`;
|
|
418
699
|
}
|