norn-cli 1.3.1 → 1.3.2
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 +15 -0
- package/README.md +33 -0
- package/dist/cli.js +172 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.2] - 2026-02-08
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
- **Contract Report View**: Enhanced property highlighting in the annotated JSON view
|
|
9
|
+
- Color-coded property highlights (green=valid, yellow=warning, red=error)
|
|
10
|
+
- Highlights wrap only the property content, not the entire line
|
|
11
|
+
- Selection-style highlighting similar to assert hover
|
|
12
|
+
- Improved indentation and spacing
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Syntax Highlighting**: Fixed JSON body coloring after nested objects
|
|
16
|
+
- Properties following `{ }` objects now correctly colored
|
|
17
|
+
- **matchesSchema Keyword**: Fixed partial coloring issue
|
|
18
|
+
- Entire keyword now highlights as operator (was matching "matches" first)
|
|
19
|
+
|
|
5
20
|
## [1.3.1] - 2026-02-08
|
|
6
21
|
|
|
7
22
|
- **Clickable JSON Properties**: Click any JSON value in the response panel to generate an assertion
|
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ A powerful REST client extension for VS Code with sequences, assertions, environ
|
|
|
28
28
|
- **Script Execution**: Run bash, PowerShell, or JavaScript scripts within sequences
|
|
29
29
|
- **Print Statements**: Debug and log messages during sequence execution
|
|
30
30
|
- **Cookie Support**: Automatic cookie jar with persistence across requests
|
|
31
|
+
- **Response Comparison**: Compare any two API responses side-by-side with VS Code's diff view
|
|
32
|
+
- **Clickable JSON**: Click any JSON value in the response panel to auto-generate assertions
|
|
31
33
|
- **Syntax Highlighting**: Full syntax highlighting for requests, headers, JSON bodies
|
|
32
34
|
- **IntelliSense**: Autocomplete for HTTP methods, headers, variables, and keywords
|
|
33
35
|
- **Diagnostics**: Error highlighting for undefined variables
|
|
@@ -742,6 +744,37 @@ end sequence
|
|
|
742
744
|
|
|
743
745
|
Use `print "Title" | "Body content"` for expandable messages in the result view.
|
|
744
746
|
|
|
747
|
+
### Response Panel Tools
|
|
748
|
+
|
|
749
|
+
The response panel provides interactive tools that accelerate test development:
|
|
750
|
+
|
|
751
|
+
#### Response Comparison
|
|
752
|
+
|
|
753
|
+
Compare any two API responses side-by-side using VS Code's built-in diff view:
|
|
754
|
+
|
|
755
|
+
1. Click **⇄ Compare** on the first response body
|
|
756
|
+
2. Click **⇄ Compare** on the second response
|
|
757
|
+
3. VS Code opens a diff view showing differences between the two responses
|
|
758
|
+
|
|
759
|
+
This is useful for comparing responses across environments, before/after changes, or expected vs actual results.
|
|
760
|
+
|
|
761
|
+
#### Clickable JSON Values
|
|
762
|
+
|
|
763
|
+
Generate assertions instantly by clicking on JSON values in the response:
|
|
764
|
+
|
|
765
|
+
1. Expand a response body in the panel
|
|
766
|
+
2. Hover over any value (string, number, boolean, null) - it highlights with a **+** badge
|
|
767
|
+
3. Click to automatically insert an assertion at the end of your sequence
|
|
768
|
+
|
|
769
|
+
For example, clicking on `"active"` in `{"status": "active"}` generates:
|
|
770
|
+
```bash
|
|
771
|
+
assert $response.body.status == "active"
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
Works with nested paths and arrays:
|
|
775
|
+
- `body.user.email` → `assert $user.body.user.email == "john@example.com"`
|
|
776
|
+
- `body.items[0].id` → `assert $order.body.items[0].id == 123`
|
|
777
|
+
|
|
745
778
|
## CLI Usage
|
|
746
779
|
|
|
747
780
|
Run tests from the command line for CI/CD pipelines. Only sequences marked with `test` are executed.
|
package/dist/cli.js
CHANGED
|
@@ -18582,16 +18582,136 @@ var require_ajv = __commonJS({
|
|
|
18582
18582
|
});
|
|
18583
18583
|
|
|
18584
18584
|
// src/schemaGenerator.ts
|
|
18585
|
-
function
|
|
18586
|
-
|
|
18587
|
-
|
|
18588
|
-
if (
|
|
18589
|
-
|
|
18585
|
+
function resolveSchemaPath(schemaPath, basePath, workspaceRoot) {
|
|
18586
|
+
if (schemaPath.startsWith(PATH_ALIAS_PREFIX)) {
|
|
18587
|
+
const relativePath = schemaPath.slice(PATH_ALIAS_PREFIX.length);
|
|
18588
|
+
if (workspaceRoot) {
|
|
18589
|
+
return path2.resolve(workspaceRoot, DEFAULT_CONTRACTS_FOLDER, relativePath);
|
|
18590
|
+
}
|
|
18591
|
+
if (basePath) {
|
|
18592
|
+
let currentDir = basePath;
|
|
18593
|
+
let attempts = 0;
|
|
18594
|
+
while (attempts < 10) {
|
|
18595
|
+
const contractsPath = path2.join(currentDir, DEFAULT_CONTRACTS_FOLDER, relativePath);
|
|
18596
|
+
if (fs.existsSync(path2.dirname(contractsPath))) {
|
|
18597
|
+
return contractsPath;
|
|
18598
|
+
}
|
|
18599
|
+
const parentDir = path2.dirname(currentDir);
|
|
18600
|
+
if (parentDir === currentDir) break;
|
|
18601
|
+
currentDir = parentDir;
|
|
18602
|
+
attempts++;
|
|
18603
|
+
}
|
|
18604
|
+
}
|
|
18605
|
+
if (basePath) {
|
|
18606
|
+
return path2.resolve(basePath, DEFAULT_CONTRACTS_FOLDER, relativePath);
|
|
18607
|
+
}
|
|
18608
|
+
return schemaPath;
|
|
18609
|
+
}
|
|
18610
|
+
if (path2.isAbsolute(schemaPath)) {
|
|
18611
|
+
return schemaPath;
|
|
18612
|
+
}
|
|
18613
|
+
if (basePath) {
|
|
18614
|
+
return path2.resolve(basePath, schemaPath);
|
|
18615
|
+
}
|
|
18616
|
+
return schemaPath;
|
|
18617
|
+
}
|
|
18618
|
+
function convertAjvError(err, value) {
|
|
18619
|
+
const instancePath = err.instancePath || "(root)";
|
|
18620
|
+
let expected;
|
|
18621
|
+
let actual = void 0;
|
|
18622
|
+
switch (err.keyword) {
|
|
18623
|
+
case "type":
|
|
18624
|
+
expected = err.params.type;
|
|
18625
|
+
actual = getValueAtPath(value, instancePath);
|
|
18626
|
+
break;
|
|
18627
|
+
case "required":
|
|
18628
|
+
expected = `property "${err.params.missingProperty}" to exist`;
|
|
18629
|
+
break;
|
|
18630
|
+
case "enum":
|
|
18631
|
+
expected = `one of: ${err.params.allowedValues?.join(", ")}`;
|
|
18632
|
+
actual = getValueAtPath(value, instancePath);
|
|
18633
|
+
break;
|
|
18634
|
+
case "pattern":
|
|
18635
|
+
expected = `to match pattern "${err.params.pattern}"`;
|
|
18636
|
+
actual = getValueAtPath(value, instancePath);
|
|
18637
|
+
break;
|
|
18638
|
+
case "minLength":
|
|
18639
|
+
expected = `length >= ${err.params.limit}`;
|
|
18640
|
+
actual = getValueAtPath(value, instancePath);
|
|
18641
|
+
break;
|
|
18642
|
+
case "maxLength":
|
|
18643
|
+
expected = `length <= ${err.params.limit}`;
|
|
18644
|
+
actual = getValueAtPath(value, instancePath);
|
|
18645
|
+
break;
|
|
18646
|
+
case "minimum":
|
|
18647
|
+
case "exclusiveMinimum":
|
|
18648
|
+
expected = `>= ${err.params.limit}`;
|
|
18649
|
+
actual = getValueAtPath(value, instancePath);
|
|
18650
|
+
break;
|
|
18651
|
+
case "maximum":
|
|
18652
|
+
case "exclusiveMaximum":
|
|
18653
|
+
expected = `<= ${err.params.limit}`;
|
|
18654
|
+
actual = getValueAtPath(value, instancePath);
|
|
18655
|
+
break;
|
|
18656
|
+
case "additionalProperties":
|
|
18657
|
+
expected = `no additional property "${err.params.additionalProperty}"`;
|
|
18658
|
+
break;
|
|
18659
|
+
case "format":
|
|
18660
|
+
expected = `format "${err.params.format}"`;
|
|
18661
|
+
actual = getValueAtPath(value, instancePath);
|
|
18662
|
+
break;
|
|
18663
|
+
default:
|
|
18664
|
+
if (err.params) {
|
|
18665
|
+
expected = JSON.stringify(err.params);
|
|
18666
|
+
}
|
|
18667
|
+
actual = getValueAtPath(value, instancePath);
|
|
18668
|
+
}
|
|
18669
|
+
let severity = "error";
|
|
18670
|
+
if (err.keyword === "additionalProperties") {
|
|
18671
|
+
severity = "warning";
|
|
18672
|
+
}
|
|
18673
|
+
return {
|
|
18674
|
+
instancePath,
|
|
18675
|
+
schemaPath: err.schemaPath,
|
|
18676
|
+
keyword: err.keyword,
|
|
18677
|
+
message: err.message || "Validation failed",
|
|
18678
|
+
params: err.params,
|
|
18679
|
+
expected,
|
|
18680
|
+
actual,
|
|
18681
|
+
severity
|
|
18682
|
+
};
|
|
18683
|
+
}
|
|
18684
|
+
function getValueAtPath(obj, jsonPointer) {
|
|
18685
|
+
if (jsonPointer === "(root)" || jsonPointer === "" || jsonPointer === "/") {
|
|
18686
|
+
return obj;
|
|
18687
|
+
}
|
|
18688
|
+
const parts = jsonPointer.replace(/^\//, "").split("/");
|
|
18689
|
+
let current = obj;
|
|
18690
|
+
for (const part of parts) {
|
|
18691
|
+
if (current === null || current === void 0) {
|
|
18692
|
+
return void 0;
|
|
18590
18693
|
}
|
|
18694
|
+
const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
18695
|
+
current = current[key];
|
|
18696
|
+
}
|
|
18697
|
+
return current;
|
|
18698
|
+
}
|
|
18699
|
+
function validateAgainstSchemaDetailed(value, schemaPath, basePath, workspaceRoot) {
|
|
18700
|
+
try {
|
|
18701
|
+
const resolvedPath = resolveSchemaPath(schemaPath, basePath, workspaceRoot);
|
|
18591
18702
|
if (!fs.existsSync(resolvedPath)) {
|
|
18592
18703
|
return {
|
|
18593
18704
|
valid: false,
|
|
18594
|
-
errors: [
|
|
18705
|
+
errors: [{
|
|
18706
|
+
instancePath: "(root)",
|
|
18707
|
+
schemaPath: "",
|
|
18708
|
+
keyword: "file",
|
|
18709
|
+
message: `Schema file not found: ${schemaPath}`,
|
|
18710
|
+
params: { path: schemaPath },
|
|
18711
|
+
severity: "error"
|
|
18712
|
+
}],
|
|
18713
|
+
errorStrings: [`Schema file not found: ${schemaPath}`],
|
|
18714
|
+
resolvedSchemaPath: resolvedPath
|
|
18595
18715
|
};
|
|
18596
18716
|
}
|
|
18597
18717
|
const schemaContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
@@ -18601,25 +18721,56 @@ function validateAgainstSchema(value, schemaPath, basePath) {
|
|
|
18601
18721
|
} catch (e) {
|
|
18602
18722
|
return {
|
|
18603
18723
|
valid: false,
|
|
18604
|
-
errors: [
|
|
18724
|
+
errors: [{
|
|
18725
|
+
instancePath: "(root)",
|
|
18726
|
+
schemaPath: "",
|
|
18727
|
+
keyword: "parse",
|
|
18728
|
+
message: `Invalid JSON in schema file: ${schemaPath}`,
|
|
18729
|
+
params: { path: schemaPath },
|
|
18730
|
+
severity: "error"
|
|
18731
|
+
}],
|
|
18732
|
+
errorStrings: [`Invalid JSON in schema file: ${schemaPath}`],
|
|
18733
|
+
resolvedSchemaPath: resolvedPath
|
|
18605
18734
|
};
|
|
18606
18735
|
}
|
|
18607
18736
|
const validate2 = ajv.compile(schema);
|
|
18608
18737
|
const valid = validate2(value);
|
|
18609
18738
|
if (!valid && validate2.errors) {
|
|
18610
|
-
const errors = validate2.errors.map((err) =>
|
|
18739
|
+
const errors = validate2.errors.map((err) => convertAjvError(err, value));
|
|
18740
|
+
const errorStrings = validate2.errors.map((err) => {
|
|
18611
18741
|
const path5 = err.instancePath || "(root)";
|
|
18612
18742
|
return `${path5}: ${err.message}`;
|
|
18613
18743
|
});
|
|
18614
|
-
return {
|
|
18744
|
+
return {
|
|
18745
|
+
valid: false,
|
|
18746
|
+
errors,
|
|
18747
|
+
errorStrings,
|
|
18748
|
+
resolvedSchemaPath: resolvedPath,
|
|
18749
|
+
schema
|
|
18750
|
+
};
|
|
18615
18751
|
}
|
|
18616
|
-
return {
|
|
18752
|
+
return {
|
|
18753
|
+
valid: true,
|
|
18754
|
+
resolvedSchemaPath: resolvedPath,
|
|
18755
|
+
schema
|
|
18756
|
+
};
|
|
18617
18757
|
} catch (e) {
|
|
18618
18758
|
const error = e instanceof Error ? e.message : String(e);
|
|
18619
|
-
return {
|
|
18759
|
+
return {
|
|
18760
|
+
valid: false,
|
|
18761
|
+
errors: [{
|
|
18762
|
+
instancePath: "(root)",
|
|
18763
|
+
schemaPath: "",
|
|
18764
|
+
keyword: "exception",
|
|
18765
|
+
message: `Schema validation error: ${error}`,
|
|
18766
|
+
params: {},
|
|
18767
|
+
severity: "error"
|
|
18768
|
+
}],
|
|
18769
|
+
errorStrings: [`Schema validation error: ${error}`]
|
|
18770
|
+
};
|
|
18620
18771
|
}
|
|
18621
18772
|
}
|
|
18622
|
-
var import_ajv, fs, path2, ajv;
|
|
18773
|
+
var import_ajv, fs, path2, ajv, PATH_ALIAS_PREFIX, DEFAULT_CONTRACTS_FOLDER;
|
|
18623
18774
|
var init_schemaGenerator = __esm({
|
|
18624
18775
|
"src/schemaGenerator.ts"() {
|
|
18625
18776
|
"use strict";
|
|
@@ -18627,6 +18778,8 @@ var init_schemaGenerator = __esm({
|
|
|
18627
18778
|
fs = __toESM(require("fs"));
|
|
18628
18779
|
path2 = __toESM(require("path"));
|
|
18629
18780
|
ajv = new import_ajv.default({ allErrors: true, strict: false });
|
|
18781
|
+
PATH_ALIAS_PREFIX = "@/";
|
|
18782
|
+
DEFAULT_CONTRACTS_FOLDER = "contracts";
|
|
18630
18783
|
}
|
|
18631
18784
|
});
|
|
18632
18785
|
|
|
@@ -18910,7 +19063,7 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2, res
|
|
|
18910
19063
|
if (schemaPath.startsWith('"') && schemaPath.endsWith('"') || schemaPath.startsWith("'") && schemaPath.endsWith("'")) {
|
|
18911
19064
|
schemaPath = schemaPath.slice(1, -1);
|
|
18912
19065
|
}
|
|
18913
|
-
const validationResult =
|
|
19066
|
+
const validationResult = validateAgainstSchemaDetailed(leftValue, schemaPath, basePath);
|
|
18914
19067
|
const passed2 = validationResult.valid;
|
|
18915
19068
|
return {
|
|
18916
19069
|
passed: passed2,
|
|
@@ -18921,7 +19074,10 @@ function evaluateAssertion(assertion, responses, variables, getValueByPath2, res
|
|
|
18921
19074
|
rightValue: schemaPath,
|
|
18922
19075
|
leftExpression: assertion.leftExpr,
|
|
18923
19076
|
rightExpression: assertion.rightExpr,
|
|
18924
|
-
error: !passed2 && validationResult.
|
|
19077
|
+
error: !passed2 && validationResult.errorStrings ? validationResult.errorStrings.join("; ") : void 0,
|
|
19078
|
+
schemaErrors: validationResult.errors,
|
|
19079
|
+
schemaPath: validationResult.resolvedSchemaPath,
|
|
19080
|
+
schema: validationResult.schema,
|
|
18925
19081
|
...!passed2 ? buildFailureContext() : {}
|
|
18926
19082
|
};
|
|
18927
19083
|
}
|
|
@@ -27691,7 +27847,8 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
27691
27847
|
const stepResult = {
|
|
27692
27848
|
type: "assertion",
|
|
27693
27849
|
stepIndex: stepIdx,
|
|
27694
|
-
assertion: result
|
|
27850
|
+
assertion: result,
|
|
27851
|
+
lineNumber: step.lineNumber
|
|
27695
27852
|
};
|
|
27696
27853
|
orderedSteps.push(stepResult);
|
|
27697
27854
|
const statusIcon = result.passed ? "\u2713" : "\u2717";
|
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.3.
|
|
5
|
+
"version": "1.3.2",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|