This commit is contained in:
199
tests/components/Table.test.tsx
Normal file
199
tests/components/Table.test.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Table, type TableHeader } from '../../src/components/Table';
|
||||
import type { SortState } from '../../src/types/sort';
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const headers: TableHeader<Row>[] = [
|
||||
{ label: 'Name', id: 'name', value: (row) => row.name, headerClassName: 'head-name' },
|
||||
{ label: 'Email', id: 'email', value: (row) => row.email, cellClassName: 'cell-email' },
|
||||
{ label: 'Static', id: 'static', value: 'Always', sortable: false },
|
||||
];
|
||||
|
||||
const data: Row[] = [{ id: '1', name: 'Jane', email: 'jane@example.com' }];
|
||||
|
||||
describe('Table', () => {
|
||||
it('renders loading state', () => {
|
||||
render(<Table headers={headers} data={[]} isLoading rowKey={(row) => row.id} />);
|
||||
|
||||
expect(document.querySelector('.animate-spin')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state message when no rows are available', () => {
|
||||
render(
|
||||
<Table
|
||||
headers={headers}
|
||||
data={[]}
|
||||
emptyMessage="Nothing here"
|
||||
rowKey={(row) => row.id}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Nothing here')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders row values from function and static header values', () => {
|
||||
const rowKey = vi.fn((row: Row) => row.id);
|
||||
render(<Table headers={headers} data={data} rowKey={rowKey} />);
|
||||
|
||||
expect(screen.getByText('Jane')).toBeInTheDocument();
|
||||
expect(screen.getByText('jane@example.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('Always')).toBeInTheDocument();
|
||||
expect(screen.getByText('Name').closest('th')).toHaveClass('head-name');
|
||||
expect(screen.getByText('jane@example.com').closest('td')).toHaveClass('cell-email');
|
||||
expect(rowKey).toHaveBeenCalledWith(data[0], 0);
|
||||
});
|
||||
|
||||
it('shows sortable buttons only when sort config is complete', () => {
|
||||
const onSortChange = vi.fn();
|
||||
const sortableHeaders: TableHeader<Row>[] = [
|
||||
{ label: 'No field', id: 'a', sortable: true, value: (row) => row.name },
|
||||
{ label: 'Empty field', id: 'b', sortable: true, sortField: '', value: (row) => row.name },
|
||||
{ label: 'Name', id: 'c', sortable: true, sortField: 'name', value: (row) => row.name },
|
||||
];
|
||||
|
||||
const { rerender } = render(
|
||||
<Table headers={sortableHeaders} data={data} rowKey={(row) => row.id} onSortChange={onSortChange} />,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Sort by No field' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Sort by Empty field' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Sort by Name' })).toBeInTheDocument();
|
||||
|
||||
rerender(<Table headers={sortableHeaders} data={data} rowKey={(row) => row.id} />);
|
||||
expect(screen.queryByRole('button', { name: 'Sort by Name' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders sort states and notifies callback on header click', () => {
|
||||
const onSortChange = vi.fn();
|
||||
const sortableHeaders: TableHeader<Row>[] = [
|
||||
{ label: 'Name', id: 'name', sortable: true, sortField: 'name', value: (row) => row.name },
|
||||
{ label: 'Email', id: 'email', sortable: true, sortField: 'email', value: (row) => row.email },
|
||||
];
|
||||
const sorting: SortState = { field: 'name', direction: 'asc' };
|
||||
|
||||
const { rerender } = render(
|
||||
<Table
|
||||
headers={sortableHeaders}
|
||||
data={data}
|
||||
rowKey={(row) => row.id}
|
||||
sorting={sorting}
|
||||
onSortChange={onSortChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const nameSort = screen.getByRole('button', { name: 'Sort by Name' });
|
||||
const emailSort = screen.getByRole('button', { name: 'Sort by Email' });
|
||||
expect(nameSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'asc');
|
||||
expect(emailSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'none');
|
||||
|
||||
fireEvent.click(nameSort);
|
||||
expect(onSortChange).toHaveBeenCalledWith('name');
|
||||
|
||||
rerender(
|
||||
<Table
|
||||
headers={sortableHeaders}
|
||||
data={data}
|
||||
rowKey={(row) => row.id}
|
||||
sorting={{ field: 'name', direction: 'desc' }}
|
||||
onSortChange={onSortChange}
|
||||
/>,
|
||||
);
|
||||
expect(nameSort.querySelector('[data-sort-state]')).toHaveAttribute('data-sort-state', 'desc');
|
||||
});
|
||||
|
||||
it('supports pagination controls and page-size changes', () => {
|
||||
const onPageChange = vi.fn();
|
||||
const onPageSizeChange = vi.fn();
|
||||
|
||||
render(
|
||||
<Table
|
||||
headers={headers}
|
||||
data={data}
|
||||
rowKey={(row) => row.id}
|
||||
pagination={{
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
total: 21,
|
||||
totalPages: 3,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Previous page' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
||||
fireEvent.change(screen.getByLabelText('Rows'), { target: { value: '20' } });
|
||||
|
||||
expect(onPageChange).toHaveBeenNthCalledWith(1, 1);
|
||||
expect(onPageChange).toHaveBeenNthCalledWith(2, 3);
|
||||
expect(onPageSizeChange).toHaveBeenCalledWith(20);
|
||||
});
|
||||
|
||||
it('hides rows selector when onPageSizeChange is absent and clamps page count display', () => {
|
||||
render(
|
||||
<Table
|
||||
headers={headers}
|
||||
data={data}
|
||||
rowKey={(row) => row.id}
|
||||
pagination={{
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 1,
|
||||
totalPages: 0,
|
||||
onPageChange: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('Rows')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Page 1 of 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables prev/next at bounds or while loading', () => {
|
||||
const { rerender } = render(
|
||||
<Table
|
||||
headers={headers}
|
||||
data={data}
|
||||
rowKey={(row) => row.id}
|
||||
pagination={{
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 10,
|
||||
totalPages: 1,
|
||||
onPageChange: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
|
||||
|
||||
rerender(
|
||||
<Table
|
||||
headers={headers}
|
||||
data={data}
|
||||
rowKey={(row) => row.id}
|
||||
isLoading
|
||||
pagination={{
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
total: 100,
|
||||
totalPages: 10,
|
||||
onPageChange: vi.fn(),
|
||||
onPageSizeChange: vi.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
|
||||
expect(screen.getByLabelText('Rows')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user