trainUs/scripts/screenshots/exercise_list.py
David 352a3d5e88
Some checks are pending
CI / Lint & Format (push) Waiting to run
CI / Build (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (chromium) (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (firefox) (push) Waiting to run
Initial commit
2026-06-18 13:06:57 +02:00

131 lines
4.9 KiB
Python

"""
Screenshot the Exercise List page at a configurable viewport.
Intended for visual investigations (e.g., layout discussions, documentation
figures). Not a test — no assertions; see `tests/` for the test suite.
Prerequisites:
- Playwright for Python installed (see `tests/requirements.txt`)
- Playwright browsers installed: `python -m playwright install firefox chromium`
Typical usage (from project root):
# 1. Start the Vite dev server in one terminal
npm run dev -- --port 5199 --strictPort
# 2. In another terminal, run the script
python scripts/screenshots/exercise_list.py
Options (all optional, override via CLI flags):
--url URL App URL (default: http://localhost:5199)
--browser NAME firefox | chromium | webkit (default: firefox)
--width N Viewport width in px (default: 1920)
--height N Viewport height in px (default: 1080)
--out PATH Output file path (default: tmp/exercise-list-<browser>-<W>x<H>.png)
--no-seed Do not create sample exercises (use the existing DB as-is)
Exit code: 0 on success, 1 on failure.
"""
from __future__ import annotations
import argparse
import os
import sys
import time
from pathlib import Path
from playwright.sync_api import sync_playwright
SAMPLE_EXERCISES = [
{"title": "Push-ups", "desc": "Classic bodyweight push-up", "reps": "15"},
{"title": "Squats", "desc": "Bodyweight squat", "reps": "20"},
{"title": "Plank", "desc": "Core stability hold", "reps": "60"},
]
def ensure_sample_exercises(page, url: str) -> None:
"""Create a few exercises via the UI so the list has visible cards.
No-op if the list already contains at least one card.
"""
page.goto(f"{url}/#/exercises", wait_until="networkidle")
cards = page.locator('[data-testid^="exercise-card-"]')
if cards.count() > 0:
return
for ex in SAMPLE_EXERCISES:
page.goto(f"{url}/#/exercises/new", wait_until="networkidle")
page.wait_for_selector('[data-testid="exercise-title"]', timeout=10000)
page.locator('[data-testid="exercise-title"]').fill(ex["title"])
page.locator('[data-testid="exercise-description"]').fill(ex["desc"])
page.locator('[data-testid="exercise-reps"]').fill(ex["reps"])
page.locator('[data-testid="exercise-save"]').click()
# After save the app navigates to the detail page; just wait for URL
# to move away from `/new`.
page.wait_for_function(
"() => !location.hash.endsWith('/exercises/new')",
timeout=5000,
)
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--url", default=os.environ.get("APP_URL", "http://localhost:5199"))
parser.add_argument("--browser", default="firefox", choices=["firefox", "chromium", "webkit"])
parser.add_argument("--width", type=int, default=1920)
parser.add_argument("--height", type=int, default=1080)
parser.add_argument("--out", default=None, help="Output file path")
parser.add_argument("--no-seed", action="store_true", help="Skip creating sample exercises")
return parser.parse_args(argv)
def default_out_path(browser: str, width: int, height: int) -> Path:
"""Default output goes to tmp/ at the project root (gitignored)."""
project_root = Path(__file__).resolve().parents[2]
tmp_dir = project_root / "tmp"
tmp_dir.mkdir(parents=True, exist_ok=True)
return tmp_dir / f"exercise-list-{browser}-{width}x{height}.png"
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv or sys.argv[1:])
out_path = Path(args.out) if args.out else default_out_path(args.browser, args.width, args.height)
out_path.parent.mkdir(parents=True, exist_ok=True)
viewport = {"width": args.width, "height": args.height}
with sync_playwright() as p:
browser_type = getattr(p, args.browser)
browser = browser_type.launch(headless=True)
try:
context = browser.new_context(viewport=viewport)
page = context.new_page()
page.goto(args.url, wait_until="networkidle")
# Wait for the app to finish DB init overlay, if any.
try:
page.wait_for_selector(".db-overlay", state="detached", timeout=10000)
except Exception:
pass
if not args.no_seed:
ensure_sample_exercises(page, args.url)
page.goto(f"{args.url}/#/exercises", wait_until="networkidle")
page.wait_for_selector('[data-testid^="exercise-card-"]', timeout=5000)
# Let layout settle before capturing.
time.sleep(0.5)
page.screenshot(path=str(out_path), full_page=True)
print(f"Saved screenshot: {out_path}")
return 0
finally:
browser.close()
if __name__ == "__main__":
sys.exit(main())