@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.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/assets/app.js +1396 -0
- package/assets/app.jsx +860 -0
- package/assets/charts.js +1156 -0
- package/assets/charts.jsx +593 -0
- package/assets/colors_and_type.css +219 -0
- package/assets/components.js +894 -0
- package/assets/components.jsx +520 -0
- package/assets/data-bridge.js +49 -0
- package/assets/data-bridge.jsx +55 -0
- package/assets/data-loader.js +543 -0
- package/assets/kaizen-mark.svg +5 -0
- package/assets/kensho-wordmark.svg +18 -0
- package/assets/pages.js +1472 -0
- package/assets/pages.jsx +822 -0
- package/assets/test-detail.js +1058 -0
- package/assets/test-detail.jsx +502 -0
- package/assets/tokens.css +357 -0
- package/assets/tree-detail.js +1705 -0
- package/assets/tree-detail.jsx +947 -0
- package/dist/component.d.ts +115 -0
- package/dist/component.js +698 -0
- package/index.html +35 -0
- package/package.json +65 -0
- package/scripts/build.js +117 -0
- package/src/component.d.ts +115 -0
- package/src/component.jsx +340 -0
- package/src/data.js +538 -0
|
@@ -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
|
+
});
|