How to Handle Tables in Playwright: A Comprehensive Guide

Playwright JavaScript code to handle table

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);
// 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/);
});
Playwright JavaScript code to handle table

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.

Leave a Reply

Your email address will not be published. Required fields are marked *