norn-cli 1.1.3 → 1.2.1
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 +34 -0
- package/README.md +36 -0
- package/dist/cli.js +77 -7
- package/norn-1.2.0.vsix +0 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.1] - 2026-02-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Inline headers for API endpoints**: Stack custom headers below endpoint calls from `.nornapi` files
|
|
9
|
+
- Example: `GET Login` followed by `x-api-key: "my-key"` on the next line
|
|
10
|
+
- Mix inline headers with header groups (e.g., `Common`)
|
|
11
|
+
- Inline headers take precedence over header group values
|
|
12
|
+
- Syntax highlighting for inline headers after API endpoint calls
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
- **IntelliSense for header values**: After selecting `Content-Type`, IntelliSense automatically triggers to show content type options (`application/json`, etc.)
|
|
16
|
+
- **Removed snippet tab stops**: Header completions no longer leave you in snippet mode requiring Tab to exit
|
|
17
|
+
- **Better trigger characters**: Added `:` as trigger character for immediate header value suggestions
|
|
18
|
+
|
|
19
|
+
## [1.2.0] - 2026-02-02
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Swagger API Coverage**: See percentage coverage of your OpenAPI/Swagger endpoints
|
|
23
|
+
- Status bar indicator shows coverage percentage when swagger specs are detected
|
|
24
|
+
- Click to open detailed coverage panel with per-endpoint breakdown
|
|
25
|
+
- Coverage tracks asserted status codes (200, 400, 404, etc.) per endpoint
|
|
26
|
+
- Supports wildcard matching (2xx, 4xx, 5xx)
|
|
27
|
+
- Only counts `test sequence` blocks toward coverage
|
|
28
|
+
- CodeLens on swagger lines shows coverage at a glance
|
|
29
|
+
- Automatic recalculation after test execution
|
|
30
|
+
- Caches swagger specs for performance
|
|
31
|
+
|
|
32
|
+
## [1.1.4] - 2026-02-02
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **Inline comments**: Fixed syntax highlighting and execution for inline comments
|
|
36
|
+
- **URL highlighting**: URLs now colored as strings with variable interpolation support
|
|
37
|
+
- **Duplicate detection**: Diagnostics for duplicate variables, named requests, and sequences
|
|
38
|
+
|
|
5
39
|
## [1.1.3] - 2026-02-01
|
|
6
40
|
|
|
7
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ A powerful REST client extension for VS Code with sequences, assertions, environ
|
|
|
14
14
|
- **Sequences**: Chain multiple requests with response capture using `$N.path`
|
|
15
15
|
- **Test Sequences**: Mark sequences as tests with `test sequence` for CLI execution
|
|
16
16
|
- **Test Explorer**: Run tests from VS Code's Testing sidebar with colorful output
|
|
17
|
+
- **Swagger Coverage**: Track API coverage with status bar indicator and detailed panel
|
|
17
18
|
- **Parameterized Tests**: Data-driven testing with `@data` and `@theory` annotations
|
|
18
19
|
- **Sequence Tags**: Tag sequences with `@smoke`, `@team(CustomerExp)` for filtering in CI/CD
|
|
19
20
|
- **Secret Variables**: Mark sensitive environment variables with `secret` for automatic redaction
|
|
@@ -841,6 +842,41 @@ jobs:
|
|
|
841
842
|
|
|
842
843
|
## Syntax Reference
|
|
843
844
|
|
|
845
|
+
### Swagger API Coverage
|
|
846
|
+
|
|
847
|
+
Track how much of your OpenAPI/Swagger spec is covered by tests:
|
|
848
|
+
|
|
849
|
+
- **Status Bar**: Shows coverage percentage when a `.nornapi` file has a `swagger` URL
|
|
850
|
+
- **Coverage Panel**: Click the status bar to see detailed per-endpoint coverage
|
|
851
|
+
- **Per Status Code**: Each response code (200, 400, 404) counts separately toward 100%
|
|
852
|
+
- **Wildcard Support**: Assert `2xx` to match 200, 201, 204, etc.
|
|
853
|
+
- **Test Sequences Only**: Only `test sequence` blocks count toward coverage
|
|
854
|
+
- **CodeLens**: Coverage shown on swagger import lines
|
|
855
|
+
|
|
856
|
+
Coverage is calculated by analyzing your test sequences for:
|
|
857
|
+
1. API calls by endpoint name (e.g., `GET GetPetById`)
|
|
858
|
+
2. Status assertions (e.g., `assert $1.status == 200`)
|
|
859
|
+
|
|
860
|
+
```bash
|
|
861
|
+
# In your .nornapi file:
|
|
862
|
+
swagger https://petstore.swagger.io/v2/swagger.json
|
|
863
|
+
|
|
864
|
+
GetOrderById: GET https://petstore.swagger.io/v2/store/order/{orderId}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
```bash
|
|
868
|
+
# In your .norn test file:
|
|
869
|
+
test sequence OrderTests
|
|
870
|
+
# This covers GET /store/order/{orderId} with 200
|
|
871
|
+
var order = GET GetOrderById(1)
|
|
872
|
+
assert order.status == 200
|
|
873
|
+
|
|
874
|
+
# This covers GET /store/order/{orderId} with 404
|
|
875
|
+
var notFound = GET GetOrderById(999999)
|
|
876
|
+
assert notFound.status == 404
|
|
877
|
+
end sequence
|
|
878
|
+
```
|
|
879
|
+
|
|
844
880
|
| Syntax | Description |
|
|
845
881
|
|--------|-------------|
|
|
846
882
|
| `var name = value` | Declare a variable (literal) |
|
package/dist/cli.js
CHANGED
|
@@ -13159,6 +13159,7 @@ function parseApiRequest(requestContent, endpoints, headerGroups) {
|
|
|
13159
13159
|
}
|
|
13160
13160
|
}
|
|
13161
13161
|
const headerGroupNames = [];
|
|
13162
|
+
const inlineHeaders = {};
|
|
13162
13163
|
if (headerGroupsOnLine) {
|
|
13163
13164
|
const names = headerGroupsOnLine.split(/\s+/).filter((n) => n);
|
|
13164
13165
|
for (const name of names) {
|
|
@@ -13168,13 +13169,28 @@ function parseApiRequest(requestContent, endpoints, headerGroups) {
|
|
|
13168
13169
|
}
|
|
13169
13170
|
}
|
|
13170
13171
|
const bodyLines = [];
|
|
13172
|
+
let inBodySection = false;
|
|
13171
13173
|
for (let i = 1; i < lines.length; i++) {
|
|
13172
13174
|
const line2 = lines[i];
|
|
13175
|
+
if (inBodySection) {
|
|
13176
|
+
bodyLines.push(line2);
|
|
13177
|
+
continue;
|
|
13178
|
+
}
|
|
13173
13179
|
if (headerGroups.some((hg) => hg.name === line2)) {
|
|
13174
13180
|
headerGroupNames.push(line2);
|
|
13175
|
-
|
|
13176
|
-
bodyLines.push(line2);
|
|
13181
|
+
continue;
|
|
13177
13182
|
}
|
|
13183
|
+
const headerMatch = line2.match(/^([a-zA-Z][a-zA-Z0-9\-]*)\s*:\s*(.+)$/);
|
|
13184
|
+
if (headerMatch) {
|
|
13185
|
+
let headerValue = headerMatch[2].trim();
|
|
13186
|
+
if (headerValue.startsWith('"') && headerValue.endsWith('"') || headerValue.startsWith("'") && headerValue.endsWith("'")) {
|
|
13187
|
+
headerValue = headerValue.slice(1, -1);
|
|
13188
|
+
}
|
|
13189
|
+
inlineHeaders[headerMatch[1]] = headerValue;
|
|
13190
|
+
continue;
|
|
13191
|
+
}
|
|
13192
|
+
inBodySection = true;
|
|
13193
|
+
bodyLines.push(line2);
|
|
13178
13194
|
}
|
|
13179
13195
|
const body = bodyLines.length > 0 ? bodyLines.join("\n") : void 0;
|
|
13180
13196
|
return {
|
|
@@ -13182,6 +13198,7 @@ function parseApiRequest(requestContent, endpoints, headerGroups) {
|
|
|
13182
13198
|
endpointName,
|
|
13183
13199
|
params,
|
|
13184
13200
|
headerGroupNames,
|
|
13201
|
+
inlineHeaders,
|
|
13185
13202
|
body
|
|
13186
13203
|
};
|
|
13187
13204
|
}
|
|
@@ -13219,6 +13236,25 @@ function unquote(value) {
|
|
|
13219
13236
|
}
|
|
13220
13237
|
|
|
13221
13238
|
// src/parser.ts
|
|
13239
|
+
function stripInlineComment(line2) {
|
|
13240
|
+
let inSingleQuote = false;
|
|
13241
|
+
let inDoubleQuote = false;
|
|
13242
|
+
for (let i = 0; i < line2.length; i++) {
|
|
13243
|
+
const char = line2[i];
|
|
13244
|
+
const prevChar = i > 0 ? line2[i - 1] : "";
|
|
13245
|
+
if (prevChar === "\\") {
|
|
13246
|
+
continue;
|
|
13247
|
+
}
|
|
13248
|
+
if (char === '"' && !inSingleQuote) {
|
|
13249
|
+
inDoubleQuote = !inDoubleQuote;
|
|
13250
|
+
} else if (char === "'" && !inDoubleQuote) {
|
|
13251
|
+
inSingleQuote = !inSingleQuote;
|
|
13252
|
+
} else if (char === "#" && !inSingleQuote && !inDoubleQuote) {
|
|
13253
|
+
return line2.substring(0, i).trimEnd();
|
|
13254
|
+
}
|
|
13255
|
+
}
|
|
13256
|
+
return line2;
|
|
13257
|
+
}
|
|
13222
13258
|
function extractNamedRequests(text) {
|
|
13223
13259
|
const lines = text.split("\n");
|
|
13224
13260
|
const namedRequests = [];
|
|
@@ -13374,9 +13410,12 @@ function parserHttpRequest(text, variables = {}) {
|
|
|
13374
13410
|
if (requestLineIndex === -1) {
|
|
13375
13411
|
throw new Error("No valid HTTP method found");
|
|
13376
13412
|
}
|
|
13377
|
-
const requestLine = allLines[requestLineIndex].trim();
|
|
13413
|
+
const requestLine = stripInlineComment(allLines[requestLineIndex].trim());
|
|
13378
13414
|
const [method, ...urlParts] = requestLine.split(" ");
|
|
13379
|
-
|
|
13415
|
+
let url2 = urlParts.join(" ");
|
|
13416
|
+
if (url2.startsWith('"') && url2.endsWith('"') || url2.startsWith("'") && url2.endsWith("'")) {
|
|
13417
|
+
url2 = url2.slice(1, -1);
|
|
13418
|
+
}
|
|
13380
13419
|
const headers = {};
|
|
13381
13420
|
let bodyStartIndex = -1;
|
|
13382
13421
|
let foundBlankLine = false;
|
|
@@ -20213,8 +20252,9 @@ function extractSequences(text) {
|
|
|
20213
20252
|
let currentSequence = null;
|
|
20214
20253
|
for (let i = 0; i < lines.length; i++) {
|
|
20215
20254
|
const line2 = lines[i].trim();
|
|
20216
|
-
const
|
|
20217
|
-
const
|
|
20255
|
+
const lineWithoutComment = stripInlineComment2(line2);
|
|
20256
|
+
const testSequenceMatch = lineWithoutComment.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?\s*$/);
|
|
20257
|
+
const sequenceMatch = lineWithoutComment.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?\s*$/);
|
|
20218
20258
|
if (testSequenceMatch || sequenceMatch) {
|
|
20219
20259
|
const isTest = !!testSequenceMatch;
|
|
20220
20260
|
const name = isTest ? testSequenceMatch[1] : sequenceMatch[1];
|
|
@@ -20377,11 +20417,31 @@ function evaluateValueExpression(expr, runtimeVariables) {
|
|
|
20377
20417
|
}
|
|
20378
20418
|
return { value: varValue };
|
|
20379
20419
|
}
|
|
20420
|
+
function stripInlineComment2(line2) {
|
|
20421
|
+
let inSingleQuote = false;
|
|
20422
|
+
let inDoubleQuote = false;
|
|
20423
|
+
for (let i = 0; i < line2.length; i++) {
|
|
20424
|
+
const char = line2[i];
|
|
20425
|
+
const prevChar = i > 0 ? line2[i - 1] : "";
|
|
20426
|
+
if (prevChar === "\\") {
|
|
20427
|
+
continue;
|
|
20428
|
+
}
|
|
20429
|
+
if (char === '"' && !inSingleQuote) {
|
|
20430
|
+
inDoubleQuote = !inDoubleQuote;
|
|
20431
|
+
} else if (char === "'" && !inDoubleQuote) {
|
|
20432
|
+
inSingleQuote = !inSingleQuote;
|
|
20433
|
+
} else if (char === "#" && !inSingleQuote && !inDoubleQuote) {
|
|
20434
|
+
return line2.substring(0, i).trimEnd();
|
|
20435
|
+
}
|
|
20436
|
+
}
|
|
20437
|
+
return line2;
|
|
20438
|
+
}
|
|
20380
20439
|
function isPrintCommand(line2) {
|
|
20381
20440
|
return /^print\s+/i.test(line2.trim());
|
|
20382
20441
|
}
|
|
20383
20442
|
function parsePrintCommand(line2) {
|
|
20384
|
-
const
|
|
20443
|
+
const lineWithoutComment = stripInlineComment2(line2);
|
|
20444
|
+
const match = lineWithoutComment.trim().match(/^print\s+(.+)$/i);
|
|
20385
20445
|
if (!match) {
|
|
20386
20446
|
return { title: "" };
|
|
20387
20447
|
}
|
|
@@ -21747,6 +21807,9 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21747
21807
|
Object.assign(combinedHeaders, resolvedHeaders);
|
|
21748
21808
|
}
|
|
21749
21809
|
}
|
|
21810
|
+
for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
|
|
21811
|
+
combinedHeaders[headerName] = substituteVariables(headerValue, runtimeVariables);
|
|
21812
|
+
}
|
|
21750
21813
|
let resolvedBody;
|
|
21751
21814
|
if (apiRequest.body) {
|
|
21752
21815
|
resolvedBody = substituteVariables(apiRequest.body, runtimeVariables);
|
|
@@ -23097,6 +23160,13 @@ async function runSingleRequest(fileContent, variables, cookieJar, apiDefinition
|
|
|
23097
23160
|
Object.assign(combinedHeaders, resolvedHeaders);
|
|
23098
23161
|
}
|
|
23099
23162
|
}
|
|
23163
|
+
for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
|
|
23164
|
+
let resolved = headerValue;
|
|
23165
|
+
resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
|
|
23166
|
+
return variables[varName] !== void 0 ? String(variables[varName]) : `{{${varName}}}`;
|
|
23167
|
+
});
|
|
23168
|
+
combinedHeaders[headerName] = resolved;
|
|
23169
|
+
}
|
|
23100
23170
|
const parsed2 = {
|
|
23101
23171
|
method: apiRequest.method,
|
|
23102
23172
|
url: resolvedPath,
|
package/norn-1.2.0.vsix
ADDED
|
Binary file
|
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.1
|
|
5
|
+
"version": "1.2.1",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|
|
@@ -68,6 +68,16 @@
|
|
|
68
68
|
"command": "norn.selectEnvironment",
|
|
69
69
|
"title": "Select Environment",
|
|
70
70
|
"category": "Norn"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"command": "norn.showCoverage",
|
|
74
|
+
"title": "Show API Coverage",
|
|
75
|
+
"category": "Norn"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"command": "norn.refreshCoverage",
|
|
79
|
+
"title": "Refresh API Coverage",
|
|
80
|
+
"category": "Norn"
|
|
71
81
|
}
|
|
72
82
|
],
|
|
73
83
|
"languages": [
|