@testsmith/api-spector 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +38 -37
- package/out/main/chunks/import-C9qdkBCH.js +154 -0
- package/out/main/chunks/ipc-validate-CscN4HfG.js +94 -0
- package/out/main/chunks/{mock-server-Cx-xG4pJ.js → mock-server-DmdvwCgj.js} +4 -0
- package/out/main/chunks/{request-collection-DMVlm0PA.js → request-collection-DIsjTggj.js} +2 -0
- package/out/main/chunks/soap-handler-Cpj-JwyA.js +326 -0
- package/out/main/index.js +321 -86
- package/out/main/mock.js +1 -1
- package/out/main/runner.js +4 -3
- package/out/main/wsdl.js +174 -0
- package/out/preload/index.js +8 -0
- package/out/renderer/assets/{index-DdHHEKaz.js → index-BC1srylp.js} +2096 -1523
- package/out/renderer/assets/index-DfkLUeA1.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +6 -1
- package/out/renderer/assets/index-CxdQOFBM.css +0 -2
package/out/main/index.js
CHANGED
|
@@ -25,19 +25,20 @@ 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 requestCollection = require("./chunks/request-collection-
|
|
28
|
+
const requestCollection = require("./chunks/request-collection-DIsjTggj.js");
|
|
29
29
|
const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
|
|
30
30
|
const uuid = require("uuid");
|
|
31
31
|
const jsYaml = require("js-yaml");
|
|
32
32
|
const undici = require("undici");
|
|
33
33
|
const JSZip = require("jszip");
|
|
34
|
-
const mockServer = require("./chunks/mock-server-
|
|
34
|
+
const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
|
|
35
35
|
const http = require("http");
|
|
36
36
|
const WebSocket = require("ws");
|
|
37
|
-
const
|
|
37
|
+
const soapHandler = require("./chunks/soap-handler-Cpj-JwyA.js");
|
|
38
38
|
const os = require("os");
|
|
39
39
|
const crypto = require("crypto");
|
|
40
40
|
const snapshots = require("./chunks/snapshots-C7YbGHM7.js");
|
|
41
|
+
const ipcValidate = require("./chunks/ipc-validate-CscN4HfG.js");
|
|
41
42
|
const simpleGit = require("simple-git");
|
|
42
43
|
const recorder = require("./chunks/recorder-DFxJgn9c.js");
|
|
43
44
|
require("vm");
|
|
@@ -46,6 +47,7 @@ require("tv4");
|
|
|
46
47
|
require("jsonpath-plus");
|
|
47
48
|
require("@xmldom/xmldom");
|
|
48
49
|
require("ajv");
|
|
50
|
+
require("https");
|
|
49
51
|
const LAST_WS_FILE = path.join(electron.app.getPath("userData"), "last-workspace.json");
|
|
50
52
|
async function saveLastWorkspacePath(wsPath) {
|
|
51
53
|
await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: wsPath }), "utf8");
|
|
@@ -63,10 +65,156 @@ let workspaceFile = null;
|
|
|
63
65
|
function atomicWrite(path2, data) {
|
|
64
66
|
return promises.writeFile(path2, data, "utf8");
|
|
65
67
|
}
|
|
68
|
+
const SPECTOR_GITIGNORE = [
|
|
69
|
+
"# API Spector — never commit secrets",
|
|
70
|
+
"*.secrets",
|
|
71
|
+
".env",
|
|
72
|
+
".env.local",
|
|
73
|
+
".env.*.local",
|
|
74
|
+
"",
|
|
75
|
+
"# Dependencies",
|
|
76
|
+
"node_modules/",
|
|
77
|
+
"",
|
|
78
|
+
"# Generated API docs",
|
|
79
|
+
"api-docs.html",
|
|
80
|
+
"api-docs.md",
|
|
81
|
+
"docs/",
|
|
82
|
+
"",
|
|
83
|
+
"# Run reports (api-spector run --output)",
|
|
84
|
+
"reports/",
|
|
85
|
+
"*-report.json",
|
|
86
|
+
"*-report.xml",
|
|
87
|
+
"*-report.html",
|
|
88
|
+
"results.json",
|
|
89
|
+
"results.xml",
|
|
90
|
+
"results.html",
|
|
91
|
+
"coverage/",
|
|
92
|
+
"",
|
|
93
|
+
"# OS / editor",
|
|
94
|
+
".DS_Store",
|
|
95
|
+
"Thumbs.db",
|
|
96
|
+
".idea/",
|
|
97
|
+
""
|
|
98
|
+
].join("\n");
|
|
99
|
+
function readmeContents(workspaceFileName) {
|
|
100
|
+
return [
|
|
101
|
+
`# API tests`,
|
|
102
|
+
``,
|
|
103
|
+
`This folder is an [API Spector](https://github.com/testsmith-io/api-spector) workspace.`,
|
|
104
|
+
`Everything here is plain JSON — diff it, commit it, review it like any other code.`,
|
|
105
|
+
``,
|
|
106
|
+
`## Layout`,
|
|
107
|
+
``,
|
|
108
|
+
"```",
|
|
109
|
+
`${workspaceFileName} ← workspace manifest (this is what you "open")`,
|
|
110
|
+
`collections/ ← your request collections`,
|
|
111
|
+
`environments/ ← per-env variable files (dev, staging, prod, …)`,
|
|
112
|
+
`mocks/ ← saved mock servers (optional)`,
|
|
113
|
+
`contracts/ ← pinned OpenAPI snapshots (optional)`,
|
|
114
|
+
`.gitignore ← excludes secrets, generated docs, run reports`,
|
|
115
|
+
`.vscode/settings.json ← maps *.spector to JSON for editor highlighting`,
|
|
116
|
+
"```",
|
|
117
|
+
``,
|
|
118
|
+
`## Open the workspace`,
|
|
119
|
+
``,
|
|
120
|
+
"```bash",
|
|
121
|
+
`# launches the GUI in this folder`,
|
|
122
|
+
`npx -y @testsmith/api-spector ui`,
|
|
123
|
+
"```",
|
|
124
|
+
``,
|
|
125
|
+
`## Run the tests from CI`,
|
|
126
|
+
``,
|
|
127
|
+
"```bash",
|
|
128
|
+
`npx -y @testsmith/api-spector run \\`,
|
|
129
|
+
` --workspace ./${workspaceFileName} \\`,
|
|
130
|
+
` --environment ci \\`,
|
|
131
|
+
` --output reports/results.xml --format junit`,
|
|
132
|
+
"```",
|
|
133
|
+
``,
|
|
134
|
+
`Other useful commands:`,
|
|
135
|
+
``,
|
|
136
|
+
`| Command | What it does |`,
|
|
137
|
+
`|---|---|`,
|
|
138
|
+
`| \`api-spector run\` | Execute requests / assertions |`,
|
|
139
|
+
`| \`api-spector mock\` | Start mock servers from this workspace |`,
|
|
140
|
+
`| \`api-spector contract\` | Manage and run pinned contract snapshots |`,
|
|
141
|
+
`| \`api-spector wsdl\` | Inspect a WSDL or import as collection / mock |`,
|
|
142
|
+
``,
|
|
143
|
+
`## A note on secrets`,
|
|
144
|
+
``,
|
|
145
|
+
`Secret values (passwords, OAuth client secrets, API keys) are stored in your`,
|
|
146
|
+
`OS keychain — **not** in this folder. Environment files only reference the`,
|
|
147
|
+
`keychain entry by name, so it's safe to commit them.`,
|
|
148
|
+
``
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
async function ensureReadme(workspaceDir2, workspaceFileName) {
|
|
152
|
+
const path$1 = path.join(workspaceDir2, "README.md");
|
|
153
|
+
try {
|
|
154
|
+
await promises.readFile(path$1, "utf8");
|
|
155
|
+
return;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err.code !== "ENOENT") {
|
|
158
|
+
console.warn("file-handler: could not check README.md", err);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await atomicWrite(path$1, readmeContents(workspaceFileName));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function ensureGitignore(workspaceDir2) {
|
|
165
|
+
const path$1 = path.join(workspaceDir2, ".gitignore");
|
|
166
|
+
try {
|
|
167
|
+
await promises.readFile(path$1, "utf8");
|
|
168
|
+
return;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err.code !== "ENOENT") {
|
|
171
|
+
console.warn("file-handler: could not check .gitignore", err);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await atomicWrite(path$1, SPECTOR_GITIGNORE);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function ensureVscodeFileAssociation(workspaceDir2) {
|
|
178
|
+
const dir = path.join(workspaceDir2, ".vscode");
|
|
179
|
+
const file = path.join(dir, "settings.json");
|
|
180
|
+
try {
|
|
181
|
+
const raw = await promises.readFile(file, "utf8");
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(raw);
|
|
185
|
+
} catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const associations = parsed["files.associations"] ?? {};
|
|
189
|
+
if (associations["*.spector"] === "json") return;
|
|
190
|
+
parsed["files.associations"] = { ...associations, "*.spector": "json" };
|
|
191
|
+
await atomicWrite(file, JSON.stringify(parsed, null, 2) + "\n");
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err.code !== "ENOENT") {
|
|
194
|
+
console.warn("file-handler: could not read existing .vscode/settings.json", err);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await promises.mkdir(dir, { recursive: true });
|
|
198
|
+
const fresh = { "files.associations": { "*.spector": "json" } };
|
|
199
|
+
await atomicWrite(file, JSON.stringify(fresh, null, 2) + "\n");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function dialogStartDir() {
|
|
203
|
+
return process.env.API_SPECTOR_LAUNCH_CWD || workspaceDir || void 0;
|
|
204
|
+
}
|
|
205
|
+
function defaultWorkspaceName() {
|
|
206
|
+
const cwd = process.env.API_SPECTOR_LAUNCH_CWD;
|
|
207
|
+
if (cwd) {
|
|
208
|
+
const base = cwd.split(/[\\/]/).filter(Boolean).pop();
|
|
209
|
+
if (base && /^[a-zA-Z0-9._-]+$/.test(base)) return `${base}.spector`;
|
|
210
|
+
}
|
|
211
|
+
return "my-workspace.spector";
|
|
212
|
+
}
|
|
66
213
|
function registerFileHandlers(ipc) {
|
|
67
214
|
ipc.handle("file:openWorkspace", async () => {
|
|
68
215
|
const result = await electron.dialog.showOpenDialog({
|
|
69
216
|
title: "Open Workspace",
|
|
217
|
+
defaultPath: dialogStartDir(),
|
|
70
218
|
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }],
|
|
71
219
|
properties: ["openFile"]
|
|
72
220
|
});
|
|
@@ -80,9 +228,11 @@ function registerFileHandlers(ipc) {
|
|
|
80
228
|
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
81
229
|
});
|
|
82
230
|
ipc.handle("file:newWorkspace", async () => {
|
|
231
|
+
const startDir = dialogStartDir();
|
|
232
|
+
const defaultPath = startDir ? path.join(startDir, defaultWorkspaceName()) : defaultWorkspaceName();
|
|
83
233
|
const result = await electron.dialog.showSaveDialog({
|
|
84
234
|
title: "Create Workspace",
|
|
85
|
-
defaultPath
|
|
235
|
+
defaultPath,
|
|
86
236
|
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }]
|
|
87
237
|
});
|
|
88
238
|
if (result.canceled || !result.filePath) return null;
|
|
@@ -91,8 +241,9 @@ function registerFileHandlers(ipc) {
|
|
|
91
241
|
await requestCollection.loadGlobals(workspaceDir);
|
|
92
242
|
await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
|
|
93
243
|
await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
|
|
94
|
-
|
|
95
|
-
await
|
|
244
|
+
await ensureGitignore(workspaceDir);
|
|
245
|
+
await ensureVscodeFileAssociation(workspaceDir);
|
|
246
|
+
await ensureReadme(workspaceDir, path.basename(result.filePath));
|
|
96
247
|
const ws = {
|
|
97
248
|
version: "1.0",
|
|
98
249
|
collections: [],
|
|
@@ -106,6 +257,7 @@ function registerFileHandlers(ipc) {
|
|
|
106
257
|
ipc.handle("file:saveWorkspace", async (_e, ws) => {
|
|
107
258
|
if (!workspaceFile) return;
|
|
108
259
|
await atomicWrite(workspaceFile, JSON.stringify(ws, null, 2));
|
|
260
|
+
if (workspaceDir) await ensureVscodeFileAssociation(workspaceDir);
|
|
109
261
|
});
|
|
110
262
|
ipc.handle("file:loadCollection", async (_e, relPath) => {
|
|
111
263
|
if (!workspaceDir) throw new Error("No workspace open");
|
|
@@ -129,6 +281,18 @@ function registerFileHandlers(ipc) {
|
|
|
129
281
|
await promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
130
282
|
await atomicWrite(fullPath, JSON.stringify(env, null, 2));
|
|
131
283
|
});
|
|
284
|
+
ipc.handle("file:deleteWorkspaceFile", async (_e, relPath) => {
|
|
285
|
+
if (!workspaceDir) throw new Error("No workspace open");
|
|
286
|
+
const fullPath = path.resolve(workspaceDir, relPath);
|
|
287
|
+
if (!fullPath.startsWith(path.resolve(workspaceDir) + (process.platform === "win32" ? "\\" : "/"))) {
|
|
288
|
+
throw new Error(`Refusing to delete file outside the workspace: ${relPath}`);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
await promises.unlink(fullPath);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
if (err.code !== "ENOENT") throw err;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
132
296
|
ipc.handle("dialog:pickDir", async () => {
|
|
133
297
|
const result = await electron.dialog.showOpenDialog({
|
|
134
298
|
title: "Select Output Directory",
|
|
@@ -160,23 +324,51 @@ function registerFileHandlers(ipc) {
|
|
|
160
324
|
ipc.handle("file:closeWorkspace", async () => {
|
|
161
325
|
workspaceDir = null;
|
|
162
326
|
workspaceFile = null;
|
|
163
|
-
await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch(() => {
|
|
327
|
+
await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch((err) => {
|
|
328
|
+
console.warn("file-handler: could not clear last-workspace pointer", err);
|
|
164
329
|
});
|
|
165
330
|
});
|
|
166
331
|
ipc.handle("file:getLastWorkspace", async () => {
|
|
332
|
+
const cwd = process.env.API_SPECTOR_LAUNCH_CWD;
|
|
333
|
+
if (cwd) {
|
|
334
|
+
const fromCwd = await tryOpenWorkspaceInDir(cwd);
|
|
335
|
+
return fromCwd;
|
|
336
|
+
}
|
|
167
337
|
const wsPath = await readLastWorkspacePath();
|
|
168
338
|
if (!wsPath) return null;
|
|
169
339
|
try {
|
|
340
|
+
const raw = await promises.readFile(wsPath, "utf8");
|
|
341
|
+
const workspace = JSON.parse(raw);
|
|
170
342
|
workspaceDir = path.dirname(wsPath);
|
|
171
343
|
workspaceFile = wsPath;
|
|
172
344
|
await requestCollection.loadGlobals(workspaceDir);
|
|
173
|
-
|
|
174
|
-
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
345
|
+
return { workspace, workspacePath: wsPath };
|
|
175
346
|
} catch {
|
|
176
347
|
return null;
|
|
177
348
|
}
|
|
178
349
|
});
|
|
179
350
|
}
|
|
351
|
+
async function tryOpenWorkspaceInDir(dir) {
|
|
352
|
+
let entries;
|
|
353
|
+
try {
|
|
354
|
+
entries = await promises.readdir(dir);
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const candidates = entries.filter((f) => f.endsWith(".spector"));
|
|
359
|
+
if (candidates.length !== 1) return null;
|
|
360
|
+
const wsPath = path.join(dir, candidates[0]);
|
|
361
|
+
try {
|
|
362
|
+
const raw = await promises.readFile(wsPath, "utf8");
|
|
363
|
+
const workspace = JSON.parse(raw);
|
|
364
|
+
workspaceDir = dir;
|
|
365
|
+
workspaceFile = wsPath;
|
|
366
|
+
await requestCollection.loadGlobals(workspaceDir);
|
|
367
|
+
return { workspace, workspacePath: wsPath };
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
180
372
|
function getWorkspaceDir() {
|
|
181
373
|
return workspaceDir;
|
|
182
374
|
}
|
|
@@ -3567,70 +3759,31 @@ function registerWsHandlers(ipc) {
|
|
|
3567
3759
|
}
|
|
3568
3760
|
});
|
|
3569
3761
|
}
|
|
3570
|
-
function
|
|
3571
|
-
return
|
|
3572
|
-
const lib = url.startsWith("https") ? https : http;
|
|
3573
|
-
const req = lib.get(url, { headers }, (res) => {
|
|
3574
|
-
const chunks = [];
|
|
3575
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
3576
|
-
res.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
3577
|
-
res.on("error", reject);
|
|
3578
|
-
});
|
|
3579
|
-
req.on("error", reject);
|
|
3580
|
-
req.setTimeout(15e3, () => {
|
|
3581
|
-
req.destroy();
|
|
3582
|
-
reject(new Error("WSDL fetch timed out"));
|
|
3583
|
-
});
|
|
3584
|
-
});
|
|
3762
|
+
function isJsonContentType(ct) {
|
|
3763
|
+
return !!ct && /\bjson\b/i.test(ct);
|
|
3585
3764
|
}
|
|
3586
|
-
function
|
|
3587
|
-
|
|
3588
|
-
const
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
const operations = [];
|
|
3604
|
-
for (const name of operationNames) {
|
|
3605
|
-
const soapAction = soapActionMap[name];
|
|
3606
|
-
const inputTemplate = buildEnvelopeTemplate(name, targetNamespace);
|
|
3607
|
-
operations.push({ name, soapAction, inputTemplate });
|
|
3608
|
-
}
|
|
3609
|
-
return { operations, targetNamespace };
|
|
3610
|
-
}
|
|
3611
|
-
function buildEnvelopeTemplate(operationName, namespace) {
|
|
3612
|
-
return `<?xml version="1.0" encoding="utf-8"?>
|
|
3613
|
-
<soap:Envelope
|
|
3614
|
-
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
|
3615
|
-
xmlns:tns="${namespace}">
|
|
3616
|
-
<soap:Header/>
|
|
3617
|
-
<soap:Body>
|
|
3618
|
-
<tns:${operationName}>
|
|
3619
|
-
<!-- Add parameters here -->
|
|
3620
|
-
</tns:${operationName}>
|
|
3621
|
-
</soap:Body>
|
|
3622
|
-
</soap:Envelope>`;
|
|
3623
|
-
}
|
|
3624
|
-
function registerSoapHandlers(ipc) {
|
|
3625
|
-
ipc.handle("wsdl:fetch", async (_event, url, extraHeaders = {}) => {
|
|
3626
|
-
const wsdlText = await fetchUrl(url, extraHeaders);
|
|
3627
|
-
return parseWsdl(wsdlText);
|
|
3628
|
-
});
|
|
3765
|
+
function formatBody(body, contentType) {
|
|
3766
|
+
if (!body) return "";
|
|
3767
|
+
const trimmed = body.length > 8e3 ? body.slice(0, 8e3) + "\n… (truncated)" : body;
|
|
3768
|
+
if (isJsonContentType(contentType)) {
|
|
3769
|
+
try {
|
|
3770
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
3771
|
+
} catch {
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return trimmed;
|
|
3775
|
+
}
|
|
3776
|
+
function langForContentType(ct) {
|
|
3777
|
+
if (!ct) return "";
|
|
3778
|
+
if (/\bjson\b/i.test(ct)) return "json";
|
|
3779
|
+
if (/\bxml\b/i.test(ct)) return "xml";
|
|
3780
|
+
if (/\bhtml\b/i.test(ct)) return "html";
|
|
3781
|
+
return "";
|
|
3629
3782
|
}
|
|
3630
3783
|
function escMd(s) {
|
|
3631
3784
|
return s.replace(/[|\\`*_{}[\]()#+\-.!]/g, (c) => `\\${c}`);
|
|
3632
3785
|
}
|
|
3633
|
-
function requestToMarkdown(req) {
|
|
3786
|
+
function requestToMarkdown(req, example) {
|
|
3634
3787
|
const lines = [];
|
|
3635
3788
|
const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
|
|
3636
3789
|
lines.push(`#### ${methodLabel} ${escMd(req.name)}`);
|
|
@@ -3697,9 +3850,31 @@ function requestToMarkdown(req) {
|
|
|
3697
3850
|
lines.push("```");
|
|
3698
3851
|
lines.push("");
|
|
3699
3852
|
}
|
|
3853
|
+
if (example?.sent?.body?.trim()) {
|
|
3854
|
+
const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
|
|
3855
|
+
const lang = langForContentType(ct);
|
|
3856
|
+
lines.push("**Example Request Body**");
|
|
3857
|
+
lines.push("```" + lang);
|
|
3858
|
+
lines.push(formatBody(example.sent.body, ct));
|
|
3859
|
+
lines.push("```");
|
|
3860
|
+
lines.push("");
|
|
3861
|
+
}
|
|
3862
|
+
if (example?.response) {
|
|
3863
|
+
const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
|
|
3864
|
+
const lang = langForContentType(ct);
|
|
3865
|
+
lines.push(`**Example Response** (${example.response.status})`);
|
|
3866
|
+
if (example.response.body?.trim()) {
|
|
3867
|
+
lines.push("```" + lang);
|
|
3868
|
+
lines.push(formatBody(example.response.body, ct));
|
|
3869
|
+
lines.push("```");
|
|
3870
|
+
} else {
|
|
3871
|
+
lines.push("_(empty body)_");
|
|
3872
|
+
}
|
|
3873
|
+
lines.push("");
|
|
3874
|
+
}
|
|
3700
3875
|
return lines.join("\n");
|
|
3701
3876
|
}
|
|
3702
|
-
function folderToMarkdown(folder, requests, depth) {
|
|
3877
|
+
function folderToMarkdown(folder, requests, depth, examples) {
|
|
3703
3878
|
const lines = [];
|
|
3704
3879
|
const heading = "#".repeat(depth);
|
|
3705
3880
|
if (folder.name !== "root") {
|
|
@@ -3713,11 +3888,11 @@ function folderToMarkdown(folder, requests, depth) {
|
|
|
3713
3888
|
for (const reqId of folder.requestIds) {
|
|
3714
3889
|
const req = requests[reqId];
|
|
3715
3890
|
if (req) {
|
|
3716
|
-
lines.push(requestToMarkdown(req));
|
|
3891
|
+
lines.push(requestToMarkdown(req, examples[reqId]));
|
|
3717
3892
|
}
|
|
3718
3893
|
}
|
|
3719
3894
|
for (const sub of folder.folders) {
|
|
3720
|
-
lines.push(folderToMarkdown(sub, requests, depth + 1));
|
|
3895
|
+
lines.push(folderToMarkdown(sub, requests, depth + 1, examples));
|
|
3721
3896
|
}
|
|
3722
3897
|
return lines.join("\n");
|
|
3723
3898
|
}
|
|
@@ -3725,6 +3900,7 @@ function generateMarkdown(payload) {
|
|
|
3725
3900
|
const lines = [];
|
|
3726
3901
|
lines.push("# API Documentation");
|
|
3727
3902
|
lines.push("");
|
|
3903
|
+
const examples = payload.examples ?? {};
|
|
3728
3904
|
for (const { collection, requests } of payload.collections) {
|
|
3729
3905
|
lines.push(`## ${escMd(collection.name)}`);
|
|
3730
3906
|
lines.push("");
|
|
@@ -3732,7 +3908,7 @@ function generateMarkdown(payload) {
|
|
|
3732
3908
|
lines.push(collection.description.trim());
|
|
3733
3909
|
lines.push("");
|
|
3734
3910
|
}
|
|
3735
|
-
lines.push(folderToMarkdown(collection.rootFolder, requests, 3));
|
|
3911
|
+
lines.push(folderToMarkdown(collection.rootFolder, requests, 3, examples));
|
|
3736
3912
|
}
|
|
3737
3913
|
return lines.join("\n");
|
|
3738
3914
|
}
|
|
@@ -3749,7 +3925,7 @@ const METHOD_COLORS = {
|
|
|
3749
3925
|
OPTIONS: "#9ca3af",
|
|
3750
3926
|
WS: "#22d3ee"
|
|
3751
3927
|
};
|
|
3752
|
-
function requestToHtml(req) {
|
|
3928
|
+
function requestToHtml(req, example) {
|
|
3753
3929
|
const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
|
|
3754
3930
|
const color = METHOD_COLORS[methodLabel] ?? "#9ca3af";
|
|
3755
3931
|
let html = `<div class="request">`;
|
|
@@ -3787,10 +3963,23 @@ function requestToHtml(req) {
|
|
|
3787
3963
|
} else if (mode === "soap" && req.body.soap?.envelope?.trim()) {
|
|
3788
3964
|
html += `<div class="label">Body (SOAP)</div><pre><code>${escHtml(req.body.soap.envelope.trim())}</code></pre>`;
|
|
3789
3965
|
}
|
|
3966
|
+
if (example?.sent?.body?.trim()) {
|
|
3967
|
+
const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
|
|
3968
|
+
html += `<div class="label">Example Request Body</div><pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.sent.body, ct))}</code></pre>`;
|
|
3969
|
+
}
|
|
3970
|
+
if (example?.response) {
|
|
3971
|
+
const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
|
|
3972
|
+
html += `<div class="label">Example Response (${example.response.status})</div>`;
|
|
3973
|
+
if (example.response.body?.trim()) {
|
|
3974
|
+
html += `<pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.response.body, ct))}</code></pre>`;
|
|
3975
|
+
} else {
|
|
3976
|
+
html += `<p class="desc"><em>(empty body)</em></p>`;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3790
3979
|
html += `</div>`;
|
|
3791
3980
|
return html;
|
|
3792
3981
|
}
|
|
3793
|
-
function folderToHtml(folder, requests, depth) {
|
|
3982
|
+
function folderToHtml(folder, requests, depth, examples) {
|
|
3794
3983
|
let html = "";
|
|
3795
3984
|
const tag = `h${Math.min(depth, 6)}`;
|
|
3796
3985
|
if (folder.name !== "root") {
|
|
@@ -3801,21 +3990,22 @@ function folderToHtml(folder, requests, depth) {
|
|
|
3801
3990
|
}
|
|
3802
3991
|
for (const reqId of folder.requestIds) {
|
|
3803
3992
|
const req = requests[reqId];
|
|
3804
|
-
if (req) html += requestToHtml(req);
|
|
3993
|
+
if (req) html += requestToHtml(req, examples[reqId]);
|
|
3805
3994
|
}
|
|
3806
3995
|
for (const sub of folder.folders) {
|
|
3807
|
-
html += folderToHtml(sub, requests, depth + 1);
|
|
3996
|
+
html += folderToHtml(sub, requests, depth + 1, examples);
|
|
3808
3997
|
}
|
|
3809
3998
|
return html;
|
|
3810
3999
|
}
|
|
3811
4000
|
function generateHtml(payload) {
|
|
3812
4001
|
let body = "";
|
|
4002
|
+
const examples = payload.examples ?? {};
|
|
3813
4003
|
for (const { collection, requests } of payload.collections) {
|
|
3814
4004
|
body += `<section class="collection"><h2>${escHtml(collection.name)}</h2>`;
|
|
3815
4005
|
if (collection.description?.trim()) {
|
|
3816
4006
|
body += `<p class="collection-desc">${escHtml(collection.description.trim())}</p>`;
|
|
3817
4007
|
}
|
|
3818
|
-
body += folderToHtml(collection.rootFolder, requests, 3);
|
|
4008
|
+
body += folderToHtml(collection.rootFolder, requests, 3, examples);
|
|
3819
4009
|
body += `</section>`;
|
|
3820
4010
|
}
|
|
3821
4011
|
return `<!DOCTYPE html>
|
|
@@ -3901,6 +4091,7 @@ async function resolveSnapshotSpec(relPath) {
|
|
|
3901
4091
|
}
|
|
3902
4092
|
function registerContractHandlers(ipc) {
|
|
3903
4093
|
ipc.handle("contract:run", async (_e, payload) => {
|
|
4094
|
+
ipcValidate.validateContractRunPayload(payload);
|
|
3904
4095
|
const { mode, requests, envVars, collectionVars = {}, requestBaseUrl } = payload;
|
|
3905
4096
|
let { specUrl, specPath } = payload;
|
|
3906
4097
|
if (payload.specSnapshotRelPath) {
|
|
@@ -3961,6 +4152,8 @@ function registerGitHandlers(ipc) {
|
|
|
3961
4152
|
});
|
|
3962
4153
|
ipc.handle("git:init", async () => {
|
|
3963
4154
|
await git().init();
|
|
4155
|
+
const dir = getWorkspaceDir();
|
|
4156
|
+
if (dir) await ensureGitignore(dir);
|
|
3964
4157
|
});
|
|
3965
4158
|
ipc.handle("git:status", async () => {
|
|
3966
4159
|
const result = await git().status();
|
|
@@ -4020,16 +4213,58 @@ function registerGitHandlers(ipc) {
|
|
|
4020
4213
|
}));
|
|
4021
4214
|
});
|
|
4022
4215
|
ipc.handle("git:branches", async () => {
|
|
4023
|
-
const
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4216
|
+
const raw = await git().raw([
|
|
4217
|
+
"for-each-ref",
|
|
4218
|
+
"--format=%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)",
|
|
4219
|
+
"refs/heads",
|
|
4220
|
+
"refs/remotes"
|
|
4221
|
+
]);
|
|
4222
|
+
const current = (await git().branch()).current;
|
|
4223
|
+
const branches = [];
|
|
4224
|
+
for (const line of raw.split("\n")) {
|
|
4225
|
+
if (!line.trim()) continue;
|
|
4226
|
+
const [shortName, head, upstream, track] = line.split("|");
|
|
4227
|
+
if (!shortName || shortName.endsWith("/HEAD")) continue;
|
|
4228
|
+
const isRemote = shortName.startsWith("origin/") || shortName.includes("/");
|
|
4229
|
+
const looksLocal = !isRemote;
|
|
4230
|
+
let ahead;
|
|
4231
|
+
let behind;
|
|
4232
|
+
const aheadM = track?.match(/ahead (\d+)/);
|
|
4233
|
+
const behindM = track?.match(/behind (\d+)/);
|
|
4234
|
+
if (aheadM) ahead = Number(aheadM[1]);
|
|
4235
|
+
if (behindM) behind = Number(behindM[1]);
|
|
4236
|
+
branches.push({
|
|
4237
|
+
name: shortName,
|
|
4238
|
+
current: looksLocal && (head === "*" || shortName === current),
|
|
4239
|
+
remote: isRemote,
|
|
4240
|
+
upstream: upstream || void 0,
|
|
4241
|
+
ahead,
|
|
4242
|
+
behind
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
return branches;
|
|
4029
4246
|
});
|
|
4030
4247
|
ipc.handle("git:checkout", async (_e, branch, create) => {
|
|
4031
|
-
if (create)
|
|
4032
|
-
|
|
4248
|
+
if (create) {
|
|
4249
|
+
await git().checkoutLocalBranch(branch);
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
const m = /^([^/]+)\/(.+)$/.exec(branch);
|
|
4253
|
+
if (m) {
|
|
4254
|
+
const remote = m[1];
|
|
4255
|
+
const localName = m[2];
|
|
4256
|
+
const localList = await git().branchLocal();
|
|
4257
|
+
if (!localList.all.includes(localName)) {
|
|
4258
|
+
await git().checkoutBranch(localName, `${remote}/${localName}`);
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
await git().checkout(localName);
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
await git().checkout(branch);
|
|
4265
|
+
});
|
|
4266
|
+
ipc.handle("git:deleteBranch", async (_e, name, force = false) => {
|
|
4267
|
+
await git().deleteLocalBranch(name, force);
|
|
4033
4268
|
});
|
|
4034
4269
|
ipc.handle("git:pull", async () => {
|
|
4035
4270
|
await git().pull();
|
|
@@ -4189,7 +4424,7 @@ electron.app.whenReady().then(async () => {
|
|
|
4189
4424
|
registerMockHandlers(electron.ipcMain);
|
|
4190
4425
|
registerOAuth2Handlers(electron.ipcMain);
|
|
4191
4426
|
registerWsHandlers(electron.ipcMain);
|
|
4192
|
-
registerSoapHandlers(electron.ipcMain);
|
|
4427
|
+
soapHandler.registerSoapHandlers(electron.ipcMain);
|
|
4193
4428
|
registerDocsHandlers(electron.ipcMain);
|
|
4194
4429
|
registerContractHandlers(electron.ipcMain);
|
|
4195
4430
|
registerGitHandlers(electron.ipcMain);
|
package/out/main/mock.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const mockServer = require("./chunks/mock-server-
|
|
5
|
+
const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
|
|
6
6
|
require("http");
|
|
7
7
|
require("crypto");
|
|
8
8
|
require("vm");
|
package/out/main/runner.js
CHANGED
|
@@ -4,7 +4,7 @@ const promises = require("fs/promises");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const undici = require("undici");
|
|
6
6
|
const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
|
|
7
|
-
const requestCollection = require("./chunks/request-collection-
|
|
7
|
+
const requestCollection = require("./chunks/request-collection-DIsjTggj.js");
|
|
8
8
|
require("crypto");
|
|
9
9
|
require("dayjs");
|
|
10
10
|
require("vm");
|
|
@@ -12,6 +12,7 @@ require("tv4");
|
|
|
12
12
|
require("jsonpath-plus");
|
|
13
13
|
require("@xmldom/xmldom");
|
|
14
14
|
require("ajv");
|
|
15
|
+
require("./chunks/ipc-validate-CscN4HfG.js");
|
|
15
16
|
function buildJsonReport(results, summary, meta = {}) {
|
|
16
17
|
return JSON.stringify({
|
|
17
18
|
timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -267,7 +268,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
267
268
|
const html = document.documentElement
|
|
268
269
|
const isLight = html.classList.toggle('light')
|
|
269
270
|
document.getElementById('themeBtn').textContent = isLight ? '🌙' : '☀️'
|
|
270
|
-
try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {}
|
|
271
|
+
try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {} /* private mode / quota — non-fatal */
|
|
271
272
|
}
|
|
272
273
|
// Restore saved preference or respect OS preference
|
|
273
274
|
(function() {
|
|
@@ -277,7 +278,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
277
278
|
document.documentElement.classList.add('light')
|
|
278
279
|
document.getElementById('themeBtn').textContent = '🌙'
|
|
279
280
|
}
|
|
280
|
-
} catch {}
|
|
281
|
+
} catch {} /* private mode / quota — non-fatal */
|
|
281
282
|
})()
|
|
282
283
|
<\/script>
|
|
283
284
|
</body>
|