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');
});
});