@testsmith/api-spector 0.1.8 → 0.2.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/out/main/chunks/{request-handler-BEFvdSz6.js → request-handler-AFBOd__c.js} +209 -10
- package/out/main/index.js +149 -19
- package/out/main/runner.js +54 -18
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{index-aZFSSHIk.js → index-CnjhlKQP.js} +1679 -510
- package/out/renderer/assets/index-FZtc_UAN.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/out/renderer/assets/index-BhOUavjZ.css +0 -2
package/out/main/runner.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const undici = require("undici");
|
|
6
|
-
const requestHandler = require("./chunks/request-handler-
|
|
6
|
+
const requestHandler = require("./chunks/request-handler-AFBOd__c.js");
|
|
7
7
|
require("crypto");
|
|
8
8
|
require("dayjs");
|
|
9
9
|
require("vm");
|
|
10
10
|
require("tv4");
|
|
11
11
|
require("jsonpath-plus");
|
|
12
12
|
require("@xmldom/xmldom");
|
|
13
|
+
require("ajv");
|
|
13
14
|
function buildJsonReport(results, summary, meta = {}) {
|
|
14
15
|
return JSON.stringify({
|
|
15
16
|
timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -62,8 +63,13 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
62
63
|
after: "AFTER",
|
|
63
64
|
afterAll: "AFTER ALL"
|
|
64
65
|
};
|
|
66
|
+
let lastScopeKey = null;
|
|
65
67
|
const cards = results.map((r, idx) => {
|
|
66
|
-
const
|
|
68
|
+
const scopeKey = (r.scopePath ?? []).join(" / ");
|
|
69
|
+
const groupHeading = scopeKey && scopeKey !== lastScopeKey ? ` <div class="scope-heading">${esc(scopeKey)}</div>
|
|
70
|
+
` : "";
|
|
71
|
+
lastScopeKey = scopeKey;
|
|
72
|
+
const statusCls = r.status === "passed" ? "badge-pass" : r.status === "failed" ? "badge-fail" : r.status === "skipped" ? "badge-skip" : "badge-err";
|
|
67
73
|
const httpCls = r.httpStatus && r.httpStatus < 300 ? "http-ok" : r.httpStatus && r.httpStatus < 400 ? "http-redir" : "http-err";
|
|
68
74
|
const dur = r.durationMs != null ? `${r.durationMs} ms` : "—";
|
|
69
75
|
const label = r.iterationLabel ? ` <span class="muted">#${esc(r.iterationLabel)}</span>` : "";
|
|
@@ -101,7 +107,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
101
107
|
${resp.body ? `<div class="section-label">Body</div><pre class="code-block">${prettyJson(resp.body)}</pre>` : ""}
|
|
102
108
|
</div>` : "";
|
|
103
109
|
const hookCls = hookLabel ? r.hookType?.startsWith("before") ? "card-hook-before" : "card-hook-after" : "";
|
|
104
|
-
return
|
|
110
|
+
return `${groupHeading}
|
|
105
111
|
<div class="card ${hookCls}" id="r${idx}">
|
|
106
112
|
<div class="card-header" onclick="toggle(${idx})">
|
|
107
113
|
<span class="chevron" id="ch${idx}">▶</span>
|
|
@@ -142,8 +148,11 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
142
148
|
.stat-pass .stat-val { color: #3fb950; }
|
|
143
149
|
.stat-fail .stat-val { color: #f85149; }
|
|
144
150
|
.stat-err .stat-val { color: #d29922; }
|
|
151
|
+
.stat-skip .stat-val { color: #8b949e; }
|
|
145
152
|
/* Cards */
|
|
146
153
|
.card { border: 1px solid #21262d; border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
|
|
154
|
+
.scope-heading { margin: 18px 0 6px; padding: 4px 2px; font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid #21262d; }
|
|
155
|
+
.scope-heading::before { content: "▸ "; color: #484f58; }
|
|
147
156
|
.card-header { display: flex; align-items: baseline; gap: 8px; padding: 10px 14px; cursor: pointer; user-select: none; }
|
|
148
157
|
.card-header:hover { background: #161b22; }
|
|
149
158
|
.chevron { font-size: 10px; color: #8b949e; min-width: 10px; transition: transform .15s; }
|
|
@@ -155,6 +164,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
155
164
|
.badge-pass { background: #0d3a1e; color: #3fb950; }
|
|
156
165
|
.badge-fail { background: #3d1014; color: #f85149; }
|
|
157
166
|
.badge-err { background: #3d2a00; color: #d29922; }
|
|
167
|
+
.badge-skip { background: #21262d; color: #8b949e; }
|
|
158
168
|
.method { font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; white-space: nowrap; }
|
|
159
169
|
.method-badge { display: inline-block; font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; min-width: 52px; }
|
|
160
170
|
.http-badge { font-family: monospace; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
@@ -208,6 +218,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
208
218
|
<div class="stat stat-pass"><div class="stat-val">${summary.passed}</div><div class="stat-lbl">Passed</div></div>
|
|
209
219
|
<div class="stat stat-fail"><div class="stat-val">${summary.failed}</div><div class="stat-lbl">Failed</div></div>
|
|
210
220
|
<div class="stat stat-err"><div class="stat-val">${summary.errors}</div><div class="stat-lbl">Errors</div></div>
|
|
221
|
+
<div class="stat stat-skip"><div class="stat-val">${summary.skipped ?? 0}</div><div class="stat-lbl">No tests</div></div>
|
|
211
222
|
<div class="stat"><div class="stat-val">${passRate}%</div><div class="stat-lbl">Pass rate</div></div>
|
|
212
223
|
<div class="stat"><div class="stat-val">${summary.durationMs} ms</div><div class="stat-lbl">Duration</div></div>
|
|
213
224
|
</div>
|
|
@@ -251,6 +262,8 @@ function buildJUnitReport(results, summary, meta = {}) {
|
|
|
251
262
|
failures.push(` <error message="${esc(r.preScriptError)}" type="PreScriptError" />`);
|
|
252
263
|
} else if (r.postScriptError) {
|
|
253
264
|
failures.push(` <error message="${esc(r.postScriptError)}" type="PostScriptError" />`);
|
|
265
|
+
} else if (r.status === "skipped") {
|
|
266
|
+
failures.push(' <skipped message="No assertions defined for this request" />');
|
|
254
267
|
} else if (r.testResults?.length) {
|
|
255
268
|
for (const t of r.testResults) {
|
|
256
269
|
if (!t.passed) {
|
|
@@ -265,29 +278,31 @@ ${failures.join("\n")}
|
|
|
265
278
|
` : "";
|
|
266
279
|
return ` <testcase name="${name}" classname="${classname}" time="${timeSec}">${inner}</testcase>`;
|
|
267
280
|
});
|
|
281
|
+
const skipped = summary.skipped ?? 0;
|
|
268
282
|
const lines = [
|
|
269
283
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
270
|
-
`<testsuites name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" time="${totalSec}">`,
|
|
271
|
-
` <testsuite name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" time="${totalSec}" timestamp="${ts}">`,
|
|
284
|
+
`<testsuites name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" skipped="${skipped}" time="${totalSec}">`,
|
|
285
|
+
` <testsuite name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" skipped="${skipped}" time="${totalSec}" timestamp="${ts}">`,
|
|
272
286
|
...cases,
|
|
273
287
|
" </testsuite>",
|
|
274
288
|
"</testsuites>"
|
|
275
289
|
];
|
|
276
290
|
return lines.join("\n") + "\n";
|
|
277
291
|
}
|
|
278
|
-
function collectTagged(folder, requests, collectionVars, filterTags) {
|
|
292
|
+
function collectTagged(folder, requests, collectionVars, filterTags, parentPath = [], isRoot = true) {
|
|
279
293
|
const results = [];
|
|
294
|
+
const scopePath = isRoot ? parentPath : [...parentPath, folder.name];
|
|
280
295
|
for (const reqId of folder.requestIds) {
|
|
281
296
|
const req = requests[reqId];
|
|
282
|
-
if (!req || req.hookType) continue;
|
|
297
|
+
if (!req || req.hookType || req.disabled) continue;
|
|
283
298
|
const tags = req.meta?.tags ?? [];
|
|
284
299
|
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
285
|
-
results.push({ request: req, collectionVars });
|
|
300
|
+
results.push({ request: req, collectionVars, scopePath });
|
|
286
301
|
}
|
|
287
302
|
for (const sub of folder.folders) {
|
|
288
303
|
const folderTags = sub.tags ?? [];
|
|
289
304
|
const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
290
|
-
results.push(...collectTagged(sub, requests, collectionVars, effectiveTags));
|
|
305
|
+
results.push(...collectTagged(sub, requests, collectionVars, effectiveTags, scopePath, false));
|
|
291
306
|
}
|
|
292
307
|
return results;
|
|
293
308
|
}
|
|
@@ -370,8 +385,10 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
370
385
|
let updatedCollectionVars = { ...collectionVars };
|
|
371
386
|
let updatedGlobals = { ...globals };
|
|
372
387
|
let preScriptError;
|
|
388
|
+
const dynamicVars = await requestHandler.buildDynamicVars();
|
|
389
|
+
let vars = requestHandler.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
|
|
373
390
|
if (req.preRequestScript?.trim()) {
|
|
374
|
-
const r = await requestHandler.runScript(req.preRequestScript, {
|
|
391
|
+
const r = await requestHandler.runScript(requestHandler.interpolate(req.preRequestScript, vars), {
|
|
375
392
|
envVars: { ...envVars },
|
|
376
393
|
collectionVars: { ...collectionVars },
|
|
377
394
|
globals: { ...globals },
|
|
@@ -387,8 +404,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
387
404
|
if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [pre] ${l}`, C.gray)));
|
|
388
405
|
if (r.error) console.error(color(` [pre-script error] ${r.error}`, C.red));
|
|
389
406
|
}
|
|
390
|
-
|
|
391
|
-
const vars = requestHandler.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
407
|
+
vars = requestHandler.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
392
408
|
const resolvedUrl = requestHandler.buildUrl(req.url, req.params, vars);
|
|
393
409
|
base.resolvedUrl = resolvedUrl;
|
|
394
410
|
const start = Date.now();
|
|
@@ -450,7 +466,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
450
466
|
let consoleOutput = [];
|
|
451
467
|
let postScriptError;
|
|
452
468
|
if (req.postRequestScript?.trim()) {
|
|
453
|
-
const r = await requestHandler.runScript(req.postRequestScript, {
|
|
469
|
+
const r = await requestHandler.runScript(requestHandler.interpolate(req.postRequestScript, vars), {
|
|
454
470
|
envVars: updatedEnvVars,
|
|
455
471
|
collectionVars: updatedCollectionVars,
|
|
456
472
|
globals: updatedGlobals,
|
|
@@ -469,8 +485,19 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
469
485
|
if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [post] ${l}`, C.gray)));
|
|
470
486
|
}
|
|
471
487
|
const allPassed = testResults.every((t) => t.passed);
|
|
472
|
-
const
|
|
473
|
-
const
|
|
488
|
+
const httpFailed = fetchResp.status >= 400;
|
|
489
|
+
const hasTests = testResults.length > 0;
|
|
490
|
+
const status = postScriptError ? "error" : httpFailed ? "failed" : hasTests ? allPassed ? "passed" : "failed" : "skipped";
|
|
491
|
+
if (httpFailed && !testResults.some((t) => !t.passed)) {
|
|
492
|
+
testResults = [
|
|
493
|
+
...testResults,
|
|
494
|
+
{
|
|
495
|
+
name: `HTTP status ${fetchResp.status} ${fetchResp.statusText}`.trim(),
|
|
496
|
+
passed: false,
|
|
497
|
+
error: `Request returned ${fetchResp.status} — no assertion was defined to verify the status code.`
|
|
498
|
+
}
|
|
499
|
+
];
|
|
500
|
+
}
|
|
474
501
|
const reqHeaders = {};
|
|
475
502
|
headers.forEach((v, k) => {
|
|
476
503
|
reqHeaders[k] = v;
|
|
@@ -510,7 +537,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
510
537
|
}
|
|
511
538
|
}
|
|
512
539
|
function printResult(r, verbose) {
|
|
513
|
-
const icon = r.status === "passed" ? color("✓", C.green, C.bold) : r.status === "failed" ? color("✗", C.red, C.bold) : color("⚠", C.yellow, C.bold);
|
|
540
|
+
const icon = r.status === "passed" ? color("✓", C.green, C.bold) : r.status === "failed" ? color("✗", C.red, C.bold) : r.status === "skipped" ? color("○", C.gray, C.bold) : color("⚠", C.yellow, C.bold);
|
|
514
541
|
const http = r.httpStatus ? color(` ${r.httpStatus}`, r.httpStatus < 400 ? C.green : C.red) : "";
|
|
515
542
|
const dur = r.durationMs !== void 0 ? color(` ${r.durationMs}ms`, C.gray) : "";
|
|
516
543
|
const method = color(r.method.padEnd(7), C.cyan);
|
|
@@ -587,7 +614,7 @@ async function main() {
|
|
|
587
614
|
} : void 0
|
|
588
615
|
};
|
|
589
616
|
}
|
|
590
|
-
const summary = { total: 0, passed: 0, failed: 0, errors: 0, durationMs: 0 };
|
|
617
|
+
const summary = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
|
|
591
618
|
const allResults = [];
|
|
592
619
|
const totalStart = Date.now();
|
|
593
620
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -605,6 +632,7 @@ async function main() {
|
|
|
605
632
|
const workspaceTls = workspace.settings?.tls;
|
|
606
633
|
const effectiveTls = col.tls ? { ...workspaceTls, ...col.tls } : workspaceTls;
|
|
607
634
|
let bailed = false;
|
|
635
|
+
let lastPrintedScope = null;
|
|
608
636
|
for (const item of items) {
|
|
609
637
|
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeRequest(
|
|
610
638
|
item.request,
|
|
@@ -619,11 +647,18 @@ async function main() {
|
|
|
619
647
|
runCollectionVars = updatedCollectionVars;
|
|
620
648
|
runGlobals = updatedGlobals;
|
|
621
649
|
runLocalVars = updatedLocalVars;
|
|
650
|
+
result.scopePath = item.scopePath;
|
|
651
|
+
const scopeKey = (item.scopePath ?? []).join(" / ");
|
|
652
|
+
if (scopeKey !== lastPrintedScope) {
|
|
653
|
+
if (scopeKey) console.log(color(` ${scopeKey}`, C.gray, C.bold));
|
|
654
|
+
lastPrintedScope = scopeKey;
|
|
655
|
+
}
|
|
622
656
|
printResult(result, verbose);
|
|
623
657
|
allResults.push(result);
|
|
624
658
|
summary.total++;
|
|
625
659
|
if (result.status === "passed") summary.passed++;
|
|
626
660
|
else if (result.status === "failed") summary.failed++;
|
|
661
|
+
else if (result.status === "skipped") summary.skipped++;
|
|
627
662
|
else summary.errors++;
|
|
628
663
|
if (bail && (result.status === "failed" || result.status === "error")) {
|
|
629
664
|
console.log(color("\n Bailing after first failure.", C.yellow));
|
|
@@ -638,8 +673,9 @@ async function main() {
|
|
|
638
673
|
const passStr = color(`${summary.passed} passed`, C.green, C.bold);
|
|
639
674
|
const failStr = summary.failed > 0 ? color(` · ${summary.failed} failed`, C.red, C.bold) : "";
|
|
640
675
|
const errStr = summary.errors > 0 ? color(` · ${summary.errors} errors`, C.yellow, C.bold) : "";
|
|
676
|
+
const skipStr = summary.skipped > 0 ? color(` · ${summary.skipped} skipped`, C.gray, C.bold) : "";
|
|
641
677
|
const totalStr = color(` · ${summary.total} total · ${summary.durationMs}ms`, C.gray);
|
|
642
|
-
console.log(` ${passStr}${failStr}${errStr}${totalStr}
|
|
678
|
+
console.log(` ${passStr}${failStr}${errStr}${skipStr}${totalStr}
|
|
643
679
|
`);
|
|
644
680
|
if (outputPath) {
|
|
645
681
|
const meta = { workspace: wsPath, environment: env?.name ?? null, collection: firstColName, timestamp };
|
package/out/preload/index.js
CHANGED
|
@@ -36,6 +36,10 @@ const api = {
|
|
|
36
36
|
importOpenApiFromUrl: (url) => electron.ipcRenderer.invoke("import:openapi-url", url),
|
|
37
37
|
importInsomnia: () => electron.ipcRenderer.invoke("import:insomnia"),
|
|
38
38
|
importBruno: () => electron.ipcRenderer.invoke("import:bruno"),
|
|
39
|
+
/** Extract response-body schemas from an OpenAPI spec file (no full import). */
|
|
40
|
+
extractOpenApiSchemas: () => electron.ipcRenderer.invoke("import:openapi-schemas"),
|
|
41
|
+
/** Extract response-body schemas from an OpenAPI spec URL (no full import). */
|
|
42
|
+
extractOpenApiSchemasFromUrl: (url) => electron.ipcRenderer.invoke("import:openapi-schemas-url", url),
|
|
39
43
|
// ─── Code generation ──────────────────────────────────────────────────────
|
|
40
44
|
generateCode: (opts) => electron.ipcRenderer.invoke("generate:code", opts),
|
|
41
45
|
pickOutputDir: () => electron.ipcRenderer.invoke("dialog:pickDir"),
|