@loadmill/droid-cua 2.44.0 → 2.45.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/README.md +11 -3
- package/build/index.js +7 -2
- package/build/src/cli/headless-execution-config.js +9 -0
- package/build/src/cli/headless-report.js +40 -11
- package/build/src/cli/headless-runner.js +28 -14
- package/build/src/commands/help.js +1 -0
- package/build/src/commands/stop.js +1 -0
- package/build/src/core/capture-sources.js +4 -0
- package/build/src/core/droid-cua-run-usage.js +9 -2
- package/build/src/core/execution-context.js +8 -0
- package/build/src/core/report/render-html-report.js +299 -94
- package/build/src/core/report/run-log-model.js +84 -1
- package/build/src/core/report/run-report-model.js +4 -1
- package/build/src/core/report/run-report-sections.js +10 -0
- package/build/src/core/runtime-config.js +11 -1
- package/build/src/core/step-watchdog.js +61 -0
- package/build/src/modes/execution-mode.js +157 -3
- package/build/src/prompts/step-watchdog.js +22 -0
- package/build/src/utils/desktop-debug.js +2 -2
- package/package.json +1 -1
- package/skill/droid-cua/SKILL.md +1 -1
- package/skill/droid-cua/references/onboarding-and-troubleshooting.md +4 -4
- package/skill/droid-cua/references/running-and-healing.md +16 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// HTML document that reproduces the Desktop App's run-details view. Pure: no I/O and
|
|
3
3
|
// no external references — screenshots must already be inlined as data URLs in
|
|
4
4
|
// `event.payload.screenshotDataUrl`. Use generate-html-report.js to inline them.
|
|
5
|
-
import { assertionData, networkCardData,
|
|
6
|
-
import { buildRunReportSections, capitalizeReportLine, cloudProviderId, cloudProviderLabel, cloudStatus, } from "./run-report-sections.js";
|
|
5
|
+
import { assertionData, isRepeatedRunError, stepFailureData, isRepeatedStepFailureMessage, networkCardData, isNetworkCaptureNotice, runNotice, hasNearbyAssertionCard, isAssistantAssertionSummary, isDuplicateAssertionLine, isHiddenExecutionLogEvent, runSummaryData, runSummaryCounts, stepCompletionData, stepCompletionReasonTooltip, } from "./run-log-model.js";
|
|
6
|
+
import { buildRunReportSections, instructionActivities, capitalizeReportLine, cloudProviderId, cloudProviderLabel, cloudStatus, } from "./run-report-sections.js";
|
|
7
7
|
import { LOADMILL_MARK_PATHS } from "../../brand/loadmill-mark.js";
|
|
8
8
|
// ---------- text / value formatting (mirrors the Desktop App) ----------
|
|
9
9
|
function escapeHtml(value) {
|
|
@@ -36,14 +36,14 @@ function formatRunTimestamp(iso) {
|
|
|
36
36
|
const parsed = Date.parse(iso);
|
|
37
37
|
if (!Number.isFinite(parsed))
|
|
38
38
|
return "—";
|
|
39
|
-
return new Date(parsed).toLocaleString("en-US");
|
|
39
|
+
return new Date(parsed).toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" });
|
|
40
40
|
}
|
|
41
41
|
function formatRunDuration(durationSeconds) {
|
|
42
42
|
if (durationSeconds === null || durationSeconds === undefined || !Number.isFinite(durationSeconds)) {
|
|
43
43
|
return "—";
|
|
44
44
|
}
|
|
45
45
|
if (durationSeconds < 60)
|
|
46
|
-
return `${
|
|
46
|
+
return `${Math.round(durationSeconds)}s`;
|
|
47
47
|
const wholeSeconds = Math.floor(durationSeconds);
|
|
48
48
|
const minutes = Math.floor(wholeSeconds / 60);
|
|
49
49
|
const seconds = wholeSeconds % 60;
|
|
@@ -56,9 +56,9 @@ function formatRunDuration(durationSeconds) {
|
|
|
56
56
|
function runStatusLabel(status) {
|
|
57
57
|
switch (status) {
|
|
58
58
|
case "pass":
|
|
59
|
-
return "
|
|
59
|
+
return "Passed";
|
|
60
60
|
case "fail":
|
|
61
|
-
return "
|
|
61
|
+
return "Failed";
|
|
62
62
|
case "error":
|
|
63
63
|
return "Error";
|
|
64
64
|
case "stopped":
|
|
@@ -80,11 +80,20 @@ function runStatusLabel(status) {
|
|
|
80
80
|
// assertion lines (the same assertion is logged several times).
|
|
81
81
|
function buildVisibleEvents(events) {
|
|
82
82
|
const base = (events ?? []).filter((event) => typeof event.text === "string" && event.text.trim().length > 0);
|
|
83
|
+
const hasSummary = base.some((event) => runSummaryData(event));
|
|
83
84
|
const filtered = base.filter((event, idx, items) => {
|
|
85
|
+
if (hasSummary && event.eventType !== "run_finished" && ["system", "info", "success"].includes(event.kind) && /^(Test completed|Duration:|Steps:|Assertions:|Retries:)/.test(event.text.trimStart()))
|
|
86
|
+
return false;
|
|
87
|
+
if (["system", "info"].includes(event.kind) && /^(Loading test:|Loaded \d+ instructions|Connecting to )/.test(event.text.trimStart()))
|
|
88
|
+
return false;
|
|
84
89
|
if (isHiddenExecutionLogEvent(event))
|
|
85
90
|
return false;
|
|
91
|
+
if (event.kind !== "user" && event.eventType !== "instruction_started" && event.text.startsWith("Running Loadmill flow: "))
|
|
92
|
+
return false;
|
|
86
93
|
if (isAssistantAssertionSummary(event) && hasNearbyAssertionCard(items, idx))
|
|
87
94
|
return false;
|
|
95
|
+
if (isRepeatedStepFailureMessage(items, idx))
|
|
96
|
+
return false;
|
|
88
97
|
if (event.eventType === "input_request")
|
|
89
98
|
return false;
|
|
90
99
|
return true;
|
|
@@ -108,7 +117,7 @@ function screenshotImg(event, shots) {
|
|
|
108
117
|
return "";
|
|
109
118
|
const index = shots.length + 1;
|
|
110
119
|
shots.push({ index, url, event });
|
|
111
|
-
return `<a class="dc-shot-link" href="#dc-shot-${index}" aria-label="Open screenshot ${index}"><img class="dc-shot" src="${escapeHtml(url)}" alt="Screenshot ${index}" /></a>`;
|
|
120
|
+
return `<a class="dc-shot-link"${url.startsWith("data:") ? "" : ' data-remote-image'} href="#dc-shot-${index}" aria-label="Open screenshot ${index}"><img class="dc-shot" src="${escapeHtml(url)}" alt="Screenshot ${index}" /></a>`;
|
|
112
121
|
}
|
|
113
122
|
function icon(name, shapes) {
|
|
114
123
|
return `<svg class="dc-icon" data-icon="${name}" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${shapes}</svg>`;
|
|
@@ -118,6 +127,18 @@ function loadmillIcon() {
|
|
|
118
127
|
return `<svg class="dc-icon" data-icon="loadmill-mark" aria-hidden="true" viewBox="0 0 1035 1035">${paths}</svg>`;
|
|
119
128
|
}
|
|
120
129
|
const ICONS = {
|
|
130
|
+
assertion: icon("shield-check", `<path d="M12 22s8-4 8-11V5l-8-3-8 3v6c0 7 8 11 8 11Z"/><path d="m9 12 2 2 4-4"/>`),
|
|
131
|
+
triangleAlert: icon("triangle-alert", `<path d="m10.3 3.9-8.1 14a2 2 0 0 0 1.7 3h16.2a2 2 0 0 0 1.7-3l-8.1-14a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4m0 4h.01"/>`),
|
|
132
|
+
check: icon("check", `<path d="m5 12 4 4L19 6"/>`),
|
|
133
|
+
alert: icon("alert", `<circle cx="12" cy="12" r="9"/><path d="M12 8v4m0 4h.01"/>`),
|
|
134
|
+
clipboard: icon("clipboard", `<rect x="5" y="4" width="14" height="18" rx="2"/><rect x="9" y="2" width="6" height="4" rx="1"/><path d="m9 13 2 2 4-4"/>`),
|
|
135
|
+
calendar: icon("calendar", `<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 11h18"/>`),
|
|
136
|
+
clock: icon("clock", `<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>`),
|
|
137
|
+
folder: icon("folder", `<path d="M3 7V5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>`),
|
|
138
|
+
device: icon("device", `<rect x="2" y="3" width="15" height="12" rx="2"/><path d="M7 19h5m-3-4v4"/><rect x="17" y="11" width="5" height="10" rx="1"/>`),
|
|
139
|
+
chevron: icon("chevron", `<path d="m9 6 6 6-6 6"/>`),
|
|
140
|
+
info: icon("info", `<circle cx="12" cy="12" r="9"/><path d="M12 11v6m0-10h.01"/>`),
|
|
141
|
+
expand: icon("expand", `<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>`),
|
|
121
142
|
network: icon("network", `<rect x="9" y="2" width="6" height="6" rx="1"/><path d="M12 8v4M5 16v-4h14v4"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="16" y="16" width="6" height="6" rx="1"/>`),
|
|
122
143
|
arrowUpDown: icon("arrow-up-down", `<path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/>`),
|
|
123
144
|
bookOpen: icon("book-open", `<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>`),
|
|
@@ -167,33 +188,19 @@ function textIcon(value) {
|
|
|
167
188
|
return `<span class="dc-icon" aria-hidden="true">${value}</span>`;
|
|
168
189
|
}
|
|
169
190
|
// ---------- per-event timeline rendering (mirrors ExecutionLogItem.tsx) ----------
|
|
170
|
-
function renderEvent(event, shots) {
|
|
191
|
+
function renderEvent(event, shots, events) {
|
|
171
192
|
if (isHiddenExecutionLogEvent(event))
|
|
172
193
|
return "";
|
|
194
|
+
if (event.eventType === "tool_call" && event.text.startsWith("Validating assertion: "))
|
|
195
|
+
return "";
|
|
196
|
+
const notice = runNotice(event);
|
|
197
|
+
if (notice)
|
|
198
|
+
return `<div role="status" class="dc-notice${notice.warning ? " dc-notice-warning" : ""}">${notice.warning ? ICONS.triangleAlert : ICONS.network}<div><p>${escapeHtml(notice.message)}</p>${notice.details ? `<details><summary>Error details</summary><pre>${escapeHtml(notice.details)}</pre></details>` : ""}</div></div>`;
|
|
199
|
+
if (isNetworkCaptureNotice(event))
|
|
200
|
+
return "";
|
|
173
201
|
const summary = runSummaryData(event);
|
|
174
202
|
if (summary) {
|
|
175
|
-
|
|
176
|
-
const label = summary.status === "passed"
|
|
177
|
-
? "Test Passed"
|
|
178
|
-
: summary.status === "stopped"
|
|
179
|
-
? "Test Stopped"
|
|
180
|
-
: summary.status === "errored"
|
|
181
|
-
? "Error"
|
|
182
|
-
: "Test Failed";
|
|
183
|
-
const badge = summary.status === "passed" ? "PASS" : summary.status === "stopped" ? "STOPPED" : summary.status === "errored" ? "ERROR" : "FAIL";
|
|
184
|
-
return `<div class="dc-card dc-${cls}">
|
|
185
|
-
<div class="dc-card-head"><span class="dc-card-title">${textIcon(summary.status === "passed" ? "✓" : "!")}${escapeHtml(label)}</span><span class="dc-pill dc-pill-${cls}">${escapeHtml(badge)}</span></div>
|
|
186
|
-
<div class="dc-card-body">
|
|
187
|
-
<div>Duration: ${escapeHtml(formatDuration(summary.durationMs))}</div>
|
|
188
|
-
<div>Steps: ${summary.instructionsCompleted ?? 0}/${summary.instructionsTotal ?? 0}</div>
|
|
189
|
-
<div>Assertions: ${summary.assertionsPassed ?? 0}/${(summary.assertionsPassed ?? 0) + (summary.assertionsFailed ?? 0)}</div>
|
|
190
|
-
<div>Retries: ${summary.retries ?? 0}</div>
|
|
191
|
-
</div>${summary.error ? `\n <div class="dc-card-error">${renderLinkedText(summary.error)}</div>` : ""}
|
|
192
|
-
</div>`;
|
|
193
|
-
}
|
|
194
|
-
const isInstruction = event.eventType === "instruction_started" || event.kind === "user";
|
|
195
|
-
if (isInstruction) {
|
|
196
|
-
return `<div class="dc-row dc-row-right"><div class="dc-bubble">${escapeHtml(event.text)}</div></div>`;
|
|
203
|
+
return summary.status !== "stopped" && summary.error && !isRepeatedRunError(summary.error, events) ? `<div class="dc-line dc-error">${renderLinkedText(summary.error)}</div>` : "";
|
|
197
204
|
}
|
|
198
205
|
const isAssistant = event.eventType === "assistant_message" || event.kind === "assistant";
|
|
199
206
|
if (isAssistant) {
|
|
@@ -210,13 +217,14 @@ function renderEvent(event, shots) {
|
|
|
210
217
|
if (event.eventType === "reasoning" || event.kind === "reasoning") {
|
|
211
218
|
return renderAgentText(event.text);
|
|
212
219
|
}
|
|
213
|
-
const
|
|
220
|
+
const stepFailure = stepFailureData(event);
|
|
221
|
+
const assertion = assertionData(event) || stepFailure;
|
|
214
222
|
if (assertion) {
|
|
215
223
|
const cls = assertion.passed ? "pass" : "fail";
|
|
216
224
|
const img = screenshotImg(event, shots);
|
|
217
|
-
return `<div class="dc-card dc-${cls}">
|
|
218
|
-
<div class="dc-card-head"><span class="dc-card-title">${
|
|
219
|
-
<div class="dc-assertion"
|
|
225
|
+
return `<div${stepFailure ? ' role="alert"' : ""} class="dc-card dc-${cls}">
|
|
226
|
+
<div class="dc-card-head"><span class="dc-card-title">${assertion.passed ? ICONS.check : ICONS.triangleAlert}${stepFailure?.title || (assertion.passed ? "Assertion Passed" : "Assertion Failed")}${img}</span></div>
|
|
227
|
+
<div class="dc-assertion-fields"><p><span class="dc-expected-label">Expected:</span> ${escapeHtml(assertion.assertion)}</p>${assertion.details ? renderAgentText(assertion.details) : ""}</div>
|
|
220
228
|
</div>`;
|
|
221
229
|
}
|
|
222
230
|
if (event.eventType === "retry") {
|
|
@@ -225,9 +233,6 @@ function renderEvent(event, shots) {
|
|
|
225
233
|
if (event.kind === "action") {
|
|
226
234
|
return `<div class="dc-line dc-action">${actionIcon(event)}<span>${escapeHtml(event.text)}</span>${screenshotImg(event, shots)}</div>`;
|
|
227
235
|
}
|
|
228
|
-
if (isNetworkCaptureStarted(event)) {
|
|
229
|
-
return `<div class="dc-line">${ICONS.network}<span>Network capture started</span></div>`;
|
|
230
|
-
}
|
|
231
236
|
if (event.eventType === "screenshot_captured") {
|
|
232
237
|
return `<div class="dc-line">${ICONS.camera}<span>${escapeHtml(event.text)}</span>${screenshotImg(event, shots)}</div>`;
|
|
233
238
|
}
|
|
@@ -245,14 +250,14 @@ function renderAppContextCard(event) {
|
|
|
245
250
|
<pre>${escapeHtml(text)}</pre>
|
|
246
251
|
</details>`;
|
|
247
252
|
}
|
|
248
|
-
function renderCloudCard(events) {
|
|
253
|
+
function renderCloudCard(events, nested = false) {
|
|
249
254
|
const label = cloudProviderLabel(events.map(cloudProviderId).find(Boolean));
|
|
250
255
|
const status = cloudStatus(events);
|
|
251
256
|
const lines = events
|
|
252
257
|
.filter((event) => event.payload?.event !== "cloud.app.check" && event.payload?.event !== "cloud.app.reupload_needed")
|
|
253
258
|
.map((event) => capitalizeReportLine(event.text))
|
|
254
259
|
.filter((text, index, all) => text !== all[index - 1]);
|
|
255
|
-
return `<details class="dc-special dc-cloud"${status === "issue" || status === "starting" ? " open" : ""}>
|
|
260
|
+
return `<details class="dc-special dc-cloud"${nested || status === "issue" || status === "starting" ? " open" : ""}>
|
|
256
261
|
<summary><span class="dc-special-title">${ICONS.cloud}${escapeHtml(label)}</span><span class="dc-pill dc-pill-${status}">${status}</span></summary>
|
|
257
262
|
<div class="dc-special-body">${lines.map((line) => `<div>${renderLinkedText(line)}</div>`).join("")}</div>
|
|
258
263
|
</details>`;
|
|
@@ -286,17 +291,17 @@ function renderLoadmillCard(events) {
|
|
|
286
291
|
const failed = events.some((event) => /\[Loadmill\]\s*FAILED|^Error:/i.test(event.text));
|
|
287
292
|
const passed = !failed && events.some((event) => /\bpassed\b/i.test(event.text));
|
|
288
293
|
const status = failed ? "failed" : passed ? "passed" : "running";
|
|
289
|
-
return `<details class="dc-special dc-loadmill
|
|
290
|
-
<summary><span class="dc-special-title">${ICONS.loadmill}Loadmill Flow</span
|
|
294
|
+
return `<details class="dc-special dc-loadmill dc-loadmill-${status}" open>
|
|
295
|
+
<summary aria-label="Loadmill Flow: ${status}"><span class="dc-special-title">${ICONS.loadmill}Loadmill Flow</span></summary>
|
|
291
296
|
<div class="dc-special-body">${renderedLines.join("")}</div>
|
|
292
297
|
</details>`;
|
|
293
298
|
}
|
|
294
|
-
function renderNetworkCard(lines) {
|
|
299
|
+
function renderNetworkCard(lines, nested = false) {
|
|
295
300
|
const { outcome, question, filters, answer } = networkCardData(lines);
|
|
296
301
|
const resultHtml = answer.structured
|
|
297
302
|
? `<details class="dc-network-data"><summary>Response data</summary><pre>${escapeHtml(answer.text)}</pre></details>`
|
|
298
303
|
: `<div class="dc-network-answer">${renderLinkedText(answer.text)}</div>`;
|
|
299
|
-
return `<details class="dc-special dc-network">
|
|
304
|
+
return `<details class="dc-special dc-network"${nested ? " open" : ""}>
|
|
300
305
|
<summary><span class="dc-special-title">${ICONS.network}Network query</span><span>${escapeHtml(outcome)}</span></summary>
|
|
301
306
|
<div class="dc-special-body">
|
|
302
307
|
<dl class="dc-network-filters">${filters.map(({ label, value }) => `<dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd>`).join("")}</dl>
|
|
@@ -306,20 +311,55 @@ function renderNetworkCard(lines) {
|
|
|
306
311
|
</details>`;
|
|
307
312
|
}
|
|
308
313
|
function renderTimeline(events, shots, warnings = []) {
|
|
309
|
-
const
|
|
310
|
-
|
|
314
|
+
const stopped = events.some((event) => runSummaryData(event)?.status === "stopped");
|
|
315
|
+
const visible = buildVisibleEvents(events).filter((event) => !stopped || event.eventType === "run_finished" || !/^(Stopping test execution|Stopped by user)/.test(event.text.trim()));
|
|
316
|
+
const renderItems = (items, nested) => buildRunReportSections(items).map((item) => {
|
|
311
317
|
if (item.kind === "app-context")
|
|
312
318
|
return renderAppContextCard(item.line);
|
|
313
319
|
if (item.kind === "cloud")
|
|
314
|
-
return renderCloudCard(item.lines);
|
|
320
|
+
return renderCloudCard(item.lines, nested);
|
|
315
321
|
if (item.kind === "network")
|
|
316
|
-
return renderNetworkCard(item.lines);
|
|
322
|
+
return renderNetworkCard(item.lines, nested);
|
|
317
323
|
if (item.kind === "loadmill")
|
|
318
324
|
return renderLoadmillCard(item.lines);
|
|
319
|
-
return renderEvent(item.line, shots);
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
|
|
325
|
+
return renderEvent(item.line, shots, visible);
|
|
326
|
+
}).filter(Boolean).join("\n");
|
|
327
|
+
const groups = [];
|
|
328
|
+
let current = [];
|
|
329
|
+
let instruction = null;
|
|
330
|
+
const flush = () => { if (instruction || current.length)
|
|
331
|
+
groups.push({ instruction, items: current }); current = []; };
|
|
332
|
+
for (const event of visible) {
|
|
333
|
+
if (event.eventType === "instruction_started" || event.kind === "user" || event.eventType === "run_finished") {
|
|
334
|
+
flush();
|
|
335
|
+
instruction = event.eventType === "run_finished" ? null : event;
|
|
336
|
+
if (instruction)
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
current.push(event);
|
|
340
|
+
}
|
|
341
|
+
flush();
|
|
342
|
+
const result = visible.map(runSummaryData).find(Boolean);
|
|
343
|
+
const outcome = result?.status;
|
|
344
|
+
const passed = outcome === "passed";
|
|
345
|
+
const instructions = groups.map((group) => group.instruction).filter(Boolean);
|
|
346
|
+
const lastInstruction = instructions.at(-1);
|
|
347
|
+
const html = groups.map(({ instruction, items }) => {
|
|
348
|
+
const content = renderItems(items, Boolean(instruction));
|
|
349
|
+
if (!instruction)
|
|
350
|
+
return content;
|
|
351
|
+
const failed = items.some((event) => event.kind === "error" || assertionData(event)?.passed === false) || (instruction === lastInstruction && (outcome === "failed" || outcome === "errored"));
|
|
352
|
+
const confirmedByCount = (instruction.instructionIndex ?? instructions.indexOf(instruction)) < (result?.instructionsCompleted ?? 0);
|
|
353
|
+
const complete = !failed && (instruction !== lastInstruction || confirmedByCount || passed || items.some((event) => stepCompletionData(event)));
|
|
354
|
+
const interrupted = stopped && !complete && !failed && instruction === lastInstruction;
|
|
355
|
+
const status = failed ? "Needs attention" : complete ? "Completed" : interrupted ? "Stopped" : "Not confirmed";
|
|
356
|
+
const end = items.find((event) => stepCompletionData(event)) ?? instructions[instructions.indexOf(instruction) + 1] ?? visible.find((event) => event.eventType === "run_finished") ?? items[items.length - 1];
|
|
357
|
+
const seconds = (Date.parse(end?.ts) - Date.parse(instruction.ts)) / 1000;
|
|
358
|
+
const duration = Number.isFinite(seconds) && seconds >= 0 ? formatRunDuration(seconds) : "";
|
|
359
|
+
const activities = instructionActivities(items);
|
|
360
|
+
const activityIcons = activities.length ? `<span class="dc-instruction-activities">${activities.map(({ type, label }) => `<span class="dc-activity dc-activity-${type}" title="${label}" aria-label="${label}">${ICONS[type]}</span>`).join("")}</span>` : "";
|
|
361
|
+
return `<details class="dc-instruction"${(failed && !passed) || interrupted ? " open" : ""}><summary><span class="dc-instruction-status ${failed ? "dc-error" : complete ? "dc-complete" : interrupted ? "dc-stopped" : ""}" aria-label="${status}">${failed || interrupted ? ICONS.alert : complete ? ICONS.check : "○"}</span><span class="dc-instruction-label">${escapeHtml(instruction.text)}<span class="dc-instruction-duration">${interrupted ? "Stopped after " : complete ? "Completed in " : "Elapsed "}${escapeHtml(duration)}${activityIcons}<span class="dc-instruction-chevron">${ICONS.chevron}</span></span></span><time class="dc-instruction-time">${escapeHtml(Number.isFinite(Date.parse(instruction.ts)) ? new Date(instruction.ts).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }) : "")}</time></summary><div class="dc-instruction-actions">${content || '<div class="dc-empty">No recorded actions</div>'}</div></details>`;
|
|
362
|
+
}).join("\n");
|
|
323
363
|
if (html)
|
|
324
364
|
return html;
|
|
325
365
|
const message = warnings.find((warning) => warning.toLowerCase().includes("event log not available")) ?? "No event timeline available for this report.";
|
|
@@ -339,7 +379,7 @@ function renderLightboxes(shots) {
|
|
|
339
379
|
const next = shot.index < count
|
|
340
380
|
? `<a class="dc-lightbox-nav dc-lightbox-next" href="#dc-shot-${shot.index + 1}" aria-label="Next screenshot">›</a>`
|
|
341
381
|
: "";
|
|
342
|
-
return `<div class="dc-lightbox" id="dc-shot-${shot.index}">
|
|
382
|
+
return `<div class="dc-lightbox" id="dc-shot-${shot.index}"${shot.url.startsWith("data:") ? "" : ' data-remote-image'}>
|
|
343
383
|
<a class="dc-lightbox-backdrop" href="#dc-top" aria-label="Close"></a>
|
|
344
384
|
${prev}
|
|
345
385
|
<img class="dc-lightbox-img" src="${escapeHtml(shot.url)}" alt="Screenshot ${shot.index}" />
|
|
@@ -349,21 +389,102 @@ function renderLightboxes(shots) {
|
|
|
349
389
|
})
|
|
350
390
|
.join("\n");
|
|
351
391
|
}
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
function
|
|
355
|
-
if (count === 0)
|
|
356
|
-
return "";
|
|
392
|
+
// Enhance native details and screenshot links for downloads. Hosted reports omit
|
|
393
|
+
// this script and retain inline details and CSS lightboxes without JavaScript.
|
|
394
|
+
function renderInteractionScript(count) {
|
|
357
395
|
return `<script>
|
|
358
396
|
(function () {
|
|
397
|
+
document.querySelectorAll('.dc-run-details, .dc-context').forEach(function(details, index) {
|
|
398
|
+
var isContext = details.classList.contains('dc-context');
|
|
399
|
+
var title = isContext ? 'App context' : 'Run details';
|
|
400
|
+
var trigger = document.createElement('button');
|
|
401
|
+
trigger.type = 'button';
|
|
402
|
+
trigger.className = 'dc-dialog-trigger';
|
|
403
|
+
trigger.innerHTML = details.querySelector('summary').innerHTML;
|
|
404
|
+
trigger.setAttribute('aria-haspopup', 'dialog');
|
|
405
|
+
var dialog = document.createElement('dialog');
|
|
406
|
+
dialog.className = 'dc-dialog';
|
|
407
|
+
dialog.setAttribute('aria-labelledby', 'dc-dialog-title-' + index);
|
|
408
|
+
var header = document.createElement('div');
|
|
409
|
+
header.className = 'dc-dialog-header';
|
|
410
|
+
var heading = document.createElement('h2');
|
|
411
|
+
heading.id = 'dc-dialog-title-' + index;
|
|
412
|
+
heading.textContent = title;
|
|
413
|
+
var close = document.createElement('button');
|
|
414
|
+
close.type = 'button';
|
|
415
|
+
close.textContent = '×';
|
|
416
|
+
close.setAttribute('aria-label', 'Close ' + title.toLowerCase());
|
|
417
|
+
header.append(heading, close);
|
|
418
|
+
dialog.append(header);
|
|
419
|
+
Array.from(details.children).forEach(function(child) {
|
|
420
|
+
if (child.tagName !== 'SUMMARY') dialog.append(child);
|
|
421
|
+
});
|
|
422
|
+
var childReport = details.closest('.dc-child-block');
|
|
423
|
+
if (isContext) {
|
|
424
|
+
var destination = childReport ? childReport.querySelector('.dc-child-head') : document.querySelector('.dc-summary');
|
|
425
|
+
if (childReport) destination.append(trigger);
|
|
426
|
+
else {
|
|
427
|
+
trigger.style.marginLeft = 'auto';
|
|
428
|
+
var runDetails = destination.querySelector('.dc-run-details');
|
|
429
|
+
if (runDetails) runDetails.style.marginLeft = '0';
|
|
430
|
+
destination.insertBefore(trigger, runDetails);
|
|
431
|
+
}
|
|
432
|
+
details.remove();
|
|
433
|
+
} else {
|
|
434
|
+
trigger.classList.add('dc-run-details');
|
|
435
|
+
details.replaceWith(trigger);
|
|
436
|
+
}
|
|
437
|
+
document.body.append(dialog);
|
|
438
|
+
trigger.addEventListener('click', function() { dialog.showModal(); });
|
|
439
|
+
close.addEventListener('click', function() { dialog.close(); });
|
|
440
|
+
dialog.addEventListener('click', function(event) {
|
|
441
|
+
var rect = dialog.getBoundingClientRect();
|
|
442
|
+
if (event.target === dialog && (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)) dialog.close();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
359
445
|
var count = ${count};
|
|
446
|
+
var panelIndex = count;
|
|
447
|
+
var panel = document.getElementById('dc-panel-image');
|
|
448
|
+
function select(index) {
|
|
449
|
+
panelIndex = Math.max(1, Math.min(count, index));
|
|
450
|
+
var source = document.querySelector('#dc-shot-' + panelIndex + ' img');
|
|
451
|
+
panel.src = source.src;
|
|
452
|
+
document.getElementById('dc-panel-open').href = '#dc-shot-' + panelIndex;
|
|
453
|
+
document.getElementById('dc-panel-expand').href = '#dc-shot-' + panelIndex;
|
|
454
|
+
document.getElementById('dc-panel-position').value = panelIndex;
|
|
455
|
+
document.getElementById('dc-panel-count').textContent = panelIndex + ' / ' + count;
|
|
456
|
+
document.querySelectorAll('[data-shot]').forEach(function(button) {
|
|
457
|
+
button.disabled = ['first', 'previous'].includes(button.dataset.shot) ? panelIndex === 1 : panelIndex === count;
|
|
458
|
+
});
|
|
459
|
+
var thumb = document.querySelector('.dc-shot-link[href="#dc-shot-' + panelIndex + '"]');
|
|
460
|
+
var group = thumb && thumb.closest('.dc-instruction');
|
|
461
|
+
document.getElementById('dc-panel-caption').textContent = group ? group.querySelector('summary > span:nth-child(2)').textContent : 'Screenshot ' + panelIndex;
|
|
462
|
+
}
|
|
463
|
+
if (panel) {
|
|
464
|
+
document.querySelectorAll('.dc-shot-link').forEach(function(thumb) {
|
|
465
|
+
thumb.addEventListener('click', function(event) {
|
|
466
|
+
event.preventDefault();
|
|
467
|
+
select(Number(thumb.getAttribute('href').replace('#dc-shot-', '')));
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
select(count);
|
|
471
|
+
document.getElementById('dc-panel-position').addEventListener('input', function(event) { select(Number(event.target.value)); });
|
|
472
|
+
document.querySelectorAll('[data-shot]').forEach(function(button) {
|
|
473
|
+
button.addEventListener('click', function() { select(button.dataset.shot === 'first' ? 1 : button.dataset.shot === 'last' ? count : panelIndex + (button.dataset.shot === 'next' ? 1 : -1)); });
|
|
474
|
+
});
|
|
475
|
+
}
|
|
360
476
|
function current() {
|
|
361
477
|
var match = /^#dc-shot-(\\d+)$/.exec(location.hash);
|
|
362
478
|
return match ? parseInt(match[1], 10) : 0;
|
|
363
479
|
}
|
|
364
480
|
document.addEventListener('keydown', function (event) {
|
|
481
|
+
if (document.querySelector('dialog[open]')) return;
|
|
365
482
|
var n = current();
|
|
366
|
-
if (
|
|
483
|
+
if (event.target.closest('input, textarea, select, [contenteditable="true"]')) return;
|
|
484
|
+
if (!n) {
|
|
485
|
+
if (panel && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) { event.preventDefault(); select(panelIndex + (event.key === 'ArrowRight' ? 1 : -1)); }
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
367
488
|
if (event.key === 'Escape') { event.preventDefault(); location.hash = 'dc-top'; }
|
|
368
489
|
else if ((event.key === 'ArrowRight' || event.key === 'ArrowDown') && n < count) { event.preventDefault(); location.hash = 'dc-shot-' + (n + 1); }
|
|
369
490
|
else if ((event.key === 'ArrowLeft' || event.key === 'ArrowUp') && n > 1) { event.preventDefault(); location.hash = 'dc-shot-' + (n - 1); }
|
|
@@ -371,24 +492,36 @@ function renderLightboxScript(count) {
|
|
|
371
492
|
})();
|
|
372
493
|
</script>`;
|
|
373
494
|
}
|
|
495
|
+
function renderScreenshotPanel(shots) {
|
|
496
|
+
if (!shots.length)
|
|
497
|
+
return '<aside class="dc-screenshots"><div class="dc-empty">No screenshots in this report</div></aside>';
|
|
498
|
+
return `<aside class="dc-screenshots" aria-label="Device screenshots"><div class="dc-panel-toolbar"><a id="dc-panel-expand" href="#dc-shot-${shots.length}" aria-label="Expand screenshot">${ICONS.expand}</a></div><a id="dc-panel-open" href="#dc-shot-${shots.length}"><img id="dc-panel-image" alt="Device screenshot" /></a><p id="dc-panel-caption"></p><input id="dc-panel-position" type="range" min="1" max="${shots.length}" value="${shots.length}" aria-label="Screenshot position" /><nav><button data-shot="first" aria-label="First screenshot">${ICONS.chevron}${ICONS.chevron}</button><button data-shot="previous" aria-label="Previous screenshot">${ICONS.chevron}</button><span id="dc-panel-count"></span><button data-shot="next" aria-label="Next screenshot">${ICONS.chevron}</button><button data-shot="last" aria-label="Last screenshot">${ICONS.chevron}${ICONS.chevron}</button></nav><noscript><a href="#dc-shot-1">Browse screenshots</a></noscript></aside>`;
|
|
499
|
+
}
|
|
374
500
|
// ---------- summary card ----------
|
|
375
501
|
function summaryRow(label, value) {
|
|
376
502
|
return `<div class="dc-sum-cell"><div class="dc-sum-label">${escapeHtml(label)}</div><div class="dc-sum-value">${value}</div></div>`;
|
|
377
503
|
}
|
|
378
|
-
function renderSummaryCard(model) {
|
|
379
|
-
const
|
|
504
|
+
function renderSummaryCard(model, showRunDetails) {
|
|
505
|
+
const recorded = (key) => (model.events ?? []).map((event) => event.payload?.[key]).find((value) => ["string", "number", "boolean"].includes(typeof value));
|
|
506
|
+
const result = runSummaryCounts(model.childTests?.length ? model.childTests.flatMap((child) => child.events ?? []) : model.events ?? []);
|
|
507
|
+
const device = recorded("deviceName") ?? "Device not recorded";
|
|
380
508
|
return `<div class="dc-summary">
|
|
381
|
-
${
|
|
382
|
-
${
|
|
383
|
-
${
|
|
384
|
-
${
|
|
385
|
-
${
|
|
386
|
-
${
|
|
509
|
+
<span title="Launched On">${ICONS.calendar}${escapeHtml(formatRunTimestamp(model.timestamp))}</span>
|
|
510
|
+
<span title="Duration">${ICONS.clock}${escapeHtml(formatRunDuration(model.durationSeconds))}</span>
|
|
511
|
+
<span title="Project">${ICONS.folder}${escapeHtml(model.suiteName ?? "—")}</span>
|
|
512
|
+
<span title="Recorded device">${ICONS.device}${escapeHtml(device)}</span>
|
|
513
|
+
${(model.events ?? []).some(isNetworkCaptureNotice) ? `<span title="Network capture enabled for this run">${ICONS.network}Network capture</span>` : ""}
|
|
514
|
+
${showRunDetails ? `<details class="dc-run-details"><summary>${ICONS.info}Run details</summary><div class="dc-run-settings">
|
|
515
|
+
${summaryRow("Model", escapeHtml(recorded("model") ?? recorded("cuaModel") ?? "Not recorded"))}
|
|
516
|
+
${summaryRow("Platform", escapeHtml(recorded("platform") ?? "Not recorded"))}
|
|
517
|
+
${result ? summaryRow("Steps", `${result.instructionsCompleted ?? 0}/${result.instructionsTotal ?? 0}`) + summaryRow("Assertions", `${result.assertionsPassed ?? 0}/${(result.assertionsPassed ?? 0) + (result.assertionsFailed ?? 0)} passed`) + summaryRow("Retries", String(result.retries ?? 0)) : ""}
|
|
518
|
+
${model.childTests?.length || model.testCases > 1 ? summaryRow("Tests passed", `${Math.round(model.successRate * model.testCases / 100)}/${model.testCases}`) : ""}
|
|
519
|
+
</div></details>` : ""}
|
|
387
520
|
</div>`;
|
|
388
521
|
}
|
|
389
522
|
function renderTestsList(childTests) {
|
|
390
523
|
const rows = childTests
|
|
391
|
-
.map((child) => `<
|
|
524
|
+
.map((child, index) => `<a href="#dc-test-${index}" class="dc-test-row"><span class="dc-test-name">${escapeHtml(child.testName.replace(/\.dcua$/i, ""))}</span><span class="dc-test-dur">${escapeHtml(formatRunDuration(child.durationSeconds))}</span><span class="dc-pill dc-pill-${escapeHtml(child.status)}">${escapeHtml(runStatusLabel(child.status))}</span></a>`)
|
|
392
525
|
.join("\n");
|
|
393
526
|
return `<div class="dc-tests"><div class="dc-tests-title">Tests</div>${rows}</div>`;
|
|
394
527
|
}
|
|
@@ -397,23 +530,32 @@ const STYLES = `
|
|
|
397
530
|
:root { color-scheme: light; }
|
|
398
531
|
* { box-sizing: border-box; }
|
|
399
532
|
body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; }
|
|
400
|
-
.dc-wrap {
|
|
401
|
-
.dc-header { display: flex; align-items: center;
|
|
402
|
-
.dc-title { font-size:
|
|
403
|
-
.dc-summary { display:
|
|
533
|
+
.dc-wrap { width: 100%; margin: 0 auto; background: #fff; min-height: 100vh; }
|
|
534
|
+
.dc-header { display: flex; align-items: center; gap: 10px; padding: 14px 20px 10px; background: #f7f8ff; }
|
|
535
|
+
.dc-title { font-size: 16px; font-weight: 500; color: #334155; }
|
|
536
|
+
.dc-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 20px; color: #94a3b8; font-size: 12px; padding: 8px 20px 14px; background: #f7f8ff; border-bottom: 1px solid #d8e2f4; }
|
|
537
|
+
.dc-notice { display: flex; align-items: flex-start; gap: 8px; margin: 8px 0; border-radius: 8px; padding: 8px 12px; font-size: 12px; line-height: 20px; background: #f8fafc; color: #64748b; }
|
|
538
|
+
.dc-notice-warning { background: #fffbeb; color: #92400e; }
|
|
539
|
+
.dc-notice > svg { flex-shrink: 0; width: 14px; height: 14px; margin-top: 2px; }
|
|
540
|
+
.dc-notice > div { min-width: 0; }
|
|
541
|
+
.dc-notice p { margin: 0; }
|
|
542
|
+
.dc-notice details { margin-top: 4px; font-size: 11px; }
|
|
543
|
+
.dc-notice summary { cursor: pointer; width: fit-content; }
|
|
544
|
+
.dc-notice pre { white-space: pre-wrap; overflow-wrap: anywhere; margin: 4px 0 0; font-size: 11px; }
|
|
545
|
+
.dc-run-details { margin-left: auto; }
|
|
546
|
+
.dc-run-details > summary { cursor: pointer; list-style: none; color: #64748b; }
|
|
547
|
+
.dc-run-settings { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 16px; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; }
|
|
404
548
|
.dc-sum-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #1e293b; }
|
|
405
549
|
.dc-sum-value { font-size: 13px; font-weight: 500; color: #334155; margin-top: 2px; }
|
|
406
550
|
.dc-tests { background: #fff; border: 1px solid #cbd5e1; border-radius: 12px; padding: 12px 16px; margin-bottom: 16px; }
|
|
407
551
|
.dc-tests-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #1e293b; margin-bottom: 8px; }
|
|
408
|
-
.dc-test-row { display: grid; grid-template-columns: 1fr 90px 90px; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 6px; }
|
|
552
|
+
.dc-test-row { text-decoration: none; display: grid; grid-template-columns: 1fr 90px 90px; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 6px; }
|
|
409
553
|
.dc-test-name { font-weight: 500; color: #334155; }
|
|
410
554
|
.dc-test-dur { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #64748b; font-size: 12px; }
|
|
411
|
-
.dc-timeline { background: #fff;
|
|
555
|
+
.dc-timeline { background: #fff; padding: 24px 20px; display: flex; flex-direction: column; gap: 6px; }
|
|
412
556
|
.dc-child-block { margin-top: 12px; }
|
|
413
557
|
.dc-child-head { font-size: 13px; font-weight: 600; color: #334155; margin: 8px 0; }
|
|
414
558
|
.dc-row { display: flex; }
|
|
415
|
-
.dc-row-right { justify-content: flex-end; }
|
|
416
|
-
.dc-bubble { display: inline-block; max-width: 70%; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 16px; border-bottom-right-radius: 6px; padding: 8px 14px; color: #0f172a; }
|
|
417
559
|
.dc-line { display: flex; align-items: center; gap: 8px; color: #475569; }
|
|
418
560
|
.dc-agent-line { align-items: flex-start; }
|
|
419
561
|
.dc-agent-line .dc-icon { margin-top: 4px; }
|
|
@@ -425,7 +567,7 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
425
567
|
.dc-error { color: #b91c1c; }
|
|
426
568
|
.dc-warning { color: #b45309; }
|
|
427
569
|
.dc-shot-link { display: inline-flex; align-items: center; justify-content: center; position: relative; min-width: 34px; min-height: 36px; margin-left: 8px; vertical-align: middle; }
|
|
428
|
-
.dc-shot-link::before, .dc-lightbox:target::before { content: ""; position: absolute; width: 14px; height: 14px; border: 2px solid #cbd5e1; border-top-color: #475569; border-radius: 50%; animation: dc-spin .7s linear infinite; }
|
|
570
|
+
.dc-shot-link[data-remote-image]::before, .dc-lightbox[data-remote-image]:target::before { content: ""; position: absolute; width: 14px; height: 14px; border: 2px solid #cbd5e1; border-top-color: #475569; border-radius: 50%; animation: dc-spin .7s linear infinite; }
|
|
429
571
|
.dc-shot { position: relative; z-index: 1; height: 34px; width: auto; border: 1px solid #e2e8f0; border-radius: 4px; cursor: zoom-in; transition: border-color .12s; }
|
|
430
572
|
.dc-shot-link:hover .dc-shot, .dc-shot-link:focus-visible .dc-shot { border-color: #94a3b8; }
|
|
431
573
|
.dc-lightbox { display: none; position: fixed; inset: 0; z-index: 1000; align-items: center; justify-content: center; padding: 32px; background: rgba(15, 23, 42, .82); }
|
|
@@ -440,20 +582,26 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
440
582
|
.dc-card { border: 1px solid; border-radius: 12px; padding: 12px 14px; }
|
|
441
583
|
.dc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; }
|
|
442
584
|
.dc-card-title { font-size: 13px; font-weight: 600; display: inline-flex; align-items: center; gap: 6px; }
|
|
585
|
+
.dc-card-title > .dc-icon { color: inherit; width: 16px; height: 16px; }
|
|
443
586
|
.dc-card-body > div { margin: 1px 0; }
|
|
444
|
-
.dc-card-error
|
|
445
|
-
.dc-assertion { font-size:
|
|
587
|
+
.dc-card-error { margin-top: 6px; font-size: 13px; }
|
|
588
|
+
.dc-assertion-fields { margin-top: 8px; font-size: 13px; line-height: 20px; }
|
|
589
|
+
.dc-assertion-fields p { margin: 0; }
|
|
590
|
+
.dc-assertion-fields .dc-agent-line { margin-top: 6px; }
|
|
591
|
+
.dc-expected-label { font-weight: 600; }
|
|
592
|
+
.dc-fail .dc-assertion-fields .dc-agent-line { color: #be123c; }
|
|
593
|
+
.dc-pass .dc-assertion-fields .dc-agent-line { color: #047857; }
|
|
446
594
|
.dc-pass { border-color: #a7f3d0; background: #ecfdf5; color: #064e3b; }
|
|
447
595
|
.dc-fail { border-color: #fecdd3; background: #fff1f2; color: #881337; }
|
|
448
596
|
.dc-amber, .dc-error.dc-card { border-color: #fde68a; background: #fffbeb; color: #78350f; }
|
|
449
|
-
.dc-pill { display: inline-flex; width: fit-content; border: 1px solid; border-radius:
|
|
450
|
-
.dc-pill-pass, .dc-pill-passed { border-color: #6ee7b7; background: #
|
|
451
|
-
.dc-pill-fail, .dc-pill-failed { border-color: #fda4af; background: #
|
|
597
|
+
.dc-pill { display: inline-flex; width: fit-content; border: 1px solid; border-radius: 4px; padding: 2px 8px; font-size: 12px; font-weight: 600; }
|
|
598
|
+
.dc-pill-pass, .dc-pill-passed { border-color: #6ee7b7; background: #ecfdf5; color: #047857; }
|
|
599
|
+
.dc-pill-fail, .dc-pill-failed { border-color: #fda4af; background: #fff1f2; color: #be123c; }
|
|
452
600
|
.dc-pill-error, .dc-pill-errored, .dc-pill-amber { border-color: #fcd34d; background: #fef3c7; color: #b45309; }
|
|
453
601
|
.dc-pill-stopped { border-color: #fdba74; background: #ffedd5; color: #c2410c; }
|
|
454
602
|
.dc-pill-skipped { border-color: #cbd5e1; background: #f1f5f9; color: #334155; }
|
|
455
|
-
.dc-pill-ready, .dc-pill-passed { border-color: #6ee7b7; background: #
|
|
456
|
-
.dc-pill-issue, .dc-pill-failed { border-color: #fda4af; background: #
|
|
603
|
+
.dc-pill-ready, .dc-pill-passed { border-color: #6ee7b7; background: #ecfdf5; color: #047857; }
|
|
604
|
+
.dc-pill-issue, .dc-pill-failed { border-color: #fda4af; background: #fff1f2; color: #be123c; }
|
|
457
605
|
.dc-pill-ended { border-color: #cbd5e1; background: #f1f5f9; color: #334155; }
|
|
458
606
|
.dc-pill-starting { border-color: #a5b4fc; background: #e0e7ff; color: #4338ca; }
|
|
459
607
|
.dc-pill-running { border-color: #7dd3fc; background: #e0f2fe; color: #0369a1; }
|
|
@@ -464,7 +612,7 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
464
612
|
.dc-special[open] > summary::before { transform: rotate(90deg); }
|
|
465
613
|
.dc-special-title { display: inline-flex; flex: 1; align-items: center; gap: 5px; font-size: 13px; font-weight: 600; }
|
|
466
614
|
.dc-special-body { margin-top: 9px; display: flex; flex-direction: column; gap: 5px; color: #334155; }
|
|
467
|
-
.dc-context { border-color: #ddd6fe; background: #f5f3ff; color: #4c1d95; }
|
|
615
|
+
.dc-context { border-color: #ddd6fe; background: #f5f3ff; color: #4c1d95; margin-bottom: 12px; }
|
|
468
616
|
.dc-context pre { margin: 10px 0 0; border: 1px solid #ede9fe; border-radius: 8px; background: rgba(255,255,255,.75); padding: 8px 10px; color: #334155; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
|
|
469
617
|
.dc-cloud { border-color: #c7d2fe; background: #eef2ff; color: #312e81; }
|
|
470
618
|
.dc-network { min-width: 0; max-width: 100%; overflow-wrap: anywhere; }
|
|
@@ -478,7 +626,64 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
478
626
|
.dc-network-data > summary { display: list-item; list-style-position: inside; cursor: pointer; }
|
|
479
627
|
.dc-network-data pre { max-height: 288px; overflow: auto; white-space: pre-wrap; font-size: 12px; }
|
|
480
628
|
.dc-network, .dc-loadmill { border-color: #bae6fd; background: #f0f9ff; color: #075985; }
|
|
629
|
+
.dc-loadmill .dc-special-title { color: #075985; }
|
|
630
|
+
.dc-loadmill-passed { border-color: #a7f3d0; background: #ecfdf5; color: #064e3b; }
|
|
631
|
+
.dc-loadmill-failed { border-color: #fecdd3; background: #fff1f2; color: #881337; }
|
|
481
632
|
.dc-empty { color: #64748b; }
|
|
633
|
+
.dc-report-layout { display: flex; align-items: stretch; }
|
|
634
|
+
.dc-report-main { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
635
|
+
.dc-screenshots { position: sticky; top: 0; width: 32%; min-width: 210px; max-width: none; height: calc(100vh - 100px); display: flex; flex-direction: column; align-items: stretch; padding: 16px 20px; background: #fafbff; border-left: 1px solid #e2e8f0; }
|
|
636
|
+
#dc-panel-open { flex: 1; min-height: 0; display: flex; justify-content: center; padding: 0 12px; }
|
|
637
|
+
#dc-panel-image { max-height: 100%; max-width: 100%; object-fit: contain; border-radius: 8px; border: 1px solid #cbd5e1; box-shadow: 0 1px 2px #0f172a15; }
|
|
638
|
+
.dc-screenshots nav { display: flex; justify-content: space-between; align-items: center; gap: 12px; color: #64748b; font-size: 12px; }
|
|
639
|
+
.dc-screenshots button { border: 0; background: transparent; font-size: 24px; cursor: pointer; color: #475569; }
|
|
640
|
+
.dc-screenshots button:disabled { opacity: .3; cursor: default; }
|
|
641
|
+
#dc-panel-caption { font-size: 12px; color: #475569; margin: 20px 0 12px; }
|
|
642
|
+
.dc-instruction { position: relative; padding: 0 0 10px 32px; }
|
|
643
|
+
.dc-instruction:not(:last-child)::before { content: ""; position: absolute; left: 10px; top: 22px; bottom: -2px; width: 1px; background: #e2e8f0; }
|
|
644
|
+
.dc-instruction > summary { display: flex; align-items: flex-start; gap: 10px; min-height: 24px; cursor: pointer; list-style: none; font-weight: 500; font-size: 14px; }
|
|
645
|
+
.dc-instruction > summary::-webkit-details-marker { display: none; }
|
|
646
|
+
.dc-instruction-status { position: absolute; left: 0; top: 1px; display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: #f1f5f9; color: #94a3b8; }
|
|
647
|
+
.dc-instruction-status .dc-icon { width: 13px; height: 13px; color: inherit; }
|
|
648
|
+
.dc-instruction-status.dc-complete { color: #10b981; background: #ecfdf5; }
|
|
649
|
+
.dc-instruction-status.dc-error { color: #f43f5e; background: #fff1f2; }
|
|
650
|
+
.dc-instruction-status.dc-stopped { color: #f97316; background: #fff7ed; }
|
|
651
|
+
.dc-instruction-duration, .dc-instruction-time { color: #94a3b8; font-size: 12px; font-weight: 400; white-space: nowrap; }
|
|
652
|
+
.dc-instruction-label { min-width: 0; }
|
|
653
|
+
.dc-instruction-duration { display: flex; align-items: center; gap: 6px; line-height: 20px; }
|
|
654
|
+
.dc-instruction-time { margin-left: auto; line-height: 24px; }
|
|
655
|
+
.dc-instruction-activities { display: inline-flex; align-items: center; gap: 6px; margin-left: 4px; padding-left: 8px; border-left: 1px solid #e2e8f0; }
|
|
656
|
+
.dc-activity { display: inline-flex; }
|
|
657
|
+
.dc-activity .dc-icon { width: 12px; height: 12px; color: inherit; overflow: visible; }
|
|
658
|
+
.dc-activity-loadmill path { fill: none; stroke: currentColor; stroke-width: 86; stroke-linejoin: round; }
|
|
659
|
+
.dc-instruction-chevron { display: inline-flex; }
|
|
660
|
+
.dc-instruction[open] > summary .dc-instruction-chevron { transform: rotate(90deg); }
|
|
661
|
+
.dc-instruction-actions { display: flex; flex-direction: column; gap: 12px; padding: 18px 0 8px; }
|
|
662
|
+
.dc-header > .dc-icon { width: 18px; height: 18px; }
|
|
663
|
+
.dc-summary > span, .dc-run-details > summary { display: inline-flex; align-items: center; gap: 7px; }
|
|
664
|
+
.dc-summary .dc-icon { width: 15px; height: 15px; }
|
|
665
|
+
.dc-run-details > summary::-webkit-details-marker { display: none; }
|
|
666
|
+
.dc-panel-toolbar { display: flex; justify-content: flex-end; margin-bottom: 18px; }
|
|
667
|
+
.dc-panel-toolbar a { display: inline-flex; padding: 4px; color: #64748b; }
|
|
668
|
+
#dc-panel-position { width: 100%; margin: 0 0 14px; accent-color: #64748b; height: 4px; cursor: pointer; }
|
|
669
|
+
.dc-screenshots nav button { display: inline-flex; align-items: center; justify-content: center; padding: 4px; }
|
|
670
|
+
.dc-screenshots nav button .dc-icon { color: inherit; width: 14px; height: 14px; }
|
|
671
|
+
.dc-screenshots nav button[data-shot="first"], .dc-screenshots nav button[data-shot="previous"] { transform: rotate(180deg); }
|
|
672
|
+
.dc-screenshots nav button .dc-icon + .dc-icon { margin-left: -8px; }
|
|
673
|
+
#dc-panel-count { flex: 1; text-align: center; }
|
|
674
|
+
.dc-dialog-trigger { display: inline-flex; align-items: center; gap: 7px; border: 0; background: transparent; padding: 4px 8px; font: inherit; color: #64748b; cursor: pointer; border-radius: 4px; }
|
|
675
|
+
.dc-dialog-trigger:hover { background: #f1f5f9; }
|
|
676
|
+
.dc-dialog-trigger .dc-special-title { font-size: inherit; font-weight: 400; }
|
|
677
|
+
.dc-child-head { display: flex; align-items: center; justify-content: space-between; }
|
|
678
|
+
.dc-dialog { width: min(640px, calc(100vw - 40px)); max-height: 80vh; padding: 24px; border: 1px solid #e2e8f0; border-radius: 12px; color: #334155; box-shadow: 0 24px 80px #0f172a30; }
|
|
679
|
+
.dc-dialog::backdrop { background: #0f172a66; }
|
|
680
|
+
.dc-dialog-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
681
|
+
.dc-dialog-header h2 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
682
|
+
.dc-dialog-header button { border: 0; background: transparent; color: #64748b; font-size: 24px; cursor: pointer; }
|
|
683
|
+
.dc-dialog .dc-run-settings { border: 0; padding: 0; gap: 20px; }
|
|
684
|
+
.dc-dialog pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
685
|
+
@media (max-width: 650px) { .dc-report-layout { flex-direction: column; } .dc-screenshots { position: static; width: 100%; max-width: none; height: 420px; } .dc-timeline { padding: 12px; } .dc-instruction > summary { flex-wrap: wrap; } .dc-instruction-time { display: none; } }
|
|
686
|
+
@media print { .dc-screenshots { display: none; } .dc-instruction::details-content { display: block; } }
|
|
482
687
|
@keyframes dc-spin { to { transform: rotate(360deg); } }
|
|
483
688
|
`.trim();
|
|
484
689
|
// Keep the machine-readable evidence separate from the presentation. Image references
|
|
@@ -509,31 +714,30 @@ function renderAgentReport(model, shots) {
|
|
|
509
714
|
return `<pre hidden id="agent-report" data-schema-version="1">${escapeHtml(json)}</pre>`;
|
|
510
715
|
}
|
|
511
716
|
// ---------- entry point ----------
|
|
512
|
-
export function renderHtmlReport(model, { title, includeScripts = true } = {}) {
|
|
717
|
+
export function renderHtmlReport(model, { title, includeScripts = true, showRunDetails = true } = {}) {
|
|
513
718
|
const docTitle = title ?? `Run Report: ${model.testName ?? model.suiteName ?? "Test run"}`;
|
|
514
|
-
const heading = model.testName ?? model.suiteName ?? "Test run";
|
|
719
|
+
const heading = (model.testName ?? model.suiteName ?? "Test run").replace(/\.dcua$/i, "").split(/[\\/]/).pop();
|
|
515
720
|
// Screenshots are collected across the whole document (top-level + child timelines)
|
|
516
721
|
// so the lightbox shares one continuous prev/next sequence.
|
|
517
722
|
const shots = [];
|
|
518
723
|
let body;
|
|
519
724
|
if (model.childTests && model.childTests.length > 0) {
|
|
520
725
|
const children = model.childTests
|
|
521
|
-
.map((child) => `<div class="dc-child-block"><div class="dc-child-head">${escapeHtml(child.testName.replace(/\.dcua$/i, ""))}</div>${renderTimeline(child.events, shots, child.warnings)}</div>`)
|
|
726
|
+
.map((child, index) => `<div id="dc-test-${index}" class="dc-child-block"><div class="dc-child-head">${escapeHtml(child.testName.replace(/\.dcua$/i, ""))}</div>${renderTimeline(child.events, shots, child.warnings)}</div>`)
|
|
522
727
|
.join("\n");
|
|
523
|
-
body = `${
|
|
524
|
-
${renderTestsList(model.childTests)}
|
|
728
|
+
body = `${renderTestsList(model.childTests)}
|
|
525
729
|
<div class="dc-timeline">
|
|
526
730
|
${children}
|
|
527
731
|
</div>`;
|
|
528
732
|
}
|
|
529
733
|
else {
|
|
530
|
-
body =
|
|
531
|
-
<div class="dc-timeline">
|
|
734
|
+
body = `<div class="dc-timeline">
|
|
532
735
|
${renderTimeline(model.events, shots, model.warnings)}
|
|
533
736
|
</div>`;
|
|
534
737
|
}
|
|
738
|
+
body = `<div class="dc-report-main">${body}</div>${includeScripts ? renderScreenshotPanel(shots) : ""}`;
|
|
535
739
|
const lightboxes = renderLightboxes(shots);
|
|
536
|
-
const script = includeScripts ?
|
|
740
|
+
const script = includeScripts ? renderInteractionScript(shots.length) : "";
|
|
537
741
|
return `<!DOCTYPE html>
|
|
538
742
|
<html lang="en">
|
|
539
743
|
<head>
|
|
@@ -548,8 +752,9 @@ ${STYLES}
|
|
|
548
752
|
<!--email_off-->
|
|
549
753
|
${renderAgentReport(model, shots)}
|
|
550
754
|
<div class="dc-wrap" id="dc-top">
|
|
551
|
-
<div class="dc-header"
|
|
552
|
-
${
|
|
755
|
+
<div class="dc-header">${ICONS.clipboard}<div class="dc-title">${escapeHtml(heading)}</div><span class="dc-pill dc-pill-${escapeHtml(model.status)}">${escapeHtml(runStatusLabel(model.status))}</span></div>
|
|
756
|
+
${renderSummaryCard(model, showRunDetails)}
|
|
757
|
+
<div class="dc-report-layout">${body}</div>
|
|
553
758
|
</div>${lightboxes ? `\n${lightboxes}` : ""}${script ? `\n${script}` : ""}
|
|
554
759
|
<!--/email_off-->
|
|
555
760
|
</body>
|