norn-cli 1.2.3 → 1.2.4
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 +11 -0
- package/README.md +21 -21
- package/dist/cli.js +61 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.4] - 2026-02-07
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
- **Enhanced failure experience**: Failed assertions now display much richer context
|
|
9
|
+
- **Friendly variable names**: Shows `user.body.status` instead of `$1.body.status` when response was assigned to a variable
|
|
10
|
+
- **Auto-expand failures**: Failed assertions automatically expand to show details
|
|
11
|
+
- **Inline response preview**: Failed assertions include a collapsible preview of the related response body
|
|
12
|
+
- **JSON path highlighting**: The failing field is highlighted in the response preview
|
|
13
|
+
- **Request labels**: Request steps in sequences show variable names (e.g., `user`) instead of `$1`
|
|
14
|
+
- Same improvements apply to CLI output and Test Explorer
|
|
15
|
+
|
|
5
16
|
## [1.2.3] - 2026-02-07
|
|
6
17
|
|
|
7
18
|
### 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) {
|
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.4",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|