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-testidon 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
| Goal | XPath 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.