Text Matching Functions
Learn to select elements by their text content using text(), contains(), and normalize-space().
Text Matching Functions
Sometimes the best way to identify an element is by its visible text. XPath provides powerful functions for text-based selection.
The text() Function
The text() function selects the text node children of an element:
//button[text()='Submit'] // Button with exact text "Submit"
//h1[text()='Welcome'] // H1 heading with text "Welcome"
//a[text()='Learn More'] // Link with exact text
Important: text() vs String Value
<button>
<span>Click</span> Here
</button>
//button[text()='Here'] // ❌ Matches - but only " Here" (with space)
//button[text()='Click Here'] // ❌ Fails - "Click" is in span
//button[.='Click Here'] // ✅ Works - checks full string value
The . (dot) represents the string value of the entire element including descendants.
The contains() Function
Match partial text content:
contains(haystack, needle)
Examples
//button[contains(text(), 'Submit')] // Text includes "Submit"
//a[contains(text(), 'Read')] // Link text includes "Read"
//p[contains(., 'error')] // Paragraph contains "error" anywhere
Text vs Attribute Contains
// Text content
//button[contains(text(), 'Add')]
// Attribute value
//button[contains(@class, 'primary')]
// Both
//button[contains(@class, 'btn') and contains(text(), 'Save')]
The normalize-space() Function
Handles whitespace inconsistencies by:
- Trimming leading/trailing whitespace
- Collapsing multiple spaces into one
<button>
Submit
Form
</button>
//button[text()='Submit Form'] // ❌ Fails - whitespace differs
//button[normalize-space()='Submit Form'] // ✅ Works - whitespace normalized
Common Use Cases
// Handle extra whitespace
//td[normalize-space()='Total']
// Combine with contains
//label[contains(normalize-space(), 'Email')]
// Ignore leading/trailing spaces
//option[normalize-space(text())='Select option']
starts-with() Function
Match text that begins with a specific string:
//button[starts-with(text(), 'Add')] // "Add to Cart", "Add Item", etc.
//a[starts-with(., 'Learn')] // Links starting with "Learn"
//h2[starts-with(normalize-space(), 'Chapter')]
Case Sensitivity
XPath 1.0 is case-sensitive. For case-insensitive matching, use translate():
// Convert to lowercase for comparison
//button[contains(
translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'),
'submit'
)]
The translate() approach is verbose. If possible, use your testing framework’s case-insensitive matching capabilities instead.
Combining Text Functions
Complex text matching scenarios:
// Text contains "Add" AND "Cart"
//button[contains(text(), 'Add') and contains(text(), 'Cart')]
// Starts with "Item" and contains number
//span[starts-with(text(), 'Item') and contains(text(), '#')]
// Normalized text equals value
//td[normalize-space(text())='$99.99']
Text in Specific Contexts
Direct Text vs Descendant Text
<div class="message">
<span class="icon"></span>
Error: Invalid email
</div>
// Direct text node
//div[@class='message']/text() // Returns " Error: Invalid email"
// Any descendant text
//div[@class='message'][contains(., 'Error')] // Matches - includes all text
Multiple Text Nodes
<p>Price: <strong>$99</strong> USD</p>
// Full text content
//p[contains(., 'Price')] // ✅ Matches
//p[contains(., '$99')] // ✅ Matches
//p[contains(., 'USD')] // ✅ Matches
// Specific text node (tricky)
//p/text()[contains(., 'Price')] // Gets the text node directly
Practical Patterns
Find Button by Label
//button[normalize-space()='Submit Order']
//button[contains(text(), 'Add to') and contains(text(), 'Cart')]
Find Table Cell by Content
//td[normalize-space()='John Doe']
//td[contains(text(), '@example.com')]
Find Error Messages
//*[contains(@class, 'error')][contains(text(), 'required')]
//div[@role='alert'][contains(., 'Invalid')]
Find Links by Partial Text
//a[contains(text(), 'View')] // "View Details", "View All", etc.
//a[starts-with(normalize-space(), 'Download')]
Robustness Considerations
//button[text()=‘Add to Shopping Cart’]Exact text match breaks if copy changes
//button[@data-testid=‘add-to-cart’]Test attribute is stable
When to use text matching:
- No better attribute available
- Text is unlikely to change (e.g., “Submit”, “Cancel”)
- Testing internationalization (different text per locale)
- Verifying specific content appears
Try It Yourself
Practice text functions on our sample pages:
Open in Playground →
Summary
| Function | Purpose | Example |
|---|---|---|
text() | Select text nodes | [text()='Submit'] |
. | Full string value | [.='Full Text Here'] |
contains() | Partial match | [contains(text(), 'Add')] |
normalize-space() | Handle whitespace | [normalize-space()='Submit'] |
starts-with() | Prefix match | [starts-with(text(), 'Item')] |
translate() | Case conversion | [translate(...) = 'lower'] |
Pro tip: Always prefer data-testid over text when available. Use text matching as a fallback, and prefer contains() over exact matches for resilience.