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.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli.js +168 -19
  3. 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
- <p class="timestamp">Generated: ${formatTimestamp()}</p>
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
- <h2>Summary</h2>
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">${passedSequences}/${totalSequences}</div>
22711
- <div class="stat-label">Sequences Passed</div>
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 ${failedAssertions > 0 ? "has-failures" : "all-passed"}">
22714
- <div class="stat-value">${passedAssertions}/${totalAssertions}</div>
22715
- <div class="stat-label">Assertions Passed</div>
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">${totalRequests}</div>
22719
- <div class="stat-label">Total Requests</div>
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
- ${results.map((r, i) => generateSequenceHtml(r, i, redaction)).join("\n")}
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
- <div><strong>Expected:</strong> ${escapeHtml(String(assertion.rightExpression || assertion.rightValue))}</div>
22849
- <div><strong>Actual:</strong> ${escapeHtml(JSON.stringify(assertion.leftValue))}</div>
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">assert ${escapeHtml(displayText)}</span>
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: 30px; }
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.4",
5
+ "version": "1.2.5",
6
6
  "publisher": "Norn-PeterKrustanov",
7
7
  "author": {
8
8
  "name": "Peter Krastanov"