"""US09-04: the user manual's screenshots, produced from the running application. A screenshot nobody can regenerate is a screenshot that silently stops being true. So every image in the manual is captured here, from a real server and a real worker driving a temporary fixture library, and never pasted in by hand. Regenerate the committed images with one command: PHOTO_PIPELINE_WRITE_SCREENSHOTS=1 work_item/scripts/python -m pytest \ tests/e2e/test_user_manual_screenshots.py -q Without that variable the capture still runs on every suite — the generator has to keep working, and a view that stops rendering must fail here rather than in a reader's browser — but the images go to a temporary directory. Regenerating on every run would leave the working tree permanently dirty, because PNG output is not byte-stable. """ from __future__ import annotations import os import re from contextlib import closing from pathlib import Path import pytest from tests.conftest import session_client from tests.e2e._pipeline_harness import ( Server, seed_album, start_worker, wait_until, ) REPO = Path(__file__).resolve().parents[2] DOCS = REPO / "docs" IMAGES = DOCS / "images" ALBUM = "rome" VIEWPORT = {"width": 1280, "height": 900} # One per stage page of the manual: the route to visit, and the element whose # presence means the view has actually finished rendering. SHOTS = ( ("workflow", "#/workflow", "stage-safety"), ("inventory", "#/inventory", "asset-row"), ("duplicates", "#/duplicates", None), ("safety", "#/safety", None), ("analysis", "#/analyze", "analyze-counts"), ("albums", f"#/albums?album={ALBUM}", "suggested-name"), ("renames", "#/renames", None), ("uploads", "#/uploads", "upload-scope"), ("archive", "#/archive", "archive-locations"), ("statistics", "#/stats", None), ) def writing() -> bool: return os.environ.get("PHOTO_PIPELINE_WRITE_SCREENSHOTS") == "1" @pytest.fixture(scope="module") def stack(tmp_path_factory): """A library in the state the manual describes: one analysed album, a duplicate to review, an archive destination, and a worker that claims jobs.""" tmp_path = tmp_path_factory.mktemp("manual") seeded = seed_album(tmp_path, ALBUM, ("forum.jpg", "colosseum.jpg")) worker = start_worker(seeded, fake_vision_log=tmp_path / "vision.log") server = Server(seeded).start() try: _prepare(server.base, seeded) yield server finally: server.stop() worker.terminate() worker.wait(timeout=10) def _prepare(base: str, seeded) -> None: """Everything the views need, established over the public API — the same calls a person would make, so the screenshots show reachable states.""" with closing(session_client(base, timeout=30)) as client: client.post("/api/v1/duplicates/detect").raise_for_status() client.post("/api/v1/albums/proposals", json={}).raise_for_status() destination = seeded.data / "archive" destination.mkdir(exist_ok=True) client.post( "/api/v1/archive-locations", json={"name": "external disk", "root": str(destination)}, ).raise_for_status() wait_until(lambda: client.get("/api/v1/workflow").status_code == 200) def _capture(page, server, target: Path) -> list[str]: target.mkdir(parents=True, exist_ok=True) page.set_viewport_size(VIEWPORT) written = [] for name, route, ready in SHOTS: page.goto(f"{server.base}/app/{route}") if ready: page.get_by_test_id(ready).first.wait_for(timeout=30_000) else: page.locator("main h1").first.wait_for(timeout=30_000) page.screenshot(path=str(target / f"{name}.png")) written.append(name) return written def test_the_generator_produces_every_screenshot_the_manual_references(page, stack, tmp_path): destination = IMAGES if writing() else tmp_path / "images" written = _capture(page, stack, destination) assert sorted(written) == sorted(name for name, _, _ in SHOTS) for name in written: produced = destination / f"{name}.png" assert produced.stat().st_size > 5_000, f"{name}.png is too small to be a view" referenced = set(re.findall(r"images/([a-z-]+)\.png", "\n".join( path.read_text() for path in DOCS.rglob("*.md") ))) assert referenced <= set(written), f"the manual references images nobody generates: {referenced - set(written)}" def test_no_screenshot_shows_a_real_path_or_a_secret(page, stack, tmp_path): """The fixture library is synthetic and lives in a temporary directory, so the pixels cannot carry someone's photographs — but the view can still print a path, and that path must be the fixture's, never the operator's.""" with closing(session_client(stack.base, timeout=30)) as client: rendered = client.get("/api/v1/inventory/assets", params={"limit": 200}).json()["items"] paths = [item["current_path"] for item in rendered if item["current_path"]] assert paths, "nothing was rendered, so nothing was proven" assert all("/manual" in path or "pytest" in path or "/tmp" in path or "/var" in path for path in paths), paths assert all(ALBUM in path or "images" in path for path in paths) def test_every_committed_screenshot_is_referenced_by_a_page(): """An image nobody shows is an image nobody updates.""" if not IMAGES.is_dir(): pytest.skip("no screenshots have been generated into the repository yet") text = "\n".join(path.read_text() for path in DOCS.rglob("*.md")) orphans = sorted( image.name for image in IMAGES.glob("*.png") if f"images/{image.name}" not in text ) assert orphans == [], f"committed but unreferenced: {orphans}"