Files
MyFastHtml/docs/testing_rendered_components.md

21 KiB

Testing Rendered Components with Matcher

Introduction

When testing FastHTML components, you need to verify that the HTML they generate is correct. Traditional approaches like string comparison are fragile and hard to maintain. The matcher module provides two powerful functions that make component testing simple and reliable:

  • matches(actual, expected) - Validates that a rendered element matches your expectations
  • find(ft, expected) - Searches for specific elements within an HTML tree

Key principle: Test only what matters. The matcher compares only the elements and attributes you explicitly define in your expected pattern, ignoring everything else.

Why use matcher?

Without matcher:

# Fragile - breaks if whitespace or attribute order changes
assert str(component.render()) == '<div id="x" class="y"><p>Text</p></div>'

With matcher:

# Robust - tests only what matters
from myfasthtml.test.matcher import matches
from fasthtml.common import Div, P

actual = component.render()
expected = Div(P("Text"))
matches(actual, expected)  # Passes - ignores id and class

Part 1: Function Reference

matches() - Validate Elements

Purpose

matches() validates that a rendered element structure corresponds exactly to an expected pattern. It's the primary tool for testing component rendering.

When to use it:

  • Verifying component output in tests
  • Checking HTML structure
  • Validating attributes and content

Basic Syntax

from myfasthtml.test.matcher import matches

matches(actual, expected)
  • actual: The element to test (usually from component.render())
  • expected: The pattern to match against (only include what you want to test)
  • Returns: True if matches, raises AssertionError if not

Simple Examples

Example 1: Basic structure matching

from myfasthtml.test.matcher import matches
from fasthtml.common import Div, P

# The actual rendered element
actual = Div(P("Hello World"), id="container", cls="main")

# Expected pattern - tests only the structure
expected = Div(P("Hello World"))

matches(actual, expected)  # ✅ Passes - id and cls are ignored

Example 2: Testing specific attributes

from fasthtml.common import Button

actual = Button("Click me",
                id="btn-1",
                cls="btn btn-primary",
                hx_post="/submit",
                hx_target="#result")

# Test only the HTMX attribute we care about
expected = Button("Click me", hx_post="/submit")

matches(actual, expected)  # ✅ Passes

Example 3: Nested structure

from fasthtml.common import Div, H1, Form, Input, Button

actual = Div(
    H1("Registration Form"),
    Form(
        Input(name="email", type="email"),
        Input(name="password", type="password"),
        Button("Submit", type="submit")
    ),
    id="page",
    cls="container"
)

# Test only the important parts
expected = Div(
    H1("Registration Form"),
    Form(
        Input(name="email"),
        Button("Submit")
    )
)

matches(actual, expected)  # ✅ Passes - ignores password field and attributes

Predicates Reference

Predicates allow flexible validation when you don't know the exact value but want to validate a pattern.

AttrPredicate - For attribute values

Contains(value) - Attribute contains the value

from myfasthtml.test.matcher import Contains
from fasthtml.common import Div

actual = Div(cls="container main-content active")
expected = Div(cls=Contains("main-content"))

matches(actual, expected)  # ✅ Passes

StartsWith(value) - Attribute starts with the value

from myfasthtml.test.matcher import StartsWith
from fasthtml.common import Input

actual = Input(id="input-username-12345")
expected = Input(id=StartsWith("input-username"))

matches(actual, expected)  # ✅ Passes

DoesNotContain(value) - Attribute does not contain the value

from myfasthtml.test.matcher import DoesNotContain
from fasthtml.common import Div

actual = Div(cls="container active")
expected = Div(cls=DoesNotContain("disabled"))

matches(actual, expected)  # ✅ Passes

AnyValue() - Attribute exists with any non-None value

from myfasthtml.test.matcher import AnyValue
from fasthtml.common import Button

actual = Button("Click", data_action="submit-form", data_id="123")
expected = Button("Click", data_action=AnyValue())

matches(actual, expected)  # ✅ Passes - just checks data_action exists
ChildrenPredicate - For element children

Empty() - Element has no children and no attributes

from myfasthtml.test.matcher import Empty
from fasthtml.common import Div

actual = Div()
expected = Div(Empty())

matches(actual, expected)  # ✅ Passes

NoChildren() - Element has no children (but can have attributes)

from myfasthtml.test.matcher import NoChildren
from fasthtml.common import Div

actual = Div(id="container", cls="empty")
expected = Div(NoChildren())

matches(actual, expected)  # ✅ Passes - has attributes but no children

AttributeForbidden(attr_name) - Attribute must not be present

from myfasthtml.test.matcher import AttributeForbidden
from fasthtml.common import Button

actual = Button("Click me")
expected = Button("Click me", AttributeForbidden("disabled"))

