@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,543 @@
1
+ /* Auto-generated. Wraps src/data.js for the static-report bootstrap. */
2
+ (function () {
3
+ // =============================================================
4
+ // loadKenshoData — pure data loader + normalizer.
5
+ //
6
+ // Fetches `${dataUrl}/index.json` and (lazily) `${dataUrl}/cases/<id>.json`,
7
+ // then maps the Kensho v1 schema into the shape the viewer's components
8
+ // expect (RUN, ENV, SUITES, RICH_TESTS, …).
9
+ //
10
+ // Used by:
11
+ // * `assets/data-bridge.jsx` — writes the normalized result onto window
12
+ // globals so the static-report bootstrap path keeps working unchanged.
13
+ // * `src/component.jsx` — feeds the same result into a React Context
14
+ // so the embeddable `<KenshoViewer>` doesn't depend on globals.
15
+ //
16
+ // Important: this module must stay framework-free. Don't import React.
17
+ // =============================================================
18
+
19
+ const STATUS = { pass: 'passed', fail: 'failed', broken: 'broken', skip: 'skipped' };
20
+
21
+ function fmtDuration(ms) {
22
+ if (ms == null) return '—';
23
+ if (ms === 0) return '—';
24
+ if (ms < 1000) return ms + 'ms';
25
+ const totalSec = ms / 1000;
26
+ if (totalSec < 60) {
27
+ const whole = Math.floor(totalSec);
28
+ const remMs = ms - whole * 1000;
29
+ return remMs ? whole + 's ' + remMs + 'ms' : whole + 's';
30
+ }
31
+ const m = Math.floor(totalSec / 60);
32
+ const s = Math.floor(totalSec % 60);
33
+ return m + 'm ' + s + 's';
34
+ }
35
+
36
+ function relTime(iso) {
37
+ if (!iso) return '';
38
+ const ms = Date.now() - new Date(iso).getTime();
39
+ if (ms < 0) return new Date(iso).toLocaleString();
40
+ if (ms < 60_000) return Math.max(1, Math.floor(ms / 1000)) + 's ago';
41
+ if (ms < 3_600_000) return Math.floor(ms / 60_000) + 'm ago';
42
+ if (ms < 86_400_000) return Math.floor(ms / 3_600_000) + 'h ago';
43
+ return Math.floor(ms / 86_400_000) + 'd ago';
44
+ }
45
+
46
+ function inferStepType(s) {
47
+ if (s.phase === 'setup') return 'setup';
48
+ if (s.phase === 'teardown') return 'teardown';
49
+ if (s.children?.length) return 'group';
50
+ if (s.assertion) return 'assertion';
51
+ if (s.network?.length) return 'http';
52
+ const t = (s.title || '').toLowerCase();
53
+ if (t.includes('navigate') || t.includes('goto') || t.startsWith('open ')) return 'navigation';
54
+ if (t.includes('verify') || t.includes('expect') || t.includes('assert')) return 'assertion';
55
+ if (t.startsWith('post ') || t.startsWith('get ') || t.includes('http')) return 'http';
56
+ if (t.includes('select ') || t.includes('insert ') || t.includes('query')) return 'db';
57
+ if (t.startsWith('screenshot')) return 'screenshot';
58
+ return 'action';
59
+ }
60
+
61
+ function mapLog(l) {
62
+ return {
63
+ ts: typeof l.t === 'number'
64
+ ? new Date(l.t).toISOString().slice(11, 23)
65
+ : (typeof l.t === 'string' ? l.t.slice(11, 23) : ''),
66
+ lvl: l.level === 'error' ? 'err' : (l.level || 'info'),
67
+ msg: l.msg || '',
68
+ };
69
+ }
70
+
71
+ // `attachmentBase` is "data/" for the static report (matches existing legacy
72
+ // behavior) and `${dataUrl}/` for the embedded component.
73
+ function makeMapStep(attachmentBase) {
74
+ function mapStep(s) {
75
+ const out = {
76
+ name: s.title || '(unnamed step)',
77
+ status: STATUS[s.status] || s.status,
78
+ duration: fmtDuration(s.duration),
79
+ type: inferStepType(s),
80
+ };
81
+ if (s.logs?.length) out.logs = s.logs.map(mapLog);
82
+ if (s.children?.length) out.children = s.children.map(mapStep);
83
+ if (s.parameters?.length) {
84
+ out.payload = s.parameters.map(p => `${p.name} = ${p.value}`).join('\n');
85
+ }
86
+ if (s.assertion) {
87
+ out.assertion = {
88
+ passed: s.status === 'pass',
89
+ matcher: s.action || 'expect',
90
+ expected: typeof s.assertion.expected === 'string'
91
+ ? s.assertion.expected
92
+ : JSON.stringify(s.assertion.expected),
93
+ actual: typeof s.assertion.received === 'string'
94
+ ? s.assertion.received
95
+ : JSON.stringify(s.assertion.received),
96
+ };
97
+ if (s.assertion.diff) out.body = s.assertion.diff;
98
+ }
99
+ if (s.network?.length) {
100
+ const n = s.network[0];
101
+ out.request = {
102
+ method: n.method, url: n.url, duration: fmtDuration(n.durationMs),
103
+ ...(n.requestBody ? { body: n.requestBody } : {}),
104
+ };
105
+ out.response = {
106
+ status: n.status, statusText: n.status >= 400 ? 'ERR' : 'OK',
107
+ size: n.sizeBytes ? n.sizeBytes + 'B' : '',
108
+ ...(n.responseBody ? { body: n.responseBody } : {}),
109
+ };
110
+ }
111
+ if (s.attachments?.length) {
112
+ const visual = s.attachments.find(a =>
113
+ a.kind === 'screenshot' || a.kind === 'image' || a.kind === 'video' ||
114
+ (a.mimeType || '').startsWith('image/') || (a.mimeType || '').startsWith('video/')
115
+ );
116
+ if (visual) {
117
+ const url = attachmentBase + (visual.relativePath || '').replace(/^\/+/, '');
118
+ const name = (visual.relativePath || '').split(/[\\/]/).pop() || visual.id || 'attachment';
119
+ const sizeKB = visual.sizeBytes ? (visual.sizeBytes / 1024).toFixed(1) + ' KB' : '';
120
+ out.screenshot = { url, name, size: sizeKB, dimensions: '' };
121
+ }
122
+ out._attachments = s.attachments;
123
+ }
124
+ return out;
125
+ }
126
+ return mapStep;
127
+ }
128
+
129
+ function buildRichTest(c, idx, runStartMs) {
130
+ const startMs = c.startedAt ? Math.max(0, new Date(c.startedAt).getTime() - runStartMs) : 0;
131
+ return {
132
+ id: c.id,
133
+ order: idx + 1,
134
+ name: c.name,
135
+ fullName: c.fullName,
136
+ status: STATUS[c.status] || c.status,
137
+ dur: fmtDuration(c.duration),
138
+ durMs: c.duration || 0,
139
+ start: startMs,
140
+ suite: (c.suite || []).join(' › '),
141
+ suiteChain: c.suite || [],
142
+ severity: c.severity || 'normal',
143
+ retries: c.retries || 0,
144
+ flakeRate: 0,
145
+ avgDurMs: c.duration || 0,
146
+ platform: [c.browser, c.platform].filter(Boolean).join(' · '),
147
+ description: '',
148
+ parameters: [],
149
+ tags: c.tags || [],
150
+ owner: c.owner || '',
151
+ file: c.filePath ? c.filePath + (c.line ? ':' + c.line : '') : '',
152
+ epic: c.behavior?.epic,
153
+ feature: c.behavior?.feature,
154
+ story: c.behavior?.scenario,
155
+ lastRun: c.startedAt ? relTime(c.startedAt) : '',
156
+ bdd: null,
157
+ labels: c.labels || {},
158
+ links: c.links || [],
159
+ error: (c.hasErrors || c.errorPreview) ? {
160
+ kind: c.errorType || 'Error',
161
+ message: c.errorPreview || '',
162
+ stack: '',
163
+ } : null,
164
+ steps: [],
165
+ _summary: c,
166
+ _full: null,
167
+ _stepsLoaded: false,
168
+ };
169
+ }
170
+
171
+ function deriveSuiteTree(cases) {
172
+ const root = { _children: new Map() };
173
+ for (const c of cases) {
174
+ const chain = (c.suite && c.suite.length) ? c.suite : ['Default'];
175
+ let node = root;
176
+ for (let i = 0; i < chain.length; i++) {
177
+ const part = chain[i];
178
+ if (!node._children.has(part)) node._children.set(part, { _name: part, _children: new Map() });
179
+ node = node._children.get(part);
180
+ }
181
+ if (!node._tests) node._tests = [];
182
+ node._tests.push(c.id);
183
+ }
184
+ let auto = 0;
185
+ function toTree(node, parentId) {
186
+ const out = [];
187
+ for (const [name, child] of node._children) {
188
+ const id = parentId + '/' + name;
189
+ const childNodes = toTree(child, id);
190
+ const leaves = (child._tests || []).map(tid => ({ id: id + '/leaf-' + (++auto), testId: tid }));
191
+ const all = [...childNodes, ...leaves];
192
+ if (all.length) out.push({ id, name, children: all });
193
+ }
194
+ return out;
195
+ }
196
+ return toTree(root, 'suite');
197
+ }
198
+
199
+ function _kvBehaviorPickFromLabels(c) {
200
+ // Some adapters populate `case.behavior.{epic,feature,scenario}`; others
201
+ // emit them as labels (`epic`/`feature`/`story`). Read both.
202
+ const labels = c.labels || {};
203
+ return {
204
+ epic: (c.behavior && c.behavior.epic) || labels.epic || labels.Epic,
205
+ feature: (c.behavior && c.behavior.feature) || labels.feature || labels.Feature,
206
+ story: (c.behavior && c.behavior.scenario) || labels.story || labels.Story,
207
+ };
208
+ }
209
+ function deriveBehaviorTree(cases) {
210
+ const tree = new Map();
211
+ for (const c of cases) {
212
+ const { epic, feature, story } = _kvBehaviorPickFromLabels(c);
213
+ if (!epic && !feature) continue;
214
+ const e = epic || '(unspecified epic)';
215
+ const f = feature || '(unspecified feature)';
216
+ if (!tree.has(e)) tree.set(e, new Map());
217
+ const features = tree.get(e);
218
+ if (!features.has(f)) features.set(f, []);
219
+ features.get(f).push({ caseId: c.id, story: story || c.name });
220
+ }
221
+ let auto = 0;
222
+ const out = [];
223
+ for (const [epic, features] of tree) {
224
+ const eId = 'epic/' + epic;
225
+ const fNodes = [];
226
+ for (const [feature, stories] of features) {
227
+ const fId = eId + '/' + feature;
228
+ const sNodes = stories.map(s => ({ id: fId + '/leaf-' + (++auto), testId: s.caseId }));
229
+ fNodes.push({ id: fId, name: 'Feature · ' + feature, children: sNodes });
230
+ }
231
+ out.push({ id: eId, name: 'Epic · ' + epic, children: fNodes });
232
+ }
233
+ return out;
234
+ }
235
+
236
+ function deriveCategories(cases) {
237
+ const map = new Map();
238
+ for (const c of cases) {
239
+ const isFail = c.status === 'fail' || c.status === 'broken';
240
+ // Always bucket failures, even when the adapter didn't populate
241
+ // errorType/errorPreview — the user expects every failed case to be
242
+ // visible somewhere on Categories.
243
+ if (!isFail && !c.errorType && !c.errorPreview) continue;
244
+ const kind = c.errorType || 'Error';
245
+ const family = c.status === 'broken' ? 'broken' : c.status === 'skip' ? 'skipped' : 'failed';
246
+ const color = family === 'failed' ? 'var(--status-failed)'
247
+ : family === 'broken' ? 'var(--status-broken)'
248
+ : 'var(--status-skipped)';
249
+ if (!map.has(kind)) map.set(kind, { kind, family, color, count: 0, tests: [], description: describeKind(kind) });
250
+ const e = map.get(kind);
251
+ e.count += 1;
252
+ e.tests.push(c.id);
253
+ }
254
+ return [...map.values()].sort((a, b) => b.count - a.count);
255
+ }
256
+
257
+ function describeKind(kind) {
258
+ const map = {
259
+ AssertionError: 'Expected vs. actual mismatch in a test assertion. Most often a real product defect.',
260
+ TimeoutError: 'A wait condition exceeded its budget. Could be a slow service or a missing element.',
261
+ NetworkError: 'Non-2xx response from a backend dependency during the test.',
262
+ Error: 'Generic error — inspect the failing test for details.',
263
+ };
264
+ return map[kind] || ('Failures classified as ' + kind + '.');
265
+ }
266
+
267
+ function deriveTimelineRows(cases, runStartMs) {
268
+ return cases
269
+ .filter(c => c.duration && c.duration > 0)
270
+ .map(c => ({
271
+ id: c.id,
272
+ suite: (c.suite && c.suite[0]) || 'Default',
273
+ name: c.name,
274
+ start: c.startedAt ? Math.max(0, new Date(c.startedAt).getTime() - runStartMs) : 0,
275
+ durMs: c.duration,
276
+ dur: fmtDuration(c.duration),
277
+ status: STATUS[c.status] || c.status,
278
+ platform: [c.browser, c.platform].filter(Boolean).join(' · '),
279
+ severity: c.severity || 'normal',
280
+ retries: c.retries || 0,
281
+ file: c.filePath ? c.filePath + (c.line ? ':' + c.line : '') : '',
282
+ }));
283
+ }
284
+
285
+ function deriveTrendRuns(history, current) {
286
+ const runs = [];
287
+ if (history?.length) {
288
+ for (const h of history) {
289
+ const t = h.totals || {};
290
+ runs.push({
291
+ short: (h.id || '').slice(-4) || h.id,
292
+ passed: t.pass || 0,
293
+ failed: t.fail || 0,
294
+ broken: t.broken || 0,
295
+ skipped: t.skip || 0,
296
+ });
297
+ }
298
+ }
299
+ if (current) runs.push(current);
300
+ if (runs.length === 0 && current) runs.push(current);
301
+ return runs;
302
+ }
303
+
304
+ function deriveDurationHistogram(cases) {
305
+ const buckets = [
306
+ { label: '<100ms', max: 100 },
307
+ { label: '<500ms', max: 500 },
308
+ { label: '<1s', max: 1000 },
309
+ { label: '<2s', max: 2000 },
310
+ { label: '<5s', max: 5000 },
311
+ { label: '<10s', max: 10000 },
312
+ { label: '≥10s', max: Infinity },
313
+ ];
314
+ return buckets.map(b => ({
315
+ label: b.label,
316
+ n: cases.filter(c => c.duration && c.duration > 0 && c.duration < b.max).length,
317
+ })).map((b, i, arr) => ({
318
+ label: b.label,
319
+ n: i === 0 ? b.n : (b.n - (arr[i - 1]?.n || 0)),
320
+ }));
321
+ }
322
+
323
+ function deriveHistoryRuns(history, current) {
324
+ const out = [];
325
+ if (current) {
326
+ out.push({
327
+ id: current.id,
328
+ when: '2m ago',
329
+ branch: current.branch,
330
+ actor: current.actor,
331
+ passed: current.counts.passed,
332
+ failed: current.counts.failed,
333
+ broken: current.counts.broken,
334
+ skipped: current.counts.skipped,
335
+ dur: current.duration,
336
+ status: current.counts.failed + current.counts.broken > 0 ? 'failed' : 'passed',
337
+ });
338
+ }
339
+ if (history?.length) {
340
+ for (const h of history) {
341
+ const t = h.totals || {};
342
+ const failed = (t.fail || 0) + (t.broken || 0);
343
+ out.push({
344
+ id: '#' + (h.id || '?'),
345
+ when: h.startedAt ? relTime(h.startedAt) : '',
346
+ branch: h.branch || 'main',
347
+ actor: 'kensho',
348
+ passed: t.pass || 0,
349
+ failed: t.fail || 0,
350
+ broken: t.broken || 0,
351
+ skipped: t.skip || 0,
352
+ dur: fmtDuration(h.durationMs),
353
+ status: failed > 0 ? 'failed' : 'passed',
354
+ });
355
+ }
356
+ }
357
+ return out;
358
+ }
359
+
360
+ const ENV_LABEL = {
361
+ ci: 'CI', branch: 'Branch', commit: 'Commit', commitMsg: 'Commit msg',
362
+ author: 'Author', runUrl: 'Run URL', repoUrl: 'Repo URL', os: 'OS', osVersion: 'OS version',
363
+ arch: 'Arch', nodeVersion: 'Node', pythonVersion: 'Python',
364
+ browsers: 'Browsers', workers: 'Workers', stage: 'Stage', baseUrl: 'Base URL',
365
+ appVersion: 'App version', buildNumber: 'Build', release: 'Release',
366
+ device: 'Device', viewport: 'Viewport', region: 'Region', locale: 'Locale',
367
+ timezone: 'Timezone', tunnel: 'Tunnel', trigger: 'Trigger', feature: 'Feature',
368
+ };
369
+
370
+ /**
371
+ * Load and normalize a Kensho run.
372
+ *
373
+ * @param {string} dataUrl
374
+ * URL (relative or absolute) to a Kensho `data/` directory. The loader
375
+ * appends `/index.json` and `/cases/<id>.json` to it.
376
+ * @param {object} [opts]
377
+ * @param {(caseId: string) => string} [opts.caseUrl]
378
+ * Override the per-case URL builder. Default:
379
+ * `${dataUrl}/cases/${caseId}.json`.
380
+ * @param {typeof fetch} [opts.fetch]
381
+ * Override the global `fetch` implementation (useful for tests / SSR).
382
+ * @returns {Promise<KenshoState>}
383
+ */
384
+ async function loadKenshoData(dataUrl, opts = {}) {
385
+ if (!dataUrl) throw new Error('loadKenshoData: dataUrl is required');
386
+ const baseUrl = String(dataUrl).replace(/\/+$/, '');
387
+ const fetchImpl = opts.fetch || (typeof fetch !== 'undefined' ? fetch : null);
388
+ if (!fetchImpl) throw new Error('loadKenshoData: no `fetch` available — pass opts.fetch.');
389
+ const caseUrl = opts.caseUrl || ((id) => `${baseUrl}/cases/${id}.json`);
390
+ // Attachments declared on `step.attachments[*].relativePath` are joined to
391
+ // `${baseUrl}/`. For the static report, baseUrl === "data" so the URL ends
392
+ // up "data/attachments/…", matching the legacy behavior. For embedded, it
393
+ // ends up something like "/v1/runs/<id>/data/attachments/…".
394
+ const attachmentBase = baseUrl + '/';
395
+ const mapStep = makeMapStep(attachmentBase);
396
+
397
+ const idx = await fetchImpl(`${baseUrl}/index.json`, { cache: 'no-cache' }).then(r => r.json());
398
+ const runStartMs = idx.startedAt ? new Date(idx.startedAt).getTime() : Date.now();
399
+ const totals = idx.totals || {};
400
+ const counts = {
401
+ passed: totals.pass || 0,
402
+ failed: totals.fail || 0,
403
+ broken: totals.broken || 0,
404
+ skipped: totals.skip || 0,
405
+ };
406
+
407
+ const run = {
408
+ id: '#' + (idx.runId || 'unknown'),
409
+ branch: idx.env?.branch || (idx.env?.ci === 'local' ? 'local' : 'main'),
410
+ commit: (idx.env?.commit || '').slice(0, 7),
411
+ commitFull: idx.env?.commit || '',
412
+ actor: idx.env?.author || idx.project?.slug || 'kensho',
413
+ startedAt: idx.startedAt ? new Date(idx.startedAt).toLocaleString() : '',
414
+ duration: fmtDuration(idx.durationMs),
415
+ counts,
416
+ repoUrl: idx.env?.repoUrl || '',
417
+ runUrl: idx.env?.runUrl || '',
418
+ };
419
+
420
+ const ENV_FIELDS = Object.keys(ENV_LABEL);
421
+ const env = ENV_FIELDS
422
+ .filter(k => idx.env?.[k] != null && idx.env[k] !== '' && (!Array.isArray(idx.env[k]) || idx.env[k].length > 0))
423
+ .map(k => [
424
+ ENV_LABEL[k] || k,
425
+ Array.isArray(idx.env[k]) ? idx.env[k].join(', ') : String(idx.env[k]),
426
+ ]);
427
+ if (idx.env?.vars && typeof idx.env.vars === 'object') {
428
+ for (const [k, v] of Object.entries(idx.env.vars)) {
429
+ if (v != null && v !== '') env.push([k, String(v)]);
430
+ }
431
+ }
432
+
433
+ const cases = idx.cases || [];
434
+ const richTests = {};
435
+ cases.forEach((c, i) => { richTests[c.id] = buildRichTest(c, i, runStartMs); });
436
+
437
+ const bySuite = new Map();
438
+ for (const c of cases) {
439
+ const key = (c.suite && c.suite[0]) || 'Default';
440
+ const arr = bySuite.get(key) || [];
441
+ arr.push(c);
442
+ bySuite.set(key, arr);
443
+ }
444
+ const suites = [...bySuite.entries()].map(([name, cs]) => {
445
+ const segs = ['pass', 'fail', 'broken', 'skip']
446
+ .map(k => ({ k: STATUS[k], n: cs.filter(c => c.status === k).length }))
447
+ .filter(s => s.n > 0);
448
+ return { name, segs, total: cs.length };
449
+ });
450
+
451
+ const tests = cases.map(c => ({
452
+ ns: '',
453
+ name: c.name,
454
+ status: STATUS[c.status] || c.status,
455
+ duration: fmtDuration(c.duration),
456
+ last: c.startedAt ? relTime(c.startedAt) : '',
457
+ retries: c.retries,
458
+ richId: c.id,
459
+ }));
460
+
461
+ const suiteTree = deriveSuiteTree(cases);
462
+ const behaviorTree = deriveBehaviorTree(cases);
463
+ const categories = deriveCategories(cases);
464
+ const timelineTests = deriveTimelineRows(cases, runStartMs);
465
+ const trendRuns = deriveTrendRuns(idx.history, {
466
+ short: (idx.runId || '').slice(-4) || idx.runId,
467
+ passed: counts.passed, failed: counts.failed,
468
+ broken: counts.broken, skipped: counts.skipped,
469
+ });
470
+ const histogram = deriveDurationHistogram(cases);
471
+ const historyRuns = deriveHistoryRuns(idx.history, run);
472
+
473
+ const reportType = idx.reportType
474
+ || (idx.framework?.name === 'playwright' ? 'e2e'
475
+ : (idx.framework?.name === 'jest' || idx.framework?.name === 'vitest' || idx.framework?.name === 'pytest' ? 'unit' : 'mixed'));
476
+
477
+ // Per-case JSON cache local to this state instance — keeps two embedded
478
+ // viewers from clobbering each other's case caches.
479
+ const caseCache = {};
480
+ async function loadCase(id) {
481
+ if (!id) return null;
482
+ if (caseCache[id]) return caseCache[id];
483
+ try {
484
+ const r = await fetchImpl(caseUrl(id), { cache: 'no-cache' });
485
+ caseCache[id] = await r.json();
486
+ return caseCache[id];
487
+ } catch (e) {
488
+ console.error('[kensho] failed to load case', id, e);
489
+ return null;
490
+ }
491
+ }
492
+
493
+ async function ensureCaseLoaded(richTest) {
494
+ if (!richTest || richTest._stepsLoaded) return richTest;
495
+ const full = await loadCase(richTest.id);
496
+ if (!full) { richTest._stepsLoaded = true; return richTest; }
497
+ richTest._full = full;
498
+ richTest.description = full.description || richTest.description;
499
+ richTest.parameters = (full.parameters || []).map(p => [p.name, p.value]);
500
+ if (full.behavior?.gherkin?.length) {
501
+ const text = full.behavior.gherkin.join(' ');
502
+ const m = text.match(/given\s+(.+?)\s+when\s+(.+?)\s+then\s+(.+)/i);
503
+ if (m) richTest.bdd = { given: m[1].trim(), when: m[2].trim(), then: m[3].trim() };
504
+ }
505
+ if (full.errors?.length) {
506
+ const e = full.errors[0];
507
+ richTest.error = { kind: e.type || 'Error', message: e.message || '', stack: e.stack || '' };
508
+ }
509
+ richTest.steps = (full.steps || []).map(mapStep);
510
+ richTest.attachments = full.attachments || [];
511
+ richTest.logs = (full.logs || []).map(mapLog);
512
+ richTest.history = full.history || [];
513
+ richTest._stepsLoaded = true;
514
+ return richTest;
515
+ }
516
+
517
+ return {
518
+ kenshoIndex: idx,
519
+ reportType,
520
+ run,
521
+ env,
522
+ suites,
523
+ tests,
524
+ richTests,
525
+ suiteTree,
526
+ behaviorTree,
527
+ categories,
528
+ timelineTests,
529
+ trendRuns,
530
+ histogram,
531
+ historyRuns,
532
+ ensureCaseLoaded,
533
+ loadCase,
534
+ fmtDuration,
535
+ relTime,
536
+ // Helpers for the data-bridge adapter (mostly unused outside it).
537
+ _baseUrl: baseUrl,
538
+ _attachmentBase: attachmentBase,
539
+ };
540
+ }
541
+
542
+ window.__KENSHO_LOAD_DATA = loadKenshoData;
543
+ })();
@@ -0,0 +1,5 @@
1
+ <svg width="300" height="300" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="none">
2
+ <circle cx="256" cy="256" r="220" stroke="#0072FF" stroke-width="20"></circle>
3
+ <path d="M180 120 L180 392 M180 256 L340 120 M180 256 L340 392" stroke="#00FF87" stroke-width="24" stroke-linecap="round"></path>
4
+ <path d="M200 260 L240 300 L320 200" stroke="#00FF87" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"></path>
5
+ </svg>
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 64" fill="none">
2
+ <defs>
3
+ <linearGradient id="wg" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#1E6BFF"></stop>
5
+ <stop offset="1" stop-color="#0B3FB5"></stop>
6
+ </linearGradient>
7
+ <linearGradient id="wg2" x1="32" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
8
+ <stop offset="0" stop-color="#3DDC84"></stop>
9
+ <stop offset="1" stop-color="#1FA864"></stop>
10
+ </linearGradient>
11
+ </defs>
12
+ <path d="M14 12h8v40h-8z" fill="url(#wg)"></path>
13
+ <path d="M22 32 L34 14 h10 L31 32 Z" fill="url(#wg)"></path>
14
+ <path d="M22 32 L34 50 h10 L31 32 Z" fill="url(#wg2)"></path>
15
+ <path d="M44 24 l4 4 l8-8" stroke="#1FA864" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"></path>
16
+ <text x="74" y="42" font-family="Manrope, Inter, system-ui, sans-serif" font-size="28" font-weight="700" fill="#0B1220" letter-spacing="-0.5">kensho</text>
17
+ <text x="74" y="56" font-family="JetBrains Mono, ui-monospace, monospace" font-size="9" font-weight="500" fill="#5B6478" letter-spacing="2">TEST · REPORTS</text>
18
+ </svg>