Advanced Patterns advanced lists components cards

Lists and Repeated Components

Cards, rows, tiles and list items all look identical to XPath. Learn to select the one you mean by its content, then reach the button inside it, without counting.

Lists and Repeated Components

A product grid has twenty identical cards. A notification list has ten identical rows. Every one of them has an “Add to cart” or “Dismiss” button. The naive locator //button[normalize-space()='Add to cart'] matches all twenty. The test needs the one for the USB-C Hub.

The pattern is always the same: find the container by its content, then descend to the control.

Step 1: identify the repeating unit

Look for the element that wraps one item. It is usually an article, li, tr, or a div with a component class or test id:

<article data-testid="product-card">
  <h3 class="product-name">USB-C Hub</h3>
  <span class="price">$49</span>
  <button>Add to cart</button>
</article>

The unit is article[@data-testid='product-card'].

Step 2: filter units by content

Use a predicate that looks inside the unit with .//:

//article[@data-testid='product-card'][.//h3[normalize-space()='USB-C Hub']]

The leading dot matters. [//h3[...]] would restart from the document root and be true for every card.

Step 3: descend to the control

//article[@data-testid='product-card'][.//h3[normalize-space()='USB-C Hub']]//button[normalize-space()='Add to cart']

That is the whole recipe. It does not care about card order, how many cards there are, or what the button’s classes are.

Variations

Container has no test id

//article[.//h3[normalize-space()='USB-C Hub']]//button
//li[contains(., 'USB-C Hub')]//button[normalize-space()='Remove']

contains(., ...) on the container is quick to write but loose: the text could appear in a description. Prefer targeting the specific child element when you can.

Walking up from the anchor instead

Some people prefer to start at the text and go up:

//h3[normalize-space()='USB-C Hub']/ancestor::article[1]//button

Both are fine. The container-first form reads more naturally and is easier to reuse as a page-object helper with a parameter.

Multiple conditions on the unit

//article[@data-testid='product-card'][.//h3[normalize-space()='USB-C Hub']][.//span[@class='badge'][normalize-space()='Sale']]

Items by state

//li[@data-status='unread']//button[@aria-label='Mark as read']
//tr[td[normalize-space()='Pending']]//button[normalize-space()='Approve']
//*[@role='option'][@aria-selected='true']

Counting and asserting

XPath is also useful for assertions on repeated content:

count(//article[@data-testid='product-card'])                       how many cards
count(//article[@data-testid='product-card'][.//span[@class='badge']])   how many on sale
//article[@data-testid='product-card'][not(.//button)]              cards with no button (bug?)

Evaluate these in DevTools or with document.evaluate in a script; Selenium and Playwright locators must return elements, so use findElements(...).size() or locator.count() for the count.

When you genuinely need “the nth”

If the test really is about position (“the first result is the best match”), keep the index tight to the container:

(//article[@data-testid='product-card'])[1]
(//article[@data-testid='product-card'])[1]//button

And avoid //article[1], which is “first article child of each parent” and may return more than one.

Page-object helper

Turn the pattern into one parameterised function and call it everywhere:

By cardButton(String productName, String buttonText) {
  return By.xpath(
    "//article[@data-testid='product-card'][.//h3[normalize-space()=" + q(productName) + "]]" +
    "//button[normalize-space()=" + q(buttonText) + "]");
}
def card_button(product_name: str, button_text: str) -> str:
    return (
        f"//article[@data-testid='product-card'][.//h3[normalize-space()={q(product_name)}]]"
        f"//button[normalize-space()={q(button_text)}]"
    )

q() is a quoting helper that handles apostrophes in product names. See XPath in Selenium for one.

Try It Yourself

Open in Playground →

Next Steps

  1. Working with Tables - The same idea for rows and columns
  2. Anchoring to Stable Context - Choosing good containers
  3. Position and Indexing - When counting is the right call