@kirrosh/zond 0.21.0 → 0.23.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/CHANGELOG.md +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
package/src/web/views/layout.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
let _devMode = false;
|
|
2
|
-
|
|
3
|
-
export function setDevMode(enabled: boolean): void {
|
|
4
|
-
_devMode = enabled;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function layout(title: string, content: string, navExtra = ""): string {
|
|
8
|
-
const devScript = _devMode
|
|
9
|
-
? `<script>new EventSource('/dev/reload').onmessage = (e) => { if (e.data === 'reload') location.reload() }</script>`
|
|
10
|
-
: "";
|
|
11
|
-
return `<!DOCTYPE html>
|
|
12
|
-
<html lang="en">
|
|
13
|
-
<head>
|
|
14
|
-
<meta charset="UTF-8">
|
|
15
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
16
|
-
<title>${escapeHtml(title)} — zond</title>
|
|
17
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
18
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
19
|
-
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
20
|
-
<link rel="stylesheet" href="/static/style.css?v=${Date.now()}">
|
|
21
|
-
<script src="/static/htmx.min.js"></script>
|
|
22
|
-
<script>htmx.config.refreshOnHistoryMiss = true;</script>
|
|
23
|
-
</head>
|
|
24
|
-
<body>
|
|
25
|
-
<nav class="navbar">
|
|
26
|
-
<a href="/" class="nav-brand" style="text-decoration:none;color:inherit;"><span class="logo-dot"></span>zond</a>
|
|
27
|
-
${navExtra}
|
|
28
|
-
</nav>
|
|
29
|
-
<main class="main-container">
|
|
30
|
-
${content}
|
|
31
|
-
</main>
|
|
32
|
-
<footer class="footer"><div class="main-container">zond</div></footer>
|
|
33
|
-
${devScript}
|
|
34
|
-
</body>
|
|
35
|
-
</html>`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function fragment(content: string): string {
|
|
39
|
-
return content;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function escapeHtml(str: string): string {
|
|
43
|
-
return str
|
|
44
|
-
.replace(/&/g, "&")
|
|
45
|
-
.replace(/</g, "<")
|
|
46
|
-
.replace(/>/g, ">")
|
|
47
|
-
.replace(/"/g, """);
|
|
48
|
-
}
|
package/src/web/views/results.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { escapeHtml } from "./layout.ts";
|
|
2
|
-
import { formatDuration } from "../../core/reporter/console.ts";
|
|
3
|
-
import type { StoredStepResult } from "../../db/queries.ts";
|
|
4
|
-
|
|
5
|
-
export function statusBadge(total: number, passed: number, failed: number): string {
|
|
6
|
-
if (total === 0) return `<span class="badge badge-skip">empty</span>`;
|
|
7
|
-
if (failed > 0) return `<span class="badge badge-fail">fail</span>`;
|
|
8
|
-
return `<span class="badge badge-pass">pass</span>`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function stepStatusBadge(status: string): string {
|
|
12
|
-
switch (status) {
|
|
13
|
-
case "pass":
|
|
14
|
-
return `<span class="badge badge-pass">✓</span>`;
|
|
15
|
-
case "fail":
|
|
16
|
-
return `<span class="badge badge-fail">✗</span>`;
|
|
17
|
-
case "skip":
|
|
18
|
-
return `<span class="badge badge-skip">○</span>`;
|
|
19
|
-
case "error":
|
|
20
|
-
return `<span class="badge badge-error">✗</span>`;
|
|
21
|
-
default:
|
|
22
|
-
return `<span class="badge">${escapeHtml(status)}</span>`;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function methodBadge(method: string): string {
|
|
27
|
-
const m = method.toLowerCase();
|
|
28
|
-
return `<span class="badge-method method-${m}">${method}</span>`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Render grouped suite results with step details, captures, and chain visualization.
|
|
33
|
-
* Used by both the dashboard panels and the /runs/:id detail page.
|
|
34
|
-
*/
|
|
35
|
-
export function renderSuiteResults(
|
|
36
|
-
results: StoredStepResult[],
|
|
37
|
-
runId: number,
|
|
38
|
-
options?: { idPrefix?: string; suiteMetadata?: Map<string, { description?: string; tags?: string[] }> },
|
|
39
|
-
): string {
|
|
40
|
-
const prefix = options?.idPrefix ?? `r${runId}`;
|
|
41
|
-
|
|
42
|
-
// Group by suite
|
|
43
|
-
const suites = new Map<string, StoredStepResult[]>();
|
|
44
|
-
for (const r of results) {
|
|
45
|
-
const list = suites.get(r.suite_name) ?? [];
|
|
46
|
-
list.push(r);
|
|
47
|
-
suites.set(r.suite_name, list);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Build capture source map
|
|
51
|
-
const captureSourceMap = new Map<string, string>();
|
|
52
|
-
for (const [, steps] of suites) {
|
|
53
|
-
for (const step of steps) {
|
|
54
|
-
if (step.captures && typeof step.captures === "object") {
|
|
55
|
-
for (const varName of Object.keys(step.captures)) {
|
|
56
|
-
captureSourceMap.set(varName, step.test_name);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let suitesHtml = "";
|
|
63
|
-
for (const [suiteName, steps] of suites) {
|
|
64
|
-
const suiteHasCaptures = steps.some(s =>
|
|
65
|
-
s.captures && typeof s.captures === "object" && Object.keys(s.captures).length > 0,
|
|
66
|
-
);
|
|
67
|
-
const isChainSuite = suiteHasCaptures || suiteName.endsWith("CRUD");
|
|
68
|
-
|
|
69
|
-
const stepsHtml = steps
|
|
70
|
-
.map((step, i) => {
|
|
71
|
-
const detailId = `detail-${prefix}-${i}`;
|
|
72
|
-
const hasFailed = step.status === "fail" || step.status === "error";
|
|
73
|
-
|
|
74
|
-
let capturesHtml = "";
|
|
75
|
-
if (step.captures && typeof step.captures === "object") {
|
|
76
|
-
const captureEntries = Object.entries(step.captures);
|
|
77
|
-
if (captureEntries.length > 0) {
|
|
78
|
-
capturesHtml = captureEntries.map(([k, v]) =>
|
|
79
|
-
`<span class="capture-badge">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`,
|
|
80
|
-
).join(" ");
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let assertionsHtml = "";
|
|
85
|
-
if (step.assertions.length > 0) {
|
|
86
|
-
const items = step.assertions
|
|
87
|
-
.map(
|
|
88
|
-
(a) =>
|
|
89
|
-
`<li class="${a.passed ? "assertion-pass" : "assertion-fail"}">${escapeHtml(a.field)}: ${escapeHtml(a.rule)} (got ${escapeHtml(String(a.actual))})</li>`,
|
|
90
|
-
)
|
|
91
|
-
.join("");
|
|
92
|
-
assertionsHtml = `<ul class="assertion-list">${items}</ul>`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let requestHtml = "";
|
|
96
|
-
if (step.request_method) {
|
|
97
|
-
requestHtml = `<div><strong>Request:</strong> ${escapeHtml(step.request_method)} ${escapeHtml(step.request_url ?? "")}</div>`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let reqBodyHtml = "";
|
|
101
|
-
if (step.request_body) {
|
|
102
|
-
reqBodyHtml = `<details class="body-details"><summary>Request Body</summary><pre>${escapeHtml(step.request_body)}</pre></details>`;
|
|
103
|
-
}
|
|
104
|
-
let resBodyHtml = "";
|
|
105
|
-
if (step.response_body) {
|
|
106
|
-
resBodyHtml = `<details class="body-details"><summary>Response Body</summary><pre>${escapeHtml(step.response_body)}</pre></details>`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let errorHtml = "";
|
|
110
|
-
if (step.error_message) {
|
|
111
|
-
errorHtml = `<div><strong>Error:</strong> ${escapeHtml(step.error_message)}</div>`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
let skipReasonHtml = "";
|
|
115
|
-
if (step.status === "skip" && step.error_message) {
|
|
116
|
-
const match = step.error_message.match(/Depends on missing capture: (\w+)/);
|
|
117
|
-
if (match) {
|
|
118
|
-
const depVar = match[1]!;
|
|
119
|
-
const sourceStep = captureSourceMap.get(depVar);
|
|
120
|
-
skipReasonHtml = sourceStep
|
|
121
|
-
? `<div class="skip-reason">Skipped: depends on <code>${escapeHtml(depVar)}</code> (from step "${escapeHtml(sourceStep)}")</div>`
|
|
122
|
-
: `<div class="skip-reason">Skipped: depends on <code>${escapeHtml(depVar)}</code></div>`;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const hasContent = requestHtml || errorHtml || skipReasonHtml || assertionsHtml || reqBodyHtml || resBodyHtml;
|
|
127
|
-
const detailPanel = hasContent
|
|
128
|
-
? `<div class="detail-panel" id="${detailId}" style="display:none">
|
|
129
|
-
${requestHtml}
|
|
130
|
-
${errorHtml}
|
|
131
|
-
${skipReasonHtml}
|
|
132
|
-
${assertionsHtml}
|
|
133
|
-
${reqBodyHtml}
|
|
134
|
-
${resBodyHtml}
|
|
135
|
-
</div>`
|
|
136
|
-
: "";
|
|
137
|
-
|
|
138
|
-
const toggle = hasContent
|
|
139
|
-
? `onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'"`
|
|
140
|
-
: "";
|
|
141
|
-
|
|
142
|
-
const chainedClass = isChainSuite ? " chained" : "";
|
|
143
|
-
const statusClass = (step.status === "fail" || step.status === "error") ? ` step-${step.status}` : "";
|
|
144
|
-
|
|
145
|
-
return `
|
|
146
|
-
<div class="step-row${chainedClass}${statusClass}" ${toggle}>
|
|
147
|
-
<div>${stepStatusBadge(step.status)}</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>
|
|
149
|
-
<div class="step-duration">${formatDuration(step.duration_ms)}</div>
|
|
150
|
-
</div>
|
|
151
|
-
${detailPanel}`;
|
|
152
|
-
})
|
|
153
|
-
.join("");
|
|
154
|
-
|
|
155
|
-
const chainClass = isChainSuite ? " chain-suite" : "";
|
|
156
|
-
|
|
157
|
-
const meta = options?.suiteMetadata?.get(suiteName);
|
|
158
|
-
const descriptionHtml = meta?.description
|
|
159
|
-
? `<p class="suite-description">${escapeHtml(meta.description)}</p>`
|
|
160
|
-
: "";
|
|
161
|
-
const tagsHtml = meta?.tags?.length
|
|
162
|
-
? `<div class="suite-tags">${meta.tags.map(t => `<span class="tag-badge">${escapeHtml(t)}</span>`).join(" ")}</div>`
|
|
163
|
-
: "";
|
|
164
|
-
|
|
165
|
-
suitesHtml += `
|
|
166
|
-
<div class="suite-section${chainClass}">
|
|
167
|
-
<h3>${escapeHtml(suiteName)}</h3>
|
|
168
|
-
${descriptionHtml}
|
|
169
|
-
${tagsHtml}
|
|
170
|
-
${isChainSuite ? '<div class="chain-connector">' : ""}
|
|
171
|
-
${stepsHtml}
|
|
172
|
-
${isChainSuite ? "</div>" : ""}
|
|
173
|
-
</div>`;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return suitesHtml;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Render the "show only failed" toggle + auto-expand failed steps script.
|
|
181
|
-
*/
|
|
182
|
-
export function failedFilterToggle(): string {
|
|
183
|
-
return `
|
|
184
|
-
<label class="failed-filter-toggle" style="display:inline-flex;align-items:center;gap:0.5rem;font-size:0.85rem;cursor:pointer;">
|
|
185
|
-
<input type="checkbox" id="failed-only-toggle" onchange="
|
|
186
|
-
var on = this.checked;
|
|
187
|
-
document.querySelectorAll('.step-row').forEach(function(el) {
|
|
188
|
-
if (on && !el.classList.contains('step-fail') && !el.classList.contains('step-error')) {
|
|
189
|
-
el.style.display = 'none';
|
|
190
|
-
var next = el.nextElementSibling;
|
|
191
|
-
if (next && next.classList.contains('detail-panel')) next.style.display = 'none';
|
|
192
|
-
} else {
|
|
193
|
-
el.style.display = '';
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
"> Show only failed
|
|
197
|
-
</label>`;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Script to auto-expand failed step detail panels on page load.
|
|
202
|
-
*/
|
|
203
|
-
export function autoExpandFailedScript(): string {
|
|
204
|
-
return `<script>
|
|
205
|
-
document.querySelectorAll('.step-row.step-fail, .step-row.step-error').forEach(function(el) {
|
|
206
|
-
var next = el.nextElementSibling;
|
|
207
|
-
if (next && next.classList.contains('detail-panel')) next.style.display = 'block';
|
|
208
|
-
});
|
|
209
|
-
</script>`;
|
|
210
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runs tab: history of test runs with comparison.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { escapeHtml } from "./layout.ts";
|
|
6
|
-
import { statusBadge } from "./results.ts";
|
|
7
|
-
import { formatDuration } from "../../core/reporter/console.ts";
|
|
8
|
-
import {
|
|
9
|
-
listRunsByCollection,
|
|
10
|
-
countRunsByCollection,
|
|
11
|
-
getResultsByRunId,
|
|
12
|
-
getRunById,
|
|
13
|
-
} from "../../db/queries.ts";
|
|
14
|
-
import type { RunSummary } from "../../db/queries.ts";
|
|
15
|
-
import { renderSuiteResults, failedFilterToggle, autoExpandFailedScript } from "./results.ts";
|
|
16
|
-
|
|
17
|
-
const PAGE_SIZE = 15;
|
|
18
|
-
|
|
19
|
-
export function renderRunsTab(collectionId: number, page = 1): string {
|
|
20
|
-
const offset = (page - 1) * PAGE_SIZE;
|
|
21
|
-
const runs = listRunsByCollection(collectionId, PAGE_SIZE, offset);
|
|
22
|
-
const total = countRunsByCollection(collectionId);
|
|
23
|
-
const hasMore = offset + runs.length < total;
|
|
24
|
-
|
|
25
|
-
if (runs.length === 0 && page === 1) {
|
|
26
|
-
return `<div class="tab-empty">No test runs yet. Click <strong>Run Tests</strong> to get started.</div>`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const rows = runs.map(r => renderRunRow(r, collectionId)).join("");
|
|
30
|
-
|
|
31
|
-
const loadMore = hasMore
|
|
32
|
-
? `<div style="text-align:center;padding:0.75rem;">
|
|
33
|
-
<button class="btn btn-sm btn-outline"
|
|
34
|
-
hx-get="/panels/runs-tab?collection_id=${collectionId}&page=${page + 1}"
|
|
35
|
-
hx-target="#tab-content" hx-swap="innerHTML">Load more...</button>
|
|
36
|
-
</div>`
|
|
37
|
-
: "";
|
|
38
|
-
|
|
39
|
-
return `
|
|
40
|
-
<div class="runs-list">
|
|
41
|
-
<div class="runs-header">
|
|
42
|
-
<span>Run</span><span>Time</span><span>Results</span><span>Duration</span><span>Status</span>
|
|
43
|
-
</div>
|
|
44
|
-
${rows}
|
|
45
|
-
${loadMore}
|
|
46
|
-
</div>`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function renderRunRow(run: RunSummary, collectionId: number): string {
|
|
50
|
-
const timeAgo = formatTimeAgo(run.started_at);
|
|
51
|
-
const duration = run.duration_ms != null ? formatDuration(run.duration_ms) : "-";
|
|
52
|
-
const total = run.total || 1;
|
|
53
|
-
|
|
54
|
-
// Mini progress bar
|
|
55
|
-
const progressBar = run.total > 0
|
|
56
|
-
? `<div class="progress-bar run-progress">
|
|
57
|
-
<div class="progress-pass" style="width:${(run.passed / total * 100).toFixed(1)}%"></div>
|
|
58
|
-
<div class="progress-fail" style="width:${(run.failed / total * 100).toFixed(1)}%"></div>
|
|
59
|
-
</div>`
|
|
60
|
-
: "";
|
|
61
|
-
|
|
62
|
-
return `
|
|
63
|
-
<div class="run-row"
|
|
64
|
-
hx-get="/panels/run-detail?run_id=${run.id}&collection_id=${collectionId}"
|
|
65
|
-
hx-target="#tab-content" hx-swap="innerHTML">
|
|
66
|
-
<span class="run-id">#${run.id}</span>
|
|
67
|
-
<span class="run-time">${escapeHtml(timeAgo)}</span>
|
|
68
|
-
<span class="run-results">
|
|
69
|
-
${progressBar}
|
|
70
|
-
<span class="run-counts">${run.passed}✓ ${run.failed}✗ ${run.skipped}○</span>
|
|
71
|
-
</span>
|
|
72
|
-
<span class="run-duration">${duration}</span>
|
|
73
|
-
<span>${statusBadge(run.total, run.passed, run.failed)}</span>
|
|
74
|
-
</div>`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function renderRunDetail(runId: number, collectionId: number): string {
|
|
78
|
-
const run = getRunById(runId);
|
|
79
|
-
if (!run) return `<p>Run not found</p>`;
|
|
80
|
-
|
|
81
|
-
const results = getResultsByRunId(runId);
|
|
82
|
-
if (results.length === 0) return `<p class="tab-empty">No results for run #${runId}.</p>`;
|
|
83
|
-
|
|
84
|
-
const timeAgo = formatTimeAgo(run.started_at);
|
|
85
|
-
const duration = run.duration_ms != null ? formatDuration(run.duration_ms) : "-";
|
|
86
|
-
|
|
87
|
-
const backButton = `<button class="btn btn-sm btn-outline" style="margin-bottom:0.75rem;"
|
|
88
|
-
hx-get="/panels/runs-tab?collection_id=${collectionId}"
|
|
89
|
-
hx-target="#tab-content" hx-swap="innerHTML">← Back to runs</button>`;
|
|
90
|
-
|
|
91
|
-
const header = `
|
|
92
|
-
<div class="run-detail-header">
|
|
93
|
-
<strong>Run #${run.id}</strong>
|
|
94
|
-
<span class="text-dim">${escapeHtml(timeAgo)}</span>
|
|
95
|
-
<span>${run.passed}✓ ${run.failed}✗ ${run.skipped}○</span>
|
|
96
|
-
<span class="text-dim">${duration}</span>
|
|
97
|
-
${statusBadge(run.total, run.passed, run.failed)}
|
|
98
|
-
<span style="flex:1;"></span>
|
|
99
|
-
<a href="/api/export/${run.id}/junit" download class="btn btn-sm btn-outline">JUnit</a>
|
|
100
|
-
<a href="/api/export/${run.id}/json" download class="btn btn-sm btn-outline">JSON</a>
|
|
101
|
-
${failedFilterToggle()}
|
|
102
|
-
</div>`;
|
|
103
|
-
|
|
104
|
-
const suitesHtml = renderSuiteResults(results, runId);
|
|
105
|
-
|
|
106
|
-
return backButton + header + suitesHtml + autoExpandFailedScript();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function formatTimeAgo(isoDate: string): string {
|
|
110
|
-
try {
|
|
111
|
-
const date = new Date(isoDate);
|
|
112
|
-
const now = new Date();
|
|
113
|
-
const diffMs = now.getTime() - date.getTime();
|
|
114
|
-
const diffSec = Math.floor(diffMs / 1000);
|
|
115
|
-
if (diffSec < 60) return "just now";
|
|
116
|
-
const diffMin = Math.floor(diffSec / 60);
|
|
117
|
-
if (diffMin < 60) return `${diffMin}m ago`;
|
|
118
|
-
const diffHr = Math.floor(diffMin / 60);
|
|
119
|
-
if (diffHr < 24) return `${diffHr}h ago`;
|
|
120
|
-
const diffDay = Math.floor(diffHr / 24);
|
|
121
|
-
if (diffDay < 7) return `${diffDay}d ago`;
|
|
122
|
-
return date.toLocaleDateString();
|
|
123
|
-
} catch {
|
|
124
|
-
return isoDate;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Suites tab: all YAML files on disk with run status and step details.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { CollectionState, SuiteViewState, StepViewState } from "../data/collection-state.ts";
|
|
6
|
-
import { escapeHtml } from "./layout.ts";
|
|
7
|
-
import { methodBadge } from "./results.ts";
|
|
8
|
-
import { basename } from "node:path";
|
|
9
|
-
|
|
10
|
-
export function renderSuitesTab(state: CollectionState): string {
|
|
11
|
-
if (state.suites.length === 0) {
|
|
12
|
-
return `<div class="tab-empty">No test suites found on disk. Generate tests with <code>zond guide</code> or use the test-generation skill.</div>`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const rows = state.suites.map((s, i) => renderSuiteRow(s, i)).join("");
|
|
16
|
-
return `<div class="suite-list">${rows}</div>`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function renderSuiteRow(suite: SuiteViewState, index: number): string {
|
|
20
|
-
const detailId = `suite-detail-${index}`;
|
|
21
|
-
|
|
22
|
-
if (suite.status === "parse_error") {
|
|
23
|
-
return `
|
|
24
|
-
<div class="suite-row suite-error-row">
|
|
25
|
-
<div class="suite-info">
|
|
26
|
-
<div class="suite-name">${escapeHtml(basename(suite.filePath || suite.name))}</div>
|
|
27
|
-
<div class="suite-desc" style="color:var(--fail);">${escapeHtml(suite.parseError ?? "Parse error")}</div>
|
|
28
|
-
</div>
|
|
29
|
-
<div class="suite-tags"></div>
|
|
30
|
-
<div class="suite-steps-count">-</div>
|
|
31
|
-
<div class="suite-result fail">error</div>
|
|
32
|
-
</div>`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const tags = suite.tags.map(t => {
|
|
36
|
-
const tagClass = t === "smoke" ? "smoke" : t === "crud" ? "crud" : t === "auth" ? "auth" : t === "destructive" ? "destructive" : "";
|
|
37
|
-
return `<span class="tag-pill ${tagClass}">${escapeHtml(t)}</span>`;
|
|
38
|
-
}).join("");
|
|
39
|
-
|
|
40
|
-
const total = suite.runResult
|
|
41
|
-
? suite.runResult.passed + suite.runResult.failed + suite.runResult.skipped
|
|
42
|
-
: 0;
|
|
43
|
-
|
|
44
|
-
let resultHtml: string;
|
|
45
|
-
if (suite.status === "passed") {
|
|
46
|
-
resultHtml = `<div class="suite-result pass">${suite.runResult!.passed}/${total} ✓</div>`;
|
|
47
|
-
} else if (suite.status === "failed") {
|
|
48
|
-
resultHtml = `<div class="suite-result fail">${suite.runResult!.passed}/${total} ✗</div>`;
|
|
49
|
-
} else {
|
|
50
|
-
resultHtml = `<div class="suite-result not-run">not run</div>`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Step detail rows
|
|
54
|
-
const stepsHtml = suite.steps.length > 0
|
|
55
|
-
? suite.steps.map((step, si) => renderStepRow(step, index, si)).join("")
|
|
56
|
-
: `<div style="font-size:0.75rem;color:var(--text-dim);padding:0.5rem;">No run results yet</div>`;
|
|
57
|
-
|
|
58
|
-
return `
|
|
59
|
-
<div class="suite-row" data-suite-name="${escapeHtml(suite.name)}"
|
|
60
|
-
onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'">
|
|
61
|
-
<div class="suite-info">
|
|
62
|
-
<div class="suite-name">${escapeHtml(suite.name)}</div>
|
|
63
|
-
${suite.description ? `<div class="suite-desc">${escapeHtml(suite.description)}</div>` : ""}
|
|
64
|
-
</div>
|
|
65
|
-
<div class="suite-tags">${tags}</div>
|
|
66
|
-
<div class="suite-steps-count">${suite.stepCount} steps</div>
|
|
67
|
-
${resultHtml}
|
|
68
|
-
</div>
|
|
69
|
-
<div class="suite-detail" id="${detailId}" style="display:none">
|
|
70
|
-
${stepsHtml}
|
|
71
|
-
</div>`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function renderStepRow(step: StepViewState, suiteIdx: number, stepIdx: number): string {
|
|
75
|
-
const icon = step.status === "pass"
|
|
76
|
-
? '<span class="step-icon pass">✓</span>'
|
|
77
|
-
: step.status === "fail" || step.status === "error"
|
|
78
|
-
? '<span class="step-icon fail">✗</span>'
|
|
79
|
-
: '<span class="step-icon skip">○</span>';
|
|
80
|
-
|
|
81
|
-
const labelStyle = step.status === "fail" || step.status === "error"
|
|
82
|
-
? ' style="color:var(--fail);"'
|
|
83
|
-
: step.status === "skip"
|
|
84
|
-
? ' style="color:var(--skip);"'
|
|
85
|
-
: "";
|
|
86
|
-
|
|
87
|
-
const duration = step.durationMs != null
|
|
88
|
-
? `<span class="step-duration">${step.durationMs}ms</span>`
|
|
89
|
-
: `<span class="step-duration">-</span>`;
|
|
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
|
-
|
|
106
|
-
// Captures
|
|
107
|
-
const captureHtml = step.captures && Object.keys(step.captures).length > 0
|
|
108
|
-
? `<span class="step-captures">${Object.entries(step.captures).map(([k, v]) =>
|
|
109
|
-
`<span class="capture-pill">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`
|
|
110
|
-
).join("")}</span>`
|
|
111
|
-
: `<span class="step-captures"></span>`;
|
|
112
|
-
|
|
113
|
-
const detailId = `s-${suiteIdx}-step-${stepIdx}`;
|
|
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);
|
|
121
|
-
|
|
122
|
-
const clickHandler = hasDetail
|
|
123
|
-
? ` onclick="event.stopPropagation();var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'"`
|
|
124
|
-
: "";
|
|
125
|
-
|
|
126
|
-
let detailPanel = "";
|
|
127
|
-
if (hasDetail) {
|
|
128
|
-
let detailContent = "";
|
|
129
|
-
|
|
130
|
-
// Request info
|
|
131
|
-
if (step.requestMethod && step.requestUrl) {
|
|
132
|
-
detailContent += `<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-dim);margin-bottom:0.4rem;">
|
|
133
|
-
${escapeHtml(step.requestMethod)} ${escapeHtml(step.requestUrl)}</div>`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Assertions
|
|
137
|
-
if (step.assertions && step.assertions.length > 0) {
|
|
138
|
-
detailContent += step.assertions.map(a => {
|
|
139
|
-
const aIcon = a.passed
|
|
140
|
-
? '<span class="assertion-icon pass">✓</span>'
|
|
141
|
-
: '<span class="assertion-icon fail">✗</span>';
|
|
142
|
-
const actual = !a.passed && a.actual !== undefined
|
|
143
|
-
? ` <span class="assertion-actual">(got ${escapeHtml(JSON.stringify(a.actual))})</span>` : "";
|
|
144
|
-
return `<div class="assertion-row">${aIcon} <span class="assertion-field">${escapeHtml(a.field)}:</span> <span class="assertion-rule">${escapeHtml(a.rule)}</span>${actual}</div>`;
|
|
145
|
-
}).join("");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Error message
|
|
149
|
-
if (step.errorMessage) {
|
|
150
|
-
detailContent += `<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--fail);margin-top:0.25rem;">${escapeHtml(step.errorMessage)}</div>`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Failure hint
|
|
154
|
-
if (step.hint) {
|
|
155
|
-
detailContent += `<div class="failure-hint"><span>⚠</span> ${escapeHtml(step.hint)}</div>`;
|
|
156
|
-
}
|
|
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
|
-
|
|
165
|
-
// Response body toggle
|
|
166
|
-
if (step.responseBody) {
|
|
167
|
-
const truncated = step.responseBody.length > 2000 ? step.responseBody.slice(0, 2000) + "..." : step.responseBody;
|
|
168
|
-
detailContent += `<div class="req-res-toggle" onclick="event.stopPropagation();var b=this.nextElementSibling;b.style.display=b.style.display==='none'?'block':'none'">▼ Response Body</div>
|
|
169
|
-
<div class="req-res-body" style="display:none;"><pre style="font-size:0.7rem;margin:0.25rem 0;">${escapeHtml(truncated)}</pre></div>`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
detailPanel = `<div class="step-detail-panel" id="${detailId}" style="display:none">${detailContent}</div>`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return `<div class="step-row"${clickHandler}>
|
|
176
|
-
${icon}
|
|
177
|
-
<span class="step-label"${labelStyle}>${primaryLabel}${nameLabel}</span>
|
|
178
|
-
${captureHtml}
|
|
179
|
-
${duration}
|
|
180
|
-
</div>${detailPanel}`;
|
|
181
|
-
}
|