Tables are one of the most challenging elements to automate in web testing. With dynamic content, pagination, and complex structures, tables require special handling in your Playwright tests. This comprehensive guide covers everything from basic table interactions to advanced scenarios you’ll encounter in real-world test automation.
According to recent test automation surveys, table validation is among the top 5 most challenging UI test scenarios, with 68% of automation engineers reporting difficulties with complex table structures.
Understanding HTML Table Structure
Before learning how to handle tables in Playwright, it’s essential to understand their basic HTML structure:
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Cell 1</td>
<td>Row 1 Cell 2</td>
</tr>
<tr>
<td>Row 2 Cell 1</td>
<td>Row 2 Cell 2</td>
</tr>
</tbody>
</table>
Basic Table Interactions in Playwright
Locating a Table
First, locate the table element:
const table = page.locator('table');
Counting Rows and Columns
Count all rows (including the header if present)
const rowCount = await table.locator('tr').count();
Count rows in the table body only
const bodyRowCount = await table.locator('tbody tr').count();
Count columns in the first row
const colCount = await table.locator('tr:first-child th, tr:first-child td').count();
Working with Table Data
Reading Cell Data
Get the text of a specific cell (row 2, column 1)
const cellText = await table.locator('tr:nth-child(2) td:nth-child(1)').textContent();
console.log(`Cell text is: ${cellText}`);
Get all cell texts in a 2D array
const allRows = await table.locator('tr').all();
const tableData = [];
for (const row of allRows) {
const cells = await row.locator('th, td').all();
const rowData = await Promise.all(cells.map(cell => cell.textContent()));
tableData.push(rowData);
}
console.log(tableData);
Finding a Row by Cell Content
Find the row containing specific text
const targetRow = table.locator('tr', { hasText: 'Search Text' });
Find the row where a specific column contains text
const targetRow = table.locator('tr', {
has: page.locator('td:nth-child(2)', { hasText: 'Email Value' })
});
Validating Table Content
Check if the table contains the expected text
await expect(table).toContainText('Expected Value');
Check specific cell content
await expect(table.locator('tr:nth-child(3) td:nth-child(2)')).toHaveText('Expected Value');
Advanced Table Interactions
Handling Dynamic Tables
For tables with dynamic content, use waiting mechanisms:
// Wait for table to have at least 5 rows
await expect(table.locator('tr')).toHaveCount(5, { timeout: 5000 });
// Wait for specific content to appear
await expect(table.locator('tr', { hasText: 'Dynamic Content' })).toBeVisible();
Sorting Tables
Test table sorting functionality:
// Click on header to sort
await table.locator('th:nth-child(1)').click();
// Verify sorting (alphabetical example)
const firstCellAfterSort = await table.locator('tbody tr:first-child td:first-child').textContent();
const secondCellAfterSort = await table.locator('tbody tr:nth-child(2) td:first-child').textContent();
expect(firstCellAfterSort.localeCompare(secondCellAfterSort)).toBeLessThanOrEqual(0);
Paginated Tables
For tables with pagination:
// Click next page button
await page.locator('.next-page-button').click();
// Verify current page
await expect(page.locator('.page-info')).toHaveText('Page 2 of 5');
// Verify table content changed
await expect(table.locator('tr:first-child td:first-child'))
.not.toHaveText(previousFirstCellText);
Tables with Actions (Buttons, Links)
// Click button in specific row
const targetRow = table.locator('tr', { hasText: 'Target Row' });
await targetRow.locator('button.action-button').click();
// Verify action result
await expect(page.locator('.result-message')).toHaveText('Action successful');
Example: Complete Table Test
import { test, expect } from '@playwright/test';
test('Verify user data table', async ({ page }) => {
await page.goto('/users');
const userTable = page.locator('#users-table');
// Verify table is visible
await expect(userTable).toBeVisible();
// Verify header count
const headers = await userTable.locator('th').all();
expect(headers.length).toBe(5);
// Verify at least 1 data row exists
await expect(userTable.locator('tbody tr')).toHaveCountGreaterThan(0);
// Find and verify specific user
const testUserRow = userTable.locator('tr', {
has: page.locator('td:nth-child(2)', { hasText: 'testuser@example.com' })
});
await expect(testUserRow.locator('td:nth-child(1)')).toHaveText('John Doe');
await expect(testUserRow.locator('td:nth-child(3)')).toHaveText('Active');
// Click edit button in the row
await testUserRow.locator('button.edit-btn').click();
await expect(page).toHaveURL(/\/users\/edit/);
});

Best Practices
- Use specific selectors: Prefer IDs or data-testid attributes over generic table selectors when possible.
- Assert wisely: Focus on verifying the most critical data rather than entire tables.
- Handle empty states: Test how your table behaves with no data.
- Consider accessibility: Use proper table headers and ARIA attributes in your app to make testing easier.
Final Words
Working with tables in Playwright involves knowledge of both the HTML table structure and Playwright’s robust locator API. By integrating CSS selectors with Playwright’s text matching and relational locators, you are able to effectively work with even intricate tables within your tests. Be sure to make reusable functions for frequent table operations so your tests remain maintainable and readable.
Related Articles
- How to Perform Double Click in Playwright Using dblclick()
- How to Perform Right Click in Playwright (With Example)
- How to Select DropDown Value in Playwright Using selectOption()
Hi, I’m Aravind, a seasoned Automation Test Engineer with 17+ years of industry experience. I specialize in tools like Selenium, JMeter, Appium, and Excel automation. Through this blog, I share practical tutorials, tips, and real-world insights to help testers and developers sharpen their automation skills.