Test CSS print-media output via Playwright `page.emulateMedia({ media: 'print' })` + `page.pdf()` - `@page` rule (size, margin, orphans, widows), `@page :first / :left / :right` pseudo-classes, `break-before/after/inside`, `@media print` selector activation, page-break suppression on headings. Use when an app exposes a Print button or a print stylesheet exists but is untested, and users report the printed or PDF copy breaking in the wrong places while the on-screen page looks fine.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Per MDN Paged Media, CSS Paged Media defines @page rules and
break-control properties for print output. Per the Playwright
page.pdf docs, page.pdf() "generates PDFs using print CSS media
by default" - call emulateMedia first if you want screen styles
applied to PDF instead.
page.emulateMedia({ media: 'print' }) to activate the print stylesheet.page.pdf(), passing preferCSSPageSize: true
when CSS @page owns the page size.break-before / break-inside rules
land chapter and section boundaries correctly.printBackground: true when branded headers or color fills must
appear in the customer-facing PDF.pdf-snapshot-tester on the rendered page - see
references/page-geometry.md.@media print selectors activateimport { test, expect } from '@playwright/test';
test('navigation hidden when printing', async ({ page }) => {
await page.goto('https://localhost:3000/invoice/inv_001');
// Default media = screen → nav visible
await expect(page.locator('nav.app-nav')).toBeVisible();
// Switch to print
await page.emulateMedia({ media: 'print' });
await expect(page.locator('nav.app-nav')).toBeHidden();
});emulateMedia activates @media print rules per MDN Paged Media.
test('print-only legal footer appears under print media', async ({ page }) => {
await page.goto('https://localhost:3000/invoice/inv_001');
await expect(page.locator('.print-only-legal')).toBeHidden();
await page.emulateMedia({ media: 'print' });
await expect(page.locator('.print-only-legal')).toBeVisible();
});test('PDF respects @page size: A4', async ({ page }) => {
await page.goto('https://localhost:3000/invoice/inv_001');
const pdf = await page.pdf({
preferCSSPageSize: true,
});
// Use a PDF inspector lib or pair with pdf-snapshot-tester
const dimensions = await getPdfDimensions(pdf);
expect(dimensions.format).toBe('A4');
});Per the Playwright page.pdf docs: preferCSSPageSize: true lets
CSS @page { size: A4 } win over the API format option. Without
it, API wins.
test('invoice fits on 1 page when standard line count', async ({ page }) => {
await page.goto('https://localhost:3000/invoice/standard');
const pdf = await page.pdf({ format: 'A4' });
const pageCount = await getPdfPageCount(pdf);
expect(pageCount).toBe(1);
});
test('invoice spills to 2 pages when many line items', async ({ page }) => {
await page.goto('https://localhost:3000/invoice/long');
const pdf = await page.pdf({ format: 'A4' });
const pageCount = await getPdfPageCount(pdf);
expect(pageCount).toBe(2);
});Page count regressions ("invoice now needs 3 pages instead of 1") are the canonical print bug.
CSS:
@media print {
h1.chapter { break-before: page; }
table.totals { break-inside: avoid; }
p { orphans: 3; widows: 3; }
}Test that the chapter break shows up:
test('each chapter starts on new page', async ({ page }) => {
await page.goto('https://localhost:3000/manual');
const pdf = await page.pdf({ format: 'A4' });
const pageTexts = await extractTextPerPage(pdf);
// Chapter 1 on page 1, Chapter 2 on page 2, ...
expect(pageTexts[0]).toContain('Chapter 1');
expect(pageTexts[1]).toContain('Chapter 2');
});By default printBackground: false - backgrounds (gradients,
images) don't render. Customer-facing PDFs usually need
printBackground: true:
test('branded header background appears', async ({ page }) => {
await page.goto('https://localhost:3000/branded-letter');
const pdf = await page.pdf({
printBackground: true,
format: 'A4',
});
const page1 = await renderPdfPage(pdf, 1);
expect(await hasBrandColorAtTop(page1)).toBe(true);
});Per the Playwright page.pdf docs: printBackground defaults
false.
@page :first / :left / :right pseudo-class targeting and margin
verification (including the CSS-vs-API margin precedence rule) are
covered in references/page-geometry.md.
Customers report a redesigned invoice prints with the totals table split across two pages, though it looks fine on screen.
/invoice/long and generate the PDF with
page.pdf({ format: 'A4' }).getPdfPageCount(pdf) returns 3; the pre-redesign baseline test
asserted toBe(2), so the count assertion fails and pins the regression.extractTextPerPage(pdf) shows the table.totals rows straddling the
page 2 / page 3 boundary - the redesign dropped break-inside: avoid.@media print { table.totals { break-inside: avoid; } } to the
stylesheet.getPdfPageCount returns 2 and the totals text sits wholly on
page 2, so both the page-count and per-page-text assertions pass.| Anti-pattern | Why it fails | Fix |
|---|---|---|
Test print stylesheet only by visually inspecting page.pdf() output | Regressions slip; not automated | Pair with pdf-snapshot-tester (see references/page-geometry.md) |
Skip preferCSSPageSize when CSS owns layout | API options override; CSS @page ignored | preferCSSPageSize: true (Step 3) |
Forget printBackground: true | Branded headers/colors missing in prod PDFs | Step 6 |
| Test only Chromium-rendered PDF | WeasyPrint / wkhtmltopdf differ; cross-engine bugs slip | html-to-pdf-regression skill covers cross-engine |
| Hard-code page-break tests against pixel positions | Slight font tweaks invalidate | Test text content per page (Step 5) |
@page marks and bleeds descriptors per MDN Paged Media
have limited browser support.@page, page pseudo-classes, break properties@page
pseudo-classes and margin verificationpdf-snapshot-tester - sister
skill for pixel-diff assertions on rendered PDF pageshtml-to-pdf-regression -
cross-engine HTML→PDF comparison