doc-detective 4.0.2-dev.1 → 4.0.2-dev.10

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.
Files changed (93) hide show
  1. package/dist/agents/adapters/claude-code.d.ts +77 -0
  2. package/dist/agents/adapters/claude-code.d.ts.map +1 -0
  3. package/dist/agents/adapters/claude-code.js +461 -0
  4. package/dist/agents/adapters/claude-code.js.map +1 -0
  5. package/dist/agents/adapters/codex.d.ts +64 -0
  6. package/dist/agents/adapters/codex.d.ts.map +1 -0
  7. package/dist/agents/adapters/codex.js +299 -0
  8. package/dist/agents/adapters/codex.js.map +1 -0
  9. package/dist/agents/adapters/copilot-cli.d.ts +29 -0
  10. package/dist/agents/adapters/copilot-cli.d.ts.map +1 -0
  11. package/dist/agents/adapters/copilot-cli.js +195 -0
  12. package/dist/agents/adapters/copilot-cli.js.map +1 -0
  13. package/dist/agents/adapters/gemini-cli.d.ts +29 -0
  14. package/dist/agents/adapters/gemini-cli.d.ts.map +1 -0
  15. package/dist/agents/adapters/gemini-cli.js +207 -0
  16. package/dist/agents/adapters/gemini-cli.js.map +1 -0
  17. package/dist/agents/adapters/opencode.d.ts +67 -0
  18. package/dist/agents/adapters/opencode.d.ts.map +1 -0
  19. package/dist/agents/adapters/opencode.js +341 -0
  20. package/dist/agents/adapters/opencode.js.map +1 -0
  21. package/dist/agents/adapters/qwen-code.d.ts +30 -0
  22. package/dist/agents/adapters/qwen-code.d.ts.map +1 -0
  23. package/dist/agents/adapters/qwen-code.js +212 -0
  24. package/dist/agents/adapters/qwen-code.js.map +1 -0
  25. package/dist/agents/command.d.ts +11 -0
  26. package/dist/agents/command.d.ts.map +1 -0
  27. package/dist/agents/command.js +41 -0
  28. package/dist/agents/command.js.map +1 -0
  29. package/dist/agents/fetcher.d.ts +30 -0
  30. package/dist/agents/fetcher.d.ts.map +1 -0
  31. package/dist/agents/fetcher.js +112 -0
  32. package/dist/agents/fetcher.js.map +1 -0
  33. package/dist/agents/prompts.d.ts +24 -0
  34. package/dist/agents/prompts.d.ts.map +1 -0
  35. package/dist/agents/prompts.js +74 -0
  36. package/dist/agents/prompts.js.map +1 -0
  37. package/dist/agents/registry.d.ts +4 -0
  38. package/dist/agents/registry.d.ts.map +1 -0
  39. package/dist/agents/registry.js +25 -0
  40. package/dist/agents/registry.js.map +1 -0
  41. package/dist/agents/runner.d.ts +13 -0
  42. package/dist/agents/runner.d.ts.map +1 -0
  43. package/dist/agents/runner.js +155 -0
  44. package/dist/agents/runner.js.map +1 -0
  45. package/dist/agents/spawn-helper.d.ts +33 -0
  46. package/dist/agents/spawn-helper.d.ts.map +1 -0
  47. package/dist/agents/spawn-helper.js +98 -0
  48. package/dist/agents/spawn-helper.js.map +1 -0
  49. package/dist/agents/types.d.ts +41 -0
  50. package/dist/agents/types.d.ts.map +1 -0
  51. package/dist/agents/types.js +2 -0
  52. package/dist/agents/types.js.map +1 -0
  53. package/dist/cli.js +42 -10
  54. package/dist/cli.js.map +1 -1
  55. package/dist/common/src/detectTests.d.ts.map +1 -1
  56. package/dist/common/src/detectTests.js +43 -12
  57. package/dist/common/src/detectTests.js.map +1 -1
  58. package/dist/common/src/fileTypes.d.ts.map +1 -1
  59. package/dist/common/src/fileTypes.js +10 -0
  60. package/dist/common/src/fileTypes.js.map +1 -1
  61. package/dist/common/src/schemas/schemas.json +594 -0
  62. package/dist/common/src/types/generated/checkLink_v3.d.ts +15 -0
  63. package/dist/common/src/types/generated/checkLink_v3.d.ts.map +1 -1
  64. package/dist/common/src/types/generated/config_v3.d.ts +4 -0
  65. package/dist/common/src/types/generated/config_v3.d.ts.map +1 -1
  66. package/dist/common/src/types/generated/resolvedTests_v3.d.ts +4 -0
  67. package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -1
  68. package/dist/common/src/types/generated/step_v3.d.ts +15 -0
  69. package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
  70. package/dist/common/src/types/generated/test_v3.d.ts +30 -0
  71. package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
  72. package/dist/core/config.d.ts.map +1 -1
  73. package/dist/core/config.js +10 -0
  74. package/dist/core/config.js.map +1 -1
  75. package/dist/core/integrations/heretto.d.ts.map +1 -1
  76. package/dist/core/integrations/heretto.js +11 -3
  77. package/dist/core/integrations/heretto.js.map +1 -1
  78. package/dist/core/tests/checkLink.d.ts.map +1 -1
  79. package/dist/core/tests/checkLink.js +136 -29
  80. package/dist/core/tests/checkLink.js.map +1 -1
  81. package/dist/core/tests/loadCookie.d.ts.map +1 -1
  82. package/dist/core/tests/loadCookie.js +12 -2
  83. package/dist/core/tests/loadCookie.js.map +1 -1
  84. package/dist/index.cjs +783 -44
  85. package/dist/reporters/htmlReporter.d.ts +2 -0
  86. package/dist/reporters/htmlReporter.d.ts.map +1 -0
  87. package/dist/reporters/htmlReporter.js +1589 -0
  88. package/dist/reporters/htmlReporter.js.map +1 -0
  89. package/dist/utils.d.ts +2 -1
  90. package/dist/utils.d.ts.map +1 -1
  91. package/dist/utils.js +43 -10
  92. package/dist/utils.js.map +1 -1
  93. package/package.json +3 -1
