norn-cli 1.3.17 → 1.3.18
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/AGENTS.md +72 -0
- package/CHANGELOG.md +21 -1
- package/README.md +4 -2
- package/dist/cli.js +113 -54
- package/out/assertionRunner.js +537 -0
- package/out/cli/colors.js +129 -0
- package/out/cli/formatters/assertion.js +75 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +187 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +634 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +211 -0
- package/out/cli.js +926 -0
- package/out/codeLensProvider.js +254 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +1886 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +756 -0
- package/out/coveragePanel.js +542 -0
- package/out/diagnosticProvider.js +980 -0
- package/out/environmentProvider.js +373 -0
- package/out/extension.js +1025 -0
- package/out/httpClient.js +269 -0
- package/out/jsonFileReader.js +320 -0
- package/out/nornapiParser.js +326 -0
- package/out/parser.js +725 -0
- package/out/responsePanel.js +4674 -0
- package/out/schemaGenerator.js +393 -0
- package/out/scriptRunner.js +419 -0
- package/out/sequenceRunner.js +3046 -0
- package/out/swaggerParser.js +339 -0
- package/out/test/extension.test.js +48 -0
- package/out/testProvider.js +658 -0
- package/out/validationCache.js +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HTML reporter - generates self-contained HTML reports
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Fully self-contained (embedded CSS/JS, no external dependencies)
|
|
7
|
+
* - Collapsible sections for requests/responses
|
|
8
|
+
* - Color-coded pass/fail status
|
|
9
|
+
* - Request/response details expandable on click
|
|
10
|
+
* - Summary statistics with charts
|
|
11
|
+
* - Works offline
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.generateHtmlReport = generateHtmlReport;
|
|
48
|
+
exports.generateHtmlReportFromResponse = generateHtmlReportFromResponse;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const redaction_1 = require("../redaction");
|
|
51
|
+
/**
|
|
52
|
+
* Escape HTML special characters
|
|
53
|
+
*/
|
|
54
|
+
function escapeHtml(text) {
|
|
55
|
+
return text
|
|
56
|
+
.replace(/&/g, '&')
|
|
57
|
+
.replace(/</g, '<')
|
|
58
|
+
.replace(/>/g, '>')
|
|
59
|
+
.replace(/"/g, '"')
|
|
60
|
+
.replace(/'/g, ''');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Format duration in human-readable form
|
|
64
|
+
*/
|
|
65
|
+
function formatDuration(ms) {
|
|
66
|
+
if (ms < 1000) {
|
|
67
|
+
return `${ms}ms`;
|
|
68
|
+
}
|
|
69
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Format timestamp
|
|
73
|
+
*/
|
|
74
|
+
function formatTimestamp() {
|
|
75
|
+
return new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generate HTML report from sequence results
|
|
79
|
+
*/
|
|
80
|
+
function generateHtmlReport(results, options) {
|
|
81
|
+
const { outputPath, redaction, title = 'Norn Test Report', environment } = options;
|
|
82
|
+
const totalSequences = results.length;
|
|
83
|
+
const passedSequences = results.filter(r => r.success).length;
|
|
84
|
+
const failedSequences = totalSequences - passedSequences;
|
|
85
|
+
const totalAssertions = results.reduce((sum, r) => sum + (r.assertionResults?.length || 0), 0);
|
|
86
|
+
const passedAssertions = results.reduce((sum, r) => sum + (r.assertionResults?.filter(a => a.passed).length || 0), 0);
|
|
87
|
+
const failedAssertions = totalAssertions - passedAssertions;
|
|
88
|
+
const totalRequests = results.reduce((sum, r) => sum + r.steps.filter(s => s.type === 'request').length, 0);
|
|
89
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
90
|
+
// Sort results: failed first, then by name
|
|
91
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
92
|
+
if (a.success !== b.success)
|
|
93
|
+
return a.success ? 1 : -1;
|
|
94
|
+
return a.name.localeCompare(b.name);
|
|
95
|
+
});
|
|
96
|
+
const passRate = totalSequences > 0 ? Math.round((passedSequences / totalSequences) * 100) : 100;
|
|
97
|
+
const html = `<!DOCTYPE html>
|
|
98
|
+
<html lang="en">
|
|
99
|
+
<head>
|
|
100
|
+
<meta charset="UTF-8">
|
|
101
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
102
|
+
<title>${escapeHtml(title)}</title>
|
|
103
|
+
<style>
|
|
104
|
+
${getEmbeddedCSS()}
|
|
105
|
+
</style>
|
|
106
|
+
</head>
|
|
107
|
+
<body>
|
|
108
|
+
<div class="container">
|
|
109
|
+
<header>
|
|
110
|
+
<h1>${escapeHtml(title)}</h1>
|
|
111
|
+
<div class="header-meta">
|
|
112
|
+
<span class="timestamp">Generated: ${formatTimestamp()}</span>
|
|
113
|
+
${environment ? `<span class="environment">Environment: <strong>${escapeHtml(environment)}</strong></span>` : ''}
|
|
114
|
+
</div>
|
|
115
|
+
</header>
|
|
116
|
+
|
|
117
|
+
<section class="summary">
|
|
118
|
+
<div class="summary-header">
|
|
119
|
+
<h2>Summary</h2>
|
|
120
|
+
<div class="pass-rate ${passRate === 100 ? 'perfect' : passRate >= 80 ? 'good' : 'poor'}">
|
|
121
|
+
<span class="rate-value">${passRate}%</span>
|
|
122
|
+
<span class="rate-label">Pass Rate</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="stats-grid">
|
|
126
|
+
<div class="stat-card clickable ${failedSequences > 0 ? 'has-failures' : 'all-passed'}" onclick="filterByStatus('all')" data-filter="all">
|
|
127
|
+
<div class="stat-value">${totalSequences}</div>
|
|
128
|
+
<div class="stat-label">Total Tests</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="stat-card clickable ${passedSequences > 0 ? 'all-passed' : ''}" onclick="filterByStatus('passed')" data-filter="passed">
|
|
131
|
+
<div class="stat-value">${passedSequences}</div>
|
|
132
|
+
<div class="stat-label">Passed</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="stat-card clickable ${failedSequences > 0 ? 'has-failures' : ''}" onclick="filterByStatus('failed')" data-filter="failed">
|
|
135
|
+
<div class="stat-value">${failedSequences}</div>
|
|
136
|
+
<div class="stat-label">Failed</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="stat-card">
|
|
139
|
+
<div class="stat-value">${formatDuration(totalDuration)}</div>
|
|
140
|
+
<div class="stat-label">Total Duration</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="progress-bar">
|
|
145
|
+
<div class="progress-fill ${failedSequences > 0 ? 'has-failures' : 'all-passed'}"
|
|
146
|
+
style="width: ${totalSequences > 0 ? (passedSequences / totalSequences * 100) : 100}%"></div>
|
|
147
|
+
</div>
|
|
148
|
+
</section>
|
|
149
|
+
|
|
150
|
+
<section class="controls">
|
|
151
|
+
<div class="filter-bar">
|
|
152
|
+
<input type="text" id="search-input" placeholder="Search tests..." onkeyup="filterTests()">
|
|
153
|
+
<div class="filter-buttons">
|
|
154
|
+
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All (${totalSequences})</button>
|
|
155
|
+
<button class="filter-btn" data-filter="passed" onclick="setFilter('passed')">Passed (${passedSequences})</button>
|
|
156
|
+
<button class="filter-btn" data-filter="failed" onclick="setFilter('failed')">Failed (${failedSequences})</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="action-buttons">
|
|
160
|
+
<button class="action-btn" onclick="expandAll()">Expand All</button>
|
|
161
|
+
<button class="action-btn" onclick="collapseAll()">Collapse All</button>
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
|
|
165
|
+
<section class="results">
|
|
166
|
+
<h2>Test Results</h2>
|
|
167
|
+
<div id="results-container">
|
|
168
|
+
${sortedResults.map((r, i) => generateSequenceHtml(r, i, redaction)).join('\n')}
|
|
169
|
+
</div>
|
|
170
|
+
<div id="no-results" class="no-results" style="display: none;">
|
|
171
|
+
No tests match your filter criteria.
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<script>
|
|
177
|
+
${getEmbeddedJS()}
|
|
178
|
+
</script>
|
|
179
|
+
</body>
|
|
180
|
+
</html>`;
|
|
181
|
+
fs.writeFileSync(outputPath, html, 'utf-8');
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Generate HTML for a single sequence result
|
|
185
|
+
*/
|
|
186
|
+
function generateSequenceHtml(result, index, redaction) {
|
|
187
|
+
const statusClass = result.success ? 'passed' : 'failed';
|
|
188
|
+
const statusIcon = result.success ? '✓' : '✗';
|
|
189
|
+
const assertionCount = result.assertionResults?.length || 0;
|
|
190
|
+
const passedAssertions = result.assertionResults?.filter(a => a.passed).length || 0;
|
|
191
|
+
const requestCount = result.steps.filter(s => s.type === 'request').length;
|
|
192
|
+
return `
|
|
193
|
+
<div class="sequence ${statusClass}" data-sequence="${index}">
|
|
194
|
+
<div class="sequence-header" onclick="toggleSequence(${index})">
|
|
195
|
+
<span class="status-icon">${statusIcon}</span>
|
|
196
|
+
<span class="sequence-name">${escapeHtml(result.name)}</span>
|
|
197
|
+
<span class="sequence-meta">
|
|
198
|
+
<span class="badge">${requestCount} requests</span>
|
|
199
|
+
${assertionCount > 0 ? `<span class="badge ${passedAssertions === assertionCount ? 'passed' : 'failed'}">${passedAssertions}/${assertionCount} assertions</span>` : ''}
|
|
200
|
+
<span class="badge duration">${formatDuration(result.duration)}</span>
|
|
201
|
+
</span>
|
|
202
|
+
<span class="chevron">▼</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="sequence-body" id="sequence-body-${index}" style="display: none;">
|
|
205
|
+
${generateStepsHtml(result.steps, index, redaction)}
|
|
206
|
+
${result.errors.length > 0 ? generateErrorsHtml(result.errors, redaction) : ''}
|
|
207
|
+
</div>
|
|
208
|
+
</div>`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Generate HTML for sequence steps
|
|
212
|
+
*/
|
|
213
|
+
function generateStepsHtml(steps, sequenceIndex, redaction) {
|
|
214
|
+
let html = '<div class="steps">';
|
|
215
|
+
let requestIndex = 0;
|
|
216
|
+
for (let i = 0; i < steps.length; i++) {
|
|
217
|
+
const step = steps[i];
|
|
218
|
+
switch (step.type) {
|
|
219
|
+
case 'request':
|
|
220
|
+
html += generateRequestHtml(step, sequenceIndex, requestIndex++, redaction);
|
|
221
|
+
break;
|
|
222
|
+
case 'assertion':
|
|
223
|
+
html += generateAssertionHtml(step, redaction);
|
|
224
|
+
break;
|
|
225
|
+
case 'print':
|
|
226
|
+
html += generatePrintHtml(step);
|
|
227
|
+
break;
|
|
228
|
+
case 'script':
|
|
229
|
+
html += generateScriptHtml(step);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
html += '</div>';
|
|
234
|
+
return html;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Generate HTML for a request step
|
|
238
|
+
*/
|
|
239
|
+
function generateRequestHtml(step, seqIndex, reqIndex, redaction) {
|
|
240
|
+
if (!step.response)
|
|
241
|
+
return '';
|
|
242
|
+
const response = step.response;
|
|
243
|
+
const isSuccess = response.status >= 200 && response.status < 300;
|
|
244
|
+
const statusClass = isSuccess ? 'passed' : 'failed';
|
|
245
|
+
const method = step.requestMethod || 'REQUEST';
|
|
246
|
+
const url = step.requestUrl ? (0, redaction_1.redactUrl)(step.requestUrl, redaction) : 'unknown';
|
|
247
|
+
// Show variable name if available (e.g., "user = GET /api/users/1")
|
|
248
|
+
const varLabel = step.variableName ? `<span class="var-label">${escapeHtml(step.variableName)}</span><span class="var-equals">=</span>` : '';
|
|
249
|
+
const bodyHtml = response.body ? generateBodyHtml(response.body, redaction) : '<em>No body</em>';
|
|
250
|
+
const headersHtml = response.headers ? generateHeadersHtml(response.headers, redaction) : '';
|
|
251
|
+
return `
|
|
252
|
+
<div class="step request ${statusClass}">
|
|
253
|
+
<div class="step-header" onclick="toggleStep('step-${seqIndex}-${reqIndex}')">
|
|
254
|
+
${varLabel}
|
|
255
|
+
<span class="method">${escapeHtml(method)}</span>
|
|
256
|
+
<span class="url">${escapeHtml(url)}</span>
|
|
257
|
+
<span class="status-code">${response.status} ${escapeHtml(response.statusText)}</span>
|
|
258
|
+
<span class="duration">${formatDuration(response.duration)}</span>
|
|
259
|
+
<span class="chevron">▶</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="step-body" id="step-${seqIndex}-${reqIndex}" style="display: none;">
|
|
262
|
+
<div class="response-section">
|
|
263
|
+
<h4>Response Headers</h4>
|
|
264
|
+
${headersHtml || '<em>No headers</em>'}
|
|
265
|
+
</div>
|
|
266
|
+
<div class="response-section">
|
|
267
|
+
<h4>Response Body</h4>
|
|
268
|
+
<pre class="body-content">${bodyHtml}</pre>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>`;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Generate HTML for response headers
|
|
275
|
+
*/
|
|
276
|
+
function generateHeadersHtml(headers, redaction) {
|
|
277
|
+
const redactedHeaders = (0, redaction_1.redactHeaders)(headers, redaction);
|
|
278
|
+
return '<table class="headers-table">' +
|
|
279
|
+
Object.entries(redactedHeaders)
|
|
280
|
+
.map(([k, v]) => `<tr><td class="header-name">${escapeHtml(k)}</td><td class="header-value">${escapeHtml(v)}</td></tr>`)
|
|
281
|
+
.join('') +
|
|
282
|
+
'</table>';
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Generate HTML for response body
|
|
286
|
+
*/
|
|
287
|
+
function generateBodyHtml(body, redaction) {
|
|
288
|
+
const redactedBody = (0, redaction_1.redactBody)(body, redaction);
|
|
289
|
+
if (typeof redactedBody === 'object') {
|
|
290
|
+
try {
|
|
291
|
+
return escapeHtml(JSON.stringify(redactedBody, null, 2));
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return escapeHtml(String(redactedBody));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return escapeHtml(String(redactedBody));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Generate HTML for an assertion step
|
|
301
|
+
*/
|
|
302
|
+
function generateAssertionHtml(step, redaction) {
|
|
303
|
+
if (!step.assertion)
|
|
304
|
+
return '';
|
|
305
|
+
const assertion = step.assertion;
|
|
306
|
+
const statusClass = assertion.passed ? 'passed' : 'failed';
|
|
307
|
+
const statusIcon = assertion.passed ? '✓' : '✗';
|
|
308
|
+
// Use friendly name if available, otherwise fall back to expression
|
|
309
|
+
const displayText = assertion.friendlyName || assertion.message || assertion.expression;
|
|
310
|
+
let detailsHtml = '';
|
|
311
|
+
if (!assertion.passed) {
|
|
312
|
+
// Format actual value nicely
|
|
313
|
+
let actualDisplay = '';
|
|
314
|
+
if (assertion.leftValue === undefined) {
|
|
315
|
+
actualDisplay = 'undefined';
|
|
316
|
+
}
|
|
317
|
+
else if (assertion.leftValue === null) {
|
|
318
|
+
actualDisplay = 'null';
|
|
319
|
+
}
|
|
320
|
+
else if (typeof assertion.leftValue === 'object') {
|
|
321
|
+
actualDisplay = JSON.stringify(assertion.leftValue, null, 2);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
actualDisplay = String(assertion.leftValue);
|
|
325
|
+
}
|
|
326
|
+
// Show jsonPath if available for context
|
|
327
|
+
const pathInfo = assertion.jsonPath ? `<div class="assertion-path"><strong>Path:</strong> <code>${escapeHtml(assertion.jsonPath)}</code></div>` : '';
|
|
328
|
+
detailsHtml = `
|
|
329
|
+
<div class="assertion-details">
|
|
330
|
+
${pathInfo}
|
|
331
|
+
<div><strong>Expected:</strong> <code>${escapeHtml(String(assertion.rightExpression || assertion.rightValue))}</code></div>
|
|
332
|
+
<div><strong>Actual:</strong> <code>${escapeHtml(actualDisplay)}</code></div>
|
|
333
|
+
${assertion.error ? `<div class="error"><strong>Error:</strong> ${escapeHtml((0, redaction_1.redactString)(assertion.error, redaction))}</div>` : ''}
|
|
334
|
+
</div>`;
|
|
335
|
+
}
|
|
336
|
+
return `
|
|
337
|
+
<div class="step assertion ${statusClass}">
|
|
338
|
+
<span class="status-icon">${statusIcon}</span>
|
|
339
|
+
<span class="assertion-text">${escapeHtml(displayText)}</span>
|
|
340
|
+
${detailsHtml}
|
|
341
|
+
</div>`;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Generate HTML for a print step
|
|
345
|
+
*/
|
|
346
|
+
function generatePrintHtml(step) {
|
|
347
|
+
if (!step.print)
|
|
348
|
+
return '';
|
|
349
|
+
return `
|
|
350
|
+
<div class="step print">
|
|
351
|
+
<span class="print-icon">📝</span>
|
|
352
|
+
<span class="print-text">${escapeHtml(step.print.title)}</span>
|
|
353
|
+
${step.print.body ? `<div class="print-body">${escapeHtml(step.print.body)}</div>` : ''}
|
|
354
|
+
</div>`;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Generate HTML for a script step
|
|
358
|
+
*/
|
|
359
|
+
function generateScriptHtml(step) {
|
|
360
|
+
if (!step.script)
|
|
361
|
+
return '';
|
|
362
|
+
const statusClass = step.script.success ? 'passed' : 'failed';
|
|
363
|
+
const statusIcon = step.script.success ? '✓' : '✗';
|
|
364
|
+
return `
|
|
365
|
+
<div class="step script ${statusClass}">
|
|
366
|
+
<span class="status-icon">${statusIcon}</span>
|
|
367
|
+
<span class="script-type">[${escapeHtml(step.script.type)}]</span>
|
|
368
|
+
${step.script.error ? `<div class="error">${escapeHtml(step.script.error)}</div>` : ''}
|
|
369
|
+
</div>`;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Generate HTML for errors
|
|
373
|
+
*/
|
|
374
|
+
function generateErrorsHtml(errors, redaction) {
|
|
375
|
+
return `
|
|
376
|
+
<div class="errors">
|
|
377
|
+
<h4>Errors</h4>
|
|
378
|
+
<ul>
|
|
379
|
+
${errors.map(e => `<li>${escapeHtml((0, redaction_1.redactString)(e, redaction))}</li>`).join('\n')}
|
|
380
|
+
</ul>
|
|
381
|
+
</div>`;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Get embedded CSS styles
|
|
385
|
+
*/
|
|
386
|
+
function getEmbeddedCSS() {
|
|
387
|
+
return `
|
|
388
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
389
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: #1e1e1e; color: #d4d4d4; line-height: 1.6; }
|
|
390
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
391
|
+
header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #333; }
|
|
392
|
+
header h1 { color: #fff; font-size: 2em; margin-bottom: 10px; }
|
|
393
|
+
.header-meta { display: flex; justify-content: center; gap: 20px; flex-wrap: wrap; }
|
|
394
|
+
.timestamp { color: #888; font-size: 0.9em; }
|
|
395
|
+
.environment { color: #888; font-size: 0.9em; }
|
|
396
|
+
.environment strong { color: #569cd6; }
|
|
397
|
+
h2 { color: #fff; margin-bottom: 15px; font-size: 1.4em; }
|
|
398
|
+
|
|
399
|
+
.summary { background: #252526; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
|
400
|
+
.summary-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
|
401
|
+
.pass-rate { text-align: center; padding: 10px 20px; border-radius: 8px; }
|
|
402
|
+
.pass-rate.perfect { background: #1e3a2f; }
|
|
403
|
+
.pass-rate.good { background: #2a3a1e; }
|
|
404
|
+
.pass-rate.poor { background: #3a1e1e; }
|
|
405
|
+
.rate-value { display: block; font-size: 2em; font-weight: bold; }
|
|
406
|
+
.pass-rate.perfect .rate-value { color: #4ec9b0; }
|
|
407
|
+
.pass-rate.good .rate-value { color: #b5cea8; }
|
|
408
|
+
.pass-rate.poor .rate-value { color: #f14c4c; }
|
|
409
|
+
.rate-label { font-size: 0.8em; color: #888; }
|
|
410
|
+
|
|
411
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
|
412
|
+
.stat-card { background: #1e1e1e; border-radius: 6px; padding: 15px; text-align: center; transition: transform 0.2s, box-shadow 0.2s; }
|
|
413
|
+
.stat-card.clickable { cursor: pointer; }
|
|
414
|
+
.stat-card.clickable:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
|
415
|
+
.stat-card.all-passed { border-left: 4px solid #4ec9b0; }
|
|
416
|
+
.stat-card.has-failures { border-left: 4px solid #f14c4c; }
|
|
417
|
+
.stat-value { font-size: 2em; font-weight: bold; color: #fff; }
|
|
418
|
+
.stat-label { color: #888; font-size: 0.85em; margin-top: 5px; }
|
|
419
|
+
|
|
420
|
+
.controls { background: #252526; border-radius: 8px; padding: 15px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
|
|
421
|
+
.filter-bar { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; flex: 1; }
|
|
422
|
+
#search-input { background: #1e1e1e; border: 1px solid #333; border-radius: 4px; padding: 8px 12px; color: #d4d4d4; min-width: 200px; font-size: 0.9em; }
|
|
423
|
+
#search-input:focus { outline: none; border-color: #569cd6; }
|
|
424
|
+
#search-input::placeholder { color: #666; }
|
|
425
|
+
.filter-buttons { display: flex; gap: 5px; }
|
|
426
|
+
.filter-btn { background: #1e1e1e; border: 1px solid #333; border-radius: 4px; padding: 8px 16px; color: #888; cursor: pointer; font-size: 0.9em; transition: all 0.2s; }
|
|
427
|
+
.filter-btn:hover { border-color: #569cd6; color: #d4d4d4; }
|
|
428
|
+
.filter-btn.active { background: #264f78; border-color: #264f78; color: #fff; }
|
|
429
|
+
.action-buttons { display: flex; gap: 10px; }
|
|
430
|
+
.action-btn { background: transparent; border: 1px solid #444; border-radius: 4px; padding: 8px 16px; color: #888; cursor: pointer; font-size: 0.85em; transition: all 0.2s; }
|
|
431
|
+
.action-btn:hover { border-color: #666; color: #d4d4d4; }
|
|
432
|
+
|
|
433
|
+
.no-results { text-align: center; padding: 40px; color: #888; font-size: 1.1em; }
|
|
434
|
+
|
|
435
|
+
.progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; }
|
|
436
|
+
.progress-fill { height: 100%; transition: width 0.3s; }
|
|
437
|
+
.progress-fill.all-passed { background: #4ec9b0; }
|
|
438
|
+
.progress-fill.has-failures { background: linear-gradient(90deg, #4ec9b0, #f14c4c); }
|
|
439
|
+
|
|
440
|
+
.sequence { background: #252526; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
|
|
441
|
+
.sequence.passed .sequence-header { border-left: 4px solid #4ec9b0; }
|
|
442
|
+
.sequence.failed .sequence-header { border-left: 4px solid #f14c4c; }
|
|
443
|
+
.sequence-header { padding: 15px; cursor: pointer; display: flex; align-items: center; gap: 10px; }
|
|
444
|
+
.sequence-header:hover { background: #2d2d2d; }
|
|
445
|
+
.status-icon { font-size: 1.2em; }
|
|
446
|
+
.sequence.passed .status-icon { color: #4ec9b0; }
|
|
447
|
+
.sequence.failed .status-icon { color: #f14c4c; }
|
|
448
|
+
.sequence-name { font-weight: 600; color: #fff; flex: 1; }
|
|
449
|
+
.sequence-meta { display: flex; gap: 8px; }
|
|
450
|
+
.badge { background: #333; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; }
|
|
451
|
+
.badge.passed { background: #1e3a2f; color: #4ec9b0; }
|
|
452
|
+
.badge.failed { background: #3a1e1e; color: #f14c4c; }
|
|
453
|
+
.badge.duration { background: #1e2d3a; color: #569cd6; }
|
|
454
|
+
.chevron { color: #888; transition: transform 0.2s; }
|
|
455
|
+
.sequence.open .chevron { transform: rotate(180deg); }
|
|
456
|
+
|
|
457
|
+
.sequence-body { padding: 15px; border-top: 1px solid #333; }
|
|
458
|
+
.steps { display: flex; flex-direction: column; gap: 8px; }
|
|
459
|
+
|
|
460
|
+
.step { padding: 10px 15px; border-radius: 4px; background: #1e1e1e; }
|
|
461
|
+
.step.request { border-left: 3px solid #569cd6; }
|
|
462
|
+
.step.request.failed { border-left-color: #f14c4c; }
|
|
463
|
+
.step.assertion { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
|
|
464
|
+
.step.assertion.passed { border-left: 3px solid #4ec9b0; }
|
|
465
|
+
.step.assertion.failed { border-left: 3px solid #f14c4c; }
|
|
466
|
+
.step.print { border-left: 3px solid #dcdcaa; }
|
|
467
|
+
.step.script { border-left: 3px solid #c586c0; }
|
|
468
|
+
|
|
469
|
+
.step-header { display: flex; align-items: center; gap: 10px; cursor: pointer; }
|
|
470
|
+
.step-header:hover { opacity: 0.9; }
|
|
471
|
+
.var-label { color: #4fc1ff; font-family: monospace; font-weight: 500; }
|
|
472
|
+
.var-equals { color: #d4d4d4; margin-right: 5px; }
|
|
473
|
+
.method { background: #264f78; color: #fff; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; font-weight: 600; }
|
|
474
|
+
.url { flex: 1; color: #9cdcfe; font-family: monospace; font-size: 0.9em; word-break: break-all; }
|
|
475
|
+
.status-code { font-weight: 600; }
|
|
476
|
+
.step.request.passed .status-code { color: #4ec9b0; }
|
|
477
|
+
.step.request.failed .status-code { color: #f14c4c; }
|
|
478
|
+
.duration { color: #888; font-size: 0.85em; }
|
|
479
|
+
|
|
480
|
+
.step-body { margin-top: 15px; padding-top: 15px; border-top: 1px solid #333; }
|
|
481
|
+
.response-section { margin-bottom: 15px; }
|
|
482
|
+
.response-section h4 { color: #888; font-size: 0.9em; margin-bottom: 8px; }
|
|
483
|
+
.headers-table { width: 100%; font-size: 0.85em; }
|
|
484
|
+
.headers-table td { padding: 4px 8px; border-bottom: 1px solid #333; }
|
|
485
|
+
.header-name { color: #9cdcfe; font-family: monospace; width: 200px; }
|
|
486
|
+
.header-value { color: #ce9178; font-family: monospace; word-break: break-all; }
|
|
487
|
+
.body-content { background: #1a1a1a; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 0.85em; color: #d4d4d4; white-space: pre-wrap; word-break: break-word; }
|
|
488
|
+
|
|
489
|
+
.assertion-text { font-family: monospace; }
|
|
490
|
+
.assertion-details { width: 100%; margin-top: 8px; padding: 10px; background: #1a1a1a; border-radius: 4px; font-size: 0.9em; }
|
|
491
|
+
.assertion-details div { margin-bottom: 5px; }
|
|
492
|
+
.assertion-details code { background: #2d2d2d; padding: 2px 6px; border-radius: 3px; font-family: monospace; color: #ce9178; }
|
|
493
|
+
.assertion-path { color: #888; }
|
|
494
|
+
.assertion-path code { color: #9cdcfe; }
|
|
495
|
+
|
|
496
|
+
.print-icon { font-size: 1em; }
|
|
497
|
+
.print-text { color: #dcdcaa; }
|
|
498
|
+
.print-body { margin-top: 5px; color: #888; font-size: 0.9em; }
|
|
499
|
+
|
|
500
|
+
.script-type { color: #c586c0; font-family: monospace; }
|
|
501
|
+
|
|
502
|
+
.error { color: #f14c4c; }
|
|
503
|
+
.errors { margin-top: 15px; padding: 15px; background: #3a1e1e; border-radius: 4px; }
|
|
504
|
+
.errors h4 { color: #f14c4c; margin-bottom: 10px; }
|
|
505
|
+
.errors ul { padding-left: 20px; }
|
|
506
|
+
.errors li { margin-bottom: 5px; }
|
|
507
|
+
`;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Get embedded JavaScript
|
|
511
|
+
*/
|
|
512
|
+
function getEmbeddedJS() {
|
|
513
|
+
return `
|
|
514
|
+
let currentFilter = 'all';
|
|
515
|
+
|
|
516
|
+
function toggleSequence(index) {
|
|
517
|
+
const body = document.getElementById('sequence-body-' + index);
|
|
518
|
+
const sequence = body.closest('.sequence');
|
|
519
|
+
if (body.style.display === 'none') {
|
|
520
|
+
body.style.display = 'block';
|
|
521
|
+
sequence.classList.add('open');
|
|
522
|
+
} else {
|
|
523
|
+
body.style.display = 'none';
|
|
524
|
+
sequence.classList.remove('open');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function toggleStep(id) {
|
|
529
|
+
const body = document.getElementById(id);
|
|
530
|
+
const step = body.closest('.step');
|
|
531
|
+
const chevron = step.querySelector('.chevron');
|
|
532
|
+
if (body.style.display === 'none') {
|
|
533
|
+
body.style.display = 'block';
|
|
534
|
+
if (chevron) chevron.textContent = '▼';
|
|
535
|
+
} else {
|
|
536
|
+
body.style.display = 'none';
|
|
537
|
+
if (chevron) chevron.textContent = '▶';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function setFilter(filter) {
|
|
542
|
+
currentFilter = filter;
|
|
543
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
544
|
+
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
545
|
+
});
|
|
546
|
+
filterTests();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function filterByStatus(filter) {
|
|
550
|
+
setFilter(filter);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function filterTests() {
|
|
554
|
+
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
|
555
|
+
const sequences = document.querySelectorAll('.sequence');
|
|
556
|
+
let visibleCount = 0;
|
|
557
|
+
|
|
558
|
+
sequences.forEach(seq => {
|
|
559
|
+
const name = seq.querySelector('.sequence-name').textContent.toLowerCase();
|
|
560
|
+
const isPassed = seq.classList.contains('passed');
|
|
561
|
+
const isFailed = seq.classList.contains('failed');
|
|
562
|
+
|
|
563
|
+
let matchesFilter = currentFilter === 'all' ||
|
|
564
|
+
(currentFilter === 'passed' && isPassed) ||
|
|
565
|
+
(currentFilter === 'failed' && isFailed);
|
|
566
|
+
|
|
567
|
+
let matchesSearch = !searchTerm || name.includes(searchTerm);
|
|
568
|
+
|
|
569
|
+
if (matchesFilter && matchesSearch) {
|
|
570
|
+
seq.style.display = 'block';
|
|
571
|
+
visibleCount++;
|
|
572
|
+
} else {
|
|
573
|
+
seq.style.display = 'none';
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
document.getElementById('no-results').style.display = visibleCount === 0 ? 'block' : 'none';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function expandAll() {
|
|
581
|
+
document.querySelectorAll('.sequence').forEach(seq => {
|
|
582
|
+
if (seq.style.display !== 'none') {
|
|
583
|
+
const index = seq.dataset.sequence;
|
|
584
|
+
const body = document.getElementById('sequence-body-' + index);
|
|
585
|
+
if (body) {
|
|
586
|
+
body.style.display = 'block';
|
|
587
|
+
seq.classList.add('open');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function collapseAll() {
|
|
594
|
+
document.querySelectorAll('.sequence').forEach(seq => {
|
|
595
|
+
const index = seq.dataset.sequence;
|
|
596
|
+
const body = document.getElementById('sequence-body-' + index);
|
|
597
|
+
if (body) {
|
|
598
|
+
body.style.display = 'none';
|
|
599
|
+
seq.classList.remove('open');
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Expand all failed sequences by default
|
|
605
|
+
document.querySelectorAll('.sequence.failed').forEach((seq, i) => {
|
|
606
|
+
const index = seq.dataset.sequence;
|
|
607
|
+
if (index) toggleSequence(index);
|
|
608
|
+
});
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Generate HTML report from a single HTTP response
|
|
613
|
+
*/
|
|
614
|
+
function generateHtmlReportFromResponse(response, testName, options) {
|
|
615
|
+
// Create a mock sequence result for the single request
|
|
616
|
+
const mockResult = {
|
|
617
|
+
name: testName,
|
|
618
|
+
success: response.status >= 200 && response.status < 300,
|
|
619
|
+
responses: [response],
|
|
620
|
+
scriptResults: [],
|
|
621
|
+
assertionResults: [],
|
|
622
|
+
steps: [{
|
|
623
|
+
type: 'request',
|
|
624
|
+
stepIndex: 0,
|
|
625
|
+
response,
|
|
626
|
+
requestMethod: 'REQUEST',
|
|
627
|
+
requestUrl: 'unknown'
|
|
628
|
+
}],
|
|
629
|
+
errors: [],
|
|
630
|
+
duration: response.duration
|
|
631
|
+
};
|
|
632
|
+
generateHtmlReport([mockResult], options);
|
|
633
|
+
}
|
|
634
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI reporters index - re-exports all reporter functions
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
17
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
__exportStar(require("./junit"), exports);
|
|
21
|
+
__exportStar(require("./html"), exports);
|
|
22
|
+
//# sourceMappingURL=index.js.map
|