Проверки
Зелёная сборка ещё не значит, что со страницами всё в порядке. Этот набор инструментов ставит по одной проверке на каждый слой — каждая ловит свой класс тихих поломок вида «сборка зелёная, страница сломана», — и две из них смотрят на страницы так, как смотрит читатель: в настоящем браузере.
§1Пять рубежей — пять слоёв
| Проверка | Едет с | Смотрит на | Когда запускать |
|---|---|---|---|
| check-content | движок, scripts/check-content.mjs | каждый исходный md/mdx | CI репозитория контента, или после письма |
| check-wikilinks | движок, scripts/check-wikilinks.mjs | граф [[вики-ссылок]] | CI репозитория контента, или после переименования заметки |
| check-dist | движок, scripts/check-dist.mjs | вывод astro build | после каждой сборки (postbuild) |
| ui_probe | этот пакет, scripts/ui_probe.mjs | страницы, отрисованные в настоящем браузере | после правок стилей и макета |
| contrast_probe | этот пакет, scripts/contrast_probe.mjs | контраст каждого текстового узла, обе темы | после любой правки токенов |
§2check-content: слой исходников
Компилирует каждый md/mdx, которому предстоит стать страницей, ровно тем же диалектом, что и сайт, — синтаксические ошибки и тихие деформации (непарные маркеры выделения, скобки, проглоченные вычислением MDX, маркеры списков, рождённые переносом строки, однострочный $$, формулы, которые KaTeX не осилит, — тот самый список, которым закрывается «Всё и сразу») окрашивают CI в красный.
Почему этот скрипт обязан приходить из движка: проверка, чей набор плагинов отличается от сайтового, хуже, чем никакой проверки — без плагина математики она читает скобки формул как выражения JSX, без GFM пропускает таблицы. Диалект написан в движке однажды; отрисовка сайта, валидация сохранений CMS и этот скрипт потребляют один и тот же — всегда.
node <engine>/scripts/check-content.mjs . --glob '**/index.{md,mdx}' --mathПомимо компиляции он ловит два класса тихих потерь во frontmatter: неэкранированный # в значении (YAML считает его комментарием и молча отрезает хвост) и YAML, который вовсе не парсится, — с указанием строки.
§3check-wikilinks: граф ссылок
Мёртвые [[вики-ссылки]] нарочно не роняют сборку: саду положено ссылаться на заметки, которые ещё не выросли. Но гнили ссылок место в CI, поэтому движок поставляет линт, который разрешает каждую вики-ссылку парсером и правилами разрешения самой библиотеки (псевдоним, брендовое имя, заголовок, локальные зеркала) и докладывает о пропавших целях, неоднозначностях и сомнительных якорях. --strict превращает мёртвые ссылки в ненулевой код выхода — именно так его запускает CI этого репозитория:
node scripts/check-links.mjs§4check-dist: слой сборки
В собранном dist/ каждая внутренняя ссылка, по которой читатель может щёлкнуть, обязана существовать. Проверка ловит тихие дыры под зелёной сборкой:
- внутренние ссылки и ассеты, указывающие на несуществующие файлы (после перекройки маршрутов такие появляются дюжинами);
- внутристраничные якоря, указывающие на несуществующие id;
- удвоенные сегменты локали в путях (
/en/en/— классический результат i18n-фолбэков, наслаивающих префикс на уже префиксованные маршруты); <a>, вложенный в<a>(HTML-парсер рано закрывает внешний, и кнопки вываливаются из карточек);- осадок ошибок KaTeX (формула стоит на странице красным текстом, а сборка остаётся зелёной).
Это демо включает его в postbuild: зелёный npm run build означает, что проверка вывода тоже пройдена.
node vendor/astro-inkbrush/scripts/check-dist.mjs dist --base ${DEMO_BASE:-/}§5Зонд слоя отрисовки
Исходники и вывод могут быть правы одновременно, а страница всё равно сломана; классика жанра — карточки главной, до которых не дотягивается ни одно правило стилей и которые отрисовываются одной сплющенной строкой голого текста: проверки ссылок и якорей остаются зелёными, ведь на отрисованную страницу они не смотрят вовсе. ui_probe прогоняет настоящий браузер по каждой странице dist на четырёх ширинах вьюпорта (1440/1024/768/430) и меряет: горизонтальное переполнение страницы, элементы шире контейнера без скролл-бокса, где им жить, классы, которые не стилизует ни одно правило, пропущенные уровни заголовков, повторяющиеся id, изображения без атрибута alt, внутристраничные якоря и aria-controls, указывающие в пустоту. Он докладывает только то, что машина может доказать, — без эстетических суждений.
npm run build
node ../scripts/ui_probe.mjs dist # сам раздаёт dist; передайте baseUrl, чтобы проверить живой серверОн осматривает документ целиком — вместе с обвязкой, сайдбаром и диалогами. Зелёный итог — это когда последняя строка отчёта гласит SAMPLES WITH FINDINGS: 0 (замер — один маршрут на одной ширине).
§6Зонд контраста
Токены заявляют AA — значит, заявление измеряется, а не постулируется. contrast_probe открывает каждую страницу dist в настоящем браузере — светлая и тёмная тема, десктопная и телефонная ширина — и меряет каждый текстовый фрагмент, отрисованный в состоянии по умолчанию: текст HTML, текст SVG и генерируемый текст ::before / ::after; каждый оверлей <dialog data-probe-open> прощупывается открытым (а в найденное там поле поиска вводится запрос) на представительной странице — этот маркер и есть заявление сайта, что диалог в авторском виде полон. Фон не вычитывается из таблицы стилей: страница отрисовывается со всеми глифами, сделанными прозрачными, снимается скриншот, и пиксель под фрагментом — его фон; так градиенты, примеси color-mix(), полупрозрачные слои и ночная палитра меряются такими, какими они рендерятся. Передний план фрагмента несёт накопленную opacity элемента и его предков — приглушённый текст меряется той силой, какой его действительно видит читатель. Состояния hover и focus рецензируются глазами, не зондом. Мелкий текст держит планку 4.5:1, крупный (24px, или 18.66px жирным) — 3:1; фрагмент, чей цвет не разобрать или чей фон не снять, сам считается находкой. Отчёт называет страницу, тему, ширину, селектор, оба цвета и коэффициент каждого фрагмента, не добравшего до планки:
node ../scripts/contrast_probe.mjs dist # PROBE_THEMES / PROBE_WIDTHS сужают матрицуui_probe и contrast_probe по её выводу — всё зелёное до коммита. Демо по совместительству — испытательный стенд пакета.