Тестирование кода HTML при помощи CSS

alexei18/08/2024 - 08:33
Тестирование кода HTML при помощи CSS

Автор Хейдон Пикеринг (Heydon Pickering). Перевод статьи "Testing HTML with morden CSS".

Давным-давно я написал довольно популярный фрагмент открытого исходного кода под названием REVENGE.CSS (заглавные буквы указаны намеренно). При этом следует сразу сказать, что данный проект не поддерживался уже годами, и если я когда-нибудь все-таки соберусь с силами для его поддержки, то только для того, чтобы добавить значок "Поддержка не планируется". Увы, технически это будет считаться поддержкой.

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

В двух словах, цель REVENGE.CSS - расстановка визуальных пояснений к HTML элементам, нарушающим шаблон. Он делает так, чтобы плохой HTML элемент выглядел плохо, оформляя его в болезненно-розовом цвете и непопулярным шрифтом Comic Sans MS. Какое-то время все это дело было представлено в виде букмарклета, но я удалил эту страницу в ходе переделки сайта в стиле Мари Кондо.

Селекторы, устанавливавшие "мстительный" стиль, широко используют отрицающий псевдокласс :not, который был уже доступен на протяжении 11 лет.

Вот пример такой декларации, относящейся к ссылкам:


a:not([href]), a[href=""], a[href$="#"], a[href^="javascript"] {...}

Данная декларация затрагивает ссылки,

  1. у которых нет атрибута href (т. е. не работают как ссылки и не получают фокус при помощи клавиатуры);
  2. у которых атрибут href пустой;
  3. у которых в атрибуте href пустой суффикс # (пустой фрагмент страницы);
  4. которые выполняют некоторые трюки при помощи JavaScript для замещения элементов <button>.

С тех пор, как я выпустил проект REVENGE.CSS, я еще немного думал о тестировании на основе CSS и даже выступил с докладом на эту тему в 2016 году на конференции Front Conference в Цюрихе.

Тогда, в своем докладе я рекомендовал использовать некорректное свойство CSS ERROR для описания недостатков в коде HTML. Таким образом, вы могли бы проверить элемент и прочитать ошибку в панели инструментов разработчика. На самом деле это довольно удобно, потому что вы за так получаете значок предупреждения!

Зачеркнутое некорректное свойство error с поясняющим текстом
Зачеркнутое некорректное свойство error с поясняющим текстом

При желании вы даже можете изменить стиль этого конкретного свойства ERROR в инструментах разработчика браузера.

Изначально в REVENGE.CSS для описания ошибок/антишаблонов использовался псевдоконтент на самой странице. При этом возникало множество проблем с версткой, и часто ошибки не были (полностью) видны. Отсюда перенаправление сообщений об ошибках в инспектор.

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

В 2017 году, примерно через год после конференции в Цюрихе, появились пользовательские свойства: стандартизированный способ создания произвольных свойств/переменных в CSS. Это означает не только то, что теперь мы можем определять и повторно использовать стиль ошибок, но и то, что мы также можем генерировать сообщения об ошибках, не делая таблицу стилей недействительной:


:root {
  --error-outline: 0.25rem solid red;
}

a:not([href]) {
  outline: var(--error-outline);
  --error: 'У ссылки нет атрибута href. Вы хотели использовать <button>?';
}

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


a[href^="javascript"] {
  outline: var(--error-outline);
  --error-javascript-href: 'В атрибуте href не указан адрес перехода. Вы хотели использовать <button>?';
}

a[disabled] {
  outline: var(--error-outline);
  --error-anchor-disabled: 'Отключенное свойство некорректно использовать с ссылками. Вы хотели использовать <button>?';
}

Теперь обе ошибки будут показаны в инспекторе инструментов разработчика.

Селекторы-выражения

С 2017 года в CSS появились селекторы-выражения. Например, когда я писал REVENGE.CSS, я бы не смог соотнести, что у элемента <label> одновременно

  • нет атрибута for, и
  • он не содержит приемлемого элемента формы.

Теперь же я могу сделать это так:


label:not(:has(:is(input,output,textarea,select))):not([for]) {
  outline: var(--error-outline);
  --error-unassociated-label: '<label> не использует ни атрибут `for`, ни оборачивает приемлемый элемент формы'
}

Тем же способом я могу проверить элементы на наличие соответствующих родителей или предков. В данном случае я использую предупреждающий стиль --warning-outline, поскольку элементы ввода расположенные за пределами элемента <form> иногда вполне допустимы.


input:not(form input) {
  outline: var(--warning-outline);
  --error-input-orphan: 'Этот элемент ввода находится вне элемента <form>. Пользователи получат больше преимуществ от семантики и поведения элемента <form>.'
}

Примечание: Интересно, что :not() позволяет таким образом "дотянуться" куда нужно.

Каскадные слои

Специфичность этих проверочных селекторов сильно различается. Проверка на пустой элемент <figcaption> требует гораздо меньшей специфичности, чем проверка элемента <figure>, у которого нет метки принадлежности или потомка <figcaption>. Чтобы убедиться, что все тесты имеют приоритет над обычными стилями, их можно разместить на самом высоком каскадном слое.


@layer base, elements, layout, theme, tests;

Чтобы убедиться, что у ошибок более высокий приоритет, чем у предупреждений, мы можем объявить в нашей таблице стилей tests.css слои error и warning. Вот как это может выглядеть для проверки элементов <figure> и <figcaption>:


