# CLAUDE.md This file documents detailed testing patterns, fixtures, and pitfalls for devplacepy/tests/ (Playwright + unit tests across api/e2e/unit tiers). Claude Code loads it automatically whenever a file under this directory is read or edited. Never run the test suite unless the user explicitly asks - validate with imports and manual checks instead. ## Never run tests unless explicitly asked (critical guardrail) **NEVER run tests unless specifically asked by user.** Not the full suite, not a single file - do not run any tests unless the user explicitly requests it. Validate each touched language manually instead: Python (compile + import), JS (parse / bracket matching), CSS (brace matching), HTML (tag matching). Zero tolerance. A clean `python -c "from devplacepy.main import app"` import is the baseline check. **Never run tests unless the user explicitly asks for it.** Not the full suite, not a single file. Validate code with a clean import (`python -c "from devplacepy.main import app"`) and per-language manual checks instead. If you do need to run a single test on explicit request: `python -m pytest tests/e2e/feed.py::test_name -v --tb=line -x`. ## Fixture stack - `app_server` (session-scoped): spawns uvicorn as a subprocess on port 10501 (the `PORT` constant) with a tempfile DB. - `browser_context` (session-scoped): one Playwright context shared by all tests. - `page` (function-scoped): `clear_cookies()` on the session context, then a fresh page. - `alice` / `bob`: seeded users `alice_test` / `bob_test` logged in via the login form. `bob` gets its own context for multi-user tests. Returns `(page, user_dict)`. Tests run on port 10501 with a tempfile SQLite DB and a dedicated `DEVPLACE_DATA_DIR`, and `DEVPLACE_DISABLE_SERVICES=1` so background services don't start. `make test` runs **serially, one test at a time, in a single process**: one isolated DB + data dir + uvicorn subprocess + Chromium, shared across the whole session, so session fixtures (`seeded_db`) and intra-file ordering hold. Serial execution is enforced centrally in `pyproject.toml` (`[tool.pytest.ini_options]` `addopts = "--tb=line -p no:xdist"`); pytest-xdist is no longer a dependency and `-n` is rejected, so the suite can never run concurrently. ## General - **Around 1959 tests across `tests/{unit,api,e2e}/`.** Playwright integration + HTTP + unit tests. All must pass before any merge. - **Tests use `-x` (fail-fast).** The suite stops at the first failure. Fix that test, then re-run. - **Validate each touched language manually:** Python (compile + import), JS (parse / bracket matching), CSS (brace matching), HTML (tag matching). Zero tolerance. - Playwright is used directly, NOT pytest-playwright (uninstall it if present - it conflicts). ## Playwright navigation - **Every `page.goto()` must use `wait_until="domcontentloaded"`**, never the default `"load"`. CDN scripts and avatar images cause `load` to timeout. - **Every `page.wait_for_url()` must also use `wait_until="domcontentloaded"`** for the same reason. - **Prefer `page.locator(...).wait_for(state="visible")`** over bare `wait_for_selector` - it gives better error messages. - **Default timeout is 15 seconds** (increased from 10s for CDN script loading). ```python page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded") page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000) ``` ## Delete button locator scoping When both a post Delete and comment Delete button exist, always scope to the comment: ```python # CORRECT - scoped to comment: page.locator(".comment-action-btn:has-text('Delete')") # WRONG - matches both post and comment Delete: page.locator("button:has-text('Delete')") ``` ## Confirm dialogs Delete buttons carry `data-confirm`, which `ModalManager` turns into a native `confirm()` dialog. Playwright auto-dismisses dialogs (treated as Cancel), so the form never submits. Accept the dialog **before** clicking, or the delete silently does nothing: ```python page.once("dialog", lambda d: d.accept()) page.locator(".comment-action-btn:has-text('Delete')").click() ``` ## Comment hash navigation Opening a URL with a `#comment-` fragment scrolls the comment into view and adds the `comment-highlight` class (`NotificationManager.initHashScroll`). Tests assert on `#comment-.comment-highlight` after the page loads. ## Browser context - **`browser_context` is session-scoped** (one per test session, shared by all tests in all files). - **Cookies are cleared per test via `browser_context.clear_cookies()`** in the `page` fixture. - **Each test gets a fresh `page`** from the shared context. - **`bob` fixture creates its own context** from the session `browser` - necessary for multi-user tests. - **Never share a page between two logged-in users** in the same test - use separate contexts. ## Test users - **`alice_test` / `bob_test` are seeded once at session level** via HTTP POST to `/auth/signup`. - **`alice` fixture logs in alice_test** via the login form. - **`bob` fixture logs in bob_test** in a separate Playwright context. - **Use `alice` for single-user tests.** It returns `(page, user_dict)`. ## Required test patterns (summary) - Every `page.goto(...)` and `page.wait_for_url(...)` MUST pass `wait_until="domcontentloaded"`. CDN libs and avatars cause the default `"load"` to time out. - Prefer `page.locator(...).wait_for(state="visible")` over `wait_for_selector`. - Scope ambiguous selectors: comment Delete is `.comment-action-btn:has-text('Delete')`; create-post Post button is `#create-post-modal button.btn-primary:has-text('Post')`; comment textarea is `.comment-form textarea[name='content']`. - Failure screenshots auto-save to `/tmp/devplace_test_screenshots/` via the `pytest_runtest_makereport` hook in `conftest.py`. ## Global `site_settings` tests - **The uvicorn server is shared for the whole session.** A test that flips a global setting (maintenance mode, registration, rate limit, session length) changes behavior for every later test, so **restore the value in a `try/finally`** - see `tests/e2e/operational.py`. - **Saving through the admin form is the reliable mutation path**, not a direct DB write: the form handler runs in the server process and calls `clear_settings_cache()`, so the change takes effect immediately (a direct DB write waits out the 60s server-side cache). - **`conftest.py`'s `app_server` seeds `rate_limit_per_minute="1000000"`** so the suite is never throttled and the settings form round-trips a safe value instead of the template's `"60"` default. - **Rate-limit unit tests patch `m.get_int_setting`**, not the `RATE_LIMIT` constant - the constant is now only the fallback default passed to `get_int_setting`. - **Service enabled/disabled state is global state too - restore it.** Stopping a service through `POST /admin/services/{name}/stop` sets its `service_{name}_enabled` setting to `"0"` for the rest of the session. A test that stops a service (e.g. `test_gateway_disabled_returns_503` stops `openai` to assert the gateway returns 503) MUST restart it in a `try/finally` (`POST /admin/services/{name}/start`), and any `finally` that ends in `stop` should end in `start` instead - the default, expected state is enabled. Leaving the gateway stopped makes a later file's request hit `is_enabled()` first and get a `503` where it expected a `401`/`200`. The `openai` gateway's `handle()` checks `is_enabled()` (503) BEFORE `authorize()` (401), so a stopped gateway masks the auth result entirely. - **The 1,000,000/min rate-limit seed only applies after the server's 60s settings cache expires.** `conftest.py` seeds `rate_limit_per_minute` with a raw DB insert (no `clear_settings_cache()`), so for the first ~60s of server uptime the server still uses the `60`/min default. A high-volume request loop (e.g. `test_auth_matrix`) run in the FIRST minute can therefore trip a spurious `429`; in the full suite that test runs well past the window so it never does. If you reproduce a `429` only in a tiny isolated run, it is this warm-up artifact, not a real auth/rate-limit issue. - Operational tests live in `tests/e2e/operational.py` plus the `tests/api/admin/settings.py` family; rate-limit tests sit with the endpoint they probe (`tests/api/root.py`, `tests/api/auth/login.py`, `tests/api/robotstxt.py`). ## Failure handling - **Failure screenshots auto-save** to `/tmp/devplace_test_screenshots/`. - **Tests stop at first failure** (`-x` flag in Makefile). No cascading failures. - **If the server won't start, kill leftover processes:** `kill -9 $(pgrep -f "uvicorn")` ## Common pitfalls | Pitfall | Fix | |---------|------| | `goto`/`wait_for_url` times out | Add `wait_until="domcontentloaded"` | | CDN scripts block page load | Use `defer` on all `