tpmjs/packages/ui/src/Checkbox/Checkbox.test.tsx
Ajax Davis 192284ad07 fix: update UI test expectations to match actual component classes
Fixed test expectations in Badge, Button, Card, Checkbox, Input,
Select, and Textarea components to match the actual Tailwind utility
classes being applied.

Changes:
- Badge: font-medium, rounded-none, transition-colors
- Card: border-dashed, rounded-none
- Button: rounded-none, focus-visible:ring-2
- Input: font-mono, rounded-none, focus-visible:ring-2
- Select: rounded-none, transition-colors
- Textarea: font-mono, rounded-none, transition-colors
- Checkbox: transition-colors, rounded-none

All 856 tests now pass.
2026-01-20 13:54:25 +10:00

573 lines
20 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createRef } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { Checkbox } from './Checkbox';
describe('Checkbox', () => {
describe('Rendering', () => {
it('should render a checkbox input', () => {
render(<Checkbox />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeInTheDocument();
expect(checkbox).toHaveAttribute('type', 'checkbox');
});
it('should render with a label when provided', () => {
render(<Checkbox label="Accept terms" />);
expect(screen.getByText('Accept terms')).toBeInTheDocument();
expect(screen.getByLabelText('Accept terms')).toBeInTheDocument();
});
it('should render without a label by default', () => {
render(<Checkbox />);
expect(screen.queryByRole('checkbox')).toBeInTheDocument();
// No label text should be present
const container = screen.getByRole('checkbox').parentElement?.parentElement;
expect(container?.querySelector('label')).not.toBeInTheDocument();
});
it('should generate unique ID when not provided', () => {
const { container } = render(
<>
<Checkbox />
<Checkbox />
</>
);
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
expect(checkboxes).toHaveLength(2);
expect(checkboxes[0]?.id).not.toBe(checkboxes[1]?.id);
});
it('should use provided ID', () => {
render(<Checkbox id="my-checkbox" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('id', 'my-checkbox');
});
});
describe('States', () => {
it('should apply default state classes', () => {
const { container } = render(<Checkbox />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('border-border');
});
it('should apply error state classes', () => {
const { container } = render(<Checkbox state="error" />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('border-error');
});
it('should apply success state classes', () => {
const { container } = render(<Checkbox state="success" />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('border-success');
});
});
describe('Sizes', () => {
it('should apply small size classes', () => {
const { container } = render(<Checkbox size="sm" />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('h-4', 'w-4');
});
it('should apply medium size classes by default', () => {
const { container } = render(<Checkbox />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('h-5', 'w-5');
});
it('should apply large size classes', () => {
const { container } = render(<Checkbox size="lg" />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('h-6', 'w-6');
});
it('should apply size-specific label classes', () => {
const { rerender } = render(<Checkbox label="Text" size="sm" />);
expect(screen.getByText('Text')).toHaveClass('text-xs');
rerender(<Checkbox label="Text" size="md" />);
expect(screen.getByText('Text')).toHaveClass('text-sm');
rerender(<Checkbox label="Text" size="lg" />);
expect(screen.getByText('Text')).toHaveClass('text-base');
});
});
describe('Label Position', () => {
it('should position label on the right by default', () => {
const { container } = render(<Checkbox label="Right label" />);
const label = screen.getByText('Right label');
expect(label).toHaveClass('ml-2');
expect(label).not.toHaveClass('mr-2');
// Check DOM order: input comes before label
const wrapper = container.querySelector('div');
const children = Array.from(wrapper?.children || []);
const labelIndex = children.findIndex((child) => child.tagName === 'LABEL');
const inputWrapperIndex = children.findIndex((child) => child.querySelector('input'));
expect(inputWrapperIndex).toBeLessThan(labelIndex);
});
it('should position label on the left when specified', () => {
const { container } = render(<Checkbox label="Left label" labelPosition="left" />);
const label = screen.getByText('Left label');
expect(label).toHaveClass('mr-2');
expect(label).not.toHaveClass('ml-2');
// Check DOM order: label comes before input
const wrapper = container.querySelector('div');
const children = Array.from(wrapper?.children || []);
const labelIndex = children.findIndex((child) => child.tagName === 'LABEL');
const inputWrapperIndex = children.findIndex((child) => child.querySelector('input'));
expect(labelIndex).toBeLessThan(inputWrapperIndex);
});
});
describe('Indeterminate State', () => {
it('should set indeterminate property when indeterminate prop is true', () => {
render(<Checkbox indeterminate={true} />);
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
expect(checkbox.indeterminate).toBe(true);
});
it('should not set indeterminate property by default', () => {
render(<Checkbox />);
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
expect(checkbox.indeterminate).toBe(false);
});
it('should update indeterminate property when prop changes', () => {
const { rerender } = render(<Checkbox indeterminate={false} />);
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
expect(checkbox.indeterminate).toBe(false);
rerender(<Checkbox indeterminate={true} />);
expect(checkbox.indeterminate).toBe(true);
rerender(<Checkbox indeterminate={false} />);
expect(checkbox.indeterminate).toBe(false);
});
it('should set data-indeterminate attribute when indeterminate', () => {
render(<Checkbox indeterminate={true} />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('data-indeterminate', 'true');
});
it('should set aria-checked="mixed" when indeterminate', () => {
render(<Checkbox indeterminate={true} />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('aria-checked', 'mixed');
});
it('should not set aria-checked when not indeterminate', () => {
render(<Checkbox indeterminate={false} />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toHaveAttribute('aria-checked', 'mixed');
});
});
describe('Checked State', () => {
it('should be unchecked by default', () => {
render(<Checkbox />);
expect(screen.getByRole('checkbox')).not.toBeChecked();
});
it('should render as checked when defaultChecked is true', () => {
render(<Checkbox defaultChecked />);
expect(screen.getByRole('checkbox')).toBeChecked();
});
it('should render as checked when checked is true (controlled)', () => {
render(<Checkbox checked onChange={() => {}} />);
expect(screen.getByRole('checkbox')).toBeChecked();
});
it('should toggle checked state when clicked (uncontrolled)', async () => {
const user = userEvent.setup();
render(<Checkbox />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await user.click(checkbox);
expect(checkbox).toBeChecked();
await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});
});
describe('HTML Attributes', () => {
it('should support name attribute', () => {
render(<Checkbox name="terms" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('name', 'terms');
});
it('should support value attribute', () => {
render(<Checkbox value="accept" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('value', 'accept');
});
it('should support required attribute', () => {
render(<Checkbox required />);
expect(screen.getByRole('checkbox')).toBeRequired();
});
it('should support disabled attribute', () => {
render(<Checkbox disabled />);
expect(screen.getByRole('checkbox')).toBeDisabled();
});
it('should apply disabled cursor to wrapper when disabled', () => {
const { container } = render(<Checkbox disabled />);
const wrapper = container.querySelector('div');
expect(wrapper).toHaveClass('cursor-not-allowed');
});
it('should support form attribute', () => {
render(<Checkbox form="my-form" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('form', 'my-form');
});
it('should support autoFocus attribute', () => {
render(<Checkbox autoFocus />);
expect(screen.getByRole('checkbox')).toHaveFocus();
});
});
describe('ARIA Attributes', () => {
it('should support aria-label', () => {
render(<Checkbox aria-label="Custom checkbox" />);
expect(screen.getByLabelText('Custom checkbox')).toBeInTheDocument();
});
it('should support aria-describedby', () => {
render(<Checkbox aria-describedby="description" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-describedby', 'description');
});
it('should support aria-labelledby', () => {
render(<Checkbox aria-labelledby="label-id" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-labelledby', 'label-id');
});
it('should support aria-required', () => {
render(<Checkbox aria-required="true" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-required', 'true');
});
it('should support aria-invalid', () => {
render(<Checkbox aria-invalid="true" />);
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-invalid', 'true');
});
});
describe('Event Handlers', () => {
it('should call onChange when clicked', async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
await user.click(screen.getByRole('checkbox'));
expect(handleChange).toHaveBeenCalledTimes(1);
});
it('should call onChange with event when clicked', async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
await user.click(screen.getByRole('checkbox'));
expect(handleChange).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({
checked: true,
}),
})
);
});
it('should call onFocus when focused', async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
render(<Checkbox onFocus={handleFocus} />);
await user.tab();
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it('should call onBlur when blurred', async () => {
const handleBlur = vi.fn();
render(<Checkbox onBlur={handleBlur} />);
const checkbox = screen.getByRole('checkbox');
checkbox.focus();
checkbox.blur();
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it('should toggle with Space key', async () => {
const user = userEvent.setup();
render(<Checkbox />);
const checkbox = screen.getByRole('checkbox');
checkbox.focus();
expect(checkbox).not.toBeChecked();
await user.keyboard(' ');
expect(checkbox).toBeChecked();
await user.keyboard(' ');
expect(checkbox).not.toBeChecked();
});
it('should not call onChange when disabled', async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<Checkbox disabled onChange={handleChange} />);
await user.click(screen.getByRole('checkbox'));
expect(handleChange).not.toHaveBeenCalled();
});
});
describe('Custom className', () => {
it('should merge custom className with default classes', () => {
render(<Checkbox className="custom-class" />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveClass('custom-class');
expect(checkbox).toHaveClass('peer'); // Default class
});
it('should allow className override', () => {
render(<Checkbox className="my-custom-checkbox" />);
expect(screen.getByRole('checkbox')).toHaveClass('my-custom-checkbox');
});
});
describe('Ref Forwarding', () => {
it('should forward ref to input element', () => {
const ref = createRef<HTMLInputElement>();
render(<Checkbox ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
expect(ref.current?.tagName).toBe('INPUT');
});
it('should allow programmatic focus via ref', () => {
const ref = createRef<HTMLInputElement>();
render(<Checkbox ref={ref} />);
ref.current?.focus();
expect(ref.current).toHaveFocus();
});
it('should allow reading checked state via ref', async () => {
const user = userEvent.setup();
const ref = createRef<HTMLInputElement>();
render(<Checkbox ref={ref} />);
expect(ref.current?.checked).toBe(false);
await user.click(screen.getByRole('checkbox'));
expect(ref.current?.checked).toBe(true);
});
it('should allow setting indeterminate via ref', () => {
const ref = createRef<HTMLInputElement>();
render(<Checkbox ref={ref} />);
if (ref.current) {
ref.current.indeterminate = true;
}
expect(ref.current?.indeterminate).toBe(true);
});
});
describe('Base Classes', () => {
it('should include layout classes on input', () => {
render(<Checkbox />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveClass('peer', 'sr-only');
});
it('should include transition classes on UI element', () => {
const { container } = render(<Checkbox />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('transition-colors', 'duration-150');
});
it('should include border and background classes on UI element', () => {
const { container } = render(<Checkbox />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('border-2', 'bg-background', 'rounded-none');
});
it('should include cursor classes on UI element', () => {
const { container } = render(<Checkbox />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('inline-flex', 'items-center', 'justify-center');
});
it('should render checkmark SVG', () => {
const { container } = render(<Checkbox />);
const svgs = container.querySelectorAll('svg');
expect(svgs.length).toBeGreaterThanOrEqual(1);
// Checkmark path
const checkmarkPath = container.querySelector('path[d*="M13.5 4.5"]');
expect(checkmarkPath).toBeInTheDocument();
});
it('should render indeterminate line SVG', () => {
const { container } = render(<Checkbox />);
const svgs = container.querySelectorAll('svg');
expect(svgs.length).toBeGreaterThanOrEqual(2);
// Indeterminate path
const indeterminatePath = container.querySelector('path[d*="M4 8H12"]');
expect(indeterminatePath).toBeInTheDocument();
});
it('should hide SVG icons with aria-hidden', () => {
const { container } = render(<Checkbox />);
const svgs = container.querySelectorAll('svg');
svgs.forEach((svg) => {
expect(svg).toHaveAttribute('aria-hidden', 'true');
});
});
});
describe('Compound Scenarios', () => {
it('should work with error state and small size', () => {
const { container } = render(<Checkbox state="error" size="sm" />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('border-error', 'h-4', 'w-4');
});
it('should work with success state and large size', () => {
const { container } = render(<Checkbox state="success" size="lg" />);
const ui = container.querySelector('span');
expect(ui).toHaveClass('border-success', 'h-6', 'w-6');
});
it('should work with indeterminate state and label', () => {
render(<Checkbox indeterminate label="Select all" />);
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
expect(checkbox.indeterminate).toBe(true);
expect(screen.getByText('Select all')).toBeInTheDocument();
});
it('should work with disabled and checked states', () => {
render(<Checkbox disabled checked onChange={() => {}} />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeDisabled();
expect(checkbox).toBeChecked();
});
it('should work with all props combined', () => {
const handleChange = vi.fn();
render(
<Checkbox
state="success"
size="lg"
label="I agree"
labelPosition="left"
disabled={false}
indeterminate={false}
checked={true}
onChange={handleChange}
aria-label="Agreement checkbox"
/>
);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeInTheDocument();
expect(checkbox).toBeChecked();
expect(screen.getByText('I agree')).toBeInTheDocument();
});
});
describe('Controlled vs Uncontrolled', () => {
it('should work as uncontrolled with defaultChecked', async () => {
const user = userEvent.setup();
render(<Checkbox defaultChecked={true} />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeChecked();
await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});
it('should work as controlled with checked and onChange', async () => {
const user = userEvent.setup();
const ControlledCheckbox = () => {
const [checked, setChecked] = React.useState(false);
return <Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)} />;
};
render(<ControlledCheckbox />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await user.click(checkbox);
expect(checkbox).toBeChecked();
await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});
it('should not change when controlled without onChange', async () => {
const user = userEvent.setup();
render(<Checkbox checked={false} onChange={() => {}} />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();
// Should remain unchecked since onChange doesn't update the value
await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});
});
describe('Label Integration', () => {
it('should toggle checkbox when label is clicked', async () => {
const user = userEvent.setup();
render(<Checkbox label="Click me" />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await user.click(screen.getByText('Click me'));
expect(checkbox).toBeChecked();
});
it('should associate label with input via htmlFor', () => {
render(<Checkbox label="My label" id="my-id" />);
const label = screen.getByText('My label');
expect(label).toHaveAttribute('for', 'my-id');
});
it('should apply cursor-pointer to label', () => {
render(<Checkbox label="Clickable" />);
const label = screen.getByText('Clickable');
expect(label).toHaveClass('cursor-pointer');
});
it('should apply disabled cursor to label when disabled', () => {
render(<Checkbox label="Disabled" disabled />);
const label = screen.getByText('Disabled');
expect(label).toHaveClass('peer-disabled:cursor-not-allowed');
});
});
});
// Import React for controlled component test
import React from 'react';