norn-cli 1.2.2 → 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 +27 -0
- package/README.md +21 -21
- package/dist/cli.js +171 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
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
|
+
|
|
16
|
+
## [1.2.3] - 2026-02-07
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Duplicate import detection**: Errors shown when importing files with duplicate definitions
|
|
20
|
+
- Header groups and endpoints from `.nornapi` files
|
|
21
|
+
- Named requests and sequences from `.norn` files
|
|
22
|
+
- Blocks execution in CLI, VS Code, and Test Explorer
|
|
23
|
+
- Red squiggly on import line with descriptive error message
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **.nornenv validation**: `secret var = value` now shows error (use `secret name = value` instead)
|
|
27
|
+
- Syntax highlighting marks invalid pattern in red
|
|
28
|
+
- Diagnostic message explains correct usage
|
|
29
|
+
- **IntelliSense context**: Script types (`bash`, `powershell`, `js`, `readJson`) only appear after `run` keyword
|
|
30
|
+
- **var keyword highlighting**: `var` keyword stays colored while typing variable name
|
|
31
|
+
|
|
5
32
|
## [1.2.2] - 2026-02-05
|
|
6
33
|
|
|
7
34
|
### 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) {
|
|
@@ -13493,6 +13524,10 @@ async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__
|
|
|
13493
13524
|
const resolvedPaths = [];
|
|
13494
13525
|
const headerGroups = [];
|
|
13495
13526
|
const endpoints = [];
|
|
13527
|
+
const headerGroupSources = /* @__PURE__ */ new Map();
|
|
13528
|
+
const endpointSources = /* @__PURE__ */ new Map();
|
|
13529
|
+
const namedRequestSources = /* @__PURE__ */ new Map();
|
|
13530
|
+
const sequenceSources = /* @__PURE__ */ new Map();
|
|
13496
13531
|
for (const imp of imports) {
|
|
13497
13532
|
const path4 = await import("path");
|
|
13498
13533
|
const absolutePath = path4.resolve(baseDir, imp.path);
|
|
@@ -13510,16 +13545,64 @@ async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__
|
|
|
13510
13545
|
resolvedPaths.push(absolutePath);
|
|
13511
13546
|
if (imp.path.endsWith(".nornapi")) {
|
|
13512
13547
|
const apiDef = parseNornApiFile(content);
|
|
13513
|
-
|
|
13514
|
-
|
|
13548
|
+
for (const group of apiDef.headerGroups) {
|
|
13549
|
+
const existingSource = headerGroupSources.get(group.name);
|
|
13550
|
+
if (existingSource) {
|
|
13551
|
+
errors.push({
|
|
13552
|
+
path: imp.path,
|
|
13553
|
+
error: `Duplicate header group '${group.name}': already defined in '${existingSource}'`,
|
|
13554
|
+
lineNumber: imp.lineNumber
|
|
13555
|
+
});
|
|
13556
|
+
} else {
|
|
13557
|
+
headerGroupSources.set(group.name, imp.path);
|
|
13558
|
+
headerGroups.push(group);
|
|
13559
|
+
}
|
|
13560
|
+
}
|
|
13561
|
+
for (const endpoint of apiDef.endpoints) {
|
|
13562
|
+
const existingSource = endpointSources.get(endpoint.name);
|
|
13563
|
+
if (existingSource) {
|
|
13564
|
+
errors.push({
|
|
13565
|
+
path: imp.path,
|
|
13566
|
+
error: `Duplicate endpoint '${endpoint.name}': already defined in '${existingSource}'`,
|
|
13567
|
+
lineNumber: imp.lineNumber
|
|
13568
|
+
});
|
|
13569
|
+
} else {
|
|
13570
|
+
endpointSources.set(endpoint.name, imp.path);
|
|
13571
|
+
endpoints.push(endpoint);
|
|
13572
|
+
}
|
|
13573
|
+
}
|
|
13515
13574
|
continue;
|
|
13516
13575
|
}
|
|
13517
13576
|
const importDir = path4.dirname(absolutePath);
|
|
13518
13577
|
const nestedResult = await resolveImports(content, importDir, readFile2, alreadyImported);
|
|
13519
13578
|
errors.push(...nestedResult.errors);
|
|
13520
13579
|
resolvedPaths.push(...nestedResult.resolvedPaths);
|
|
13521
|
-
|
|
13522
|
-
|
|
13580
|
+
for (const group of nestedResult.headerGroups) {
|
|
13581
|
+
const existingSource = headerGroupSources.get(group.name);
|
|
13582
|
+
if (existingSource) {
|
|
13583
|
+
errors.push({
|
|
13584
|
+
path: imp.path,
|
|
13585
|
+
error: `Duplicate header group '${group.name}': already defined in '${existingSource}'`,
|
|
13586
|
+
lineNumber: imp.lineNumber
|
|
13587
|
+
});
|
|
13588
|
+
} else {
|
|
13589
|
+
headerGroupSources.set(group.name, imp.path);
|
|
13590
|
+
headerGroups.push(group);
|
|
13591
|
+
}
|
|
13592
|
+
}
|
|
13593
|
+
for (const endpoint of nestedResult.endpoints) {
|
|
13594
|
+
const existingSource = endpointSources.get(endpoint.name);
|
|
13595
|
+
if (existingSource) {
|
|
13596
|
+
errors.push({
|
|
13597
|
+
path: imp.path,
|
|
13598
|
+
error: `Duplicate endpoint '${endpoint.name}': already defined in '${existingSource}'`,
|
|
13599
|
+
lineNumber: imp.lineNumber
|
|
13600
|
+
});
|
|
13601
|
+
} else {
|
|
13602
|
+
endpointSources.set(endpoint.name, imp.path);
|
|
13603
|
+
endpoints.push(endpoint);
|
|
13604
|
+
}
|
|
13605
|
+
}
|
|
13523
13606
|
if (nestedResult.importedContent) {
|
|
13524
13607
|
importedContents.push(nestedResult.importedContent);
|
|
13525
13608
|
}
|
|
@@ -13527,15 +13610,37 @@ async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__
|
|
|
13527
13610
|
const importedNamedRequests = extractNamedRequests(content);
|
|
13528
13611
|
const importedSequences = extractSequencesFromText(content);
|
|
13529
13612
|
for (const req of importedNamedRequests) {
|
|
13530
|
-
const
|
|
13531
|
-
|
|
13613
|
+
const lowerName = req.name.toLowerCase();
|
|
13614
|
+
const existingSource = namedRequestSources.get(lowerName);
|
|
13615
|
+
if (existingSource) {
|
|
13616
|
+
errors.push({
|
|
13617
|
+
path: imp.path,
|
|
13618
|
+
error: `Duplicate named request '${req.name}': already defined in '${existingSource}'`,
|
|
13619
|
+
lineNumber: imp.lineNumber
|
|
13620
|
+
});
|
|
13621
|
+
} else {
|
|
13622
|
+
namedRequestSources.set(lowerName, imp.path);
|
|
13623
|
+
const resolvedContent = substituteVariables(req.content, importedVariables);
|
|
13624
|
+
importedContents.push(`[${req.name}]
|
|
13532
13625
|
${resolvedContent}`);
|
|
13626
|
+
}
|
|
13533
13627
|
}
|
|
13534
13628
|
for (const seq of importedSequences) {
|
|
13535
|
-
const
|
|
13536
|
-
|
|
13629
|
+
const lowerName = seq.name.toLowerCase();
|
|
13630
|
+
const existingSource = sequenceSources.get(lowerName);
|
|
13631
|
+
if (existingSource) {
|
|
13632
|
+
errors.push({
|
|
13633
|
+
path: imp.path,
|
|
13634
|
+
error: `Duplicate sequence '${seq.name}': already defined in '${existingSource}'`,
|
|
13635
|
+
lineNumber: imp.lineNumber
|
|
13636
|
+
});
|
|
13637
|
+
} else {
|
|
13638
|
+
sequenceSources.set(lowerName, imp.path);
|
|
13639
|
+
const resolvedContent = substituteVariables(seq.content, importedVariables);
|
|
13640
|
+
importedContents.push(`sequence ${seq.name}
|
|
13537
13641
|
${resolvedContent}
|
|
13538
13642
|
end sequence`);
|
|
13643
|
+
}
|
|
13539
13644
|
}
|
|
13540
13645
|
} catch (error) {
|
|
13541
13646
|
errors.push({
|
|
@@ -20921,6 +21026,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
20921
21026
|
responses.push(response);
|
|
20922
21027
|
runtimeVariables[`$${responses.length}`] = response;
|
|
20923
21028
|
};
|
|
21029
|
+
const responseIndexToVariable = /* @__PURE__ */ new Map();
|
|
20924
21030
|
const steps = extractStepsFromSequence(sequenceContent);
|
|
20925
21031
|
const captures = extractCaptureDirectives(sequenceContent);
|
|
20926
21032
|
const totalSteps = steps.length;
|
|
@@ -20987,7 +21093,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
20987
21093
|
duration: Date.now() - startTime
|
|
20988
21094
|
};
|
|
20989
21095
|
}
|
|
20990
|
-
const result = evaluateAssertion(parsed, responses, runtimeVariables, getValueByPath);
|
|
21096
|
+
const result = evaluateAssertion(parsed, responses, runtimeVariables, getValueByPath, responseIndexToVariable);
|
|
20991
21097
|
assertionResults.push(result);
|
|
20992
21098
|
const stepResult = {
|
|
20993
21099
|
type: "assertion",
|
|
@@ -21284,13 +21390,15 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21284
21390
|
const requestParsed = parserHttpRequest(requestText, runtimeVariables);
|
|
21285
21391
|
const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar) : await sendRequest(requestParsed);
|
|
21286
21392
|
addResponse(response);
|
|
21393
|
+
responseIndexToVariable.set(responses.length, parsed.varName);
|
|
21287
21394
|
runtimeVariables[parsed.varName] = response;
|
|
21288
21395
|
const stepResult = {
|
|
21289
21396
|
type: "request",
|
|
21290
21397
|
stepIndex: stepIdx,
|
|
21291
21398
|
response,
|
|
21292
21399
|
requestMethod: parsed.method,
|
|
21293
|
-
requestUrl: resolvedUrl
|
|
21400
|
+
requestUrl: resolvedUrl,
|
|
21401
|
+
variableName: parsed.varName
|
|
21294
21402
|
};
|
|
21295
21403
|
orderedSteps.push(stepResult);
|
|
21296
21404
|
reportProgress(stepIdx, "request", `var ${parsed.varName} = ${requestDescription}`, stepResult);
|
|
@@ -22223,7 +22331,11 @@ function formatAssertion(assertion, options) {
|
|
|
22223
22331
|
const { colors, verbose } = options;
|
|
22224
22332
|
const lines = [];
|
|
22225
22333
|
const icon = assertion.passed ? colors.checkmark : colors.cross;
|
|
22226
|
-
|
|
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;
|
|
22227
22339
|
lines.push(`${icon} assert ${displayMessage}`);
|
|
22228
22340
|
if (!assertion.passed || verbose) {
|
|
22229
22341
|
if (assertion.error) {
|
|
@@ -23403,9 +23515,22 @@ async function main() {
|
|
|
23403
23515
|
workingDir,
|
|
23404
23516
|
async (importPath) => fsPromises.readFile(importPath, "utf-8")
|
|
23405
23517
|
);
|
|
23406
|
-
|
|
23518
|
+
const duplicateErrors = importResult.errors.filter(
|
|
23519
|
+
(err) => err.error.includes("Duplicate header group") || err.error.includes("Duplicate endpoint") || err.error.includes("Duplicate named request") || err.error.includes("Duplicate sequence")
|
|
23520
|
+
);
|
|
23521
|
+
const otherErrors = importResult.errors.filter(
|
|
23522
|
+
(err) => !err.error.includes("Duplicate header group") && !err.error.includes("Duplicate endpoint") && !err.error.includes("Duplicate named request") && !err.error.includes("Duplicate sequence")
|
|
23523
|
+
);
|
|
23524
|
+
for (const err of otherErrors) {
|
|
23407
23525
|
console.error(colors.warning(`Import warning: ${err.path} - ${err.error}`));
|
|
23408
23526
|
}
|
|
23527
|
+
if (duplicateErrors.length > 0) {
|
|
23528
|
+
console.error(colors.error("Cannot execute - duplicate definitions found:"));
|
|
23529
|
+
for (const err of duplicateErrors) {
|
|
23530
|
+
console.error(colors.error(` ${err.path}: ${err.error}`));
|
|
23531
|
+
}
|
|
23532
|
+
process.exit(1);
|
|
23533
|
+
}
|
|
23409
23534
|
const fileContentWithImports = importResult.importedContent ? `${importResult.importedContent}
|
|
23410
23535
|
|
|
23411
23536
|
${fileContent}` : fileContent;
|
|
@@ -23512,9 +23637,22 @@ ${fileContent}` : fileContent;
|
|
|
23512
23637
|
workingDir,
|
|
23513
23638
|
async (importPath) => fsPromises.readFile(importPath, "utf-8")
|
|
23514
23639
|
);
|
|
23515
|
-
|
|
23640
|
+
const duplicateErrors2 = importResult.errors.filter(
|
|
23641
|
+
(err) => err.error.includes("Duplicate header group") || err.error.includes("Duplicate endpoint") || err.error.includes("Duplicate named request") || err.error.includes("Duplicate sequence")
|
|
23642
|
+
);
|
|
23643
|
+
const otherErrors2 = importResult.errors.filter(
|
|
23644
|
+
(err) => !err.error.includes("Duplicate header group") && !err.error.includes("Duplicate endpoint") && !err.error.includes("Duplicate named request") && !err.error.includes("Duplicate sequence")
|
|
23645
|
+
);
|
|
23646
|
+
for (const err of otherErrors2) {
|
|
23516
23647
|
console.error(colors.warning(`Import warning: ${err.path} - ${err.error}`));
|
|
23517
23648
|
}
|
|
23649
|
+
if (duplicateErrors2.length > 0) {
|
|
23650
|
+
console.error(colors.error("Cannot execute - duplicate definitions found:"));
|
|
23651
|
+
for (const err of duplicateErrors2) {
|
|
23652
|
+
console.error(colors.error(` ${err.path}: ${err.error}`));
|
|
23653
|
+
}
|
|
23654
|
+
process.exit(1);
|
|
23655
|
+
}
|
|
23518
23656
|
const fileContentWithImports = importResult.importedContent ? `${importResult.importedContent}
|
|
23519
23657
|
|
|
23520
23658
|
${fileContent}` : fileContent;
|
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"
|