<div class="docs-content" data-render>
# Test framework and rules
The structure of the correctness suite, the fixtures that back it, and the mandatory patterns every test must follow. See also [Testing overview](/docs/testing.html) and [Running tests via make](/docs/testing-make.html).
## Playwright, not pytest-playwright
The suite uses **raw Playwright** driven by plain `pytest`, not the `pytest-playwright` plugin. The two conflict over fixture names. If `pytest-playwright` is installed, uninstall it before running the suite.
## Fixture stack
All fixtures live in `tests/conftest.py`. They layer from a shared server up to per-test isolation:
| Fixture | Scope | Purpose |
|---------|-------|---------|
| `app_server` | session | Spawns Uvicorn as a subprocess on a per-worker port with a tempfile SQLite database, then waits for it to answer. |
| `playwright_instance` / `browser` | session | One Playwright runtime and one Chromium browser for the whole run. |
| `browser_context` | module | One shared browser context. |
| `page` | function | Clears cookies on the shared context, then yields a fresh page. |
| `seeded_db` | session | Seeds baseline users and content once. |
| `alice` / `bob` | function | Logged-in seeded users (`alice_test` / `bob_test`); `bob` gets its own context for multi-user tests. Each returns `(page, user_dict)`. |
In-process tests skip the browser entirely and call a FastAPI `TestClient` or run coroutines through the `run_async` helper, which executes them on a worker thread and refreshes the database snapshot afterward.
## Isolation and ports
Each xdist worker runs its own server on its own port, derived from the worker index: master uses `10501`, `gw0` uses `10501`, `gw1` uses `10502`, and so on. Every worker gets a separate tempfile database. Background services are disabled in tests via `DEVPLACE_DISABLE_SERVICES=1`, and the rate limit is seeded to `1000000` so the suite is never throttled.
## Mandatory patterns
These are not style preferences. Violating them produces flaky or hanging tests:
- **Always pass `wait_until="domcontentloaded"`** to every `page.goto(...)` and `page.wait_for_url(...)`. CDN libraries and avatars make the default `"load"` event time out.
- **Prefer `page.locator(...).wait_for(state="visible")`** over `wait_for_selector`.
- **Scope ambiguous selectors.** Examples: comment Delete is `.comment-action-btn:has-text('Delete')`; the create-post Post button is `#create-post-modal button.btn-primary:has-text('Post')`; the comment textarea is `.comment-form textarea[name='content']`.
- **Restore global state in `try/finally`.** The server is shared session-wide, so a test that flips a `site_settings` value (maintenance mode, registration, rate limit, session length) MUST restore it. Rate-limit unit tests patch `get_int_setting` rather than the fallback constant.
## Failure diagnostics
A `pytest_runtest_makereport` hook saves a screenshot to `/tmp/devplace_test_screenshots/` whenever a Playwright test fails. In CI those screenshots are uploaded as a `failure-screenshots` artifact.
## When to write which test
- **UI or browser behavior** (rendering, clicks, forms, navigation, JS): end-to-end Playwright test. This is required for every user-facing feature.
- **JSON response shape, auth guards, data helpers, pure logic**: in-process `TestClient` or `asyncio` test.
Many features need both: the in-process test pins the JSON contract and the E2E test proves the page works.
## Running a single test
```bash
python -m pytest tests/test_feed.py::test_name -v --tb=line -x
```
The full suite is run through the `make` targets documented in [Running tests via make](/docs/testing-make.html).
</div>