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:

  1. Define assertions early, before the DOM exists

  2. Execute assertions later, when the container becomes available

  3. Reuse assertions across multiple test scenarios

  4. 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 | Node via __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

GetByRole

role, level=None, name=None

Find by ARIA role

GetByText

text

Find by text content

GetByLabelText

label

Find by label text

GetByTestId

test_id

Find by data-testid

GetByClass

class_name

Find by CSS class

GetById

id

Find by ID attribute

GetByTagName

tag_name

Find by HTML tag

List Helpers (with .count() and .nth())

Helper

Parameters

Description

GetAllByRole

role, level=None, name=None

Find all by ARIA role

GetAllByText

text

Find all by text content

GetAllByLabelText

label

Find all by label text

GetAllByTestId

test_id

Find all by data-testid

GetAllByClass

class_name

Find all by CSS class

GetAllByTagName

tag_name

Find all by HTML tag

Fluent API Methods

Method

Applies To

Description

.not_()

All helpers

Assert element does NOT exist

.text_content(expected)

All helpers

Verify element’s text content

.with_attribute(name, value=None)

All helpers

Verify attribute exists/matches

.count(expected)

GetAllBy* only

Verify number of elements

.nth(index)

GetAllBy* only

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:

  1. Execute underlying aria-testing query function

  2. If .not_() used:

    • Element found → raise AssertionError

    • Element not found → success (no error)

  3. Otherwise:

    • Element not found → raise AssertionError

    • Element found → check modifiers (text, attributes)

  4. Apply .text_content() verification if specified

  5. Apply .with_attribute() checks if specified

  6. For 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_button not helper)

  • Combine with test fixtures for reusable assertion sets

  • Use .not_() for absence assertions rather than wrapping in try/except

  • Prefer 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