Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Three tiers:
2. **Integration** (`tests/integration/`) — FastAPI `TestClient` + Django test `Client`. Real HTTP, no browser.
3. **E2E** (`tests/e2e/`) — Playwright against live servers. Django E2E server runs as a subprocess with isolated settings to avoid Django singleton conflicts. Parameterized over `["js_on", "js_off"]`.

The Cotton unit tests pass even when cotton is not in INSTALLED_APPS (variables injected as raw context). Only E2E actually exercises `<c-vars>` compilation. Keep this in mind when debugging Cotton rendering issues.
The Cotton unit tests pass even when cotton is not in INSTALLED_APPS (variables injected as raw context) — that tier still doesn't exercise the django-cotton compiler. Integration and E2E both do: `tests/integration/cotton_app/settings.py` and `tests/e2e/_e2e_django_settings.py` both register `django_cotton`, so both exercise real `<c-vars>` compilation (each in its own process — see `tests/integration/conftest.py` for why that isolation matters). Keep this in mind when debugging Cotton rendering issues.

## Component Naming Convention

Expand Down
11 changes: 10 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,17 @@ test-integration:
test-e2e:
pytest tests/e2e -q --tb=short

# Separate pytest invocations, not `pytest tests/` — django-cotton's
# AppConfig.ready() mutates settings.TEMPLATES in place and resets Django's
# global template-engine cache the moment it's in INSTALLED_APPS (see
# tests/integration/cotton_app/settings.py), so any single process that
# combines the integration tier with the unit tier leaks real cotton
# compilation into the unit tier's render_to_string calls. Matches how
# .github/workflows/ci.yml already runs each tier as its own step.
test-all:
pytest tests/ -q --tb=short
pytest tests/unit -q --tb=short
pytest tests/integration -q --tb=short
pytest tests/e2e -q --tb=short

check: lint lint-templates test

Expand Down
50 changes: 35 additions & 15 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
"""
Integration test configuration.

Django can only be set up once per process. When running the full test suite,
tests/unit/conftest.py configures Django first with minimal settings. This
conftest handles the case where we run integration tests in isolation (so
Django is NOT yet configured) by setting DJANGO_SETTINGS_MODULE before
collection begins.

When the full suite runs together, the unit conftest already configured Django.
The cotton integration tests use Django's test Client which works regardless of
which settings module was used to configure Django, as long as the required
apps (cf_ui.django.CfUiConfig) are in INSTALLED_APPS — which they are in both
settings modules.

The ROOT_URLCONF setting differs: unit tests don't set it, integration tests
need it. The session-scoped fixture below ensures ROOT_URLCONF is set for all
integration tests regardless of which conftest configured Django first.
Django can only be set up once per process, so this tier must never share a
pytest process with tests/unit — see the justfile's `test-all` recipe and
.github/workflows/ci.yml, which both run tests/unit and tests/integration as
separate invocations for exactly this reason. django-cotton's
AppConfig.ready() (see tests/integration/cotton_app/settings.py) mutates
settings.TEMPLATES in place and resets Django's global template-engine cache
the moment it's in INSTALLED_APPS, so a combined process would leak real
cotton compilation into the unit tier's render_to_string calls.

This conftest sets DJANGO_SETTINGS_MODULE before collection begins so Django
configures from tests/integration/cotton_app/settings — the settings module
this tier actually needs cotton registered under.
"""

import os
Expand All @@ -34,6 +31,29 @@ def pytest_configure(config):
django.setup()


def pytest_collection_modifyitems(items):
"""Fail fast if tests/unit and tests/integration get collected together.

A bare `pytest` (pyproject.toml's `testpaths = ["tests"]` default),
`pytest tests/`, or an IDE "run all tests" still collects both tiers
into one process — silently reintroducing the settings.TEMPLATES leak
this conftest's docstring describes, as hundreds of unrelated unit
failures rather than an obvious error. Catch it here instead.
"""
has_unit = any(item.nodeid.startswith("tests/unit/") for item in items)
has_integration = any(item.nodeid.startswith("tests/integration/") for item in items)
if has_unit and has_integration:
raise pytest.UsageError(
"tests/unit and tests/integration were collected into the same "
"pytest process. django-cotton's AppConfig.ready() mutates "
"Django's global template settings once tests/integration "
"registers it, which silently breaks tests/unit's "
"render_to_string calls. Run the tiers separately — "
"`just test-all`, or `pytest tests/unit` then "
"`pytest tests/integration`."
)


@pytest.fixture(scope="session", autouse=True)
def _set_root_urlconf():
"""Ensure ROOT_URLCONF is set for integration URL routing."""
Expand Down
5 changes: 5 additions & 0 deletions tests/integration/cotton_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"django.contrib.contenttypes",
"django.contrib.auth",
"django.contrib.staticfiles",
"django_cotton",
"cf_ui.django.CfUiConfig",
]
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
Expand All @@ -23,5 +24,9 @@
}
]
CF_UI_THEME = "bulma"
# django-cotton default COTTON_DIR="cotton" resolves <c-cf.card> ->
# cotton/cf/card.html, picked up via APP_DIRS from cf_ui's package templates.
# Allow hyphenated filenames (form-field.html, checkbox-group.html) — see #79.
COTTON_SNAKE_CASED_NAMES = False
ROOT_URLCONF = "tests.integration.cotton_app.urls"
STATIC_URL = "/static/"
5 changes: 5 additions & 0 deletions tests/integration/test_cotton_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ def test_form_field_cotton_renders(client):
assert r.status_code == 200
assert b'name="email"' in r.content
assert b"Email" in r.content
# The gallery template's raw `<c-cf.form-field ...>` tag must be
# compiled away, not passed through as literal text — see #79.
assert b"<c-cf.form-field" not in r.content


def test_modal_cotton_renders(client):
r = client.get("/modal/")
assert r.status_code == 200
assert b"modal" in r.content
assert b"test-modal" in r.content
assert b"<c-cf.modal" not in r.content


def test_card_cotton_renders(client):
Expand All @@ -28,3 +32,4 @@ def test_card_cotton_renders(client):
assert b"Card Title" in r.content
assert b"Card body content" in r.content
assert b"Card Footer" in r.content
assert b"<c-cf.card" not in r.content
Loading