Scroll Press includes comprehensive testing with both unit/integration tests and end-to-end browser tests to ensure reliability and prevent regressions.
# Run all tests
just test
# Run with coverage report
just test-cov
# Run specific test file
uv run pytest tests/test_main.py -v
# Run specific test function
uv run pytest tests/test_auth.py::test_registration -v- Framework: pytest with pytest-asyncio
- Database: SQLite in-memory for fast, isolated execution
- HTTP Client: httpx for async requests
- Fixtures: Defined in
tests/conftest.py
- Authentication (registration, login, email verification, password reset)
- Scroll upload and management (draft/publish workflow)
- GDPR data export
- Subject categorization
- Session management
- Email service integration
Each test gets a fresh database:
- In-memory SQLite created
- All migrations applied
- Test executes
- Database destroyed
This ensures tests don't interfere with each other.
Tests use InMemoryStorage (a dict-backed fake implementing the StorageBackend protocol) instead of real S3/Tigris. This runs in-process with no external dependencies.
For integration tests that need to verify S3-specific behavior (e.g., TigrisStorage), use moto's @mock_aws decorator. No MinIO or Docker required.
E2E tests use Playwright to verify complete user journeys in real browsers.
# Install Playwright browsers (one-time setup)
uv run playwright install chromium firefox
# Start development server
just dev
# Run e2e tests (in another terminal)
uv run pytest -m e2e- Registration → Upload → Public Access: Verifies scrolls remain publicly accessible
- Registration → Upload → Account Deletion → Public Access: Verifies scroll persistence after user deletion
- License Selection: Tests CC BY 4.0 and All Rights Reserved license workflows
- Mobile Responsive: Validates mobile upload and interaction flows
- Search & Discovery: Tests content search and subject browsing
CRITICAL: Do NOT use session-scoped Playwright fixtures (page, browser, browser_context) as they cause deadlocks with pytest-asyncio.
Correct pattern:
async def test_something():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# Test code here
await browser.close()This pattern avoids event loop conflicts and ensures tests don't hang.
For detailed E2E testing information, see tests/e2e/README.md.
# Run linting and unit/integration tests
just check
# This runs:
# 1. ruff check . && ruff format .
# 2. pytest (unit/integration only, excludes e2e)
# Run all tests including e2e (server must be running)
just check && uv run pytest -m e2eTests run automatically on:
- Every push to main branch
- Every pull request
CI configuration: .github/workflows/ci.yml
tests/
├── conftest.py # Shared fixtures
├── test_auth.py # Authentication tests
├── test_data_export.py # GDPR export tests
├── test_main.py # Core functionality tests
├── test_scrolls.py # Scroll management tests
├── test_subjects.py # Subject categorization tests
└── e2e/ # End-to-end tests
├── README.md # E2E documentation
├── test_upload_flow.py # Upload workflows
├── test_licenses.py # License selection
└── test_mobile.py # Mobile responsive tests
async def test_registration(client, db_session):
response = await client.post("/auth/register", json={
"email": "[email protected]",
"password": "SecurePassword123",
"display_name": "Test User"
})
assert response.status_code == 200async def test_upload_scroll():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto("https://localhost:7999")
await page.fill("#email", "[email protected]")
await page.click("button[type=submit]")
await browser.close()- Use descriptive test names:
test_user_cannot_upload_without_verification - One assertion focus per test: Test one thing at a time
- Use fixtures for setup: Don't repeat database/client setup
- Clean up resources: Always close browsers in E2E tests
- Avoid sleep(): Use Playwright's auto-waiting instead of arbitrary waits
- Test error cases: Don't just test happy paths
uv run pytest -v -s tests/test_auth.pyuv run pytest -xuv run pytest --lf# Run with visible browser
uv run pytest tests/e2e/ --headed
# Run with slow motion
uv run pytest tests/e2e/ --slowmo 1000
# Take screenshots on failure
# (Already configured in tests)