executable-stories-react 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ssr.js CHANGED
@@ -51,11 +51,23 @@ function useReport() {
51
51
  return ctx.report;
52
52
  }
53
53
 
54
- // src/components/ReportScenario.tsx
55
- import { formatDuration as formatDuration3 } from "executable-stories-core/utils/duration";
56
-
57
- // src/components/ReportSteps.tsx
58
- import { formatDuration } from "executable-stories-core/utils/duration";
54
+ // ../executable-stories-core/dist/utils/duration.js
55
+ function formatDuration(ms) {
56
+ if (ms < 1e3) {
57
+ return `${Math.round(ms)} ms`;
58
+ }
59
+ if (ms < 6e4) {
60
+ return `${(ms / 1e3).toFixed(2)} s`;
61
+ }
62
+ const minutes = Math.floor(ms / 6e4);
63
+ const seconds = Math.floor(ms % 6e4 / 1e3);
64
+ if (minutes < 60) {
65
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
66
+ }
67
+ const hours = Math.floor(minutes / 60);
68
+ const remainingMinutes = minutes % 60;
69
+ return `${hours}h ${remainingMinutes}m`;
70
+ }
59
71
 
60
72
  // src/components/doc/DocNote.tsx
61
73
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -538,9 +550,6 @@ function ReportAttachments({ attachments }) {
538
550
  );
539
551
  }
540
552
 
541
- // src/components/ReportTrace.tsx
542
- import { formatDuration as formatDuration2 } from "executable-stories-core/utils/duration";
543
-
544
553
  // src/lib/trace.ts
545
554
  var VALID = /* @__PURE__ */ new Set(["ok", "error", "unset"]);
546
555
  var safeStatus = (s) => VALID.has(s) ? s : "unset";
@@ -635,7 +644,7 @@ function ReportTrace({ spans }) {
635
644
  " span",
636
645
  rows.length === 1 ? "" : "s",
637
646
  ", ",
638
- formatDuration2(totalMs),
647
+ formatDuration(totalMs),
639
648
  ")"
640
649
  ] }),
641
650
  /* @__PURE__ */ jsx19("ul", { className: "flex flex-col gap-0.5 px-3 pb-3 font-mono text-[0.6875rem]", children: rows.map((row, i) => {
@@ -658,33 +667,128 @@ function ReportTrace({ spans }) {
658
667
  style: { left: `${leftPct}%`, width: `${widthPct}%` }
659
668
  }
660
669
  ) }),
661
- /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration2(row.durationMs) })
670
+ /* @__PURE__ */ jsx19("span", { className: "w-16 shrink-0 text-right text-muted-foreground", children: formatDuration(row.durationMs) })
662
671
  ] }, `${row.spanId}-${i}`);
663
672
  }) })
664
673
  ] });
665
674
  }
666
675
 
