doc-detective 4.0.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 (208) 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 +101 -0
  56. package/dist/common/src/detectTests.d.ts.map +1 -0
  57. package/dist/common/src/detectTests.js +693 -0
  58. package/dist/common/src/detectTests.js.map +1 -0
  59. package/dist/common/src/fileTypes.d.ts +35 -0
  60. package/dist/common/src/fileTypes.d.ts.map +1 -0
  61. package/dist/common/src/fileTypes.js +303 -0
  62. package/dist/common/src/fileTypes.js.map +1 -0
  63. package/dist/common/src/index.d.ts +10 -0
  64. package/dist/common/src/index.d.ts.map +1 -0
  65. package/dist/common/src/index.js +5 -0
  66. package/dist/common/src/index.js.map +1 -0
  67. package/dist/common/src/schemas/index.d.ts +5 -0
  68. package/dist/common/src/schemas/index.d.ts.map +1 -0
  69. package/dist/common/src/schemas/index.js +3 -0
  70. package/dist/common/src/schemas/index.js.map +1 -0
  71. package/dist/common/src/schemas/schemas.json +128711 -0
  72. package/dist/common/src/types/generated/checkLink_v3.d.ts +42 -0
  73. package/dist/common/src/types/generated/checkLink_v3.d.ts.map +1 -0
  74. package/dist/common/src/types/generated/checkLink_v3.js +7 -0
  75. package/dist/common/src/types/generated/checkLink_v3.js.map +1 -0
  76. package/dist/common/src/types/generated/click_v3.d.ts +16 -0
  77. package/dist/common/src/types/generated/click_v3.d.ts.map +1 -0
  78. package/dist/common/src/types/generated/click_v3.js +7 -0
  79. package/dist/common/src/types/generated/click_v3.js.map +1 -0
  80. package/dist/common/src/types/generated/config_v3.d.ts +402 -0
  81. package/dist/common/src/types/generated/config_v3.d.ts.map +1 -0
  82. package/dist/common/src/types/generated/config_v3.js +7 -0
  83. package/dist/common/src/types/generated/config_v3.js.map +1 -0
  84. package/dist/common/src/types/generated/context_v3.d.ts +108 -0
  85. package/dist/common/src/types/generated/context_v3.d.ts.map +1 -0
  86. package/dist/common/src/types/generated/context_v3.js +7 -0
  87. package/dist/common/src/types/generated/context_v3.js.map +1 -0
  88. package/dist/common/src/types/generated/dragAndDrop_v3.d.ts +37 -0
  89. package/dist/common/src/types/generated/dragAndDrop_v3.d.ts.map +1 -0
  90. package/dist/common/src/types/generated/dragAndDrop_v3.js +7 -0
  91. package/dist/common/src/types/generated/dragAndDrop_v3.js.map +1 -0
  92. package/dist/common/src/types/generated/endRecord_v3.d.ts +9 -0
  93. package/dist/common/src/types/generated/endRecord_v3.d.ts.map +1 -0
  94. package/dist/common/src/types/generated/endRecord_v3.js +7 -0
  95. package/dist/common/src/types/generated/endRecord_v3.js.map +1 -0
  96. package/dist/common/src/types/generated/find_v3.d.ts +16 -0
  97. package/dist/common/src/types/generated/find_v3.d.ts.map +1 -0
  98. package/dist/common/src/types/generated/find_v3.js +7 -0
  99. package/dist/common/src/types/generated/find_v3.js.map +1 -0
  100. package/dist/common/src/types/generated/goTo_v3.d.ts +46 -0
  101. package/dist/common/src/types/generated/goTo_v3.d.ts.map +1 -0
  102. package/dist/common/src/types/generated/goTo_v3.js +7 -0
  103. package/dist/common/src/types/generated/goTo_v3.js.map +1 -0
  104. package/dist/common/src/types/generated/httpRequest_v3.d.ts +16 -0
  105. package/dist/common/src/types/generated/httpRequest_v3.d.ts.map +1 -0
  106. package/dist/common/src/types/generated/httpRequest_v3.js +7 -0
  107. package/dist/common/src/types/generated/httpRequest_v3.js.map +1 -0
  108. package/dist/common/src/types/generated/loadCookie_v3.d.ts +16 -0
  109. package/dist/common/src/types/generated/loadCookie_v3.d.ts.map +1 -0
  110. package/dist/common/src/types/generated/loadCookie_v3.js +7 -0
  111. package/dist/common/src/types/generated/loadCookie_v3.js.map +1 -0
  112. package/dist/common/src/types/generated/loadVariables_v3.d.ts +9 -0
  113. package/dist/common/src/types/generated/loadVariables_v3.d.ts.map +1 -0
  114. package/dist/common/src/types/generated/loadVariables_v3.js +7 -0
  115. package/dist/common/src/types/generated/loadVariables_v3.js.map +1 -0
  116. package/dist/common/src/types/generated/openApi_v3.d.ts +62 -0
  117. package/dist/common/src/types/generated/openApi_v3.d.ts.map +1 -0
  118. package/dist/common/src/types/generated/openApi_v3.js +7 -0
  119. package/dist/common/src/types/generated/openApi_v3.js.map +1 -0
  120. package/dist/common/src/types/generated/record_v3.d.ts +32 -0
  121. package/dist/common/src/types/generated/record_v3.d.ts.map +1 -0
  122. package/dist/common/src/types/generated/record_v3.js +7 -0
  123. package/dist/common/src/types/generated/record_v3.js.map +1 -0
  124. package/dist/common/src/types/generated/report_v3.d.ts +174 -0
  125. package/dist/common/src/types/generated/report_v3.d.ts.map +1 -0
  126. package/dist/common/src/types/generated/report_v3.js +7 -0
  127. package/dist/common/src/types/generated/report_v3.js.map +1 -0
  128. package/dist/common/src/types/generated/resolvedTests_v3.d.ts +575 -0
  129. package/dist/common/src/types/generated/resolvedTests_v3.d.ts.map +1 -0
  130. package/dist/common/src/types/generated/resolvedTests_v3.js +7 -0
  131. package/dist/common/src/types/generated/resolvedTests_v3.js.map +1 -0
  132. package/dist/common/src/types/generated/runCode_v3.d.ts +57 -0
  133. package/dist/common/src/types/generated/runCode_v3.d.ts.map +1 -0
  134. package/dist/common/src/types/generated/runCode_v3.js +7 -0
  135. package/dist/common/src/types/generated/runCode_v3.js.map +1 -0
  136. package/dist/common/src/types/generated/runShell_v3.d.ts +56 -0
  137. package/dist/common/src/types/generated/runShell_v3.d.ts.map +1 -0
  138. package/dist/common/src/types/generated/runShell_v3.js +7 -0
  139. package/dist/common/src/types/generated/runShell_v3.js.map +1 -0
  140. package/dist/common/src/types/generated/saveCookie_v3.d.ts +16 -0
  141. package/dist/common/src/types/generated/saveCookie_v3.d.ts.map +1 -0
  142. package/dist/common/src/types/generated/saveCookie_v3.js +7 -0
  143. package/dist/common/src/types/generated/saveCookie_v3.js.map +1 -0
  144. package/dist/common/src/types/generated/screenshot_v3.d.ts +74 -0
  145. package/dist/common/src/types/generated/screenshot_v3.d.ts.map +1 -0
  146. package/dist/common/src/types/generated/screenshot_v3.js +7 -0
  147. package/dist/common/src/types/generated/screenshot_v3.js.map +1 -0
  148. package/dist/common/src/types/generated/sourceIntegration_v3.d.ts +30 -0
  149. package/dist/common/src/types/generated/sourceIntegration_v3.d.ts.map +1 -0
  150. package/dist/common/src/types/generated/sourceIntegration_v3.js +7 -0
  151. package/dist/common/src/types/generated/sourceIntegration_v3.js.map +1 -0
  152. package/dist/common/src/types/generated/spec_v3.d.ts +159 -0
  153. package/dist/common/src/types/generated/spec_v3.d.ts.map +1 -0
  154. package/dist/common/src/types/generated/spec_v3.js +7 -0
  155. package/dist/common/src/types/generated/spec_v3.js.map +1 -0
  156. package/dist/common/src/types/generated/step_v3.d.ts +1573 -0
  157. package/dist/common/src/types/generated/step_v3.d.ts.map +1 -0
  158. package/dist/common/src/types/generated/step_v3.js +7 -0
  159. package/dist/common/src/types/generated/step_v3.js.map +1 -0
  160. package/dist/common/src/types/generated/stopRecord_v3.d.ts +9 -0
  161. package/dist/common/src/types/generated/stopRecord_v3.d.ts.map +1 -0
  162. package/dist/common/src/types/generated/stopRecord_v3.js +7 -0
  163. package/dist/common/src/types/generated/stopRecord_v3.js.map +1 -0
  164. package/dist/common/src/types/generated/test_v3.d.ts +3521 -0
  165. package/dist/common/src/types/generated/test_v3.d.ts.map +1 -0
  166. package/dist/common/src/types/generated/test_v3.js +7 -0
  167. package/dist/common/src/types/generated/test_v3.js.map +1 -0
  168. package/dist/common/src/types/generated/type_v3.d.ts +54 -0
  169. package/dist/common/src/types/generated/type_v3.d.ts.map +1 -0
  170. package/dist/common/src/types/generated/type_v3.js +7 -0
  171. package/dist/common/src/types/generated/type_v3.js.map +1 -0
  172. package/dist/common/src/types/generated/wait_v3.d.ts +12 -0
  173. package/dist/common/src/types/generated/wait_v3.d.ts.map +1 -0
  174. package/dist/common/src/types/generated/wait_v3.js +7 -0
  175. package/dist/common/src/types/generated/wait_v3.js.map +1 -0
  176. package/dist/common/src/validate.d.ts +41 -0
  177. package/dist/common/src/validate.d.ts.map +1 -0
  178. package/dist/common/src/validate.js +557 -0
  179. package/dist/common/src/validate.js.map +1 -0
  180. package/dist/core/config.d.ts.map +1 -1
  181. package/dist/core/config.js +10 -0
  182. package/dist/core/config.js.map +1 -1
  183. package/dist/core/detectTests.d.ts.map +1 -1
  184. package/dist/core/detectTests.js +50 -2
  185. package/dist/core/detectTests.js.map +1 -1
  186. package/dist/core/integrations/heretto.d.ts +32 -0
  187. package/dist/core/integrations/heretto.d.ts.map +1 -1
  188. package/dist/core/integrations/heretto.js +368 -0
  189. package/dist/core/integrations/heretto.js.map +1 -1
  190. package/dist/core/tests/checkLink.d.ts.map +1 -1
  191. package/dist/core/tests/checkLink.js +136 -29
  192. package/dist/core/tests/checkLink.js.map +1 -1
  193. package/dist/core/tests/loadCookie.d.ts.map +1 -1
  194. package/dist/core/tests/loadCookie.js +12 -2
  195. package/dist/core/tests/loadCookie.js.map +1 -1
  196. package/dist/index.cjs +2083 -965
  197. package/dist/reporters/htmlReporter.d.ts +2 -0
  198. package/dist/reporters/htmlReporter.d.ts.map +1 -0
  199. package/dist/reporters/htmlReporter.js +1589 -0
  200. package/dist/reporters/htmlReporter.js.map +1 -0
  201. package/dist/utils.d.ts +2 -1
  202. package/dist/utils.d.ts.map +1 -1
  203. package/dist/utils.js +43 -10
  204. package/dist/utils.js.map +1 -1
  205. package/package.json +146 -135
  206. package/.doc-detective.json +0 -1
  207. package/CONTRIBUTIONS.md +0 -27
  208. package/scripts/createCjsWrapper.js +0 -31
@@ -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