2025, Oct 07 09:23

Почему XPath ломает ожидание в Selenium и как это исправить

Разбор кейса, когда ожидание элемента в Selenium срывается из-за неверного XPath. Узнайте, как переписать локатор на @src с contains и избежать хрупких путей.

Ожидание появления иконки в заголовке таблицы — типичный сценарий UI‑тестов. Но нередко тест «не ждёт» не из‑за таймингов, а из‑за локатора. Ниже — точечный разбор реального случая, когда XPath мешал Selenium корректно ждать элемент, и аккуратное решение проблемы.

Проблема

Страница рендерит заголовок: внутри ячейки заголовка таблицы находится input с изображением:

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

Тест пытается дождаться появления этого элемента:

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)

Несмотря на то что элемент есть на странице, ожидание ведёт себя не так, как задумано. Вопрос — корректен ли селектор?

Почему ожидание не срабатывает

XPath составлен неверно. В нём выполняется сравнение самого элемента с фрагментом имени файла через input=cell_state_header_icon.png вместо проверки атрибута. Чтобы найти input по имени файла изображения, сравнение должно обращаться к атрибуту src — например, через @src вместе с contains().

Есть и вторая структурная проблема: путь абсолютный и жёстко привязан к верстке страницы. Длинные цепочки div и внутренностей таблицы хрупкие: при малейшем сдвиге DOM они ломаются. На практике tbody во время выполнения может отсутствовать, даже если в DevTools он виден под HTML. Надёжный XPath не должен завязываться на случайные детали структуры.

Если код заключён в широкий try/except, ошибки XPath могут «проглатываться» молча. Запустите ожидание без этой обёртки — появятся осмысленные сообщения об ошибках. Без реального URL нельзя исключить и другие факторы страницы: например, если элемент внутри iframe, прежде чем ждать, нужно переключить контекст.

Исправляем локатор

Чтобы обратиться к элементу по имени файла изображения, сравнивайте значение атрибута src. Начните с максимально простого запроса, который способен найти нужный узел. Для самого input компактный вариант выглядит так:

//input[contains(@src, "cell_state_header_icon.png")]

Если цель — ячейка заголовка таблицы, в которой находится этот input, выберите родительский th с предикатом:

//th[input[contains(@src, "cell_state_header_icon.png")]]

Чтобы взять только первую подходящую ячейку заголовка, можно использовать любой из вариантов порядка предикатов:

//th[1][input[contains(@src, "cell_state_header_icon.png")]]
//th[input[contains(@src, "cell_state_header_icon.png")]][1]

Возможны и варианты с группировкой через скобки:

(//th[1])[input[contains(@src, "cell_state_header_icon.png")]]
(//th[input[contains(@src, "cell_state_header_icon.png")]])[1]

Как быстрый sanity‑чек при отладке, проверьте, что драйвер вообще находит хоть какие‑то inputs, сначала выполнив //input. Затем постепенно уточняйте XPath вместо того, чтобы начинать с абсолютного пути.

Рабочий пример

Ниже — фрагмент, который ждёт первый th, содержащий image‑input с заданным именем файла. Показана и альтернатива ends-with(): используется выражение с подстрокой.

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)

    # Пример с использованием contains()
    #node = browser.wait_for_element_present('//th[1][input[contains(@src, "icon.png")]]')

    # Альтернатива ends-with(): используйте сравнение подстроки
    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)

Если доступ к элементу мешает более общая причина — например, он изолирован внутри iframe, — ожидание всё равно не сработает по причинам, не связанным с XPath. Сначала решите задачу доступности, затем вернитесь к упрощённому локатору.

Почему это важно

Именно селекторы определяют, насколько устойчивы ваши UI‑тесты. Слишком абсолютные XPath повышают «хрупкость» и скрывают реальные проблемы за шумом таймингов. Локаторы по атрибутам, особенно построенные вокруг стабильных сигналов вроде @src, обычно переживают правки верстки и перестановки разметки. Ранние простые проверки экономят время: если //input не находит ничего, никакой сложный XPath не исправит исходное несовпадение.

Выводы

Если «ожидание не ждёт», не начинайте с подкрутки тайм‑аута. Сначала проверьте XPath. Явно таргетируйте атрибуты, избегайте абсолютных путей и временных контейнеров, и подтверждайте базовые вещи минимальными запросами. Уберите непрозрачную обработку ошибок, чтобы увидеть реальные ошибки парсера или поиска. Если структура страницы накладывает дополнительные ограничения — например, содержимое внутри iframe, — сначала настройте контекст, а уже затем рассчитывайте на работу локатора. Чистый XPath, сфокусированный на атрибутах, делает ожидания детерминированными и поддерживаемыми.

Статья основана на вопросе на StackOverflow от robm и ответе от furas.