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>
80 lines
3.3 KiB
TypeScript
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);
|
|
});
|
|
});
|