@testsmith/api-spector 0.1.3 → 0.1.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/LICENSE +19 -667
- package/out/main/index.js +64 -2
- package/out/main/runner.js +32 -4
- package/out/preload/index.js +2 -0
- package/out/renderer/assets/{index-BEMJvOJV.js → index-D-jqC4fC.js} +9085 -8904
- package/out/renderer/assets/index-Dhvg6Rst.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +5 -5
- package/readme.md +5 -1
- package/out/renderer/assets/index-B46MEuwy.css +0 -2
package/out/main/index.js
CHANGED
|
@@ -2399,8 +2399,56 @@ function registerRunnerHandler(ipc) {
|
|
|
2399
2399
|
let runEnvVars = { ...envVars };
|
|
2400
2400
|
let runCollectionVars = {};
|
|
2401
2401
|
let runGlobals = { ...globals };
|
|
2402
|
+
const failedScopes = /* @__PURE__ */ new Set();
|
|
2403
|
+
const skipRequests = /* @__PURE__ */ new Set();
|
|
2402
2404
|
for (const item of items) {
|
|
2403
|
-
const
|
|
2405
|
+
const { isHook, hookType, scopeId, scopeAncestors, mainRequestId } = item;
|
|
2406
|
+
let skipReason;
|
|
2407
|
+
if (isHook) {
|
|
2408
|
+
if (hookType === "beforeAll") {
|
|
2409
|
+
if ((scopeAncestors ?? []).some((id) => failedScopes.has(id))) {
|
|
2410
|
+
skipReason = "Skipped — outer scope hook failed";
|
|
2411
|
+
}
|
|
2412
|
+
} else if (hookType === "before") {
|
|
2413
|
+
const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
|
|
2414
|
+
if (allScopes.some((id) => failedScopes.has(id))) {
|
|
2415
|
+
skipReason = "Skipped — scope hook failed";
|
|
2416
|
+
} else if (mainRequestId && skipRequests.has(mainRequestId)) {
|
|
2417
|
+
skipReason = "Skipped — before hook failed";
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
} else {
|
|
2421
|
+
const allScopes = [...item.scopeAncestors ?? [], item.scopeId].filter(Boolean);
|
|
2422
|
+
if (allScopes.some((id) => failedScopes.has(id))) {
|
|
2423
|
+
skipReason = "Skipped — beforeAll hook failed";
|
|
2424
|
+
} else if (skipRequests.has(item.request.id)) {
|
|
2425
|
+
skipReason = "Skipped — before hook failed";
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (skipReason) {
|
|
2429
|
+
const skipped = {
|
|
2430
|
+
requestId: item.request.id,
|
|
2431
|
+
name: item.request.name,
|
|
2432
|
+
method: item.request.method,
|
|
2433
|
+
resolvedUrl: item.request.url,
|
|
2434
|
+
status: "failed",
|
|
2435
|
+
error: skipReason,
|
|
2436
|
+
isHook: item.isHook,
|
|
2437
|
+
hookType: item.hookType,
|
|
2438
|
+
scopeId: item.scopeId,
|
|
2439
|
+
iterationLabel: item.iterationLabel
|
|
2440
|
+
};
|
|
2441
|
+
summary.failed++;
|
|
2442
|
+
event.sender.send("runner:progress", skipped);
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
const runningUpdate = {
|
|
2446
|
+
status: "running",
|
|
2447
|
+
iterationLabel: item.iterationLabel,
|
|
2448
|
+
isHook: item.isHook,
|
|
2449
|
+
hookType: item.hookType,
|
|
2450
|
+
scopeId: item.scopeId
|
|
2451
|
+
};
|
|
2404
2452
|
event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
|
|
2405
2453
|
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals } = await executeOne(
|
|
2406
2454
|
item.request,
|
|
@@ -2414,10 +2462,23 @@ function registerRunnerHandler(ipc) {
|
|
|
2414
2462
|
runEnvVars = updatedEnvVars;
|
|
2415
2463
|
runCollectionVars = updatedCollectionVars;
|
|
2416
2464
|
runGlobals = updatedGlobals;
|
|
2465
|
+
if (isHook && result.status !== "passed") {
|
|
2466
|
+
if (hookType === "beforeAll" && scopeId) {
|
|
2467
|
+
failedScopes.add(scopeId);
|
|
2468
|
+
} else if (hookType === "before" && mainRequestId) {
|
|
2469
|
+
skipRequests.add(mainRequestId);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2417
2472
|
if (result.status === "passed") summary.passed++;
|
|
2418
2473
|
else if (result.status === "failed") summary.failed++;
|
|
2419
2474
|
else summary.errors++;
|
|
2420
|
-
event.sender.send("runner:progress", {
|
|
2475
|
+
event.sender.send("runner:progress", {
|
|
2476
|
+
...result,
|
|
2477
|
+
iterationLabel: item.iterationLabel,
|
|
2478
|
+
isHook: item.isHook,
|
|
2479
|
+
hookType: item.hookType,
|
|
2480
|
+
scopeId: item.scopeId
|
|
2481
|
+
});
|
|
2421
2482
|
if (requestDelay > 0 && item !== items[items.length - 1]) {
|
|
2422
2483
|
await sleep(requestDelay);
|
|
2423
2484
|
}
|
|
@@ -3686,6 +3747,7 @@ electron.app.whenReady().then(async () => {
|
|
|
3686
3747
|
registerContractHandlers(electron.ipcMain);
|
|
3687
3748
|
registerGitHandlers(electron.ipcMain);
|
|
3688
3749
|
registerRecordHandlers(electron.ipcMain, () => electron.BrowserWindow.getAllWindows()[0]?.webContents ?? null);
|
|
3750
|
+
electron.ipcMain.handle("shell:openExternal", (_e, url) => electron.shell.openExternal(url));
|
|
3689
3751
|
createWindow();
|
|
3690
3752
|
electron.app.on("activate", () => {
|
|
3691
3753
|
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
|
package/out/main/runner.js
CHANGED
|
@@ -22,6 +22,8 @@ function buildJsonReport(results, summary, meta = {}) {
|
|
|
22
22
|
method: r.method,
|
|
23
23
|
url: r.resolvedUrl,
|
|
24
24
|
status: r.status,
|
|
25
|
+
isHook: r.isHook ?? false,
|
|
26
|
+
hookType: r.hookType ?? null,
|
|
25
27
|
httpStatus: r.httpStatus ?? null,
|
|
26
28
|
durationMs: r.durationMs ?? null,
|
|
27
29
|
iterationLabel: r.iterationLabel ?? null,
|
|
@@ -54,11 +56,18 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
54
56
|
const collection = meta.collection ?? "API Tests";
|
|
55
57
|
const env = meta.environment ?? "—";
|
|
56
58
|
const passRate = summary.total > 0 ? Math.round(summary.passed / summary.total * 100) : 0;
|
|
59
|
+
const HOOK_LABELS = {
|
|
60
|
+
beforeAll: "BEFORE ALL",
|
|
61
|
+
before: "BEFORE",
|
|
62
|
+
after: "AFTER",
|
|
63
|
+
afterAll: "AFTER ALL"
|
|
64
|
+
};
|
|
57
65
|
const cards = results.map((r, idx) => {
|
|
58
66
|
const statusCls = r.status === "passed" ? "badge-pass" : r.status === "failed" ? "badge-fail" : "badge-err";
|
|
59
67
|
const httpCls = r.httpStatus && r.httpStatus < 300 ? "http-ok" : r.httpStatus && r.httpStatus < 400 ? "http-redir" : "http-err";
|
|
60
68
|
const dur = r.durationMs != null ? `${r.durationMs} ms` : "—";
|
|
61
69
|
const label = r.iterationLabel ? ` <span class="muted">#${esc(r.iterationLabel)}</span>` : "";
|
|
70
|
+
const hookLabel = r.isHook && r.hookType ? HOOK_LABELS[r.hookType] ?? r.hookType.toUpperCase() : null;
|
|
62
71
|
const testRows = (r.testResults ?? []).map(
|
|
63
72
|
(t) => `<div class="test-row ${t.passed ? "test-pass" : "test-fail"}">
|
|
64
73
|
<span class="dot">${t.passed ? "✓" : "✗"}</span> ${esc(t.name)}
|
|
@@ -91,12 +100,13 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
91
100
|
${headersTable(resp.headers)}
|
|
92
101
|
${resp.body ? `<div class="section-label">Body</div><pre class="code-block">${prettyJson(resp.body)}</pre>` : ""}
|
|
93
102
|
</div>` : "";
|
|
103
|
+
const hookCls = hookLabel ? r.hookType?.startsWith("before") ? "card-hook-before" : "card-hook-after" : "";
|
|
94
104
|
return `
|
|
95
|
-
<div class="card" id="r${idx}">
|
|
105
|
+
<div class="card ${hookCls}" id="r${idx}">
|
|
96
106
|
<div class="card-header" onclick="toggle(${idx})">
|
|
97
107
|
<span class="chevron" id="ch${idx}">▶</span>
|
|
98
108
|
<span class="badge ${statusCls}">${r.status}</span>
|
|
99
|
-
|
|
109
|
+
${hookLabel ? `<span class="hook-badge ${r.hookType?.startsWith("before") ? "hook-before" : "hook-after"}">${hookLabel}</span>` : `<span class="method mono">${esc(r.method)}</span>`}
|
|
100
110
|
<span class="card-name">${esc(r.name)}${label}</span>
|
|
101
111
|
<span class="card-url muted">${esc(r.resolvedUrl ?? "")}</span>
|
|
102
112
|
<span class="dur muted">${dur}</span>
|
|
@@ -177,6 +187,16 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
177
187
|
.dot { font-weight: 700; }
|
|
178
188
|
/* Console */
|
|
179
189
|
.err-row { color: #f85149; font-size: 12px; }
|
|
190
|
+
/* Footer */
|
|
191
|
+
.report-footer { margin-top: 32px; text-align: center; font-size: 11px; color: #484f58; }
|
|
192
|
+
.footer-link { color: #3d7fb2; text-decoration: none; }
|
|
193
|
+
.footer-link:hover { text-decoration: underline; }
|
|
194
|
+
/* Hook cards */
|
|
195
|
+
.card-hook-before { border-left: 3px solid #7c3aed; }
|
|
196
|
+
.card-hook-after { border-left: 3px solid #0e7490; }
|
|
197
|
+
.hook-badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 10px; font-weight: 700; white-space: nowrap; letter-spacing: .04em; }
|
|
198
|
+
.hook-before { background: #4c1d95; color: #c4b5fd; }
|
|
199
|
+
.hook-after { background: #164e63; color: #67e8f9; }
|
|
180
200
|
</style>
|
|
181
201
|
</head>
|
|
182
202
|
<body>
|
|
@@ -192,6 +212,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
192
212
|
<div class="stat"><div class="stat-val">${summary.durationMs} ms</div><div class="stat-lbl">Duration</div></div>
|
|
193
213
|
</div>
|
|
194
214
|
<div class="cards">${cards}</div>
|
|
215
|
+
<div class="report-footer">Generated by <a href="https://testsmith.io" target="_blank" rel="noopener" class="footer-link">Testsmith</a> · api Spector</div>
|
|
195
216
|
</div>
|
|
196
217
|
<script>
|
|
197
218
|
function toggle(i) {
|
|
@@ -211,9 +232,16 @@ function buildJUnitReport(results, summary, meta = {}) {
|
|
|
211
232
|
const suiteName = esc(meta.collection ?? "API Tests");
|
|
212
233
|
const totalSec = (summary.durationMs / 1e3).toFixed(3);
|
|
213
234
|
const ts = meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
235
|
+
const HOOK_JUNIT_LABELS = {
|
|
236
|
+
beforeAll: "Hook: Before All",
|
|
237
|
+
before: "Hook: Before",
|
|
238
|
+
after: "Hook: After",
|
|
239
|
+
afterAll: "Hook: After All"
|
|
240
|
+
};
|
|
214
241
|
const cases = results.map((r) => {
|
|
215
242
|
const label = r.iterationLabel ? ` #${r.iterationLabel}` : "";
|
|
216
|
-
const
|
|
243
|
+
const hookPrefix = r.isHook && r.hookType ? `[${HOOK_JUNIT_LABELS[r.hookType] ?? "Hook"}] ` : "";
|
|
244
|
+
const name = esc(hookPrefix + r.name + label);
|
|
217
245
|
const classname = esc(`${r.method} ${r.resolvedUrl}`);
|
|
218
246
|
const timeSec = ((r.durationMs ?? 0) / 1e3).toFixed(3);
|
|
219
247
|
const failures = [];
|
|
@@ -251,7 +279,7 @@ function collectTagged(folder, requests, collectionVars, filterTags) {
|
|
|
251
279
|
const results = [];
|
|
252
280
|
for (const reqId of folder.requestIds) {
|
|
253
281
|
const req = requests[reqId];
|
|
254
|
-
if (!req) continue;
|
|
282
|
+
if (!req || req.hookType) continue;
|
|
255
283
|
const tags = req.meta?.tags ?? [];
|
|
256
284
|
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
257
285
|
results.push({ request: req, collectionVars });
|
package/out/preload/index.js
CHANGED
|
@@ -116,6 +116,8 @@ const api = {
|
|
|
116
116
|
offRecordHit: () => {
|
|
117
117
|
electron.ipcRenderer.removeAllListeners("record:hit");
|
|
118
118
|
},
|
|
119
|
+
// ─── Shell ────────────────────────────────────────────────────────────────
|
|
120
|
+
openExternal: (url) => electron.ipcRenderer.invoke("shell:openExternal", url),
|
|
119
121
|
// ─── Zoom ─────────────────────────────────────────────────────────────────
|
|
120
122
|
setZoomFactor: (factor) => electron.webFrame.setZoomFactor(factor),
|
|
121
123
|
// ─── Platform ─────────────────────────────────────────────────────────────
|