@testsmith/api-spector 0.7.7 → 0.7.8

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.
Files changed (31) hide show
  1. package/bin/cli.js +4 -0
  2. package/out/main/chunks/{broker-client-Df4PpJqB.js → broker-client-BYpWeOWT.js} +4 -1
  3. package/out/main/chunks/{flow-engine-BVzjz4lJ.js → flow-engine-BxwjEy37.js} +66 -1
  4. package/out/main/chunks/{handle-Bz0C56Eq.js → handle-Crv1i9hb.js} +60 -0
  5. package/out/main/chunks/{import-BhGBiW7j.js → import-DTcZMRNu.js} +2 -2
  6. package/out/main/chunks/perf-report-ChaO3K0w.js +124 -0
  7. package/out/main/chunks/perf-run-uZBqnG3S.js +578 -0
  8. package/out/main/chunks/{request-collection-KGPuvN7o.js → request-collection-gkOYlWml.js} +68 -11
  9. package/out/main/chunks/{request-exec-B-F0K-pN.js → request-exec-Cv2jkrO1.js} +439 -12
  10. package/out/main/chunks/run-concurrent-468tH0AX.js +26 -0
  11. package/out/main/chunks/{soap-handler-DqIPcIQL.js → soap-handler-Brr7BPWO.js} +2 -2
  12. package/out/main/contract.js +6 -3
  13. package/out/main/flow.js +8 -5
  14. package/out/main/index.js +1338 -179
  15. package/out/main/lib.js +6 -3
  16. package/out/main/perf-worker.js +45 -0
  17. package/out/main/perf.js +253 -0
  18. package/out/main/runner.js +181 -7
  19. package/out/main/socketio.js +172 -0
  20. package/out/main/wsdl.js +3 -3
  21. package/out/preload/index.js +123 -0
  22. package/out/renderer/assets/{GraphQLEditor-BXpdmpn6.js → GraphQLEditor-Dv2K4wdd.js} +59 -14
  23. package/out/renderer/assets/{SoapEditor-De6WlP6E.js → SoapEditor-Cnz3M4Dz.js} +3 -3
  24. package/out/renderer/assets/ajv-CJPwFjiP.js +6718 -0
  25. package/out/renderer/assets/{codemirror-F6m3Hm1M.js → codemirror-CdIFBPs2.js} +1214 -1207
  26. package/out/renderer/assets/{index-Gcd_t6pX.js → index-CtCdFiRx.js} +15213 -12165
  27. package/out/renderer/assets/index-DkhdKzQJ.css +627 -0
  28. package/out/renderer/index.html +4 -4
  29. package/package.json +12 -6
  30. package/out/renderer/assets/ajv-2MAg6kGF.js +0 -6153
  31. package/out/renderer/assets/index-CYArpqm5.css +0 -627
package/out/main/index.js CHANGED
@@ -22,29 +22,33 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  const electron = require("electron");
25
- const handle = require("./chunks/handle-Bz0C56Eq.js");
25
+ const handle = require("./chunks/handle-Crv1i9hb.js");
26
26
  const path = require("node:path");
27
27
  const fs = require("node:fs");
28
28
  const promises = require("node:fs/promises");
29
29
  const crypto = require("node:crypto");
30
30
  const JSZip = require("jszip");
31
31
  const dataFiles = require("./chunks/data-files-BYqhsoPe.js");
32
- const requestExec = require("./chunks/request-exec-B-F0K-pN.js");
32
+ const requestExec = require("./chunks/request-exec-Cv2jkrO1.js");
33
33
  const ipcValidate = require("./chunks/ipc-validate-sqUFqRmE.js");
34
34
  const uuid = require("uuid");
35
35
  const jsYaml = require("js-yaml");
36
36
  const undici = require("undici");
37
- const requestCollection = require("./chunks/request-collection-KGPuvN7o.js");
38
- const flowEngine = require("./chunks/flow-engine-BVzjz4lJ.js");
37
+ const requestCollection = require("./chunks/request-collection-gkOYlWml.js");
38
+ const runConcurrent = require("./chunks/run-concurrent-468tH0AX.js");
39
+ const flowEngine = require("./chunks/flow-engine-BxwjEy37.js");
39
40
  const mockServer = require("./chunks/mock-server-C4kTFkod.js");
40
41
  const http = require("node:http");
41
42
  const WebSocket = require("ws");
43
+ const socket_ioClient = require("socket.io-client");
44
+ const mqtt = require("mqtt");
45
+ const perfReport = require("./chunks/perf-report-ChaO3K0w.js");
42
46
  const os = require("node:os");
43
47
  const grpc = require("@grpc/grpc-js");
44
48
  const protoLoader = require("@grpc/proto-loader");
45
- const soapHandler = require("./chunks/soap-handler-DqIPcIQL.js");
49
+ const soapHandler = require("./chunks/soap-handler-Brr7BPWO.js");
46
50
  const escape = require("./chunks/escape-DDVAlOvZ.js");
47
- const brokerClient = require("./chunks/broker-client-Df4PpJqB.js");
51
+ const brokerClient = require("./chunks/broker-client-BYpWeOWT.js");
48
52
  const simpleGit = require("simple-git");
49
53
  const recorder = require("./chunks/recorder-BkO8G_mB.js");
50
54
  const electronUpdater = require("electron-updater");
@@ -1257,11 +1261,11 @@ function parseFolder(item, requests, collectionAuth) {
1257
1261
  folders: [],
1258
1262
  requestIds: []
1259
1263
  };
