import { describe, expect, test } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import { createRawSnippet } from 'svelte'; import AlphabeticalGrid from './AlphabeticalGrid.svelte'; type Item = { id: string; key: string; label: string }; const items: Item[] = [ { id: '1', key: 'A', label: 'Apple' }, { id: '2', key: 'A', label: 'Avocado' }, { id: '3', key: 'B', label: 'Banana' }, { id: '4', key: 'D', label: 'Date' } // skips C — divider must jump to D directly ]; // createRawSnippet receives getters: each param is () => ParamType const itemSnippet = createRawSnippet<[Item]>((getIt) => ({ render: () => `${getIt().label}` })); // The generic component's T parameter can't be inferred by testing-library's render, // so we cast props to avoid a spurious unknown-assignability error. // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyProps = any; describe('AlphabeticalGrid', () => { test('rail renders the full #/A-Z/& set; empty buckets are disabled when no more data could load', () => { render(AlphabeticalGrid, { props: { items, getKey: (it: Item) => it.key, item: itemSnippet // hasMore default = false → empty buckets stay disabled. } as AnyProps }); // Populated buckets are enabled with 'Jump to ' label. expect(screen.getByRole('button', { name: 'Jump to A' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Jump to B' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Jump to D' })).not.toBeDisabled(); // Empty buckets disabled when nothing more could load. aria-label // changes too so screen readers announce the empty state. expect(screen.getByRole('button', { name: 'C — no entries' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Z — no entries' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Numbers — no entries' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Symbols — no entries' })).toBeDisabled(); }); test('with hasMore=true, empty buckets are enabled (clicking would trigger onLoadMore)', () => { render(AlphabeticalGrid, { props: { items, getKey: (it: Item) => it.key, item: itemSnippet, hasMore: true, onLoadMore: () => Promise.resolve() } as AnyProps }); // Even with no C/Z items loaded, the buttons remain enabled so a // click can chase pages until the letter surfaces. expect(screen.getByRole('button', { name: 'Jump to C' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Jump to Z' })).not.toBeDisabled(); }); test('renders items in given order followed by the jump rail', () => { const { container } = render(AlphabeticalGrid, { props: { items, getKey: (it: Item) => it.key, item: itemSnippet } as AnyProps }); expect(container.textContent).toMatch(/Apple.*Avocado.*Banana.*Date.*A.*B.*C.*D/s); }); test('tags the first item of each populated bucket with an alpha- id', () => { const { container } = render(AlphabeticalGrid, { props: { items, getKey: (it: Item) => it.key, item: itemSnippet } as AnyProps }); expect(container.querySelector('#alpha-A')).not.toBeNull(); expect(container.querySelector('#alpha-B')).not.toBeNull(); expect(container.querySelector('#alpha-D')).not.toBeNull(); // C has no items so no anchor exists in the grid. expect(container.querySelector('#alpha-C')).toBeNull(); }); test('digit-starting items bucket under #', () => { const mixed: Item[] = [ { id: 'n1', key: '2', label: '2-Mello' }, { id: 'n2', key: '88', label: '88-Keys' }, { id: 'n3', key: 'A', label: 'Apple' } ]; const { container } = render(AlphabeticalGrid, { props: { items: mixed, getKey: (it: Item) => it.key, item: itemSnippet } as AnyProps }); expect(container.querySelector('#alpha-\\#')).not.toBeNull(); expect(screen.getByRole('button', { name: 'Jump to numbers' })).not.toBeDisabled(); }); });