import { readFileSync } from 'node:fs'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; import { DonutMetricCard } from '../../src/components/DonutMetricCard/DonutMetricCard'; import { donutArcPath } from '../../src/components/DonutMetricCard/donut-arc'; // Line endings depend on the checkout (CRLF on Windows working copies); the contract is about content. const readStyle = (file: string) => readFileSync(new URL(`../../src/styles/${file}`, import.meta.url), 'utf8').replace(/\r\n/g, '\n'); const styles = readStyle('components.css'); const tileContract = readStyle('tile-contract.css'); const tokens = readStyle('ui-tokens.css'); describe('DonutMetricCard binding design contract', () => { it('renders the shared full-circle reference with a bold count above a regular percentage', () => { const html = renderToStaticMarkup( ); expect(html).toContain('class="donut-metric-card donut-metric-card--deviation"'); expect(html).toMatch(/

Deviation<\/h3>
/); expect(html).toContain('class="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100"'); expect(html).toContain('class="donut-metric-card__progress" d="M 114 60 A 54 54 0 1 1'); expect(html).toContain('
453
'); expect(html).toContain('51%'); expect(styles).toContain('.donut-metric-card__progress {\n fill: currentColor;'); expect(styles).toContain('transform: rotate(-90deg);'); expect(styles).toMatch(/\.donut-metric-card__values\s*\{[^}]*inset:\s*calc\(var\(--donut-ring-stroke\) \+ var\(--space-2\)\);/s); expect(styles).not.toContain('filter: var(--donut-ring-shadow);'); expect(styles).toMatch(/\.donut-metric-card__primary strong\s*\{[^}]*font-weight:\s*var\(--fw-bold\);/s); expect(styles).toMatch(/\.donut-metric-card__values > small\s*\{[^}]*font-weight:\s*var\(--fw-regular\);/s); expect(styles).not.toContain('stroke-linecap: round;'); expect(styles).toContain('@keyframes donut-progress-build'); expect(styles).not.toContain('.donut-metric-card::before'); expect(styles).not.toContain('@keyframes donut-accent-build'); expect(styles).toContain('animation: donut-progress-build var(--dur-data-build) var(--ease-data-build) both;'); expect(styles).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s); expect(tileContract).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s); expect(tokens).toContain('--dur-data-build: 700ms;'); expect(tokens).toContain('--donut-value-size: 26px;'); expect(tokens).not.toContain('--donut-ring-shadow:'); expect(tokens).toContain('--donut-percent-size: 14px;'); expect(tokens).toContain('--donut-compact-value-size: 22px;'); expect(tokens).toContain('--donut-compact-percent-size: 12px;'); expect(styles).not.toContain('.sidebar-severity-card'); }); it('renders an empty progress arc at zero and clamps percentages', () => { const zero = renderToStaticMarkup( ); const clamped = renderToStaticMarkup( ); expect(zero).not.toContain('donut-metric-card__progress'); expect(zero).toContain('2'); expect(zero).toContain('0%'); expect(clamped).toContain(`d="${donutArcPath(100)}"`); expect(clamped).toContain('885'); expect(clamped).toContain('100%'); expect(zero).not.toContain(' { const tiny = renderToStaticMarkup( ); const onePercent = renderToStaticMarkup( ); expect(tiny).toContain(`d="${donutArcPath(4 / 1833 * 100)}"`); expect(tiny).toContain('donut-small-arc-marker'); expect(tiny).toMatch(/class="donut-small-arc-marker" x="102" y="60"/); expect(tiny).toContain('<1%'); expect(onePercent).toContain(`d="${donutArcPath(1)}"`); expect(onePercent).toContain('donut-small-arc-marker'); expect(onePercent).toContain('1%'); }); it('draws flat radial ends at 12 o’clock for small arcs and exact adjacent boundaries for segments', () => { const tenPercent = donutArcPath(10); expect(tenPercent).toMatch(/^M 114 60 A 54 54 0 0 1 /); expect(tenPercent).toMatch(/A 42 42 0 0 0 102 60 Z$/); expect(donutArcPath(0)).toBe(''); expect(donutArcPath(25, 10)).toContain('A 54 54 0 0 1'); expect(donutArcPath(100)).toContain('A 54 54 0 0 1 6 60'); }); it('distinguishes a pending metric from an observed zero', () => { const pending = renderToStaticMarkup( ); expect(pending).toContain('aria-label="Deviation: Loading…"'); expect(pending).toContain('—'); expect(pending).toContain('Loading…'); expect(pending).not.toContain('donut-metric-card__progress'); expect(pending).not.toContain('0%'); }); it('supports a single suffixed score without a duplicate percentage label', () => { const html = renderToStaticMarkup( ); expect(html).toContain('36%'); expect(html).not.toContain('36%'); }); it('becomes an accessible active filter control when clickable', () => { const html = renderToStaticMarkup( undefined} className="test-donut-card" /> ); expect(html).toContain('type="button"'); expect(html).toContain('aria-pressed="true"'); expect(html).toContain('donut-metric-card--high test-donut-card'); }); });