2026-01-08 13:14:04 +03:00
|
|
|
import { createTypographyControl } from '$shared/lib';
|
2026-01-06 12:22:38 +03:00
|
|
|
import {
|
|
|
|
|
fireEvent,
|
|
|
|
|
render,
|
2026-01-08 13:14:04 +03:00
|
|
|
screen,
|
|
|
|
|
waitFor,
|
2026-06-02 16:21:32 +03:00
|
|
|
within,
|
2026-01-06 12:22:38 +03:00
|
|
|
} from '@testing-library/svelte';
|
|
|
|
|
import ComboControl from './ComboControl.svelte';
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-08 13:14:04 +03:00
|
|
|
|
2026-06-02 16:21:32 +03:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
describe('ComboControl', () => {
|
2026-01-08 13:14:04 +03:00
|
|
|
describe('Rendering', () => {
|
2026-04-17 22:16:44 +03:00
|
|
|
it('renders decrease and increase buttons', () => {
|
|
|
|
|
render(ComboControl, { control: makeControl(50) });
|
|
|
|
|
expect(screen.getByLabelText('Decrease')).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByLabelText('Increase')).toBeInTheDocument();
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('renders the current integer value', () => {
|
|
|
|
|
render(ComboControl, { control: makeControl(42) });
|
2026-06-02 16:21:32 +03:00
|
|
|
expect(within(getTrigger()).getByText('42')).toBeInTheDocument();
|
2026-01-08 13:14:04 +03:00
|
|
|
});
|
2026-01-06 12:22:38 +03:00
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('formats decimal value to 1 decimal place when step >= 0.1', () => {
|
|
|
|
|
render(ComboControl, { control: makeControl(1.5, { step: 0.1 }) });
|
2026-06-02 16:21:32 +03:00
|
|
|
expect(within(getTrigger()).getByText('1.5')).toBeInTheDocument();
|
2026-04-17 22:16:44 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('formats decimal value to 2 decimal places when step < 0.1', () => {
|
|
|
|
|
render(ComboControl, { control: makeControl(1.55, { step: 0.01 }) });
|
2026-06-02 16:21:32 +03:00
|
|
|
expect(within(getTrigger()).getByText('1.55')).toBeInTheDocument();
|
2026-04-17 22:16:44 +03:00
|
|
|
});
|
2026-01-08 13:14:04 +03:00
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('renders label when label prop is provided', () => {
|
|
|
|
|
render(ComboControl, { control: makeControl(16), label: 'Size' });
|
|
|
|
|
expect(screen.getByText('Size')).toBeInTheDocument();
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('applies custom aria-labels to buttons', () => {
|
2026-01-08 13:14:04 +03:00
|
|
|
render(ComboControl, {
|
2026-04-17 22:16:44 +03:00
|
|
|
control: makeControl(50),
|
2026-01-08 13:14:04 +03:00
|
|
|
decreaseLabel: 'Decrease font size',
|
|
|
|
|
increaseLabel: 'Increase font size',
|
|
|
|
|
});
|
|
|
|
|
expect(screen.getByLabelText('Decrease font size')).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByLabelText('Increase font size')).toBeInTheDocument();
|
|
|
|
|
});
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
describe('Disabled state', () => {
|
|
|
|
|
it('disables decrease button when at min', () => {
|
|
|
|
|
render(ComboControl, { control: makeControl(0, { min: 0 }) });
|
|
|
|
|
expect(screen.getByLabelText('Decrease')).toBeDisabled();
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('disables increase button when at max', () => {
|
|
|
|
|
render(ComboControl, { control: makeControl(100, { max: 100 }) });
|
|
|
|
|
expect(screen.getByLabelText('Increase')).toBeDisabled();
|
2026-01-08 13:14:04 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
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();
|
2026-01-08 13:14:04 +03:00
|
|
|
});
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
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'));
|
2026-01-08 13:14:04 +03:00
|
|
|
expect(control.value).toBe(45);
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('increases value on increase button click', async () => {
|
|
|
|
|
const control = makeControl(50, { step: 5 });
|
|
|
|
|
render(ComboControl, { control });
|
|
|
|
|
await fireEvent.click(screen.getByLabelText('Increase'));
|
2026-01-08 13:14:04 +03:00
|
|
|
expect(control.value).toBe(55);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('clamps decrease at min', async () => {
|
|
|
|
|
const control = makeControl(2, { min: 0, step: 5 });
|
|
|
|
|
render(ComboControl, { control });
|
|
|
|
|
await fireEvent.click(screen.getByLabelText('Decrease'));
|
2026-01-08 13:14:04 +03:00
|
|
|
expect(control.value).toBe(0);
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('clamps increase at max', async () => {
|
|
|
|
|
const control = makeControl(98, { max: 100, step: 5 });
|
|
|
|
|
render(ComboControl, { control });
|
|
|
|
|
await fireEvent.click(screen.getByLabelText('Increase'));
|
2026-01-08 13:14:04 +03:00
|
|
|
expect(control.value).toBe(100);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('updates displayed value after click', async () => {
|
|
|
|
|
const control = makeControl(50);
|
|
|
|
|
render(ComboControl, { control });
|
|
|
|
|
await fireEvent.click(screen.getByLabelText('Increase'));
|
2026-06-02 16:21:32 +03:00
|
|
|
await waitFor(() => expect(within(getTrigger()).getByText('51')).toBeInTheDocument());
|
2026-01-08 13:14:04 +03:00
|
|
|
});
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
describe('Popover', () => {
|
2026-06-02 16:21:32 +03:00
|
|
|
/**
|
|
|
|
|
* 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 () => {
|
2026-04-17 22:16:44 +03:00
|
|
|
render(ComboControl, { control: makeControl(50), controlLabel: 'Size control' });
|
2026-06-02 16:21:32 +03:00
|
|
|
|
|
|
|
|
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');
|
2026-01-08 13:14:04 +03:00
|
|
|
});
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
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();
|
2026-01-08 13:14:04 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-17 22:16:44 +03:00
|
|
|
it('shows formatted value in reduced mode', () => {
|
|
|
|
|
const { container } = render(ComboControl, { control: makeControl(75), reduced: true });
|
|
|
|
|
expect(container.textContent).toContain('75');
|
2026-01-08 13:14:04 +03:00
|
|
|
});
|
2026-01-06 12:22:38 +03:00
|
|
|
});
|
|
|
|
|
});
|