How to Test Data Grids, Sorting, and Bulk Actions
Short answer
Data grids fail on virtual scroll hiding rows, bulk actions targeting wrong selection scopes, and sort+filter interactions—not on whether one column header clicks. Seed 100+ rows with stable ids, scroll into view for interactions, probe bulk mutation results, and prioritize grid_action slices via TrueCoverage for admin consoles.
Part of Testing Guides by UI patterns.
Who this is for
Teams shipping admin consoles, ops tables, CRM lists, or AG Grid / MUI DataGrid / TanStack Table views with sort, filter, pagination, row selection, and bulk delete/export.
Why testing data grids matters
Grid bugs cause irreversible ops mistakes:
- Bulk delete wrong rows — "select all on page" vs "select all matching filter" confusion deletes 10k records.
- Virtual scroll — automation clicks wrong row; permission change applied to neighbor id.
- Sort instability — equal keys reorder between pages; reconciliation reports wrong.
- Export skew — CSV missing filtered subset; compliance export incomplete.
- Stale selection — filter changes but selection bitmap retains old ids; silent wrong bulk update.
Probe affected row ids after every bulk action—never toast alone.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Virtual scroll | Row not mounted | Click miss | scrollIntoView + data-id |
| Select all page | vs select all results | Wrong scope | Probe count deleted |
| Sort + filter | Sort resets filter | Bad export | Combined probe |
| Column resize/reorder | Persist prefs | Low risk | Optional snapshot |
| Inline edit | Tab between cells | Save race | Probe cell value |
| Bulk action async | Job queue | Early assert | poll job probe |
| Row expansion | Nested grid | Locator depth | getByRole row |
| Keyboard nav | Shift select range | Untested | keyboard.select |
| Empty filter | Zero rows select all | Error state | Disabled bulk bar |
| Permission column | Action hidden but API open | Security | Probe 403 |
| Cross-page select | IDs preserved | Leak on page 2 | Probe selection set |
| CSV export | Background job | Download flake | Probe export job ids |
Seed large table
await request.post('/api/test/seed-grid', {
data: { runId, rowCount: 150, pattern: 'status:pending' },
});
await page.goto('/admin/users');
Sort and filter probe
await page.getByRole('columnheader', { name: 'Created' }).click();
await expect(page.getByRole('columnheader', { name: 'Created' })).toHaveAttribute('aria-sort', 'ascending');
const sorted = await request.get(`/api/test/grid?runId=${runId}&sort=created_asc`).then(r => r.json());
expect(sorted.ids).toEqual(sorted.ids.slice().sort(/* match server */));
Pair with search filters guide for facet overlap.
Bulk select scoped action
await page.getByRole('checkbox', { name: 'Filter: Pending' }).check();
await page.getByRole('checkbox', { name: 'Select all rows' }).check(); // know your grid semantics!
await page.getByRole('button', { name: 'Archive' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
const probe = await request.get(`/api/test/grid-archived?runId=${runId}`).then(r => r.json());
expect(probe.archivedCount).toBe(probe.pendingCount);
expect(probe.archivedIds.every(id => probe.pendingIds.includes(id))).toBe(true);
Critical: assert semantic of "select all" matches product definition.
Virtual row interaction
const row = page.getByRole('row', { name: /user-042/ });
await row.scrollIntoViewIfNeeded();
await row.getByRole('checkbox').check();
await page.getByRole('button', { name: 'Disable' }).click();
const user = await request.get('/api/test/user/user-042').then(r => r.json());
expect(user.disabled).toBe(true);
Requirement slices to cover
grid_action— archive, export, disable, tagselection_scope— page, filtered, explicit_ids
When prod shows bulk export on filtered subsets at 30% but tests only single-row delete, run /testchimp evolve—link scenario annotations for SOC2 controls (audit logs).
CI checklist
- Seed 100+ rows for virtual scroll specs
- Bulk action specs include selection scope assertion via probe
- Sort uses aria-sort or probe ordering
- Export: probe job result ids, not only download event
- Negative: bulk API without permission returns 403
- Parallel-safe runId on all row ids
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Click row by index | Virtualization | data-id / name |
| Trust select-all label | Ambiguous product | Probe counts |
| DOM row count = total | Virtual cap | Probe total |
| Skip export content | Wrong CSV | Job probe ids |
| One sort direction only | Desc bugs | Toggle twice |
| No async job poll | Flaky pass | expect.poll job |
Example scenario
Situation: Admin filters pending users, selects all matching, bulk archives.
Expected outcome: Only pending rows archived; active users untouched; audit log bulk_archive.
Why UI-only automation breaks: Toast says 50 archived; probe shows 50 random ids including active users.
- Arrange: Seed 80 pending + 20 active with distinct ids for runId.
- Act: Filter pending, select all matching, archive confirm.
- Assert: Probe archivedIds subset of pendingIds; active unchanged; audit probe event bulk_archive.
TestChimp workflow: Compare grid_action × selection_scope prod vs test; evolve filtered bulk paths.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with scenario annotations for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Search and filters — grid facets
- CSV import/export — export jobs
- Drag and drop — row reorder
- RBAC permissions — admin actions
- Audit compliance — bulk trails
External references
Frequently asked questions
Which grid actions are used in prod admin?
Compare grid_action × selection_scope in TrueCoverage. Prioritize bulk operations with high prod traffic—run /testchimp evolve when adding new bulk buttons.
How do virtualized grids affect row clicks?
scrollIntoViewIfNeeded on row locator located by stable id or accessible name—never nth-row index on 1000-row virtual grids.
Select all on page vs all results?
Product-specific—probe archived/deleted ids match intended scope. One of the highest severity admin bugs when wrong.
How do I test CSV export?
Trigger export, poll job probe for row ids included, optionally parse downloaded file in test env—probe first for speed.
How do inline edits differ from bulk?
Inline: probe single cell after blur/save. Bulk: probe batch job affecting many ids—async poll required.
Should sort tests use DOM or API?
API probe for order authoritative; aria-sort on columnheader for UX. Combine for sort+filter combos.
How do audit logs tie to grid bulk actions?
After bulk archive, probe audit_event_type bulk_archive with actor and id list—required for compliance verticals.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.