Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 2m59s
CI / Unit & Component Tests (pull_request) Failing after 3m10s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m1s
Frontend side of the /documents pagination work. The page.server.ts load reads ?page= from the URL, forwards page+size=50 to the backend, and exposes the new totalElements/pageNumber/pageSize/totalPages fields on `data`. +page.svelte renders a <Pagination> component below the result list; buildPageHref preserves every filter param and only updates page. The existing triggerSearch debounce flow intentionally drops `page` when any filter changes, so filter edits reset to page 0 automatically. <Pagination> uses plain <a href> links (not goto) so SvelteKit's default scroll restoration scrolls new pages to the top — the expected senior-UX behaviour. Decorative chevrons wrapped in aria-hidden spans, 44px touch targets, focus-visible ring, stacks vertically under 640px. The control hides itself when totalPages ≤ 1. Test coverage: 9 cases on Pagination (label, aria-current, prev/next enable/disable, makeHref invocation, decorative chevron, touch target), plus a filter-reset assertion on +page.svelte (page 5 → edit q → goto URL must drop page=). Adds i18n keys in de/en/es. Manual edit to api.ts pending a post-merge npm run generate:api against a rebuilt dev backend. (#315) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
85 lines
3.0 KiB
TypeScript
85 lines
3.0 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('disables prev on page 0 (no href, aria-disabled="true")', async () => {
|
|
render(Pagination, { page: 0, totalPages: 3, makeHref });
|
|
|
|
const prev = page.getByTestId('pagination-prev');
|
|
await expect.element(prev).toHaveAttribute('aria-disabled', 'true');
|
|
await expect.element(prev).not.toHaveAttribute('href');
|
|
});
|
|
|
|
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('disables next on the last page (no href, aria-disabled="true")', async () => {
|
|
render(Pagination, { page: 2, totalPages: 3, makeHref });
|
|
|
|
const next = page.getByTestId('pagination-next');
|
|
await expect.element(next).toHaveAttribute('aria-disabled', 'true');
|
|
await expect.element(next).not.toHaveAttribute('href');
|
|
});
|
|
|
|
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\]/);
|
|
});
|
|
});
|