norn-cli 1.2.4 → 1.3.0
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 +29 -0
- package/dist/cli.js +408 -144
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-02-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Retry and Backoff**: Automatically retry failed HTTP requests with configurable backoff
|
|
9
|
+
- Syntax: `GET url retry 3 backoff 200 ms` (retry up to 3 times with 200ms linear backoff)
|
|
10
|
+
- Works with direct URLs, named endpoints, and named requests
|
|
11
|
+
- Supports time units: `ms`, `s`, `seconds`, `milliseconds`
|
|
12
|
+
- Response panel shows retry indicator: 🔄 `retried 2x`
|
|
13
|
+
- CLI shows retry attempts in real-time
|
|
14
|
+
- Retries on 5xx errors, 429 rate limiting, and network failures
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
- **Syntax highlighting**: Variable references `{{var}}` now highlighted inside print strings
|
|
18
|
+
- **Better pattern recognition**: `test sequence` correctly terminates request blocks in all contexts
|
|
19
|
+
|
|
20
|
+
## [1.2.5] - 2026-02-07
|
|
21
|
+
|
|
22
|
+
### Improved
|
|
23
|
+
- **Rich HTML Reports**: Completely redesigned HTML report output for QA professionals
|
|
24
|
+
- **Pass rate badge**: Large visual indicator showing pass percentage (green/yellow/red)
|
|
25
|
+
- **Filter bar**: Search tests by name, filter by All/Passed/Failed
|
|
26
|
+
- **Clickable stat cards**: Click total/passed/failed to filter results
|
|
27
|
+
- **Expand/Collapse All**: Buttons to quickly expand or collapse all test details
|
|
28
|
+
- **Failures first**: Failed tests automatically sorted to top and expanded
|
|
29
|
+
- **Friendly assertion names**: Shows `user.body.status` instead of `$1.body.status`
|
|
30
|
+
- **JSON path context**: Failed assertions show the path that failed
|
|
31
|
+
- **Variable labels on requests**: Shows `user = GET /api/users` instead of `$1`
|
|
32
|
+
- **Environment display**: Shows which environment was used in report header
|
|
33
|
+
|
|
5
34
|
## [1.2.4] - 2026-02-07
|
|
6
35
|
|
|
7
36
|
### Improved
|
package/dist/cli.js
CHANGED
|
@@ -13286,6 +13286,31 @@ function stripInlineComment(line2) {
|
|
|
13286
13286
|
}
|
|
13287
13287
|
return line2;
|
|
13288
13288
|
}
|
|
13289
|
+
function extractRetryOptions(line2) {
|
|
13290
|
+
let cleanedLine = line2;
|
|
13291
|
+
let retryCount;
|
|
13292
|
+
let backoffMs;
|
|
13293
|
+
const retryMatch = line2.match(/\bretry\s+(\d+)\b/i);
|
|
13294
|
+
if (retryMatch) {
|
|
13295
|
+
retryCount = parseInt(retryMatch[1], 10);
|
|
13296
|
+
cleanedLine = cleanedLine.replace(retryMatch[0], "").trim();
|
|
13297
|
+
}
|
|
13298
|
+
const backoffMatch = line2.match(/\bbackoff\s+(\d+(?:\.\d+)?)\s*(s|ms|seconds?|milliseconds?)?\b/i);
|
|
13299
|
+
if (backoffMatch) {
|
|
13300
|
+
const value = parseFloat(backoffMatch[1]);
|
|
13301
|
+
const unit = (backoffMatch[2] || "ms").toLowerCase();
|
|
13302
|
+
if (unit === "s" || unit.startsWith("second")) {
|
|
13303
|
+
backoffMs = value * 1e3;
|
|
13304
|
+
} else {
|
|
13305
|
+
backoffMs = value;
|
|
13306
|
+
}
|
|
13307
|
+
cleanedLine = cleanedLine.replace(backoffMatch[0], "").trim();
|
|
13308
|
+
}
|
|
13309
|
+
if (retryCount !== void 0 && backoffMs === void 0) {
|
|
13310
|
+
backoffMs = 1e3;
|
|
13311
|
+
}
|
|
13312
|
+
return { cleanedLine, retryCount, backoffMs };
|
|
13313
|
+
}
|
|
13289
13314
|
function extractNamedRequests(text) {
|
|
13290
13315
|
const lines = text.split("\n");
|
|
13291
13316
|
const namedRequests = [];
|
|
@@ -13441,7 +13466,9 @@ function parserHttpRequest(text, variables = {}) {
|
|
|
13441
13466
|
if (requestLineIndex === -1) {
|
|
13442
13467
|
throw new Error("No valid HTTP method found");
|
|
13443
13468
|
}
|
|
13444
|
-
|
|
13469
|
+
let requestLine = stripInlineComment(allLines[requestLineIndex].trim());
|
|
13470
|
+
const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(requestLine);
|
|
13471
|
+
requestLine = cleanedLine;
|
|
13445
13472
|
const [method, ...urlParts] = requestLine.split(" ");
|
|
13446
13473
|
let url2 = urlParts.join(" ");
|
|
13447
13474
|
if (url2.startsWith('"') && url2.endsWith('"') || url2.startsWith("'") && url2.endsWith("'")) {
|
|
@@ -13490,7 +13517,7 @@ function parserHttpRequest(text, variables = {}) {
|
|
|
13490
13517
|
body = void 0;
|
|
13491
13518
|
}
|
|
13492
13519
|
}
|
|
13493
|
-
return { method: method.toUpperCase(), url: url2, headers, body };
|
|
13520
|
+
return { method: method.toUpperCase(), url: url2, headers, body, retryCount, backoffMs };
|
|
13494
13521
|
}
|
|
13495
13522
|
function extractImports(text) {
|
|
13496
13523
|
const lines = text.split("\n");
|
|
@@ -19463,114 +19490,157 @@ async function getAllCookies(jar) {
|
|
|
19463
19490
|
secure: Boolean(c.secure)
|
|
19464
19491
|
}));
|
|
19465
19492
|
}
|
|
19466
|
-
async function sendRequest(request) {
|
|
19467
|
-
return sendRequestWithJar(request, sharedCookieJar);
|
|
19493
|
+
async function sendRequest(request, retryOptions) {
|
|
19494
|
+
return sendRequestWithJar(request, sharedCookieJar, retryOptions);
|
|
19468
19495
|
}
|
|
19469
|
-
|
|
19470
|
-
|
|
19471
|
-
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
19475
|
-
|
|
19476
|
-
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
|
|
19481
|
-
|
|
19482
|
-
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19496
|
-
|
|
19497
|
-
|
|
19498
|
-
|
|
19496
|
+
function sleep(ms) {
|
|
19497
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
19498
|
+
}
|
|
19499
|
+
function shouldRetry(response, error) {
|
|
19500
|
+
if (error) return true;
|
|
19501
|
+
if (response && (response.status >= 500 || response.status === 429)) return true;
|
|
19502
|
+
return false;
|
|
19503
|
+
}
|
|
19504
|
+
async function sendRequestWithJar(request, jar, retryOptions) {
|
|
19505
|
+
const totalAttempts = (retryOptions?.retryCount ?? 0) + 1;
|
|
19506
|
+
const backoffMs = retryOptions?.backoffMs ?? 1e3;
|
|
19507
|
+
const retriedErrors = [];
|
|
19508
|
+
let lastError = null;
|
|
19509
|
+
let attemptsMade = 0;
|
|
19510
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
19511
|
+
attemptsMade = attempt;
|
|
19512
|
+
const startTime = Date.now();
|
|
19513
|
+
const maxRedirects = 10;
|
|
19514
|
+
let redirectCount = 0;
|
|
19515
|
+
let currentUrl = request.url;
|
|
19516
|
+
let currentMethod = request.method;
|
|
19517
|
+
let currentBody = request.body;
|
|
19518
|
+
let finalResponse = null;
|
|
19519
|
+
try {
|
|
19520
|
+
while (redirectCount <= maxRedirects) {
|
|
19521
|
+
const existingCookies = await jar.getCookies(currentUrl);
|
|
19522
|
+
const cookieHeader = existingCookies.map((c) => `${c.key}=${c.value}`).join("; ");
|
|
19523
|
+
const headers = { ...request.headers };
|
|
19524
|
+
if (cookieHeader) {
|
|
19525
|
+
headers["Cookie"] = cookieHeader;
|
|
19526
|
+
}
|
|
19527
|
+
let data = void 0;
|
|
19528
|
+
if (currentBody) {
|
|
19529
|
+
const contentType = Object.keys(headers).find(
|
|
19530
|
+
(key) => key.toLowerCase() === "content-type"
|
|
19531
|
+
);
|
|
19532
|
+
const contentTypeValue = contentType ? headers[contentType] : "";
|
|
19533
|
+
if (contentTypeValue.includes("application/x-www-form-urlencoded")) {
|
|
19534
|
+
const lines = currentBody.split("\n").map((line2) => line2.trim()).filter((line2) => line2);
|
|
19535
|
+
const params = lines.map((line2) => {
|
|
19536
|
+
if (line2.includes("=")) {
|
|
19537
|
+
const eqIndex = line2.indexOf("=");
|
|
19538
|
+
const key = line2.substring(0, eqIndex).trim();
|
|
19539
|
+
const value = line2.substring(eqIndex + 1).trim();
|
|
19540
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
19541
|
+
}
|
|
19542
|
+
const colonIndex = line2.indexOf(":");
|
|
19543
|
+
if (colonIndex > 0) {
|
|
19544
|
+
const key = line2.substring(0, colonIndex).trim();
|
|
19545
|
+
const value = line2.substring(colonIndex + 1).trim();
|
|
19546
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
19547
|
+
}
|
|
19548
|
+
return encodeURIComponent(line2);
|
|
19549
|
+
});
|
|
19550
|
+
data = params.join("&");
|
|
19551
|
+
} else if (contentTypeValue.includes("application/json")) {
|
|
19552
|
+
try {
|
|
19553
|
+
data = JSON.parse(currentBody);
|
|
19554
|
+
} catch {
|
|
19555
|
+
data = currentBody;
|
|
19499
19556
|
}
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19557
|
+
} else {
|
|
19558
|
+
try {
|
|
19559
|
+
data = JSON.parse(currentBody);
|
|
19560
|
+
} catch {
|
|
19561
|
+
data = currentBody;
|
|
19505
19562
|
}
|
|
19506
|
-
return encodeURIComponent(line2);
|
|
19507
|
-
});
|
|
19508
|
-
data = params.join("&");
|
|
19509
|
-
} else if (contentTypeValue.includes("application/json")) {
|
|
19510
|
-
try {
|
|
19511
|
-
data = JSON.parse(currentBody);
|
|
19512
|
-
} catch {
|
|
19513
|
-
data = currentBody;
|
|
19514
19563
|
}
|
|
19515
|
-
}
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
|
|
19519
|
-
|
|
19564
|
+
}
|
|
19565
|
+
const response = await axios_default({
|
|
19566
|
+
method: currentMethod,
|
|
19567
|
+
url: currentUrl,
|
|
19568
|
+
headers,
|
|
19569
|
+
data,
|
|
19570
|
+
timeout: 3e4,
|
|
19571
|
+
maxRedirects: 0,
|
|
19572
|
+
validateStatus: () => true
|
|
19573
|
+
});
|
|
19574
|
+
const setCookieHeader = response.headers["set-cookie"];
|
|
19575
|
+
if (setCookieHeader) {
|
|
19576
|
+
const cookies2 = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
|
19577
|
+
for (const cookieStr of cookies2) {
|
|
19578
|
+
try {
|
|
19579
|
+
await jar.setCookie(cookieStr, currentUrl);
|
|
19580
|
+
} catch {
|
|
19581
|
+
}
|
|
19520
19582
|
}
|
|
19521
19583
|
}
|
|
19522
|
-
|
|
19523
|
-
|
|
19524
|
-
|
|
19525
|
-
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
maxRedirects: 0,
|
|
19530
|
-
// Disable automatic redirects
|
|
19531
|
-
validateStatus: () => true
|
|
19532
|
-
});
|
|
19533
|
-
const setCookieHeader = response.headers["set-cookie"];
|
|
19534
|
-
if (setCookieHeader) {
|
|
19535
|
-
const cookies2 = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
|
19536
|
-
for (const cookieStr of cookies2) {
|
|
19537
|
-
try {
|
|
19538
|
-
await jar.setCookie(cookieStr, currentUrl);
|
|
19539
|
-
} catch {
|
|
19584
|
+
if (response.status >= 300 && response.status < 400 && response.headers["location"]) {
|
|
19585
|
+
redirectCount++;
|
|
19586
|
+
const location = response.headers["location"];
|
|
19587
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
19588
|
+
if (currentMethod !== "GET" && currentMethod !== "HEAD") {
|
|
19589
|
+
currentMethod = "GET";
|
|
19590
|
+
currentBody = void 0;
|
|
19540
19591
|
}
|
|
19592
|
+
continue;
|
|
19541
19593
|
}
|
|
19594
|
+
finalResponse = response;
|
|
19595
|
+
break;
|
|
19596
|
+
}
|
|
19597
|
+
if (!finalResponse) {
|
|
19598
|
+
throw new Error("Too many redirects");
|
|
19542
19599
|
}
|
|
19543
|
-
if (
|
|
19544
|
-
|
|
19545
|
-
const
|
|
19546
|
-
|
|
19547
|
-
if (
|
|
19548
|
-
|
|
19549
|
-
currentBody = void 0;
|
|
19600
|
+
if (shouldRetry(finalResponse, null) && attempt < totalAttempts) {
|
|
19601
|
+
const waitMs = backoffMs * attempt;
|
|
19602
|
+
const errorMessage = `HTTP ${finalResponse.status} ${finalResponse.statusText}`;
|
|
19603
|
+
retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
|
|
19604
|
+
if (retryOptions?.onRetry) {
|
|
19605
|
+
retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
|
|
19550
19606
|
}
|
|
19607
|
+
await sleep(waitMs);
|
|
19551
19608
|
continue;
|
|
19552
19609
|
}
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
19610
|
+
const cookies = await getAllCookies(jar);
|
|
19611
|
+
const result = {
|
|
19612
|
+
status: finalResponse.status,
|
|
19613
|
+
statusText: finalResponse.statusText,
|
|
19614
|
+
headers: finalResponse.headers,
|
|
19615
|
+
body: finalResponse.data,
|
|
19616
|
+
duration: Date.now() - startTime,
|
|
19617
|
+
cookies
|
|
19618
|
+
};
|
|
19619
|
+
if (attemptsMade > 1 || totalAttempts > 1) {
|
|
19620
|
+
result.retryInfo = {
|
|
19621
|
+
attemptsMade,
|
|
19622
|
+
totalAttempts,
|
|
19623
|
+
retriedErrors
|
|
19624
|
+
};
|
|
19625
|
+
}
|
|
19626
|
+
return result;
|
|
19627
|
+
} catch (error) {
|
|
19628
|
+
const errorMessage = error.message || String(error);
|
|
19629
|
+
lastError = new Error(`${errorMessage}
|
|
19630
|
+
URL used: ${currentUrl || request.url}`);
|
|
19631
|
+
if (attempt < totalAttempts) {
|
|
19632
|
+
const waitMs = backoffMs * attempt;
|
|
19633
|
+
retriedErrors.push(`Attempt ${attempt}: ${errorMessage}`);
|
|
19634
|
+
if (retryOptions?.onRetry) {
|
|
19635
|
+
retryOptions.onRetry(attempt, totalAttempts, errorMessage, waitMs);
|
|
19636
|
+
}
|
|
19637
|
+
await sleep(waitMs);
|
|
19638
|
+
continue;
|
|
19639
|
+
}
|
|
19640
|
+
throw lastError;
|
|
19558
19641
|
}
|
|
19559
|
-
const cookies = await getAllCookies(jar);
|
|
19560
|
-
return {
|
|
19561
|
-
status: finalResponse.status,
|
|
19562
|
-
statusText: finalResponse.statusText,
|
|
19563
|
-
headers: finalResponse.headers,
|
|
19564
|
-
body: finalResponse.data,
|
|
19565
|
-
duration: Date.now() - startTime,
|
|
19566
|
-
cookies
|
|
19567
|
-
};
|
|
19568
|
-
} catch (error) {
|
|
19569
|
-
const originalError = error.message || String(error);
|
|
19570
|
-
const urlUsed = currentUrl || request.url;
|
|
19571
|
-
throw new Error(`${originalError}
|
|
19572
|
-
URL used: ${urlUsed}`);
|
|
19573
19642
|
}
|
|
19643
|
+
throw lastError || new Error("Request failed");
|
|
19574
19644
|
}
|
|
19575
19645
|
|
|
19576
19646
|
// src/scriptRunner.ts
|
|
@@ -19955,17 +20025,20 @@ function isRunNamedRequestCommand(line2) {
|
|
|
19955
20025
|
if (/^(bash|js|powershell)\s+/i.test(afterRun)) {
|
|
19956
20026
|
return false;
|
|
19957
20027
|
}
|
|
19958
|
-
return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))
|
|
20028
|
+
return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?$/i.test(afterRun);
|
|
19959
20029
|
}
|
|
19960
20030
|
function parseRunNamedRequestCommand(line2) {
|
|
19961
20031
|
const trimmed = line2.trim();
|
|
19962
|
-
const
|
|
20032
|
+
const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(trimmed);
|
|
20033
|
+
const match = cleanedLine.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?$/i);
|
|
19963
20034
|
if (!match) {
|
|
19964
20035
|
return null;
|
|
19965
20036
|
}
|
|
19966
20037
|
return {
|
|
19967
20038
|
name: match[1],
|
|
19968
|
-
args: parseRunArguments(match[2] || "")
|
|
20039
|
+
args: parseRunArguments(match[2] || ""),
|
|
20040
|
+
retryCount,
|
|
20041
|
+
backoffMs
|
|
19969
20042
|
};
|
|
19970
20043
|
}
|
|
19971
20044
|
function isReturnStatement(line2) {
|
|
@@ -20061,7 +20134,7 @@ function parseRunArguments(argsStr) {
|
|
|
20061
20134
|
}
|
|
20062
20135
|
function isVarRunSequenceCommand(line2) {
|
|
20063
20136
|
const trimmed = line2.trim();
|
|
20064
|
-
if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?\s*$/i.test(trimmed)) {
|
|
20137
|
+
if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?\s*$/i.test(trimmed)) {
|
|
20065
20138
|
return false;
|
|
20066
20139
|
}
|
|
20067
20140
|
const match = trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)/i);
|
|
@@ -20075,14 +20148,17 @@ function isVarRunSequenceCommand(line2) {
|
|
|
20075
20148
|
}
|
|
20076
20149
|
function parseVarRunSequenceCommand(line2) {
|
|
20077
20150
|
const trimmed = line2.trim();
|
|
20078
|
-
const
|
|
20151
|
+
const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(trimmed);
|
|
20152
|
+
const match = cleanedLine.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
|
|
20079
20153
|
if (!match) {
|
|
20080
20154
|
return null;
|
|
20081
20155
|
}
|
|
20082
20156
|
return {
|
|
20083
20157
|
varName: match[1],
|
|
20084
20158
|
sequenceName: match[2],
|
|
20085
|
-
args: parseRunArguments(match[3] || "")
|
|
20159
|
+
args: parseRunArguments(match[3] || ""),
|
|
20160
|
+
retryCount,
|
|
20161
|
+
backoffMs
|
|
20086
20162
|
};
|
|
20087
20163
|
}
|
|
20088
20164
|
function bindSequenceArguments(params, args, runtimeVariables) {
|
|
@@ -20434,10 +20510,13 @@ function parseVarRequestCommand(line2) {
|
|
|
20434
20510
|
if (!match) {
|
|
20435
20511
|
return null;
|
|
20436
20512
|
}
|
|
20513
|
+
const { cleanedLine: url2, retryCount, backoffMs } = extractRetryOptions(match[3].trim());
|
|
20437
20514
|
return {
|
|
20438
20515
|
varName: match[1],
|
|
20439
20516
|
method: match[2].toUpperCase(),
|
|
20440
|
-
url:
|
|
20517
|
+
url: url2,
|
|
20518
|
+
retryCount,
|
|
20519
|
+
backoffMs
|
|
20441
20520
|
};
|
|
20442
20521
|
}
|
|
20443
20522
|
function isVarAssignCommand(line2) {
|
|
@@ -20668,7 +20747,7 @@ function valueToString2(value) {
|
|
|
20668
20747
|
}
|
|
20669
20748
|
return String(value);
|
|
20670
20749
|
}
|
|
20671
|
-
function
|
|
20750
|
+
function sleep2(ms) {
|
|
20672
20751
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
20673
20752
|
}
|
|
20674
20753
|
function isIfCommand(line2) {
|
|
@@ -21147,7 +21226,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21147
21226
|
if (step.type === "wait") {
|
|
21148
21227
|
const durationMs = parseWaitCommand(step.content);
|
|
21149
21228
|
reportProgress(stepIdx, "wait", `wait: ${durationMs}ms`);
|
|
21150
|
-
await
|
|
21229
|
+
await sleep2(durationMs);
|
|
21151
21230
|
const stepResult = {
|
|
21152
21231
|
type: "print",
|
|
21153
21232
|
stepIndex: stepIdx,
|
|
@@ -21388,7 +21467,14 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21388
21467
|
requestText += "\n" + headerLines.join("\n");
|
|
21389
21468
|
}
|
|
21390
21469
|
const requestParsed = parserHttpRequest(requestText, runtimeVariables);
|
|
21391
|
-
const
|
|
21470
|
+
const retryOpts = parsed.retryCount ? {
|
|
21471
|
+
retryCount: parsed.retryCount,
|
|
21472
|
+
backoffMs: parsed.backoffMs || 1e3,
|
|
21473
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
21474
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, void 0);
|
|
21475
|
+
}
|
|
21476
|
+
} : void 0;
|
|
21477
|
+
const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
|
|
21392
21478
|
addResponse(response);
|
|
21393
21479
|
responseIndexToVariable.set(responses.length, parsed.varName);
|
|
21394
21480
|
runtimeVariables[parsed.varName] = response;
|
|
@@ -21641,20 +21727,29 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21641
21727
|
let requestUrl = "";
|
|
21642
21728
|
let requestMethod = "";
|
|
21643
21729
|
try {
|
|
21644
|
-
const
|
|
21645
|
-
requestUrl =
|
|
21646
|
-
requestMethod =
|
|
21647
|
-
const
|
|
21730
|
+
const requestParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
|
|
21731
|
+
requestUrl = requestParsed.url;
|
|
21732
|
+
requestMethod = requestParsed.method;
|
|
21733
|
+
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
21734
|
+
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
|
|
21735
|
+
const retryOpts = effectiveRetryCount ? {
|
|
21736
|
+
retryCount: effectiveRetryCount,
|
|
21737
|
+
backoffMs: effectiveBackoffMs,
|
|
21738
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
21739
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] run ${targetName} - waiting ${waitMs}ms`, void 0);
|
|
21740
|
+
}
|
|
21741
|
+
} : void 0;
|
|
21742
|
+
const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
|
|
21648
21743
|
addResponse(response);
|
|
21649
21744
|
const stepResult = {
|
|
21650
21745
|
type: "request",
|
|
21651
21746
|
stepIndex: stepIdx,
|
|
21652
21747
|
response,
|
|
21653
|
-
requestMethod:
|
|
21654
|
-
requestUrl:
|
|
21748
|
+
requestMethod: requestParsed.method,
|
|
21749
|
+
requestUrl: requestParsed.url
|
|
21655
21750
|
};
|
|
21656
21751
|
orderedSteps.push(stepResult);
|
|
21657
|
-
reportProgress(stepIdx, "namedRequest", `run ${targetName} \u2192 ${
|
|
21752
|
+
reportProgress(stepIdx, "namedRequest", `run ${targetName} \u2192 ${requestParsed.method} ${requestParsed.url}`, stepResult);
|
|
21658
21753
|
const relevantCaptures = captures.filter((c) => c.afterRequest === requestIndex);
|
|
21659
21754
|
for (const capture of relevantCaptures) {
|
|
21660
21755
|
const value = getValueByPath(response, capture.path);
|
|
@@ -21732,20 +21827,29 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21732
21827
|
let requestUrl = "";
|
|
21733
21828
|
let requestMethod = "";
|
|
21734
21829
|
try {
|
|
21735
|
-
const
|
|
21736
|
-
requestUrl =
|
|
21737
|
-
requestMethod =
|
|
21738
|
-
const
|
|
21830
|
+
const requestParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
|
|
21831
|
+
requestUrl = requestParsed.url;
|
|
21832
|
+
requestMethod = requestParsed.method;
|
|
21833
|
+
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
21834
|
+
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1e3;
|
|
21835
|
+
const retryOpts = effectiveRetryCount ? {
|
|
21836
|
+
retryCount: effectiveRetryCount,
|
|
21837
|
+
backoffMs: effectiveBackoffMs,
|
|
21838
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
21839
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] var ${varName} = run ${sequenceName} - waiting ${waitMs}ms`, void 0);
|
|
21840
|
+
}
|
|
21841
|
+
} : void 0;
|
|
21842
|
+
const response = cookieJar ? await sendRequestWithJar(requestParsed, cookieJar, retryOpts) : await sendRequest(requestParsed, retryOpts);
|
|
21739
21843
|
addResponse(response);
|
|
21740
21844
|
const stepResult = {
|
|
21741
21845
|
type: "request",
|
|
21742
21846
|
stepIndex: stepIdx,
|
|
21743
21847
|
response,
|
|
21744
|
-
requestMethod:
|
|
21745
|
-
requestUrl:
|
|
21848
|
+
requestMethod: requestParsed.method,
|
|
21849
|
+
requestUrl: requestParsed.url
|
|
21746
21850
|
};
|
|
21747
21851
|
orderedSteps.push(stepResult);
|
|
21748
|
-
reportProgress(stepIdx, "namedRequest", `var ${varName} = run ${sequenceName} \u2192 ${
|
|
21852
|
+
reportProgress(stepIdx, "namedRequest", `var ${varName} = run ${sequenceName} \u2192 ${requestParsed.method} ${requestParsed.url}`, stepResult);
|
|
21749
21853
|
const capturedResponse = {
|
|
21750
21854
|
status: response.status,
|
|
21751
21855
|
statusText: response.statusText,
|
|
@@ -21951,7 +22055,15 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
|
|
|
21951
22055
|
parsed = parserHttpRequest(step.content, runtimeVariables);
|
|
21952
22056
|
requestDescription = `${parsed.method} ${parsed.url}`;
|
|
21953
22057
|
}
|
|
21954
|
-
const
|
|
22058
|
+
const { retryCount, backoffMs } = extractRetryOptions(step.content);
|
|
22059
|
+
const retryOpts = retryCount ?? parsed.retryCount ? {
|
|
22060
|
+
retryCount: retryCount ?? parsed.retryCount ?? 0,
|
|
22061
|
+
backoffMs: backoffMs ?? parsed.backoffMs ?? 1e3,
|
|
22062
|
+
onRetry: (attempt, total, error, waitMs) => {
|
|
22063
|
+
reportProgress(stepIdx, "request", `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, void 0);
|
|
22064
|
+
}
|
|
22065
|
+
} : void 0;
|
|
22066
|
+
const response = cookieJar ? await sendRequestWithJar(parsed, cookieJar, retryOpts) : await sendRequest(parsed, retryOpts);
|
|
21955
22067
|
addResponse(response);
|
|
21956
22068
|
const stepResult = {
|
|
21957
22069
|
type: "request",
|
|
@@ -22264,12 +22376,14 @@ function formatResponse(response, options) {
|
|
|
22264
22376
|
const icon = isSuccess ? colors.checkmark : colors.cross;
|
|
22265
22377
|
if (method && url2) {
|
|
22266
22378
|
const displayUrl = redactUrl(url2, redaction);
|
|
22379
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
|
|
22267
22380
|
lines.push(
|
|
22268
|
-
`${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`(${response.status} ${response.statusText})`)} ${colors.duration(formatDuration(response.duration))}`
|
|
22381
|
+
`${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`(${response.status} ${response.statusText})`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`
|
|
22269
22382
|
);
|
|
22270
22383
|
} else {
|
|
22384
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
|
|
22271
22385
|
lines.push(
|
|
22272
|
-
`${prefix}${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(`(${formatDuration(response.duration)})`)}`
|
|
22386
|
+
`${prefix}${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(`(${formatDuration(response.duration)})`)}${retryInfo}`
|
|
22273
22387
|
);
|
|
22274
22388
|
}
|
|
22275
22389
|
if (verbose || showDetails) {
|
|
@@ -22319,11 +22433,12 @@ function formatResponseCompact(response, options) {
|
|
|
22319
22433
|
const statusColor = getStatusColor(colors, response.status);
|
|
22320
22434
|
const isSuccess = response.status >= 200 && response.status < 300;
|
|
22321
22435
|
const icon = isSuccess ? colors.checkmark : colors.cross;
|
|
22436
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1 ? ` ${colors.warning(`[retried ${response.retryInfo.attemptsMade - 1}x]`)}` : "";
|
|
22322
22437
|
if (method && url2) {
|
|
22323
22438
|
const displayUrl = redactUrl(url2, redaction);
|
|
22324
|
-
return `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`${response.status}`)} ${colors.duration(formatDuration(response.duration))}`;
|
|
22439
|
+
return `${prefix}${icon} ${colors.method(method)} ${displayUrl} ${statusColor(`${response.status}`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`;
|
|
22325
22440
|
}
|
|
22326
|
-
return `${prefix}${icon} ${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(formatDuration(response.duration))}`;
|
|
22441
|
+
return `${prefix}${icon} ${statusColor(`${response.status} ${response.statusText}`)} ${colors.duration(formatDuration(response.duration))}${retryInfo}`;
|
|
22327
22442
|
}
|
|
22328
22443
|
|
|
22329
22444
|
// src/cli/formatters/assertion.ts
|
|
@@ -22677,7 +22792,7 @@ function formatTimestamp() {
|
|
|
22677
22792
|
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
|
22678
22793
|
}
|
|
22679
22794
|
function generateHtmlReport(results, options) {
|
|
22680
|
-
const { outputPath, redaction, title = "Norn Test Report" } = options;
|
|
22795
|
+
const { outputPath, redaction, title = "Norn Test Report", environment } = options;
|
|
22681
22796
|
const totalSequences = results.length;
|
|
22682
22797
|
const passedSequences = results.filter((r) => r.success).length;
|
|
22683
22798
|
const failedSequences = totalSequences - passedSequences;
|
|
@@ -22686,6 +22801,11 @@ function generateHtmlReport(results, options) {
|
|
|
22686
22801
|
const failedAssertions = totalAssertions - passedAssertions;
|
|
22687
22802
|
const totalRequests = results.reduce((sum, r) => sum + r.steps.filter((s) => s.type === "request").length, 0);
|
|
22688
22803
|
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
22804
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
22805
|
+
if (a.success !== b.success) return a.success ? 1 : -1;
|
|
22806
|
+
return a.name.localeCompare(b.name);
|
|
22807
|
+
});
|
|
22808
|
+
const passRate = totalSequences > 0 ? Math.round(passedSequences / totalSequences * 100) : 100;
|
|
22689
22809
|
const html = `<!DOCTYPE html>
|
|
22690
22810
|
<html lang="en">
|
|
22691
22811
|
<head>
|
|
@@ -22700,23 +22820,32 @@ function generateHtmlReport(results, options) {
|
|
|
22700
22820
|
<div class="container">
|
|
22701
22821
|
<header>
|
|
22702
22822
|
<h1>${escapeHtml(title)}</h1>
|
|
22703
|
-
<
|
|
22823
|
+
<div class="header-meta">
|
|
22824
|
+
<span class="timestamp">Generated: ${formatTimestamp()}</span>
|
|
22825
|
+
${environment ? `<span class="environment">Environment: <strong>${escapeHtml(environment)}</strong></span>` : ""}
|
|
22826
|
+
</div>
|
|
22704
22827
|
</header>
|
|
22705
22828
|
|
|
22706
22829
|
<section class="summary">
|
|
22707
|
-
<
|
|
22830
|
+
<div class="summary-header">
|
|
22831
|
+
<h2>Summary</h2>
|
|
22832
|
+
<div class="pass-rate ${passRate === 100 ? "perfect" : passRate >= 80 ? "good" : "poor"}">
|
|
22833
|
+
<span class="rate-value">${passRate}%</span>
|
|
22834
|
+
<span class="rate-label">Pass Rate</span>
|
|
22835
|
+
</div>
|
|
22836
|
+
</div>
|
|
22708
22837
|
<div class="stats-grid">
|
|
22709
|
-
<div class="stat-card ${failedSequences > 0 ? "has-failures" : "all-passed"}">
|
|
22710
|
-
<div class="stat-value">${
|
|
22711
|
-
<div class="stat-label">
|
|
22838
|
+
<div class="stat-card clickable ${failedSequences > 0 ? "has-failures" : "all-passed"}" onclick="filterByStatus('all')" data-filter="all">
|
|
22839
|
+
<div class="stat-value">${totalSequences}</div>
|
|
22840
|
+
<div class="stat-label">Total Tests</div>
|
|
22712
22841
|
</div>
|
|
22713
|
-
<div class="stat-card ${
|
|
22714
|
-
<div class="stat-value">${
|
|
22715
|
-
<div class="stat-label">
|
|
22842
|
+
<div class="stat-card clickable ${passedSequences > 0 ? "all-passed" : ""}" onclick="filterByStatus('passed')" data-filter="passed">
|
|
22843
|
+
<div class="stat-value">${passedSequences}</div>
|
|
22844
|
+
<div class="stat-label">Passed</div>
|
|
22716
22845
|
</div>
|
|
22717
|
-
<div class="stat-card">
|
|
22718
|
-
<div class="stat-value">${
|
|
22719
|
-
<div class="stat-label">
|
|
22846
|
+
<div class="stat-card clickable ${failedSequences > 0 ? "has-failures" : ""}" onclick="filterByStatus('failed')" data-filter="failed">
|
|
22847
|
+
<div class="stat-value">${failedSequences}</div>
|
|
22848
|
+
<div class="stat-label">Failed</div>
|
|
22720
22849
|
</div>
|
|
22721
22850
|
<div class="stat-card">
|
|
22722
22851
|
<div class="stat-value">${formatDuration2(totalDuration)}</div>
|
|
@@ -22730,9 +22859,29 @@ function generateHtmlReport(results, options) {
|
|
|
22730
22859
|
</div>
|
|
22731
22860
|
</section>
|
|
22732
22861
|
|
|
22862
|
+
<section class="controls">
|
|
22863
|
+
<div class="filter-bar">
|
|
22864
|
+
<input type="text" id="search-input" placeholder="Search tests..." onkeyup="filterTests()">
|
|
22865
|
+
<div class="filter-buttons">
|
|
22866
|
+
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All (${totalSequences})</button>
|
|
22867
|
+
<button class="filter-btn" data-filter="passed" onclick="setFilter('passed')">Passed (${passedSequences})</button>
|
|
22868
|
+
<button class="filter-btn" data-filter="failed" onclick="setFilter('failed')">Failed (${failedSequences})</button>
|
|
22869
|
+
</div>
|
|
22870
|
+
</div>
|
|
22871
|
+
<div class="action-buttons">
|
|
22872
|
+
<button class="action-btn" onclick="expandAll()">Expand All</button>
|
|
22873
|
+
<button class="action-btn" onclick="collapseAll()">Collapse All</button>
|
|
22874
|
+
</div>
|
|
22875
|
+
</section>
|
|
22876
|
+
|
|
22733
22877
|
<section class="results">
|
|
22734
22878
|
<h2>Test Results</h2>
|
|
22735
|
-
|
|
22879
|
+
<div id="results-container">
|
|
22880
|
+
${sortedResults.map((r, i) => generateSequenceHtml(r, i, redaction)).join("\n")}
|
|
22881
|
+
</div>
|
|
22882
|
+
<div id="no-results" class="no-results" style="display: none;">
|
|
22883
|
+
No tests match your filter criteria.
|
|
22884
|
+
</div>
|
|
22736
22885
|
</section>
|
|
22737
22886
|
</div>
|
|
22738
22887
|
|
|
@@ -22797,11 +22946,13 @@ function generateRequestHtml(step, seqIndex, reqIndex, redaction) {
|
|
|
22797
22946
|
const statusClass = isSuccess ? "passed" : "failed";
|
|
22798
22947
|
const method = step.requestMethod || "REQUEST";
|
|
22799
22948
|
const url2 = step.requestUrl ? redactUrl(step.requestUrl, redaction) : "unknown";
|
|
22949
|
+
const varLabel = step.variableName ? `<span class="var-label">${escapeHtml(step.variableName)}</span><span class="var-equals">=</span>` : "";
|
|
22800
22950
|
const bodyHtml = response.body ? generateBodyHtml(response.body, redaction) : "<em>No body</em>";
|
|
22801
22951
|
const headersHtml = response.headers ? generateHeadersHtml(response.headers, redaction) : "";
|
|
22802
22952
|
return `
|
|
22803
22953
|
<div class="step request ${statusClass}">
|
|
22804
22954
|
<div class="step-header" onclick="toggleStep('step-${seqIndex}-${reqIndex}')">
|
|
22955
|
+
${varLabel}
|
|
22805
22956
|
<span class="method">${escapeHtml(method)}</span>
|
|
22806
22957
|
<span class="url">${escapeHtml(url2)}</span>
|
|
22807
22958
|
<span class="status-code">${response.status} ${escapeHtml(response.statusText)}</span>
|
|
@@ -22840,20 +22991,32 @@ function generateAssertionHtml(step, redaction) {
|
|
|
22840
22991
|
const assertion = step.assertion;
|
|
22841
22992
|
const statusClass = assertion.passed ? "passed" : "failed";
|
|
22842
22993
|
const statusIcon = assertion.passed ? "\u2713" : "\u2717";
|
|
22843
|
-
const displayText = assertion.message || assertion.expression;
|
|
22994
|
+
const displayText = assertion.friendlyName || assertion.message || assertion.expression;
|
|
22844
22995
|
let detailsHtml = "";
|
|
22845
22996
|
if (!assertion.passed) {
|
|
22997
|
+
let actualDisplay = "";
|
|
22998
|
+
if (assertion.leftValue === void 0) {
|
|
22999
|
+
actualDisplay = "undefined";
|
|
23000
|
+
} else if (assertion.leftValue === null) {
|
|
23001
|
+
actualDisplay = "null";
|
|
23002
|
+
} else if (typeof assertion.leftValue === "object") {
|
|
23003
|
+
actualDisplay = JSON.stringify(assertion.leftValue, null, 2);
|
|
23004
|
+
} else {
|
|
23005
|
+
actualDisplay = String(assertion.leftValue);
|
|
23006
|
+
}
|
|
23007
|
+
const pathInfo = assertion.jsonPath ? `<div class="assertion-path"><strong>Path:</strong> <code>${escapeHtml(assertion.jsonPath)}</code></div>` : "";
|
|
22846
23008
|
detailsHtml = `
|
|
22847
23009
|
<div class="assertion-details">
|
|
22848
|
-
|
|
22849
|
-
<div><strong>
|
|
23010
|
+
${pathInfo}
|
|
23011
|
+
<div><strong>Expected:</strong> <code>${escapeHtml(String(assertion.rightExpression || assertion.rightValue))}</code></div>
|
|
23012
|
+
<div><strong>Actual:</strong> <code>${escapeHtml(actualDisplay)}</code></div>
|
|
22850
23013
|
${assertion.error ? `<div class="error"><strong>Error:</strong> ${escapeHtml(redactString(assertion.error, redaction))}</div>` : ""}
|
|
22851
23014
|
</div>`;
|
|
22852
23015
|
}
|
|
22853
23016
|
return `
|
|
22854
23017
|
<div class="step assertion ${statusClass}">
|
|
22855
23018
|
<span class="status-icon">${statusIcon}</span>
|
|
22856
|
-
<span class="assertion-text"
|
|
23019
|
+
<span class="assertion-text">${escapeHtml(displayText)}</span>
|
|
22857
23020
|
${detailsHtml}
|
|
22858
23021
|
</div>`;
|
|
22859
23022
|
}
|
|
@@ -22893,17 +23056,48 @@ function getEmbeddedCSS() {
|
|
|
22893
23056
|
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
22894
23057
|
header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #333; }
|
|
22895
23058
|
header h1 { color: #fff; font-size: 2em; margin-bottom: 10px; }
|
|
23059
|
+
.header-meta { display: flex; justify-content: center; gap: 20px; flex-wrap: wrap; }
|
|
22896
23060
|
.timestamp { color: #888; font-size: 0.9em; }
|
|
23061
|
+
.environment { color: #888; font-size: 0.9em; }
|
|
23062
|
+
.environment strong { color: #569cd6; }
|
|
22897
23063
|
h2 { color: #fff; margin-bottom: 15px; font-size: 1.4em; }
|
|
22898
23064
|
|
|
22899
|
-
.summary { background: #252526; border-radius: 8px; padding: 20px; margin-bottom:
|
|
23065
|
+
.summary { background: #252526; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
|
|
23066
|
+
.summary-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
|
23067
|
+
.pass-rate { text-align: center; padding: 10px 20px; border-radius: 8px; }
|
|
23068
|
+
.pass-rate.perfect { background: #1e3a2f; }
|
|
23069
|
+
.pass-rate.good { background: #2a3a1e; }
|
|
23070
|
+
.pass-rate.poor { background: #3a1e1e; }
|
|
23071
|
+
.rate-value { display: block; font-size: 2em; font-weight: bold; }
|
|
23072
|
+
.pass-rate.perfect .rate-value { color: #4ec9b0; }
|
|
23073
|
+
.pass-rate.good .rate-value { color: #b5cea8; }
|
|
23074
|
+
.pass-rate.poor .rate-value { color: #f14c4c; }
|
|
23075
|
+
.rate-label { font-size: 0.8em; color: #888; }
|
|
23076
|
+
|
|
22900
23077
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; }
|
|
22901
|
-
.stat-card { background: #1e1e1e; border-radius: 6px; padding: 15px; text-align: center; }
|
|
23078
|
+
.stat-card { background: #1e1e1e; border-radius: 6px; padding: 15px; text-align: center; transition: transform 0.2s, box-shadow 0.2s; }
|
|
23079
|
+
.stat-card.clickable { cursor: pointer; }
|
|
23080
|
+
.stat-card.clickable:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
|
22902
23081
|
.stat-card.all-passed { border-left: 4px solid #4ec9b0; }
|
|
22903
23082
|
.stat-card.has-failures { border-left: 4px solid #f14c4c; }
|
|
22904
23083
|
.stat-value { font-size: 2em; font-weight: bold; color: #fff; }
|
|
22905
23084
|
.stat-label { color: #888; font-size: 0.85em; margin-top: 5px; }
|
|
22906
23085
|
|
|
23086
|
+
.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; }
|
|
23087
|
+
.filter-bar { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; flex: 1; }
|
|
23088
|
+
#search-input { background: #1e1e1e; border: 1px solid #333; border-radius: 4px; padding: 8px 12px; color: #d4d4d4; min-width: 200px; font-size: 0.9em; }
|
|
23089
|
+
#search-input:focus { outline: none; border-color: #569cd6; }
|
|
23090
|
+
#search-input::placeholder { color: #666; }
|
|
23091
|
+
.filter-buttons { display: flex; gap: 5px; }
|
|
23092
|
+
.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; }
|
|
23093
|
+
.filter-btn:hover { border-color: #569cd6; color: #d4d4d4; }
|
|
23094
|
+
.filter-btn.active { background: #264f78; border-color: #264f78; color: #fff; }
|
|
23095
|
+
.action-buttons { display: flex; gap: 10px; }
|
|
23096
|
+
.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; }
|
|
23097
|
+
.action-btn:hover { border-color: #666; color: #d4d4d4; }
|
|
23098
|
+
|
|
23099
|
+
.no-results { text-align: center; padding: 40px; color: #888; font-size: 1.1em; }
|
|
23100
|
+
|
|
22907
23101
|
.progress-bar { height: 8px; background: #333; border-radius: 4px; overflow: hidden; }
|
|
22908
23102
|
.progress-fill { height: 100%; transition: width 0.3s; }
|
|
22909
23103
|
.progress-fill.all-passed { background: #4ec9b0; }
|
|
@@ -22940,6 +23134,8 @@ function getEmbeddedCSS() {
|
|
|
22940
23134
|
|
|
22941
23135
|
.step-header { display: flex; align-items: center; gap: 10px; cursor: pointer; }
|
|
22942
23136
|
.step-header:hover { opacity: 0.9; }
|
|
23137
|
+
.var-label { color: #4fc1ff; font-family: monospace; font-weight: 500; }
|
|
23138
|
+
.var-equals { color: #d4d4d4; margin-right: 5px; }
|
|
22943
23139
|
.method { background: #264f78; color: #fff; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; font-weight: 600; }
|
|
22944
23140
|
.url { flex: 1; color: #9cdcfe; font-family: monospace; font-size: 0.9em; word-break: break-all; }
|
|
22945
23141
|
.status-code { font-weight: 600; }
|
|
@@ -22959,6 +23155,9 @@ function getEmbeddedCSS() {
|
|
|
22959
23155
|
.assertion-text { font-family: monospace; }
|
|
22960
23156
|
.assertion-details { width: 100%; margin-top: 8px; padding: 10px; background: #1a1a1a; border-radius: 4px; font-size: 0.9em; }
|
|
22961
23157
|
.assertion-details div { margin-bottom: 5px; }
|
|
23158
|
+
.assertion-details code { background: #2d2d2d; padding: 2px 6px; border-radius: 3px; font-family: monospace; color: #ce9178; }
|
|
23159
|
+
.assertion-path { color: #888; }
|
|
23160
|
+
.assertion-path code { color: #9cdcfe; }
|
|
22962
23161
|
|
|
22963
23162
|
.print-icon { font-size: 1em; }
|
|
22964
23163
|
.print-text { color: #dcdcaa; }
|
|
@@ -22975,6 +23174,8 @@ function getEmbeddedCSS() {
|
|
|
22975
23174
|
}
|
|
22976
23175
|
function getEmbeddedJS() {
|
|
22977
23176
|
return `
|
|
23177
|
+
let currentFilter = 'all';
|
|
23178
|
+
|
|
22978
23179
|
function toggleSequence(index) {
|
|
22979
23180
|
const body = document.getElementById('sequence-body-' + index);
|
|
22980
23181
|
const sequence = body.closest('.sequence');
|
|
@@ -23000,6 +23201,69 @@ function getEmbeddedJS() {
|
|
|
23000
23201
|
}
|
|
23001
23202
|
}
|
|
23002
23203
|
|
|
23204
|
+
function setFilter(filter) {
|
|
23205
|
+
currentFilter = filter;
|
|
23206
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
23207
|
+
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
23208
|
+
});
|
|
23209
|
+
filterTests();
|
|
23210
|
+
}
|
|
23211
|
+
|
|
23212
|
+
function filterByStatus(filter) {
|
|
23213
|
+
setFilter(filter);
|
|
23214
|
+
}
|
|
23215
|
+
|
|
23216
|
+
function filterTests() {
|
|
23217
|
+
const searchTerm = document.getElementById('search-input').value.toLowerCase();
|
|
23218
|
+
const sequences = document.querySelectorAll('.sequence');
|
|
23219
|
+
let visibleCount = 0;
|
|
23220
|
+
|
|
23221
|
+
sequences.forEach(seq => {
|
|
23222
|
+
const name = seq.querySelector('.sequence-name').textContent.toLowerCase();
|
|
23223
|
+
const isPassed = seq.classList.contains('passed');
|
|
23224
|
+
const isFailed = seq.classList.contains('failed');
|
|
23225
|
+
|
|
23226
|
+
let matchesFilter = currentFilter === 'all' ||
|
|
23227
|
+
(currentFilter === 'passed' && isPassed) ||
|
|
23228
|
+
(currentFilter === 'failed' && isFailed);
|
|
23229
|
+
|
|
23230
|
+
let matchesSearch = !searchTerm || name.includes(searchTerm);
|
|
23231
|
+
|
|
23232
|
+
if (matchesFilter && matchesSearch) {
|
|
23233
|
+
seq.style.display = 'block';
|
|
23234
|
+
visibleCount++;
|
|
23235
|
+
} else {
|
|
23236
|
+
seq.style.display = 'none';
|
|
23237
|
+
}
|
|
23238
|
+
});
|
|
23239
|
+
|
|
23240
|
+
document.getElementById('no-results').style.display = visibleCount === 0 ? 'block' : 'none';
|
|
23241
|
+
}
|
|
23242
|
+
|
|
23243
|
+
function expandAll() {
|
|
23244
|
+
document.querySelectorAll('.sequence').forEach(seq => {
|
|
23245
|
+
if (seq.style.display !== 'none') {
|
|
23246
|
+
const index = seq.dataset.sequence;
|
|
23247
|
+
const body = document.getElementById('sequence-body-' + index);
|
|
23248
|
+
if (body) {
|
|
23249
|
+
body.style.display = 'block';
|
|
23250
|
+
seq.classList.add('open');
|
|
23251
|
+
}
|
|
23252
|
+
}
|
|
23253
|
+
});
|
|
23254
|
+
}
|
|
23255
|
+
|
|
23256
|
+
function collapseAll() {
|
|
23257
|
+
document.querySelectorAll('.sequence').forEach(seq => {
|
|
23258
|
+
const index = seq.dataset.sequence;
|
|
23259
|
+
const body = document.getElementById('sequence-body-' + index);
|
|
23260
|
+
if (body) {
|
|
23261
|
+
body.style.display = 'none';
|
|
23262
|
+
seq.classList.remove('open');
|
|
23263
|
+
}
|
|
23264
|
+
});
|
|
23265
|
+
}
|
|
23266
|
+
|
|
23003
23267
|
// Expand all failed sequences by default
|
|
23004
23268
|
document.querySelectorAll('.sequence.failed').forEach((seq, i) => {
|
|
23005
23269
|
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.
|
|
5
|
+
"version": "1.3.0",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|