242 lines
9.3 KiB
Python
242 lines
9.3 KiB
Python
"""
|
||
Capture screenshots of EMOM and TABATA combo timers in Execution Mode,
|
||
including the TABATA rest screen — not covered by all_pages.py (which only
|
||
seeds an AMRAP combo).
|
||
|
||
The app is driven in English by default (use --lang fr for French
|
||
screenshots) with a seeded database so each screenshot shows meaningful
|
||
content rather than empty states.
|
||
|
||
Intended for visual investigations and documentation figures — not a test.
|
||
|
||
Prerequisites:
|
||
- Playwright for Python installed (see `tests/requirements.txt`)
|
||
- Playwright browsers installed: `python -m playwright install firefox chromium`
|
||
- Vite dev server running: `npm run dev -- --port 5199 --strictPort`
|
||
|
||
Usage (from project root):
|
||
|
||
python scripts/screenshots/combo_timers.py
|
||
python scripts/screenshots/combo_timers.py --browser chromium
|
||
python scripts/screenshots/combo_timers.py --lang fr
|
||
python scripts/screenshots/combo_timers.py --out-dir /tmp/screenshots
|
||
|
||
Options:
|
||
|
||
--url URL App URL (default: http://localhost:5199)
|
||
--browser NAME firefox | chromium | webkit (default: firefox)
|
||
--lang LANG en | fr (default: en)
|
||
--width N Viewport width in px (default: 1920)
|
||
--height N Viewport height in px (default: 1080)
|
||
--out-dir PATH Output directory (default: tmp/screenshots/combo-timers-<lang>-<browser>-<W>x<H>)
|
||
|
||
Exit code: 0 on success, 1 on failure.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
import time
|
||
import traceback
|
||
from pathlib import Path
|
||
|
||
from playwright.sync_api import Page, sync_playwright
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Seed helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# TABATA work/rest kept short (6s / 5s) so the script doesn't have to wait
|
||
# long to reach the rest screen.
|
||
_SEED_SCRIPT = """
|
||
async () => {
|
||
const uuid = await window.__repos.settings.get('user_uuid');
|
||
|
||
const ex1 = await window.__repos.exercises.create({
|
||
title: 'Jumping Jacks',
|
||
description: 'Cardio warm-up movement.',
|
||
asymmetric: false, alternate: false, image_urls: [], video_urls: [],
|
||
default_reps: 20, default_weight: 0, created_by: uuid
|
||
});
|
||
const ex2 = await window.__repos.exercises.create({
|
||
title: 'Mountain Climbers',
|
||
description: 'Core and cardio movement.',
|
||
asymmetric: false, alternate: false, image_urls: [], video_urls: [],
|
||
default_reps: 20, default_weight: 0, created_by: uuid
|
||
});
|
||
|
||
// ── EMOM combo — work phase is always a 60s slot per exercise ─────────
|
||
const emomComboId = await window.__repos.combos.create({
|
||
title: 'EMOM Cardio', description: '3-minute EMOM.',
|
||
type: 'EMOM', timer_config: {}, created_by: uuid
|
||
});
|
||
await window.__repos.combos.setExercises(emomComboId, [
|
||
{ exerciseId: ex1, position: 0, defaultReps: 20, defaultWeight: 0 }
|
||
]);
|
||
const emomSessId = await window.__repos.sessions.create({
|
||
title: 'EMOM Session', description: '', created_by: uuid
|
||
});
|
||
await window.__repos.sessions.setItems(emomSessId, [
|
||
{ item_id: emomComboId, item_type: 'combo', repetitions: 3, weight: 0.0 }
|
||
]);
|
||
|
||
// ── TABATA combo ────────────────────────────────────────────────────
|
||
const tabataComboId = await window.__repos.combos.create({
|
||
title: 'TABATA Blast', description: 'Work/rest intervals.',
|
||
type: 'TABATA', timer_config: { workTime: 6, restTime: 5 }, created_by: uuid
|
||
});
|
||
await window.__repos.combos.setExercises(tabataComboId, [
|
||
{ exerciseId: ex1, position: 0, defaultReps: 0, defaultWeight: 0 },
|
||
{ exerciseId: ex2, position: 1, defaultReps: 0, defaultWeight: 0 }
|
||
]);
|
||
const tabataSessId = await window.__repos.sessions.create({
|
||
title: 'TABATA Session', description: '', created_by: uuid
|
||
});
|
||
await window.__repos.sessions.setItems(tabataSessId, [
|
||
{ item_id: tabataComboId, item_type: 'combo', repetitions: 4, weight: 0.0 }
|
||
]);
|
||
|
||
return { emomSessId, tabataSessId };
|
||
}
|
||
"""
|
||
|
||
_COUNT_SCRIPT = """
|
||
async () => (await window.__repos.exercises.getAll()).length
|
||
"""
|
||
|
||
|
||
def wait_for_db(page: Page, url: str) -> None:
|
||
page.goto(url)
|
||
page.wait_for_function(
|
||
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
||
timeout=15000,
|
||
)
|
||
|
||
|
||
def ensure_language(page: Page, lang: str) -> None:
|
||
current = page.evaluate("async () => window.__repos.settings.get('language')")
|
||
if current != lang:
|
||
page.evaluate(f"async () => window.__repos.settings.set('language', '{lang}')")
|
||
page.reload()
|
||
page.wait_for_function(
|
||
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
||
timeout=15000,
|
||
)
|
||
|
||
|
||
def has_data(page: Page) -> bool:
|
||
return page.evaluate(_COUNT_SCRIPT) > 0
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Screenshot helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def shot(page: Page, out_dir: Path, name: str, full_page: bool = True) -> None:
|
||
time.sleep(0.3)
|
||
path = out_dir / f"{name}.png"
|
||
page.screenshot(path=str(path), full_page=full_page)
|
||
print(f" ✓ {path.name}")
|
||
|
||
|
||
def goto(page: Page, url: str, route: str, wait_selector: str | None = None) -> None:
|
||
page.goto(f"{url}/#/{route}", wait_until="domcontentloaded")
|
||
if wait_selector:
|
||
page.wait_for_selector(wait_selector, timeout=8000)
|
||
else:
|
||
time.sleep(0.3)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Main capture flow
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def capture(page: Page, url: str, out_dir: Path, lang: str) -> None:
|
||
wait_for_db(page, url)
|
||
ensure_language(page, lang)
|
||
|
||
if not has_data(page):
|
||
print(" Seeding database…")
|
||
ids = page.evaluate(_SEED_SCRIPT)
|
||
print(" Seed complete.")
|
||
else:
|
||
raise RuntimeError("DB already has data — start with a fresh browser context")
|
||
|
||
# ── EMOM — work phase starts immediately ───────────────────────────────
|
||
goto(page, url, f"sessions/{ids['emomSessId']}/execute", '[data-testid="execute-timer"]')
|
||
shot(page, out_dir, "combo-emom")
|
||
|
||
# ── TABATA — work phase ─────────────────────────────────────────────
|
||
# Navigate away first so Vue Router remounts SessionExecuteView (it reuses
|
||
# the component when only params change, see all_pages.py for the same
|
||
# workaround).
|
||
goto(page, url, "sessions", '[data-testid="session-new-btn"]')
|
||
goto(page, url, f"sessions/{ids['tabataSessId']}/execute", '[data-testid="execute-timer"]')
|
||
shot(page, out_dir, "combo-tabata-work")
|
||
|
||
# ── TABATA — rest phase (work duration is 6s in the seed above) ────────
|
||
page.wait_for_selector('[data-testid="execute-tabata-rest"]', timeout=12000)
|
||
shot(page, out_dir, "combo-tabata-rest")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CLI
|
||
# ---------------------------------------------------------------------------
|
||
|
||
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("--lang", default="en", choices=["en", "fr"])
|
||
parser.add_argument("--width", type=int, default=1920)
|
||
parser.add_argument("--height", type=int, default=1080)
|
||
parser.add_argument("--out-dir", default=None)
|
||
return parser.parse_args(argv)
|
||
|
||
|
||
def default_out_dir(browser: str, lang: str, width: int, height: int) -> Path:
|
||
project_root = Path(__file__).resolve().parents[2]
|
||
prefix = f"combo-timers-{browser}" if lang == "en" else f"combo-timers-{lang}-{browser}"
|
||
return project_root / "tmp" / "screenshots" / f"{prefix}-{width}x{height}"
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
args = parse_args(argv or sys.argv[1:])
|
||
out_dir = (
|
||
Path(args.out_dir)
|
||
if args.out_dir
|
||
else default_out_dir(args.browser, args.lang, args.width, args.height)
|
||
)
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
print(f"\n── {args.browser} {args.width}×{args.height} → {out_dir}")
|
||
|
||
with sync_playwright() as p:
|
||
browser_type = getattr(p, args.browser)
|
||
browser = browser_type.launch(headless=True)
|
||
try:
|
||
context = browser.new_context(viewport={"width": args.width, "height": args.height})
|
||
page = context.new_page()
|
||
page.on("dialog", lambda dialog: dialog.accept())
|
||
capture(page, args.url, out_dir, args.lang)
|
||
count = len(list(out_dir.glob("*.png")))
|
||
print(f"\n Done — {count} screenshots saved")
|
||
except Exception as exc:
|
||
print(f"\n ERROR: {exc}", file=sys.stderr)
|
||
traceback.print_exc()
|
||
return 1
|
||
finally:
|
||
browser.close()
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|