@healflow/playwright 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +9 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +74 -0
- package/dist/api-client.js.map +1 -0
- package/dist/artifacts.d.ts +10 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +37 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/auto.d.ts +9 -0
- package/dist/auto.d.ts.map +1 -0
- package/dist/auto.js +51 -0
- package/dist/auto.js.map +1 -0
- package/dist/client-info.d.ts +7 -0
- package/dist/client-info.d.ts.map +1 -0
- package/dist/client-info.js +30 -0
- package/dist/client-info.js.map +1 -0
- package/dist/heal-store.d.ts +9 -0
- package/dist/heal-store.d.ts.map +1 -0
- package/dist/heal-store.js +105 -0
- package/dist/heal-store.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +706 -0
- package/dist/index.js.map +1 -0
- package/dist/list-reporter.d.ts +31 -0
- package/dist/list-reporter.d.ts.map +1 -0
- package/dist/list-reporter.js +23 -0
- package/dist/list-reporter.js.map +1 -0
- package/dist/register.d.ts +4 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +9 -0
- package/dist/register.js.map +1 -0
- package/dist/report-html.d.ts +7 -0
- package/dist/report-html.d.ts.map +1 -0
- package/dist/report-html.js +862 -0
- package/dist/report-html.js.map +1 -0
- package/dist/reporter.d.ts +34 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +160 -0
- package/dist/reporter.js.map +1 -0
- package/dist/setup-global.d.ts +2 -0
- package/dist/setup-global.d.ts.map +1 -0
- package/dist/setup-global.js +10 -0
- package/dist/setup-global.js.map +1 -0
- package/dist/terminal.d.ts +16 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +158 -0
- package/dist/terminal.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildReportHtml = buildReportHtml;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const terminal_js_1 = require("./terminal.js");
|
|
6
|
+
function escapeHtml(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"');
|
|
12
|
+
}
|
|
13
|
+
function formatDuration(ms) {
|
|
14
|
+
if (ms < 1000)
|
|
15
|
+
return `${Math.round(ms)}ms`;
|
|
16
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
17
|
+
}
|
|
18
|
+
function formatTimestamp(iso) {
|
|
19
|
+
try {
|
|
20
|
+
return new Date(iso).toLocaleString();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return iso;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function formatConfidence(confidence) {
|
|
27
|
+
return `${Math.round(confidence * 100)}%`;
|
|
28
|
+
}
|
|
29
|
+
function slugify(value) {
|
|
30
|
+
return value
|
|
31
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-|-$/g, '')
|
|
33
|
+
.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
function buildTestTree(artifacts) {
|
|
36
|
+
const fixByKey = new Map();
|
|
37
|
+
for (const fix of artifacts.fixes) {
|
|
38
|
+
fixByKey.set(`${fix.testFile}::${fix.testTitle}`, fix);
|
|
39
|
+
}
|
|
40
|
+
const testMap = new Map();
|
|
41
|
+
for (const heal of artifacts.heals) {
|
|
42
|
+
const testFile = heal.testFile ?? 'unknown';
|
|
43
|
+
const testTitle = heal.testTitle ?? 'Unknown test';
|
|
44
|
+
const key = `${testFile}::${testTitle}`;
|
|
45
|
+
const existing = testMap.get(key);
|
|
46
|
+
if (existing) {
|
|
47
|
+
existing.heals.push(heal);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
testMap.set(key, {
|
|
51
|
+
id: slugify(key),
|
|
52
|
+
testFile,
|
|
53
|
+
testTitle,
|
|
54
|
+
heals: [heal],
|
|
55
|
+
fix: fixByKey.get(key),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const fileMap = new Map();
|
|
59
|
+
for (const test of testMap.values()) {
|
|
60
|
+
const fileName = (0, node_path_1.basename)(test.testFile);
|
|
61
|
+
const group = fileMap.get(test.testFile);
|
|
62
|
+
if (group) {
|
|
63
|
+
group.tests.push(test);
|
|
64
|
+
group.healCount += test.heals.length;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
fileMap.set(test.testFile, {
|
|
68
|
+
testFile: test.testFile,
|
|
69
|
+
fileName,
|
|
70
|
+
tests: [test],
|
|
71
|
+
healCount: test.heals.length,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return [...fileMap.values()].sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
75
|
+
}
|
|
76
|
+
function categoryCounts(heals) {
|
|
77
|
+
const counts = new Map();
|
|
78
|
+
for (const heal of heals) {
|
|
79
|
+
counts.set(heal.category, (counts.get(heal.category) ?? 0) + 1);
|
|
80
|
+
}
|
|
81
|
+
return [...counts.entries()]
|
|
82
|
+
.map(([category, count]) => ({ category, count }))
|
|
83
|
+
.sort((a, b) => b.count - a.count);
|
|
84
|
+
}
|
|
85
|
+
function statusLabel(status, failureCount) {
|
|
86
|
+
if (failureCount > 0 ||
|
|
87
|
+
status === 'failed' ||
|
|
88
|
+
status === 'timedout' ||
|
|
89
|
+
status === 'interrupted') {
|
|
90
|
+
return 'failed';
|
|
91
|
+
}
|
|
92
|
+
return 'passed';
|
|
93
|
+
}
|
|
94
|
+
function renderSidebarTree(groups) {
|
|
95
|
+
if (groups.length === 0) {
|
|
96
|
+
return '<div class="tree-empty">No heals recorded this run.</div>';
|
|
97
|
+
}
|
|
98
|
+
return groups
|
|
99
|
+
.map((group) => {
|
|
100
|
+
const tests = group.tests
|
|
101
|
+
.map((test) => {
|
|
102
|
+
const healLabel = test.heals.length > 1 ? ` (${test.heals.length})` : '';
|
|
103
|
+
return `
|
|
104
|
+
<div class="tree-item test-tree-item" data-test-id="${escapeHtml(test.id)}" role="button" tabindex="0">
|
|
105
|
+
<span class="status-icon status-healed" aria-hidden="true">${STATUS_ICON_HEALED}</span>
|
|
106
|
+
<span class="tree-item-title">${escapeHtml(test.testTitle)}${escapeHtml(healLabel)}</span>
|
|
107
|
+
</div>`;
|
|
108
|
+
})
|
|
109
|
+
.join('');
|
|
110
|
+
return `
|
|
111
|
+
<div class="file-group" data-file="${escapeHtml(group.testFile)}">
|
|
112
|
+
<div class="tree-item file-tree-item" role="presentation">
|
|
113
|
+
<span class="file-chevron" aria-hidden="true">▸</span>
|
|
114
|
+
<span class="tree-item-title file-title">${escapeHtml(group.fileName)}</span>
|
|
115
|
+
<span class="file-heal-count">${group.healCount}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="file-tests">${tests}</div>
|
|
118
|
+
</div>`;
|
|
119
|
+
})
|
|
120
|
+
.join('');
|
|
121
|
+
}
|
|
122
|
+
function renderHealCards(test) {
|
|
123
|
+
return test.heals
|
|
124
|
+
.map((heal) => {
|
|
125
|
+
const selectorChange = heal.oldValue || heal.newValue
|
|
126
|
+
? `
|
|
127
|
+
<div class="detail-row">
|
|
128
|
+
<span class="detail-label">Selector</span>
|
|
129
|
+
<span class="detail-value selector-change">
|
|
130
|
+
<code>${escapeHtml(heal.oldValue ?? '—')}</code>
|
|
131
|
+
<span class="arrow">→</span>
|
|
132
|
+
<code>${escapeHtml(heal.newValue ?? '—')}</code>
|
|
133
|
+
</span>
|
|
134
|
+
</div>`
|
|
135
|
+
: '';
|
|
136
|
+
return `
|
|
137
|
+
<article class="heal-card" data-heal-id="${escapeHtml(heal.id)}">
|
|
138
|
+
<div class="heal-card-header">
|
|
139
|
+
<span class="category-badge category-${escapeHtml(heal.category.toLowerCase())}">${escapeHtml(heal.category)}</span>
|
|
140
|
+
<span class="confidence-badge">${formatConfidence(heal.confidence)} confidence</span>
|
|
141
|
+
</div>
|
|
142
|
+
<p class="heal-description">${escapeHtml((0, terminal_js_1.formatHealDescription)(heal))}</p>
|
|
143
|
+
<div class="detail-grid">
|
|
144
|
+
<div class="detail-row">
|
|
145
|
+
<span class="detail-label">Root cause</span>
|
|
146
|
+
<span class="detail-value">${escapeHtml(heal.rootCause)}</span>
|
|
147
|
+
</div>
|
|
148
|
+
${selectorChange}
|
|
149
|
+
<div class="detail-row">
|
|
150
|
+
<span class="detail-label">Healed at</span>
|
|
151
|
+
<span class="detail-value">${escapeHtml(formatTimestamp(heal.healedAt))}</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="detail-row">
|
|
154
|
+
<span class="detail-label">Heal ID</span>
|
|
155
|
+
<span class="detail-value mono">${escapeHtml(heal.id)}</span>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</article>`;
|
|
159
|
+
})
|
|
160
|
+
.join('');
|
|
161
|
+
}
|
|
162
|
+
function renderFixProposal(fix) {
|
|
163
|
+
if (!fix)
|
|
164
|
+
return '';
|
|
165
|
+
return `
|
|
166
|
+
<section class="detail-section">
|
|
167
|
+
<div class="chip-header">Fix proposal</div>
|
|
168
|
+
<div class="chip-body">
|
|
169
|
+
<div class="detail-grid">
|
|
170
|
+
<div class="detail-row">
|
|
171
|
+
<span class="detail-label">Strategy</span>
|
|
172
|
+
<span class="detail-value">${escapeHtml(fix.strategy)}</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="detail-row">
|
|
175
|
+
<span class="detail-label">Dry run</span>
|
|
176
|
+
<span class="detail-value">${fix.dryRun ? 'Yes' : 'No'}</span>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="detail-row">
|
|
179
|
+
<span class="detail-label">Confidence</span>
|
|
180
|
+
<span class="detail-value">${formatConfidence(fix.confidence)}</span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</section>`;
|
|
185
|
+
}
|
|
186
|
+
function renderTestDetailPanels(groups) {
|
|
187
|
+
const panels = groups.flatMap((group) => group.tests.map((test) => {
|
|
188
|
+
const relativePath = test.testFile;
|
|
189
|
+
return `
|
|
190
|
+
<section class="test-detail" id="test-${escapeHtml(test.id)}" hidden>
|
|
191
|
+
<div class="test-file-header">
|
|
192
|
+
<div class="test-file-title-row">
|
|
193
|
+
<span class="status-icon status-healed" aria-hidden="true">${STATUS_ICON_HEALED}</span>
|
|
194
|
+
<h2 class="test-file-title">${escapeHtml(test.testTitle)}</h2>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="test-file-path">${escapeHtml(relativePath)}</div>
|
|
197
|
+
</div>
|
|
198
|
+
<section class="detail-section">
|
|
199
|
+
<div class="chip-header">Runtime heals (${test.heals.length})</div>
|
|
200
|
+
<div class="chip-body heal-cards">${renderHealCards(test)}</div>
|
|
201
|
+
</section>
|
|
202
|
+
${renderFixProposal(test.fix)}
|
|
203
|
+
</section>`;
|
|
204
|
+
}));
|
|
205
|
+
return panels.join('');
|
|
206
|
+
}
|
|
207
|
+
function renderCategorySummary(heals) {
|
|
208
|
+
const counts = categoryCounts(heals);
|
|
209
|
+
if (counts.length === 0) {
|
|
210
|
+
return '<p class="muted">No heal categories to summarize.</p>';
|
|
211
|
+
}
|
|
212
|
+
return `
|
|
213
|
+
<div class="category-summary">
|
|
214
|
+
${counts
|
|
215
|
+
.map((entry) => `
|
|
216
|
+
<div class="category-stat">
|
|
217
|
+
<span class="category-badge category-${escapeHtml(entry.category.toLowerCase())}">${escapeHtml(entry.category)}</span>
|
|
218
|
+
<span class="category-count">${entry.count}</span>
|
|
219
|
+
</div>`)
|
|
220
|
+
.join('')}
|
|
221
|
+
</div>`;
|
|
222
|
+
}
|
|
223
|
+
const STATUS_ICON_PASSED = '<svg viewBox="0 0 16 16" width="16" height="16"><circle cx="8" cy="8" r="8" fill="#1a7f37"/><path d="M6.5 10.5 4 8l1-1 1.5 1.5L11 4l1 1-5.5 5.5Z" fill="#fff"/></svg>';
|
|
224
|
+
const STATUS_ICON_FAILED = '<svg viewBox="0 0 16 16" width="16" height="16"><circle cx="8" cy="8" r="8" fill="#cf222e"/><path d="M5 5l6 6M11 5l-6 6" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
|
225
|
+
const STATUS_ICON_HEALED = '<svg viewBox="0 0 16 16" width="16" height="16"><circle cx="8" cy="8" r="8" fill="#0969da"/><path d="M8 3.5a1 1 0 0 1 .9.6l1.4 2.8 3.1.5a1 1 0 0 1 .55 1.7l-2.2 2.2.5 3.1a1 1 0 0 1-1.45 1.05L8 13.2l-2.8 1.5a1 1 0 0 1-1.45-1.05l.5-3.1-2.2-2.2a1 1 0 0 1 .55-1.7l3.1-.5 1.4-2.8A1 1 0 0 1 8 3.5Z" fill="#fff"/></svg>';
|
|
226
|
+
const REPORT_STYLES = `
|
|
227
|
+
:root {
|
|
228
|
+
--color-canvas-default: #ffffff;
|
|
229
|
+
--color-canvas-subtle: #f6f8fa;
|
|
230
|
+
--color-fg-default: #24292f;
|
|
231
|
+
--color-fg-muted: #57606a;
|
|
232
|
+
--color-border-default: #d0d7de;
|
|
233
|
+
--color-success-fg: #1a7f37;
|
|
234
|
+
--color-success-subtle: #dafbe1;
|
|
235
|
+
--color-danger-fg: #cf222e;
|
|
236
|
+
--color-danger-subtle: #ffebe9;
|
|
237
|
+
--color-attention-fg: #9a6700;
|
|
238
|
+
--color-attention-subtle: #fff8c5;
|
|
239
|
+
--color-accent-fg: #0969da;
|
|
240
|
+
--color-accent-subtle: #ddf4ff;
|
|
241
|
+
--color-heal-fg: #0550ae;
|
|
242
|
+
--color-heal-subtle: #ddf4ff;
|
|
243
|
+
--sidebar-width: 320px;
|
|
244
|
+
--font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
245
|
+
--mono-stack: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
* { box-sizing: border-box; }
|
|
249
|
+
html, body { height: 100%; }
|
|
250
|
+
body {
|
|
251
|
+
margin: 0;
|
|
252
|
+
font-family: var(--font-stack);
|
|
253
|
+
font-size: 14px;
|
|
254
|
+
line-height: 1.5;
|
|
255
|
+
color: var(--color-fg-default);
|
|
256
|
+
background: var(--color-canvas-default);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
button, input { font: inherit; }
|
|
260
|
+
|
|
261
|
+
.app { display: flex; flex-direction: column; min-height: 100vh; }
|
|
262
|
+
|
|
263
|
+
.header-view {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 16px;
|
|
267
|
+
padding: 12px 20px;
|
|
268
|
+
border-bottom: 1px solid var(--color-border-default);
|
|
269
|
+
background: var(--color-canvas-default);
|
|
270
|
+
flex-wrap: wrap;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.header-brand {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: 10px;
|
|
277
|
+
min-width: 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.header-logo {
|
|
281
|
+
width: 28px;
|
|
282
|
+
height: 28px;
|
|
283
|
+
border-radius: 6px;
|
|
284
|
+
background: linear-gradient(135deg, #2da44e, #0969da);
|
|
285
|
+
color: #fff;
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
justify-content: center;
|
|
289
|
+
font-weight: 700;
|
|
290
|
+
font-size: 13px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.header-title {
|
|
294
|
+
font-size: 18px;
|
|
295
|
+
font-weight: 600;
|
|
296
|
+
letter-spacing: -0.2px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.header-subtitle {
|
|
300
|
+
color: var(--color-fg-muted);
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.header-view-status-container {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
gap: 8px;
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
margin-left: auto;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.summary-chip {
|
|
313
|
+
display: inline-flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
gap: 6px;
|
|
316
|
+
padding: 4px 10px;
|
|
317
|
+
border-radius: 2em;
|
|
318
|
+
border: 1px solid var(--color-border-default);
|
|
319
|
+
background: var(--color-canvas-subtle);
|
|
320
|
+
font-size: 13px;
|
|
321
|
+
font-weight: 500;
|
|
322
|
+
white-space: nowrap;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.summary-chip.passed { color: var(--color-success-fg); background: var(--color-success-subtle); border-color: #82d8a5; }
|
|
326
|
+
.summary-chip.failed { color: var(--color-danger-fg); background: var(--color-danger-subtle); border-color: #ffbbb9; }
|
|
327
|
+
.summary-chip.healed { color: var(--color-heal-fg); background: var(--color-heal-subtle); border-color: #9cd7ff; }
|
|
328
|
+
.summary-chip.neutral { color: var(--color-fg-muted); }
|
|
329
|
+
|
|
330
|
+
.main-layout {
|
|
331
|
+
display: flex;
|
|
332
|
+
flex: 1;
|
|
333
|
+
min-height: 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.sidebar {
|
|
337
|
+
width: var(--sidebar-width);
|
|
338
|
+
flex-shrink: 0;
|
|
339
|
+
border-right: 1px solid var(--color-border-default);
|
|
340
|
+
background: var(--color-canvas-subtle);
|
|
341
|
+
display: flex;
|
|
342
|
+
flex-direction: column;
|
|
343
|
+
min-height: calc(100vh - 57px);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.sidebar-toolbar {
|
|
347
|
+
padding: 12px;
|
|
348
|
+
border-bottom: 1px solid var(--color-border-default);
|
|
349
|
+
background: var(--color-canvas-default);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.search-input {
|
|
353
|
+
width: 100%;
|
|
354
|
+
padding: 6px 10px;
|
|
355
|
+
border: 1px solid var(--color-border-default);
|
|
356
|
+
border-radius: 6px;
|
|
357
|
+
background: var(--color-canvas-default);
|
|
358
|
+
color: var(--color-fg-default);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.search-input:focus {
|
|
362
|
+
outline: 2px solid var(--color-accent-fg);
|
|
363
|
+
outline-offset: -1px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.tree-panel {
|
|
367
|
+
flex: 1;
|
|
368
|
+
overflow: auto;
|
|
369
|
+
padding: 8px 0 24px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.tree-empty {
|
|
373
|
+
padding: 16px;
|
|
374
|
+
color: var(--color-fg-muted);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.file-group { margin-bottom: 4px; }
|
|
378
|
+
|
|
379
|
+
.tree-item {
|
|
380
|
+
display: flex;
|
|
381
|
+
align-items: center;
|
|
382
|
+
gap: 8px;
|
|
383
|
+
padding: 6px 12px;
|
|
384
|
+
line-height: 20px;
|
|
385
|
+
overflow: hidden;
|
|
386
|
+
white-space: nowrap;
|
|
387
|
+
text-overflow: ellipsis;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.file-tree-item {
|
|
391
|
+
color: var(--color-fg-default);
|
|
392
|
+
font-weight: 600;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
user-select: none;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.test-tree-item {
|
|
398
|
+
padding-left: 28px;
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
border-left: 3px solid transparent;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.test-tree-item:hover,
|
|
404
|
+
.test-tree-item:focus-visible {
|
|
405
|
+
background: rgba(9, 105, 218, 0.08);
|
|
406
|
+
outline: none;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.test-tree-item.selected {
|
|
410
|
+
background: #fff;
|
|
411
|
+
border-left-color: var(--color-accent-fg);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.overview-tree-item {
|
|
415
|
+
padding-left: 12px;
|
|
416
|
+
border-bottom: 1px solid var(--color-border-default);
|
|
417
|
+
margin-bottom: 8px;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.file-chevron {
|
|
421
|
+
width: 12px;
|
|
422
|
+
color: var(--color-fg-muted);
|
|
423
|
+
transition: transform 0.15s ease;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.file-group.expanded .file-chevron { transform: rotate(90deg); }
|
|
427
|
+
|
|
428
|
+
.file-tests { display: none; }
|
|
429
|
+
.file-group.expanded .file-tests { display: block; }
|
|
430
|
+
|
|
431
|
+
.tree-item-title {
|
|
432
|
+
overflow: hidden;
|
|
433
|
+
text-overflow: ellipsis;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.file-title { flex: 1; }
|
|
437
|
+
|
|
438
|
+
.file-heal-count {
|
|
439
|
+
font-size: 11px;
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
color: var(--color-heal-fg);
|
|
442
|
+
background: var(--color-heal-subtle);
|
|
443
|
+
border-radius: 10px;
|
|
444
|
+
padding: 1px 7px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.status-icon {
|
|
448
|
+
display: inline-flex;
|
|
449
|
+
flex-shrink: 0;
|
|
450
|
+
align-items: center;
|
|
451
|
+
justify-content: center;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.content {
|
|
455
|
+
flex: 1;
|
|
456
|
+
overflow: auto;
|
|
457
|
+
padding: 20px 24px 40px;
|
|
458
|
+
background: var(--color-canvas-default);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.overview-panel { max-width: 960px; }
|
|
462
|
+
|
|
463
|
+
.overview-grid {
|
|
464
|
+
display: grid;
|
|
465
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
466
|
+
gap: 12px;
|
|
467
|
+
margin: 16px 0 24px;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.meta-card {
|
|
471
|
+
border: 1px solid var(--color-border-default);
|
|
472
|
+
border-radius: 6px;
|
|
473
|
+
padding: 14px 16px;
|
|
474
|
+
background: var(--color-canvas-subtle);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.meta-card .label {
|
|
478
|
+
font-size: 12px;
|
|
479
|
+
color: var(--color-fg-muted);
|
|
480
|
+
margin-bottom: 4px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.meta-card .value {
|
|
484
|
+
font-size: 16px;
|
|
485
|
+
font-weight: 600;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.muted { color: var(--color-fg-muted); }
|
|
489
|
+
|
|
490
|
+
.chip-header {
|
|
491
|
+
border: 1px solid var(--color-border-default);
|
|
492
|
+
border-top-left-radius: 6px;
|
|
493
|
+
border-top-right-radius: 6px;
|
|
494
|
+
background: var(--color-canvas-subtle);
|
|
495
|
+
padding: 0 12px;
|
|
496
|
+
font-weight: 600;
|
|
497
|
+
line-height: 38px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.chip-body {
|
|
501
|
+
border: 1px solid var(--color-border-default);
|
|
502
|
+
border-top: none;
|
|
503
|
+
border-bottom-left-radius: 6px;
|
|
504
|
+
border-bottom-right-radius: 6px;
|
|
505
|
+
padding: 12px;
|
|
506
|
+
margin-bottom: 16px;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.detail-section { margin-bottom: 16px; }
|
|
510
|
+
|
|
511
|
+
.test-file-header {
|
|
512
|
+
margin-bottom: 20px;
|
|
513
|
+
padding-bottom: 16px;
|
|
514
|
+
border-bottom: 1px solid var(--color-border-default);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.test-file-title-row {
|
|
518
|
+
display: flex;
|
|
519
|
+
align-items: center;
|
|
520
|
+
gap: 10px;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.test-file-title {
|
|
524
|
+
margin: 0;
|
|
525
|
+
font-size: 20px;
|
|
526
|
+
font-weight: 600;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.test-file-path {
|
|
530
|
+
margin-top: 6px;
|
|
531
|
+
color: var(--color-fg-muted);
|
|
532
|
+
font-family: var(--mono-stack);
|
|
533
|
+
font-size: 12px;
|
|
534
|
+
word-break: break-all;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.heal-cards { display: grid; gap: 12px; }
|
|
538
|
+
|
|
539
|
+
.heal-card {
|
|
540
|
+
border: 1px solid var(--color-border-default);
|
|
541
|
+
border-radius: 6px;
|
|
542
|
+
padding: 12px 14px;
|
|
543
|
+
background: var(--color-canvas-default);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.heal-card-header {
|
|
547
|
+
display: flex;
|
|
548
|
+
align-items: center;
|
|
549
|
+
gap: 8px;
|
|
550
|
+
margin-bottom: 8px;
|
|
551
|
+
flex-wrap: wrap;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.heal-description {
|
|
555
|
+
margin: 0 0 12px;
|
|
556
|
+
color: var(--color-fg-default);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.detail-grid { display: grid; gap: 8px; }
|
|
560
|
+
|
|
561
|
+
.detail-row {
|
|
562
|
+
display: grid;
|
|
563
|
+
grid-template-columns: 120px 1fr;
|
|
564
|
+
gap: 12px;
|
|
565
|
+
align-items: start;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.detail-label {
|
|
569
|
+
color: var(--color-fg-muted);
|
|
570
|
+
font-size: 12px;
|
|
571
|
+
font-weight: 600;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.detail-value { word-break: break-word; }
|
|
575
|
+
.detail-value.mono { font-family: var(--mono-stack); font-size: 12px; }
|
|
576
|
+
|
|
577
|
+
.selector-change {
|
|
578
|
+
display: flex;
|
|
579
|
+
align-items: center;
|
|
580
|
+
gap: 8px;
|
|
581
|
+
flex-wrap: wrap;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.selector-change code {
|
|
585
|
+
font-family: var(--mono-stack);
|
|
586
|
+
font-size: 12px;
|
|
587
|
+
background: var(--color-canvas-subtle);
|
|
588
|
+
border: 1px solid var(--color-border-default);
|
|
589
|
+
border-radius: 4px;
|
|
590
|
+
padding: 2px 6px;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.arrow { color: var(--color-fg-muted); }
|
|
594
|
+
|
|
595
|
+
.category-badge {
|
|
596
|
+
display: inline-flex;
|
|
597
|
+
align-items: center;
|
|
598
|
+
padding: 2px 8px;
|
|
599
|
+
border-radius: 2em;
|
|
600
|
+
font-size: 11px;
|
|
601
|
+
font-weight: 700;
|
|
602
|
+
letter-spacing: 0.3px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.category-timing { background: var(--color-attention-subtle); color: var(--color-attention-fg); }
|
|
606
|
+
.category-overlay { background: var(--color-danger-subtle); color: var(--color-danger-fg); }
|
|
607
|
+
.category-selector { background: var(--color-success-subtle); color: var(--color-success-fg); }
|
|
608
|
+
.category-iframe, .category-shadow_dom, .category-auth, .category-session, .category-network {
|
|
609
|
+
background: var(--color-accent-subtle);
|
|
610
|
+
color: var(--color-accent-fg);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.confidence-badge {
|
|
614
|
+
font-size: 12px;
|
|
615
|
+
color: var(--color-fg-muted);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.category-summary {
|
|
619
|
+
display: flex;
|
|
620
|
+
flex-wrap: wrap;
|
|
621
|
+
gap: 10px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.category-stat {
|
|
625
|
+
display: inline-flex;
|
|
626
|
+
align-items: center;
|
|
627
|
+
gap: 8px;
|
|
628
|
+
border: 1px solid var(--color-border-default);
|
|
629
|
+
border-radius: 6px;
|
|
630
|
+
padding: 8px 10px;
|
|
631
|
+
background: var(--color-canvas-subtle);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.category-count {
|
|
635
|
+
font-weight: 700;
|
|
636
|
+
min-width: 1.5em;
|
|
637
|
+
text-align: right;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.run-status {
|
|
641
|
+
display: inline-flex;
|
|
642
|
+
align-items: center;
|
|
643
|
+
gap: 6px;
|
|
644
|
+
font-weight: 600;
|
|
645
|
+
text-transform: capitalize;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.run-status.passed { color: var(--color-success-fg); }
|
|
649
|
+
.run-status.failed { color: var(--color-danger-fg); }
|
|
650
|
+
|
|
651
|
+
@media (max-width: 900px) {
|
|
652
|
+
.main-layout { flex-direction: column; }
|
|
653
|
+
.sidebar {
|
|
654
|
+
width: 100%;
|
|
655
|
+
min-height: auto;
|
|
656
|
+
max-height: 40vh;
|
|
657
|
+
border-right: none;
|
|
658
|
+
border-bottom: 1px solid var(--color-border-default);
|
|
659
|
+
}
|
|
660
|
+
.detail-row { grid-template-columns: 1fr; gap: 2px; }
|
|
661
|
+
}
|
|
662
|
+
`;
|
|
663
|
+
const REPORT_SCRIPT = `
|
|
664
|
+
(function () {
|
|
665
|
+
const data = JSON.parse(document.getElementById('healflow-data').textContent);
|
|
666
|
+
const searchInput = document.getElementById('tree-search');
|
|
667
|
+
const overview = document.getElementById('overview-panel');
|
|
668
|
+
const details = Array.from(document.querySelectorAll('.test-detail'));
|
|
669
|
+
const testItems = Array.from(document.querySelectorAll('.test-tree-item'));
|
|
670
|
+
const fileGroups = Array.from(document.querySelectorAll('.file-group'));
|
|
671
|
+
|
|
672
|
+
function showOverview() {
|
|
673
|
+
overview.hidden = false;
|
|
674
|
+
details.forEach((panel) => { panel.hidden = true; });
|
|
675
|
+
testItems.forEach((item) => {
|
|
676
|
+
item.classList.toggle('selected', !item.dataset.testId);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function showTest(testId) {
|
|
681
|
+
overview.hidden = true;
|
|
682
|
+
details.forEach((panel) => {
|
|
683
|
+
panel.hidden = panel.id !== 'test-' + testId;
|
|
684
|
+
});
|
|
685
|
+
testItems.forEach((item) => {
|
|
686
|
+
item.classList.toggle('selected', item.dataset.testId === testId);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
testItems.forEach((item) => {
|
|
691
|
+
item.addEventListener('click', () => {
|
|
692
|
+
if (!item.dataset.testId) {
|
|
693
|
+
showOverview();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
showTest(item.dataset.testId);
|
|
697
|
+
});
|
|
698
|
+
item.addEventListener('keydown', (event) => {
|
|
699
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
700
|
+
event.preventDefault();
|
|
701
|
+
if (!item.dataset.testId) {
|
|
702
|
+
showOverview();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
showTest(item.dataset.testId);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
fileGroups.forEach((group) => {
|
|
711
|
+
group.classList.add('expanded');
|
|
712
|
+
const header = group.querySelector('.file-tree-item');
|
|
713
|
+
header.addEventListener('click', () => group.classList.toggle('expanded'));
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
searchInput.addEventListener('input', () => {
|
|
717
|
+
const query = searchInput.value.trim().toLowerCase();
|
|
718
|
+
fileGroups.forEach((group) => {
|
|
719
|
+
let visibleTests = 0;
|
|
720
|
+
group.querySelectorAll('.test-tree-item').forEach((item) => {
|
|
721
|
+
const title = item.textContent.toLowerCase();
|
|
722
|
+
const match = !query || title.includes(query);
|
|
723
|
+
item.style.display = match ? '' : 'none';
|
|
724
|
+
if (match) visibleTests += 1;
|
|
725
|
+
});
|
|
726
|
+
const fileName = group.querySelector('.file-title').textContent.toLowerCase();
|
|
727
|
+
const showGroup = visibleTests > 0 || fileName.includes(query);
|
|
728
|
+
group.style.display = showGroup ? '' : 'none';
|
|
729
|
+
if (showGroup && query) group.classList.add('expanded');
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
})();
|
|
734
|
+
`;
|
|
735
|
+
function buildReportHtml(artifacts, options = {}) {
|
|
736
|
+
const groups = buildTestTree(artifacts);
|
|
737
|
+
const durationMs = options.durationMs ??
|
|
738
|
+
Math.max(0, new Date(artifacts.completedAt).getTime() - new Date(artifacts.startedAt).getTime());
|
|
739
|
+
const passedCount = options.passedCount ?? 0;
|
|
740
|
+
const runStatus = statusLabel(artifacts.status, artifacts.failureCount);
|
|
741
|
+
const statusIcon = runStatus === 'passed' ? STATUS_ICON_PASSED : STATUS_ICON_FAILED;
|
|
742
|
+
const embeddedData = {
|
|
743
|
+
runId: artifacts.runId,
|
|
744
|
+
};
|
|
745
|
+
const passedChip = passedCount > 0 ? `<span class="summary-chip passed">${passedCount} passed</span>` : '';
|
|
746
|
+
const failedChip = artifacts.failureCount > 0
|
|
747
|
+
? `<span class="summary-chip failed">${artifacts.failureCount} failed</span>`
|
|
748
|
+
: '';
|
|
749
|
+
const healedChip = artifacts.healCount > 0
|
|
750
|
+
? `<span class="summary-chip healed">⚡ ${artifacts.healCount} healed</span>`
|
|
751
|
+
: `<span class="summary-chip neutral">0 healed</span>`;
|
|
752
|
+
return `<!DOCTYPE html>
|
|
753
|
+
<html lang="en">
|
|
754
|
+
<head>
|
|
755
|
+
<meta charset="UTF-8" />
|
|
756
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
757
|
+
<title>HealFlow Report</title>
|
|
758
|
+
<style>${REPORT_STYLES}</style>
|
|
759
|
+
</head>
|
|
760
|
+
<body>
|
|
761
|
+
<div class="app">
|
|
762
|
+
<header class="header-view">
|
|
763
|
+
<div class="header-brand">
|
|
764
|
+
<div class="header-logo" aria-hidden="true">HF</div>
|
|
765
|
+
<div>
|
|
766
|
+
<div class="header-title">HealFlow Report</div>
|
|
767
|
+
<div class="header-subtitle">${escapeHtml(formatTimestamp(artifacts.completedAt))}</div>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
<div class="header-view-status-container">
|
|
771
|
+
${passedChip}
|
|
772
|
+
${failedChip}
|
|
773
|
+
${healedChip}
|
|
774
|
+
<span class="summary-chip neutral">${escapeHtml(formatDuration(durationMs))}</span>
|
|
775
|
+
<span class="summary-chip neutral">${escapeHtml(artifacts.mode)} mode</span>
|
|
776
|
+
</div>
|
|
777
|
+
</header>
|
|
778
|
+
|
|
779
|
+
<div class="main-layout">
|
|
780
|
+
<aside class="sidebar" aria-label="Test tree">
|
|
781
|
+
<div class="sidebar-toolbar">
|
|
782
|
+
<input id="tree-search" class="search-input" type="search" placeholder="Search tests…" aria-label="Search tests" />
|
|
783
|
+
</div>
|
|
784
|
+
<div class="tree-panel">
|
|
785
|
+
<div class="tree-item test-tree-item overview-tree-item selected" data-test-id="" role="button" tabindex="0">
|
|
786
|
+
<span class="status-icon" aria-hidden="true">${statusIcon}</span>
|
|
787
|
+
<span class="tree-item-title">Run summary</span>
|
|
788
|
+
</div>
|
|
789
|
+
${renderSidebarTree(groups)}
|
|
790
|
+
</div>
|
|
791
|
+
</aside>
|
|
792
|
+
|
|
793
|
+
<main class="content">
|
|
794
|
+
<section class="overview-panel" id="overview-panel">
|
|
795
|
+
<div class="test-file-title-row">
|
|
796
|
+
<span class="status-icon" aria-hidden="true">${statusIcon}</span>
|
|
797
|
+
<h2 class="test-file-title">Run summary</h2>
|
|
798
|
+
</div>
|
|
799
|
+
<p class="muted">Status: <span class="run-status ${runStatus}">${escapeHtml(runStatus)}</span></p>
|
|
800
|
+
|
|
801
|
+
<div class="overview-grid">
|
|
802
|
+
<div class="meta-card">
|
|
803
|
+
<div class="label">Heals</div>
|
|
804
|
+
<div class="value">${artifacts.healCount}</div>
|
|
805
|
+
</div>
|
|
806
|
+
<div class="meta-card">
|
|
807
|
+
<div class="label">Failures</div>
|
|
808
|
+
<div class="value">${artifacts.failureCount}</div>
|
|
809
|
+
</div>
|
|
810
|
+
<div class="meta-card">
|
|
811
|
+
<div class="label">Fix proposals</div>
|
|
812
|
+
<div class="value">${artifacts.fixes.length}</div>
|
|
813
|
+
</div>
|
|
814
|
+
<div class="meta-card">
|
|
815
|
+
<div class="label">Duration</div>
|
|
816
|
+
<div class="value">${escapeHtml(formatDuration(durationMs))}</div>
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
|
|
820
|
+
<section class="detail-section">
|
|
821
|
+
<div class="chip-header">Run metadata</div>
|
|
822
|
+
<div class="chip-body">
|
|
823
|
+
<div class="detail-grid">
|
|
824
|
+
<div class="detail-row">
|
|
825
|
+
<span class="detail-label">Run ID</span>
|
|
826
|
+
<span class="detail-value mono">${escapeHtml(artifacts.runId)}</span>
|
|
827
|
+
</div>
|
|
828
|
+
<div class="detail-row">
|
|
829
|
+
<span class="detail-label">Started</span>
|
|
830
|
+
<span class="detail-value">${escapeHtml(formatTimestamp(artifacts.startedAt))}</span>
|
|
831
|
+
</div>
|
|
832
|
+
<div class="detail-row">
|
|
833
|
+
<span class="detail-label">Completed</span>
|
|
834
|
+
<span class="detail-value">${escapeHtml(formatTimestamp(artifacts.completedAt))}</span>
|
|
835
|
+
</div>
|
|
836
|
+
<div class="detail-row">
|
|
837
|
+
<span class="detail-label">Mode</span>
|
|
838
|
+
<span class="detail-value">${escapeHtml(artifacts.mode)}</span>
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
</section>
|
|
843
|
+
|
|
844
|
+
<section class="detail-section">
|
|
845
|
+
<div class="chip-header">Heal categories</div>
|
|
846
|
+
<div class="chip-body">
|
|
847
|
+
${renderCategorySummary(artifacts.heals)}
|
|
848
|
+
</div>
|
|
849
|
+
</section>
|
|
850
|
+
</section>
|
|
851
|
+
|
|
852
|
+
${renderTestDetailPanels(groups)}
|
|
853
|
+
</main>
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
|
|
857
|
+
<script id="healflow-data" type="application/json">${escapeHtml(JSON.stringify(embeddedData))}</script>
|
|
858
|
+
<script>${REPORT_SCRIPT}</script>
|
|
859
|
+
</body>
|
|
860
|
+
</html>`;
|
|
861
|
+
}
|
|
862
|
+
//# sourceMappingURL=report-html.js.map
|