@@ -0,0 +1,1589 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export async function htmlReporter(config = {}, outputPath, results, options = {}) {
4
+ const outputExtensions = [".html", ".htm"];
5
+ outputPath = path.resolve(outputPath);
6
+ let outputFile = "";
7
+ let outputDir = "";
8
+ let reportType = "doc-detective-results";
9
+ if (options.command) {
10
+ if (options.command === "runCoverage") {
11
+ reportType = "coverageResults";
12
+ }
13
+ else if (options.command === "runTests") {
14
+ reportType = "testResults";
15
+ }
16
+ }
17
+ if (outputExtensions.some((ext) => outputPath.endsWith(ext))) {
18
+ outputDir = path.dirname(outputPath);
19
+ outputFile = outputPath;
20
+ if (fs.existsSync(outputFile)) {
21
+ let counter = 0;
22
+ const ext = path.extname(outputFile);
23
+ const base = outputFile.slice(0, -ext.length);
24
+ while (fs.existsSync(`${base}-${counter}${ext}`)) {
25
+ counter++;
26
+ }
27
+ outputFile = `${base}-${counter}${ext}`;
28
+ }
29
+ }
30
+ else {
31
+ outputDir = outputPath;
32
+ outputFile = path.resolve(outputDir, `${reportType}-${Date.now()}.html`);
33
+ }
34
+ try {
35
+ if (!fs.existsSync(outputDir)) {
36
+ fs.mkdirSync(outputDir, { recursive: true });
37
+ }
38
+ const html = buildHtml(results);
39
+ fs.writeFileSync(outputFile, html);
40
+ console.log(`See HTML report at ${outputFile}\n`);
41
+ return outputFile;
42
+ }
43
+ catch (err) {
44
+ console.error(`Error writing HTML report to ${outputFile}. ${err}`);
45
+ return null;
46
+ }
47
+ }
48
+ function buildHtml(results) {
49
+ const reportJson = JSON.stringify(results, null, 2)
50
+ .replace(/</g, "\\u003c")
51
+ .replace(/>/g, "\\u003e")
52
+ .replace(/\u2028/g, "\\u2028")
53
+ .replace(/\u2029/g, "\\u2029");
54
+ return `<!DOCTYPE html>
55
+ <html lang="en">
56
+ <head>
57
+ <meta charset="utf-8"/>
58
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
59
+ <title>Doc Detective — Test Report</title>
60
+ <style>
61
+ ${CSS_CONTENT}
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div id="root"></div>
66
+ <script id="dd-report-data" type="application/json">${reportJson}</script>
67
+ <script>
68
+ window.REPORT_DATA = JSON.parse(document.getElementById("dd-report-data").textContent);
69
+ </script>
70
+ <script>
71
+ ${JS_CONTENT}
72
+ </script>
73
+ </body>
74
+ </html>`;
75
+ }
76
+ const CSS_CONTENT = `
77
+
78
+ :root {
79
+ --dd-green: #4B9A47;
80
+ --dd-green-deep: #22623D;
81
+ --dd-green-bright: #3EB16E;
82
+ --dd-green-electric: #00C122;
83
+ --dd-green-forest: #2E8555;
84
+ --dd-green-tint: #E8F3E7;
85
+ --dd-green-tint-2: #D2E8CE;
86
+ --dd-ink: #0D0E11;
87
+ --dd-ink-2: #1A1C21;
88
+ --dd-ink-3: #2A2D34;
89
+ --dd-gray-900: #3A3F47;
90
+ --dd-gray-700: #5B616B;
91
+ --dd-gray-500: #8A909B;
92
+ --dd-gray-300: #C7CBD3;
93
+ --dd-gray-200: #E2E5EA;
94
+ --dd-gray-100: #F1F3F6;
95
+ --dd-gray-50: #F7F8FA;
96
+ --dd-paper: #FFFFFF;
97
+ --dd-pass: #22623D;
98
+ --dd-pass-bg: #E8F3E7;
99
+ --dd-fail: #B0261A;
100
+ --dd-fail-bg: #FBEAE7;
101
+ --dd-warn: #8A5A00;
102
+ --dd-warn-bg: #FBF1DB;
103
+ --dd-skip: #4A5058;
104
+ --dd-skip-bg: #F1F3F6;
105
+ --dd-info: #2563A0;
106
+ --dd-info-bg: #E5EEF7;
107
+ --dd-code-bg: #0D0E11;
108
+ --dd-code-fg: #E6E8EC;
109
+ --fg1: var(--dd-ink);
110
+ --fg2: #4A5058;
111
+ --fg3: #606770;
112
+ --fg-inverse: var(--dd-paper);
113
+ --fg-brand: var(--dd-green-deep);
114
+ --bg1: var(--dd-paper);
115
+ --bg2: var(--dd-gray-50);
116
+ --bg3: var(--dd-gray-100);
117
+ --bg-brand: var(--dd-green-tint);
118
+ --bg-ink: var(--dd-ink);
119
+ --border-subtle: var(--dd-gray-200);
120
+ --border-strong: var(--dd-gray-300);
121
+ --border-brand: var(--dd-green-bright);
122
+ --shadow-xs: 0 1px 2px rgba(13,14,17,0.04);
123
+ --shadow-sm: 0 1px 2px rgba(13,14,17,0.06), 0 1px 1px rgba(13,14,17,0.04);
124
+ --shadow-md: 0 4px 10px rgba(13,14,17,0.06), 0 2px 4px rgba(13,14,17,0.04);
125
+ --shadow-lg: 0 16px 32px rgba(13,14,17,0.08), 0 4px 8px rgba(13,14,17,0.04);
126
+ --shadow-focus: 0 0 0 3px rgba(62,177,110,0.35);
127
+ --radius-xs: 4px;
128
+ --radius-sm: 6px;
129
+ --radius-md: 8px;
130
+ --radius-lg: 12px;
131
+ --radius-xl: 16px;
132
+ --radius-pill: 999px;
133
+ --space-1: 4px;
134
+ --space-2: 8px;
135
+ --space-3: 12px;
136
+ --space-4: 16px;
137
+ --space-5: 24px;
138
+ --space-6: 32px;
139
+ --space-7: 48px;
140
+ --space-8: 64px;
141
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
142
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
143
+ --font-display: 'Inter', sans-serif;
144
+ --fs-display: 56px;
145
+ --fs-h1: 40px;
146
+ --fs-h2: 30px;
147
+ --fs-h3: 22px;
148
+ --fs-h4: 18px;
149
+ --fs-body: 16px;
150
+ --fs-small: 14px;
151
+ --fs-micro: 12px;
152
+ --fs-code: 14.5px;
153
+ --lh-tight: 1.15;
154
+ --lh-snug: 1.3;
155
+ --lh-normal: 1.55;
156
+ --lh-loose: 1.7;
157
+ --tracking-tight: -0.02em;
158
+ --tracking-normal: 0;
159
+ --tracking-wide: 0.04em;
160
+ --tracking-caps: 0.08em;
161
+ }
162
+
163
+ * { box-sizing: border-box; }
164
+ html, body { margin: 0; padding: 0; }
165
+ body {
166
+ font-family: var(--font-sans);
167
+ font-size: var(--fs-body);
168
+ line-height: var(--lh-normal);
169
+ color: var(--fg1);
170
+ background: var(--bg2);
171
+ -webkit-font-smoothing: antialiased;
172
+ text-rendering: optimizeLegibility;
173
+ }
174
+ button { font: inherit; }
175
+
176
+ .app {
177
+ min-height: 100vh;
178
+ display: grid;
179
+ grid-template-rows: auto 1fr;
180
+ }
181
+
182
+ /* Header */
183
+ .hdr {
184
+ background: var(--dd-ink);
185
+ color: #F1F3F6;
186
+ border-bottom: 1px solid #000;
187
+ position: relative;
188
+ overflow: hidden;
189
+ }
190
+ .hdr::after {
191
+ content: "";
192
+ position: absolute; inset: auto 0 0 0;
193
+ height: 3px;
194
+ background: linear-gradient(90deg,
195
+ var(--dd-pass) 0%, var(--dd-pass) var(--pct-pass,0%),
196
+ var(--dd-fail) var(--pct-pass,0%), var(--dd-fail) var(--pct-fail-end,0%),
197
+ var(--dd-warn) var(--pct-fail-end,0%), var(--dd-warn) var(--pct-warn-end,0%),
198
+ var(--dd-skip) var(--pct-warn-end,0%), var(--dd-skip) 100%);
199
+ }
200
+ .hdr-inner {
201
+ max-width: 1280px; margin: 0 auto;
202
+ padding: 18px 28px 20px;
203
+ display: flex; align-items: center; gap: 18px;
204
+ }
205
+ .brand { display: flex; align-items: center; gap: 12px; }
206
+ .brand svg { width: 30px; height: 30px; }
207
+ .brand .wm {
208
+ font-weight: 800; font-size: 15px; letter-spacing: -0.01em;
209
+ }
210
+ .brand .wm .tag { color: var(--dd-green-bright); }
211
+ .brand .divider {
212
+ width: 1px; height: 22px; background: #2A2D34; margin: 0 6px;
213
+ }
214
+ .hdr-title { flex: 1; min-width: 0; }
215
+ .hdr-title .eyebrow {
216
+ font-family: var(--font-mono); font-size: 11px;
217
+ color: var(--dd-gray-500); letter-spacing: 0.08em; text-transform: uppercase;
218
+ margin-bottom: 3px;
219
+ }
220
+ .hdr-title h1 {
221
+ margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -0.01em;
222
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
223
+ }
224
+ .hdr-title h1 .sub { color: var(--dd-gray-500); font-weight: 500; }
225
+ .hdr-actions { display: flex; gap: 8px; align-items: center; }
226
+ .hdr-btn {
227
+ display: inline-flex; align-items: center; gap: 8px;
228
+ padding: 8px 12px; border-radius: 8px;
229
+ border: 1px solid #2A2D34; background: #15171B; color: #E6E8EC;
230
+ font-size: 13px; font-weight: 500; cursor: pointer;
231
+ transition: background .15s, border-color .15s;
232
+ }
233
+ .hdr-btn:hover { background: #1F222A; border-color: #3A3F47; }
234
+ .hdr-btn.primary { background: var(--dd-green-bright); color: #07150C; border-color: transparent; font-weight: 600; }
235
+ .hdr-btn.primary:hover { background: #4DC482; }
236
+
237
+ /* Meta strip */
238
+ .metastrip {
239
+ background: #15171B;
240
+ color: var(--dd-gray-300);
241
+ border-top: 1px solid #000;
242
+ font-family: var(--font-mono); font-size: 12px;
243
+ }
244
+ .metastrip-inner {
245
+ max-width: 1280px; margin: 0 auto;
246
+ padding: 10px 28px;
247
+ display: flex; flex-wrap: wrap; gap: 24px;
248
+ }
249
+ .metastrip .m { display: inline-flex; gap: 6px; align-items: baseline; }
250
+ .metastrip .m .k { color: var(--dd-gray-500); }
251
+ .metastrip .m .v { color: #E6E8EC; }
252
+
253
+ /* Main */
254
+ main {
255
+ max-width: 1280px; margin: 0 auto; width: 100%;
256
+ padding: 32px 28px 96px;
257
+ }
258
+
259
+ /* Verdict */
260
+ .verdict {
261
+ display: grid;
262
+ grid-template-columns: minmax(320px, 420px) 1fr;
263
+ gap: 20px;
264
+ margin-bottom: 32px;
265
+ }
266
+ @media (max-width: 900px) {
267
+ .verdict { grid-template-columns: 1fr; }
268
+ }
269
+
270
+ .verdict-card {
271
+ background: var(--dd-paper);
272
+ border: 1px solid var(--border-subtle);
273
+ border-radius: var(--radius-xl);
274
+ padding: 22px 24px;
275
+ position: relative;
276
+ overflow: hidden;
277
+ }
278
+ .verdict-card .vk {
279
+ font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
280
+ text-transform: uppercase; color: var(--fg3);
281
+ margin-bottom: 8px;
282
+ }
283
+ .verdict-card .vv {
284
+ display: flex; align-items: baseline; gap: 12px;
285
+ }
286
+ .verdict-card .vv .big {
287
+ font-size: 48px; font-weight: 800; letter-spacing: -0.02em; line-height: 1;
288
+ font-family: var(--font-mono);
289
+ }
290
+ .verdict-card.fail .vv .big { color: var(--dd-fail); }
291
+ .verdict-card.warn .vv .big { color: var(--dd-warn); }
292
+ .verdict-card.pass .vv .big { color: var(--dd-pass); }
293
+ .verdict-card.skip .vv .big { color: var(--dd-skip); }
294
+ .verdict-card .vv .note {
295
+ color: var(--fg2); font-size: 14px; line-height: 1.4;
296
+ }
297
+ .verdict-card .vbar {
298
+ margin-top: 18px; height: 8px; border-radius: 999px; overflow: hidden;
299
+ display: grid;
300
+ background: var(--bg3);
301
+ }
302
+ .verdict-card .vbar span { display: block; }
303
+ .verdict-card .vbar .pass { background: var(--dd-pass); }
304
+ .verdict-card .vbar .fail { background: var(--dd-fail); }
305
+ .verdict-card .vbar .warn { background: var(--dd-warn); }
306
+ .verdict-card .vbar .skip { background: var(--dd-skip); }
307
+
308
+ /* Summary tiles */
309
+ .summary {
310
+ display: grid; grid-template-columns: repeat(4, 1fr);
311
+ gap: 12px;
312
+ }
313
+ @media (max-width: 900px) {
314
+ .summary { grid-template-columns: repeat(2, 1fr); }
315
+ }
316
+ .sum {
317
+ background: var(--dd-paper);
318
+ border: 1px solid var(--border-subtle);
319
+ border-radius: var(--radius-lg);
320
+ padding: 16px 18px 14px;
321
+ position: relative;
322
+ display: flex; flex-direction: column; justify-content: space-between;
323
+ min-height: 132px;
324
+ overflow: hidden;
325
+ }
326
+ .sum .lbl {
327
+ font-family: var(--font-mono);
328
+ font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
329
+ text-transform: uppercase; color: var(--fg3);
330
+ }
331
+ .sum .row { display: flex; align-items: baseline; gap: 10px; margin-top: 6px; }
332
+ .sum .num { font-size: 30px; font-weight: 800; line-height: 1; font-family: var(--font-mono); letter-spacing: -0.01em; }
333
+ .sum .of { font-size: 12px; color: var(--fg3); font-family: var(--font-mono); }
334
+ .sum .miniBar {
335
+ margin-top: 14px; height: 6px; border-radius: 999px; overflow: hidden;
336
+ display: grid; background: var(--bg3);
337
+ }
338
+ .sum .miniBar span { display: block; }
339
+ .sum .legend {
340
+ margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap;
341
+ font-family: var(--font-mono); font-size: 11px; color: var(--fg3);
342
+ }
343
+ .sum .legend i {
344
+ display: inline-block; width: 8px; height: 8px; border-radius: 2px;
345
+ margin-right: 4px; vertical-align: 1px;
346
+ }
347
+ .sum.pass .num { color: var(--dd-pass); }
348
+ .sum.fail .num { color: var(--dd-fail); }
349
+ .sum.warn .num { color: var(--dd-warn); }
350
+ .sum.skip .num { color: var(--dd-skip); }
351
+ .sum .miniBar .p { background: var(--dd-pass); }
352
+ .sum .miniBar .f { background: var(--dd-fail); }
353
+ .sum .miniBar .w { background: var(--dd-warn); }
354
+ .sum .miniBar .s { background: var(--dd-skip); }
355
+ .sum .corner-stripe {
356
+ position: absolute; top: 0; left: 0; bottom: 0; width: 3px;
357
+ }
358
+ .sum.pass .corner-stripe { background: var(--dd-pass); }
359
+ .sum.fail .corner-stripe { background: var(--dd-fail); }
360
+ .sum.warn .corner-stripe { background: var(--dd-warn); }
361
+ .sum.skip .corner-stripe { background: var(--dd-skip); }
362
+
363
+ /* Toolbar */
364
+ .toolbar {
365
+ display: flex; gap: 8px; align-items: center;
366
+ margin: 28px 0 14px;
367
+ flex-wrap: wrap;
368
+ }
369
+ .toolbar h2 {
370
+ margin: 0 8px 0 0;
371
+ font-size: 15px; font-weight: 700; letter-spacing: -0.005em;
372
+ color: var(--fg1);
373
+ }
374
+ .toolbar .count {
375
+ font-family: var(--font-mono); font-size: 12px; color: var(--fg3);
376
+ margin-right: auto;
377
+ }
378
+ .filter {
379
+ display: inline-flex; align-items: center; gap: 6px;
380
+ padding: 6px 10px; border-radius: 999px;
381
+ border: 1px solid var(--border-subtle);
382
+ background: var(--dd-paper);
383
+ font-size: 12px; font-weight: 600; color: var(--fg2);
384
+ font-family: var(--font-mono);
385
+ cursor: pointer; transition: background .12s, border-color .12s, color .12s;
386
+ letter-spacing: 0.04em;
387
+ }
388
+ .filter:hover { border-color: var(--border-strong); color: var(--fg1); }
389
+ .filter .d {
390
+ width: 8px; height: 8px; border-radius: 999px;
391
+ }
392
+ .filter.pass .d { background: var(--dd-pass); }
393
+ .filter.fail .d { background: var(--dd-fail); }
394
+ .filter.warn .d { background: var(--dd-warn); }
395
+ .filter.skip .d { background: var(--dd-skip); }
396
+ .filter.all .d { background: var(--fg2); }
397
+ .filter.active.pass { background: var(--dd-pass-bg); color: var(--dd-pass); border-color: color-mix(in oklab, var(--dd-pass) 35%, transparent); }
398
+ .filter.active.fail { background: var(--dd-fail-bg); color: var(--dd-fail); border-color: color-mix(in oklab, var(--dd-fail) 35%, transparent); }
399
+ .filter.active.warn { background: var(--dd-warn-bg); color: var(--dd-warn); border-color: color-mix(in oklab, var(--dd-warn) 35%, transparent); }
400
+ .filter.active.skip { background: var(--dd-skip-bg); color: var(--dd-skip); border-color: var(--border-strong); }
401
+ .filter.active.all { background: var(--bg3); color: var(--fg1); border-color: var(--border-strong); }
402
+ .toolbar .spacer { width: 1px; height: 22px; background: var(--border-subtle); margin: 0 4px; }
403
+ .toolbar .linkbtn {
404
+ border: 0; background: transparent; color: var(--fg-brand);
405
+ font-size: 13px; font-weight: 600; cursor: pointer; padding: 6px 8px;
406
+ border-radius: 6px;
407
+ }
408
+ .toolbar .linkbtn:hover { background: var(--bg3); }
409
+
410
+ .search-input {
411
+ display: inline-flex; align-items: center; gap: 8px;
412
+ padding: 7px 12px; border-radius: 8px;
413
+ border: 1px solid var(--border-subtle); background: var(--dd-paper);
414
+ color: var(--fg2); font-size: 13px; min-width: 240px;
415
+ }
416
+ .search-input svg { color: var(--fg3); flex-shrink: 0; }
417
+ .search-input input {
418
+ border: 0; background: transparent; outline: none;
419
+ font: inherit; color: var(--fg1); flex: 1; min-width: 0;
420
+ }
421
+ .search-input input::placeholder { color: var(--fg3); }
422
+
423
+ /* Badges */
424
+ .badge {
425
+ display: inline-flex; align-items: center; gap: 6px;
426
+ padding: 3px 10px; border-radius: 999px;
427
+ font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;
428
+ letter-spacing: 0.08em;
429
+ border: 1px solid transparent;
430
+ white-space: nowrap;
431
+ }
432
+ .badge .dot { width: 6px; height: 6px; border-radius: 999px; }
433
+ .badge.pass { background: var(--dd-pass-bg); color: var(--dd-pass); }
434
+ .badge.pass .dot { background: var(--dd-pass); }
435
+ .badge.fail { background: var(--dd-fail-bg); color: var(--dd-fail); }
436
+ .badge.fail .dot { background: var(--dd-fail); }
437
+ .badge.warn { background: var(--dd-warn-bg); color: var(--dd-warn); }
438
+ .badge.warn .dot { background: var(--dd-warn); }
439
+ .badge.skip { background: var(--dd-skip-bg); color: var(--dd-skip); }
440
+ .badge.skip .dot { background: var(--dd-skip); }
441
+
442
+ .tag {
443
+ display: inline-flex; align-items: center;
444
+ padding: 2px 8px; border-radius: 4px;
445
+ font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
446
+ background: var(--bg3); color: var(--dd-gray-900);
447
+ border: 1px solid var(--border-subtle);
448
+ }
449
+
450
+ /* Spec card */
451
+ .spec {
452
+ background: var(--dd-paper);
453
+ border: 1px solid var(--border-subtle);
454
+ border-radius: var(--radius-lg);
455
+ margin-bottom: 12px;
456
+ position: relative;
457
+ overflow: hidden;
458
+ transition: border-color .12s, box-shadow .12s;
459
+ }
460
+ .spec:hover { border-color: var(--border-strong); }
461
+ .spec.open { box-shadow: var(--shadow-sm); }
462
+ .spec .stripe {
463
+ position: absolute; top: 0; left: 0; bottom: 0; width: 4px;
464
+ }
465
+ .spec.pass .stripe { background: var(--dd-pass); }
466
+ .spec.fail .stripe { background: var(--dd-fail); }
467
+ .spec.warn .stripe { background: var(--dd-warn); }
468
+ .spec.skip .stripe { background: var(--dd-skip); }
469
+ .spec-head {
470
+ display: grid;
471
+ grid-template-columns: 20px auto 1fr auto;
472
+ align-items: center;
473
+ gap: 14px;
474
+ padding: 16px 20px 16px 24px;
475
+ cursor: pointer; user-select: none;
476
+ }
477
+ .spec-head .chev {
478
+ color: var(--fg3); font-size: 12px;
479
+ transition: transform .15s;
480
+ }
481
+ .spec.open > .spec-head .chev { transform: rotate(90deg); }
482
+ .spec-head .title-col { min-width: 0; }
483
+ .spec-head .title {
484
+ font-size: 15px; font-weight: 600; color: var(--fg1); letter-spacing: -0.005em;
485
+ display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
486
+ }
487
+ .spec-head .desc {
488
+ margin-top: 3px; font-size: 13px; color: var(--fg2); line-height: 1.4;
489
+ max-width: 78ch;
490
+ }
491
+ .spec-head .path {
492
+ font-family: var(--font-mono); font-size: 12px; color: var(--fg3);
493
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
494
+ }
495
+ .spec-head .metrics {
496
+ display: flex; align-items: center; gap: 14px;
497
+ font-family: var(--font-mono); font-size: 12px; color: var(--fg3);
498
+ }
499
+ .spec-head .metrics .m { display: inline-flex; align-items: center; gap: 5px; }
500
+ .spec-head .metrics .m.pass { color: var(--dd-pass); }
501
+ .spec-head .metrics .m.fail { color: var(--dd-fail); }
502
+ .spec-head .metrics .m.warn { color: var(--dd-warn); }
503
+ .spec-head .metrics .m.skip { color: var(--dd-skip); }
504
+ .spec-head .metrics .sep {
505
+ width: 1px; height: 14px; background: var(--border-subtle);
506
+ }
507
+ .spec-body {
508
+ border-top: 1px solid var(--border-subtle);
509
+ padding: 6px 0 10px 0;
510
+ background: var(--bg2);
511
+ }
512
+
513
+ /* Test row */
514
+ .test {
515
+ background: var(--dd-paper);
516
+ border: 1px solid var(--border-subtle);
517
+ border-radius: var(--radius-md);
518
+ margin: 8px 18px 8px 38px;
519
+ overflow: hidden;
520
+ position: relative;
521
+ }
522
+ .test::before {
523
+ content: ""; position: absolute; top: 0; left: 0; bottom: 0; width: 3px;
524
+ }
525
+ .test.pass::before { background: var(--dd-pass); }
526
+ .test.fail::before { background: var(--dd-fail); }
527
+ .test.warn::before { background: var(--dd-warn); }
528
+ .test.skip::before { background: var(--dd-skip); }
529
+ .test-head {
530
+ display: grid;
531
+ grid-template-columns: 16px auto 1fr auto;
532
+ align-items: center;
533
+ gap: 12px;
534
+ padding: 11px 16px 11px 18px;
535
+ cursor: pointer; user-select: none;
536
+ }
537
+ .test-head .chev { color: var(--fg3); font-size: 11px; transition: transform .15s; }
538
+ .test.open > .test-head .chev { transform: rotate(90deg); }
539
+ .test-head .title {
540
+ font-size: 14px; font-weight: 600; color: var(--fg1);
541
+ display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
542
+ }
543
+ .test-head .desc {
544
+ margin-top: 2px; font-size: 12.5px; color: var(--fg2); line-height: 1.4;
545
+ }
546
+ .test-head .metrics {
547
+ display: flex; align-items: center; gap: 10px;
548
+ font-family: var(--font-mono); font-size: 11.5px; color: var(--fg3);
549
+ }
550
+ .test-body {
551
+ border-top: 1px solid var(--border-subtle);
552
+ padding: 0;
553
+ }
554
+
555
+ /* Context block */
556
+ .context {
557
+ border-bottom: 1px solid var(--border-subtle);
558
+ }
559
+ .context:last-child { border-bottom: 0; }
560
+ .context-head {
561
+ display: grid;
562
+ grid-template-columns: auto auto 1fr auto;
563
+ align-items: center;
564
+ gap: 10px;
565
+ padding: 10px 16px;
566
+ background: var(--bg2);
567
+ font-family: var(--font-mono); font-size: 12px; color: var(--fg2);
568
+ }
569
+ .context-head .what { color: var(--fg1); font-weight: 600; }
570
+ .context-head .meta {
571
+ display: inline-flex; align-items: center; gap: 8px;
572
+ color: var(--fg1); font-weight: 600;
573
+ }
574
+ .context-head .meta svg { color: var(--fg2); }
575
+
576
+ /* Steps */
577
+ .steps { padding: 0 8px 8px 8px; background: var(--dd-paper); }
578
+ .step {
579
+ display: grid;
580
+ grid-template-columns: 16px 88px 120px 1fr auto;
581
+ align-items: center;
582
+ gap: 12px;
583
+ padding: 10px 10px 10px 12px;
584
+ border-bottom: 1px dashed var(--border-subtle);
585
+ cursor: pointer;
586
+ user-select: none;
587
+ font-size: 13.5px;
588
+ transition: background .08s;
589
+ position: relative;
590
+ }
591
+ .step:last-of-type { border-bottom: 0; }
592
+ .step:hover { background: var(--bg2); }
593
+ .step.pass::before, .step.fail::before, .step.warn::before, .step.skip::before {
594
+ content: ""; position: absolute; left: 0; top: 8px; bottom: 8px;
595
+ width: 2px; border-radius: 2px;
596
+ }
597
+ .step.pass::before { background: var(--dd-pass); opacity: .55; }
598
+ .step.fail::before { background: var(--dd-fail); opacity: .9; }
599
+ .step.warn::before { background: var(--dd-warn); opacity: .9; }
600
+ .step.skip::before { background: var(--dd-skip); opacity: .55; }
601
+ .step .chev { color: var(--fg3); font-size: 10px; transition: transform .15s; }
602
+ .step.open .chev { transform: rotate(90deg); }
603
+ .step .desc {
604
+ color: var(--fg1); min-width: 0;
605
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
606
+ }
607
+ .step.has-fail .desc, .step.has-warn .desc { font-weight: 500; }
608
+ .step .dur {
609
+ font-family: var(--font-mono); font-size: 11.5px; color: var(--fg3);
610
+ }
611
+
612
+ /* Step detail */
613
+ .step-detail {
614
+ grid-column: 1 / -1;
615
+ margin: 2px 0 12px 12px;
616
+ padding: 12px 14px;
617
+ border: 1px solid var(--border-subtle);
618
+ border-radius: var(--radius-md);
619
+ background: var(--bg2);
620
+ display: grid;
621
+ grid-template-columns: 1fr;
622
+ gap: 12px;
623
+ cursor: default;
624
+ }
625
+ .step-detail .result-note {
626
+ display: grid; grid-template-columns: auto 1fr; gap: 10px;
627
+ align-items: start;
628
+ padding: 10px 12px;
629
+ border-radius: var(--radius-sm);
630
+ background: var(--dd-paper);
631
+ border: 1px solid var(--border-subtle);
632
+ }
633
+ .step-detail .result-note svg { margin-top: 2px; flex-shrink: 0; }
634
+ .step-detail.fail .result-note { border-color: color-mix(in oklab, var(--dd-fail) 35%, var(--border-subtle)); background: var(--dd-fail-bg); }
635
+ .step-detail.fail .result-note svg { color: var(--dd-fail); }
636
+ .step-detail.warn .result-note { border-color: color-mix(in oklab, var(--dd-warn) 35%, var(--border-subtle)); background: var(--dd-warn-bg); }
637
+ .step-detail.warn .result-note svg { color: var(--dd-warn); }
638
+ .step-detail.pass .result-note { background: var(--dd-paper); }
639
+ .step-detail.pass .result-note svg { color: var(--dd-pass); }
640
+ .step-detail.skip .result-note svg { color: var(--dd-skip); }
641
+ .step-detail .result-note .body { font-size: 13px; line-height: 1.55; color: var(--fg1); word-break: break-word; }
642
+ .step-detail .result-note .body code { font-family: var(--font-mono); font-size: 0.92em; background: rgba(0,0,0,0.06); padding: 1px 5px; border-radius: 3px; }
643
+
644
+ /* Detail grid */
645
+ .detail-grid {
646
+ display: grid; gap: 12px;
647
+ grid-template-columns: 1fr 1fr;
648
+ }
649
+ @media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }
650
+
651
+ .detail-panel {
652
+ border: 1px solid var(--border-subtle);
653
+ border-radius: var(--radius-sm);
654
+ background: var(--dd-paper);
655
+ overflow: hidden;
656
+ }
657
+ .detail-panel .dp-head {
658
+ display: flex; align-items: center; justify-content: space-between;
659
+ padding: 8px 12px; font-family: var(--font-mono); font-size: 11px;
660
+ color: var(--fg3); letter-spacing: 0.06em; text-transform: uppercase;
661
+ background: var(--bg2); border-bottom: 1px solid var(--border-subtle);
662
+ }
663
+ .detail-panel .dp-head .copy-btn {
664
+ border: 0; background: transparent; color: var(--fg3);
665
+ font-size: 11px; cursor: pointer; padding: 2px 6px; border-radius: 4px;
666
+ font-family: var(--font-mono); letter-spacing: 0;
667
+ }
668
+ .detail-panel .dp-head .copy-btn:hover { background: var(--bg3); color: var(--fg1); }
669
+ .detail-panel pre {
670
+ margin: 0;
671
+ padding: 12px 14px;
672
+ font-family: var(--font-mono); font-size: 12px; line-height: 1.55;
673
+ color: var(--fg1);
674
+ white-space: pre-wrap; word-break: break-word;
675
+ max-height: 380px; overflow: auto;
676
+ }
677
+ .detail-panel pre .k { color: #2563A0; }
678
+ .detail-panel pre .s { color: #4d7e2c; }
679
+ .detail-panel pre .n { color: #9a5a1a; }
680
+ .detail-panel pre .b { color: #5B616B; }
681
+
682
+ /* Key-value list */
683
+ .kv {
684
+ font-family: var(--font-mono); font-size: 12px;
685
+ padding: 10px 12px;
686
+ display: grid; grid-template-columns: auto 1fr; gap: 4px 12px;
687
+ }
688
+ .kv .k { color: var(--fg3); }
689
+ .kv .v { color: var(--fg1); word-break: break-word; }
690
+
691
+ /* Media panel */
692
+ .media-panel {
693
+ border: 1px solid var(--border-subtle);
694
+ border-radius: var(--radius-sm);
695
+ background: var(--dd-paper);
696
+ overflow: hidden;
697
+ grid-column: 1 / -1;
698
+ }
699
+ .media-panel .dp-head { justify-content: space-between; }
700
+ .media-panel .mp-body {
701
+ padding: 10px;
702
+ display: flex; flex-wrap: wrap; gap: 10px;
703
+ background: #fafbfc;
704
+ }
705
+ .media-thumb {
706
+ position: relative;
707
+ border: 1px solid var(--border-subtle);
708
+ border-radius: var(--radius-xs);
709
+ overflow: hidden;
710
+ background: var(--dd-ink);
711
+ cursor: zoom-in;
712
+ transition: transform .15s, box-shadow .15s;
713
+ }
714
+ .media-thumb:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
715
+ .media-thumb img, .media-thumb video {
716
+ display: block; max-width: 360px; max-height: 240px;
717
+ width: 100%; object-fit: contain; background: #000;
718
+ }
719
+ .media-thumb .cap {
720
+ padding: 6px 10px;
721
+ font-family: var(--font-mono); font-size: 11px;
722
+ color: var(--fg3); background: var(--bg2);
723
+ border-top: 1px solid var(--border-subtle);
724
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 360px;
725
+ }
726
+ .media-thumb .kind {
727
+ position: absolute; top: 6px; left: 6px;
728
+ font-family: var(--font-mono); font-size: 10px; font-weight: 700;
729
+ padding: 2px 6px; border-radius: 3px;
730
+ background: rgba(13,14,17,0.78); color: #E6E8EC;
731
+ letter-spacing: 0.05em;
732
+ }
733
+ .media-thumb .changed-flag {
734
+ position: absolute; top: 6px; right: 6px;
735
+ font-family: var(--font-mono); font-size: 10px; font-weight: 700;
736
+ padding: 2px 6px; border-radius: 3px;
737
+ background: var(--dd-green-bright); color: #07150C;
738
+ letter-spacing: 0.05em;
739
+ }
740
+
741
+ /* Lightbox */
742
+ .lightbox {
743
+ position: fixed; inset: 0; background: rgba(13,14,17,0.88);
744
+ display: flex; align-items: center; justify-content: center;
745
+ z-index: 100; padding: 40px;
746
+ backdrop-filter: blur(4px);
747
+ }
748
+ .lightbox img, .lightbox video { max-width: 100%; max-height: 100%; background: #000; }
749
+ .lightbox .close {
750
+ position: absolute; top: 18px; right: 18px;
751
+ width: 36px; height: 36px; border-radius: 8px;
752
+ display: inline-flex; align-items: center; justify-content: center;
753
+ background: #15171B; color: #E6E8EC; border: 1px solid #2A2D34;
754
+ cursor: pointer;
755
+ }
756
+ .lightbox .cap {
757
+ position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
758
+ font-family: var(--font-mono); font-size: 12px; color: #B7BCC5;
759
+ background: #15171B; padding: 6px 12px; border-radius: 6px;
760
+ }
761
+
762
+ /* Empty state */
763
+ .empty {
764
+ padding: 40px; text-align: center;
765
+ color: var(--fg3); font-size: 14px;
766
+ border: 1px dashed var(--border-subtle);
767
+ border-radius: var(--radius-md); background: var(--dd-paper);
768
+ }
769
+
770
+ /* Dark mode */
771
+ body.dark {
772
+ --fg1: #F1F3F6;
773
+ --fg2: #D2D6DD;
774
+ --fg3: #B0B6C0;
775
+ --fg-brand: var(--dd-green-bright);
776
+ --bg1: #0D0E11;
777
+ --bg2: #15171B;
778
+ --bg3: #1A1C21;
779
+ --border-subtle: #2A2D34;
780
+ --border-strong: #3A3F47;
781
+ --dd-pass: #4FC285;
782
+ --dd-pass-bg: rgba(62,177,110,0.18);
783
+ --dd-fail: #FF6A5E;
784
+ --dd-fail-bg: rgba(255,106,94,0.15);
785
+ --dd-warn: #F2B53A;
786
+ --dd-warn-bg: rgba(242,181,58,0.15);
787
+ --dd-skip: #B0B6C0;
788
+ --dd-skip-bg: rgba(176,182,192,0.12);
789
+ background: #0D0E11;
790
+ color: #F1F3F6;
791
+ }
792
+ body.dark .spec, body.dark .test, body.dark .detail-panel,
793
+ body.dark .media-panel, body.dark .verdict-card, body.dark .sum {
794
+ background: #15171B; border-color: #2A2D34; color: #E6E8EC;
795
+ }
796
+ body.dark .spec-body { background: #0D0E11; }
797
+ body.dark .test { background-color: rgba(0,0,0,0) !important; }
798
+ body.dark .steps { background-color: rgba(0,0,0,0) !important; }
799
+ body.dark .test-head .title, body.dark .spec-head .title, body.dark .step .desc { color: #F1F3F6 !important; }
800
+ body.dark .step .desc > span:first-child { color: #B0B6C0 !important; }
801
+ body.dark .spec-head .desc, body.dark .test-head .desc { color: #D2D6DD; }
802
+ body.dark .spec-head .path { color: #B0B6C0; }
803
+ body.dark .context-head { background: #0D0E11; border-color: #2A2D34; color: #D2D6DD; }
804
+ body.dark .context-head .meta { color: #F1F3F6; }
805
+ body.dark .context-head .meta svg { color: #D2D6DD; }
806
+ body.dark .step { border-color: rgba(42,45,52,0.6); }
807
+ body.dark .step:hover { background: #1A1C21; }
808
+ body.dark .step-detail { background: #0D0E11; border-color: #2A2D34; }
809
+ body.dark .step-detail .result-note { background: #15171B; border-color: #2A2D34; }
810
+ body.dark .step-detail.fail .result-note { background: #2a1414; border-color: #612323; }
811
+ body.dark .step-detail.warn .result-note { background: #2a210a; border-color: #5a4510; }
812
+ body.dark .detail-panel .dp-head { background: #0D0E11; border-color: #2A2D34; color: #B0B6C0; }
813
+ body.dark .detail-panel pre { color: #E6E8EC; }
814
+ body.dark .search-input { background: #15171B; border-color: #3A3F47; color: #F1F3F6; }
815
+ body.dark .search-input input { color: #F1F3F6; }
816
+ body.dark .search-input input::placeholder { color: #B0B6C0; }
817
+ body.dark .filter { background: #15171B; border-color: #3A3F47; color: #D2D6DD; }
818
+ body.dark main { color: #E6E8EC; }
819
+ body.dark .tag { background: #1A1C21; color: #F1F3F6; border-color: #3A3F47; }
820
+ body.dark .empty { background: #15171B; border-color: #2A2D34; color: #B0B6C0; }
821
+ body.dark .media-panel .mp-body { background: #0D0E11; }
822
+ body.dark .media-thumb .cap { background: #15171B; border-color: #2A2D34; color: #B0B6C0; }
823
+ body.dark .badge.pass { background: rgba(62,177,110,0.18); color: #6FD69A; }
824
+ body.dark .badge.pass .dot { background: #6FD69A; }
825
+ body.dark .badge.fail { background: rgba(255,106,94,0.18); color: #FF8A80; }
826
+ body.dark .badge.fail .dot { background: #FF8A80; }
827
+ body.dark .badge.warn { background: rgba(242,181,58,0.18); color: #F2B53A; }
828
+ body.dark .badge.warn .dot { background: #F2B53A; }
829
+ body.dark .badge.skip { background: rgba(176,182,192,0.12); color: #D2D6DD; }
830
+ body.dark .badge.skip .dot { background: #D2D6DD; }
831
+ body.dark .metastrip { color: #D2D6DD; }
832
+ body.dark .metastrip .m .k { color: #B0B6C0; }
833
+ body.dark .metastrip .m .v { color: #F1F3F6; }
834
+ body.dark .hdr-title .eyebrow { color: #B0B6C0; }
835
+ body.dark .hdr-title h1 .sub { color: #B0B6C0; }
836
+ body.dark .detail-panel pre .k { color: #6FB8FF; }
837
+ body.dark .detail-panel pre .s { color: #B8D88A; }
838
+ body.dark .detail-panel pre .n { color: #E7A45B; }
839
+ body.dark .detail-panel pre .b { color: #8A909B; }
840
+
841
+ @media print {
842
+ body { background: #fff; }
843
+ .hdr, .toolbar, .metastrip { display: none !important; }
844
+ .spec, .test { break-inside: avoid; box-shadow: none; }
845
+ .spec-body, .test-body, .step-detail { display: block !important; }
846
+ }
847
+ `;
848
+ const JS_CONTENT = `
849
+ (function() {
850
+ "use strict";
851
+
852
+ var report = window.REPORT_DATA;
853
+ function isValidReport(r) {
854
+ return r && r.summary && r.summary.specs && Array.isArray(r.specs);
855
+ }
856
+ if (!isValidReport(report)) {
857
+ var root = document.getElementById("root");
858
+ root.innerHTML = '';
859
+ var msg = document.createElement("div");
860
+ msg.style.cssText = "max-width:800px;margin:64px auto;padding:24px;font-family:system-ui,sans-serif;";
861
+ var h = document.createElement("h2");
862
+ h.textContent = "Unsupported report format";
863
+ h.style.cssText = "font-size:20px;margin:0 0 8px 0;";
864
+ msg.appendChild(h);
865
+ var p = document.createElement("p");
866
+ p.textContent = "The provided data does not match the expected Doc Detective report shape (summary.specs + specs[]). Raw data below:";
867
+ p.style.cssText = "color:#4A5058;margin:0 0 16px 0;";
868
+ msg.appendChild(p);
869
+ var pre = document.createElement("pre");
870
+ pre.textContent = JSON.stringify(report, null, 2);
871
+ pre.style.cssText = "background:#F1F3F6;border:1px solid #E2E5EA;border-radius:8px;padding:12px;overflow:auto;font-size:12px;line-height:1.5;max-height:70vh;";
872
+ msg.appendChild(pre);
873
+ root.appendChild(msg);
874
+ return;
875
+ }
876
+
877
+ var STATUS_ORDER = ["FAIL", "WARNING", "PASS", "SKIPPED"];
878
+ var STATUS_META = {
879
+ PASS: { slug: "pass", label: "PASS" },
880
+ FAIL: { slug: "fail", label: "FAIL" },
881
+ WARNING: { slug: "warn", label: "WARNING" },
882
+ SKIPPED: { slug: "skip", label: "SKIPPED" }
883
+ };
884
+
885
+ function statusSlug(s) { return (STATUS_META[s] && STATUS_META[s].slug) || "skip"; }
886
+
887
+ function fmtDuration(ms) {
888
+ if (ms == null) return "\\u2014";
889
+ if (ms < 1000) return ms + " ms";
890
+ if (ms < 60000) return (ms / 1000).toFixed(2).replace(/\\.?0+$/, "") + " s";
891
+ var m = Math.floor(ms / 60000), s = Math.round((ms % 60000) / 1000);
892
+ return m + "m " + s + "s";
893
+ }
894
+
895
+ function esc(s) { var d = document.createElement("div"); d.textContent = s == null ? "" : String(s); return d.innerHTML; }
896
+ function escAttr(s) { return String(s == null ? "" : s).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/'/g,"&#39;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
897
+
898
+ function wireDisclosure(node, expanded, toggle) {
899
+ node.setAttribute("role", "button");
900
+ node.setAttribute("tabindex", "0");
901
+ node.setAttribute("aria-expanded", String(expanded));
902
+ node.onclick = toggle;
903
+ node.onkeydown = function(e) {
904
+ if (e.key === "Enter" || e.key === " ") {
905
+ e.preventDefault();
906
+ toggle(e);
907
+ }
908
+ };
909
+ }
910
+
911
+ function hlJson(json) {
912
+ return esc(json).replace(
913
+ /("(?:[^"\\\\\\\\]|\\\\\\\\.)*")(\\s*:)?|\\b(true|false|null)\\b|-?\\d+\\.?\\d*(?:[eE][+-]?\\d+)?/g,
914
+ function(m, str, colon, bool) {
915
+ if (str) return colon ? '<span class="k">' + str + '</span>' + colon : '<span class="s">' + str + '</span>';
916
+ if (bool) return '<span class="b">' + m + '</span>';
917
+ return '<span class="n">' + m + '</span>';
918
+ }
919
+ );
920
+ }
921
+
922
+ var ACTIONS = ["goTo","find","click","screenshot","checkLink","httpRequest",
923
+ "runShell","runCode","type","typeKeys","wait","record","stopRecord",
924
+ "loadVariables","loadCookie","saveCookie","dragAndDrop","moveTo","scroll"];
925
+ function actionKey(step) {
926
+ for (var i = 0; i < ACTIONS.length; i++) if (ACTIONS[i] in step) return ACTIONS[i];
927
+ return "step";
928
+ }
929
+
930
+ var ICON = {
931
+ chevron: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M4.5 2.5L8 6L4.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
932
+ search: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.5"/><path d="M9.5 9.5L13 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
933
+ print: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3.5 5V1.5h7V5M3.5 10H2a.5.5 0 01-.5-.5v-4A.5.5 0 012 5h10a.5.5 0 01.5.5v4a.5.5 0 01-.5.5h-1.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/><rect x="3.5" y="8" width="7" height="4.5" rx=".5" stroke="currentColor" stroke-width="1.2"/></svg>',
934
+ download: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1.5v8M3.5 6.5L7 10l3.5-3.5M2 12.5h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
935
+ sun: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="2.5" stroke="currentColor" stroke-width="1.3"/><path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.75 2.75l1.06 1.06M10.19 10.19l1.06 1.06M11.25 2.75l-1.06 1.06M3.81 10.19l-1.06 1.06" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
936
+ moon: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M12.5 7.5a5.5 5.5 0 01-6-6 5.5 5.5 0 106 6z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
937
+ monitor: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2" width="11" height="8" rx="1" stroke="currentColor" stroke-width="1.3"/><path d="M5 12.5h4M7 10v2.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
938
+ close: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>',
939
+ check: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2.5 6.5L5 9l4.5-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
940
+ warn: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1.5L1 12.5h12L7 1.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M7 6v3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="7" cy="10.5" r=".6" fill="currentColor"/></svg>',
941
+ xmark: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 5l4 4M9 5l-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
942
+ skip: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l4 4-4 4M8 3v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
943
+ copy: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><rect x="4" y="4" width="6.5" height="6.5" rx="1" stroke="currentColor" stroke-width="1.2"/><path d="M8 4V2.5A1 1 0 007 1.5H2.5A1 1 0 001.5 2.5V7a1 1 0 001 1H4" stroke="currentColor" stroke-width="1.2"/></svg>',
944
+ chip: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1"/><path d="M5 1v2M7 1v2M5 9v2M7 9v2M1 5h2M1 7h2M9 5h2M9 7h2" stroke="currentColor" stroke-width="1" stroke-linecap="round"/></svg>',
945
+ file: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M7 1H3.5A1 1 0 002.5 2v8a1 1 0 001 1h5a1 1 0 001-1V3.5L7 1z" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/><path d="M7 1v2.5h2.5" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/></svg>',
946
+ finger: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M8 5.5V4a1 1 0 00-2 0v3L4.5 5.5a1 1 0 00-1.4 1.4L6 10h3l1.5-3.5V5.5A1 1 0 009 4.5" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/></svg>',
947
+ expand: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M4 5l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 8l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
948
+ collapse: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M4 9l3-3 3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 6l3-3 3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'
949
+ };
950
+
951
+ var LOGO_SVG = '<svg width="30" height="30" viewBox="0 0 1256 1256" fill="none"><path d="M378.014 0.515785L828.006 0.5C848.561 0.499279 868.275 8.66442 882.809 23.1992L1232.81 373.199C1247.34 387.734 1255.51 407.446 1255.51 428.001L1255.5 1178C1255.5 1220.8 1220.8 1255.5 1178 1255.5H378C335.198 1255.5 300.5 1220.8 300.5 1178V997.53C129.173 961.767 0.5 809.934 0.5 628C0.5 446.064 129.176 294.23 300.505 258.469L300.516 78.0107C300.519 35.2117 335.215 0.517288 378.014 0.515785Z" fill="white"/><path fill-rule="evenodd" clip-rule="evenodd" d="M378.015 40.5158L828.007 40.5C837.953 40.4997 847.492 44.4506 854.525 51.4835L1204.53 401.483C1211.56 408.516 1215.51 418.055 1215.51 428L1215.5 1178C1215.5 1198.71 1198.71 1215.5 1178 1215.5H378C357.289 1215.5 340.5 1198.71 340.5 1178V963.44C171.752 944.786 40.5 801.721 40.5 628C40.5 454.278 171.753 311.213 340.502 292.56L340.516 78.0133C340.518 57.3041 357.306 40.5165 378.015 40.5158ZM415.502 292.56C584.249 311.215 715.5 454.28 715.5 628C715.5 707.673 687.857 780.941 641.694 838.661L804.516 1001.48C819.161 1016.13 819.161 1039.87 804.516 1054.52C789.872 1069.16 766.128 1069.16 751.484 1054.52L588.661 891.694C540.123 930.513 480.59 956.236 415.5 963.438V1140.5H1140.5L1140.51 465.5H828.009C807.298 465.5 790.509 448.711 790.509 428V115.501L415.514 115.514L415.502 292.56ZM865.509 168.533L1087.48 390.5H865.509V168.533ZM378 365.5C233.025 365.5 115.5 483.025 115.5 628C115.5 772.975 233.025 890.5 378 890.5C450.498 890.5 516.071 861.16 563.616 813.616C611.16 766.071 640.5 700.498 640.5 628C640.5 483.025 522.975 365.5 378 365.5Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M165.5 628C165.5 510.639 260.639 415.5 378 415.5C495.361 415.5 590.5 510.639 590.5 628C590.5 686.689 566.748 739.772 528.26 778.26C489.772 816.748 436.689 840.5 378 840.5C260.639 840.5 165.5 745.361 165.5 628ZM283 525.5C262.289 525.5 245.5 542.289 245.5 563C245.5 583.711 262.289 600.5 283 600.5H473C493.711 600.5 510.5 583.711 510.5 563C510.5 542.289 493.711 525.5 473 525.5H283ZM283 655.5C262.289 655.5 245.5 672.289 245.5 693C245.5 713.711 262.289 730.5 383 730.5H383C403.711 730.5 420.5 713.711 420.5 693C420.5 672.289 403.711 655.5 383 655.5H283Z" fill="#4B9A47"/></svg>';
952
+
953
+ function statusIcon(slug) {
954
+ if (slug === "pass") return ICON.check;
955
+ if (slug === "fail") return ICON.xmark;
956
+ if (slug === "warn") return ICON.warn;
957
+ return ICON.skip;
958
+ }
959
+
960
+ // State
961
+ var state = {
962
+ statusFilters: new Set(),
963
+ query: "",
964
+ openSpecs: {},
965
+ openTests: {},
966
+ openSteps: {},
967
+ themeMode: "system",
968
+ lightbox: null
969
+ };
970
+
971
+ // Detect system dark preference
972
+ var mql = window.matchMedia ? window.matchMedia("(prefers-color-scheme: dark)") : null;
973
+ function isDark() {
974
+ if (state.themeMode === "dark") return true;
975
+ if (state.themeMode === "light") return false;
976
+ return !!(mql && mql.matches);
977
+ }
978
+ function applyTheme() {
979
+ document.body.classList.toggle("dark", isDark());
980
+ }
981
+ if (mql) {
982
+ var onChange = function() { applyTheme(); };
983
+ mql.addEventListener ? mql.addEventListener("change", onChange) : mql.addListener(onChange);
984
+ }
985
+
986
+ // Default expansion: open failures/warnings
987
+ function defaultOpen(node) {
988
+ return node.result === "FAIL" || node.result === "WARNING";
989
+ }
990
+ function isSpecOpen(id, spec) { return id in state.openSpecs ? state.openSpecs[id] : defaultOpen(spec); }
991
+ function isTestOpen(id, test) { return id in state.openTests ? state.openTests[id] : defaultOpen(test); }
992
+
993
+ // Helpers
994
+ function el(tag, cls, html) {
995
+ var e = document.createElement(tag);
996
+ if (cls) e.className = cls;
997
+ if (html != null) e.innerHTML = html;
998
+ return e;
999
+ }
1000
+
1001
+ function badge(status) {
1002
+ var m = STATUS_META[status] || STATUS_META.SKIPPED;
1003
+ return '<span class="badge ' + m.slug + '"><span class="dot"></span>' + m.label + '</span>';
1004
+ }
1005
+
1006
+ function tag(text) { return '<span class="tag">' + esc(text) + '</span>'; }
1007
+
1008
+ function metric(slug, n, label) {
1009
+ if (!n) return "";
1010
+ return '<span class="m ' + slug + '"><span class="dot" style="display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--dd-' + slug + ')"></span>' + n + (label ? " " + label : "") + '</span>';
1011
+ }
1012
+
1013
+ function countTree(spec) {
1014
+ var tests = 0, contexts = 0, steps = 0;
1015
+ var sc = { pass: 0, fail: 0, warning: 0, skipped: 0 };
1016
+ (spec.tests || []).forEach(function(t) {
1017
+ tests++;
1018
+ (t.contexts || []).forEach(function(c) {
1019
+ contexts++;
1020
+ (c.steps || []).forEach(function(s) {
1021
+ steps++;
1022
+ var k = (s.result || "").toLowerCase();
1023
+ if (k in sc) sc[k]++;
1024
+ });
1025
+ });
1026
+ });
1027
+ return { tests: tests, contexts: contexts, steps: steps, stepCounts: sc };
1028
+ }
1029
+
1030
+ function collectMedia(step) {
1031
+ var media = [];
1032
+ var outs = step.outputs || {};
1033
+ if (step.screenshot && (step.screenshot.path || outs.screenshotPath)) {
1034
+ media.push({ kind: "image", path: outs.screenshotPath || step.screenshot.path, changed: outs.changed });
1035
+ }
1036
+ if (outs.screenshotPath && !media.find(function(m) { return m.path === outs.screenshotPath; })) {
1037
+ media.push({ kind: "image", path: outs.screenshotPath, changed: outs.changed });
1038
+ }
1039
+ if (step.record && (step.record.path || outs.recordingPath)) {
1040
+ media.push({ kind: "video", path: outs.recordingPath || step.record.path });
1041
+ }
1042
+ if (step.stopRecord && outs.recordingPath) {
1043
+ media.push({ kind: "video", path: outs.recordingPath });
1044
+ }
1045
+ return media;
1046
+ }
1047
+
1048
+ // Build header
1049
+ function buildHeader() {
1050
+ var s = report.summary.specs;
1051
+ var tot = s.pass + s.fail + s.warning + s.skipped;
1052
+ var pctPass = tot ? (s.pass / tot * 100) + "%" : "0%";
1053
+ var pctFailEnd = tot ? ((s.pass + s.fail) / tot * 100) + "%" : "0%";
1054
+ var pctWarnEnd = tot ? ((s.pass + s.fail + s.warning) / tot * 100) + "%" : "0%";
1055
+
1056
+ var meta = report.meta || {};
1057
+ var started = meta.startedAt ? new Date(meta.startedAt) : null;
1058
+ var reportIdShort = (report.reportId || "").slice(0, 8);
1059
+
1060
+ var hdr = el("header", "hdr");
1061
+ hdr.style.setProperty("--pct-pass", pctPass);
1062
+ hdr.style.setProperty("--pct-fail-end", pctFailEnd);
1063
+ hdr.style.setProperty("--pct-warn-end", pctWarnEnd);
1064
+
1065
+ var inner = el("div", "hdr-inner");
1066
+
1067
+ // Brand
1068
+ inner.innerHTML = '<div class="brand">' + LOGO_SVG +
1069
+ '<div class="wm">Doc Detective <span class="tag">/ report</span></div>' +
1070
+ '<div class="divider"></div></div>';
1071
+
1072
+ // Title
1073
+ var title = el("div", "hdr-title");
1074
+ title.innerHTML = '<div class="eyebrow">Report' + (reportIdShort ? " \\u00B7 " + esc(reportIdShort) : "") + '</div>' +
1075
+ '<h1>Test run<span class="sub"> \\u00B7 ' + (started ? started.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }) : "\\u2014") + '</span></h1>';
1076
+ inner.appendChild(title);
1077
+
1078
+ // Actions
1079
+ var actions = el("div", "hdr-actions");
1080
+
1081
+ var themeBtn = el("button", "hdr-btn");
1082
+ function updateThemeBtn() {
1083
+ var m = state.themeMode;
1084
+ themeBtn.innerHTML = (m === "dark" ? ICON.moon : m === "light" ? ICON.sun : ICON.monitor) +
1085
+ '<span style="text-transform:capitalize">' + m + '</span>';
1086
+ themeBtn.title = "Theme: " + m;
1087
+ }
1088
+ updateThemeBtn();
1089
+ themeBtn.onclick = function() {
1090
+ var order = ["system", "light", "dark"];
1091
+ state.themeMode = order[(order.indexOf(state.themeMode) + 1) % 3];
1092
+ updateThemeBtn();
1093
+ applyTheme();
1094
+ };
1095
+ actions.appendChild(themeBtn);
1096
+
1097
+ var printBtn = el("button", "hdr-btn", ICON.print + " Print");
1098
+ printBtn.onclick = function() { window.print(); };
1099
+ actions.appendChild(printBtn);
1100
+
1101
+ var jsonBtn = el("button", "hdr-btn primary", ICON.download + " JSON");
1102
+ jsonBtn.onclick = function() {
1103
+ var blob = new Blob([JSON.stringify(report, null, 2)], { type: "application/json" });
1104
+ var url = URL.createObjectURL(blob);
1105
+ var a = document.createElement("a");
1106
+ a.href = url;
1107
+ a.download = "testResults-" + (report.reportId || "report").slice(0, 8) + ".json";
1108
+ document.body.appendChild(a); a.click(); a.remove();
1109
+ URL.revokeObjectURL(url);
1110
+ };
1111
+ actions.appendChild(jsonBtn);
1112
+
1113
+ inner.appendChild(actions);
1114
+ hdr.appendChild(inner);
1115
+
1116
+ // Meta strip
1117
+ var ms = el("div", "metastrip");
1118
+ var msInner = el("div", "metastrip-inner");
1119
+ var fields = [
1120
+ ["tool", (meta.tool || "doc-detective") + "@" + (meta.version || "\\u2014")],
1121
+ ["runtime", (meta.platform || "\\u2014") + " \\u00B7 node " + (meta.node || "\\u2014")],
1122
+ ["branch", meta.branch || meta.commit ? (meta.branch || "\\u2014") + (meta.commit ? "@" + meta.commit.slice(0, 7) : "") : "\\u2014"],
1123
+ ["actor", meta.actor || "\\u2014"],
1124
+ ["duration", fmtDuration(meta.startedAt && meta.finishedAt ? new Date(meta.finishedAt) - new Date(meta.startedAt) : null)],
1125
+ ["cwd", meta.cwd || "\\u2014"]
1126
+ ];
1127
+ fields.forEach(function(f) {
1128
+ msInner.innerHTML += '<span class="m"><span class="k">' + f[0] + '</span> <span class="v">' + esc(f[1]) + '</span></span>';
1129
+ });
1130
+ ms.appendChild(msInner);
1131
+ hdr.appendChild(ms);
1132
+
1133
+ return hdr;
1134
+ }
1135
+
1136
+ // Verdict banner
1137
+ function buildVerdict() {
1138
+ var s = report.summary.specs;
1139
+ var total = s.pass + s.fail + s.warning + s.skipped;
1140
+ var slug = s.fail ? "fail" : s.warning ? "warn" : s.pass ? "pass" : "skip";
1141
+ var bigNum, note;
1142
+ if (s.fail) {
1143
+ bigNum = s.fail;
1144
+ note = "spec" + (s.fail > 1 ? "s" : "") + " failing";
1145
+ } else if (s.warning) {
1146
+ bigNum = s.warning;
1147
+ note = "warning" + (s.warning > 1 ? "s" : "");
1148
+ } else if (s.pass) {
1149
+ bigNum = s.pass;
1150
+ note = "spec" + (s.pass > 1 ? "s" : "") + " passed";
1151
+ } else {
1152
+ bigNum = 0;
1153
+ note = "specs ran";
1154
+ }
1155
+ var pct = function(n) { return total ? n / total * 100 : 0; };
1156
+
1157
+ var card = el("div", "verdict-card " + slug);
1158
+ card.innerHTML = '<div class="vk">Overall verdict</div>' +
1159
+ '<div class="vv"><div class="big">' + esc(String(bigNum)) + '</div><div class="note">' + esc(note) + '</div></div>' +
1160
+ '<div class="vbar" style="grid-template-columns:' + pct(s.pass) + '% ' + pct(s.fail) + '% ' + pct(s.warning) + '% ' + pct(s.skipped) + '%">' +
1161
+ '<span class="pass"></span><span class="fail"></span><span class="warn"></span><span class="skip"></span></div>';
1162
+ return card;
1163
+ }
1164
+
1165
+ function buildSumTile(label, counts, levelLabel) {
1166
+ var total = counts.pass + counts.fail + counts.warning + counts.skipped;
1167
+ var kind = counts.fail ? "fail" : counts.warning ? "warn" : counts.pass ? "pass" : "skip";
1168
+ var primaryN = kind === "pass" ? counts.pass : kind === "fail" ? counts.fail : kind === "warn" ? counts.warning : counts.skipped;
1169
+ var cols = total
1170
+ ? (counts.pass / total * 100) + "% " + (counts.fail / total * 100) + "% " + (counts.warning / total * 100) + "% " + (counts.skipped / total * 100) + "%"
1171
+ : "1fr";
1172
+
1173
+ var barSegments = total
1174
+ ? '<span class="p"></span><span class="f"></span><span class="w"></span><span class="s"></span>'
1175
+ : '';
1176
+
1177
+ var tile = el("div", "sum " + kind);
1178
+ tile.innerHTML = '<div class="corner-stripe"></div><div><div class="lbl">' + esc(label) + '</div>' +
1179
+ '<div class="row"><div class="num">' + primaryN + '</div><div class="of">of ' + total + " " + esc(levelLabel) + '</div></div></div>' +
1180
+ '<div><div class="miniBar" style="grid-template-columns:' + cols + '">' +
1181
+ barSegments + '</div>' +
1182
+ '<div class="legend">' +
1183
+ '<span><i style="background:var(--dd-pass)"></i>' + counts.pass + ' pass</span>' +
1184
+ '<span><i style="background:var(--dd-fail)"></i>' + counts.fail + ' fail</span>' +
1185
+ '<span><i style="background:var(--dd-warn)"></i>' + counts.warning + ' warn</span>' +
1186
+ '<span><i style="background:var(--dd-skip)"></i>' + counts.skipped + ' skip</span></div></div>';
1187
+ return tile;
1188
+ }
1189
+
1190
+ // Step detail
1191
+ function buildStepDetail(step) {
1192
+ var slug = statusSlug(step.result);
1193
+ var ak = actionKey(step);
1194
+ var media = collectMedia(step);
1195
+ var detail = el("div", "step-detail " + slug);
1196
+ detail.onclick = function(e) { e.stopPropagation(); };
1197
+
1198
+ if (step.resultDescription) {
1199
+ detail.innerHTML += '<div class="result-note">' + statusIcon(slug) +
1200
+ '<div class="body">' + esc(step.resultDescription) + '</div></div>';
1201
+ }
1202
+
1203
+ if (media.length) {
1204
+ var mp = '<div class="media-panel"><div class="dp-head"><span>MEDIA \\u00B7 ' + media.length + ' item' + (media.length > 1 ? 's' : '') + '</span></div><div class="mp-body">';
1205
+ media.forEach(function(m) {
1206
+ mp += '<div class="media-thumb" data-media-path="' + escAttr(m.path || '') + '" data-media-kind="' + escAttr(m.kind) + '">' +
1207
+ '<span class="kind">' + (m.kind === "video" ? "MP4" : "PNG") + '</span>' +
1208
+ (m.changed ? '<span class="changed-flag">UPDATED</span>' : '') +
1209
+ (m.kind === "video"
1210
+ ? '<video src="' + escAttr(m.path || '') + '" muted playsinline preload="metadata"></video>'
1211
+ : '<img src="' + escAttr(m.path || '') + '" alt="' + escAttr(m.path || '') + '" onerror="this.style.display=\\'none\\'"/>') +
1212
+ '<div class="cap">' + esc(m.path || '') + '</div></div>';
1213
+ });
1214
+ mp += '</div></div>';
1215
+ detail.innerHTML += mp;
1216
+ }
1217
+
1218
+ // Input/output panels
1219
+ var rest = Object.assign({}, step);
1220
+ delete rest.result; delete rest.resultDescription; delete rest.stepId; delete rest.outputs; delete rest.description; delete rest.duration;
1221
+ var inputJson = JSON.stringify(rest, null, 2);
1222
+ var outputJson = step.outputs && Object.keys(step.outputs).length ? JSON.stringify(step.outputs, null, 2) : null;
1223
+
1224
+ var grid = el("div", "detail-grid");
1225
+ grid.innerHTML = '<div class="detail-panel"><div class="dp-head"><span>INPUT \\u00B7 ' + esc(ak) + '</span><button class="copy-btn">' + ICON.copy + ' Copy</button></div><pre>' + hlJson(inputJson) + '</pre></div>';
1226
+ if (outputJson) {
1227
+ grid.innerHTML += '<div class="detail-panel"><div class="dp-head"><span>OUTPUTS</span><button class="copy-btn">' + ICON.copy + ' Copy</button></div><pre>' + hlJson(outputJson) + '</pre></div>';
1228
+ }
1229
+ detail.appendChild(grid);
1230
+
1231
+ return detail;
1232
+ }
1233
+
1234
+ // Build step row
1235
+ function stepKey(step, ctxId, idx) {
1236
+ return step.stepId || (ctxId + ":" + idx);
1237
+ }
1238
+
1239
+ function buildStep(step, idx, ctxId, keyIdx) {
1240
+ var slug = statusSlug(step.result);
1241
+ var ak = actionKey(step);
1242
+ var sk = stepKey(step, ctxId, keyIdx == null ? idx : keyIdx);
1243
+ var primary = step.description
1244
+ || (step.goTo && "Go to " + step.goTo)
1245
+ || (step.httpRequest && (step.httpRequest.method || "GET") + " " + (step.httpRequest.url || ""))
1246
+ || (step.runShell && (step.runShell.command || "") + " " + (step.runShell.args || []).join(" "))
1247
+ || step.resultDescription || "(step)";
1248
+
1249
+ var isOpen = !!state.openSteps[sk];
1250
+ var row = el("div", "step " + slug + (isOpen ? " open" : ""));
1251
+ row.innerHTML = '<span class="chev">' + ICON.chevron + '</span>' +
1252
+ badge(step.result) + tag(ak) +
1253
+ '<span class="desc" title="' + escAttr(primary) + '"><span style="color:var(--fg3);font-family:var(--font-mono);font-size:11px;margin-right:8px">' + String(idx + 1).padStart(2, "0") + '</span>' + esc(primary) + '</span>' +
1254
+ '<span class="dur">' + fmtDuration(step.duration) + '</span>';
1255
+
1256
+ if (isOpen) {
1257
+ row.appendChild(buildStepDetail(step));
1258
+ }
1259
+
1260
+ wireDisclosure(row, isOpen, function(e) {
1261
+ if (e && e.target && e.target.closest && e.target.closest(".step-detail")) return;
1262
+ state.openSteps[sk] = !state.openSteps[sk];
1263
+ render();
1264
+ });
1265
+
1266
+ return row;
1267
+ }
1268
+
1269
+ // Context block
1270
+ function buildContext(ctx) {
1271
+ var browser = ctx.browser && ctx.browser.name;
1272
+ var headless = ctx.browser && ctx.browser.headless;
1273
+ var vw = ctx.browser && ctx.browser.viewport;
1274
+ var contextLabel = browser ? browser + " / " + ctx.platform : ctx.platform || "shell";
1275
+ if (headless) contextLabel += " \\u00B7 headless";
1276
+ if (vw) contextLabel += " \\u00B7 " + vw.width + "\\u00D7" + vw.height;
1277
+
1278
+ var block = el("div", "context");
1279
+ var head = el("div", "context-head");
1280
+ head.innerHTML = ICON.chip +
1281
+ '<span class="what">' + esc(contextLabel) + '</span>' +
1282
+ '<span class="meta">' + ICON.finger + ' ' + esc((ctx.contextId || "").slice(0, 8)) + '</span>' +
1283
+ badge(ctx.result);
1284
+ block.appendChild(head);
1285
+
1286
+ var visibleSteps = (ctx.steps || [])
1287
+ .map(function(s, i) { return { step: s, origIdx: i }; })
1288
+ .filter(function(item) {
1289
+ var s = item.step;
1290
+ if (state.statusFilters.size && !state.statusFilters.has(s.result)) return false;
1291
+ if (state.query) return JSON.stringify(s).toLowerCase().indexOf(state.query.toLowerCase()) !== -1;
1292
+ return true;
1293
+ });
1294
+
1295
+ if (visibleSteps.length === 0) {
1296
+ block.innerHTML += '<div class="empty" style="margin:8px 16px 10px;padding:16px">No steps match the current filter.</div>';
1297
+ } else {
1298
+ var stepsDiv = el("div", "steps");
1299
+ var cId = ctx.contextId || "";
1300
+ visibleSteps.forEach(function(item, i) {
1301
+ stepsDiv.appendChild(buildStep(item.step, i, cId, item.origIdx));
1302
+ });
1303
+ block.appendChild(stepsDiv);
1304
+ }
1305
+
1306
+ return block;
1307
+ }
1308
+
1309
+ // Test card
1310
+ function buildTest(test) {
1311
+ var slug = statusSlug(test.result);
1312
+ var isOpen = isTestOpen(test.testId, test);
1313
+ var card = el("div", "test " + slug + (isOpen ? " open" : ""));
1314
+
1315
+ var stepAgg = { pass: 0, fail: 0, warning: 0, skipped: 0 };
1316
+ (test.contexts || []).forEach(function(c) {
1317
+ (c.steps || []).forEach(function(s) {
1318
+ var k = (s.result || "").toLowerCase();
1319
+ if (k in stepAgg) stepAgg[k]++;
1320
+ });
1321
+ });
1322
+
1323
+ var nCtx = (test.contexts || []).length;
1324
+ var head = el("div", "test-head");
1325
+ head.innerHTML = '<span class="chev">' + ICON.chevron + '</span>' +
1326
+ badge(test.result) +
1327
+ '<div class="title-col"><div class="title">' + esc(test.description || test.testId) +
1328
+ ' ' + tag(nCtx + " context" + (nCtx !== 1 ? "s" : "")) + '</div>' +
1329
+ (test.contentPath ? '<div class="desc" style="font-family:var(--font-mono);font-size:12px;color:var(--fg3)">' + ICON.file + ' ' + esc(test.contentPath) + '</div>' : '') +
1330
+ '</div>' +
1331
+ '<div class="metrics">' +
1332
+ metric("pass", stepAgg.pass, "pass") + metric("fail", stepAgg.fail, "fail") +
1333
+ metric("warn", stepAgg.warning, "warn") + metric("skip", stepAgg.skipped, "skip") + '</div>';
1334
+
1335
+ wireDisclosure(head, isOpen, function() {
1336
+ state.openTests[test.testId] = !isOpen;
1337
+ render();
1338
+ });
1339
+ card.appendChild(head);
1340
+
1341
+ if (isOpen) {
1342
+ var body = el("div", "test-body");
1343
+ (test.contexts || []).forEach(function(c) { body.appendChild(buildContext(c)); });
1344
+ card.appendChild(body);
1345
+ }
1346
+
1347
+ return card;
1348
+ }
1349
+
1350
+ // Spec card
1351
+ function buildSpec(spec) {
1352
+ var slug = statusSlug(spec.result);
1353
+ var isOpen = isSpecOpen(spec.specId, spec);
1354
+ var counts = countTree(spec);
1355
+ var card = el("div", "spec " + slug + (isOpen ? " open" : ""));
1356
+
1357
+ card.innerHTML = '<div class="stripe"></div>';
1358
+
1359
+ var head = el("div", "spec-head");
1360
+ head.innerHTML = '<span class="chev">' + ICON.chevron + '</span>' +
1361
+ badge(spec.result) +
1362
+ '<div class="title-col"><div class="title">' + esc(spec.description || spec.specId) +
1363
+ (spec.specPath ? ' <span class="path" title="' + escAttr(spec.specPath) + '">' + ICON.file + ' ' + esc(spec.specPath) + '</span>' : '') +
1364
+ '</div>' +
1365
+ (spec.contentPath && spec.contentPath !== spec.specPath
1366
+ ? '<div class="desc">Source: <code style="font-family:var(--font-mono);font-size:12px">' + esc(spec.contentPath) + '</code></div>'
1367
+ : '') +
1368
+ '</div>' +
1369
+ '<div class="metrics"><span class="m">' + counts.tests + ' test' + (counts.tests !== 1 ? 's' : '') + '</span><span class="sep"></span>' +
1370
+ metric("pass", counts.stepCounts.pass, "") + metric("fail", counts.stepCounts.fail, "") +
1371
+ metric("warn", counts.stepCounts.warning, "") + metric("skip", counts.stepCounts.skipped, "") + '</div>';
1372
+
1373
+ wireDisclosure(head, isOpen, function() {
1374
+ state.openSpecs[spec.specId] = !isOpen;
1375
+ render();
1376
+ });
1377
+ card.appendChild(head);
1378
+
1379
+ if (isOpen) {
1380
+ var body = el("div", "spec-body");
1381
+ (spec.tests || []).forEach(function(t) { body.appendChild(buildTest(t)); });
1382
+ card.appendChild(body);
1383
+ }
1384
+
1385
+ return card;
1386
+ }
1387
+
1388
+ // Precompute lowercase search strings once per spec
1389
+ var specSearchCache = new WeakMap();
1390
+ function getSpecSearchStr(sp) {
1391
+ if (specSearchCache.has(sp)) return specSearchCache.get(sp);
1392
+ var s = JSON.stringify(sp).toLowerCase();
1393
+ specSearchCache.set(sp, s);
1394
+ return s;
1395
+ }
1396
+
1397
+ // Filter specs
1398
+ function getVisibleSpecs() {
1399
+ return (report.specs || []).filter(function(sp) {
1400
+ if (state.statusFilters.size && !state.statusFilters.has(sp.result)) {
1401
+ var hasMatching = (sp.tests || []).some(function(t) {
1402
+ return (t.contexts || []).some(function(c) {
1403
+ return (c.steps || []).some(function(s) { return state.statusFilters.has(s.result); });
1404
+ });
1405
+ });
1406
+ if (!hasMatching) return false;
1407
+ }
1408
+ if (state.query) {
1409
+ if (getSpecSearchStr(sp).indexOf(state.query.toLowerCase()) === -1) return false;
1410
+ }
1411
+ return true;
1412
+ });
1413
+ }
1414
+
1415
+ // Render
1416
+ function render() {
1417
+ applyTheme();
1418
+ var root = document.getElementById("root");
1419
+ root.innerHTML = "";
1420
+ var app = el("div", "app");
1421
+
1422
+ app.appendChild(buildHeader());
1423
+
1424
+ var main = el("main");
1425
+
1426
+ // Verdict + summary
1427
+ var verdict = el("section", "verdict");
1428
+ verdict.appendChild(buildVerdict());
1429
+ var summary = el("div", "summary");
1430
+ summary.appendChild(buildSumTile("Specs", report.summary.specs, "specs"));
1431
+ summary.appendChild(buildSumTile("Tests", report.summary.tests, "tests"));
1432
+ summary.appendChild(buildSumTile("Contexts", report.summary.contexts, "contexts"));
1433
+ summary.appendChild(buildSumTile("Steps", report.summary.steps, "steps"));
1434
+ verdict.appendChild(summary);
1435
+ main.appendChild(verdict);
1436
+
1437
+ // Toolbar
1438
+ var visibleSpecs = getVisibleSpecs();
1439
+ var toolbar = el("div", "toolbar");
1440
+ toolbar.innerHTML = '<h2>Specifications</h2><span class="count">' + visibleSpecs.length + ' of ' + report.specs.length + ' shown</span>';
1441
+
1442
+ STATUS_ORDER.forEach(function(s) {
1443
+ var m = STATUS_META[s];
1444
+ var active = state.statusFilters.has(s);
1445
+ var n = report.summary.specs[m.slug === "warn" ? "warning" : m.slug === "skip" ? "skipped" : m.slug];
1446
+ var btn = el("button", "filter " + m.slug + (active ? " active" : ""), '<span class="d"></span>' + m.label + ' \\u00B7 ' + n);
1447
+ btn.onclick = function() {
1448
+ if (state.statusFilters.has(s)) state.statusFilters.delete(s); else state.statusFilters.add(s);
1449
+ render();
1450
+ };
1451
+ toolbar.appendChild(btn);
1452
+ });
1453
+
1454
+ if (state.statusFilters.size > 0 || state.query) {
1455
+ var clearBtn = el("button", "linkbtn", "Clear filters");
1456
+ clearBtn.onclick = function() { state.statusFilters.clear(); state.query = ""; render(); };
1457
+ toolbar.appendChild(clearBtn);
1458
+ }
1459
+
1460
+ toolbar.appendChild(el("span", "spacer"));
1461
+
1462
+ var searchDiv = el("div", "search-input");
1463
+ searchDiv.innerHTML = ICON.search;
1464
+ var searchInput = document.createElement("input");
1465
+ searchInput.placeholder = "Search specs, tests, steps, paths\\u2026";
1466
+ searchInput.value = state.query;
1467
+ var searchTimer = null;
1468
+ searchInput.oninput = function() {
1469
+ state.query = searchInput.value;
1470
+ clearTimeout(searchTimer);
1471
+ searchTimer = setTimeout(render, 200);
1472
+ };
1473
+ searchDiv.appendChild(searchInput);
1474
+ toolbar.appendChild(searchDiv);
1475
+
1476
+ toolbar.appendChild(el("span", "spacer"));
1477
+ var expandBtn = el("button", "linkbtn", ICON.expand + ' Expand all');
1478
+ expandBtn.onclick = function() {
1479
+ (report.specs || []).forEach(function(sp) {
1480
+ state.openSpecs[sp.specId] = true;
1481
+ (sp.tests || []).forEach(function(t) {
1482
+ state.openTests[t.testId] = true;
1483
+ (t.contexts || []).forEach(function(c) {
1484
+ (c.steps || []).forEach(function(s, i) { state.openSteps[stepKey(s, c.contextId || "", i)] = true; });
1485
+ });
1486
+ });
1487
+ });
1488
+ render();
1489
+ };
1490
+ toolbar.appendChild(expandBtn);
1491
+
1492
+ var collapseBtn = el("button", "linkbtn", ICON.collapse + ' Collapse');
1493
+ collapseBtn.onclick = function() {
1494
+ (report.specs || []).forEach(function(sp) {
1495
+ state.openSpecs[sp.specId] = false;
1496
+ (sp.tests || []).forEach(function(t) {
1497
+ state.openTests[t.testId] = false;
1498
+ (t.contexts || []).forEach(function(c) {
1499
+ (c.steps || []).forEach(function(s, i) { state.openSteps[stepKey(s, c.contextId || "", i)] = false; });
1500
+ });
1501
+ });
1502
+ });
1503
+ render();
1504
+ };
1505
+ toolbar.appendChild(collapseBtn);
1506
+
1507
+ main.appendChild(toolbar);
1508
+
1509
+ // Spec list
1510
+ if (visibleSpecs.length === 0) {
1511
+ var empty = el("div", "empty");
1512
+ empty.innerHTML = '<div style="font-size:18px;color:var(--fg2);margin-bottom:6px">Nothing matches the current filter.</div>';
1513
+ var clrBtn = el("button", "linkbtn", "Clear filters");
1514
+ clrBtn.onclick = function() { state.statusFilters.clear(); state.query = ""; render(); };
1515
+ empty.appendChild(clrBtn);
1516
+ main.appendChild(empty);
1517
+ } else {
1518
+ visibleSpecs.forEach(function(sp) { main.appendChild(buildSpec(sp)); });
1519
+ }
1520
+
1521
+ app.appendChild(main);
1522
+ root.appendChild(app);
1523
+
1524
+ // Lightbox
1525
+ if (state.lightbox) {
1526
+ var lb = el("div", "lightbox");
1527
+ var m = state.lightbox;
1528
+ lb.innerHTML = '<button class="close" aria-label="Close lightbox">' + ICON.close + '</button>' +
1529
+ (m.kind === "video"
1530
+ ? '<video src="' + escAttr(m.path) + '" controls autoplay></video>'
1531
+ : '<img src="' + escAttr(m.path) + '" alt="' + escAttr(m.path) + '"/>') +
1532
+ '<div class="cap">' + esc(m.path) + '</div>';
1533
+ lb.onclick = function(e) {
1534
+ // only close on backdrop clicks
1535
+ if (e.target === lb) { state.lightbox = null; render(); }
1536
+ };
1537
+ var closeEl = lb.querySelector(".close");
1538
+ if (closeEl) closeEl.onclick = function(e) {
1539
+ e.stopPropagation();
1540
+ state.lightbox = null;
1541
+ render();
1542
+ };
1543
+ var mediaEl = lb.querySelector("img,video");
1544
+ if (mediaEl) mediaEl.onclick = function(e) { e.stopPropagation(); };
1545
+ root.appendChild(lb);
1546
+ }
1547
+
1548
+ // Attach copy button handlers — read text from sibling <pre> element
1549
+ root.querySelectorAll(".copy-btn").forEach(function(btn) {
1550
+ btn.onclick = function(e) {
1551
+ e.stopPropagation();
1552
+ var panel = btn.closest(".detail-panel");
1553
+ var pre = panel && panel.querySelector("pre");
1554
+ var text = pre ? pre.textContent : "";
1555
+ if (navigator.clipboard) {
1556
+ navigator.clipboard.writeText(text).then(function() {
1557
+ btn.innerHTML = ICON.check + " Copied";
1558
+ setTimeout(function() { btn.innerHTML = ICON.copy + " Copy"; }, 1200);
1559
+ });
1560
+ }
1561
+ };
1562
+ });
1563
+
1564
+ // Attach media click handlers
1565
+ root.querySelectorAll(".media-thumb").forEach(function(thumb) {
1566
+ thumb.onclick = function(e) {
1567
+ e.stopPropagation();
1568
+ state.lightbox = { path: thumb.getAttribute("data-media-path"), kind: thumb.getAttribute("data-media-kind") };
1569
+ render();
1570
+ };
1571
+ });
1572
+
1573
+ // Focus search if it had focus
1574
+ if (document.activeElement === document.body && state.query) {
1575
+ var si = root.querySelector(".search-input input");
1576
+ if (si) { si.focus(); si.setSelectionRange(si.value.length, si.value.length); }
1577
+ }
1578
+ }
1579
+
1580
+ // Escape key closes lightbox
1581
+ document.addEventListener("keydown", function(e) {
1582
+ if (e.key === "Escape" && state.lightbox) { state.lightbox = null; render(); }
1583
+ });
1584
+
1585
+ // Initial render
1586
+ render();
1587
+ })();
1588
+ `;
1589
+ //# sourceMappingURL=htmlReporter.js.map