Всем привет! Хочу сделать небольшой гайд по xpath в контексте тестирования web-приложений с целью систематизировать свои знания и помочь начинающим.
Гайд состоит из 2 частей:
- Немного теории про то, как, собственно писать xpath;
- Проверка и отладка написанных xpath в консоли браузера.
Поехали!
1. Теория
Для начала нужно понять, с чем мы работаем. Для этого возьмём страницу https://www.google.com/ (ничего нового) и нажмём F12 на клавиатуре:
Слева у нас сжавшаяся страница гугла (таким образом, кстати, можно проверять поведение страницы при изменении расширения, просто уменьшая-увеличивая область страницы), справа панель разработчика со стандартно открытой вкладкой Elements. На этой вкладке у нас DOM-модель страницы, в которой и находятся элементы страницы.
Попробуем найти кнопку Картинки
справа вверху страницы. Для этого нажмём на неё правой кнопкой мыши и затем Просмотреть код
И увидим этот элемент в дереве:
Как нам теперь найти эту кнопку по xpath? Посмотрим, что тэг элемента “a”, атрибут class = "gb_g"
, а вложенный текст “Картинки”.
Для зануд
Да, можно писать css-селекторы, но сейчас речь об xpath;
Да, искать по тексту не самая лучшая затея, если мы будем использовать разные языки.
Задача - показать, как составлять как можно менее хрупкие локаторы
Давайте напишем этот xpath:
//a[@class = 'gb_g'][contains(text(), 'Картинки')]
-
//
- для указания поиска от самого начала -
a
- тэг элемента -
@class
- через @ передаем название атрибута, а в одинарных кавычках его значение -
contains
- указываем, что будем искать, не передавая полное содержимое атрибута или свойства, а часть искомого значения; через запятую в круглых скобках в чём должно содержаться и что именно -
text()
- так как текст - это не атрибут (подскажите в комментах, что это такое, пожалуйста), то его передаем со скобками без @ -
'Картинки'
- что именно ищем в text().
Теперь давайте перепишем этот xpath:
//*[local-name() = 'a' and contains(@class, 'gb_g') and text() = 'Картинки']
Что поменялось?
- мы использовали один набор квадратных скобок, в которых условия поиска передали через
AND
, то есть искомый элемент должен соответствовать всем переданным условиям -
//
- поиск от самого начала -
*
- элементы с любыми тэгами -
local-name() = 'a'
- отсев по тэгу. Используется, если у вас кастомные тэги во фрейворке фронтенда, по которым selenium не может найти элемент как в первом примере. Тогда просто говорим в xpath, что сначала ищи со всеми тэгами, а потом отсеиваем по свойствуlocal-name()
-
contains(@class, 'gb_g')
- поиск элемента, в атрибуте @class которого будет'gb_g'
Аналогично поиску по тексту в первом примере - указали, что искать будем в атрибуте class подстроку gb_g -
text() = 'Картинки'
- свойствоtext()
должно быть именно равноКартинки
, а не содержать в себе такой текст.
В принципе, этого уже вполне достаточно, чтобы находить элементы с заданной точностью. Теперь надо поговорить о том, зачем используются xpath-локаторами.
Оси xpath
Осями называют функции перемещения по дереву в любом направлении. Например:
- Найти всех предков текущего элемента -
ancestor::
- Найти всех потомков текущего элемента -
descendant::
- и куча других, почитать тут Оси языка Xpath — Umicms
Для чего это используется?
Если у вас на странице много однотипных элементов, к которым нельзя написать уникальный локатор напрямую (чекбоксы, радиобаттоны и тому подобные штуки), но рядом есть другие элементы с уникальным текстом или другими атрибутами и свойствами, за которые вы сможете зацепиться.
Допустим, нам надо получить количество ответов в какой-то теме на этом форуме:
Думаю, очевидно, нужно каким-то образом завязаться на название темы, чтобы затем как-то через неё получить количество ответов. Посмотрим на DOM-модель:
- Найдем элемент, содержащий название темы
- Поднимемся до элемента строки, в которой находятся вся информация по конкретной теме
- Спустимся до элемента с количеством ответов
//*[contains(text(), 'DRY / KISS')]
/ ancestor::*[contains(@class, 'topic-list-item')]
/ descendant::*[contains(@class, 'posts')]
/ descendant::*[@class = 'number']
-
//*[contains(text(), 'DRY / KISS')]
- нашли элемент с названием темы -
/ ancestor::*[contains(@class, 'topic-list-item')]
- элемент строки темы со всей информацией, предок первого элемента -
/ descendant::*[contains(@class, 'posts')]
- промежуточный элемент-потомок строки, который содержит в себе именно количество ответов (если не брать этот, то в следующем шаге будет коллекция с количеством ответов и количеством просмотров темы) -
/ descendant::*[@class = 'number']
- элемент, в котором и содержится нужная нам информация по количеству ответов
Собственно всё по xpath и осям. Надеюсь, этого вам будет достаточно для понимания принципа написания локаторов и хождения по дереву элементов с помощью осей.
2. Отладка локаторов
После того как вы написали локатор до нужного элемента, стоит определиться, что с этим элементом вы хотите сделать:
- кликнуть – самое простое,
_driver.findElement(By.Xpath("ваш xpath")).Click()
; - ввести текст или опционально предварительно очистить поле ввода и уже вводить текст:
var element = _driver.findElement(By.Xpath("ваш xpath")).Click();
element.Clear();
element.SendKeys("текст для отправки в поле");
А если вам, наоборот, надо получить какое-то значение со страницы, и конструкция _driver.findElement(By.Xpath("ваш xpath")).Text
возвращает пустую строку? В таком случае вам поможет консоль браузера в тех же инструментах разработчика.
Перейдем в google.com, введём в строку поиска текст, найдём локатор и посмотрим, что есть в дереве элементов:
Мы написали локатор, который сейчас однозначно находит строку поиска, но выцепить введённый текст не получится. И кодом через
element.Text
получим, скорее всего, пустую строку.Давайте перейдём на вкладку
Console
и выполним следующий код:
var elems = $x("//input[@type = 'text']")
elems.forEach(x => console.log(x.innerText))
elems.forEach(x => console.log(x.value))
elems.forEach(x => console.log(x.outerText))
elems.forEach(x => console.log(x.innerHTML))
elems.forEach(x => console.log(x.outerHTML))
Что мы сделали?
-
var elems = $x("//input[@type = 'text']")
– в переменную elems положили все элементы по локатору, или если кодом селениумаvar elems = _driver.findElements(By.Xpath("//input[@type = 'text']"))
-
elems.forEach(x => console.log(x.innerText))
– для каждого элемента коллекции вывести текст -
elems.forEach(x => console.log(x.value))
– вывести текущее значение каждого элемента -
elems.forEach(x => console.log(x.outerText))
– чем отличается innerText от outerText сходу не скажу, но порой так можно получить доступ к тексту -
elems.forEach(x => console.log(x.innerHTML))
– получить HTML-код всех вложенных элементов в элементы коллекции -
elems.forEach(x => console.log(x.outerHTML))
– получить HTML-код самих элементов коллекции и вложенных в них
Также можно обратиться к элементу коллекции по индексу и через точку посмотреть, какие свойства и атрибуты доступны:
Если кодом Selenium, то выглядеть это будет так:
var elem = _driver.findElement(By.Xpath("//input[@type = 'text']"));
string text = elem.Text;
text = elem.GetAttribute("value");
text = elem.GetAttribute("outerText");
text = elem.GetAttribute("innerHTML");
text = elem.GetAttribute("outerHTML");
На этом я закончу. Надеюсь, гайд окажется полезным!
Если считаете нужным что-то добавить, пишите в комменты, я добавлю.