Hit both today doing a live QA pass on the gateway: plain nixpkgs#chromium has no color-emoji font (tofu boxes read exactly like a missing-icon bug), and a page whose content loads async after the initial paint (SSE, fetch-on-mount) screenshots as a bare "loading..." placeholder without --virtual-time-budget. Neither is a real product bug, but both need a documented fix for when an accurate render actually matters, not just "know to discount it."
104 lines
5.4 KiB
Markdown
104 lines
5.4 KiB
Markdown
---
|
|
name: headless-screenshot
|
|
description: Take a real screenshot of an HTML/CSS page from inside a container with no browser installed - `nix shell nixpkgs#chromium --command chromium --headless=new --disable-gpu --no-sandbox --screenshot=<out.png> --window-size=<w>,<h> "file://<path-or-url>"` renders the page and writes a PNG in one command, no puppeteer/playwright or pre-installed browser needed (only needed for scripted interaction like clicking, not a static render). Use this whenever you're about to claim a CSS/layout/frontend change looks right, whenever a PR reviewer or the operator asks to see a screenshot of a web UI change, or before asking a peer GUI-testing agent to screenshot something you could just render yourself - reasoning about a visual change from source or compiled CSS alone is not the same as actually seeing it rendered, and a real browser is one shell command away even when `which chromium` comes back empty.
|
|
---
|
|
|
|
# Headless Screenshot
|
|
|
|
"No browser in this container" usually means no browser is
|
|
*pre-installed*, not that one is unreachable. `nixpkgs#chromium` is a
|
|
`nix shell` away, and a single headless invocation renders a page and
|
|
writes a PNG without a running server, a display, or any scripted
|
|
interaction library.
|
|
|
|
## The command
|
|
|
|
```
|
|
nix shell nixpkgs#chromium --command chromium \
|
|
--headless=new --disable-gpu --no-sandbox \
|
|
--screenshot=/path/to/out.png \
|
|
--window-size=1200,900 \
|
|
"file:///path/to/page.html"
|
|
```
|
|
|
|
- `--no-sandbox` is required inside most agent containers (no user
|
|
namespaces for Chromium's sandbox); `--disable-gpu` sidesteps GPU
|
|
init failures in a headless environment. Both are safe defaults here
|
|
- this isn't a browser you're exposing to untrusted input.
|
|
- The target can be a local `file://` path or a real `http(s)://` URL
|
|
(a dev server, a served `dist/`, a deployed page) - same command
|
|
either way.
|
|
- `--window-size` sets the viewport; match it to what you actually want
|
|
to see (a narrow width to check a mobile layout, a wide one for a
|
|
dashboard row).
|
|
- Swap `--screenshot=` for `--dump-dom=` to get the rendered DOM as
|
|
text instead of an image - useful for confirming markup/attributes
|
|
landed correctly without needing to eyeball pixels.
|
|
|
|
Stray `dbus`/`UPower` errors on stderr are harmless container noise
|
|
(no session bus, no power daemon) - ignore them as long as the PNG
|
|
file actually gets written.
|
|
|
|
## When source-reading isn't enough
|
|
|
|
Grepping compiled CSS or tracing class names tells you the rules
|
|
exist and target the right selectors - it doesn't tell you what the
|
|
result actually looks like (wrong tier picked, two rules fighting,
|
|
a value that's technically correct but visually off). Before telling
|
|
someone a layout/CSS change "looks right," or before reaching for a
|
|
peer agent's GUI-testing capability, try rendering it yourself first:
|
|
|
|
- **Testing a real page**: serve the built `dist/` (or point at
|
|
wherever it's already deployed) and screenshot that - it's the
|
|
actual production bundle, not a reconstruction.
|
|
- **Testing an isolated CSS change** (e.g. a shared stylesheet used
|
|
by several pages): a small self-contained HTML file with the real
|
|
CSS inlined and a few representative markup samples is faster to
|
|
build than standing up a server, and just as honest about what the
|
|
CSS actually renders.
|
|
- **Attach it**: post the PNG as a PR/issue comment (or file
|
|
attachment) alongside a one-line note on what it's rendered from -
|
|
a reviewer with no browser either benefits from the same shortcut.
|
|
|
|
## What this doesn't replace
|
|
|
|
This is a static render, not a browser automation tool. Verifying an
|
|
interaction (a click, a hover state, a form submission) needs
|
|
puppeteer/playwright or similar - reach for those only when the
|
|
question is genuinely about *behavior*, not appearance. A missing
|
|
emoji/glyph rendering as a box in the screenshot is usually a font
|
|
availability artifact of the headless environment, not a real bug -
|
|
don't mistake one for the other when reporting results.
|
|
|
|
## Two false-positive traps (and their fixes)
|
|
|
|
Both of these look exactly like a real product bug in a naive
|
|
screenshot and aren't - check for them before filing anything, and
|
|
use the fix below if you actually need an accurate render (e.g. to
|
|
judge icon/glyph layout, not just to note "there'd be an icon here").
|
|
|
|
- **Tofu boxes instead of emoji.** Plain `nixpkgs#chromium` has no
|
|
color-emoji font, so any 🖥/📡/🔨-style glyph renders as an empty
|
|
box. Add `nixpkgs#noto-fonts-color-emoji` to the `nix shell` - but a
|
|
`FONTCONFIG_FILE` that lists *only* the emoji dir throws away the
|
|
base fonts and blanks all normal text instead, which is its own
|
|
false alarm. Merge, don't override:
|
|
```
|
|
cat > fonts.conf <<EOF
|
|
<?xml version="1.0"?>
|
|
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
|
<fontconfig>
|
|
<include ignore_missing="yes">/etc/fonts/fonts.conf</include>
|
|
<dir>$(nix eval --raw nixpkgs#noto-fonts-color-emoji)/share/fonts</dir>
|
|
</fontconfig>
|
|
EOF
|
|
FONTCONFIG_FILE=$PWD/fonts.conf nix shell nixpkgs#chromium nixpkgs#noto-fonts-color-emoji \
|
|
--command chromium --headless=new --disable-gpu --no-sandbox \
|
|
--screenshot=out.png --window-size=1440,900 "https://example/"
|
|
```
|
|
- **A bare "loading..." placeholder instead of real content.** A page
|
|
whose content arrives after the initial paint (SSE, a `fetch` on
|
|
mount) can get captured before that data lands - the default
|
|
`--screenshot` doesn't wait for it. Add
|
|
`--virtual-time-budget=8000` (milliseconds; tune to the page) to let
|
|
it actually settle first.
|