@testsmith/api-spector 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +2 -0
- package/out/main/agents.js +1 -1
- package/out/main/chunks/{broker-client-BSTNMPtT.js → broker-client-VwFF9sbp.js} +1 -1
- package/out/main/chunks/{cli-common-Cq_cphpZ.js → cli-common-GnihVSjU.js} +13 -0
- package/out/main/chunks/flow-engine-DuO1iX7c.js +533 -0
- package/out/main/chunks/{handle-DDZ37J8G.js → handle-BY_jaVBT.js} +16 -0
- package/out/main/chunks/{import-CBAklepT.js → import-DR_r1DUP.js} +2 -2
- package/out/main/chunks/{request-exec-DLBjLZe1.js → request-exec-CcLHtkwG.js} +59 -18
- package/out/main/chunks/{soap-handler-C_LCcPwE.js → soap-handler-CkoLJHoI.js} +2 -2
- package/out/main/compare.js +1 -1
- package/out/main/contract.js +4 -4
- package/out/main/coverage.js +1 -1
- package/out/main/flow.js +401 -0
- package/out/main/generate-tests.js +1 -1
- package/out/main/index.js +161 -5
- package/out/main/lib.js +61 -2
- package/out/main/mock.js +1 -1
- package/out/main/record.js +1 -1
- package/out/main/runner.js +4 -4
- package/out/main/wsdl.js +4 -4
- package/out/preload/index.js +30 -0
- package/out/renderer/assets/{GraphQLEditor-D-zPak2W.js → GraphQLEditor-jCEopi26.js} +1 -1
- package/out/renderer/assets/{SoapEditor-080lveVz.js → SoapEditor-PGdehf_W.js} +1 -1
- package/out/renderer/assets/index-CkWcesWQ.css +627 -0
- package/out/renderer/assets/{index-LLqTi-5a.js → index-aKbixApk.js} +12380 -1440
- package/out/renderer/index.html +2 -2
- package/package.json +2 -1
- package/out/renderer/assets/index-DfWnNkH3.css +0 -2
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
const undici = require("undici");
|
|
25
25
|
const promises$1 = require("fs/promises");
|
|
26
26
|
const nodeTls = require("tls");
|
|
27
|
-
const handle = require("./handle-
|
|
27
|
+
const handle = require("./handle-BY_jaVBT.js");
|
|
28
28
|
const fs = require("node:fs");
|
|
29
29
|
const os = require("node:os");
|
|
30
30
|
const path = require("node:path");
|
|
@@ -1036,22 +1036,7 @@ function buildAt(ctx, testResults, consoleOutput) {
|
|
|
1036
1036
|
}
|
|
1037
1037
|
return sp;
|
|
1038
1038
|
}
|
|
1039
|
-
|
|
1040
|
-
const faker = await getFaker();
|
|
1041
|
-
const testResults = [];
|
|
1042
|
-
const consoleOutput = [];
|
|
1043
|
-
const envVarsCopy = { ...ctx.envVars };
|
|
1044
|
-
const collectionCopy = { ...ctx.collectionVars };
|
|
1045
|
-
const globalsCopy = { ...ctx.globals };
|
|
1046
|
-
const localVarsCopy = { ...ctx.localVars };
|
|
1047
|
-
const scriptCtx = {
|
|
1048
|
-
envVars: envVarsCopy,
|
|
1049
|
-
collectionVars: collectionCopy,
|
|
1050
|
-
globals: globalsCopy,
|
|
1051
|
-
localVars: localVarsCopy,
|
|
1052
|
-
response: ctx.response,
|
|
1053
|
-
piiMaskPatterns: ctx.piiMaskPatterns
|
|
1054
|
-
};
|
|
1039
|
+
function buildSandbox(scriptCtx, testResults, consoleOutput, faker) {
|
|
1055
1040
|
const sp = buildAt(scriptCtx, testResults, consoleOutput);
|
|
1056
1041
|
const captureConsole = {
|
|
1057
1042
|
log: (...args) => consoleOutput.push(args.map(String).join(" ")),
|
|
@@ -1059,8 +1044,9 @@ async function runScript(code, ctx, timeoutMs = 5e3) {
|
|
|
1059
1044
|
error: (...args) => consoleOutput.push("[error] " + args.map(String).join(" ")),
|
|
1060
1045
|
info: (...args) => consoleOutput.push("[info] " + args.map(String).join(" "))
|
|
1061
1046
|
};
|
|
1062
|
-
|
|
1047
|
+
return {
|
|
1063
1048
|
sp,
|
|
1049
|
+
data: scriptCtx.data ?? {},
|
|
1064
1050
|
DOMParser: makeSandboxDOMParser,
|
|
1065
1051
|
dayjs,
|
|
1066
1052
|
faker,
|
|
@@ -1082,6 +1068,24 @@ async function runScript(code, ctx, timeoutMs = 5e3) {
|
|
|
1082
1068
|
// not available in sync vm context
|
|
1083
1069
|
setInterval: void 0
|
|
1084
1070
|
};
|
|
1071
|
+
}
|
|
1072
|
+
async function runScript(code, ctx, timeoutMs = 5e3) {
|
|
1073
|
+
const faker = await getFaker();
|
|
1074
|
+
const testResults = [];
|
|
1075
|
+
const consoleOutput = [];
|
|
1076
|
+
const envVarsCopy = { ...ctx.envVars };
|
|
1077
|
+
const collectionCopy = { ...ctx.collectionVars };
|
|
1078
|
+
const globalsCopy = { ...ctx.globals };
|
|
1079
|
+
const localVarsCopy = { ...ctx.localVars };
|
|
1080
|
+
const scriptCtx = {
|
|
1081
|
+
envVars: envVarsCopy,
|
|
1082
|
+
collectionVars: collectionCopy,
|
|
1083
|
+
globals: globalsCopy,
|
|
1084
|
+
localVars: localVarsCopy,
|
|
1085
|
+
response: ctx.response,
|
|
1086
|
+
piiMaskPatterns: ctx.piiMaskPatterns
|
|
1087
|
+
};
|
|
1088
|
+
const sandbox = buildSandbox(scriptCtx, testResults, consoleOutput, faker);
|
|
1085
1089
|
try {
|
|
1086
1090
|
vm__namespace.runInNewContext(code, sandbox, { timeout: timeoutMs, filename: "script.js" });
|
|
1087
1091
|
} catch (err) {
|
|
@@ -1105,6 +1109,42 @@ async function runScript(code, ctx, timeoutMs = 5e3) {
|
|
|
1105
1109
|
updatedLocalVars: localVarsCopy
|
|
1106
1110
|
};
|
|
1107
1111
|
}
|
|
1112
|
+
async function evaluateExpression(code, ctx, timeoutMs = 5e3) {
|
|
1113
|
+
const faker = await getFaker();
|
|
1114
|
+
const testResults = [];
|
|
1115
|
+
const consoleOutput = [];
|
|
1116
|
+
const envVarsCopy = { ...ctx.envVars };
|
|
1117
|
+
const collectionCopy = { ...ctx.collectionVars };
|
|
1118
|
+
const globalsCopy = { ...ctx.globals };
|
|
1119
|
+
const localVarsCopy = { ...ctx.localVars };
|
|
1120
|
+
const scriptCtx = {
|
|
1121
|
+
envVars: envVarsCopy,
|
|
1122
|
+
collectionVars: collectionCopy,
|
|
1123
|
+
globals: globalsCopy,
|
|
1124
|
+
localVars: localVarsCopy,
|
|
1125
|
+
response: ctx.response,
|
|
1126
|
+
piiMaskPatterns: ctx.piiMaskPatterns,
|
|
1127
|
+
data: ctx.data
|
|
1128
|
+
};
|
|
1129
|
+
const sandbox = buildSandbox(scriptCtx, testResults, consoleOutput, faker);
|
|
1130
|
+
const scopes = {
|
|
1131
|
+
updatedEnvVars: envVarsCopy,
|
|
1132
|
+
updatedCollectionVars: collectionCopy,
|
|
1133
|
+
updatedGlobals: globalsCopy,
|
|
1134
|
+
updatedLocalVars: localVarsCopy
|
|
1135
|
+
};
|
|
1136
|
+
try {
|
|
1137
|
+
let value;
|
|
1138
|
+
try {
|
|
1139
|
+
value = vm__namespace.runInNewContext(`(${code})`, sandbox, { timeout: timeoutMs, filename: "expression.js" });
|
|
1140
|
+
} catch {
|
|
1141
|
+
value = vm__namespace.runInNewContext(code, sandbox, { timeout: timeoutMs, filename: "expression.js" });
|
|
1142
|
+
}
|
|
1143
|
+
return { value, consoleOutput, ...scopes };
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
return { value: void 0, consoleOutput, ...scopes, error: err instanceof Error ? err.message : String(err) };
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1108
1148
|
function buildProxyUri(proxy) {
|
|
1109
1149
|
const raw = proxy.url.trim();
|
|
1110
1150
|
if (!raw) throw new Error("Proxy URL is empty");
|
|
@@ -2277,6 +2317,7 @@ exports.buildProxyUri = buildProxyUri;
|
|
|
2277
2317
|
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
2278
2318
|
exports.buildUrl = buildUrl;
|
|
2279
2319
|
exports.deriveRunStatus = deriveRunStatus;
|
|
2320
|
+
exports.evaluateExpression = evaluateExpression;
|
|
2280
2321
|
exports.executeRunnerRequest = executeRunnerRequest;
|
|
2281
2322
|
exports.fetchOAuth2Token = fetchOAuth2Token;
|
|
2282
2323
|
exports.getGlobals = getGlobals;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const handle = require("./handle-
|
|
2
|
+
const handle = require("./handle-BY_jaVBT.js");
|
|
3
3
|
const https = require("node:https");
|
|
4
4
|
const http = require("node:http");
|
|
5
5
|
const xmldom = require("@xmldom/xmldom");
|
|
@@ -313,7 +313,7 @@ function registerSoapHandlers(ipc) {
|
|
|
313
313
|
handle.handleIpc(ipc, handle.IPC.wsdl.import, async (_event, opts) => {
|
|
314
314
|
const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-sqUFqRmE.js"));
|
|
315
315
|
validateWsdlImport(opts);
|
|
316
|
-
const { importWsdl } = await Promise.resolve().then(() => require("./import-
|
|
316
|
+
const { importWsdl } = await Promise.resolve().then(() => require("./import-DR_r1DUP.js"));
|
|
317
317
|
const wsdlText = opts.xml ?? (opts.url ? await fetchUrl(opts.url) : "");
|
|
318
318
|
if (!wsdlText) throw new Error("wsdl:import requires either `url` or `xml`");
|
|
319
319
|
return importWsdl(wsdlText, { name: opts.name, existingMockPorts: opts.existingMockPorts });
|
package/out/main/compare.js
CHANGED
|
@@ -4,7 +4,7 @@ const promises = require("node:fs/promises");
|
|
|
4
4
|
const jsYaml = require("js-yaml");
|
|
5
5
|
const undici = require("undici");
|
|
6
6
|
const coverage = require("./chunks/coverage-C0U4QM83.js");
|
|
7
|
-
const cliCommon = require("./chunks/cli-common-
|
|
7
|
+
const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
|
|
8
8
|
require("node:path");
|
|
9
9
|
require("./chunks/data-files-BYqhsoPe.js");
|
|
10
10
|
require("node:crypto");
|
package/out/main/contract.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const promises = require("node:fs/promises");
|
|
4
4
|
const path = require("node:path");
|
|
5
|
-
const brokerClient = require("./chunks/broker-client-
|
|
5
|
+
const brokerClient = require("./chunks/broker-client-VwFF9sbp.js");
|
|
6
6
|
const escape = require("./chunks/escape-DDVAlOvZ.js");
|
|
7
7
|
const crypto = require("node:crypto");
|
|
8
8
|
const undici = require("undici");
|
|
9
9
|
const os = require("node:os");
|
|
10
|
-
const cliCommon = require("./chunks/cli-common-
|
|
10
|
+
const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
|
|
11
11
|
const node_child_process = require("node:child_process");
|
|
12
12
|
const jsYaml = require("js-yaml");
|
|
13
13
|
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
14
|
-
require("./chunks/request-exec-
|
|
14
|
+
require("./chunks/request-exec-CcLHtkwG.js");
|
|
15
15
|
require("fs/promises");
|
|
16
16
|
require("tls");
|
|
17
|
-
require("./chunks/handle-
|
|
17
|
+
require("./chunks/handle-BY_jaVBT.js");
|
|
18
18
|
require("node:fs");
|
|
19
19
|
require("dayjs");
|
|
20
20
|
require("node:vm");
|
package/out/main/coverage.js
CHANGED
|
@@ -4,7 +4,7 @@ const promises = require("node:fs/promises");
|
|
|
4
4
|
const jsYaml = require("js-yaml");
|
|
5
5
|
const undici = require("undici");
|
|
6
6
|
const coverage = require("./chunks/coverage-C0U4QM83.js");
|
|
7
|
-
const cliCommon = require("./chunks/cli-common-
|
|
7
|
+
const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
|
|
8
8
|
require("node:path");
|
|
9
9
|
require("./chunks/data-files-BYqhsoPe.js");
|
|
10
10
|
require("node:crypto");
|
package/out/main/flow.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const requestExec = require("./chunks/request-exec-CcLHtkwG.js");
|
|
6
|
+
const requestCollection = require("./chunks/request-collection-KGPuvN7o.js");
|
|
7
|
+
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
8
|
+
const flowEngine = require("./chunks/flow-engine-DuO1iX7c.js");
|
|
9
|
+
const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
|
|
10
|
+
require("undici");
|
|
11
|
+
require("fs/promises");
|
|
12
|
+
require("tls");
|
|
13
|
+
require("./chunks/handle-BY_jaVBT.js");
|
|
14
|
+
require("node:fs");
|
|
15
|
+
require("node:os");
|
|
16
|
+
require("node:crypto");
|
|
17
|
+
require("dayjs");
|
|
18
|
+
require("node:vm");
|
|
19
|
+
require("tv4");
|
|
20
|
+
require("jsonpath-plus");
|
|
21
|
+
require("@xmldom/xmldom");
|
|
22
|
+
require("node:http");
|
|
23
|
+
require("ajv");
|
|
24
|
+
require("uuid");
|
|
25
|
+
require("./chunks/data-files-BYqhsoPe.js");
|
|
26
|
+
function esc(s) {
|
|
27
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
28
|
+
}
|
|
29
|
+
function pretty(v) {
|
|
30
|
+
if (typeof v === "string") {
|
|
31
|
+
try {
|
|
32
|
+
return esc(JSON.stringify(JSON.parse(v), null, 2));
|
|
33
|
+
} catch {
|
|
34
|
+
return esc(v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return esc(JSON.stringify(v, null, 2));
|
|
39
|
+
} catch {
|
|
40
|
+
return esc(String(v));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const STATUS_CLS = {
|
|
44
|
+
passed: "badge-pass",
|
|
45
|
+
failed: "badge-fail",
|
|
46
|
+
error: "badge-err",
|
|
47
|
+
done: "badge-done",
|
|
48
|
+
skipped: "badge-skip",
|
|
49
|
+
running: "badge-done"
|
|
50
|
+
};
|
|
51
|
+
function blockDetail(b) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
if (b.error) parts.push(`<div class="err-row">⚠ ${esc(b.error)}</div>`);
|
|
54
|
+
if (b.testResults?.length) {
|
|
55
|
+
const rows = b.testResults.map(
|
|
56
|
+
(tr) => `<div class="test-row ${tr.passed ? "test-pass" : "test-fail"}"><span class="dot">${tr.passed ? "✓" : "✗"}</span> ${esc(tr.name)}${!tr.passed && tr.error ? `<div class="test-err">${esc(tr.error)}</div>` : ""}</div>`
|
|
57
|
+
).join("");
|
|
58
|
+
parts.push(`<div class="section-label">Tests</div><div class="tests-wrap">${rows}</div>`);
|
|
59
|
+
}
|
|
60
|
+
if (b.type === "request" && b.resolvedUrl) {
|
|
61
|
+
parts.push(`<div class="section-label">Request</div><div class="mono url">${esc(b.method ?? "")} ${esc(b.resolvedUrl)}</div>`);
|
|
62
|
+
}
|
|
63
|
+
if (b.type === "log" && b.message) {
|
|
64
|
+
parts.push(`<div class="section-label">Message</div><pre class="code-block">${esc(b.message)}</pre>`);
|
|
65
|
+
}
|
|
66
|
+
if (b.type === "display" && b.display !== void 0) {
|
|
67
|
+
parts.push(`<div class="section-label">Value</div><pre class="code-block">${pretty(b.display)}</pre>`);
|
|
68
|
+
}
|
|
69
|
+
return parts.join("");
|
|
70
|
+
}
|
|
71
|
+
function buildFlowHtmlReport(summary, meta = {}) {
|
|
72
|
+
const ts = meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
73
|
+
const flow = meta.flow ?? "Flow";
|
|
74
|
+
const env = meta.environment ?? "—";
|
|
75
|
+
const passRate = summary.total > 0 ? Math.round(summary.passed / summary.total * 100) : 0;
|
|
76
|
+
const cards = summary.blocks.map((b, idx) => {
|
|
77
|
+
const statusCls = STATUS_CLS[b.status] ?? "badge-done";
|
|
78
|
+
const httpCls = b.httpStatus == null ? "" : b.httpStatus < 400 ? "http-ok" : "http-err";
|
|
79
|
+
const dur = b.durationMs != null ? `${b.durationMs} ms` : "";
|
|
80
|
+
const at = b.atMs != null ? `+${b.atMs} ms` : "";
|
|
81
|
+
const detail = blockDetail(b);
|
|
82
|
+
return `
|
|
83
|
+
<div class="card" id="r${idx}">
|
|
84
|
+
<div class="card-header" ${detail ? `onclick="toggle(${idx})"` : ""}>
|
|
85
|
+
<span class="chevron" id="ch${idx}">${detail ? "▶" : ""}</span>
|
|
86
|
+
<span class="at mono muted">${esc(at)}</span>
|
|
87
|
+
<span class="badge ${statusCls}">${esc(b.status)}</span>
|
|
88
|
+
<span class="type mono">${esc(b.type)}</span>
|
|
89
|
+
<span class="card-name">${esc(b.label)}${b.iteration != null ? ` <span class="muted">#${b.iteration}</span>` : ""}</span>
|
|
90
|
+
<span class="card-url muted">${esc(b.resolvedUrl ?? "")}</span>
|
|
91
|
+
<span class="dur muted">${esc(dur)}</span>
|
|
92
|
+
${b.httpStatus != null ? `<span class="http-badge ${httpCls}">${b.httpStatus}</span>` : ""}
|
|
93
|
+
</div>
|
|
94
|
+
${detail ? `<div class="card-body" id="cb${idx}" style="display:none">${detail}</div>` : ""}
|
|
95
|
+
</div>`;
|
|
96
|
+
}).join("\n");
|
|
97
|
+
const vars = summary.variables;
|
|
98
|
+
const varRows = vars ? [
|
|
99
|
+
...Object.entries(vars.localVars).map(([k, v]) => [k, v, "local"]),
|
|
100
|
+
...Object.entries(vars.collectionVars).map(([k, v]) => [k, v, "collection"]),
|
|
101
|
+
...Object.entries(vars.globals).map(([k, v]) => [k, v, "global"])
|
|
102
|
+
].map(([k, v, scope]) => `<tr><td class="hk mono">${esc(k)}</td><td class="muted">${esc(scope)}</td><td class="hv mono">${esc(v)}</td></tr>`).join("") : "";
|
|
103
|
+
return `<!DOCTYPE html>
|
|
104
|
+
<html lang="en">
|
|
105
|
+
<head>
|
|
106
|
+
<meta charset="UTF-8">
|
|
107
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
108
|
+
<title>${esc(flow)} — Flow Report</title>
|
|
109
|
+
<style>
|
|
110
|
+
:root { --bg:#0f1117; --surface:#161b22; --border:#21262d; --text:#c9d1d9; --bright:#e6edf3; --muted:#8b949e;
|
|
111
|
+
--green:#3fb950; --red:#f85149; --amber:#d29922; --blue:#79c0ff;
|
|
112
|
+
--pass-bg:#0d3a1e; --fail-bg:#3d1014; --err-bg:#3d2a00; --skip-bg:#21262d; }
|
|
113
|
+
html.light { --bg:#fff; --surface:#f6f8fa; --border:#d0d7de; --text:#1f2328; --bright:#1f2328; --muted:#656d76;
|
|
114
|
+
--green:#1a7f37; --red:#cf222e; --amber:#9a6700; --blue:#0969da;
|
|
115
|
+
--pass-bg:#dafbe1; --fail-bg:#ffebe9; --err-bg:#fff8c5; --skip-bg:#f6f8fa; }
|
|
116
|
+
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
|
|
117
|
+
body { font-family:system-ui,sans-serif; background:var(--bg); color:var(--text); font-size:13px; line-height:1.5; }
|
|
118
|
+
.wrap { max-width:1100px; margin:0 auto; padding:32px 24px; }
|
|
119
|
+
h1 { font-size:20px; font-weight:600; color:var(--bright); margin-bottom:4px; }
|
|
120
|
+
.meta-line { color:var(--muted); font-size:11px; margin-bottom:24px; }
|
|
121
|
+
.mono { font-family:ui-monospace,Menlo,Consolas,monospace; }
|
|
122
|
+
.muted { color:var(--muted); }
|
|
123
|
+
.theme-toggle { position:fixed; top:16px; right:16px; background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:4px 10px; font-size:16px; cursor:pointer; line-height:1; }
|
|
124
|
+
.summary { display:flex; gap:12px; margin-bottom:24px; flex-wrap:wrap; }
|
|
125
|
+
.stat { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:12px 20px; min-width:90px; }
|
|
126
|
+
.stat-val { font-size:22px; font-weight:700; color:var(--bright); }
|
|
127
|
+
.stat-lbl { font-size:11px; color:var(--muted); margin-top:2px; }
|
|
128
|
+
.stat-pass .stat-val { color:var(--green); } .stat-fail .stat-val { color:var(--red); } .stat-err .stat-val { color:var(--amber); }
|
|
129
|
+
.section-title { font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); font-weight:600; margin:24px 0 8px; }
|
|
130
|
+
.card { background:var(--surface); border:1px solid var(--border); border-radius:8px; margin-bottom:6px; overflow:hidden; }
|
|
131
|
+
.card-header { display:flex; align-items:center; gap:10px; padding:10px 14px; cursor:default; }
|
|
132
|
+
.card-header[onclick] { cursor:pointer; }
|
|
133
|
+
.chevron { width:10px; color:var(--muted); font-size:9px; transition:transform .15s; }
|
|
134
|
+
.chevron.open { transform:rotate(90deg); }
|
|
135
|
+
.at { width:74px; font-size:11px; text-align:right; flex-shrink:0; }
|
|
136
|
+
.badge { font-size:10px; font-weight:700; text-transform:uppercase; padding:2px 7px; border-radius:4px; letter-spacing:.04em; }
|
|
137
|
+
.badge-pass { background:var(--pass-bg); color:var(--green); } .badge-fail { background:var(--fail-bg); color:var(--red); }
|
|
138
|
+
.badge-err { background:var(--err-bg); color:var(--amber); } .badge-done,.badge-skip { background:var(--skip-bg); color:var(--muted); }
|
|
139
|
+
.type { font-size:11px; color:var(--blue); min-width:64px; }
|
|
140
|
+
.card-name { font-weight:500; color:var(--bright); }
|
|
141
|
+
.card-url { font-family:ui-monospace,monospace; font-size:11px; margin-left:auto; max-width:40%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
142
|
+
.dur { font-size:11px; }
|
|
143
|
+
.http-badge { font-family:ui-monospace,monospace; font-size:11px; font-weight:600; padding:1px 6px; border-radius:4px; background:var(--skip-bg); }
|
|
144
|
+
.http-ok { color:var(--green); } .http-err { color:var(--red); }
|
|
145
|
+
.card-body { padding:12px 16px 14px 34px; border-top:1px solid var(--border); }
|
|
146
|
+
.section-label { font-size:10px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); font-weight:600; margin:10px 0 4px; }
|
|
147
|
+
.code-block { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px 10px; font-family:ui-monospace,monospace; font-size:11px; white-space:pre-wrap; word-break:break-all; max-height:360px; overflow:auto; }
|
|
148
|
+
.url { font-size:11px; }
|
|
149
|
+
.err-row { color:var(--red); font-size:12px; margin:4px 0; }
|
|
150
|
+
.test-row { font-size:12px; padding:2px 0; } .test-pass .dot { color:var(--green); } .test-fail { color:var(--red); } .test-fail .dot { color:var(--red); }
|
|
151
|
+
.test-err { color:var(--red); font-family:ui-monospace,monospace; font-size:11px; margin-left:16px; }
|
|
152
|
+
table.vars { width:100%; border-collapse:collapse; background:var(--surface); border:1px solid var(--border); border-radius:8px; overflow:hidden; }
|
|
153
|
+
table.vars td { padding:5px 10px; border-bottom:1px solid var(--border); font-size:12px; }
|
|
154
|
+
.hk { color:var(--bright); } .hv { color:var(--text); word-break:break-all; }
|
|
155
|
+
</style>
|
|
156
|
+
</head>
|
|
157
|
+
<body>
|
|
158
|
+
<button class="theme-toggle" onclick="document.documentElement.classList.toggle('light')" title="Toggle theme">◐</button>
|
|
159
|
+
<div class="wrap">
|
|
160
|
+
<h1>${esc(flow)}</h1>
|
|
161
|
+
<div class="meta-line">Flow report · ${esc(env)} · ${esc(ts)}</div>
|
|
162
|
+
<div class="summary">
|
|
163
|
+
<div class="stat stat-pass"><div class="stat-val">${summary.passed}</div><div class="stat-lbl">Passed</div></div>
|
|
164
|
+
<div class="stat stat-fail"><div class="stat-val">${summary.failed}</div><div class="stat-lbl">Failed</div></div>
|
|
165
|
+
<div class="stat stat-err"><div class="stat-val">${summary.errors}</div><div class="stat-lbl">Errors</div></div>
|
|
166
|
+
<div class="stat"><div class="stat-val">${summary.total}</div><div class="stat-lbl">Blocks</div></div>
|
|
167
|
+
<div class="stat"><div class="stat-val">${passRate}%</div><div class="stat-lbl">Pass rate</div></div>
|
|
168
|
+
<div class="stat"><div class="stat-val">${summary.durationMs}<span style="font-size:13px">ms</span></div><div class="stat-lbl">Duration</div></div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="section-title">Run log</div>
|
|
171
|
+
${cards}
|
|
172
|
+
${varRows ? `<div class="section-title">Variables</div><table class="vars"><tbody>${varRows}</tbody></table>` : ""}
|
|
173
|
+
</div>
|
|
174
|
+
<script>
|
|
175
|
+
function toggle(i){ var b=document.getElementById('cb'+i), c=document.getElementById('ch'+i);
|
|
176
|
+
if(!b)return; var open=b.style.display!=='none'; b.style.display=open?'none':'block'; if(c)c.classList.toggle('open',!open); }
|
|
177
|
+
<\/script>
|
|
178
|
+
</body>
|
|
179
|
+
</html>`;
|
|
180
|
+
}
|
|
181
|
+
function buildJunit(flowName, summary) {
|
|
182
|
+
const cases = summary.blocks.filter((b) => b.type === "request" || b.type === "validate").map((b) => {
|
|
183
|
+
const name = `${b.label}${b.resolvedUrl ? ` ${b.resolvedUrl}` : ""}`;
|
|
184
|
+
const inner = b.status === "failed" || b.status === "error" ? `
|
|
185
|
+
<failure message="${escapeXml(b.error ?? "assertion failed")}"/>
|
|
186
|
+
` : "";
|
|
187
|
+
return ` <testcase name="${escapeXml(name)}" classname="${escapeXml(flowName)}" time="${((b.durationMs ?? 0) / 1e3).toFixed(3)}">${inner}</testcase>`;
|
|
188
|
+
}).join("\n");
|
|
189
|
+
const failures = summary.failed;
|
|
190
|
+
const errors = summary.errors;
|
|
191
|
+
const tests = summary.blocks.filter((b) => b.type === "request" || b.type === "validate").length;
|
|
192
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
193
|
+
<testsuites>
|
|
194
|
+
<testsuite name="${escapeXml(flowName)}" tests="${tests}" failures="${failures}" errors="${errors}" time="${(summary.durationMs / 1e3).toFixed(3)}">
|
|
195
|
+
${cases}
|
|
196
|
+
</testsuite>
|
|
197
|
+
</testsuites>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
function escapeXml(s) {
|
|
201
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
202
|
+
}
|
|
203
|
+
async function main() {
|
|
204
|
+
const argv = process.argv.slice(2);
|
|
205
|
+
const positionals = [];
|
|
206
|
+
for (const a of argv) {
|
|
207
|
+
if (a.startsWith("--")) break;
|
|
208
|
+
positionals.push(a);
|
|
209
|
+
}
|
|
210
|
+
const sub = positionals[0];
|
|
211
|
+
const name = positionals[1];
|
|
212
|
+
const args = cliCommon.parseArgs(argv);
|
|
213
|
+
if (args.help || !sub || sub !== "run" && sub !== "list") {
|
|
214
|
+
console.log(
|
|
215
|
+
"\nUsage:\n api-spector flow list --workspace <path>\n api-spector flow run <name> --workspace <path> [--environment <name>]\n [--output <path>] [--format json|junit] [--verbose]\n"
|
|
216
|
+
);
|
|
217
|
+
process.exit(args.help ? 0 : 1);
|
|
218
|
+
}
|
|
219
|
+
const wsPath = args.workspace;
|
|
220
|
+
if (!wsPath) {
|
|
221
|
+
console.error(cliCommon.color("Error: --workspace is required", cliCommon.C.red));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
let workspace, wsDir;
|
|
225
|
+
try {
|
|
226
|
+
;
|
|
227
|
+
({ workspace, dir: wsDir } = await cliCommon.loadWorkspace(wsPath));
|
|
228
|
+
} catch {
|
|
229
|
+
console.error(cliCommon.color(`Error: could not read workspace: ${wsPath}`, cliCommon.C.red));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
await requestExec.loadGlobals(wsDir);
|
|
233
|
+
requestExec.setSecretsConfig(workspace.settings?.secrets);
|
|
234
|
+
const flows = (await cliCommon.loadFlows(
|
|
235
|
+
workspace,
|
|
236
|
+
wsDir,
|
|
237
|
+
(relPath) => console.error(cliCommon.color(` [warn] Could not load flow: ${relPath}`, cliCommon.C.yellow))
|
|
238
|
+
)).map(flowEngine.normalizeFlow);
|
|
239
|
+
if (sub === "list") {
|
|
240
|
+
if (flows.length === 0) {
|
|
241
|
+
console.log(cliCommon.color(" No flows in this workspace.", cliCommon.C.gray));
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
console.log("");
|
|
245
|
+
for (const f of flows) {
|
|
246
|
+
const steps = f.nodes.filter((n) => n.type !== "start" && n.type !== "end").length;
|
|
247
|
+
console.log(` ${cliCommon.color(f.name, cliCommon.C.bold, cliCommon.C.white)} ${cliCommon.color(`(${steps} blocks)`, cliCommon.C.gray)}`);
|
|
248
|
+
}
|
|
249
|
+
console.log("");
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
if (!name) {
|
|
253
|
+
console.error(cliCommon.color("Error: a flow name is required: api-spector flow run <name>", cliCommon.C.red));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
const flow = flows.find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
257
|
+
if (!flow) {
|
|
258
|
+
console.error(cliCommon.color(`Error: flow "${name}" not found. Run \`api-spector flow list\` to see available flows.`, cliCommon.C.red));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const collections = await cliCommon.loadCollections(workspace, wsDir, {
|
|
262
|
+
onError: (relPath) => console.error(cliCommon.color(` [warn] Could not load collection: ${relPath}`, cliCommon.C.yellow))
|
|
263
|
+
});
|
|
264
|
+
const environments$1 = await cliCommon.loadEnvironments(workspace, wsDir);
|
|
265
|
+
const envName = args.environment ?? args.env;
|
|
266
|
+
const env = environments.selectEnvironment(workspace, environments$1, envName);
|
|
267
|
+
const verbose = Boolean(args.verbose);
|
|
268
|
+
const version = `v${"0.7.3"}`;
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log(cliCommon.color(" API Flow Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
|
|
271
|
+
console.log(cliCommon.color(` Flow: ${flow.name}`, cliCommon.C.gray));
|
|
272
|
+
console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
|
|
273
|
+
console.log(cliCommon.color(` Environment: ${env?.name ?? "(none)"}`, cliCommon.C.gray));
|
|
274
|
+
console.log("");
|
|
275
|
+
const envVars = await requestExec.buildEnvVars(env);
|
|
276
|
+
const globals = requestExec.getGlobals();
|
|
277
|
+
const dispatcher = await requestExec.buildDispatcher(workspace.settings?.proxy, workspace.settings?.tls);
|
|
278
|
+
const piiMaskPatterns = workspace.settings?.piiMaskPatterns ?? [];
|
|
279
|
+
const colById = new Map(collections.map((c) => [c.id, c]));
|
|
280
|
+
const printEvent = (evt) => {
|
|
281
|
+
const spec = flowEngine.BLOCK_SPECS[evt.blockType];
|
|
282
|
+
if (evt.kind === "log") {
|
|
283
|
+
console.log(cliCommon.color(` · ${evt.message}`, cliCommon.C.cyan));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (evt.kind === "display") {
|
|
287
|
+
const v = evt.display?.value;
|
|
288
|
+
console.log(cliCommon.color(` ▣ ${evt.display?.label ?? "display"}:`, cliCommon.C.magenta), typeof v === "string" ? v : JSON.stringify(v, null, 2));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (evt.kind !== "block-result") return;
|
|
292
|
+
const icon = evt.status === "passed" ? cliCommon.color("✓", cliCommon.C.green, cliCommon.C.bold) : evt.status === "failed" ? cliCommon.color("✗", cliCommon.C.red, cliCommon.C.bold) : evt.status === "error" ? cliCommon.color("⚠", cliCommon.C.yellow, cliCommon.C.bold) : cliCommon.color("•", cliCommon.C.gray);
|
|
293
|
+
const http = evt.httpStatus ? cliCommon.color(` ${evt.httpStatus}`, evt.httpStatus < 400 ? cliCommon.C.green : cliCommon.C.red) : "";
|
|
294
|
+
const dur = evt.durationMs !== void 0 ? cliCommon.color(` ${evt.durationMs}ms`, cliCommon.C.gray) : "";
|
|
295
|
+
const iter = evt.iteration !== void 0 ? cliCommon.color(` [#${evt.iteration}]`, cliCommon.C.gray) : "";
|
|
296
|
+
console.log(` ${icon} ${cliCommon.color((spec?.label ?? evt.blockType).padEnd(12), cliCommon.C.cyan)}${iter}${http}${dur}`);
|
|
297
|
+
if (verbose && evt.testResults?.length) {
|
|
298
|
+
for (const t of evt.testResults) {
|
|
299
|
+
console.log(` ${t.passed ? cliCommon.color("✓", cliCommon.C.green) : cliCommon.color("✗", cliCommon.C.red)} ${t.name}${t.error ? cliCommon.color(` - ${t.error}`, cliCommon.C.red) : ""}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (evt.error) console.log(cliCommon.color(` Error: ${evt.error}`, cliCommon.C.red));
|
|
303
|
+
};
|
|
304
|
+
const deps = {
|
|
305
|
+
runRequest: async (ref, input) => {
|
|
306
|
+
const col = colById.get(ref.collectionId);
|
|
307
|
+
const original = col?.requests[ref.requestId];
|
|
308
|
+
if (!col || !original) return null;
|
|
309
|
+
const req = JSON.parse(JSON.stringify(original));
|
|
310
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(original.id, col);
|
|
311
|
+
if (!requestCollection.authIsConfigured(req.auth) && inherited.auth && inherited.auth.type !== "none") req.auth = inherited.auth;
|
|
312
|
+
const inheritedHeaders = inherited.headers.filter((h) => h.enabled && h.key);
|
|
313
|
+
if (inheritedHeaders.length) req.headers = [...inheritedHeaders, ...req.headers ?? []];
|
|
314
|
+
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await requestExec.executeRunnerRequest({
|
|
315
|
+
req,
|
|
316
|
+
collectionVars: { ...col.collectionVariables ?? {}, ...input.collectionVars },
|
|
317
|
+
envVars: input.envVars,
|
|
318
|
+
globals: input.globals,
|
|
319
|
+
localVars: input.localVars,
|
|
320
|
+
// Flows chain on the real response body (not the PII-masked report copy).
|
|
321
|
+
dispatcher,
|
|
322
|
+
piiMaskPatterns: [],
|
|
323
|
+
proxy: workspace.settings?.proxy,
|
|
324
|
+
tls: workspace.settings?.tls
|
|
325
|
+
});
|
|
326
|
+
const rr = result.receivedResponse;
|
|
327
|
+
const response = rr ? { status: rr.status, statusText: rr.statusText, headers: rr.headers, body: rr.body, bodySize: rr.body?.length ?? 0, durationMs: result.durationMs ?? 0 } : null;
|
|
328
|
+
return {
|
|
329
|
+
envVars: updatedEnvVars,
|
|
330
|
+
collectionVars: updatedCollectionVars,
|
|
331
|
+
globals: updatedGlobals,
|
|
332
|
+
localVars: updatedLocalVars,
|
|
333
|
+
response,
|
|
334
|
+
status: result.status,
|
|
335
|
+
httpStatus: result.httpStatus,
|
|
336
|
+
durationMs: result.durationMs,
|
|
337
|
+
error: result.error,
|
|
338
|
+
testResults: result.testResults,
|
|
339
|
+
name: result.name,
|
|
340
|
+
method: result.method,
|
|
341
|
+
resolvedUrl: result.resolvedUrl
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
runScript: async (code, input) => {
|
|
345
|
+
const out = await requestExec.runScript(code, { envVars: input.envVars, collectionVars: input.collectionVars, globals: input.globals, localVars: input.localVars, response: input.response ?? void 0, piiMaskPatterns, data: input.data });
|
|
346
|
+
if (verbose && out.consoleOutput?.length) for (const line of out.consoleOutput) console.log(cliCommon.color(` ${line}`, cliCommon.C.gray));
|
|
347
|
+
return { envVars: out.updatedEnvVars, collectionVars: out.updatedCollectionVars, globals: out.updatedGlobals, localVars: out.updatedLocalVars, testResults: out.testResults, consoleOutput: out.consoleOutput, error: out.error };
|
|
348
|
+
},
|
|
349
|
+
evalExpression: async (code, input) => {
|
|
350
|
+
const out = await requestExec.evaluateExpression(code, { envVars: input.envVars, collectionVars: input.collectionVars, globals: input.globals, localVars: input.localVars, response: input.response ?? void 0, piiMaskPatterns, data: input.data });
|
|
351
|
+
return { envVars: out.updatedEnvVars, collectionVars: out.updatedCollectionVars, globals: out.updatedGlobals, localVars: out.updatedLocalVars, value: out.value, consoleOutput: out.consoleOutput, error: out.error };
|
|
352
|
+
},
|
|
353
|
+
resolveFlow: (flowId) => flows.find((f) => f.id === flowId) ?? null,
|
|
354
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
355
|
+
interpolate: (str, vars) => requestExec.interpolate(str, vars),
|
|
356
|
+
onEvent: printEvent
|
|
357
|
+
};
|
|
358
|
+
let summary;
|
|
359
|
+
try {
|
|
360
|
+
summary = await flowEngine.runFlow(flow, deps, { envVars, globals });
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.error(cliCommon.color(`
|
|
363
|
+
Flow aborted: ${err instanceof Error ? err.message : String(err)}`, cliCommon.C.red));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
console.log("");
|
|
367
|
+
console.log(
|
|
368
|
+
` ${cliCommon.color(`${summary.passed} passed`, cliCommon.C.green)}` + (summary.failed ? ` ${cliCommon.color(`${summary.failed} failed`, cliCommon.C.red)}` : "") + (summary.errors ? ` ${cliCommon.color(`${summary.errors} errors`, cliCommon.C.yellow)}` : "") + cliCommon.color(` ${summary.total} blocks · ${summary.durationMs}ms`, cliCommon.C.gray)
|
|
369
|
+
);
|
|
370
|
+
console.log("");
|
|
371
|
+
if (verbose && summary.variables) {
|
|
372
|
+
const v = summary.variables;
|
|
373
|
+
const rows = [
|
|
374
|
+
...Object.entries(v.localVars).map(([k, val]) => [k, val, "local"]),
|
|
375
|
+
...Object.entries(v.collectionVars).map(([k, val]) => [k, val, "collection"]),
|
|
376
|
+
...Object.entries(v.globals).map(([k, val]) => [k, val, "global"])
|
|
377
|
+
];
|
|
378
|
+
if (rows.length) {
|
|
379
|
+
console.log(cliCommon.color(" Variables:", cliCommon.C.bold));
|
|
380
|
+
for (const [k, val, scope] of rows) {
|
|
381
|
+
console.log(` ${cliCommon.color(k, cliCommon.C.cyan)} ${cliCommon.color(`(${scope})`, cliCommon.C.gray)} = ${val.length > 80 ? val.slice(0, 80) + "…" : val}`);
|
|
382
|
+
}
|
|
383
|
+
console.log("");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const outputPath = args.output;
|
|
387
|
+
if (outputPath) {
|
|
388
|
+
const explicit = args.format?.toLowerCase();
|
|
389
|
+
const ext = path.extname(outputPath).toLowerCase();
|
|
390
|
+
const fmt = explicit ?? (ext === ".xml" ? "junit" : ext === ".html" ? "html" : "json");
|
|
391
|
+
const content = fmt === "junit" ? buildJunit(flow.name, summary) : fmt === "html" ? buildFlowHtmlReport(summary, { flow: flow.name, environment: env?.name ?? null }) : JSON.stringify(summary, null, 2);
|
|
392
|
+
await promises.writeFile(outputPath, content, "utf8");
|
|
393
|
+
console.log(cliCommon.color(` Report written to ${outputPath} (${fmt})`, cliCommon.C.gray));
|
|
394
|
+
console.log("");
|
|
395
|
+
}
|
|
396
|
+
process.exit(summary.failed + summary.errors > 0 ? 1 : 0);
|
|
397
|
+
}
|
|
398
|
+
main().catch((err) => {
|
|
399
|
+
console.error(err);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
});
|
|
@@ -5,7 +5,7 @@ const jsYaml = require("js-yaml");
|
|
|
5
5
|
const undici = require("undici");
|
|
6
6
|
const uuid = require("uuid");
|
|
7
7
|
const coverage = require("./chunks/coverage-C0U4QM83.js");
|
|
8
|
-
const cliCommon = require("./chunks/cli-common-
|
|
8
|
+
const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
|
|
9
9
|
require("node:path");
|
|
10
10
|
require("./chunks/data-files-BYqhsoPe.js");
|
|
11
11
|
require("node:crypto");
|