@testsmith/api-spector 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/main/chunks/{request-handler-9MdHOWVf.js → request-handler-CvqESn11.js} +7 -0
- package/out/main/index.js +118 -2
- package/out/main/runner.js +228 -10
- package/out/preload/index.js +20 -0
- package/out/renderer/assets/{index-CTaJ83Dp.js → index-BXpiAoKF.js} +931 -104
- package/out/renderer/assets/index-QI8AWMd3.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +2 -1
- package/out/renderer/assets/index-DE4e3iXO.css +0 -2
|
@@ -208,6 +208,13 @@ async function buildEnvVars(environment) {
|
|
|
208
208
|
} catch {
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
if (vars[v.key] === void 0 && process.env[v.key] !== void 0) {
|
|
212
|
+
vars[v.key] = process.env[v.key];
|
|
213
|
+
}
|
|
214
|
+
} else if (v.secret) {
|
|
215
|
+
if (process.env[v.key] !== void 0) {
|
|
216
|
+
vars[v.key] = process.env[v.key];
|
|
217
|
+
}
|
|
211
218
|
} else {
|
|
212
219
|
vars[v.key] = v.value;
|
|
213
220
|
}
|
package/out/main/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const electron = require("electron");
|
|
|
25
25
|
const path = require("path");
|
|
26
26
|
const fs = require("fs");
|
|
27
27
|
const promises = require("fs/promises");
|
|
28
|
-
const requestHandler = require("./chunks/request-handler-
|
|
28
|
+
const requestHandler = require("./chunks/request-handler-CvqESn11.js");
|
|
29
29
|
const uuid = require("uuid");
|
|
30
30
|
const jsYaml = require("js-yaml");
|
|
31
31
|
const undici = require("undici");
|
|
@@ -35,6 +35,7 @@ const http = require("http");
|
|
|
35
35
|
const WebSocket = require("ws");
|
|
36
36
|
const https = require("https");
|
|
37
37
|
const Ajv = require("ajv");
|
|
38
|
+
const simpleGit = require("simple-git");
|
|
38
39
|
require("crypto");
|
|
39
40
|
require("dayjs");
|
|
40
41
|
require("vm");
|
|
@@ -86,7 +87,7 @@ function registerFileHandlers(ipc) {
|
|
|
86
87
|
await requestHandler.loadGlobals(workspaceDir);
|
|
87
88
|
await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
|
|
88
89
|
await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
|
|
89
|
-
const gitignore = "# api Spector — never commit secrets\n*.secrets\n.env.local\n";
|
|
90
|
+
const gitignore = "# api Spector — never commit secrets\n*.secrets\n.env.local\n\n# Dependencies\nnode_modules/\n";
|
|
90
91
|
await atomicWrite(path.join(workspaceDir, ".gitignore"), gitignore);
|
|
91
92
|
const ws = {
|
|
92
93
|
version: "1.0",
|
|
@@ -3436,6 +3437,120 @@ function registerContractHandlers(ipc) {
|
|
|
3436
3437
|
return schema ? JSON.stringify(schema, null, 2) : null;
|
|
3437
3438
|
});
|
|
3438
3439
|
}
|
|
3440
|
+
function git() {
|
|
3441
|
+
const dir = getWorkspaceDir();
|
|
3442
|
+
if (!dir) throw new Error("No workspace open");
|
|
3443
|
+
return simpleGit.simpleGit(dir);
|
|
3444
|
+
}
|
|
3445
|
+
function registerGitHandlers(ipc) {
|
|
3446
|
+
ipc.handle("git:isRepo", async () => {
|
|
3447
|
+
try {
|
|
3448
|
+
await git().revparse(["--git-dir"]);
|
|
3449
|
+
return true;
|
|
3450
|
+
} catch {
|
|
3451
|
+
return false;
|
|
3452
|
+
}
|
|
3453
|
+
});
|
|
3454
|
+
ipc.handle("git:init", async () => {
|
|
3455
|
+
await git().init();
|
|
3456
|
+
});
|
|
3457
|
+
ipc.handle("git:status", async () => {
|
|
3458
|
+
const result = await git().status();
|
|
3459
|
+
return {
|
|
3460
|
+
staged: result.staged.map((f) => ({ path: f, status: resolveStatus(result, f, true) })),
|
|
3461
|
+
unstaged: result.modified.filter((f) => !result.staged.includes(f)).concat(result.deleted.filter((f) => !result.staged.includes(f))).map((f) => ({ path: f, status: resolveStatus(result, f, false) })),
|
|
3462
|
+
untracked: result.not_added.map((f) => ({ path: f, status: "untracked" })),
|
|
3463
|
+
branch: result.current ?? "",
|
|
3464
|
+
ahead: result.ahead,
|
|
3465
|
+
behind: result.behind,
|
|
3466
|
+
remote: result.tracking ?? null
|
|
3467
|
+
};
|
|
3468
|
+
});
|
|
3469
|
+
ipc.handle("git:diff", async (_e, filePath) => {
|
|
3470
|
+
if (filePath) return git().diff(["--", filePath]);
|
|
3471
|
+
return git().diff();
|
|
3472
|
+
});
|
|
3473
|
+
ipc.handle("git:diffStaged", async (_e, filePath) => {
|
|
3474
|
+
if (filePath) return git().diff(["--cached", "--", filePath]);
|
|
3475
|
+
return git().diff(["--cached"]);
|
|
3476
|
+
});
|
|
3477
|
+
ipc.handle("git:stage", async (_e, paths) => {
|
|
3478
|
+
await git().add(paths);
|
|
3479
|
+
});
|
|
3480
|
+
ipc.handle("git:unstage", async (_e, paths) => {
|
|
3481
|
+
await git().reset(["HEAD", "--", ...paths]);
|
|
3482
|
+
});
|
|
3483
|
+
ipc.handle("git:stageAll", async () => {
|
|
3484
|
+
await git().add(["."]);
|
|
3485
|
+
});
|
|
3486
|
+
ipc.handle("git:commit", async (_e, message) => {
|
|
3487
|
+
await git().commit(message);
|
|
3488
|
+
});
|
|
3489
|
+
ipc.handle("git:log", async (_e, limit = 50) => {
|
|
3490
|
+
const result = await git().log({ maxCount: limit });
|
|
3491
|
+
return result.all.map((c) => ({
|
|
3492
|
+
hash: c.hash,
|
|
3493
|
+
short: c.hash.slice(0, 7),
|
|
3494
|
+
message: c.message,
|
|
3495
|
+
author: c.author_name,
|
|
3496
|
+
email: c.author_email,
|
|
3497
|
+
date: c.date
|
|
3498
|
+
}));
|
|
3499
|
+
});
|
|
3500
|
+
ipc.handle("git:branches", async () => {
|
|
3501
|
+
const result = await git().branch(["-a", "--format=%(refname:short)|%(objectname:short)|%(upstream:short)|%(upstream:track)"]);
|
|
3502
|
+
return result.all.filter((name) => !name.includes("HEAD")).map((name) => ({
|
|
3503
|
+
name: name.replace(/^remotes\//, ""),
|
|
3504
|
+
current: name === result.current,
|
|
3505
|
+
remote: name.startsWith("remotes/")
|
|
3506
|
+
}));
|
|
3507
|
+
});
|
|
3508
|
+
ipc.handle("git:checkout", async (_e, branch, create) => {
|
|
3509
|
+
if (create) await git().checkoutLocalBranch(branch);
|
|
3510
|
+
else await git().checkout(branch);
|
|
3511
|
+
});
|
|
3512
|
+
ipc.handle("git:pull", async () => {
|
|
3513
|
+
await git().pull();
|
|
3514
|
+
});
|
|
3515
|
+
ipc.handle("git:push", async (_e, setUpstream) => {
|
|
3516
|
+
if (setUpstream) {
|
|
3517
|
+
const status = await git().status();
|
|
3518
|
+
await git().push(["--set-upstream", "origin", status.current ?? "main"]);
|
|
3519
|
+
} else {
|
|
3520
|
+
await git().push();
|
|
3521
|
+
}
|
|
3522
|
+
});
|
|
3523
|
+
ipc.handle("git:remotes", async () => {
|
|
3524
|
+
const result = await git().getRemotes(true);
|
|
3525
|
+
return result.map((r) => ({ name: r.name, url: r.refs.fetch || r.refs.push || "" }));
|
|
3526
|
+
});
|
|
3527
|
+
ipc.handle("git:addRemote", async (_e, name, url) => {
|
|
3528
|
+
await git().addRemote(name, url);
|
|
3529
|
+
});
|
|
3530
|
+
ipc.handle("git:setRemoteUrl", async (_e, name, url) => {
|
|
3531
|
+
await git().remote(["set-url", name, url]);
|
|
3532
|
+
});
|
|
3533
|
+
ipc.handle("git:removeRemote", async (_e, name) => {
|
|
3534
|
+
await git().removeRemote(name);
|
|
3535
|
+
});
|
|
3536
|
+
ipc.handle("git:writeCiFile", async (_e, relPath, content) => {
|
|
3537
|
+
const wsDir = getWorkspaceDir();
|
|
3538
|
+
if (!wsDir) throw new Error("No workspace open");
|
|
3539
|
+
const fullPath = path.join(wsDir, relPath);
|
|
3540
|
+
await promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
3541
|
+
await promises.writeFile(fullPath, content, "utf8");
|
|
3542
|
+
});
|
|
3543
|
+
}
|
|
3544
|
+
function resolveStatus(result, filePath, staged) {
|
|
3545
|
+
if (staged) {
|
|
3546
|
+
if (result.created.includes(filePath)) return "added";
|
|
3547
|
+
if (result.deleted.includes(filePath)) return "deleted";
|
|
3548
|
+
if (result.renamed.find((r) => r.to === filePath || r.from === filePath)) return "renamed";
|
|
3549
|
+
return "modified";
|
|
3550
|
+
}
|
|
3551
|
+
if (result.deleted.includes(filePath)) return "deleted";
|
|
3552
|
+
return "modified";
|
|
3553
|
+
}
|
|
3439
3554
|
if (process.env.ELECTRON_NO_SANDBOX === "1") {
|
|
3440
3555
|
electron.app.commandLine.appendSwitch("no-sandbox");
|
|
3441
3556
|
electron.app.commandLine.appendSwitch("disable-features", "RendererCodeIntegrity");
|
|
@@ -3536,6 +3651,7 @@ electron.app.whenReady().then(async () => {
|
|
|
3536
3651
|
registerSoapHandlers(electron.ipcMain);
|
|
3537
3652
|
registerDocsHandlers(electron.ipcMain);
|
|
3538
3653
|
registerContractHandlers(electron.ipcMain);
|
|
3654
|
+
registerGitHandlers(electron.ipcMain);
|
|
3539
3655
|
createWindow();
|
|
3540
3656
|
electron.app.on("activate", () => {
|
|
3541
3657
|
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
|
package/out/main/runner.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const undici = require("undici");
|
|
6
|
-
const requestHandler = require("./chunks/request-handler-
|
|
6
|
+
const requestHandler = require("./chunks/request-handler-CvqESn11.js");
|
|
7
7
|
require("crypto");
|
|
8
8
|
require("dayjs");
|
|
9
9
|
require("vm");
|
|
@@ -35,6 +35,177 @@ function buildJsonReport(results, summary, meta = {}) {
|
|
|
35
35
|
}))
|
|
36
36
|
}, null, 2);
|
|
37
37
|
}
|
|
38
|
+
function buildHtmlReport(results, summary, meta = {}) {
|
|
39
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
40
|
+
function prettyJson(s) {
|
|
41
|
+
try {
|
|
42
|
+
return esc(JSON.stringify(JSON.parse(s), null, 2));
|
|
43
|
+
} catch {
|
|
44
|
+
return esc(s);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function headersTable(h) {
|
|
48
|
+
const rows = Object.entries(h).map(
|
|
49
|
+
([k, v]) => `<tr><td class="hk">${esc(k)}</td><td class="hv">${esc(v)}</td></tr>`
|
|
50
|
+
).join("");
|
|
51
|
+
return rows ? `<table class="htable"><tbody>${rows}</tbody></table>` : '<span class="muted">none</span>';
|
|
52
|
+
}
|
|
53
|
+
const ts = meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
54
|
+
const collection = meta.collection ?? "API Tests";
|
|
55
|
+
const env = meta.environment ?? "—";
|
|
56
|
+
const passRate = summary.total > 0 ? Math.round(summary.passed / summary.total * 100) : 0;
|
|
57
|
+
const cards = results.map((r, idx) => {
|
|
58
|
+
const statusCls = r.status === "passed" ? "badge-pass" : r.status === "failed" ? "badge-fail" : "badge-err";
|
|
59
|
+
const httpCls = r.httpStatus && r.httpStatus < 300 ? "http-ok" : r.httpStatus && r.httpStatus < 400 ? "http-redir" : "http-err";
|
|
60
|
+
const dur = r.durationMs != null ? `${r.durationMs} ms` : "—";
|
|
61
|
+
const label = r.iterationLabel ? ` <span class="muted">#${esc(r.iterationLabel)}</span>` : "";
|
|
62
|
+
const testRows = (r.testResults ?? []).map(
|
|
63
|
+
(t) => `<div class="test-row ${t.passed ? "test-pass" : "test-fail"}">
|
|
64
|
+
<span class="dot">${t.passed ? "✓" : "✗"}</span> ${esc(t.name)}
|
|
65
|
+
${!t.passed ? `<div class="test-err">${esc(t.error ?? "")}</div>` : ""}
|
|
66
|
+
</div>`
|
|
67
|
+
).join("");
|
|
68
|
+
const errRows = [
|
|
69
|
+
r.error ? `<div class="err-row">⚠ ${esc(r.error)}</div>` : "",
|
|
70
|
+
r.preScriptError ? `<div class="err-row">⚠ Pre-script: ${esc(r.preScriptError)}</div>` : "",
|
|
71
|
+
r.postScriptError ? `<div class="err-row">⚠ Post-script: ${esc(r.postScriptError)}</div>` : ""
|
|
72
|
+
].filter(Boolean).join("");
|
|
73
|
+
const consoleHtml = (r.consoleOutput ?? []).length ? `<div class="section-label">Console</div>
|
|
74
|
+
<div class="code-block">${(r.consoleOutput ?? []).map((l) => `<div>${esc(l)}</div>`).join("")}</div>` : "";
|
|
75
|
+
const reqHeaders = r.sentRequest?.headers ?? {};
|
|
76
|
+
const reqBody = r.sentRequest?.body;
|
|
77
|
+
const reqHtml = `
|
|
78
|
+
<div class="panel-label">Request</div>
|
|
79
|
+
<div class="panel req-panel">
|
|
80
|
+
<div class="req-line"><span class="method-badge">${esc(r.method)}</span> <span class="mono">${esc(r.resolvedUrl ?? "")}</span></div>
|
|
81
|
+
<div class="section-label">Headers</div>
|
|
82
|
+
${headersTable(reqHeaders)}
|
|
83
|
+
${reqBody ? `<div class="section-label">Body</div><pre class="code-block">${prettyJson(reqBody)}</pre>` : ""}
|
|
84
|
+
</div>`;
|
|
85
|
+
const resp = r.receivedResponse;
|
|
86
|
+
const respHtml = resp ? `
|
|
87
|
+
<div class="panel-label">Response</div>
|
|
88
|
+
<div class="panel resp-panel">
|
|
89
|
+
<div class="resp-status ${httpCls}">${resp.status} ${esc(resp.statusText)}</div>
|
|
90
|
+
<div class="section-label">Headers</div>
|
|
91
|
+
${headersTable(resp.headers)}
|
|
92
|
+
${resp.body ? `<div class="section-label">Body</div><pre class="code-block">${prettyJson(resp.body)}</pre>` : ""}
|
|
93
|
+
</div>` : "";
|
|
94
|
+
return `
|
|
95
|
+
<div class="card" id="r${idx}">
|
|
96
|
+
<div class="card-header" onclick="toggle(${idx})">
|
|
97
|
+
<span class="chevron" id="ch${idx}">▶</span>
|
|
98
|
+
<span class="badge ${statusCls}">${r.status}</span>
|
|
99
|
+
<span class="method mono">${esc(r.method)}</span>
|
|
100
|
+
<span class="card-name">${esc(r.name)}${label}</span>
|
|
101
|
+
<span class="card-url muted">${esc(r.resolvedUrl ?? "")}</span>
|
|
102
|
+
<span class="dur muted">${dur}</span>
|
|
103
|
+
${r.httpStatus ? `<span class="http-badge ${httpCls}">${r.httpStatus}</span>` : ""}
|
|
104
|
+
</div>
|
|
105
|
+
<div class="card-body" id="cb${idx}" style="display:none">
|
|
106
|
+
${errRows}
|
|
107
|
+
${testRows ? `<div class="section-label">Tests</div><div class="tests-wrap">${testRows}</div>` : ""}
|
|
108
|
+
${consoleHtml}
|
|
109
|
+
<div class="req-resp-grid">
|
|
110
|
+
${reqHtml}
|
|
111
|
+
${respHtml}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>`;
|
|
115
|
+
}).join("\n");
|
|
116
|
+
return `<!DOCTYPE html>
|
|
117
|
+
<html lang="en">
|
|
118
|
+
<head>
|
|
119
|
+
<meta charset="UTF-8">
|
|
120
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
121
|
+
<title>${esc(collection)} — Test Results</title>
|
|
122
|
+
<style>
|
|
123
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
124
|
+
body { font-family: system-ui, sans-serif; background: #0f1117; color: #c9d1d9; font-size: 13px; line-height: 1.5; }
|
|
125
|
+
.wrap { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
|
|
126
|
+
h1 { font-size: 20px; font-weight: 600; color: #e6edf3; margin-bottom: 4px; }
|
|
127
|
+
.meta-line { color: #8b949e; font-size: 11px; margin-bottom: 24px; }
|
|
128
|
+
.summary { display: flex; gap: 12px; margin-bottom: 28px; flex-wrap: wrap; }
|
|
129
|
+
.stat { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 20px; min-width: 90px; }
|
|
130
|
+
.stat-val { font-size: 22px; font-weight: 700; color: #e6edf3; }
|
|
131
|
+
.stat-lbl { font-size: 11px; color: #8b949e; margin-top: 2px; }
|
|
132
|
+
.stat-pass .stat-val { color: #3fb950; }
|
|
133
|
+
.stat-fail .stat-val { color: #f85149; }
|
|
134
|
+
.stat-err .stat-val { color: #d29922; }
|
|
135
|
+
/* Cards */
|
|
136
|
+
.card { border: 1px solid #21262d; border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
|
|
137
|
+
.card-header { display: flex; align-items: baseline; gap: 8px; padding: 10px 14px; cursor: pointer; user-select: none; }
|
|
138
|
+
.card-header:hover { background: #161b22; }
|
|
139
|
+
.chevron { font-size: 10px; color: #8b949e; min-width: 10px; transition: transform .15s; }
|
|
140
|
+
.card-name { font-weight: 500; color: #e6edf3; white-space: nowrap; }
|
|
141
|
+
.card-url { font-family: monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
142
|
+
.card-body { padding: 12px 14px; border-top: 1px solid #21262d; display: flex; flex-direction: column; gap: 10px; }
|
|
143
|
+
/* Badges */
|
|
144
|
+
.badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
145
|
+
.badge-pass { background: #0d3a1e; color: #3fb950; }
|
|
146
|
+
.badge-fail { background: #3d1014; color: #f85149; }
|
|
147
|
+
.badge-err { background: #3d2a00; color: #d29922; }
|
|
148
|
+
.method { font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; white-space: nowrap; }
|
|
149
|
+
.method-badge { display: inline-block; font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; min-width: 52px; }
|
|
150
|
+
.http-badge { font-family: monospace; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
151
|
+
.http-ok { color: #3fb950; }
|
|
152
|
+
.http-redir { color: #79c0ff; }
|
|
153
|
+
.http-err { color: #f85149; }
|
|
154
|
+
.dur { font-family: monospace; font-size: 11px; white-space: nowrap; }
|
|
155
|
+
.muted { color: #8b949e; }
|
|
156
|
+
.mono { font-family: monospace; font-size: 12px; }
|
|
157
|
+
/* Request / Response */
|
|
158
|
+
.req-resp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
159
|
+
@media (max-width: 700px) { .req-resp-grid { grid-template-columns: 1fr; } }
|
|
160
|
+
.panel-label { font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
|
|
161
|
+
.panel { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
162
|
+
.req-line { font-family: monospace; font-size: 12px; color: #c9d1d9; word-break: break-all; }
|
|
163
|
+
.resp-status { font-family: monospace; font-size: 13px; font-weight: 700; }
|
|
164
|
+
.section-label { font-size: 10px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
|
|
165
|
+
.htable { width: 100%; border-collapse: collapse; }
|
|
166
|
+
.htable td { font-family: monospace; font-size: 11px; padding: 1px 0; vertical-align: top; }
|
|
167
|
+
.hk { color: #79c0ff; padding-right: 12px; white-space: nowrap; }
|
|
168
|
+
.hv { color: #c9d1d9; word-break: break-all; }
|
|
169
|
+
.code-block { font-family: monospace; font-size: 11px; color: #c9d1d9; background: #0d1117; border: 1px solid #21262d; border-radius: 4px; padding: 8px; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; }
|
|
170
|
+
/* Tests */
|
|
171
|
+
.section-label { font-size: 11px; font-weight: 600; color: #8b949e; }
|
|
172
|
+
.tests-wrap { display: flex; flex-direction: column; gap: 2px; }
|
|
173
|
+
.test-row { font-size: 12px; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
174
|
+
.test-pass { color: #3fb950; }
|
|
175
|
+
.test-fail { color: #f85149; }
|
|
176
|
+
.test-err { color: #8b949e; padding-left: 16px; width: 100%; font-family: monospace; font-size: 11px; }
|
|
177
|
+
.dot { font-weight: 700; }
|
|
178
|
+
/* Console */
|
|
179
|
+
.err-row { color: #f85149; font-size: 12px; }
|
|
180
|
+
</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<div class="wrap">
|
|
184
|
+
<h1>${esc(collection)}</h1>
|
|
185
|
+
<div class="meta-line">Environment: ${esc(env)} · ${esc(ts)}</div>
|
|
186
|
+
<div class="summary">
|
|
187
|
+
<div class="stat"><div class="stat-val">${summary.total}</div><div class="stat-lbl">Total</div></div>
|
|
188
|
+
<div class="stat stat-pass"><div class="stat-val">${summary.passed}</div><div class="stat-lbl">Passed</div></div>
|
|
189
|
+
<div class="stat stat-fail"><div class="stat-val">${summary.failed}</div><div class="stat-lbl">Failed</div></div>
|
|
190
|
+
<div class="stat stat-err"><div class="stat-val">${summary.errors}</div><div class="stat-lbl">Errors</div></div>
|
|
191
|
+
<div class="stat"><div class="stat-val">${passRate}%</div><div class="stat-lbl">Pass rate</div></div>
|
|
192
|
+
<div class="stat"><div class="stat-val">${summary.durationMs} ms</div><div class="stat-lbl">Duration</div></div>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="cards">${cards}</div>
|
|
195
|
+
</div>
|
|
196
|
+
<script>
|
|
197
|
+
function toggle(i) {
|
|
198
|
+
const body = document.getElementById('cb' + i)
|
|
199
|
+
const ch = document.getElementById('ch' + i)
|
|
200
|
+
const open = body.style.display !== 'none'
|
|
201
|
+
body.style.display = open ? 'none' : 'block'
|
|
202
|
+
ch.style.transform = open ? '' : 'rotate(90deg)'
|
|
203
|
+
}
|
|
204
|
+
<\/script>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
38
209
|
function buildJUnitReport(results, summary, meta = {}) {
|
|
39
210
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
40
211
|
const suiteName = esc(meta.collection ?? "API Tests");
|
|
@@ -123,9 +294,18 @@ function parseArgs(argv) {
|
|
|
123
294
|
}
|
|
124
295
|
return args;
|
|
125
296
|
}
|
|
297
|
+
async function resolveWorkspacePath(wsPath) {
|
|
298
|
+
const s = await promises.stat(wsPath);
|
|
299
|
+
if (!s.isDirectory()) return wsPath;
|
|
300
|
+
const entries = await promises.readdir(wsPath);
|
|
301
|
+
const spector = entries.find((e) => e.endsWith(".spector"));
|
|
302
|
+
if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
|
|
303
|
+
return path.join(wsPath, spector);
|
|
304
|
+
}
|
|
126
305
|
async function loadWorkspace(wsPath) {
|
|
127
|
-
const
|
|
128
|
-
|
|
306
|
+
const resolved = await resolveWorkspacePath(wsPath);
|
|
307
|
+
const raw = await promises.readFile(resolved, "utf8");
|
|
308
|
+
return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)) };
|
|
129
309
|
}
|
|
130
310
|
async function loadCollections(workspace, dir) {
|
|
131
311
|
const cols = [];
|
|
@@ -258,8 +438,24 @@ async function executeRequest(req, collectionVars, envVars, globals, verbose, tl
|
|
|
258
438
|
if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [post] ${l}`, C.gray)));
|
|
259
439
|
}
|
|
260
440
|
const allPassed = testResults.every((t) => t.passed);
|
|
261
|
-
const
|
|
262
|
-
|
|
441
|
+
const httpOk = fetchResp.status < 400;
|
|
442
|
+
const status = postScriptError ? "error" : testResults.length > 0 ? allPassed ? "passed" : "failed" : httpOk ? "passed" : "failed";
|
|
443
|
+
const reqHeaders = {};
|
|
444
|
+
headers.forEach((v, k) => {
|
|
445
|
+
reqHeaders[k] = v;
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
...base,
|
|
449
|
+
status,
|
|
450
|
+
httpStatus: fetchResp.status,
|
|
451
|
+
durationMs,
|
|
452
|
+
testResults,
|
|
453
|
+
consoleOutput,
|
|
454
|
+
preScriptError,
|
|
455
|
+
postScriptError,
|
|
456
|
+
sentRequest: { headers: reqHeaders, body },
|
|
457
|
+
receivedResponse: response
|
|
458
|
+
};
|
|
263
459
|
} catch (err) {
|
|
264
460
|
return {
|
|
265
461
|
...base,
|
|
@@ -289,7 +485,7 @@ async function main() {
|
|
|
289
485
|
const args = parseArgs(process.argv.slice(2));
|
|
290
486
|
if (args.help) {
|
|
291
487
|
console.log(
|
|
292
|
-
"\nUsage:\n api-spector run --workspace <path> [--
|
|
488
|
+
"\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit]\n [--verbose] [--bail]\n"
|
|
293
489
|
);
|
|
294
490
|
process.exit(0);
|
|
295
491
|
}
|
|
@@ -299,13 +495,14 @@ async function main() {
|
|
|
299
495
|
process.exit(1);
|
|
300
496
|
}
|
|
301
497
|
const filterTags = args.tags ? args.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
302
|
-
const envName = args.env;
|
|
498
|
+
const envName = args.environment ?? args.env;
|
|
303
499
|
const colName = args.collection;
|
|
304
500
|
const verbose = Boolean(args.verbose);
|
|
305
501
|
const bail = Boolean(args.bail);
|
|
306
502
|
const outputPath = args.output;
|
|
307
|
-
const inferredFormat = outputPath
|
|
308
|
-
const
|
|
503
|
+
const inferredFormat = outputPath ? path.extname(outputPath).toLowerCase() === ".xml" ? "junit" : path.extname(outputPath).toLowerCase() === ".html" ? "html" : "json" : "json";
|
|
504
|
+
const explicitFormat = args.format?.toLowerCase();
|
|
505
|
+
const outputFormat = explicitFormat === "junit" || explicitFormat === "html" ? explicitFormat : inferredFormat;
|
|
309
506
|
let workspace, wsDir;
|
|
310
507
|
try {
|
|
311
508
|
;
|
|
@@ -327,6 +524,26 @@ async function main() {
|
|
|
327
524
|
console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
|
|
328
525
|
if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
|
|
329
526
|
console.log("");
|
|
527
|
+
const envVarsSnapshot = await requestHandler.buildEnvVars(env);
|
|
528
|
+
const secretValuesToMask = (env?.variables ?? []).filter((v) => v.secret && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
|
|
529
|
+
function redact(s) {
|
|
530
|
+
let out = s;
|
|
531
|
+
for (const secret of secretValuesToMask) out = out.split(secret).join("***");
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
function maskResult(r) {
|
|
535
|
+
return {
|
|
536
|
+
...r,
|
|
537
|
+
sentRequest: r.sentRequest ? {
|
|
538
|
+
headers: Object.fromEntries(Object.entries(r.sentRequest.headers).map(([k, v]) => [k, redact(v)])),
|
|
539
|
+
body: r.sentRequest.body != null ? redact(r.sentRequest.body) : void 0
|
|
540
|
+
} : void 0,
|
|
541
|
+
receivedResponse: r.receivedResponse ? {
|
|
542
|
+
...r.receivedResponse,
|
|
543
|
+
body: redact(r.receivedResponse.body)
|
|
544
|
+
} : void 0
|
|
545
|
+
};
|
|
546
|
+
}
|
|
330
547
|
const summary = { total: 0, passed: 0, failed: 0, errors: 0, durationMs: 0 };
|
|
331
548
|
const allResults = [];
|
|
332
549
|
const totalStart = Date.now();
|
|
@@ -369,7 +586,8 @@ async function main() {
|
|
|
369
586
|
`);
|
|
370
587
|
if (outputPath) {
|
|
371
588
|
const meta = { workspace: wsPath, environment: env?.name ?? null, collection: firstColName, timestamp };
|
|
372
|
-
const
|
|
589
|
+
const maskedResults = allResults.map(maskResult);
|
|
590
|
+
const report = outputFormat === "junit" ? buildJUnitReport(maskedResults, summary, meta) : outputFormat === "html" ? buildHtmlReport(maskedResults, summary, meta) : buildJsonReport(maskedResults, summary, meta);
|
|
373
591
|
await promises.writeFile(path.resolve(outputPath), report, "utf8");
|
|
374
592
|
console.log(color(` Report written: ${outputPath} (${outputFormat})
|
|
375
593
|
`, C.gray));
|
package/out/preload/index.js
CHANGED
|
@@ -81,6 +81,26 @@ const api = {
|
|
|
81
81
|
inferContractSchema: (jsonBody) => electron.ipcRenderer.invoke("contract:inferSchema", jsonBody),
|
|
82
82
|
// ─── Script hooks ─────────────────────────────────────────────────────────
|
|
83
83
|
runScriptHook: (payload) => electron.ipcRenderer.invoke("script:run-hook", payload),
|
|
84
|
+
// ─── Git ──────────────────────────────────────────────────────────────────
|
|
85
|
+
gitIsRepo: () => electron.ipcRenderer.invoke("git:isRepo"),
|
|
86
|
+
gitInit: () => electron.ipcRenderer.invoke("git:init"),
|
|
87
|
+
gitStatus: () => electron.ipcRenderer.invoke("git:status"),
|
|
88
|
+
gitDiff: (filePath) => electron.ipcRenderer.invoke("git:diff", filePath),
|
|
89
|
+
gitDiffStaged: (filePath) => electron.ipcRenderer.invoke("git:diffStaged", filePath),
|
|
90
|
+
gitStage: (paths) => electron.ipcRenderer.invoke("git:stage", paths),
|
|
91
|
+
gitUnstage: (paths) => electron.ipcRenderer.invoke("git:unstage", paths),
|
|
92
|
+
gitStageAll: () => electron.ipcRenderer.invoke("git:stageAll"),
|
|
93
|
+
gitCommit: (message) => electron.ipcRenderer.invoke("git:commit", message),
|
|
94
|
+
gitLog: (limit) => electron.ipcRenderer.invoke("git:log", limit),
|
|
95
|
+
gitBranches: () => electron.ipcRenderer.invoke("git:branches"),
|
|
96
|
+
gitCheckout: (branch, create) => electron.ipcRenderer.invoke("git:checkout", branch, create),
|
|
97
|
+
gitPull: () => electron.ipcRenderer.invoke("git:pull"),
|
|
98
|
+
gitPush: (setUpstream) => electron.ipcRenderer.invoke("git:push", setUpstream),
|
|
99
|
+
gitRemotes: () => electron.ipcRenderer.invoke("git:remotes"),
|
|
100
|
+
gitAddRemote: (name, url) => electron.ipcRenderer.invoke("git:addRemote", name, url),
|
|
101
|
+
gitSetRemoteUrl: (name, url) => electron.ipcRenderer.invoke("git:setRemoteUrl", name, url),
|
|
102
|
+
gitRemoveRemote: (name) => electron.ipcRenderer.invoke("git:removeRemote", name),
|
|
103
|
+
gitWriteCiFile: (relPath, content) => electron.ipcRenderer.invoke("git:writeCiFile", relPath, content),
|
|
84
104
|
// ─── Zoom ─────────────────────────────────────────────────────────────────
|
|
85
105
|
setZoomFactor: (factor) => electron.webFrame.setZoomFactor(factor),
|
|
86
106
|
// ─── Platform ─────────────────────────────────────────────────────────────
|