@kaizenreport/kensho-viewer 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.
@@ -0,0 +1,1705 @@
1
+ /* Auto-generated from tree-detail.jsx by packages/viewer/scripts/build.js. Edit the .jsx — DO NOT edit this file. */
2
+ /* global React, RetryWaterfall, TestHeader, StepTreeV2 */
3
+ const {
4
+ useState: useStateT
5
+ } = React;
6
+
7
+ // Stable no-op context for the static-report path. We always call
8
+ // React.useContext(...) (Rules of Hooks) but pass this null-ish context when
9
+ // the embed wrapper isn't present, so consumers see `null` and behave as
10
+ // before.
11
+ const _kvNullCtx = React.createContext(null);
12
+
13
+ // RICH_TESTS is owned by data-bridge.jsx and exposed on window.RICH_TESTS
14
+ // from real Kensho run data. Do NOT redefine it here.
15
+
16
+ // ============== Tree node ==============
17
+ function TreeNode({
18
+ node,
19
+ depth,
20
+ openIds,
21
+ onToggle,
22
+ selectedId,
23
+ onSelect,
24
+ leafLabel
25
+ }) {
26
+ const isLeaf = !node.children;
27
+ const open = openIds.has(node.id);
28
+ const test = isLeaf ? window.RICH_TESTS[node.testId] : null;
29
+ const sumCounts = node.counts || {};
30
+ const indent = 12 + depth * 16;
31
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
32
+ onClick: () => isLeaf ? onSelect(node.testId) : onToggle(node.id),
33
+ style: {
34
+ display: 'grid',
35
+ gridTemplateColumns: '14px 1fr auto',
36
+ alignItems: 'center',
37
+ gap: 10,
38
+ padding: '7px 14px',
39
+ paddingLeft: indent,
40
+ cursor: 'pointer',
41
+ background: selectedId === node.testId ? 'var(--accent-soft)' : 'transparent',
42
+ borderLeft: selectedId === node.testId ? '2px solid var(--brand-blue-500)' : '2px solid transparent',
43
+ fontSize: 13,
44
+ transition: 'background var(--dur-fast)'
45
+ },
46
+ onMouseEnter: e => {
47
+ if (selectedId !== node.testId) e.currentTarget.style.background = 'var(--bg-hover)';
48
+ },
49
+ onMouseLeave: e => {
50
+ if (selectedId !== node.testId) e.currentTarget.style.background = 'transparent';
51
+ }
52
+ }, isLeaf ? /*#__PURE__*/React.createElement("span", {
53
+ className: `s-icon ${test.status}`,
54
+ style: {
55
+ width: 14,
56
+ height: 14,
57
+ fontSize: 9
58
+ }
59
+ }, test.status === 'passed' ? '✓' : test.status === 'failed' ? '✕' : test.status === 'broken' ? '!' : '⊘') : /*#__PURE__*/React.createElement("span", {
60
+ style: {
61
+ color: 'var(--fg3)',
62
+ fontFamily: 'var(--font-mono)',
63
+ fontSize: 12,
64
+ lineHeight: 1,
65
+ transform: open ? 'rotate(90deg)' : 'none',
66
+ transition: 'transform var(--dur-fast)'
67
+ }
68
+ }, "\u203A"), /*#__PURE__*/React.createElement("span", {
69
+ style: {
70
+ fontFamily: isLeaf ? 'var(--font-body)' : 'var(--font-mono)',
71
+ fontSize: isLeaf ? 13 : 12.5,
72
+ fontWeight: isLeaf ? 500 : 600,
73
+ color: 'var(--fg1)',
74
+ overflow: 'hidden',
75
+ textOverflow: 'ellipsis',
76
+ whiteSpace: 'nowrap'
77
+ }
78
+ }, isLeaf && /*#__PURE__*/React.createElement("span", {
79
+ style: {
80
+ color: 'var(--fg3)',
81
+ marginRight: 6,
82
+ fontFamily: 'var(--font-mono)',
83
+ fontSize: 11
84
+ }
85
+ }, "#", test.order), leafLabel && isLeaf ? leafLabel(test) : isLeaf ? test.name : node.name, isLeaf && test.retries > 0 && /*#__PURE__*/React.createElement("span", {
86
+ style: {
87
+ color: 'var(--status-broken)',
88
+ marginLeft: 6,
89
+ fontSize: 11,
90
+ fontFamily: 'var(--font-mono)'
91
+ }
92
+ }, "\u21BB", test.retries)), isLeaf ? /*#__PURE__*/React.createElement("span", {
93
+ style: {
94
+ fontFamily: 'var(--font-mono)',
95
+ fontSize: 11,
96
+ color: 'var(--fg3)',
97
+ fontVariantNumeric: 'tabular-nums'
98
+ }
99
+ }, test.dur) : /*#__PURE__*/React.createElement("div", {
100
+ style: {
101
+ display: 'flex',
102
+ gap: 3
103
+ }
104
+ }, sumCounts.failed > 0 && /*#__PURE__*/React.createElement("span", {
105
+ style: {
106
+ background: 'var(--status-failed)',
107
+ color: '#fff',
108
+ fontSize: 10,
109
+ fontWeight: 700,
110
+ padding: '1px 6px',
111
+ borderRadius: 3
112
+ }
113
+ }, sumCounts.failed), sumCounts.broken > 0 && /*#__PURE__*/React.createElement("span", {
114
+ style: {
115
+ background: 'var(--status-broken)',
116
+ color: '#fff',
117
+ fontSize: 10,
118
+ fontWeight: 700,
119
+ padding: '1px 6px',
120
+ borderRadius: 3
121
+ }
122
+ }, sumCounts.broken), sumCounts.passed > 0 && /*#__PURE__*/React.createElement("span", {
123
+ style: {
124
+ background: 'var(--status-passed)',
125
+ color: '#fff',
126
+ fontSize: 10,
127
+ fontWeight: 700,
128
+ padding: '1px 6px',
129
+ borderRadius: 3
130
+ }
131
+ }, sumCounts.passed), sumCounts.skipped > 0 && /*#__PURE__*/React.createElement("span", {
132
+ style: {
133
+ background: 'var(--status-skipped)',
134
+ color: '#fff',
135
+ fontSize: 10,
136
+ fontWeight: 700,
137
+ padding: '1px 6px',
138
+ borderRadius: 3
139
+ }
140
+ }, sumCounts.skipped))), !isLeaf && open && /*#__PURE__*/React.createElement("div", null, node.children.map(c => /*#__PURE__*/React.createElement(TreeNode, {
141
+ key: c.id,
142
+ node: c,
143
+ depth: depth + 1,
144
+ openIds: openIds,
145
+ onToggle: onToggle,
146
+ selectedId: selectedId,
147
+ onSelect: onSelect,
148
+ leafLabel: leafLabel
149
+ }))));
150
+ }
151
+
152
+ // ============== Status filter chips ==============
153
+ function StatusFilters({
154
+ counts,
155
+ active,
156
+ onToggle
157
+ }) {
158
+ const all = ['passed', 'failed', 'broken', 'skipped', 'unknown'];
159
+ const glyph = {
160
+ passed: '✓',
161
+ failed: '✕',
162
+ broken: '!',
163
+ skipped: '⊘',
164
+ unknown: '◌'
165
+ };
166
+ return /*#__PURE__*/React.createElement("div", {
167
+ style: {
168
+ display: 'flex',
169
+ gap: 6,
170
+ alignItems: 'center'
171
+ }
172
+ }, /*#__PURE__*/React.createElement("span", {
173
+ style: {
174
+ fontSize: 11,
175
+ letterSpacing: '.12em',
176
+ textTransform: 'uppercase',
177
+ fontWeight: 600,
178
+ color: 'var(--fg3)',
179
+ marginRight: 4
180
+ }
181
+ }, "Status"), all.map(k => /*#__PURE__*/React.createElement("button", {
182
+ key: k,
183
+ onClick: () => onToggle(k),
184
+ style: {
185
+ display: 'inline-flex',
186
+ alignItems: 'center',
187
+ gap: 5,
188
+ height: 24,
189
+ padding: '0 8px',
190
+ border: '1px solid var(--line)',
191
+ borderRadius: 6,
192
+ background: active.has(k) ? `var(--status-${k})` : '#fff',
193
+ color: active.has(k) ? '#fff' : 'var(--fg2)',
194
+ fontFamily: 'var(--font-mono)',
195
+ fontSize: 11,
196
+ fontWeight: 600,
197
+ cursor: 'pointer',
198
+ fontVariantNumeric: 'tabular-nums',
199
+ transition: 'background var(--dur-fast),color var(--dur-fast)'
200
+ }
201
+ }, /*#__PURE__*/React.createElement("span", {
202
+ style: {
203
+ fontSize: 9
204
+ }
205
+ }, glyph[k]), counts[k] || 0)));
206
+ }
207
+
208
+ // ============== Detail pane (right) ==============
209
+
210
+ // Steps are already mapped to the V2 StepTreeV2 shape by data-bridge.jsx
211
+ // (each step has { name, status, duration, type, logs?, children?, payload?,
212
+ // assertion?, request?, response? }). This is now a near-identity pass-through —
213
+ // we only recurse into children. No name-guessing, no synthetic logs/screenshots.
214
+ function enrichSteps(steps, _test) {
215
+ return steps.map(s => ({
216
+ ...s,
217
+ ...(s.children ? {
218
+ children: enrichSteps(s.children, _test)
219
+ } : {})
220
+ }));
221
+ }
222
+ function DetailPane({
223
+ test,
224
+ defaultTab = 'steps'
225
+ }) {
226
+ const [tab, setTab] = useStateT(defaultTab);
227
+ const [loaded, setLoaded] = useStateT(0);
228
+ const scrollRef = React.useRef(null);
229
+ // Embed-mode extras. Static-report path: __KenshoContext is undefined →
230
+ // ctx === null → no extras. Use a stable no-op context so the hook order
231
+ // stays consistent in both modes.
232
+ const _kvCtx = React.useContext(window.__KenshoContext || _kvNullCtx);
233
+ const extraTabs = _kvCtx?.extraTabs || [];
234
+ // reset scroll to top whenever the selected test changes — otherwise the
235
+ // user can't tell the panel updated (and may think they hit a blank page)
236
+ React.useEffect(() => {
237
+ if (scrollRef.current) scrollRef.current.scrollTop = 0;
238
+ }, [test?.id]);
239
+
240
+ // Lazy-load full case data (steps/error/attachments/history) when the
241
+ // selection changes. data-bridge mutates the richTest in place and sets
242
+ // _stepsLoaded=true; we bump `loaded` to force a re-render once it's ready.
243
+ React.useEffect(() => {
244
+ if (!test) return;
245
+ if (test._stepsLoaded) {
246
+ setLoaded(l => l + 1);
247
+ return;
248
+ }
249
+ window._kenshoEnsureCase(test).then(() => setLoaded(l => l + 1));
250
+ }, [test?.id]);
251
+ if (!test) {
252
+ // Useful empty state — shows run-level summary so the right pane isn't
253
+ // visually dead while the user explores the tree on the left.
254
+ const all = Object.values(window.RICH_TESTS || {});
255
+ const counts = {
256
+ passed: 0,
257
+ failed: 0,
258
+ broken: 0,
259
+ skipped: 0
260
+ };
261
+ for (const t of all) counts[t.status] = (counts[t.status] || 0) + 1;
262
+ const RUN = window.RUN || {};
263
+ const ROWS = [['passed', counts.passed, 'var(--status-passed)'], ['failed', counts.failed, 'var(--status-failed)'], ['broken', counts.broken, 'var(--status-broken)'], ['skipped', counts.skipped, 'var(--status-skipped)']].filter(r => r[1] > 0);
264
+ return /*#__PURE__*/React.createElement("div", {
265
+ style: {
266
+ flex: 1,
267
+ padding: '56px 40px',
268
+ overflow: 'auto'
269
+ }
270
+ }, /*#__PURE__*/React.createElement("div", {
271
+ style: {
272
+ maxWidth: 420,
273
+ margin: '0 auto'
274
+ }
275
+ }, /*#__PURE__*/React.createElement("div", {
276
+ className: "k-overline",
277
+ style: {
278
+ marginBottom: 6
279
+ }
280
+ }, "This run"), /*#__PURE__*/React.createElement("div", {
281
+ style: {
282
+ fontFamily: 'var(--font-display)',
283
+ fontSize: 32,
284
+ fontWeight: 700,
285
+ letterSpacing: -0.5,
286
+ color: 'var(--fg1)',
287
+ marginBottom: 6
288
+ }
289
+ }, all.length, " ", /*#__PURE__*/React.createElement("span", {
290
+ style: {
291
+ color: 'var(--fg3)',
292
+ fontWeight: 500
293
+ }
294
+ }, "test", all.length === 1 ? '' : 's')), /*#__PURE__*/React.createElement("div", {
295
+ style: {
296
+ fontFamily: 'var(--font-mono)',
297
+ fontSize: 12,
298
+ color: 'var(--fg3)',
299
+ marginBottom: 24
300
+ }
301
+ }, RUN.duration || '', RUN.duration ? ' · ' : '', RUN.branch || '', RUN.commit ? ' · ' + RUN.commit : ''), /*#__PURE__*/React.createElement("div", {
302
+ style: {
303
+ display: 'flex',
304
+ flexDirection: 'column',
305
+ gap: 8,
306
+ marginBottom: 28
307
+ }
308
+ }, ROWS.map(([k, n, color]) => /*#__PURE__*/React.createElement("div", {
309
+ key: k,
310
+ style: {
311
+ display: 'grid',
312
+ gridTemplateColumns: '14px 80px 1fr 40px',
313
+ gap: 10,
314
+ alignItems: 'center',
315
+ fontFamily: 'var(--font-mono)',
316
+ fontSize: 12
317
+ }
318
+ }, /*#__PURE__*/React.createElement("span", {
319
+ style: {
320
+ width: 10,
321
+ height: 10,
322
+ borderRadius: 2,
323
+ background: color
324
+ }
325
+ }), /*#__PURE__*/React.createElement("span", {
326
+ style: {
327
+ color: 'var(--fg2)',
328
+ textTransform: 'uppercase',
329
+ letterSpacing: '.08em',
330
+ fontSize: 10.5
331
+ }
332
+ }, k), /*#__PURE__*/React.createElement("div", {
333
+ style: {
334
+ height: 8,
335
+ background: 'var(--bg-sunken)',
336
+ borderRadius: 2,
337
+ overflow: 'hidden'
338
+ }
339
+ }, /*#__PURE__*/React.createElement("div", {
340
+ style: {
341
+ width: `${n / all.length * 100}%`,
342
+ height: '100%',
343
+ background: color
344
+ }
345
+ })), /*#__PURE__*/React.createElement("span", {
346
+ style: {
347
+ color: 'var(--fg1)',
348
+ fontWeight: 600,
349
+ textAlign: 'right',
350
+ fontVariantNumeric: 'tabular-nums'
351
+ }
352
+ }, n)))), /*#__PURE__*/React.createElement("div", {
353
+ style: {
354
+ border: '1px dashed var(--line)',
355
+ borderRadius: 8,
356
+ padding: '18px 20px',
357
+ color: 'var(--fg2)',
358
+ fontFamily: 'var(--font-body)',
359
+ fontSize: 13,
360
+ lineHeight: 1.55
361
+ }
362
+ }, "Pick a test from the tree on the left to inspect its steps, attachments, history, and metadata.", /*#__PURE__*/React.createElement("div", {
363
+ style: {
364
+ marginTop: 6,
365
+ fontFamily: 'var(--font-mono)',
366
+ fontSize: 11,
367
+ color: 'var(--fg3)'
368
+ }
369
+ }, "tip: search by name above, or click ", /*#__PURE__*/React.createElement("b", null, "Expand all"), " to flatten the tree."))));
370
+ }
371
+
372
+ // adapt the rich-tree test shape into the TestHeader props
373
+ const headerTest = {
374
+ id: test.id,
375
+ title: test.name,
376
+ status: test.status,
377
+ duration: test.dur,
378
+ retries: test.retries,
379
+ severity: test.severity,
380
+ owner: (test.owner || '').replace(/^@/, ''),
381
+ // blank → row hides
382
+ suite: test.suite,
383
+ // blank → row hides
384
+ epic: test.epic,
385
+ feature: test.feature,
386
+ story: test.story,
387
+ language: test.language,
388
+ framework: test.framework,
389
+ platform: test.platform,
390
+ lastRun: test.lastRun,
391
+ // only show when supplied
392
+ file: test.file,
393
+ tags: test.tags || [],
394
+ links: test.links || []
395
+ };
396
+
397
+ // While case JSON hasn't been fetched yet, render the header (we have
398
+ // enough metadata for it from the index) plus a skeleton placeholder.
399
+ if (!test._stepsLoaded) return /*#__PURE__*/React.createElement("div", {
400
+ ref: scrollRef,
401
+ style: {
402
+ flex: 1,
403
+ overflow: 'auto',
404
+ padding: 24
405
+ }
406
+ }, /*#__PURE__*/React.createElement(TestHeader, {
407
+ test: headerTest
408
+ }), /*#__PURE__*/React.createElement("div", {
409
+ style: {
410
+ padding: 30,
411
+ color: 'var(--fg3)',
412
+ fontFamily: 'var(--font-mono)',
413
+ fontSize: 12,
414
+ textAlign: 'center'
415
+ }
416
+ }, "Loading steps\u2026"));
417
+ const steps = test.steps || [];
418
+ const enriched = enrichSteps(steps, test);
419
+ const failedCount = function count(ss) {
420
+ return ss.reduce((a, s) => a + (s.status === 'failed' || s.status === 'broken' ? 1 : 0) + (s.children ? count(s.children) : 0), 0);
421
+ }(steps);
422
+ return /*#__PURE__*/React.createElement("div", {
423
+ ref: scrollRef,
424
+ style: {
425
+ flex: 1,
426
+ overflow: 'auto',
427
+ padding: 24,
428
+ minHeight: 0
429
+ }
430
+ }, /*#__PURE__*/React.createElement(TestHeader, {
431
+ test: headerTest
432
+ }), /*#__PURE__*/React.createElement("div", {
433
+ className: "tabs",
434
+ style: {
435
+ marginBottom: 18
436
+ }
437
+ }, ['steps', 'overview', 'log', 'retries', 'history', 'attachments', 'metadata'].map(t => {
438
+ // Hide retries tab when there were none — keeps the chrome tight.
439
+ if (t === 'retries' && !(test.retries > 0)) return null;
440
+ return /*#__PURE__*/React.createElement("div", {
441
+ key: t,
442
+ className: `tab ${tab === t ? 'active' : ''}`,
443
+ onClick: () => setTab(t)
444
+ }, t[0].toUpperCase() + t.slice(1));
445
+ }), extraTabs.map(ex => /*#__PURE__*/React.createElement("div", {
446
+ key: ex.id,
447
+ className: `tab ${tab === ex.id ? 'active' : ''}`,
448
+ onClick: () => setTab(ex.id)
449
+ }, ex.label))), tab === 'overview' && /*#__PURE__*/React.createElement(OverviewTab, {
450
+ test: test
451
+ }), tab === 'steps' && /*#__PURE__*/React.createElement("div", {
452
+ className: "card",
453
+ style: {
454
+ padding: 0,
455
+ marginTop: 0
456
+ }
457
+ }, /*#__PURE__*/React.createElement("div", {
458
+ className: "hd"
459
+ }, /*#__PURE__*/React.createElement("h3", null, "Steps"), /*#__PURE__*/React.createElement("div", {
460
+ className: "meta"
461
+ }, steps.length, " steps \xB7 ", failedCount, " failed")), /*#__PURE__*/React.createElement("div", {
462
+ style: {
463
+ padding: '0 14px 14px'
464
+ }
465
+ }, /*#__PURE__*/React.createElement(StepTreeV2, {
466
+ steps: enriched
467
+ }))), tab === 'log' && /*#__PURE__*/React.createElement(CaseLogTab, {
468
+ test: test
469
+ }), tab === 'retries' && /*#__PURE__*/React.createElement(RetriesTab, {
470
+ test: test
471
+ }), tab === 'history' && /*#__PURE__*/React.createElement(HistoryTab, {
472
+ test: test
473
+ }), tab === 'attachments' && /*#__PURE__*/React.createElement(AttachmentsTab, {
474
+ test: test
475
+ }), tab === 'metadata' && /*#__PURE__*/React.createElement(MetadataTab, {
476
+ test: test
477
+ }), (() => {
478
+ const ex = extraTabs.find(t => t.id === tab);
479
+ return ex ? ex.render(test) : null;
480
+ })());
481
+ }
482
+
483
+ // Aggregate every log line attached to a step (recursively) so the Log tab
484
+ // can present a unified case-level console even when the adapter only
485
+ // captured step-scoped logs. Each line is tagged with the step it came from
486
+ // so we can render a subtle step-context column next to it.
487
+ function collectStepLogs(steps, parentName) {
488
+ const out = [];
489
+ for (const s of steps || []) {
490
+ const ctx = parentName ? `${parentName} › ${s.name}` : s.name;
491
+ for (const l of s.logs || []) {
492
+ out.push({
493
+ ...l,
494
+ _step: ctx
495
+ });
496
+ }
497
+ if (s.children?.length) {
498
+ out.push(...collectStepLogs(s.children, ctx));
499
+ }
500
+ }
501
+ return out;
502
+ }
503
+
504
+ // Case-level Log tab — renders the unified console for this test. Prefers
505
+ // case-level test.logs when the adapter shipped them; falls back to
506
+ // flattening every step's logs so the user sees something useful instead
507
+ // of an "empty" tab when only step-scoped logs were captured.
508
+ function CaseLogTab({
509
+ test
510
+ }) {
511
+ const caseLogs = test.logs || [];
512
+ const stepLogs = caseLogs.length === 0 ? collectStepLogs(test.steps || []) : [];
513
+ const logs = caseLogs.length ? caseLogs : stepLogs;
514
+ const source = caseLogs.length ? 'case' : 'aggregated from steps';
515
+ if (!logs.length) return /*#__PURE__*/React.createElement("div", {
516
+ className: "card",
517
+ style: {
518
+ padding: 30,
519
+ textAlign: 'center',
520
+ color: 'var(--fg3)',
521
+ fontFamily: 'var(--font-mono)',
522
+ fontSize: 12
523
+ }
524
+ }, "No console output captured for this test.", /*#__PURE__*/React.createElement("div", {
525
+ style: {
526
+ marginTop: 6,
527
+ fontSize: 11,
528
+ color: 'var(--fg4)',
529
+ lineHeight: 1.55
530
+ }
531
+ }, "Adapters can ship logs at the case level (", /*#__PURE__*/React.createElement("code", {
532
+ style: {
533
+ background: 'var(--bg-sunken)',
534
+ padding: '1px 6px',
535
+ borderRadius: 3
536
+ }
537
+ }, "case.logs"), ") or per-step (", /*#__PURE__*/React.createElement("code", {
538
+ style: {
539
+ background: 'var(--bg-sunken)',
540
+ padding: '1px 6px',
541
+ borderRadius: 3
542
+ }
543
+ }, "step.logs"), ").", /*#__PURE__*/React.createElement("br", null), "When step logs exist, they're aggregated here automatically."));
544
+ const LVL_COLOR = {
545
+ info: 'var(--fg2)',
546
+ warn: 'var(--status-broken-fg)',
547
+ err: 'var(--status-failed)',
548
+ error: 'var(--status-failed)',
549
+ debug: 'var(--fg3)'
550
+ };
551
+ const LVL_BG = {
552
+ info: 'transparent',
553
+ warn: 'var(--status-broken-bg)',
554
+ err: 'var(--status-failed-bg)',
555
+ error: 'var(--status-failed-bg)',
556
+ debug: 'transparent'
557
+ };
558
+
559
+ // Filter chips: All · Errors · Warnings · Info — let users zero in on
560
+ // what failed without scrolling through hundreds of lines.
561
+ const counts = {
562
+ info: 0,
563
+ warn: 0,
564
+ err: 0,
565
+ debug: 0
566
+ };
567
+ for (const l of logs) {
568
+ const lvl = l.lvl === 'error' ? 'err' : l.lvl || 'info';
569
+ if (counts[lvl] != null) counts[lvl]++;
570
+ }
571
+ const [filter, setFilter] = React.useState('all');
572
+ const FILTERS = [['all', 'All', logs.length], ['err', 'Errors', counts.err], ['warn', 'Warnings', counts.warn], ['info', 'Info', counts.info], ['debug', 'Debug', counts.debug]].filter(([id, _, n]) => id === 'all' || n > 0);
573
+ const visible = filter === 'all' ? logs : logs.filter(l => {
574
+ const lvl = l.lvl === 'error' ? 'err' : l.lvl;
575
+ return lvl === filter;
576
+ });
577
+ return /*#__PURE__*/React.createElement("div", {
578
+ className: "card",
579
+ style: {
580
+ padding: 0
581
+ }
582
+ }, /*#__PURE__*/React.createElement("div", {
583
+ className: "hd"
584
+ }, /*#__PURE__*/React.createElement("h3", null, "Console"), /*#__PURE__*/React.createElement("div", {
585
+ className: "meta"
586
+ }, logs.length, " entries \xB7 ", source)), /*#__PURE__*/React.createElement("div", {
587
+ style: {
588
+ display: 'flex',
589
+ gap: 6,
590
+ padding: '0 14px 12px',
591
+ flexWrap: 'wrap'
592
+ }
593
+ }, FILTERS.map(([id, label, n]) => {
594
+ const active = filter === id;
595
+ const tone = id === 'err' ? 'var(--status-failed)' : id === 'warn' ? 'var(--status-broken)' : null;
596
+ return /*#__PURE__*/React.createElement("button", {
597
+ key: id,
598
+ onClick: () => setFilter(id),
599
+ style: {
600
+ display: 'inline-flex',
601
+ alignItems: 'center',
602
+ gap: 6,
603
+ padding: '3px 10px',
604
+ borderRadius: 999,
605
+ border: '1px solid ' + (active ? tone || 'var(--brand-blue-500)' : 'var(--line)'),
606
+ background: active ? tone || 'var(--brand-blue-500)' : 'var(--bg-elev)',
607
+ color: active ? '#fff' : 'var(--fg2)',
608
+ fontFamily: 'var(--font-body)',
609
+ fontSize: 11.5,
610
+ fontWeight: 600,
611
+ cursor: 'pointer',
612
+ transition: 'all var(--dur-fast)'
613
+ }
614
+ }, label, /*#__PURE__*/React.createElement("span", {
615
+ style: {
616
+ fontFamily: 'var(--font-mono)',
617
+ fontSize: 10.5,
618
+ opacity: 0.9
619
+ }
620
+ }, n));
621
+ })), /*#__PURE__*/React.createElement("div", {
622
+ style: {
623
+ background: 'var(--bg-sunken)',
624
+ borderTop: '1px solid var(--line)',
625
+ padding: '4px 0 12px',
626
+ maxHeight: 520,
627
+ overflow: 'auto'
628
+ }
629
+ }, visible.map((l, i) => {
630
+ const lvl = l.lvl === 'error' ? 'err' : l.lvl || 'info';
631
+ return /*#__PURE__*/React.createElement("div", {
632
+ key: i,
633
+ style: {
634
+ display: 'grid',
635
+ gridTemplateColumns: `80px 50px ${l._step ? '180px ' : ''}1fr`,
636
+ gap: 10,
637
+ padding: '3px 14px',
638
+ background: LVL_BG[lvl] || 'transparent',
639
+ fontFamily: 'var(--font-mono)',
640
+ fontSize: 11.5,
641
+ lineHeight: 1.55
642
+ }
643
+ }, /*#__PURE__*/React.createElement("span", {
644
+ style: {
645
+ color: 'var(--fg3)'
646
+ }
647
+ }, l.ts), /*#__PURE__*/React.createElement("span", {
648
+ style: {
649
+ color: LVL_COLOR[lvl] || 'var(--fg2)',
650
+ fontWeight: 700,
651
+ letterSpacing: 0.5
652
+ }
653
+ }, lvl.toUpperCase()), l._step && /*#__PURE__*/React.createElement("span", {
654
+ style: {
655
+ color: 'var(--fg3)',
656
+ overflow: 'hidden',
657
+ textOverflow: 'ellipsis',
658
+ whiteSpace: 'nowrap'
659
+ },
660
+ title: l._step
661
+ }, l._step), /*#__PURE__*/React.createElement("span", {
662
+ style: {
663
+ color: 'var(--fg1)',
664
+ whiteSpace: 'pre-wrap',
665
+ wordBreak: 'break-word'
666
+ }
667
+ }, l.msg));
668
+ }), visible.length === 0 && /*#__PURE__*/React.createElement("div", {
669
+ style: {
670
+ padding: '30px 14px',
671
+ textAlign: 'center',
672
+ color: 'var(--fg3)',
673
+ fontFamily: 'var(--font-mono)',
674
+ fontSize: 12
675
+ }
676
+ }, "No ", filter, " entries.")));
677
+ }
678
+
679
+ // Metadata tab — user-supplied data only (Allure-style). The header above
680
+ // already shows the canonical fields (severity, owner, suite, epic, etc.),
681
+ // so this tab focuses on what's actually customizable per-test:
682
+ // · Labels — free-form key/value pairs (case.labels) added by the adapter
683
+ // · Parameters — runtime parameters (case.parameters)
684
+ // · Tags — annotations (case.tags)
685
+ // · Links — external references (case.links) — also chip'd in header
686
+ // · Identity — Test ID + file path (always useful for grep + correlation)
687
+ // · Runtime — browser/platform/worker/started (debugging context)
688
+ function MetadataTab({
689
+ test
690
+ }) {
691
+ const labels = test.labels || {};
692
+ const labelEntries = Object.entries(labels);
693
+ const params = test.parameters || [];
694
+ const tags = test.tags || [];
695
+ const links = test.links || [];
696
+ const startedAt = test._summary?.startedAt;
697
+ const Section = ({
698
+ title,
699
+ hint,
700
+ children
701
+ }) => /*#__PURE__*/React.createElement("section", {
702
+ style: {
703
+ marginBottom: 18
704
+ }
705
+ }, /*#__PURE__*/React.createElement("div", {
706
+ style: {
707
+ display: 'flex',
708
+ alignItems: 'baseline',
709
+ gap: 8,
710
+ marginBottom: 8
711
+ }
712
+ }, /*#__PURE__*/React.createElement("div", {
713
+ className: "k-overline"
714
+ }, title), hint && /*#__PURE__*/React.createElement("span", {
715
+ style: {
716
+ fontFamily: 'var(--font-mono)',
717
+ fontSize: 11,
718
+ color: 'var(--fg3)'
719
+ }
720
+ }, hint)), children);
721
+ const KVTable = ({
722
+ rows
723
+ }) => /*#__PURE__*/React.createElement("div", {
724
+ style: {
725
+ border: '1px solid var(--line)',
726
+ borderRadius: 8,
727
+ overflow: 'hidden'
728
+ }
729
+ }, rows.map(([k, v], i) => /*#__PURE__*/React.createElement("div", {
730
+ key: k,
731
+ style: {
732
+ display: 'grid',
733
+ gridTemplateColumns: '200px 1fr',
734
+ borderTop: i ? '1px solid var(--line)' : 'none'
735
+ }
736
+ }, /*#__PURE__*/React.createElement("div", {
737
+ style: {
738
+ padding: '10px 14px',
739
+ background: 'var(--bg-sunken)',
740
+ fontFamily: 'var(--font-mono)',
741
+ fontSize: 11.5,
742
+ color: 'var(--fg3)'
743
+ }
744
+ }, k), /*#__PURE__*/React.createElement("div", {
745
+ style: {
746
+ padding: '10px 14px',
747
+ fontFamily: 'var(--font-mono)',
748
+ fontSize: 12.5,
749
+ color: 'var(--fg1)',
750
+ wordBreak: 'break-all'
751
+ }
752
+ }, v))));
753
+ const isEmpty = labelEntries.length === 0 && params.length === 0 && tags.length === 0 && links.length === 0;
754
+ return /*#__PURE__*/React.createElement("div", {
755
+ className: "card",
756
+ style: {
757
+ padding: 0
758
+ }
759
+ }, /*#__PURE__*/React.createElement("div", {
760
+ className: "hd"
761
+ }, /*#__PURE__*/React.createElement("h3", null, "Metadata"), /*#__PURE__*/React.createElement("div", {
762
+ className: "meta"
763
+ }, "user-supplied data \xB7 adapter-driven")), /*#__PURE__*/React.createElement("div", {
764
+ style: {
765
+ padding: '0 14px 14px'
766
+ }
767
+ }, labelEntries.length > 0 && /*#__PURE__*/React.createElement(Section, {
768
+ title: `Labels · ${labelEntries.length}`,
769
+ hint: "custom key/value pairs from your reporter"
770
+ }, /*#__PURE__*/React.createElement(KVTable, {
771
+ rows: labelEntries.map(([k, v]) => [k, String(v)])
772
+ })), params.length > 0 && /*#__PURE__*/React.createElement(Section, {
773
+ title: `Parameters · ${params.length}`,
774
+ hint: "runtime arguments / data-row values"
775
+ }, /*#__PURE__*/React.createElement(KVTable, {
776
+ rows: params
777
+ })), tags.length > 0 && /*#__PURE__*/React.createElement(Section, {
778
+ title: `Tags · ${tags.length}`
779
+ }, /*#__PURE__*/React.createElement("div", {
780
+ style: {
781
+ display: 'flex',
782
+ flexWrap: 'wrap',
783
+ gap: 6
784
+ }
785
+ }, tags.map(t => /*#__PURE__*/React.createElement("span", {
786
+ key: t,
787
+ style: {
788
+ display: 'inline-flex',
789
+ alignItems: 'center',
790
+ padding: '3px 9px',
791
+ borderRadius: 4,
792
+ background: 'var(--bg-sunken)',
793
+ border: '1px solid var(--line)',
794
+ color: 'var(--fg2)',
795
+ fontFamily: 'var(--font-mono)',
796
+ fontSize: 11.5,
797
+ fontWeight: 500
798
+ }
799
+ }, t)))), links.length > 0 && /*#__PURE__*/React.createElement(Section, {
800
+ title: `External links · ${links.length}`,
801
+ hint: "referenced from the test header above too"
802
+ }, /*#__PURE__*/React.createElement("div", {
803
+ style: {
804
+ display: 'flex',
805
+ flexDirection: 'column',
806
+ gap: 6
807
+ }
808
+ }, links.map((l, i) => /*#__PURE__*/React.createElement("a", {
809
+ key: i,
810
+ href: l.url,
811
+ target: "_blank",
812
+ rel: "noopener noreferrer",
813
+ style: {
814
+ display: 'flex',
815
+ alignItems: 'center',
816
+ gap: 10,
817
+ padding: '10px 14px',
818
+ border: '1px solid var(--line)',
819
+ borderRadius: 6,
820
+ background: 'var(--bg-elev)',
821
+ textDecoration: 'none',
822
+ fontFamily: 'var(--font-mono)',
823
+ fontSize: 12
824
+ }
825
+ }, /*#__PURE__*/React.createElement("span", {
826
+ style: {
827
+ padding: '1px 7px',
828
+ borderRadius: 3,
829
+ background: 'var(--bg-sunken)',
830
+ color: 'var(--fg2)',
831
+ fontSize: 10,
832
+ fontWeight: 700,
833
+ letterSpacing: 0.5,
834
+ textTransform: 'uppercase'
835
+ }
836
+ }, l.kind || 'link'), /*#__PURE__*/React.createElement("span", {
837
+ style: {
838
+ color: 'var(--fg1)',
839
+ fontWeight: 600
840
+ }
841
+ }, l.label || l.url), /*#__PURE__*/React.createElement("span", {
842
+ style: {
843
+ flex: 1,
844
+ color: 'var(--fg3)',
845
+ overflow: 'hidden',
846
+ textOverflow: 'ellipsis',
847
+ whiteSpace: 'nowrap'
848
+ }
849
+ }, l.url))))), /*#__PURE__*/React.createElement(Section, {
850
+ title: "Identity",
851
+ hint: "always-shown locator info"
852
+ }, /*#__PURE__*/React.createElement(KVTable, {
853
+ rows: [['Test ID', test.id], ['Full name', test.fullName || test.name], ['File', test.file || '—'], startedAt ? ['Started at', new Date(startedAt).toLocaleString()] : null].filter(Boolean)
854
+ })), /*#__PURE__*/React.createElement(Section, {
855
+ title: "Runtime",
856
+ hint: "execution context for debugging"
857
+ }, /*#__PURE__*/React.createElement(KVTable, {
858
+ rows: [test._summary?.browser ? ['Browser', test._summary.browser] : null, test.platform ? ['Platform', test.platform] : null, test._summary?.worker != null ? ['Worker', String(test._summary.worker)] : null, test.retries > 0 ? ['Retries', String(test.retries)] : null].filter(Boolean)
859
+ })), isEmpty && /*#__PURE__*/React.createElement("div", {
860
+ style: {
861
+ padding: '30px 14px',
862
+ textAlign: 'center',
863
+ color: 'var(--fg3)',
864
+ fontFamily: 'var(--font-mono)',
865
+ fontSize: 12,
866
+ lineHeight: 1.55
867
+ }
868
+ }, "No labels / parameters / tags / links on this test.", /*#__PURE__*/React.createElement("div", {
869
+ style: {
870
+ marginTop: 10,
871
+ fontSize: 11,
872
+ color: 'var(--fg4)'
873
+ }
874
+ }, "Adapters can attach ", /*#__PURE__*/React.createElement("code", {
875
+ style: {
876
+ background: 'var(--bg-sunken)',
877
+ padding: '1px 6px',
878
+ borderRadius: 3
879
+ }
880
+ }, "case.labels"), ",", /*#__PURE__*/React.createElement("code", {
881
+ style: {
882
+ background: 'var(--bg-sunken)',
883
+ padding: '1px 6px',
884
+ borderRadius: 3,
885
+ marginLeft: 4
886
+ }
887
+ }, "case.parameters"), ", and", /*#__PURE__*/React.createElement("code", {
888
+ style: {
889
+ background: 'var(--bg-sunken)',
890
+ padding: '1px 6px',
891
+ borderRadius: 3,
892
+ marginLeft: 4
893
+ }
894
+ }, "case.links"), " for richer metadata."))));
895
+ }
896
+ function OverviewTab({
897
+ test
898
+ }) {
899
+ return /*#__PURE__*/React.createElement("div", {
900
+ style: {
901
+ display: 'grid',
902
+ gridTemplateColumns: '1fr',
903
+ gap: 18
904
+ }
905
+ }, /*#__PURE__*/React.createElement("section", null, /*#__PURE__*/React.createElement("div", {
906
+ className: "k-overline",
907
+ style: {
908
+ marginBottom: 6
909
+ }
910
+ }, "Description"), /*#__PURE__*/React.createElement("p", {
911
+ className: "k-body",
912
+ style: {
913
+ margin: 0
914
+ }
915
+ }, test.description)), test.bdd && /*#__PURE__*/React.createElement("section", null, /*#__PURE__*/React.createElement("div", {
916
+ className: "k-overline",
917
+ style: {
918
+ marginBottom: 8
919
+ }
920
+ }, "Behavior \xB7 Given / When / Then"), /*#__PURE__*/React.createElement("div", {
921
+ style: {
922
+ border: '1px solid var(--line)',
923
+ borderRadius: 8,
924
+ overflow: 'hidden'
925
+ }
926
+ }, [['GIVEN', test.bdd.given, '#0E5BD9'], ['WHEN', test.bdd.when, '#5B5BD6'], ['THEN', test.bdd.then, '#10864E']].map(([k, v, c], i) => /*#__PURE__*/React.createElement("div", {
927
+ key: k,
928
+ style: {
929
+ display: 'grid',
930
+ gridTemplateColumns: '80px 1fr',
931
+ borderTop: i ? '1px solid var(--line)' : 'none'
932
+ }
933
+ }, /*#__PURE__*/React.createElement("div", {
934
+ style: {
935
+ padding: '10px 12px',
936
+ background: 'var(--bg-sunken)',
937
+ fontFamily: 'var(--font-mono)',
938
+ fontSize: 11,
939
+ fontWeight: 700,
940
+ color: c,
941
+ letterSpacing: '.08em'
942
+ }
943
+ }, k), /*#__PURE__*/React.createElement("div", {
944
+ style: {
945
+ padding: '10px 12px',
946
+ fontFamily: 'var(--font-body)',
947
+ fontSize: 13,
948
+ color: 'var(--fg1)'
949
+ }
950
+ }, v))))), test.parameters?.length > 0 && /*#__PURE__*/React.createElement("section", null, /*#__PURE__*/React.createElement("div", {
951
+ className: "k-overline",
952
+ style: {
953
+ marginBottom: 8
954
+ }
955
+ }, "Parameters \xB7 ", test.parameters.length), /*#__PURE__*/React.createElement("div", {
956
+ style: {
957
+ border: '1px solid var(--line)',
958
+ borderRadius: 8,
959
+ overflow: 'hidden'
960
+ }
961
+ }, test.parameters.map(([k, v], i) => /*#__PURE__*/React.createElement("div", {
962
+ key: k,
963
+ style: {
964
+ display: 'grid',
965
+ gridTemplateColumns: '160px 1fr',
966
+ borderTop: i ? '1px solid var(--line)' : 'none'
967
+ }
968
+ }, /*#__PURE__*/React.createElement("div", {
969
+ style: {
970
+ padding: '8px 12px',
971
+ background: 'var(--bg-sunken)',
972
+ fontFamily: 'var(--font-mono)',
973
+ fontSize: 12,
974
+ color: 'var(--fg3)'
975
+ }
976
+ }, k), /*#__PURE__*/React.createElement("div", {
977
+ style: {
978
+ padding: '8px 12px',
979
+ fontFamily: 'var(--font-mono)',
980
+ fontSize: 12.5,
981
+ color: 'var(--fg1)'
982
+ }
983
+ }, v))))), test.error && /*#__PURE__*/React.createElement("section", null, /*#__PURE__*/React.createElement("div", {
984
+ className: "k-overline",
985
+ style: {
986
+ marginBottom: 8
987
+ }
988
+ }, "Failure"), /*#__PURE__*/React.createElement("div", {
989
+ style: {
990
+ background: 'var(--status-failed-bg)',
991
+ border: '1px solid var(--status-failed-border)',
992
+ borderRadius: 8,
993
+ padding: '12px 14px'
994
+ }
995
+ }, /*#__PURE__*/React.createElement("div", {
996
+ style: {
997
+ display: 'flex',
998
+ alignItems: 'center',
999
+ gap: 8,
1000
+ marginBottom: 6,
1001
+ flexWrap: 'wrap'
1002
+ }
1003
+ }, /*#__PURE__*/React.createElement("span", {
1004
+ style: {
1005
+ fontFamily: 'var(--font-mono)',
1006
+ fontWeight: 700,
1007
+ color: 'var(--status-failed)',
1008
+ fontSize: 11,
1009
+ padding: '2px 7px',
1010
+ background: 'var(--bg-elev)',
1011
+ border: '1px solid var(--status-failed-border)',
1012
+ borderRadius: 3
1013
+ }
1014
+ }, test.error.kind), /*#__PURE__*/React.createElement("span", {
1015
+ style: {
1016
+ fontFamily: 'var(--font-mono)',
1017
+ fontSize: 12.5,
1018
+ color: 'var(--status-failed-fg)'
1019
+ }
1020
+ }, test.error.message)), test.error.stack && /*#__PURE__*/React.createElement("pre", {
1021
+ style: {
1022
+ margin: 0,
1023
+ fontFamily: 'var(--font-mono)',
1024
+ fontSize: 11.5,
1025
+ color: 'var(--status-failed-fg)',
1026
+ whiteSpace: 'pre-wrap'
1027
+ }
1028
+ }, test.error.stack))), /*#__PURE__*/React.createElement("section", null, /*#__PURE__*/React.createElement("div", {
1029
+ className: "k-overline",
1030
+ style: {
1031
+ marginBottom: 8
1032
+ }
1033
+ }, "Execution timeline"), /*#__PURE__*/React.createElement(StepTreeRich, {
1034
+ steps: test.steps || []
1035
+ })));
1036
+ }
1037
+ function StepTreeRich({
1038
+ steps
1039
+ }) {
1040
+ return /*#__PURE__*/React.createElement("div", {
1041
+ style: {
1042
+ display: 'flex',
1043
+ flexDirection: 'column',
1044
+ border: '1px solid var(--line)',
1045
+ borderRadius: 8,
1046
+ overflow: 'hidden'
1047
+ }
1048
+ }, steps.map((s, i) => /*#__PURE__*/React.createElement(StepRichNode, {
1049
+ key: i,
1050
+ step: s,
1051
+ depth: 0,
1052
+ last: i === steps.length - 1
1053
+ })));
1054
+ }
1055
+ function StepRichNode({
1056
+ step,
1057
+ depth,
1058
+ last
1059
+ }) {
1060
+ const [open, setOpen] = useStateT(true);
1061
+ const has = step.children && step.children.length;
1062
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
1063
+ onClick: () => has && setOpen(!open),
1064
+ style: {
1065
+ display: 'grid',
1066
+ gridTemplateColumns: '14px 1fr auto',
1067
+ alignItems: 'center',
1068
+ gap: 10,
1069
+ padding: '10px 14px',
1070
+ paddingLeft: 14 + depth * 18,
1071
+ cursor: has ? 'pointer' : 'default',
1072
+ borderBottom: !last || has ? '1px solid var(--line)' : 'none',
1073
+ background: depth > 0 ? 'var(--bg-sunken)' : 'transparent'
1074
+ }
1075
+ }, /*#__PURE__*/React.createElement("span", {
1076
+ className: `s-icon ${step.status}`,
1077
+ style: {
1078
+ width: 14,
1079
+ height: 14,
1080
+ fontSize: 9
1081
+ }
1082
+ }, step.status === 'passed' ? '✓' : step.status === 'failed' ? '✕' : step.status === 'broken' ? '!' : '⊘'), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
1083
+ style: {
1084
+ fontSize: 13,
1085
+ color: 'var(--fg1)',
1086
+ fontWeight: 500,
1087
+ display: 'flex',
1088
+ alignItems: 'center',
1089
+ gap: 6
1090
+ }
1091
+ }, has && /*#__PURE__*/React.createElement("span", {
1092
+ style: {
1093
+ color: 'var(--fg3)',
1094
+ display: 'inline-block',
1095
+ fontSize: 11,
1096
+ transform: open ? 'rotate(90deg)' : 'none',
1097
+ transition: 'transform var(--dur-fast)'
1098
+ }
1099
+ }, "\u203A"), step.name), step.params && /*#__PURE__*/React.createElement("div", {
1100
+ style: {
1101
+ marginTop: 3,
1102
+ fontFamily: 'var(--font-mono)',
1103
+ fontSize: 11,
1104
+ color: 'var(--fg3)'
1105
+ }
1106
+ }, step.params.map(([k, v], i) => /*#__PURE__*/React.createElement("span", {
1107
+ key: i,
1108
+ style: {
1109
+ marginRight: 10
1110
+ }
1111
+ }, k, "=", /*#__PURE__*/React.createElement("span", {
1112
+ style: {
1113
+ color: 'var(--fg2)'
1114
+ }
1115
+ }, v))))), /*#__PURE__*/React.createElement("span", {
1116
+ style: {
1117
+ fontFamily: 'var(--font-mono)',
1118
+ fontSize: 11,
1119
+ color: 'var(--fg3)',
1120
+ fontVariantNumeric: 'tabular-nums'
1121
+ }
1122
+ }, step.dur || step.duration)), has && open && step.children.map((c, i) => /*#__PURE__*/React.createElement(StepRichNode, {
1123
+ key: i,
1124
+ step: c,
1125
+ depth: depth + 1,
1126
+ last: i === step.children.length - 1 && last
1127
+ })));
1128
+ }
1129
+ function RetriesTab({
1130
+ test
1131
+ }) {
1132
+ const attempts = test.retries > 0 ? Array.from({
1133
+ length: test.retries + 1
1134
+ }, (_, i) => ({
1135
+ status: i === test.retries ? test.status : 'failed',
1136
+ dur: 1800 + i * 400,
1137
+ label: i === test.retries ? `attempt ${i + 1} — ${test.status === 'passed' ? 'recovered' : 'final ' + (test.error?.kind || 'error')}` : `attempt ${i + 1} — ${test.error?.kind || 'TimeoutError'}`
1138
+ })) : null;
1139
+ if (!attempts) return /*#__PURE__*/React.createElement("div", {
1140
+ style: {
1141
+ color: 'var(--fg3)',
1142
+ fontFamily: 'var(--font-mono)',
1143
+ fontSize: 12,
1144
+ padding: '30px 20px',
1145
+ textAlign: 'center',
1146
+ border: '1px dashed var(--line)',
1147
+ borderRadius: 8
1148
+ }
1149
+ }, "No retries on this run.");
1150
+ return /*#__PURE__*/React.createElement(RetryWaterfall, {
1151
+ attempts: attempts
1152
+ });
1153
+ }
1154
+ function HistoryTab({
1155
+ test
1156
+ }) {
1157
+ const STATUS_MAP = {
1158
+ pass: 'passed',
1159
+ fail: 'failed',
1160
+ broken: 'broken',
1161
+ skip: 'skipped'
1162
+ };
1163
+ const runs = (test.history || []).map(h => ({
1164
+ id: '#' + h.runId,
1165
+ when: window._kenshoRelTime ? window._kenshoRelTime(h.startedAt) : h.startedAt,
1166
+ status: STATUS_MAP[h.status] || h.status,
1167
+ dur: window._kenshoFmtDuration ? window._kenshoFmtDuration(h.duration) : h.duration
1168
+ }));
1169
+ if (runs.length === 0) {
1170
+ return /*#__PURE__*/React.createElement("div", {
1171
+ style: {
1172
+ padding: 30,
1173
+ textAlign: 'center',
1174
+ color: 'var(--fg3)',
1175
+ fontFamily: 'var(--font-mono)',
1176
+ fontSize: 12
1177
+ }
1178
+ }, "No prior run history available for this test.");
1179
+ }
1180
+ return /*#__PURE__*/React.createElement("div", {
1181
+ style: {
1182
+ border: '1px solid var(--line)',
1183
+ borderRadius: 8,
1184
+ overflow: 'hidden'
1185
+ }
1186
+ }, runs.map((r, i) => /*#__PURE__*/React.createElement("div", {
1187
+ key: i,
1188
+ style: {
1189
+ display: 'grid',
1190
+ gridTemplateColumns: '24px 1fr 90px 100px',
1191
+ alignItems: 'center',
1192
+ gap: 10,
1193
+ padding: '10px 14px',
1194
+ borderBottom: i < runs.length - 1 ? '1px solid var(--line)' : 'none'
1195
+ }
1196
+ }, /*#__PURE__*/React.createElement("span", {
1197
+ className: `s-icon ${r.status}`
1198
+ }, r.status === 'passed' ? '✓' : r.status === 'failed' ? '✕' : '!'), /*#__PURE__*/React.createElement("span", {
1199
+ style: {
1200
+ fontFamily: 'var(--font-mono)',
1201
+ fontSize: 12
1202
+ }
1203
+ }, r.id), /*#__PURE__*/React.createElement("span", {
1204
+ style: {
1205
+ fontFamily: 'var(--font-mono)',
1206
+ fontSize: 12,
1207
+ color: 'var(--fg3)'
1208
+ }
1209
+ }, r.dur), /*#__PURE__*/React.createElement("span", {
1210
+ style: {
1211
+ fontFamily: 'var(--font-mono)',
1212
+ fontSize: 11,
1213
+ color: 'var(--fg3)',
1214
+ textAlign: 'right'
1215
+ }
1216
+ }, r.when))));
1217
+ }
1218
+ function AttachmentsTab({
1219
+ test
1220
+ }) {
1221
+ const ICON_MAP = {
1222
+ screenshot: 'image',
1223
+ image: 'image',
1224
+ video: 'video',
1225
+ trace: 'terminal',
1226
+ log: 'terminal',
1227
+ text: 'terminal',
1228
+ har: 'globe',
1229
+ json: 'code',
1230
+ html: 'code'
1231
+ };
1232
+ const prettyBytes = n => {
1233
+ if (n == null || isNaN(n)) return '';
1234
+ if (n < 1024) return `${n} B`;
1235
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
1236
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
1237
+ return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GB`;
1238
+ };
1239
+ const basename = p => {
1240
+ if (!p) return '';
1241
+ const parts = String(p).split(/[\\/]/);
1242
+ return parts[parts.length - 1] || p;
1243
+ };
1244
+ const items = (test.attachments || []).map(a => ({
1245
+ name: basename(a.relativePath) || a.id,
1246
+ size: prettyBytes(a.sizeBytes),
1247
+ icon: ICON_MAP[a.kind] || 'file',
1248
+ preview: a.kind === 'screenshot' || a.kind === 'video' || a.kind === 'image'
1249
+ }));
1250
+ if (items.length === 0) {
1251
+ return /*#__PURE__*/React.createElement("div", {
1252
+ style: {
1253
+ padding: 30,
1254
+ textAlign: 'center',
1255
+ color: 'var(--fg3)',
1256
+ fontFamily: 'var(--font-mono)',
1257
+ fontSize: 12
1258
+ }
1259
+ }, "No attachments captured for this test.");
1260
+ }
1261
+ return /*#__PURE__*/React.createElement("div", {
1262
+ style: {
1263
+ display: 'grid',
1264
+ gridTemplateColumns: '1fr 1fr',
1265
+ gap: 10
1266
+ }
1267
+ }, items.map(a => /*#__PURE__*/React.createElement("div", {
1268
+ key: a.name,
1269
+ style: {
1270
+ border: '1px solid var(--line)',
1271
+ borderRadius: 8,
1272
+ overflow: 'hidden',
1273
+ background: 'var(--bg-elev)'
1274
+ }
1275
+ }, a.preview && /*#__PURE__*/React.createElement("div", {
1276
+ style: {
1277
+ height: 110,
1278
+ background: 'repeating-linear-gradient(135deg, var(--bg-sunken) 0 12px, var(--bg-elev) 12px 24px)',
1279
+ borderBottom: '1px solid var(--line)'
1280
+ }
1281
+ }), /*#__PURE__*/React.createElement("div", {
1282
+ style: {
1283
+ display: 'flex',
1284
+ alignItems: 'center',
1285
+ gap: 8,
1286
+ padding: '10px 12px'
1287
+ }
1288
+ }, /*#__PURE__*/React.createElement("i", {
1289
+ "data-lucide": a.icon,
1290
+ style: {
1291
+ width: 14,
1292
+ height: 14
1293
+ }
1294
+ }), /*#__PURE__*/React.createElement("span", {
1295
+ style: {
1296
+ flex: 1,
1297
+ fontFamily: 'var(--font-mono)',
1298
+ fontSize: 12,
1299
+ color: 'var(--fg1)'
1300
+ }
1301
+ }, a.name), /*#__PURE__*/React.createElement("span", {
1302
+ style: {
1303
+ fontFamily: 'var(--font-mono)',
1304
+ fontSize: 11,
1305
+ color: 'var(--fg3)'
1306
+ }
1307
+ }, a.size)))));
1308
+ }
1309
+
1310
+ // ============== Splitter constants ==============
1311
+ const KV_SPLIT_KEY = 'kensho.tree.split';
1312
+ const KV_SPLIT_MIN = 280;
1313
+ const KV_SPLIT_DEFAULT = 480;
1314
+ const KV_SPLIT_KEY_STEP = 16;
1315
+ function readPersistedSplit() {
1316
+ try {
1317
+ const raw = window.localStorage?.getItem(KV_SPLIT_KEY);
1318
+ if (!raw) return KV_SPLIT_DEFAULT;
1319
+ const n = parseFloat(raw);
1320
+ if (!Number.isFinite(n) || n < KV_SPLIT_MIN) return KV_SPLIT_DEFAULT;
1321
+ return n;
1322
+ } catch (_) {
1323
+ return KV_SPLIT_DEFAULT;
1324
+ }
1325
+ }
1326
+
1327
+ // ============== Generic Tree+Detail page ==============
1328
+ function TreeDetailPage({
1329
+ title,
1330
+ subtitle,
1331
+ tree,
1332
+ leafLabel,
1333
+ headerExtra,
1334
+ defaultOpenAll = false
1335
+ }) {
1336
+ const allIds = collectIds(tree);
1337
+ // Default: every branch collapsed, no leaf selected. The detail pane shows
1338
+ // a summary placeholder so the user explicitly picks what to inspect — at
1339
+ // 800+ tests, auto-loading the first leaf wastes a fetch and renders a
1340
+ // misleading "first thing alphabetically" view.
1341
+ const [openIds, setOpenIds] = useStateT(new Set(defaultOpenAll ? allIds : []));
1342
+ const [selectedId, setSelectedId] = useStateT(null);
1343
+ const [filters, setFilters] = useStateT(new Set(['passed', 'failed', 'broken', 'skipped', 'unknown']));
1344
+ const [query, setQuery] = useStateT('');
1345
+
1346
+ // ============== Splitter (resizable tree column) ==============
1347
+ // Width of the left tree column. Restored from localStorage on mount;
1348
+ // clamped to [MIN, 70% of parent] on every drag/key tick. We persist on
1349
+ // pointerup / keyup, not on every mousemove, to avoid hammering storage.
1350
+ const [splitWidth, setSplitWidth] = useStateT(() => readPersistedSplit());
1351
+ const [dragging, setDragging] = useStateT(false);
1352
+ const splitContainerRef = React.useRef(null);
1353
+ const dragStateRef = React.useRef(null);
1354
+ const persistSplit = React.useCallback(w => {
1355
+ try {
1356
+ window.localStorage?.setItem(KV_SPLIT_KEY, String(Math.round(w)));
1357
+ } catch (_) {}
1358
+ }, []);
1359
+ const clampWidth = React.useCallback(w => {
1360
+ const parentW = splitContainerRef.current?.getBoundingClientRect().width || 0;
1361
+ const max = Math.max(KV_SPLIT_MIN, Math.floor(parentW * 0.7));
1362
+ return Math.max(KV_SPLIT_MIN, Math.min(max, w));
1363
+ }, []);
1364
+ const onSplitPointerDown = React.useCallback(e => {
1365
+ if (e.button !== 0 && e.pointerType === 'mouse') return;
1366
+ e.preventDefault();
1367
+ const target = e.currentTarget;
1368
+ try {
1369
+ target.setPointerCapture(e.pointerId);
1370
+ } catch (_) {}
1371
+ dragStateRef.current = {
1372
+ startX: e.clientX,
1373
+ startWidth: splitWidth,
1374
+ pointerId: e.pointerId,
1375
+ target
1376
+ };
1377
+ setDragging(true);
1378
+ // Prevent text selection of the tree while dragging.
1379
+ document.body.style.userSelect = 'none';
1380
+ document.body.style.cursor = 'col-resize';
1381
+ }, [splitWidth]);
1382
+ const onSplitPointerMove = React.useCallback(e => {
1383
+ const ds = dragStateRef.current;
1384
+ if (!ds) return;
1385
+ const dx = e.clientX - ds.startX;
1386
+ const next = clampWidth(ds.startWidth + dx);
1387
+ setSplitWidth(next);
1388
+ }, [clampWidth]);
1389
+ const endDrag = React.useCallback(() => {
1390
+ const ds = dragStateRef.current;
1391
+ if (!ds) return;
1392
+ try {
1393
+ ds.target?.releasePointerCapture?.(ds.pointerId);
1394
+ } catch (_) {}
1395
+ dragStateRef.current = null;
1396
+ setDragging(false);
1397
+ document.body.style.userSelect = '';
1398
+ document.body.style.cursor = '';
1399
+ // Persist the latest committed width (functional setter so we read the
1400
+ // freshest value, not a stale closure copy).
1401
+ setSplitWidth(w => {
1402
+ persistSplit(w);
1403
+ return w;
1404
+ });
1405
+ }, [persistSplit]);
1406
+ const onSplitKeyDown = React.useCallback(e => {
1407
+ let next = null;
1408
+ if (e.key === 'ArrowLeft') next = splitWidth - KV_SPLIT_KEY_STEP;else if (e.key === 'ArrowRight') next = splitWidth + KV_SPLIT_KEY_STEP;else if (e.key === 'Home') next = KV_SPLIT_MIN;else if (e.key === 'End') {
1409
+ const parentW = splitContainerRef.current?.getBoundingClientRect().width || 0;
1410
+ next = Math.floor(parentW * 0.7);
1411
+ }
1412
+ if (next == null) return;
1413
+ e.preventDefault();
1414
+ const clamped = clampWidth(next);
1415
+ setSplitWidth(clamped);
1416
+ persistSplit(clamped);
1417
+ }, [splitWidth, clampWidth, persistSplit]);
1418
+
1419
+ // Track parent width so we can publish a useful aria-valuemax to AT and
1420
+ // re-clamp on viewport changes. Falls back to current splitWidth before
1421
+ // the ref attaches (so aria-valuemax never trails aria-valuenow).
1422
+ const [parentWidth, setParentWidth] = useStateT(0);
1423
+ React.useEffect(() => {
1424
+ const measure = () => {
1425
+ const w = splitContainerRef.current?.getBoundingClientRect().width || 0;
1426
+ if (w) setParentWidth(w);
1427
+ };
1428
+ measure();
1429
+ const onResize = () => {
1430
+ measure();
1431
+ setSplitWidth(w => clampWidth(w));
1432
+ };
1433
+ window.addEventListener('resize', onResize);
1434
+ return () => window.removeEventListener('resize', onResize);
1435
+ }, [clampWidth]);
1436
+ const ariaMax = parentWidth > 0 ? Math.max(KV_SPLIT_MIN, Math.floor(parentWidth * 0.7)) : Math.max(KV_SPLIT_MIN, splitWidth);
1437
+ const totalCounts = countTree(tree);
1438
+ const filteredTree = filterTree(tree, filters, query);
1439
+ const toggle = id => {
1440
+ const n = new Set(openIds);
1441
+ n.has(id) ? n.delete(id) : n.add(id);
1442
+ setOpenIds(n);
1443
+ };
1444
+ const toggleFilter = k => {
1445
+ const n = new Set(filters);
1446
+ n.has(k) ? n.delete(k) : n.add(k);
1447
+ setFilters(n);
1448
+ };
1449
+ const test = selectedId ? window.RICH_TESTS[selectedId] : null;
1450
+ const totalTests = Object.values(totalCounts).reduce((a, b) => a + b, 0);
1451
+
1452
+ // Visible-leaf order — recomputed on every change to filteredTree+openIds.
1453
+ // Drives `j` / `k` / Enter shortcuts: navigate among VISIBLE leaves only.
1454
+ const visibleLeafIds = React.useMemo(() => {
1455
+ const out = [];
1456
+ const walk = nodes => {
1457
+ for (const n of nodes) {
1458
+ if (!n.children) {
1459
+ out.push(n.testId);
1460
+ continue;
1461
+ }
1462
+ if (openIds.has(n.id)) walk(n.children);
1463
+ }
1464
+ };
1465
+ walk(filteredTree);
1466
+ return out;
1467
+ }, [filteredTree, openIds]);
1468
+
1469
+ // Wire up keyboard shortcuts dispatched from app.jsx.
1470
+ React.useEffect(() => {
1471
+ const onMove = e => {
1472
+ const delta = e.detail?.delta || 0;
1473
+ if (visibleLeafIds.length === 0) return;
1474
+ const idx = selectedId ? visibleLeafIds.indexOf(selectedId) : -1;
1475
+ const nextIdx = idx === -1 ? delta > 0 ? 0 : visibleLeafIds.length - 1 : Math.max(0, Math.min(visibleLeafIds.length - 1, idx + delta));
1476
+ setSelectedId(visibleLeafIds[nextIdx]);
1477
+ };
1478
+ const onOpen = () => {
1479
+ if (selectedId) window.__openTest?.(selectedId);
1480
+ };
1481
+ const onClear = () => {
1482
+ setQuery('');
1483
+ setSelectedId(null);
1484
+ };
1485
+ window.addEventListener('kensho:move-selection', onMove);
1486
+ window.addEventListener('kensho:open-selection', onOpen);
1487
+ window.addEventListener('kensho:clear-search', onClear);
1488
+ return () => {
1489
+ window.removeEventListener('kensho:move-selection', onMove);
1490
+ window.removeEventListener('kensho:open-selection', onOpen);
1491
+ window.removeEventListener('kensho:clear-search', onClear);
1492
+ };
1493
+ }, [selectedId, visibleLeafIds]);
1494
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
1495
+ style: {
1496
+ display: 'flex',
1497
+ alignItems: 'baseline',
1498
+ justifyContent: 'space-between',
1499
+ marginBottom: 14,
1500
+ gap: 16,
1501
+ flexWrap: 'wrap'
1502
+ }
1503
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h1", {
1504
+ className: "k-h1",
1505
+ style: {
1506
+ marginBottom: 2
1507
+ }
1508
+ }, title), /*#__PURE__*/React.createElement("div", {
1509
+ className: "k-meta"
1510
+ }, subtitle, " \xB7 ", totalTests, " tests")), headerExtra), /*#__PURE__*/React.createElement("div", {
1511
+ style: {
1512
+ display: 'flex',
1513
+ gap: 12,
1514
+ alignItems: 'center',
1515
+ marginBottom: 14,
1516
+ flexWrap: 'wrap'
1517
+ }
1518
+ }, /*#__PURE__*/React.createElement("div", {
1519
+ "data-kv-search": true,
1520
+ className: "kv-tree-search",
1521
+ style: {
1522
+ display: 'flex',
1523
+ alignItems: 'center',
1524
+ gap: 8,
1525
+ height: 32,
1526
+ padding: '0 10px',
1527
+ background: 'var(--bg-elev)',
1528
+ border: '1px solid var(--line)',
1529
+ borderRadius: 6,
1530
+ flex: '0 0 280px'
1531
+ }
1532
+ }, /*#__PURE__*/React.createElement("i", {
1533
+ "data-lucide": "search",
1534
+ style: {
1535
+ width: 14,
1536
+ height: 14,
1537
+ color: 'var(--fg3)'
1538
+ }
1539
+ }), /*#__PURE__*/React.createElement("input", {
1540
+ placeholder: "Search tests\u2026 (press /)",
1541
+ value: query,
1542
+ onChange: e => setQuery(e.target.value),
1543
+ style: {
1544
+ flex: 1,
1545
+ border: 0,
1546
+ outline: 0,
1547
+ fontFamily: 'var(--font-body)',
1548
+ fontSize: 13,
1549
+ background: 'transparent'
1550
+ }
1551
+ })), /*#__PURE__*/React.createElement(StatusFilters, {
1552
+ counts: totalCounts,
1553
+ active: filters,
1554
+ onToggle: toggleFilter
1555
+ }), /*#__PURE__*/React.createElement("div", {
1556
+ style: {
1557
+ flex: 1
1558
+ }
1559
+ }), /*#__PURE__*/React.createElement("button", {
1560
+ className: "btn btn-secondary",
1561
+ onClick: () => setOpenIds(new Set(allIds)),
1562
+ style: {
1563
+ height: 30
1564
+ }
1565
+ }, "Expand all"), /*#__PURE__*/React.createElement("button", {
1566
+ className: "btn btn-secondary",
1567
+ onClick: () => setOpenIds(new Set()),
1568
+ style: {
1569
+ height: 30
1570
+ }
1571
+ }, "Collapse all")), /*#__PURE__*/React.createElement("div", {
1572
+ ref: splitContainerRef,
1573
+ style: {
1574
+ display: 'grid',
1575
+ gridTemplateColumns: `${splitWidth}px 8px 1fr`,
1576
+ gap: 0,
1577
+ background: 'var(--bg-elev)',
1578
+ border: '1px solid var(--line)',
1579
+ borderRadius: 12,
1580
+ overflow: 'hidden',
1581
+ height: 'calc(100vh - 220px)',
1582
+ minHeight: 560
1583
+ }
1584
+ }, /*#__PURE__*/React.createElement("div", {
1585
+ style: {
1586
+ overflow: 'auto',
1587
+ minHeight: 0
1588
+ }
1589
+ }, filteredTree.length === 0 ? /*#__PURE__*/React.createElement("div", {
1590
+ style: {
1591
+ padding: 30,
1592
+ textAlign: 'center',
1593
+ color: 'var(--fg3)',
1594
+ fontFamily: 'var(--font-mono)',
1595
+ fontSize: 12
1596
+ }
1597
+ }, "No tests match the current filter.") : filteredTree.map(n => /*#__PURE__*/React.createElement(TreeNode, {
1598
+ key: n.id,
1599
+ node: n,
1600
+ depth: 0,
1601
+ openIds: openIds,
1602
+ onToggle: toggle,
1603
+ selectedId: selectedId,
1604
+ onSelect: setSelectedId,
1605
+ leafLabel: leafLabel
1606
+ }))), /*#__PURE__*/React.createElement("div", {
1607
+ className: `kv-split-handle${dragging ? ' kv-split-handle--active' : ''}`,
1608
+ role: "separator",
1609
+ "aria-orientation": "vertical",
1610
+ "aria-valuenow": Math.round(splitWidth),
1611
+ "aria-valuemin": KV_SPLIT_MIN,
1612
+ "aria-valuemax": ariaMax,
1613
+ "aria-label": "Resize test tree column",
1614
+ tabIndex: 0,
1615
+ onPointerDown: onSplitPointerDown,
1616
+ onPointerMove: onSplitPointerMove,
1617
+ onPointerUp: endDrag,
1618
+ onPointerCancel: endDrag,
1619
+ onKeyDown: onSplitKeyDown
1620
+ }), /*#__PURE__*/React.createElement(DetailPane, {
1621
+ test: test
1622
+ })));
1623
+ }
1624
+
1625
+ // helpers
1626
+ function collectIds(tree) {
1627
+ const out = [];
1628
+ const walk = ns => ns.forEach(n => {
1629
+ if (n.children) {
1630
+ out.push(n.id);
1631
+ walk(n.children);
1632
+ }
1633
+ });
1634
+ walk(tree);
1635
+ return out;
1636
+ }
1637
+ function firstLeaf(tree) {
1638
+ for (const n of tree) {
1639
+ if (!n.children) return n.testId;
1640
+ const r = firstLeaf(n.children);
1641
+ if (r) return r;
1642
+ }
1643
+ return null;
1644
+ }
1645
+ function countTree(tree) {
1646
+ const c = {
1647
+ passed: 0,
1648
+ failed: 0,
1649
+ broken: 0,
1650
+ skipped: 0,
1651
+ unknown: 0
1652
+ };
1653
+ const walk = ns => ns.forEach(n => {
1654
+ if (!n.children) {
1655
+ const t = window.RICH_TESTS[n.testId];
1656
+ if (t) c[t.status]++;
1657
+ } else walk(n.children);
1658
+ });
1659
+ walk(tree);
1660
+ return c;
1661
+ }
1662
+ function filterTree(tree, statusSet, query) {
1663
+ const q = query.trim().toLowerCase();
1664
+ const matchLeaf = n => {
1665
+ const t = window.RICH_TESTS[n.testId];
1666
+ if (!t) return false;
1667
+ if (!statusSet.has(t.status)) return false;
1668
+ if (q && !t.name.toLowerCase().includes(q)) return false;
1669
+ return true;
1670
+ };
1671
+ const recount = list => {
1672
+ const c = {
1673
+ passed: 0,
1674
+ failed: 0,
1675
+ broken: 0,
1676
+ skipped: 0
1677
+ };
1678
+ const walk = ls => ls.forEach(k => {
1679
+ if (!k.children) {
1680
+ const t = window.RICH_TESTS[k.testId];
1681
+ if (t && c[t.status] !== undefined) c[t.status]++;
1682
+ } else walk(k.children);
1683
+ });
1684
+ walk(list);
1685
+ return c;
1686
+ };
1687
+ const walk = ns => ns.map(n => {
1688
+ if (!n.children) return matchLeaf(n) ? n : null;
1689
+ const kids = walk(n.children).filter(Boolean);
1690
+ if (!kids.length) return null;
1691
+ return {
1692
+ ...n,
1693
+ children: kids,
1694
+ counts: recount(kids)
1695
+ };
1696
+ }).filter(Boolean);
1697
+ return walk(tree);
1698
+ }
1699
+ Object.assign(window, {
1700
+ TreeDetailPage,
1701
+ DetailPane,
1702
+ StepTreeRich,
1703
+ CaseLogTab,
1704
+ MetadataTab
1705
+ });