test: remove legacy tests and add new ones
This commit is contained in:
@@ -1,38 +1,3 @@
|
|||||||
/**
|
|
||||||
* Test Suite for ComboControl Component
|
|
||||||
*
|
|
||||||
* IMPORTANT: These tests require a proper browser environment to run.
|
|
||||||
*
|
|
||||||
* Svelte 5's $state() and $effect() runes do not work in jsdom (server-side simulation).
|
|
||||||
* The current vitest.config.component.ts uses 'environment: jsdom', which doesn't support Svelte 5 reactivity.
|
|
||||||
*
|
|
||||||
* To run these tests, you need to:
|
|
||||||
* 1. Update vitest to use browser-based testing with @vitest/browser-playwright
|
|
||||||
* 2. OR use Playwright E2E tests in e2e/ComboControl.e2e.test.ts
|
|
||||||
*
|
|
||||||
* To run E2E tests (recommended):
|
|
||||||
* ```bash
|
|
||||||
* yarn test:e2e ComboControl
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* This suite tests the actual Svelte component rendering, interactions, and behavior.
|
|
||||||
* Tests for the createTypographyControl helper function are in createTypographyControl.test.ts
|
|
||||||
*
|
|
||||||
* Test Coverage:
|
|
||||||
* 1. Component Rendering: Button labels, icons, and initial state
|
|
||||||
* 2. Button States: Disabled states based on isAtMin/isAtMax
|
|
||||||
* 3. Button Clicks: Increase/decrease button functionality
|
|
||||||
* 4. Popover Behavior: Opening/closing popover with slider and input
|
|
||||||
* 5. Slider Interaction: Dragging slider to update values
|
|
||||||
* 6. Input Field: Typing values directly
|
|
||||||
* 7. Accessibility: ARIA labels and keyboard navigation
|
|
||||||
* 8. Reactivity: Value updates propagating through the component
|
|
||||||
* 9. Edge Cases: Boundary conditions and special values
|
|
||||||
*
|
|
||||||
* Note: This file is intentionally left as-is with comprehensive @testing-library/svelte tests
|
|
||||||
* as a reference for when the browser environment is properly set up.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createTypographyControl } from '$shared/lib';
|
import { createTypographyControl } from '$shared/lib';
|
||||||
import {
|
import {
|
||||||
fireEvent,
|
fireEvent,
|
||||||
@@ -40,837 +5,131 @@ import {
|
|||||||
screen,
|
screen,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from '@testing-library/svelte';
|
} from '@testing-library/svelte';
|
||||||
import {
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
it,
|
|
||||||
} from 'vitest';
|
|
||||||
import ComboControl from './ComboControl.svelte';
|
import ComboControl from './ComboControl.svelte';
|
||||||
|
|
||||||
describe('ComboControl Component', () => {
|
function makeControl(value: number, opts: { min?: number; max?: number; step?: number } = {}) {
|
||||||
/**
|
|
||||||
* Helper function to create a TypographyControl for testing
|
|
||||||
*/
|
|
||||||
function createTestControl(initialValue: number, options?: {
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
step?: number;
|
|
||||||
}) {
|
|
||||||
return createTypographyControl({
|
return createTypographyControl({
|
||||||
value: initialValue,
|
value,
|
||||||
min: options?.min ?? 0,
|
min: opts.min ?? 0,
|
||||||
max: options?.max ?? 100,
|
max: opts.max ?? 100,
|
||||||
step: options?.step ?? 1,
|
step: opts.step ?? 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('ComboControl', () => {
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('renders all three buttons (decrease, control, increase)', () => {
|
it('renders decrease and increase buttons', () => {
|
||||||
const control = createTestControl(50);
|
render(ComboControl, { control: makeControl(50) });
|
||||||
render(ComboControl, {
|
expect(screen.getByLabelText('Decrease')).toBeInTheDocument();
|
||||||
control,
|
expect(screen.getByLabelText('Increase')).toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = screen.getAllByRole('button');
|
|
||||||
expect(buttons).toHaveLength(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays current value on control button', () => {
|
|
||||||
const control = createTestControl(42);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the current integer value', () => {
|
||||||
|
render(ComboControl, { control: makeControl(42) });
|
||||||
expect(screen.getByText('42')).toBeInTheDocument();
|
expect(screen.getByText('42')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays decimal values on control button', () => {
|
it('formats decimal value to 1 decimal place when step >= 0.1', () => {
|
||||||
const control = createTestControl(12.5, { min: 0, max: 100, step: 0.5 });
|
render(ComboControl, { control: makeControl(1.5, { step: 0.1 }) });
|
||||||
render(ComboControl, {
|
expect(screen.getByText('1.5')).toBeInTheDocument();
|
||||||
control,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText('12.5')).toBeInTheDocument();
|
it('formats decimal value to 2 decimal places when step < 0.1', () => {
|
||||||
|
render(ComboControl, { control: makeControl(1.55, { step: 0.01 }) });
|
||||||
|
expect(screen.getByText('1.55')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies custom ARIA labels to buttons', () => {
|
it('renders label when label prop is provided', () => {
|
||||||
const control = createTestControl(50);
|
render(ComboControl, { control: makeControl(16), label: 'Size' });
|
||||||
|
expect(screen.getByText('Size')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies custom aria-labels to buttons', () => {
|
||||||
render(ComboControl, {
|
render(ComboControl, {
|
||||||
control,
|
control: makeControl(50),
|
||||||
decreaseLabel: 'Decrease font size',
|
decreaseLabel: 'Decrease font size',
|
||||||
controlLabel: 'Font size control',
|
|
||||||
increaseLabel: 'Increase font size',
|
increaseLabel: 'Increase font size',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByLabelText('Decrease font size')).toBeInTheDocument();
|
expect(screen.getByLabelText('Decrease font size')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('Font size control')).toBeInTheDocument();
|
|
||||||
expect(screen.getByLabelText('Increase font size')).toBeInTheDocument();
|
expect(screen.getByLabelText('Increase font size')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders decrease button with minus icon', () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const decreaseBtn = screen.getAllByRole('button')[0];
|
describe('Disabled state', () => {
|
||||||
expect(decreaseBtn).toBeInTheDocument();
|
it('disables decrease button when at min', () => {
|
||||||
// Check for lucide icon SVG
|
render(ComboControl, { control: makeControl(0, { min: 0 }) });
|
||||||
const svg = container.querySelector('button svg');
|
expect(screen.getByLabelText('Decrease')).toBeDisabled();
|
||||||
expect(svg).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders increase button with plus icon', () => {
|
it('disables increase button when at max', () => {
|
||||||
const control = createTestControl(50);
|
render(ComboControl, { control: makeControl(100, { max: 100 }) });
|
||||||
const { container } = render(ComboControl, {
|
expect(screen.getByLabelText('Increase')).toBeDisabled();
|
||||||
control,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const increaseBtn = screen.getAllByRole('button')[2];
|
it('enables both buttons when value is within bounds', () => {
|
||||||
expect(increaseBtn).toBeInTheDocument();
|
render(ComboControl, { control: makeControl(50, { min: 0, max: 100 }) });
|
||||||
// Check for lucide icon SVG
|
expect(screen.getByLabelText('Decrease')).not.toBeDisabled();
|
||||||
const svgs = container.querySelectorAll('button svg');
|
expect(screen.getByLabelText('Increase')).not.toBeDisabled();
|
||||||
expect(svgs.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles zero value correctly', () => {
|
|
||||||
const control = createTestControl(0, { min: 0, max: 100 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('0')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles negative values correctly', () => {
|
|
||||||
const control = createTestControl(-5, { min: -10, max: 10 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('-5')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Button States', () => {
|
describe('Click interactions', () => {
|
||||||
it('disables decrease button when at min value', () => {
|
it('decreases value on decrease button click', async () => {
|
||||||
const control = createTestControl(0, { min: 0, max: 100 });
|
const control = makeControl(50, { step: 5 });
|
||||||
const { container } = render(ComboControl, {
|
render(ComboControl, { control });
|
||||||
control,
|
await fireEvent.click(screen.getByLabelText('Decrease'));
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = container.querySelectorAll('button');
|
|
||||||
const decreaseBtn = buttons[0];
|
|
||||||
expect(decreaseBtn).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables increase button when at max value', () => {
|
|
||||||
const control = createTestControl(100, { min: 0, max: 100 });
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = container.querySelectorAll('button');
|
|
||||||
const increaseBtn = buttons[2];
|
|
||||||
expect(increaseBtn).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('both buttons enabled when within bounds', () => {
|
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = container.querySelectorAll('button');
|
|
||||||
expect(buttons[0]).not.toBeDisabled(); // decrease
|
|
||||||
expect(buttons[1]).not.toBeDisabled(); // control
|
|
||||||
expect(buttons[2]).not.toBeDisabled(); // increase
|
|
||||||
});
|
|
||||||
|
|
||||||
it('control button always enabled regardless of value', () => {
|
|
||||||
const control = createTestControl(0, { min: 0, max: 0 });
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = container.querySelectorAll('button');
|
|
||||||
const controlBtn = buttons[1];
|
|
||||||
expect(controlBtn).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Button Clicks', () => {
|
|
||||||
it('decrease button reduces value by step', async () => {
|
|
||||||
const control = createTestControl(50, { min: 0, max: 100, step: 5 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const decreaseBtn = screen.getAllByRole('button')[0];
|
|
||||||
await fireEvent.click(decreaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBe(45);
|
expect(control.value).toBe(45);
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('45')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('increase button increases value by step', async () => {
|
it('increases value on increase button click', async () => {
|
||||||
const control = createTestControl(50, { min: 0, max: 100, step: 5 });
|
const control = makeControl(50, { step: 5 });
|
||||||
render(ComboControl, {
|
render(ComboControl, { control });
|
||||||
control,
|
await fireEvent.click(screen.getByLabelText('Increase'));
|
||||||
});
|
|
||||||
|
|
||||||
const increaseBtn = screen.getAllByRole('button')[2];
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBe(55);
|
expect(control.value).toBe(55);
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('55')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('value updates on control button after multiple clicks', async () => {
|
it('clamps decrease at min', async () => {
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
const control = makeControl(2, { min: 0, step: 5 });
|
||||||
render(ComboControl, {
|
render(ComboControl, { control });
|
||||||
control,
|
await fireEvent.click(screen.getByLabelText('Decrease'));
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = screen.getAllByRole('button');
|
|
||||||
const decreaseBtn = buttons[0];
|
|
||||||
const increaseBtn = buttons[2];
|
|
||||||
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
expect(control.value).toBe(53);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('53')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await fireEvent.click(decreaseBtn);
|
|
||||||
expect(control.value).toBe(52);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('52')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('decrease button does not go below min', async () => {
|
|
||||||
const control = createTestControl(1, { min: 0, max: 100, step: 5 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const decreaseBtn = screen.getAllByRole('button')[0];
|
|
||||||
await fireEvent.click(decreaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBe(0);
|
expect(control.value).toBe(0);
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('0')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('increase button does not go above max', async () => {
|
it('clamps increase at max', async () => {
|
||||||
const control = createTestControl(99, { min: 0, max: 100, step: 5 });
|
const control = makeControl(98, { max: 100, step: 5 });
|
||||||
render(ComboControl, {
|
render(ComboControl, { control });
|
||||||
control,
|
await fireEvent.click(screen.getByLabelText('Increase'));
|
||||||
});
|
|
||||||
|
|
||||||
const increaseBtn = screen.getAllByRole('button')[2];
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBe(100);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('100')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('respects step precision on button clicks', async () => {
|
|
||||||
const control = createTestControl(5.5, { min: 0, max: 10, step: 0.25 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const increaseBtn = screen.getAllByRole('button')[2];
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBeCloseTo(5.75);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('5.75')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Popover Behavior', () => {
|
|
||||||
it('popover content not visible initially', () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Popover content should not be visible initially
|
|
||||||
const popover = screen.queryByTestId('combo-control-popover');
|
|
||||||
expect(popover).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
const sliderInput = screen.queryByRole('slider');
|
|
||||||
expect(sliderInput).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
const numberInput = screen.queryByTestId('combo-control-input');
|
|
||||||
expect(numberInput).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clicking control button toggles popover', async () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
|
|
||||||
// Click to open popover
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
// Wait for popover to render (it's portaled to body)
|
|
||||||
await waitFor(() => {
|
|
||||||
const popover = screen.getByTestId('combo-control-popover');
|
|
||||||
expect(popover).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const slider = screen.queryByRole('slider');
|
|
||||||
expect(slider).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const numberInput = screen.queryByTestId('combo-control-input');
|
|
||||||
expect(numberInput).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('popover contains slider and input', async () => {
|
|
||||||
const control = createTestControl(50, { min: 10, max: 90, step: 5 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
// Verify both slider and input are present
|
|
||||||
const slider = await screen.findByRole('slider');
|
|
||||||
expect(slider).toBeInTheDocument();
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input');
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Both should show current value
|
|
||||||
const inputElement = input as HTMLInputElement;
|
|
||||||
expect(inputElement.value).toBe('50');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('popover contains input field with current value', async () => {
|
|
||||||
const control = createTestControl(42);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
await waitFor(async () => {
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
expect(input.value).toBe('42');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('input field has min/max attributes', async () => {
|
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
await waitFor(async () => {
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toHaveAttribute('min', '0');
|
|
||||||
expect(input).toHaveAttribute('max', '100');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Slider Rendering', () => {
|
|
||||||
it('slider is present in popover', async () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
// Verify slider is present
|
|
||||||
const slider = await screen.findByRole('slider');
|
|
||||||
expect(slider).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('slider value syncs with control value', async () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
// Slider should be present and reflect initial value
|
|
||||||
const slider = await screen.findByRole('slider');
|
|
||||||
expect(slider).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Change value via input (which we know works)
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
await fireEvent.change(input, { target: { value: '75' } });
|
|
||||||
await fireEvent.blur(input);
|
|
||||||
|
|
||||||
// Slider should still be present (not re-rendered)
|
|
||||||
const sliderAfter = await screen.findByRole('slider');
|
|
||||||
expect(sliderAfter).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Input Field Interaction', () => {
|
|
||||||
it('typing valid number updates control value', async () => {
|
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Type new value
|
|
||||||
await fireEvent.change(input, { target: { value: '75' } });
|
|
||||||
await fireEvent.blur(input); // onchange fires on blur
|
|
||||||
|
|
||||||
// Wait for control value to update
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(control.value).toBe(75);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check that control button text updates
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('75')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('input respects step precision', async () => {
|
|
||||||
const control = createTestControl(5, { min: 0, max: 10, step: 0.25 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Type value with more precision than step allows (0.25 has 2 decimal places)
|
|
||||||
await fireEvent.change(input, { target: { value: '5.23' } });
|
|
||||||
await fireEvent.blur(input);
|
|
||||||
|
|
||||||
// Should be rounded to step precision (2 decimal places)
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(control.value).toBeCloseTo(5.23, 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('input clamps to min', async () => {
|
|
||||||
const control = createTestControl(50, { min: 10, max: 100 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Type below min
|
|
||||||
await fireEvent.change(input, { target: { value: '5' } });
|
|
||||||
await fireEvent.blur(input);
|
|
||||||
|
|
||||||
// Should be clamped to min
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(control.value).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('input clamps to max', async () => {
|
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Type above max
|
|
||||||
await fireEvent.change(input, { target: { value: '150' } });
|
|
||||||
await fireEvent.blur(input);
|
|
||||||
|
|
||||||
// Should be clamped to max
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(control.value).toBe(100);
|
expect(control.value).toBe(100);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects invalid input (non-numeric)', async () => {
|
it('updates displayed value after click', async () => {
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
const control = makeControl(50);
|
||||||
render(ComboControl, {
|
render(ComboControl, { control });
|
||||||
control,
|
await fireEvent.click(screen.getByLabelText('Increase'));
|
||||||
});
|
await waitFor(() => expect(screen.getByText('51')).toBeInTheDocument());
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
const originalValue = control.value;
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Type invalid value
|
|
||||||
await fireEvent.change(input, { target: { value: 'abc' } });
|
|
||||||
await fireEvent.blur(input);
|
|
||||||
|
|
||||||
// Value should not change for invalid input
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(control.value).toBe(originalValue);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles empty input gracefully', async () => {
|
describe('Popover', () => {
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
it('opens popover with vertical slider on trigger click', async () => {
|
||||||
render(ComboControl, {
|
render(ComboControl, { control: makeControl(50), controlLabel: 'Size control' });
|
||||||
control,
|
expect(screen.queryByRole('slider')).not.toBeInTheDocument();
|
||||||
});
|
await fireEvent.click(screen.getByLabelText('Size control'));
|
||||||
|
await waitFor(() => expect(screen.getByRole('slider')).toBeInTheDocument());
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
const originalValue = control.value;
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
expect(input).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Clear input
|
|
||||||
await fireEvent.change(input, { target: { value: '' } });
|
|
||||||
await fireEvent.blur(input);
|
|
||||||
|
|
||||||
// Value should not change for empty input
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(control.value).toBe(originalValue);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Reactivity', () => {
|
|
||||||
it('external control value change updates control button text', async () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('50')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Change value externally
|
|
||||||
control.value = 75;
|
|
||||||
|
|
||||||
// Wait for UI to update
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('75')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('button states update when external value changes', async () => {
|
describe('Reduced mode', () => {
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
it('renders horizontal slider directly without popover trigger', () => {
|
||||||
const { container } = render(ComboControl, {
|
render(ComboControl, { control: makeControl(50), reduced: true });
|
||||||
control,
|
expect(screen.getByRole('slider')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText('Decrease')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText('Increase')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = container.querySelectorAll('button');
|
it('shows formatted value in reduced mode', () => {
|
||||||
|
const { container } = render(ComboControl, { control: makeControl(75), reduced: true });
|
||||||
// Both should be enabled
|
expect(container.textContent).toContain('75');
|
||||||
expect(buttons[0]).not.toBeDisabled();
|
|
||||||
expect(buttons[2]).not.toBeDisabled();
|
|
||||||
|
|
||||||
// Set to max
|
|
||||||
control.value = 100;
|
|
||||||
|
|
||||||
// Wait for button state to update
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(buttons[2]).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('input and slider sync when external value changes', async () => {
|
|
||||||
const control = createTestControl(50, { min: 0, max: 100 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
// Both should be present
|
|
||||||
const _slider = await screen.findByRole('slider');
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
|
|
||||||
// Input should show initial value
|
|
||||||
expect(input.value).toBe('50');
|
|
||||||
|
|
||||||
// Change value externally
|
|
||||||
control.value = 75;
|
|
||||||
|
|
||||||
// Wait for input to update
|
|
||||||
await waitFor(async () => {
|
|
||||||
const updatedInput = await screen.findByTestId(
|
|
||||||
'combo-control-input',
|
|
||||||
) as HTMLInputElement;
|
|
||||||
expect(updatedInput.value).toBe('75');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Slider should still be present
|
|
||||||
const updatedSlider = await screen.findByRole('slider');
|
|
||||||
expect(updatedSlider).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('decrease button becomes enabled when value increases externally', async () => {
|
|
||||||
const control = createTestControl(0, { min: 0, max: 100 });
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const decreaseBtn = container.querySelectorAll('button')[0];
|
|
||||||
|
|
||||||
// Initially disabled
|
|
||||||
expect(decreaseBtn).toBeDisabled();
|
|
||||||
|
|
||||||
// Increase value externally
|
|
||||||
control.value = 10;
|
|
||||||
|
|
||||||
// Wait for button to become enabled
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(decreaseBtn).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('increase button becomes enabled when value decreases externally', async () => {
|
|
||||||
const control = createTestControl(100, { min: 0, max: 100 });
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const increaseBtn = container.querySelectorAll('button')[2];
|
|
||||||
|
|
||||||
// Initially disabled
|
|
||||||
expect(increaseBtn).toBeDisabled();
|
|
||||||
|
|
||||||
// Decrease value externally
|
|
||||||
control.value = 90;
|
|
||||||
|
|
||||||
// Wait for button to become enabled
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(increaseBtn).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
|
||||||
it('handles equal min and max', () => {
|
|
||||||
const control = createTestControl(5, { min: 5, max: 5 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should render without errors
|
|
||||||
expect(screen.getByText('5')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Both decrease and increase should be disabled
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
const buttons = container.querySelectorAll('button');
|
|
||||||
expect(buttons[0]).toBeDisabled();
|
|
||||||
expect(buttons[2]).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles very small step values', () => {
|
|
||||||
const control = createTestControl(5, { min: 0, max: 10, step: 0.001 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('5')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles negative range with positive and negative values', async () => {
|
|
||||||
const control = createTestControl(-5, { min: -10, max: 10, step: 1 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('-5')).toBeInTheDocument();
|
|
||||||
|
|
||||||
const increaseBtn = screen.getAllByRole('button')[2];
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBe(-4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles zero as min value', async () => {
|
|
||||||
const control = createTestControl(0, { min: 0, max: 10 });
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('0')).toBeInTheDocument();
|
|
||||||
|
|
||||||
const decreaseBtn = container.querySelectorAll('button')[0];
|
|
||||||
expect(decreaseBtn).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles large step value', async () => {
|
|
||||||
const control = createTestControl(5, { min: 0, max: 100, step: 50 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const increaseBtn = screen.getAllByRole('button')[2];
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
|
|
||||||
// Should jump by 50
|
|
||||||
expect(control.value).toBe(55);
|
|
||||||
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
expect(control.value).toBe(100); // Clamped to max
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
|
||||||
it('all buttons have aria-label when provided', () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
decreaseLabel: 'Decrease value',
|
|
||||||
controlLabel: 'Current value',
|
|
||||||
increaseLabel: 'Increase value',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByLabelText('Decrease value')).toBeInTheDocument();
|
|
||||||
expect(screen.getByLabelText('Current value')).toBeInTheDocument();
|
|
||||||
expect(screen.getByLabelText('Increase value')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('buttons are keyboard accessible', async () => {
|
|
||||||
const control = createTestControl(50);
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const buttons = screen.getAllByRole('button');
|
|
||||||
expect(buttons).toHaveLength(3);
|
|
||||||
|
|
||||||
// All buttons should be focusable
|
|
||||||
buttons.forEach(btn => {
|
|
||||||
expect(btn).not.toHaveAttribute('disabled');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disabled buttons are properly marked', () => {
|
|
||||||
const control = createTestControl(0, { min: 0, max: 100 });
|
|
||||||
const { container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
const decreaseBtn = container.querySelectorAll('button')[0];
|
|
||||||
expect(decreaseBtn).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Integration Scenarios', () => {
|
|
||||||
it('typical font size control workflow', async () => {
|
|
||||||
const control = createTestControl(16, { min: 12, max: 72, step: 1 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
controlLabel: 'Font size',
|
|
||||||
decreaseLabel: 'Decrease font size',
|
|
||||||
increaseLabel: 'Increase font size',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial state
|
|
||||||
expect(screen.getByText('16')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Increase via button
|
|
||||||
const increaseBtn = screen.getByTestId('increase-button');
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
expect(control.value).toBe(17);
|
|
||||||
|
|
||||||
// Decrease via button
|
|
||||||
const decreaseBtn = screen.getByTestId('decrease-button');
|
|
||||||
await fireEvent.click(decreaseBtn);
|
|
||||||
expect(control.value).toBe(16);
|
|
||||||
|
|
||||||
// Open popover and use input
|
|
||||||
const controlBtn = screen.getByTestId('combo-control-value');
|
|
||||||
await fireEvent.click(controlBtn);
|
|
||||||
|
|
||||||
const input = await screen.findByTestId('combo-control-input') as HTMLInputElement;
|
|
||||||
await fireEvent.change(input, { target: { value: '24' } });
|
|
||||||
await fireEvent.blur(input);
|
|
||||||
|
|
||||||
expect(control.value).toBe(24);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('letter spacing control with decimal precision', async () => {
|
|
||||||
const control = createTestControl(0, { min: -0.1, max: 0.5, step: 0.01 });
|
|
||||||
const { container: _container } = render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('0')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Increase to positive value
|
|
||||||
const increaseBtn = screen.getAllByRole('button')[2];
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
await fireEvent.click(increaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBeCloseTo(0.02);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('line height control with 0.1 step', async () => {
|
|
||||||
const control = createTestControl(1.5, { min: 0.8, max: 2.0, step: 0.1 });
|
|
||||||
render(ComboControl, {
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('1.5')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Decrease to 1.3
|
|
||||||
const decreaseBtn = screen.getAllByRole('button')[0];
|
|
||||||
await fireEvent.click(decreaseBtn);
|
|
||||||
await fireEvent.click(decreaseBtn);
|
|
||||||
|
|
||||||
expect(control.value).toBeCloseTo(1.3);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,573 +1,104 @@
|
|||||||
import {
|
import { createFilter } from '$shared/lib';
|
||||||
type Property,
|
|
||||||
createFilter,
|
|
||||||
} from '$shared/lib';
|
|
||||||
import {
|
import {
|
||||||
fireEvent,
|
fireEvent,
|
||||||
render,
|
render,
|
||||||
screen,
|
screen,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from '@testing-library/svelte';
|
} from '@testing-library/svelte';
|
||||||
import {
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
it,
|
|
||||||
} from 'vitest';
|
|
||||||
import FilterGroup from './FilterGroup.svelte';
|
import FilterGroup from './FilterGroup.svelte';
|
||||||
|
|
||||||
/**
|
function makeProperties(count: number, selectedIndices: number[] = []) {
|
||||||
* Test Suite for FilterGroup Component
|
|
||||||
*
|
|
||||||
* This suite tests the actual Svelte component rendering, interactions, and behavior
|
|
||||||
* using a real browser environment (Playwright) via @vitest/browser-playwright.
|
|
||||||
*
|
|
||||||
* Tests for the createFilter helper function are in createFilter.test.ts
|
|
||||||
*
|
|
||||||
* IMPORTANT: These tests use the browser environment because Svelte 5's $state,
|
|
||||||
* $derived, and onMount lifecycle require a browser environment. The bits-ui
|
|
||||||
* Checkbox component renders as <button type="button"> with role="checkbox",
|
|
||||||
* not as <input type="checkbox">.
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe('FilterGroup Component', () => {
|
|
||||||
/**
|
|
||||||
* Helper function to create a filter for testing
|
|
||||||
*/
|
|
||||||
function createTestFilter<T extends string>(properties: Property<T>[]) {
|
|
||||||
return createFilter({ properties });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to create mock properties
|
|
||||||
*/
|
|
||||||
function createMockProperties(count: number, selectedIndices: number[] = []) {
|
|
||||||
return Array.from({ length: count }, (_, i) => ({
|
return Array.from({ length: count }, (_, i) => ({
|
||||||
id: `prop-${i}`,
|
id: `prop-${i}`,
|
||||||
name: `Property ${i}`,
|
name: `Option ${i}`,
|
||||||
value: `Value ${i}`,
|
value: `val-${i}`,
|
||||||
selected: selectedIndices.includes(i),
|
selected: selectedIndices.includes(i),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('FilterGroup', () => {
|
||||||
describe('Rendering', () => {
|
describe('Rendering', () => {
|
||||||
it('displays the label', () => {
|
it('renders the group label', () => {
|
||||||
const filter = createTestFilter(createMockProperties(3));
|
const filter = createFilter({ properties: makeProperties(2) });
|
||||||
render(FilterGroup, {
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
displayedLabel: 'Test Label',
|
expect(screen.getByText('Category')).toBeInTheDocument();
|
||||||
filter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText('Test Label')).toBeInTheDocument();
|
it('renders all properties as buttons', () => {
|
||||||
|
const filter = createFilter({ properties: makeProperties(3) });
|
||||||
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
|
expect(screen.getByText('Option 0')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders all properties as checkboxes with labels', () => {
|
it('renders no buttons for empty filter', () => {
|
||||||
const properties = createMockProperties(3);
|
const filter = createFilter({ properties: [] });
|
||||||
const filter = createTestFilter(properties);
|
render(FilterGroup, { displayedLabel: 'Empty', filter });
|
||||||
render(FilterGroup, {
|
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that all property names are rendered
|
|
||||||
expect(screen.getByText('Property 0')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Property 1')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Property 2')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows selected count badge when items are selected', () => {
|
|
||||||
const properties = createMockProperties(3, [0, 2]); // Select 2 items
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('2')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides badge when no items selected', () => {
|
describe('Selection state', () => {
|
||||||
const properties = createMockProperties(3);
|
it('selected property button has active styling', () => {
|
||||||
const filter = createTestFilter(properties);
|
const filter = createFilter({ properties: makeProperties(2, [0]) });
|
||||||
const { container } = render(FilterGroup, {
|
const { container } = render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
displayedLabel: 'Test',
|
const buttons = container.querySelectorAll<HTMLButtonElement>('button');
|
||||||
filter,
|
expect(buttons[0]).toHaveClass('shadow-sm');
|
||||||
|
expect(buttons[1]).not.toHaveClass('shadow-sm');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Badge should not be in the document
|
it('toggling a button updates property.selected', async () => {
|
||||||
const badges = container.querySelectorAll('[class*="badge"]');
|
const filter = createFilter({ properties: makeProperties(2) });
|
||||||
expect(badges).toHaveLength(0);
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
|
expect(filter.properties[0].selected).toBe(false);
|
||||||
|
await fireEvent.click(screen.getByText('Option 0'));
|
||||||
|
expect(filter.properties[0].selected).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with no properties', () => {
|
it('clicking selected button deselects it', async () => {
|
||||||
const filter = createTestFilter([]);
|
const filter = createFilter({ properties: makeProperties(2, [0]) });
|
||||||
render(FilterGroup, {
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
displayedLabel: 'Empty Filter',
|
expect(filter.properties[0].selected).toBe(true);
|
||||||
filter,
|
await fireEvent.click(screen.getByText('Option 0'));
|
||||||
|
expect(filter.properties[0].selected).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText('Empty Filter')).toBeInTheDocument();
|
it('multiple properties can be selected independently', async () => {
|
||||||
|
const filter = createFilter({ properties: makeProperties(3) });
|
||||||
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
|
await fireEvent.click(screen.getByText('Option 0'));
|
||||||
|
await fireEvent.click(screen.getByText('Option 2'));
|
||||||
|
expect(filter.properties[0].selected).toBe(true);
|
||||||
|
expect(filter.properties[1].selected).toBe(false);
|
||||||
|
expect(filter.properties[2].selected).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Checkbox Interactions', () => {
|
describe('Show more', () => {
|
||||||
it('checkboxes reflect initial selected state', async () => {
|
it('shows all properties when count <= 10', () => {
|
||||||
const properties = createMockProperties(3, [0, 2]);
|
const filter = createFilter({ properties: makeProperties(10) });
|
||||||
const filter = createTestFilter(properties);
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
render(FilterGroup, {
|
for (let i = 0; i < 10; i++) {
|
||||||
displayedLabel: 'Test',
|
expect(screen.getByText(`Option ${i}`)).toBeInTheDocument();
|
||||||
filter,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for component to render
|
|
||||||
// bits-ui Checkbox renders as <button type="button"> with role="checkbox"
|
|
||||||
const checkboxes = screen.getAllByRole('checkbox');
|
|
||||||
expect(checkboxes).toHaveLength(3);
|
|
||||||
|
|
||||||
// Check that the correct checkboxes are checked using aria-checked attribute
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
|
|
||||||
expect(checkboxes[1]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
expect(checkboxes[2]).toHaveAttribute('data-state', 'checked');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clicking checkbox toggles property.selected state', async () => {
|
|
||||||
const properties = createMockProperties(3, [0]);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
|
|
||||||
// Initially, first checkbox is checked
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
|
|
||||||
expect(filter.selectedCount).toBe(1);
|
|
||||||
|
|
||||||
// Click to uncheck it
|
|
||||||
await fireEvent.click(checkboxes[0]);
|
|
||||||
|
|
||||||
// Now it should be unchecked
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
});
|
|
||||||
expect(filter.selectedCount).toBe(0);
|
|
||||||
|
|
||||||
// Click it again to re-check
|
|
||||||
await fireEvent.click(checkboxes[0]);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
|
|
||||||
});
|
|
||||||
expect(filter.selectedCount).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('label styling changes based on selection state', async () => {
|
|
||||||
const properties = createMockProperties(2, [0]);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
|
|
||||||
// Find label elements - they are siblings of checkboxes
|
|
||||||
const labels = checkboxes.map(cb => cb.nextElementSibling);
|
|
||||||
|
|
||||||
// First label should have font-medium and text-foreground classes
|
|
||||||
expect(labels[0]).toHaveClass('font-medium', 'text-foreground');
|
|
||||||
|
|
||||||
// Second label should not have these classes
|
|
||||||
expect(labels[1]).not.toHaveClass('font-medium', 'text-foreground');
|
|
||||||
|
|
||||||
// Uncheck the first checkbox
|
|
||||||
await fireEvent.click(checkboxes[0]);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// Now first label should not have these classes
|
|
||||||
expect(labels[0]).not.toHaveClass('font-medium', 'text-foreground');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('multiple checkboxes can be toggled independently', async () => {
|
|
||||||
const properties = createMockProperties(3);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
|
|
||||||
// Check all three checkboxes
|
|
||||||
await fireEvent.click(checkboxes[0]);
|
|
||||||
await fireEvent.click(checkboxes[1]);
|
|
||||||
await fireEvent.click(checkboxes[2]);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(filter.selectedCount).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Uncheck middle one
|
|
||||||
await fireEvent.click(checkboxes[1]);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(filter.selectedCount).toBe(2);
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
|
|
||||||
expect(checkboxes[1]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
expect(checkboxes[2]).toHaveAttribute('data-state', 'checked');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Collapsible Behavior', () => {
|
|
||||||
it('is open by default', () => {
|
|
||||||
const filter = createTestFilter(createMockProperties(2));
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check that properties are visible (content is expanded)
|
|
||||||
expect(screen.getByText('Property 0')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Property 1')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clicking trigger toggles open/close state', async () => {
|
|
||||||
const filter = createTestFilter(createMockProperties(2));
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Content is initially visible
|
|
||||||
expect(screen.getByText('Property 0')).toBeVisible();
|
|
||||||
|
|
||||||
// Click the trigger (button) - use role and text to find it
|
|
||||||
const trigger = screen.getByRole('button', { name: /Test/ });
|
|
||||||
await fireEvent.click(trigger);
|
|
||||||
|
|
||||||
// Content should now be hidden
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByText('Property 0')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click again to open
|
|
||||||
await fireEvent.click(trigger);
|
|
||||||
|
|
||||||
// Content should be visible again
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Property 0')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('chevron icon rotates based on open state', async () => {
|
|
||||||
const filter = createTestFilter(createMockProperties(2));
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const trigger = screen.getByRole('button', { name: /Test/ });
|
|
||||||
const chevronContainer = trigger.querySelector('.lucide-chevron-down')
|
|
||||||
?.parentElement as HTMLElement;
|
|
||||||
|
|
||||||
// Initially open, transform should be rotate(0deg) or no rotation
|
|
||||||
expect(chevronContainer?.style.transform).toContain('0deg');
|
|
||||||
|
|
||||||
// Click to close
|
|
||||||
await fireEvent.click(trigger);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// Now should be rotated -90deg
|
|
||||||
expect(chevronContainer?.style.transform).toContain('-90deg');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click to open again
|
|
||||||
await fireEvent.click(trigger);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// Back to 0deg
|
|
||||||
expect(chevronContainer?.style.transform).toContain('0deg');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Count Display', () => {
|
|
||||||
it('badge shows correct count based on filter.selectedCount', async () => {
|
|
||||||
const properties = createMockProperties(5, [0, 2, 4]);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should show 3
|
|
||||||
expect(screen.getByText('3')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Click a checkbox to change selection
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
await fireEvent.click(checkboxes[1]);
|
|
||||||
|
|
||||||
// Should now show 4
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('4')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('badge visibility changes with hasSelection (selectedCount > 0)', async () => {
|
it('hides extra properties and shows show-more button when count > 10', () => {
|
||||||
const properties = createMockProperties(2, [0]);
|
const filter = createFilter({ properties: makeProperties(15) });
|
||||||
const filter = createTestFilter(properties);
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
render(FilterGroup, {
|
expect(screen.queryByText('Option 10')).not.toBeInTheDocument();
|
||||||
displayedLabel: 'Test',
|
expect(screen.getByRole('button', { name: '' })).toBeInTheDocument();
|
||||||
filter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initially has 1 selection, badge should be visible
|
it('clicking show-more reveals all properties', async () => {
|
||||||
expect(screen.getByText('1')).toBeInTheDocument();
|
const filter = createFilter({ properties: makeProperties(12) });
|
||||||
|
render(FilterGroup, { displayedLabel: 'Category', filter });
|
||||||
// Uncheck the selected item
|
expect(screen.queryByText('Option 10')).not.toBeInTheDocument();
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
const showMoreBtn = screen.getAllByRole('button').at(-1)!;
|
||||||
await fireEvent.click(checkboxes[0]);
|
await fireEvent.click(showMoreBtn);
|
||||||
|
await waitFor(() => expect(screen.getByText('Option 10')).toBeInTheDocument());
|
||||||
// Now 0 selections, badge should be hidden
|
await waitFor(() => expect(screen.getByText('Option 11')).toBeInTheDocument());
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.queryByText('0')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check it again
|
|
||||||
await fireEvent.click(checkboxes[0]);
|
|
||||||
|
|
||||||
// Badge should be visible again
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('1')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('badge shows count correctly when all items are selected', () => {
|
|
||||||
const properties = createMockProperties(5, [0, 1, 2, 3, 4]);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('5')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
|
||||||
it('provides proper ARIA labels on buttons', () => {
|
|
||||||
const filter = createTestFilter(createMockProperties(2));
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test Label',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// The trigger button should be findable by its text
|
|
||||||
const trigger = screen.getByRole('button', { name: /Test Label/ });
|
|
||||||
expect(trigger).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('labels are properly associated with checkboxes', async () => {
|
|
||||||
const properties = createMockProperties(3);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
|
|
||||||
checkboxes.forEach((checkbox, index) => {
|
|
||||||
// Each checkbox should have an id
|
|
||||||
expect(checkbox).toHaveAttribute('id', `prop-${index}`);
|
|
||||||
|
|
||||||
// Find the label element (Label component wraps checkbox)
|
|
||||||
const labelElement = checkbox.closest('label');
|
|
||||||
expect(labelElement).toHaveAttribute('for', `prop-${index}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checkboxes have proper role', async () => {
|
|
||||||
const filter = createTestFilter(createMockProperties(2));
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
checkboxes.forEach(checkbox => {
|
|
||||||
expect(checkbox).toHaveAttribute('role', 'checkbox');
|
|
||||||
expect(checkbox).toHaveAttribute('type', 'button');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('labels are clickable and toggle associated checkboxes', async () => {
|
|
||||||
const properties = createMockProperties(2);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
// Find the label text element (span inside label)
|
|
||||||
const firstLabelText = screen.getByText('Property 0');
|
|
||||||
|
|
||||||
// Initially unchecked
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
|
|
||||||
// Click the label text
|
|
||||||
await fireEvent.click(firstLabelText);
|
|
||||||
|
|
||||||
// Checkbox should now be checked
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click again
|
|
||||||
await fireEvent.click(firstLabelText);
|
|
||||||
|
|
||||||
// Should be unchecked again
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
|
||||||
it('handles long property names', () => {
|
|
||||||
const properties: Property<string>[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'This is a very long property name that might wrap to multiple lines',
|
|
||||||
value: '1',
|
|
||||||
selected: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByText(
|
|
||||||
'This is a very long property name that might wrap to multiple lines',
|
|
||||||
),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles special characters in property names', () => {
|
|
||||||
const properties: Property<string>[] = [
|
|
||||||
{ id: '1', name: 'Café & Restaurant', value: '1', selected: true },
|
|
||||||
{ id: '2', name: '100% Organic', value: '2', selected: false },
|
|
||||||
{ id: '3', name: '(Special) <Characters>', value: '3', selected: false },
|
|
||||||
];
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('Café & Restaurant')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('100% Organic')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('(Special) <Characters>')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles single property filter', () => {
|
|
||||||
const properties: Property<string>[] = [
|
|
||||||
{ id: '1', name: 'Only One', value: '1', selected: true },
|
|
||||||
];
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Single',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText('Only One')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('1')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles very large number of properties', async () => {
|
|
||||||
const properties = createMockProperties(50, [0, 25, 49]);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Large List',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
expect(checkboxes).toHaveLength(50);
|
|
||||||
expect(screen.getByText('3')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates badge when filter is manipulated externally', async () => {
|
|
||||||
const properties = createMockProperties(3);
|
|
||||||
const filter = createTestFilter(properties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Test',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initially no badge (0 selections)
|
|
||||||
expect(screen.queryByText('0')).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
// Externally select properties
|
|
||||||
filter.selectProperty('prop-0');
|
|
||||||
filter.selectProperty('prop-1');
|
|
||||||
|
|
||||||
// Badge should now show 2
|
|
||||||
// Note: This might not update immediately in the DOM due to Svelte reactivity
|
|
||||||
// In a real browser environment, this would update
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('2')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Component Integration', () => {
|
|
||||||
it('works correctly with real filter data', async () => {
|
|
||||||
const realProperties: Property<string>[] = [
|
|
||||||
{ id: 'sans-serif', name: 'Sans-serif', value: 'sans-serif', selected: true },
|
|
||||||
{ id: 'serif', name: 'Serif', value: 'serif', selected: false },
|
|
||||||
{ id: 'display', name: 'Display', value: 'display', selected: false },
|
|
||||||
{ id: 'handwriting', name: 'Handwriting', value: 'handwriting', selected: true },
|
|
||||||
{ id: 'monospace', name: 'Monospace', value: 'monospace', selected: false },
|
|
||||||
];
|
|
||||||
const filter = createTestFilter(realProperties);
|
|
||||||
render(FilterGroup, {
|
|
||||||
displayedLabel: 'Font Category',
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check label
|
|
||||||
expect(screen.getByText('Font Category')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check count badge
|
|
||||||
expect(screen.getByText('2')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check property names
|
|
||||||
expect(screen.getByText('Sans-serif')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Serif')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Display')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Handwriting')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Monospace')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check initial checkbox states
|
|
||||||
const checkboxes = await screen.findAllByRole('checkbox');
|
|
||||||
expect(checkboxes[0]).toHaveAttribute('data-state', 'checked');
|
|
||||||
expect(checkboxes[1]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
expect(checkboxes[2]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
expect(checkboxes[3]).toHaveAttribute('data-state', 'checked');
|
|
||||||
expect(checkboxes[4]).toHaveAttribute('data-state', 'unchecked');
|
|
||||||
|
|
||||||
// Interact with checkboxes
|
|
||||||
await fireEvent.click(checkboxes[1]);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(filter.selectedCount).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user