Last updated on August 1st, 2025 at 10:22 am
This guide will show you how to handle tables in Playwright with step-by-step examples. You’ll learn how to locate tables, read cell values, loop through rows, and interact with dynamic table data in your automation scripts.
Tables are often considered one of the most difficult elements to automate during web testing. This is mainly because they come with dynamic content, pagination, and sometimes very complex structures. As a result, they need extra care when writing your Playwright tests. Fortunately, this complete guide on how to handle tables in Playwright walks you through everything—from simple table interactions to more advanced, real-world scenarios you’re likely to face in automation.
In fact, based on recent test automation surveys, table validation ranks among the top five most challenging UI test tasks. Interestingly, about 68% of automation engineers say they struggle when working with complicated table layouts.
Tables require special handling in your Playwright tests. You can also check how to handle dropdowns using selectOption(), which is another tricky UI element to automate effectively
Understanding HTML Table Structure
Before diving into how to handle tables in Playwright, it’s important first to understand their basic HTML structure. This foundational knowledge will make it much easier to work with tables effectively in your test scripts.
<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 Playwright Table Interactions
Locating a Table
First, locate the table element:
const table = page.locator('table');
Counting Table Rows and Columns In Playwright
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();
Before interacting with dynamic elements, make sure they’re fully visible. Learn how to scroll to elements in Playwright to avoid flaky test issues.
Working with Table Data in Playwright
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);
While working with dynamic data, key events may be needed. See how to simulate keyboard actions in Playwright such as navigation or editing rows.
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 Playwright Tutorial Quick Links
- Handle alerts, confirmations, and prompts in Playwright
- Handle Date Pickers in Playwright
- Scroll Down and Top in Playwright
- Maximize Browser Window in Playwright
- Hover Over Element in Playwright
- Focus on an Element Using Playwright
Advanced Table Interactions
Playwright Dynamic Table Handling
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 Playwright 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 requires a good understanding of both the HTML table layout and Playwright’s powerful locator API. Fortunately, by combining CSS selectors with Playwright’s text-based and relational locators, you can interact with even complex tables more easily. Additionally, to keep your tests clean and maintainable, it’s a smart idea to create reusable functions for common table actions like row selection, data extraction, or cell validation.
FAQs on Handling Tables in Playwright
Can Playwright interact with HTML tables directly?
Yes, Playwright can easily interact with HTML tables. You can use page.locator()
to select specific rows, columns, or even cell values using CSS selectors or XPath.
How do I extract data from a table using Playwright?
To extract table data, use a loop with locator.nth(index)
to iterate through rows or cells. Then, use methods like .textContent()
or .innerText()
to get the text inside each cell.
Can I filter table rows in Playwright based on cell text?
Absolutely! Playwright allows filtering rows using locator.filter()
or locator.locator(":has-text('value')")
to match rows that contain specific text.
Is XPath useful for working with tables in Playwright?
Yes, XPath is very useful—especially when CSS selectors fall short. It helps you select rows, columns, or even nested cells based on their structure or text content.
How do I click a button inside a table row using Playwright?
You can chain locators to narrow down to a specific row and then find the button within it. For example:
await page.locator('table tr:has-text("Product 1") button').click();

Hi, I’m Aravind — a seasoned Automation Test Engineer with over 17 years of hands-on experience in the software testing industry. I specialize in tech troubleshooting and tools like Selenium, Playwright, Appium, JMeter, and Excel automation. Through this blog, I share practical tutorials, expert tips, and real-world insights to help testers and developers improve their automation skills.In addition to software testing, I also explore tech trends and user-focused topics, including Snapchat guides, codeless test automation, and more.