import React from 'react';
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { PixelTable } from '../../data/PixelTable';
/* Extracted from __tests__/data-display.test.tsx (Ola 4a additive upgrade)
into the mirrored per-component file. */
describe('PixelTable — legacy behavior preserved', () => {
it('renders headers + rows from the legacy columns/data shape', () => {
const cols = [
{ key: 'name', header: 'Name' },
{ key: 'role', header: 'Role' },
];
const rows = [
{ name: 'Alice', role: 'Admin' },
{ name: 'Bob', role: 'User' },
];
const { container } = render();
const ths = container.querySelectorAll('th');
expect(ths.length).toBe(2);
expect(ths[0].textContent).toBe('Name');
const tds = container.querySelectorAll('tbody td');
expect(tds.length).toBe(4);
expect(tds[0].textContent).toBe('Alice');
expect(tds[3].textContent).toBe('User');
});
it('applies striped class on odd rows by default', () => {
const cols = [{ key: 'n', header: 'N' }];
const rows = [{ n: 'a' }, { n: 'b' }, { n: 'c' }];
const { container } = render();
const trs = container.querySelectorAll('tbody tr');
expect(trs[1].className).toContain('bg-retro-surface/15');
expect(trs[0].className).not.toContain('bg-retro-surface/15');
});
it('forwards legacy column.className to th and td', () => {
const cols = [{ key: 'n', header: 'N', className: 'legacy-col-cls' }];
const rows = [{ n: 'a' }];
const { container } = render();
const th = container.querySelector('th')!;
const td = container.querySelector('tbody td')!;
expect(th.className).toContain('legacy-col-cls');
expect(td.className).toContain('legacy-col-cls');
});
});
describe('PixelTable — columns + data drives header + rows', () => {
it('uses column.render when provided, falling back to row[key]', () => {
type R = { id: string; name: string };
const cols = [
{ key: 'name', header: 'Name' },
{ key: 'badge', header: 'Tag', render: (row: R) => ★{row.name} },
];
const rows: R[] = [{ id: '1', name: 'Ann' }];
const { getByTestId, container } = render( columns={cols} data={rows} />);
expect(getByTestId('b-1').textContent).toBe('★Ann');
const tds = container.querySelectorAll('tbody td');
expect(tds[0].textContent).toBe('Ann');
});
it('applies align className when column.align is set', () => {
const cols = [
{ key: 'n', header: 'N', align: 'right' as const },
];
const rows = [{ n: 'a' }];
const { container } = render();
const th = container.querySelector('th')!;
const td = container.querySelector('tbody td')!;
expect(th.className).toContain('text-right');
expect(td.className).toContain('text-right');
});
it('applies column.width as inline style', () => {
const cols = [
{ key: 'n', header: 'N', width: 120 },
{ key: 'm', header: 'M', width: '40%' },
];
const rows = [{ n: 'a', m: 'b' }];
const { container } = render();
const ths = container.querySelectorAll('th');
expect((ths[0] as HTMLElement).style.width).toBe('120px');
expect((ths[1] as HTMLElement).style.width).toBe('40%');
});
});
describe('PixelTable — sortable header', () => {
it('renders a sort button when column.sortable is true and onSortChange is set', () => {
const cols = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age' },
];
const rows = [{ name: 'a', age: '1' }];
const onSort = vi.fn();
const { container } = render(
,
);
const buttons = container.querySelectorAll('th button');
expect(buttons.length).toBe(1);
expect(buttons[0].getAttribute('aria-label')).toBe('Sort by Name');
});
it('fires onSortChange with asc on first click of an unsorted column', () => {
const cols = [{ key: 'name', header: 'Name', sortable: true }];
const rows = [{ name: 'a' }];
const onSort = vi.fn();
const { container } = render(
,
);
const button = container.querySelector('th button')!;
fireEvent.click(button);
expect(onSort).toHaveBeenCalledWith({ key: 'name', dir: 'asc' });
});
it('toggles asc -> desc when clicked again on same column', () => {
const cols = [{ key: 'name', header: 'Name', sortable: true }];
const rows = [{ name: 'a' }];
const onSort = vi.fn();
const { container } = render(
,
);
const button = container.querySelector('th button')!;
fireEvent.click(button);
expect(onSort).toHaveBeenCalledWith({ key: 'name', dir: 'desc' });
});
it('sets aria-sort to ascending/descending/none on sortable columns', () => {
const cols = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age', sortable: true },
{ key: 'tag', header: 'Tag' },
];
const rows = [{ name: 'a', age: '1', tag: 't' }];
const { container } = render(
,
);
const ths = container.querySelectorAll('th');
expect(ths[0].getAttribute('aria-sort')).toBe('descending');
expect(ths[1].getAttribute('aria-sort')).toBe('none');
expect(ths[2].getAttribute('aria-sort')).toBeNull();
});
it('uncontrolled: renders a sort button when column.sortable is true even without onSortChange', () => {
const cols = [{ key: 'name', header: 'Name', sortable: true }];
const rows = [{ name: 'a' }];
const { container } = render();
expect(container.querySelectorAll('th button').length).toBe(1);
});
it('uncontrolled: clicking sort header reorders rows by the sorted column', () => {
const cols = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'age', header: 'Age', sortable: true },
];
const rows = [
{ id: 'a', name: 'Charlie', age: 30 },
{ id: 'b', name: 'Alice', age: 25 },
{ id: 'c', name: 'Bob', age: 28 },
];
const { container } = render();
const nameBtn = container.querySelectorAll('th button')[0] as HTMLButtonElement;
fireEvent.click(nameBtn);
const bodyRows = container.querySelectorAll('tbody tr');
expect(bodyRows[0].textContent).toContain('Alice');
expect(bodyRows[1].textContent).toContain('Bob');
expect(bodyRows[2].textContent).toContain('Charlie');
// aria-sort reflects the internal state
const nameTh = container.querySelectorAll('th')[0];
expect(nameTh.getAttribute('aria-sort')).toBe('ascending');
fireEvent.click(nameBtn);
expect(nameTh.getAttribute('aria-sort')).toBe('descending');
});
});
describe('PixelTable — selection', () => {
it('renders a header checkbox + row checkboxes when selection="multi"', () => {
const cols = [{ key: 'name', header: 'Name' }];
const rows = [{ id: 'r1', name: 'a' }, { id: 'r2', name: 'b' }];
const { container } = render(
r.id}
/>,
);
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
expect(checkboxes.length).toBe(3); // 1 header + 2 rows
expect(checkboxes[0].getAttribute('aria-label')).toBe('Select all rows');
});
it('toggles a row when its checkbox is clicked (multi)', () => {
const cols = [{ key: 'name', header: 'Name' }];
const rows = [{ id: 'r1', name: 'a' }, { id: 'r2', name: 'b' }];
const onSel = vi.fn();
const { container } = render(
r.id}
/>,
);
const rowCheckboxes = Array.from(container.querySelectorAll('tbody input[type="checkbox"]'));
fireEvent.click(rowCheckboxes[1]);
expect(onSel).toHaveBeenCalledWith(['r2']);
});
it('selecting all via header checkbox emits all row ids', () => {
const cols = [{ key: 'name', header: 'Name' }];
const rows = [{ id: 'r1', name: 'a' }, { id: 'r2', name: 'b' }];
const onSel = vi.fn();
const { container } = render(
r.id}
/>,
);
const headerCb = container.querySelector('thead input[type="checkbox"]')!;
fireEvent.click(headerCb);
expect(onSel).toHaveBeenCalledWith(['r1', 'r2']);
});
it('replaces the previous selection when selection="single"', () => {
const cols = [{ key: 'name', header: 'Name' }];
const rows = [{ id: 'r1', name: 'a' }, { id: 'r2', name: 'b' }];
const onSel = vi.fn();
const { container } = render(
r.id}
/>,
);
// single mode: no header checkbox (sr-only span)
expect(container.querySelector('thead input[type="checkbox"]')).toBeNull();
const rowCheckboxes = Array.from(container.querySelectorAll('tbody input[type="checkbox"]'));
fireEvent.click(rowCheckboxes[1]);
expect(onSel).toHaveBeenCalledWith(['r2']);
});
it('marks selected rows with data-selected attr + selected bg class', () => {
const cols = [{ key: 'name', header: 'Name' }];
const rows = [{ id: 'r1', name: 'a' }, { id: 'r2', name: 'b' }];
const { container } = render(
r.id}
/>,
);
const trs = container.querySelectorAll('tbody tr');
expect(trs[0].getAttribute('data-selected')).toBe('true');
expect(trs[0].className).toContain('bg-retro-surface/40');
expect(trs[1].getAttribute('data-selected')).toBeNull();
});
});
describe('PixelTable — loading state', () => {
it('renders skeleton rows when loading is true', () => {
const cols = [{ key: 'a', header: 'A' }, { key: 'b', header: 'B' }];
const { container } = render();
const skels = container.querySelectorAll('[data-skeleton]');
// 5 skeleton rows * 2 cols
expect(skels.length).toBe(10);
});
it('does not render the empty-state message while loading', () => {
const cols = [{ key: 'a', header: 'A' }];
const { container } = render(
NOPE} />,
);
expect(container.textContent).not.toContain('NOPE');
});
});
describe('PixelTable — empty state', () => {
it('renders the default empty placeholder when data is empty', () => {
const cols = [{ key: 'a', header: 'A' }];
const { container } = render();
expect(container.textContent).toContain('No data.');
});
it('renders a custom emptyState node when provided', () => {
const cols = [{ key: 'a', header: 'A' }];
render(
Nothing here} />,
);
expect(screen.getByTestId('empty').textContent).toBe('Nothing here');
});
});
describe('PixelTable — row click', () => {
it('fires onRowClick with row + index', () => {
const cols = [{ key: 'n', header: 'N' }];
const rows = [{ n: 'a' }, { n: 'b' }];
const onClick = vi.fn();
const { container } = render(
,
);
const trs = container.querySelectorAll('tbody tr');
fireEvent.click(trs[1]);
expect(onClick).toHaveBeenCalledWith({ n: 'b' }, 1);
expect(trs[1].className).toContain('cursor-pointer');
});
});
describe('PixelTable — density', () => {
it('uses compact padding when density="compact"', () => {
const cols = [{ key: 'n', header: 'N' }];
const rows = [{ n: 'a' }];
const { container } = render(
,
);
const td = container.querySelector('tbody td')!;
expect(td.className).toContain('px-3');
expect(td.className).toContain('py-1');
});
it('uses comfortable padding when density="comfortable"', () => {
const cols = [{ key: 'n', header: 'N' }];
const rows = [{ n: 'a' }];
const { container } = render(
,
);
const td = container.querySelector('tbody td')!;
expect(td.className).toContain('py-4');
});
});
describe('PixelTable — sticky header / first column', () => {
it('applies sticky top class to thead when stickyHeader is set', () => {
const cols = [{ key: 'n', header: 'N' }];
const rows = [{ n: 'a' }];
const { container } = render(
,
);
const thead = container.querySelector('thead')!;
expect(thead.className).toContain('sticky');
expect(thead.className).toContain('top-0');
});
it('applies sticky left class to first column when stickyFirstColumn is set', () => {
const cols = [{ key: 'a', header: 'A' }, { key: 'b', header: 'B' }];
const rows = [{ a: '1', b: '2' }];
const { container } = render(
,
);
const ths = container.querySelectorAll('th');
expect(ths[0].className).toContain('sticky');
expect(ths[0].className).toContain('left-0');
expect(ths[1].className).not.toContain('sticky');
});
});