Test your UI¶
Hedron components are deterministic Python objects, so most interface tests need neither an ASGI server nor a browser. Add progressively broader tests only where HTTP or browser behavior is part of the contract.
Render a component¶
from hedron import Card, Text
from hedron.testing import assert_renders, render_html
def test_welcome_card() -> None:
card = Card(Text("Welcome, Ada"))
assert_renders(card, contains="Welcome, Ada")
assert "<script" not in render_html(card)
render_html is useful for focused assertions. assert_renders and
assert_render_result retain render metadata when assets or diagnostics are the behavior
under test.
Exercise an HTMX fragment¶
from hedron.testing import fragment_client
def test_home_fragment(app) -> None:
with fragment_client(app) as client:
response = client.get("/")
assert response.status_code == 200
assert "<html" not in response.text
The fragment client supplies the relevant HTMX request context so the test verifies the fragment contract rather than an ordinary full-page response.
Portable adapter harness (0.11)¶
For PAGE/FRAGMENT/POST+CSRF scenarios shared across FastAPI, Flask, and Django, use
hedron.testing.adapters (fastapi_fixture / flask_fixture / django_fixture plus
assert_page_document / assert_fragment_body / assert_htmx_trigger). See
TESTING.md.
AppScenario and HTMX asserts (0.15)¶
Use AppScenario from hedron.testing for multi-step application flows, and the HTMX
assert helpers (#22–#26) for InteractionResult / fragment / undeclared-target checks
(for example assert_undeclared_target_rejected requires 403 plus a region-specific
signal). See TESTING.md.
Override FastAPI dependencies¶
from hedron.testing import override_dependencies
def test_as_an_admin(app, current_user, fake_admin) -> None:
with override_dependencies(app, {current_user: lambda: fake_admin}):
# Make requests while the override is active.
...
The context manager restores dependency_overrides even if an assertion fails, keeping
tests isolated.
Snapshot intentionally¶
Use normalize_snapshot_html only for documented nondeterminism. Prefer semantic
assertions for text, attributes, diagnostics, and assets; broad snapshots can obscure a
small but important accessibility or security regression.
Browser-level accessibility checks are available from hedron.testing.browser after
installing hedron[browser]. Keep those for interaction and platform behavior that a
render test cannot prove.
Real browser scenarios (1.3 beta)¶
Use hedron.testing.pytest_plugin when a test needs a real browser, HTMX requests, and an
isolated ASGI app. The 1.3 API is still an unpublished candidate; from the Hedron repository,
prepare the locked development environment and browser once:
Load the plugin explicitly in the test directory's conftest.py:
For a new FastAPI app, hedron new myapp --with-ui-tests opts into a minimal-template example: it adds a managed-host factory, an explicit plugin conftest.py, a native Playwright smoke test, and a separate test dependency group. The default scaffold is unchanged, and this option currently supports only the minimal FastAPI template. The package release has not occurred yet, so these generated-project setup commands describe post-release use; candidate source tests use the locked workspace environment above.
hedron new myapp --with-ui-tests
cd myapp
uv sync --group test
uv run playwright install chromium
HEDRON_BROWSER=1 uv run pytest -q --browser chromium -n 0
Run the packaged synthetic example with an installed browser:
Use --browser firefox or --browser webkit to select the other Playwright engines. The
repository browser CI runs tests/browser and both UI-testing examples. On failure, structured
manifests go to .pytest_cache/hedron-ui; set --hedron-ui-artifacts DIR to choose a dedicated
private directory.
Then keep native Playwright in control of locators, actions, and assertions. Hedron supplies a managed ASGI process, a function-scoped context from pytest-playwright, semantic marks/regions, scoped HTTP expectations, and checkpoint-based HTMX settling:
from playwright.sync_api import expect
def test_profile(hedron_ui) -> None:
with hedron_ui.from_app(
"myapp.testing:create_app",
config={"root_path": "/account"},
root_path="/account",
) as ui:
ui.page.goto(ui.url)
name = ui.page.get_by_label("Display Name")
cursor = ui.checkpoint()
with ui.expect_http(
method="POST", path="/account/profile", status=422, since=cursor
):
name.fill("Al")
name.press("Enter")
ui.settle(since=cursor, target="hedron-errors")
expect(ui.page.locator("#hedron-validation-errors")).to_be_visible()
expect(name).to_be_focused()
The example app and full validation/save test live in examples/ui-testing. An external server can be selected with
ui.from_url("http://127.0.0.1:8000"); the caller owns it, and lifespan/server-exception facts
are reported as unavailable. Both modes reject non-loopback hosts and block unlisted browser
origins. Redirect responses are denied because Playwright cannot safely re-check each redirect hop.
Server-sent event requests are rejected, and routed responses are buffered; do not use this profile
for long-lived or streaming responses.
Specialist packages can declare a versioned UITestingContribution and explicitly load it with
@pytest.mark.hedron_ui_contribution("module:CONTRIBUTION"). The sample-kit implementation in
examples/ui-testing-satellite demonstrates capability,
fixture-resource, and version checks without taking ownership of the browser or host.
Unexpected browser errors, failed required assets, unexpected HTTP errors, blocked requests,
server exceptions from a managed app, and teardown problems fail the scenario. An exact
expect_http rule accepts only its method, path, status, count, and checkpoint. It does not
disable page-error or unrelated console checks. ui.mark("save") selects a real
data-testid="save" element, and ui.region("results") re-queries a real element by id after
swaps.
ui.settle matches trusted user-triggered HTMX requests by default, so a same-target background
poll does not satisfy a user action. If multiple user actions target the same region after one
checkpoint, settle raises AmbiguousSettleError; set a stable
data-hedron-action-generation="N" on each initiating element and pass generation=N to select
one. Use user_action=False for explicitly programmatic HTMX events, or user_action=None when
the test intentionally accepts any trigger.
The default failure artifact is a bounded structured manifest under
.pytest_cache/hedron-ui; it omits page text, request URLs, form values, headers, cookies,
console messages, and target identifiers. Native trace/screenshot capture is a separate
synthetic-data-only profile and labels retained output unredacted. Its resource ceilings are
still part of the 1.3 release gate, so do not use it for real or confidential data.
On Linux, opted-in native capture must run through Hedron's budget wrapper with pytest-playwright
output inside a private 128 MiB tmpfs, a monitored 64 MiB total-tree threshold, 25 ms process-group
checks, and a 64 MiB per-file limit. Crossing the tree threshold terminates and fails the test
process; direct pytest --tracing use is rejected.
The release CI browser lane configures this wrapper and tmpfs automatically.
From a source checkout, the equivalent command is:
export HEDRON_BROWSER=1
python scripts/run_ui_budgeted.py \
--output-json test-results/ui-resources.json \
--tempfs-limit-mib 128 --tempfs-usage-limit-mib 64 \
--sample-interval-ms 25 --max-file-mib 64 -- \
python -m pytest -p hedron.testing.pytest_plugin -q \
--browser chromium --tracing=retain-on-failure \
--output "$TMPDIR/playwright-output" path/to/test_ui.py
This API is beta. It supports synchronous pytest with pytest-playwright; asyncpytest, WSGI host
launchers, remote services, and visual snapshot management are outside this cut. The lower-cost
render helpers and AppScenario remain the default for tests that do not need a real browser.
See the testing API contract for the complete helper inventory.