norn-cli 1.2.3 → 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 CHANGED
@@ -2,6 +2,31 @@
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
+
19
+ ## [1.2.4] - 2026-02-07
20
+
21
+ ### Improved
22
+ - **Enhanced failure experience**: Failed assertions now display much richer context
23
+ - **Friendly variable names**: Shows `user.body.status` instead of `$1.body.status` when response was assigned to a variable
24
+ - **Auto-expand failures**: Failed assertions automatically expand to show details
25
+ - **Inline response preview**: Failed assertions include a collapsible preview of the related response body
26
+ - **JSON path highlighting**: The failing field is highlighted in the response preview
27
+ - **Request labels**: Request steps in sequences show variable names (e.g., `user`) instead of `$1`
28
+ - Same improvements apply to CLI output and Test Explorer
29
+
5
30
  ## [1.2.3] - 2026-02-07
6
31
 
7
32
  ### Added
package/README.md CHANGED
@@ -79,7 +79,7 @@ sequence Example
79
79
  GET https://api.example.com/users/{{userId}}
80
80
  var userName = $1.body.name
81
81
 
82
- print Result | {{message}}, Name: {{userName}}
82
+ print "Result" | "{{message}}, Name: {{userName}}"
83
83
  end sequence
84
84
  ```
85
85
 
@@ -133,14 +133,14 @@ sequence Login
133
133
  Content-Type: application/json
134
134
  {"username": "admin", "password": "secret"}
135
135
  var token = $1.body.accessToken
136
- print Logged in | Token: {{token}}
136
+ print "Logged in" | "Token: {{token}}"
137
137
  end sequence
138
138
 
139
139
  # Define reusable teardown sequence
140
140
  sequence Logout
141
141
  POST {{baseUrl}}/auth/logout
142
142
  Authorization: Bearer {{token}}
143
- print Logged out
143
+ print "Logged out"
144
144
  end sequence
145
145
 
146
146
  # Main test sequence uses setup/teardown
@@ -171,7 +171,7 @@ Sequences can accept parameters with optional default values:
171
171
  ```bash
172
172
  # Required parameter
173
173
  sequence Greet(name)
174
- print Hello, {{name}}!
174
+ print "Hello, {{name}}!"
175
175
  end sequence
176
176
 
177
177
  # Optional parameter with default
@@ -218,8 +218,8 @@ sequence MyTests
218
218
  var user = run FetchUser("123")
219
219
 
220
220
  # Access individual fields
221
- print User name: {{user.name}}
222
- print User email: {{user.email}}
221
+ print "User name" | "{{user.name}}"
222
+ print "User email" | "{{user.email}}"
223
223
 
224
224
  # Use in requests
225
225
  POST {{baseUrl}}/messages
@@ -434,7 +434,7 @@ sequence UserTests
434
434
 
435
435
  # Capture response to variable
436
436
  var user = GET GetUser(1) Json
437
- print User | {{user.body.name}}
437
+ print "User" | "{{user.body.name}}"
438
438
 
439
439
  # Use variables in endpoint parameters
440
440
  var userId = 5
@@ -551,7 +551,7 @@ sequence AuthFlow
551
551
 
552
552
  # Run another named request
553
553
  run GetProfile
554
- print Profile | Welcome, {{$2.body.name}}!
554
+ print "Profile" | "Welcome, {{$2.body.name}}!"
555
555
  end sequence
556
556
  ```
557
557
 
@@ -565,7 +565,7 @@ sequence ConditionalFlow
565
565
 
566
566
  # Execute block only if condition is true
567
567
  if $1.status == 200
568
- print Success | User found!
568
+ print "Success" | "User found!"
569
569
 
570
570
  GET https://api.example.com/users/1/orders
571
571
  assert $2.status == 200
@@ -573,12 +573,12 @@ sequence ConditionalFlow
573
573
 
574
574
  # Check for errors
575
575
  if $1.status == 404
576
- print Error | User not found
576
+ print "Error" | "User not found"
577
577
  end if
578
578
 
579
579
  # Conditions support all assertion operators
580
580
  if $1.body.role == "admin"
581
- print Admin | User has admin privileges
581
+ print "Admin" | "User has admin privileges"
582
582
  end if
583
583
  end sequence
584
584
  ```
@@ -592,7 +592,7 @@ sequence RateLimitedFlow
592
592
  POST https://api.example.com/jobs
593
593
  var jobId = $1.body.id
594
594
 
