Playwright PDF позволяет автоматически превращать HTML-страницы, личные кабинеты, отчёты, счета и другие веб-интерфейсы в PDF с помощью метода page.pdf(): сценарий открывает нужный экран в Chromium, дожидается данных, шрифтов и графики, задаёт формат бумаги, поля, фон, колонтитулы и диапазон страниц, а затем сохраняет готовый документ в файл или возвращает его как буфер для дальнейшей отправки пользователю.
Рабочий процесс строится вокруг браузерной страницы, которой управляет код. Сначала сценарий создаёт контекст и вкладку, затем загружает адрес либо передаёт разметку через setContent, выполняет вход, раскрывает скрытые блоки, переключает печатные стили и только после этого запускает экспорт. Отдельного визуального редактора макета нет: результат определяется HTML, CSS, состоянием страницы и параметрами вызова, поэтому подготовка содержимого происходит там же, где формируется веб-экран.
Основные настройки собраны в объекте PDFOptions. В нём выбираются A4, Letter и другие стандартные размеры либо собственные width и height, книжная или альбомная ориентация, масштаб, поля, печать фоновой графики, CSS-размер из правила @page, нумерация через шаблоны верхнего и нижнего колонтитула, выбор отдельных листов, закладки документа и тегированная структура. Эти параметры дополняют печатный CSS, но не заменяют его: переносы, повтор заголовков таблицы и поведение длинных блоков задаются стилями страницы.
Скачать Playwright PDF
- Редактирование PDF
- Русский интерфейс
- Просто новичкам
- PDF только через Chromium
- Нет визуального редактора
- Нужны навыки кода
Как устроена печать веб-страницы в PDF
Метод page.pdf() обращается к механизму печати Chromium и возвращает двоичный Buffer. Если передан параметр path, тот же буфер записывается на диск; без path его можно сразу положить в тело HTTP-ответа, сохранить в объектное хранилище, прикрепить к письму или объединить с дальнейшим конвейером обработки. Это важное различие для серверных задач: генератор не обязан создавать временный файл, а значит, можно уменьшить количество операций ввода-вывода и упростить очистку временного каталога.
Перед печатью страница переводится в CSS-медиарежим print. Поэтому правила внутри @media print применяются автоматически, а элементы, скрытые только в печатной версии, исчезают. Когда требуется получить внешний вид, близкий к экрану, перед page.pdf() вызывают page.emulateMedia({ media: 'screen' }). Такой режим полезен для дашбордов и карточек, у которых печатная таблица стилей отсутствует или намеренно упрощает цвета и навигацию.
Экспорт выполняется после всех действий сценария. Можно открыть страницу, заполнить фильтры, выбрать период, нажать кнопку построения отчёта, дождаться появления итоговой таблицы и лишь затем сохранить PDF. В документ попадёт фактическое состояние DOM в момент вызова, включая текст, раскрытые панели, отрисованные графики, текущие значения полей и видимость условных компонентов.
Минимальный сценарий создания документа
Базовая последовательность состоит из запуска Chromium, создания вкладки, подготовки содержимого и вызова page.pdf(). Браузер необходимо закрывать в блоке finally, иначе при исключении процесс может остаться в памяти. Каталог для выходного файла создают заранее: Playwright умеет записать сам файл, но не создаёт отсутствующую цепочку папок.
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.setContent(reportHtml, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await mkdir('output', { recursive: true });
await page.pdf({
path: 'output/report.pdf',
format: 'A4',
printBackground: true,
margin: { top: '16mm', right: '14mm', bottom: '18mm', left: '14mm' }
});
} finally {
await browser.close();
}
В этом варианте разметка уже находится в переменной reportHtml. Если документ строится существующим приложением, вместо setContent используется goto, после чего сценарий проходит нужные шаги интерфейса. Для защищённого экрана сначала выполняется аутентификация либо загружается заранее сохранённое состояние контекста. В обоих случаях настройки PDF одинаковы, потому что метод печатает уже подготовленную вкладку.
Подготовка HTML через setContent
setContent удобен для счетов, актов, этикеток и отчётов, которые собираются из шаблона без отдельного веб-сервера. В разметку можно встроить таблицу стилей, SVG, изображения в виде data URI и заранее рассчитанные данные. Такой подход делает документ независимым от маршрутизации приложения, но разработчик самостоятельно отвечает за экранирование пользовательских значений, подключение шрифтов и корректные абсолютные пути к ресурсам.
Опция waitUntil: 'load' означает, что событие load уже произошло, однако она не гарантирует завершение всех асинхронных компонентов. React, Vue или другой клиентский код может продолжать запрашивать данные и менять DOM. Поэтому шаблонный документ лучше формировать полностью до setContent либо добавлять явный признак готовности, например атрибут data-report-ready на корневом элементе, и ждать его через locator.waitFor().
await page.setContent(html, { waitUntil: 'domcontentloaded' });
await page.locator('[data-report-ready="true"]').waitFor();
await page.evaluate(async () => {
await document.fonts.ready;
await Promise.all(
[...document.images]
.filter(image => !image.complete)
.map(image => new Promise(resolve => {
image.addEventListener('load', resolve, { once: true });
image.addEventListener('error', resolve, { once: true });
}))
);
});
Если HTML содержит относительные ссылки на CSS или изображения, у него должен быть понятный базовый адрес. Без него браузер попробует разрешить путь относительно пустого документа, и ресурс не загрузится. Надёжнее встраивать критические стили внутрь шаблона, а статические файлы передавать абсолютными файловыми путями через безопасный обработчик либо публиковать в контролируемой внутренней точке приложения.
Печать уже открытого личного кабинета
Для закрытых кабинетов Playwright PDF особенно полезен тем, что способен пройти тот же путь, что и пользователь. Сценарий вводит учётные данные, подтверждает вход, выбирает организацию, период и фильтры, открывает нужный отчёт. После этого PDF формируется из страницы, доступной только внутри авторизованного контекста. Куки, localStorage и заголовки запросов остаются связанными с этим контекстом до его закрытия.
В автоматической среде пароль не помещают в код или HTML. Его передают через переменные окружения либо секреты системы запуска. Сохранённое storage state позволяет не выполнять интерактивный вход при каждом документе, но файл состояния содержит чувствительные токены и требует таких же ограничений доступа, как пароль. При истечении сессии сценарий должен распознать форму входа и завершиться понятной ошибкой, а не печатать её вместо отчёта.
const context = await browser.newContext({ storageState: 'secrets/session.json' });
const page = await context.newPage();
await page.goto(process.env.REPORT_PAGE);
if (await page.getByRole('heading', { name: 'Вход' }).isVisible().catch(() => false)) {
throw new Error('Сессия истекла: требуется повторная авторизация');
}
await page.getByLabel('Период').selectOption('month');
await page.getByRole('button', { name: 'Сформировать' }).click();
await page.locator('[data-report-state="ready"]').waitFor();
Для многоарендных систем отдельный browser context создают на каждого клиента или задачу. Это исключает перенос cookie и кэша между документами. Одна вкладка не должна последовательно обслуживать разных пользователей без полной очистки состояния: даже если адрес страницы одинаков, старые запросы, service worker и локальное хранилище могут повлиять на следующий PDF.
Как выбрать момент готовности страницы
Качество результата чаще зависит не от page.pdf(), а от правильного ожидания. Событие domcontentloaded подтверждает разбор исходного HTML, load — загрузку обычных ресурсов, но ни одно из них не знает, завершил ли приложение расчёт диаграммы или получил ли API-ответ. Стабильный сценарий ждёт бизнес-признак: строку Отчёт сформирован, исчезновение индикатора, определённое число строк таблицы или установленный компонентом атрибут.
Состояние networkidle не следует считать универсальным сигналом. Страница с аналитикой может постоянно поддерживать WebSocket, посылать телеметрию или обновлять счётчик, из-за чего ожидание не завершится. Обратная ситуация тоже возможна: сеть замолчала, но браузер ещё выполняет тяжёлую отрисовку canvas. Явный маркер готовности лучше отражает условие, которое действительно требуется для документа.
- Ждите появления итогового контейнера, а не только навигации.
- Проверяйте отсутствие спиннеров и skeleton-блоков.
- Дожидайтесь document.fonts.ready перед печатью текста.
- Для графиков используйте событие или атрибут, выставляемый после render.
- Фиксируйте число ожидаемых строк, если данные загружаются порциями.
При ошибке ожидания полезно сохранить HTML и обычный PNG-снимок страницы до закрытия браузера. Они показывают, что находилось в DOM и как вкладка выглядела в момент сбоя. Такой диагностический набор быстрее выявляет форму входа, пустой контейнер, сообщение API или перекрывающий модальный диалог, чем один стек исключения.
Интерфейс UI Mode при настройке сценария
UI Mode помогает запускать шаги в интерактивном режиме, просматривать их последовательность и повторять проблемный участок. Для PDF-задачи это удобно при подборе действий до печати: можно убедиться, что фильтр применён, раскрывающаяся панель действительно открыта, а элемент готовности появился. Сам документ всё равно создаётся вызовом в коде, но состояние страницы перед ним легче проверить визуально.
При отладке полезно временно запускать Chromium с headless: false и вставлять page.pause() перед page.pdf(). Откроется инспектор, а выполнение остановится на нужной строке. Разработчик может проверить DOM, локаторы, размеры блоков и медиастили, затем продолжить сценарий. В рабочем конвейере паузу удаляют: она требует ручного вмешательства и не подходит для фоновой генерации.
Формат бумаги и собственные размеры
Параметр format принимает распространённые размеры Letter, Legal, Tabloid, Ledger и диапазон A0–A6. Если format задан, он имеет приоритет над width и height. Это правило важно при поиске причины неправильного размера: оставшийся в конфигурации format заставит Chromium игнорировать собственные значения ширины и высоты.
width, height и поля принимают числа либо строки с единицами px, in, cm и mm. Число без единицы трактуется как пиксели, поэтому запись 210 не означает 210 миллиметров. Для стандартного листа безопаснее использовать format: 'A4', а для чека, талона или этикетки задавать явные значения вроде width: '80mm'. Высота может быть фиксированной либо рассчитываться по содержимому заранее.
await page.pdf({
path: 'output/label.pdf',
width: '100mm',
height: '150mm',
margin: { top: '4mm', right: '4mm', bottom: '4mm', left: '4mm' },
printBackground: true
});
При нестандартной узкой бумаге необходимо проверить минимальные поля конкретного принтера уже после генерации. PDF может быть корректным, но физическое устройство обрежет содержимое у края. Макет с технологическим запасом обычно надёжнее, чем попытка использовать всю площадь листа. Для типографии дополнительно согласуют вылеты и метки, потому что page.pdf() не превращает обычную веб-страницу в полноценный препресс-макет автоматически.
Размер из CSS и параметр preferCSSPageSize
Правило @page позволяет хранить размер листа рядом с печатными стилями. Когда preferCSSPageSize включён, Chromium отдаёт приоритет size из @page. Без этой опции содержимое масштабируется под формат, выбранный в PDFOptions. Это позволяет одной и той же функции печатать разные шаблоны: каждый шаблон объявляет собственный размер, а код не содержит отдельной таблицы форматов.
<style>
@page {
size: A4 portrait;
margin: 14mm 12mm 18mm;
}
@media print {
.screen-only { display: none !important; }
.report-section { break-inside: avoid; }
}
</style>
Не следует одновременно разносить критические размеры между CSS и объектом options без явного правила приоритета. Иначе небольшое изменение шаблона неожиданно повлияет на серверный результат. Команда обычно выбирает один подход: либо формат задаёт вызывающий код, либо каждый шаблон полностью управляет @page и всегда печатается с preferCSSPageSize.
Книжная и альбомная ориентация
landscape: true меняет ориентацию выбранного листа. Альбомный A4 подходит для широких сравнительных таблиц, календарей и диаграмм с длинной горизонтальной осью. Однако ориентация не решает проблему бесконечно широкой таблицы: если сумма минимальных ширин колонок превышает страницу, браузер начнёт сжимать текст, переносить значения или обрезать блок с фиксированной шириной.
Перед переключением в landscape полезно оптимизировать саму таблицу: убрать второстепенные колонки, сократить подписи, разрешить переносы в заголовках, задать table-layout: fixed и разумные ширины. Для очень большого набора полей лучше сформировать несколько тематических таблиц или отдельное приложение. PDF остаётся удобным для чтения, когда каждую страницу можно воспринимать без постоянного увеличения.
Поля страницы и безопасная область
По умолчанию поля равны нулю. Если документ содержит обычный текст, это почти всегда слишком мало: строки подходят к краю, а колонтитулы накладываются на содержимое. margin в PDFOptions резервирует пространство вокруг области печати. Внутренние padding у body и контейнеров работают дополнительно, поэтому их сумма может неожиданно сузить макет.
Для документов с верхним колонтитулом верхнее поле должно быть больше высоты шаблона. Chromium не сдвигает основной контент автоматически под высокий headerTemplate. Аналогично нижний номер страницы требует достаточного margin.bottom. Если колонтитул исчезает или перекрывает текст, первым делом увеличивают соответствующее поле и уменьшают внутренние отступы шаблона.
Единицы лучше выбирать одинаковые во всём проекте. Миллиметры удобны для бумаги, CSS-пиксели — для экранной сетки, но их смешение затрудняет расчёт. Проверка нескольких страниц с коротким и длинным содержимым обязательна: наложение часто проявляется только на листе, где строка заголовка переносится на две линии.
Масштабирование без разрушения макета
scale изменяет масштаб веб-страницы в диапазоне от 0,1 до 2. Значение меньше единицы может помочь разместить слегка переполненную таблицу, но оно одновременно уменьшает шрифт, линии и изображения. Это аварийный инструмент, а не замена адаптивной печатной вёрстке. Если для каждого отчёта нужен scale 0.6, макет следует переработать.
При выборе масштаба сравнивают читаемость распечатки, а не только число страниц. Текст должен сохранять достаточный кегль, штрихкоды — требуемую физическую ширину модуля, а QR-коды — свободную зону вокруг. Для этикеток и форм с измеряемыми элементами scale обычно оставляют равным единице и управляют размерами напрямую.
Фоновые цвета, изображения и точная цветопередача
printBackground по умолчанию выключен, поэтому заливки карточек, фоновые изображения и цветные полосы могут исчезнуть. В отчётах, где цвет кодирует статус или категорию, параметр включают явно. При этом браузер всё равно может корректировать оттенки для печати, стремясь повысить контраст и сократить расход чернил.
Для сохранения заданных цветов в CSS применяют -webkit-print-color-adjust: exact. Правило задают на нужных компонентах или на корневом контейнере, понимая, что оно может увеличить насыщенность заливок. Для текста важнее контраст: светло-серые подписи, приемлемые на мониторе, на бумаге и в офисном принтере становятся почти незаметными.
@media print {
html, body, .report {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.status-ok { background: #dff5e3; }
.status-alert { background: #ffe4e1; }
}
Цветовой профиль и профессиональные параметры допечатной подготовки методом page.pdf() не настраиваются. Для деловых отчётов и архивных копий этого обычно достаточно, но макет упаковки, рассчитанный на конкретный печатный процесс, требует специализированного конвейера и отдельной проверки типографией.
Режим print и сохранение экранного оформления
Разница между print и screen часто объясняет непохожий PDF. Сайт может скрывать меню, менять шрифт, убирать тени, разворачивать ссылки и перестраивать сетку внутри @media print. Это не ошибка Playwright: браузер применяет правила, предназначенные автором страницы для печати. Перед исправлением параметров следует открыть эмуляцию печатных стилей в инструментах разработчика и посмотреть, какие селекторы срабатывают.
Если задача состоит в фиксации экранного дашборда, используют page.emulateMedia({ media: 'screen' }) перед экспортом. При этом лист всё равно имеет заданный формат, поэтому широкая экранная сетка может не поместиться. Иногда нужен отдельный класс, который сохраняет цвета экрана, но перестраивает колонки под бумагу. Такой гибрид даёт более предсказуемый результат, чем полное игнорирование печатного CSS.
await page.emulateMedia({ media: 'screen' });
await page.addStyleTag({ content: `
.dashboard { width: 1120px; transform-origin: top left; }
.toolbar, .live-indicator { display: none !important; }
` });
await page.pdf({ path: 'output/dashboard.pdf', format: 'A3', landscape: true, printBackground: true });
Колонтитулы и номера страниц
displayHeaderFooter включает верхний и нижний шаблоны. Внутри headerTemplate и footerTemplate можно использовать элементы с классами date, title, url, pageNumber и totalPages; Chromium подставит соответствующие значения при печати. Обычно шаблон задаёт минимальный HTML с инлайновыми стилями, потому что стили самой страницы внутри колонтитула недоступны.
const footerTemplate = `
<div style="width:100%;font-size:8px;color:#666;padding:0 12mm;display:flex;justify-content:space-between">
<span>Внутренний отчёт</span>
<span><span class="pageNumber"></span> / <span class="totalPages"></span></span>
</div>`;
await page.pdf({
path: 'output/report.pdf',
format: 'A4',
displayHeaderFooter: true,
headerTemplate: '<div></div>',
footerTemplate,
margin: { top: '14mm', bottom: '20mm', left: '12mm', right: '12mm' }
});
Сценарии внутри шаблонов не выполняются, а классы и шрифты из основного документа туда не наследуются. Поэтому попытка подключить общий CSS или вычислить текст JavaScript-кодом не сработает. Динамическое название лучше подготовить в Node.js и вставить в строку шаблона после обязательного HTML-экранирования.
Номер можно поместить и в основной поток документа, но тогда он не будет автоматически повторяться на каждой странице. Встроенные классы pageNumber и totalPages предназначены именно для этого случая. Если требуется разный колонтитул у разделов, проще печатать разделы отдельно и затем объединять документы, поскольку один вызов применяет один комплект шаблонов ко всему результату.
Диапазоны страниц
pageRanges принимает строку вроде 1-5, 8, 11-13 и позволяет сохранить только выбранные листы. Нумерация относится к итоговой раскладке Chromium после всех переносов. Поэтому диапазон нельзя надёжно вычислить по числу HTML-разделов: одна секция может занять несколько листов, а изменение шрифта сдвинет последующие страницы.
Функция полезна для повторной выдачи конкретного фрагмента уже стабильного шаблона или для исключения служебного титульного листа. Для программного выбора логических разделов лучше до печати скрывать ненужные блоки через класс, параметр шаблона или DOM-операцию. Тогда результат не зависит от случайного номера страницы.
Разрывы страниц в CSS
Управление разрывами выполняется печатными CSS-свойствами break-before, break-after и break-inside. Заголовок нового раздела можно начать с отдельного листа, карточку — попросить не разрывать, а после титульного блока — принудительно вставить переход. Старые page-break-* свойства всё ещё встречаются в шаблонах, но новые break-* яснее описывают намерение.
@media print {
.chapter { break-before: page; }
.summary-card { break-inside: avoid; }
.keep-with-next { break-after: avoid; }
h2, h3 { break-after: avoid; }
p { orphans: 3; widows: 3; }
}
break-inside: avoid не является абсолютной гарантией, если сам блок выше доступной области листа. Браузеру всё равно придётся разорвать его либо создать неудобный overflow. Поэтому длинные таблицы, ленты событий и описания нельзя безусловно помещать в неразрывный контейнер. Ограничение применяют к компактным карточкам, подписям с изображением и небольшим итоговым панелям.
Пустые страницы обычно появляются из-за сочетания принудительного break-before, фиксированной высоты и элемента, который уже оказался в начале нового листа. При поиске проблемы временно отключают правила разрыва по одному и добавляют контур печатным контейнерам. Так видно, какой блок занимает лишнее пространство.
Длинные таблицы и повтор заголовков
Для многостраничной таблицы используют семантические thead, tbody и tfoot. Chromium способен повторять группу заголовка на новых листах, если она оформлена как table-header-group. Строки с итогами можно отделить визуально, но повтор footer зависит от структуры и сложности макета, поэтому его нужно проверять на реальных данных.
@media print {
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
tr { break-inside: avoid; }
th, td { padding: 2.2mm; border: 0.2mm solid #bbb; overflow-wrap: anywhere; }
}
Фиксированная ширина помогает распределить колонки предсказуемо, но длинные идентификаторы и адреса должны уметь переноситься. Для денежных значений задают nowrap только там, где строка гарантированно короткая. Если одна ячейка содержит много абзацев, запрет разрыва всей строки создаёт большой пустой участок; для таких данных лучше разрешить перенос или вынести подробности в отдельный блок.
Виртуализированные таблицы требуют особого внимания. Библиотека может держать в DOM только видимые строки, и PDF сохранит лишь их. Перед печатью виртуализацию отключают, переключают компонент в режим экспорта либо запрашивают все данные и строят отдельную печатную таблицу. Простое увеличение viewport не всегда помогает, потому что компонент продолжает переиспользовать ограниченный набор DOM-узлов.
Шрифты и отсутствующие символы
Шрифты загружаются внутри браузера, а не подставляются Playwright отдельно. Если веб-шрифт недоступен, Chromium использует запасную гарнитуру, что меняет ширину строк и количество страниц. В контейнере или на сервере также должен присутствовать системный шрифт, указанный в CSS. Особенно заметны проблемы с кириллицей, иероглифами, математическими знаками и эмодзи.
document.fonts.ready следует ждать после того, как на странице появились элементы, использующие нужные начертания. Для критического семейства можно дополнительно проверить document.fonts.check. Если проверка возвращает false, генерацию лучше остановить с диагностикой, чем выпускать документ со случайной подстановкой.
await page.evaluate(async () => {
await document.fonts.ready;
const required = ['400 12px "Report Sans"', '700 16px "Report Sans"'];
const missing = required.filter(font => !document.fonts.check(font));
if (missing.length) throw new Error(`Не загружены шрифты: ${missing.join(', ')}`);
});
При подключении файла через @font-face учитывают права доступа, CORS и формат. Встроить шрифт как data URI проще для автономного шаблона, но размер HTML растёт. Для массовой генерации разумно держать проверенные файлы рядом с приложением и обслуживать их из контролируемого каталога. Нельзя полагаться на гарнитуру, установленную только на компьютере разработчика.
Изображения, SVG и ленивые загрузчики
Обычное изображение может ещё загружаться после события DOMContentLoaded. Перед печатью проверяют image.complete и naturalWidth. Если naturalWidth равен нулю, ресурс не декодирован или завершился ошибкой. Для важных схем и печатей сценарий должен завершаться ошибкой, а не молча выпускать пустое место.
Lazy loading часто привязан к IntersectionObserver. Элемент далеко ниже viewport не запрашивает файл, хотя при печати должен оказаться на последующей странице. Решение — отключить ленивый режим в шаблоне экспорта, заменить data-src на src или прокрутить документ до конца с ожиданием каждого блока. Отдельный печатный шаблон надёжнее искусственной прокрутки сложной ленты.
SVG хорошо масштабируется и подходит для схем, логотипов и иконок. Внешние шрифты и стили внутри SVG также должны быть доступны. Если SVG создаётся JavaScript-компонентом, ждут окончания его render-фазы и проверяют наличие ожидаемых path или text. Встроенный SVG легче контролировать, чем ссылка на удалённый файл с меняющимся содержимым.
Canvas, диаграммы и карты
Canvas попадает в PDF как уже отрисованная поверхность. Если библиотека ещё выполняет анимацию или перерисовывает кадры, документ может получить промежуточное состояние. Перед экспортом отключают анимации, вызывают предусмотренный библиотекой метод завершения и ждут собственный признак готовности. Универсальная задержка в несколько секунд работает нестабильно: быстрый сервер теряет время, а медленный иногда не успевает.
Разрешение canvas задаётся его внутренними width и height, а не только CSS-размером. Низкое внутреннее разрешение даст размытую диаграмму при увеличении PDF. Для печати компоненту передают повышенный pixel ratio либо строят SVG-версию, если библиотека её поддерживает. При этом чрезмерный canvas резко увеличивает память и может привести к падению вкладки.
Интерактивная карта обычно содержит плитки, подписи и слои, загружаемые независимо. Нужно дождаться события idle самой карты и убедиться, что лицензионные подписи остаются видимыми. В печатном режиме полезно отключить элементы управления и зафиксировать масштаб, иначе карта выглядит как интерфейс, а не как иллюстрация отчёта.
Отключение анимаций и мигающих элементов
Анимация влияет не только на графики. Скелетоны, плавное раскрытие, карусель и мигающий курсор могут попасть в случайную фазу. Контекст можно создать с reducedMotion: 'reduce', а перед печатью добавить CSS, который обнуляет animation и transition. Компоненты, реагирующие на prefers-reduced-motion, сразу переходят в спокойное состояние.
const context = await browser.newContext({ reducedMotion: 'reduce' });
const page = await context.newPage();
await page.goto(process.env.REPORT_PAGE);
await page.addStyleTag({ content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition: none !important;
caret-color: transparent !important;
}
` });
Добавлять стиль нужно после навигации или через init script, если приложение заменяет документ. Для страницы, где анимация запускает обязательную логику, грубое отключение может помешать инициализации. Тогда используют API компонента: сначала дожидаются готовности, затем переводят его в статическое состояние.
Встраиваемые фреймы и сторонние виджеты
Содержимое iframe печатается вместе со страницей, если оно успело загрузиться и допускает отображение. Однако сторонний виджет может требовать отдельную авторизацию, блокировать автоматизированный браузер или рисовать только видимую область. Для отчёта лучше получать данные напрямую и формировать собственное представление, чем зависеть от чужого интерфейса.
Когда фрейм принадлежит тому же приложению, Playwright может ждать элементы через frameLocator. Готовность проверяют внутри соответствующего документа. Если фрейм бесконечно показывает загрузчик, общий page.pdf() всё равно завершится и зафиксирует его. Поэтому состояние каждого критического фрейма входит в условие готовности отчёта.
Формы и значения полей
Текущие значения input, select и textarea видны в печати в той мере, в какой Chromium отображает их в печатном CSS. Для официального бланка лучше не полагаться на внешний вид системных контролов: вместо них создают отдельный печатный слой с обычным текстом, галочками и рамками. Он выглядит одинаковее на разных средах и не содержит интерактивных особенностей браузера.
page.pdf() не превращает HTML-поля в редактируемые AcroForm-поля PDF. Результат фиксирует визуальное состояние страницы. Если получателю нужна заполняемая форма, подпись в специализированном поле или сложные PDF-аннотации, потребуется отдельная библиотека после генерации либо другой инструмент, который создаёт структуру формы напрямую.
Доступность: tagged и outline
Параметр tagged просит Chromium сформировать тегированный PDF, а outline — включить структуру закладок документа. Полезность результата зависит от семантики исходного HTML: заголовки должны быть настоящими h2/h3, таблицы — иметь th и подписи, изображения — осмысленный alt, порядок DOM — соответствовать чтению. Визуально красивый набор div без ролей не становится доступным только от одного переключателя.
После включения tagged документ проверяют специализированным валидатором и программой чтения с экрана. Автоматическая структура не гарантирует соответствия всем требованиям доступности, особенно для сложных таблиц, диаграмм и декоративных элементов. Артефакты, которые не несут смысла, следует исключать из дерева доступности ещё в HTML.
outline удобен для длинных регламентов и отчётов с разделами. Закладки строятся на основе структуры документа, поэтому пропуски уровней и декоративные заголовки ухудшают навигацию. Для короткого счёта или чека закладки не нужны и только увеличивают сложность проверки.
Ссылки и навигация внутри документа
Обычные ссылки из HTML могут сохраняться как кликабельные области PDF. Перед массовой выдачей проверяют, не попали ли в документ внутренние административные адреса, временные токены или ссылки на закрытые маршруты. Печатный шаблон должен выводить только те переходы, которые безопасно передать получателю.
Якорные ссылки внутри длинного документа зависят от структуры и поведения движка печати. Для критической навигации лучше сочетать оглавление, понятные заголовки и outline. Если адрес в тексте нужен как реквизит, его выводят обычной строкой и проверяют переносы, чтобы длинное значение не раздвигало таблицу.
Получение Buffer вместо файла
Когда path не задан, page.pdf() возвращает Buffer. Это оптимально для веб-обработчика, который сразу отдаёт документ, и для очереди, отправляющей объект в удалённое хранилище. Нужно правильно выставить Content-Type, Content-Disposition и длину ответа, а имя файла кодировать так, чтобы кириллица не ломала заголовок.
const pdf = await page.pdf({ format: 'A4', printBackground: true });
response.setHeader('Content-Type', 'application/pdf');
response.setHeader('Content-Disposition', 'attachment; filename="report.pdf"');
response.setHeader('Content-Length', String(pdf.length));
response.end(pdf);
Буфер занимает память целиком. Для небольших счетов это удобно, но сотни длинных документов, создаваемых параллельно, дают заметную нагрузку. Ограничение конкурентности и контроль размера результата важнее попытки запустить максимум вкладок. Если нужен файл, безопасно писать во временное имя и атомарно переименовывать после успешного завершения, чтобы другой процесс не увидел недописанный документ.
Пакетная генерация
При серии документов браузер обычно запускают один раз, а для задач создают отдельные контексты и страницы. Запуск нового процесса Chromium на каждый счёт тратит время и память. Но бесконечно переиспользовать одну вкладку тоже рискованно: накопленные обработчики, кэш приложения и утечки JavaScript постепенно увеличивают потребление ресурсов.
Практичная схема использует пул с ограниченным числом одновременных страниц. Каждая задача получает новый context, формирует один или несколько связанных документов и закрывает context в finally. Сам браузер периодически перезапускается после заданного числа задач или при росте памяти. Повторная попытка допустима для временной сетевой ошибки, но не должна скрывать постоянный дефект шаблона.
async function renderJob(browser, job) {
const context = await browser.newContext({ locale: job.locale, timezoneId: job.timezone });
try {
const page = await context.newPage();
await prepareReport(page, job);
return await page.pdf(job.pdfOptions);
} finally {
await context.close();
}
}
Очередь хранит идентификатор задачи, параметры шаблона и контрольную сумму входных данных. Повторный запуск с тем же идентификатором не должен создавать разные версии без причины. Для идемпотентности готовый файл записывают по детерминированному ключу, а статус меняют на готов только после проверки размера и сигнатуры PDF.
Производительность и потребление памяти
Наиболее дорогие операции — запуск браузера, загрузка тяжёлой страницы, рендер больших изображений и формирование многостраничного документа. Время page.pdf() растёт с объёмом DOM, количеством шрифтов и графики. Тысячи скрытых узлов тоже участвуют в расчёте стилей, поэтому печатный шаблон должен содержать только нужные данные.
Память особенно быстро расходуют canvas, фотографии без уменьшения и длинные таблицы с большим числом элементов. Изображение следует подготавливать под реальный размер печати, а не передавать многомегапиксельный оригинал для маленькой ячейки. Для очень длинного отчёта разумно разбить данные на части, сформировать несколько PDF и объединить их контролируемым инструментом.
Событие page.on('crash') и исключения с упоминанием закрытой страницы помогают отличить падение вкладки от обычного тайм-аута. При краше задача завершается, context закрывается, а браузер при необходимости перезапускается. Слепой повтор без уменьшения нагрузки часто приводит к тому же результату.
Стабильность дат, часовых поясов и локали
Один и тот же отчёт может отличаться на разных серверах из-за локали и часового пояса. browser.newContext принимает locale и timezoneId, поэтому формат дат, названия месяцев и результат клиентских вычислений можно зафиксировать. Значения, важные для бухгалтерии, лучше подготовить на сервере и вывести готовой строкой, не полагаясь на настройки машины.
Класс date в колонтитуле подставляет дату печати, что делает файл недетерминированным. Для архива, где документ должен повторяться байт в байт или хотя бы визуально, дату формируют заранее и вставляют обычным текстом. Случайные идентификаторы, текущие часы, рекламные блоки и персонализированные подсказки также убирают из печатного представления.
Через addInitScript можно заранее стабилизировать Math.random или подменить отдельные значения окружения, но вмешательство должно быть осознанным. Если страница использует случайность для безопасности или загрузки данных, глобальная замена нарушит работу. Надёжнее проектировать шаблон отчёта без случайного содержимого.
Сетевые запросы и воспроизводимость
Страница может обращаться к аналитике, рекламе, внешним шрифтам и необязательным API. Эти запросы увеличивают время и создают точки отказа. Через context.route можно блокировать ненужные домены или типы ресурсов, но правило не должно отрезать данные отчёта. Список разрешений лучше строить по фактической сетевой трассе тестового прогона.
await context.route('**/*', async route => {
const request = route.request();
const type = request.resourceType();
if (type === 'media' || type === 'websocket') return route.abort();
return route.continue();
});
Полное отключение сети подходит только для автономного setContent с встроенными ресурсами. Для страницы приложения полезно записывать ошибки ответов и вывод console.error. PDF может сформироваться даже после ответа 500, если интерфейс показал сообщение об ошибке. Сценарий обязан распознать такое состояние и не выдавать его как корректный отчёт.
Безопасность при печати недоверенного HTML
Передача пользовательского HTML в браузер означает выполнение его сценариев и загрузку указанных ресурсов. Если вход не доверен, изолированный контекст сам по себе недостаточен: страница может обращаться к внутренним адресам, считывать доступные данные приложения или создавать чрезмерную нагрузку. Разметку очищают, скрипты запрещают, сетевой доступ ограничивают, а процесс запускают с минимальными правами.
Нельзя отключать штатную песочницу Chromium только ради удобства контейнера без оценки риска. Параметры окружения и файловая система процесса также требуют ограничения. В шаблон передают только необходимые поля, экранируют текст и отдельно проверяют разрешённые data URI. Для публичной услуги вводят пределы на длину HTML, число изображений, время выполнения и размер результата.
Навигацию и всплывающие окна можно блокировать обработчиками. Диалоги alert или beforeunload способны остановить действия, если их не принять или не закрыть. Для печатного контекста обычно устанавливают обработчик dialog, который отклоняет неожиданные окна и записывает событие в журнал.
Пути к файлам и права записи
Относительный path разрешается от текущего рабочего каталога процесса, который в службе может отличаться от каталога проекта. Для предсказуемости путь строят через модуль path от известной рабочей директории. Перед печатью создают родительскую папку и проверяют, что имя не содержит разделителей, полученных от пользователя.
Ошибка доступа возникает, если процесс не имеет права записи, файл уже открыт другой программой или каталог смонтирован только для чтения. Временный каталог должен иметь ограниченные разрешения и периодическую очистку. После успешной передачи буфера временный файл удаляют в finally; после сбоя сохраняют его только когда он нужен для диагностики и не содержит лишних персональных данных.
Инспектор и пошаговая отладка
Playwright Inspector показывает выполняемые действия, локатор и состояние страницы. Перед строкой page.pdf() можно поставить page.pause(), пройти сценарий до нужного места и исследовать вкладку. Это особенно полезно, когда отчёт строится после нескольких кликов и фильтров, а ошибка возникает только в определённой комбинации данных.
Инспектор не является редактором PDF. Он помогает проверить подготовительный браузерный сценарий: какой элемент найден, что нажато, появилась ли таблица и не перекрыл ли её диалог. Для макета дополнительно используют инструменты разработчика Chromium, переключают print media и исследуют вычисленные CSS-свойства.
Подбор устойчивых локаторов
Сценарий генерации ломается, если перед печатью он нажимает элементы по случайным CSS-классам или позициям. Предпочтительны getByRole, getByLabel, getByText с точным контекстом и data-testid для служебных признаков. Локатор должен описывать назначение элемента, а не его текущий цвет или номер вложенного div.
Режим выбора локатора подсвечивает элемент в браузере и предлагает выражение. Полученный вариант проверяют на уникальность и устойчивость. Для отчётной страницы полезно добавить отдельные признаки data-report-state, data-chart-ready и data-export-view: они уменьшают зависимость автоматики от декоративной вёрстки.
Когда кнопка имеет одинаковую подпись в нескольких карточках, локатор сужают родительским блоком по заголовку. Нельзя просто брать first(), если порядок способен измениться. Ошибка строгого режима, сообщающая о нескольких совпадениях, полезна: она выявляет неоднозначность до того, как сценарий сформирует документ не того клиента или периода.
Запись шагов с Codegen
Codegen помогает быстро получить начальную последовательность действий для существующей страницы. Разработчик проходит вход, выбирает фильтр и открывает отчёт, а инструмент предлагает локаторы. Сгенерированный код нельзя считать готовым конвейером: в него добавляют секреты через безопасные параметры, явные ожидания, обработку ошибок, очистку контекста и вызов page.pdf().
Запись особенно полезна при сложных компонентах выбора даты и многошаговой навигации. После неё лишние клики удаляют, локаторы упрощают и проверяют на нескольких наборах данных. Стабильный сценарий должен работать независимо от скорости анимации и не опираться на координаты мыши, если действие можно выразить семантическим локатором.
Трассировка проблемного прогона
Trace Viewer хранит снимки DOM, сетевые события, консоль, шаги и вложения. Для плавающей ошибки это информативнее финального скриншота: можно перейти к моменту, когда таблица ещё была пустой, увидеть ответ API и проверить, какой локатор сработал. Трассу включают для повторной попытки или только при сбое, чтобы не раздувать хранилище.
В трассу могут попасть персональные данные, токены в заголовках и содержимое страницы. Файлы диагностики защищают и удаляют по установленному сроку. Перед передачей внешнему исполнителю их обезличивают. Для постоянного мониторинга достаточно метрик времени, размера PDF и кода ошибки; полная трасса нужна не для каждого успешного документа.
HTML-отчёт о сбоях сценария
HTML Reporter группирует успешные и неудачные прогоны, показывает шаги, ошибки и прикреплённые файлы. В проекте генерации PDF к неудачному тесту можно приложить обычный screenshot, сохранённый HTML и диагностический текст. Это помогает отделить дефект печатного CSS от проблемы входа, данных или ожидания.
Автоматический тест должен проверять не только существование файла. Минимальные проверки включают размер больше разумного порога, сигнатуру PDF, ожидаемое число страниц для фиксированного примера и наличие ключевого текста после извлечения. Для визуально критичных шаблонов эталонные страницы рендерят в изображения и сравнивают с допустимым порогом различий.
Проверка готового PDF
Успешный page.pdf() означает, что Chromium вернул байты, но не подтверждает смысловую корректность. После генерации проверяют начало файла, ненулевой размер и возможность открыть документ библиотекой чтения. Для важных форм извлекают текст и убеждаются, что присутствуют номер, дата и итоговая сумма. Эти проверки ловят страницу входа, пустой отчёт и серверное сообщение, даже если они визуально напечатались без ошибки.
Число страниц полезно как диапазон, а не всегда как точное значение. Данные меняются, и одна дополнительная строка может создать новый лист. Для фиксированного тестового набора точное число допустимо. В рабочей задаче лучше проверять, что листов не ноль и не подозрительно много, иначе бесконечный блок или повторяющийся элемент способен породить огромный файл.
Визуальный контроль выполняют рендерингом PDF в PNG. Сравнение эталона обнаруживает исчезнувший шрифт, сдвиг столбцов и обрезанный колонтитул. Динамические даты и идентификаторы перед сравнением фиксируют либо маскируют, иначе каждое выполнение будет отличаться без реального дефекта.
Типовые причины пустого PDF
Пустой документ появляется, когда основной контейнер скрыт в @media print, данные ещё не загружены или приложение показывает содержимое внутри виртуализированного viewport. Сначала проверяют обычный screenshot непосредственно перед page.pdf(), затем включают эмуляцию print и смотрят вычисленный display у корневых блоков. Если экран заполнен, а print-режим пуст, причина почти всегда в CSS.
Другой вариант — навигация привела на страницу входа или ошибки, но её стили скрывают форму при печати. Сценарий должен проверять характерный заголовок отчёта и отсутствие сообщений об ошибке до экспорта. Ожидание произвольной паузы не заменяет такую проверку.
- Убедитесь, что нужный контейнер существует и видим.
- Проверьте @media print и классы screen-only.
- Дождитесь данных и шрифтов по явному признаку.
- Отключите виртуализацию для печатного представления.
- Сохраните PNG и HTML перед закрытием вкладки.
Почему не печатаются фоны
Первая проверка — printBackground: true. Затем исследуют, не переопределён ли background в @media print. Если цвет присутствует, но стал бледнее, добавляют print-color-adjust и оценивают контраст на бумаге. Фоновое изображение также должно успеть загрузиться; CSS-ссылка не отображается, если ресурс недоступен из среды генерации.
Градиенты и полупрозрачность могут выглядеть иначе при разных просмотрщиках и принтерах. Для диаграмм, где оттенок несёт смысл, добавляют текстовую подпись или узор. Документ не должен становиться непонятным после чёрно-белой печати.
Обрезанный текст и горизонтальный overflow
Фиксированная ширина в пикселях, min-width у таблицы и white-space: nowrap часто выводят содержимое за пределы листа. Chromium не всегда автоматически уменьшает отдельный блок. В печатном CSS сбрасывают min-width, разрешают перенос, переводят сетку в одну колонку и скрывают второстепенные панели.
@media print {
.app-shell, .content, .report { width: auto !important; min-width: 0 !important; }
.sidebar, .toolbar { display: none !important; }
.grid { display: block !important; }
.cell { overflow-wrap: anywhere; white-space: normal; }
}
Если блок использует transform для масштабирования экрана, его визуальная ширина может не совпадать с занимаемым местом в потоке. Это создаёт пустые области и обрезание. Для печати transforms лучше сбрасывать и задавать реальные размеры. Аналогично position: fixed следует применять только к действительно повторяемым элементам.
Колонтитул не отображается или перекрывает текст
displayHeaderFooter должен быть включён, а шаблон — содержать видимый элемент с собственным размером шрифта. Пустой body-стиль страницы на него не действует. Если шаблон есть, но его не видно, увеличивают margin.top или margin.bottom и проверяют цвет текста. Белая подпись, рассчитанная на тёмный фон сайта, на белой бумаге исчезнет.
Слишком длинный заголовок переносится и увеличивает высоту, но основной поток не знает об этом. Колонтитул должен быть коротким и иметь контролируемую высоту. Полное название отчёта лучше разместить в теле первого листа, а сверху оставить сокращённую метку.
Пропавшие шрифты и квадраты вместо символов
Квадраты указывают, что выбранная гарнитура не содержит глифов или файл шрифта не загружен. Проверяют console, failed requests и document.fonts.check для конкретного начертания. В серверном окружении набор системных шрифтов обычно меньше, чем на рабочем компьютере, поэтому явный @font-face делает результат воспроизводимее.
Эмодзи особенно зависят от установленного цветного шрифта и поддержки рендеринга. Для делового документа надёжнее заменить их SVG-иконками или текстовыми символами из проверенной гарнитуры. Символ валюты, минус и неразрывный пробел тестируют отдельно, потому что ошибка может проявиться только в одной строке.
Пустые диаграммы и незавершённая анимация
Если контейнер есть, а график пуст, проверяют его SVG или canvas непосредственно перед экспортом. Для SVG считают элементы path, для canvas можно получить data URL и убедиться, что поверхность не прозрачна. Компонент должен сообщать о завершении отрисовки; ожидание появления контейнера недостаточно.
Анимацию отключают настройкой библиотеки, а не только CSS, если данные дорисовываются JavaScript-таймером. При серверной генерации выбирают статический режим графика и фиксированные размеры. Responsive-компонент, помещённый в скрытую вкладку с нулевой шириной, может построить холст 0×0; перед печатью нужную вкладку делают видимой и вызывают resize.
Тайм-ауты и зависшие действия
Тайм-аут — симптом, а не диагноз. Нужно записать, какой именно шаг не завершился: навигация, локатор, ожидание ответа, шрифт или page.pdf(). Для каждого этапа устанавливают реалистичный предел и добавляют контекст в сообщение. Один глобальный тайм-аут на несколько минут затрудняет поиск причины.
При сбое сохраняют состояние страницы и сетевые ошибки. Повторяют только операции, которые безопасны и идемпотентны. Если нажатие Сформировать создаёт документ на стороне сервера, автоматический повтор может породить дубликат; сначала проверяют статус предыдущей операции.
Ошибка отсутствующего исполняемого файла браузера
Сообщение о missing executable означает, что библиотека установлена, но подходящий браузерный бинарник не найден в ожидаемом кэше. В среде сборки выполняют установку браузеров и необходимых системных зависимостей, а полученный образ используют без изменения в рабочем запуске. Кэш нельзя случайно удалить между сборкой и выполнением.
Путь к произвольному системному Chromium можно указать вручную, но совместимость тогда отвечает команда проекта. Предпочтительнее использовать браузер, рассчитанный на установленный пакет Playwright. После обновления зависимостей образ пересобирают вместе с браузерами, а не копируют старый кэш.
Различия между локальной машиной и контейнером
На локальном компьютере могут быть шрифты, сертификаты и системные библиотеки, отсутствующие в контейнере. Поэтому финальную визуальную проверку выполняют в той же среде, где работает генерация. Скриншот, созданный разработчиком, не доказывает, что серверный PDF будет таким же.
Контейнеру задают достаточный размер shared memory и ограничения ресурсов. Падения на больших страницах часто связаны не с HTML, а с нехваткой памяти. Одновременно проверяют локаль, часовой пояс и доступ к внутренним сертификатам. Все эти параметры влияют на содержимое до вызова page.pdf().
Создание счетов и актов
Для счёта данные сначала валидируют и рассчитывают вне браузера: номера строк, налоги, скидки и итог не должны зависеть от округления в DOM. HTML отвечает за представление. Шаблон содержит реквизиты, таблицу позиций, итоговый блок и условия оплаты; page.pdf() печатает его с фиксированными полями и фоном.
Длинное название позиции должно переноситься, а строка с итогом — не отрываться от подписи. Для подписи и печати оставляют физическое место в миллиметрах. Если документ занимает две страницы, реквизиты контрагента обычно остаются на первой, заголовок таблицы повторяется, а итоговый блок не разрывается.
Контрольный тест включает нулевую скидку, максимальное число знаков после запятой, очень длинное название, одну и сотни позиций. Именно крайние наборы обнаруживают переполнение, которое не видно на обычном примере.
Отчёты и аналитические дашборды
Для аналитики сценарий воспроизводит фильтры, ждёт запросы и фиксирует период. Печатный вид убирает элементы управления, оставляя выбранные значения в заголовке. Графики получают статическую легенду, а интерактивные подсказки заменяются видимыми подписями или таблицей данных.
Широкий дашборд лучше перестроить под A3 landscape либо разбить на тематические страницы. Попытка уменьшить весь экран до A4 делает подписи нечитаемыми. Печатный шаблон может использовать те же компоненты, но задавать другой grid и порядок блоков через @media print.
Для воспроизводимости в документ выводят время среза данных и часовой пояс. Сценарий ждёт, пока все виджеты сообщат одинаковый идентификатор обновления; иначе один график может отражать предыдущий период, а таблица — новый.
Архивные копии веб-страниц
Playwright PDF может фиксировать страницу после принятия cookie, раскрытия секций и загрузки отложенных изображений. Перед архивированием убирают плавающие панели, анимацию и персональные рекомендации, если они не относятся к предмету сохранения. PDF передаёт визуальное состояние, но не сохраняет полноценную интерактивность сайта.
Для доказуемой архивной процедуры рядом хранят время, параметры контекста, контрольную сумму PDF и идентификатор сценария. Если важна возможность воспроизвести страницу, одного PDF недостаточно: потребуются данные и ресурсы, из которых она была построена. Документ удобен как читаемая фиксация, а не как точная копия веб-приложения.
Печать нескольких языковых версий
Контексту задают locale, а приложению передают язык явно. После загрузки проверяют, что заголовок и формат даты соответствуют ожидаемой локали. Шрифт должен покрывать все используемые алфавиты. Один универсальный fallback часто меняет метрики строк между языками и сдвигает разрывы.
Для языков с направлением справа налево проверяют dir, выравнивание таблиц, порядок колонок и нумерацию. Перевод способен увеличить подпись в два-три раза, поэтому кнопки, метки и колонтитулы не должны иметь жёсткую ширину. Визуальные эталоны создают хотя бы для самых длинных и самых отличающихся локалей.
Интеграция с HTTP-сервисом
Веб-обработчик не должен держать пользовательское соединение открытым дольше допустимого времени. Короткий документ можно вернуть сразу, а тяжёлый отчёт лучше поставить в очередь и выдать идентификатор задачи. Рабочий процесс получает данные, генерирует PDF, проверяет его и меняет статус. Пользователь скачивает уже готовый объект.
Входные параметры проверяют до запуска браузера. Ограничивают диапазон дат, число строк и допустимые шаблоны. Ошибка внутри Playwright преобразуется в понятный статус, но внутренний стек и пути файлов не передают наружу. Журнал связывает request id, job id и идентификатор документа без записи секретов.
Тестирование печатного шаблона
Модульный тест проверяет расчёты и HTML-экранирование, браузерный — состояние страницы и PDF. Для каждого шаблона создают фиксированные наборы данных: минимальный, типовой, длинный и содержащий специальные символы. Сценарий формирует документы в контролируемой локали и сравнивает ключевые признаки.
Визуальная регрессия строится на рендеринге страниц PDF в изображения. Порог учитывает небольшие различия сглаживания, но не должен пропускать смещение колонок. Изменение эталона принимают только после просмотра, а не автоматически вслед за обновлением зависимости.
Отдельно тестируют печатный CSS через эмуляцию media: 'print' и screenshot. Такой снимок быстрее генерировать, чем PDF, и он хорошо показывает скрытые блоки и overflow. Финальные проверки всё равно выполняют на PDF, потому что пагинация и колонтитулы проявляются только при печати.
Разделение экранного и печатного представления
Один универсальный DOM подходит, когда экран и бумага отличаются главным образом видимостью панелей и расположением колонок. В более сложном кабинете лучше создать отдельный маршрут экспорта или компонент печатного представления. Он получает те же проверенные данные, но не содержит виртуального скролла, всплывающих подсказок, бесконечной ленты и элементов управления. Playwright открывает этот маршрут после авторизации и печатает его тем же методом page.pdf().
Отдельное представление не должно дублировать расчёты. Итоги, налоги, права доступа и отбор строк остаются в общей бизнес-логике, а печатный компонент отвечает только за разметку. Тогда экран и PDF не расходятся по значениям, а изменения дизайна документа не затрагивают интерактивный интерфейс. В тестах один набор данных подаётся в оба представления и сравниваются ключевые суммы.
Для предварительного просмотра можно открыть тот же печатный маршрут во вкладке, эмулировать media print и показать пользователю результат до генерации. Такой просмотр помогает заметить длинное название или лишнюю страницу, но финальный файл всё равно проверяют отдельно: браузерная область просмотра не воспроизводит пагинацию, встроенные колонтитулы и окончательное распределение листов полностью.
Журналирование и измерение качества генерации
Для каждой задачи полезно записывать время навигации, ожидания данных, загрузки шрифтов и самого page.pdf(), а также размер и число страниц результата. Резкий рост одного этапа показывает проблему раньше жалобы пользователя. Например, увеличение времени ожидания данных указывает на API, а рост длительности печати при том же объёме строк — на тяжёлую графику или усложнившийся DOM.
Журнал не должен содержать HTML документа, токены или персональные реквизиты без необходимости. Достаточно идентификатора шаблона, обезличенного номера задачи, набора включённых опций и кода результата. Диагностический HTML и трассу сохраняют только при сбое с ограниченным сроком хранения. Контрольная сумма готового файла помогает подтвердить, что пользователю выдан именно проверенный объект.
Метрики качества дополняют технические показатели. Можно считать долю повторных попыток, число документов с отсутствующим шрифтом, ошибки по шаблонам и превышение допустимого числа страниц. Эти сигналы позволяют найти системный дефект: например, один отчёт постоянно требует повторной загрузки или конкретная локаль чаще выходит за границы таблицы.
Сравнение Playwright PDF с аналогами
| Программа | Лучше подходит для | Главное ограничение |
|---|---|---|
| Playwright PDF | Авторизованные веб-приложения, динамические отчёты и автоматизация действий перед печатью | Экспорт PDF выполняется через Chromium и требует программного сценария |
| Puppeteer | Node.js-проекты, где нужна прямая автоматизация Chrome или Chromium и печать через Page.pdf | Сценарии и инфраструктура ориентированы на собственный API Puppeteer |
| WeasyPrint | Печатные HTML/CSS-шаблоны, документы с развитой пагинацией и серверная генерация без управления веб-интерфейсом | Не воспроизводит полноценное поведение интерактивного браузерного приложения |
| wkhtmltopdf | Простые командные конвейеры преобразования стабильных HTML-страниц | Старый движок Qt WebKit хуже совместим с современной вёрсткой |
| Prince | Сложная типографика, paged media и профессионально оформленные публикации из HTML и CSS | Не предназначен для пошагового управления интерактивным сайтом |
| PDF Commander | Ручное редактирование, перестановка страниц, добавление текста и работа с уже созданными PDF | Не автоматизирует вход и рендер динамического веб-интерфейса кодом |
Playwright PDF выбирают, когда документ зависит от действий в настоящем веб-интерфейсе: входа, фильтров, клиентского JavaScript и готовности динамических компонентов. Puppeteer решает близкую задачу в экосистеме своего API. WeasyPrint и Prince удобнее, когда HTML изначально проектируется как печатная публикация и не требует управления приложением. wkhtmltopdf остаётся вариантом для простых устоявшихся страниц, совместимых с его движком. PDF Commander нужен на другом этапе — когда готовый файл требуется открыть и вручную исправить, объединить или дополнить.
Ограничения, которые нужно учитывать заранее
PDF создаётся только методом Chromium; запуск того же сценария в Firefox или WebKit не даёт эквивалентного page.pdf(). Поэтому визуальный результат следует тестировать именно в том браузере, который выполняет экспорт. Кроссбраузерные проверки Playwright полезны для сайта, но не заменяют проверку печати Chromium.
Вызов не предоставляет визуальный конструктор страниц и не редактирует существующий PDF. Макет формируется HTML и CSS, а изменения в готовом файле требуют другого инструмента. Колонтитулы имеют отдельный ограниченный контекст, скрипты в их шаблонах не выполняются, стили основного документа туда не переходят.
Движок печати хорошо подходит для деловых документов и экранных отчётов, но не предоставляет полный набор функций издательской системы: управление цветовыми профилями, спусками полос, интерактивными PDF-формами и произвольными объектами готового файла находится за пределами page.pdf(). Эти требования нужно определить до выбора конвейера.
Практический чек-лист перед запуском
- Создайте отдельное печатное представление или проверенный набор @media print.
- Зафиксируйте формат, ориентацию, поля и правило приоритета @page.
- Добавьте явный признак готовности данных и графики.
- Дождитесь document.fonts.ready и загрузки критических изображений.
- Отключите анимации, мигающие элементы и ненужные панели.
- Проверьте длинные строки, многостраничные таблицы и крайние наборы данных.
- Не храните пароль и storage state рядом с публичными файлами.
- Ограничьте сетевой доступ для недоверенных шаблонов.
- Закрывайте page, context и browser в гарантированных ветках очистки.
- Проверяйте сигнатуру, размер, ключевой текст и визуальный рендер PDF.
После настройки одного стабильного шаблона те же принципы переносятся на остальные документы: явная готовность, контролируемая среда, печатный CSS, ограниченная параллельность и проверка результата. Надёжность достигается не дополнительной задержкой перед page.pdf(), а тем, что каждый асинхронный компонент сообщает о завершении, а сценарий умеет отличить готовый отчёт от формы входа, ошибки API и незагруженного ресурса.
Итоговый рабочий процесс
Сначала данные и права доступа проверяются вне браузера. Затем новый изолированный контекст открывает страницу или получает готовый HTML, выполняет необходимые действия и ждёт конкретный признак завершения. После загрузки шрифтов, изображений и графиков сценарий включает требуемый media-режим, добавляет печатные стили и вызывает page.pdf() с форматом, полями, фоном и колонтитулами.
Полученный Buffer проверяется, сохраняется или отправляется дальше только после минимальной валидации. При ошибке создаются диагностические вложения, контекст закрывается, а повтор выполняется по понятному правилу. Такой конвейер позволяет выпускать одинаково оформленные счета, акты, отчёты и архивные копии без ручной печати, сохраняя контроль над данными, пагинацией и моментом фиксации страницы.