test-web / test (push) Successful in 32s
Operator: prior rail disabled empty buckets, so clicking a letter like 'Z' did nothing if it wasn't loaded yet. AlphabeticalGrid now accepts a paginated source and walks pages on click until the target letter surfaces. - New props: hasMore, onLoadMore. When hasMore=true, every empty bucket stays enabled (clicking will chase pages); when hasMore= false, only populated buckets are clickable. - jumpTo(bucket): if populated, scrollIntoView. Otherwise loop onLoadMore + tick() until the bucket appears or no more pages. Loader2 spinner replaces the letter on the pending button; cursor:wait + aria-busy. Other rail buttons disable while one is pending so clicks don't stack. - Library Artists + Albums pass hasMore + onLoadMore through to the grid. Existing InfiniteScrollSentinel still handles scroll-driven loading. Test: new case asserts rail enables empty buckets when hasMore=true (the click would trigger onLoadMore).
106 lines
4.1 KiB
TypeScript
106 lines
4.1 KiB
TypeScript
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<Params> receives getters: each param is () => ParamType
|
|
const itemSnippet = createRawSnippet<[Item]>((getIt) => ({
|
|
render: () => `<span>${getIt().label}</span>`
|
|
}));
|
|
|
|
// 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 <letter>' 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-<bucket> 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();
|
|
});
|
|
});
|