@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/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 runningUpdate = { status: "running", iterationLabel: item.iterationLabel };
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", { ...result, iterationLabel: item.iterationLabel });
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();
@@ -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
- <span class="method mono">${esc(r.method)}</span>
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 name = esc(r.name + label);
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 });
@@ -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 ─────────────────────────────────────────────────────────────