595
- print Waiting | Job submitted, waiting for completion...
595
+ print "Waiting" | "Job submitted, waiting for completion..."
596
596
 
597
597
  # Wait 2 seconds
598
598
  wait 2s
@@ -616,19 +616,19 @@ sequence DataDrivenTest
616
616
  var config = run readJson ./test-config.json
617
617
 
618
618
  # Access properties
619
- print Config | Using API: {{config.baseUrl}}
619
+ print "Config" | "Using API: {{config.baseUrl}}"
620
620
 
621
621
  # Use in requests
622
622
  GET {{config.baseUrl}}/users/{{config.testUser.id}}
623
623
 
624
624
  # Access nested values and arrays
625
- print First Role | {{config.testUser.roles[0]}}
625
+ print "First Role" | "{{config.testUser.roles[0]}}"
626
626
 
627
627
  # Modify loaded data inline
628
628
  config.baseUrl = https://api.updated.com
629
629
  config.testUser.name = Updated Name
630
630
 
631
- print Updated | New URL: {{config.baseUrl}}
631
+ print "Updated" | "New URL: {{config.baseUrl}}"
632
632
  end sequence
633
633
  ```
634
634
 
@@ -674,7 +674,7 @@ sequence DatabaseQuery
674
674
  var dbResult = run powershell ./scripts/query-db.ps1
675
675
 
676
676
  # Access properties from the JSON output
677
- print User Found | ID: {{dbResult.id}}, Name: {{dbResult.name}}
677
+ print "User Found" | "ID: {{dbResult.id}}, Name: {{dbResult.name}}"
678
678
 
679
679
  # Use in requests
680
680
  GET https://api.example.com/users/{{dbResult.id}}
@@ -697,18 +697,18 @@ Add debug output to your sequences:
697
697
 
698
698
  ```bash
699
699
  sequence DebugFlow
700
- print Starting authentication...
700
+ print "Starting authentication..."
701
701
 
702
702
  POST https://api.example.com/login
703
703
  Content-Type: application/json
704
704
  {"user": "admin"}
705
705
 
706
706
  var token = $1.token
707
- print Token received | Value: {{token}}
707
+ print "Token received" | "Value: {{token}}"
708
708
  end sequence