matches(actual, expected)  # ✅ Passes - disabled attribute is not present

Error Messages Explained

When a test fails, matches() provides a visual diff showing exactly where the problem is:

from fasthtml.common import Div, Button

actual = Div(Button("Submit", cls="btn-primary"), id="form")
expected = Div(Button("Cancel", cls="btn-secondary"))

matches(actual, expected)

Error output:

Path  : 'div.button'
Error : The values are different

(div "id"="form"           | (div
  (button "cls"="btn-prim  |   (button "cls"="btn-seco
          ^^^^^^^^^^^^^^^^ |           
    "Submit")              |     "Cancel")
    ^^^^^^^                |   
)                          | )

Reading the error:

  • Left side: Actual element
  • Right side: Expected pattern
  • ^^^ markers: Highlight differences 'only on the left side', the right side (the expected pattern) is always correct
  • Path: Shows location in the tree (div.button = button inside div)

find() - Search Elements

Purpose

find() searches for all elements matching a pattern within an HTML tree. It's useful when you need to verify the presence of specific elements without knowing their exact position. Or when you want to validate (using matches) a subset of elements.

When to use it:

  • Finding elements by attributes
  • Verifying element count
  • Extracting elements for further validation
  • Testing without strict hierarchy requirements

Basic Syntax

from myfasthtml.test.matcher import find

results = find(ft, expected)
  • ft: Element or list of elements to search in
  • expected: Pattern to match, follows the same syntax and rules as matches()
  • Returns: List of all matching elements
  • Raises: AssertionError if no matches found

Simple Examples

Example 1: Find all elements of a type

from myfasthtml.test.matcher import find
from fasthtml.common import Div, P

page = Div(
    Div(P("First paragraph")),
    Div(P("Second paragraph")),
    P("Third paragraph")
)

# Find all paragraphs
paragraphs = find(page, P())

assert len(paragraphs) == 3

Example 2: Find by attribute

from fasthtml.common import Div, Button
from myfasthtml.test.matcher import find

page = Div(
    Button("Cancel", cls="btn-secondary"),
    Div(Button("Submit", cls="btn-primary", id="submit"))
)

# Find the primary button
primary_buttons = find(page, Button(cls="btn-primary"))

assert len(primary_buttons) == 1
assert primary_buttons[0].attrs["id"] == "submit"

Example 3: Find nested structure

from fasthtml.common import Div, Form, Input

page = Div(
    Div(Input(name="search")),
    Form(
        Input(name="email", type="email"),
        Input(name="password", type="password")
    )
)

# Find all email inputs
email_inputs = find(page, Input(type="email"))

assert len(email_inputs) == 1
assert email_inputs[0].attrs["name"] == "email"

Example 4: Search in a list

from fasthtml.common import Div, P, Span

elements = [
    Div(P("First")),
    Div(P("Second")),
    Span(P("Third"))
]

# Find all paragraphs across all elements
all_paragraphs = find(elements, P())

assert len(all_paragraphs) == 3

Common Patterns

Verify element count:

buttons = find(page, Button())
assert len(buttons) == 3, f"Expected 3 buttons, found {len(buttons)}"

Check element exists:

submit_buttons = find(page, Button(type="submit"))
assert len(submit_buttons) > 0, "No submit button found"

Extract for further testing:

form = find(page, Form())[0]  # Get first form
inputs = find(form, Input())   # Find inputs within form
assert all(inp.attrs.get("type") in ["text", "email"] for inp in inputs)

Handle missing elements:

try:
    admin_section = find(page, Div(id="admin"))
    print("Admin section found")
except AssertionError:
    print("Admin section not present")

Part 2: Testing Rendered Components Guide

This section provides practical patterns for testing different aspects of rendered components.

1. Testing Element Structure

Goal: Verify the hierarchy and organization of elements.

Pattern: Use matches() with only the structural elements you care about.

from myfasthtml.test.matcher import matches
from fasthtml.common import Div, Header, Nav, Main, Footer

# Your component renders a page layout
actual = page_component.render()

# Test only the main structure
expected = Div(
    Header(Nav()),
    Main(),
    Footer()
)

matches(actual, expected)

Tip: Don't include every single child element. Focus on the important structural elements.

2. Testing Attributes

Goal: Verify that specific attributes are set correctly.

Pattern: Include only the attributes you want to test.

from fasthtml.common import Button

# Component renders a button with HTMX
actual = button_component.render()

# Test only HTMX attributes
expected = Button(
    hx_post="/api/submit",
    hx_target="#result",
    hx_swap="innerHTML"
)

matches(actual, expected)

With predicates for dynamic values:

from myfasthtml.test.matcher import StartsWith

