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: 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: 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.