@testsmith/api-spector 0.1.3 → 0.1.5

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.
@@ -995,6 +995,7 @@ function registerRequestHandler(ipc) {
995
995
  updatedEnvVars = result.updatedEnvVars;
996
996
  updatedCollectionVars = result.updatedCollectionVars;
997
997
  updatedGlobals = result.updatedGlobals;
998
+ localVars = result.updatedLocalVars;
998
999
  patchGlobals(result.updatedGlobals);
999
1000
  await persistGlobals();
1000
1001
  }
@@ -1004,6 +1005,7 @@ function registerRequestHandler(ipc) {
1004
1005
  updatedEnvVars,
1005
1006
  updatedCollectionVars,
1006
1007
  updatedGlobals,
1008
+ updatedLocalVars: localVars,
1007
1009
  resolvedUrl,
1008
1010
  preScriptError: preScriptMeta.error,
1009
1011
  postScriptError: postError
package/out/main/index.js CHANGED
@@ -25,7 +25,7 @@ const electron = require("electron");
25
25
  const path = require("path");
26
26
  const fs = require("fs");
27
27
  const promises = require("fs/promises");
28
- const requestHandler = require("./chunks/request-handler-CvqESn11.js");
28
+ const requestHandler = require("./chunks/request-handler-BEFvdSz6.js");
29
29
  const uuid = require("uuid");
30
30
  const jsYaml = require("js-yaml");
31
31
  const undici = require("undici");
@@ -2340,6 +2340,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
2340
2340
  updatedEnvVars = r.updatedEnvVars;
2341
2341
  updatedCollectionVars = r.updatedCollectionVars;
2342
2342
  updatedGlobals = r.updatedGlobals;
2343
+ localVars = r.updatedLocalVars;
2343
2344
  requestHandler.patchGlobals(r.updatedGlobals);
2344
2345
  await requestHandler.persistGlobals();
2345
2346
  }
@@ -2369,7 +2370,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
2369
2370
  },
2370
2371
  updatedEnvVars,
2371
2372
  updatedCollectionVars,
2372
- updatedGlobals
2373
+ updatedGlobals,
2374
+ updatedLocalVars: localVars
2373
2375
  };
2374
2376
  } catch (err) {
2375
2377
  return {
@@ -2382,7 +2384,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
2382
2384
  },
2383
2385
  updatedEnvVars,
2384
2386
  updatedCollectionVars,
2385
- updatedGlobals
2387
+ updatedGlobals,
2388
+ updatedLocalVars: localVars
2386
2389
  };
2387
2390
  }
2388
2391
  }
