**Phase 01 final verification pass — all criteria green**
- Verified (nothing left to implement; all 4 task files already in `complete/`): toolchain lock, typed `Settings`+`.env.example`, debugpy bootstrap, app factory, CLI (`--host/--port/--reload/--levels-dir`, `python -m undercooked`), static shell + `/static`, security headers, no-CDN/doc guards, `validate.sh`.
- `uv sync --extra dev` → exit 0; `uv.lock` present (untracked → harness commits it with the phase).
- `uv run undercooked --host 127.0.0.1 --port 8000` → `curl /healthz` = 200 `{"status":"ok","version":"0.1.0","rooms":0}` (superset of the criterion's body — now documented in README); `curl /` = 200 shell HTML; static css/js 200; CSP/nosniff/no-referrer/DENY on 200, 404 and 405 too.
- `DEBUGPY=1`: port 5678 is held on this machine by the user's own podman dev-container debugpy adapter — **no live change made/killed**; verified via `DEBUGPY=1 DEBUGPY_PORT=5679` → listener opens, `/healthz` still 200; the occupied-port case degraded gracefully as designed (warning, server serves). Bad config → one line + exit 2 ✓.
- `bash .agents/validate.sh` (root **and** `.agents/`) → exit 0: `ruff check .` 0 errors, `pyright` 0 errors, levels-CLI notice-skip (matches README prose), **284 passed** full suite incl. 10 Playwright e2e, coverage **100.00%** (floor 90), repo hygiene clean. `uv run pytest -m e2e` → 10 passed.
- Docs fixed: README Status → phase 01 complete + next pending 02 (was pointing at `todo/01…`), documented real `/healthz` body, precedence now "CLI flags > env > .env > default", added the PLAN §13-promised **How to play** section (honest about shell-only today), Layout rows marked what exists vs phases 02/07. AGENTS.md: recorded the `data:`-URL exemption in the no-CDN rule (test allows it) and matched the session-token rule to the browser's WebCrypto/base64url implementation. Also corrected a stale "session-wide chromium" docstring in `tests/e2e/conftest.py` (fixtures are function-scoped).
- Deviations: no container build run (needs an image pull; not a phase criterion — `Containerfile`/`compose.yaml` reviewed and consistent with `docs/DEPLOYMENT.md`); `HEAD /` is 405 (routes are GET-only by design; health checks use GET). No git index/history commands run.
- Next pending phase: `02_level_content_and_validation`.
67 lines
2.3 KiB
Python
67 lines
2.3 KiB
Python
"""Playwright fixtures for the browser suites in `tests/e2e/`.
|
|
|
|
The project depends on the `playwright` package, not on `pytest-playwright`, so
|
|
the three fixtures the plugin would provide are defined here — a headless chromium,
|
|
a fresh context and a fresh page, all function-scoped (see `browser` for why the scope
|
|
is load-bearing). A fresh context per test matters too: `sessionStorage` (the anonymous
|
|
`uc.token`, `uc.lastCode`) is scoped to it, so tests cannot observe each other's
|
|
identity.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
from playwright.sync_api import Browser, BrowserContext, Page, ViewportSize
|
|
|
|
# The default desktop viewport; story tests that care about small screens call
|
|
# `page.set_viewport_size` before navigating.
|
|
VIEWPORT: ViewportSize = {"width": 1280, "height": 720}
|
|
|
|
|
|
@pytest.fixture
|
|
def browser() -> Iterator[Browser]:
|
|
"""A headless chromium, live for exactly one test.
|
|
|
|
Function scope is load-bearing, not shyness about startup time. The
|
|
*synchronous* Playwright API drives the browser from an asyncio event loop it
|
|
owns in the main thread, and that loop stays flagged as running for as long as
|
|
`sync_playwright()` is open — so a session- or module-scoped fixture makes every
|
|
later `async def` test in the run die with
|
|
`RuntimeError: Runner.run() cannot be called from a running event loop`
|
|
(pytest-asyncio, `asyncio_mode = "auto"`). Closing the whole stack inside the
|
|
test leaves the main thread clean for whatever runs next.
|
|
"""
|
|
with sync_playwright() as playwright:
|
|
launched = playwright.chromium.launch(headless=True)
|
|
try:
|
|
yield launched
|
|
finally:
|
|
launched.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def context(browser: Browser) -> Iterator[BrowserContext]:
|
|
"""A brand-new browser context per test: isolated storage and cookies."""
|
|
created = browser.new_context(viewport=VIEWPORT)
|
|
try:
|
|
yield created
|
|
finally:
|
|
created.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def page(context: BrowserContext) -> Iterator[Page]:
|
|
"""A tab inside `context`, closed after the test."""
|
|
opened = context.new_page()
|
|
try:
|
|
yield opened
|
|
finally:
|
|
opened.close()
|