fuzzyworld

fuzzyworld-tools / files / runner / shoot.py

shoot.py

Blame: the commit that last changed each line · file · history

1758548c 2026-10-27 Pavel Safronov1#!/usr/bin/env python3
2"""Screenshots of a built app, run by fw-runner.py inside a Playwright container (no network).
3
4 shoot.py <shots dir> <build output dir>
5
6<shots dir>/spec.json: {"app": "<app>", "shots": [{"file", "path", "w", "h", "wait"}, ...]}.
7The build is served at http://127.0.0.1:<port>/<app>/, the same path it has on
8apps.fuzzyworld.net, and each shot is written to <shots dir>/<file>.
9"""
10
11import functools
12import http.server
13import json
14import os
15import sys
16import threading
17
18from playwright.sync_api import sync_playwright
19
20
21class AppHandler(http.server.SimpleHTTPRequestHandler):
22 """Serves the build directory under /<app>/, and nothing else."""
23
24 def __init__(self, *args, app, **kw):
25 self.app = app
26 super().__init__(*args, **kw)
27
28 def translate_path(self, path):
29 prefix = f"/{self.app}/"
30 path = path.split("?", 1)[0].split("#", 1)[0]
31 if path == f"/{self.app}":
32 path = prefix
33 if not path.startswith(prefix):
34 return os.path.join(self.directory, ".fw-not-found")
35 return super().translate_path("/" + path[len(prefix):])
36
37 def log_message(self, *args):
38 pass
39
40
41def main(shots_dir, out_dir):
42 with open(os.path.join(shots_dir, "spec.json")) as f:
43 spec = json.load(f)
44 app = spec["app"]
45 handler = functools.partial(AppHandler, app=app, directory=out_dir)
46 server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
47 threading.Thread(target=server.serve_forever, daemon=True).start()
48 base = f"http://127.0.0.1:{server.server_address[1]}/{app}/"
49 with sync_playwright() as p:
50 browser = p.chromium.launch()
51 for s in spec["shots"]:
52 page = browser.new_page(viewport={"width": s["w"], "height": s["h"]}, device_scale_factor=1,
53 is_mobile=s["w"] < 600, has_touch=s["w"] < 600)
54 page.goto(base + s["path"].lstrip("/"), wait_until="load")
55 page.wait_for_timeout(s["wait"])
56 page.screenshot(path=os.path.join(shots_dir, s["file"]))
57 page.close()
58 print(f"shot {s['file']}: {base + s['path'].lstrip('/')}", flush=True)
59 browser.close()
60 server.shutdown()
61
62
63if __name__ == "__main__":
64 main(sys.argv[1], sys.argv[2])