Перейти к содержанию

Устойчивые локаторы

В первом сценарии мы нашли заголовок и список курсов. Теперь рассмотрим, как выбрать нужное поле и кнопку так, чтобы проверка описывала пользовательское действие. Посетитель видит подписи «Запрос» и «Найти», а техническая структура страницы может меняться: появятся обёртка, дополнительный блок или другой класс оформления.

Наш результат — поиск курса «Современный JavaScript» через форму каталога. В lesson-03/expected/lesson.spec.js лежит полный файл, заменяющий сценарий предыдущего урока. Общие исходники страницы и данные остаются прежними. Примеры не выполнялись; результат ниже описывает ожидаемое состояние отдельной лаборатории из архива.

Локатор как описание элемента

Локатор задаёт правило поиска, а не навсегда сохранённую ссылку на один DOM-объект. Если приложение перерисовало карточку, следующая операция снова применяет правило к текущему документу. Для нашего каталога это полезно: фильтр заменяет элементы списка, и прежние узлы перестают быть частью страницы.

Рассмотрим элемент ввода. У него есть имя поля q, тип search и связанный текст метки «Запрос». Сценарий выбирает метку, потому что она обозначает назначение элемента для человека. Перенос поля внутрь дополнительной обёртки не изменит эту связь. Выбор через третий дочерний элемент формы, напротив, зависит от структуры, которая не входит в обещание поиска.

const search = page.getByRole('form', { name: 'Поиск курсов' });
const query = search.getByLabel('Запрос', { exact: true });
const submit = search.getByRole('button', { name: 'Найти', exact: true });

Именованная форма ограничивает область. На странице есть ещё форма сообщения об ошибке. Сегодня её поля называются иначе, но со временем одинаковые подписи вполне могут появиться в нескольких местах. Сначала выбрать форму, а затем элемент внутри неё — способ выразить отношение частей интерфейса и избежать случайного совпадения.

В документации локаторов описаны способы поиска по роли, метке и специальному идентификатору. В нашем примере роль и подпись совпадают с учебной задачей. data-testid используется для статуса результатов, потому что на странице два независимых элемента role="status": каталог и сообщение формы. Это явная техническая договорённость, которую можно сохранить при изменении дизайна.

Поиск и состав результата

Полная важная последовательность после открытия страницы выглядит так:

await expect(page.getByTestId('results-status'))
  .toHaveText('Найдено курсов: 4');
await query.fill('JavaScript');
await submit.click();
const courses = page.getByRole('list', { name: 'Курсы' });
await expect(courses.getByRole('listitem')).toHaveCount(1);
await expect(courses.getByRole('link', {
  name: 'Современный JavaScript', exact: true
})).toBeVisible();

Сначала ждём исходный каталог. Затем заменяем значение поля и нажимаем кнопку. Метод fill задаёт полный текст поля, поэтому результат не зависит от старого содержимого. Поиск в приложении приводит заголовок и запрос к нижнему регистру, удаляет внешние пробелы и проверяет вхождение подстроки. Слово JavaScript соответствует только записи c002.

Первое утверждение после поиска подтверждает единственный элемент, а второе — его название. Если программа оставила одну неверную карточку, проверка числа пройдёт, но ожидание ссылки не выполнится. Если программа показала нужную карточку вместе с лишней, число не совпадёт. Два утверждения рассматривают один результат с разных сторон.

Не следует искать ссылку по всему документу без необходимости. Название курса позже может встретиться в рекомендации или футере. Область списка означает, что нас интересует именно результат фильтрации. Она также делает сообщение о неудаче понятнее: не найден элемент внутри определённого списка, а не просто текст где-нибудь на странице.

Неоднозначность полезно исправлять в модели

Предположим, вторая кнопка тоже называется «Найти». Если сценарий использует только глобальный поиск этой кнопки, действие оказывается неоднозначным. Выбор .first() может убрать ошибку, но не объяснит, почему первая кнопка правильная. При перестановке блоков сценарий нажмёт другую кнопку и станет проверять случайное поведение.

Правильнее сузить область до формы «Поиск курсов». Если две одинаковые кнопки находятся в одной форме и выполняют разные действия, стоит пересмотреть их доступные названия. Проверка обнаружила проблему интерфейса: человеку, использующему список кнопок или чтение с экрана, тоже будет трудно различить действия. Локатор не обязан компенсировать такую неопределённость сложным селектором.

Технический идентификатор уместен там, где смысловая выборка недостаточна либо объект не имеет естественного доступного имени. В наших карточках есть data-testid="course-c002", но основной пример продолжает пользоваться названием курса. Идентификатор помогает диагностике данных; название отражает то, что обещано читателю. Эти роли можно сочетать, не превращая каждую проверку в зависимость от невидимых атрибутов.

CSS-селекторы также допустимы. Небольшой постоянный идентификатор области может быть понятнее длинной цепочки ролей. Важно отличать сознательный контракт от случайного класса оформления. Правило .cards > li:nth-child(2) > h2 > a одновременно предполагает порядок, вложенность и конкретные теги. Проверка поиска нужного курса не должна зависеть от всех этих деталей.

Изменение условий того же примера

Замените поисковую строку на javascript и затем на JavaScript. В обоих случаях ожидается та же карточка, поскольку приложение выполняет нормализацию. Это отдельные варианты входа для того же механизма. Для них не требуется другой сайт, другое состояние или новая терминология. В архивном сценарии оставлен один основной вариант, а дополнительные можно оформить позже при необходимости.

Если заголовок курса изменится, например станет «JavaScript для начинающих», тест на точное название перестанет соответствовать данным. Это не означает, что точное сравнение вредно. Оно выявляет изменение обещанного текста. Редактор должен решить, является ли новое название намеренным, и только после этого обновить ожидаемое значение. Автоматическая замена ожиданий на текущий DOM скрывала бы случайную потерю названия.

Поиск по ролям не заменяет полный аудит доступности. Он помогает связать действие с семантикой и обнаружить некоторые проблемы имени или структуры. Видимость результата не гарантирует достаточный контраст, понятность всех форм или работу вспомогательных технологий. Такие задачи требуют собственных условий и наблюдений, часть которых мы добавим в уроке о клавиатуре.

На этом шаге сценарий выражает простое действие читателя: открыть поиск, ввести название, получить единственный соответствующий курс. Локаторы сохраняют этот смысл, даже когда карточки создаются заново. Следующая тема — как ждать изменения состояния, не привязываясь к произвольной паузе между вводом и результатом.

Одно правило поиска может выглядеть устойчивым технически и при этом проверять неверный смысл. Например, идентификатор второй карточки останется прежним, но редактор случайно подставит название другого курса. Проверка только идентификатора не заметит потерю обещанного текста. Поэтому мы сопоставляем результат с названием, которое увидит читатель.

С другой стороны, безусловное ожидание точной вложенности тегов не помогает обнаружить смысловую ошибку. Заголовок может перейти из h2 в другой компонент при сохранённой роли, имени и ссылки. Выбор локатора всегда связан с тем, какую часть договорённости мы защищаем. В этом уроке договорённость — найти нужный курс через именованную форму и получить его в результирующем списке.