709
709
  ```
710
710
 
711
- Use `print Title | Body content` for expandable messages in the result view.
711
+ Use `print "Title" | "Body content"` for expandable messages in the result view.
712
712
 
713
713
  ## CLI Usage
714
714
 
@@ -942,8 +942,8 @@ end sequence
942
942
  | `var x = run js ./script.js` | Run script and capture output |
943
943
  | `var data = run readJson ./file.json` | Load JSON file into variable |
944
944
  | `data.property = value` | Update loaded JSON property |
945
- | `print Message` | Print a message |
946
- | `print Title \| Body` | Print with expandable body |
945
+ | `print "Message"` | Print a message |
946
+ | `print "Title" \| "Body"` | Print with expandable body |
947
947
 
948
948
  ### Environments (.nornenv)
949
949
 
package/dist/cli.js CHANGED
@@ -12216,11 +12216,12 @@ function findUnquotedPipe(str) {
12216
12216
  }
12217
12217
  return -1;
12218
12218
  }
12219
- function resolveValue(expr, responses, variables, getValueByPath2) {
12219
+ function resolveValue(expr, responses, variables, getValueByPath2, responseIndexToVariable) {
12220
12220
  const trimmed = expr.trim();
12221
12221
  const refMatch = trimmed.match(/^\$(\d+)\.(.+)$/);
12222
12222
  if (refMatch) {
12223
- const responseIndex = parseInt(refMatch[1], 10) - 1;
12223
+ const responseIdx = parseInt(refMatch[1], 10);
12224
+ const responseIndex = responseIdx - 1;
12224
12225
  const path4 = refMatch[2];
12225
12226
  if (responseIndex < 0 || responseIndex >= responses.length) {
12226
12227
  return {
@@ -12228,8 +12229,15 @@ function resolveValue(expr, responses, variables, getValueByPath2) {
12228
12229
  error: `Response $${refMatch[1]} does not exist (only ${responses.length} responses so far)`
12229
12230
  };
12230
12231
  }
12231
- const value = getValueByPath2(responses[responseIndex], path4);
12232
- return { value };
12232
+ const response = responses[responseIndex];
12233
+ const value = getValueByPath2(response, path4);
12234
+ return {
12235
+ value,
12236
+ responseIndex: responseIdx,
12237
+ response,
12238
+ jsonPath: path4,
12239
+ variableName: responseIndexToVariable?.get(responseIdx)
12240
+ };
12233
12241
  }
12234
12242
  if (/^\$\d+$/.test(trimmed)) {
12235
12243
  return {
@@ -12246,14 +12254,20 @@ function resolveValue(expr, responses, variables, getValueByPath2) {
12246
12254
  if (typeof varValue === "object" && varValue !== null) {
12247
12255
  const path4 = pathPart.replace(/^\./, "");
12248
12256
  const value = getNestedValue2(varValue, path4);
12249
- return { value };
12257
+ const isHttpResponse = "status" in varValue && "body" in varValue;
12258
+ return {
12259
+ value,
12260
+ variableName: varName,
12261
+ jsonPath: path4,
12262
+ response: isHttpResponse ? varValue : void 0
12263
+ };
12250
12264
  }
12251
12265
  if (typeof varValue === "string") {
12252
12266
  try {
12253
12267
  const parsed = JSON.parse(varValue);
12254
12268
  const path4 = pathPart.replace(/^\./, "");
12255
12269
  const value = getNestedValue2(parsed, path4);
12256
- return { value };
12270
+ return { value, variableName: varName, jsonPath: path4 };
12257
12271
  } catch {
12258
12272
  return { value: void 0, error: `Cannot access path on non-object variable: ${varName}` };
12259
12273
  }
@@ -12317,8 +12331,14 @@ function getNestedValue2(obj, path4) {
12317
12331
  }
12318
12332
  return current;
12319
12333
  }
12320
- function evaluateAssertion(assertion, responses, variables, getValueByPath2) {
12321
- const leftResult = resolveValue(assertion.leftExpr, responses, variables, getValueByPath2);
12334
+ function evaluateAssertion(assertion, responses, variables, getValueByPath2, responseIndexToVariable) {
12335
+ const leftResult = resolveValue(assertion.leftExpr, responses, variables, getValueByPath2, responseIndexToVariable);
12336
+ const buildFailureContext = () => ({
12337
+ responseIndex: leftResult.responseIndex,
12338
+ friendlyName: leftResult.variableName,
12339
+ relatedResponse: leftResult.response,
12340
+ jsonPath: leftResult.jsonPath
12341
+ });
12322
12342
  if (leftResult.error) {
12323
12343
  return {
12324
12344
  passed: false,
@@ -12328,28 +12348,33 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2) {
12328
12348
  leftValue: void 0,
12329
12349
  leftExpression: assertion.leftExpr,
12330
12350
  rightExpression: assertion.rightExpr,
12331
- error: leftResult.error
12351
+ error: leftResult.error,
12352
+ ...buildFailureContext()
12332
12353
  };
12333
12354
  }
12334
12355
  const leftValue = leftResult.value;
12335
12356
  if (assertion.operator === "exists") {
12357
+ const passed2 = leftValue !== void 0 && leftValue !== null;
12336
12358
  return {
12337
- passed: leftValue !== void 0 && leftValue !== null,
12359
+ passed: passed2,
12338
12360
  expression: formatExpression(assertion),
12339
12361
  message: assertion.message,
12340
12362
  operator: assertion.operator,
12341
12363
  leftValue,
12342
- leftExpression: assertion.leftExpr
12364
+ leftExpression: assertion.leftExpr,
12365
+ ...!passed2 ? buildFailureContext() : {}
12343
12366
  };
12344
12367
  }
12345
12368
  if (assertion.operator === "!exists") {
12369
+ const passed2 = leftValue === void 0 || leftValue === null;
12346
12370
  return {
12347
- passed: leftValue === void 0 || leftValue === null,
12371
+ passed: passed2,
12348
12372
  expression: formatExpression(assertion),
12349
12373
  message: assertion.message,
12350
12374
  operator: assertion.operator,
12351
12375
  leftValue,
12352
- leftExpression: assertion.leftExpr
12376
+ leftExpression: assertion.leftExpr,
12377
+ ...!passed2 ? buildFailureContext() : {}
12353
12378
  };
12354
12379
  }
12355
12380
  if (!assertion.rightExpr) {
@@ -12360,7 +12385,8 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2) {
12360
12385
  operator: assertion.operator,
12361
12386
  leftValue,
12362
12387
  leftExpression: assertion.leftExpr,
12363
- error: `Operator ${assertion.operator} requires a right-hand value`
12388
+ error: `Operator ${assertion.operator} requires a right-hand value`,
12389
+ ...buildFailureContext()
12364
12390
  };
12365
12391
  }
12366
12392
  if (assertion.operator === "isType") {
@@ -12373,18 +12399,20 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2) {
12373
12399
  } else {
12374
12400
  actualType = typeof leftValue;
12375
12401
  }
12402
+ const passed2 = actualType === expectedType;
12376
12403
  return {
12377
- passed: actualType === expectedType,
12404
+ passed: passed2,
12378
12405
  expression: formatExpression(assertion),
12379
12406
  message: assertion.message,
12380
12407
  operator: assertion.operator,
12381
12408
  leftValue,
12382
12409
  rightValue: expectedType,
12383
12410
  leftExpression: assertion.leftExpr,
12384
- rightExpression: assertion.rightExpr
12411
+ rightExpression: assertion.rightExpr,
12412
+ ...!passed2 ? buildFailureContext() : {}
12385
12413
  };
12386
12414
  }
12387
- const rightResult = resolveValue(assertion.rightExpr, responses, variables, getValueByPath2);
12415
+ const rightResult = resolveValue(assertion.rightExpr, responses, variables, getValueByPath2, responseIndexToVariable);
12388
12416
  if (rightResult.error) {
12389
12417
  return {
12390
12418
  passed: false,
@@ -12395,7 +12423,8 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2) {
12395
12423
  rightValue: void 0,
12396
12424
  leftExpression: assertion.leftExpr,
12397
12425
  rightExpression: assertion.rightExpr,
12398
- error: rightResult.error
12426
+ error: rightResult.error,
12427
+ ...buildFailureContext()
12399
12428
  };
12400
12429
  }
12401
12430
  const rightValue = rightResult.value;
@@ -12449,7 +12478,8 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2) {
12449
12478
  rightValue,
12450
12479
  leftExpression: assertion.leftExpr,
12451
12480
  rightExpression: assertion.rightExpr,
12452
- error: `Invalid regex pattern: ${rightValue}`
12481
+ error: `Invalid regex pattern: ${rightValue}`,
12482
+ ...buildFailureContext()
12453
12483
  };
12454
12484
  }
12455
12485
  break;
@@ -12462,7 +12492,8 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2) {
12462
12492
  leftValue,
12463
12493
  rightValue,
12464
12494
  leftExpression: assertion.leftExpr,
12465
- rightExpression: assertion.rightExpr
12495
+ rightExpression: assertion.rightExpr,
12496
+ ...!passed ? buildFailureContext() : {}
12466
12497
  };
12467
12498
  }
12468
12499
  function formatExpression(assertion) {
@@ -20995,6 +21026,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
20995
21026
  responses.push(response);
20996
21027
  runtimeVariables[`$${responses.length}`] = response;
20997
21028
  };
21029
+ const responseIndexToVariable = /* @__PURE__ */ new Map();
20998
21030
  const steps = extractStepsFromSequence(sequenceContent);
20999
21031
  const captures = extractCaptureDirectives(sequenceContent);
21000
21032
  const totalSteps = steps.length;
@@ -21061,7 +21093,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
21061
21093
  duration: Date.now() - startTime
21062
21094
  };
21063
21095
  }
21064
- const result = evaluateAssertion(parsed, responses, runtimeVariables, getValueByPath);
21096
+ const result = evaluateAssertion(parsed, responses, runtimeVariables, getValueByPath, responseIndexToVariable);
21065
21097
  assertionResults.push(result);
21066
21098
  const stepResult = {
21067
21099
  type: "assertion",
@@ -21358,13 +21390,15 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
21358
21390
  const requestParsed = parserHttpRequest(requestText, runtimeVariables);
21359
21391
  const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar) : await sendRequest(requestParsed);
21360
21392
  addResponse(response);
21393
+ responseIndexToVariable.set(responses.length, parsed.varName);
21361
21394
  runtimeVariables[parsed.varName] = response;
21362
21395
  const stepResult = {
21363
21396
  type: "request",
21364
21397
  stepIndex: stepIdx,
21365
21398
  response,
21366
21399
  requestMethod: parsed.method,
21367
- requestUrl: resolvedUrl
21400
+ requestUrl: resolvedUrl,
21401
+ variableName: parsed.varName
21368
21402
  };
21369
21403
  orderedSteps.push(stepResult);
21370
21404
  reportProgress(stepIdx, "request", `var ${parsed.varName} = ${requestDescription}`, stepResult);
@@ -22297,7 +22331,11 @@ function formatAssertion(assertion, options) {
22297
22331
  const { colors, verbose } = options;
22298
22332
  const lines = [];
22299
22333
  const icon = assertion.passed ? colors.checkmark : colors.cross;
22300
- const displayMessage = assertion.message || assertion.expression;
22334
+ let displayExpr = assertion.expression;
22335
+ if (assertion.friendlyName && assertion.responseIndex) {
22336
+ displayExpr = displayExpr.replace("$" + assertion.responseIndex, assertion.friendlyName);
22337
+ }
22338
+ const displayMessage = assertion.message || displayExpr;
22301
22339
  lines.push(`${icon} assert ${displayMessage}`);
22302
22340
  if (!assertion.passed || verbose) {
22303
22341
  if (assertion.error) {
@@ -22639,7 +22677,7 @@ function formatTimestamp() {
22639
22677
  return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
22640
22678
  }
22641
22679
  function generateHtmlReport(results, options) {
22642
- const { outputPath, redaction, title = "Norn Test Report" } = options;
22680
+ const { outputPath, redaction, title = "Norn Test Report", environment } = options;
22643
22681
  const totalSequences = results.length;
22644
22682
  const passedSequences = results.filter((r) => r.success).length;
22645
22683
  const failedSequences = totalSequences - passedSequences;
@@ -22648,6 +22686,11 @@ function generateHtmlReport(results, options) {
22648
22686
  const failedAssertions = totalAssertions - passedAssertions;
22649
22687
  const totalRequests = results.reduce((sum, r) => sum + r.steps.filter((s) => s.type === "request").length, 0);
22650
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;
22651
22694
  const html = `<!DOCTYPE html>
