import { describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { iconSizes } from '@/lib/cva-presets';
import { Button } from './button';
describe('Button', () => {
it('renders as a button element by default', () => {
render();
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
});
it('calls onClick when pressed', async () => {
const onClick = vi.fn();
render();
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(onClick).toHaveBeenCalledOnce();
});
it('does not fire onClick while disabled', async () => {
const onClick = vi.fn();
render(
);
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(onClick).not.toHaveBeenCalled();
});
it('renders the child element when asChild is set', () => {
render(
);
const link = screen.getByRole('link', { name: 'Docs' });
expect(link).toHaveAttribute('href', '/docs');
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('lets a consumer className override a built-in utility', () => {
render();
// tailwind-merge should drop the variant's own background for the passed one.
expect(screen.getByRole('button')).toHaveClass('bg-red-500');
expect(screen.getByRole('button')).not.toHaveClass('bg-(--ctl-solid)');
});
describe('size ladder', () => {
/*
* Measured off CBAR's Button component set (node 2446:6959) rather than
* inferred: 32 / 40 / 48 / 56px tall, with the icon stepping 16 → 24px at
* `md`. Nothing pinned these before, so a refactor of the presets could move
* the whole ladder and still go green.
*/
it.each([
['xs', 'h-8', iconSizes.base],
['sm', 'h-9', iconSizes.base],
['md', 'h-10', iconSizes.lg],
['lg', 'h-12', iconSizes.lg],
['xl', 'h-14', iconSizes.lg],
] as const)('%s carries CBAR’s height and icon size', (size, height, icon) => {
render();
const button = screen.getByRole('button');
expect(button).toHaveClass(height);
expect(button).toHaveClass(icon);
});
/* The pixel values the two rungs stand for. Asserted apart from the wiring
above so a reworded selector and a wrong size fail differently. */
it('draws icons at 16px and 24px', () => {
expect(iconSizes.base).toContain('size-4');
expect(iconSizes.lg).toContain('size-6');
});
/* The size rule lives on each rung rather than on the base string, so an
icon-only button has to state its own — otherwise it falls through to the
Icon chassis' 24px default. */
it.each(['icon-xs', 'icon-sm', 'icon', 'icon-lg'] as const)(
'%s holds a 16px glyph',
(size) => {
render(
);
expect(screen.getByRole('button')).toHaveClass(iconSizes.base);
}
);
});
describe('colour axes', () => {
/*
* Two ways to colour a button coexist: CBAR's composable `variant` ×
* `colorPalette`, and the one-word shorthands kept from the kit's original
* API. The shorthands paint from the semantic roles instead of the palette
* roles, which is what stops the two from fighting over CSS specificity —
* so the thing worth pinning down is that they stay on separate tracks.
*/
it('applies the palette class alongside the treatment', () => {
render(
);
const button = screen.getByRole('button');
expect(button).toHaveClass('palette-tertiary');
expect(button).toHaveClass('border-(--ctl-border)');
});
/* `third` was the palette's name before the Figma variables became
readable and turned out to say `brand/tertiary`. It stays as an alias for
one major so a consumer's markup keeps rendering. */
it('still accepts the deprecated "third" spelling', () => {
render();
expect(screen.getByRole('button')).toHaveClass('palette-third');
});
it('defaults to a solid button on the primary palette', () => {
render();
const button = screen.getByRole('button');
expect(button).toHaveClass('palette-primary');
expect(button).toHaveClass('bg-(--ctl-solid)');
});
it('paints a shorthand from the semantic roles, not the palette', () => {
render();
const button = screen.getByRole('button');
expect(button).toHaveClass('bg-destructive');
/* The palette class still lands — colorPalette has its own default — but
the shorthand must not read from it, or the two would race. */
expect(button).not.toHaveClass('bg-(--ctl-solid)');
});
it('leaves a shorthand alone when a palette is also passed', () => {
render(
);
// Documented behaviour: a shorthand pins its colour and ignores the palette.
expect(screen.getByRole('button')).toHaveClass('bg-destructive');
});
});
});