import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { axe } from 'vitest-axe'
import { Meter, MeterLabel, MeterValue, MeterProgress } from './meter'
describe('Meter', () => {
it('renders with role="meter" and value/min/max attrs', () => {
render(
,
)
const el = screen.getByRole('meter')
expect(el).toHaveAttribute('data-slot', 'meter')
expect(el).toHaveAttribute('aria-valuenow', '60')
expect(el).toHaveAttribute('aria-valuemin', '0')
expect(el).toHaveAttribute('aria-valuemax', '100')
})
it('renders MeterProgress with Track and Indicator; indicator width set by Base UI', () => {
const { container } = render(
,
)
const track = container.querySelector('[data-slot=meter-progress]') as HTMLElement
const indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(track).toBeInTheDocument()
expect(indicator).toBeInTheDocument()
expect(indicator.style.width).toBe('50%')
})
it('forwards className alongside the layout classes', () => {
render(
,
)
const root = screen.getByRole('meter')
expect(root.className).toContain('custom-class')
expect(root.className).toContain('grid')
})
it('links MeterLabel to root via aria-labelledby', () => {
render(
Storage
,
)
const root = screen.getByRole('meter')
const labelId = root.getAttribute('aria-labelledby')
expect(labelId).toBeTruthy()
expect(document.getElementById(labelId!)?.textContent).toBe('Storage')
})
it('renders the formatted value via MeterValue', () => {
render(
,
)
expect(screen.getByText('42%')).toBeInTheDocument()
})
it('does not set data-status and uses bg-primary when no thresholds are provided', () => {
const { container } = render(
,
)
const root = screen.getByRole('meter')
expect(root.hasAttribute('data-status')).toBe(false)
const indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(indicator.className).toContain('bg-primary')
})
describe('threshold coloring (optimum in low region: optimum=10, low=30, high=70)', () => {
it.each([
{ value: 20, status: 'optimum', bg: 'bg-success-emphasis' },
{ value: 50, status: 'suboptimum', bg: 'bg-warning-emphasis' },
{ value: 90, status: 'invalid', bg: 'bg-error-emphasis' },
])('value=$value → data-status=$status, indicator has $bg', ({ value, status, bg }) => {
const { container } = render(
,
)
expect(screen.getByRole('meter')).toHaveAttribute('data-status', status)
const indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(indicator.className).toContain(bg)
})
})
describe('threshold coloring (optimum in mid region: optimum=50, low=30, high=70)', () => {
it.each([
{ value: 20, status: 'suboptimum', bg: 'bg-warning-emphasis' },
{ value: 50, status: 'optimum', bg: 'bg-success-emphasis' },
{ value: 90, status: 'suboptimum', bg: 'bg-warning-emphasis' },
])('value=$value → data-status=$status, indicator has $bg', ({ value, status, bg }) => {
const { container } = render(
,
)
expect(screen.getByRole('meter')).toHaveAttribute('data-status', status)
const indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(indicator.className).toContain(bg)
})
})
describe('threshold coloring (optimum in high region: optimum=90, low=30, high=70)', () => {
it.each([
{ value: 20, status: 'invalid', bg: 'bg-error-emphasis' },
{ value: 50, status: 'suboptimum', bg: 'bg-warning-emphasis' },
{ value: 90, status: 'optimum', bg: 'bg-success-emphasis' },
])('value=$value → data-status=$status, indicator has $bg', ({ value, status, bg }) => {
const { container } = render(
,
)
expect(screen.getByRole('meter')).toHaveAttribute('data-status', status)
const indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(indicator.className).toContain(bg)
})
})
it('statusClassNames overrides the per-status default class', () => {
const { container, rerender } = render(
,
)
let indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(indicator.className).toContain('bg-emerald-500')
expect(indicator.className).not.toContain('bg-success-emphasis')
// Other statuses keep their defaults
rerender(
,
)
indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(indicator.className).toContain('bg-warning-emphasis')
expect(indicator.className).not.toContain('bg-emerald-500')
})
it('statusClassNames.default overrides the no-threshold class', () => {
const { container } = render(
,
)
const indicator = container.querySelector('[data-slot=meter-indicator]') as HTMLElement
expect(indicator.className).toContain('bg-sky-500')
expect(indicator.className).not.toContain('bg-primary')
})
it('has no accessibility violations', async () => {
const { container } = render(
Score
,
)
expect(await axe(container)).toHaveNoViolations()
})
})