@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.
- package/.turbo/turbo-build.log +3 -0
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.mjs +138 -0
- package/dist/index.es.mjs.map +1 -0
- package/dist/index.umd.js +154 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +20 -0
- package/rollup.config.mjs +6 -0
- package/src/index.ts +230 -0
- package/tsconfig.json +12 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|