Advanced advanced tables data

Working with Tables

Learn strategies for selecting table cells, rows, and data dynamically based on headers and content.

Working with Tables

Tables are among the most challenging structures to navigate with XPath. This guide covers strategies from simple to complex.

Table Structure Basics

Typical HTML table structure:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Status</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>John Doe</td>
      <td>john@example.com</td>
      <td>Active</td>
      <td><button>Edit</button></td>
    </tr>
  </tbody>
</table>

Basic Table Selection

All Rows

//table//tr                      // All rows
//table/tbody/tr                 // Body rows only
//table/thead/tr                 // Header rows only

Specific Cell by Position

//table//tr[2]/td[3]             // Row 2, Column 3 (body)
//table//tr[1]/th[1]             // First header cell
//table/tbody/tr[1]/td[1]        // First data cell
Warning:

Position-based selection (tr[2]/td[3]) is fragile. If rows or columns are added/removed, your selector breaks.

Dynamic Column Selection

The key technique: find the column index from the header, then use that index to select data cells.

Step 1: Find Column Index

// Count columns before "Status" header + 1
count(//th[text()='Status']/preceding-sibling::th) + 1

Step 2: Use Index in Selection

// Select all cells in the "Status" column
//tbody/tr/td[count(//th[text()='Status']/preceding-sibling::th) + 1]

// Or with XPath 2.0+ (not all browsers support)
//tbody/tr/td[position() = //th[text()='Status']/count(preceding-sibling::th) + 1]

Complete Pattern

// Get Status for row containing "John Doe"
//tr[td[1][text()='John Doe']]/td[
  count(//th[text()='Status']/preceding-sibling::th) + 1
]

Row Selection Strategies

By Cell Content

// Row containing specific email
//tr[td[contains(text(), '@example.com')]]

// Row where Name column equals "John Doe"
//tr[td[1][text()='John Doe']]

// Row with specific status
//tr[.//td[text()='Active']]

By Data Attribute

// If rows have data attributes
//tr[@data-user-id='123']
//tr[@data-status='pending']

Multiple Conditions

// Row with specific name AND status
//tr[td[1][text()='John Doe']][.//td[text()='Active']]

// Or more explicitly
//tr[td[text()='John Doe'] and td[text()='Active']]

Cell Selection Patterns

Find Cell by Row and Column Names

// Price for "Product A"
//tr[td[text()='Product A']]/td[
  count(//th[text()='Price']/preceding-sibling::th) + 1
]

Find Cell Relative to Another

// Cell after the one containing "Total"
//td[text()='Total']/following-sibling::td[1]

// Edit button in same row as "John"
//tr[.//td[contains(text(), 'John')]]//button[text()='Edit']

Get All Values in a Column

// All emails (assuming Email is column 2)
//tbody/tr/td[2]

// All emails dynamically
//tbody/tr/td[count(//th[text()='Email']/preceding-sibling::th) + 1]

Action Buttons in Tables

Button in Specific Row

// Edit button for row with user "john@example.com"
//tr[.//td[text()='john@example.com']]//button[text()='Edit']

// Delete button for first row
//tbody/tr[1]//button[contains(@class, 'delete')]

Button by Data Attributes

// Action button with data attribute
//button[@data-action='edit'][@data-user-id='123']

// Or find row first, then button
//tr[@data-user-id='123']//button[@data-action='edit']

Complex Table Patterns

Nested Tables

// Inner table cells (avoid outer table)
//table[@id='inner']//td

// Outer table only
//table[@id='outer']/tbody/tr/td[not(.//table)]

Tables with Colspan/Rowspan

// These are tricky - position counting gets complicated
// Best strategy: use unique identifiers when available
//td[@data-cell-id='total-value']

// Or identify by content
//td[contains(@class, 'total-cell')]

Sortable Tables

// Find sort button for column
//th[text()='Name']//button[@class='sort']

// Selected sort header
//th[contains(@class, 'sorted')]

Pagination Context

// First row of current page
//table/tbody/tr[1]

// Last row of current page
//table/tbody/tr[last()]

// Row count
count(//table/tbody/tr)

Practical Examples

Get All Active Users

//tr[td[count(//th[text()='Status']/preceding-sibling::th) + 1][text()='Active']]

Find Row with Highest Price

// XPath 1.0 doesn't have max(), so this requires multiple steps
// First get all prices, then find the max programmatically
//tr/td[count(//th[text()='Price']/preceding-sibling::th) + 1]

Select Action for Multiple Rows

// All Edit buttons for pending items
//tr[.//td[text()='Pending']]//button[text()='Edit']

// All Delete buttons (if confirmation needed)
//tr//button[@data-action='delete']

Robust Table Selector Strategies

Do:
  • Use data-testid on rows and cells
  • Use data attributes for row identification
  • Calculate column index from headers
  • Select by cell content when unique
Avoid:
  • Hard-coded row/column numbers
  • Relying on visual position
  • Selecting by row color/style
  • Long absolute paths through table structure

Try It Yourself

Practice table navigation with our inventory table:

Open in Playground →

Summary

GoalXPath Pattern
All body rows//tbody/tr
Specific column//td[count(//th[text()='ColName']/preceding-sibling::th)+1]
Row by content//tr[.//td[text()='value']]
Cell by row content//tr[.//td[text()='row-id']]/td[col-index]
Button in row//tr[.//td[text()='identifier']]//button

Best practice: Request data-testid attributes on table rows (data-row-id) and important cells. This makes table testing dramatically simpler.