676
+ // src/interactive/scenario-history-context.ts
677
+ import { createContext as createContext2, useContext as useContext3 } from "react";
678
+ var ScenarioHistoryContext = createContext2(null);
679
+ var ScenarioHistoryProvider = ScenarioHistoryContext.Provider;
680
+ function useScenarioHistory() {
681
+ return useContext3(ScenarioHistoryContext);
682
+ }
683
+
684
+ // src/lib/run-history.ts
685
+ function currentStreak(entries) {
686
+ const last = entries[entries.length - 1];
687
+ if (!last) return void 0;
688
+ let count = 0;
689
+ for (let i = entries.length - 1; i >= 0 && entries[i].status === last.status; i--) count++;
690
+ return { status: last.status, count };
691
+ }
692
+ var STREAK_VERB = {
693
+ passed: "Passing",
694
+ failed: "Failing",
695
+ skipped: "Skipped",
696
+ pending: "Pending"
697
+ };
698
+ function describeRunHistory(entries) {
699
+ const passed = entries.filter((e) => e.status === "passed").length;
700
+ const base = `${passed}/${entries.length} runs passed`;
701
+ const streak = currentStreak(entries);
702
+ if (!streak || entries.length < 2) return base;
703
+ return `${base} \xB7 ${STREAK_VERB[streak.status]} for the last ${streak.count === 1 ? "run" : `${streak.count} runs`}`;
704
+ }
705
+ var MIN_FLAKINESS_SAMPLES = 3;
706
+ function flakinessOf(entries) {
707
+ const countable = entries.filter((e) => e.status === "passed" || e.status === "failed");
708
+ if (countable.length < MIN_FLAKINESS_SAMPLES) return "stable";
709
+ let transitions = 0;
710
+ for (let i = 1; i < countable.length; i++) {
711
+ if (countable[i].status !== countable[i - 1].status) transitions++;
712
+ }
713
+ const transitionScore = transitions / (countable.length - 1);
714
+ const failureRate = countable.filter((e) => e.status === "failed").length / countable.length;
715
+ if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) return "flaky";
716
+ if (transitionScore > 0.2 || failureRate > 0.3) return "unstable";
717
+ return "stable";
718
+ }
719
+
720
+ // src/components/ScenarioRunHistory.tsx
721
+ import { Fragment as Fragment7, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
722
+ var MAX_DOTS = 10;
723
+ var DOT_COLOR = {
724
+ passed: "bg-pass",
725
+ failed: "bg-fail",
726
+ skipped: "bg-skip",
727
+ pending: "bg-pend"
728
+ };
729
+ function dotTitle(e) {
730
+ const parts = [new Date(e.timestamp).toLocaleString(), e.status];
731
+ if (e.commitSha) parts.push(e.commitSha.slice(0, 8));
732
+ return parts.join(" \xB7 ");
733
+ }
734
+ function ScenarioRunHistory({ scenarioId }) {
735
+ const history = useScenarioHistory();
736
+ const entries = history?.[scenarioId];
737
+ if (!entries || entries.length < 2) return null;
738
+ const recent = entries.slice(-MAX_DOTS);
739
+ const flaky = flakinessOf(recent) === "flaky";
740
+ const summary = flaky ? `${describeRunHistory(recent)} \xB7 Flaky` : describeRunHistory(recent);
741
+ return /* @__PURE__ */ jsxs13(Fragment7, { children: [
742
+ flaky ? /* @__PURE__ */ jsx20(Badge, { variant: "pending", "aria-label": "Flaky: status flips between recent runs", title: summary, children: "Flaky" }) : null,
743
+ /* @__PURE__ */ jsx20(
744
+ "span",
745
+ {
746
+ role: "img",
747
+ "aria-label": `Run history: ${summary}`,
748
+ title: summary,
749
+ className: "flex shrink-0 items-center gap-[3px]",
750
+ children: recent.map((e, i) => /* @__PURE__ */ jsx20(
751
+ "span",
752
+ {
753
+ "aria-hidden": true,
754
+ title: dotTitle(e),
755
+ className: cn(
756
+ "inline-block size-1.5 rounded-full",
757
+ DOT_COLOR[e.status],
758
+ // The newest run (rightmost dot) is the one the card's status
759
+ // badge already describes; ring it so the timeline reads as
760
+ // "…leading up to now".
761
+ i === recent.length - 1 && "ring-1 ring-border ring-offset-1 ring-offset-card"
762
+ )
763
+ },
764
+ `${e.timestamp}-${i}`
765
+ ))
766
+ }
767
+ )
768
+ ] });
769
+ }
770
+
667
771
  // src/interactive/collapse-context.ts
668
- import { createContext as createContext2, useCallback, useContext as useContext3, useState } from "react";
669
- var CollapseContext = createContext2(null);
772
+ import { createContext as createContext3, useCallback, useContext as useContext4, useState } from "react";
773
+ var CollapseContext = createContext3(null);
670
774
  var CollapseProvider = CollapseContext.Provider;
671
775
  function useCollapse() {
672
- return useContext3(CollapseContext);
776
+ return useContext4(CollapseContext);
673
777
  }
674
778
 
675
779
  // src/interactive/scenario-actions.tsx
676
- import { createContext as createContext3, useContext as useContext4 } from "react";
677
- var ScenarioActionsContext = createContext3(null);
780
+ import { createContext as createContext4, useContext as useContext5 } from "react";
781
+ var ScenarioActionsContext = createContext4(null);
678
782
  var ScenarioActionsProvider = ScenarioActionsContext.Provider;
679
783
  function useScenarioActions() {
680
- return useContext4(ScenarioActionsContext);
784
+ return useContext5(ScenarioActionsContext);
681
785
  }
682
786
 
683
787
  // src/components/ui/card.tsx
684
788
  import "react";
685
- import { jsx as jsx20 } from "react/jsx-runtime";
789
+ import { jsx as jsx21 } from "react/jsx-runtime";
686
790
  function Card({ className, ...props }) {
687
- return /* @__PURE__ */ jsx20(
791
+ return /* @__PURE__ */ jsx21(
688
792
  "div",
689
793
  {
690
794
  "data-slot": "card",
@@ -697,7 +801,7 @@ function Card({ className, ...props }) {
697
801
  );
698
802
  }
699
803
  function CardHeader({ className, ...props }) {
700
- return /* @__PURE__ */ jsx20(
804
+ return /* @__PURE__ */ jsx21(
701
805
  "div",
702
806
  {
703
807
  "data-slot": "card-header",
@@ -710,7 +814,7 @@ function CardHeader({ className, ...props }) {
710
814
  );
711
815
  }
712
816
  function CardContent({ className, ...props }) {
713
- return /* @__PURE__ */ jsx20(
817
+ return /* @__PURE__ */ jsx21(
714
818
  "div",
715
819
  {
716
820
  "data-slot": "card-content",
@@ -721,7 +825,7 @@ function CardContent({ className, ...props }) {
721
825
  }
722
826
 
723
827
  // src/components/ReportScenario.tsx
724
- import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
828
+ import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
725
829
  var STATUS_LABEL = {
726
830
  passed: "Passed",
727
831
  failed: "Failed",
@@ -749,12 +853,12 @@ var STATUS_BADGE = {
749
853
  function ReportScenario({ scenario, hideTitle = false }) {
750
854
  const titleId = `${scenario.id}-title`;
751
855
  const bodyId = `${scenario.id}-body`;
752
- const label = STATUS_LABEL[scenario.status];
856
+ const label = scenario.planned ? "Planned" : STATUS_LABEL[scenario.status];
753
857
  const collapse = useCollapse();
754
858
  const collapsible = collapse !== null && !hideTitle;
755
859
  const collapsed = collapsible ? collapse.isCollapsed(scenario.id) : false;
756
860
  const actions = useScenarioActions();
757
- return /* @__PURE__ */ jsxs13(
861
+ return /* @__PURE__ */ jsxs14(
758
862
  Card,
759
863
  {
760
864
  id: scenario.id,
@@ -762,10 +866,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
762
866
  ...hideTitle ? { "aria-label": `${scenario.title} \u2014 ${label}` } : { "aria-labelledby": titleId },
763
867
  className: "gap-0 py-0",
764
868
  children: [
765
- /* @__PURE__ */ jsxs13(CardHeader, { className: "px-4 py-3", children: [
766
- /* @__PURE__ */ jsxs13("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
767
- hideTitle ? null : /* @__PURE__ */ jsxs13("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
768
- collapsible ? /* @__PURE__ */ jsx21(
869
+ /* @__PURE__ */ jsxs14(CardHeader, { className: "px-4 py-3", children: [
870
+ /* @__PURE__ */ jsxs14("div", { className: cn("flex items-start gap-4", hideTitle ? "justify-end" : "justify-between"), children: [
871
+ hideTitle ? null : /* @__PURE__ */ jsxs14("h3", { id: titleId, className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
872
+ collapsible ? /* @__PURE__ */ jsx22(
769
873
  "button",
770
874
  {
771
875
  type: "button",
@@ -774,17 +878,18 @@ function ReportScenario({ scenario, hideTitle = false }) {
774
878
  "aria-controls": bodyId,
775
879
  "aria-label": `Toggle ${scenario.title}`,
776
880
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
777
- children: /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
881
+ children: /* @__PURE__ */ jsx22("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
778
882
  }
779
883
  ) : null,
780
- /* @__PURE__ */ jsx21("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
781
- /* @__PURE__ */ jsx21("span", { children: scenario.title })
884
+ /* @__PURE__ */ jsx22("span", { "aria-hidden": true, className: cn("text-sm leading-none", STATUS_GLYPH_COLOR[scenario.status]), children: STATUS_GLYPH[scenario.status] }),
885
+ /* @__PURE__ */ jsx22("span", { children: scenario.title })
782
886
  ] }),
783
- /* @__PURE__ */ jsxs13("div", { className: "flex shrink-0 items-center gap-2", children: [
784
- scenario.durationMs > 0 ? /* @__PURE__ */ jsx21("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration3(scenario.durationMs) }) : null,
785
- /* @__PURE__ */ jsx21(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
786
- actions ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-0.5", children: [
787
- /* @__PURE__ */ jsx21(
887
+ /* @__PURE__ */ jsxs14("div", { className: "flex shrink-0 items-center gap-2", children: [
888
+ /* @__PURE__ */ jsx22(ScenarioRunHistory, { scenarioId: scenario.id }),
889
+ scenario.durationMs > 0 ? /* @__PURE__ */ jsx22("span", { className: "whitespace-nowrap font-mono text-[0.6875rem] text-muted-foreground", children: formatDuration(scenario.durationMs) }) : null,
890
+ /* @__PURE__ */ jsx22(Badge, { variant: STATUS_BADGE[scenario.status], "aria-label": `Status: ${label}`, children: label }),
891
+ actions ? /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-0.5", children: [
892
+ /* @__PURE__ */ jsx22(
788
893
  "button",
789
894
  {
790
895
  type: "button",
@@ -795,7 +900,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
795
900
  children: "link"
796
901
  }
797
902
  ),
798
- /* @__PURE__ */ jsx21(
903
+ /* @__PURE__ */ jsx22(
799
904
  "button",
800
905
  {
801
906
  type: "button",
@@ -806,7 +911,7 @@ function ReportScenario({ scenario, hideTitle = false }) {
806
911
  children: "md"
807
912
  }
808
913
  ),
809
- /* @__PURE__ */ jsx21(
914
+ /* @__PURE__ */ jsx22(
810
915
  "button",
811
916
  {
812
917
  type: "button",
@@ -820,13 +925,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
820
925
  ] }) : null
821
926
  ] })
822
927
  ] }),
823
- scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs13("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
824
- scenario.tags.map((t) => /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsx21(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
825
- (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx21("li", { children: ticket.url ? /* @__PURE__ */ jsx21("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx21(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
928
+ scenario.tags.length > 0 || (scenario.tickets?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs14("ul", { "aria-label": "Tags and tickets", className: "flex flex-wrap gap-1.5", children: [
929
+ scenario.tags.map((t) => /* @__PURE__ */ jsx22("li", { children: /* @__PURE__ */ jsx22(Badge, { variant: "tag", children: t }) }, `tag-${t}`)),
930
+ (scenario.tickets ?? []).map((ticket) => /* @__PURE__ */ jsx22("li", { children: ticket.url ? /* @__PURE__ */ jsx22("a", { href: ticket.url, target: "_blank", rel: "noreferrer noopener", children: /* @__PURE__ */ jsx22(Badge, { variant: "outline", children: ticket.id }) }) : /* @__PURE__ */ jsx22(Badge, { variant: "outline", children: ticket.id }) }, `ticket-${ticket.id}`))
826
931
  ] }) : null
827
932
  ] }),
828
- /* @__PURE__ */ jsxs13(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
829
- scenario.errorMessage ? /* @__PURE__ */ jsx21(
933
+ /* @__PURE__ */ jsxs14(CardContent, { id: bodyId, hidden: collapsed, className: "px-4 pt-0 pb-4", children: [
934
+ scenario.errorMessage ? /* @__PURE__ */ jsx22(
830
935
  "pre",
831
936
  {
832
937
  role: "alert",
@@ -834,10 +939,10 @@ function ReportScenario({ scenario, hideTitle = false }) {
834
939
  children: scenario.errorMessage
835
940
  }
836
941
  ) : null,
837
- /* @__PURE__ */ jsx21(ReportSteps, { scenario }),
838
- scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx21("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx21(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
839
- /* @__PURE__ */ jsx21(ReportAttachments, { attachments: scenario.attachments }),
840
- /* @__PURE__ */ jsx21(ReportTrace, { spans: scenario.otelSpans })
942
+ /* @__PURE__ */ jsx22(ReportSteps, { scenario }),
943
+ scenario.docEntries.length > 0 ? /* @__PURE__ */ jsx22("div", { "data-es-docs": true, className: "mt-3", children: /* @__PURE__ */ jsx22(ReportDocEntries, { entries: scenario.docEntries }) }) : null,
944
+ /* @__PURE__ */ jsx22(ReportAttachments, { attachments: scenario.attachments }),
945
+ /* @__PURE__ */ jsx22(ReportTrace, { spans: scenario.otelSpans })
841
946
  ] })
842
947
  ]
843
948
  }
@@ -845,13 +950,13 @@ function ReportScenario({ scenario, hideTitle = false }) {
845
950
  }
846
951
 
847
952
  // src/components/ReportScenarioList.tsx
848
- import { Fragment as Fragment7, jsx as jsx22 } from "react/jsx-runtime";
953
+ import { Fragment as Fragment8, jsx as jsx23 } from "react/jsx-runtime";
849
954
  function ReportScenarioList({ feature }) {
850
- return /* @__PURE__ */ jsx22(Fragment7, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx22(ReportScenario, { scenario }, scenario.id)) });
955
+ return /* @__PURE__ */ jsx23(Fragment8, { children: feature.scenarios.map((scenario) => /* @__PURE__ */ jsx23(ReportScenario, { scenario }, scenario.id)) });
851
956
  }
852
957
 
853
958
  // src/components/ReportFeature.tsx
854
- import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
959
+ import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
855
960
  function ReportFeature({ feature }) {
856
961
  const titleId = `${feature.id}-title`;
857
962
  const bodyId = `${feature.id}-body`;
@@ -859,7 +964,7 @@ function ReportFeature({ feature }) {
859
964
  const skipped = s.skipped + s.pending;
860
965
  const collapse = useCollapse();
861
966
  const collapsed = collapse?.isCollapsed(feature.id) ?? false;
862
- return /* @__PURE__ */ jsxs14(
967
+ return /* @__PURE__ */ jsxs15(
863
968
  "section",
864
969
  {
865
970
  id: feature.id,
@@ -867,9 +972,9 @@ function ReportFeature({ feature }) {
867
972
  "aria-labelledby": titleId,
868
973
  className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
869
974
  children: [
870
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
871
- /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 items-center gap-2", children: [
872
- collapse ? /* @__PURE__ */ jsx23(
975
+ /* @__PURE__ */ jsxs15("div", { className: "flex items-center justify-between gap-4 border-b border-border bg-muted/40 px-4 py-3", children: [
976
+ /* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 items-center gap-2", children: [
977
+ collapse ? /* @__PURE__ */ jsx24(
873
978
  "button",
874
979
  {
875
980
  type: "button",
@@ -878,30 +983,30 @@ function ReportFeature({ feature }) {
878
983
  "aria-controls": bodyId,
879
984
  "aria-label": `Toggle ${feature.title}`,
880
985
  className: "shrink-0 cursor-pointer text-muted-foreground hover:text-foreground",
881
- children: /* @__PURE__ */ jsx23("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
986
+ children: /* @__PURE__ */ jsx24("span", { "aria-hidden": true, className: cn("inline-block text-xs transition-transform", !collapsed && "rotate-90"), children: "\u25B8" })
882
987
  }
883
988
  ) : null,
884
- /* @__PURE__ */ jsxs14("div", { className: "min-w-0", children: [
885
- /* @__PURE__ */ jsx23("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
886
- /* @__PURE__ */ jsx23("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
989
+ /* @__PURE__ */ jsxs15("div", { className: "min-w-0", children: [
990
+ /* @__PURE__ */ jsx24("h2", { id: titleId, className: "text-sm font-semibold tracking-tight text-foreground", children: feature.title }),
991
+ /* @__PURE__ */ jsx24("p", { className: "mt-0.5 truncate font-mono text-xs text-muted-foreground", children: feature.sourceFile })
887
992
  ] })
888
993
  ] }),
889
- /* @__PURE__ */ jsxs14(
994
+ /* @__PURE__ */ jsxs15(
890
995
  "div",
891
996
  {
892
997
  role: "img",
893
998
  className: "flex shrink-0 items-center gap-2.5 font-mono text-xs font-medium",
894
999
  "aria-label": `${s.passed} passed, ${s.failed} failed, ${skipped} skipped`,
895
1000
  children: [
896
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-pass", children: [
1001
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-pass", children: [
897
1002
  "\u2713",
898
1003
  s.passed
899
1004
  ] }),
900
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-fail", children: [
1005
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-fail", children: [
901
1006
  "\u2717",
902
1007
  s.failed
903
1008
  ] }),
904
- /* @__PURE__ */ jsxs14("span", { "aria-hidden": true, className: "text-skip", children: [
1009
+ /* @__PURE__ */ jsxs15("span", { "aria-hidden": true, className: "text-skip", children: [
905
1010
  "\u25CB",
906
1011
  skipped
907
1012
  ] })
@@ -909,24 +1014,24 @@ function ReportFeature({ feature }) {
909
1014
  }
910
1015
  )
911
1016
  ] }),
912
- /* @__PURE__ */ jsx23("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx23(ReportScenarioList, { feature }) })
1017
+ /* @__PURE__ */ jsx24("div", { id: bodyId, hidden: collapsed, className: "flex flex-col gap-2 p-3", children: /* @__PURE__ */ jsx24(ReportScenarioList, { feature }) })
913
1018
  ]
914
1019
  }
915
1020
  );
916
1021
  }
917
1022
 
918
1023
  // src/components/ReportFeatureList.tsx
919
- import { Fragment as Fragment8, jsx as jsx24 } from "react/jsx-runtime";
1024
+ import { Fragment as Fragment9, jsx as jsx25 } from "react/jsx-runtime";
920
1025
  function ReportFeatureList() {
921
1026
  const report = useReport();
922
- return /* @__PURE__ */ jsx24(Fragment8, { children: report.features.map((feature) => /* @__PURE__ */ jsx24(ReportFeature, { feature }, feature.id)) });
1027
+ return /* @__PURE__ */ jsx25(Fragment9, { children: report.features.map((feature) => /* @__PURE__ */ jsx25(ReportFeature, { feature }, feature.id)) });
923
1028
  }
924
1029
 
925
1030
  // src/components/ui/empty.tsx
926
1031
  import { cva as cva2 } from "class-variance-authority";
927
- import { jsx as jsx25 } from "react/jsx-runtime";
1032
+ import { jsx as jsx26 } from "react/jsx-runtime";
928
1033
  function Empty({ className, ...props }) {
929
- return /* @__PURE__ */ jsx25(
1034
+ return /* @__PURE__ */ jsx26(
930
1035
  "div",
931
1036
  {
932
1037
  "data-slot": "empty",
@@ -939,7 +1044,7 @@ function Empty({ className, ...props }) {
939
1044
  );
940
1045
  }
941
1046
  function EmptyHeader({ className, ...props }) {
942
- return /* @__PURE__ */ jsx25(
1047
+ return /* @__PURE__ */ jsx26(
943
1048
  "div",
944
1049
  {
945
1050
  "data-slot": "empty-header",
@@ -966,7 +1071,7 @@ var emptyMediaVariants = cva2(
966
1071
  }
967
1072
  );
968
1073
  function EmptyTitle({ className, ...props }) {
969
- return /* @__PURE__ */ jsx25(
1074
+ return /* @__PURE__ */ jsx26(
970
1075
  "div",
971
1076
  {
972
1077
  "data-slot": "empty-title",
@@ -977,16 +1082,16 @@ function EmptyTitle({ className, ...props }) {
977
1082
  }
978
1083
 
979
1084
  // src/components/ReportEmpty.tsx
980
- import { jsx as jsx26 } from "react/jsx-runtime";
1085
+ import { jsx as jsx27 } from "react/jsx-runtime";
981
1086
  function ReportEmpty({ message }) {
982
- return /* @__PURE__ */ jsx26(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx26(EmptyHeader, { children: /* @__PURE__ */ jsx26(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
1087
+ return /* @__PURE__ */ jsx27(Empty, { "aria-live": "polite", className: "border", children: /* @__PURE__ */ jsx27(EmptyHeader, { children: /* @__PURE__ */ jsx27(EmptyTitle, { children: message ?? "No scenarios in this report." }) }) });
983
1088
  }
984
1089
 
985
1090
  // src/components/ReportSummary.tsx
986
- import { Fragment as Fragment9, jsx as jsx27, jsxs as jsxs15 } from "react/jsx-runtime";
1091
+ import { Fragment as Fragment10, jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
987
1092
  function ReportSummary({ className }) {
988
1093
  const report = useReport();
989
- return /* @__PURE__ */ jsx27(
1094
+ return /* @__PURE__ */ jsx28(
990
1095
  ReportSummaryView,
991
1096
  {
992
1097
  summary: report.summary,
@@ -996,32 +1101,32 @@ function ReportSummary({ className }) {
996
1101
  );
997
1102
  }
998
1103
  function ReportSummaryView({ summary, className, ariaLabel }) {
999
- return /* @__PURE__ */ jsxs15("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1000
- /* @__PURE__ */ jsxs15("span", { children: [
1001
- /* @__PURE__ */ jsx27("strong", { className: "font-semibold text-foreground", children: summary.total }),
1104
+ return /* @__PURE__ */ jsxs16("p", { className: cn("text-sm text-muted-foreground", className), "aria-label": ariaLabel, children: [
1105
+ /* @__PURE__ */ jsxs16("span", { children: [
1106
+ /* @__PURE__ */ jsx28("strong", { className: "font-semibold text-foreground", children: summary.total }),
1002
1107
  " scenario",
1003
1108
  summary.total === 1 ? "" : "s"
1004
1109
  ] }),
1005
1110
  " \xB7 ",
1006
- /* @__PURE__ */ jsxs15("span", { "data-status": "passed", className: "text-pass", children: [
1111
+ /* @__PURE__ */ jsxs16("span", { "data-status": "passed", className: "text-pass", children: [
1007
1112
  summary.passed,
1008
1113
  " passed"
1009
1114
  ] }),
1010
1115
  " \xB7 ",
1011
- /* @__PURE__ */ jsxs15("span", { "data-status": "failed", className: "text-fail", children: [
1116
+ /* @__PURE__ */ jsxs16("span", { "data-status": "failed", className: "text-fail", children: [
1012
1117
  summary.failed,
1013
1118
  " failed"
1014
1119
  ] }),
1015
- summary.skipped > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1120
+ summary.skipped > 0 ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
1016
1121
  " \xB7 ",
1017
- /* @__PURE__ */ jsxs15("span", { "data-status": "skipped", className: "text-skip", children: [
1122
+ /* @__PURE__ */ jsxs16("span", { "data-status": "skipped", className: "text-skip", children: [
1018
1123
  summary.skipped,
1019
1124
  " skipped"
1020
1125
  ] })
1021
1126
  ] }) : null,
1022
- summary.pending > 0 ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
1127
+ summary.pending > 0 ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
1023
1128
  " \xB7 ",
1024
- /* @__PURE__ */ jsxs15("span", { "data-status": "pending", className: "text-pend", children: [
1129
+ /* @__PURE__ */ jsxs16("span", { "data-status": "pending", className: "text-pend", children: [
1025
1130
  summary.pending,
1026
1131
  " pending"
1027
1132
  ] })
@@ -1029,69 +1134,116 @@ function ReportSummaryView({ summary, className, ariaLabel }) {
1029
1134
  ] });
1030
1135
  }
1031
1136
 
1137
+ // src/lib/provenance.ts
1138
+ var CI_DISPLAY_NAMES = {
1139
+ github: "GitHub Actions",
1140
+ gitlab: "GitLab CI",
1141
+ circleci: "CircleCI",
1142
+ jenkins: "Jenkins",
1143
+ azure: "Azure DevOps",
1144
+ buildkite: "Buildkite",
1145
+ travis: "Travis CI",
1146
+ ci: "CI"
1147
+ };
1148
+ function ciDisplayName(name) {
1149
+ return CI_DISPLAY_NAMES[name] ?? name;
1150
+ }
1151
+ function commitUrl(ci, sha) {
1152
+ const commitSha = ci?.commitSha ?? sha;
1153
+ if (!ci?.url || !commitSha) return void 0;
1154
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1155
+ if (github) return `${github[1]}/commit/${commitSha}`;
1156
+ const gitlab = ci.url.match(/^(https?:\/\/.+)\/-\/pipelines\//);
1157
+ if (gitlab) return `${gitlab[1]}/-/commit/${commitSha}`;
1158
+ return void 0;
1159
+ }
1160
+ function prUrl(ci) {
1161
+ if (!ci?.url || !ci.prNumber) return void 0;
1162
+ const github = ci.url.match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/actions\/runs\//);
1163
+ if (github) return `${github[1]}/pull/${ci.prNumber}`;
1164
+ return void 0;
1165
+ }
1166
+
1032
1167
  // src/components/ReportMeta.tsx
1033
- import { formatDuration as formatDuration4 } from "executable-stories-core/utils/duration";
1034
- import { jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
1168
+ import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1035
1169
  function ReportMeta() {
1036
1170
  const report = useReport();
1037
1171
  const items = [];
1172
+ const linkClass = "text-primary underline underline-offset-2";
1038
1173
  if (report.startedAtMs) {
1039
1174
  items.push({ label: "Started", value: new Date(report.startedAtMs).toLocaleString() });
1040
1175
  }
1041
1176
  if (report.durationMs) {
1042
- items.push({ label: "Duration", value: formatDuration4(report.durationMs) });
1177
+ items.push({ label: "Duration", value: formatDuration(report.durationMs) });
1043
1178
  }
1044
1179
  if (report.packageVersion) {
1045
1180
  items.push({ label: "Version", value: report.packageVersion });
1046
1181
  }
1047
- if (report.gitSha) {
1048
- items.push({ label: "Commit", value: report.gitSha.slice(0, 8) });
1182
+ if (report.ci?.branch) {
1183
+ items.push({ label: "Branch", value: report.ci.branch });
1184
+ }
1185
+ const sha = report.ci?.commitSha ?? report.gitSha;
1186
+ if (sha) {
1187
+ const url = commitUrl(report.ci, report.gitSha);
1188
+ const short = sha.slice(0, 8);
1189
+ items.push({
1190
+ label: "Commit",
1191
+ value: url ? /* @__PURE__ */ jsx29("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: short }) : short
1192
+ });
1049
1193
  }
1050
1194
  if (report.ci) {
1051
1195
  items.push({
1052
1196
  label: "CI",
1053
- value: report.ci.url ? /* @__PURE__ */ jsxs16("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: "text-primary underline underline-offset-2", children: [
1054
- report.ci.name,
1197
+ value: report.ci.url ? /* @__PURE__ */ jsxs17("a", { href: report.ci.url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: [
1198
+ ciDisplayName(report.ci.name),
1055
1199
  report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""
1056
- ] }) : `${report.ci.name}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1200
+ ] }) : `${ciDisplayName(report.ci.name)}${report.ci.buildNumber ? ` #${report.ci.buildNumber}` : ""}`
1201
+ });
1202
+ }
1203
+ if (report.ci?.prNumber) {
1204
+ const url = prUrl(report.ci);
1205
+ const label = `#${report.ci.prNumber}`;
1206
+ items.push({
1207
+ label: "PR",
1208
+ value: url ? /* @__PURE__ */ jsx29("a", { href: url, target: "_blank", rel: "noreferrer noopener", className: linkClass, children: label }) : label
1057
1209
  });
1058
1210
  }
1059
1211
  if (items.length === 0) return null;
1060
- return /* @__PURE__ */ jsx28("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ jsxs16("div", { className: "flex gap-1.5", children: [
1061
- /* @__PURE__ */ jsx28("dt", { className: "font-medium", children: item.label }),
1062
- /* @__PURE__ */ jsx28("dd", { className: "font-mono text-foreground", children: item.value })
1212
+ return /* @__PURE__ */ jsx29("dl", { "aria-label": "Run metadata", className: "flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground", children: items.map((item) => /* @__PURE__ */ jsxs17("div", { className: "flex gap-1.5", children: [
1213
+ /* @__PURE__ */ jsx29("dt", { className: "font-medium", children: item.label }),
1214
+ /* @__PURE__ */ jsx29("dd", { className: "font-mono text-foreground", children: item.value })
1063
1215
  ] }, item.label)) });
1064
1216
  }
1065
1217
 
1066
1218
  // src/components/ReportSchemaError.tsx
1067
- import { jsx as jsx29, jsxs as jsxs17 } from "react/jsx-runtime";
1219
+ import { jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1068
1220
  function ReportSchemaError({ error }) {
1069
- return /* @__PURE__ */ jsxs17("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1070
- /* @__PURE__ */ jsx29("p", { children: /* @__PURE__ */ jsx29("strong", { children: "Report could not be displayed." }) }),
1071
- /* @__PURE__ */ jsx29("p", { children: error.message }),
1072
- error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs17("p", { children: [
1221
+ return /* @__PURE__ */ jsxs18("section", { className: "es-schema-error", role: "alert", "aria-live": "assertive", children: [
1222
+ /* @__PURE__ */ jsx30("p", { children: /* @__PURE__ */ jsx30("strong", { children: "Report could not be displayed." }) }),
1223
+ /* @__PURE__ */ jsx30("p", { children: error.message }),
1224
+ error.code === "SCHEMA_VERSION_MISMATCH" ? /* @__PURE__ */ jsxs18("p", { children: [
1073
1225
  "The report bundle is newer than this version of ",
1074
- /* @__PURE__ */ jsx29("code", { children: "executable-stories-react" }),
1226
+ /* @__PURE__ */ jsx30("code", { children: "executable-stories-react" }),
1075
1227
  ". Upgrade the package, or regenerate the report with an older formatters CLI."
1076
1228
  ] }) : null,
1077
- error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs17("details", { children: [
1078
- /* @__PURE__ */ jsxs17("summary", { children: [
1229
+ error.issues && error.issues.length > 0 ? /* @__PURE__ */ jsxs18("details", { children: [
1230
+ /* @__PURE__ */ jsxs18("summary", { children: [
1079
1231
  error.issues.length,
1080
1232
  " validation issue",
1081
1233
  error.issues.length === 1 ? "" : "s"
1082
1234
  ] }),
1083
- /* @__PURE__ */ jsx29("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1235
+ /* @__PURE__ */ jsx30("pre", { children: error.issues.slice(0, 20).map((i) => `${i.path}: ${i.message}`).join("\n") })
1084
1236
  ] }) : null
1085
1237
  ] });
1086
1238
  }
1087
1239
 
1088
1240
  // src/components/ReportShell.tsx
1089
- import { Fragment as Fragment10, jsx as jsx30, jsxs as jsxs18 } from "react/jsx-runtime";
1241
+ import { Fragment as Fragment11, jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1090
1242
  function ReportTitleBlock({ title }) {
1091
- return /* @__PURE__ */ jsxs18(Fragment10, { children: [
1092
- /* @__PURE__ */ jsx30("h1", { children: title ?? "Story Report" }),
1093
- /* @__PURE__ */ jsx30(ReportSummary, {}),
1094
- /* @__PURE__ */ jsx30(ReportMeta, {})
1243
+ return /* @__PURE__ */ jsxs19(Fragment11, { children: [
1244
+ /* @__PURE__ */ jsx31("h1", { children: title ?? "Story Report" }),
1245
+ /* @__PURE__ */ jsx31(ReportSummary, {}),
1246
+ /* @__PURE__ */ jsx31(ReportMeta, {})
1095
1247
  ] });
1096
1248
  }
1097
1249
  function ReportErrorShell({
@@ -1100,28 +1252,29 @@ function ReportErrorShell({
1100
1252
  title,
1101
1253
  dataTheme
1102
1254
  }) {
1103
- return /* @__PURE__ */ jsx30("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx30(ReportSchemaError, { error }) });
1255
+ return /* @__PURE__ */ jsx31("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx31(ReportSchemaError, { error }) });
1104
1256
  }
1105
1257
 
1106
1258
  // src/components/Report.tsx
1107
- import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1259
+ import { jsx as jsx32, jsxs as jsxs20 } from "react/jsx-runtime";
1108
1260
  function Report(props) {
1109
1261
  const { report, customRenderers, renderers, className, title, dataTheme } = props;
1110
1262
  const result = unwrapReport(report);
1111
1263
  if (!result.ok) {
1112
- return /* @__PURE__ */ jsx31(ReportErrorShell, { error: result.error, className, title, dataTheme });
1264
+ return /* @__PURE__ */ jsx32(ReportErrorShell, { error: result.error, className, title, dataTheme });
1113
1265
  }
1114
1266
  const data = result.data;
1115
1267
  const hasContent = data.features.length > 0;
1116
- return /* @__PURE__ */ jsx31(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs19("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1117
- /* @__PURE__ */ jsx31("header", { className: "es-report-header", children: /* @__PURE__ */ jsx31(ReportTitleBlock, { title }) }),
1118
- hasContent ? /* @__PURE__ */ jsx31(ReportFeatureList, {}) : /* @__PURE__ */ jsx31(ReportEmpty, {})
1268
+ return /* @__PURE__ */ jsx32(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs20("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1269
+ /* @__PURE__ */ jsx32("header", { className: "es-report-header", children: /* @__PURE__ */ jsx32(ReportTitleBlock, { title }) }),
1270
+ hasContent ? /* @__PURE__ */ jsx32(ReportFeatureList, {}) : /* @__PURE__ */ jsx32(ReportEmpty, {})
1119
1271
  ] }) });
1120
1272
  }
1121
1273
 
1122
1274
  // src/ssr-entry.ts
1123
1275
  var ROOT_ID = "es-report-root";
1124
1276
  var DATA_ID = "es-report-data";
1277
+ var HISTORY_ID = "es-report-history";
1125
1278
  function escapeJsonForScript(json) {
1126
1279
  return json.replace(/[<\u2028\u2029]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
1127
1280
  }
@@ -1170,7 +1323,9 @@ function renderReportToHtml(report, options = {}) {
1170
1323
  lang = "en",
1171
1324
  syntaxHighlighting = true,
1172
1325
  mermaid = true,
1173
- islandScript = ""
1326
+ islandScript = "",
1327
+ staleAfterDays = 7,
1328
+ scenarioHistory
1174
1329
  } = options;
1175
1330
  const markup = renderToStaticMarkup(
1176
1331
  createElement(Report, { report, title })
@@ -1180,9 +1335,10 @@ function renderReportToHtml(report, options = {}) {
1180
1335
  const rawReport = parsed.ok ? parsed.data : void 0;
1181
1336
  const interactive = islandScript.length > 0 && rawReport !== void 0;
1182
1337
  const dataScript = interactive ? `<script type="application/json" id="${DATA_ID}">${escapeJsonForScript(JSON.stringify(rawReport))}</script>` : "";
1338
+ const historyScript = interactive && scenarioHistory && Object.keys(scenarioHistory).length > 0 ? `<script type="application/json" id="${HISTORY_ID}">${escapeJsonForScript(JSON.stringify(scenarioHistory))}</script>` : "";
1183
1339
  const islandTag = interactive ? `<script>${islandScript}</script>` : "";
1184
1340
  const cdnBody = interactive ? "" : cdn.body;
1185
- const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}"` : "";
1341
+ const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}" data-es-stale-days="${Number.isFinite(staleAfterDays) && staleAfterDays >= 0 ? staleAfterDays : 7}"` : "";
1186
1342
  const rootAttrs = interactive ? ` id="${ROOT_ID}" data-title="${escapeHtml(title)}"${islandConfigAttrs}` : "";
1187
1343
  return `<!DOCTYPE html>
1188
1344
  <html lang="${escapeHtml(lang)}" data-theme="${theme === "dark" ? "dark" : "light"}">
@@ -1198,6 +1354,7 @@ ${headExtra}
1198
1354
  <body>
1199
1355
  <div${rootAttrs} class="es-report-island font-sans text-foreground">${markup}</div>
1200
1356
  ${dataScript}
1357
+ ${historyScript}
1201
1358
  ${bodyExtra}
1202
1359
  ${cdnBody}
1203
1360
  ${islandTag}