@cypress-design/constants-runresults 1.0.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,3 @@
1
+ 
2
+ ./src/index.ts → ./dist/index.umd.js, ./dist/index.es.mjs...
3
+ created ./dist/index.umd.js, ./dist/index.es.mjs in 7.3s
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @cypress-design/constants-runresults
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#676](https://github.com/cypress-io/cypress-design/pull/676) [`1531f94`](https://github.com/cypress-io/cypress-design/commit/1531f9439d73493e3eb08c72f41c0e7f0db958de) Thanks [@emilmilanov](https://github.com/emilmilanov)! - Add RunResults component — a pill of test-result counts (passed / failed / skipped / pending) with optional `flaky` and `self-healed` leading stats separated by a vertical divider. Each stat is an icon + count, optionally wrapped in a link (`links` prop or a custom `renderLink` callback) and an optional tooltip. Supports `light` and `dark` themes and an `expanded` mode that shows zero-count regular stats. Available for React and Vue with shared constants. See `components/RunResults/instructions.md` for the full props API.
@@ -0,0 +1,56 @@
1
+ export type StatKey = 'passed' | 'failed' | 'skipped' | 'pending' | 'flaky' | 'selfHealed';
2
+ export declare const RegularStatKeys: readonly ["skipped", "pending", "passed", "failed"];
3
+ export type RegularStatKey = (typeof RegularStatKeys)[number];
4
+ export declare const LeadingStatKeys: readonly ["flaky", "selfHealed"];
5
+ export type LeadingStatKey = (typeof LeadingStatKeys)[number];
6
+ export declare const CssClasses: {
7
+ readonly container: "inline-flex pointer-events-auto";
8
+ readonly list: "flex items-center text-[14px] leading-[24px] font-medium list-none border rounded-[4px]";
9
+ readonly item: "h-full whitespace-nowrap flex items-center";
10
+ readonly link: "flex items-center h-full w-full px-[6px] no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500 focus-visible:outline-offset-0";
11
+ readonly unlinked: "flex items-center h-full w-full px-[6px]";
12
+ readonly icon: "mx-[4px]";
13
+ readonly iconFlaky: "mx-[4px] [&_path:first-child]:fill-transparent";
14
+ readonly iconSelfHealed: "mx-[4px] w-3 h-3";
15
+ readonly separatorAfter: "after:content-[''] after:border-r after:h-3 after:mx-1 after:self-center";
16
+ };
17
+ export declare const CssTheme: {
18
+ readonly light: {
19
+ readonly list: "bg-white text-gray-700 border-gray-100";
20
+ readonly link: "text-gray-700 hover:bg-gray-50 focus-visible:bg-gray-50";
21
+ readonly separator: "after:border-gray-100";
22
+ };
23
+ readonly dark: {
24
+ readonly list: "bg-gray-1000 text-gray-400 border-gray-800";
25
+ readonly link: "text-gray-300 hover:bg-gray-900 focus-visible:bg-gray-900";
26
+ readonly separator: "after:border-gray-800";
27
+ };
28
+ };
29
+ export type RunResultsTheme = keyof typeof CssTheme;
30
+ export declare const TooltipColorForTheme: Record<RunResultsTheme, 'light' | 'dark'>;
31
+ export declare const CssTooltipPopperDark = "[&>div]:!text-gray-300 [&>div>div]:!text-[14px] [&>div>div]:!leading-[20px] [&>div>div]:!min-w-0";
32
+ export declare const CssTooltipPopperLight = "[&>div]:!text-gray-700 [&>div>div]:!text-[14px] [&>div>div]:!leading-[20px] [&>div>div]:!min-w-0";
33
+ export declare function getTooltipPlacement(key: StatKey): 'top-start' | 'top-end';
34
+ export declare function statKeyToKebab(key: StatKey): string;
35
+ export declare function getFlakyTooltipText(count: number): string;
36
+ export declare function getTooltipLabel(key: StatKey, count: number, isLinked: boolean): string;
37
+ export interface RunResultsProps {
38
+ passed: number | null;
39
+ failed: number | null;
40
+ skipped: number | null;
41
+ pending: number | null;
42
+ flaky?: number | null;
43
+ selfHealed?: number | null;
44
+ showSelfHealed?: boolean;
45
+ theme?: RunResultsTheme;
46
+ expanded?: boolean;
47
+ links?: Partial<Record<StatKey, string>>;
48
+ renderLink?: (href: string, children: unknown) => unknown;
49
+ showTooltip?: boolean;
50
+ className?: string;
51
+ }
52
+ export declare function statValue(count: number | null | undefined): number;
53
+ export declare function showRegularStat(count: number | null | undefined, expanded: boolean): boolean;
54
+ export declare function getSeparatorAfterKey(props: Pick<RunResultsProps, 'flaky' | 'selfHealed' | 'showSelfHealed' | 'passed' | 'failed' | 'skipped' | 'pending' | 'expanded'>): LeadingStatKey | null;
55
+ export declare function hasAnyStat(props: Pick<RunResultsProps, 'flaky' | 'selfHealed' | 'showSelfHealed' | 'passed' | 'failed' | 'skipped' | 'pending' | 'expanded'>): boolean;
56
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,OAAO,GACf,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,SAAS,GACT,OAAO,GACP,YAAY,CAAA;AAGhB,eAAO,MAAM,eAAe,qDAKlB,CAAA;AACV,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAG7D,eAAO,MAAM,eAAe,kCAAmC,CAAA;AAC/D,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7D,eAAO,MAAM,UAAU;;;;;;;;;;CA4Bb,CAAA;AAEV,eAAO,MAAM,QAAQ;;;;;;;;;;;CAWX,CAAA;AAEV,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,QAAQ,CAAA;AAGnD,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,eAAe,EAAE,OAAO,GAAG,MAAM,CAG1E,CAAA;AAYD,eAAO,MAAM,oBAAoB,qGAAmD,CAAA;AACpF,eAAO,MAAM,qBAAqB,qGAAmD,CAAA;AAKrF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAEzE;AAID,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEnD;AAOD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIzD;AAID,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,OAAO,GAChB,MAAM,CAMR;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAMrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAA;IAExB,KAAK,CAAC,EAAE,eAAe,CAAA;IAKvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA;IAGxC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAA;IAEzD,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAElE;AAGD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,QAAQ,EAAE,OAAO,GAChB,OAAO,CAET;AAWD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,IAAI,CACT,eAAe,EACb,OAAO,GACP,YAAY,GACZ,gBAAgB,GAChB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,SAAS,GACT,UAAU,CACb,GACA,cAAc,GAAG,IAAI,CAcvB;AAGD,wBAAgB,UAAU,CACxB,KAAK,EAAE,IAAI,CACT,eAAe,EACb,OAAO,GACP,YAAY,GACZ,gBAAgB,GAChB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,SAAS,GACT,UAAU,CACb,GACA,OAAO,CAUT"}
@@ -0,0 +1,138 @@
1
+ // Regular stats (the four primary outcomes). Order is fixed.
2
+ const RegularStatKeys = [
3
+ 'skipped',
4
+ 'pending',
5
+ 'passed',
6
+ 'failed',
7
+ ];
8
+ // Leading stats (rendered before the separator). Order is fixed.
9
+ const LeadingStatKeys = ['flaky', 'selfHealed'];
10
+ const CssClasses = {
11
+ // Outer wrapper. `inline-flex` so the component shrinks to content width.
12
+ container: 'inline-flex pointer-events-auto',
13
+ // The <ul> pill itself. Theme overrides border and text colors via CssTheme.
14
+ list: 'flex items-center text-[14px] leading-[24px] font-medium list-none border rounded-[4px]',
15
+ // Each <li> stat.
16
+ item: 'h-full whitespace-nowrap flex items-center',
17
+ // Inner <a> wrapper for linked stats.
18
+ link: 'flex items-center h-full w-full px-[6px] no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500 focus-visible:outline-offset-0',
19
+ // Inner <span> wrapper for unlinked stats.
20
+ unlinked: 'flex items-center h-full w-full px-[6px]',
21
+ // Icon margin matches source `svg { margin: 0 4px }`.
22
+ icon: 'mx-[4px]',
23
+ // Flaky icon override — drop the yellow background rect (first path in the
24
+ // SVG). Matches the source SCSS's `.flakyIcon svg path:first-child { fill:
25
+ // transparent !important }`. Scoped to this component; the shared
26
+ // IconStatusFlaky is unchanged.
27
+ iconFlaky: 'mx-[4px] [&_path:first-child]:fill-transparent',
28
+ // Self-healed icon override — `IconGeneralSparkleSingleSmall` only ships
29
+ // a `["16"]` variant in the icon registry, but the rest of the stats
30
+ // render at 12px. The icon component IS an <svg>, so `w-3 h-3` on the
31
+ // className overrides the icon's intrinsic `width`/`height` attributes
32
+ // via CSS and pins the rendered size to 12 × 12 — visual consistency
33
+ // without depending on a 12px icon variant that doesn't exist.
34
+ iconSelfHealed: 'mx-[4px] w-3 h-3',
35
+ // Separator after the last leading <li>. Border color comes from CssTheme.
36
+ separatorAfter: "after:content-[''] after:border-r after:h-3 after:mx-1 after:self-center",
37
+ };
38
+ const CssTheme = {
39
+ light: {
40
+ list: 'bg-white text-gray-700 border-gray-100',
41
+ link: 'text-gray-700 hover:bg-gray-50 focus-visible:bg-gray-50',
42
+ separator: 'after:border-gray-100',
43
+ },
44
+ dark: {
45
+ list: 'bg-gray-1000 text-gray-400 border-gray-800',
46
+ link: 'text-gray-300 hover:bg-gray-900 focus-visible:bg-gray-900',
47
+ separator: 'after:border-gray-800',
48
+ },
49
+ };
50
+ // Tooltip color contrasts with the surface the pill sits on.
51
+ const TooltipColorForTheme = {
52
+ light: 'dark',
53
+ dark: 'light',
54
+ };
55
+ // RunResults-specific overrides applied via Tooltip's `popperClassName`:
56
+ // - drop the 160px min-width so the tooltip auto-fits content
57
+ // - shrink text to 14px / 20px (DS body size; shared Tooltip defaults to 16px / 24px)
58
+ // - text color: gray-300 for dark tooltips (on light RunResults), gray-700 for light tooltips (on dark RunResults)
59
+ // `[&>div]` targets the colored container inside the popper (where text color
60
+ // is set on the shared Tooltip); `[&>div>div]` targets the inner text container
61
+ // (where font-size / line-height / min-width are set on the shared Tooltip).
62
+ // `!` is required because the shared Tooltip applies these on the same elements.
63
+ const CssTooltipPopperBase = '[&>div>div]:!text-[14px] [&>div>div]:!leading-[20px] [&>div>div]:!min-w-0';
64
+ const CssTooltipPopperDark = `[&>div]:!text-gray-300 ${CssTooltipPopperBase}`;
65
+ const CssTooltipPopperLight = `[&>div]:!text-gray-700 ${CssTooltipPopperBase}`;
66
+ // `top-start` for flaky: left-aligns the tooltip with the stat so the arrow
67
+ // points at the element rather than at the center of a wide tooltip.
68
+ // `top-end` for the rest (right-aligned stats on the right side of the pill).
69
+ function getTooltipPlacement(key) {
70
+ return key === 'flaky' ? 'top-start' : 'top-end';
71
+ }
72
+ // Convert the API key (camelCase) to the DOM-attribute / display form (kebab-case).
73
+ // Single multi-word case — explicit string match instead of a generic camel→kebab utility.
74
+ function statKeyToKebab(key) {
75
+ return key === 'selfHealed' ? 'self-healed' : key;
76
+ }
77
+ function capitalize(str) {
78
+ return str.charAt(0).toUpperCase() + str.slice(1);
79
+ }
80
+ // Long-form flaky description used in tooltips (always shown regardless of link).
81
+ function getFlakyTooltipText(count) {
82
+ return count === 1
83
+ ? 'This test both passed and failed when retried within a run'
84
+ : `${count} tests both passed and failed when retried within a run`;
85
+ }
86
+ // Tooltip / aria-label text. `isLinked` flips "View X tests" ↔ "X tests".
87
+ // Flaky linked stat uses "View flaky tests"; tooltip content uses getFlakyTooltipText.
88
+ function getTooltipLabel(key, count, isLinked) {
89
+ if (key === 'flaky') {
90
+ return isLinked ? 'View flaky tests' : getFlakyTooltipText(count);
91
+ }
92
+ const display = statKeyToKebab(key);
93
+ return isLinked ? `View ${display} tests` : `${capitalize(display)} tests`;
94
+ }
95
+ // Null-safe count → numeric value for display & visibility logic.
96
+ function statValue(count) {
97
+ return count ?? 0;
98
+ }
99
+ // Should a regular stat render?
100
+ function showRegularStat(count, expanded) {
101
+ return expanded || statValue(count) > 0;
102
+ }
103
+ // Which leading key (if any) gets the separator-after modifier?
104
+ // - selfHealed wins if it would render (it's the second leading stat)
105
+ // - else flaky if it would render
106
+ // - else null (no separator at all)
107
+ // Also returns null when there are no regular stats to follow — keep separators
108
+ // from dangling at the end of the pill.
109
+ //
110
+ // Self-healed renders whenever `showSelfHealed` is true (regardless of count);
111
+ // flaky renders only when its count > 0.
112
+ function getSeparatorAfterKey(props) {
113
+ const showFlaky = statValue(props.flaky) > 0;
114
+ const showSelfHealed = !!props.showSelfHealed;
115
+ if (!showFlaky && !showSelfHealed)
116
+ return null;
117
+ const expanded = !!props.expanded;
118
+ const anyRegular = showRegularStat(props.passed, expanded) ||
119
+ showRegularStat(props.failed, expanded) ||
120
+ showRegularStat(props.skipped, expanded) ||
121
+ showRegularStat(props.pending, expanded);
122
+ if (!anyRegular)
123
+ return null;
124
+ return showSelfHealed ? 'selfHealed' : 'flaky';
125
+ }
126
+ // Does anything render at all? Used to short-circuit to `null` on empty state.
127
+ function hasAnyStat(props) {
128
+ const expanded = !!props.expanded;
129
+ return (statValue(props.flaky) > 0 ||
130
+ !!props.showSelfHealed ||
131
+ showRegularStat(props.passed, expanded) ||
132
+ showRegularStat(props.failed, expanded) ||
133
+ showRegularStat(props.skipped, expanded) ||
134
+ showRegularStat(props.pending, expanded));
135
+ }
136
+
137
+ export { CssClasses, CssTheme, CssTooltipPopperDark, CssTooltipPopperLight, LeadingStatKeys, RegularStatKeys, TooltipColorForTheme, getFlakyTooltipText, getSeparatorAfterKey, getTooltipLabel, getTooltipPlacement, hasAnyStat, showRegularStat, statKeyToKebab, statValue };
138
+ //# sourceMappingURL=index.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.es.mjs","sources":["../src/index.ts"],"sourcesContent":["// Status keys this component accepts in its API.\n// camelCase to match StatusIcon's multi-word keys (noTests, timedOut, overLimit).\n// `data-cy` attributes use kebab-case (\"self-healed\") — see `statKeyToKebab`.\nexport type StatKey =\n | 'passed'\n | 'failed'\n | 'skipped'\n | 'pending'\n | 'flaky'\n | 'selfHealed'\n\n// Regular stats (the four primary outcomes). Order is fixed.\nexport const RegularStatKeys = [\n 'skipped',\n 'pending',\n 'passed',\n 'failed',\n] as const\nexport type RegularStatKey = (typeof RegularStatKeys)[number]\n\n// Leading stats (rendered before the separator). Order is fixed.\nexport const LeadingStatKeys = ['flaky', 'selfHealed'] as const\nexport type LeadingStatKey = (typeof LeadingStatKeys)[number]\n\nexport const CssClasses = {\n // Outer wrapper. `inline-flex` so the component shrinks to content width.\n container: 'inline-flex pointer-events-auto',\n // The <ul> pill itself. Theme overrides border and text colors via CssTheme.\n list: 'flex items-center text-[14px] leading-[24px] font-medium list-none border rounded-[4px]',\n // Each <li> stat.\n item: 'h-full whitespace-nowrap flex items-center',\n // Inner <a> wrapper for linked stats.\n link: 'flex items-center h-full w-full px-[6px] no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500 focus-visible:outline-offset-0',\n // Inner <span> wrapper for unlinked stats.\n unlinked: 'flex items-center h-full w-full px-[6px]',\n // Icon margin matches source `svg { margin: 0 4px }`.\n icon: 'mx-[4px]',\n // Flaky icon override — drop the yellow background rect (first path in the\n // SVG). Matches the source SCSS's `.flakyIcon svg path:first-child { fill:\n // transparent !important }`. Scoped to this component; the shared\n // IconStatusFlaky is unchanged.\n iconFlaky: 'mx-[4px] [&_path:first-child]:fill-transparent',\n // Self-healed icon override — `IconGeneralSparkleSingleSmall` only ships\n // a `[\"16\"]` variant in the icon registry, but the rest of the stats\n // render at 12px. The icon component IS an <svg>, so `w-3 h-3` on the\n // className overrides the icon's intrinsic `width`/`height` attributes\n // via CSS and pins the rendered size to 12 × 12 — visual consistency\n // without depending on a 12px icon variant that doesn't exist.\n iconSelfHealed: 'mx-[4px] w-3 h-3',\n // Separator after the last leading <li>. Border color comes from CssTheme.\n separatorAfter:\n \"after:content-[''] after:border-r after:h-3 after:mx-1 after:self-center\",\n} as const\n\nexport const CssTheme = {\n light: {\n list: 'bg-white text-gray-700 border-gray-100',\n link: 'text-gray-700 hover:bg-gray-50 focus-visible:bg-gray-50',\n separator: 'after:border-gray-100',\n },\n dark: {\n list: 'bg-gray-1000 text-gray-400 border-gray-800',\n link: 'text-gray-300 hover:bg-gray-900 focus-visible:bg-gray-900',\n separator: 'after:border-gray-800',\n },\n} as const\n\nexport type RunResultsTheme = keyof typeof CssTheme\n\n// Tooltip color contrasts with the surface the pill sits on.\nexport const TooltipColorForTheme: Record<RunResultsTheme, 'light' | 'dark'> = {\n light: 'dark',\n dark: 'light',\n}\n\n// RunResults-specific overrides applied via Tooltip's `popperClassName`:\n// - drop the 160px min-width so the tooltip auto-fits content\n// - shrink text to 14px / 20px (DS body size; shared Tooltip defaults to 16px / 24px)\n// - text color: gray-300 for dark tooltips (on light RunResults), gray-700 for light tooltips (on dark RunResults)\n// `[&>div]` targets the colored container inside the popper (where text color\n// is set on the shared Tooltip); `[&>div>div]` targets the inner text container\n// (where font-size / line-height / min-width are set on the shared Tooltip).\n// `!` is required because the shared Tooltip applies these on the same elements.\nconst CssTooltipPopperBase =\n '[&>div>div]:!text-[14px] [&>div>div]:!leading-[20px] [&>div>div]:!min-w-0'\nexport const CssTooltipPopperDark = `[&>div]:!text-gray-300 ${CssTooltipPopperBase}`\nexport const CssTooltipPopperLight = `[&>div]:!text-gray-700 ${CssTooltipPopperBase}`\n\n// `top-start` for flaky: left-aligns the tooltip with the stat so the arrow\n// points at the element rather than at the center of a wide tooltip.\n// `top-end` for the rest (right-aligned stats on the right side of the pill).\nexport function getTooltipPlacement(key: StatKey): 'top-start' | 'top-end' {\n return key === 'flaky' ? 'top-start' : 'top-end'\n}\n\n// Convert the API key (camelCase) to the DOM-attribute / display form (kebab-case).\n// Single multi-word case — explicit string match instead of a generic camel→kebab utility.\nexport function statKeyToKebab(key: StatKey): string {\n return key === 'selfHealed' ? 'self-healed' : key\n}\n\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1)\n}\n\n// Long-form flaky description used in tooltips (always shown regardless of link).\nexport function getFlakyTooltipText(count: number): string {\n return count === 1\n ? 'This test both passed and failed when retried within a run'\n : `${count} tests both passed and failed when retried within a run`\n}\n\n// Tooltip / aria-label text. `isLinked` flips \"View X tests\" ↔ \"X tests\".\n// Flaky linked stat uses \"View flaky tests\"; tooltip content uses getFlakyTooltipText.\nexport function getTooltipLabel(\n key: StatKey,\n count: number,\n isLinked: boolean,\n): string {\n if (key === 'flaky') {\n return isLinked ? 'View flaky tests' : getFlakyTooltipText(count)\n }\n const display = statKeyToKebab(key)\n return isLinked ? `View ${display} tests` : `${capitalize(display)} tests`\n}\n\nexport interface RunResultsProps {\n passed: number | null\n failed: number | null\n skipped: number | null\n pending: number | null\n flaky?: number | null\n\n // Self-healed (independent of flaky). Rendered whenever `showSelfHealed`\n // is true — the count (including 0, including `null` coerced to 0) is shown\n // verbatim. Consumers set the flag based on whether the run could have\n // self-healed tests at all (e.g. `cy.prompt` was available).\n selfHealed?: number | null\n showSelfHealed?: boolean\n\n theme?: RunResultsTheme\n // When true, regular stats render even with a zero count.\n // Does NOT affect leading stats. Flaky still renders only when its count\n // is > 0; self-healed renders whenever `showSelfHealed` is true (including\n // count 0). See `getSeparatorAfterKey` and `hasAnyStat` for the exact rules.\n expanded?: boolean\n\n links?: Partial<Record<StatKey, string>>\n // Same signature in React and Vue. `children` is whatever the framework\n // renders for the inner icon + count.\n renderLink?: (href: string, children: unknown) => unknown\n\n showTooltip?: boolean\n className?: string\n}\n\n// Null-safe count → numeric value for display & visibility logic.\nexport function statValue(count: number | null | undefined): number {\n return count ?? 0\n}\n\n// Should a regular stat render?\nexport function showRegularStat(\n count: number | null | undefined,\n expanded: boolean,\n): boolean {\n return expanded || statValue(count) > 0\n}\n\n// Which leading key (if any) gets the separator-after modifier?\n// - selfHealed wins if it would render (it's the second leading stat)\n// - else flaky if it would render\n// - else null (no separator at all)\n// Also returns null when there are no regular stats to follow — keep separators\n// from dangling at the end of the pill.\n//\n// Self-healed renders whenever `showSelfHealed` is true (regardless of count);\n// flaky renders only when its count > 0.\nexport function getSeparatorAfterKey(\n props: Pick<\n RunResultsProps,\n | 'flaky'\n | 'selfHealed'\n | 'showSelfHealed'\n | 'passed'\n | 'failed'\n | 'skipped'\n | 'pending'\n | 'expanded'\n >,\n): LeadingStatKey | null {\n const showFlaky = statValue(props.flaky) > 0\n const showSelfHealed = !!props.showSelfHealed\n if (!showFlaky && !showSelfHealed) return null\n\n const expanded = !!props.expanded\n const anyRegular =\n showRegularStat(props.passed, expanded) ||\n showRegularStat(props.failed, expanded) ||\n showRegularStat(props.skipped, expanded) ||\n showRegularStat(props.pending, expanded)\n if (!anyRegular) return null\n\n return showSelfHealed ? 'selfHealed' : 'flaky'\n}\n\n// Does anything render at all? Used to short-circuit to `null` on empty state.\nexport function hasAnyStat(\n props: Pick<\n RunResultsProps,\n | 'flaky'\n | 'selfHealed'\n | 'showSelfHealed'\n | 'passed'\n | 'failed'\n | 'skipped'\n | 'pending'\n | 'expanded'\n >,\n): boolean {\n const expanded = !!props.expanded\n return (\n statValue(props.flaky) > 0 ||\n !!props.showSelfHealed ||\n showRegularStat(props.passed, expanded) ||\n showRegularStat(props.failed, expanded) ||\n showRegularStat(props.skipped, expanded) ||\n showRegularStat(props.pending, expanded)\n )\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // Regular stats (the four primary outcomes). Order is fixed.
4
+ const RegularStatKeys = [
5
+ 'skipped',
6
+ 'pending',
7
+ 'passed',
8
+ 'failed',
9
+ ];
10
+ // Leading stats (rendered before the separator). Order is fixed.
11
+ const LeadingStatKeys = ['flaky', 'selfHealed'];
12
+ const CssClasses = {
13
+ // Outer wrapper. `inline-flex` so the component shrinks to content width.
14
+ container: 'inline-flex pointer-events-auto',
15
+ // The <ul> pill itself. Theme overrides border and text colors via CssTheme.
16
+ list: 'flex items-center text-[14px] leading-[24px] font-medium list-none border rounded-[4px]',
17
+ // Each <li> stat.
18
+ item: 'h-full whitespace-nowrap flex items-center',
19
+ // Inner <a> wrapper for linked stats.
20
+ link: 'flex items-center h-full w-full px-[6px] no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500 focus-visible:outline-offset-0',
21
+ // Inner <span> wrapper for unlinked stats.
22
+ unlinked: 'flex items-center h-full w-full px-[6px]',
23
+ // Icon margin matches source `svg { margin: 0 4px }`.
24
+ icon: 'mx-[4px]',
25
+ // Flaky icon override — drop the yellow background rect (first path in the
26
+ // SVG). Matches the source SCSS's `.flakyIcon svg path:first-child { fill:
27
+ // transparent !important }`. Scoped to this component; the shared
28
+ // IconStatusFlaky is unchanged.
29
+ iconFlaky: 'mx-[4px] [&_path:first-child]:fill-transparent',
30
+ // Self-healed icon override — `IconGeneralSparkleSingleSmall` only ships
31
+ // a `["16"]` variant in the icon registry, but the rest of the stats
32
+ // render at 12px. The icon component IS an <svg>, so `w-3 h-3` on the
33
+ // className overrides the icon's intrinsic `width`/`height` attributes
34
+ // via CSS and pins the rendered size to 12 × 12 — visual consistency
35
+ // without depending on a 12px icon variant that doesn't exist.
36
+ iconSelfHealed: 'mx-[4px] w-3 h-3',
37
+ // Separator after the last leading <li>. Border color comes from CssTheme.
38
+ separatorAfter: "after:content-[''] after:border-r after:h-3 after:mx-1 after:self-center",
39
+ };
40
+ const CssTheme = {
41
+ light: {
42
+ list: 'bg-white text-gray-700 border-gray-100',
43
+ link: 'text-gray-700 hover:bg-gray-50 focus-visible:bg-gray-50',
44
+ separator: 'after:border-gray-100',
45
+ },
46
+ dark: {
47
+ list: 'bg-gray-1000 text-gray-400 border-gray-800',
48
+ link: 'text-gray-300 hover:bg-gray-900 focus-visible:bg-gray-900',
49
+ separator: 'after:border-gray-800',
50
+ },
51
+ };
52
+ // Tooltip color contrasts with the surface the pill sits on.
53
+ const TooltipColorForTheme = {
54
+ light: 'dark',
55
+ dark: 'light',
56
+ };
57
+ // RunResults-specific overrides applied via Tooltip's `popperClassName`:
58
+ // - drop the 160px min-width so the tooltip auto-fits content
59
+ // - shrink text to 14px / 20px (DS body size; shared Tooltip defaults to 16px / 24px)
60
+ // - text color: gray-300 for dark tooltips (on light RunResults), gray-700 for light tooltips (on dark RunResults)
61
+ // `[&>div]` targets the colored container inside the popper (where text color
62
+ // is set on the shared Tooltip); `[&>div>div]` targets the inner text container
63
+ // (where font-size / line-height / min-width are set on the shared Tooltip).
64
+ // `!` is required because the shared Tooltip applies these on the same elements.
65
+ const CssTooltipPopperBase = '[&>div>div]:!text-[14px] [&>div>div]:!leading-[20px] [&>div>div]:!min-w-0';
66
+ const CssTooltipPopperDark = `[&>div]:!text-gray-300 ${CssTooltipPopperBase}`;
67
+ const CssTooltipPopperLight = `[&>div]:!text-gray-700 ${CssTooltipPopperBase}`;
68
+ // `top-start` for flaky: left-aligns the tooltip with the stat so the arrow
69
+ // points at the element rather than at the center of a wide tooltip.
70
+ // `top-end` for the rest (right-aligned stats on the right side of the pill).
71
+ function getTooltipPlacement(key) {
72
+ return key === 'flaky' ? 'top-start' : 'top-end';
73
+ }
74
+ // Convert the API key (camelCase) to the DOM-attribute / display form (kebab-case).
75
+ // Single multi-word case — explicit string match instead of a generic camel→kebab utility.
76
+ function statKeyToKebab(key) {
77
+ return key === 'selfHealed' ? 'self-healed' : key;
78
+ }
79
+ function capitalize(str) {
80
+ return str.charAt(0).toUpperCase() + str.slice(1);
81
+ }
82
+ // Long-form flaky description used in tooltips (always shown regardless of link).
83
+ function getFlakyTooltipText(count) {
84
+ return count === 1
85
+ ? 'This test both passed and failed when retried within a run'
86
+ : `${count} tests both passed and failed when retried within a run`;
87
+ }
88
+ // Tooltip / aria-label text. `isLinked` flips "View X tests" ↔ "X tests".
89
+ // Flaky linked stat uses "View flaky tests"; tooltip content uses getFlakyTooltipText.
90
+ function getTooltipLabel(key, count, isLinked) {
91
+ if (key === 'flaky') {
92
+ return isLinked ? 'View flaky tests' : getFlakyTooltipText(count);
93
+ }
94
+ const display = statKeyToKebab(key);
95
+ return isLinked ? `View ${display} tests` : `${capitalize(display)} tests`;
96
+ }
97
+ // Null-safe count → numeric value for display & visibility logic.
98
+ function statValue(count) {
99
+ return count ?? 0;
100
+ }
101
+ // Should a regular stat render?
102
+ function showRegularStat(count, expanded) {
103
+ return expanded || statValue(count) > 0;
104
+ }
105
+ // Which leading key (if any) gets the separator-after modifier?
106
+ // - selfHealed wins if it would render (it's the second leading stat)
107
+ // - else flaky if it would render
108
+ // - else null (no separator at all)
109
+ // Also returns null when there are no regular stats to follow — keep separators
110
+ // from dangling at the end of the pill.
111
+ //
112
+ // Self-healed renders whenever `showSelfHealed` is true (regardless of count);
113
+ // flaky renders only when its count > 0.
114
+ function getSeparatorAfterKey(props) {
115
+ const showFlaky = statValue(props.flaky) > 0;
116
+ const showSelfHealed = !!props.showSelfHealed;
117
+ if (!showFlaky && !showSelfHealed)
118
+ return null;
119
+ const expanded = !!props.expanded;
120
+ const anyRegular = showRegularStat(props.passed, expanded) ||
121
+ showRegularStat(props.failed, expanded) ||
122
+ showRegularStat(props.skipped, expanded) ||
123
+ showRegularStat(props.pending, expanded);
124
+ if (!anyRegular)
125
+ return null;
126
+ return showSelfHealed ? 'selfHealed' : 'flaky';
127
+ }
128
+ // Does anything render at all? Used to short-circuit to `null` on empty state.
129
+ function hasAnyStat(props) {
130
+ const expanded = !!props.expanded;
131
+ return (statValue(props.flaky) > 0 ||
132
+ !!props.showSelfHealed ||
133
+ showRegularStat(props.passed, expanded) ||
134
+ showRegularStat(props.failed, expanded) ||
135
+ showRegularStat(props.skipped, expanded) ||
136
+ showRegularStat(props.pending, expanded));
137
+ }
138
+
139
+ exports.CssClasses = CssClasses;
140
+ exports.CssTheme = CssTheme;
141
+ exports.CssTooltipPopperDark = CssTooltipPopperDark;
142
+ exports.CssTooltipPopperLight = CssTooltipPopperLight;
143
+ exports.LeadingStatKeys = LeadingStatKeys;
144
+ exports.RegularStatKeys = RegularStatKeys;
145
+ exports.TooltipColorForTheme = TooltipColorForTheme;
146
+ exports.getFlakyTooltipText = getFlakyTooltipText;
147
+ exports.getSeparatorAfterKey = getSeparatorAfterKey;
148
+ exports.getTooltipLabel = getTooltipLabel;
149
+ exports.getTooltipPlacement = getTooltipPlacement;
150
+ exports.hasAnyStat = hasAnyStat;
151
+ exports.showRegularStat = showRegularStat;
152
+ exports.statKeyToKebab = statKeyToKebab;
153
+ exports.statValue = statValue;
154
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../src/index.ts"],"sourcesContent":["// Status keys this component accepts in its API.\n// camelCase to match StatusIcon's multi-word keys (noTests, timedOut, overLimit).\n// `data-cy` attributes use kebab-case (\"self-healed\") — see `statKeyToKebab`.\nexport type StatKey =\n | 'passed'\n | 'failed'\n | 'skipped'\n | 'pending'\n | 'flaky'\n | 'selfHealed'\n\n// Regular stats (the four primary outcomes). Order is fixed.\nexport const RegularStatKeys = [\n 'skipped',\n 'pending',\n 'passed',\n 'failed',\n] as const\nexport type RegularStatKey = (typeof RegularStatKeys)[number]\n\n// Leading stats (rendered before the separator). Order is fixed.\nexport const LeadingStatKeys = ['flaky', 'selfHealed'] as const\nexport type LeadingStatKey = (typeof LeadingStatKeys)[number]\n\nexport const CssClasses = {\n // Outer wrapper. `inline-flex` so the component shrinks to content width.\n container: 'inline-flex pointer-events-auto',\n // The <ul> pill itself. Theme overrides border and text colors via CssTheme.\n list: 'flex items-center text-[14px] leading-[24px] font-medium list-none border rounded-[4px]',\n // Each <li> stat.\n item: 'h-full whitespace-nowrap flex items-center',\n // Inner <a> wrapper for linked stats.\n link: 'flex items-center h-full w-full px-[6px] no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500 focus-visible:outline-offset-0',\n // Inner <span> wrapper for unlinked stats.\n unlinked: 'flex items-center h-full w-full px-[6px]',\n // Icon margin matches source `svg { margin: 0 4px }`.\n icon: 'mx-[4px]',\n // Flaky icon override — drop the yellow background rect (first path in the\n // SVG). Matches the source SCSS's `.flakyIcon svg path:first-child { fill:\n // transparent !important }`. Scoped to this component; the shared\n // IconStatusFlaky is unchanged.\n iconFlaky: 'mx-[4px] [&_path:first-child]:fill-transparent',\n // Self-healed icon override — `IconGeneralSparkleSingleSmall` only ships\n // a `[\"16\"]` variant in the icon registry, but the rest of the stats\n // render at 12px. The icon component IS an <svg>, so `w-3 h-3` on the\n // className overrides the icon's intrinsic `width`/`height` attributes\n // via CSS and pins the rendered size to 12 × 12 — visual consistency\n // without depending on a 12px icon variant that doesn't exist.\n iconSelfHealed: 'mx-[4px] w-3 h-3',\n // Separator after the last leading <li>. Border color comes from CssTheme.\n separatorAfter:\n \"after:content-[''] after:border-r after:h-3 after:mx-1 after:self-center\",\n} as const\n\nexport const CssTheme = {\n light: {\n list: 'bg-white text-gray-700 border-gray-100',\n link: 'text-gray-700 hover:bg-gray-50 focus-visible:bg-gray-50',\n separator: 'after:border-gray-100',\n },\n dark: {\n list: 'bg-gray-1000 text-gray-400 border-gray-800',\n link: 'text-gray-300 hover:bg-gray-900 focus-visible:bg-gray-900',\n separator: 'after:border-gray-800',\n },\n} as const\n\nexport type RunResultsTheme = keyof typeof CssTheme\n\n// Tooltip color contrasts with the surface the pill sits on.\nexport const TooltipColorForTheme: Record<RunResultsTheme, 'light' | 'dark'> = {\n light: 'dark',\n dark: 'light',\n}\n\n// RunResults-specific overrides applied via Tooltip's `popperClassName`:\n// - drop the 160px min-width so the tooltip auto-fits content\n// - shrink text to 14px / 20px (DS body size; shared Tooltip defaults to 16px / 24px)\n// - text color: gray-300 for dark tooltips (on light RunResults), gray-700 for light tooltips (on dark RunResults)\n// `[&>div]` targets the colored container inside the popper (where text color\n// is set on the shared Tooltip); `[&>div>div]` targets the inner text container\n// (where font-size / line-height / min-width are set on the shared Tooltip).\n// `!` is required because the shared Tooltip applies these on the same elements.\nconst CssTooltipPopperBase =\n '[&>div>div]:!text-[14px] [&>div>div]:!leading-[20px] [&>div>div]:!min-w-0'\nexport const CssTooltipPopperDark = `[&>div]:!text-gray-300 ${CssTooltipPopperBase}`\nexport const CssTooltipPopperLight = `[&>div]:!text-gray-700 ${CssTooltipPopperBase}`\n\n// `top-start` for flaky: left-aligns the tooltip with the stat so the arrow\n// points at the element rather than at the center of a wide tooltip.\n// `top-end` for the rest (right-aligned stats on the right side of the pill).\nexport function getTooltipPlacement(key: StatKey): 'top-start' | 'top-end' {\n return key === 'flaky' ? 'top-start' : 'top-end'\n}\n\n// Convert the API key (camelCase) to the DOM-attribute / display form (kebab-case).\n// Single multi-word case — explicit string match instead of a generic camel→kebab utility.\nexport function statKeyToKebab(key: StatKey): string {\n return key === 'selfHealed' ? 'self-healed' : key\n}\n\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1)\n}\n\n// Long-form flaky description used in tooltips (always shown regardless of link).\nexport function getFlakyTooltipText(count: number): string {\n return count === 1\n ? 'This test both passed and failed when retried within a run'\n : `${count} tests both passed and failed when retried within a run`\n}\n\n// Tooltip / aria-label text. `isLinked` flips \"View X tests\" ↔ \"X tests\".\n// Flaky linked stat uses \"View flaky tests\"; tooltip content uses getFlakyTooltipText.\nexport function getTooltipLabel(\n key: StatKey,\n count: number,\n isLinked: boolean,\n): string {\n if (key === 'flaky') {\n return isLinked ? 'View flaky tests' : getFlakyTooltipText(count)\n }\n const display = statKeyToKebab(key)\n return isLinked ? `View ${display} tests` : `${capitalize(display)} tests`\n}\n\nexport interface RunResultsProps {\n passed: number | null\n failed: number | null\n skipped: number | null\n pending: number | null\n flaky?: number | null\n\n // Self-healed (independent of flaky). Rendered whenever `showSelfHealed`\n // is true — the count (including 0, including `null` coerced to 0) is shown\n // verbatim. Consumers set the flag based on whether the run could have\n // self-healed tests at all (e.g. `cy.prompt` was available).\n selfHealed?: number | null\n showSelfHealed?: boolean\n\n theme?: RunResultsTheme\n // When true, regular stats render even with a zero count.\n // Does NOT affect leading stats. Flaky still renders only when its count\n // is > 0; self-healed renders whenever `showSelfHealed` is true (including\n // count 0). See `getSeparatorAfterKey` and `hasAnyStat` for the exact rules.\n expanded?: boolean\n\n links?: Partial<Record<StatKey, string>>\n // Same signature in React and Vue. `children` is whatever the framework\n // renders for the inner icon + count.\n renderLink?: (href: string, children: unknown) => unknown\n\n showTooltip?: boolean\n className?: string\n}\n\n// Null-safe count → numeric value for display & visibility logic.\nexport function statValue(count: number | null | undefined): number {\n return count ?? 0\n}\n\n// Should a regular stat render?\nexport function showRegularStat(\n count: number | null | undefined,\n expanded: boolean,\n): boolean {\n return expanded || statValue(count) > 0\n}\n\n// Which leading key (if any) gets the separator-after modifier?\n// - selfHealed wins if it would render (it's the second leading stat)\n// - else flaky if it would render\n// - else null (no separator at all)\n// Also returns null when there are no regular stats to follow — keep separators\n// from dangling at the end of the pill.\n//\n// Self-healed renders whenever `showSelfHealed` is true (regardless of count);\n// flaky renders only when its count > 0.\nexport function getSeparatorAfterKey(\n props: Pick<\n RunResultsProps,\n | 'flaky'\n | 'selfHealed'\n | 'showSelfHealed'\n | 'passed'\n | 'failed'\n | 'skipped'\n | 'pending'\n | 'expanded'\n >,\n): LeadingStatKey | null {\n const showFlaky = statValue(props.flaky) > 0\n const showSelfHealed = !!props.showSelfHealed\n if (!showFlaky && !showSelfHealed) return null\n\n const expanded = !!props.expanded\n const anyRegular =\n showRegularStat(props.passed, expanded) ||\n showRegularStat(props.failed, expanded) ||\n showRegularStat(props.skipped, expanded) ||\n showRegularStat(props.pending, expanded)\n if (!anyRegular) return null\n\n return showSelfHealed ? 'selfHealed' : 'flaky'\n}\n\n// Does anything render at all? Used to short-circuit to `null` on empty state.\nexport function hasAnyStat(\n props: Pick<\n RunResultsProps,\n | 'flaky'\n | 'selfHealed'\n | 'showSelfHealed'\n | 'passed'\n | 'failed'\n | 'skipped'\n | 'pending'\n | 'expanded'\n >,\n): boolean {\n const expanded = !!props.expanded\n return (\n statValue(props.flaky) > 0 ||\n !!props.showSelfHealed ||\n showRegularStat(props.passed, expanded) ||\n showRegularStat(props.failed, expanded) ||\n showRegularStat(props.skipped, expanded) ||\n showRegularStat(props.pending, expanded)\n )\n}\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@cypress-design/constants-runresults",
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "*"
6
+ ],
7
+ "main": "dist/index.umd.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.es.mjs",
12
+ "require": "./dist/index.umd.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "rollup -c ./rollup.config.mjs"
18
+ },
19
+ "license": "MIT"
20
+ }
@@ -0,0 +1,6 @@
1
+ import rootRollupConfig from '../../const.rollup.config.mjs'
2
+ import pkg from './package.json' with { type: 'json' }
3
+
4
+ export default rootRollupConfig({
5
+ external: Object.keys(pkg.dependencies ?? {}),
6
+ })
package/src/index.ts ADDED
@@ -0,0 +1,230 @@
1
+ // Status keys this component accepts in its API.
2
+ // camelCase to match StatusIcon's multi-word keys (noTests, timedOut, overLimit).
3
+ // `data-cy` attributes use kebab-case ("self-healed") — see `statKeyToKebab`.
4
+ export type StatKey =
5
+ | 'passed'
6
+ | 'failed'
7
+ | 'skipped'
8
+ | 'pending'
9
+ | 'flaky'
10
+ | 'selfHealed'
11
+
12
+ // Regular stats (the four primary outcomes). Order is fixed.
13
+ export const RegularStatKeys = [
14
+ 'skipped',
15
+ 'pending',
16
+ 'passed',
17
+ 'failed',
18
+ ] as const
19
+ export type RegularStatKey = (typeof RegularStatKeys)[number]
20
+
21
+ // Leading stats (rendered before the separator). Order is fixed.
22
+ export const LeadingStatKeys = ['flaky', 'selfHealed'] as const
23
+ export type LeadingStatKey = (typeof LeadingStatKeys)[number]
24
+
25
+ export const CssClasses = {
26
+ // Outer wrapper. `inline-flex` so the component shrinks to content width.
27
+ container: 'inline-flex pointer-events-auto',
28
+ // The <ul> pill itself. Theme overrides border and text colors via CssTheme.
29
+ list: 'flex items-center text-[14px] leading-[24px] font-medium list-none border rounded-[4px]',
30
+ // Each <li> stat.
31
+ item: 'h-full whitespace-nowrap flex items-center',
32
+ // Inner <a> wrapper for linked stats.
33
+ link: 'flex items-center h-full w-full px-[6px] no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-500 focus-visible:outline-offset-0',
34
+ // Inner <span> wrapper for unlinked stats.
35
+ unlinked: 'flex items-center h-full w-full px-[6px]',
36
+ // Icon margin matches source `svg { margin: 0 4px }`.
37
+ icon: 'mx-[4px]',
38
+ // Flaky icon override — drop the yellow background rect (first path in the
39
+ // SVG). Matches the source SCSS's `.flakyIcon svg path:first-child { fill:
40
+ // transparent !important }`. Scoped to this component; the shared
41
+ // IconStatusFlaky is unchanged.
42
+ iconFlaky: 'mx-[4px] [&_path:first-child]:fill-transparent',
43
+ // Self-healed icon override — `IconGeneralSparkleSingleSmall` only ships
44
+ // a `["16"]` variant in the icon registry, but the rest of the stats
45
+ // render at 12px. The icon component IS an <svg>, so `w-3 h-3` on the
46
+ // className overrides the icon's intrinsic `width`/`height` attributes
47
+ // via CSS and pins the rendered size to 12 × 12 — visual consistency
48
+ // without depending on a 12px icon variant that doesn't exist.
49
+ iconSelfHealed: 'mx-[4px] w-3 h-3',
50
+ // Separator after the last leading <li>. Border color comes from CssTheme.
51
+ separatorAfter:
52
+ "after:content-[''] after:border-r after:h-3 after:mx-1 after:self-center",
53
+ } as const
54
+
55
+ export const CssTheme = {
56
+ light: {
57
+ list: 'bg-white text-gray-700 border-gray-100',
58
+ link: 'text-gray-700 hover:bg-gray-50 focus-visible:bg-gray-50',
59
+ separator: 'after:border-gray-100',
60
+ },
61
+ dark: {
62
+ list: 'bg-gray-1000 text-gray-400 border-gray-800',
63
+ link: 'text-gray-300 hover:bg-gray-900 focus-visible:bg-gray-900',
64
+ separator: 'after:border-gray-800',
65
+ },
66
+ } as const
67
+
68
+ export type RunResultsTheme = keyof typeof CssTheme
69
+
70
+ // Tooltip color contrasts with the surface the pill sits on.
71
+ export const TooltipColorForTheme: Record<RunResultsTheme, 'light' | 'dark'> = {
72
+ light: 'dark',
73
+ dark: 'light',
74
+ }
75
+
76
+ // RunResults-specific overrides applied via Tooltip's `popperClassName`:
77
+ // - drop the 160px min-width so the tooltip auto-fits content
78
+ // - shrink text to 14px / 20px (DS body size; shared Tooltip defaults to 16px / 24px)
79
+ // - text color: gray-300 for dark tooltips (on light RunResults), gray-700 for light tooltips (on dark RunResults)
80
+ // `[&>div]` targets the colored container inside the popper (where text color
81
+ // is set on the shared Tooltip); `[&>div>div]` targets the inner text container
82
+ // (where font-size / line-height / min-width are set on the shared Tooltip).
83
+ // `!` is required because the shared Tooltip applies these on the same elements.
84
+ const CssTooltipPopperBase =
85
+ '[&>div>div]:!text-[14px] [&>div>div]:!leading-[20px] [&>div>div]:!min-w-0'
86
+ export const CssTooltipPopperDark = `[&>div]:!text-gray-300 ${CssTooltipPopperBase}`
87
+ export const CssTooltipPopperLight = `[&>div]:!text-gray-700 ${CssTooltipPopperBase}`
88
+
89
+ // `top-start` for flaky: left-aligns the tooltip with the stat so the arrow
90
+ // points at the element rather than at the center of a wide tooltip.
91
+ // `top-end` for the rest (right-aligned stats on the right side of the pill).
92
+ export function getTooltipPlacement(key: StatKey): 'top-start' | 'top-end' {
93
+ return key === 'flaky' ? 'top-start' : 'top-end'
94
+ }
95
+
96
+ // Convert the API key (camelCase) to the DOM-attribute / display form (kebab-case).
97
+ // Single multi-word case — explicit string match instead of a generic camel→kebab utility.
98
+ export function statKeyToKebab(key: StatKey): string {
99
+ return key === 'selfHealed' ? 'self-healed' : key
100
+ }
101
+
102
+ function capitalize(str: string): string {
103
+ return str.charAt(0).toUpperCase() + str.slice(1)
104
+ }
105
+
106
+ // Long-form flaky description used in tooltips (always shown regardless of link).
107
+ export function getFlakyTooltipText(count: number): string {
108
+ return count === 1
109
+ ? 'This test both passed and failed when retried within a run'
110
+ : `${count} tests both passed and failed when retried within a run`
111
+ }
112
+
113
+ // Tooltip / aria-label text. `isLinked` flips "View X tests" ↔ "X tests".
114
+ // Flaky linked stat uses "View flaky tests"; tooltip content uses getFlakyTooltipText.
115
+ export function getTooltipLabel(
116
+ key: StatKey,
117
+ count: number,
118
+ isLinked: boolean,
119
+ ): string {
120
+ if (key === 'flaky') {
121
+ return isLinked ? 'View flaky tests' : getFlakyTooltipText(count)
122
+ }
123
+ const display = statKeyToKebab(key)
124
+ return isLinked ? `View ${display} tests` : `${capitalize(display)} tests`
125
+ }
126
+
127
+ export interface RunResultsProps {
128
+ passed: number | null
129
+ failed: number | null
130
+ skipped: number | null
131
+ pending: number | null
132
+ flaky?: number | null
133
+
134
+ // Self-healed (independent of flaky). Rendered whenever `showSelfHealed`
135
+ // is true — the count (including 0, including `null` coerced to 0) is shown
136
+ // verbatim. Consumers set the flag based on whether the run could have
137
+ // self-healed tests at all (e.g. `cy.prompt` was available).
138
+ selfHealed?: number | null
139
+ showSelfHealed?: boolean
140
+
141
+ theme?: RunResultsTheme
142
+ // When true, regular stats render even with a zero count.
143
+ // Does NOT affect leading stats. Flaky still renders only when its count
144
+ // is > 0; self-healed renders whenever `showSelfHealed` is true (including
145
+ // count 0). See `getSeparatorAfterKey` and `hasAnyStat` for the exact rules.
146
+ expanded?: boolean
147
+
148
+ links?: Partial<Record<StatKey, string>>
149
+ // Same signature in React and Vue. `children` is whatever the framework
150
+ // renders for the inner icon + count.
151
+ renderLink?: (href: string, children: unknown) => unknown
152
+
153
+ showTooltip?: boolean
154
+ className?: string
155
+ }
156
+
157
+ // Null-safe count → numeric value for display & visibility logic.
158
+ export function statValue(count: number | null | undefined): number {
159
+ return count ?? 0
160
+ }
161
+
162
+ // Should a regular stat render?
163
+ export function showRegularStat(
164
+ count: number | null | undefined,
165
+ expanded: boolean,
166
+ ): boolean {
167
+ return expanded || statValue(count) > 0
168
+ }
169
+
170
+ // Which leading key (if any) gets the separator-after modifier?
171
+ // - selfHealed wins if it would render (it's the second leading stat)
172
+ // - else flaky if it would render
173
+ // - else null (no separator at all)
174
+ // Also returns null when there are no regular stats to follow — keep separators
175
+ // from dangling at the end of the pill.
176
+ //
177
+ // Self-healed renders whenever `showSelfHealed` is true (regardless of count);
178
+ // flaky renders only when its count > 0.
179
+ export function getSeparatorAfterKey(
180
+ props: Pick<
181
+ RunResultsProps,
182
+ | 'flaky'
183
+ | 'selfHealed'
184
+ | 'showSelfHealed'
185
+ | 'passed'
186
+ | 'failed'
187
+ | 'skipped'
188
+ | 'pending'
189
+ | 'expanded'
190
+ >,
191
+ ): LeadingStatKey | null {
192
+ const showFlaky = statValue(props.flaky) > 0
193
+ const showSelfHealed = !!props.showSelfHealed
194
+ if (!showFlaky && !showSelfHealed) return null
195
+
196
+ const expanded = !!props.expanded
197
+ const anyRegular =
198
+ showRegularStat(props.passed, expanded) ||
199
+ showRegularStat(props.failed, expanded) ||
200
+ showRegularStat(props.skipped, expanded) ||
201
+ showRegularStat(props.pending, expanded)
202
+ if (!anyRegular) return null
203
+
204
+ return showSelfHealed ? 'selfHealed' : 'flaky'
205
+ }
206
+
207
+ // Does anything render at all? Used to short-circuit to `null` on empty state.
208
+ export function hasAnyStat(
209
+ props: Pick<
210
+ RunResultsProps,
211
+ | 'flaky'
212
+ | 'selfHealed'
213
+ | 'showSelfHealed'
214
+ | 'passed'
215
+ | 'failed'
216
+ | 'skipped'
217
+ | 'pending'
218
+ | 'expanded'
219
+ >,
220
+ ): boolean {
221
+ const expanded = !!props.expanded
222
+ return (
223
+ statValue(props.flaky) > 0 ||
224
+ !!props.showSelfHealed ||
225
+ showRegularStat(props.passed, expanded) ||
226
+ showRegularStat(props.failed, expanded) ||
227
+ showRegularStat(props.skipped, expanded) ||
228
+ showRegularStat(props.pending, expanded)
229
+ )
230
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "include": ["src/*.ts"],
4
+ "compilerOptions": {
5
+ "rootDir": "./src",
6
+ "noEmit": false,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "outDir": "dist",
10
+ "types": []
11
+ }
12
+ }