22652
22695
  <html lang="en">
22653
22696
  <head>
@@ -22662,23 +22705,32 @@ function generateHtmlReport(results, options) {
22662
22705
  <div class="container">
22663
22706
  <header>
22664
22707
  <h1>${escapeHtml(title)}</h1>
22665
- <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>
22666
22712
  </header>
22667
22713
 
22668
22714
  <section class="summary">
22669
- <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>
22670
22722
  <div class="stats-grid">
22671
- <div class="stat-card ${failedSequences > 0 ? "has-failures" : "all-passed"}">
22672
- <div class="stat-value">${passedSequences}/${totalSequences}</div>
22673
- <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>
22674
22726
  </div>
22675
- <div class="stat-card ${failedAssertions > 0 ? "has-failures" : "all-passed"}">
22676
- <div class="stat-value">${passedAssertions}/${totalAssertions}</div>
22677
- <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>
22678
22730
  </div>
22679
- <div class="stat-card">
22680
- <div class="stat-value">${totalRequests}</div>
22681
- <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>
22682
22734
  </div>
22683
22735
  <div class="stat-card">
22684
22736
  <div class="stat-value">${formatDuration2(totalDuration)}</div>
@@ -22692,9 +22744,29 @@ function generateHtmlReport(results, options) {
22692
22744
  </div>
22693
22745
  </section>
22694
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
+
22695
22762
  <section class="results">
22696
22763
  <h2>Test Results</h2>
22697
- ${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>
22698
22770
  </section>
22699
22771
  </div>
22700
22772
 
@@ -22759,11 +22831,13 @@ function generateRequestHtml(step, seqIndex, reqIndex, redaction) {
22759
22831
  const statusClass = isSuccess ? "passed" : "failed";
22760
22832
  const method = step.requestMethod || "REQUEST";
22761
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>` : "";
22762
22835
  const bodyHtml = response.body ? generateBodyHtml(response.body, redaction) : "<em>No body</em>";
22763
22836
  const headersHtml = response.headers ? generateHeadersHtml(response.headers, redaction) : "";
22764
22837
  return `
22765
22838
  <div class="step request ${statusClass}">
22766
22839
  <div class="step-header" onclick="toggleStep('step-${seqIndex}-${reqIndex}')">
22840
+ ${varLabel}
22767
22841
  <span class="method">${escapeHtml(method)}</span>
22768
22842
  <span class="url">${escapeHtml(url2)}</span>
22769
22843
  <span class="status-code">${response.status} ${escapeHtml(response.statusText)}</span>
@@ -22802,20 +22876,32 @@ function generateAssertionHtml(step, redaction) {
22802
22876
  const assertion = step.assertion;
22803
22877
  const statusClass = assertion.passed ? "passed" : "failed";
22804
22878
  const statusIcon = assertion.passed ? "\u2713" : "\u2717";
22805
- const displayText = assertion.message || assertion.expression;
22879
+ const displayText = assertion.friendlyName || assertion.message || assertion.expression;
22806
22880
  let detailsHtml = "";
22807
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>` : "";
22808
22893
  detailsHtml = `
22809
22894
  <div class="assertion-details">
22810
- <div><strong>Expected:</strong> ${escapeHtml(String(assertion.rightExpression || assertion.rightValue))}</div>
22811
- <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>
22812
22898
  ${assertion.error ? `<div class="error"><strong>Error:</strong> ${escapeHtml(redactString(assertion.error, redaction))}</div>` : ""}
22813
22899
  </div>`;
22814
22900
  }
22815
22901
  return `
22816
22902
  <div class="step assertion ${statusClass}">
22817
22903
  <span class="status-icon">${statusIcon}</span>
22818
- <span class="assertion-text">assert ${escapeHtml(displayText)}</span>
22904
+ <span class="assertion-text">${escapeHtml(displayText)}</span>
22819
22905
  ${detailsHtml}
22820
22906
  </div>`;
22821
22907
  }
@@ -22855,17 +22941,48 @@ function getEmbeddedCSS() {
22855
22941
  .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
22856
22942
  header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #333; }
22857
22943
  header h1 { color: #fff; font-size: 2em; margin-bottom: 10px; }
22944
+ .header-meta { display: flex; justify-content: center; gap: 20px; flex-wrap: wrap; }
22858
22945
  .timestamp { color: #888; font-size: 0.9em; }
22946
+ .environment { color: #888; font-size: 0.9em; }
22947
+ .environment strong { color: #569cd6; }
22859
22948
  h2 { color: #fff; margin-bottom: 15px; font-size: 1.4em; }
22860
22949
 
22861
- .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
+
22862
22962
  .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; }
22863
- .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); }
22864
22966
  .stat-card.all-passed { border-left: 4px solid #4ec9b0; }
22865
22967
  .stat-card.has-failures { border-left: 4px solid #f14c4c; }
22866
22968
  .stat-value { font-size: 2em; font-weight: bold; color: #fff; }
22867
22969
  .stat-label { color: #888; font-size: 0.85em; margin-top: 5px; }
22868
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
+
22869
22986
  .progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; }
22870
22987
  .progress-fill { height: 100%; transition: width 0.3s; }
22871
22988
  .progress-fill.all-passed { background: #4ec9b0; }
@@ -22902,6 +23019,8 @@ function getEmbeddedCSS() {
22902
23019
 
22903
23020
  .step-header { display: flex; align-items: center; gap: 10px; cursor: pointer; }
22904
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; }
22905
23024
  .method { background: #264f78; color: #fff; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; font-weight: 600; }
22906
23025
  .url { flex: 1; color: #9cdcfe; font-family: monospace; font-size: 0.9em; word-break: break-all; }
22907
23026
  .status-code { font-weight: 600; }
@@ -22921,6 +23040,9 @@ function getEmbeddedCSS() {
22921
23040
  .assertion-text { font-family: monospace; }
22922
23041
  .assertion-details { width: 100%; margin-top: 8px; padding: 10px; background: #1a1a1a; border-radius: 4px; font-size: 0.9em; }
22923
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; }
22924
23046
 
22925
23047
  .print-icon { font-size: 1em; }
22926
23048
  .print-text { color: #dcdcaa; }
@@ -22937,6 +23059,8 @@ function getEmbeddedCSS() {
22937
23059
  }
22938
23060
  function getEmbeddedJS() {
22939
23061
  return `
23062
+ let currentFilter = 'all';
23063
+
22940
23064
  function toggleSequence(index) {
22941
23065
  const body = document.getElementById('sequence-body-' + index);
22942
23066
  const sequence = body.closest('.sequence');
@@ -22962,6 +23086,69 @@ function getEmbeddedJS() {
22962
23086
  }
22963
23087
  }
22964
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
+
22965
23152
  // Expand all failed sequences by default
22966
23153
  document.querySelectorAll('.sequence.failed').forEach((seq, i) => {
22967
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.3",
5
+ "version": "1.2.5",
6
6
  "publisher": "Norn-PeterKrustanov",
7
7
  "author": {
8
8
  "name": "Peter Krastanov"