@testsmith/api-spector 0.1.9 → 0.2.1
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 +12 -2
- package/out/main/agents.js +212 -0
- package/out/main/chunks/{request-handler-DAXUMRyd.js → request-collection-Dx0ZqB54.js} +280 -12
- package/out/main/index.js +1060 -191
- package/out/main/runner.js +169 -90
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{index-DheIaFRx.js → index-DPHZdvL5.js} +14718 -1387
- package/out/renderer/assets/index-DVaubmCJ.css +2 -0
- package/out/renderer/index.html +3 -3
- package/package.json +5 -3
- package/readme.md +1 -1
- package/resources/splash.html +21 -9
- package/out/renderer/assets/index-BhOUavjZ.css +0 -2
package/out/main/runner.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const undici = require("undici");
|
|
6
|
-
const
|
|
6
|
+
const requestCollection = require("./chunks/request-collection-Dx0ZqB54.js");
|
|
7
7
|
require("crypto");
|
|
8
8
|
require("dayjs");
|
|
9
9
|
require("vm");
|
|
10
10
|
require("tv4");
|
|
11
11
|
require("jsonpath-plus");
|
|
12
12
|
require("@xmldom/xmldom");
|
|
13
|
+
require("ajv");
|
|
13
14
|
function buildJsonReport(results, summary, meta = {}) {
|
|
14
15
|
return JSON.stringify({
|
|
15
16
|
timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -62,8 +63,13 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
62
63
|
after: "AFTER",
|
|
63
64
|
afterAll: "AFTER ALL"
|
|
64
65
|
};
|
|
66
|
+
let lastScopeKey = null;
|
|
65
67
|
const cards = results.map((r, idx) => {
|
|
66
|
-
const
|
|
68
|
+
const scopeKey = (r.scopePath ?? []).join(" / ");
|
|
69
|
+
const groupHeading = scopeKey && scopeKey !== lastScopeKey ? ` <div class="scope-heading">${esc(scopeKey)}</div>
|
|
70
|
+
` : "";
|
|
71
|
+
lastScopeKey = scopeKey;
|
|
72
|
+
const statusCls = r.status === "passed" ? "badge-pass" : r.status === "failed" ? "badge-fail" : r.status === "skipped" ? "badge-skip" : "badge-err";
|
|
67
73
|
const httpCls = r.httpStatus && r.httpStatus < 300 ? "http-ok" : r.httpStatus && r.httpStatus < 400 ? "http-redir" : "http-err";
|
|
68
74
|
const dur = r.durationMs != null ? `${r.durationMs} ms` : "—";
|
|
69
75
|
const label = r.iterationLabel ? ` <span class="muted">#${esc(r.iterationLabel)}</span>` : "";
|
|
@@ -101,7 +107,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
101
107
|
${resp.body ? `<div class="section-label">Body</div><pre class="code-block">${prettyJson(resp.body)}</pre>` : ""}
|
|
102
108
|
</div>` : "";
|
|
103
109
|
const hookCls = hookLabel ? r.hookType?.startsWith("before") ? "card-hook-before" : "card-hook-after" : "";
|
|
104
|
-
return
|
|
110
|
+
return `${groupHeading}
|
|
105
111
|
<div class="card ${hookCls}" id="r${idx}">
|
|
106
112
|
<div class="card-header" onclick="toggle(${idx})">
|
|
107
113
|
<span class="chevron" id="ch${idx}">▶</span>
|
|
@@ -130,76 +136,109 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
130
136
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
131
137
|
<title>${esc(collection)} — Test Results</title>
|
|
132
138
|
<style>
|
|
139
|
+
/* ─── CSS custom properties for theming ──────────────────────────────── */
|
|
140
|
+
:root {
|
|
141
|
+
--bg: #0f1117; --bg-surface: #161b22; --bg-panel: #0d1117;
|
|
142
|
+
--border: #21262d; --border-hover:#30363d;
|
|
143
|
+
--text: #c9d1d9; --text-bright: #e6edf3; --text-muted: #8b949e; --text-dim: #484f58;
|
|
144
|
+
--green: #3fb950; --red: #f85149; --amber: #d29922; --blue: #79c0ff;
|
|
145
|
+
--badge-pass-bg: #0d3a1e; --badge-fail-bg: #3d1014; --badge-err-bg: #3d2a00; --badge-skip-bg: #21262d;
|
|
146
|
+
--hook-before-border: #7c3aed; --hook-after-border: #0e7490;
|
|
147
|
+
--hook-before-bg: #4c1d95; --hook-before-fg: #c4b5fd;
|
|
148
|
+
--hook-after-bg: #164e63; --hook-after-fg: #67e8f9;
|
|
149
|
+
--link: #3d7fb2;
|
|
150
|
+
}
|
|
151
|
+
html.light {
|
|
152
|
+
--bg: #ffffff; --bg-surface: #f6f8fa; --bg-panel: #ffffff;
|
|
153
|
+
--border: #d0d7de; --border-hover:#c4c9cf;
|
|
154
|
+
--text: #1f2328; --text-bright: #1f2328; --text-muted: #656d76; --text-dim: #8b949e;
|
|
155
|
+
--green: #1a7f37; --red: #cf222e; --amber: #9a6700; --blue: #0969da;
|
|
156
|
+
--badge-pass-bg: #dafbe1; --badge-fail-bg: #ffebe9; --badge-err-bg: #fff8c5; --badge-skip-bg: #f6f8fa;
|
|
157
|
+
--hook-before-border: #8b5cf6; --hook-after-border: #06b6d4;
|
|
158
|
+
--hook-before-bg: #ede9fe; --hook-before-fg: #6d28d9;
|
|
159
|
+
--hook-after-bg: #ecfeff; --hook-after-fg: #0e7490;
|
|
160
|
+
--link: #0969da;
|
|
161
|
+
}
|
|
162
|
+
/* ─── Base ───────────────────────────────────────────────────────────── */
|
|
133
163
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
134
|
-
body { font-family: system-ui, sans-serif; background:
|
|
164
|
+
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.5; }
|
|
135
165
|
.wrap { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
|
|
136
|
-
h1 { font-size: 20px; font-weight: 600; color:
|
|
137
|
-
.meta-line { color:
|
|
166
|
+
h1 { font-size: 20px; font-weight: 600; color: var(--text-bright); margin-bottom: 4px; }
|
|
167
|
+
.meta-line { color: var(--text-muted); font-size: 11px; margin-bottom: 24px; }
|
|
168
|
+
/* Theme toggle */
|
|
169
|
+
.theme-toggle { position: fixed; top: 16px; right: 16px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; font-size: 16px; cursor: pointer; z-index: 100; line-height: 1; }
|
|
170
|
+
.theme-toggle:hover { border-color: var(--blue); }
|
|
171
|
+
/* Summary */
|
|
138
172
|
.summary { display: flex; gap: 12px; margin-bottom: 28px; flex-wrap: wrap; }
|
|
139
|
-
.stat { background:
|
|
140
|
-
.stat-val { font-size: 22px; font-weight: 700; color:
|
|
141
|
-
.stat-lbl { font-size: 11px; color:
|
|
142
|
-
.stat-pass .stat-val { color:
|
|
143
|
-
.stat-fail .stat-val { color:
|
|
144
|
-
.stat-err .stat-val { color:
|
|
173
|
+
.stat { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 20px; min-width: 90px; }
|
|
174
|
+
.stat-val { font-size: 22px; font-weight: 700; color: var(--text-bright); }
|
|
175
|
+
.stat-lbl { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
|
176
|
+
.stat-pass .stat-val { color: var(--green); }
|
|
177
|
+
.stat-fail .stat-val { color: var(--red); }
|
|
178
|
+
.stat-err .stat-val { color: var(--amber); }
|
|
179
|
+
.stat-skip .stat-val { color: var(--text-muted); }
|
|
145
180
|
/* Cards */
|
|
146
|
-
.card { border: 1px solid
|
|
181
|
+
.card { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
|
|
182
|
+
.scope-heading { margin: 18px 0 6px; padding: 4px 2px; font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
|
|
183
|
+
.scope-heading::before { content: "\\25B8 "; color: var(--text-dim); }
|
|
147
184
|
.card-header { display: flex; align-items: baseline; gap: 8px; padding: 10px 14px; cursor: pointer; user-select: none; }
|
|
148
|
-
.card-header:hover { background:
|
|
149
|
-
.chevron { font-size: 10px; color:
|
|
150
|
-
.card-name { font-weight: 500; color:
|
|
185
|
+
.card-header:hover { background: var(--bg-surface); }
|
|
186
|
+
.chevron { font-size: 10px; color: var(--text-muted); min-width: 10px; transition: transform .15s; }
|
|
187
|
+
.card-name { font-weight: 500; color: var(--text-bright); white-space: nowrap; }
|
|
151
188
|
.card-url { font-family: monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
152
|
-
.card-body { padding: 12px 14px; border-top: 1px solid
|
|
189
|
+
.card-body { padding: 12px 14px; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 10px; }
|
|
153
190
|
/* Badges */
|
|
154
191
|
.badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
155
|
-
.badge-pass { background:
|
|
156
|
-
.badge-fail { background:
|
|
157
|
-
.badge-err { background:
|
|
158
|
-
.
|
|
159
|
-
.method
|
|
192
|
+
.badge-pass { background: var(--badge-pass-bg); color: var(--green); }
|
|
193
|
+
.badge-fail { background: var(--badge-fail-bg); color: var(--red); }
|
|
194
|
+
.badge-err { background: var(--badge-err-bg); color: var(--amber); }
|
|
195
|
+
.badge-skip { background: var(--badge-skip-bg); color: var(--text-muted); }
|
|
196
|
+
.method { font-family: monospace; font-size: 11px; font-weight: 700; color: var(--blue); white-space: nowrap; }
|
|
197
|
+
.method-badge { display: inline-block; font-family: monospace; font-size: 11px; font-weight: 700; color: var(--blue); min-width: 52px; }
|
|
160
198
|
.http-badge { font-family: monospace; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
161
|
-
.http-ok { color:
|
|
162
|
-
.http-redir { color:
|
|
163
|
-
.http-err { color:
|
|
199
|
+
.http-ok { color: var(--green); }
|
|
200
|
+
.http-redir { color: var(--blue); }
|
|
201
|
+
.http-err { color: var(--red); }
|
|
164
202
|
.dur { font-family: monospace; font-size: 11px; white-space: nowrap; }
|
|
165
|
-
.muted { color:
|
|
203
|
+
.muted { color: var(--text-muted); }
|
|
166
204
|
.mono { font-family: monospace; font-size: 12px; }
|
|
167
205
|
/* Request / Response */
|
|
168
206
|
.req-resp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
169
207
|
@media (max-width: 700px) { .req-resp-grid { grid-template-columns: 1fr; } }
|
|
170
|
-
.panel-label { font-size: 11px; font-weight: 600; color:
|
|
171
|
-
.panel { background:
|
|
172
|
-
.req-line { font-family: monospace; font-size: 12px; color:
|
|
208
|
+
.panel-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
|
|
209
|
+
.panel { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
210
|
+
.req-line { font-family: monospace; font-size: 12px; color: var(--text); word-break: break-all; }
|
|
173
211
|
.resp-status { font-family: monospace; font-size: 13px; font-weight: 700; }
|
|
174
|
-
.section-label { font-size: 10px; font-weight: 600; color:
|
|
212
|
+
.section-label { font-size: 10px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
|
|
175
213
|
.htable { width: 100%; border-collapse: collapse; }
|
|
176
214
|
.htable td { font-family: monospace; font-size: 11px; padding: 1px 0; vertical-align: top; }
|
|
177
|
-
.hk { color:
|
|
178
|
-
.hv { color:
|
|
179
|
-
.code-block { font-family: monospace; font-size: 11px; color:
|
|
215
|
+
.hk { color: var(--blue); padding-right: 12px; white-space: nowrap; }
|
|
216
|
+
.hv { color: var(--text); word-break: break-all; }
|
|
217
|
+
.code-block { font-family: monospace; font-size: 11px; color: var(--text); background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 8px; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; }
|
|
180
218
|
/* Tests */
|
|
181
|
-
.section-label { font-size: 11px; font-weight: 600; color:
|
|
219
|
+
.section-label { font-size: 11px; font-weight: 600; color: var(--text-muted); }
|
|
182
220
|
.tests-wrap { display: flex; flex-direction: column; gap: 2px; }
|
|
183
221
|
.test-row { font-size: 12px; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
184
|
-
.test-pass { color:
|
|
185
|
-
.test-fail { color:
|
|
186
|
-
.test-err { color:
|
|
222
|
+
.test-pass { color: var(--green); }
|
|
223
|
+
.test-fail { color: var(--red); }
|
|
224
|
+
.test-err { color: var(--text-muted); padding-left: 16px; width: 100%; font-family: monospace; font-size: 11px; }
|
|
187
225
|
.dot { font-weight: 700; }
|
|
188
226
|
/* Console */
|
|
189
|
-
.err-row { color:
|
|
227
|
+
.err-row { color: var(--red); font-size: 12px; }
|
|
190
228
|
/* Footer */
|
|
191
|
-
.report-footer { margin-top: 32px; text-align: center; font-size: 11px; color:
|
|
192
|
-
.footer-link { color:
|
|
229
|
+
.report-footer { margin-top: 32px; text-align: center; font-size: 11px; color: var(--text-dim); }
|
|
230
|
+
.footer-link { color: var(--link); text-decoration: none; }
|
|
193
231
|
.footer-link:hover { text-decoration: underline; }
|
|
194
232
|
/* Hook cards */
|
|
195
|
-
.card-hook-before { border-left: 3px solid
|
|
196
|
-
.card-hook-after { border-left: 3px solid
|
|
233
|
+
.card-hook-before { border-left: 3px solid var(--hook-before-border); }
|
|
234
|
+
.card-hook-after { border-left: 3px solid var(--hook-after-border); }
|
|
197
235
|
.hook-badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 10px; font-weight: 700; white-space: nowrap; letter-spacing: .04em; }
|
|
198
|
-
.hook-before { background:
|
|
199
|
-
.hook-after { background:
|
|
236
|
+
.hook-before { background: var(--hook-before-bg); color: var(--hook-before-fg); }
|
|
237
|
+
.hook-after { background: var(--hook-after-bg); color: var(--hook-after-fg); }
|
|
200
238
|
</style>
|
|
201
239
|
</head>
|
|
202
240
|
<body>
|
|
241
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle dark/light mode" id="themeBtn">☀️</button>
|
|
203
242
|
<div class="wrap">
|
|
204
243
|
<h1>${esc(collection)}</h1>
|
|
205
244
|
<div class="meta-line">Environment: ${esc(env)} · ${esc(ts)}</div>
|
|
@@ -208,11 +247,12 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
208
247
|
<div class="stat stat-pass"><div class="stat-val">${summary.passed}</div><div class="stat-lbl">Passed</div></div>
|
|
209
248
|
<div class="stat stat-fail"><div class="stat-val">${summary.failed}</div><div class="stat-lbl">Failed</div></div>
|
|
210
249
|
<div class="stat stat-err"><div class="stat-val">${summary.errors}</div><div class="stat-lbl">Errors</div></div>
|
|
250
|
+
<div class="stat stat-skip"><div class="stat-val">${summary.skipped ?? 0}</div><div class="stat-lbl">No tests</div></div>
|
|
211
251
|
<div class="stat"><div class="stat-val">${passRate}%</div><div class="stat-lbl">Pass rate</div></div>
|
|
212
252
|
<div class="stat"><div class="stat-val">${summary.durationMs} ms</div><div class="stat-lbl">Duration</div></div>
|
|
213
253
|
</div>
|
|
214
254
|
<div class="cards">${cards}</div>
|
|
215
|
-
<div class="report-footer">Generated by <a href="https://testsmith.io" target="_blank" rel="noopener" class="footer-link">Testsmith</a> ·
|
|
255
|
+
<div class="report-footer">Generated by <a href="https://testsmith.io" target="_blank" rel="noopener" class="footer-link">Testsmith</a> · API Spector</div>
|
|
216
256
|
</div>
|
|
217
257
|
<script>
|
|
218
258
|
function toggle(i) {
|
|
@@ -222,6 +262,22 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
222
262
|
body.style.display = open ? 'none' : 'block'
|
|
223
263
|
ch.style.transform = open ? '' : 'rotate(90deg)'
|
|
224
264
|
}
|
|
265
|
+
function toggleTheme() {
|
|
266
|
+
const html = document.documentElement
|
|
267
|
+
const isLight = html.classList.toggle('light')
|
|
268
|
+
document.getElementById('themeBtn').textContent = isLight ? '🌙' : '☀️'
|
|
269
|
+
try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {}
|
|
270
|
+
}
|
|
271
|
+
// Restore saved preference or respect OS preference
|
|
272
|
+
(function() {
|
|
273
|
+
try {
|
|
274
|
+
const saved = localStorage.getItem('theme')
|
|
275
|
+
if (saved === 'light' || (!saved && window.matchMedia('(prefers-color-scheme: light)').matches)) {
|
|
276
|
+
document.documentElement.classList.add('light')
|
|
277
|
+
document.getElementById('themeBtn').textContent = '🌙'
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
280
|
+
})()
|
|
225
281
|
<\/script>
|
|
226
282
|
</body>
|
|
227
283
|
</html>
|
|
@@ -251,6 +307,8 @@ function buildJUnitReport(results, summary, meta = {}) {
|
|
|
251
307
|
failures.push(` <error message="${esc(r.preScriptError)}" type="PreScriptError" />`);
|
|
252
308
|
} else if (r.postScriptError) {
|
|
253
309
|
failures.push(` <error message="${esc(r.postScriptError)}" type="PostScriptError" />`);
|
|
310
|
+
} else if (r.status === "skipped") {
|
|
311
|
+
failures.push(' <skipped message="No assertions defined for this request" />');
|
|
254
312
|
} else if (r.testResults?.length) {
|
|
255
313
|
for (const t of r.testResults) {
|
|
256
314
|
if (!t.passed) {
|
|
@@ -265,32 +323,17 @@ ${failures.join("\n")}
|
|
|
265
323
|
` : "";
|
|
266
324
|
return ` <testcase name="${name}" classname="${classname}" time="${timeSec}">${inner}</testcase>`;
|
|
267
325
|
});
|
|
326
|
+
const skipped = summary.skipped ?? 0;
|
|
268
327
|
const lines = [
|
|
269
328
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
270
|
-
`<testsuites name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" time="${totalSec}">`,
|
|
271
|
-
` <testsuite name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" time="${totalSec}" timestamp="${ts}">`,
|
|
329
|
+
`<testsuites name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" skipped="${skipped}" time="${totalSec}">`,
|
|
330
|
+
` <testsuite name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" skipped="${skipped}" time="${totalSec}" timestamp="${ts}">`,
|
|
272
331
|
...cases,
|
|
273
332
|
" </testsuite>",
|
|
274
333
|
"</testsuites>"
|
|
275
334
|
];
|
|
276
335
|
return lines.join("\n") + "\n";
|
|
277
336
|
}
|
|
278
|
-
function collectTagged(folder, requests, collectionVars, filterTags) {
|
|
279
|
-
const results = [];
|
|
280
|
-
for (const reqId of folder.requestIds) {
|
|
281
|
-
const req = requests[reqId];
|
|
282
|
-
if (!req || req.hookType) continue;
|
|
283
|
-
const tags = req.meta?.tags ?? [];
|
|
284
|
-
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
285
|
-
results.push({ request: req, collectionVars });
|
|
286
|
-
}
|
|
287
|
-
for (const sub of folder.folders) {
|
|
288
|
-
const folderTags = sub.tags ?? [];
|
|
289
|
-
const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
290
|
-
results.push(...collectTagged(sub, requests, collectionVars, effectiveTags));
|
|
291
|
-
}
|
|
292
|
-
return results;
|
|
293
|
-
}
|
|
294
337
|
const C = {
|
|
295
338
|
reset: "\x1B[0m",
|
|
296
339
|
bold: "\x1B[1m",
|
|
@@ -359,6 +402,10 @@ async function loadEnvironments(workspace, dir) {
|
|
|
359
402
|
return envs;
|
|
360
403
|
}
|
|
361
404
|
async function executeRequest(req, collectionVars, envVars, globals, localVars, verbose, tls) {
|
|
405
|
+
if (!req.headers) req.headers = [];
|
|
406
|
+
if (!req.params) req.params = [];
|
|
407
|
+
if (!req.body) req.body = { mode: "none" };
|
|
408
|
+
if (!req.auth) req.auth = { type: "none" };
|
|
362
409
|
const base = {
|
|
363
410
|
requestId: req.id,
|
|
364
411
|
name: req.name,
|
|
@@ -370,8 +417,10 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
370
417
|
let updatedCollectionVars = { ...collectionVars };
|
|
371
418
|
let updatedGlobals = { ...globals };
|
|
372
419
|
let preScriptError;
|
|
420
|
+
const dynamicVars = await requestCollection.buildDynamicVars();
|
|
421
|
+
let vars = requestCollection.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
|
|
373
422
|
if (req.preRequestScript?.trim()) {
|
|
374
|
-
const r = await
|
|
423
|
+
const r = await requestCollection.runScript(requestCollection.interpolate(req.preRequestScript, vars), {
|
|
375
424
|
envVars: { ...envVars },
|
|
376
425
|
collectionVars: { ...collectionVars },
|
|
377
426
|
globals: { ...globals },
|
|
@@ -382,42 +431,41 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
382
431
|
updatedEnvVars = r.updatedEnvVars;
|
|
383
432
|
updatedCollectionVars = r.updatedCollectionVars;
|
|
384
433
|
updatedGlobals = r.updatedGlobals;
|
|
385
|
-
|
|
386
|
-
await
|
|
434
|
+
requestCollection.patchGlobals(r.updatedGlobals);
|
|
435
|
+
await requestCollection.persistGlobals();
|
|
387
436
|
if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [pre] ${l}`, C.gray)));
|
|
388
437
|
if (r.error) console.error(color(` [pre-script error] ${r.error}`, C.red));
|
|
389
438
|
}
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
const resolvedUrl = requestHandler.buildUrl(req.url, req.params, vars);
|
|
439
|
+
vars = requestCollection.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
440
|
+
const resolvedUrl = requestCollection.buildUrl(req.url, req.params, vars);
|
|
393
441
|
base.resolvedUrl = resolvedUrl;
|
|
394
442
|
const start = Date.now();
|
|
395
443
|
try {
|
|
396
444
|
const authH = {};
|
|
397
445
|
if (req.auth.type === "bearer") {
|
|
398
446
|
let token = req.auth.token ?? "";
|
|
399
|
-
if (!token && req.auth.tokenSecretRef) token = await
|
|
400
|
-
if (token) authH["Authorization"] = `Bearer ${
|
|
447
|
+
if (!token && req.auth.tokenSecretRef) token = await requestCollection.getSecret(req.auth.tokenSecretRef) ?? "";
|
|
448
|
+
if (token) authH["Authorization"] = `Bearer ${requestCollection.interpolate(token, vars)}`;
|
|
401
449
|
}
|
|
402
450
|
const headers = new undici.Headers();
|
|
403
451
|
for (const h of req.headers) {
|
|
404
|
-
if (h.enabled && h.key) headers.set(
|
|
452
|
+
if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
|
|
405
453
|
}
|
|
406
454
|
for (const [k, v] of Object.entries(authH)) headers.set(k, v);
|
|
407
455
|
let body;
|
|
408
456
|
if (req.body.mode === "json" && req.body.json) {
|
|
409
|
-
body =
|
|
457
|
+
body = requestCollection.interpolate(req.body.json, vars);
|
|
410
458
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
411
459
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
412
|
-
body =
|
|
460
|
+
body = requestCollection.interpolate(req.body.raw, vars);
|
|
413
461
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
414
462
|
} else if (req.body.mode === "graphql" && req.body.graphql) {
|
|
415
463
|
const gql = req.body.graphql;
|
|
416
|
-
const gqlBody = { query:
|
|
464
|
+
const gqlBody = { query: requestCollection.interpolate(gql.query, vars) };
|
|
417
465
|
const rawVars = gql.variables?.trim();
|
|
418
466
|
if (rawVars) {
|
|
419
467
|
try {
|
|
420
|
-
gqlBody.variables = JSON.parse(
|
|
468
|
+
gqlBody.variables = JSON.parse(requestCollection.interpolate(rawVars, vars));
|
|
421
469
|
} catch {
|
|
422
470
|
}
|
|
423
471
|
}
|
|
@@ -425,7 +473,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
425
473
|
body = JSON.stringify(gqlBody);
|
|
426
474
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
427
475
|
}
|
|
428
|
-
const dispatcher = await
|
|
476
|
+
const dispatcher = await requestCollection.buildDispatcher(void 0, tls);
|
|
429
477
|
const fetchResp = await undici.fetch(resolvedUrl, {
|
|
430
478
|
method: req.method,
|
|
431
479
|
headers,
|
|
@@ -450,7 +498,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
450
498
|
let consoleOutput = [];
|
|
451
499
|
let postScriptError;
|
|
452
500
|
if (req.postRequestScript?.trim()) {
|
|
453
|
-
const r = await
|
|
501
|
+
const r = await requestCollection.runScript(requestCollection.interpolate(req.postRequestScript, vars), {
|
|
454
502
|
envVars: updatedEnvVars,
|
|
455
503
|
collectionVars: updatedCollectionVars,
|
|
456
504
|
globals: updatedGlobals,
|
|
@@ -464,13 +512,24 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
464
512
|
updatedCollectionVars = r.updatedCollectionVars;
|
|
465
513
|
updatedGlobals = r.updatedGlobals;
|
|
466
514
|
localVars = r.updatedLocalVars;
|
|
467
|
-
|
|
468
|
-
await
|
|
515
|
+
requestCollection.patchGlobals(r.updatedGlobals);
|
|
516
|
+
await requestCollection.persistGlobals();
|
|
469
517
|
if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [post] ${l}`, C.gray)));
|
|
470
518
|
}
|
|
471
519
|
const allPassed = testResults.every((t) => t.passed);
|
|
472
|
-
const
|
|
473
|
-
const
|
|
520
|
+
const httpFailed = fetchResp.status >= 400;
|
|
521
|
+
const hasTests = testResults.length > 0;
|
|
522
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "skipped";
|
|
523
|
+
if (httpFailed && testResults.length === 0) {
|
|
524
|
+
testResults = [
|
|
525
|
+
...testResults,
|
|
526
|
+
{
|
|
527
|
+
name: `HTTP status ${fetchResp.status} ${fetchResp.statusText}`.trim(),
|
|
528
|
+
passed: false,
|
|
529
|
+
error: `Request returned ${fetchResp.status} — no assertion was defined to verify the status code.`
|
|
530
|
+
}
|
|
531
|
+
];
|
|
532
|
+
}
|
|
474
533
|
const reqHeaders = {};
|
|
475
534
|
headers.forEach((v, k) => {
|
|
476
535
|
reqHeaders[k] = v;
|
|
@@ -510,7 +569,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
510
569
|
}
|
|
511
570
|
}
|
|
512
571
|
function printResult(r, verbose) {
|
|
513
|
-
const icon = r.status === "passed" ? color("✓", C.green, C.bold) : r.status === "failed" ? color("✗", C.red, C.bold) : color("⚠", C.yellow, C.bold);
|
|
572
|
+
const icon = r.status === "passed" ? color("✓", C.green, C.bold) : r.status === "failed" ? color("✗", C.red, C.bold) : r.status === "skipped" ? color("○", C.gray, C.bold) : color("⚠", C.yellow, C.bold);
|
|
514
573
|
const http = r.httpStatus ? color(` ${r.httpStatus}`, r.httpStatus < 400 ? C.green : C.red) : "";
|
|
515
574
|
const dur = r.durationMs !== void 0 ? color(` ${r.durationMs}ms`, C.gray) : "";
|
|
516
575
|
const method = color(r.method.padEnd(7), C.cyan);
|
|
@@ -554,7 +613,7 @@ async function main() {
|
|
|
554
613
|
console.error(color(`Error: could not read workspace file: ${wsPath}`, C.red));
|
|
555
614
|
process.exit(1);
|
|
556
615
|
}
|
|
557
|
-
await
|
|
616
|
+
await requestCollection.loadGlobals(wsDir);
|
|
558
617
|
const collections = await loadCollections(workspace, wsDir);
|
|
559
618
|
const environments = await loadEnvironments(workspace, wsDir);
|
|
560
619
|
const env = envName ? environments.find((e) => e.name.toLowerCase() === envName.toLowerCase()) ?? null : null;
|
|
@@ -567,7 +626,7 @@ async function main() {
|
|
|
567
626
|
console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
|
|
568
627
|
if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
|
|
569
628
|
console.log("");
|
|
570
|
-
const envVarsSnapshot = await
|
|
629
|
+
const envVarsSnapshot = await requestCollection.buildEnvVars(env);
|
|
571
630
|
const secretValuesToMask = (env?.variables ?? []).filter((v) => v.secret && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
|
|
572
631
|
function redact(s) {
|
|
573
632
|
let out = s;
|
|
@@ -587,24 +646,36 @@ async function main() {
|
|
|
587
646
|
} : void 0
|
|
588
647
|
};
|
|
589
648
|
}
|
|
590
|
-
const summary = { total: 0, passed: 0, failed: 0, errors: 0, durationMs: 0 };
|
|
649
|
+
const summary = { total: 0, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
|
|
591
650
|
const allResults = [];
|
|
592
651
|
const totalStart = Date.now();
|
|
593
652
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
594
653
|
let firstColName;
|
|
595
654
|
for (const col of collections) {
|
|
596
655
|
if (colName && col.name.toLowerCase() !== colName.toLowerCase()) continue;
|
|
597
|
-
const items = collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
|
|
656
|
+
const items = requestCollection.collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
|
|
598
657
|
if (items.length === 0) continue;
|
|
599
658
|
if (!firstColName) firstColName = col.name;
|
|
600
|
-
let runEnvVars = await
|
|
601
|
-
let runGlobals =
|
|
659
|
+
let runEnvVars = await requestCollection.buildEnvVars(env);
|
|
660
|
+
let runGlobals = requestCollection.getGlobals();
|
|
602
661
|
let runCollectionVars = { ...col.collectionVariables ?? {} };
|
|
603
662
|
let runLocalVars = {};
|
|
604
663
|
console.log(color(` ┌ ${col.name}`, C.bold, C.white));
|
|
605
664
|
const workspaceTls = workspace.settings?.tls;
|
|
606
665
|
const effectiveTls = col.tls ? { ...workspaceTls, ...col.tls } : workspaceTls;
|
|
666
|
+
for (const item of items) {
|
|
667
|
+
const req = item.request;
|
|
668
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(req.id, col);
|
|
669
|
+
if (req.auth.type === "none" && inherited.auth && inherited.auth.type !== "none") {
|
|
670
|
+
req.auth = inherited.auth;
|
|
671
|
+
}
|
|
672
|
+
const inheritedHeaders = inherited.headers.filter((h) => h.enabled && h.key);
|
|
673
|
+
if (inheritedHeaders.length) {
|
|
674
|
+
req.headers = [...inheritedHeaders, ...req.headers];
|
|
675
|
+
}
|
|
676
|
+
}
|
|
607
677
|
let bailed = false;
|
|
678
|
+
let lastPrintedScope = null;
|
|
608
679
|
for (const item of items) {
|
|
609
680
|
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeRequest(
|
|
610
681
|
item.request,
|
|
@@ -619,11 +690,18 @@ async function main() {
|
|
|
619
690
|
runCollectionVars = updatedCollectionVars;
|
|
620
691
|
runGlobals = updatedGlobals;
|
|
621
692
|
runLocalVars = updatedLocalVars;
|
|
693
|
+
result.scopePath = item.scopePath;
|
|
694
|
+
const scopeKey = (item.scopePath ?? []).join(" / ");
|
|
695
|
+
if (scopeKey !== lastPrintedScope) {
|
|
696
|
+
if (scopeKey) console.log(color(` ${scopeKey}`, C.gray, C.bold));
|
|
697
|
+
lastPrintedScope = scopeKey;
|
|
698
|
+
}
|
|
622
699
|
printResult(result, verbose);
|
|
623
700
|
allResults.push(result);
|
|
624
701
|
summary.total++;
|
|
625
702
|
if (result.status === "passed") summary.passed++;
|
|
626
703
|
else if (result.status === "failed") summary.failed++;
|
|
704
|
+
else if (result.status === "skipped") summary.skipped++;
|
|
627
705
|
else summary.errors++;
|
|
628
706
|
if (bail && (result.status === "failed" || result.status === "error")) {
|
|
629
707
|
console.log(color("\n Bailing after first failure.", C.yellow));
|
|
@@ -638,8 +716,9 @@ async function main() {
|
|
|
638
716
|
const passStr = color(`${summary.passed} passed`, C.green, C.bold);
|
|
639
717
|
const failStr = summary.failed > 0 ? color(` · ${summary.failed} failed`, C.red, C.bold) : "";
|
|
640
718
|
const errStr = summary.errors > 0 ? color(` · ${summary.errors} errors`, C.yellow, C.bold) : "";
|
|
719
|
+
const skipStr = summary.skipped > 0 ? color(` · ${summary.skipped} skipped`, C.gray, C.bold) : "";
|
|
641
720
|
const totalStr = color(` · ${summary.total} total · ${summary.durationMs}ms`, C.gray);
|
|
642
|
-
console.log(` ${passStr}${failStr}${errStr}${totalStr}
|
|
721
|
+
console.log(` ${passStr}${failStr}${errStr}${skipStr}${totalStr}
|
|
643
722
|
`);
|
|
644
723
|
if (outputPath) {
|
|
645
724
|
const meta = { workspace: wsPath, environment: env?.name ?? null, collection: firstColName, timestamp };
|
package/out/preload/index.js
CHANGED
|
@@ -36,6 +36,10 @@ const api = {
|
|
|
36
36
|
importOpenApiFromUrl: (url) => electron.ipcRenderer.invoke("import:openapi-url", url),
|
|
37
37
|
importInsomnia: () => electron.ipcRenderer.invoke("import:insomnia"),
|
|
38
38
|
importBruno: () => electron.ipcRenderer.invoke("import:bruno"),
|
|
39
|
+
/** Extract response-body schemas from an OpenAPI spec file (no full import). */
|
|
40
|
+
extractOpenApiSchemas: () => electron.ipcRenderer.invoke("import:openapi-schemas"),
|
|
41
|
+
/** Extract response-body schemas from an OpenAPI spec URL (no full import). */
|
|
42
|
+
extractOpenApiSchemasFromUrl: (url) => electron.ipcRenderer.invoke("import:openapi-schemas-url", url),
|
|
39
43
|
// ─── Code generation ──────────────────────────────────────────────────────
|
|
40
44
|
generateCode: (opts) => electron.ipcRenderer.invoke("generate:code", opts),
|
|
41
45
|
pickOutputDir: () => electron.ipcRenderer.invoke("dialog:pickDir"),
|