Skip to content

Query by role, not by class

The table renders real semantics, which is exactly what a test should hold on to. A class name is an implementation detail; an accessible name is a promise.

await user.click(screen.getByRole('button', { name: 'Sort by Name' }));
expect(screen.getByRole('columnheader', { name: /Name/ })).toHaveAttribute(
  'aria-sort',
  'ascending',
);

Rows are row, cells are cell or columnheader, and a selectable table is a grid rather than a table. That is the ARIA rule, not a quirk, and asserting it is a useful regression test in itself.

Control the clock

Search and filters are debounced, so a test that types and asserts immediately will fail for the right reason. Either use fake timers, or remove the delay:

render(<DataTable searchDebounceMs={0} searchMinLength={0} data={rows} columns={columns} />);

Setting both to zero in tests is legitimate: you are testing the filtering, not the debounce. Test the debounce once, on its own, with fake timers.

Test the engine without the DOM

Most table logic is not about rendering. createTable gives you the engine as a plain object, with no React and no jsdom:

const table = createTable({ data, columns, getRowId: (r) => r.id });
table.setSorting([{ id: 'age', desc: true }]);
expect(table.getRowModel().rows.map((r) => r.getValue('age'))).toEqual([45, 36, 28]);

These tests are fast and they do not break when the markup changes. Use them for comparators, filters, aggregates and query building; save rendered tests for interaction.

Fake the server, not the fetch

createLocalDataSource wraps an array in the same interface a REST source has, so a server-mode table can be tested without a network. It takes the table's columns, so it filters and sorts the way the table would:

<DataTable
  dataMode="server"
  dataSource={createLocalDataSource(rows, { columns })}
  columns={columns}
/>

The table takes the same path it would in production (query, fetch, loading, empty), which is what makes the test worth having.

Wait for what you mean

Assert on the result, not on a timeout. Testing Library's findBy* queries and waitFor retry until the table has settled; a fixed sleep is either flaky or slow, and usually both.

Accessibility in the suite

An axe assertion per configuration catches the regressions that reviews miss: a slot that dropped its props, a custom cell that nested a button inside a button. Run it against the configurations you actually ship, not just the defaults.

Visual tests, carefully

Screenshots catch what assertions cannot and fail for reasons that have nothing to do with your change. Store baselines per platform, seed the data so every run renders the same rows, and keep them in their own suite so a font update does not block a bug fix.