Assertions

Assert on the shape of a hypermedia response — fragment vs page, HX-* headers, OOB targets, SSE events

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Overview

Chirp ships assertion helpers that check the shape of a hypermedia response — is it a fragment or a full page, does it carry the rightHX-*header, does it contain the expected out-of-band (OOB) swap targets. Your tests assert on intent, not on brittle substring matching.

Import them fromchirp.testingand pair them with a TestClient:

from chirp.testing import TestClient, assert_is_fragment, assert_hx_redirect

Reach for these whenever a plain assert "x" in response.textwould miss the thing that actually matters: the response type, the htmx headers, or the OOB targets a fragment response carries.

The assertion family at a glance

Seventeen helpers, grouped by the job they do. Find the one you need, then jump to the example below.

Job Helpers
Fragment shape — is this a partial or a full page? assert_is_fragment · assert_is_full_page · assert_no_full_document · assert_fragment_contains · assert_fragment_not_contains · assert_is_error_fragment · assert_has_id
htmx headers — did the response steer the client? hx_headers · assert_hx_redirect · assert_hx_push_url · assert_hx_trigger · assert_hx_retarget · assert_hx_reswap
OOB + mutation — did a write land its swaps? assert_oob_targets · assert_mutation_fragments · assert_mutation_redirect
Status assert_status

Minimal working example

The most common check: a route returns a full page for a browser and a fragment for htmx.

from chirp.testing import TestClient, assert_is_fragment, assert_is_full_page

async def test_search_negotiates():
    async with TestClient(app) as client:
        # Browser navigation -> full page
        page = await client.get("/search")
        assert_is_full_page(page)

        # htmx request -> fragment
        fragment = await client.fragment("/search?q=python")
        assert_is_fragment(fragment)

assert_is_fragment checks three things: the status is 200 (override with status=), the body has no <html>/</html> wrapper, and the body is non-empty. assert_is_full_page checks for an <html> tag or a doctype. client.fragment(...) sets the HX-Requestheader for you — see the TestClient guide.

Common patterns

Assert on fragment content

from chirp.testing import assert_fragment_contains, assert_fragment_not_contains

async def test_search_results():
    async with TestClient(app) as client:
        response = await client.fragment("/search?q=apple")
        assert_fragment_contains(response, "apple")
        assert_fragment_contains(response, '<div id="results">')
        assert_fragment_not_contains(response, "banana")

Assert an error fragment

assert_is_error_fragment matches the chirp-error CSS class that Chirp's error fragments carry. Pass status= to also assert the HTTP status and the matching data-statusattribute.

from chirp.testing import assert_is_error_fragment

async def test_missing_item():
    async with TestClient(app) as client:
        response = await client.fragment("/items/9999")
        assert_is_error_fragment(response, status=404)

Assert htmx headers

When a handler steers the client withHX-Redirect, HX-Push-Url, or an HX-Triggerevent, assert on the header rather than scraping the body.

from chirp.testing import assert_hx_redirect, assert_hx_trigger

async def test_login_redirects():
    async with TestClient(app) as client:
        response = await client.post("/login", data={"user": "alice", "pass": "secret"})
        assert_hx_redirect(response, "/dashboard")
        assert_hx_trigger(response, "login-success")

Assert OOB swaps and mutation results

A mutation that returnsOOB(...) writes several swap targets into one response. assert_oob_targets confirms each hx-swap-oob target is present. For a route built on FormAction/MutationResult, assert_mutation_fragments checks the htmx path (200 + OOB targets) and assert_mutation_redirect checks the plain-POST path (a 303redirect by default).

from chirp.testing import assert_mutation_fragments, assert_mutation_redirect

async def test_save_item():
    async with TestClient(app) as client:
        # htmx POST -> inline OOB swaps
        htmx = await client.fragment("/items/save", method="POST")
        assert_mutation_fragments(htmx, "item-row", "count")

        # plain POST -> redirect
        plain = await client.post("/items/save")
        assert_mutation_redirect(plain, "/items")

If a swap target never appears in the DOM you expect, the failure message lists the OOB ids it did find — see debugging swaps and the OOB registry.

SSE testing

Collect events from a Server-Sent Events endpoint withclient.sse() and assert on the structured result, never on the raw text/event-streambytes.

async def test_notifications():
    async with TestClient(app) as client:
        result = await client.sse("/notifications", max_events=3)

        assert result.status == 200
        assert len(result.events) == 3
        assert "notification" in result.events[0].data

client.sse() returns a frozen SSETestResult:

Field Type Description
events tuple[SSEEvent, ...] Collected data events, in order.
heartbeats int Count of heartbeat comment frames.
status int HTTP status from the stream's response start.
headers dict[str, str] Response headers.

EachSSEEvent carries data: str, event: str | None, id: str | None, and retry: int | None. Filter by event type to assert a typed channel fired:

async def test_typed_events():
    async with TestClient(app) as client:
        result = await client.sse("/events", max_events=5)
        joins = [e for e in result.events if e.event == "user-join"]
        assert len(joins) >= 1

Full assertion reference

See also