Мастерство CSS-локаторов: от основ до стабильных автотестов

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

1. Основы CSS-селекторов: выбор элементов по типу тега и уникальному идентификатору ID

Основы CSS-селекторов: выбор элементов по типу тега и уникальному идентификатору ID

Представьте, что вы стоите перед огромным складским стеллажом, где лежат тысячи коробок. Ваша задача — найти одну конкретную деталь для сборки механизма. Если вы скажете помощнику: «Принеси мне синюю коробку», он может вернуться с десятком разных вариантов. Если вы скажете: «Принеси коробку с артикулом AX-505», он доставит именно то, что нужно. В мире веб-разработки и автоматизации тестирования ситуация идентична: браузер — это огромный склад элементов (DOM-дерево), а CSS-селекторы — это те самые команды, которые позволяют нам безошибочно указывать на нужные объекты.

Для автоматизатора выбор правильного «пути» к элементу — это вопрос не только скорости, но и стабильности теста. Плохой локатор сломается при малейшем изменении дизайна, хороший — переживет рефакторинг кода. Начнем с фундамента: выбора элементов по их «биологическому виду» (тегу) и «личному номеру» (ID).

Анатомия веб-страницы через призму локаторов

Прежде чем писать первый селектор, необходимо понять, на что мы смотрим. Веб-страница — это иерархическая структура, известная как DOM (Document Object Model). Каждый элемент в этой структуре имеет имя (тег), набор атрибутов (свойства) и текстовое содержимое.

Когда мы пишем CSS-правила, браузер использует селекторы, чтобы понять, к каким узлам применить стили. Когда мы пишем автотесты на Selenium, Playwright или Cypress, мы используем те же самые селекторы, чтобы сказать инструменту: «Кликни вот сюда».

Селектор типа (Type Selector)

Самый простой и очевидный способ найти элемент — обратиться к нему по имени тега. Если нам нужны все заголовки первого уровня, мы пишем h1. Если нам нужны все кнопки — button.

Синтаксис селектора типа максимально лаконичен: это просто имя тега без каких-либо дополнительных символов.

В контексте тренажера CSS Diner, который имитирует обеденный стол с посудой, селектор типа работает так: если на столе стоят тарелки (<plate />) и яблоки (<apple />), то селектор plate выберет все тарелки, независимо от того, что на них лежит или какого они цвета.

