print-stylesheet-tests
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.
Install with skills.sh (any agent)
npx skills add testland/qa --skill print-stylesheet-testsprint-stylesheet-tests
Per MDN Paged Media (opens in new window), CSS Paged Media defines @page rules and break-control properties for print output. Per the Playwright page.pdf docs (opens in new window), page.pdf() "generates PDFs using print CSS media by default" - call emulateMedia first if you want screen styles applied to PDF instead.
When to use
How to use
Step 1 - Test @media print selectors activate
import { 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 (opens in new window).
Step 2 - Test print-only content appears
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();
});Step 3 - Generate PDF + verify @page size honored
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 (opens in new window): preferCSSPageSize: true lets CSS @page { size: A4 } win over the API format option. Without it, API wins.
Step 4 - Test page count
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.
Step 5 - Test break-before / break-after / break-inside
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');
});Step 6 - printBackground for branded headers
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 (opens in new window): printBackground defaults false.
Advanced @page geometry
@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.
Worked example
Customers report a redesigned invoice prints with the totals table split across two pages, though it looks fine on screen.
Anti-patterns
| 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) |
Limitations
References
@page pseudo-classes and margins
View source (opens in new window)@page pseudo-classes and margins
@page :first / :left / :right testing
Per MDN Paged Media (opens in new window), pseudo-class selectors target specific pages:
@page :first {
margin-top: 5cm;
background: url(letterhead.png);
}
@page :left { margin-left: 3cm; margin-right: 2cm; }
@page :right { margin-left: 2cm; margin-right: 3cm; }test('first page has letterhead margin', async ({ page }) => {
await page.goto('https://localhost:3000/contract/c001');
const pdf = await page.pdf({ format: 'A4', preferCSSPageSize: true });
// Render page 1 to image, look for letterhead at top
const page1 = await renderPdfPage(pdf, 1);
expect(await hasLetterhead(page1)).toBe(true);
});Pair with pdf-snapshot-tester for the rendered-page assertion.
Margin verification
test('PDF generated with 2cm margins', async ({ page }) => {
await page.goto('https://localhost:3000/letter');
const pdf = await page.pdf({
format: 'A4',
margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' },
});
const margins = await getPdfMargins(pdf);
// Allow ±2mm rendering tolerance
expect(margins.top).toBeCloseTo(20, 0);
});Note: the Playwright margin API option overrides CSS @page margin unless preferCSSPageSize: true.
Related skills
html-to-pdf-regression
Cross-engine HTML→PDF regression - generate the same source HTML through Chromium `page.pdf()`, WeasyPrint (Python, BSD), and wkhtmltopdf, then compare per-engine outputs page-by-page. Catches engine-specific font embedding bugs, page-break drift, @page rule support gaps. Pair with pdf-snapshot-tester for the per-engine pixel diff. Use when a project is swapping or upgrading its PDF engine, or when one shared HTML template is rendered by more than one engine and the outputs must stay equivalent.
pdf-accessibility-checker
Test PDF accessibility (PDF/UA conformance) - tagged-PDF structure (StructTreeRoot), alternative text on images (Alt), reading-order, language metadata (Lang), document title, heading hierarchy. Use veraPDF / PAC (PDF Accessibility Checker) / pdfix / Adobe Acrobat Pro headless; map each finding back to WCAG 2.1 PDF Techniques (PDF1 - PDF23). Use when a product ships customer-facing PDFs into a context that mandates PDF/UA - US Section 508, EU Directive 2016/2102, or a public-sector tender - and each file must be proven tagged before release.
pdf-snapshot-tester
Test PDF outputs by converting per-page to images (`pdftocairo` / pdf2image / Poppler) and running pixel-diff (pixelmatch / Resemble.js / Pillow `ImageChops`) against approved baselines. Per-page-range targeting, threshold tuning, font-substitution warnings, byte-stable PDF metadata stripping (CreationDate, /ID). Use when a product generates invoices, contracts, or regulatory filings whose layout must not shift, and a PDF template, font pack, or generation library is about to change.