# Test that id follows a pattern
expected = Button(id=StartsWith("btn-"))
matches(actual, expected)

3. Testing Content

Goal: Verify text content in elements.

Pattern: Match elements with their text content.

from fasthtml.common import Div, H1, P

actual = article_component.render()

expected = Div(
    H1("Article Title"),
    P("First paragraph content")
)

matches(actual, expected)

Partial content matching:

from myfasthtml.test.matcher import Contains

# Check that paragraph contains key phrase
expected = P(Contains("important information"))
matches(actual, expected)

4. Testing with Predicates

Goal: Validate patterns rather than exact values.

Pattern: Use predicates for flexibility.

Example: Testing generated IDs

from myfasthtml.test.matcher import StartsWith, AnyValue
from fasthtml.common import Div

# Component generates unique IDs
actual = widget_component.render()

expected = Div(
    id=StartsWith("widget-"),
    data_timestamp=AnyValue()  # Just check it exists
)

matches(actual, expected)

Example: Testing CSS classes

from myfasthtml.test.matcher import Contains
from fasthtml.common import Button

actual = dynamic_button.render()

# Check button has 'active' class among others
expected = Button(cls=Contains("active"))
matches(actual, expected)

Example: Forbidden attributes

from myfasthtml.test.matcher import AttributeForbidden
from fasthtml.common import Input

# Verify input is NOT disabled
actual = input_component.render()

expected = Input(
    name="username",
    AttributeForbidden("disabled")
)

matches(actual, expected)

5. Combining matches() and find()

Goal: First find elements, then validate them in detail.

Pattern: Use find() to locate, then matches() to validate.

Example: Testing a form

from myfasthtml.test.matcher import find, matches
from fasthtml.common import Form, Input, Button

# Render a complex page
actual = page_component.render()

# Step 1: Find the registration form
forms = find(actual, Form(id="registration"))
assert len(forms) == 1

# Step 2: Validate the form structure
registration_form = forms[0]
expected_form = Form(
    Input(name="email", type="email"),
    Input(name="password", type="password"),
    Button("Register", type="submit")
)

matches(registration_form, expected_form)

Example: Testing multiple similar elements

from fasthtml.common import Div, Card

actual = dashboard.render()

# Find all cards
cards = find(actual, Card())

# Verify we have the right number
assert len(cards) == 3

# Validate each card has required structure
for card in cards:
    expected_card = Card(
        Div(cls="card-header"),
        Div(cls="card-body")
    )
    matches(card, expected_card)

6. Testing Edge Cases

Testing empty elements:

from myfasthtml.test.matcher import Empty, NoChildren
from fasthtml.common import Div

# Test completely empty element
actual = Div()
expected = Div(Empty())
matches(actual, expected)

# Test element with attributes but no children
actual = Div(id="container", cls="empty-state")
expected = Div(NoChildren())
matches(actual, expected)

Testing absence of elements:

from myfasthtml.test.matcher import find
from fasthtml.common import Div

actual = basic_view.render()

# Verify admin section is not present
try:
    find(actual, Div(id="admin-section"))
    assert False, "Admin section should not be present"
except AssertionError as e:
    if "No element found" in str(e):
        pass  # Expected - admin section absent
    else:
        raise

Part 3: How It Works (Technical Overview)

Understanding how the matcher works helps you write better tests and debug failures more effectively.

The Matching Algorithm

When you call matches(actual, expected), here's what happens:

  1. Type Comparison: Checks if elements have the same type/tag

    • For FastHTML elements: compares .tag (e.g., "div", "button")
    • For Python objects: compares class types
  2. Attribute Validation: For each attribute in expected:

    • Checks if attribute exists in actual
    • If it's a Predicate: calls .validate(actual_value)
    • Otherwise: checks for exact equality
    • Key point: Only attributes in expected are checked
  3. Children Validation:

    • Applies ChildrenPredicate validators if present
    • Recursively matches children in order
    • Key point: Only checks as many children as defined in expected
  4. Path Tracking: Maintains a path through the tree for error reporting

Understanding the Path

The matcher builds a path as it traverses your element tree:

div#container.form.input[name=email]

This path means:

  • Started at a div with id="container"
  • Went to a form element
  • Then to an input with name="email"

The path appears in error messages to help you locate problems:

Path  : 'div#form.input[name=email]'
Error : 'type' is not found in Actual

How Predicates Work

Predicates are objects that implement a validate() method:

class Contains(AttrPredicate):
    def validate(self, actual):
        return self.value in actual

When matching encounters a predicate:

  1. Gets the actual attribute value
  2. Calls predicate.validate(actual_value)
  3. If returns False, reports validation error

This allows flexible matching without hardcoding exact values.

Error Output Generation

When a test fails, the matcher generates a side-by-side comparison:

