import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import { ProgressCircle } from './progress-circle'; const indicatorOf = (container: HTMLElement) => container.querySelector('[data-slot="progress-circle-indicator"]')!; describe('ProgressCircle', () => { it('reports its value to assistive technology', () => { render(); const bar = screen.getByRole('progressbar'); expect(bar).toHaveAttribute('aria-valuenow', '72'); expect(bar).toHaveAttribute('aria-valuemax', '100'); }); it('prints the percentage in the middle by default', () => { render(); expect(screen.getByText('72%')).toBeInTheDocument(); }); /* * A 24px ring has 16px of clear space inside its own stroke, so "72%" at * CBAR's 12px spills over the ring rather than sitting in it. Hiding the * label is the only thing that keeps those two sizes usable. */ it('drops the label at the two sizes it cannot fit in', () => { const { rerender } = render(); expect(screen.queryByText('72%')).not.toBeInTheDocument(); rerender(); expect(screen.queryByText('72%')).not.toBeInTheDocument(); rerender(); expect(screen.getByText('72%')).toBeInTheDocument(); }); it('still prints the label at a small size when asked explicitly', () => { render( v} />); expect(screen.getByText('9')).toBeInTheDocument(); }); it('takes a custom label formatter', () => { render( `${v}/100`} />); expect(screen.getByText('72/100')).toBeInTheDocument(); expect(screen.queryByText('72%')).not.toBeInTheDocument(); }); /* * The arc is drawn with `stroke-dashoffset`, so the offset *is* the geometry. * At 0 the ring must be fully open and at 100 fully closed — a sign error * here still renders a plausible-looking ring, just backwards. */ it('closes the arc as the value climbs', () => { const { container, rerender } = render(); const circumference = Number(indicatorOf(container).getAttribute('stroke-dasharray')); expect(Number(indicatorOf(container).getAttribute('stroke-dashoffset'))).toBeCloseTo( circumference ); rerender(); expect(Number(indicatorOf(container).getAttribute('stroke-dashoffset'))).toBeCloseTo(0); rerender(); expect(Number(indicatorOf(container).getAttribute('stroke-dashoffset'))).toBeCloseTo( circumference / 2 ); }); it('clamps a value outside 0–100 rather than overdrawing the ring', () => { const { container } = render(); expect(Number(indicatorOf(container).getAttribute('stroke-dashoffset'))).toBeCloseTo(0); expect(screen.getByText('100%')).toBeInTheDocument(); }); /* Radix marks a value-less progressbar indeterminate; the ring answers with a quarter arc that spins, and drops the label since there is nothing to say. */ it('spins without a value instead of showing an empty ring', () => { const { container } = render(); expect(container.querySelector('svg')).toHaveClass('animate-spin'); expect(screen.queryByText(/%$/)).not.toBeInTheDocument(); }); it('keeps the ring inside the box at every size', () => { for (const [size, box, stroke] of [ ['xs', 24, 4], ['md', 40, 6], ['xl', 64, 8], ] as const) { const { container, unmount } = render(); const circle = indicatorOf(container); /* Stroke is centred on the path, so radius + half the stroke must not exceed half the viewBox or the ring is clipped. */ const r = Number(circle.getAttribute('r')); expect(r + stroke / 2).toBeLessThanOrEqual(box / 2); unmount(); } }); it('applies the palette the ring colours are read from', () => { render(); expect(screen.getByRole('progressbar')).toHaveClass('palette-green'); }); });