Skip to content
Overview

Tables are one of the densest UI patterns to automate well — rows repeat the same structure, columns are sortable, and content changes with search, filters, and pagination. This page walks through locating rows/cells reliably and asserting behavior across all three frameworks.

1 · Static Table

For a fixed table, use role-based locators (table/columnheader/row/cell) plus data-testid when a cell needs to be uniquely identified by row and column.

TypeScript
// Playwright — locate a cell by row and column testid
const table = page.getByTestId('static-table');
await expect(table).toBeVisible();

const cell = page.getByTestId('cell-1-price');
await expect(cell).toHaveText('$1,299');
2 · Sortable Columns

Sortable headers usually cycle through three states: ascending, descending, and unsorted. Assert the aria-sort attribute rather than relying on visual arrows, and verify actual row order after each click.

TypeScript
// Playwright — 3-click sort cycle: asc -> desc -> unsorted
const header = page.getByTestId('sort-th-name');

await header.click();
await expect(header).toHaveAttribute('aria-sort', 'ascending');

await header.click();
await expect(header).toHaveAttribute('aria-sort', 'descending');

await header.click();
await expect(header).toHaveAttribute('aria-sort', 'none');
3 · Search & Filter

Search and dropdown filters often combine with AND logic. Test them independently first, then together, and always cover the empty-results state.

TypeScript
// Playwright — search + department filter (AND logic)
await page.getByTestId('search-input').fill('Priya');
await page.getByTestId('dept-filter').selectOption('Engineering');

await expect(page.getByTestId('search-result-count')).toContainText('Showing');
4 · Pagination

Assert the exact row count per page, the disabled state of Prev/Next at the boundaries, and that the info text (e.g. 'Showing 1–5 of 15') updates after navigation.

TypeScript
// Playwright — pagination boundaries
await expect(page.getByTestId('pag-info')).toHaveText('Showing 1–5 of 15');
await expect(page.getByTestId('pag-prev')).toBeDisabled();

await page.getByTestId('pag-btn-3').click();
await expect(page.getByTestId('pag-next')).toBeDisabled();
5 · Row Actions

Edit/Delete buttons repeat per row, so every locator must be scoped to that row's unique data-testid or id — never rely on a global button selector.

TypeScript
// Playwright — scoped edit / save
await page.getByTestId('edit-btn-1').click();
await page.getByTestId('edit-name-1').fill('New Name');
await page.getByTestId('save-btn-1').click();

await expect(page.getByTestId('actions-name-1')).toHaveText('New Name');
6 · Combined Data Grid

When search, sort, and pagination combine, changing any one of them typically resets the page back to 1. Assert that reset explicitly instead of assuming it.

TypeScript
// Playwright — sort resets the combined grid to page 1
await page.getByTestId('grid-search').fill('a');
await page.getByTestId('grid-btn-2').click(); // go to page 2

await page.getByTestId('grid-th-name').click(); // sort
await expect(page.getByTestId('grid-btn-1')).toHaveAttribute('aria-current', 'page');
Method Summary

Quick reference across all three frameworks.

ActionSeleniumPlaywright JSPlaywright PYCypress
Locate a cell by row + columnfindElement(By.css("[data-testid='cell-1-price']"))getByTestId('cell-1-price')get_by_test_id('cell-1-price')get('[data-testid="cell-1-price"]')
Assert sort stategetAttribute("aria-sort")toHaveAttribute('aria-sort', 'ascending')to_have_attribute('aria-sort', 'ascending')should('have.attr', 'aria-sort', 'ascending')
Select a dropdown filternew Select(el).selectByVisibleText(...)selectOption('Engineering')select_option('Engineering').select('Engineering')
Assert disabled pagination button!element.isEnabled()expect(locator).toBeDisabled()expect(locator).to_be_disabled().should('be.disabled')
Scope a row action buttonfindElement(By.css("[data-testid='edit-btn-1']"))getByTestId('edit-btn-1')get_by_test_id('edit-btn-1')get('[data-testid="edit-btn-1"]')
FAQ