1143 lines
28 KiB
Markdown
1143 lines
28 KiB
Markdown
# 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:**
|
|
```python
|
|
# Fragile - breaks if whitespace or attribute order changes
|
|
assert str(component.render()) == '<div id="x" class="y"><p>Text</p></div>'
|
|
```
|
|
|
|
**With matcher:**
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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)
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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:**
|
|
|
|
```python
|
|
buttons = find(page, Button())
|
|
assert len(buttons) == 3, f"Expected 3 buttons, found {len(buttons)}"
|
|
```
|
|
|
|
**Check element exists:**
|
|
|
|
```python
|
|
submit_buttons = find(page, Button(type="submit"))
|
|
assert len(submit_buttons) > 0, "No submit button found"
|
|
```
|
|
|
|
**Extract for further testing:**
|
|
|
|
```python
|
|
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:**
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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:**
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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:**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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. Testing MyFastHtml Components with Test Helpers
|
|
|
|
**Goal**: Understand why test helpers exist and how they simplify testing MyFastHtml controls.
|
|
|
|
When testing components built with `mk` helpers (buttons, labels, icons), writing the expected pattern manually quickly becomes verbose. Consider testing a label with an icon:
|
|
|
|
```python
|
|
# Without test helpers - verbose and fragile
|
|
from fastcore.basics import NotStr
|
|
from fasthtml.common import Span
|
|
from myfasthtml.test.matcher import matches, Regex
|
|
|
|
actual = label_component.render()
|
|
expected = Span(
|
|
Span(NotStr('<svg name="fluent-Info"')),
|
|
Span("My Label")
|
|
)
|
|
matches(actual, expected)
|
|
```
|
|
|
|
MyFastHtml provides **test helpers** — specialized `TestObject` subclasses that encapsulate these patterns. They know how `mk` renders components and abstract away the implementation details:
|
|
|
|
```python
|
|
# With test helpers - concise and readable
|
|
from myfasthtml.test.matcher import matches, TestLabel
|
|
|
|
actual = label_component.render()
|
|
matches(actual, TestLabel("My Label", icon="info"))
|
|
```
|
|
|
|
`TestObject` is the base class for all these helpers. You can also create your own helpers for custom components by subclassing it.
|
|
|
|
**TestObject constructor:**
|
|
|
|
| Parameter | Type | Description |
|
|
|-----------|------|-------------|
|
|
| `cls` | type or str | The element type to match (e.g., `"div"`, `"span"`, `NotStr`) |
|
|
| `**kwargs` | any | Attributes to match on the element |
|
|
|
|
**Creating a custom helper:**
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import TestObject, Contains
|
|
from fasthtml.common import Div, H2
|
|
|
|
class TestCard(TestObject):
|
|
def __init__(self, title: str):
|
|
super().__init__("div")
|
|
self.attrs["cls"] = Contains("card")
|
|
self.children = [
|
|
Div(H2(title), cls="card-header")
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
### 6. Testing Labels with TestLabel
|
|
|
|
**Goal**: Verify elements produced by `mk.label()` — text with an optional icon and optional command.
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import TestLabel
|
|
```
|
|
|
|
| Parameter | Type | Description | Default |
|
|
|-----------|------|-------------|---------|
|
|
| `label` | str | The text content to match | - |
|
|
| `icon` | str | Icon name (`snake_case` or `PascalCase`) | `None` |
|
|
| `command` | Command | Command whose HTMX params to verify | `None` |
|
|
|
|
**Example 1: Label with text only**
|
|
|
|
```python
|
|
from myfasthtml.controls.helpers import mk
|
|
from myfasthtml.test.matcher import matches, TestLabel
|
|
|
|
actual = mk.label("Settings")
|
|
matches(actual, TestLabel("Settings"))
|
|
```
|
|
|
|
**Example 2: Label with icon**
|
|
|
|
```python
|
|
from myfasthtml.controls.helpers import mk
|
|
from myfasthtml.test.matcher import matches, TestLabel
|
|
|
|
actual = mk.label("Settings", icon="settings")
|
|
matches(actual, TestLabel("Settings", icon="settings"))
|
|
```
|
|
|
|
**Note:** Icon names can be passed in `snake_case` or `PascalCase` — `TestLabel` handles the conversion automatically.
|
|
|
|
**Example 3: Label with command**
|
|
|
|
```python
|
|
from myfasthtml.controls.helpers import mk
|
|
from myfasthtml.core.commands import Command
|
|
from myfasthtml.test.matcher import matches, TestLabel
|
|
|
|
def save():
|
|
return "Saved"
|
|
|
|
save_cmd = Command("save", "Save document", save)
|
|
actual = mk.label("Save", command=save_cmd)
|
|
|
|
matches(actual, TestLabel("Save", command=save_cmd))
|
|
```
|
|
|
|
---
|
|
|
|
### 7. Testing Icons with TestIcon and TestIconNotStr
|
|
|
|
**Goal**: Verify icon elements produced by `mk.icon()`.
|
|
|
|
MyFastHtml renders icons in two ways depending on context:
|
|
- **With a wrapper** (`div` or `span`): use `TestIcon`
|
|
- **As a raw SVG `NotStr`** without wrapper: use `TestIconNotStr`
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import TestIcon, TestIconNotStr
|
|
```
|
|
|
|
#### TestIcon — Icon with wrapper
|
|
|
|
| Parameter | Type | Description | Default |
|
|
|-----------|------|-------------|---------|
|
|
| `name` | str | Icon name (`snake_case` or `PascalCase`) | `''` |
|
|
| `wrapper` | str | Wrapper element: `"div"` or `"span"` | `"div"` |
|
|
| `command` | Command | Command whose HTMX params to verify | `None` |
|
|
|
|
**Example 1: Simple icon in a div**
|
|
|
|
```python
|
|
from myfasthtml.controls.helpers import mk
|
|
from myfasthtml.test.matcher import matches, TestIcon
|
|
|
|
actual = mk.icon(info_svg)
|
|
matches(actual, TestIcon("info"))
|
|
```
|
|
|
|
**Example 2: Icon in a span with command**
|
|
|
|
```python
|
|
from myfasthtml.controls.helpers import mk
|
|
from myfasthtml.core.commands import Command
|
|
from myfasthtml.test.matcher import matches, TestIcon
|
|
|
|
delete_cmd = Command("delete", "Delete item", lambda: None)
|
|
actual = mk.icon(trash_svg, command=delete_cmd)
|
|
|
|
matches(actual, TestIcon("trash", wrapper="span", command=delete_cmd))
|
|
```
|
|
|
|
#### TestIconNotStr — Raw SVG without wrapper
|
|
|
|
Use `TestIconNotStr` when the icon appears directly as a `NotStr` in the element tree (e.g., embedded inside another element without its own wrapper).
|
|
|
|
| Parameter | Type | Description | Default |
|
|
|-----------|------|-------------|---------|
|
|
| `name` | str | Icon name (`snake_case` or `PascalCase`) | `''` |
|
|
|
|
**Example: Raw SVG icon embedded in a button**
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import find, TestIconNotStr
|
|
|
|
actual = button_component.render()
|
|
|
|
icons = find(actual, TestIconNotStr("info"))
|
|
assert len(icons) == 1
|
|
```
|
|
|
|
---
|
|
|
|
### 8. Testing Commands with TestCommand
|
|
|
|
**Goal**: Verify that an element is linked to a specific command.
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import TestCommand
|
|
```
|
|
|
|
| Parameter | Type | Description |
|
|
|-----------|------|-------------|
|
|
| `name` | str | The command name to match |
|
|
| `**kwargs` | any | Additional command attributes to verify |
|
|
|
|
**Example 1: Verify a button is bound to a command**
|
|
|
|
```python
|
|
from myfasthtml.controls.helpers import mk
|
|
from myfasthtml.core.commands import Command
|
|
from myfasthtml.test.matcher import find, TestCommand
|
|
|
|
delete_cmd = Command("delete_row", "Delete row", lambda: None)
|
|
button = mk.button("Delete", command=delete_cmd)
|
|
|
|
commands = find(button, TestCommand("delete_row"))
|
|
assert len(commands) == 1
|
|
```
|
|
|
|
**Example 2: Verify command with additional attributes**
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import find, TestCommand
|
|
|
|
commands = find(component.render(), TestCommand("save", target="#result"))
|
|
assert len(commands) == 1
|
|
```
|
|
|
|
---
|
|
|
|
### 9. Testing Scripts with TestScript
|
|
|
|
**Goal**: Verify the content of `<script>` elements injected by a component.
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import TestScript
|
|
```
|
|
|
|
| Parameter | Type | Description |
|
|
|-----------|------|-------------|
|
|
| `script` | str | The expected script content (checked with `startswith`) |
|
|
|
|
**Example 1: Verify a script is present**
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import find, TestScript
|
|
|
|
actual = widget.render()
|
|
|
|
scripts = find(actual, TestScript("initWidget("))
|
|
assert len(scripts) == 1
|
|
```
|
|
|
|
**Example 2: Verify a specific script content**
|
|
|
|
```python
|
|
from myfasthtml.test.matcher import find, TestScript
|
|
|
|
scripts = find(actual, TestScript("document.getElementById('my-component')"))
|
|
assert len(scripts) == 1
|
|
```
|
|
|
|
---
|
|
|
|
### 10. 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**
|
|
|
|
```python
|
|
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**
|
|
|
|
```python
|
|
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)
|
|
```
|
|
|
|
### 11. Testing Edge Cases
|
|
|
|
**Testing empty elements:**
|
|
|
|
```python
|
|
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:**
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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**
|
|
```python
|
|
# ✅ 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**
|
|
```python
|
|
# ✅ 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**
|
|
```python
|
|
# ✅ 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()**
|
|
```python
|
|
# ✅ 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**
|
|
```python
|
|
# ❌ 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**
|
|
```python
|
|
# ❌ Bad - fragile
|
|
assert str(component.render()) == '<div><p>Text</p></div>'
|
|
|
|
# ✅ Good - structural matching
|
|
matches(component.render(), Div(P("Text")))
|
|
```
|
|
|
|
**Don't over-specify**
|
|
```python
|
|
# ❌ 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**
|
|
```python
|
|
# Define reusable patterns
|
|
STANDARD_BUTTON = Button(cls=Contains("btn"))
|
|
|
|
# Use in multiple tests
|
|
matches(actual, Div(STANDARD_BUTTON))
|
|
```
|
|
|
|
**Use find() efficiently**
|
|
```python
|
|
# ✅ 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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! 🧪
|