Process:

  1. Renders both actual and expected as tree structures
  2. Compares them element by element
  3. Marks differences with ^^^ characters
  4. Aligns output for easy visual comparison

Example:

(div "id"="old"        | (div "id"="new"
     ^^^^              |      

The ^^^ appears under attributes or content that don't match.

The find() Algorithm

find() uses depth-first search:

  1. Check current element: Does it match the pattern?

    • If yes: add to results
  2. Search children: Recursively search all children

  3. Return all matches: Collects matches from entire tree

Key difference from matches(): find() looks for any occurrence anywhere in the tree, while matches() validates exact structure.


Best Practices

Do's

Test only what matters

# ✅ Good - tests only the submit action
expected = Button("Submit", hx_post="/api/save")

# ❌ Bad - tests irrelevant details
expected = Button("Submit", hx_post="/api/save", id="btn-123", cls="btn btn-primary")

Use predicates for dynamic values

# ✅ Good - flexible for generated IDs
expected = Div(id=StartsWith("generated-"))

# ❌ Bad - brittle, will break on regeneration
expected = Div(id="generated-12345")

Structure tests in layers

# ✅ Good - separate concerns
# Test 1: Overall structure
matches(page, Div(Header(), Main(), Footer()))

# Test 2: Header details
header = find(page, Header())[0]
matches(header, Header(Nav(), Div(cls="user-menu")))

# ❌ Bad - everything in one giant test
matches(page, Div(
    Header(Nav(...), Div(...)),
    Main(...),
    Footer(...)
))

Verify element counts with find()

# ✅ Good - explicit count check
buttons = find(page, Button())
assert len(buttons) == 3, f"Expected 3 buttons, found {len(buttons)}"

# ❌ Bad - implicit assumption
button = find(page, Button())[0]  # Fails if 0 or multiple buttons

Don'ts

Don't test implementation details

# ❌ Bad - internal div structure might change
expected = Div(
    Div(
        Div(Button("Click"))
    )
)

# ✅ Good - test the important element
expected = Div(Button("Click"))

Don't use exact string matching

# ❌ Bad - fragile
assert str(component.render()) == '<div><p>Text</p></div>'

# ✅ Good - structural matching
matches(component.render(), Div(P("Text")))

Don't over-specify

# ❌ Bad - tests too much
expected = Form(
    Input(name="email", type="email", id="email-input", cls="form-control"),
    Input(name="password", type="password", id="pwd-input", cls="form-control"),
    Button("Submit", type="submit", id="submit-btn", cls="btn btn-primary")
)

# ✅ Good - tests what matters
expected = Form(
    Input(name="email", type="email"),
    Input(name="password", type="password"),
    Button("Submit", type="submit")
)

Performance Tips

Reuse patterns

# Define reusable patterns
STANDARD_BUTTON = Button(cls=Contains("btn"))

# Use in multiple tests
matches(actual, Div(STANDARD_BUTTON))

Use find() efficiently

# ✅ Good - specific pattern
buttons = find(page, Button(cls="primary"))

# ❌ Inefficient - too broad then filter
all_buttons = find(page, Button())
primary = [b for b in all_buttons if "primary" in b.attrs.get("cls", "")]

Quick Reference

Import Statements

# Core functions
from myfasthtml.test.matcher import matches, find

# AttrPredicates
from myfasthtml.test.matcher import Contains, StartsWith, DoesNotContain, AnyValue

# ChildrenPredicates
from myfasthtml.test.matcher import Empty, NoChildren, AttributeForbidden

# For custom test objects
from myfasthtml.test.matcher import TestObject

Common Patterns Cheatsheet

# Basic validation
matches(actual, expected)

# Find and validate
elements = find(tree, pattern)
assert len(elements) == 1
matches(elements[0], detailed_pattern)

# Flexible attribute matching
expected = Div(
    id=StartsWith("prefix-"),
    cls=Contains("active"),
    data_value=AnyValue()
)

# Empty element validation
expected = Div(Empty())          # No children, no attributes
expected = Div(NoChildren())     # No children, attributes OK

# Forbidden attribute
expected = Button(AttributeForbidden("disabled"))

# Multiple children
expected = Div(
    Header(),
    Main(),
    Footer()
)

Conclusion

The matcher module provides powerful tools for testing FastHTML components:

  • matches() validates structure with precise, readable error messages
  • find() locates elements anywhere in your HTML tree
  • Predicates enable flexible, maintainable tests

By focusing on what matters and using the right patterns, you can write component tests that are both robust and easy to maintain.

Next steps:

  • Practice with simple components first
  • Gradually introduce predicates as needed
  • Review error messages carefully - they guide you to the problem
  • Refactor tests to remove duplication

Happy testing! 🧪