Xpath в web-тестировании, написание и отладка

Всем привет! Хочу сделать небольшой гайд по xpath в контексте тестирования web-приложений с целью систематизировать свои знания и помочь начинающим.
Гайд состоит из 2 частей:

  1. Немного теории про то, как, собственно писать xpath;
  2. Проверка и отладка написанных xpath в консоли браузера.

Поехали!

1. Теория
Для начала нужно понять, с чем мы работаем. Для этого возьмём страницу https://www.google.com/ (ничего нового) и нажмём F12 на клавиатуре:


Слева у нас сжавшаяся страница гугла (таким образом, кстати, можно проверять поведение страницы при изменении расширения, просто уменьшая-увеличивая область страницы), справа панель разработчика со стандартно открытой вкладкой Elements. На этой вкладке у нас DOM-модель страницы, в которой и находятся элементы страницы.

Попробуем найти кнопку Картинки справа вверху страницы. Для этого нажмём на неё правой кнопкой мыши и затем Просмотреть код
image

И увидим этот элемент в дереве:
image

Как нам теперь найти эту кнопку по xpath? Посмотрим, что тэг элемента “a”, атрибут class = "gb_g", а вложенный текст “Картинки”.

Для зануд

Да, можно писать css-селекторы, но сейчас речь об xpath;
Да, искать по тексту не самая лучшая затея, если мы будем использовать разные языки.
Задача - показать, как составлять как можно менее хрупкие локаторы

Давайте напишем этот xpath:

//a[@class = 'gb_g'][contains(text(), 'Картинки')]

image

  • // - для указания поиска от самого начала
  • 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-модель:

  1. Найдем элемент, содержащий название темы
  2. Поднимемся до элемента строки, в которой находятся вся информация по конкретной теме
  3. Спустимся до элемента с количеством ответов
//*[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");

На этом я закончу. Надеюсь, гайд окажется полезным!
Если считаете нужным что-то добавить, пишите в комменты, я добавлю.

17 лайков

интересная статья :muscle:

2 лайка