@layer warnings {

  figure[aria-label]:not(:has(figcaption)) {
    outline: var(--warning-outline);
    --warning-figure-label-not-visible: 'Использованный способ установки метки не виден и доступен только для вспомогательных программ';
  }

  figure[aria-label] figcaption {
    outline: var(--warning-outline);
    --warning-overridden-figcaption: 'У figure есть figcaption, который переопределяет метку ARIA';
  }
  
}

@layer errors {

  figcaption:not(figure > figcaption) {
    outline: var(--error-outline);
    --error-figcaption-not-child: 'Элемент figcaption не прямой потомок элемента figure';
  }

  figcaption:empty {
    padding: 0.5ex; /* немного отступа */
    outline: var(--error-outline);
    --error-figcaption-empty: 'Элемент figcaption пустой';
  }

  figure:not(:is([aria-label], [aria-labelledby])):not(:has(figcaption)) {
    outline: var(--error-outline);
    --error-no-figure-label: 'Элемент figure не имеет никакой метки, поставленной каким-либо приемлемым способом';
  }
  
  figure > figcaption ~ figcaption {
    outline: var(--error-outline);
    --error-multiple-figcaptions: 'Есть два элемента figcaption на один элемент figure';
  }
  
}

Проверка без JavaScript?

Неизбежно у некоторых из вас возникнет вопрос: "Почему бы не проводить подобные проверки при помощи JavaScript, как это уже делает большинство людей?"

Нет ничего плохого в использовании JavaScript для тестирования JavaScript, и немного хуже, если использовать JavaScript для тестирования HTML. Но, учитывая мощь современных селекторов CSS, можно протестировать большинство типов шаблонов кода HTML, используя только CSS. Больше никаких махинаций с elem.parentNode!

Как разработчик, работающий визуально/графически, в основном в браузере, я предпочитаю видеть визуальные отклонения и информацию инспектора, а не записи в командной строке. Этот способ тестирования соответствует моему рабочему процессу и технологиям, которые мне наиболее удобно использовать.

Мне нравится работать с CSS, но я также считаю уместным использовать декларативный код для тестирования декларативного кода. Также, удобно, что эти тесты просто находятся внутри .css файла. Разделение задач означает, что вы можете использовать тесты в своем стеке разработки, в разных стеках разработки или перенести их в букмарклет для тестирования любой страницы в Интернете.

Я сторонник систем проектирования, в которых нет поведенческих реакций (JavaScript). Вместо этого я предпочитаю предоставлять только стили и параллельно с ними состояние документа. Для этого есть несколько причин, но главная из них заключается в том, чтобы сублимировать систему проектирования вдали от текучести JS фреймворка. Идея тестирования при помощи компонента CSS, написанного на CSS, мне вполне нравится.

Как я использую все это при работе с клиентами

У меня был клиент, для которого я проводил аудит различных сайтов/свойств на предмет доступности. В процессе этого я определил несколько шаблонов, не отвечавших принципам доступности. Они были достаточно уникальными и не выявлялись обычными тестами (вроде Lighthouse accessibility suite).

Одним из таких шаблонов были маршруты "хлебных крошек", не ограниченные маркированным ориентирным элементом <nav> (как рекомендовано WAI). Я могу определить любое использование этого шаблона с помощью следующего теста:


ol[class*="breadcrumb"]:not(:is(nav[aria-label], nav[aria-labelledby]) ol) {
  outline: var(--error-outline);
  --error-undiscoverable-breadcrumbs: 'Похоже ваша навигация хлебных крошек находится за пределами маркированного ориентирного элемента `<nav>`';
}

Обратите внимание, что этот тест обнаруживает как пропуск элемента <nav>, так и использование элемента <nav>, но без метки.

Еще одна возможная проблема - это контент, не попадающий внутрь элементов ориентирной разметки (в результате, программы чтения с экрана не могли определиться с навигацией):


body :not(:is(header,nav,main,aside,footer)):not(:is(header,nav,main,aside,footer) *):not(.skip-link) {
  outline: var(--error-outline);
  --error-content-outside-landmark: 'Есть контент вне ориентирной разметки (header, nav, main, aside, footer)';
}

Более обобщенная версия этого теста должна включать эквивалентные роли области [role="banner"], [role="navigation"] и т.д.

Как консультанту, мне часто не разрешают напрямую обращаться к клиентскому стеку для настройки или расширения тестов, связанных с проверкой доступности. Там, где я могу получить доступ к стеку, зачастую приходится очень долго изучать то, как все сочетается друг с другом. Также приходится изучать различные внутренние процессы, чтобы понять что к чему. Часто бывает так, что задействовано несколько стеков/сайтов/платформ, и у каждого из них свои подходы к тестированию. У некоторых может вообще не быть возможности тестирования на основе узлов. Некоторые могут проводить тестирование на языке программирования, который я едва понимаю, например Java.

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

И при этом CSS тесты можно использовать не только, чтобы выявить проблемы с доступностью. Как насчет чрезмерного раздувания HTML кода?


:is(div > div > div > div > *) {
  outline: var(--warning-outline);
  --warning-divitis: 'Присутствует слишком много вложенных элементов. Это нужно для верстки?';
}

Или общего удобства использования?


header nav:has(ul > ul) {
  outline: var(--warning-outline);
  --warning-nested-navigation: 'Похоже вы используете многоуровневую/вложенную структуру навигации в шапке. Это может вызвать сложности при проходе. Индексные страницы с таблицами контента предпочтительнее.';
}