Cypress tests can verify table data and sorting by asserting that the rendered table content matches the expected data and that rows reorder correctly based on column sorts.

Let’s look at a typical scenario: you have a user management table, and you want to ensure that when a user sorts by "Last Name," the table rows reorder accordingly.

Here’s a simplified HTML structure for such a table:

<table id="user-table">
  <thead>
    <tr>
      <th data-cy="sort-header" data-sort-key="firstName">First Name</th>
      <th data-cy="sort-header" data-sort-key="lastName">Last Name</th>
      <th data-cy="sort-header" data-sort-key="email">Email</th>
    </tr>
  </thead>
  <tbody id="user-rows">
    <tr data-cy="user-row" data-user-id="1">
      <td>Alice</td>
      <td>Smith</td>
      <td>alice.smith@example.com</td>
    </tr>
    <tr data-cy="user-row" data-user-id="2">
      <td>Bob</td>
      <td>Johnson</td>
      <td>bob.johnson@example.com</td>
    </tr>
    <tr data-cy="user-row" data-user-id="3">
      <td>Charlie</td>
      <td>Williams</td>
      <td>charlie.williams@example.com</td>
    </tr>
  </tbody>
</table>

And here’s how you might write a Cypress test to verify the initial state and then test sorting by "Last Name":

describe('User Table Sorting', () => {
  beforeEach(() => {
    // Seed data or visit a page that renders the table
    cy.visit('/users'); // Assuming your table is on this page
    // You might need to mock API responses here if data is fetched
  });

  it('should display user data correctly and allow sorting by last name', () => {
    // 1. Verify initial data render
    cy.get('#user-rows tr[data-cy="user-row"]').should('have.length', 3);

    // Assert initial order (e.g., by first name)
    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(0)
      .find('td')
      .eq(1) // Last Name column
      .should('contain', 'Smith'); // Assuming initial sort is not by last name

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(1)
      .find('td')
      .eq(1)
      .should('contain', 'Johnson');

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(2)
      .find('td')
      .eq(1)
      .should('contain', 'Williams');

    // 2. Trigger sorting by Last Name
    // Find the header for 'Last Name' and click it
    cy.get('th[data-cy="sort-header"][data-sort-key="lastName"]').click();

    // Wait for potential re-rendering or animations if any
    cy.wait(100); // Adjust if your UI has significant transitions

    // 3. Verify sorted data
    cy.get('#user-rows tr[data-cy="user-row"]').should('have.length', 3);

    // Assert the new order based on Last Name (ascending)
    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(0)
      .find('td')
      .eq(1) // Last Name column
      .should('contain', 'Johnson');

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(1)
      .find('td')
      .eq(1)
      .should('contain', 'Smith');

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(2)
      .find('td')
      .eq(1)
      .should('contain', 'Williams');

    // 4. Trigger sorting by Last Name again (descending)
    cy.get('th[data-cy="sort-header"][data-sort-key="lastName"]').click();
    cy.wait(100);

    // 5. Verify descending sorted data
    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(0)
      .find('td')
      .eq(1) // Last Name column
      .should('contain', 'Williams');

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(1)
      .find('td')
      .eq(1)
      .should('contain', 'Smith');

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(2)
      .find('td')
      .eq(1)
      .should('contain', 'Johnson');
  });

  it('should verify specific row data after sorting', () => {
    // Sort by Last Name ascending
    cy.get('th[data-cy="sort-header"][data-sort-key="lastName"]').click();
    cy.wait(100);

    // Find the row containing 'Charlie Williams' and assert its position and other data
    cy.get('#user-rows tr[data-cy="user-row"]')
      .contains('td', 'Williams') // Find the row containing 'Williams' in any cell
      .parent() // Move up to the <tr> element
      .within(() => {
        cy.get('td').eq(0).should('contain', 'Charlie');
        cy.get('td').eq(2).should('contain', 'charlie.williams@example.com');
      });

    // Assert Charlie's position in the sorted list (should be last)
    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(2)
      .find('td')
      .eq(1)
      .should('contain', 'Williams');
  });
});

The key here is using cy.get() with specific selectors to target table rows (tr) and cells (td), and then using .eq() to access elements by their index. data-cy attributes are invaluable for stable selectors that aren’t tied to implementation details.

When verifying sorted data, you’re essentially extracting the relevant data from each row (e.g., the content of the "Last Name" td) and asserting that this extracted data forms the expected sorted sequence. You can also use .contains() to find a specific row based on its content and then assert the data within that row, or assert its position in the overall table.

The most surprising true thing about testing tables is that you can often get away with treating the table as a simple array of arrays in your tests, even if the underlying implementation uses complex DOM manipulation or virtual scrolling.

Here’s how you might see this in action with a dynamic dataset, perhaps fetched from an API. Imagine your users.json looks like this:

[
  {"id": 1, "firstName": "Alice", "lastName": "Smith", "email": "alice.smith@example.com"},
  {"id": 2, "firstName": "Bob", "lastName": "Johnson", "email": "bob.johnson@example.com"},
  {"id": 3, "firstName": "Charlie", "lastName": "Williams", "email": "charlie.williams@example.com"}
]

And your Cypress users.cy.js might use cy.intercept to mock the API call:

describe('User Table Sorting with Mocked API', () => {
  beforeEach(() => {
    cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
    cy.visit('/users');
    cy.wait('@getUsers'); // Wait for the API call to complete
  });

  it('should sort users by last name', () => {
    // Initial check (optional, but good for confidence)
    cy.get('#user-rows tr[data-cy="user-row"]').eq(0).find('td').eq(1).should('contain', 'Smith');

    // Sort by Last Name
    cy.get('th[data-cy="sort-header"][data-sort-key="lastName"]').click();
    cy.wait(100); // Allow DOM to update

    // Verify sorted order
    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(0)
      .find('td')
      .eq(1)
      .should('contain', 'Johnson'); // Bob Johnson should be first

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(1)
      .find('td')
      .eq(1)
      .should('contain', 'Smith'); // Alice Smith second

    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(2)
      .find('td')
      .eq(1)
      .should('contain', 'Williams'); // Charlie Williams third
  });

  it('should verify specific user data after sorting', () => {
    // Sort by Last Name
    cy.get('th[data-cy="sort-header"][data-sort-key="lastName"]').click();
    cy.wait(100);

    // Find Charlie Williams row and check its details
    cy.get('#user-rows tr[data-cy="user-row"]')
      .filter((index, el) => Cypress.$(el).find('td').eq(1).text() === 'Williams') // Find the row with 'Williams' in the last name column
      .should('have.length', 1) // Ensure only one such row exists
      .find('td')
      .eq(0) // First Name cell
      .should('contain', 'Charlie');

    // Verify Charlie is in the last position after ascending sort
    cy.get('#user-rows tr[data-cy="user-row"]')
      .eq(2)
      .find('td')
      .eq(1)
      .should('contain', 'Williams');
  });
});

When testing complex tables, especially those with features like pagination, filtering, or infinite scrolling, you might find yourself writing helper functions to extract all visible row data into a JavaScript array of objects. This array can then be sorted, filtered, and compared against the expected state, making the assertions much cleaner and more robust than trying to traverse the DOM repeatedly for each check.

The next concept you’ll likely encounter is testing interactive table elements, like inline editing or row selection, which requires chaining commands to interact with cells and then asserting the updated state.

Want structured learning?

Take the full Cypress course →