1260
- for (const child of item.item ?? []) {
1261
- if (Array.isArray(child.item)) {
1262
- folder.folders.push(parseFolder(child, requests, collectionAuth));
1264
+ for (const child2 of item.item ?? []) {
1265
+ if (Array.isArray(child2.item)) {
1266
+ folder.folders.push(parseFolder(child2, requests, collectionAuth));
1263
1267
  } else {
1264
- const req = parseRequest(child, collectionAuth);
1268
+ const req = parseRequest(child2, collectionAuth);
1265
1269
  requests[req.id] = req;
1266
1270
  folder.requestIds.push(req.id);
1267
1271
  }
@@ -1415,12 +1419,29 @@ function buildResponseSchema(operation, spec) {
1415
1419
  }
1416
1420
  return void 0;
1417
1421
  }
1418
- function buildBody(operation, spec) {
1419
- const content = resolve(spec, operation.requestBody?.content ?? {});
1420
- if ("application/json" in content) {
1421
- const schema = resolve(spec, content["application/json"].schema ?? {});
1422
- const example = schemaToExample(schema);
1423
- return { mode: "json", json: JSON.stringify(example, null, 2) };
1422
+ function buildBody(rawOperation, params, spec) {
1423
+ const requestBody = resolve(spec, rawOperation?.requestBody ?? {}, /* @__PURE__ */ new Set());
1424
+ const content = requestBody?.content ?? {};
1425
+ const jsonKey = Object.keys(content).find((k) => k.includes("json"));
1426
+ if (jsonKey) {
1427
+ return { mode: "json", json: JSON.stringify(schemaToExample(content[jsonKey].schema ?? {}), null, 2) };
1428
+ }
1429
+ const bodyParam = params.find((p) => p?.in === "body");
1430
+ if (bodyParam) {
1431
+ const schema = resolve(spec, bodyParam.schema ?? {}, /* @__PURE__ */ new Set());
1432
+ return { mode: "json", json: JSON.stringify(schemaToExample(schema), null, 2) };
1433
+ }
1434
+ const formParams = params.filter((p) => p?.in === "formData");
1435
+ if (formParams.length) {
1436
+ return {
1437
+ mode: "form",
1438
+ form: formParams.map((p) => ({
1439
+ key: String(p.name ?? ""),
1440
+ value: String(schemaToExample(p.schema ?? p) ?? ""),
1441
+ enabled: p.required ?? false,
1442
+ description: p.description ?? ""
1443
+ }))
1444
+ };
1424
1445
  }
1425
1446
  return { mode: "none" };
1426
1447
  }
@@ -1463,17 +1484,28 @@ function buildExamples(operation, spec, baseParams, baseHeaders, baseBody) {
1463
1484
  }
1464
1485
  return examples.length ? examples : void 0;
1465
1486
  }
1487
+ function enumChoices(schema) {
1488
+ if (!schema || !Array.isArray(schema.enum) || !schema.enum.length) return void 0;
1489
+ return schema.enum.filter((v) => v !== null && v !== void 0).map((v) => String(v));
1490
+ }
1466
1491
  function buildParams(operation) {
1467
- return (operation.parameters ?? []).filter((p) => p.in === "query").map((p) => ({
1468
- key: p.name,
1469
- value: String(schemaToExample(p.schema ?? {}) ?? ""),
1470
- enabled: p.required ?? false,
1471
- description: p.description ?? ""
1472
- }));
1492
+ return (operation.parameters ?? []).filter((p) => p.in === "query").map((p) => {
1493
+ const schema = p.schema ?? p;
1494
+ const choices = enumChoices(schema);
1495
+ return {
1496
+ key: p.name,
1497
+ // When the schema is an enum, default to its first value so the dropdown
1498
+ // starts on a valid choice; otherwise use the usual example.
1499
+ value: choices ? choices[0] : String(schemaToExample(schema) ?? ""),
1500
+ enabled: p.required ?? false,
1501
+ description: p.description ?? "",
1502
+ ...choices ? { enum: choices } : {}
1503
+ };
1504
+ });
1473
1505
  }
1474
1506
  function buildPathParamRows(operation) {
1475
1507
  return (operation.parameters ?? []).filter((p) => p.in === "path" && p.name).map((p) => {
1476
- const schema = p.schema ?? {};
1508
+ const schema = p.schema ?? p;
1477
1509
  let value;
1478
1510
  if (p.example !== void 0) value = p.example;
1479
1511
  else if (schema.example !== void 0) value = schema.example;
@@ -1492,12 +1524,14 @@ function buildPathParamRows(operation) {
1492
1524
  value = "";
1493
1525
  }
1494
1526
  }
1527
+ const choices = enumChoices(schema);
1495
1528
  return {
1496
1529
  key: String(p.name),
1497
1530
  value: value === null || value === void 0 ? "" : String(value),
1498
1531
  enabled: true,
1499
1532
  description: p.description ?? "",
1500
- paramType: "path"
1533
+ paramType: "path",
1534
+ ...choices ? { enum: choices } : {}
1501
1535
  };
1502
1536
  });
1503
1537
  }
@@ -1534,10 +1568,17 @@ function buildAuth(security, securitySchemes) {
1534
1568
  return { type: "none" };
1535
1569
  }
1536
1570
  const HTTP_METHODS$1 = ["get", "post", "put", "patch", "delete", "head", "options", "query"];
1571
+ function resolveBaseUrl(spec) {
1572
+ if (Array.isArray(spec.servers) && spec.servers[0]?.url) return String(spec.servers[0].url).replace(/\/$/, "");
1573
+ if (spec.host) {
1574
+ const scheme = Array.isArray(spec.schemes) && spec.schemes[0] || "https";
1575
+ return `${scheme}://${spec.host}${spec.basePath ?? ""}`.replace(/\/$/, "");
1576
+ }
1577
+ return "";
1578
+ }
1537
1579
  function buildCollection(spec) {
1538
1580
  const info = spec.info ?? {};
1539
- const servers = spec.servers ?? [{}];
1540
- const baseUrl = servers[0]?.url ?? "";
1581
+ const baseUrl = resolveBaseUrl(spec);
1541
1582
  const securitySchemes = resolve(spec, spec.components?.securitySchemes ?? {});
1542
1583
  const globalSecurity = spec.security ?? [];
1543
1584
  const requests = {};
@@ -1548,23 +1589,23 @@ function buildCollection(spec) {
1548
1589
  }
1549
1590
  for (const [pathStr, pathItem] of Object.entries(spec.paths ?? {})) {
1550
1591
  const resolved = resolve(spec, pathItem);
1551
- const pathLevelParams = resolved.parameters ?? [];
1552
1592
  for (const method of HTTP_METHODS$1) {
1553
1593
  const operation = resolved[method];
1554
1594
  if (!operation) continue;
1555
1595
  const tags = operation.tags?.length ? operation.tags : ["default"];
1556
1596
  const tag = tags[0];
1557
- const allParams = [...pathLevelParams, ...operation.parameters ?? []];
1597
+ const rawOperation = pathItem[method] ?? {};
1598
+ const rawParams = [...pathItem.parameters ?? [], ...rawOperation.parameters ?? []];
1599
+ const allParams = rawParams.map((p) => resolve(spec, p, /* @__PURE__ */ new Set()));
1558
1600
  const opWithParams = { ...operation, parameters: allParams };
1559
1601
  const security = operation.security ?? globalSecurity;
1560
1602
  const params = [
1561
1603
  ...buildPathParamRows(opWithParams),
1562
1604
  ...buildParams(opWithParams)
1563
1605
  ];
1564
- const rawOperation = pathItem[method];
1565
1606
  const responseSchema = buildResponseSchema(rawOperation, spec);
1566
1607
  const headers = buildHeaders(opWithParams);
1567
- const body = buildBody(opWithParams, spec);
1608
+ const body = buildBody(rawOperation, allParams, spec);
1568
1609
  const examples = buildExamples(opWithParams, spec, params, headers, body);
1569
1610
  const req = {
1570
1611
  id: uuid.v4(),
@@ -1576,7 +1617,7 @@ function buildCollection(spec) {
1576
1617
  auth: buildAuth(security, securitySchemes),
1577
1618
  body,
1578
1619
  description: operation.description || operation.summary || "",
1579
- meta: { tags },
1620
+ meta: { tags, ...operation.operationId ? { operationId: String(operation.operationId) } : {} },
1580
1621
  ...responseSchema ? { schema: responseSchema } : {},
1581
1622
  ...examples ? { examples } : {}
1582
1623
  };
@@ -2054,6 +2095,421 @@ async function importHttpFile(filePath) {
2054
2095
  const name = path.basename(filePath).replace(/\.(http|rest)$/i, "") || "Imported HTTP";
2055
2096
  return parseHttpFile(raw, name);
2056
2097
  }
2098
+ const STATIC_EXT = /\.(?:js|mjs|cjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|map|mp4|webm|mp3|wav)(?:\?|#|$)/i;
2099
+ const SKIP_HEADER = /* @__PURE__ */ new Set(["host", "connection", "content-length", "accept-encoding", "cookie"]);
2100
+ function isApiLike(entry) {
2101
+ const rt = (entry._resourceType ?? "").toLowerCase();
2102
+ if (rt) return rt === "xhr" || rt === "fetch";
2103
+ const url = entry.request?.url ?? "";
2104
+ if (STATIC_EXT.test(url)) return false;
2105
+ const ct = (entry.response?.content?.mimeType ?? "").toLowerCase();
2106
+ if (ct.startsWith("image/") || ct.startsWith("font/") || ct.startsWith("audio/") || ct.startsWith("video/")) return false;
2107
+ if (ct.includes("css") || ct.includes("javascript")) return false;
2108
+ return true;
2109
+ }
2110
+ function bodyFrom(postData) {
2111
+ if (!postData) return { mode: "none" };
2112
+ const mime = (postData.mimeType ?? "").toLowerCase();
2113
+ if (postData.params && postData.params.length && mime.includes("x-www-form-urlencoded")) {
2114
+ return { mode: "form", form: postData.params.map((p) => ({ key: p.name, value: p.value ?? "", enabled: true })) };
2115
+ }
2116
+ const text = postData.text ?? "";
2117
+ if (!text.trim()) return { mode: "none" };
2118
+ if (mime.includes("json")) return { mode: "json", json: text };
2119
+ return { mode: "raw", raw: text, rawContentType: postData.mimeType || "text/plain" };
2120
+ }
2121
+ function requestName(method, url) {
2122
+ try {
2123
+ const u = new URL(url);
2124
+ const path2 = u.pathname.replace(/\/+$/, "") || "/";
2125
+ return `${method} ${path2}`;
2126
+ } catch {
2127
+ return `${method} ${url}`;
2128
+ }
2129
+ }
2130
+ function hostOf(url) {
2131
+ try {
2132
+ return new URL(url).host;
2133
+ } catch {
2134
+ return "other";
2135
+ }
2136
+ }
2137
+ function harToCollection(data, name) {
2138
+ const entries = data.log?.entries;
2139
+ if (!Array.isArray(entries)) throw new Error("Not a HAR file (no log.entries array found).");
2140
+ const requests = {};
2141
+ const byHost = /* @__PURE__ */ new Map();
2142
+ const seen = /* @__PURE__ */ new Set();
2143
+ for (const entry of entries) {
2144
+ const r = entry.request;
2145
+ if (!r?.url || !r.method || !isApiLike(entry)) continue;
2146
+ const body = bodyFrom(r.postData);
2147
+ const bodyText = body.mode === "json" ? body.json ?? "" : body.mode === "raw" ? body.raw ?? "" : "";
2148
+ const dedupeKey = `${r.method} ${r.url} ${bodyText}`;
2149
+ if (seen.has(dedupeKey)) continue;
2150
+ seen.add(dedupeKey);
2151
+ const headers = (r.headers ?? []).filter((h) => h.name && !h.name.startsWith(":") && !SKIP_HEADER.has(h.name.toLowerCase())).map((h) => ({ key: h.name, value: h.value ?? "", enabled: true }));
2152
+ const req = {
2153
+ id: uuid.v4(),
2154
+ name: requestName(r.method, r.url),
2155
+ method: r.method,
2156
+ url: r.url,
2157
+ headers,
2158
+ params: [],
2159
+ auth: { type: "none" },
2160
+ body
2161
+ };
2162
+ requests[req.id] = req;
2163
+ const host = hostOf(r.url);
2164
+ const list = byHost.get(host) ?? [];
2165
+ list.push(req.id);
2166
+ byHost.set(host, list);
2167
+ }
2168
+ const folders = [...byHost.entries()].map(([host, requestIds]) => ({
2169
+ id: uuid.v4(),
2170
+ name: host,
2171
+ description: "",
2172
+ folders: [],
2173
+ requestIds
2174
+ }));
2175
+ return {
2176
+ version: "1.0",
2177
+ id: uuid.v4(),
2178
+ name: name || "Imported HAR",
2179
+ description: "",
2180
+ // Single host collapses to a flat list; multiple hosts group into folders.
2181
+ rootFolder: folders.length === 1 ? { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: folders[0].requestIds } : { id: uuid.v4(), name: "root", description: "", folders, requestIds: [] },
2182
+ requests
2183
+ };
2184
+ }
2185
+ async function importHar(filePath) {
2186
+ const raw = await promises.readFile(filePath, "utf8");
2187
+ let data;
2188
+ try {
2189
+ data = JSON.parse(raw);
2190
+ } catch {
2191
+ throw new Error("Not valid JSON — pick a .har file exported from your browser or proxy.");
2192
+ }
2193
+ return harToCollection(data, path.basename(filePath).replace(/\.har$/i, ""));
2194
+ }
2195
+ function jsonPointerToAccessor(pointer) {
2196
+ const parts = pointer.split("/").filter((p) => p !== "");
2197
+ let out = "";
2198
+ for (const raw of parts) {
2199
+ const part = raw.replace(/~1/g, "/").replace(/~0/g, "~");
2200
+ if (/^\d+$/.test(part)) out += `[${part}]`;
2201
+ else if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(part)) out += `.${part}`;
2202
+ else out += `[${JSON.stringify(part)}]`;
2203
+ }
2204
+ return out;
2205
+ }
2206
+ function translateRuntimeExpr(expr, warn) {
2207
+ const e = expr.trim();
2208
+ if (e === "$statusCode") return "sp.response.code";
2209
+ if (e === "$response.body") return "sp.response.json()";
2210
+ let m = /^\$response\.body#(\/.*)$/.exec(e);
2211
+ if (m) return `sp.response.json()${jsonPointerToAccessor(m[1])}`;
2212
+ m = /^\$response\.header[s]?\.(.+)$/.exec(e);
2213
+ if (m) return `sp.response.headers.get(${JSON.stringify(m[1])})`;
2214
+ m = /^\$inputs\.(.+)$/.exec(e);
2215
+ if (m) return `sp.variables.get(${JSON.stringify(m[1])})`;
2216
+ m = /^\$(?:steps|workflows)\.[^.]+\.outputs\.(.+)$/.exec(e);
2217
+ if (m) return `sp.variables.get(${JSON.stringify(m[1])})`;
2218
+ m = /^\$outputs\.(.+)$/.exec(e);
2219
+ if (m) return `sp.variables.get(${JSON.stringify(m[1])})`;
2220
+ warn?.(`Could not translate runtime expression: ${e}`);
2221
+ return `/* TODO: ${e} */ undefined`;
2222
+ }
2223
+ function translateTemplateValue(value, warn) {
2224
+ if (typeof value !== "string") return value == null ? "" : JSON.stringify(value);
2225
+ if (/^\$[A-Za-z]/.test(value)) {
2226
+ const m = /^\$(?:inputs|outputs)\.(.+)$/.exec(value) ?? /^\$(?:steps|workflows)\.[^.]+\.outputs\.(.+)$/.exec(value);
2227
+ if (m) return `{{${m[1]}}}`;
2228
+ warn?.(`Parameter value uses an expression that needs a variable: ${value}`);
2229
+ return value;
2230
+ }
2231
+ return value.replace(/\{\$([^}]+)\}/g, (_full, inner) => {
2232
+ const ref = `$${inner}`;
2233
+ const m = /^\$(?:inputs|outputs)\.(.+)$/.exec(ref) ?? /^\$(?:steps|workflows)\.[^.]+\.outputs\.(.+)$/.exec(ref);
2234
+ if (m) return `{{${m[1]}}}`;
2235
+ warn?.(`Embedded expression left as-is: {$${inner}}`);
2236
+ return `{$${inner}}`;
2237
+ });
2238
+ }
2239
+ function translateCondition(cond, warn) {
2240
+ const translated = cond.replace(/\$[A-Za-z][\w.#/~[\]]*/g, (tok) => `(${translateRuntimeExpr(tok, warn)})`);
2241
+ return translated.replace(/([^=!<>])={2}([^=])/g, "$1===$2").replace(/!={1}([^=])/g, "!==$1");
2242
+ }
2243
+ function buildValidateScript(criteria, warn) {
2244
+ const lines = [];
2245
+ criteria.forEach((c, i) => {
2246
+ const name = `criterion ${i + 1}`;
2247
+ const type = c.type?.type ?? c.type;
2248
+ if (type === "regex") {
2249
+ const ctx = c.context ? translateRuntimeExpr(c.context, warn) : "sp.response.text()";
2250
+ lines.push(`sp.test(${JSON.stringify(name)}, () => { sp.expect(String(${ctx})).to.match(${toRegexLiteral(c.condition)}); });`);
2251
+ } else if (type === "jsonpath") {
2252
+ const ctx = c.context ? translateRuntimeExpr(c.context, warn) : "sp.response.json()";
2253
+ lines.push(`sp.test(${JSON.stringify(name)}, () => { sp.expect(sp.jsonPath(${ctx}, ${JSON.stringify(c.condition)}).length).to.be.above(0); });`);
2254
+ } else if (type === "xpath") {
2255
+ warn?.(`xpath successCriteria left as a TODO: ${c.condition}`);
2256
+ lines.push(`sp.test(${JSON.stringify(name)}, () => { /* TODO xpath: ${c.condition} */ });`);
2257
+ } else {
2258
+ lines.push(`sp.test(${JSON.stringify(name)}, () => { sp.expect(Boolean(${translateCondition(String(c.condition), warn)})).to.equal(true); });`);
2259
+ }
2260
+ });
2261
+ return lines.join("\n");
2262
+ }
2263
+ function toRegexLiteral(pattern) {
2264
+ return `/${String(pattern).replace(/\//g, "\\/")}/`;
2265
+ }
2266
+ function buildOutputsScript(outputs, warn) {
2267
+ return Object.entries(outputs).map(([name, expr]) => `sp.variables.set(${JSON.stringify(name)}, ${typeof expr === "string" && expr.startsWith("$") ? translateRuntimeExpr(expr, warn) : JSON.stringify(expr)});`).join("\n");
2268
+ }
2269
+ function resolveStepOperation(step, ctx) {
2270
+ const candidates = [];
2271
+ const add = (v) => {
2272
+ if (typeof v !== "string" || !v) return;
2273
+ candidates.push(v);
2274
+ const tail = v.split(/[./#]/).filter(Boolean).pop();
2275
+ if (tail && tail !== v) candidates.push(tail);
2276
+ };
2277
+ add(step?.operationId);
2278
+ add(step?.operationPath);
2279
+ add(step?.operationRef);
2280
+ for (const key of candidates) {
2281
+ const id = ctx.opToRequest.get(key);
2282
+ if (id) return id;
2283
+ }
2284
+ return void 0;
2285
+ }
2286
+ function cloneRequestForStep(baseId, step, ctx) {
2287
+ const base = ctx.requests[baseId];
2288
+ const clone = structuredClone(base);
2289
+ clone.id = uuid.v4();
2290
+ clone.name = step.stepId ? String(step.stepId) : clone.name;
2291
+ const warn = (m) => ctx.warnings.push(m);
2292
+ for (const p of step.parameters ?? []) {
2293
+ if (!p || !p.name) continue;
2294
+ const value = translateTemplateValue(p.value, warn);
2295
+ const where = p.in ?? "query";
2296
+ if (where === "header") {
2297
+ const h = clone.headers.find((x) => x.key.toLowerCase() === String(p.name).toLowerCase());
2298
+ if (h) h.value = value;
2299
+ else clone.headers.push({ key: String(p.name), value, enabled: true });
2300
+ } else if (where === "path") {
2301
+ const pp = clone.params.find((x) => x.key === p.name && x.paramType === "path");
2302
+ if (pp) pp.value = value;
2303
+ else clone.params.push({ key: String(p.name), value, enabled: true, paramType: "path" });
2304
+ } else {
2305
+ const qp = clone.params.find((x) => x.key === p.name && x.paramType !== "path");
2306
+ if (qp) qp.value = value;
2307
+ else clone.params.push({ key: String(p.name), value, enabled: true, paramType: "query" });
2308
+ }
2309
+ }
2310
+ if (step.requestBody && step.requestBody.payload !== void 0) {
2311
+ const payload = step.requestBody.payload;
2312
+ const text = translateTemplateValue(
2313
+ typeof payload === "string" ? payload : JSON.stringify(payload, null, 2),
2314
+ warn
2315
+ );
2316
+ clone.body = { ...clone.body, mode: "json", json: text };
2317
+ }
2318
+ ctx.requests[clone.id] = clone;
2319
+ return clone.id;
2320
+ }
2321
+ function buildWorkflowFlow(workflow, ctx) {
2322
+ const nodes = [];
2323
+ const edges = [];
2324
+ const warn = (m) => ctx.warnings.push(m);
2325
+ const node = (type, config, col, row, label) => {
2326
+ const b = { id: uuid.v4(), type, position: { x: 120 + col * 240, y: 80 + row * 120 }, config, label };
2327
+ nodes.push(b);
2328
+ return b;
2329
+ };
2330
+ const link = (source, target, sourcePort) => {
2331
+ edges.push({ id: uuid.v4(), source, target, sourcePort });
2332
+ };
2333
+ const inputProps = workflow.inputs?.properties ?? {};
2334
+ const inputs = Object.entries(inputProps).map(([key, schema]) => ({
2335
+ key,
2336
+ value: schema?.default !== void 0 ? String(schema.default) : ""
2337
+ }));
2338
+ const start = node("start", { inputs }, 0, 0, workflow.workflowId);
2339
+ const end = node("end", {}, (workflow.steps?.length ?? 0) + 1, 0);
2340
+ const stepFirstBlock = /* @__PURE__ */ new Map();
2341
+ const stepExit = [];
2342
+ (workflow.steps ?? []).forEach((step, i) => {
2343
+ const col = i + 1;
2344
+ let requestId;
2345
+ const baseId = resolveStepOperation(step, ctx);
2346
+ if (baseId) {
2347
+ requestId = cloneRequestForStep(baseId, step, ctx);
2348
+ } else {
2349
+ const ref = step.operationId ?? step.operationPath ?? step.operationRef ?? step.workflowId ?? "(unknown)";
2350
+ warn(`Step "${step.stepId}" references ${ref}, which was not found among the imported operations. Check that the OpenAPI spec defines that operationId.`);
2351
+ }
2352
+ const reqBlock = node("request", requestId ? { ref: { collectionId: ctx.collectionId, requestId } } : {}, col, 0, step.stepId);
2353
+ stepFirstBlock.set(String(step.stepId), reqBlock.id);
2354
+ let exitId = reqBlock.id;
2355
+ let exitPort = "success";
2356
+ if (Array.isArray(step.successCriteria) && step.successCriteria.length) {
2357
+ const v = node("validate", { script: buildValidateScript(step.successCriteria, warn) }, col, 1, `${step.stepId} checks`);
2358
+ link(exitId, v.id, exitPort);
2359
+ exitId = v.id;
2360
+ exitPort = "pass";
2361
+ }
2362
+ if (step.outputs && Object.keys(step.outputs).length) {
2363
+ const ev = node("evaluate", { script: buildOutputsScript(step.outputs, warn) }, col, 2, `${step.stepId} outputs`);
2364
+ link(exitId, ev.id, exitPort);
2365
+ exitId = ev.id;
2366
+ exitPort = "out";
2367
+ }
2368
+ stepExit.push({ stepId: String(step.stepId), blockId: exitId, requestId: reqBlock.id });
2369
+ const onFailure = step.onFailure ?? [];
2370
+ const failGoto = onFailure.find((a) => a.type === "goto" && a.stepId);
2371
+ if (failGoto) {
2372
+ step.__failGoto = failGoto.stepId;
2373
+ } else {
2374
+ link(reqBlock.id, end.id, "fail");
2375
+ }
2376
+ });
2377
+ const steps = workflow.steps ?? [];
2378
+ steps.forEach((step, i) => {
2379
+ const exit = stepExit[i];
2380
+ if (!exit) return;
2381
+ const exitPort = exit.blockId === exit.requestId ? "success" : nodes.find((n) => n.id === exit.blockId)?.type === "validate" ? "pass" : "out";
2382
+ const onSuccess = step.onSuccess ?? [];
2383
+ const sucEnd = onSuccess.find((a) => a.type === "end");
2384
+ const sucGoto = onSuccess.find((a) => a.type === "goto" && a.stepId);
2385
+ if (sucEnd) {
2386
+ link(exit.blockId, end.id, exitPort);
2387
+ } else if (sucGoto && stepFirstBlock.has(String(sucGoto.stepId))) {
2388
+ link(exit.blockId, stepFirstBlock.get(String(sucGoto.stepId)), exitPort);
2389
+ } else if (i + 1 < steps.length && stepExit[i + 1]) {
2390
+ link(exit.blockId, stepFirstBlock.get(String(steps[i + 1].stepId)), exitPort);
2391
+ } else {
2392
+ link(exit.blockId, end.id, exitPort);
2393
+ }
2394
+ if (step.__failGoto && stepFirstBlock.has(String(step.__failGoto))) {
2395
+ link(exit.requestId, stepFirstBlock.get(String(step.__failGoto)), "fail");
2396
+ }
2397
+ });
2398
+ const firstStepId = steps[0]?.stepId;
2399
+ link(start.id, firstStepId ? stepFirstBlock.get(String(firstStepId)) : end.id);
2400
+ return {
2401
+ version: "1.0",
2402
+ id: uuid.v4(),
2403
+ name: String(workflow.workflowId ?? workflow.summary ?? "Imported workflow"),
2404
+ description: workflow.summary ?? workflow.description ?? "",
2405
+ nodes,
2406
+ edges
2407
+ };
2408
+ }
2409
+ async function importSource(source, baseDir, sourcePaths, allowSingleFallback) {
2410
+ const url = typeof source?.url === "string" ? source.url : "";
2411
+ if (/^https?:\/\//i.test(url)) {
2412
+ try {
2413
+ return await importOpenApiFromUrl(url);
2414
+ } catch {
2415
+ }
2416
+ } else if (url) {
2417
+ try {
2418
+ return await importOpenApi(path.resolve(baseDir, url));
2419
+ } catch {
2420
+ }
2421
+ }
2422
+ if (url) {
2423
+ const base = url.split(/[\\/]/).pop();
2424
+ const match = sourcePaths.find((p) => path.basename(p) === base);
2425
+ if (match) {
2426
+ try {
2427
+ return await importOpenApi(match);
2428
+ } catch {
2429
+ }
2430
+ }
2431
+ }
2432
+ if (allowSingleFallback && sourcePaths.length === 1) {
2433
+ try {
2434
+ return await importOpenApi(sourcePaths[0]);
2435
+ } catch {
2436
+ }
2437
+ }
2438
+ return null;
2439
+ }
2440
+ function mergeSourceIntoCollection(col, source, folderName, opMap) {
2441
+ for (const [id, req] of Object.entries(source.requests)) {
2442
+ col.requests[id] = req;
2443
+ const opId = req.meta?.operationId;
2444
+ if (opId) {
2445
+ opMap.set(opId, id);
2446
+ opMap.set(`${folderName}.${opId}`, id);
2447
+ }
2448
+ }
2449
+ col.rootFolder.folders.push({
2450
+ id: uuid.v4(),
2451
+ name: folderName,
2452
+ description: source.description ?? "",
2453
+ folders: source.rootFolder.folders,
2454
+ requestIds: source.rootFolder.requestIds
2455
+ });
2456
+ }
2457
+ async function importArazzoDoc(doc, baseDir, sourcePaths = []) {
2458
+ const warnings = [];
2459
+ const title = doc?.info?.title ?? "Arazzo import";
2460
+ const collection = {
2461
+ version: "1.0",
2462
+ id: uuid.v4(),
2463
+ name: String(title),
2464
+ description: doc?.info?.summary ?? doc?.info?.description ?? "",
2465
+ rootFolder: { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: [] },
2466
+ requests: {}
2467
+ };
2468
+ const opToRequest = /* @__PURE__ */ new Map();
2469
+ const sources = Array.isArray(doc?.sourceDescriptions) ? doc.sourceDescriptions : [];
2470
+ const singleSource = sources.length === 1;
2471
+ for (const source of sources) {
2472
+ const imported = await importSource(source, baseDir, sourcePaths, singleSource);
2473
+ if (!imported) {
2474
+ warnings.push(`Could not load the OpenAPI spec for source "${source?.name ?? source?.url ?? "(unnamed)"}". Import the Arazzo file together with its OpenAPI spec file(s), or make the sourceDescription url reachable.`);
2475
+ continue;
2476
+ }
2477
+ mergeSourceIntoCollection(collection, imported, String(source?.name ?? "source"), opToRequest);
2478
+ }
2479
+ const ctx = { collectionId: collection.id, opToRequest, requests: collection.requests, warnings };
2480
+ const workflows = Array.isArray(doc?.workflows) ? doc.workflows : [];
2481
+ const flows = workflows.map((wf) => buildWorkflowFlow(wf, ctx));
2482
+ if (flows.length === 0) warnings.push("The Arazzo document defined no workflows.");
2483
+ return { collection, flows, warnings };
2484
+ }
2485
+ async function loadDoc(filePath) {
2486
+ const raw = await promises.readFile(filePath, "utf8");
2487
+ return filePath.endsWith(".yaml") || filePath.endsWith(".yml") ? jsYaml.load(raw) : JSON.parse(raw);
2488
+ }
2489
+ function looksLikeArazzo(doc) {
2490
+ return Boolean(doc && (doc.arazzo || Array.isArray(doc.workflows)));
2491
+ }
2492
+ async function importArazzo(filePath, sourcePaths = []) {
2493
+ const doc = await loadDoc(filePath);
2494
+ return importArazzoDoc(doc, path.dirname(filePath), sourcePaths);
2495
+ }
2496
+ async function importArazzoFiles(paths) {
2497
+ let arazzoPath;
2498
+ const sourcePaths = [];
2499
+ for (const p of paths) {
2500
+ let doc;
2501
+ try {
2502
+ doc = await loadDoc(p);
2503
+ } catch {
2504
+ sourcePaths.push(p);
2505
+ continue;
2506
+ }
2507
+ if (!arazzoPath && looksLikeArazzo(doc)) arazzoPath = p;
2508
+ else sourcePaths.push(p);
2509
+ }
2510
+ if (!arazzoPath) throw new Error('No Arazzo workflow document was found among the selected files (expected an "arazzo" field or "workflows").');
2511
+ return importArazzo(arazzoPath, sourcePaths);
2512
+ }
2057
2513
  function registerImportHandlers(ipc) {
2058
2514
  handle.handleIpc(ipc, handle.IPC.import.postman, async () => {
2059
2515
  const result = await electron.dialog.showOpenDialog({
@@ -2112,6 +2568,24 @@ function registerImportHandlers(ipc) {
2112
2568
  if (result.canceled || !result.filePaths[0]) return null;
2113
2569
  return importHttpFile(result.filePaths[0]);
2114
2570
  });
2571
+ handle.handleIpc(ipc, handle.IPC.import.har, async () => {
2572
+ const result = await electron.dialog.showOpenDialog({
2573
+ title: "Import HTTP Archive (.har)",
2574
+ filters: [{ name: "HTTP Archive", extensions: ["har", "json"] }],
2575
+ properties: ["openFile"]
2576
+ });
2577
+ if (result.canceled || !result.filePaths[0]) return null;
2578
+ return importHar(result.filePaths[0]);
2579
+ });
2580
+ handle.handleIpc(ipc, handle.IPC.import.arazzo, async () => {
2581
+ const result = await electron.dialog.showOpenDialog({
2582
+ title: "Import Arazzo workflow — select the .arazzo file and its OpenAPI spec file(s)",
2583
+ filters: [{ name: "Arazzo + OpenAPI", extensions: ["arazzo", "yaml", "yml", "json"] }],
2584
+ properties: ["openFile", "multiSelections"]
2585
+ });
2586
+ if (result.canceled || result.filePaths.length === 0) return null;
2587
+ return importArazzoFiles(result.filePaths);
2588
+ });
2115
2589
  handle.handleIpc(ipc, handle.IPC.import.spector, async () => {
2116
2590
  const result = await electron.dialog.showOpenDialog({
2117
2591
  title: "Import API Spector Collection",
@@ -2189,6 +2663,11 @@ function parsePostScript(script) {
2189
2663
  if (statusMatch) {
2190
2664
  assertions.push({ name: `status is ${statusMatch[1]}`, accessor: "", kind: "status", expected: statusMatch[1] });
2191
2665
  }
2666
+ const jsonPathLocals = /* @__PURE__ */ new Map();
2667
+ const localRegex = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*sp\.jsonPath\(\s*[^,]+,\s*(['"])((?:(?!\2).|\\.)*)\2\s*\)\s*;?/g;
2668
+ while (match = localRegex.exec(script)) {
2669
+ jsonPathLocals.set(match[1], match[3]);
2670
+ }
2192
2671
  const extractRegex = /sp\.(variables|environment|globals)\.set\(\s*"([^"]+)"\s*,\s*(.+)\s*\)\s*;/g;
2193
2672
  while (match = extractRegex.exec(script)) {
2194
2673
  const target = match[1];
@@ -2196,9 +2675,42 @@ function parsePostScript(script) {
2196
2675
  let accessor = match[3].trim();
2197
2676
  const stringWrap = /^String\((.+)\)$/.exec(accessor);
2198
2677
  if (stringWrap) accessor = stringWrap[1];
2199
- extractions.push({ varName, accessor, target });
2678
+ const jsonPath = parseJsonPathAccessor(accessor, jsonPathLocals);
2679
+ if (jsonPath) {
2680
+ extractions.push({ varName, accessor: "", target, jsonPath });
2681
+ } else {
2682
+ extractions.push({ varName, accessor, target });
2683
+ }
2200
2684
  }
2201
- return { assertions, extractions };
2685
+ return {
2686
+ assertions: assertions.filter((a) => a.kind === "status" || isSimpleAccessor(a.accessor)),
2687
+ extractions: extractions.filter((e) => e.jsonPath || isSimpleAccessor(e.accessor))
2688
+ };
2689
+ }
2690
+ function parseJsonPathAccessor(accessor, locals) {
2691
+ let core = accessor.trim();
2692
+ let fallback = null;
2693
+ const fbMatch = /\?\?\s*(.+)$/.exec(core);
2694
+ if (fbMatch) {
2695
+ fallback = fbMatch[1].trim().replace(/^(['"])(.*)\1$/, "$2");
2696
+ core = core.slice(0, fbMatch.index).trim();
2697
+ }
2698
+ let index = null;
2699
+ const idxMatch = /\[(\d+)\]$/.exec(core);
2700
+ if (idxMatch) {
2701
+ index = Number.parseInt(idxMatch[1], 10);
2702
+ core = core.slice(0, idxMatch.index).trim();
2703
+ }
2704
+ const inline = /^sp\.jsonPath\(\s*[^,]+,\s*(['"])((?:(?!\1).|\\.)*)\1\s*\)$/.exec(core);
2705
+ if (inline) return { query: inline[2], index, fallback };
2706
+ if (locals.has(core)) return { query: locals.get(core), index, fallback };
2707
+ return null;
2708
+ }
2709
+ function isSimpleAccessor(accessor) {
2710
+ if (!accessor) return false;
2711
+ if (/\?\?|\|\||&&|=>|[`]|\bfunction\b/.test(accessor)) return false;
2712
+ const withoutBracketKeys = accessor.replaceAll(/\[(['"]).*?\1\]/g, "");
2713
+ return !/\s/.test(withoutBracketKeys);
2202
2714
  }
2203
2715
  function accessorToJsonPath(accessor) {
2204
2716
  return accessor.replace(/^json\.?/, "").replaceAll(/\["([^"]+)"\]/g, "['$1']");
@@ -2301,6 +2813,29 @@ function restAssuredCall(method, pathArg) {
2301
2813
  return verbs.includes(method) ? `.${method}(${pathArg})` : `.request("${method.toUpperCase()}", ${pathArg})`;
2302
2814
  }
2303
2815
  const ROBOT_REQUESTS_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
2816
+ function jsJsonPathExpr(q, jsonVar) {
2817
+ const base = `JSONPath({ path: ${JSON.stringify(q.query)}, json: ${jsonVar} })`;
2818
+ const picked = q.index !== null ? `${base}[${q.index}]` : base;
2819
+ if (q.fallback !== null) return `(${picked} ?? ${JSON.stringify(q.fallback)})`;
2820
+ return picked;
2821
+ }
2822
+ function karateJsonPathExpr(q) {
2823
+ const base = `karate.jsonPath(response, '${q.query.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}')`;
2824
+ const picked = q.index !== null ? `${base}[${q.index}]` : base;
2825
+ if (q.fallback !== null) return `(${picked} || '${q.fallback.replaceAll("'", "\\'")}')`;
2826
+ return picked;
2827
+ }
2828
+ function collectionUsesJsonPath(collection) {
2829
+ return Object.values(collection.requests).some(
2830
+ (req) => parsePostScript(req.postRequestScript).extractions.some((e) => e.jsonPath)
2831
+ );
2832
+ }
2833
+ function folderUsesJsonPath(folder, collection) {
2834
+ return folder.requestIds.some((id) => {
2835
+ const req = collection.requests[id];
2836
+ return !!req && parsePostScript(req.postRequestScript).extractions.some((e) => e.jsonPath);
2837
+ });
2838
+ }
2304
2839
  function safeName(name) {
2305
2840
  return name.replaceAll(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2306
2841
  }
@@ -2354,6 +2889,33 @@ function buildVariablesFile(environment) {
2354
2889
  }
2355
2890
  return lines.join("\n") + "\n";
2356
2891
  }
2892
+ function compactJsonBody(json) {
2893
+ try {
2894
+ return JSON.stringify(JSON.parse(json));
2895
+ } catch {
2896
+ return json.replace(/\s*\r?\n\s*/g, " ").trim();
2897
+ }
2898
+ }
2899
+ function isNestedPath(jp) {
2900
+ return /[.[]/.test(jp);
2901
+ }
2902
+ function pyIndex(jp) {
2903
+ return jp.replaceAll(/\.([A-Za-z_$][\w$]*)/g, "['$1']").replace(/^([A-Za-z_$][\w$]*)/, "['$1']");
2904
+ }
2905
+ function jsonPathPlaceholder(name, query) {
2906
+ return [
2907
+ ` # JSONPath not auto-translated to Robot: ${query}`,
2908
+ ` # Set \${${name}} manually, or use a JSONPath library and adapt the query to jsonpath-ng syntax.`,
2909
+ ` VAR \${${name}} \${EMPTY}`,
2910
+ ` Set Suite Variable \${${name}}`
2911
+ ];
2912
+ }
2913
+ function readJsonValue(name, jp) {
2914
+ if (isNestedPath(jp)) {
2915
+ return [` \${${name}}= Evaluate str($response.json()${pyIndex(jp)})`];
2916
+ }
2917
+ return [` \${${name}}= Get From Dictionary \${response.json()} ${jp}`];
2918
+ }
2357
2919
  function jsonToRfDictPairs(json, vars) {
2358
2920
  try {
2359
2921
  const parsed = JSON.parse(json);
@@ -2393,7 +2955,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
2393
2955
  if (bodyPairs !== null) {
2394
2956
  lines.push(` VAR &{body} ${bodyPairs}`);
2395
2957
  } else {
2396
- lines.push(` VAR \${body} ${interpolate(body.json, varMap)}`);
2958
+ lines.push(` VAR \${body} ${interpolate(compactJsonBody(body.json), varMap)}`);
2397
2959
  }
2398
2960
  }
2399
2961
  const callArgs = [];
@@ -2402,10 +2964,13 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
2402
2964
  if (callArgs.length) lines.push(` ... ${callArgs.join(" ")}`);
2403
2965
  const parsed = parsePostScript(req.postRequestScript);
2404
2966
  for (const e of parsed.extractions) {
2405
- const jp = accessorToJsonPath(e.accessor);
2406
2967
  const rfVar = e.varName.replaceAll(/\W+/g, "_").toUpperCase();
2407
2968
  hookExtractedVars.add(rfVar);
2408
- lines.push(` \${${rfVar}}= Evaluate str($response.json().get('${jp}', ''))`, ` Set Suite Variable \${${rfVar}}`);
2969
+ if (e.jsonPath) {
2970
+ lines.push(...jsonPathPlaceholder(rfVar, e.jsonPath.query));
2971
+ } else {
2972
+ lines.push(...readJsonValue(rfVar, accessorToJsonPath(e.accessor)), ` Set Suite Variable \${${rfVar}}`);
2973
+ }
2409
2974
  }
2410
2975
  lines.push(` RETURN \${response}`, "");
2411
2976
  }
@@ -2468,7 +3033,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
2468
3033
  if (bodyPairs !== null) {
2469
3034
  lines.push(` VAR &{body} ${bodyPairs}`);
2470
3035
  } else {
2471
- lines.push(` VAR \${body} ${interpolate(body.json, varMap)}`);
3036
+ lines.push(` VAR \${body} ${interpolate(compactJsonBody(body.json), varMap)}`);
2472
3037
  }
2473
3038
  }
2474
3039
  const method = req.method.charAt(0) + req.method.slice(1).toLowerCase();
@@ -2490,6 +3055,9 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
2490
3055
  function buildTestSuite(collection, environment, nameMap) {
2491
3056
  const colName = safeName(collection.name);
2492
3057
  const envName = environment?.name ?? "default";
3058
+ const varMap = new Map(
3059
+ (environment?.variables ?? []).map((v) => [v.key, v])
3060
+ );
2493
3061
  const beforeAllHooks = [];
2494
3062
  const afterAllHooks = [];
2495
3063
  function collectAll(folder) {
@@ -2528,17 +3096,21 @@ function buildTestSuite(collection, environment, nameMap) {
2528
3096
  lines.push(` Status Should Be ${a.expected ?? 200} \${response}`);
2529
3097
  break;
2530
3098
  case "equals": {
2531
- const expected = a.expected?.replaceAll(/^"|"$/g, "") ?? "";
2532
- lines.push(` \${value}= Get From Dictionary \${response.json()} ${jp}`, ` Should Be Equal As Strings \${value} ${expected}`);
3099
+ const expected = interpolate(a.expected?.replaceAll(/^"|"$/g, "") ?? "", varMap);
3100
+ lines.push(...readJsonValue("value", jp), ` Should Be Equal As Strings \${value} ${expected}`);
2533
3101
  break;
2534
3102
  }
2535
3103
  case "contains": {
2536
- const expected = a.expected?.replaceAll(/^"|"$/g, "") ?? "";
2537
- lines.push(` \${value}= Get From Dictionary \${response.json()} ${jp}`, ` Should Contain \${value} ${expected}`);
3104
+ const expected = interpolate(a.expected?.replaceAll(/^"|"$/g, "") ?? "", varMap);
3105
+ lines.push(...readJsonValue("value", jp), ` Should Contain \${value} ${expected}`);
2538
3106
  break;
2539
3107
  }
2540
3108
  case "exists":
2541
- lines.push(` Dictionary Should Contain Key \${response.json()} ${jp}`);
3109
+ if (isNestedPath(jp)) {
3110
+ lines.push(` \${value}= Evaluate $response.json()${pyIndex(jp)}`, ` Should Not Be Equal \${value} \${None}`);
3111
+ } else {
3112
+ lines.push(` Dictionary Should Contain Key \${response.json()} ${jp}`);
3113
+ }
2542
3114
  break;
2543
3115
  default:
2544
3116
  lines.push(` Status Should Be 200 \${response}`);
@@ -2548,8 +3120,11 @@ function buildTestSuite(collection, environment, nameMap) {
2548
3120
  lines.push(` Status Should Be 200 \${response}`);
2549
3121
  }
2550
3122
  for (const e of parsed.extractions) {
2551
- const jp = accessorToJsonPath(e.accessor);
2552
- lines.push(` \${${e.varName}}= Get From Dictionary \${response.json()} ${jp}`, ` Set Suite Variable \${${e.varName}}`);
3123
+ if (e.jsonPath) {
3124
+ lines.push(...jsonPathPlaceholder(e.varName, e.jsonPath.query));
3125
+ } else {
3126
+ lines.push(...readJsonValue(e.varName, accessorToJsonPath(e.accessor)), ` Set Suite Variable \${${e.varName}}`);
3127
+ }
2553
3128
  }
2554
3129
  lines.push("");
2555
3130
  }
@@ -2658,8 +3233,7 @@ function buildHookLines$1(req, sharedVars) {
2658
3233
  if (parsed.extractions.length > 0) {
2659
3234
  lines.push(` const hookResponse = await ${hookCall};`, ` const hookJson = await hookResponse.json();`);
2660
3235
  for (const e of parsed.extractions) {
2661
- const jsonPath = e.accessor.replace(/^json\.?/, "");
2662
- const expr = jsonPath ? `hookJson.${jsonPath}` : "hookJson";
3236
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookJson") : e.accessor.replace(/^json\.?/, "") ? `hookJson.${e.accessor.replace(/^json\.?/, "")}` : "hookJson";
2663
3237
  const varName = toEnvVar(e.varName);
2664
3238
  sharedVars.add(varName);
2665
3239
  lines.push(` ${varName} = String(${expr});`);
@@ -2817,14 +3391,15 @@ ${optionParts.join(",\n")},
2817
3391
  }
2818
3392
  for (const e of parsed.extractions) {
2819
3393
  const path22 = e.accessor.replace(/^json\.?/, "");
2820
- const expr = path22 ? `json.${path22}` : "json";
3394
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "json") : path22 ? `json.${path22}` : "json";
2821
3395
  lines.push(` // Extract: ${e.varName} = ${expr}`, ` process.env.${toEnvVar(e.varName)} = String(${expr});`);
2822
3396
  }
2823
3397
  lines.push(` });`);
2824
3398
  tests.push(lines.join("\n"));
2825
3399
  }
3400
+ const jsonPathImport = folderUsesJsonPath(folder, collection) ? "import { JSONPath } from 'jsonpath-plus'\n" : "";
2826
3401
  return `import { test, expect } from '@playwright/test'
2827
-
3402
+ ${jsonPathImport}
2828
3403
  test.describe('${folderName}', () => {
2829
3404
 
2830
3405
  ${tests.join("\n\n")}
@@ -2832,7 +3407,7 @@ ${tests.join("\n\n")}
2832
3407
  })
2833
3408
  `;
2834
3409
  }
2835
- function buildPackageJson$3(collectionName) {
3410
+ function buildPackageJson$3(collectionName, usesJsonPath) {
2836
3411
  const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
2837
3412
  return JSON.stringify({
2838
3413
  name: `${name}-api-tests`,
@@ -2845,6 +3420,7 @@ function buildPackageJson$3(collectionName) {
2845
3420
  devDependencies: {
2846
3421
  "@playwright/test": "^1.44.0",
2847
3422
  dotenv: "^16.4.0",
3423
+ ...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {},
2848
3424
  typescript: "^5.4.0"
2849
3425
  }
2850
3426
  }, null, 2) + "\n";
@@ -2879,11 +3455,10 @@ function generatePlaywright(collection, environment) {
2879
3455
  }
2880
3456
  for (const sub of folder.folders) processFolder(sub, sub.name);
2881
3457
  }
2882
- if (collection.rootFolder.requestIds.length > 0) processFolder(collection.rootFolder, collection.name);
2883
- for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
3458
+ processFolder(collection.rootFolder, collection.name);
2884
3459
  const scaffoldPaths = ["package.json", "playwright.config.ts", ...files.map((f) => f.path)];
2885
3460
  files.unshift(
2886
- { path: "package.json", content: buildPackageJson$3(collection.name) },
3461
+ { path: "package.json", content: buildPackageJson$3(collection.name, collectionUsesJsonPath(collection)) },
2887
3462
  { path: "playwright.config.ts", content: buildPlaywrightConfig$1(environment) },
2888
3463
  { path: "README.md", content: buildReadme$5(collection.name, scaffoldPaths) }
2889
3464
  );
@@ -2941,7 +3516,7 @@ function buildHookLines(req, sharedVars) {
2941
3516
  lines.push(` const hookResponse = await ${hookCall};`, ` const hookJson = await hookResponse.json();`);
2942
3517
  for (const e of parsed.extractions) {
2943
3518
  const jp = e.accessor.replace(/^json\.?/, "");
2944
- const expr = jp ? `hookJson.${jp}` : "hookJson";
3519
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookJson") : jp ? `hookJson.${jp}` : "hookJson";
2945
3520
  const varName = toEnvVar(e.varName);
2946
3521
  sharedVars.add(varName);
2947
3522
  lines.push(` ${varName} = String(${expr});`);
@@ -3090,14 +3665,15 @@ ${optionParts.join(",\n")},
3090
3665
  }
3091
3666
  for (const e of parsed.extractions) {
3092
3667
  const path22 = e.accessor.replace(/^json\.?/, "");
3093
- const expr = path22 ? `json.${path22}` : "json";
3668
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "json") : path22 ? `json.${path22}` : "json";
3094
3669
  lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
3095
3670
  }
3096
3671
  lines.push(` });`);
3097
3672
  tests.push(lines.join("\n"));
3098
3673
  }
3674
+ const jsonPathImport = folderUsesJsonPath(folder, collection) ? "const { JSONPath } = require('jsonpath-plus');\n" : "";
3099
3675
  return `const { test, expect } = require('@playwright/test');
3100
-
3676
+ ${jsonPathImport}
3101
3677
  test.describe('${folderName}', () => {
3102
3678
 
3103
3679
  ${tests.join("\n\n")}
@@ -3105,7 +3681,7 @@ ${tests.join("\n\n")}
3105
3681
  });
3106
3682
  `;
3107
3683
  }
3108
- function buildPackageJson$2(collectionName) {
3684
+ function buildPackageJson$2(collectionName, usesJsonPath) {
3109
3685
  const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
3110
3686
  return JSON.stringify({
3111
3687
  name: `${name}-api-tests`,
@@ -3117,7 +3693,8 @@ function buildPackageJson$2(collectionName) {
3117
3693
  },
3118
3694
  devDependencies: {
3119
3695
  "@playwright/test": "^1.44.0",
3120
- dotenv: "^16.4.0"
3696
+ dotenv: "^16.4.0",
3697
+ ...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {}
3121
3698
  }
3122
3699
  }, null, 2) + "\n";
3123
3700
  }
@@ -3151,11 +3728,10 @@ function generatePlaywrightJs(collection, environment) {
3151
3728
  }
3152
3729
  for (const sub of folder.folders) processFolder(sub, sub.name);
3153
3730
  }
3154
- if (collection.rootFolder.requestIds.length > 0) processFolder(collection.rootFolder, collection.name);
3155
- for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
3731
+ processFolder(collection.rootFolder, collection.name);
3156
3732
  const scaffoldPaths = ["package.json", "playwright.config.js", ...files.map((f) => f.path)];
3157
3733
  files.unshift(
3158
- { path: "package.json", content: buildPackageJson$2(collection.name) },
3734
+ { path: "package.json", content: buildPackageJson$2(collection.name, collectionUsesJsonPath(collection)) },
3159
3735
  { path: "playwright.config.js", content: buildPlaywrightConfig(environment) },
3160
3736
  { path: "README.md", content: buildReadme$4(collection.name, scaffoldPaths) }
3161
3737
  );
@@ -3225,7 +3801,7 @@ function buildTestFile$1(folderName, folder, collection) {
3225
3801
  lines.push(` const hookRes = await api.${method}('${path2}');`);
3226
3802
  for (const e of parsed.extractions) {
3227
3803
  const jp = e.accessor.replace(/^json\.?/, "");
3228
- const expr = jp ? `hookRes.body.${jp}` : "hookRes.body";
3804
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookRes.body") : jp ? `hookRes.body.${jp}` : "hookRes.body";
3229
3805
  const varName = toEnvVar(e.varName);
3230
3806
  sharedVars.add(varName);
3231
3807
  lines.push(` ${varName} = String(${expr});`);
@@ -3331,20 +3907,21 @@ ${lines.join("\n")}
3331
3907
  }
3332
3908
  for (const e of parsed.extractions) {
3333
3909
  const path22 = e.accessor.replace(/^json\.?/, "");
3334
- const expr = path22 ? `res.body.${path22}` : "res.body";
3910
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "res.body") : path22 ? `res.body.${path22}` : "res.body";
3335
3911
  lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
3336
3912
  }
3337
3913
  lines.push(` })`);
3338
3914
  tests.push(lines.join("\n"));
3339
3915
  }
3916
+ const jsonPathImport = folderUsesJsonPath(folder, collection) ? "import { JSONPath } from 'jsonpath-plus'\n" : "";
3340
3917
  return `import { api } from '../helpers/api-client'
3341
-
3918
+ ${jsonPathImport}
3342
3919
  describe('${folderName}', () => {
3343
3920
  ${tests.join("\n\n")}
3344
3921
  })
3345
3922
  `;
3346
3923
  }
3347
- function buildPackageJson$1(collectionName) {
3924
+ function buildPackageJson$1(collectionName, usesJsonPath) {
3348
3925
  const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
3349
3926
  return JSON.stringify({
3350
3927
  name: `${name}-api-tests`,
@@ -3356,6 +3933,7 @@ function buildPackageJson$1(collectionName) {
3356
3933
  "@types/supertest": "^6.0.0",
3357
3934
  dotenv: "^16.4.0",
3358
3935
  jest: "^29.7.0",
3936
+ ...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {},
3359
3937
  supertest: "^7.0.0",
3360
3938
  "ts-jest": "^29.1.0",
3361
3939
  "ts-node": "^10.9.2",
@@ -3412,15 +3990,10 @@ function generateSupertestTs(collection, environment) {
3412
3990
  processFolder(sub, sub.name);
3413
3991
  }
3414
3992
  }
3415
- if (collection.rootFolder.requestIds.length > 0) {
3416
- processFolder(collection.rootFolder, collection.name);
3417
- }
3418
- for (const sub of collection.rootFolder.folders) {
3419
- processFolder(sub, sub.name);
3420
- }
3993
+ processFolder(collection.rootFolder, collection.name);
3421
3994
  const scaffoldPaths = ["package.json", "tsconfig.json", ...files.map((f) => f.path)];
3422
3995
  files.unshift(
3423
- { path: "package.json", content: buildPackageJson$1(collection.name) },
3996
+ { path: "package.json", content: buildPackageJson$1(collection.name, collectionUsesJsonPath(collection)) },
3424
3997
  { path: "tsconfig.json", content: buildTsConfig() },
3425
3998
  { path: "README.md", content: buildReadme$3(collection.name, scaffoldPaths) }
3426
3999
  );
@@ -3484,7 +4057,7 @@ function buildTestFile(folderName, folder, collection) {
3484
4057
  lines.push(` const hookRes = await api.${method}('${path2}');`);
3485
4058
  for (const e of parsed.extractions) {
3486
4059
  const jp = e.accessor.replace(/^json\.?/, "");
3487
- const expr = jp ? `hookRes.body.${jp}` : "hookRes.body";
4060
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookRes.body") : jp ? `hookRes.body.${jp}` : "hookRes.body";
3488
4061
  const varName = toEnvVar(e.varName);
3489
4062
  sharedVars.add(varName);
3490
4063
  lines.push(` ${varName} = String(${expr});`);
@@ -3590,20 +4163,21 @@ ${lines.join("\n")}
3590
4163
  }
3591
4164
  for (const e of parsed.extractions) {
3592
4165
  const path22 = e.accessor.replace(/^json\.?/, "");
3593
- const expr = path22 ? `res.body.${path22}` : "res.body";
4166
+ const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "res.body") : path22 ? `res.body.${path22}` : "res.body";
3594
4167
  lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
3595
4168
  }
3596
4169
  lines.push(` })`);
3597
4170
  tests.push(lines.join("\n"));
3598
4171
  }
4172
+ const jsonPathImport = folderUsesJsonPath(folder, collection) ? "const { JSONPath } = require('jsonpath-plus');\n" : "";
3599
4173
  return `const { api } = require('../helpers/api-client');
3600
-
4174
+ ${jsonPathImport}
3601
4175
  describe('${folderName}', () => {
3602
4176
  ${tests.join("\n\n")}
3603
4177
  })
3604
4178
  `;
3605
4179
  }
3606
- function buildPackageJson(collectionName) {
4180
+ function buildPackageJson(collectionName, usesJsonPath) {
3607
4181
  const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
3608
4182
  return JSON.stringify({
3609
4183
  name: `${name}-api-tests`,
@@ -3613,6 +4187,7 @@ function buildPackageJson(collectionName) {
3613
4187
  devDependencies: {
3614
4188
  dotenv: "^16.4.0",
3615
4189
  jest: "^29.7.0",
4190
+ ...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {},
3616
4191
  supertest: "^7.0.0"
3617
4192
  }
3618
4193
  }, null, 2) + "\n";
@@ -3653,19 +4228,22 @@ function generateSupertestJs(collection, environment) {
3653
4228
  processFolder(sub, sub.name);
3654
4229
  }
3655
4230
  }
3656
- if (collection.rootFolder.requestIds.length > 0) {
3657
- processFolder(collection.rootFolder, collection.name);
3658
- }
3659
- for (const sub of collection.rootFolder.folders) {
3660
- processFolder(sub, sub.name);
3661
- }
4231
+ processFolder(collection.rootFolder, collection.name);
3662
4232
  const scaffoldPaths = ["package.json", ...files.map((f) => f.path)];
3663
4233
  files.unshift(
3664
- { path: "package.json", content: buildPackageJson(collection.name) },
4234
+ { path: "package.json", content: buildPackageJson(collection.name, collectionUsesJsonPath(collection)) },
3665
4235
  { path: "README.md", content: buildReadme$2(collection.name, scaffoldPaths) }
3666
4236
  );
3667
4237
  return files;
3668
4238
  }
4239
+ function javaStr(s) {
4240
+ return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
4241
+ }
4242
+ function restAssuredJsonPath(q) {
4243
+ const index = q.index ?? -1;
4244
+ const fallback = q.fallback ?? "";
4245
+ return `jsonPath(hookResponse.asString(), "${javaStr(q.query)}", ${index}, "${javaStr(fallback)}")`;
4246
+ }
3669
4247
  function javaMethod(name) {
3670
4248
  const parts = name.replaceAll(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean);
3671
4249
  return parts[0].toLowerCase() + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
@@ -3691,8 +4269,17 @@ function interpolateJava(value, sharedVars = /* @__PURE__ */ new Set()) {
3691
4269
  return `" + System.getenv("${envKey}") + "`;
3692
4270
  }) + '"';
3693
4271
  }
3694
- function buildPom$1(collectionName) {
4272
+ function buildPom$1(collectionName, usesJsonPath) {
3695
4273
  const artifact = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
4274
+ const jsonPathDep = usesJsonPath ? `
4275
+ <!-- Jayway JSONPath (for sp.jsonPath extractions) -->
4276
+ <dependency>
4277
+ <groupId>com.jayway.jsonpath</groupId>
4278
+ <artifactId>json-path</artifactId>
4279
+ <version>2.9.0</version>
4280
+ <scope>test</scope>
4281
+ </dependency>
4282
+ ` : "";
3696
4283
  return `<?xml version="1.0" encoding="UTF-8"?>
3697
4284
  <project xmlns="http://maven.apache.org/POM/4.0.0"
3698
4285
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
@@ -3744,7 +4331,7 @@ function buildPom$1(collectionName) {
3744
4331
  <version>2.2</version>
3745
4332
  <scope>test</scope>
3746
4333
  </dependency>
3747
- </dependencies>
4334
+ ${jsonPathDep} </dependencies>
3748
4335
 
3749
4336
  <build>
3750
4337
  <plugins>
@@ -3758,12 +4345,29 @@ function buildPom$1(collectionName) {
3758
4345
  </project>
3759
4346
  `;
3760
4347
  }
3761
- function buildBaseTest(environment) {
4348
+ function buildBaseTest(environment, usesJsonPath) {
3762
4349
  const baseUrl = getEnvBaseUrl(environment, "http://localhost:8080");
3763
4350
  const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
3764
4351
  const secretComments = secretVars.map(
3765
4352
  (v) => ` * - ${toEnvVar(v.key)}=<value from keychain>`
3766
4353
  ).join("\n");
4354
+ const jsonPathHelper = usesJsonPath ? `
4355
+ private static final com.jayway.jsonpath.Configuration JSONPATH_CFG =
4356
+ com.jayway.jsonpath.Configuration.defaultConfiguration()
4357
+ .addOptions(com.jayway.jsonpath.Option.SUPPRESS_EXCEPTIONS);
4358
+
4359
+ /** Resolve a JSONPath query; pick element \`index\` (or -1 for the whole result); fall back when absent. */
4360
+ protected static String jsonPath(String body, String query, int index, String fallback) {
4361
+ Object value = com.jayway.jsonpath.JsonPath.using(JSONPATH_CFG).parse(body).read(query);
4362
+ Object out = value;
4363
+ if (index >= 0) {
4364
+ out = (value instanceof java.util.List && index < ((java.util.List<?>) value).size())
4365
+ ? ((java.util.List<?>) value).get(index)
4366
+ : null;
4367
+ }
4368
+ return out == null ? fallback : String.valueOf(out);
4369
+ }
4370
+ ` : "";
3767
4371
  return `package com.example.api;
3768
4372
 
3769
4373
  import io.restassured.RestAssured;
@@ -3781,7 +4385,7 @@ ${secretComments ? secretComments + "\n" : ""} * - BASE_URL (optional, default:
3781
4385
  public class BaseTest {
3782
4386
 
3783
4387
  protected static RequestSpecification requestSpec;
3784
-
4388
+ ${jsonPathHelper}
3785
4389
  @BeforeAll
3786
4390
  static void setupRestAssured() {
3787
4391
  String baseUrl = System.getenv("BASE_URL") != null
@@ -3820,10 +4424,13 @@ function buildTestClass(folderName, folder, collection) {
3820
4424
  }
3821
4425
  lines.push(` .when()${restAssuredCall(method, `"${path2}"`)};`);
3822
4426
  for (const e of parsed.extractions) {
3823
- const jp = accessorToJsonPath(e.accessor);
3824
4427
  const varName = toEnvVar(e.varName);
3825
4428
  sharedVars.add(varName);
3826
- lines.push(` ${varName} = hookResponse.jsonPath().getString("${jp}");`);
4429
+ if (e.jsonPath) {
4430
+ lines.push(` ${varName} = ${restAssuredJsonPath(e.jsonPath)};`);
4431
+ } else {
4432
+ lines.push(` ${varName} = hookResponse.jsonPath().getString("${accessorToJsonPath(e.accessor)}");`);
4433
+ }
3827
4434
  }
3828
4435
  } else {
3829
4436
  lines.push(` given().spec(requestSpec).when()${restAssuredCall(method, `"${path2}"`)};`);
@@ -3971,9 +4578,10 @@ BASE_URL=https://api.example.com mvn test
3971
4578
  `;
3972
4579
  }
3973
4580
  function generateRestAssured(collection, environment) {
4581
+ const usesJsonPath = collectionUsesJsonPath(collection);
3974
4582
  const files = [
3975
- { path: "pom.xml", content: buildPom$1(collection.name) },
3976
- { path: "src/test/java/com/example/api/BaseTest.java", content: buildBaseTest(environment) }
4583
+ { path: "pom.xml", content: buildPom$1(collection.name, usesJsonPath) },
4584
+ { path: "src/test/java/com/example/api/BaseTest.java", content: buildBaseTest(environment, usesJsonPath) }
3977
4585
  ];
3978
4586
  function processFolder(folder, name) {
3979
4587
  if (folder.requestIds.length > 0) {
@@ -3987,12 +4595,7 @@ function generateRestAssured(collection, environment) {
3987
4595
  processFolder(sub, sub.name);
3988
4596
  }
3989
4597
  }
3990
- if (collection.rootFolder.requestIds.length > 0) {
3991
- processFolder(collection.rootFolder, collection.name);
3992
- }
3993
- for (const sub of collection.rootFolder.folders) {
3994
- processFolder(sub, sub.name);
3995
- }
4598
+ processFolder(collection.rootFolder, collection.name);
3996
4599
  files.unshift({ path: "README.md", content: buildReadme$1(collection.name, files.map((f) => f.path)) });
3997
4600
  return files;
3998
4601
  }
@@ -4159,7 +4762,7 @@ function buildBackground(folderId, collection) {
4159
4762
  const parsed = parsePostScript(h.postRequestScript);
4160
4763
  for (const e of parsed.extractions) {
4161
4764
  const jp = accessorToJsonPath(e.accessor).replace(/^json\.?/, "");
4162
- const expr = jp ? `response.${jp}` : "response";
4765
+ const expr = e.jsonPath ? karateJsonPathExpr(e.jsonPath) : jp ? `response.${jp}` : "response";
4163
4766
  const name = configKey(e.varName);
4164
4767
  defined.add(name);
4165
4768
  lines.push(` * def ${name} = ${expr}`);
@@ -4383,10 +4986,7 @@ function generateKarate(collection, environment) {
4383
4986
  }
4384
4987
  for (const sub of folder.folders) processFolder(sub, sub.name);
4385
4988
  }
4386
- if (collection.rootFolder.requestIds.length > 0) {
4387
- processFolder(collection.rootFolder, collection.name);
4388
- }
4389
- for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
4989
+ processFolder(collection.rootFolder, collection.name);
4390
4990
  files.unshift({ path: "README.md", content: buildReadme(collection.name, files.map((f) => f.path)) });
4391
4991
  return files;
4392
4992
  }
@@ -4649,6 +5249,7 @@ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
4649
5249
  function registerRunnerHandler(ipc) {
4650
5250
  handle.handleIpc(ipc, handle.IPC.runner.start, async (event, payload) => {
4651
5251
  const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
5252
+ const concurrency = Math.max(1, Math.min(20, Math.floor(payload.concurrency ?? 1)));
4652
5253
  const envVars = await requestExec.buildEnvVars(environment);
4653
5254
  const liveGlobals = requestExec.getGlobals();
4654
5255
  const globals = { ...payloadGlobals, ...liveGlobals };
@@ -4659,72 +5260,111 @@ function registerRunnerHandler(ipc) {
4659
5260
  let runCollectionVars = {};
4660
5261
  let runGlobals = { ...globals };
4661
5262
  let runLocalVars = {};
5263
+ const runFolderVars = {};
4662
5264
  const skipTracker = new requestExec.HookSkipTracker();
4663
- for (const item of items) {
4664
- const skipReason = skipTracker.shouldSkip(item);
4665
- if (skipReason) {
4666
- const skipped = {
4667
- requestId: item.request.id,
4668
- name: item.request.name,
4669
- method: item.request.method,
4670
- resolvedUrl: item.request.url,
4671
- status: "failed",
4672
- error: skipReason,
4673
- isHook: item.isHook,
4674
- hookType: item.hookType,
4675
- scopeId: item.scopeId,
4676
- collection: item.collection,
4677
- scopePath: item.scopePath,
4678
- iterationLabel: item.iterationLabel
4679
- };
4680
- summary.failed++;
4681
- event.sender.send(handle.IPC.runner.progress, skipped);
4682
- continue;
4683
- }
4684
- const runningUpdate = {
4685
- status: "running",
4686
- iterationLabel: item.iterationLabel,
4687
- isHook: item.isHook,
4688
- hookType: item.hookType,
4689
- scopeId: item.scopeId,
4690
- collection: item.collection,
4691
- scopePath: item.scopePath
4692
- };
4693
- event.sender.send(handle.IPC.runner.progress, { requestId: item.request.id, ...runningUpdate });
4694
- const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await requestExec.executeRunnerRequest({
4695
- req: item.request,
4696
- collectionVars: { ...item.collectionVars, ...runCollectionVars },
4697
- envVars: runEnvVars,
4698
- globals: runGlobals,
4699
- localVars: { ...runLocalVars, ...item.dataRow },
4700
- dispatcher,
4701
- piiMaskPatterns,
4702
- proxy,
4703
- tls,
4704
- expectStatus: item.expectStatus,
4705
- owaspTag: item.owaspTag
4706
- });
4707
- runEnvVars = updatedEnvVars;
4708
- runCollectionVars = updatedCollectionVars;
4709
- runGlobals = updatedGlobals;
4710
- runLocalVars = updatedLocalVars;
4711
- skipTracker.recordResult(item, result.status);
4712
- if (result.status === "passed") summary.passed++;
4713
- else if (result.status === "failed") summary.failed++;
4714
- else if (result.status === "skipped") summary.skipped++;
5265
+ const meta = (item, index) => ({
5266
+ index,
5267
+ iterationLabel: item.iterationLabel,
5268
+ isHook: item.isHook,
5269
+ hookType: item.hookType,
5270
+ scopeId: item.scopeId,
5271
+ collection: item.collection,
5272
+ scopePath: item.scopePath
5273
+ });
5274
+ const emitRunning = (item, index) => event.sender.send(handle.IPC.runner.progress, { requestId: item.request.id, status: "running", ...meta(item, index) });
5275
+ const emitResult = (item, index, result) => event.sender.send(handle.IPC.runner.progress, { ...result, ...meta(item, index) });
5276
+ const tally = (status) => {
5277
+ if (status === "passed") summary.passed++;
5278
+ else if (status === "failed") summary.failed++;
5279
+ else if (status === "skipped") summary.skipped++;
4715
5280
  else summary.errors++;
4716
- event.sender.send(handle.IPC.runner.progress, {
4717
- ...result,
4718
- iterationLabel: item.iterationLabel,
4719
- isHook: item.isHook,
4720
- hookType: item.hookType,
4721
- scopeId: item.scopeId,
4722
- collection: item.collection,
4723
- scopePath: item.scopePath
4724
- });
4725
- if (requestDelay > 0 && item !== items[items.length - 1]) {
4726
- await sleep(requestDelay);
5281
+ };
5282
+ const skippedResult = (item, reason) => ({
5283
+ requestId: item.request.id,
5284
+ name: item.request.name,
5285
+ method: item.request.method,
5286
+ resolvedUrl: item.request.url,
5287
+ status: "failed",
5288
+ error: reason,
5289
+ isHook: item.isHook,
5290
+ hookType: item.hookType,
5291
+ scopeId: item.scopeId,
5292
+ collection: item.collection,
5293
+ scopePath: item.scopePath,
5294
+ iterationLabel: item.iterationLabel
5295
+ });
5296
+ const execItem = (item) => requestExec.executeRunnerRequest({
5297
+ req: item.request,
5298
+ collectionVars: { ...item.collectionVars, ...runCollectionVars },
5299
+ envVars: runEnvVars,
5300
+ globals: runGlobals,
5301
+ localVars: { ...runLocalVars, ...item.dataRow },
5302
+ folderVars: { ...item.folderVars ?? {}, ...item.folderId ? runFolderVars[item.folderId] ?? {} : {} },
5303
+ dispatcher,
5304
+ piiMaskPatterns,
5305
+ proxy,
5306
+ tls,
5307
+ expectStatus: item.expectStatus,
5308
+ owaspTag: item.owaspTag
5309
+ });
5310
+ const applyUpdates = (item, out, mode) => {
5311
+ if (mode === "replace") {
5312
+ runEnvVars = out.updatedEnvVars;
5313
+ runCollectionVars = out.updatedCollectionVars;
5314
+ runGlobals = out.updatedGlobals;
5315
+ runLocalVars = out.updatedLocalVars;
5316
+ } else {
5317
+ runEnvVars = { ...runEnvVars, ...out.updatedEnvVars };
5318
+ runCollectionVars = { ...runCollectionVars, ...out.updatedCollectionVars };
5319
+ runGlobals = { ...runGlobals, ...out.updatedGlobals };
5320
+ runLocalVars = { ...runLocalVars, ...out.updatedLocalVars };
5321
+ }
5322
+ if (item.folderId) {
5323
+ runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
4727
5324
  }
5325
+ };
5326
+ if (concurrency <= 1) {
5327
+ for (let i = 0; i < items.length; i++) {
5328
+ const item = items[i];
5329
+ const skipReason = skipTracker.shouldSkip(item);
5330
+ if (skipReason) {
5331
+ const skipped = skippedResult(item, skipReason);
5332
+ tally(skipped.status);
5333
+ emitResult(item, i, skipped);
5334
+ continue;
5335
+ }
5336
+ emitRunning(item, i);
5337
+ const out = await execItem(item);
5338
+ applyUpdates(item, out, "replace");
5339
+ skipTracker.recordResult(item, out.result.status);
5340
+ tally(out.result.status);
5341
+ emitResult(item, i, out.result);
5342
+ if (requestDelay > 0 && i < items.length - 1) await sleep(requestDelay);
5343
+ }
5344
+ } else {
5345
+ const runItem = async (item, index) => {
5346
+ const skipReason = skipTracker.shouldSkip(item);
5347
+ if (skipReason) {
5348
+ const sk = skippedResult(item, skipReason);
5349
+ tally(sk.status);
5350
+ emitResult(item, index, sk);
5351
+ return;
5352
+ }
5353
+ emitRunning(item, index);
5354
+ try {
5355
+ const out = await execItem(item);
5356
+ applyUpdates(item, out, "merge");
5357
+ skipTracker.recordResult(item, out.result.status);
5358
+ tally(out.result.status);
5359
+ emitResult(item, index, out.result);
5360
+ } catch (err) {
5361
+ const r = { ...skippedResult(item, err instanceof Error ? err.message : String(err)), status: "error" };
5362
+ skipTracker.recordResult(item, "error");
5363
+ tally("error");
5364
+ emitResult(item, index, r);
5365
+ }
5366
+ };
5367
+ await runConcurrent.runPlanConcurrent(concurrency, items, (it) => Boolean(it.isHook), runItem);
4728
5368
  }
4729
5369
  summary.durationMs = Date.now() - totalStart;
4730
5370
  return summary;
@@ -5069,30 +5709,42 @@ function registerVaultHandlers(ipc) {
5069
5709
  };
5070
5710
  });
5071
5711
  }
5072
- const connections = /* @__PURE__ */ new Map();
5712
+ const connections$3 = /* @__PURE__ */ new Map();
5073
5713
  function closeAllWsConnections() {
5074
- for (const [, ws] of connections) {
5714
+ for (const [, ws] of connections$3) {
5075
5715
  try {
5076
5716
  ws.close();
5077
5717
  } catch {
5078
5718
  }
5079
5719
  }
5080
- connections.clear();
5720
+ connections$3.clear();
5081
5721
  }
5082
5722
  function registerWsHandlers(ipc) {
5083
5723
  handle.handleIpc(ipc, handle.IPC.ws.connect, async (event, requestId, url, headers) => {
5084
- const existing = connections.get(requestId);
5724
+ const existing = connections$3.get(requestId);
5085
5725
  if (existing) {
5086
5726
  existing.close();
5087
- connections.delete(requestId);
5727
+ connections$3.delete(requestId);
5088
5728
  }
5089
- event.sender.send(handle.IPC.ws.status, { requestId, status: "connecting" });
5729
+ const alive = () => !event.sender.isDestroyed();
5730
+ const status = (s, error) => {
5731
+ if (alive()) event.sender.send(handle.IPC.ws.status, { requestId, status: s, error });
5732
+ };
5733
+ status("connecting");
5090
5734
  const ws = new WebSocket(url, { headers });
5091
- connections.set(requestId, ws);
5735
+ connections$3.set(requestId, ws);
5736
+ event.sender.once("destroyed", () => {
5737
+ try {
5738
+ ws.close();
5739
+ } catch {
5740
+ }
5741
+ connections$3.delete(requestId);
5742
+ });
5092
5743
  ws.on("open", () => {
5093
- event.sender.send(handle.IPC.ws.status, { requestId, status: "connected" });
5744
+ status("connected");
5094
5745
  });
5095
5746
  ws.on("message", (data) => {
5747
+ if (!alive()) return;
5096
5748
  const message = {
5097
5749
  id: uuid.v4(),
5098
5750
  direction: "received",
@@ -5102,27 +5754,526 @@ function registerWsHandlers(ipc) {
5102
5754
  event.sender.send(handle.IPC.ws.message, { requestId, message });
5103
5755
  });
5104
5756
  ws.on("error", (err) => {
5105
- event.sender.send(handle.IPC.ws.status, { requestId, status: "error", error: err.message });
5106
- connections.delete(requestId);
5757
+ status("error", err.message);
5758
+ connections$3.delete(requestId);
5107
5759
  });
5108
5760
  ws.on("close", () => {
5109
- event.sender.send(handle.IPC.ws.status, { requestId, status: "disconnected" });
5110
- connections.delete(requestId);
5761
+ status("disconnected");
5762
+ connections$3.delete(requestId);
5111
5763
  });
5112
5764
  });
5113
5765
  handle.handleIpc(ipc, handle.IPC.ws.send, async (_event, requestId, data) => {
5114
- const ws = connections.get(requestId);
5766
+ const ws = connections$3.get(requestId);
5115
5767
  if (ws?.readyState !== WebSocket.OPEN) {
5116
5768
  throw new Error("WebSocket is not connected");
5117
5769
  }
5118
5770
  ws.send(data);
5119
5771
  });
5772
+ handle.handleIpc(ipc, handle.IPC.ws.test, async (_event, cfg) => {
5773
+ const t0 = Date.now();
5774
+ const res = await requestExec.runWsTest(cfg);
5775
+ console.log(`[ws:test] url=${cfg.url} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
5776
+ return res;
5777
+ });
5120
5778
  handle.handleIpc(ipc, handle.IPC.ws.disconnect, async (_event, requestId) => {
5121
- const ws = connections.get(requestId);
5779
+ const ws = connections$3.get(requestId);
5122
5780
  if (ws) {
5123
5781
  ws.close();
5782
+ connections$3.delete(requestId);
5783
+ }
5784
+ });
5785
+ }
5786
+ const connections$2 = /* @__PURE__ */ new Map();
5787
+ function closeAllSocketIoConnections() {
5788
+ for (const [, socket] of connections$2) {
5789
+ try {
5790
+ socket.disconnect();
5791
+ } catch {
5792
+ }
5793
+ }
5794
+ connections$2.clear();
5795
+ }
5796
+ function targetUrl(url, namespace) {
5797
+ const ns = (namespace ?? "").trim();
5798
+ if (!ns || ns === "/") return url;
5799
+ return url.replace(/\/+$/, "") + (ns.startsWith("/") ? ns : `/${ns}`);
5800
+ }
5801
+ function jsonText(value) {
5802
+ try {
5803
+ return JSON.stringify(value);
5804
+ } catch {
5805
+ return String(value);
5806
+ }
5807
+ }
5808
+ function registerSocketIoHandlers(ipc) {
5809
+ handle.handleIpc(ipc, handle.IPC.socketio.connect, async (event, requestId, opts) => {
5810
+ const existing = connections$2.get(requestId);
5811
+ if (existing) {
5812
+ existing.disconnect();
5813
+ connections$2.delete(requestId);
5814
+ }
5815
+ const alive = () => !event.sender.isDestroyed();
5816
+ const send = (message) => {
5817
+ if (alive()) event.sender.send(handle.IPC.socketio.message, { requestId, message });
5818
+ };
5819
+ const status = (s, error) => {
5820
+ if (alive()) event.sender.send(handle.IPC.socketio.status, { requestId, status: s, error });
5821
+ };
5822
+ const note = (ev, text) => send({ id: uuid.v4(), direction: "received", kind: "system", event: ev, data: text, timestamp: Date.now() });
5823
+ let auth;
5824
+ if (opts.auth && opts.auth.trim()) {
5825
+ try {
5826
+ const parsed = JSON.parse(opts.auth);
5827
+ auth = parsed && typeof parsed === "object" ? parsed : void 0;
5828
+ } catch {
5829
+ status("error", "Auth is not valid JSON");
5830
+ return;
5831
+ }
5832
+ }
5833
+ status("connecting");
5834
+ let socket;
5835
+ try {
5836
+ socket = socket_ioClient.io(targetUrl(opts.url, opts.namespace), {
5837
+ path: opts.path?.trim() || "/socket.io",
5838
+ transports: ["websocket", "polling"],
5839
+ extraHeaders: opts.headers,
5840
+ auth,
5841
+ forceNew: true,
5842
+ reconnection: true
5843
+ });
5844
+ } catch (err) {
5845
+ status("error", err instanceof Error ? err.message : String(err));
5846
+ return;
5847
+ }
5848
+ connections$2.set(requestId, socket);
5849
+ event.sender.once("destroyed", () => {
5850
+ try {
5851
+ socket.disconnect();
5852
+ } catch {
5853
+ }
5854
+ connections$2.delete(requestId);
5855
+ });
5856
+ socket.on("connect", () => {
5857
+ status("connected");
5858
+ note("connect", `Connected (id ${socket.id})`);
5859
+ });
5860
+ socket.on("disconnect", (reason) => {
5861
+ status("disconnected");
5862
+ note("disconnect", `Disconnected: ${reason}`);
5863
+ });
5864
+ socket.on("connect_error", (err) => {
5865
+ status("error", err.message);
5866
+ });
5867
+ socket.onAny((eventName, ...args) => {
5868
+ send({ id: uuid.v4(), direction: "received", kind: "event", event: eventName, data: jsonText(args), timestamp: Date.now() });
5869
+ });
5870
+ });
5871
+ handle.handleIpc(ipc, handle.IPC.socketio.emit, async (event, requestId, opts) => {
5872
+ const socket = connections$2.get(requestId);
5873
+ if (!socket || !socket.connected) {
5874
+ throw new Error("Socket.IO is not connected");
5875
+ }
5876
+ let args = [];
5877
+ if (opts.args && opts.args.trim()) {
5878
+ let parsed;
5879
+ try {
5880
+ parsed = JSON.parse(opts.args);
5881
+ } catch {
5882
+ throw new Error("Emit arguments are not valid JSON");
5883
+ }
5884
+ args = Array.isArray(parsed) ? parsed : [parsed];
5885
+ }
5886
+ if (opts.withAck) {
5887
+ const send = (message) => {
5888
+ if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.socketio.message, { requestId, message });
5889
+ };
5890
+ socket.timeout(opts.ackTimeoutMs ?? 5e3).emit(opts.event, ...args, (err, ...response) => {
5891
+ send(err ? { id: uuid.v4(), direction: "received", kind: "system", event: opts.event, data: `No acknowledgement: ${err.message}`, timestamp: Date.now() } : { id: uuid.v4(), direction: "received", kind: "ack", event: opts.event, data: jsonText(response), timestamp: Date.now() });
5892
+ });
5893
+ } else {
5894
+ socket.emit(opts.event, ...args);
5895
+ }
5896
+ });
5897
+ handle.handleIpc(ipc, handle.IPC.socketio.test, async (_event, cfg) => {
5898
+ const t0 = Date.now();
5899
+ const res = await requestExec.runSocketIoTest(cfg);
5900
+ console.log(`[socketio:test] url=${cfg.url} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
5901
+ return res;
5902
+ });
5903
+ handle.handleIpc(ipc, handle.IPC.socketio.disconnect, async (_event, requestId) => {
5904
+ const socket = connections$2.get(requestId);
5905
+ if (socket) {
5906
+ socket.disconnect();
5907
+ connections$2.delete(requestId);
5908
+ }
5909
+ });
5910
+ }
5911
+ let jqPromise = null;
5912
+ function getJq() {
5913
+ if (!jqPromise) {
5914
+ jqPromise = (async () => {
5915
+ const mod = require("jq-web");
5916
+ return mod && typeof mod.json === "function" ? mod : await mod;
5917
+ })();
5918
+ }
5919
+ return jqPromise;
5920
+ }
5921
+ function registerJqHandlers(ipc) {
5922
+ handle.handleIpc(ipc, handle.IPC.jq.filter, async (_event, jsonText2, filter) => {
5923
+ const expr = (filter ?? "").trim();
5924
+ if (!expr) return jsonText2;
5925
+ let data;
5926
+ try {
5927
+ data = JSON.parse(jsonText2);
5928
+ } catch {
5929
+ throw new Error("The response body is not valid JSON, so it cannot be filtered with jq.");
5930
+ }
5931
+ const jq = await getJq();
5932
+ let result;
5933
+ try {
5934
+ result = jq.json(data, expr);
5935
+ } catch (e) {
5936
+ throw new Error(e instanceof Error ? e.message.trim() : String(e));
5937
+ }
5938
+ return JSON.stringify(result, null, 2);
5939
+ });
5940
+ }
5941
+ const SUB_ID = "1";
5942
+ const connections$1 = /* @__PURE__ */ new Map();
5943
+ function closeAllGraphqlWsConnections() {
5944
+ for (const [, ws] of connections$1) {
5945
+ try {
5946
+ ws.close();
5947
+ } catch {
5948
+ }
5949
+ }
5950
+ connections$1.clear();
5951
+ }
5952
+ function parseJsonObject(text, label) {
5953
+ if (!text || !text.trim()) return void 0;
5954
+ let parsed;
5955
+ try {
5956
+ parsed = JSON.parse(text);
5957
+ } catch {
5958
+ throw new Error(`${label} is not valid JSON`);
5959
+ }
5960
+ return parsed && typeof parsed === "object" ? parsed : void 0;
5961
+ }
5962
+ function registerGraphqlWsHandlers(ipc) {
5963
+ handle.handleIpc(ipc, handle.IPC.graphqlws.subscribe, async (event, requestId, opts) => {
5964
+ const existing = connections$1.get(requestId);
5965
+ if (existing) {
5966
+ existing.close();
5967
+ connections$1.delete(requestId);
5968
+ }
5969
+ const alive = () => !event.sender.isDestroyed();
5970
+ const send = (message) => {
5971
+ if (alive()) event.sender.send(handle.IPC.graphqlws.message, { requestId, message });
5972
+ };
5973
+ const status = (s, error) => {
5974
+ if (alive()) event.sender.send(handle.IPC.graphqlws.status, { requestId, status: s, error });
5975
+ };
5976
+ const note = (text) => send({ id: uuid.v4(), kind: "system", data: text, timestamp: Date.now() });
5977
+ let variables;
5978
+ let connectionParams;
5979
+ try {
5980
+ variables = parseJsonObject(opts.variables, "Variables");
5981
+ connectionParams = parseJsonObject(opts.connectionParams, "Connection params");
5982
+ } catch (e) {
5983
+ status("error", e instanceof Error ? e.message : String(e));
5984
+ return;
5985
+ }
5986
+ status("connecting");
5987
+ let ws;
5988
+ try {
5989
+ ws = new WebSocket(opts.url, "graphql-transport-ws", { headers: opts.headers });
5990
+ } catch (e) {
5991
+ status("error", e instanceof Error ? e.message : String(e));
5992
+ return;
5993
+ }
5994
+ connections$1.set(requestId, ws);
5995
+ event.sender.once("destroyed", () => {
5996
+ try {
5997
+ ws.close();
5998
+ } catch {
5999
+ }
6000
+ connections$1.delete(requestId);
6001
+ });
6002
+ ws.on("open", () => {
6003
+ ws.send(JSON.stringify({ type: "connection_init", payload: connectionParams }));
6004
+ });
6005
+ ws.on("message", (raw) => {
6006
+ let msg;
6007
+ try {
6008
+ msg = JSON.parse(raw.toString());
6009
+ } catch {
6010
+ return;
6011
+ }
6012
+ switch (msg.type) {
6013
+ case "connection_ack":
6014
+ status("connected");
6015
+ note("Connected; subscription started");
6016
+ ws.send(JSON.stringify({ id: SUB_ID, type: "subscribe", payload: { query: opts.query, variables, operationName: opts.operationName } }));
6017
+ break;
6018
+ case "next":
6019
+ send({ id: uuid.v4(), kind: "next", data: JSON.stringify(msg.payload), timestamp: Date.now() });
6020
+ break;
6021
+ case "error":
6022
+ send({ id: uuid.v4(), kind: "error", data: JSON.stringify(msg.payload), timestamp: Date.now() });
6023
+ break;
6024
+ case "complete":
6025
+ send({ id: uuid.v4(), kind: "complete", data: "Subscription complete", timestamp: Date.now() });
6026
+ ws.close();
6027
+ break;
6028
+ case "ping":
6029
+ ws.send(JSON.stringify({ type: "pong" }));
6030
+ break;
6031
+ default:
6032
+ break;
6033
+ }
6034
+ });
6035
+ ws.on("error", (err) => {
6036
+ status("error", err.message);
6037
+ connections$1.delete(requestId);
6038
+ });
6039
+ ws.on("close", () => {
6040
+ status("disconnected");
6041
+ connections$1.delete(requestId);
6042
+ });
6043
+ });
6044
+ handle.handleIpc(ipc, handle.IPC.graphqlws.test, async (_event, cfg) => {
6045
+ const t0 = Date.now();
6046
+ const res = await requestExec.runGraphqlWsTest(cfg);
6047
+ console.log(`[graphqlws:test] url=${cfg.url} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
6048
+ return res;
6049
+ });
6050
+ handle.handleIpc(ipc, handle.IPC.graphqlws.stop, async (_event, requestId) => {
6051
+ const ws = connections$1.get(requestId);
6052
+ if (!ws) return;
6053
+ try {
6054
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ id: SUB_ID, type: "complete" }));
6055
+ } catch {
6056
+ }
6057
+ ws.close();
6058
+ connections$1.delete(requestId);
6059
+ });
6060
+ }
6061
+ const connections = /* @__PURE__ */ new Map();
6062
+ function closeAllMqttConnections() {
6063
+ for (const [, client] of connections) {
6064
+ try {
6065
+ client.end(true);
6066
+ } catch {
6067
+ }
6068
+ }
6069
+ connections.clear();
6070
+ }
6071
+ function normalizeQos(q) {
6072
+ return q === 1 || q === 2 ? q : 0;
6073
+ }
6074
+ function registerMqttHandlers(ipc) {
6075
+ handle.handleIpc(ipc, handle.IPC.mqtt.connect, async (event, requestId, opts) => {
6076
+ const existing = connections.get(requestId);
6077
+ if (existing) {
6078
+ try {
6079
+ existing.end(true);
6080
+ } catch {
6081
+ }
5124
6082
  connections.delete(requestId);
5125
6083
  }
6084
+ const alive = () => !event.sender.isDestroyed();
6085
+ const send = (message) => {
6086
+ if (alive()) event.sender.send(handle.IPC.mqtt.message, { requestId, message });
6087
+ };
6088
+ const status = (s, error) => {
6089
+ if (alive()) event.sender.send(handle.IPC.mqtt.status, { requestId, status: s, error });
6090
+ };
6091
+ const note = (topic, text) => send({ id: uuid.v4(), direction: "received", kind: "system", topic, data: text, timestamp: Date.now() });
6092
+ const url = (opts.url ?? "").trim();
6093
+ if (!url) {
6094
+ status("error", "No broker URL");
6095
+ return;
6096
+ }
6097
+ status("connecting");
6098
+ let client;
6099
+ try {
6100
+ client = mqtt.connect(url, {
6101
+ clientId: opts.clientId?.trim() || `api-spector-${Math.random().toString(16).slice(2, 10)}`,
6102
+ username: opts.username?.trim() || void 0,
6103
+ password: opts.password ? opts.password : void 0,
6104
+ clean: opts.cleanSession !== false,
6105
+ // Recover from a transient drop on its own and re-apply subscriptions,
6106
+ // so a brief hiccup doesn't force the user to reconnect by hand.
6107
+ reconnectPeriod: 2e3,
6108
+ resubscribe: true,
6109
+ connectTimeout: 3e4
6110
+ });
6111
+ } catch (err) {
6112
+ status("error", err instanceof Error ? err.message : String(err));
6113
+ return;
6114
+ }
6115
+ connections.set(requestId, client);
6116
+ event.sender.once("destroyed", () => {
6117
+ try {
6118
+ client.end(true);
6119
+ } catch {
6120
+ }
6121
+ connections.delete(requestId);
6122
+ });
6123
+ client.on("connect", () => {
6124
+ status("connected");
6125
+ note("", `Connected to ${url}`);
6126
+ const topic = opts.subscribeTopic?.trim();
6127
+ if (topic) {
6128
+ client.subscribe(topic, { qos: normalizeQos(opts.qos) }, (err) => {
6129
+ note(topic, err ? `Subscribe failed: ${err.message}` : `Subscribed to ${topic}`);
6130
+ });
6131
+ }
6132
+ });
6133
+ client.on("message", (topic, payload, packet) => {
6134
+ send({
6135
+ id: uuid.v4(),
6136
+ direction: "received",
6137
+ kind: "message",
6138
+ topic,
6139
+ data: payload.toString("utf8"),
6140
+ qos: normalizeQos(packet?.qos),
6141
+ retain: Boolean(packet?.retain),
6142
+ timestamp: Date.now()
6143
+ });
6144
+ });
6145
+ client.on("error", (err) => {
6146
+ note("", `Error: ${err.message}`);
6147
+ });
6148
+ client.on("reconnect", () => {
6149
+ status("connecting");
6150
+ });
6151
+ client.on("offline", () => {
6152
+ status("connecting");
6153
+ note("", "Connection lost, reconnecting…");
6154
+ });
6155
+ client.on("close", () => {
6156
+ if (connections.get(requestId) === client) status("connecting");
6157
+ });
6158
+ });
6159
+ handle.handleIpc(ipc, handle.IPC.mqtt.publish, async (_event, requestId, opts) => {
6160
+ const client = connections.get(requestId);
6161
+ if (!client || !client.connected) throw new Error("MQTT is not connected");
6162
+ const topic = (opts.topic ?? "").trim();
6163
+ if (!topic) throw new Error("Publish topic is required");
6164
+ await new Promise((resolve2, reject) => {
6165
+ client.publish(topic, opts.payload ?? "", { qos: normalizeQos(opts.qos), retain: Boolean(opts.retain) }, (err) => {
6166
+ if (err) reject(err);
6167
+ else resolve2();
6168
+ });
6169
+ });
6170
+ });
6171
+ handle.handleIpc(ipc, handle.IPC.mqtt.subscribe, async (event, requestId, opts) => {
6172
+ const client = connections.get(requestId);
6173
+ if (!client || !client.connected) throw new Error("MQTT is not connected");
6174
+ const topic = (opts.topic ?? "").trim();
6175
+ if (!topic) throw new Error("Subscribe topic is required");
6176
+ const send = (message) => {
6177
+ if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.mqtt.message, { requestId, message });
6178
+ };
6179
+ await new Promise((resolve2, reject) => {
6180
+ client.subscribe(topic, { qos: normalizeQos(opts.qos) }, (err) => {
6181
+ send({ id: uuid.v4(), direction: "received", kind: "system", topic, data: err ? `Subscribe failed: ${err.message}` : `Subscribed to ${topic}`, timestamp: Date.now() });
6182
+ if (err) reject(err);
6183
+ else resolve2();
6184
+ });
6185
+ });
6186
+ });
6187
+ handle.handleIpc(ipc, handle.IPC.mqtt.unsubscribe, async (event, requestId, opts) => {
6188
+ const client = connections.get(requestId);
6189
+ if (!client || !client.connected) return;
6190
+ const topic = (opts.topic ?? "").trim();
6191
+ if (!topic) return;
6192
+ const send = (message) => {
6193
+ if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.mqtt.message, { requestId, message });
6194
+ };
6195
+ client.unsubscribe(topic, () => {
6196
+ send({ id: uuid.v4(), direction: "received", kind: "system", topic, data: `Unsubscribed from ${topic}`, timestamp: Date.now() });
6197
+ });
6198
+ });
6199
+ handle.handleIpc(ipc, handle.IPC.mqtt.test, async (_event, cfg) => {
6200
+ const t0 = Date.now();
6201
+ const res = await requestExec.runMqttTest(cfg);
6202
+ console.log(`[mqtt:test] url=${cfg.url} sub=${cfg.subscribeTopic ?? "(none)"} clientId=${res.clientId} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
6203
+ return res;
6204
+ });
6205
+ handle.handleIpc(ipc, handle.IPC.mqtt.disconnect, async (event, requestId) => {
6206
+ const client = connections.get(requestId);
6207
+ if (client) {
6208
+ connections.delete(requestId);
6209
+ try {
6210
+ client.end(true);
6211
+ } catch {
6212
+ }
6213
+ }
6214
+ if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.mqtt.status, { requestId, status: "disconnected" });
6215
+ });
6216
+ }
6217
+ let child = null;
6218
+ function runInWorker(payload, onProgress) {
6219
+ return new Promise((resolve2, reject) => {
6220
+ const proc = electron.utilityProcess.fork(path.join(__dirname, "perf-worker.js"));
6221
+ child = proc;
6222
+ let settled = false;
6223
+ const done = () => {
6224
+ settled = true;
6225
+ if (child === proc) child = null;
6226
+ try {
6227
+ proc.kill();
6228
+ } catch {
6229
+ }
6230
+ };
6231
+ proc.on("message", (msg) => {
6232
+ if (msg.type === "progress") {
6233
+ onProgress(msg.p);
6234
+ return;
6235
+ }
6236
+ if (msg.type === "result") {
6237
+ done();
6238
+ resolve2(msg.result);
6239
+ return;
6240
+ }
6241
+ if (msg.type === "error") {
6242
+ done();
6243
+ reject(new Error(msg.message || "Load test failed"));
6244
+ return;
6245
+ }
6246
+ });
6247
+ proc.on("exit", () => {
6248
+ if (!settled) {
6249
+ if (child === proc) child = null;
6250
+ reject(new Error("Load test worker exited unexpectedly"));
6251
+ }
6252
+ });
6253
+ proc.postMessage({ type: "run", payload });
6254
+ });
6255
+ }
6256
+ function registerPerfHandlers(ipc) {
6257
+ handle.handleIpc(ipc, handle.IPC.perf.run, async (event, payload) => {
6258
+ if ((payload.plan.target.kind === "sequence" || payload.plan.target.kind === "flow") && !payload.resolvedEnvVars) {
6259
+ payload.resolvedEnvVars = await requestExec.buildEnvVars(payload.environment ?? null);
6260
+ }
6261
+ return runInWorker(payload, (p) => {
6262
+ if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.perf.progress, p);
6263
+ });
6264
+ });
6265
+ handle.handleIpc(ipc, handle.IPC.perf.stop, async () => {
6266
+ child?.postMessage({ type: "stop" });
6267
+ });
6268
+ handle.handleIpc(ipc, handle.IPC.perf.exportHtml, async (_e, result, meta = {}) => {
6269
+ const { canceled, filePath } = await electron.dialog.showSaveDialog({
6270
+ title: "Save performance report",
6271
+ defaultPath: `perf-report-${(meta.planName ?? "run").replace(/[^a-z0-9]+/gi, "-").toLowerCase()}.html`,
6272
+ filters: [{ name: "HTML", extensions: ["html"] }]
6273
+ });
6274
+ if (canceled || !filePath) return false;
6275
+ await promises.writeFile(filePath, perfReport.perfReportToHtml(result, { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...meta }), "utf8");
6276
+ return true;
5126
6277
  });
5127
6278
  }
5128
6279
  const calls = /* @__PURE__ */ new Map();
@@ -6142,7 +7293,7 @@ function isNewer(a, b) {
6142
7293
  return a2 > b2;
6143
7294
  }
6144
7295
  async function checkForUpdate() {
6145
- const current = "0.7.7";
7296
+ const current = "0.7.8";
6146
7297
  try {
6147
7298
  const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(4e3) });
6148
7299
  if (!res.ok) return null;
@@ -6360,6 +7511,11 @@ electron.app.whenReady().then(async () => {
6360
7511
  registerOAuth2Handlers(electron.ipcMain);
6361
7512
  registerVaultHandlers(electron.ipcMain);
6362
7513
  registerWsHandlers(electron.ipcMain);
7514
+ registerSocketIoHandlers(electron.ipcMain);
7515
+ registerJqHandlers(electron.ipcMain);
7516
+ registerGraphqlWsHandlers(electron.ipcMain);
7517
+ registerMqttHandlers(electron.ipcMain);
7518
+ registerPerfHandlers(electron.ipcMain);
6363
7519
  registerGrpcHandlers(electron.ipcMain);
6364
7520
  soapHandler.registerSoapHandlers(electron.ipcMain);
6365
7521
  registerDocsHandlers(electron.ipcMain);
@@ -6387,6 +7543,9 @@ electron.app.on("window-all-closed", () => {
6387
7543
  });
6388
7544
  electron.app.on("before-quit", async () => {
6389
7545
  closeAllWsConnections();
7546
+ closeAllSocketIoConnections();
7547
+ closeAllGraphqlWsConnections();
7548
+ closeAllMqttConnections();
6390
7549
  closeAllGrpcConnections();
6391
7550
  await mockServer.stopAll();
6392
7551
  });