import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DataView } from './data-view';
import { DATA_VIEW_CHANGE_EVENT } from './data-view.types';
const ROWS = [
{ name: 'charlie', age: '30' },
{ name: 'alice', age: '25' },
{ name: 'bob', age: '40' },
{ name: 'dave', age: '20' },
];
function markup(): string {
const rows = ROWS.map(
(r) =>
`
${r.name}
`,
).join('');
return `
`;
}
function setup(): HTMLElement {
document.body.innerHTML = markup();
return document.body.firstElementChild as HTMLElement;
}
function visibleNames(root: HTMLElement): string[] {
return Array.from(root.querySelectorAll('[data-c42-dataview-row]'))
.filter((r) => !r.hasAttribute('hidden'))
.map((r) => r.getAttribute('data-field-name')!);
}
describe('DataView', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('paginates rows to the page size', () => {
const root = setup();
new DataView(root, { pageSize: 2 });
expect(visibleNames(root)).toHaveLength(2);
expect(root.querySelector('[data-c42-dataview-pages]')!.textContent).toBe('2');
expect(root.querySelector('[data-c42-dataview-info]')!.textContent).toBe('1-2 of 4');
});
it('disables prev on the first page and next on the last', () => {
const root = setup();
const dv = new DataView(root, { pageSize: 2 });
expect((root.querySelector('[data-c42-dataview-prev]') as HTMLButtonElement).disabled).toBe(true);
dv.setPage(2);
expect((root.querySelector('[data-c42-dataview-next]') as HTMLButtonElement).disabled).toBe(true);
});
it('navigates pages with next/prev buttons', () => {
const root = setup();
new DataView(root, { pageSize: 2 });
root.querySelector('[data-c42-dataview-next]')!.click();
expect(root.querySelector('[data-c42-dataview-page]')!.textContent).toBe('2');
expect(root.querySelector('[data-c42-dataview-info]')!.textContent).toBe('3-4 of 4');
});
it('filters rows by search and resets to page 1', () => {
const root = setup();
const dv = new DataView(root, { pageSize: 2 });
dv.setPage(2);
const input = root.querySelector('[data-c42-dataview-search]')!;
input.value = 'a';
input.dispatchEvent(new Event('input', { bubbles: true }));
// names containing "a": charlie, alice, dave
expect(visibleNames(root).length).toBeGreaterThan(0);
expect(dv.getState().page).toBe(1);
expect(dv.getState().query).toBe('a');
});
it('sorts ascending then descending on header click', () => {
const root = setup();
new DataView(root, { pageSize: 10 });
const ageBtn = root.querySelector('[data-c42-dataview-sort-field="age"]')!;
ageBtn.click(); // asc by age
expect(visibleNames(root)).toEqual(['dave', 'alice', 'charlie', 'bob']);
expect(ageBtn.getAttribute('aria-sort')).toBe('ascending');
ageBtn.click(); // desc
expect(visibleNames(root)).toEqual(['bob', 'charlie', 'alice', 'dave']);
expect(ageBtn.getAttribute('aria-sort')).toBe('descending');
});
it('sorts strings alphabetically', () => {
const root = setup();
const dv = new DataView(root, { pageSize: 10 });
dv.setSort('name', 'asc');
expect(visibleNames(root)).toEqual(['alice', 'bob', 'charlie', 'dave']);
});
it('emits change with the full state', () => {
const root = setup();
const dv = new DataView(root, { pageSize: 2 });
const spy = vi.fn();
dv.on(DATA_VIEW_CHANGE_EVENT, spy);
dv.setPage(2);
const detail = (spy.mock.calls[0][0] as CustomEvent).detail;
expect(detail).toMatchObject({ page: 2, pageSize: 2, total: 4, totalPages: 2 });
});
it('supports server-driven mode with a manual total and no rows', () => {
document.body.innerHTML = `
`;
const root = document.body.firstElementChild as HTMLElement;
const dv = new DataView(root, { pageSize: 10, total: 35 });
expect(dv.getState().totalPages).toBe(4);
dv.setPage(3);
expect(dv.getState().page).toBe(3);
});
it('honors defaultSort and defaultQuery', () => {
const root = setup();
const dv = new DataView(root, { pageSize: 10, defaultSort: 'name:desc', defaultQuery: 'a' });
expect(dv.getState().sortField).toBe('name');
expect(dv.getState().sortOrder).toBe('desc');
expect(dv.getState().query).toBe('a');
});
it('stops reacting after destroy', () => {
const root = setup();
const dv = new DataView(root, { pageSize: 2 });
dv.destroy();
root.querySelector('[data-c42-dataview-next]')!.click();
expect(dv.getState().page).toBe(1);
});
});