Skip to content

An Automatic PDF Manual from Playwright Captures

Adityo Guni Waluyo

One manifest from the Playwright capture tours generates the user manual: an HTML page per tour group, an index, and an A4 PDF with an automatic size guard.

TL;DR

Playwright test videos only save after the context closes, so ffmpeg conversion must move to afterAll. A single manifest then drives three HTML outputs and an A4 PDF via Chromium. Cleanup needs trash before permanent delete, and a node:assert size guard flags empty manuals.

The KotaPortal handover process requires one user-manual file for the operations team. In the repository, the Playwright end-to-end suite already walks almost every screen: four capture specs, 24 tour groups (6 + 7 + 8 + 3), all read-only. The remaining work looks small: turn those tours into documentation.

The tempting first guess: take the video recording from the test run and extract its frames with ffmpeg. Or, without video, paste screenshots one by one into the document. Both feel exhausting, but video looks like the fastest path.

That guess fails at an unexpected point. The test.use({ video: 'on' }) configuration is active with a 1280x720 viewport, and ffmpeg is called inside the test body to turn video.webm into images. The result is an ENOENT error mid-test: the video file does not exist yet. According to the Playwright video documentation, recordings are saved only after the browser context closes, so the video becomes available only after the page or browser context is closed [1]. The assumption that conversion can happen inside the test body simply collapses.

One manifest, three HTML outputs

The decision: separate the staging and finalization stages. The test body only collects tour artifacts; finalizeCaptures runs ffmpeg and writes manifest.json in afterAll, after every context has closed and the videos are final.

The generator generate_manual.mjs reads that single manifest.json and nothing else. For each tour group it emits one HTML page, then an index, and the printable variant print.html. One manifest means the manual's chapter order is always identical to the tour order of the test run, with no second list to keep in sync.

The final part of generate_manual.mjs calls page.pdf({ format: 'A4' }) against the print file print.html. The Playwright documentation states that page.pdf() produces a PDF with print css media, and colors need the -webkit-print-color-adjust property for the printout to use exact colors [2]. The A4 format (8.27in x 11.7in) is available as a built-in option [2]. Chromium is chosen as the generator browser, an implementation decision in this project.

The following example is a generic reconstruction of the pattern:

// capture_tour.spec.js (generic reconstruction)
import { test } from '@playwright/test';

test.use({ video: 'on', viewport: { width: 1280, height: 720 } });

test('tur dashboard admin', async ({ page }, testInfo) => {
  await page.goto('https://example.com/admin/dashboard');
  // read-only tour steps: open menu, fill filter, read table
  await stageCapture(testInfo, 'dashboard');
});

test.afterAll(async () => {
  await finalizeCaptures(); // ffmpeg + write manifest.json
});
// generate_manual.mjs (generic reconstruction)
const manifest = JSON.parse(await readFile('manifest.json', 'utf8'));
// per tour group: page.html, index.html, print.html
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(`file://${printPath}`);
const pdf = await page.pdf({ path: 'manual.pdf', format: 'A4' });
assert.ok(pdf.length >= 100_000, `PDF terlalu kecil: ${pdf.length} byte`);

Finalization uncovers a second problem

Moving ffmpeg into afterAll opens up one more finding. The tours are read-only, but the test data they use has to be cleaned up after the run. Permanent deletion of test rows turns out to silently do nothing while those rows have not been moved out of the trash. The correct cleanup order is trash first, then permanent delete. This behavior comes from the project's internal API and is unrelated to Playwright's behavior.

The detail is small, but it decides whether the next run uses clean data or leftovers. The same pattern applies to artifact staging: anything that depends on a context's final state must wait for that context to close.

The wrapper and size as a guard

The whole flow runs with one command: run_manual_capture.sh calls the capture, then generate_manual.mjs, then a Node verification that inspects the output artifacts. The check is essentially assertions over artifacts, and Node.js ships a built-in module for exactly that job: node:assert provides assertion functions for verifying invariants [3].

The guard is simple: the run fails when manual.pdf is under 100,000 bytes. A PDF that small almost always means print.html is empty or the screenshots never made it into the manifest, and that failure is clearer than handing a broken file to the operations team. The 100,000-byte limit is just a rough failure threshold.

One recorded run of this entry produced 24 tour pages, all PASS, with a manual.pdf of about 12.5 MB. Those figures are the output of a single run with specific capture content and do not serve as a benchmark.

What remains after the first run is only the tour content, not the pipeline. Adding a tour group means adding one entry to manifest.json, and the three HTML files plus the PDF follow untouched.

Sources:

[1] Playwright: Videos

[2] Playwright: Page (page.pdf())

[3] Node.js: Assertions (node:assert)

Related articles