executable-stories-react 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,820 @@
1
+ "use client";
2
+
3
+ // src/interactive/ReportInteractive.tsx
4
+ import {
5
+ useCallback as useCallback2,
6
+ useMemo as useMemo2,
7
+ useRef as useRef3,
8
+ useState
9
+ } from "react";
10
+
11
+ // src/context/ReportRoot.tsx
12
+ import { useMemo } from "react";
13
+
14
+ // src/context/ReportContext.ts
15
+ import { createContext } from "react";
16
+ var ReportContext = createContext(null);
17
+
18
+ // src/context/ReportRoot.tsx
19
+ import { jsx } from "react/jsx-runtime";
20
+ var EMPTY_CUSTOM = {};
21
+ var EMPTY_RENDERERS = {};
22
+ function ReportRoot({
23
+ report,
24
+ customRenderers,
25
+ renderers,
26
+ children
27
+ }) {
28
+ const value = useMemo(
29
+ () => ({
30
+ report,
31
+ customRenderers: customRenderers ?? EMPTY_CUSTOM,
32
+ renderers: renderers ?? EMPTY_RENDERERS
33
+ }),
34
+ [report, customRenderers, renderers]
35
+ );
36
+ return /* @__PURE__ */ jsx(ReportContext.Provider, { value, children });
37
+ }
38
+
39
+ // src/hooks/useReport.ts
40
+ import { useContext } from "react";
41
+ function useReport() {
42
+ const ctx = useContext(ReportContext);
43
+ if (!ctx) {
44
+ throw new Error(
45
+ "useReport must be used inside <ReportRoot> or <Report>. Wrap your tree with one of those."
46
+ );
47
+ }
48
+ return ctx.report;
49
+ }
50
+
51
+ // src/components/ReportSummary.tsx
52
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
53
+ function ReportSummary({ className }) {
54
+ const report = useReport();
55
+ return /* @__PURE__ */ jsx2(
56
+ ReportSummaryView,
57
+ {
58
+ summary: report.summary,
59
+ ...className !== void 0 && { className },
60
+ ariaLabel: "Run summary"
61
+ }
62
+ );
63
+ }
64
+ function ReportSummaryView({ summary, className, ariaLabel }) {
65
+ return /* @__PURE__ */ jsxs(
66
+ "p",
67
+ {
68
+ className: ["es-report-summary", className].filter(Boolean).join(" "),
69
+ "aria-label": ariaLabel,
70
+ children: [
71
+ /* @__PURE__ */ jsxs("span", { children: [
72
+ /* @__PURE__ */ jsx2("strong", { children: summary.total }),
73
+ " scenario",
74
+ summary.total === 1 ? "" : "s"
75
+ ] }),
76
+ " \xB7 ",
77
+ /* @__PURE__ */ jsxs("span", { "data-status": "passed", children: [
78
+ summary.passed,
79
+ " passed"
80
+ ] }),
81
+ " \xB7 ",
82
+ /* @__PURE__ */ jsxs("span", { "data-status": "failed", children: [
83
+ summary.failed,
84
+ " failed"
85
+ ] }),
86
+ summary.skipped > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
87
+ " \xB7 ",
88
+ /* @__PURE__ */ jsxs("span", { "data-status": "skipped", children: [
89
+ summary.skipped,
90
+ " skipped"
91
+ ] })
92
+ ] }) : null,
93
+ summary.pending > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
94
+ " \xB7 ",
95
+ /* @__PURE__ */ jsxs("span", { "data-status": "pending", children: [
96
+ summary.pending,
97
+ " pending"
98
+ ] })
99
+ ] }) : null
100
+ ]
101
+ }
102
+ );
103
+ }
104
+
105
+ // src/components/doc/DocNote.tsx
106
+ import { jsx as jsx3 } from "react/jsx-runtime";
107
+ function DocNote({ entry }) {
108
+ return /* @__PURE__ */ jsx3("p", { className: "es-doc es-doc-note", children: entry.text });
109
+ }
110
+
111
+ // src/components/doc/DocTag.tsx
112
+ import { jsx as jsx4 } from "react/jsx-runtime";
113
+ function DocTag({ entry }) {
114
+ return /* @__PURE__ */ jsx4("ul", { className: "es-doc es-tags", "aria-label": "Tags", children: entry.names.map((n) => /* @__PURE__ */ jsx4("li", { children: n }, n)) });
115
+ }
116
+
117
+ // src/components/doc/DocKv.tsx
118
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
119
+ function formatValue(value) {
120
+ if (value === null) return "null";
121
+ if (typeof value === "string") return value;
122
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
123
+ try {
124
+ return JSON.stringify(value);
125
+ } catch {
126
+ return String(value);
127
+ }
128
+ }
129
+ function DocKv({ entry }) {
130
+ return /* @__PURE__ */ jsxs2("dl", { className: "es-doc es-doc-kv", children: [
131
+ /* @__PURE__ */ jsx5("dt", { children: entry.label }),
132
+ /* @__PURE__ */ jsx5("dd", { children: formatValue(entry.value) })
133
+ ] });
134
+ }
135
+
136
+ // src/hooks/useRenderers.ts
137
+ import { useContext as useContext2 } from "react";
138
+ var EMPTY_CUSTOM2 = {};
139
+ var EMPTY_RENDERERS2 = {};
140
+ function useCustomRenderers() {
141
+ const ctx = useContext2(ReportContext);
142
+ return ctx?.customRenderers ?? EMPTY_CUSTOM2;
143
+ }
144
+ function useBuiltinRenderers() {
145
+ const ctx = useContext2(ReportContext);
146
+ return ctx?.renderers ?? EMPTY_RENDERERS2;
147
+ }
148
+
149
+ // src/components/doc/DocCode.tsx
150
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
151
+ function DocCode({ entry }) {
152
+ const renderers = useBuiltinRenderers();
153
+ if (renderers.code) {
154
+ return /* @__PURE__ */ jsx6(Fragment2, { children: renderers.code(entry) });
155
+ }
156
+ return /* @__PURE__ */ jsxs3("figure", { className: "es-doc es-doc-code", children: [
157
+ /* @__PURE__ */ jsx6("figcaption", { children: entry.label }),
158
+ /* @__PURE__ */ jsx6("pre", { children: /* @__PURE__ */ jsx6("code", { className: entry.lang ? `language-${entry.lang}` : void 0, children: entry.content }) })
159
+ ] });
160
+ }
161
+
162
+ // src/components/doc/DocTable.tsx
163
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
164
+ function DocTable({ entry }) {
165
+ return /* @__PURE__ */ jsxs4("figure", { className: "es-doc es-doc-table", children: [
166
+ /* @__PURE__ */ jsx7("figcaption", { children: entry.label }),
167
+ /* @__PURE__ */ jsxs4("table", { children: [
168
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsx7("tr", { children: entry.columns.map((c) => /* @__PURE__ */ jsx7("th", { scope: "col", children: c }, c)) }) }),
169
+ /* @__PURE__ */ jsx7("tbody", { children: entry.rows.map((row, i) => /* @__PURE__ */ jsx7("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx7("td", { children: cell }, j)) }, i)) })
170
+ ] })
171
+ ] });
172
+ }
173
+
174
+ // src/components/doc/DocLink.tsx
175
+ import { jsx as jsx8 } from "react/jsx-runtime";
176
+ function DocLink({ entry }) {
177
+ return /* @__PURE__ */ jsx8(
178
+ "a",
179
+ {
180
+ className: "es-doc es-doc-link",
181
+ href: entry.url,
182
+ rel: "noreferrer noopener",
183
+ target: "_blank",
184
+ children: entry.label
185
+ }
186
+ );
187
+ }
188
+
189
+ // src/components/doc/DocSection.tsx
190
+ import { marked } from "marked";
191
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
192
+ function safeMarkdownHtml(markdown) {
193
+ const raw = marked.parse(markdown, { async: false });
194
+ return raw.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "").replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "").replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "").replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "").replace(/(href|src)\s*=\s*"\s*javascript:[^"]*"/gi, '$1="#"').replace(/(href|src)\s*=\s*'\s*javascript:[^']*'/gi, "$1='#'");
195
+ }
196
+ function DocSection({ entry }) {
197
+ const renderers = useBuiltinRenderers();
198
+ if (renderers.section) {
199
+ return /* @__PURE__ */ jsx9(Fragment3, { children: renderers.section(entry) });
200
+ }
201
+ const html = safeMarkdownHtml(entry.markdown);
202
+ return /* @__PURE__ */ jsxs5("section", { className: "es-doc es-doc-section", "aria-label": entry.title, children: [
203
+ entry.title ? /* @__PURE__ */ jsx9("h4", { className: "es-doc-section-title", children: entry.title }) : null,
204
+ /* @__PURE__ */ jsx9(
205
+ "div",
206
+ {
207
+ className: "es-doc-section-content",
208
+ dangerouslySetInnerHTML: { __html: html }
209
+ }
210
+ )
211
+ ] });
212
+ }
213
+
214
+ // src/components/doc/DocMermaid.tsx
215
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
216
+ function DocMermaid({ entry }) {
217
+ const renderers = useBuiltinRenderers();
218
+ if (renderers.mermaid) {
219
+ return /* @__PURE__ */ jsx10(Fragment4, { children: renderers.mermaid(entry) });
220
+ }
221
+ return /* @__PURE__ */ jsxs6(
222
+ "figure",
223
+ {
224
+ className: "es-doc es-doc-mermaid",
225
+ "aria-label": entry.title ?? "Diagram",
226
+ children: [
227
+ entry.title ? /* @__PURE__ */ jsx10("figcaption", { children: entry.title }) : null,
228
+ /* @__PURE__ */ jsx10("pre", { "data-mermaid": true, children: entry.code })
229
+ ]
230
+ }
231
+ );
232
+ }
233
+
234
+ // src/components/doc/DocScreenshot.tsx
235
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
236
+ function DocScreenshot({ entry }) {
237
+ return /* @__PURE__ */ jsxs7("figure", { className: "es-doc es-doc-screenshot", children: [
238
+ /* @__PURE__ */ jsx11("img", { src: entry.path, alt: entry.alt ?? "", loading: "lazy" }),
239
+ entry.alt ? /* @__PURE__ */ jsx11("figcaption", { children: entry.alt }) : null
240
+ ] });
241
+ }
242
+
243
+ // src/components/doc/DocCustom.tsx
244
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
245
+ function DocCustom({ entry }) {
246
+ const renderers = useCustomRenderers();
247
+ const renderer = renderers[entry.type];
248
+ if (renderer) {
249
+ return /* @__PURE__ */ jsx12(Fragment5, { children: renderer(entry) });
250
+ }
251
+ return /* @__PURE__ */ jsxs8("div", { className: "es-doc es-doc-custom", "data-type": entry.type, children: [
252
+ /* @__PURE__ */ jsx12("p", { className: "es-doc-custom-type", children: entry.type }),
253
+ /* @__PURE__ */ jsx12("pre", { children: safeStringify(entry.data) })
254
+ ] });
255
+ }
256
+ function safeStringify(value) {
257
+ try {
258
+ return JSON.stringify(value, null, 2);
259
+ } catch {
260
+ return String(value);
261
+ }
262
+ }
263
+
264
+ // src/components/doc/DocEntry.tsx
265
+ import { jsx as jsx13 } from "react/jsx-runtime";
266
+ function DocEntry({ entry }) {
267
+ switch (entry.kind) {
268
+ case "note":
269
+ return /* @__PURE__ */ jsx13(DocNote, { entry });
270
+ case "tag":
271
+ return /* @__PURE__ */ jsx13(DocTag, { entry });
272
+ case "kv":
273
+ return /* @__PURE__ */ jsx13(DocKv, { entry });
274
+ case "code":
275
+ return /* @__PURE__ */ jsx13(DocCode, { entry });
276
+ case "table":
277
+ return /* @__PURE__ */ jsx13(DocTable, { entry });
278
+ case "link":
279
+ return /* @__PURE__ */ jsx13(DocLink, { entry });
280
+ case "section":
281
+ return /* @__PURE__ */ jsx13(DocSection, { entry });
282
+ case "mermaid":
283
+ return /* @__PURE__ */ jsx13(DocMermaid, { entry });
284
+ case "screenshot":
285
+ return /* @__PURE__ */ jsx13(DocScreenshot, { entry });
286
+ case "custom":
287
+ return /* @__PURE__ */ jsx13(DocCustom, { entry });
288
+ }
289
+ }
290
+
291
+ // src/components/ReportDocEntries.tsx
292
+ import { Fragment as Fragment6, jsx as jsx14 } from "react/jsx-runtime";
293
+ function ReportDocEntries({ entries }) {
294
+ if (entries.length === 0) return null;
295
+ return /* @__PURE__ */ jsx14(Fragment6, { children: entries.map((entry, i) => /* @__PURE__ */ jsx14(DocEntry, { entry }, i)) });
296
+ }
297
+
298
+ // src/components/ReportSteps.tsx
299
+ import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
300
+ function ReportSteps({ scenario }) {
301
+ if (scenario.steps.length === 0) return null;
302
+ return /* @__PURE__ */ jsx15("ol", { className: "es-steps", children: scenario.steps.map((step) => /* @__PURE__ */ jsx15(ReportStepItem, { step }, step.id)) });
303
+ }
304
+ function ReportStepItem({ step }) {
305
+ return /* @__PURE__ */ jsxs9(
306
+ "li",
307
+ {
308
+ id: step.id,
309
+ className: `es-step es-step-${step.status}`,
310
+ "data-status": step.status,
311
+ children: [
312
+ /* @__PURE__ */ jsx15("span", { className: "es-step-keyword", children: step.keyword }),
313
+ /* @__PURE__ */ jsx15("span", { className: "es-step-text", children: step.text }),
314
+ step.errorMessage ? /* @__PURE__ */ jsx15("pre", { className: "es-scenario-error", role: "alert", children: step.errorMessage }) : null,
315
+ step.docEntries.length > 0 ? /* @__PURE__ */ jsx15("div", { className: "es-step-docs", children: /* @__PURE__ */ jsx15(ReportDocEntries, { entries: step.docEntries }) }) : null
316
+ ]
317
+ }
318
+ );
319
+ }
320
+
321
+ // src/components/ReportScenario.tsx
322
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
323
+ var STATUS_LABEL = {
324
+ passed: "Passed",
325
+ failed: "Failed",
326
+ skipped: "Skipped",
327
+ pending: "Pending"
328
+ };
329
+ function ReportScenario({ scenario }) {
330
+ const titleId = `${scenario.id}-title`;
331
+ return /* @__PURE__ */ jsxs10(
332
+ "article",
333
+ {
334
+ id: scenario.id,
335
+ className: `es-scenario es-status-${scenario.status}`,
336
+ "aria-labelledby": titleId,
337
+ "data-status": scenario.status,
338
+ children: [
339
+ /* @__PURE__ */ jsxs10("h3", { id: titleId, className: "es-scenario-title", children: [
340
+ /* @__PURE__ */ jsx16("span", { children: scenario.title }),
341
+ /* @__PURE__ */ jsx16("span", { className: "es-scenario-status", "aria-label": `Status: ${STATUS_LABEL[scenario.status]}`, children: STATUS_LABEL[scenario.status] })
342
+ ] }),
343
+ scenario.tags.length > 0 ? /* @__PURE__ */ jsx16("ul", { className: "es-tags", "aria-label": "Tags", children: scenario.tags.map((t) => /* @__PURE__ */ jsx16("li", { children: t }, t)) }) : null,
344
+ scenario.errorMessage ? /* @__PURE__ */ jsx16("pre", { className: "es-scenario-error", role: "alert", children: scenario.errorMessage }) : null,
345
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx16("div", { className: "es-scenario-docs", children: /* @__PURE__ */ jsx16(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
346
+ /* @__PURE__ */ jsx16(ReportSteps, { scenario })
347
+ ]
348
+ }
349
+ );
350
+ }
351
+
352
+ // src/components/ReportScenarioList.tsx
353
+ import { Fragment as Fragment7, jsx as jsx17 } from "react/jsx-runtime";
354
+ function ReportScenarioList({ feature }) {
355
+ return /* @__PURE__ */ jsx17(Fragment7, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx17(ReportScenario, { scenario }, scenario.id)) });
356
+ }
357
+
358
+ // src/components/ReportFeature.tsx
359
+ import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
360
+ function ReportFeature({ feature }) {
361
+ const titleId = `${feature.id}-title`;
362
+ return /* @__PURE__ */ jsxs11(
363
+ "section",
364
+ {
365
+ id: feature.id,
366
+ className: "es-feature",
367
+ "aria-labelledby": titleId,
368
+ children: [
369
+ /* @__PURE__ */ jsx18("h2", { id: titleId, className: "es-feature-title", children: feature.title }),
370
+ /* @__PURE__ */ jsx18("p", { className: "es-feature-source", children: feature.sourceFile }),
371
+ /* @__PURE__ */ jsx18(ReportSummaryView, { summary: feature.summary, className: "es-feature-summary" }),
372
+ /* @__PURE__ */ jsx18(ReportScenarioList, { feature })
373
+ ]
374
+ }
375
+ );
376
+ }
377
+
378
+ // src/components/ReportFeatureList.tsx
379
+ import { Fragment as Fragment8, jsx as jsx19 } from "react/jsx-runtime";
380
+ function ReportFeatureList() {
381
+ const report = useReport();
382
+ return /* @__PURE__ */ jsx19(Fragment8, { children: report.features.map((feature) => /* @__PURE__ */ jsx19(ReportFeature, { feature }, feature.id)) });
383
+ }
384
+
385
+ // src/components/ReportEmpty.tsx
386
+ import { jsx as jsx20 } from "react/jsx-runtime";
387
+ function ReportEmpty({ message }) {
388
+ return /* @__PURE__ */ jsx20("section", { className: "es-empty", "aria-live": "polite", children: /* @__PURE__ */ jsx20("p", { children: message ?? "No scenarios in this report." }) });
389
+ }
390
+
391
+ // src/components/ReportSchemaError.tsx
392
+ import { jsx as jsx21, jsxs as jsxs12 } from "react/jsx-runtime";
393
+ function ReportSchemaError({ error }) {
394
+ return /* @__PURE__ */ jsxs12("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
395
+ /* @__PURE__ */ jsx21("p", { children: /* @__PURE__ */ jsx21("strong", { children: "Report could not be displayed." }) }),
396
+ /* @__PURE__ */ jsx21("p", { children: error.message }),
397
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs12("p", { children: [
398
+ "The report bundle is newer than this version of ",
399
+ /* @__PURE__ */ jsx21("code", { children: "executable-stories-react" }),
400
+ ". Upgrade the package, or regenerate the report with an older formatters CLI."
401
+ ] }) : null,
402
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs12("details", { children: [
403
+ /* @__PURE__ */ jsxs12("summary", { children: [
404
+ error.issues.length,
405
+ " validation issue",
406
+ error.issues.length === 1 ? "" : "s"
407
+ ] }),
408
+ /* @__PURE__ */ jsx21("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
409
+ ] }) : null
410
+ ] });
411
+ }
412
+
413
+ // src/interactive/ReportSearch.tsx
414
+ import {
415
+ forwardRef,
416
+ useId
417
+ } from "react";
418
+ import { jsx as jsx22, jsxs as jsxs13 } from "react/jsx-runtime";
419
+ var ReportSearch = forwardRef(
420
+ function ReportSearch2(props, ref) {
421
+ const {
422
+ value,
423
+ onChange,
424
+ matchedCount,
425
+ totalCount,
426
+ placeholder = "Search scenarios, tags, or step text\u2026",
427
+ className
428
+ } = props;
429
+ const inputId = useId();
430
+ const showCounts = typeof matchedCount === "number" && typeof totalCount === "number";
431
+ function handleChange(e) {
432
+ onChange(e.target.value);
433
+ }
434
+ function handleKeyDown(e) {
435
+ if (e.key === "Escape" && value !== "") {
436
+ onChange("");
437
+ e.preventDefault();
438
+ }
439
+ }
440
+ return /* @__PURE__ */ jsxs13("div", { className: ["es-search", className].filter(Boolean).join(" "), children: [
441
+ /* @__PURE__ */ jsx22("label", { htmlFor: inputId, className: "es-search-label", children: "Search" }),
442
+ /* @__PURE__ */ jsx22(
443
+ "input",
444
+ {
445
+ ref,
446
+ id: inputId,
447
+ type: "search",
448
+ value,
449
+ onChange: handleChange,
450
+ onKeyDown: handleKeyDown,
451
+ placeholder,
452
+ autoComplete: "off",
453
+ spellCheck: false,
454
+ "aria-keyshortcuts": "/"
455
+ }
456
+ ),
457
+ showCounts ? /* @__PURE__ */ jsx22("span", { className: "es-search-counts", "aria-live": "polite", children: value ? `${matchedCount} of ${totalCount}` : `${totalCount} total` }) : null
458
+ ] });
459
+ }
460
+ );
461
+
462
+ // src/interactive/ReportFailureBanner.tsx
463
+ import { useCallback } from "react";
464
+ import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
465
+ function ReportFailureBanner({ failures }) {
466
+ const first = failures[0];
467
+ const jumpToFirst = useCallback(() => {
468
+ if (!first) return;
469
+ if (typeof window === "undefined") return;
470
+ const el = document.getElementById(first.scenarioId);
471
+ el?.scrollIntoView({ behavior: "smooth", block: "start" });
472
+ if (typeof history !== "undefined") {
473
+ history.replaceState(null, "", `#${first.scenarioId}`);
474
+ }
475
+ }, [first]);
476
+ if (failures.length === 0) return null;
477
+ return /* @__PURE__ */ jsxs14(
478
+ "aside",
479
+ {
480
+ className: "es-failure-banner",
481
+ role: "status",
482
+ "aria-live": "polite",
483
+ "aria-label": "Failure summary",
484
+ children: [
485
+ /* @__PURE__ */ jsxs14("span", { className: "es-failure-banner-text", children: [
486
+ /* @__PURE__ */ jsx23("strong", { children: failures.length }),
487
+ " ",
488
+ "failure",
489
+ failures.length === 1 ? "" : "s"
490
+ ] }),
491
+ /* @__PURE__ */ jsx23(
492
+ "button",
493
+ {
494
+ type: "button",
495
+ className: "es-failure-banner-jump",
496
+ onClick: jumpToFirst,
497
+ "aria-label": "Jump to first failure",
498
+ children: "Jump to first \u2193"
499
+ }
500
+ )
501
+ ]
502
+ }
503
+ );
504
+ }
505
+
506
+ // src/interactive/ReportShortcutsHelp.tsx
507
+ import { useEffect, useRef } from "react";
508
+ import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
509
+ var SHORTCUTS = [
510
+ { keys: "/", description: "Focus search" },
511
+ { keys: "f", description: "Jump to next failure" },
512
+ { keys: "Shift+F", description: "Jump to previous failure" },
513
+ { keys: "Esc", description: "Clear search / close dialog" },
514
+ { keys: "?", description: "Toggle this help" }
515
+ ];
516
+ function ReportShortcutsHelp({ open, onClose }) {
517
+ const dialogRef = useRef(null);
518
+ useEffect(() => {
519
+ const dialog = dialogRef.current;
520
+ if (!dialog) return;
521
+ if (open && !dialog.open) dialog.showModal();
522
+ else if (!open && dialog.open) dialog.close();
523
+ }, [open]);
524
+ return /* @__PURE__ */ jsxs15(
525
+ "dialog",
526
+ {
527
+ ref: dialogRef,
528
+ className: "es-shortcuts-help",
529
+ "aria-label": "Keyboard shortcuts",
530
+ onClose,
531
+ children: [
532
+ /* @__PURE__ */ jsx24("h2", { children: "Keyboard shortcuts" }),
533
+ /* @__PURE__ */ jsx24("dl", { children: SHORTCUTS.map((s) => /* @__PURE__ */ jsxs15("div", { children: [
534
+ /* @__PURE__ */ jsx24("dt", { children: /* @__PURE__ */ jsx24("kbd", { children: s.keys }) }),
535
+ /* @__PURE__ */ jsx24("dd", { children: s.description })
536
+ ] }, s.keys)) }),
537
+ /* @__PURE__ */ jsx24("form", { method: "dialog", children: /* @__PURE__ */ jsx24("button", { type: "submit", className: "es-shortcuts-close", children: "Close" }) })
538
+ ]
539
+ }
540
+ );
541
+ }
542
+
543
+ // src/interactive/use-keyboard-shortcuts.ts
544
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
545
+ function isEditableTarget(target) {
546
+ if (!(target instanceof HTMLElement)) return false;
547
+ const tag = target.tagName;
548
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
549
+ if (target.isContentEditable) return true;
550
+ return false;
551
+ }
552
+ function useKeyboardShortcuts(handlers) {
553
+ const ref = useRef2(handlers);
554
+ ref.current = handlers;
555
+ useEffect2(() => {
556
+ function onKeyDown(e) {
557
+ const h = ref.current;
558
+ if (e.key === "Escape") {
559
+ h.onEscape?.();
560
+ return;
561
+ }
562
+ if (isEditableTarget(e.target)) return;
563
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
564
+ switch (e.key) {
565
+ case "/":
566
+ h.onFocusSearch?.();
567
+ e.preventDefault();
568
+ return;
569
+ case "?":
570
+ h.onToggleHelp?.();
571
+ e.preventDefault();
572
+ return;
573
+ case "f":
574
+ h.onNextFailure?.();
575
+ e.preventDefault();
576
+ return;
577
+ case "F":
578
+ h.onPrevFailure?.();
579
+ e.preventDefault();
580
+ return;
581
+ default:
582
+ return;
583
+ }
584
+ }
585
+ window.addEventListener("keydown", onKeyDown);
586
+ return () => window.removeEventListener("keydown", onKeyDown);
587
+ }, []);
588
+ }
589
+
590
+ // src/interactive/use-deep-link-scroll.ts
591
+ import { useEffect as useEffect3 } from "react";
592
+ function useDeepLinkScroll() {
593
+ useEffect3(() => {
594
+ function scrollToHash() {
595
+ const hash = window.location.hash.replace(/^#/, "");
596
+ if (!hash) return;
597
+ const el = document.getElementById(hash);
598
+ if (!el) return;
599
+ requestAnimationFrame(() => {
600
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
601
+ });
602
+ }
603
+ scrollToHash();
604
+ window.addEventListener("hashchange", scrollToHash);
605
+ return () => window.removeEventListener("hashchange", scrollToHash);
606
+ }, []);
607
+ }
608
+
609
+ // src/interactive/filter.ts
610
+ function normalizeQuery(query) {
611
+ return query.trim().toLowerCase();
612
+ }
613
+ function scenarioMatches(scenario, q) {
614
+ if (q === "") return true;
615
+ if (scenario.title.toLowerCase().includes(q)) return true;
616
+ for (const tag of scenario.tags) {
617
+ if (tag.toLowerCase().includes(q)) return true;
618
+ }
619
+ for (const step of scenario.steps) {
620
+ if (step.text.toLowerCase().includes(q)) return true;
621
+ }
622
+ return false;
623
+ }
624
+ function summarizeScenarios(scenarios) {
625
+ let total = 0, passed = 0, failed = 0, skipped = 0, pending = 0, durationMs = 0;
626
+ for (const s of scenarios) {
627
+ total += 1;
628
+ durationMs += s.durationMs;
629
+ if (s.status === "passed") passed += 1;
630
+ else if (s.status === "failed") failed += 1;
631
+ else if (s.status === "skipped") skipped += 1;
632
+ else pending += 1;
633
+ }
634
+ return { total, passed, failed, skipped, pending, durationMs };
635
+ }
636
+ function filterReport(report, query) {
637
+ const q = normalizeQuery(query);
638
+ if (q === "") return report;
639
+ const features = [];
640
+ let topTotal = 0, topPassed = 0, topFailed = 0, topSkipped = 0, topPending = 0, topDuration = 0;
641
+ for (const feature of report.features) {
642
+ const matched = feature.scenarios.filter((s) => scenarioMatches(s, q));
643
+ if (matched.length === 0) continue;
644
+ const summary = summarizeScenarios(matched);
645
+ features.push({ ...feature, summary, scenarios: matched });
646
+ topTotal += summary.total;
647
+ topPassed += summary.passed;
648
+ topFailed += summary.failed;
649
+ topSkipped += summary.skipped;
650
+ topPending += summary.pending;
651
+ topDuration += summary.durationMs;
652
+ }
653
+ return {
654
+ ...report,
655
+ summary: {
656
+ total: topTotal,
657
+ passed: topPassed,
658
+ failed: topFailed,
659
+ skipped: topSkipped,
660
+ pending: topPending,
661
+ durationMs: topDuration
662
+ },
663
+ features
664
+ };
665
+ }
666
+ function listFailures(report) {
667
+ const out = [];
668
+ for (const feature of report.features) {
669
+ for (const scenario of feature.scenarios) {
670
+ if (scenario.status === "failed") {
671
+ const ref = {
672
+ featureId: feature.id,
673
+ scenarioId: scenario.id,
674
+ scenarioTitle: scenario.title
675
+ };
676
+ if (scenario.errorMessage !== void 0) {
677
+ ref.errorMessage = scenario.errorMessage;
678
+ }
679
+ out.push(ref);
680
+ }
681
+ }
682
+ }
683
+ return out;
684
+ }
685
+
686
+ // src/interactive/ReportInteractive.tsx
687
+ import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
688
+ function isResult(value) {
689
+ return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean";
690
+ }
691
+ function ReportInteractive(props) {
692
+ const { report, className, title, dataTheme } = props;
693
+ if (isResult(report)) {
694
+ if (!report.ok) {
695
+ return /* @__PURE__ */ jsx25(
696
+ "main",
697
+ {
698
+ className: ["es-report", className].filter(Boolean).join(" "),
699
+ "aria-label": title ?? "Test report",
700
+ "data-theme": dataTheme,
701
+ children: /* @__PURE__ */ jsx25(ReportSchemaError, { error: report.error })
702
+ }
703
+ );
704
+ }
705
+ return /* @__PURE__ */ jsx25(ReportInteractiveView, { ...props, report: report.data });
706
+ }
707
+ return /* @__PURE__ */ jsx25(ReportInteractiveView, { ...props, report });
708
+ }
709
+ function ReportInteractiveView({
710
+ report,
711
+ customRenderers,
712
+ renderers,
713
+ className,
714
+ title,
715
+ dataTheme
716
+ }) {
717
+ const [query, setQuery] = useState("");
718
+ const [helpOpen, setHelpOpen] = useState(false);
719
+ const searchRef = useRef3(null);
720
+ const failures = useMemo2(() => listFailures(report), [report]);
721
+ const filtered = useMemo2(() => filterReport(report, query), [report, query]);
722
+ const failureIndexRef = useRef3(0);
723
+ const focusSearch = useCallback2(() => {
724
+ searchRef.current?.focus();
725
+ }, []);
726
+ const scrollToScenario = useCallback2((scenarioId) => {
727
+ if (typeof document === "undefined") return;
728
+ const el = document.getElementById(scenarioId);
729
+ if (!el) return;
730
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
731
+ if (typeof history !== "undefined") {
732
+ history.replaceState(null, "", `#${scenarioId}`);
733
+ }
734
+ }, []);
735
+ const stepFailure = useCallback2(
736
+ (direction) => {
737
+ if (failures.length === 0) return;
738
+ failureIndexRef.current = (failureIndexRef.current + direction + failures.length) % failures.length;
739
+ const target = failures[failureIndexRef.current];
740
+ if (target) scrollToScenario(target.scenarioId);
741
+ },
742
+ [failures, scrollToScenario]
743
+ );
744
+ const toggleHelp = useCallback2(() => {
745
+ setHelpOpen((v) => !v);
746
+ }, []);
747
+ const escape = useCallback2(() => {
748
+ if (helpOpen) setHelpOpen(false);
749
+ else if (query !== "") setQuery("");
750
+ }, [helpOpen, query]);
751
+ useKeyboardShortcuts({
752
+ onFocusSearch: focusSearch,
753
+ onNextFailure: () => stepFailure(1),
754
+ onPrevFailure: () => stepFailure(-1),
755
+ onToggleHelp: toggleHelp,
756
+ onEscape: escape
757
+ });
758
+ useDeepLinkScroll();
759
+ const hasContent = filtered.features.length > 0;
760
+ const totalScenarios = report.summary.total;
761
+ const matchedScenarios = filtered.summary.total;
762
+ return /* @__PURE__ */ jsx25(
763
+ ReportRoot,
764
+ {
765
+ report: filtered,
766
+ customRenderers,
767
+ renderers,
768
+ children: /* @__PURE__ */ jsxs16(
769
+ "main",
770
+ {
771
+ className: ["es-report", "es-report-interactive", className].filter(Boolean).join(" "),
772
+ "aria-label": title ?? "Test report",
773
+ "data-theme": dataTheme,
774
+ children: [
775
+ /* @__PURE__ */ jsxs16("header", { className: "es-report-header", children: [
776
+ /* @__PURE__ */ jsx25("h1", { children: title ?? "Story Report" }),
777
+ /* @__PURE__ */ jsx25(ReportSummary, {}),
778
+ /* @__PURE__ */ jsx25(
779
+ ReportSearch,
780
+ {
781
+ ref: searchRef,
782
+ value: query,
783
+ onChange: setQuery,
784
+ matchedCount: matchedScenarios,
785
+ totalCount: totalScenarios
786
+ }
787
+ )
788
+ ] }),
789
+ /* @__PURE__ */ jsx25(ReportFailureBanner, { failures }),
790
+ hasContent ? /* @__PURE__ */ jsx25(ReportFeatureList, {}) : /* @__PURE__ */ jsx25(ReportEmpty, { message: query ? "No scenarios match the search." : void 0 }),
791
+ /* @__PURE__ */ jsx25(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ className: "es-shortcuts-trigger",
796
+ "aria-label": "Keyboard shortcuts",
797
+ "aria-keyshortcuts": "Shift+?",
798
+ onClick: toggleHelp,
799
+ children: "?"
800
+ }
801
+ ),
802
+ /* @__PURE__ */ jsx25(ReportShortcutsHelp, { open: helpOpen, onClose: () => setHelpOpen(false) })
803
+ ]
804
+ }
805
+ )
806
+ }
807
+ );
808
+ }
809
+ export {
810
+ ReportFailureBanner,
811
+ ReportInteractive,
812
+ ReportSearch,
813
+ ReportShortcutsHelp,
814
+ filterReport,
815
+ listFailures,
816
+ normalizeQuery,
817
+ useDeepLinkScroll,
818
+ useKeyboardShortcuts
819
+ };
820
+ //# sourceMappingURL=interactive.js.map