executable-stories-react 0.8.0 → 0.9.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.
@@ -47,7 +47,7 @@ __export(interactive_exports, {
47
47
  module.exports = __toCommonJS(interactive_exports);
48
48
 
49
49
  // src/interactive/ReportInteractive.tsx
50
- var import_react18 = require("react");
50
+ var import_react19 = require("react");
51
51
 
52
52
  // src/result.ts
53
53
  var ok = (data) => ({ ok: true, data });
@@ -1012,11 +1012,11 @@ function flakinessOf(entries) {
1012
1012
  if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
1013
1013
  return "stable";
1014
1014
  }
1015
- function diffSinceLastRun(history2, scenarios) {
1015
+ function diffSinceLastRun(history, scenarios) {
1016
1016
  let hasPreviousRun = false;
1017
1017
  const delta = { newlyFailing: [], newlyPassing: [], added: [] };
1018
1018
  for (const scenario of scenarios) {
1019
- const entries = history2[scenario.id];
1019
+ const entries = history[scenario.id];
1020
1020
  if (!entries || entries.length === 0) continue;
1021
1021
  const last = entries[entries.length - 1];
1022
1022
  const prev = entries[entries.length - 2];
@@ -1046,8 +1046,8 @@ function dotTitle(e) {
1046
1046
  return parts.join(" \xB7 ");
1047
1047
  }
1048
1048
  function ScenarioRunHistory({ scenarioId }) {
1049
- const history2 = useScenarioHistory();
1050
- const entries = history2?.[scenarioId];
1049
+ const history = useScenarioHistory();
1050
+ const entries = history?.[scenarioId];
1051
1051
  if (!entries || entries.length < 2) return null;
1052
1052
  const recent = entries.slice(-MAX_DOTS);
1053
1053
  const flaky = flakinessOf(recent) === "flaky";
@@ -2011,6 +2011,54 @@ var ReportSearch = (0, import_react8.forwardRef)(
2011
2011
  // src/interactive/ReportFailureBanner.tsx
2012
2012
  var import_react9 = require("react");
2013
2013
 
2014
+ // src/lib/hash-state.ts
2015
+ var DEFAULT_URL_STATE = {
2016
+ query: "",
2017
+ status: "all",
2018
+ tags: [],
2019
+ detail: "full"
2020
+ };
2021
+ var STATUSES = ["all", "passed", "failed", "skipped", "pending"];
2022
+ function splitHash(hash) {
2023
+ const raw = hash.replace(/^#/, "");
2024
+ const split = raw.indexOf("?");
2025
+ if (split === -1) return { anchor: raw, params: new URLSearchParams() };
2026
+ return { anchor: raw.slice(0, split), params: new URLSearchParams(raw.slice(split + 1)) };
2027
+ }
2028
+ function decodeUrlState(params) {
2029
+ const status = params.get("status");
2030
+ const tags = params.get("tags");
2031
+ return {
2032
+ query: params.get("q") ?? DEFAULT_URL_STATE.query,
2033
+ status: STATUSES.includes(status) ? status : DEFAULT_URL_STATE.status,
2034
+ tags: tags ? tags.split(",").filter(Boolean) : [],
2035
+ detail: params.get("docs") === "0" ? "minimal" : "full"
2036
+ };
2037
+ }
2038
+ function encodeUrlState(state) {
2039
+ const params = new URLSearchParams();
2040
+ if (state.query) params.set("q", state.query);
2041
+ if (state.status !== "all") params.set("status", state.status);
2042
+ if (state.tags.length > 0) params.set("tags", state.tags.join(","));
2043
+ if (state.detail === "minimal") params.set("docs", "0");
2044
+ return params.toString();
2045
+ }
2046
+ function formatHash(anchor, params) {
2047
+ if (params) return `#${anchor}?${params}`;
2048
+ return anchor ? `#${anchor}` : "";
2049
+ }
2050
+ function writeHash(anchor, params) {
2051
+ if (typeof window === "undefined") return;
2052
+ const next = formatHash(anchor, params);
2053
+ if (next === window.location.hash) return;
2054
+ const url = next || window.location.href.split("#")[0];
2055
+ try {
2056
+ window.history.replaceState(window.history.state, "", url);
2057
+ } catch {
2058
+ window.location.hash = next.slice(1);
2059
+ }
2060
+ }
2061
+
2014
2062
  // src/lib/scroll.ts
2015
2063
  function prefersReducedMotion() {
2016
2064
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
@@ -2026,8 +2074,8 @@ function scrollToScenarioId(scenarioId, options = {}) {
2026
2074
  behavior: prefersReducedMotion() ? "auto" : "smooth",
2027
2075
  block: "start"
2028
2076
  });
2029
- if (options.updateHash !== false && typeof history !== "undefined") {
2030
- history.replaceState(null, "", `#${scenarioId}`);
2077
+ if (options.updateHash !== false) {
2078
+ writeHash(scenarioId, splitHash(window.location.hash).params.toString());
2031
2079
  }
2032
2080
  }
2033
2081
 
@@ -2129,12 +2177,12 @@ function ScenarioLinks({ scenarios, tone }) {
2129
2177
  ] });
2130
2178
  }
2131
2179
  function ReportLastRunDelta({
2132
- history: history2,
2180
+ history,
2133
2181
  report
2134
2182
  }) {
2135
- if (!history2) return null;
2183
+ if (!history) return null;
2136
2184
  const scenarios = report.features.flatMap((f) => f.scenarios.map((s) => ({ id: s.id, title: s.title })));
2137
- const delta = diffSinceLastRun(history2, scenarios);
2185
+ const delta = diffSinceLastRun(history, scenarios);
2138
2186
  if (!delta) return null;
2139
2187
  const { newlyFailing, newlyPassing, added } = delta;
2140
2188
  if (newlyFailing.length === 0 && newlyPassing.length === 0 && added.length === 0) {
@@ -2320,12 +2368,14 @@ function useKeyboardShortcuts(handlers) {
2320
2368
  // src/interactive/use-deep-link-scroll.ts
2321
2369
  var import_react13 = require("react");
2322
2370
  function useDeepLinkScroll() {
2371
+ const lastAnchor = (0, import_react13.useRef)(null);
2323
2372
  (0, import_react13.useEffect)(() => {
2324
2373
  function scrollToHash() {
2325
- const hash = window.location.hash.replace(/^#/, "");
2326
- if (!hash) return;
2374
+ const { anchor } = splitHash(window.location.hash);
2375
+ if (!anchor || anchor === lastAnchor.current) return;
2376
+ lastAnchor.current = anchor;
2327
2377
  requestAnimationFrame(() => {
2328
- scrollToScenarioId(hash, { updateHash: false });
2378
+ scrollToScenarioId(anchor, { updateHash: false });
2329
2379
  });
2330
2380
  }
2331
2381
  scrollToHash();
@@ -2691,6 +2741,34 @@ function useTheme() {
2691
2741
  return { pref, setPref };
2692
2742
  }
2693
2743
 
2744
+ // src/interactive/use-url-state.ts
2745
+ var import_react18 = require("react");
2746
+ var WRITE_DELAY_MS = 250;
2747
+ function useUrlState() {
2748
+ const [state, setState] = (0, import_react18.useState)(DEFAULT_URL_STATE);
2749
+ const ready = (0, import_react18.useRef)(false);
2750
+ (0, import_react18.useEffect)(() => {
2751
+ const read = () => setState(decodeUrlState(splitHash(window.location.hash).params));
2752
+ read();
2753
+ ready.current = true;
2754
+ window.addEventListener("hashchange", read);
2755
+ return () => window.removeEventListener("hashchange", read);
2756
+ }, []);
2757
+ (0, import_react18.useEffect)(() => {
2758
+ if (!ready.current) return;
2759
+ const timer = setTimeout(
2760
+ () => writeHash(splitHash(window.location.hash).anchor, encodeUrlState(state)),
2761
+ WRITE_DELAY_MS
2762
+ );
2763
+ return () => clearTimeout(timer);
2764
+ }, [state]);
2765
+ const update = (0, import_react18.useCallback)(
2766
+ (patch) => setState((prev) => ({ ...prev, ...patch })),
2767
+ []
2768
+ );
2769
+ return [state, update];
2770
+ }
2771
+
2694
2772
  // src/interactive/ReportInteractive.tsx
2695
2773
  var import_jsx_runtime46 = require("react/jsx-runtime");
2696
2774
  function ReportInteractive(props) {
@@ -2733,25 +2811,32 @@ function ReportInteractiveView({
2733
2811
  staleAfterDays = 7,
2734
2812
  scenarioHistory
2735
2813
  }) {
2736
- const [query, setQuery] = (0, import_react18.useState)("");
2737
- const [statusFilter, setStatusFilter] = (0, import_react18.useState)("all");
2738
- const [activeTags, setActiveTags] = (0, import_react18.useState)([]);
2739
- const [detail, setDetail] = (0, import_react18.useState)("full");
2740
- const [helpOpen, setHelpOpen] = (0, import_react18.useState)(false);
2814
+ const [urlState, setUrlState] = useUrlState();
2815
+ const { query, status: statusFilter, tags: activeTags, detail } = urlState;
2816
+ const setQuery = (0, import_react19.useCallback)((next) => setUrlState({ query: next }), [setUrlState]);
2817
+ const setStatusFilter = (0, import_react19.useCallback)(
2818
+ (next) => setUrlState({ status: next }),
2819
+ [setUrlState]
2820
+ );
2821
+ const setDetail = (0, import_react19.useCallback)(
2822
+ (next) => setUrlState({ detail: next }),
2823
+ [setUrlState]
2824
+ );
2825
+ const [helpOpen, setHelpOpen] = (0, import_react19.useState)(false);
2741
2826
  const { pref: themePref, setPref: setThemePref } = useTheme();
2742
- const searchRef = (0, import_react18.useRef)(null);
2743
- const expandId = (0, import_react18.useId)();
2744
- const docsId = (0, import_react18.useId)();
2745
- const deferredQuery = (0, import_react18.useDeferredValue)(query);
2746
- const failures = (0, import_react18.useMemo)(() => listFailures(report), [report]);
2747
- const filtered = (0, import_react18.useMemo)(
2827
+ const searchRef = (0, import_react19.useRef)(null);
2828
+ const expandId = (0, import_react19.useId)();
2829
+ const docsId = (0, import_react19.useId)();
2830
+ const deferredQuery = (0, import_react19.useDeferredValue)(query);
2831
+ const failures = (0, import_react19.useMemo)(() => listFailures(report), [report]);
2832
+ const filtered = (0, import_react19.useMemo)(
2748
2833
  // Features carrying failures float to the top so the first thing on screen
2749
2834
  // is the thing that needs attention; an all-green run keeps source order.
2750
2835
  () => failuresFirst(filterReport(report, { query: deferredQuery, status: statusFilter, tags: activeTags })),
2751
2836
  [report, deferredQuery, statusFilter, activeTags]
2752
2837
  );
2753
2838
  const isFiltering = query !== deferredQuery;
2754
- const statusOptions = (0, import_react18.useMemo)(() => {
2839
+ const statusOptions = (0, import_react19.useMemo)(() => {
2755
2840
  const s = report.summary;
2756
2841
  const opts = [
2757
2842
  { key: "all", label: "All", count: s.total }
@@ -2762,19 +2847,21 @@ function ReportInteractiveView({
2762
2847
  if (s.pending) opts.push({ key: "pending", label: "Pending", count: s.pending });
2763
2848
  return opts;
2764
2849
  }, [report]);
2765
- const tagOptions = (0, import_react18.useMemo)(() => allTags(report), [report]);
2766
- const toggleTag = (0, import_react18.useCallback)(
2767
- (tag) => setActiveTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]),
2768
- []
2850
+ const tagOptions = (0, import_react19.useMemo)(() => allTags(report), [report]);
2851
+ const toggleTag = (0, import_react19.useCallback)(
2852
+ (tag) => setUrlState({
2853
+ tags: activeTags.includes(tag) ? activeTags.filter((t) => t !== tag) : [...activeTags, tag]
2854
+ }),
2855
+ [activeTags, setUrlState]
2769
2856
  );
2770
- const [toast, setToast] = (0, import_react18.useState)(null);
2771
- const toastTimer = (0, import_react18.useRef)(null);
2772
- const showToast = (0, import_react18.useCallback)((msg) => {
2857
+ const [toast, setToast] = (0, import_react19.useState)(null);
2858
+ const toastTimer = (0, import_react19.useRef)(null);
2859
+ const showToast = (0, import_react19.useCallback)((msg) => {
2773
2860
  setToast(msg);
2774
2861
  if (toastTimer.current) clearTimeout(toastTimer.current);
2775
2862
  toastTimer.current = setTimeout(() => setToast(null), 1800);
2776
2863
  }, []);
2777
- const copy = (0, import_react18.useCallback)(
2864
+ const copy = (0, import_react19.useCallback)(
2778
2865
  (text, msg) => {
2779
2866
  if (typeof navigator !== "undefined" && navigator.clipboard) {
2780
2867
  navigator.clipboard.writeText(text).then(() => showToast(msg), () => showToast("Copy failed"));
@@ -2782,7 +2869,7 @@ function ReportInteractiveView({
2782
2869
  },
2783
2870
  [showToast]
2784
2871
  );
2785
- const scenarioActions = (0, import_react18.useMemo)(
2872
+ const scenarioActions = (0, import_react19.useMemo)(
2786
2873
  () => ({
2787
2874
  copyLink: (s) => copy(scenarioPermalink(s), "Link copied"),
2788
2875
  copyMarkdown: (s) => copy(scenarioToMarkdown2(s), "Markdown copied"),
@@ -2790,15 +2877,15 @@ function ReportInteractiveView({
2790
2877
  }),
2791
2878
  [copy]
2792
2879
  );
2793
- const failureIndexRef = (0, import_react18.useRef)(0);
2794
- const allCollapsibleIds = (0, import_react18.useMemo)(
2880
+ const failureIndexRef = (0, import_react19.useRef)(0);
2881
+ const allCollapsibleIds = (0, import_react19.useMemo)(
2795
2882
  () => report.features.flatMap((f) => [f.id, ...f.scenarios.map((s) => s.id)]),
2796
2883
  [report]
2797
2884
  );
2798
- const collapse = useCollapseState((0, import_react18.useCallback)(() => defaultCollapsedIds(report), [report]));
2799
- const collapseAll = (0, import_react18.useCallback)(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2800
- const [expandedAll, setExpandedAll] = (0, import_react18.useState)(() => report.summary.failed === 0);
2801
- const setExpanded = (0, import_react18.useCallback)(
2885
+ const collapse = useCollapseState((0, import_react19.useCallback)(() => defaultCollapsedIds(report), [report]));
2886
+ const collapseAll = (0, import_react19.useCallback)(() => collapse.collapseAll(allCollapsibleIds), [collapse, allCollapsibleIds]);
2887
+ const [expandedAll, setExpandedAll] = (0, import_react19.useState)(() => report.summary.failed === 0);
2888
+ const setExpanded = (0, import_react19.useCallback)(
2802
2889
  (expanded) => {
2803
2890
  if (expanded) collapse.expandAll();
2804
2891
  else collapseAll();
@@ -2806,10 +2893,10 @@ function ReportInteractiveView({
2806
2893
  },
2807
2894
  [collapse, collapseAll]
2808
2895
  );
2809
- const focusSearch = (0, import_react18.useCallback)(() => {
2896
+ const focusSearch = (0, import_react19.useCallback)(() => {
2810
2897
  searchRef.current?.focus();
2811
2898
  }, []);
2812
- const stepFailure = (0, import_react18.useCallback)(
2899
+ const stepFailure = (0, import_react19.useCallback)(
2813
2900
  (direction) => {
2814
2901
  if (failures.length === 0) return;
2815
2902
  failureIndexRef.current = (failureIndexRef.current + direction + failures.length) % failures.length;
@@ -2818,10 +2905,10 @@ function ReportInteractiveView({
2818
2905
  },
2819
2906
  [failures]
2820
2907
  );
2821
- const toggleHelp = (0, import_react18.useCallback)(() => {
2908
+ const toggleHelp = (0, import_react19.useCallback)(() => {
2822
2909
  setHelpOpen((v) => !v);
2823
2910
  }, []);
2824
- const escape = (0, import_react18.useCallback)(() => {
2911
+ const escape = (0, import_react19.useCallback)(() => {
2825
2912
  if (helpOpen) setHelpOpen(false);
2826
2913
  else if (query !== "") setQuery("");
2827
2914
  }, [helpOpen, query]);
@@ -2958,13 +3045,13 @@ function ReportInteractiveView({
2958
3045
  }
2959
3046
 
2960
3047
  // src/interactive/ReportInteractiveIsland.tsx
2961
- var import_react21 = require("react");
3048
+ var import_react22 = require("react");
2962
3049
 
2963
3050
  // src/interactive/island-renderers.tsx
2964
- var import_react20 = require("react");
3051
+ var import_react21 = require("react");
2965
3052
 
2966
3053
  // src/components/doc/MermaidView.tsx
2967
- var import_react19 = require("react");
3054
+ var import_react20 = require("react");
2968
3055
  var import_jsx_runtime47 = require("react/jsx-runtime");
2969
3056
  function prefersDark(el) {
2970
3057
  const themed = el?.closest("[data-theme]");
@@ -2973,23 +3060,40 @@ function prefersDark(el) {
2973
3060
  return typeof window !== "undefined" && !!window.matchMedia?.("(prefers-color-scheme: dark)").matches;
2974
3061
  }
2975
3062
  function MermaidView({ entry, load }) {
2976
- const ref = (0, import_react19.useRef)(null);
2977
- const [svg, setSvg] = (0, import_react19.useState)(null);
2978
- (0, import_react19.useEffect)(() => {
3063
+ const ref = (0, import_react20.useRef)(null);
3064
+ const [svg, setSvg] = (0, import_react20.useState)(null);
3065
+ const [error, setError] = (0, import_react20.useState)(null);
3066
+ (0, import_react20.useEffect)(() => {
2979
3067
  let cancelled = false;
2980
3068
  const draw = async () => {
3069
+ let mermaid;
3070
+ try {
3071
+ mermaid = await load();
3072
+ } catch {
3073
+ if (!cancelled) {
3074
+ setSvg(null);
3075
+ setError(null);
3076
+ }
3077
+ return;
3078
+ }
2981
3079
  try {
2982
- const mermaid = await load();
2983
3080
  mermaid.initialize({
2984
3081
  startOnLoad: false,
2985
3082
  securityLevel: "strict",
2986
3083
  theme: prefersDark(ref.current) ? "dark" : "default"
2987
3084
  });
3085
+ await mermaid.parse(entry.code);
2988
3086
  const id = `es-mermaid-${Math.random().toString(36).slice(2)}`;
2989
3087
  const { svg: rendered } = await mermaid.render(id, entry.code);
2990
- if (!cancelled) setSvg(rendered);
2991
- } catch {
2992
- if (!cancelled) setSvg(null);
3088
+ if (!cancelled) {
3089
+ setSvg(rendered);
3090
+ setError(null);
3091
+ }
3092
+ } catch (err) {
3093
+ if (!cancelled) {
3094
+ setSvg(null);
3095
+ setError(err instanceof Error ? err.message : String(err));
3096
+ }
2993
3097
  }
2994
3098
  };
2995
3099
  void draw();
@@ -3015,7 +3119,21 @@ function MermaidView({ entry, load }) {
3015
3119
  )
3016
3120
  ] });
3017
3121
  }
3018
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(MermaidSource, { entry, ref });
3122
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_jsx_runtime47.Fragment, { children: [
3123
+ error ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3124
+ "p",
3125
+ {
3126
+ role: "alert",
3127
+ "data-es-mermaid-error": true,
3128
+ className: "mt-3 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
3129
+ children: [
3130
+ "Diagram failed to render: ",
3131
+ error
3132
+ ]
3133
+ }
3134
+ ) : null,
3135
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(MermaidSource, { entry, ref })
3136
+ ] });
3019
3137
  }
3020
3138
 
3021
3139
  // src/interactive/island-renderers.tsx
@@ -3036,8 +3154,8 @@ function cachedCdnModule(url) {
3036
3154
  var loadMermaidFromCdn = cachedCdnModule(MERMAID_ESM);
3037
3155
  var loadHljs = cachedCdnModule(HLJS_ESM);
3038
3156
  function CdnHighlightedCode({ entry }) {
3039
- const [html, setHtml] = (0, import_react20.useState)(null);
3040
- (0, import_react20.useEffect)(() => {
3157
+ const [html, setHtml] = (0, import_react21.useState)(null);
3158
+ (0, import_react21.useEffect)(() => {
3041
3159
  let cancelled = false;
3042
3160
  void (async () => {
3043
3161
  try {
@@ -3064,10 +3182,10 @@ function CdnHighlightedCode({ entry }) {
3064
3182
  function buildIslandRenderers(opts) {
3065
3183
  const renderers = {};
3066
3184
  if (opts.mermaid) {
3067
- renderers.mermaid = (entry) => (0, import_react20.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
3185
+ renderers.mermaid = (entry) => (0, import_react21.createElement)(MermaidView, { entry, load: loadMermaidFromCdn });
3068
3186
  }
3069
3187
  if (opts.syntaxHighlighting) {
3070
- renderers.code = (entry) => (0, import_react20.createElement)(CdnHighlightedCode, { entry });
3188
+ renderers.code = (entry) => (0, import_react21.createElement)(CdnHighlightedCode, { entry });
3071
3189
  }
3072
3190
  return renderers;
3073
3191
  }
@@ -3083,7 +3201,7 @@ function ReportInteractiveIsland({
3083
3201
  syntaxHighlighting = true,
3084
3202
  mermaid = true
3085
3203
  }) {
3086
- const renderers = (0, import_react21.useMemo)(
3204
+ const renderers = (0, import_react22.useMemo)(
3087
3205
  () => buildIslandRenderers({ syntaxHighlighting, mermaid }),
3088
3206
  [syntaxHighlighting, mermaid]
3089
3207
  );