@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,1058 @@
1
+ /* Auto-generated from test-detail.jsx by packages/viewer/scripts/build.js. Edit the .jsx — DO NOT edit this file. */
2
+ // ============================================================
3
+ // TEST DETAIL — header + step tree with logs/payloads/screenshots
4
+ // Multi-platform: Web · Mobile · API · DB
5
+ // Multi-language: TS · JS · Python · Java · Kotlin · Swift · Go · Ruby · C#
6
+ // ============================================================
7
+
8
+ // ----------- helpers -----------
9
+
10
+ const SEVERITY_COLORS = {
11
+ blocker: {
12
+ bg: 'var(--status-failed-bg)',
13
+ fg: 'var(--status-failed-fg)',
14
+ border: 'var(--status-failed-border)'
15
+ },
16
+ critical: {
17
+ bg: 'var(--status-failed-bg)',
18
+ fg: 'var(--status-failed-fg)',
19
+ border: 'var(--status-failed-border)'
20
+ },
21
+ normal: {
22
+ bg: 'var(--status-broken-bg)',
23
+ fg: 'var(--status-broken-fg)',
24
+ border: 'var(--status-broken-border)'
25
+ },
26
+ minor: {
27
+ bg: 'var(--status-skipped-bg)',
28
+ fg: 'var(--status-skipped-fg)',
29
+ border: 'var(--status-skipped-border)'
30
+ },
31
+ trivial: {
32
+ bg: 'var(--status-skipped-bg)',
33
+ fg: 'var(--status-skipped-fg)',
34
+ border: 'var(--status-skipped-border)'
35
+ }
36
+ };
37
+ function CopyPath({
38
+ path
39
+ }) {
40
+ const [copied, setCopied] = React.useState(false);
41
+ const handle = () => {
42
+ navigator.clipboard?.writeText(path);
43
+ setCopied(true);
44
+ setTimeout(() => setCopied(false), 1500);
45
+ };
46
+ return /*#__PURE__*/React.createElement("button", {
47
+ onClick: handle,
48
+ style: {
49
+ display: 'inline-flex',
50
+ alignItems: 'center',
51
+ gap: 6,
52
+ fontFamily: 'var(--font-mono)',
53
+ fontSize: 12,
54
+ color: 'var(--fg2)',
55
+ background: 'transparent',
56
+ border: 'none',
57
+ padding: '2px 6px',
58
+ borderRadius: 4,
59
+ cursor: 'pointer'
60
+ },
61
+ onMouseEnter: e => e.currentTarget.style.background = 'var(--bg-hover)',
62
+ onMouseLeave: e => e.currentTarget.style.background = 'transparent'
63
+ }, /*#__PURE__*/React.createElement("span", {
64
+ style: {
65
+ color: 'var(--fg2)'
66
+ }
67
+ }, path), /*#__PURE__*/React.createElement(Icon, {
68
+ name: copied ? 'check' : 'copy',
69
+ size: 11
70
+ }), copied && /*#__PURE__*/React.createElement("span", {
71
+ style: {
72
+ fontSize: 10,
73
+ color: 'var(--status-passed)'
74
+ }
75
+ }, "copied"));
76
+ }
77
+
78
+ // CopyPermalink — round-trip the current URL with a `#/case/<id>` fragment so
79
+ // users can paste a deep-link to a specific case into Slack/Jira/etc.
80
+ function CopyPermalink({
81
+ testId
82
+ }) {
83
+ const [copied, setCopied] = React.useState(false);
84
+ const handle = e => {
85
+ e.stopPropagation();
86
+ const base = window.location.href.split('#')[0];
87
+ const url = base + '#/case/' + encodeURIComponent(testId);
88
+ navigator.clipboard?.writeText(url);
89
+ setCopied(true);
90
+ window.__kenshoToast?.('Link copied to clipboard');
91
+ setTimeout(() => setCopied(false), 1500);
92
+ };
93
+ return /*#__PURE__*/React.createElement("button", {
94
+ onClick: handle,
95
+ title: "Copy permalink to this test",
96
+ style: {
97
+ display: 'inline-flex',
98
+ alignItems: 'center',
99
+ gap: 5,
100
+ fontFamily: 'var(--font-mono)',
101
+ fontSize: 11,
102
+ fontWeight: 600,
103
+ color: copied ? 'var(--status-passed)' : 'var(--fg3)',
104
+ background: 'transparent',
105
+ border: '1px solid var(--line)',
106
+ padding: '2px 8px',
107
+ borderRadius: 4,
108
+ cursor: 'pointer',
109
+ transition: 'background var(--dur-fast), color var(--dur-fast)'
110
+ },
111
+ onMouseEnter: e => e.currentTarget.style.background = 'var(--bg-hover)',
112
+ onMouseLeave: e => e.currentTarget.style.background = 'transparent'
113
+ }, /*#__PURE__*/React.createElement(Icon, {
114
+ name: copied ? 'check' : 'link',
115
+ size: 11
116
+ }), copied ? 'copied' : 'copy link');
117
+ }
118
+ function StatusPill({
119
+ status
120
+ }) {
121
+ const map = {
122
+ passed: {
123
+ label: 'PASSED',
124
+ fg: 'var(--status-passed)',
125
+ bg: 'var(--status-passed-bg)'
126
+ },
127
+ failed: {
128
+ label: 'FAILED',
129
+ fg: 'var(--status-failed)',
130
+ bg: 'var(--status-failed-bg)'
131
+ },
132
+ broken: {
133
+ label: 'BROKEN',
134
+ fg: 'var(--status-broken)',
135
+ bg: 'var(--status-broken-bg)'
136
+ },
137
+ skipped: {
138
+ label: 'SKIPPED',
139
+ fg: 'var(--status-skipped)',
140
+ bg: 'var(--status-skipped-bg)'
141
+ }
142
+ };
143
+ const m = map[status] || map.passed;
144
+ return /*#__PURE__*/React.createElement("span", {
145
+ style: {
146
+ display: 'inline-flex',
147
+ alignItems: 'center',
148
+ gap: 6,
149
+ padding: '3px 8px',
150
+ borderRadius: 4,
151
+ background: m.bg,
152
+ color: m.fg,
153
+ fontFamily: 'var(--font-mono)',
154
+ fontSize: 11,
155
+ fontWeight: 700,
156
+ letterSpacing: 0.5
157
+ }
158
+ }, /*#__PURE__*/React.createElement("span", {
159
+ style: {
160
+ width: 6,
161
+ height: 6,
162
+ borderRadius: 999,
163
+ background: m.fg
164
+ }
165
+ }), m.label);
166
+ }
167
+ function MetaField({
168
+ label,
169
+ children
170
+ }) {
171
+ return /*#__PURE__*/React.createElement("div", {
172
+ style: {
173
+ display: 'inline-flex',
174
+ alignItems: 'center',
175
+ gap: 6
176
+ }
177
+ }, /*#__PURE__*/React.createElement("span", {
178
+ style: {
179
+ fontFamily: 'var(--font-mono)',
180
+ fontSize: 10.5,
181
+ color: 'var(--fg3)',
182
+ letterSpacing: 1,
183
+ textTransform: 'uppercase'
184
+ }
185
+ }, label), /*#__PURE__*/React.createElement("span", {
186
+ style: {
187
+ fontFamily: 'var(--font-mono)',
188
+ fontSize: 12,
189
+ color: 'var(--fg1)'
190
+ }
191
+ }, children));
192
+ }
193
+ function Tag({
194
+ children
195
+ }) {
196
+ return /*#__PURE__*/React.createElement("span", {
197
+ style: {
198
+ display: 'inline-flex',
199
+ alignItems: 'center',
200
+ padding: '2px 8px',
201
+ borderRadius: 4,
202
+ background: 'var(--bg-2)',
203
+ border: '1px solid var(--line)',
204
+ color: 'var(--fg2)',
205
+ fontFamily: 'var(--font-mono)',
206
+ fontSize: 11,
207
+ fontWeight: 500
208
+ }
209
+ }, children);
210
+ }
211
+
212
+ // LinkChip — renders external test links (Jira ticket, GitHub PR, runbook,
213
+ // design doc, defect, etc.) as clickable, color-coded chips next to tags.
214
+ // Each kind gets a distinct tint so users can scan visually. Falls back to
215
+ // the URL host when no label is provided.
216
+ const LINK_KIND_STYLE = {
217
+ jira: {
218
+ bg: '#E7F0FB',
219
+ fg: '#1D4ED8',
220
+ border: '#BCD3F0',
221
+ icon: 'square-pen'
222
+ },
223
+ github: {
224
+ bg: '#F4EFFF',
225
+ fg: '#5B21B6',
226
+ border: '#D4C4F4',
227
+ icon: 'github'
228
+ },
229
+ gitlab: {
230
+ bg: '#FFF4EC',
231
+ fg: '#B7430C',
232
+ border: '#F4D2BC',
233
+ icon: 'git-branch'
234
+ },
235
+ linear: {
236
+ bg: '#EEEDFB',
237
+ fg: '#3F3DB7',
238
+ border: '#C8C5F0',
239
+ icon: 'square-arrow-out-up-right'
240
+ },
241
+ pr: {
242
+ bg: '#F4EFFF',
243
+ fg: '#5B21B6',
244
+ border: '#D4C4F4',
245
+ icon: 'git-pull-request'
246
+ },
247
+ bug: {
248
+ bg: '#FCEBEC',
249
+ fg: '#B91C1C',
250
+ border: '#F2C8CB',
251
+ icon: 'bug'
252
+ },
253
+ defect: {
254
+ bg: '#FCEBEC',
255
+ fg: '#B91C1C',
256
+ border: '#F2C8CB',
257
+ icon: 'bug'
258
+ },
259
+ doc: {
260
+ bg: '#FFF4DD',
261
+ fg: '#92400E',
262
+ border: '#F4DDA8',
263
+ icon: 'book-open'
264
+ },
265
+ runbook: {
266
+ bg: '#FFF4DD',
267
+ fg: '#92400E',
268
+ border: '#F4DDA8',
269
+ icon: 'play-square'
270
+ },
271
+ slack: {
272
+ bg: '#EAFBF1',
273
+ fg: '#0E5C39',
274
+ border: '#C5E5D2',
275
+ icon: 'message-square'
276
+ },
277
+ other: {
278
+ bg: 'var(--bg-sunken)',
279
+ fg: 'var(--fg2)',
280
+ border: 'var(--line)',
281
+ icon: 'link-2'
282
+ }
283
+ };
284
+ function LinkChip({
285
+ link
286
+ }) {
287
+ const kind = (link.kind || 'other').toLowerCase();
288
+ const s = LINK_KIND_STYLE[kind] || LINK_KIND_STYLE.other;
289
+ const label = link.label || (() => {
290
+ try {
291
+ return new URL(link.url).hostname.replace(/^www\./, '');
292
+ } catch {
293
+ return link.url;
294
+ }
295
+ })();
296
+ return /*#__PURE__*/React.createElement("a", {
297
+ href: link.url,
298
+ target: "_blank",
299
+ rel: "noopener noreferrer",
300
+ title: `${(link.kind || 'link').toUpperCase()} · ${link.url}`,
301
+ style: {
302
+ display: 'inline-flex',
303
+ alignItems: 'center',
304
+ gap: 6,
305
+ padding: '2px 8px',
306
+ borderRadius: 4,
307
+ background: s.bg,
308
+ color: s.fg,
309
+ border: `1px solid ${s.border}`,
310
+ fontFamily: 'var(--font-mono)',
311
+ fontSize: 11,
312
+ fontWeight: 600,
313
+ textDecoration: 'none',
314
+ transition: 'background var(--dur-fast)'
315
+ }
316
+ }, /*#__PURE__*/React.createElement(Icon, {
317
+ name: s.icon,
318
+ size: 11
319
+ }), label);
320
+ }
321
+ function SeverityBadge({
322
+ level
323
+ }) {
324
+ const c = SEVERITY_COLORS[level] || SEVERITY_COLORS.normal;
325
+ return /*#__PURE__*/React.createElement("span", {
326
+ style: {
327
+ display: 'inline-flex',
328
+ alignItems: 'center',
329
+ padding: '1px 7px',
330
+ borderRadius: 3,
331
+ background: c.bg,
332
+ color: c.fg,
333
+ border: `1px solid ${c.border}`,
334
+ fontFamily: 'var(--font-mono)',
335
+ fontSize: 10.5,
336
+ fontWeight: 700,
337
+ textTransform: 'uppercase',
338
+ letterSpacing: 0.5
339
+ }
340
+ }, level);
341
+ }
342
+
343
+ // ----------- header -----------
344
+
345
+ function TestHeader({
346
+ test,
347
+ onCopyId
348
+ }) {
349
+ // Visual hierarchy:
350
+ // 1) file path (copyable, monospace, deemphasized)
351
+ // 2) title row: status pill · title · tags right-aligned
352
+ // 3) facts grid — vertical key-value stack, 2 cols on wide, 1 col on narrow
353
+ //
354
+ // Why: the previous horizontal strip wrapped clumsily. A definition-list
355
+ // style grid stays readable at any width, and a vertical cascade is what
356
+ // real test-report tools (Allure, Cypress dashboard, BrowserStack) use.
357
+
358
+ // Conditional metadata — only render fields the user actually provided.
359
+ // Empty/null/'unassigned'/'—' all suppress the row so the grid stays clean.
360
+ // Severity, Duration and Test ID are always shown (core identity).
361
+ const isBlank = v => v == null || v === '' || v === '—' || v === 'unassigned';
362
+ const facts = [{
363
+ k: 'Severity',
364
+ v: /*#__PURE__*/React.createElement(SeverityBadge, {
365
+ level: test.severity
366
+ }),
367
+ always: true
368
+ }, {
369
+ k: 'Duration',
370
+ v: /*#__PURE__*/React.createElement("b", {
371
+ style: {
372
+ fontWeight: 600
373
+ }
374
+ }, test.duration),
375
+ always: true
376
+ }, !isBlank(test.owner) && {
377
+ k: 'Owner',
378
+ v: /*#__PURE__*/React.createElement("span", {
379
+ style: {
380
+ color: 'var(--accent)'
381
+ }
382
+ }, "@", test.owner)
383
+ }, !isBlank(test.suite) && {
384
+ k: 'Suite',
385
+ v: test.suite
386
+ }, !isBlank(test.lastRun) && {
387
+ k: 'Last run',
388
+ v: test.lastRun
389
+ }, !isBlank(test.platform) && {
390
+ k: 'Platform',
391
+ v: test.platform
392
+ }, !isBlank(test.epic) && {
393
+ k: 'Epic',
394
+ v: test.epic
395
+ }, !isBlank(test.feature) && {
396
+ k: 'Feature',
397
+ v: test.feature
398
+ }, !isBlank(test.story) && {
399
+ k: 'Story',
400
+ v: test.story
401
+ }, !isBlank(test.language) && {
402
+ k: 'Language',
403
+ v: test.language
404
+ }, !isBlank(test.framework) && {
405
+ k: 'Framework',
406
+ v: test.framework
407
+ }, {
408
+ k: 'Test ID',
409
+ v: /*#__PURE__*/React.createElement("span", {
410
+ style: {
411
+ fontFamily: 'var(--font-mono)',
412
+ fontSize: 11.5
413
+ }
414
+ }, test.id),
415
+ always: true
416
+ }].filter(Boolean);
417
+ return /*#__PURE__*/React.createElement("div", {
418
+ style: {
419
+ marginBottom: 22
420
+ }
421
+ }, /*#__PURE__*/React.createElement("div", {
422
+ style: {
423
+ marginBottom: 8,
424
+ marginLeft: -6,
425
+ display: 'flex',
426
+ alignItems: 'center',
427
+ gap: 8,
428
+ flexWrap: 'wrap'
429
+ }
430
+ }, /*#__PURE__*/React.createElement(CopyPath, {
431
+ path: test.file
432
+ }), /*#__PURE__*/React.createElement(CopyPermalink, {
433
+ testId: test.id
434
+ })), /*#__PURE__*/React.createElement("div", {
435
+ style: {
436
+ display: 'flex',
437
+ alignItems: 'flex-start',
438
+ gap: 14,
439
+ flexWrap: 'wrap',
440
+ marginBottom: 14
441
+ }
442
+ }, /*#__PURE__*/React.createElement("div", {
443
+ style: {
444
+ display: 'flex',
445
+ alignItems: 'center',
446
+ gap: 10,
447
+ flexWrap: 'wrap',
448
+ flex: 1,
449
+ minWidth: 0
450
+ }
451
+ }, /*#__PURE__*/React.createElement(StatusPill, {
452
+ status: test.status
453
+ }), test.retries > 0 && /*#__PURE__*/React.createElement("span", {
454
+ style: {
455
+ display: 'inline-flex',
456
+ alignItems: 'center',
457
+ gap: 5,
458
+ fontFamily: 'var(--font-mono)',
459
+ fontSize: 12,
460
+ color: 'var(--fg2)'
461
+ }
462
+ }, /*#__PURE__*/React.createElement(Icon, {
463
+ name: "rotate-ccw",
464
+ size: 12
465
+ }), test.retries, " ", test.retries === 1 ? 'retry' : 'retries'), /*#__PURE__*/React.createElement("h1", {
466
+ style: {
467
+ fontSize: 26,
468
+ fontWeight: 600,
469
+ color: 'var(--fg1)',
470
+ margin: 0,
471
+ letterSpacing: -0.3,
472
+ lineHeight: 1.2
473
+ }
474
+ }, test.title)), (test.tags?.length > 0 || test.links?.length > 0) && /*#__PURE__*/React.createElement("div", {
475
+ style: {
476
+ display: 'flex',
477
+ gap: 6,
478
+ flexWrap: 'wrap',
479
+ alignItems: 'center'
480
+ }
481
+ }, (test.links || []).map((l, i) => /*#__PURE__*/React.createElement(LinkChip, {
482
+ key: 'L' + i,
483
+ link: l
484
+ })), test.tags?.map(t => /*#__PURE__*/React.createElement(Tag, {
485
+ key: t
486
+ }, t)))), /*#__PURE__*/React.createElement("div", {
487
+ style: {
488
+ borderTop: '1px solid var(--line)',
489
+ paddingTop: 14,
490
+ display: 'grid',
491
+ gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
492
+ gap: '10px 28px'
493
+ }
494
+ }, facts.map(f => /*#__PURE__*/React.createElement("div", {
495
+ key: f.k,
496
+ style: {
497
+ display: 'grid',
498
+ gridTemplateColumns: '88px 1fr',
499
+ alignItems: 'start',
500
+ gap: 10,
501
+ minWidth: 0
502
+ }
503
+ }, /*#__PURE__*/React.createElement("span", {
504
+ style: {
505
+ fontFamily: 'var(--font-mono)',
506
+ fontSize: 10.5,
507
+ color: 'var(--fg3)',
508
+ letterSpacing: 1,
509
+ textTransform: 'uppercase',
510
+ paddingTop: 2
511
+ }
512
+ }, f.k), /*#__PURE__*/React.createElement("span", {
513
+ style: {
514
+ fontFamily: 'var(--font-mono)',
515
+ fontSize: 12.5,
516
+ color: 'var(--fg1)',
517
+ wordBreak: 'break-word',
518
+ overflowWrap: 'anywhere',
519
+ minWidth: 0
520
+ }
521
+ }, f.v)))));
522
+ }
523
+
524
+ // ----------- step tree v2 -----------
525
+ //
526
+ // A step has: { name, status, duration, type?, kind?, body?, logs?, payload?, screenshot?, request?, response?, assertion?, children? }
527
+ // type: 'action' (default) | 'assertion' | 'http' | 'screenshot' | 'device' | 'db' | 'group'
528
+ // logs: [{ ts, lvl: info|warn|err|debug, msg }]
529
+
530
+ const STEP_TYPE_ICON = {
531
+ action: 'mouse-pointer-2',
532
+ assertion: 'check-check',
533
+ http: 'globe',
534
+ screenshot: 'image',
535
+ device: 'smartphone',
536
+ db: 'database',
537
+ group: 'folder',
538
+ navigation: 'navigation',
539
+ api: 'globe',
540
+ setup: 'wrench',
541
+ teardown: 'eraser'
542
+ };
543
+ function StepIcon({
544
+ type
545
+ }) {
546
+ const name = STEP_TYPE_ICON[type] || 'chevron-right';
547
+ return /*#__PURE__*/React.createElement(Icon, {
548
+ name: name,
549
+ size: 12
550
+ });
551
+ }
552
+ function LogLine({
553
+ log
554
+ }) {
555
+ const lvlColor = {
556
+ info: 'var(--fg2)',
557
+ warn: 'var(--status-broken-fg)',
558
+ err: 'var(--status-failed)',
559
+ debug: 'var(--fg3)'
560
+ }[log.lvl] || 'var(--fg2)';
561
+ const lvlBg = {
562
+ info: 'transparent',
563
+ warn: 'var(--status-broken-bg)',
564
+ err: 'var(--status-failed-bg)',
565
+ debug: 'transparent'
566
+ }[log.lvl] || 'transparent';
567
+ return /*#__PURE__*/React.createElement("div", {
568
+ style: {
569
+ display: 'grid',
570
+ gridTemplateColumns: '90px 50px 1fr',
571
+ gap: 10,
572
+ padding: '3px 8px',
573
+ borderRadius: 3,
574
+ background: lvlBg,
575
+ fontFamily: 'var(--font-mono)',
576
+ fontSize: 11.5,
577
+ lineHeight: 1.55
578
+ }
579
+ }, /*#__PURE__*/React.createElement("span", {
580
+ style: {
581
+ color: 'var(--fg3)'
582
+ }
583
+ }, log.ts), /*#__PURE__*/React.createElement("span", {
584
+ style: {
585
+ color: lvlColor,
586
+ fontWeight: 700,
587
+ letterSpacing: 0.5
588
+ }
589
+ }, log.lvl.toUpperCase()), /*#__PURE__*/React.createElement("span", {
590
+ style: {
591
+ color: 'var(--fg1)',
592
+ whiteSpace: 'pre-wrap',
593
+ wordBreak: 'break-word'
594
+ }
595
+ }, log.msg));
596
+ }
597
+ function HttpBlock({
598
+ request,
599
+ response
600
+ }) {
601
+ const statusColor = response.status >= 500 ? 'var(--status-failed)' : response.status >= 400 ? 'var(--status-broken)' : 'var(--status-passed)';
602
+ return /*#__PURE__*/React.createElement("div", {
603
+ style: {
604
+ border: '1px solid var(--line)',
605
+ borderRadius: 6,
606
+ overflow: 'hidden',
607
+ fontFamily: 'var(--font-mono)',
608
+ fontSize: 11.5
609
+ }
610
+ }, /*#__PURE__*/React.createElement("div", {
611
+ style: {
612
+ padding: '8px 12px',
613
+ borderBottom: '1px solid var(--line)',
614
+ background: 'var(--bg-2)'
615
+ }
616
+ }, /*#__PURE__*/React.createElement("div", {
617
+ style: {
618
+ display: 'flex',
619
+ alignItems: 'center',
620
+ gap: 8
621
+ }
622
+ }, /*#__PURE__*/React.createElement("span", {
623
+ style: {
624
+ padding: '1px 6px',
625
+ borderRadius: 3,
626
+ background: 'var(--dark-bg)',
627
+ color: '#fff',
628
+ fontSize: 10,
629
+ fontWeight: 700
630
+ }
631
+ }, request.method), /*#__PURE__*/React.createElement("span", {
632
+ style: {
633
+ color: 'var(--fg1)'
634
+ }
635
+ }, request.url), /*#__PURE__*/React.createElement("span", {
636
+ style: {
637
+ marginLeft: 'auto',
638
+ color: 'var(--fg3)'
639
+ }
640
+ }, request.duration)), request.headers && /*#__PURE__*/React.createElement("details", {
641
+ style: {
642
+ marginTop: 6
643
+ }
644
+ }, /*#__PURE__*/React.createElement("summary", {
645
+ style: {
646
+ cursor: 'pointer',
647
+ color: 'var(--fg3)',
648
+ fontSize: 10.5,
649
+ letterSpacing: 0.5,
650
+ textTransform: 'uppercase'
651
+ }
652
+ }, "Request headers (", Object.keys(request.headers).length, ")"), /*#__PURE__*/React.createElement("pre", {
653
+ style: {
654
+ margin: '6px 0 0',
655
+ padding: 8,
656
+ background: 'var(--bg-elev)',
657
+ border: '1px solid var(--line)',
658
+ borderRadius: 4,
659
+ fontSize: 11,
660
+ color: 'var(--fg2)',
661
+ overflow: 'auto'
662
+ }
663
+ }, Object.entries(request.headers).map(([k, v]) => `${k}: ${v}`).join('\n'))), request.body && /*#__PURE__*/React.createElement("details", {
664
+ style: {
665
+ marginTop: 6
666
+ },
667
+ open: true
668
+ }, /*#__PURE__*/React.createElement("summary", {
669
+ style: {
670
+ cursor: 'pointer',
671
+ color: 'var(--fg3)',
672
+ fontSize: 10.5,
673
+ letterSpacing: 0.5,
674
+ textTransform: 'uppercase'
675
+ }
676
+ }, "Request body"), /*#__PURE__*/React.createElement("pre", {
677
+ style: {
678
+ margin: '6px 0 0',
679
+ padding: 8,
680
+ background: 'var(--bg-elev)',
681
+ border: '1px solid var(--line)',
682
+ borderRadius: 4,
683
+ fontSize: 11,
684
+ color: 'var(--fg2)',
685
+ overflow: 'auto'
686
+ }
687
+ }, typeof request.body === 'string' ? request.body : JSON.stringify(request.body, null, 2)))), /*#__PURE__*/React.createElement("div", {
688
+ style: {
689
+ padding: '8px 12px'
690
+ }
691
+ }, /*#__PURE__*/React.createElement("div", {
692
+ style: {
693
+ display: 'flex',
694
+ alignItems: 'center',
695
+ gap: 8
696
+ }
697
+ }, /*#__PURE__*/React.createElement("span", {
698
+ style: {
699
+ padding: '1px 6px',
700
+ borderRadius: 3,
701
+ background: statusColor,
702
+ color: '#fff',
703
+ fontSize: 10,
704
+ fontWeight: 700
705
+ }
706
+ }, response.status, " ", response.statusText), /*#__PURE__*/React.createElement("span", {
707
+ style: {
708
+ color: 'var(--fg3)'
709
+ }
710
+ }, response.size)), response.body && /*#__PURE__*/React.createElement("details", {
711
+ style: {
712
+ marginTop: 6
713
+ },
714
+ open: true
715
+ }, /*#__PURE__*/React.createElement("summary", {
716
+ style: {
717
+ cursor: 'pointer',
718
+ color: 'var(--fg3)',
719
+ fontSize: 10.5,
720
+ letterSpacing: 0.5,
721
+ textTransform: 'uppercase'
722
+ }
723
+ }, "Response body"), /*#__PURE__*/React.createElement("pre", {
724
+ style: {
725
+ margin: '6px 0 0',
726
+ padding: 8,
727
+ background: 'var(--bg-2)',
728
+ border: '1px solid var(--line)',
729
+ borderRadius: 4,
730
+ fontSize: 11,
731
+ color: 'var(--fg2)',
732
+ overflow: 'auto',
733
+ maxHeight: 200
734
+ }
735
+ }, typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2)))));
736
+ }
737
+ function AssertionBlock({
738
+ assertion
739
+ }) {
740
+ const ok = assertion.passed;
741
+ const c = ok ? {
742
+ fg: 'var(--status-passed)',
743
+ bg: 'var(--status-passed-bg)',
744
+ border: 'var(--status-passed-border)'
745
+ } : {
746
+ fg: 'var(--status-failed)',
747
+ bg: 'var(--status-failed-bg)',
748
+ border: 'var(--status-failed-border)'
749
+ };
750
+ return /*#__PURE__*/React.createElement("div", {
751
+ style: {
752
+ border: `1px solid ${c.border}`,
753
+ background: c.bg,
754
+ borderRadius: 6,
755
+ padding: 10,
756
+ fontFamily: 'var(--font-mono)',
757
+ fontSize: 11.5,
758
+ color: 'var(--fg1)'
759
+ }
760
+ }, /*#__PURE__*/React.createElement("div", {
761
+ style: {
762
+ display: 'flex',
763
+ gap: 8,
764
+ alignItems: 'center',
765
+ marginBottom: 6
766
+ }
767
+ }, /*#__PURE__*/React.createElement("span", {
768
+ style: {
769
+ color: c.fg,
770
+ fontWeight: 700,
771
+ letterSpacing: 0.5
772
+ }
773
+ }, ok ? '✓ ASSERTION PASSED' : '✕ ASSERTION FAILED'), /*#__PURE__*/React.createElement("span", {
774
+ style: {
775
+ color: 'var(--fg3)'
776
+ }
777
+ }, assertion.matcher)), /*#__PURE__*/React.createElement("div", {
778
+ style: {
779
+ display: 'grid',
780
+ gridTemplateColumns: '70px 1fr',
781
+ gap: '4px 10px'
782
+ }
783
+ }, /*#__PURE__*/React.createElement("span", {
784
+ style: {
785
+ color: 'var(--fg3)'
786
+ }
787
+ }, "expected"), /*#__PURE__*/React.createElement("span", {
788
+ style: {
789
+ color: 'var(--status-passed)'
790
+ }
791
+ }, assertion.expected), /*#__PURE__*/React.createElement("span", {
792
+ style: {
793
+ color: 'var(--fg3)'
794
+ }
795
+ }, "actual"), /*#__PURE__*/React.createElement("span", {
796
+ style: {
797
+ color: ok ? 'var(--status-passed)' : 'var(--status-failed)'
798
+ }
799
+ }, assertion.actual)));
800
+ }
801
+ function ScreenshotBlock({
802
+ screenshot
803
+ }) {
804
+ // `screenshot.url` (if present) is the direct path to the image file. When
805
+ // the bridge maps `step.attachments[]` to this shape it sets `.url` to
806
+ // `data/<relativePath>`. Falls back to the placeholder hatching when the
807
+ // adapter only described the screenshot without copying it through.
808
+ const url = screenshot.url;
809
+ return /*#__PURE__*/React.createElement("div", {
810
+ style: {
811
+ display: 'flex',
812
+ gap: 10,
813
+ alignItems: 'flex-start'
814
+ }
815
+ }, /*#__PURE__*/React.createElement("a", {
816
+ href: url || '#',
817
+ target: "_blank",
818
+ rel: "noopener noreferrer",
819
+ onClick: url ? undefined : e => e.preventDefault(),
820
+ style: {
821
+ flexShrink: 0,
822
+ display: 'block'
823
+ },
824
+ title: url ? 'Open full-size in new tab' : screenshot.name
825
+ }, url ? /*#__PURE__*/React.createElement("img", {
826
+ src: url,
827
+ alt: screenshot.name || 'screenshot',
828
+ style: {
829
+ width: 200,
830
+ height: 120,
831
+ objectFit: 'cover',
832
+ borderRadius: 6,
833
+ border: '1px solid var(--line)',
834
+ display: 'block',
835
+ cursor: 'zoom-in'
836
+ },
837
+ onError: e => {
838
+ e.currentTarget.style.display = 'none';
839
+ }
840
+ }) : /*#__PURE__*/React.createElement("div", {
841
+ style: {
842
+ width: 160,
843
+ height: 100,
844
+ borderRadius: 6,
845
+ border: '1px solid var(--line)',
846
+ background: `repeating-linear-gradient(135deg, var(--bg-sunken) 0 12px, var(--bg-hover) 12px 24px)`,
847
+ display: 'flex',
848
+ alignItems: 'center',
849
+ justifyContent: 'center'
850
+ }
851
+ }, /*#__PURE__*/React.createElement(Icon, {
852
+ name: "image",
853
+ size: 20
854
+ }))), /*#__PURE__*/React.createElement("div", {
855
+ style: {
856
+ flex: 1,
857
+ fontFamily: 'var(--font-mono)',
858
+ fontSize: 11.5,
859
+ color: 'var(--fg2)',
860
+ minWidth: 0
861
+ }
862
+ }, /*#__PURE__*/React.createElement("div", {
863
+ style: {
864
+ color: 'var(--fg1)',
865
+ fontWeight: 600,
866
+ overflow: 'hidden',
867
+ textOverflow: 'ellipsis',
868
+ whiteSpace: 'nowrap'
869
+ }
870
+ }, screenshot.name), /*#__PURE__*/React.createElement("div", {
871
+ style: {
872
+ color: 'var(--fg3)',
873
+ marginTop: 2
874
+ }
875
+ }, [screenshot.size, screenshot.dimensions].filter(Boolean).join(' · '))));
876
+ }
877
+ function StepNode({
878
+ step,
879
+ depth = 0,
880
+ defaultOpen
881
+ }) {
882
+ const hasContent = step.logs?.length || step.body || step.children?.length || step.request || step.response || step.assertion || step.screenshot || step.payload;
883
+ const isProblem = step.status === 'failed' || step.status === 'broken';
884
+ const [open, setOpen] = React.useState(defaultOpen ?? isProblem);
885
+ const [hover, setHover] = React.useState(false);
886
+ const stepIcon = step.status === 'passed' ? '✓' : step.status === 'failed' ? '✕' : step.status === 'broken' ? '!' : '⊘';
887
+
888
+ // What's behind this row? Build a hint string so users know it's clickable.
889
+ const hints = [];
890
+ if (step.children?.length) hints.push(`${step.children.length} sub-step${step.children.length === 1 ? '' : 's'}`);
891
+ if (step.request || step.response) hints.push('request');
892
+ if (step.assertion) hints.push('assertion');
893
+ if (step.screenshot) hints.push('screenshot');
894
+ if (step.logs?.length) hints.push(`${step.logs.length} log${step.logs.length === 1 ? '' : 's'}`);
895
+ if (step.body) hints.push('details');
896
+ if (step.payload && hints.length === 0) hints.push('params');
897
+ return /*#__PURE__*/React.createElement("div", {
898
+ className: `step ${step.status}`
899
+ }, /*#__PURE__*/React.createElement("div", {
900
+ className: "head",
901
+ onClick: () => hasContent && setOpen(o => !o),
902
+ onMouseEnter: () => setHover(true),
903
+ onMouseLeave: () => setHover(false),
904
+ style: {
905
+ cursor: hasContent ? 'pointer' : 'default',
906
+ userSelect: 'none',
907
+ background: hasContent && hover ? 'var(--bg-hover)' : 'transparent',
908
+ borderRadius: 4,
909
+ padding: '4px 6px',
910
+ margin: '-4px -6px',
911
+ transition: 'background 120ms'
912
+ }
913
+ }, /*#__PURE__*/React.createElement("span", {
914
+ style: {
915
+ width: 12,
916
+ height: 12,
917
+ display: 'inline-flex',
918
+ alignItems: 'center',
919
+ justifyContent: 'center',
920
+ color: hasContent ? open ? 'var(--fg1)' : 'var(--fg2)' : 'transparent',
921
+ transition: 'transform 140ms, color 120ms',
922
+ transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
923
+ fontSize: 10,
924
+ lineHeight: 1,
925
+ fontFamily: 'var(--font-mono)'
926
+ }
927
+ }, hasContent ? '▶' : ''), /*#__PURE__*/React.createElement("span", {
928
+ className: `s-icon ${step.status}`,
929
+ style: {
930
+ width: 14,
931
+ height: 14,
932
+ fontSize: 9
933
+ }
934
+ }, stepIcon), step.type && step.type !== 'action' && /*#__PURE__*/React.createElement("span", {
935
+ style: {
936
+ color: 'var(--fg3)',
937
+ display: 'inline-flex',
938
+ alignItems: 'center'
939
+ }
940
+ }, /*#__PURE__*/React.createElement(StepIcon, {
941
+ type: step.type
942
+ })), /*#__PURE__*/React.createElement("span", {
943
+ className: "name",
944
+ style: {
945
+ flex: 1,
946
+ minWidth: 0,
947
+ overflow: 'hidden',
948
+ textOverflow: 'ellipsis',
949
+ whiteSpace: 'nowrap'
950
+ }
951
+ }, step.name), hasContent && hints.length > 0 && /*#__PURE__*/React.createElement("span", {
952
+ style: {
953
+ fontFamily: 'var(--font-mono)',
954
+ fontSize: 10,
955
+ color: 'var(--fg3)',
956
+ padding: '1px 6px',
957
+ background: 'var(--bg-2)',
958
+ borderRadius: 3,
959
+ border: '1px solid var(--line)'
960
+ }
961
+ }, hints.join(' · ')), /*#__PURE__*/React.createElement("span", {
962
+ className: "dur"
963
+ }, step.duration)), open && /*#__PURE__*/React.createElement("div", {
964
+ style: {
965
+ marginTop: 6,
966
+ marginLeft: 14,
967
+ display: 'flex',
968
+ flexDirection: 'column',
969
+ gap: 8
970
+ }
971
+ }, step.body && /*#__PURE__*/React.createElement("pre", {
972
+ style: {
973
+ margin: 0,
974
+ padding: '10px 12px',
975
+ background: isProblem ? 'var(--status-failed-bg)' : 'var(--bg-2)',
976
+ border: `1px solid ${isProblem ? 'var(--status-failed-border)' : 'var(--line)'}`,
977
+ borderRadius: 6,
978
+ fontFamily: 'var(--font-mono)',
979
+ fontSize: 11.5,
980
+ color: isProblem ? 'var(--status-failed-fg)' : 'var(--fg2)',
981
+ whiteSpace: 'pre-wrap',
982
+ wordBreak: 'break-word',
983
+ lineHeight: 1.55
984
+ }
985
+ }, step.body), step.request && step.response && /*#__PURE__*/React.createElement(HttpBlock, {
986
+ request: step.request,
987
+ response: step.response
988
+ }), step.assertion && /*#__PURE__*/React.createElement(AssertionBlock, {
989
+ assertion: step.assertion
990
+ }), step.screenshot && /*#__PURE__*/React.createElement(ScreenshotBlock, {
991
+ screenshot: step.screenshot
992
+ }), step.payload && /*#__PURE__*/React.createElement("pre", {
993
+ style: {
994
+ margin: 0,
995
+ padding: '8px 12px',
996
+ background: 'var(--bg-2)',
997
+ border: '1px solid var(--line)',
998
+ borderRadius: 6,
999
+ fontFamily: 'var(--font-mono)',
1000
+ fontSize: 11.5,
1001
+ color: 'var(--fg2)',
1002
+ overflow: 'auto'
1003
+ }
1004
+ }, step.payload), step.logs?.length > 0 && /*#__PURE__*/React.createElement("div", {
1005
+ style: {
1006
+ background: 'var(--bg-sunken)',
1007
+ border: '1px solid var(--line)',
1008
+ borderRadius: 6,
1009
+ padding: '6px 4px'
1010
+ }
1011
+ }, /*#__PURE__*/React.createElement("div", {
1012
+ style: {
1013
+ padding: '4px 10px',
1014
+ fontFamily: 'var(--font-mono)',
1015
+ fontSize: 10,
1016
+ color: 'var(--fg3)',
1017
+ letterSpacing: 1,
1018
+ textTransform: 'uppercase'
1019
+ }
1020
+ }, "Logs \xB7 ", step.logs.length), /*#__PURE__*/React.createElement("div", {
1021
+ style: {
1022
+ background: 'var(--bg-elev)',
1023
+ border: '1px solid var(--line)',
1024
+ borderRadius: 4,
1025
+ margin: '4px',
1026
+ padding: '4px 0'
1027
+ }
1028
+ }, step.logs.map((l, i) => /*#__PURE__*/React.createElement(LogLine, {
1029
+ key: i,
1030
+ log: l
1031
+ })))), step.children?.length > 0 && /*#__PURE__*/React.createElement("div", {
1032
+ className: "children"
1033
+ }, /*#__PURE__*/React.createElement(StepTreeV2, {
1034
+ steps: step.children,
1035
+ depth: depth + 1
1036
+ }))));
1037
+ }
1038
+ function StepTreeV2({
1039
+ steps,
1040
+ depth = 0
1041
+ }) {
1042
+ return /*#__PURE__*/React.createElement("div", null, steps.map((s, i) => /*#__PURE__*/React.createElement(StepNode, {
1043
+ key: i,
1044
+ step: s,
1045
+ depth: depth
1046
+ })));
1047
+ }
1048
+ Object.assign(window, {
1049
+ TestHeader,
1050
+ StepTreeV2,
1051
+ StatusPill,
1052
+ Tag,
1053
+ SeverityBadge,
1054
+ MetaField,
1055
+ CopyPath,
1056
+ CopyPermalink,
1057
+ LinkChip
1058
+ });