Assessor_2.0/tests/components/donut-metric-card.test.tsx

128 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(
<DonutMetricCard label="Deviation" value={453} percentage={51} tone="deviation" />
);
expect(html).toContain('class="donut-metric-card donut-metric-card--deviation"');
expect(html).toMatch(/<h3 class="donut-metric-card__title">Deviation<\/h3><div class="donut-metric-card__plot">/);
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('<div class="donut-metric-card__primary"><strong>453</strong></div>');
expect(html).toContain('<small>51%</small>');
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(
<DonutMetricCard label="Error" value={2} percentage={0} tone="error" />
);
const clamped = renderToStaticMarkup(
<DonutMetricCard label="Results All" value={885} percentage={120} tone="all" />
);
expect(zero).not.toContain('donut-metric-card__progress');
expect(zero).toContain('<strong>2</strong>');
expect(zero).toContain('<small>0%</small>');
expect(clamped).toContain(`d="${donutArcPath(100)}"`);
expect(clamped).toContain('<strong>885</strong>');
expect(clamped).toContain('<small>100%</small>');
expect(zero).not.toContain('<animate');
expect(zero).not.toContain('donut-small-arc-marker');
});
it('keeps a tiny positive arc proportional and marks it legibly at compact sizes', () => {
const tiny = renderToStaticMarkup(
<DonutMetricCard label="Not Applicable" value={4} percentage={4 / 1833 * 100} tone="not-applicable" />
);
const onePercent = renderToStaticMarkup(
<DonutMetricCard label="Not Applicable" value={4} percentage={1} tone="not-applicable" />
);
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('<small>&lt;1%</small>');
expect(onePercent).toContain(`d="${donutArcPath(1)}"`);
expect(onePercent).toContain('donut-small-arc-marker');
expect(onePercent).toContain('<small>1%</small>');
});
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(
<DonutMetricCard label="Deviation" value={null} percentage={0} tone="deviation" pendingLabel="Loading…" />
);
expect(pending).toContain('aria-label="Deviation: Loading…"');
expect(pending).toContain('<strong>—</strong>');
expect(pending).toContain('<small>Loading…</small>');
expect(pending).not.toContain('donut-metric-card__progress');
expect(pending).not.toContain('<small>0%</small>');
});
it('supports a single suffixed score without a duplicate percentage label', () => {
const html = renderToStaticMarkup(
<DonutMetricCard label="Compliance Score" value={36} valueSuffix="%" showPercentage={false} percentage={36} tone="all" />
);
expect(html).toContain('<strong>36%</strong>');
expect(html).not.toContain('<small>36%</small>');
});
it('becomes an accessible active filter control when clickable', () => {
const html = renderToStaticMarkup(
<DonutMetricCard
label="High"
value={12}
percentage={24}
tone="high"
active
onClick={() => 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');
});
});