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 +25 -0
- package/README.md +21 -21
- package/dist/cli.js +229 -42
- package/package.json +1 -1
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
|
|
222
|
-
print 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
|
|
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
|
|
12232
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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">${
|
|
22673
|
-
<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>
|
|
22674
22726
|
</div>
|
|
22675
|
-
<div class="stat-card ${
|
|
22676
|
-
<div class="stat-value">${
|
|
22677
|
-
<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>
|
|
22678
22730
|
</div>
|
|
22679
|
-
<div class="stat-card">
|
|
22680
|
-
<div class="stat-value">${
|
|
22681
|
-
<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>
|
|
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
|
-
|
|
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
|
-
|
|
22811
|
-
<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>
|
|
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"
|
|
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:
|
|
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.
|
|
5
|
+
"version": "1.2.5",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|