Нюансы использования селекторов типа в тестах:

  • Низкая уникальность. На реальной странице может быть 50 элементов div или 20 элементов a (ссылок). Использование голого селектора типа в 99% случаев приведет к тому, что ваш автотест найдет «первый попавшийся» элемент или выдаст ошибку о не уникальности локатора.
  • Смысловая нагрузка. Селекторы типа полезны, когда они комбинируются с другими признаками или когда элемент на странице гарантированно один (например, nav или footer, хотя и это не всегда так).
  • Зависимость от верстки. Если разработчик решит заменить span на div для улучшения блочной модели, ваш локатор span мгновенно перестанет работать, хотя визуально для пользователя ничего не изменится.
  • Магия уникальности: Селектор ID

    Если тег — это фамилия (очень распространенная, как «Иванов»), то ID — это номер паспорта. Согласно спецификации HTML, идентификатор (id) должен быть уникальным в пределах всей страницы. Это делает его «золотым стандартом» для автоматизации.

    Синтаксис селектора по ID в CSS обозначается символом решетки #.

    В коде страницы это выглядит так: <orange id="fancy" />

    Почему ID так важен для тестировщика? Представьте форму регистрации. На ней может быть пять текстовых полей (input). Если вы используете селектор input, тест запутается. Но если у поля «Имя пользователя» есть id="username", а у поля «Пароль» — id="password", вы получаете прямой и быстрый доступ к цели.

    Почему ID может подвести?

    Несмотря на кажущуюся идеальность, у идентификаторов есть свои «подводные камни»:

    * Динамические ID. Многие современные фреймворки (React, Angular, Vue) при использовании определенных библиотек компонентов генерируют ID автоматически. Если вы видите локатор вида #j_id_jsp_12345_6, скорее всего, при следующей перезагрузке страницы цифры изменятся. Использовать такие ID в тестах категорически нельзя — это путь к «хрупким» тестам, которые падают без видимых причин. * Дублирование ID. К сожалению, браузеры прощают ошибки разработчиков. Если на странице окажется два элемента с id="submit", страница все равно отобразится. Однако CSS-селектор применится к обоим, а метод поиска элемента в автотестах (например, findElement) вернет только первый. Это может создать ложное ощущение стабильности, пока порядок элементов не изменится. * Отсутствие ID. В идеальном мире разработчики расставляют id для всех ключевых элементов (это называется «тестопригодностью» или testability). В реальности же ID часто отсутствуют, и тестировщику приходится выстраивать сложные цепочки из других селекторов.

    Практический разбор на примере CSS Diner

    Рассмотрим уровни тренажера, чтобы закрепить базу.

    Кейс 1: Выбор по тегу

    На столе стоят две тарелки. В HTML это выглядит так:

    Чтобы выбрать их обе, нам достаточно написать селектор plate. Это самый базовый уровень абстракции. Мы не смотрим на содержимое, нам важен тип объекта.

    Кейс 2: Выбор по ID

    На столе стоит обычная тарелка и тарелка с идентификатором fancy:

    Если мы напишем plate, мы выберем обе. Но если задача — выбрать только «модную» тарелку, наш выбор — #fancy. Обратите внимание: мы могли бы написать plate#fancy (сочетание тега и ID), но это избыточно. Поскольку ID уникален, #fancy и так найдет нужный объект быстрее и лаконичнее.

    Комбинирование селекторов: когда типа и ID мало

    Иногда нам нужно уточнить запрос. Хотя ID уникален сам по себе, в сложных системах или при изучении чужого кода вы можете встретить конструкцию tag#id.

    Например: div#main-container. Зачем это нужно, если #main-container и так сработает?

  • Дополнительная валидация. Вы ожидаете, что элемент с этим ID — именно div. Если разработчик изменит его на section, локатор перестанет работать, что может быть полезно для проверки строгого соответствия верстки спецификации.
  • Читаемость. Глядя на локатор в коде теста, коллега сразу понимает, что мы ищем контейнерный блок, а не кнопку или ссылку.
  • Однако в автоматизации действует правило: чем короче и проще локатор, тем он лучше. Избыточность — враг поддержки. Если #login-button работает, не пишите button#login-button.

    Сравнение: Тег против ID

    Для наглядности сопоставим эти два фундаментальных способа поиска в таблице:

    | Характеристика | Селектор типа (tag) | Селектор ID (#id) | | :--- | :--- | :--- | | Синтаксис | div, p, button | #my-element, #header | | Уникальность | Низкая (много элементов на странице) | Высокая (один элемент на странице) | | Стабильность | Средняя (зависит от структуры тегов) | Высокая (если ID не динамический) | | Скорость поиска | Высокая (нативный поиск браузера) | Максимальная (индексируется браузером) | | Применение | Групповые операции, простые страницы | Точечное взаимодействие с элементами |

    Приоритетность и специфичность (краткий экскурс)

    В CSS существует понятие «специфичности» (вес селектора). Хотя для поиска элементов в автотестах это не так критично, как для стилизации, понимание веса помогает разобраться, почему один селектор «сильнее» другого.

    Математически это можно представить так: * Селектор типа дает 1 балл. * Селектор ID дает 100 баллов.

    Если у вас есть элемент <button id="send-mail" />, то селектор #send-mail будет гораздо более специфичным и точным, чем просто button. Для инструментов автоматизации это означает, что поиск по ID — это самый «быстрый» путь, так как браузеры создают внутренние хэш-таблицы для всех ID на странице, что позволяет находить их практически мгновенно, не перебирая всё дерево DOM.

    Граничные случаи и ошибки новичков

    Ошибка 1: Использование ID с символом решетки в атрибутах

    Новички часто путают значение атрибута и синтаксис селектора. В HTML мы пишем: id="user-name". В CSS мы пишем: #user-name. Нельзя писать в коде теста локатор id="#user-name", если метод уже подразумевает использование CSS-селектора. Решетка — это указатель типа «ищи по идентификатору».

    Ошибка 2: Попытка найти элемент по ID, которого нет в DOM

    Часто в динамических приложениях элемент появляется не сразу. Если вы ищете #success-message сразу после клика на кнопку, тест может упасть, потому что скрипт еще не успел отрисовать этот блок. Всегда помните: наличие уникального ID не отменяет необходимости ожиданий (waits) в автотестах.

    Ошибка 3: Игнорирование регистра

    Хотя теги в HTML обычно нечувствительны к регистру (DIV и div — это одно и то же), ID в некоторых браузерах и стандартах могут быть чувствительны. Хорошим тоном считается писать всё в нижнем регистре или в стиле kebab-case (через дефис), как это принято в современной веб-разработке.

    Почему мы начинаем именно с этого?

    Выбор по тегу и ID — это фундамент. Это как умение читать буквы перед тем, как писать романы. В следующих темах мы разберем классы, которые позволяют группировать элементы, и иерархические связи, позволяющие перемещаться между «родителями» и «детьми». Но даже в самых сложных проектах по автоматизации, если у элемента есть вменяемый, статичный ID — опытный тестировщик всегда выберет именно его.

    Умение вовремя остановиться и не усложнять локатор — признак мастерства. Если задача решается простым #submit, не нужно строить конструкцию из пяти вложенных тегов. Простота — залог того, что через полгода, когда вы вернетесь к своим тестам, вы поймете, что они делают, а не будете разгадывать ребусы из CSS-символов.

    На этапе обучения в CSS Diner старайтесь замечать, как меняется поведение элементов при наведении: тег выбирает «все похожие», а ID — «тот самый единственный». Это различие должно закрепиться на уровне интуиции.

    2. Селекторы классов: синтаксис, группировка и комбинирование для точного поиска

    Селекторы классов: синтаксис, группировка и комбинирование для точного поиска

    Представьте, что вы проводите инвентаризацию на огромном складе. Идентификаторы ID — это уникальные серийные номера на коробках: если они есть, найти нужный объект не составит труда. Но что делать, если перед вами сотни одинаковых контейнеров, у которых нет личного номера, но есть маркировка «Хрупкое», «Срочно» или «Склад №5»? В веб-разработке роль такой маркировки выполняют классы. Если ID — это паспортные данные элемента, то класс — это его характеристика, роль или принадлежность к группе. Для автоматизатора это основной инструмент, так как именно на классах строится визуальная логика современных интерфейсов.

    Анатомия селектора класса

    В HTML атрибут class позволяет присвоить элементу одно или несколько имен, разделенных пробелами. В CSS для обращения к такому элементу используется символ точки .. Этот символ сообщает браузеру: «Ищи не тег с таким названием, а любой элемент, у которого в списке классов есть указанное слово».

    Рассмотрим классический пример из тренажера CSS Diner. У нас есть элемент: <apple class="small" />

    Чтобы выбрать его, мы пишем локатор: .small

    Важно понимать механику: точка заменяет собой конструкцию «атрибут class равен». Если мы запишем это через селектор атрибутов (который разберем позже), это выглядело бы как [class~="small"]. Но точка — это синтаксический сахар, который делает код чище и понятнее.

    Почему классы — это «золотая середина» тестировщика

    При написании автотестов мы постоянно балансируем между двумя крайностями:

  • Хрупкость: Локатор ломается при малейшем изменении верстки (например, длинные пути div > div > ul > li).
  • Избыточность: Локатор находит слишком много лишних элементов (например, просто button).
  • Классы позволяют создавать локаторы, которые описывают смысл элемента, а не его положение. Если кнопка называется .btn-submit, она, скорее всего, останется таковой, даже если разработчик перенесет ее из правой части хедера в центр модального окна.

    Множественные классы и их комбинирование

    Одна из самых частых ошибок начинающих автоматизаторов — попытка копировать всё содержимое атрибута class целиком. Взгляните на типичную кнопку в современном фреймворке вроде Bootstrap или Tailwind: <button class="btn btn-primary btn-lg loading active">Купить</button>

    Если вы напишете локатор как .btn btn-primary btn-lg, он не сработает. Почему? Потому что пробел в CSS — это оператор вложенности (потомок). Такой локатор будет искать элемент с классом btn-primary, который находится внутри элемента с классом btn.

    Чтобы выбрать элемент, обладающий одновременно несколькими классами, их нужно писать слитно, каждый через свою точку: .btn.btn-primary.btn-lg

    Логическое И в селекторах

    Когда мы соединяем селекторы без пробелов, мы создаем условие «Логическое И».

  • .small — найдет все маленькие объекты.
  • apple.small — найдет только яблоки, которые являются маленькими.
  • apple.small.red — найдет только маленькие красные яблоки.
  • Это критически важный навык для точного поиска. Если на странице есть несколько корзин (одна в шапке, другая в футере), и обе имеют класс .cart-icon, нам нужно уточнение. Если одна из них находится внутри блока с классом .header, мы можем комбинировать их, но об этом мы поговорим в разделе про иерархию. Сейчас сосредоточимся на уточнении через свойства самого элемента.

    > Специфичность (вес) комбинированного селектора выше, чем у одиночного. > > Каждая точка в селекторе добавляет ему «веса». Селектор .btn.active будет более приоритетным для браузера, чем просто .active. Для нас как для тестировщиков это означает, что такой локатор более надежен и менее склонен к случайному выбору «соседа».

    Группировка селекторов: Логическое ИЛИ

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

    Запятая в CSS — это «Логическое ИЛИ». plate, bento — выберет все элементы <plate /> И все элементы <bento />.

    Это применимо и к классам. Если у нас есть элементы с классами .urgent и .important, и мы хотим проверить их видимость: .urgent, .important

    Важное правило: Запятая полностью разделяет селекторы. Это значит, что если вы напишете div.user, .admin, браузер найдет:

  • Все теги div с классом user.
  • ВООБЩЕ ВСЕ элементы с классом admin (независимо от того, div это или нет).
  • Если вы хотели найти и пользователей, и админов именно среди div, правильно будет написать: div.user, div.admin.

    Пересечение типов и классов

    Комбинирование тега и класса — это первый шаг к созданию «неубиваемых» локаторов. Рассмотрим ситуацию: на странице есть заголовок статьи и имя автора. Оба могут иметь класс .title.

  • <h1 class="title">Заголовок</h1>
  • <span class="title">Имя автора</span>
  • Локатор .title вернет нам два элемента. В автотестах (например, в Selenium или Playwright) это приведет к тому, что метод click() либо выдаст ошибку «найдено более одного элемента», либо кликнет по первому попавшемуся.

    Уточнение тегом решает проблему: h1.title — гарантированно выберет только заголовок.

    Однако здесь кроется ловушка. Если разработчик решит изменить h1 на h2 ради SEO-оптимизации, ваш локатор h1.title сломается. Поэтому правило хорошего тона в автоматизации: используйте тег только тогда, когда без него невозможно достичь уникальности. Если класс .main-heading уникален сам по себе, лучше использовать просто его.

    Работа с динамическими классами

    В современном фронтенде (React, Angular, Vue) классы часто генерируются динамически или меняются в зависимости от состояния приложения. Это главный враг стабильных тестов.

    Состояния (State classes)

    Часто к базовому классу добавляется класс состояния: .is-open, .active, .loading, .error. Никогда не завязывайтесь в тестах на классы состояния, если вы ищете сам элемент для взаимодействия, а не проверяете это самое состояние. Плохо: click('.button.active') — если кнопка перестанет быть активной в момент поиска, тест упадет. Хорошо: click('.button'), а затем отдельной проверкой expect('.button').to_have_class('active').

    Хешированные классы

    Вы можете встретить такое: <div class="Header_root__3a5fG">. Это результат работы CSS Modules. Часть Header_root — понятная и стабильная, а __3a5fG — это хеш, который изменится при следующей сборке проекта (deploy). Использовать .Header_root__3a5fG в локаторе нельзя — тест «протухнет» очень быстро.

    В таких случаях мы используем частичное совпадение, но так как селектор класса по определению ищет полное совпадение слова, нам приходится использовать синтаксис атрибутов: [class*="Header_root"]

    Но об этом механизме мы подробно поговорим в главе про атрибуты. Пока запомните: если вы видите в классе странный набор букв и цифр в конце — это сигнал тревоги.

    Практический разбор: CSS Diner и реальные кейсы

    Вернемся к тренажеру CSS Diner, чтобы закрепить материал на конкретных задачах.

    Кейс 1: Выбор по классу

    Уровень: apple, .small Задача: выбрать маленькое яблоко. HTML: <apple class="small" /> Решение: .small

    Здесь всё просто. Но что если на столе лежат и маленькие яблоки, и маленькие апельсины, а нам нужно только яблоко? Решение: apple.small

    Кейс 2: Комбинирование для исключения лишнего

    Представьте таблицу со списком пользователей. У каждой строки tr есть класс .row. У четных строк есть доп. класс .even. Если мы хотим нажать на кнопку удаления в конкретной строке, нам нужно максимально сузить поиск. Локатор .row.even выберет только четные строки. Если мы добавим туда еще и тег — tr.row.even, мы получим очень специфичный и точный адрес.

    Кейс 3: Множественные классы в сложном интерфейсе

    Рассмотрим карточку товара: ``html <div class="product-card featured special-offer"> <span class="price discount">99(A, B, C)ABCB = 2(0, 2, 0)B = 1C = 1(0, 1, 1)2 > 1$ во втором разряде, селектор .btn.active «сильнее». В автоматизации нам это важно для понимания того, почему наш локатор может перекрываться другими элементами или почему мы находим не то, что ожидали.

    Использование классов для поиска в коллекциях

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

  • «Все элементы с классом .price должны иметь символ валюты».
  • «Ни один элемент с классом .error не должен быть видимым при загрузке».
  • Использование чистого селектора класса без привязки к ID позволяет нам получить массив (List) элементов и итерироваться по нему.

    Например, в Selenium: List<WebElement> products = driver.findElements(By.cssSelector(".product-title"));

    Если бы мы использовали ID, мы бы получили только один элемент. Если бы использовали теги (например, h3), мы бы рисковали собрать лишние заголовки, не относящиеся к продуктам. Класс здесь выступает как идеальный фильтр.

    Резюме по стратегии выбора

    Приступая к написанию локатора на основе классов, задайте себе три вопроса:

  • Является ли этот класс уникальным для данного элемента на странице? Если да — используйте его в одиночку (.unique-class).
  • Нужно ли мне уточнение тегом, чтобы отсечь лишнее? Если есть другие элементы с таким же классом, добавьте тег (input.search-field).
  • Не является ли этот класс динамическим или служебным? Избегайте хешей и оформительских классов (типа .mt-5`).
  • Классы — это язык, на котором общаются дизайнеры и разработчики. Научившись понимать этот язык, вы сможете писать локаторы, которые не просто «находят кнопку», а описывают структуру и поведение интерфейса. В следующей главе мы углубимся в иерархию и научимся связывать эти классы между собой, чтобы находить элементы в самых запутанных лабиринтах DOM-дерева.