How to Sort a Table in Cypress Tests
Click a sortable column and verify the displayed order in Cypress, including numeric values, accessible sort state, and virtualized grids.
To test table sorting in Cypress, click the column header, wait for a retryable sort-state or data-order assertion, then compare the rendered values with the expected order. Convert numeric cell text to numbers before comparing. For virtualized or CSS-positioned grids, inspect the component’s rendered-order signal: DOM order may not match the order a user sees.
1. Test a standard HTML table
This runnable example assumes the page has a table with data-cy="orders-table", a button in the Price header, and price values in the third column. Adjust selectors to match your application. It checks both the visible sort direction and the actual cell order.
describe('sortable orders table', () => {
it('sorts prices from lowest to highest', () => {
cy.visit('/orders')
cy.get('[data-cy="orders-table"]').within(() => {
cy.contains('th', 'Price').find('button').click()
// This assumes the application exposes the active direction accessibly.
cy.contains('th', 'Price').should('have.attr', 'aria-sort', 'ascending')
cy.get('tbody tr').then(($rows) => {
const prices = [...$rows].map((row) =>
Number(row.querySelector('td:nth-child(3)').textContent.replace(/[$,]/g, '').trim())
)
const expected = [...prices].sort((a, b) => a - b)
expect(prices).to.deep.equal(expected)
})
})
})
})
The selector and accessibility assertion depend on the markup. If the header itself is a button or the component uses another sort indicator, target the real control and assert the component’s documented state. The data-order assertion is separate on purpose: an arrow or aria-sort value alone does not prove that the displayed records are correctly sorted.
2. Normalize values and choose the right comparator
Cell text is a string. For numbers, strip formatting such as currency symbols and thousands separators, then convert with Number. Check for invalid values when blanks or placeholders can occur, rather than allowing NaN to make a misleading comparison.
const values = ['9', '80', '100'].map(Number)
const ascending = [...values].sort((a, b) => a - b)
expect(ascending).to.deep.equal([9, 80, 100])
JavaScript’s default sort() compares string representations and mutates the array. Use a comparator for numeric order and copy the array first if the original sequence must be preserved. [MDN: Array.prototype.sort()]
- Numbers: parse to finite numbers and use
(a, b) => a - b. - Text: use
localeCompareif the product’s ordering is locale-sensitive; match the app’s locale and case rules. - Dates: parse the displayed date into timestamps or compare a stable machine-readable value. Avoid lexicographic comparison of localized date strings.
- Blanks and ties: encode the application’s blank placement and tie-break rules in the expected comparator. If order among equal primary values is unspecified, compare primary values monotonically instead of requiring one exact tie sequence.
3. Handle grids whose DOM order differs from display order
Some data grids keep row elements in their original DOM positions and move them visually with CSS transforms. A test that reads cells in DOM sequence can fail even though the screen looks sorted. Cypress documents this trap in its Sorting the Table recipe, using Ag-Grid as its example. The recipe associates values with the grid’s row index and sorts by that displayed-position signal. Those selectors and attributes are specific to that grid, not general Cypress conventions.
// Ag-Grid-style example only: verify the actual markup in your application.
cy.get('#myGrid').within(() => {
cy.contains('.ag-header-cell-label', 'Price').click()
cy.contains('.ag-header-cell-label', 'Price')
.find('[ref=eSortAsc]')
.should('be.visible')
cy.get('[col-id="price"].ag-cell').then(($cells) => {
const positioned = [...$cells].map((cell) => ({
price: Number(cell.textContent.trim()),
rowIndex: Number(cell.closest('[row-index]').getAttribute('row-index'))
}))
const displayed = [...positioned].sort((a, b) => a.rowIndex - b.rowIndex)
const prices = displayed.map((row) => row.price)
expect(prices).to.deep.equal([...prices].sort((a, b) => a - b))
})
})
For your grid, determine whether sorting reorders row nodes, changes a row index, renders only a viewport, or exposes another user-visible order. Assert against the signal that represents displayed order. With virtualization, the DOM may contain only visible rows; either assert the visible sorted window or use a supported grid API/test contract to verify the complete result.
4. Test descending order and sort state
Many tables toggle direction on successive clicks. Make the initial state deterministic, click the same header again, then assert the descending contract and values.
cy.visit('/orders')
cy.get('[data-cy="orders-table"]').within(() => {
cy.contains('th', 'Price').find('button').click() // ascending from known initial state
cy.contains('th', 'Price').should('have.attr', 'aria-sort', 'ascending')
cy.contains('th', 'Price').find('button').click()
cy.contains('th', 'Price').should('have.attr', 'aria-sort', 'descending')
cy.get('tbody tr').then(($rows) => {
const prices = [...$rows].map((row) => Number(row.querySelector('td:nth-child(3)').textContent.trim()))
expect(prices).to.deep.equal([...prices].sort((a, b) => b - a))
})
})
For an accessible HTML table, sortable headers should communicate sort direction, including with aria-sort; verify the contract your component actually implements. [MDN: aria-sort]
5. Make the test deterministic and retryable
- Load or seed known table data for the test so the expected state does not depend on a previous test.
- Scope queries with
.within()when a page has more than one table or repeated header labels. - Click the same control a user would click, then assert state and values.
- Prefer retryable Cypress queries and assertions over fixed-duration waits. Cypress retries queries and assertions while waiting for the application state to match. [Cypress API overview]
- Keep the test independently runnable. Cypress end-to-end test isolation is enabled by default; set up the state the test needs rather than relying on another test. [Cypress: test isolation]
A fixed .wait(1000) can make a demonstration easier to watch, but it is usually a poor synchronization condition. Wait on a visible state change, sorted values, or an application request when sorting is server-backed.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Values like 100, 20, 3 appear “sorted” incorrectly | Strings are being sorted lexicographically. | Convert to numbers and use a numeric comparator. |
| The grid looks sorted but the assertion fails | DOM order differs from visual order, or only a virtualized window is rendered. | Inspect row positioning and use the grid’s displayed-order signal; assert only what the DOM actually represents. |
| The header assertion passes but rows are wrong | Sort state and data order are separate behaviors. | Keep both assertions: direction state and ordered cell values. |
| The test is flaky after a sort click | The test reads before an asynchronous render or server response completes, or relies on timing. | Wait for a retryable state/data assertion or the relevant request. Avoid arbitrary sleeps. |
| Selector finds the wrong header or multiple tables | Text is duplicated or selectors are global. | Scope to a stable table/grid selector and identify the header control within it. |
Currency parsing produces NaN |
Text includes symbols, separators, whitespace, or placeholders. | Normalize the known display format; explicitly handle empty values and check parsed values with Number.isFinite. |
| Equal values make exact-array comparison fail | The component uses a secondary tie-breaker or leaves ties unspecified. | Assert primary-key monotonicity, or include the documented secondary ordering in the expected result. |
7. Performance, reliability, and cost
Keep the sorting check focused on the smallest useful fixture. Large tables increase render time and make tests harder to diagnose. For server-side sorting, seed a small deterministic dataset and wait for the specific request or resulting state; for client-side sorting, assert the visible result without imposing a fixed delay. Avoid relying on locale-specific display text when a stable value is available to the test.
Cypress itself does not require a screenshot for a sorting assertion. If a visual artifact is useful for debugging or documentation, capture the page after the test reaches its asserted state. ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo; a screenshot complements the value assertions but does not replace them.
8. Or skip the browser setup
For a screenshot of the sorted table state, ScreenshotNeo takes a URL in one GET request and returns an image or PDF. See the ScreenshotNeo API documentation for options and authentication.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/orders -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/orders"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/orders' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing. Response headers report the page verdict and billing status.
- An MCP server lets AI agents, including Claude, Cursor, and other MCP clients, take screenshots.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.
Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.
9. FAQ
Should a sorting test compare the entire row or just the sorted column?
For a focused sorting behavior test, checking the sort key is often enough. Include row identity or a secondary key when the feature promises stable tie ordering or when you need to prove records stayed intact.
Can I assert only that the sort arrow changed?
No. The indicator communicates the chosen direction, while ordered values establish that the displayed data follows it.
Can I use Cypress to verify an entire virtualized dataset?
Only if the application exposes all rows or provides a supported way to inspect the complete sorted result. A virtualized DOM may represent only the rows currently in view.
Does this test require a screenshot?
No. A screenshot can help inspect visual presentation, but the primary assertion should verify the rendered values and sort-state contract.


