Text Handling intermediate text functions

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:

  1. Trimming leading/trailing whitespace
  2. 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'
)]
Note:

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')]
//a[contains(text(), 'View')]          // "View Details", "View All", etc.
//a[starts-with(normalize-space(), 'Download')]

Robustness Considerations

Fragile:
//button[text()=‘Add to Shopping Cart’]

Exact text match breaks if copy changes

Robust:
//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

FunctionPurposeExample
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.