Подготовка эталонного изображения
В предыдущем уроке мы добавили сравнение изображения области main. Но прежде чем одобрять первый PNG, нужно точно определить, что на нём изображено. Одно имя файла не описывает состояние: тот же каталог может показывать все курсы, результат поиска, тёмную тему или незавершённую загрузку. Эталон имеет смысл, когда изображение связано с воспроизводимыми данными и окружением.
В этом уроке подготовим исходный каталог для будущего снимка. Продолжение находится в отдельном архиве уроков 17–24: lesson-17/start полностью повторяет состояние после урока 16, а expected уточняет сценарий. Старый архив сохраняется. Тесты, установка зависимостей и создание PNG при подготовке не выполнялись; ожидаемый эффект здесь описывает договорённость, которую предстоит подтвердить отдельно.
Состояние важнее времени ожидания
Наш каталог содержит четыре курса и пятьдесят уроков. Для начального изображения нужны пустой запрос, все разделы, светлая тема и анонимный контекст. Фикстура catalogPage уже подменяет ответ четырьмя известными записями и ждёт статус готового списка. В новом сценарии дополним эту подготовку точными значениями полей и названиями карточек.
Это отличается от ожидания «страница открылась». Документ может быть доступен раньше ответа каталога. Число четыре само по себе тоже недостаточно: другой набор из четырёх курсов изменит текст, высоту карточек и переносы. Поэтому проверка названий объясняет, какие данные должны оказаться на изображении. Если данные не соответствуют договорённости, визуальный этап должен остановиться до создания нового эталона.
Следующий фрагмент входит в полный tests/lesson.spec.js состояния expected. Его выполняют после подготовки фикстуры:
await expect(page.getByLabel('Запрос', { exact: true })).toHaveValue('');
await expect(page.getByLabel('Раздел', { exact: true })).toHaveValue('all');
await expect(page.getByRole('button', { name: 'Тёмная тема', exact: true }))
.toHaveAttribute('aria-pressed', 'false');
await expect(page.getByRole('list', { name: 'Курсы' }).getByRole('link'))
.toHaveText([
'Основы HTML', 'Современный JavaScript', 'HTTP и API',
'Статический сайт из Markdown'
]);
Кроме этого, полный файл ждёт data-session-ready=true: запрос определения учебной роли завершён. Контекст начинает без сохранённой сессии. Мы не импортируем чужой storageState, потому что изображение редактора и изображение анонимного читателя выражали бы разные условия. Такая подготовка остаётся полезной даже при снимке только основного содержимого.
Шрифт и положение фокуса
Когда веб-шрифт загружается позднее, сначала текст может занимать одну ширину, затем другую. В лаборатории выбран системный шрифт, однако готовность шрифтов всё равно зададим явно. Затем снимем фокус с активного элемента. Так начальный каталог не получает случайную рамку поля, в которое до снимка могло попасть действие.
await page.evaluate(() => document.fonts.ready);
await page.evaluate(() => document.activeElement?.blur());
await expect(page.getByRole('main')).toHaveScreenshot(
'catalog-main-light-1280.png',
{ animations: 'disabled', caret: 'hide', maxDiffPixels: 0 }
);
document.fonts.ready относится к состоянию браузерного документа. Оно не делает системные шрифты одинаковыми на разных операционных системах. blur() тоже не является приёмом проверки доступности: здесь мы выбираем изображение без активного управления. Для изображения фокуса нужен другой сценарий, который, наоборот, устанавливает фокус и проверяет его видимость.
Имя снимка теперь называет область, тему и ширину. Новый файл отличается от catalog-main.png предыдущего урока. Если вы уже отдельно создавали прежний эталон, его нельзя автоматически считать подходящим к новому имени. В текущем архиве оба PNG отсутствуют; исходники задают процедуру, а не готовый набор принятых изображений.
При сохранении будущего эталона полезно записать ревизию исходников и условия его одобрения. Сам PNG не объясняет, почему четыре курса считаются правильными. Если позже изменится состав данных, договорённость нужно пересмотреть вместе с изображением. Нельзя молча заменить список и считать всякое отличие старого эталона ошибкой отрисовки.
Отдельно различайте изменяемое содержимое и устойчивую часть изображения. Например, дата последнего обновления потребовала бы фиксированного времени либо обоснованного исключения области. В нашей лаборатории такого поля нет, поэтому маска не нужна. Скрывать целые карточки только ради стабильного снимка означало бы убрать из наблюдения главный результат каталога. Предпочтительнее заранее понять источник изменчивости и выбрать данные, которые выражают задачу пользователя.
Одно согласованное окружение
Конфигурация продолжает использовать Chromium, размер 1280 на 800, ru-RU, часовой пояс UTC и светлую цветовую схему. Версии лаборатории остаются Node 24.21.0 и Playwright 1.64.0. Параллельные сценарии получают отдельные контексты, но два независимых запуска старой и новой лаборатории не должны одновременно занимать порт 4407.
Изображение зависит не только от JavaScript. Версия браузера, система, установленный шрифт и масштабирование могут изменить пиксели. Поэтому будущий эталон создают в выбранной среде и сравнивают в согласованной среде. Перенос эталона с одного компьютера на другой требует оценки различий. Повышать допустимое число отличающихся пикселей до исчезновения всех ошибок означает потерять наблюдение, ради которого сравнение добавлялось.
Пока используем maxDiffPixels:0. Это делает отличие видимым, но не обещает отсутствие отличий. Возможный дальнейший допуск должен относиться к объяснённой погрешности. Например, исчезновение карточки нельзя принять как сглаживание шрифта: оно меняет задачу читателя. Причина отличия определяет решение, а не красный или зелёный цвет отчёта.
От исходников к принятому изображению
После отдельного решения выполнить лабораторную работу содержимое expected переносится в самостоятельную папку. Там находится полный проект, включая конфигурацию, сервер и единственный сценарий. Смешивать этот файл со сценариями всех предыдущих уроков не нужно: наши состояния показывают последовательное изменение примера, а не готовую общую систему регрессий.
Первый запуск с обновлением снимков относится к будущему этапу. После создания PNG его открывают и читают глазами: четыре правильных названия, светлый фон, пустая форма, понятные интервалы, отсутствие случайного перекрытия. Только после такого решения изображение становится эталоном. Совпадение с некорректным изображением сохраняло бы ошибку, поэтому автоматическое сравнение не заменяет первую оценку.
Порядок работы и ограничения платформ описаны в документации визуальных сравнений Playwright. Для нашего примера главный результат подготовки — именованное состояние, которое можно воспроизвести. Реального совпадения, времени выполнения и качества будущего PNG мы пока не измеряли.
Снимок относится только к области main начального каталога. Он не проверяет хедер, футер, узкое меню или страницы курсов. Для пустого поиска, тёмной темы и мобильной ширины понадобятся самостоятельные состояния и изображения. Это расширение следует выбирать по значимым читательским задачам, чтобы набор не превратился в сотни похожих картинок без понятной цели.
В следующем уроке тот же каталог получит скачиваемый JSON. Там внешний вид уступит место другой договорённости: действие пользователя должно привести к файлу с правильным именем и содержимым. Проверка изображения и проверка файла будут наблюдать разные свойства одного учебного сайта.