UI Component Testing Guide
How to Test TanStack Table with Playwright: Sorting, Filters, Pagination, and Beyond
A scenario-by-scenario walkthrough of testing TanStack Table (formerly React Table) with Playwright. Column sorting state, global and column filters, pagination controls, row selection checkboxes, column visibility toggles, and pinned columns that survive horizontal scrolling.
“TanStack Table averages over five million weekly npm downloads, making it the most widely used headless table library in the React ecosystem.”
npm trends, March 2026
TanStack Table Rendering and State Flow
1. Why Testing TanStack Table Is Harder Than It Looks
TanStack Table (the successor to React Table v7) is a headless table library. Unlike opinionated UI table components that ship their own markup, TanStack Table gives you state management, sorting algorithms, filter pipelines, and pagination logic without rendering a single DOM element. You provide the markup. Your design system provides the CSS. That flexibility is exactly what makes testing difficult: there are no stable, universal selectors. Every team's implementation looks different.
The headless architecture means your Playwright tests cannot rely on predictable class names or data attributes unless you add them yourself. A column header might be a <th>, a <div>, or a <button> depending on your implementation. Sort indicators might be SVG icons, Unicode arrows, or background images. Pagination could be buttons, links, or a select dropdown. Row selection checkboxes could be native inputs or custom styled elements with hidden inputs underneath.
Six structural challenges make TanStack Table tests fragile. First, sort state is tri-state (ascending, descending, unsorted) and the visual indicator for each state varies across implementations. Second, global filters and column filters interact: applying a column filter changes what the global filter operates on, and vice versa. Third, pagination resets when filters change, which means a test that filters after navigating to page 3 will silently jump back to page 1. Fourth, row selection state is maintained by row ID, not by position, so selecting row 0 before and after a sort gives you a different data row. Fifth, column visibility toggles remove columns from the DOM entirely, shifting all subsequent column indices. Sixth, pinned (sticky) columns rely on CSS position: sticky and left or right offsets that only work correctly when the table container is scrollable, forcing your test to scroll horizontally to verify pinning behavior.
TanStack Table State Pipeline
Raw Data
Array of row objects
Column Filters
Per-column filter fns
Global Filter
Cross-column search
Sorting
Multi-column sort state
Pagination
Slice into pages
Row Model
Final visible rows
2. Setting Up a Reliable Test Environment
Before writing any table interaction tests, you need a deterministic dataset and a page that renders TanStack Table with all the features you plan to test: sorting, filtering, pagination, row selection, column visibility, and column pinning. The simplest approach is a dedicated test page or Storybook story that renders the table with a fixed dataset of 50 to 100 rows.
A fixed dataset eliminates flakiness from API response variations. If your production table fetches data from a server, mock the API endpoint in Playwright using page.route() to return the same JSON every time. This guarantees that sort order, filter matches, and pagination boundaries are predictable across runs.
Mock the data endpoint
Intercept API calls with a fixed response
Add test-friendly data attributes
Because TanStack Table is headless, the single most impactful thing you can do for test reliability is add data-testid attributes to your table markup. Add them to column headers (including the current sort direction), filter inputs, pagination controls, row selection checkboxes, and the column visibility toggle panel. This costs nothing at runtime and eliminates fragile text-based selectors.
Test Environment Setup Flow
Fixed Dataset
50 deterministic rows
API Mocking
page.route() intercept
Data Attributes
data-testid on all controls
Page Load
Wait for first row visible
Ready
Tests begin
Column Sorting State
Moderate3. Scenario: Column Sorting State
TanStack Table sorting is tri-state by default: clicking a column header cycles through ascending, descending, and unsorted. Your test must verify all three states and confirm that the row order actually changes. A common mistake is asserting only that a sort indicator appears without checking whether the data re-ordered correctly.
Multi-column sorting adds another dimension. Holding Shift while clicking a second column header adds it as a secondary sort key. Your test should verify that the secondary sort applies within groups of identical primary values.
Column Sorting: Playwright vs Assrt
import { test, expect } from '@playwright/test';
test('sort by name ascending', async ({ page }) => {
await page.goto('/admin/users');
await expect(page.locator('tbody tr').first()).toBeVisible();
const nameHeader = page.locator('[data-testid="col-header-name"]');
await nameHeader.click();
await expect(nameHeader).toHaveAttribute(
'data-sort-direction', 'asc'
);
const firstRow = page.locator('tbody tr').first();
await expect(firstRow).toContainText('User 001');
await nameHeader.click();
await expect(nameHeader).toHaveAttribute(
'data-sort-direction', 'desc'
);
const firstRowDesc = page.locator('tbody tr').first();
await expect(firstRowDesc).toContainText('User 050');
});Global Filter Search
Moderate4. Scenario: Global Filter Search
The global filter in TanStack Table searches across all visible columns simultaneously. When a user types “admin” into the search box, every row where any cell contains “admin” (case-insensitive by default) remains visible. The tricky part is that the filter runs client-side through the table's globalFilterFn, and the DOM updates asynchronously after React re-renders. Your Playwright test must wait for the row count to stabilize rather than asserting immediately after typing.
Another subtlety: global filter interacts with pagination. If your table shows 10 rows per page and the filter reduces the result set from 50 to 4 rows, pagination should reset to page 1 and the page count should drop. Your test should verify both the visible row count and the pagination state after filtering.
Per-Column Filters
Complex5. Scenario: Per-Column Filters
Per-column filters in TanStack Table let users narrow results within a specific column. The implementation varies widely: some teams use text inputs in the header row, others use dropdown selects for enum columns like “role” or “status,” and some use range sliders for numeric columns. Your Playwright tests need to handle whatever control type your implementation uses.
The interaction between column filters and the global filter is where bugs hide. When both are active, TanStack Table applies column filters first, then the global filter operates on the already-filtered dataset. This means removing a column filter can cause the global filter to match more rows, which can change pagination. Your test should verify this chain reaction.
Column Filter: Playwright vs Assrt
import { test, expect } from '@playwright/test';
test('filter by Admin role', async ({ page }) => {
await page.goto('/admin/users');
await expect(page.locator('tbody tr').first()).toBeVisible();
const roleFilter = page.locator('[data-testid="filter-role"]');
await roleFilter.selectOption('Admin');
const rows = page.locator('tbody tr');
const rowCount = await rows.count();
for (let i = 0; i < rowCount; i++) {
await expect(rows.nth(i)).toContainText('Admin');
}
await expect(page.locator('[data-testid="page-info"]'))
.toContainText('Page 1');
});Pagination Controls
Straightforward6. Scenario: Pagination Controls
Pagination in TanStack Table is controlled by pageIndex and pageSize state values. The table exposes helpers like getCanNextPage(), getCanPreviousPage(), and getPageCount() that your UI buttons consume. Your tests should verify navigation between pages, boundary conditions (first page, last page), and the interaction between pagination and page size changes.
Row Selection and Bulk Actions
Complex7. Scenario: Row Selection and Bulk Actions
Row selection in TanStack Table tracks selected rows by their row ID, not by their visual position. This distinction matters because sorting or filtering can reorder rows, and a test that selects “the first row” before and after a sort will target different data. Your tests should always assert both the selection state and the identity of the selected row.
The “select all” checkbox has its own behavioral nuances. In TanStack Table, “select all” can mean “select all rows on the current page” or “select all rows across all pages” depending on your configuration of enableRowSelection and your select-all handler. Your test should verify which behavior your app implements. Bulk actions (delete selected, export selected, change role) should appear only when at least one row is selected and should operate on exactly the rows that were selected.
Row Selection: Playwright vs Assrt
import { test, expect } from '@playwright/test';
test('select all and bulk delete', async ({ page }) => {
await page.goto('/admin/users');
await expect(page.locator('tbody tr').first()).toBeVisible();
const selectAll = page.locator('[data-testid="select-all"]');
await selectAll.check();
const checkboxes = page.locator('tbody input[type="checkbox"]');
const count = await checkboxes.count();
for (let i = 0; i < count; i++) {
await expect(checkboxes.nth(i)).toBeChecked();
}
await expect(page.locator('[data-testid="selection-count"]'))
.toContainText('10 selected');
await page.locator('[data-testid="bulk-delete"]').click();
await page.locator('[data-testid="confirm-delete"]').click();
});Column Visibility Toggle and Pinned Columns
Complex8. Scenario: Column Visibility Toggle and Pinned Columns
Column visibility in TanStack Table works by toggling the columnVisibility state object. When a column is hidden, it is completely removed from the DOM. This means all column indices shift: if you hide column 2, what was column 3 becomes column 2. Tests that reference columns by index (like td:nth-child(3)) will break silently when column visibility changes. Always use data-testid or column-specific selectors instead.
Pinned (sticky) columns add a scrolling dimension to your tests. A pinned column uses position: sticky with a computed left or right offset so it remains visible while the user scrolls horizontally through other columns. To test this in Playwright, you need to scroll the table container horizontally and verify that the pinned column stays visible while unpinned columns scroll out of view.
9. Common Pitfalls That Break TanStack Table Test Suites
TanStack Table tests fail in predictable ways. These pitfalls come from real GitHub issues, Stack Overflow questions, and the TanStack Table Discord community. Avoid them proactively.
Pitfalls to Avoid
- Asserting row order immediately after click without waiting for re-render. TanStack Table sorting is synchronous in React state, but the DOM update is asynchronous. Use expect().toContainText() with Playwright auto-waiting instead of reading textContent immediately.
- Using td:nth-child(N) selectors that break when column visibility changes. When a column is hidden, all subsequent indices shift. Use data-testid attributes tied to column IDs instead.
- Forgetting that pagination resets when filters change. A test that navigates to page 3, then applies a filter, will silently be on page 1. Always re-assert the page number after any filter operation.
- Testing sort indicators without verifying actual row data. The sort icon might update correctly while a bug in the sortingFn produces wrong row order. Always check the content of at least the first and last visible rows.
- Selecting rows by position instead of by ID. Row position changes after sort or filter. Use data-testid="select-{rowId}" to target specific rows regardless of their position.
- Not mocking the data source for client-side table tests. Random or changing API responses cause sort order, filter match counts, and pagination boundaries to vary between runs, producing flaky tests.
- Testing pinned columns without scrolling. If the table container is not wide enough to require scrolling, position: sticky has no visible effect. Your test must ensure the container has enough columns to overflow, then scroll horizontally.
- Assuming select-all means all rows across all pages. TanStack Table's default select-all behavior depends on your getToggleAllRowsSelectedHandler vs getToggleAllPageRowsSelectedHandler. Verify which one your implementation uses.
Pre-flight Checklist for TanStack Table Tests
- Deterministic mock data with known sort order, filter matches, and pagination boundaries
- data-testid attributes on column headers, filter inputs, pagination controls, row checkboxes, and column toggle panel
- data-sort-direction attribute on sortable column headers for easy state assertion
- API route mocking in beforeEach to prevent test-to-test data drift
- At least one test per table feature: sort, global filter, column filter, pagination, row selection, column visibility
- Tests that verify feature interactions (sort + select, filter + pagination, visibility + pinning)
10. Writing These Scenarios in Plain English with Assrt
Every scenario in this guide required understanding TanStack Table's internal state model, choosing the right selectors for your specific DOM implementation, handling async re-renders, and reasoning about feature interactions like filter plus pagination reset. That is a lot of accidental complexity for tests that fundamentally describe simple user behavior: “sort this column, check that the rows reordered.”
Assrt lets you write those scenarios in plain English. You describe what a user does and what they expect to see. Assrt compiles each scenario into the same Playwright TypeScript you saw above, using your codebase's actual selectors, data attributes, and DOM structure. When your team redesigns the table or swaps the column visibility panel from a dropdown to a popover, Assrt detects the failure, analyzes the updated DOM, and opens a pull request with corrected locators. Your scenario files stay unchanged.
Each scenario block compiles into the same Playwright TypeScript shown in the preceding sections. The config block sets up API mocking automatically, and the fixture file contains the deterministic dataset. No data-testid memorization. No nth-child math. No manual re-render waits.
Start with the sort scenario. Once it passes in CI, add the global filter, then column filters, then pagination boundary tests, then row selection with bulk actions, then column visibility and pinning. In an afternoon you can have complete TanStack Table coverage that most applications never achieve by hand.
Related Guides
How to Test Airtable Form Embed
Step-by-step guide to testing embedded Airtable forms with Playwright. Covers iframe...
How to Test Copy Button
Step-by-step guide to testing code block copy buttons with Playwright. Clipboard API...
How to Test Combobox Multiselect
A practical guide to testing combobox multiselect components with Playwright. Covers...
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.