2025, Oct 07 09:00
Fix Selenium waits with resilient XPath: target @src for image inputs in table headers
Learn why Selenium waits fail with malformed XPath and brittle absolute paths. Use @src-based selectors for image inputs in table headers, plus debug tips.
Waiting for a header icon to appear in a table is a routine UI test scenario. Yet tests often “don’t wait” not because timing is off, but because the locator is. Here’s a focused walkthrough of a real-world case where an XPath prevented Selenium from waiting properly, and how to fix it cleanly.
Problem
The page renders a header with an image-based input inside a table header cell:
<th><input type="image" src="../../..//images/icons/cell_state_header_icon.png" onclick="javascript:__doPostBack('ctl00$left_pane$gvSelectedFeatures','Sort$Status')" style="border-width:0px;"></th>The test attempts to wait for this element to be present:
runner.wait_for_element_present("//*[/html/body/form/div[3]/div/div[3]/div/div[5]/div[2]/div/div[2]/table/tbody/tr[1]/th[1]/input=cell_state_header_icon.png]", timeout=None)Despite the element being on the page, the wait does not behave as expected. The question is whether the selector is correct.
Why the wait fails
The XPath is malformed. It tries to compare an element directly to a filename fragment using input=cell_state_header_icon.png instead of checking an attribute. To match an input by its image filename, the comparison must target the src attribute, for example via @src with contains().
There is a second structural problem: the path is absolute and deeply tied to the page’s layout. Such long chains of div and table internals are brittle. They often break when the DOM shifts, and in practice tbody can be missing at runtime even if DevTools shows it under HTML. A robust XPath should avoid coupling to incidental structure.
If the code is wrapped in a broad try/except, XPath errors may be swallowed silently. Running the wait without that wrapper helps surface meaningful error messages. Without a real URL, other page-specific factors cannot be excluded; for example, an element inside an iframe would require switching context before waiting.
Fixing the locator
To target the element by its image filename, compare against the src attribute. Start with the simplest possible query that can match the intended node. For the input itself, a concise form looks like this:
//input[contains(@src, "cell_state_header_icon.png")]If the goal is the table header cell that contains that input, select the parent th with a predicate:
//th[input[contains(@src, "cell_state_header_icon.png")]]To target only the first matching header cell, either predicate ordering can be used:
//th[1][input[contains(@src, "cell_state_header_icon.png")]]
//th[input[contains(@src, "cell_state_header_icon.png")]][1]Parentheses-based grouping variants are also possible:
(//th[1])[input[contains(@src, "cell_state_header_icon.png")]]
(//th[input[contains(@src, "cell_state_header_icon.png")]])[1]As a quick sanity check during debugging, verify the driver can find any inputs at all by testing //input first. Then refine the XPath gradually instead of starting from an absolute path.
Working example
The following snippet demonstrates waiting for the first th that contains an image input with a specific filename. It also shows an alternative to ends-with() by using a substring expression.
from seleniumbase import SB
import seleniumbase as sb_mod
print("SeleniumBase:", sb_mod.__version__)
markup = """
<table>
<tr>
<th><input type="image" src="../../..//images/icons/cell_state_header_icon.png" onclick="javascript:__doPostBack('ctl00$left_pane$gvSelectedFeatures','Sort$Status')" style="border-width:0px;">Header 1</th>
<th><input type="image" src="../../..//images/icons/cell_state_header_icon.png" onclick="javascript:__doPostBack('ctl00$left_pane$gvSelectedFeatures','Sort$Status')" style="border-width:0px;">Header 2</th>
<th><input type="image" src="../../..//images/icons/cell_state_header_icon.png" onclick="javascript:__doPostBack('ctl00$left_pane$gvSelectedFeatures','Sort$Status')" style="border-width:0px;">Header 3</th>
</tr>
<table>
"""
with SB(uc=True) as browser:
    browser.load_html_string(markup)
    # Example using contains()
    #node = browser.wait_for_element_present('//th[1][input[contains(@src, "icon.png")]]')
    # Alternative to ends-with(): use substring comparison
    node = browser.wait_for_element_present(
        '//th[1][input[substring(@src, string-length(@src) - string-length("icon.png") + 1) = "icon.png"]]'
    )
    print('text:', node.text)If a broader cause prevents the element from being reachable, such as isolation within an iframe, the wait would still not succeed for reasons unrelated to XPath. In that case, address reachability first, then re-apply the simplified locator approach.
Why this matters
Selectors define how resilient your UI tests are. Overly absolute XPaths increase flakiness and hide genuine issues behind timing noise. Attribute-based locators, especially those built around stable signals like @src, tend to be durable across layout tweaks and markup reshuffles. Early, simple checks save time: if //input doesn’t match anything, no complex XPath will fix the underlying mismatch.
Conclusion
When a wait “doesn’t wait,” don’t tune the timeout first. Inspect the XPath. Target attributes explicitly, avoid absolute paths and transient containers, and verify the basics with minimal queries. Remove opaque error handling to surface real parser or lookup errors. If the page structure imposes additional constraints, like content inside an iframe, handle context before expecting any locator to work. Clean, attribute-focused XPath keeps your waits deterministic and your suite maintainable.