@kirrosh/zond 0.19.0 → 0.20.0
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/package.json +1 -1
- package/src/db/queries.ts +6 -4
- package/src/web/data/collection-state.ts +2 -0
- package/src/web/routes/dashboard.ts +22 -0
- package/src/web/static/style.css +31 -0
- package/src/web/views/endpoints-tab.ts +11 -7
- package/src/web/views/results.ts +6 -5
- package/src/web/views/suites-tab.ts +32 -4
package/package.json
CHANGED
package/src/db/queries.ts
CHANGED
|
@@ -255,7 +255,9 @@ export function saveResults(runId: number, suiteResults: TestRunResult[]): void
|
|
|
255
255
|
db.transaction(() => {
|
|
256
256
|
for (const suite of suiteResults) {
|
|
257
257
|
for (const step of suite.steps) {
|
|
258
|
-
const
|
|
258
|
+
const maxBodySize = 50_000;
|
|
259
|
+
const truncBody = (s: string | null | undefined) =>
|
|
260
|
+
s && s.length > maxBodySize ? s.slice(0, maxBodySize) + "\n...[truncated]" : (s ?? null);
|
|
259
261
|
stmt.run({
|
|
260
262
|
$run_id: runId,
|
|
261
263
|
$suite_name: suite.suite_name,
|
|
@@ -264,10 +266,10 @@ export function saveResults(runId: number, suiteResults: TestRunResult[]): void
|
|
|
264
266
|
$duration_ms: step.duration_ms,
|
|
265
267
|
$request_method: step.request.method,
|
|
266
268
|
$request_url: step.request.url,
|
|
267
|
-
$request_body: step.request.body
|
|
269
|
+
$request_body: truncBody(step.request.body),
|
|
268
270
|
$response_status: step.response?.status ?? null,
|
|
269
|
-
$response_body:
|
|
270
|
-
$response_headers:
|
|
271
|
+
$response_body: truncBody(step.response?.body),
|
|
272
|
+
$response_headers: step.response?.headers
|
|
271
273
|
? JSON.stringify(step.response.headers)
|
|
272
274
|
: null,
|
|
273
275
|
$error_message: step.error ?? null,
|
|
@@ -40,6 +40,7 @@ export interface StepViewState {
|
|
|
40
40
|
durationMs?: number;
|
|
41
41
|
requestMethod?: string;
|
|
42
42
|
requestUrl?: string;
|
|
43
|
+
requestBody?: string;
|
|
43
44
|
responseStatus?: number;
|
|
44
45
|
responseBody?: string;
|
|
45
46
|
assertions?: { field: string; rule: string; passed: boolean; actual?: unknown; expected?: unknown }[];
|
|
@@ -281,6 +282,7 @@ export async function buildCollectionState(collection: CollectionRecord): Promis
|
|
|
281
282
|
durationMs: r.duration_ms ?? undefined,
|
|
282
283
|
requestMethod: r.request_method ?? undefined,
|
|
283
284
|
requestUrl: r.request_url ?? undefined,
|
|
285
|
+
requestBody: r.request_body ?? undefined,
|
|
284
286
|
responseStatus: r.response_status ?? undefined,
|
|
285
287
|
responseBody: r.response_body ?? undefined,
|
|
286
288
|
assertions: Array.isArray(r.assertions) ? r.assertions : undefined,
|
|
@@ -253,6 +253,28 @@ async function renderCollectionContent(collection: CollectionRecord): Promise<st
|
|
|
253
253
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('tab-active'));
|
|
254
254
|
el.classList.add('tab-active');
|
|
255
255
|
}
|
|
256
|
+
function switchToSuite(suiteName) {
|
|
257
|
+
var suitesBtn = document.querySelector('[data-tab="suites"]');
|
|
258
|
+
if (!suitesBtn) return;
|
|
259
|
+
suitesBtn.click();
|
|
260
|
+
document.addEventListener('htmx:afterSwap', function handler(e) {
|
|
261
|
+
if (e.detail.target && e.detail.target.id === 'tab-content') {
|
|
262
|
+
document.removeEventListener('htmx:afterSwap', handler);
|
|
263
|
+
setTimeout(function() {
|
|
264
|
+
var rows = document.querySelectorAll('.suite-row[data-suite-name]');
|
|
265
|
+
for (var i = 0; i < rows.length; i++) {
|
|
266
|
+
if (rows[i].dataset.suiteName === suiteName) {
|
|
267
|
+
rows[i].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
268
|
+
rows[i].click();
|
|
269
|
+
rows[i].classList.add('suite-highlight');
|
|
270
|
+
setTimeout(function() { rows[i].classList.remove('suite-highlight'); }, 2000);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}, 50);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
256
278
|
</script>`;
|
|
257
279
|
|
|
258
280
|
return `
|
package/src/web/static/style.css
CHANGED
|
@@ -816,6 +816,37 @@ h3 { font-size: 1.05rem; margin: 1rem 0 0.25rem; }
|
|
|
816
816
|
.history-row:hover { background: var(--bg-hover); }
|
|
817
817
|
.pagination { display: flex; gap: 0.5rem; margin: 1.5rem 0; align-items: center; }
|
|
818
818
|
|
|
819
|
+
/* ── Step method+path+status labels ── */
|
|
820
|
+
.step-path { font-family: var(--font-mono); font-size: 0.8rem; }
|
|
821
|
+
.step-status-code {
|
|
822
|
+
display: inline-block;
|
|
823
|
+
font-family: var(--font-mono);
|
|
824
|
+
font-size: 0.65rem;
|
|
825
|
+
font-weight: 600;
|
|
826
|
+
padding: 0.1rem 0.35rem;
|
|
827
|
+
border-radius: 3px;
|
|
828
|
+
margin-left: 0.35rem;
|
|
829
|
+
vertical-align: middle;
|
|
830
|
+
}
|
|
831
|
+
.step-status-code.status-ok { background: var(--pass-dim); color: var(--pass); }
|
|
832
|
+
.step-status-code.status-error { background: var(--fail-dim); color: var(--fail); }
|
|
833
|
+
.step-name-dim { color: var(--text-dim); font-size: 0.7rem; margin-left: 0.5rem; }
|
|
834
|
+
|
|
835
|
+
/* ── Suite link (endpoints → suites navigation) ── */
|
|
836
|
+
.suite-link {
|
|
837
|
+
color: var(--accent);
|
|
838
|
+
text-decoration: none;
|
|
839
|
+
cursor: pointer;
|
|
840
|
+
}
|
|
841
|
+
.suite-link:hover { text-decoration: underline; }
|
|
842
|
+
.suite-highlight {
|
|
843
|
+
animation: suite-flash 2s ease-out;
|
|
844
|
+
}
|
|
845
|
+
@keyframes suite-flash {
|
|
846
|
+
0% { background: rgba(61, 139, 253, 0.25); }
|
|
847
|
+
100% { background: transparent; }
|
|
848
|
+
}
|
|
849
|
+
|
|
819
850
|
/* ── Responsive ── */
|
|
820
851
|
@media (max-width: 768px) {
|
|
821
852
|
.health-strip { grid-template-columns: 1fr; gap: 1rem; }
|
|
@@ -93,7 +93,7 @@ function renderWarningBadges(warnings: string[]): string {
|
|
|
93
93
|
if (w === "deprecated") return '<span class="warning-badge warning-deprecated">DEPRECATED</span>';
|
|
94
94
|
if (w === "no_response_schema") return '<span class="warning-badge warning-schema">NO SCHEMA</span>';
|
|
95
95
|
if (w === "no_responses_defined") return '<span class="warning-badge warning-schema">NO RESPONSES</span>';
|
|
96
|
-
if (w.startsWith("required_params_no_examples")) return
|
|
96
|
+
if (w.startsWith("required_params_no_examples")) return "";
|
|
97
97
|
return `<span class="warning-badge">${escapeHtml(w)}</span>`;
|
|
98
98
|
}).join(" ");
|
|
99
99
|
}
|
|
@@ -140,7 +140,8 @@ function renderEndpointDetail(ep: EndpointViewState): string {
|
|
|
140
140
|
|
|
141
141
|
return `<div class="covering-suite">
|
|
142
142
|
${icon}
|
|
143
|
-
<
|
|
143
|
+
<a class="suite-ref suite-link" href="#" data-suite="${escapeHtml(step.suiteName)}"
|
|
144
|
+
onclick="event.stopPropagation();switchToSuite(this.dataset.suite)">${escapeHtml(step.file)}</a>
|
|
144
145
|
<span class="dim" style="font-size:0.75rem;">→ "${escapeHtml(step.stepName)}"</span>
|
|
145
146
|
<span style="margin-left:auto;display:flex;align-items:center;gap:0.5rem;">${statusBadge}${duration}</span>
|
|
146
147
|
</div>${assertionsHtml}${hintHtml}`;
|
|
@@ -149,13 +150,16 @@ function renderEndpointDetail(ep: EndpointViewState): string {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// Fallback: just file names
|
|
152
|
-
const files = ep.coveringFiles.map(f =>
|
|
153
|
-
|
|
153
|
+
const files = ep.coveringFiles.map(f => {
|
|
154
|
+
const fileName = basename(f);
|
|
155
|
+
const suiteName = fileName.replace(/\.(ya?ml)$/i, "");
|
|
156
|
+
return `<div class="covering-suite">
|
|
154
157
|
<span class="step-icon" style="color:var(--text-dim);">○</span>
|
|
155
|
-
<
|
|
158
|
+
<a class="suite-ref suite-link" href="#" data-suite="${escapeHtml(suiteName)}"
|
|
159
|
+
onclick="event.stopPropagation();switchToSuite(this.dataset.suite)">${escapeHtml(fileName)}</a>
|
|
156
160
|
<span class="dim" style="font-size:0.75rem;">not run</span>
|
|
157
|
-
</div
|
|
158
|
-
).join("");
|
|
161
|
+
</div>`;
|
|
162
|
+
}).join("");
|
|
159
163
|
return files;
|
|
160
164
|
}
|
|
161
165
|
|
package/src/web/views/results.ts
CHANGED
|
@@ -98,11 +98,11 @@ export function renderSuiteResults(
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
let reqBodyHtml = "";
|
|
101
|
-
if (
|
|
101
|
+
if (step.request_body) {
|
|
102
102
|
reqBodyHtml = `<details class="body-details"><summary>Request Body</summary><pre>${escapeHtml(step.request_body)}</pre></details>`;
|
|
103
103
|
}
|
|
104
104
|
let resBodyHtml = "";
|
|
105
|
-
if (
|
|
105
|
+
if (step.response_body) {
|
|
106
106
|
resBodyHtml = `<details class="body-details"><summary>Response Body</summary><pre>${escapeHtml(step.response_body)}</pre></details>`;
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -123,7 +123,8 @@ export function renderSuiteResults(
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const
|
|
126
|
+
const hasContent = requestHtml || errorHtml || skipReasonHtml || assertionsHtml || reqBodyHtml || resBodyHtml;
|
|
127
|
+
const detailPanel = hasContent
|
|
127
128
|
? `<div class="detail-panel" id="${detailId}" style="display:none">
|
|
128
129
|
${requestHtml}
|
|
129
130
|
${errorHtml}
|
|
@@ -134,7 +135,7 @@ export function renderSuiteResults(
|
|
|
134
135
|
</div>`
|
|
135
136
|
: "";
|
|
136
137
|
|
|
137
|
-
const toggle =
|
|
138
|
+
const toggle = hasContent
|
|
138
139
|
? `onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'"`
|
|
139
140
|
: "";
|
|
140
141
|
|
|
@@ -144,7 +145,7 @@ export function renderSuiteResults(
|
|
|
144
145
|
return `
|
|
145
146
|
<div class="step-row${chainedClass}${statusClass}" ${toggle}>
|
|
146
147
|
<div>${stepStatusBadge(step.status)}</div>
|
|
147
|
-
<div class="step-name">${escapeHtml(step.test_name)}${capturesHtml ? ` ${capturesHtml}` : ""}</div>
|
|
148
|
+
<div class="step-name">${step.request_method && step.request_url ? (() => { let p: string; try { p = new URL(step.request_url).pathname; } catch { p = step.request_url; } const sc = step.response_status ? ` <span class="step-status-code ${step.response_status >= 400 ? "status-error" : "status-ok"}">${step.response_status}</span>` : ""; return `${methodBadge(step.request_method)} <span class="step-path">${escapeHtml(p)}</span>${sc} <span class="step-name-dim">${escapeHtml(step.test_name)}</span>`; })() : escapeHtml(step.test_name)}${capturesHtml ? ` ${capturesHtml}` : ""}</div>
|
|
148
149
|
<div class="step-duration">${formatDuration(step.duration_ms)}</div>
|
|
149
150
|
</div>
|
|
150
151
|
${detailPanel}`;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { CollectionState, SuiteViewState, StepViewState } from "../data/collection-state.ts";
|
|
6
6
|
import { escapeHtml } from "./layout.ts";
|
|
7
|
+
import { methodBadge } from "./results.ts";
|
|
7
8
|
import { basename } from "node:path";
|
|
8
9
|
|
|
9
10
|
export function renderSuitesTab(state: CollectionState): string {
|
|
@@ -55,7 +56,7 @@ function renderSuiteRow(suite: SuiteViewState, index: number): string {
|
|
|
55
56
|
: `<div style="font-size:0.75rem;color:var(--text-dim);padding:0.5rem;">No run results yet</div>`;
|
|
56
57
|
|
|
57
58
|
return `
|
|
58
|
-
<div class="suite-row"
|
|
59
|
+
<div class="suite-row" data-suite-name="${escapeHtml(suite.name)}"
|
|
59
60
|
onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'">
|
|
60
61
|
<div class="suite-info">
|
|
61
62
|
<div class="suite-name">${escapeHtml(suite.name)}</div>
|
|
@@ -87,6 +88,21 @@ function renderStepRow(step: StepViewState, suiteIdx: number, stepIdx: number):
|
|
|
87
88
|
? `<span class="step-duration">${step.durationMs}ms</span>`
|
|
88
89
|
: `<span class="step-duration">-</span>`;
|
|
89
90
|
|
|
91
|
+
// Primary label: prefer METHOD /path [status] over step name
|
|
92
|
+
let primaryLabel: string;
|
|
93
|
+
let nameLabel = "";
|
|
94
|
+
if (step.requestMethod && step.requestUrl) {
|
|
95
|
+
let urlPath: string;
|
|
96
|
+
try { urlPath = new URL(step.requestUrl).pathname; } catch { urlPath = step.requestUrl; }
|
|
97
|
+
const statusTag = step.responseStatus
|
|
98
|
+
? ` <span class="step-status-code ${step.responseStatus >= 400 ? "status-error" : "status-ok"}">${step.responseStatus}</span>`
|
|
99
|
+
: "";
|
|
100
|
+
primaryLabel = `${methodBadge(step.requestMethod)} <span class="step-path">${escapeHtml(urlPath)}</span>${statusTag}`;
|
|
101
|
+
nameLabel = ` <span class="step-name-dim">${escapeHtml(step.name)}</span>`;
|
|
102
|
+
} else {
|
|
103
|
+
primaryLabel = escapeHtml(step.name);
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
// Captures
|
|
91
107
|
const captureHtml = step.captures && Object.keys(step.captures).length > 0
|
|
92
108
|
? `<span class="step-captures">${Object.entries(step.captures).map(([k, v]) =>
|
|
@@ -95,8 +111,13 @@ function renderStepRow(step: StepViewState, suiteIdx: number, stepIdx: number):
|
|
|
95
111
|
: `<span class="step-captures"></span>`;
|
|
96
112
|
|
|
97
113
|
const detailId = `s-${suiteIdx}-step-${stepIdx}`;
|
|
98
|
-
const hasDetail =
|
|
99
|
-
(
|
|
114
|
+
const hasDetail =
|
|
115
|
+
(step.assertions && step.assertions.length > 0) ||
|
|
116
|
+
step.hint ||
|
|
117
|
+
step.responseBody ||
|
|
118
|
+
step.requestBody ||
|
|
119
|
+
step.errorMessage ||
|
|
120
|
+
(step.requestMethod && step.requestUrl);
|
|
100
121
|
|
|
101
122
|
const clickHandler = hasDetail
|
|
102
123
|
? ` onclick="event.stopPropagation();var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'"`
|
|
@@ -134,6 +155,13 @@ function renderStepRow(step: StepViewState, suiteIdx: number, stepIdx: number):
|
|
|
134
155
|
detailContent += `<div class="failure-hint"><span>⚠</span> ${escapeHtml(step.hint)}</div>`;
|
|
135
156
|
}
|
|
136
157
|
|
|
158
|
+
// Request body toggle
|
|
159
|
+
if (step.requestBody) {
|
|
160
|
+
const truncatedReq = step.requestBody.length > 2000 ? step.requestBody.slice(0, 2000) + "..." : step.requestBody;
|
|
161
|
+
detailContent += `<div class="req-res-toggle" onclick="event.stopPropagation();var b=this.nextElementSibling;b.style.display=b.style.display==='none'?'block':'none'">▼ Request Body</div>
|
|
162
|
+
<div class="req-res-body" style="display:none;"><pre style="font-size:0.7rem;margin:0.25rem 0;">${escapeHtml(truncatedReq)}</pre></div>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
137
165
|
// Response body toggle
|
|
138
166
|
if (step.responseBody) {
|
|
139
167
|
const truncated = step.responseBody.length > 2000 ? step.responseBody.slice(0, 2000) + "..." : step.responseBody;
|
|
@@ -146,7 +174,7 @@ function renderStepRow(step: StepViewState, suiteIdx: number, stepIdx: number):
|
|
|
146
174
|
|
|
147
175
|
return `<div class="step-row"${clickHandler}>
|
|
148
176
|
${icon}
|
|
149
|
-
<span class="step-label"${labelStyle}>${
|
|
177
|
+
<span class="step-label"${labelStyle}>${primaryLabel}${nameLabel}</span>
|
|
150
178
|
${captureHtml}
|
|
151
179
|
${duration}
|
|
152
180
|
</div>${detailPanel}`;
|