Files
frontend-svelte/src/shared/ui/ComboControl/ComboControl.svelte.test.ts
T
Ilia Mashkov 7798c4bbdf refactor(combo-control): use native Popover instead of bits-ui
The native Popover always renders its content (the vertical slider), so the
slider's value label is in the DOM even when closed, and opening is driven by
the browser's declarative popovertarget invoker (not simulated by jsdom on
click). Update the tests to scope value assertions to the trigger and drive
open via showPopover(), matching Popover.svelte.test.ts.
2026-06-02 16:21:32 +03:00

163 lines
6.9 KiB
TypeScript

import { createTypographyControl } from '$shared/lib';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/svelte';
import ComboControl from './ComboControl.svelte';
function makeControl(value: number, opts: { min?: number; max?: number; step?: number } = {}) {
return createTypographyControl({
value,
min: opts.min ?? 0,
max: opts.max ?? 100,
step: opts.step ?? 1,
});
}
/**
* The trigger is the button wired to the popover (has popovertarget). The native
* Popover always renders its content (the vertical slider, which also displays the
* value) in the DOM, so value assertions must be scoped to the trigger to avoid
* matching the slider's own value label.
*/
function getTrigger(): HTMLElement {
return document.querySelector('button[popovertarget]') as HTMLElement;
}
describe('ComboControl', () => {
describe('Rendering', () => {
it('renders decrease and increase buttons', () => {
render(ComboControl, { control: makeControl(50) });
expect(screen.getByLabelText('Decrease')).toBeInTheDocument();
expect(screen.getByLabelText('Increase')).toBeInTheDocument();
});
it('renders the current integer value', () => {
render(ComboControl, { control: makeControl(42) });
expect(within(getTrigger()).getByText('42')).toBeInTheDocument();
});
it('formats decimal value to 1 decimal place when step >= 0.1', () => {
render(ComboControl, { control: makeControl(1.5, { step: 0.1 }) });
expect(within(getTrigger()).getByText('1.5')).toBeInTheDocument();
});
it('formats decimal value to 2 decimal places when step < 0.1', () => {
render(ComboControl, { control: makeControl(1.55, { step: 0.01 }) });
expect(within(getTrigger()).getByText('1.55')).toBeInTheDocument();
});
it('renders label when label prop is provided', () => {
render(ComboControl, { control: makeControl(16), label: 'Size' });
expect(screen.getByText('Size')).toBeInTheDocument();
});
it('applies custom aria-labels to buttons', () => {
render(ComboControl, {
control: makeControl(50),
decreaseLabel: 'Decrease font size',
increaseLabel: 'Increase font size',
});
expect(screen.getByLabelText('Decrease font size')).toBeInTheDocument();
expect(screen.getByLabelText('Increase font size')).toBeInTheDocument();
});
});
describe('Disabled state', () => {
it('disables decrease button when at min', () => {
render(ComboControl, { control: makeControl(0, { min: 0 }) });
expect(screen.getByLabelText('Decrease')).toBeDisabled();
});
it('disables increase button when at max', () => {
render(ComboControl, { control: makeControl(100, { max: 100 }) });
expect(screen.getByLabelText('Increase')).toBeDisabled();
});
it('enables both buttons when value is within bounds', () => {
render(ComboControl, { control: makeControl(50, { min: 0, max: 100 }) });
expect(screen.getByLabelText('Decrease')).not.toBeDisabled();
expect(screen.getByLabelText('Increase')).not.toBeDisabled();
});
});
describe('Click interactions', () => {
it('decreases value on decrease button click', async () => {
const control = makeControl(50, { step: 5 });
render(ComboControl, { control });
await fireEvent.click(screen.getByLabelText('Decrease'));
expect(control.value).toBe(45);
});
it('increases value on increase button click', async () => {
const control = makeControl(50, { step: 5 });
render(ComboControl, { control });
await fireEvent.click(screen.getByLabelText('Increase'));
expect(control.value).toBe(55);
});
it('clamps decrease at min', async () => {
const control = makeControl(2, { min: 0, step: 5 });
render(ComboControl, { control });
await fireEvent.click(screen.getByLabelText('Decrease'));
expect(control.value).toBe(0);
});
it('clamps increase at max', async () => {
const control = makeControl(98, { max: 100, step: 5 });
render(ComboControl, { control });
await fireEvent.click(screen.getByLabelText('Increase'));
expect(control.value).toBe(100);
});
it('updates displayed value after click', async () => {
const control = makeControl(50);
render(ComboControl, { control });
await fireEvent.click(screen.getByLabelText('Increase'));
await waitFor(() => expect(within(getTrigger()).getByText('51')).toBeInTheDocument());
});
});
describe('Popover', () => {
/**
* The native Popover always renders its content; opening is driven by the
* browser's declarative popovertarget invoker, which jsdom does not simulate
* on click (mirrors Popover.svelte.test.ts). So assert the wired-but-closed
* state, then drive the open through the API the browser would call.
*/
it('exposes a popover trigger with the vertical slider as its content', async () => {
render(ComboControl, { control: makeControl(50), controlLabel: 'Size control' });
const trigger = getTrigger();
expect(trigger).toHaveAttribute('aria-expanded', 'false');
const content = document.getElementById(trigger.getAttribute('popovertarget')!) as HTMLElement;
expect(content).toHaveAttribute('data-state', 'closed');
// The vertical slider lives inside the popover content. While closed the
// content is visibility:hidden, so query including hidden elements.
expect(within(content).getByRole('slider', { hidden: true })).toBeInTheDocument();
content.showPopover();
await waitFor(() => expect(content).toHaveAttribute('data-state', 'open'));
expect(trigger).toHaveAttribute('aria-expanded', 'true');
});
});
describe('Reduced mode', () => {
it('renders horizontal slider directly without popover trigger', () => {
render(ComboControl, { control: makeControl(50), reduced: true });
expect(screen.getByRole('slider')).toBeInTheDocument();
expect(screen.queryByLabelText('Decrease')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Increase')).not.toBeInTheDocument();
});
it('shows formatted value in reduced mode', () => {
const { container } = render(ComboControl, { control: makeControl(75), reduced: true });
expect(container.textContent).toContain('75');
});
});
});