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

Подготовка эталонного изображения

В предыдущем уроке мы добавили сравнение изображения области 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. Там внешний вид уступит место другой договорённости: действие пользователя должно привести к файлу с правильным именем и содержимым. Проверка изображения и проверка файла будут наблюдать разные свойства одного учебного сайта.