Files
Mangalord/frontend/src/lib/components/IconButton.svelte.test.ts
MechaCat02 79a1432db8 refactor(ui): fold the 36px header/search buttons into IconButton
Completes the IconButton extraction by adding `size` and `radius` props and
converting the two remaining outliers that were left out of the first pass:
the desktop header logout button (+layout) and the home search submit button
(+page), both 36px with the medium radius.

IconButton now drives every icon button in the app (except profile/history's,
which the shared-HistoryList change removes separately). Defaults stay 32px /
small radius, so the four already-converted sites are untouched. Rendered
size, radius, and variant match the old markup — no behaviour change, no
version bump.

This also sets up applying the --tap-min touch floor in one place once the
touch-target change lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:46:54 +02:00

80 lines
3.3 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { render, screen, cleanup, fireEvent } from '@testing-library/svelte';
import { createRawSnippet } from 'svelte';
import IconButton from './IconButton.svelte';
afterEach(() => cleanup());
// A stand-in for the lucide icon callers pass as the child.
const icon = createRawSnippet(() => ({
render: () => `<svg data-testid="glyph"></svg>`
}));
describe('IconButton', () => {
it('renders a button containing the icon child', () => {
render(IconButton, { props: { children: icon } });
const btn = screen.getByRole('button');
expect(btn.querySelector('[data-testid="glyph"]')).toBeTruthy();
});
it('defaults to type=button so it never submits a surrounding form by accident', () => {
render(IconButton, { props: { children: icon } });
expect(screen.getByRole('button').getAttribute('type')).toBe('button');
});
it('lets a caller override the type (e.g. submit)', () => {
render(IconButton, { props: { type: 'submit', children: icon } });
expect(screen.getByRole('button').getAttribute('type')).toBe('submit');
});
it('applies the variant as a class (plain by default)', () => {
const { container } = render(IconButton, { props: { children: icon } });
expect(container.querySelector('button.icon-btn.plain')).toBeTruthy();
cleanup();
const { container: c2 } = render(IconButton, {
props: { variant: 'danger', children: icon }
});
expect(c2.querySelector('button.icon-btn.danger')).toBeTruthy();
});
it('defaults to a 32px square with the small radius', () => {
const { container } = render(IconButton, { props: { children: icon } });
const btn = container.querySelector('button.icon-btn') as HTMLButtonElement;
expect(btn.classList.contains('radius-sm')).toBe(true);
expect(btn.style.getPropertyValue('--ib-size')).toBe('32px');
});
it('honours an explicit size and radius (the larger header/search buttons)', () => {
const { container } = render(IconButton, {
props: { size: 36, radius: 'md', children: icon }
});
const btn = container.querySelector('button.icon-btn') as HTMLButtonElement;
expect(btn.style.getPropertyValue('--ib-size')).toBe('36px');
expect(btn.classList.contains('radius-md')).toBe(true);
expect(btn.classList.contains('radius-sm')).toBe(false);
});
it('forwards onclick', async () => {
const onclick = vi.fn();
render(IconButton, { props: { onclick, children: icon } });
await fireEvent.click(screen.getByRole('button'));
expect(onclick).toHaveBeenCalledOnce();
});
it('forwards arbitrary button attributes (aria-label, title, disabled, data-testid)', () => {
render(IconButton, {
props: {
'aria-label': 'Delete collection',
title: 'Delete',
disabled: true,
'data-testid': 'collection-delete',
children: icon
}
});
const btn = screen.getByTestId('collection-delete');
expect(btn.getAttribute('aria-label')).toBe('Delete collection');
expect(btn.getAttribute('title')).toBe('Delete');
expect((btn as HTMLButtonElement).disabled).toBe(true);
});
});