Assertion Helpers¶
Assertion helpers are frozen dataclass-based wrappers around aria-testing query functions, designed for deferred execution in dynamic systems. They allow you to define assertions declaratively before the DOM is available, then apply them later when the container becomes available.
The Problem They Solve¶
In dynamic testing systems—component testing frameworks, story-based testing, test fixtures—you often need to:
Define assertions early, before the DOM exists
Execute assertions later, when the container becomes available
Reuse assertions across multiple test scenarios
Compose complex assertions from simpler building blocks
Traditional imperative assertions don’t support this pattern:
# ❌ Imperative - requires immediate DOM access
def test_button():
container = render_component()
button = get_by_role(container, "button")
assert get_text_content(button) == "Save"
assert button.attrs["type"] == "submit"
Assertion helpers enable declarative, deferred execution:
# ✅ Declarative - define assertion before DOM exists
assert_save_button = (
GetByRole(role="button")
.text_content("Save")
.with_attribute("type", "submit")
)
# Apply later when container is available
def test_button():
container = render_component()
assert_save_button(container) # Raises AssertionError if fails
Core Concept: Frozen Dataclasses with __call__¶
Each helper is a frozen dataclass that stores query parameters and assertion conditions. The __call__ method executes the assertion against a container:
from aria_testing import GetByRole
# Create helper instance (frozen dataclass)
helper = GetByRole(role="button")
# Later: call it with a container
helper(container) # Executes query, raises AssertionError if not found
Key characteristics:
Immutable - Frozen dataclasses prevent accidental modification
Callable - Accepts
Element | Fragment | Nodevia__call__Composable - Fluent API returns new instances
Type-safe - Full type hints with IDE support
Basic Usage¶
Single Element Assertions¶
from tdom import html
from aria_testing import (
GetByRole,
GetByText,
GetByLabelText,
GetByTestId,
GetByClass,
GetById,
GetByTagName,
)
container = html(t'<div><button type="submit">Save</button></div>')
# Assert element exists
GetByRole(role="button")(container)
GetByText(text="Save")(container)
GetByTagName(tag_name="button")(container)
# All raise AssertionError if element not found
With Role Parameters¶
# Heading with level
GetByRole(role="heading", level=1)(container)
# Button with accessible name
GetByRole(role="button", name="Submit")(container)
Fluent API: Building Complex Assertions¶
Assertion helpers support method chaining to build complex assertions. Each method returns a new instance (immutability preserved).
Negation: .not_()¶
Assert that an element does not exist:
# Assert no modal dialog is present
GetByRole(role="dialog").not_()(container)
# Assert button is not in the DOM
GetByText(text="Delete").not_()(container)
Text Content: .text_content(expected)¶
Verify element’s text content matches expected value:
# Button must have specific text
GetByRole(role="button").text_content("Save Changes")(container)
# Heading must have title text
GetByRole(role="heading", level=1).text_content("Welcome")(container)
Attribute Checks: .with_attribute(name, value=None)¶
Verify element has specific attributes:
# Check attribute value
GetByRole(role="button").with_attribute("type", "submit")(container)
# Check attribute exists (any value)
GetByRole(role="button").with_attribute("disabled")(container)
# Chain multiple attribute checks
GetByRole(role="button").with_attribute("type", "submit").with_attribute("aria-label", "Save")(container)
Method Chaining¶
Combine multiple modifiers:
# Complex assertion
assert_submit_button = (
GetByRole(role="button")
.text_content("Submit")
.with_attribute("type", "submit")
.with_attribute("aria-label", "Submit form")
)
# Apply to any container
assert_submit_button(container1)
assert_submit_button(container2)
List Assertions: GetAllBy* Helpers¶
For multiple elements, use GetAllBy* variants with count and selection operations.
Available List Helpers¶
from aria_testing import (
GetAllByRole,
GetAllByText,
GetAllByLabelText,
GetAllByTestId,
GetAllByClass,
GetAllByTagName,
)
Count Assertions: .count(expected)¶
Verify exact number of matching elements:
container = html(t'''
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
''')
# Assert exactly 3 list items
GetAllByRole(role="listitem").count(3)(container)
# Assert 5 buttons total
GetAllByTagName(tag_name="button").count(5)(container)
Error message on failure:
AssertionError: Expected count: 3 but found: 5 elements
Query: role='listitem'
Item Selection: .nth(index)¶
Select a specific element from the list (zero-indexed):
container = html(t'''
<div>
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>
''')
# Assert first button has specific text
GetAllByRole(role="button").nth(0).text_content("First")(container)
# Assert third button is disabled
GetAllByRole(role="button").nth(2).with_attribute("disabled")(container)
Combining Count and Selection¶
# Assert exactly 3 buttons, and verify the second one
assert_buttons = (
GetAllByRole(role="button")
.count(3)
.nth(1)
.text_content("Middle Button")
)
assert_buttons(container)
Out of Bounds Handling¶
# Only 2 buttons, but requesting index 5
GetAllByRole(role="button").nth(5)(container)
# AssertionError: Index 5 out of bounds, found 2 elements
Use Cases¶
1. Component Testing Frameworks¶
Define reusable assertions for component verification:
# Define assertion set for a card component
class CardAssertions:
title = GetByRole(role="heading", level=2)
description = GetByRole(role="paragraph")
action_button = GetByRole(role="button").text_content("Learn More")
def test_card_component():
container = render_card(title="Product", description="...", action="Learn More")
CardAssertions.title(container)
CardAssertions.description(container)
CardAssertions.action_button(container)
2. Story-Based Testing¶
Separate assertion definition from execution:
from dataclasses import dataclass
@dataclass
class Story:
name: str
render: Callable[[], Node]
assertions: list[Callable[[Node], None]]
# Define stories with assertions
button_story = Story(
name="Primary Button",
render=lambda: render_button(variant="primary"),
assertions=[
GetByRole(role="button").with_attribute("class", "btn-primary"),
GetByRole(role="button").text_content("Click Me"),
]
)
# Execute story later
def test_story(story: Story):
container = story.render()
for assertion in story.assertions:
assertion(container) # Apply each assertion
3. Test Fixtures with Deferred Verification¶
Set up fixtures with pre-defined assertions:
import pytest
@pytest.fixture
def form_assertions():
"""Reusable form assertions."""
return {
"username": GetByLabelText(label="Username").with_attribute("type", "text"),
"password": GetByLabelText(label="Password").with_attribute("type", "password"),
"submit": GetByRole(role="button", name="Submit"),
}
def test_login_form(form_assertions):
container = render_login_form()
# Apply all assertions
form_assertions["username"](container)
form_assertions["password"](container)
form_assertions["submit"](container)
4. Parameterized Testing¶
Reuse assertions across different inputs:
import pytest
# Define assertion once
assert_heading = GetByRole(role="heading", level=1)
@pytest.mark.parametrize("page", ["home", "about", "contact"])
def test_page_has_heading(page):
container = render_page(page)
assert_heading(container) # Same assertion, different containers
5. Assertion Composition¶
Build complex assertions from simpler ones:
# Base assertions
has_button = GetByRole(role="button")
has_submit_button = has_button.with_attribute("type", "submit")
has_submit_with_text = has_submit_button.text_content("Submit Form")
# Progressive refinement
def test_form():
container = render_form()
# Start simple, add constraints
has_button(container) # Any button
has_submit_button(container) # Submit button specifically
has_submit_with_text(container) # With exact text
Error Messages¶
Assertion helpers provide detailed error messages on failure:
GetByRole(role="button").text_content("Save")(container)
Error output:
AssertionError: Unable to find element with role 'button'
Query: role='button'
Searched in:
<div class="container">
<span>No buttons here</span>
</div>
Text mismatch:
AssertionError: Expected text: 'Save' but got: 'Cancel'
Query: role='button'
Attribute errors:
AssertionError: Expected attribute 'type'='submit' but got 'button'
Query: role='button'
Complete Reference¶
Single Element Helpers¶
Helper |
Parameters |
Description |
|---|---|---|
|
|
Find by ARIA role |
|
|
Find by text content |
|
|
Find by label text |
|
|
Find by data-testid |
|
|
Find by CSS class |
|
|
Find by ID attribute |
|
|
Find by HTML tag |
List Helpers (with .count() and .nth())¶
Helper |
Parameters |
Description |
|---|---|---|
|
|
Find all by ARIA role |
|
|
Find all by text content |
|
|
Find all by label text |
|
|
Find all by data-testid |
|
|
Find all by CSS class |
|
|
Find all by HTML tag |
Fluent API Methods¶
Method |
Applies To |
Description |
|---|---|---|
|
All helpers |
Assert element does NOT exist |
|
All helpers |
Verify element’s text content |
|
All helpers |
Verify attribute exists/matches |
|
|
Verify number of elements |
|
|
Select specific element (0-indexed) |
Implementation Details¶
Type Signature¶
All helpers accept Container which is defined as:
Container = Element | Fragment | Node
This allows passing any tdom structure returned by html().
Immutability¶
Helpers are frozen dataclasses—all fluent methods return new instances:
original = GetByRole(role="button")
modified = original.not_()
assert original.negate is False # Original unchanged
assert modified.negate is True # New instance created
Execution Flow¶
When calling a helper:
Execute underlying aria-testing query function
If
.not_()used:Element found → raise AssertionError
Element not found → success (no error)
Otherwise:
Element not found → raise AssertionError
Element found → check modifiers (text, attributes)
Apply
.text_content()verification if specifiedApply
.with_attribute()checks if specifiedFor
GetAllBy*with.nth():Verify index is in bounds
Select element at index
Apply text/attribute checks to selected element
Best Practices¶
✅ Do¶
Define assertions once, reuse multiple times
Use descriptive variable names (
assert_submit_buttonnothelper)Combine with test fixtures for reusable assertion sets
Use
.not_()for absence assertions rather than wrapping in try/exceptPrefer semantic queries (
GetByRole,GetByLabelText) over test IDs
❌ Don’t¶
Don’t mutate helpers (they’re frozen—won’t work anyway)
Don’t catch AssertionError to implement custom logic (defeats the purpose)
Don’t create one-off helpers for simple direct queries (just use the query functions)
Don’t over-chain modifiers when separate assertions would be clearer
Migration from Direct Queries¶
Before (imperative):
def test_form():
container = render_form()
button = get_by_role(container, "button")
assert get_text_content(button) == "Submit"
assert button.attrs["type"] == "submit"
After (declarative):
assert_submit = (
GetByRole(role="button")
.text_content("Submit")
.with_attribute("type", "submit")
)
def test_form():
container = render_form()
assert_submit(container)
See Also¶
Query Reference - Underlying query functions
Examples - More usage examples
API Reference - Complete API documentation