Screenshot Automation¶
Every dashboard screenshot in these docs is generated deterministically by a Playwright script that drives a Flask subprocess wired to a fixture cache. There is no real Last.fm or YouTube Music traffic, no real credentials, and no real clock re-running the script produces byte-identical PNGs.
The whole pipeline is additive: no application code is modified, and the
generator lives entirely under tests/screenshots/.
Regenerate everything¶
From the project root, with the venv created:
That wrapper:
- Confirms
.venv/bin/pythonexists (create one withpip install -e '.[web,dev,web-docs]'if missing). - Downloads the Chromium browser (
playwright install chromium) - a one-time step after installing theweb-docsextra. - Runs
tests/screenshots/generate.py --out docs/screenshots.
Pass any generator flag through, for example:
./scripts/regen-screenshots.sh --only settings_modal
./scripts/regen-screenshots.sh --headed --keep-server
Generator flags¶
| Flag | Default | Description |
|---|---|---|
--out DIR |
docs/screenshots |
Output directory |
--only NAME |
(all) | Capture a single target; repeatable |
--port N |
(random free) | Pin the demo Flask port |
--headed |
off | Run Chromium headed (debugging) |
--keep-server |
off | Leave the demo server running after captures |
What gets captured¶
| Name | File | UI state |
|---|---|---|
dashboard |
dashboard.png |
Default Playlist tab with fixture tracks and stats |
notfound |
notfound.png |
"Not Found" tab populated from null-video_id cache rows |
overrides |
overrides.png |
Overrides + blacklist tab |
custom_playlists |
custom_playlists.png |
Custom playlists tab |
playlists |
playlists.png |
Playlists tab (discover/track/prune autogenerated playlists) |
history |
history.png |
History tab with seeded stats / charts |
settings_modal |
settings_modal.png |
Settings modal (demo credentials only) |
setup_wizard |
setup_wizard.png |
First-time setup wizard |
teleporter |
teleporter.png |
Teleporter export/import modal |
sync_console |
sync_console.png |
Sync drawer with a realistic canned run log |
How it works¶
- Fixture cache
tests/screenshots/fixtures/holds hand-curatedcache/*.json,config/*.json, and a freshly seeded SQLite history DB (regenerated each run via the realHistoryDBschema). - Subprocess server
generate.pyspawnstests/screenshots/_serve.pywith everyCACHE_*_FILE,TAG_*_FILE,CUSTOM_PLAYLISTS_FILE, andHISTORY_DB_FILEenv var pointed at the fixtures. These per-file overrides are required because the project.envmay set them with relative paths that would otherwise shadowCACHE_DIR/CONFIG_DIR. - Route stubs Playwright intercepts the volatile JSON endpoints
(
/api/now-playing,/api/scheduler/status,/api/update-status,/api/failure_log,/api/auth/status,/api/auth/validate,/api/setup/status,/api/settings,/api/playlists/tracked,/api/playlists/discover) and serves canned payloads. This is what keeps the settings modal from ever rendering real credentials, even on a developer machine with a live.env, and lets the Playlists tab render a stable library scan without touching YouTube Music. - Frozen clock an init script monkey-patches
globalThis.DateandDate.now()to a fixed instant (2026-05-28T10:00:00Z- an arbitrary reference chosen so it sits shortly after the fixture timestamps; the exact value carries no significance), so relative time labels ("45 minutes ago") don't drift between regenerations. - UI driving tabs and modals are opened by calling the dashboard's
own
window.*entry points (switchTab,showSettingsModal,openSyncDrawer,showSetupWizard,showTeleporterModal) instead of clicking elements. That keeps captures resilient to layout changes.
Adding a new capture¶
- If it's a tab, add
(name, tab_id)to_TAB_CAPTURESingenerate.py. - If it's a modal, add
(name, js_open_call, visible_selector)to_MODAL_CAPTURES. - For anything bespoke (interactive flows, scrolling, fake data injection),
write a dedicated
capture_<name>(context, base_url, out)function and add it to theCAPTURESdict at the bottom. - If the new view needs data not present in
tests/screenshots/fixtures/, extend the existing JSON / SQL seed don't add new files unless necessary. - If the view talks to a volatile endpoint, add a stub to
ROUTE_STUBS. Letting the real handler run is the most common cause of "this screenshot keeps changing".
Troubleshooting¶
libatk-1.0.so.0: cannot open shared object file(Linux only) runsudo .venv/bin/python -m playwright install-deps chromium. The wrapper script only downloads the Chromium binary; the system shared libraries it depends on must be installed separately (and that step needs sudo).- Stats / counts don't match the fixture the real
.envis leaking through. Confirm withgrep CACHE_ .env; everyCACHE_*_FILEsetting must be overridden instart_server()'s env block. - Settings modal shows your real username / API key the
/api/settings,/api/auth/status, or/api/setup/statusstubs are missing or malformed; seeROUTE_STUBSingenerate.py. - Server "didn't come up" bump the
start_server()timeout, or run with--keep-server --headedto poke at it manually in a browser.