Data Nexus

Infrastructure · We run it ourselves

Playwright

A Node library that launches and drives Chromium, Firefox and WebKit through one API — written for end-to-end tests, equally usable as a scriptable browser.

The interesting use of Playwright in this practice is not testing. It is the only dependency in the site repository that ships an entire rendering engine, and that engine is used three ways: as the layout engine for printed documents, as a rasteriser at build time, and as a measurement instrument pointed at other people's websites. The test runner is a separate package and it is not installed.

playwright.dev

01/Why this one

  1. 01

    Playwright ships two things under similar names and this repository installs only one of them — `playwright`, the library that launches and drives a browser, rather than `@playwright/test`, the runner with fixtures, retries, sharding and a trace viewer. The distinction decides what the dependency costs over a year. A driven browser is a function call that either produces a file or throws; a test suite is a standing obligation against markup that changes. Seven scripts here import `chromium` from the library, and not one of them asserts anything about the site's own DOM.


  2. 02

    Printing through `page.pdf()` makes CSS the layout language for a tax invoice — `@page` margins, the print stylesheet, break rules — resolved by the same engine that renders the HTML twin of that same document. The alternatives trade differently: pdfkit and ReportLab want boxes placed at coordinates, which is precise at first and unmaintainable by the third revision, and wkhtmltopdf is frozen on a WebKit that predates modern layout. Here one renderer emits a string, the script writes the .html and the .pdf from it, and changing the design of an invoice is changing a stylesheet.


  3. 03

    Determinism has to be earned rather than assumed. `page.pdf()` prints the moment the load event fires — before the webfont is parsed and before images decode — and it fails silently: the document ships with a fallback face and nobody notices until the counterparty opens it. The fix is to wait on conditions rather than on a clock: `await document.fonts.ready`, then `img.decode()` on every image on the page. A two-second `waitForTimeout` in the same place is a bet on the slowest machine that will ever run the script, and it is a bet that gets lost in CI.


  4. 04

    Print is a separate rendering mode rather than a screen render with different margins, and Playwright exposes the switch through `emulateMedia({ media: "print" })`. That makes a build-time assertion on a document possible. After printing an invoice the script sets the viewport to the printable width in CSS pixels — 210 mm less two 15 mm margins, at 96/25.4 px per mm — re-measures the body box under print media, and reports when the document has outgrown its single sheet. The same measurement under the screen stylesheet answers a question nobody asked, because the screen layout uses a different width.


  5. 05

    Several properties of a web page exist only while it is running. A countdown that restarts for every visitor is indistinguishable in the HTML source from a genuine deadline — the difference is what the script does in a fresh browsing context — so the dark-patterns census loads each origin twice in separate contexts and compares the two deadlines. An HTTP client cannot see that, and cannot see a consent overlay inserted after paint or a stock counter generated at random either. A real browser is not a convenience in that work; it is the instrument.


  6. 06

    The same dependency covers the small rendering chores a site accumulates — the open-graph image for an article, and `favicon.ico`, which is drawn from the SVG at 16, 32, 48, 64, 128 and 256 px, each size rendered at its own resolution through a canvas instead of downsampled from one raster. Hairlines that read at 256 do not survive being scaled to 16, and a canvas with an alpha channel always exports RGBA, where Chrome's screenshot of a fully opaque page emits RGB and Turbopack then refuses the ICO. Rasterising artwork in the engine the reader will view it in removes a class of disagreement between build output and browser.


  7. 07

    The browser also reads the artefact back. A DOM-based check of how full a printed page is measures blocks, not where Chrome decided to break the page, and page breaks are the only place print layout genuinely fails. So the inspection script serves pdf.js and the finished document from a loopback port, draws every page into a canvas and reports ink coverage per page — below roughly 78 per cent is a page worth opening. The document is produced and audited by one engine, which is a closed loop rather than two opinions about the same file.

02/What you can check

An entry that cannot point at something you can open, run or read does not compile. That is a property of the type, not a promise in a paragraph.

Case record
Eighty-five pages of brand documentation rendered from HTML to PDF as a build step, so correcting a value in the source corrects the book.Read the record

In our code
scripts/build-invoice.mts — Renders an invoice to HTML, awaits document.fonts.ready and img.decode() on every image, prints A4 with printBackground and 14/15/9/15 mm margins, then re-measures the body box under emulateMedia("print") at the printable width and warns when the document no longer fits one sheet.

In our code
scripts/audit-dark-patterns.mjs — Drives Chromium over the .ae sample at concurrency 4 with a 1,200 ms pause between requests, checks robots.txt before each origin, loads every domain twice in fresh contexts to detect countdowns that restart per visitor, follows one internal page deeper because scarcity timers do not live on home pages, and appends observations as JSONL so an interrupted multi-hour run resumes instead of restarting.

Run it yourself
node scripts/audit-dark-patterns.mjs selftest → Three fixture pages served over loopback and measured by the same detector as the census: genuine.html false, reset.html true, decoys.html null. Exit code 1 if the countdown detector disagrees with any of them.

On this site
621 rows across 356 measured .ae domains — the published output of that browser pass, downloadable as CSV or JSON.Open it

In our code
scripts/inspect-pdf.mjs — Serves pdf.js and the built PDF from a random loopback port, rasterises each page into a canvas and prints per-page ink coverage with the running footer excluded from the count, because a DOM-based fill check cannot see where Chrome broke the page.

In our code
scripts/build-favicon.mjs — Renders app/icon.svg at six icon sizes, each from the vector at its own resolution through a canvas rather than downsampled, because Turbopack rejects an ICO whose PNGs are not RGBA and a screenshot of an opaque page emits RGB.

Property of the thing
page.pdf() is implemented in Chromium only; the same API on the firefox and webkit drivers throws rather than producing a document, so Playwright's cross-engine promise does not extend to the print pipeline. — Launch firefox instead of chromium in any of the document scripts above and call page.pdf() — the call throws immediately; Playwright's own PDF documentation states the Chromium restriction.

Property of the thing
The `playwright` package is a driver plus browser binaries, not a test harness — fixtures, retries and the runner live in the separate `@playwright/test` package, so installing the library carries no suite to maintain. — Read package.json in this repository: playwright ^1.62.0 under devDependencies, no @playwright/test, no playwright.config, and no spec file anywhere in the tree.
03/Where it stops

Invariant

Every use above needs a real browser binary on the machine — roughly 150 MB per engine after `playwright install`, plus a few hundred megabytes of resident memory per context — so none of it runs inside a Vercel function. Document rendering here is a local command or a build step, never a request handler; putting it behind an HTTP endpoint means paying for a container that keeps a browser warm, and that container is the actual cost of "generate the PDF on demand". The print path is narrower still, because page.pdf() exists only in Chromium. And the use the tool is named for is the one this practice declines: an end-to-end suite is priced in maintenance rather than in the number of tests, every selector is a bet on markup that the next redesign invalidates, and a suite that goes intermittently amber gets muted instead of fixed — at which point it costs money and asserts nothing. This repository therefore has no playwright.config and no spec file; the gates that fail the build are Node scripts that assert on structured data and on route agreement, which do not flake.