@@ -2399,25 +2402,88 @@ function registerRunnerHandler(ipc) {
2399
2402
  let runEnvVars = { ...envVars };
2400
2403
  let runCollectionVars = {};
2401
2404
  let runGlobals = { ...globals };
2405
+ let runLocalVars = {};
2406
+ const failedScopes = /* @__PURE__ */ new Set();
2407
+ const skipRequests = /* @__PURE__ */ new Set();
2402
2408
  for (const item of items) {
2403
- const runningUpdate = { status: "running", iterationLabel: item.iterationLabel };
2409
+ const { isHook, hookType, scopeId, scopeAncestors, mainRequestId } = item;
2410
+ let skipReason;
2411
+ if (isHook) {
2412
+ if (hookType === "beforeAll") {
2413
+ if ((scopeAncestors ?? []).some((id) => failedScopes.has(id))) {
2414
+ skipReason = "Skipped — outer scope hook failed";
2415
+ }
2416
+ } else if (hookType === "before") {
2417
+ const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
2418
+ if (allScopes.some((id) => failedScopes.has(id))) {
2419
+ skipReason = "Skipped — scope hook failed";
2420
+ } else if (mainRequestId && skipRequests.has(mainRequestId)) {
2421
+ skipReason = "Skipped — before hook failed";
2422
+ }
2423
+ }
2424
+ } else {
2425
+ const allScopes = [...item.scopeAncestors ?? [], item.scopeId].filter(Boolean);
2426
+ if (allScopes.some((id) => failedScopes.has(id))) {
2427
+ skipReason = "Skipped — beforeAll hook failed";
2428
+ } else if (skipRequests.has(item.request.id)) {
2429
+ skipReason = "Skipped — before hook failed";
2430
+ }
2431
+ }
2432
+ if (skipReason) {
2433
+ const skipped = {
2434
+ requestId: item.request.id,
2435
+ name: item.request.name,
2436
+ method: item.request.method,
2437
+ resolvedUrl: item.request.url,
2438
+ status: "failed",
2439
+ error: skipReason,
2440
+ isHook: item.isHook,
2441
+ hookType: item.hookType,
2442
+ scopeId: item.scopeId,
2443
+ iterationLabel: item.iterationLabel
2444
+ };
2445
+ summary.failed++;
2446
+ event.sender.send("runner:progress", skipped);
2447
+ continue;
2448
+ }
2449
+ const runningUpdate = {
2450
+ status: "running",
2451
+ iterationLabel: item.iterationLabel,
2452
+ isHook: item.isHook,
2453
+ hookType: item.hookType,
2454
+ scopeId: item.scopeId
2455
+ };
2404
2456
  event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
2405
- const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals } = await executeOne(
2457
+ const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeOne(
2406
2458
  item.request,
2407
2459
  { ...item.collectionVars, ...runCollectionVars },
2408
2460
  runEnvVars,
2409
2461
  runGlobals,
2410
- item.dataRow ?? {},
2462
+ { ...runLocalVars, ...item.dataRow ?? {} },
2411
2463
  dispatcher,
2412
2464
  piiMaskPatterns
2413
2465
  );
2414
2466
  runEnvVars = updatedEnvVars;
2415
2467
  runCollectionVars = updatedCollectionVars;
2416
2468
  runGlobals = updatedGlobals;
2469
+ runLocalVars = updatedLocalVars;
2470
+ if (isHook && result.status !== "passed") {
2471
+ if (hookType === "beforeAll" && scopeId) {
2472
+ failedScopes.add(scopeId);
2473
+ } else if (hookType === "before" && mainRequestId) {
2474
+ skipRequests.add(mainRequestId);
2475
+ }
2476
+ }
2417
2477
  if (result.status === "passed") summary.passed++;
2418
2478
  else if (result.status === "failed") summary.failed++;
2419
2479
  else summary.errors++;
2420
- event.sender.send("runner:progress", { ...result, iterationLabel: item.iterationLabel });
2480
+ event.sender.send("runner:progress", {
2481
+ ...result,
2482
+ iterationLabel: item.iterationLabel,
2483
+ isHook: item.isHook,
2484
+ hookType: item.hookType,
2485
+ scopeId: item.scopeId
2486
+ });
2421
2487
  if (requestDelay > 0 && item !== items[items.length - 1]) {
2422
2488
  await sleep(requestDelay);
2423
2489
  }
@@ -3686,6 +3752,7 @@ electron.app.whenReady().then(async () => {
3686
3752
  registerContractHandlers(electron.ipcMain);
3687
3753
  registerGitHandlers(electron.ipcMain);
3688
3754
  registerRecordHandlers(electron.ipcMain, () => electron.BrowserWindow.getAllWindows()[0]?.webContents ?? null);
3755
+ electron.ipcMain.handle("shell:openExternal", (_e, url) => electron.shell.openExternal(url));
3689
3756
  createWindow();
3690
3757
  electron.app.on("activate", () => {
3691
3758
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
@@ -3,7 +3,7 @@
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-CvqESn11.js");
6
+ const requestHandler = require("./chunks/request-handler-BEFvdSz6.js");
7
7
  require("crypto");
8
8
  require("dayjs");
9
9
  require("vm");
@@ -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 });
@@ -330,7 +358,7 @@ async function loadEnvironments(workspace, dir) {
330
358
  }
331
359
  return envs;
332
360
  }
333
- async function executeRequest(req, collectionVars, envVars, globals, verbose, tls) {
361
+ async function executeRequest(req, collectionVars, envVars, globals, localVars, verbose, tls) {
334
362
  const base = {
335
363
  requestId: req.id,
336
364
  name: req.name,
@@ -338,7 +366,6 @@ async function executeRequest(req, collectionVars, envVars, globals, verbose, tl
338
366
  resolvedUrl: req.url,
339
367
  status: "running"
340
368
  };
341
- let localVars = {};
342
369
  let updatedEnvVars = { ...envVars };
343
370
  let updatedCollectionVars = { ...collectionVars };
344
371
  let updatedGlobals = { ...globals };
@@ -348,7 +375,7 @@ async function executeRequest(req, collectionVars, envVars, globals, verbose, tl
348
375
  envVars: { ...envVars },
349
376
  collectionVars: { ...collectionVars },
350
377
  globals: { ...globals },
351
- localVars: {}
378
+ localVars: { ...localVars }
352
379
  });
353
380
  preScriptError = r.error;
354
381
  localVars = r.updatedLocalVars;
@@ -433,6 +460,10 @@ async function executeRequest(req, collectionVars, envVars, globals, verbose, tl
433
460
  testResults = r.testResults;
434
461
  consoleOutput = r.consoleOutput;
435
462
  postScriptError = r.error;
463
+ updatedEnvVars = r.updatedEnvVars;
464
+ updatedCollectionVars = r.updatedCollectionVars;
465
+ updatedGlobals = r.updatedGlobals;
466
+ localVars = r.updatedLocalVars;
436
467
  requestHandler.patchGlobals(r.updatedGlobals);
437
468
  await requestHandler.persistGlobals();
438
469
  if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [post] ${l}`, C.gray)));
@@ -445,24 +476,36 @@ async function executeRequest(req, collectionVars, envVars, globals, verbose, tl
445
476
  reqHeaders[k] = v;
446
477
  });
447
478
  return {
448
- ...base,
449
- status,
450
- httpStatus: fetchResp.status,
451
- durationMs,
452
- testResults,
453
- consoleOutput,
454
- preScriptError,
455
- postScriptError,
456
- sentRequest: { headers: reqHeaders, body },
457
- receivedResponse: response
479
+ result: {
480
+ ...base,
481
+ status,
482
+ httpStatus: fetchResp.status,
483
+ durationMs,
484
+ testResults,
485
+ consoleOutput,
486
+ preScriptError,
487
+ postScriptError,
488
+ sentRequest: { headers: reqHeaders, body },
489
+ receivedResponse: response
490
+ },
491
+ updatedEnvVars,
492
+ updatedCollectionVars,
493
+ updatedGlobals,
494
+ updatedLocalVars: localVars
458
495
  };
459
496
  } catch (err) {
460
497
  return {
461
- ...base,
462
- status: "error",
463
- durationMs: Date.now() - start,
464
- error: err instanceof Error ? err.message : String(err),
465
- preScriptError
498
+ result: {
499
+ ...base,
500
+ status: "error",
501
+ durationMs: Date.now() - start,
502
+ error: err instanceof Error ? err.message : String(err),
503
+ preScriptError
504
+ },
505
+ updatedEnvVars,
506
+ updatedCollectionVars,
507
+ updatedGlobals,
508
+ updatedLocalVars: localVars
466
509
  };
467
510
  }
468
511
  }
@@ -554,14 +597,28 @@ async function main() {
554
597
  const items = collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
555
598
  if (items.length === 0) continue;
556
599
  if (!firstColName) firstColName = col.name;
557
- const envVars = await requestHandler.buildEnvVars(env);
558
- const globals = requestHandler.getGlobals();
600
+ let runEnvVars = await requestHandler.buildEnvVars(env);
601
+ let runGlobals = requestHandler.getGlobals();
602
+ let runCollectionVars = { ...col.collectionVariables ?? {} };
603
+ let runLocalVars = {};
559
604
  console.log(color(` ┌ ${col.name}`, C.bold, C.white));
560
605
  const workspaceTls = workspace.settings?.tls;
561
606
  const effectiveTls = col.tls ? { ...workspaceTls, ...col.tls } : workspaceTls;
562
607
  let bailed = false;
563
608
  for (const item of items) {
564
- const result = await executeRequest(item.request, item.collectionVars, envVars, globals, verbose, effectiveTls);
609
+ const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeRequest(
610
+ item.request,
611
+ { ...item.collectionVars, ...runCollectionVars },
612
+ runEnvVars,
613
+ runGlobals,
614
+ { ...runLocalVars },
615
+ verbose,
616
+ effectiveTls
617
+ );
618
+ runEnvVars = updatedEnvVars;
619
+ runCollectionVars = updatedCollectionVars;
620
+ runGlobals = updatedGlobals;
621
+ runLocalVars = updatedLocalVars;
565
622
  printResult(result, verbose);
566
623
  allResults.push(result);
567
624
  summary.total++;
@@ -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 ─────────────────────────────────────────────────────────────