Files
familienarchiv/frontend/src/lib/components/Pagination.svelte.spec.ts
Marcel 18b88672ec fix(pagination): bound controls render as aria-hidden spans — address @leonievoss
<a aria-disabled="true"> is the documented pattern but screen readers
still announce "Previous, link, disabled" on pagination bounds — noise
users don't need because the disabled state is purely visual. Switching
to <span aria-hidden="true"> removes the bound control from the AT tree
entirely (Leonie's recommendation). Visual parity preserved via a
disabledBase Tailwind class (same layout + cursor-not-allowed + opacity-40).

Tests updated: "disabled prev/next" assertions now check for aria-hidden
and no href — the active-state href/aria-current assertions are
unchanged. (#316)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 13:20:24 +02:00

87 lines
3.2 KiB
TypeScript

import { describe, it, expect, afterEach, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import Pagination from './Pagination.svelte';
afterEach(() => {
cleanup();
});
const makeHref = (p: number) => `/documents?page=${p}`;
describe('Pagination', () => {
it('renders the page-of-total label for the current page', async () => {
render(Pagination, { page: 2, totalPages: 10, makeHref });
const label = page.getByTestId('pagination-page-label');
await expect.element(label).toHaveTextContent(/3/); // page is 0-indexed, label is 1-indexed
await expect.element(label).toHaveTextContent(/10/);
});
it('marks the current page label with aria-current="page"', async () => {
render(Pagination, { page: 0, totalPages: 3, makeHref });
const label = page.getByTestId('pagination-page-label');
await expect.element(label).toHaveAttribute('aria-current', 'page');
});
it('renders prev as a link pointing at page - 1 when not on first page', async () => {
render(Pagination, { page: 4, totalPages: 10, makeHref });
const prev = page.getByTestId('pagination-prev');
await expect.element(prev).toHaveAttribute('href', '/documents?page=3');
});
it('renders disabled prev as an aria-hidden non-link so screen readers skip it', async () => {
render(Pagination, { page: 0, totalPages: 3, makeHref });
const prev = page.getByTestId('pagination-prev');
// Not a link — no href, no role=link
await expect.element(prev).not.toHaveAttribute('href');
// Hidden from assistive tech — AT shouldn't read "Previous, link, disabled"
await expect.element(prev).toHaveAttribute('aria-hidden', 'true');
});
it('renders next as a link pointing at page + 1 when not on last page', async () => {
render(Pagination, { page: 0, totalPages: 3, makeHref });
const next = page.getByTestId('pagination-next');
await expect.element(next).toHaveAttribute('href', '/documents?page=1');
});
it('renders disabled next as an aria-hidden non-link on the last page', async () => {
render(Pagination, { page: 2, totalPages: 3, makeHref });
const next = page.getByTestId('pagination-next');
await expect.element(next).not.toHaveAttribute('href');
await expect.element(next).toHaveAttribute('aria-hidden', 'true');
});
it('calls makeHref with p-1 and p+1', async () => {
const spy = vi.fn(makeHref);
render(Pagination, { page: 3, totalPages: 10, makeHref: spy });
const calls = spy.mock.calls.map((c) => c[0]).sort((a, b) => a - b);
expect(calls).toContain(2);
expect(calls).toContain(4);
});
it('renders decorative chevron inside aria-hidden span so screen readers skip it', async () => {
render(Pagination, { page: 1, totalPages: 3, makeHref });
const prev = page.getByTestId('pagination-prev');
await expect
.element(prev.getByText('«', { exact: true }))
.toHaveAttribute('aria-hidden', 'true');
});
it('prev and next have min 44px touch targets', async () => {
render(Pagination, { page: 1, totalPages: 3, makeHref });
const prev = page.getByTestId('pagination-prev');
await expect.element(prev).toHaveClass(/min-h-\[44px\]/);
await expect.element(prev).toHaveClass(/min-w-\[44px\]/);
});
});