@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,698 @@
1
+ // src/component.jsx
2
+ import React, { useEffect, useRef, useState } from "react";
3
+
4
+ // src/data.js
5
+ var STATUS = { pass: "passed", fail: "failed", broken: "broken", skip: "skipped" };
6
+ function fmtDuration(ms) {
7
+ if (ms == null) return "\u2014";
8
+ if (ms === 0) return "\u2014";
9
+ if (ms < 1e3) return ms + "ms";
10
+ const totalSec = ms / 1e3;
11
+ if (totalSec < 60) {
12
+ const whole = Math.floor(totalSec);
13
+ const remMs = ms - whole * 1e3;
14
+ return remMs ? whole + "s " + remMs + "ms" : whole + "s";
15
+ }
16
+ const m = Math.floor(totalSec / 60);
17
+ const s = Math.floor(totalSec % 60);
18
+ return m + "m " + s + "s";
19
+ }
20
+ function relTime(iso) {
21
+ if (!iso) return "";
22
+ const ms = Date.now() - new Date(iso).getTime();
23
+ if (ms < 0) return new Date(iso).toLocaleString();
24
+ if (ms < 6e4) return Math.max(1, Math.floor(ms / 1e3)) + "s ago";
25
+ if (ms < 36e5) return Math.floor(ms / 6e4) + "m ago";
26
+ if (ms < 864e5) return Math.floor(ms / 36e5) + "h ago";
27
+ return Math.floor(ms / 864e5) + "d ago";
28
+ }
29
+ function inferStepType(s) {
30
+ if (s.phase === "setup") return "setup";
31
+ if (s.phase === "teardown") return "teardown";
32
+ if (s.children?.length) return "group";
33
+ if (s.assertion) return "assertion";
34
+ if (s.network?.length) return "http";
35
+ const t = (s.title || "").toLowerCase();
36
+ if (t.includes("navigate") || t.includes("goto") || t.startsWith("open ")) return "navigation";
37
+ if (t.includes("verify") || t.includes("expect") || t.includes("assert")) return "assertion";
38
+ if (t.startsWith("post ") || t.startsWith("get ") || t.includes("http")) return "http";
39
+ if (t.includes("select ") || t.includes("insert ") || t.includes("query")) return "db";
40
+ if (t.startsWith("screenshot")) return "screenshot";
41
+ return "action";
42
+ }
43
+ function mapLog(l) {
44
+ return {
45
+ ts: typeof l.t === "number" ? new Date(l.t).toISOString().slice(11, 23) : typeof l.t === "string" ? l.t.slice(11, 23) : "",
46
+ lvl: l.level === "error" ? "err" : l.level || "info",
47
+ msg: l.msg || ""
48
+ };
49
+ }
50
+ function makeMapStep(attachmentBase) {
51
+ function mapStep(s) {
52
+ const out = {
53
+ name: s.title || "(unnamed step)",
54
+ status: STATUS[s.status] || s.status,
55
+ duration: fmtDuration(s.duration),
56
+ type: inferStepType(s)
57
+ };
58
+ if (s.logs?.length) out.logs = s.logs.map(mapLog);
59
+ if (s.children?.length) out.children = s.children.map(mapStep);
60
+ if (s.parameters?.length) {
61
+ out.payload = s.parameters.map((p) => `${p.name} = ${p.value}`).join("\n");
62
+ }
63
+ if (s.assertion) {
64
+ out.assertion = {
65
+ passed: s.status === "pass",
66
+ matcher: s.action || "expect",
67
+ expected: typeof s.assertion.expected === "string" ? s.assertion.expected : JSON.stringify(s.assertion.expected),
68
+ actual: typeof s.assertion.received === "string" ? s.assertion.received : JSON.stringify(s.assertion.received)
69
+ };
70
+ if (s.assertion.diff) out.body = s.assertion.diff;
71
+ }
72
+ if (s.network?.length) {
73
+ const n = s.network[0];
74
+ out.request = {
75
+ method: n.method,
76
+ url: n.url,
77
+ duration: fmtDuration(n.durationMs),
78
+ ...n.requestBody ? { body: n.requestBody } : {}
79
+ };
80
+ out.response = {
81
+ status: n.status,
82
+ statusText: n.status >= 400 ? "ERR" : "OK",
83
+ size: n.sizeBytes ? n.sizeBytes + "B" : "",
84
+ ...n.responseBody ? { body: n.responseBody } : {}
85
+ };
86
+ }
87
+ if (s.attachments?.length) {
88
+ const visual = s.attachments.find(
89
+ (a) => a.kind === "screenshot" || a.kind === "image" || a.kind === "video" || (a.mimeType || "").startsWith("image/") || (a.mimeType || "").startsWith("video/")
90
+ );
91
+ if (visual) {
92
+ const url = attachmentBase + (visual.relativePath || "").replace(/^\/+/, "");
93
+ const name = (visual.relativePath || "").split(/[\\/]/).pop() || visual.id || "attachment";
94
+ const sizeKB = visual.sizeBytes ? (visual.sizeBytes / 1024).toFixed(1) + " KB" : "";
95
+ out.screenshot = { url, name, size: sizeKB, dimensions: "" };
96
+ }
97
+ out._attachments = s.attachments;
98
+ }
99
+ return out;
100
+ }
101
+ return mapStep;
102
+ }
103
+ function buildRichTest(c, idx, runStartMs) {
104
+ const startMs = c.startedAt ? Math.max(0, new Date(c.startedAt).getTime() - runStartMs) : 0;
105
+ return {
106
+ id: c.id,
107
+ order: idx + 1,
108
+ name: c.name,
109
+ fullName: c.fullName,
110
+ status: STATUS[c.status] || c.status,
111
+ dur: fmtDuration(c.duration),
112
+ durMs: c.duration || 0,
113
+ start: startMs,
114
+ suite: (c.suite || []).join(" \u203A "),
115
+ suiteChain: c.suite || [],
116
+ severity: c.severity || "normal",
117
+ retries: c.retries || 0,
118
+ flakeRate: 0,
119
+ avgDurMs: c.duration || 0,
120
+ platform: [c.browser, c.platform].filter(Boolean).join(" \xB7 "),
121
+ description: "",
122
+ parameters: [],
123
+ tags: c.tags || [],
124
+ owner: c.owner || "",
125
+ file: c.filePath ? c.filePath + (c.line ? ":" + c.line : "") : "",
126
+ epic: c.behavior?.epic,
127
+ feature: c.behavior?.feature,
128
+ story: c.behavior?.scenario,
129
+ lastRun: c.startedAt ? relTime(c.startedAt) : "",
130
+ bdd: null,
131
+ labels: c.labels || {},
132
+ links: c.links || [],
133
+ error: c.hasErrors || c.errorPreview ? {
134
+ kind: c.errorType || "Error",
135
+ message: c.errorPreview || "",
136
+ stack: ""
137
+ } : null,
138
+ steps: [],
139
+ _summary: c,
140
+ _full: null,
141
+ _stepsLoaded: false
142
+ };
143
+ }
144
+ function deriveSuiteTree(cases) {
145
+ const root = { _children: /* @__PURE__ */ new Map() };
146
+ for (const c of cases) {
147
+ const chain = c.suite && c.suite.length ? c.suite : ["Default"];
148
+ let node = root;
149
+ for (let i = 0; i < chain.length; i++) {
150
+ const part = chain[i];
151
+ if (!node._children.has(part)) node._children.set(part, { _name: part, _children: /* @__PURE__ */ new Map() });
152
+ node = node._children.get(part);
153
+ }
154
+ if (!node._tests) node._tests = [];
155
+ node._tests.push(c.id);
156
+ }
157
+ let auto = 0;
158
+ function toTree(node, parentId) {
159
+ const out = [];
160
+ for (const [name, child] of node._children) {
161
+ const id = parentId + "/" + name;
162
+ const childNodes = toTree(child, id);
163
+ const leaves = (child._tests || []).map((tid) => ({ id: id + "/leaf-" + ++auto, testId: tid }));
164
+ const all = [...childNodes, ...leaves];
165
+ if (all.length) out.push({ id, name, children: all });
166
+ }
167
+ return out;
168
+ }
169
+ return toTree(root, "suite");
170
+ }
171
+ function _kvBehaviorPickFromLabels(c) {
172
+ const labels = c.labels || {};
173
+ return {
174
+ epic: c.behavior && c.behavior.epic || labels.epic || labels.Epic,
175
+ feature: c.behavior && c.behavior.feature || labels.feature || labels.Feature,
176
+ story: c.behavior && c.behavior.scenario || labels.story || labels.Story
177
+ };
178
+ }
179
+ function deriveBehaviorTree(cases) {
180
+ const tree = /* @__PURE__ */ new Map();
181
+ for (const c of cases) {
182
+ const { epic, feature, story } = _kvBehaviorPickFromLabels(c);
183
+ if (!epic && !feature) continue;
184
+ const e = epic || "(unspecified epic)";
185
+ const f = feature || "(unspecified feature)";
186
+ if (!tree.has(e)) tree.set(e, /* @__PURE__ */ new Map());
187
+ const features = tree.get(e);
188
+ if (!features.has(f)) features.set(f, []);
189
+ features.get(f).push({ caseId: c.id, story: story || c.name });
190
+ }
191
+ let auto = 0;
192
+ const out = [];
193
+ for (const [epic, features] of tree) {
194
+ const eId = "epic/" + epic;
195
+ const fNodes = [];
196
+ for (const [feature, stories] of features) {
197
+ const fId = eId + "/" + feature;
198
+ const sNodes = stories.map((s) => ({ id: fId + "/leaf-" + ++auto, testId: s.caseId }));
199
+ fNodes.push({ id: fId, name: "Feature \xB7 " + feature, children: sNodes });
200
+ }
201
+ out.push({ id: eId, name: "Epic \xB7 " + epic, children: fNodes });
202
+ }
203
+ return out;
204
+ }
205
+ function deriveCategories(cases) {
206
+ const map = /* @__PURE__ */ new Map();
207
+ for (const c of cases) {
208
+ const isFail = c.status === "fail" || c.status === "broken";
209
+ if (!isFail && !c.errorType && !c.errorPreview) continue;
210
+ const kind = c.errorType || "Error";
211
+ const family = c.status === "broken" ? "broken" : c.status === "skip" ? "skipped" : "failed";
212
+ const color = family === "failed" ? "var(--status-failed)" : family === "broken" ? "var(--status-broken)" : "var(--status-skipped)";
213
+ if (!map.has(kind)) map.set(kind, { kind, family, color, count: 0, tests: [], description: describeKind(kind) });
214
+ const e = map.get(kind);
215
+ e.count += 1;
216
+ e.tests.push(c.id);
217
+ }
218
+ return [...map.values()].sort((a, b) => b.count - a.count);
219
+ }
220
+ function describeKind(kind) {
221
+ const map = {
222
+ AssertionError: "Expected vs. actual mismatch in a test assertion. Most often a real product defect.",
223
+ TimeoutError: "A wait condition exceeded its budget. Could be a slow service or a missing element.",
224
+ NetworkError: "Non-2xx response from a backend dependency during the test.",
225
+ Error: "Generic error \u2014 inspect the failing test for details."
226
+ };
227
+ return map[kind] || "Failures classified as " + kind + ".";
228
+ }
229
+ function deriveTimelineRows(cases, runStartMs) {
230
+ return cases.filter((c) => c.duration && c.duration > 0).map((c) => ({
231
+ id: c.id,
232
+ suite: c.suite && c.suite[0] || "Default",
233
+ name: c.name,
234
+ start: c.startedAt ? Math.max(0, new Date(c.startedAt).getTime() - runStartMs) : 0,
235
+ durMs: c.duration,
236
+ dur: fmtDuration(c.duration),
237
+ status: STATUS[c.status] || c.status,
238
+ platform: [c.browser, c.platform].filter(Boolean).join(" \xB7 "),
239
+ severity: c.severity || "normal",
240
+ retries: c.retries || 0,
241
+ file: c.filePath ? c.filePath + (c.line ? ":" + c.line : "") : ""
242
+ }));
243
+ }
244
+ function deriveTrendRuns(history, current) {
245
+ const runs = [];
246
+ if (history?.length) {
247
+ for (const h of history) {
248
+ const t = h.totals || {};
249
+ runs.push({
250
+ short: (h.id || "").slice(-4) || h.id,
251
+ passed: t.pass || 0,
252
+ failed: t.fail || 0,
253
+ broken: t.broken || 0,
254
+ skipped: t.skip || 0
255
+ });
256
+ }
257
+ }
258
+ if (current) runs.push(current);
259
+ if (runs.length === 0 && current) runs.push(current);
260
+ return runs;
261
+ }
262
+ function deriveDurationHistogram(cases) {
263
+ const buckets = [
264
+ { label: "<100ms", max: 100 },
265
+ { label: "<500ms", max: 500 },
266
+ { label: "<1s", max: 1e3 },
267
+ { label: "<2s", max: 2e3 },
268
+ { label: "<5s", max: 5e3 },
269
+ { label: "<10s", max: 1e4 },
270
+ { label: "\u226510s", max: Infinity }
271
+ ];
272
+ return buckets.map((b) => ({
273
+ label: b.label,
274
+ n: cases.filter((c) => c.duration && c.duration > 0 && c.duration < b.max).length
275
+ })).map((b, i, arr) => ({
276
+ label: b.label,
277
+ n: i === 0 ? b.n : b.n - (arr[i - 1]?.n || 0)
278
+ }));
279
+ }
280
+ function deriveHistoryRuns(history, current) {
281
+ const out = [];
282
+ if (current) {
283
+ out.push({
284
+ id: current.id,
285
+ when: "2m ago",
286
+ branch: current.branch,
287
+ actor: current.actor,
288
+ passed: current.counts.passed,
289
+ failed: current.counts.failed,
290
+ broken: current.counts.broken,
291
+ skipped: current.counts.skipped,
292
+ dur: current.duration,
293
+ status: current.counts.failed + current.counts.broken > 0 ? "failed" : "passed"
294
+ });
295
+ }
296
+ if (history?.length) {
297
+ for (const h of history) {
298
+ const t = h.totals || {};
299
+ const failed = (t.fail || 0) + (t.broken || 0);
300
+ out.push({
301
+ id: "#" + (h.id || "?"),
302
+ when: h.startedAt ? relTime(h.startedAt) : "",
303
+ branch: h.branch || "main",
304
+ actor: "kensho",
305
+ passed: t.pass || 0,
306
+ failed: t.fail || 0,
307
+ broken: t.broken || 0,
308
+ skipped: t.skip || 0,
309
+ dur: fmtDuration(h.durationMs),
310
+ status: failed > 0 ? "failed" : "passed"
311
+ });
312
+ }
313
+ }
314
+ return out;
315
+ }
316
+ var ENV_LABEL = {
317
+ ci: "CI",
318
+ branch: "Branch",
319
+ commit: "Commit",
320
+ commitMsg: "Commit msg",
321
+ author: "Author",
322
+ runUrl: "Run URL",
323
+ repoUrl: "Repo URL",
324
+ os: "OS",
325
+ osVersion: "OS version",
326
+ arch: "Arch",
327
+ nodeVersion: "Node",
328
+ pythonVersion: "Python",
329
+ browsers: "Browsers",
330
+ workers: "Workers",
331
+ stage: "Stage",
332
+ baseUrl: "Base URL",
333
+ appVersion: "App version",
334
+ buildNumber: "Build",
335
+ release: "Release",
336
+ device: "Device",
337
+ viewport: "Viewport",
338
+ region: "Region",
339
+ locale: "Locale",
340
+ timezone: "Timezone",
341
+ tunnel: "Tunnel",
342
+ trigger: "Trigger",
343
+ feature: "Feature"
344
+ };
345
+ async function loadKenshoData(dataUrl, opts = {}) {
346
+ if (!dataUrl) throw new Error("loadKenshoData: dataUrl is required");
347
+ const baseUrl = String(dataUrl).replace(/\/+$/, "");
348
+ const fetchImpl = opts.fetch || (typeof fetch !== "undefined" ? fetch : null);
349
+ if (!fetchImpl) throw new Error("loadKenshoData: no `fetch` available \u2014 pass opts.fetch.");
350
+ const caseUrl = opts.caseUrl || ((id) => `${baseUrl}/cases/${id}.json`);
351
+ const attachmentBase = baseUrl + "/";
352
+ const mapStep = makeMapStep(attachmentBase);
353
+ const idx = await fetchImpl(`${baseUrl}/index.json`, { cache: "no-cache" }).then((r) => r.json());
354
+ const runStartMs = idx.startedAt ? new Date(idx.startedAt).getTime() : Date.now();
355
+ const totals = idx.totals || {};
356
+ const counts = {
357
+ passed: totals.pass || 0,
358
+ failed: totals.fail || 0,
359
+ broken: totals.broken || 0,
360
+ skipped: totals.skip || 0
361
+ };
362
+ const run = {
363
+ id: "#" + (idx.runId || "unknown"),
364
+ branch: idx.env?.branch || (idx.env?.ci === "local" ? "local" : "main"),
365
+ commit: (idx.env?.commit || "").slice(0, 7),
366
+ commitFull: idx.env?.commit || "",
367
+ actor: idx.env?.author || idx.project?.slug || "kensho",
368
+ startedAt: idx.startedAt ? new Date(idx.startedAt).toLocaleString() : "",
369
+ duration: fmtDuration(idx.durationMs),
370
+ counts,
371
+ repoUrl: idx.env?.repoUrl || "",
372
+ runUrl: idx.env?.runUrl || ""
373
+ };
374
+ const ENV_FIELDS = Object.keys(ENV_LABEL);
375
+ const env = ENV_FIELDS.filter((k) => idx.env?.[k] != null && idx.env[k] !== "" && (!Array.isArray(idx.env[k]) || idx.env[k].length > 0)).map((k) => [
376
+ ENV_LABEL[k] || k,
377
+ Array.isArray(idx.env[k]) ? idx.env[k].join(", ") : String(idx.env[k])
378
+ ]);
379
+ if (idx.env?.vars && typeof idx.env.vars === "object") {
380
+ for (const [k, v] of Object.entries(idx.env.vars)) {
381
+ if (v != null && v !== "") env.push([k, String(v)]);
382
+ }
383
+ }
384
+ const cases = idx.cases || [];
385
+ const richTests = {};
386
+ cases.forEach((c, i) => {
387
+ richTests[c.id] = buildRichTest(c, i, runStartMs);
388
+ });
389
+ const bySuite = /* @__PURE__ */ new Map();
390
+ for (const c of cases) {
391
+ const key = c.suite && c.suite[0] || "Default";
392
+ const arr = bySuite.get(key) || [];
393
+ arr.push(c);
394
+ bySuite.set(key, arr);
395
+ }
396
+ const suites = [...bySuite.entries()].map(([name, cs]) => {
397
+ const segs = ["pass", "fail", "broken", "skip"].map((k) => ({ k: STATUS[k], n: cs.filter((c) => c.status === k).length })).filter((s) => s.n > 0);
398
+ return { name, segs, total: cs.length };
399
+ });
400
+ const tests = cases.map((c) => ({
401
+ ns: "",
402
+ name: c.name,
403
+ status: STATUS[c.status] || c.status,
404
+ duration: fmtDuration(c.duration),
405
+ last: c.startedAt ? relTime(c.startedAt) : "",
406
+ retries: c.retries,
407
+ richId: c.id
408
+ }));
409
+ const suiteTree = deriveSuiteTree(cases);
410
+ const behaviorTree = deriveBehaviorTree(cases);
411
+ const categories = deriveCategories(cases);
412
+ const timelineTests = deriveTimelineRows(cases, runStartMs);
413
+ const trendRuns = deriveTrendRuns(idx.history, {
414
+ short: (idx.runId || "").slice(-4) || idx.runId,
415
+ passed: counts.passed,
416
+ failed: counts.failed,
417
+ broken: counts.broken,
418
+ skipped: counts.skipped
419
+ });
420
+ const histogram = deriveDurationHistogram(cases);
421
+ const historyRuns = deriveHistoryRuns(idx.history, run);
422
+ const reportType = idx.reportType || (idx.framework?.name === "playwright" ? "e2e" : idx.framework?.name === "jest" || idx.framework?.name === "vitest" || idx.framework?.name === "pytest" ? "unit" : "mixed");
423
+ const caseCache = {};
424
+ async function loadCase(id) {
425
+ if (!id) return null;
426
+ if (caseCache[id]) return caseCache[id];
427
+ try {
428
+ const r = await fetchImpl(caseUrl(id), { cache: "no-cache" });
429
+ caseCache[id] = await r.json();
430
+ return caseCache[id];
431
+ } catch (e) {
432
+ console.error("[kensho] failed to load case", id, e);
433
+ return null;
434
+ }
435
+ }
436
+ async function ensureCaseLoaded(richTest) {
437
+ if (!richTest || richTest._stepsLoaded) return richTest;
438
+ const full = await loadCase(richTest.id);
439
+ if (!full) {
440
+ richTest._stepsLoaded = true;
441
+ return richTest;
442
+ }
443
+ richTest._full = full;
444
+ richTest.description = full.description || richTest.description;
445
+ richTest.parameters = (full.parameters || []).map((p) => [p.name, p.value]);
446
+ if (full.behavior?.gherkin?.length) {
447
+ const text = full.behavior.gherkin.join(" ");
448
+ const m = text.match(/given\s+(.+?)\s+when\s+(.+?)\s+then\s+(.+)/i);
449
+ if (m) richTest.bdd = { given: m[1].trim(), when: m[2].trim(), then: m[3].trim() };
450
+ }
451
+ if (full.errors?.length) {
452
+ const e = full.errors[0];
453
+ richTest.error = { kind: e.type || "Error", message: e.message || "", stack: e.stack || "" };
454
+ }
455
+ richTest.steps = (full.steps || []).map(mapStep);
456
+ richTest.attachments = full.attachments || [];
457
+ richTest.logs = (full.logs || []).map(mapLog);
458
+ richTest.history = full.history || [];
459
+ richTest._stepsLoaded = true;
460
+ return richTest;
461
+ }
462
+ return {
463
+ kenshoIndex: idx,
464
+ reportType,
465
+ run,
466
+ env,
467
+ suites,
468
+ tests,
469
+ richTests,
470
+ suiteTree,
471
+ behaviorTree,
472
+ categories,
473
+ timelineTests,
474
+ trendRuns,
475
+ histogram,
476
+ historyRuns,
477
+ ensureCaseLoaded,
478
+ loadCase,
479
+ fmtDuration,
480
+ relTime,
481
+ // Helpers for the data-bridge adapter (mostly unused outside it).
482
+ _baseUrl: baseUrl,
483
+ _attachmentBase: attachmentBase
484
+ };
485
+ }
486
+
487
+ // src/component.jsx
488
+ var KenshoContext = React.createContext(null);
489
+ function useKenshoCtx() {
490
+ return React.useContext(KenshoContext);
491
+ }
492
+ if (typeof window !== "undefined") {
493
+ window.__KenshoContext = KenshoContext;
494
+ }
495
+ var LEGACY_SCRIPTS = [
496
+ "data-loader.js",
497
+ // exposes window.__KENSHO_LOAD_DATA
498
+ "components.js",
499
+ "charts.js",
500
+ "test-detail.js",
501
+ "tree-detail.js",
502
+ "pages.js",
503
+ "app.js"
504
+ ];
505
+ var _injected = /* @__PURE__ */ new Set();
506
+ function injectScript(url) {
507
+ return new Promise((resolve, reject) => {
508
+ const existing = document.querySelector(`script[data-kv-src="${url}"]`);
509
+ if (existing) {
510
+ if (existing.dataset.kvLoaded === "1") return resolve();
511
+ existing.addEventListener("load", () => resolve(), { once: true });
512
+ existing.addEventListener("error", () => reject(new Error("failed: " + url)), { once: true });
513
+ return;
514
+ }
515
+ const s = document.createElement("script");
516
+ s.src = url;
517
+ s.dataset.kvSrc = url;
518
+ s.async = false;
519
+ s.addEventListener("load", () => {
520
+ s.dataset.kvLoaded = "1";
521
+ resolve();
522
+ }, { once: true });
523
+ s.addEventListener("error", () => reject(new Error("failed: " + url)), { once: true });
524
+ document.head.appendChild(s);
525
+ });
526
+ }
527
+ async function loadLegacyAssets(assetsBaseUrl) {
528
+ const base = String(assetsBaseUrl).replace(/\/+$/, "");
529
+ for (const name of LEGACY_SCRIPTS) {
530
+ const url = `${base}/${name}`;
531
+ if (_injected.has(url)) continue;
532
+ await injectScript(url);
533
+ _injected.add(url);
534
+ }
535
+ if (!window.lucide) {
536
+ await injectScript("https://unpkg.com/lucide@latest/dist/umd/lucide.min.js");
537
+ }
538
+ }
539
+ function installNoAutoMount() {
540
+ if (!window.__KENSHO_BOOT) window.__KENSHO_BOOT = Promise.resolve();
541
+ const dom = window.ReactDOM;
542
+ if (!dom) {
543
+ window.ReactDOM = { createRoot: () => ({ render() {
544
+ }, unmount() {
545
+ } }) };
546
+ }
547
+ if (!window.React) {
548
+ window.React = React;
549
+ }
550
+ }
551
+ function KenshoViewer(props) {
552
+ const {
553
+ dataUrl,
554
+ caseUrl,
555
+ assetsUrl,
556
+ // optional: where to load the viewer's compiled JS from. Default: same package.
557
+ onCaseOpen,
558
+ onPageChange,
559
+ extraSidebar,
560
+ extraTabs,
561
+ initial,
562
+ ownKeyboard = false
563
+ } = props;
564
+ if (!dataUrl) throw new Error('<KenshoViewer dataUrl="..." /> is required');
565
+ const containerRef = useRef(null);
566
+ const [phase, setPhase] = useState("boot");
567
+ const [errMsg, setErrMsg] = useState("");
568
+ const [state, setState] = useState(null);
569
+ const resolvedAssetsUrl = assetsUrl || guessDefaultAssetsUrl();
570
+ useEffect(() => {
571
+ let cancelled = false;
572
+ setPhase("loading");
573
+ installNoAutoMount();
574
+ loadLegacyAssets(resolvedAssetsUrl).then(() => loadKenshoData(dataUrl, { caseUrl: caseUrl ? (id) => caseUrl(id) : void 0 })).then((s) => {
575
+ if (cancelled) return;
576
+ applyToWindow(s);
577
+ setState(s);
578
+ setPhase("ready");
579
+ }).catch((err) => {
580
+ if (cancelled) return;
581
+ console.error("[KenshoViewer] failed to boot:", err);
582
+ setErrMsg(err?.message || String(err));
583
+ setPhase("error");
584
+ });
585
+ return () => {
586
+ cancelled = true;
587
+ };
588
+ }, [dataUrl]);
589
+ const rootRef = useRef(null);
590
+ useEffect(() => {
591
+ if (phase !== "ready" || !state || !containerRef.current) return;
592
+ const App = window.App;
593
+ if (typeof App !== "function") {
594
+ console.error("[KenshoViewer] window.App not found after legacy load. Build out of date?");
595
+ return;
596
+ }
597
+ let cancelled = false;
598
+ import("react-dom/client").then(({ createRoot }) => {
599
+ if (cancelled) return;
600
+ const root = createRoot(containerRef.current);
601
+ rootRef.current = root;
602
+ renderLegacy(root);
603
+ });
604
+ return () => {
605
+ cancelled = true;
606
+ try {
607
+ rootRef.current?.unmount();
608
+ } catch {
609
+ }
610
+ rootRef.current = null;
611
+ };
612
+ }, [phase, state]);
613
+ useEffect(() => {
614
+ if (rootRef.current) renderLegacy(rootRef.current);
615
+ }, [extraSidebar, extraTabs, onCaseOpen, onPageChange, ownKeyboard]);
616
+ function renderLegacy(root) {
617
+ const App = window.App;
618
+ if (typeof App !== "function") return;
619
+ const ctxValue = {
620
+ state,
621
+ extraSidebar: extraSidebar || [],
622
+ extraTabs: extraTabs || [],
623
+ onCaseOpen,
624
+ onPageChange,
625
+ ownKeyboard,
626
+ // `page` / `caseId` here are the host-controlled values from
627
+ // `props.initial`. The legacy App reads `ctx?.page` only as an
628
+ // optional one-way sync source (see app.jsx). Local navigation
629
+ // inside the viewer keeps using the App's own `useState`.
630
+ page: initial?.page,
631
+ caseId: initial?.caseId
632
+ };
633
+ const legacyApp = React.createElement(App, null);
634
+ root.render(
635
+ React.createElement(KenshoContext.Provider, { value: ctxValue }, legacyApp)
636
+ );
637
+ }
638
+ useEffect(() => {
639
+ if (!ownKeyboard) return;
640
+ const node = containerRef.current;
641
+ if (!node) return;
642
+ const onKeyDown = (e) => {
643
+ e.stopPropagation();
644
+ };
645
+ node.addEventListener("keydown", onKeyDown, true);
646
+ return () => node.removeEventListener("keydown", onKeyDown, true);
647
+ }, [ownKeyboard, phase]);
648
+ if (phase === "error") {
649
+ return React.createElement(
650
+ "div",
651
+ { className: "kv-embed-error", style: { padding: 24, color: "#E5484D", fontFamily: "sans-serif" } },
652
+ React.createElement("h3", null, "Failed to load Kensho report"),
653
+ React.createElement("pre", { style: { whiteSpace: "pre-wrap", background: "#fcebec", padding: 12, borderRadius: 6 } }, errMsg)
654
+ );
655
+ }
656
+ return React.createElement("div", {
657
+ ref: containerRef,
658
+ className: "kv-embed-root",
659
+ "data-kensho-viewer": "",
660
+ style: { width: "100%", height: "100%", minHeight: 480 }
661
+ });
662
+ }
663
+ function guessDefaultAssetsUrl() {
664
+ if (typeof document === "undefined") return "/kensho-viewer-assets";
665
+ const marker = document.querySelector("[data-kensho-viewer-assets]");
666
+ if (marker) return marker.getAttribute("data-kensho-viewer-assets") || marker.getAttribute("href");
667
+ return "./kensho-viewer-assets";
668
+ }
669
+ function applyToWindow(state) {
670
+ Object.assign(window, {
671
+ KENSHO_INDEX: state.kenshoIndex,
672
+ KENSHO_REPORT_TYPE: state.reportType,
673
+ RUN: state.run,
674
+ ENV: state.env,
675
+ SUITES: state.suites,
676
+ TESTS: state.tests,
677
+ RICH_TESTS: state.richTests,
678
+ SUITE_TREE: state.suiteTree,
679
+ BEHAVIOR_TREE: state.behaviorTree,
680
+ CATEGORIES: state.categories,
681
+ TIMELINE_TESTS: state.timelineTests,
682
+ TREND_RUNS: state.trendRuns,
683
+ HISTOGRAM: state.histogram,
684
+ HISTORY_RUNS: state.historyRuns,
685
+ _kenshoEnsureCase: state.ensureCaseLoaded,
686
+ _kenshoLoadCase: state.loadCase,
687
+ _kenshoFmtDuration: state.fmtDuration,
688
+ _kenshoRelTime: state.relTime
689
+ });
690
+ window.__KENSHO_BOOT = Promise.resolve();
691
+ }
692
+ var component_default = KenshoViewer;
693
+ export {
694
+ KenshoViewer,
695
+ component_default as default,
696
+ loadKenshoData,
697
+ useKenshoCtx
698
+ };