norn-cli 1.2.4 → 1.2.5
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/CHANGELOG.md +14 -0
- package/dist/cli.js +168 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.5] - 2026-02-07
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
- **Rich HTML Reports**: Completely redesigned HTML report output for QA professionals
|
|
9
|
+
- **Pass rate badge**: Large visual indicator showing pass percentage (green/yellow/red)
|
|
10
|
+
- **Filter bar**: Search tests by name, filter by All/Passed/Failed
|
|
11
|
+
- **Clickable stat cards**: Click total/passed/failed to filter results
|
|
12
|
+
- **Expand/Collapse All**: Buttons to quickly expand or collapse all test details
|
|
13
|
+
- **Failures first**: Failed tests automatically sorted to top and expanded
|
|
14
|
+
- **Friendly assertion names**: Shows `user.body.status` instead of `$1.body.status`
|
|
15
|
+
- **JSON path context**: Failed assertions show the path that failed
|
|
16
|
+
- **Variable labels on requests**: Shows `user = GET /api/users` instead of `$1`
|
|
17
|
+
- **Environment display**: Shows which environment was used in report header
|
|
18
|
+
|
|
5
19
|
## [1.2.4] - 2026-02-07
|
|
6
20
|
|
|
7
21
|
### Improved
|
package/dist/cli.js
CHANGED
|
@@ -22677,7 +22677,7 @@ function formatTimestamp() {
|
|
|
22677
22677
|
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
|
22678
22678
|
}
|
|
22679
22679
|
function generateHtmlReport(results, options) {
|
|
22680
|
-
const { outputPath, redaction, title = "Norn Test Report" } = options;
|
|
22680
|
+
const { outputPath, redaction, title = "Norn Test Report", environment } = options;
|
|
22681
22681
|
const totalSequences = results.length;
|
|
22682
22682
|
const passedSequences = results.filter((r) => r.success).length;
|
|
22683
22683
|
const failedSequences = totalSequences - passedSequences;
|
|
@@ -22686,6 +22686,11 @@ function generateHtmlReport(results, options) {
|
|
|
22686
22686
|
const failedAssertions = totalAssertions - passedAssertions;
|
|
22687
22687
|
const totalRequests = results.reduce((sum, r) => sum + r.steps.filter((s) => s.type === "request").length, 0);
|
|
22688
22688
|
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
22689
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
22690
|
+
if (a.success !== b.success) return a.success ? 1 : -1;
|
|
22691
|
+
return a.name.localeCompare(b.name);
|
|
22692
|
+
});
|
|
22693
|
+
const passRate = totalSequences > 0 ? Math.round(passedSequences / totalSequences * 100) : 100;
|
|
22689
22694
|
const html = `<!DOCTYPE html>
|
|
22690
22695
|
<html lang="en">
|
|
22691
22696
|
<head>
|
|
@@ -22700,23 +22705,32 @@ function generateHtmlReport(results, options) {
|
|
|
22700
22705
|
<div class="container">
|
|
22701
22706
|
<header>
|
|
22702
22707
|
<h1>${escapeHtml(title)}</h1>
|
|
22703
|
-
<
|
|
22708
|
+
<div class="header-meta">
|
|
22709
|
+
<span class="timestamp">Generated: ${formatTimestamp()}</span>
|
|
22710
|
+
${environment ? `<span class="environment">Environment: <strong>${escapeHtml(environment)}</strong></span>` : ""}
|
|
22711
|
+
</div>
|
|
22704
22712
|
</header>
|
|
22705
22713
|
|
|
22706
22714
|
<section class="summary">
|
|
22707
|
-
<
|
|
22715
|
+
<div class="summary-header">
|
|
22716
|
+
<h2>Summary</h2>
|
|
22717
|
+
<div class="pass-rate ${passRate === 100 ? "perfect" : passRate >= 80 ? "good" : "poor"}">
|
|
22718
|
+
<span class="rate-value">${passRate}%</span>
|
|
22719
|
+
<span class="rate-label">Pass Rate</span>
|
|
22720
|
+
</div>
|
|
22721
|
+
</div>
|
|
22708
22722
|
<div class="stats-grid">
|
|
22709
|
-
<div class="stat-card ${failedSequences > 0 ? "has-failures" : "all-passed"}">
|
|
22710
|
-
<div class="stat-value">${
|
|
22711
|
-
<div class="stat-label">
|
|
22723
|
+
<div class="stat-card clickable ${failedSequences > 0 ? "has-failures" : "all-passed"}" onclick="filterByStatus('all')" data-filter="all">
|
|
22724
|
+
<div class="stat-value">${totalSequences}</div>
|
|
22725
|
+
<div class="stat-label">Total Tests</div>
|
|
22712
22726
|
</div>
|
|
22713
|
-
<div class="stat-card ${
|
|
22714
|
-
<div class="stat-value">${
|
|
22715
|
-
<div class="stat-label">
|
|
22727
|
+
<div class="stat-card clickable ${passedSequences > 0 ? "all-passed" : ""}" onclick="filterByStatus('passed')" data-filter="passed">
|
|
22728
|
+
<div class="stat-value">${passedSequences}</div>
|
|
22729
|
+
<div class="stat-label">Passed</div>
|
|
22716
22730
|
</div>
|
|
22717
|
-
<div class="stat-card">
|
|
22718
|
-
<div class="stat-value">${
|
|
22719
|
-
<div class="stat-label">
|
|
22731
|
+
<div class="stat-card clickable ${failedSequences > 0 ? "has-failures" : ""}" onclick="filterByStatus('failed')" data-filter="failed">
|
|
22732
|
+
<div class="stat-value">${failedSequences}</div>
|
|
22733
|
+
<div class="stat-label">Failed</div>
|
|
22720
22734
|
</div>
|
|
22721
22735
|
<div class="stat-card">
|
|
22722
22736
|
<div class="stat-value">${formatDuration2(totalDuration)}</div>
|
|
@@ -22730,9 +22744,29 @@ function generateHtmlReport(results, options) {
|
|
|
22730
22744
|
</div>
|
|
22731
22745
|
</section>
|
|
22732
22746
|
|
|
22747
|
+
<section class="controls">
|
|
22748
|
+
<div class="filter-bar">
|
|
22749
|
+
<input type="text" id="search-input" placeholder="Search tests..." onkeyup="filterTests()">
|
|
22750
|
+
<div class="filter-buttons">
|
|
22751
|
+
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All (${totalSequences})</button>
|
|
22752
|
+
<button class="filter-btn" data-filter="passed" onclick="setFilter('passed')">Passed (${passedSequences})</button>
|
|
22753
|
+
<button class="filter-btn" data-filter="failed" onclick="setFilter('failed')">Failed (${failedSequences})</button>
|
|
22754
|
+
</div>
|
|
22755
|
+
</div>
|
|
22756
|
+
<div class="action-buttons">
|
|
22757
|
+
<button class="action-btn" onclick="expandAll()">Expand All</button>
|
|
22758
|
+
<button class="action-btn" onclick="collapseAll()">Collapse All</button>
|
|
22759
|
+
</div>
|
|
22760
|
+
</section>
|
|
22761
|
+
|
|
22733
22762
|
<section class="results">
|
|
22734
22763
|
<h2>Test Results</h2>
|
|
22735
|
-
|
|
22764
|
+
<div id="results-container">
|
|
22765
|
+
${sortedResults.map((r, i) => generateSequenceHtml(r, i, redaction)).join("\n")}
|
|
22766
|
+
</div>
|
|
22767
|
+
<div id="no-results" class="no-results" style="display: none;">
|
|
22768
|
+
No tests match your filter criteria.
|
|
22769
|
+
</div>
|
|
22736
22770
|
</section>
|
|
22737
22771
|
</div>
|
|
22738
22772
|
|
|
@@ -22797,11 +22831,13 @@ function generateRequestHtml(step, seqIndex, reqIndex, redaction) {
|
|
|
22797
22831
|
const statusClass = isSuccess ? "passed" : "failed";
|
|
22798
22832
|
const method = step.requestMethod || "REQUEST";
|
|
22799
22833
|
const url2 = step.requestUrl ? redactUrl(step.requestUrl, redaction) : "unknown";
|
|
22834
|
+
const varLabel = step.variableName ? `<span class="var-label">${escapeHtml(step.variableName)}</span><span class="var-equals">=</span>` : "";
|
|
22800
22835
|
const bodyHtml = response.body ? generateBodyHtml(response.body, redaction) : "<em>No body</em>";
|
|
22801
22836
|
const headersHtml = response.headers ? generateHeadersHtml(response.headers, redaction) : "";
|
|
22802
22837
|
return `
|
|
22803
22838
|
<div class="step request ${statusClass}">
|
|
22804
22839
|
<div class="step-header" onclick="toggleStep('step-${seqIndex}-${reqIndex}')">
|
|
22840
|
+
${varLabel}
|
|
22805
22841
|
<span class="method">${escapeHtml(method)}</span>
|
|
22806
22842
|
<span class="url">${escapeHtml(url2)}</span>
|
|
22807
22843
|
<span class="status-code">${response.status} ${escapeHtml(response.statusText)}</span>
|
|
@@ -22840,20 +22876,32 @@ function generateAssertionHtml(step, redaction) {
|
|
|
22840
22876
|
const assertion = step.assertion;
|
|
22841
22877
|
const statusClass = assertion.passed ? "passed" : "failed";
|
|
22842
22878
|
const statusIcon = assertion.passed ? "\u2713" : "\u2717";
|
|
22843
|
-
const displayText = assertion.message || assertion.expression;
|
|
22879
|
+
const displayText = assertion.friendlyName || assertion.message || assertion.expression;
|
|
22844
22880
|
let detailsHtml = "";
|
|
22845
22881
|
if (!assertion.passed) {
|
|
22882
|
+
let actualDisplay = "";
|
|
22883
|
+
if (assertion.leftValue === void 0) {
|
|
22884
|
+
actualDisplay = "undefined";
|
|
22885
|
+
} else if (assertion.leftValue === null) {
|
|
22886
|
+
actualDisplay = "null";
|
|
22887
|
+
} else if (typeof assertion.leftValue === "object") {
|
|
22888
|
+
actualDisplay = JSON.stringify(assertion.leftValue, null, 2);
|
|
22889
|
+
} else {
|
|
22890
|
+
actualDisplay = String(assertion.leftValue);
|
|
22891
|
+
}
|
|
22892
|
+
const pathInfo = assertion.jsonPath ? `<div class="assertion-path"><strong>Path:</strong> <code>${escapeHtml(assertion.jsonPath)}</code></div>` : "";
|
|
22846
22893
|
detailsHtml = `
|
|
22847
22894
|
<div class="assertion-details">
|
|
22848
|
-
|
|
22849
|
-
<div><strong>
|
|
22895
|
+
${pathInfo}
|
|
22896
|
+
<div><strong>Expected:</strong> <code>${escapeHtml(String(assertion.rightExpression || assertion.rightValue))}</code></div>
|
|
22897
|
+
<div><strong>Actual:</strong> <code>${escapeHtml(actualDisplay)}</code></div>
|
|
22850
22898
|
${assertion.error ? `<div class="error"><strong>Error:</strong> ${escapeHtml(redactString(assertion.error, redaction))}</div>` : ""}
|
|
22851
22899
|
</div>`;
|
|
22852
22900
|
}
|
|
22853
22901
|
return `
|
|
22854
22902
|
<div class="step assertion ${statusClass}">
|
|
22855
22903
|
<span class="status-icon">${statusIcon}</span>
|
|
22856
|
-
<span class="assertion-text"
|
|
22904
|
+
<span class="assertion-text">${escapeHtml(displayText)}</span>
|
|
22857
22905
|
${detailsHtml}
|
|
22858
22906
|
</div>`;
|
|
22859
22907
|
}
|
|
@@ -22893,17 +22941,48 @@ function getEmbeddedCSS() {
|
|
|
22893
22941
|
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
22894
22942
|
header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #333; }
|
|
22895
22943
|
header h1 { color: #fff; font-size: 2em; margin-bottom: 10px; }
|
|
22944
|
+
.header-meta { display: flex; justify-content: center; gap: 20px; flex-wrap: wrap; }
|
|
22896
22945
|
.timestamp { color: #888; font-size: 0.9em; }
|
|
22946
|
+
.environment { color: #888; font-size: 0.9em; }
|
|
22947
|
+
.environment strong { color: #569cd6; }
|
|
22897
22948
|
h2 { color: #fff; margin-bottom: 15px; font-size: 1.4em; }
|
|
22898
22949
|
|
|
22899
|
-
.summary { background: #252526; border-radius: 8px; padding: 20px; margin-bottom:
|
|
22950
|
+
.summary { background: #252526; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
|
22951
|
+
.summary-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
|
22952
|
+
.pass-rate { text-align: center; padding: 10px 20px; border-radius: 8px; }
|
|
22953
|
+
.pass-rate.perfect { background: #1e3a2f; }
|
|
22954
|
+
.pass-rate.good { background: #2a3a1e; }
|
|
22955
|
+
.pass-rate.poor { background: #3a1e1e; }
|
|
22956
|
+
.rate-value { display: block; font-size: 2em; font-weight: bold; }
|
|
22957
|
+
.pass-rate.perfect .rate-value { color: #4ec9b0; }
|
|
22958
|
+
.pass-rate.good .rate-value { color: #b5cea8; }
|
|
22959
|
+
.pass-rate.poor .rate-value { color: #f14c4c; }
|
|
22960
|
+
.rate-label { font-size: 0.8em; color: #888; }
|
|
22961
|
+
|
|
22900
22962
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
|
22901
|
-
.stat-card { background: #1e1e1e; border-radius: 6px; padding: 15px; text-align: center; }
|
|
22963
|
+
.stat-card { background: #1e1e1e; border-radius: 6px; padding: 15px; text-align: center; transition: transform 0.2s, box-shadow 0.2s; }
|
|
22964
|
+
.stat-card.clickable { cursor: pointer; }
|
|
22965
|
+
.stat-card.clickable:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
|
22902
22966
|
.stat-card.all-passed { border-left: 4px solid #4ec9b0; }
|
|
22903
22967
|
.stat-card.has-failures { border-left: 4px solid #f14c4c; }
|
|
22904
22968
|
.stat-value { font-size: 2em; font-weight: bold; color: #fff; }
|
|
22905
22969
|
.stat-label { color: #888; font-size: 0.85em; margin-top: 5px; }
|
|
22906
22970
|
|
|
22971
|
+
.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; }
|
|
22972
|
+
.filter-bar { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; flex: 1; }
|
|
22973
|
+
#search-input { background: #1e1e1e; border: 1px solid #333; border-radius: 4px; padding: 8px 12px; color: #d4d4d4; min-width: 200px; font-size: 0.9em; }
|
|
22974
|
+
#search-input:focus { outline: none; border-color: #569cd6; }
|
|
22975
|
+
#search-input::placeholder { color: #666; }
|
|
22976
|
+
.filter-buttons { display: flex; gap: 5px; }
|
|
22977
|
+
.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; }
|
|
22978
|
+
.filter-btn:hover { border-color: #569cd6; color: #d4d4d4; }
|
|
22979
|
+
.filter-btn.active { background: #264f78; border-color: #264f78; color: #fff; }
|
|
22980
|
+
.action-buttons { display: flex; gap: 10px; }
|
|
22981
|
+
.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; }
|
|
22982
|
+
.action-btn:hover { border-color: #666; color: #d4d4d4; }
|
|
22983
|
+
|
|
22984
|
+
.no-results { text-align: center; padding: 40px; color: #888; font-size: 1.1em; }
|
|
22985
|
+
|
|
22907
22986
|
.progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; }
|
|
22908
22987
|
.progress-fill { height: 100%; transition: width 0.3s; }
|
|
22909
22988
|
.progress-fill.all-passed { background: #4ec9b0; }
|
|
@@ -22940,6 +23019,8 @@ function getEmbeddedCSS() {
|
|
|
22940
23019
|
|
|
22941
23020
|
.step-header { display: flex; align-items: center; gap: 10px; cursor: pointer; }
|
|
22942
23021
|
.step-header:hover { opacity: 0.9; }
|
|
23022
|
+
.var-label { color: #4fc1ff; font-family: monospace; font-weight: 500; }
|
|
23023
|
+
.var-equals { color: #d4d4d4; margin-right: 5px; }
|
|
22943
23024
|
.method { background: #264f78; color: #fff; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; font-weight: 600; }
|
|
22944
23025
|
.url { flex: 1; color: #9cdcfe; font-family: monospace; font-size: 0.9em; word-break: break-all; }
|
|
22945
23026
|
.status-code { font-weight: 600; }
|
|
@@ -22959,6 +23040,9 @@ function getEmbeddedCSS() {
|
|
|
22959
23040
|
.assertion-text { font-family: monospace; }
|
|
22960
23041
|
.assertion-details { width: 100%; margin-top: 8px; padding: 10px; background: #1a1a1a; border-radius: 4px; font-size: 0.9em; }
|
|
22961
23042
|
.assertion-details div { margin-bottom: 5px; }
|
|
23043
|
+
.assertion-details code { background: #2d2d2d; padding: 2px 6px; border-radius: 3px; font-family: monospace; color: #ce9178; }
|
|
23044
|
+
.assertion-path { color: #888; }
|
|
23045
|
+
.assertion-path code { color: #9cdcfe; }
|
|
22962
23046
|
|
|
22963
23047
|
.print-icon { font-size: 1em; }
|
|
22964
23048
|
.print-text { color: #dcdcaa; }
|
|
@@ -22975,6 +23059,8 @@ function getEmbeddedCSS() {
|
|
|
22975
23059
|
}
|
|
22976
23060
|
function getEmbeddedJS() {
|
|
22977
23061
|
return `
|
|
23062
|
+
let currentFilter = 'all';
|
|
23063
|
+
|
|
22978
23064
|
function toggleSequence(index) {
|
|
22979
23065
|
const body = document.getElementById('sequence-body-' + index);
|
|
22980
23066
|
const sequence = body.closest('.sequence');
|
|
@@ -23000,6 +23086,69 @@ function getEmbeddedJS() {
|
|
|
23000
23086
|
}
|
|
23001
23087
|
}
|
|
23002
23088
|
|
|
23089
|
+
function setFilter(filter) {
|
|
23090
|
+
currentFilter = filter;
|
|
23091
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
23092
|
+
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
23093
|
+
});
|
|
23094
|
+
filterTests();
|
|
23095
|
+
}
|
|
23096
|
+
|
|
23097
|
+
function filterByStatus(filter) {
|
|
23098
|
+
setFilter(filter);
|
|
23099
|
+
}
|
|
23100
|
+
|
|
23101
|
+
function filterTests() {
|
|
23102
|
+
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
|
23103
|
+
const sequences = document.querySelectorAll('.sequence');
|
|
23104
|
+
let visibleCount = 0;
|
|
23105
|
+
|
|
23106
|
+
sequences.forEach(seq => {
|
|
23107
|
+
const name = seq.querySelector('.sequence-name').textContent.toLowerCase();
|
|
23108
|
+
const isPassed = seq.classList.contains('passed');
|
|
23109
|
+
const isFailed = seq.classList.contains('failed');
|
|
23110
|
+
|
|
23111
|
+
let matchesFilter = currentFilter === 'all' ||
|
|
23112
|
+
(currentFilter === 'passed' && isPassed) ||
|
|
23113
|
+
(currentFilter === 'failed' && isFailed);
|
|
23114
|
+
|
|
23115
|
+
let matchesSearch = !searchTerm || name.includes(searchTerm);
|
|
23116
|
+
|
|
23117
|
+
if (matchesFilter && matchesSearch) {
|
|
23118
|
+
seq.style.display = 'block';
|
|
23119
|
+
visibleCount++;
|
|
23120
|
+
} else {
|
|
23121
|
+
seq.style.display = 'none';
|
|
23122
|
+
}
|
|
23123
|
+
});
|
|
23124
|
+
|
|
23125
|
+
document.getElementById('no-results').style.display = visibleCount === 0 ? 'block' : 'none';
|
|
23126
|
+
}
|
|
23127
|
+
|
|
23128
|
+
function expandAll() {
|
|
23129
|
+
document.querySelectorAll('.sequence').forEach(seq => {
|
|
23130
|
+
if (seq.style.display !== 'none') {
|
|
23131
|
+
const index = seq.dataset.sequence;
|
|
23132
|
+
const body = document.getElementById('sequence-body-' + index);
|
|
23133
|
+
if (body) {
|
|
23134
|
+
body.style.display = 'block';
|
|
23135
|
+
seq.classList.add('open');
|
|
23136
|
+
}
|
|
23137
|
+
}
|
|
23138
|
+
});
|
|
23139
|
+
}
|
|
23140
|
+
|
|
23141
|
+
function collapseAll() {
|
|
23142
|
+
document.querySelectorAll('.sequence').forEach(seq => {
|
|
23143
|
+
const index = seq.dataset.sequence;
|
|
23144
|
+
const body = document.getElementById('sequence-body-' + index);
|
|
23145
|
+
if (body) {
|
|
23146
|
+
body.style.display = 'none';
|
|
23147
|
+
seq.classList.remove('open');
|
|
23148
|
+
}
|
|
23149
|
+
});
|
|
23150
|
+
}
|
|
23151
|
+
|
|
23003
23152
|
// Expand all failed sequences by default
|
|
23004
23153
|
document.querySelectorAll('.sequence.failed').forEach((seq, i) => {
|
|
23005
23154
|
const index = seq.dataset.sequence;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "norn-cli",
|
|
3
3
|
"displayName": "Norn - REST Client",
|
|
4
4
|
"description": "A powerful REST client for making HTTP requests with sequences, variables, scripts, and cookie support",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.5",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|