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