Added Controls testing + documentation

This commit is contained in:
2025-12-05 19:17:21 +01:00
parent 1d20fb8650
commit 8e5fa7f752
37 changed files with 4868 additions and 363 deletions

583
docs/Layout.md Normal file
View File

@@ -0,0 +1,583 @@
# Layout Component
## Introduction
The Layout component provides a complete application structure with fixed header and footer, a scrollable main content
area, and optional collapsible side drawers. It's designed to be the foundation of your FastHTML application's UI.
**Key features:**
- Fixed header and footer that stay visible while scrolling
- Collapsible left and right drawers for navigation, tools, or auxiliary content
- Resizable drawers with drag handles
- Automatic state persistence per session
- Single instance per session (singleton pattern)
**Common use cases:**
- Application with navigation sidebar
- Dashboard with tools panel
- Admin interface with settings drawer
- Documentation site with table of contents
## Quick Start
Here's a minimal example showing an application with a navigation sidebar:
```python
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
# Create the layout instance
layout = Layout(parent=root_instance, app_name="My App")
# Add navigation items to the left drawer
layout.left_drawer.add(
mk.mk(Div("Home"), command=Command(...))
)
layout.left_drawer.add(
mk.mk(Div("About"), command=Command(...))
)
layout.left_drawer.add(
mk.mk(Div("Contact"), command=Command(...))
)
# Set the main content
layout.set_main(
Div(
H1("Welcome"),
P("This is the main content area")
)
)
# Render the layout
return layout
```
This creates a complete application layout with:
- A header displaying the app name and drawer toggle button
- A collapsible left drawer with interactive navigation items
- A main content area that updates when navigation items are clicked
- An empty footer
**Note:** Navigation items use commands to update the main content area without page reload. See the Commands section
below for details.
## Basic Usage
### Creating a Layout
The Layout component is a `SingleInstance`, meaning there's only one instance per session. Create it by providing a
parent instance and an application name:
```python
layout = Layout(parent=root_instance, app_name="My Application")
```
### Content Zones
The Layout provides six content zones where you can add components:
```
┌──────────────────────────────────────────────────────────┐
│ Header │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ header_left │ │ header_right │ │
│ └─────────────────┘ └─────────────────┘ │
├─────────┬────────────────────────────────────┬───────────┤
│ │ │ │
│ left │ │ right │
│ drawer │ Main Content │ drawer │
│ │ │ │
│ │ │ │
├─────────┴────────────────────────────────────┴───────────┤
│ Footer │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ footer_left │ │ footer_right │ │
│ └─────────────────┘ └─────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
**Zone details:**
| Zone | Typical Use |
|----------------|-----------------------------------------------|
| `header_left` | App logo, menu button, breadcrumbs |
| `header_right` | User profile, notifications, settings |
| `left_drawer` | Navigation menu, tree view, filters |
| `right_drawer` | Tools panel, properties inspector, debug info |
| `footer_left` | Copyright, legal links, version |
| `footer_right` | Status indicators, connection state |
### Adding Content to Zones
Use the `.add()` method to add components to any zone:
```python
# Header
layout.header_left.add(Div("Logo"))
layout.header_right.add(Div("User: Admin"))
# Drawers
layout.left_drawer.add(Div("Navigation"))
layout.right_drawer.add(Div("Tools"))
# Footer
layout.footer_left.add(Div("© 2024 My App"))
layout.footer_right.add(Div("v1.0.0"))
```
### Setting Main Content
The main content area displays your page content and can be updated dynamically:
```python
# Set initial content
layout.set_main(
Div(
H1("Dashboard"),
P("Welcome to your dashboard")
)
)
# Update later (typically via commands)
layout.set_main(
Div(
H1("Settings"),
P("Configure your preferences")
)
)
```
### Controlling Drawers
By default, both drawers are visible. The drawer state is managed automatically:
- Users can toggle drawers using the icon buttons in the header
- Users can resize drawers by dragging the handle
- Drawer state persists within the session
The initial drawer widths are:
- Left drawer: 250px
- Right drawer: 250px
These can be adjusted by users and the state is preserved automatically.
## Content System
### Understanding Groups
Each content zone (header_left, header_right, drawers, footer) supports **groups** to organize related items. Groups are
separated visually by dividers and can have optional labels.
### Adding Content to Groups
When adding content, you can optionally specify a group name:
```python
# Add items to different groups in the left drawer
layout.left_drawer.add(Div("Dashboard"), group="main")
layout.left_drawer.add(Div("Analytics"), group="main")
layout.left_drawer.add(Div("Settings"), group="preferences")
layout.left_drawer.add(Div("Profile"), group="preferences")
```
This creates two groups:
- **main**: Dashboard, Analytics
- **preferences**: Settings, Profile
A visual divider automatically appears between groups.
### Custom Group Labels
You can provide a custom FastHTML element to display as the group header:
```python
# Add a styled group header
layout.left_drawer.add_group(
"Navigation",
group_ft=Div("MAIN MENU", cls="font-bold text-sm opacity-60 px-2 py-1")
)
# Then add items to this group
layout.left_drawer.add(Div("Home"), group="Navigation")
layout.left_drawer.add(Div("About"), group="Navigation")
```
### Ungrouped Content
If you don't specify a group, content is added to the default (`None`) group:
```python
# These items are in the default group
layout.left_drawer.add(Div("Quick Action 1"))
layout.left_drawer.add(Div("Quick Action 2"))
```
### Preventing Duplicates
The Content system automatically prevents adding duplicate items based on their `id` attribute:
```python
item = Div("Unique Item", id="my-item")
layout.left_drawer.add(item)
layout.left_drawer.add(item) # Ignored - already added
```
### Group Rendering Options
Groups render differently depending on the zone:
**In drawers** (vertical layout):
- Groups stack vertically
- Dividers are horizontal lines
- Group labels appear above their content
**In header/footer** (horizontal layout):
- Groups arrange side-by-side
- Dividers are vertical lines
- Group labels are typically hidden
## Advanced Features
### Resizable Drawers
Both drawers can be resized by users via drag handles:
- **Drag handle location**:
- Left drawer: Right edge
- Right drawer: Left edge
- **Width constraints**: 150px (minimum) to 600px (maximum)
- **Persistence**: Resized width is automatically saved in the session state
Users can drag the handle to adjust drawer width. The new width is preserved throughout their session.
### Programmatic Drawer Control
You can control drawers programmatically using commands:
```python
# Toggle drawer visibility
toggle_left = layout.commands.toggle_drawer("left")
toggle_right = layout.commands.toggle_drawer("right")
# Update drawer width
update_left_width = layout.commands.update_drawer_width("left", width=300)
update_right_width = layout.commands.update_drawer_width("right", width=350)
```
These commands are typically used with buttons or other interactive elements:
```python
# Add a button to toggle the right drawer
button = mk.button("Toggle Tools", command=layout.commands.toggle_drawer("right"))
layout.header_right.add(button)
```
### State Persistence
The Layout automatically persists its state within the user's session:
| State Property | Description | Default |
|----------------------|---------------------------------|---------|
| `left_drawer_open` | Whether left drawer is visible | `True` |
| `right_drawer_open` | Whether right drawer is visible | `True` |
| `left_drawer_width` | Left drawer width in pixels | `250` |
| `right_drawer_width` | Right drawer width in pixels | `250` |
State changes (toggle, resize) are automatically saved and restored within the session.
### Dynamic Content Updates
Content zones can be updated dynamically during the session:
```python
# Initial setup
layout.left_drawer.add(Div("Item 1"))
# Later, add more items (e.g., in a command handler)
def add_dynamic_content():
layout.left_drawer.add(Div("New Item"), group="dynamic")
return layout.left_drawer # Return updated drawer for HTMX swap
```
**Note**: When updating content dynamically, you typically return the updated zone to trigger an HTMX swap.
### CSS Customization
The Layout uses CSS classes that you can customize:
| Class | Element |
|----------------------------|----------------------------------|
| `mf-layout` | Root layout container |
| `mf-layout-header` | Header section |
| `mf-layout-footer` | Footer section |
| `mf-layout-main` | Main content area |
| `mf-layout-drawer` | Drawer container |
| `mf-layout-left-drawer` | Left drawer specifically |
| `mf-layout-right-drawer` | Right drawer specifically |
| `mf-layout-drawer-content` | Scrollable content within drawer |
| `mf-resizer` | Resize handle |
| `mf-layout-group` | Content group wrapper |
You can override these classes in your custom CSS to change colors, spacing, or behavior.
### User Profile Integration
The Layout automatically includes a UserProfile component in the header right area. This component handles user
authentication display and logout functionality when auth is enabled.
## Examples
### Example 1: Dashboard with Navigation Sidebar
A typical dashboard application with a navigation menu in the left drawer:
```python
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
# Create layout
layout = Layout(parent=root_instance, app_name="Analytics Dashboard")
# Navigation menu in left drawer
def show_dashboard():
layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics")))
return layout._mk_main()
def show_reports():
layout.set_main(Div(H1("Reports"), P("Detailed analytics reports")))
return layout._mk_main()
def show_settings():
layout.set_main(Div(H1("Settings"), P("Configure your preferences")))
return layout._mk_main()
# Add navigation items with groups
layout.left_drawer.add_group("main", group_ft=Div("MENU", cls="font-bold text-xs px-2 opacity-60"))
layout.left_drawer.add(mk.mk(Div("Dashboard"), command=Command("nav_dash", "Show dashboard", show_dashboard)),
group="main")
layout.left_drawer.add(mk.mk(Div("Reports"), command=Command("nav_reports", "Show reports", show_reports)),
group="main")
layout.left_drawer.add_group("config", group_ft=Div("CONFIGURATION", cls="font-bold text-xs px-2 opacity-60"))
layout.left_drawer.add(mk.mk(Div("Settings"), command=Command("nav_settings", "Show settings", show_settings)),
group="config")
# Header content
layout.header_left.add(Div("📊 Analytics", cls="font-bold"))
# Footer
layout.footer_left.add(Div("© 2024 Analytics Co."))
layout.footer_right.add(Div("v1.0.0"))
# Set initial main content
layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics")))
```
### Example 2: Development Tool with Debug Panel
An application with development tools in the right drawer:
```python
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
# Create layout
layout = Layout(parent=root_instance, app_name="Dev Tools")
# Main content: code editor
layout.set_main(
Div(
H2("Code Editor"),
Textarea("# Write your code here", rows=20, cls="w-full font-mono")
)
)
# Right drawer: debug and tools
layout.right_drawer.add_group("debug", group_ft=Div("DEBUG INFO", cls="font-bold text-xs px-2 opacity-60"))
layout.right_drawer.add(Div("Console output here..."), group="debug")
layout.right_drawer.add(Div("Variables: x=10, y=20"), group="debug")
layout.right_drawer.add_group("tools", group_ft=Div("TOOLS", cls="font-bold text-xs px-2 opacity-60"))
layout.right_drawer.add(Button("Run Code"), group="tools")
layout.right_drawer.add(Button("Clear Console"), group="tools")
# Header
layout.header_left.add(Div("DevTools IDE"))
layout.header_right.add(Button("Save"))
```
### Example 3: Minimal Layout (Main Content Only)
A simple layout without drawers, focusing only on main content:
```python
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
# Create layout
layout = Layout(parent=root_instance, app_name="Simple Blog")
# Header
layout.header_left.add(Div("My Blog", cls="text-xl font-bold"))
layout.header_right.add(A("About", href="/about"))
# Main content
layout.set_main(
Article(
H1("Welcome to My Blog"),
P("This is a simple blog layout without side drawers."),
P("The focus is on the content in the center.")
)
)
# Footer
layout.footer_left.add(Div("© 2024 Blog Author"))
layout.footer_right.add(A("RSS", href="/rss"))
# Note: Drawers are present but can be collapsed by users if not needed
```
### Example 4: Dynamic Content Loading
Loading content dynamically based on user interaction:
```python
from fasthtml.common import *
from myfasthtml.controls.Layout import Layout
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
layout = Layout(parent=root_instance, app_name="Dynamic App")
# Function that loads content dynamically
def load_page(page_name):
# Simulate loading different content
content = {
"home": Div(H1("Home"), P("Welcome to the home page")),
"profile": Div(H1("Profile"), P("User profile information")),
"settings": Div(H1("Settings"), P("Application settings")),
}
layout.set_main(content.get(page_name, Div("Page not found")))
return layout._mk_main()
# Create navigation commands
pages = ["home", "profile", "settings"]
for page in pages:
cmd = Command(f"load_{page}", f"Load {page} page", load_page, page)
layout.left_drawer.add(
mk.mk(Div(page.capitalize()), command=cmd)
)
# Set initial content
layout.set_main(Div(H1("Home"), P("Welcome to the home page")))
```
---
## Developer Reference
This section contains technical details for developers working on the Layout component itself.
### State
The Layout component maintains the following state properties:
| Name | Type | Description | Default |
|----------------------|---------|----------------------------------|---------|
| `left_drawer_open` | boolean | True if the left drawer is open | True |
| `right_drawer_open` | boolean | True if the right drawer is open | True |
| `left_drawer_width` | integer | Width of the left drawer | 250 |
| `right_drawer_width` | integer | Width of the right drawer | 250 |
### Commands
Available commands for programmatic control:
| Name | Description |
|-----------------------------------------|----------------------------------------------------------------------------------------|
| `toggle_drawer(side)` | Toggles the drawer on the specified side |
| `update_drawer_width(side, width=None)` | Updates the drawer width on the specified side. The width is given by the HTMX request |
### Public Methods
| Method | Description |
|---------------------|-----------------------------|
| `set_main(content)` | Sets the main content area |
| `render()` | Renders the complete layout |
### High Level Hierarchical Structure
```
Div(id="layout")
├── Header
│ ├── Div(id="layout_hl")
│ │ ├── Icon # Left drawer icon button
│ │ └── Div # Left content for the header
│ └── Div(id="layout_hr")
│ ├── Div # Right content for the header
│ └── UserProfile # user profile icon button
├── Div # Left Drawer
├── Main # Main content
├── Div # Right Drawer
├── Footer # Footer
└── Script # To initialize the resizing
```
### Element IDs
| Name | Description |
|-------------|-------------------------------------|
| `layout` | Root layout container (singleton) |
| `layout_h` | Header section (not currently used) |
| `layout_hl` | Header left side |
| `layout_hr` | Header right side |
| `layout_f` | Footer section (not currently used) |
| `layout_fl` | Footer left side |
| `layout_fr` | Footer right side |
| `layout_ld` | Left drawer |
| `layout_rd` | Right drawer |
### Internal Methods
These methods are used internally for rendering:
| Method | Description |
|---------------------------|--------------------------------------------------------|
| `_mk_header()` | Renders the header component |
| `_mk_footer()` | Renders the footer component |
| `_mk_main()` | Renders the main content area |
| `_mk_left_drawer()` | Renders the left drawer |
| `_mk_right_drawer()` | Renders the right drawer |
| `_mk_left_drawer_icon()` | Renders the left drawer toggle icon |
| `_mk_right_drawer_icon()` | Renders the right drawer toggle icon |
| `_mk_content_wrapper()` | Static method to wrap content with groups and dividers |
### Content Class
The `Layout.Content` nested class manages content zones:
| Method | Description |
|-----------------------------------|----------------------------------------------------------|
| `add(content, group=None)` | Adds content to a group, prevents duplicates based on ID |
| `add_group(group, group_ft=None)` | Creates a new group with optional custom header element |
| `get_content()` | Returns dictionary of groups and their content |
| `get_groups()` | Returns list of (group_name, group_ft) tuples |

596
docs/TreeView.md Normal file
View File

@@ -0,0 +1,596 @@
# TreeView Component
## Introduction
The TreeView component provides an interactive hierarchical data visualization with full CRUD operations. It's designed for displaying tree-structured data like file systems, organizational charts, or navigation menus with inline editing capabilities.
**Key features:**
- Expand/collapse nodes with visual indicators
- Add child and sibling nodes dynamically
- Inline rename with keyboard support (ESC to cancel)
- Delete nodes (only leaf nodes without children)
- Node selection tracking
- Persistent state per session
- Configurable icons per node type
**Common use cases:**
- File/folder browser
- Category/subcategory management
- Organizational hierarchy viewer
- Navigation menu builder
- Document outline editor
## Quick Start
Here's a minimal example showing a file system tree:
```python
from fasthtml.common import *
from myfasthtml.controls.TreeView import TreeView, TreeNode
# Create TreeView instance
tree = TreeView(parent=root_instance, _id="file-tree")
# Add root folder
root = TreeNode(id="root", label="Documents", type="folder")
tree.add_node(root)
# Add some files
file1 = TreeNode(id="file1", label="report.pdf", type="file")
file2 = TreeNode(id="file2", label="budget.xlsx", type="file")
tree.add_node(file1, parent_id="root")
tree.add_node(file2, parent_id="root")
# Expand root to show children
tree.expand_all()
# Render the tree
return tree
```
This creates an interactive tree where users can:
- Click chevrons to expand/collapse folders
- Click labels to select items
- Use action buttons (visible on hover) to add, rename, or delete nodes
**Note:** All interactions use commands and update via HTMX without page reload.
## Basic Usage
### Creating a TreeView
TreeView is a `MultipleInstance`, allowing multiple trees per session. Create it with a parent instance:
```python
tree = TreeView(parent=root_instance, _id="my-tree")
```
### TreeNode Structure
Nodes are represented by the `TreeNode` dataclass:
```python
from myfasthtml.controls.TreeView import TreeNode
node = TreeNode(
id="unique-id", # Auto-generated UUID if not provided
label="Node Label", # Display text
type="default", # Type for icon mapping
parent=None, # Parent node ID (None for root)
children=[] # List of child node IDs
)
```
### Adding Nodes
Add nodes using the `add_node()` method:
```python
# Add root node
root = TreeNode(id="root", label="Root", type="folder")
tree.add_node(root)
# Add child node
child = TreeNode(label="Child 1", type="item")
tree.add_node(child, parent_id="root")
# Add with specific position
sibling = TreeNode(label="Child 2", type="item")
tree.add_node(sibling, parent_id="root", insert_index=0) # Insert at start
```
### Visual Structure
```
TreeView
├── Root Node 1
│ ├── [>] Child 1-1 # Collapsed node with children
│ ├── [ ] Child 1-2 # Leaf node (no children)
│ └── [v] Child 1-3 # Expanded node
│ ├── [ ] Grandchild
│ └── [ ] Grandchild
└── Root Node 2
└── [>] Child 2-1
```
**Legend:**
- `[>]` - Collapsed node (has children)
- `[v]` - Expanded node (has children)
- `[ ]` - Leaf node (no children)
### Expanding Nodes
Control node expansion programmatically:
```python
# Expand all nodes with children
tree.expand_all()
# Expand specific nodes by adding to opened list
tree._state.opened.append("node-id")
```
**Note:** Users can also toggle nodes by clicking the chevron icon.
## Interactive Features
### Node Selection
Users can select nodes by clicking on labels. The selected node is visually highlighted:
```python
# Programmatically select a node
tree._state.selected = "node-id"
# Check current selection
current = tree._state.selected
```
### Adding Nodes
Users can add nodes via action buttons (visible on hover):
**Add Child:**
- Adds a new node as a child of the target node
- Automatically expands the parent
- Creates node with same type as parent
**Add Sibling:**
- Adds a new node next to the target node (same parent)
- Inserts after the target node
- Cannot add sibling to root nodes
```python
# Programmatically add child
tree._add_child(parent_id="root", new_label="New Child")
# Programmatically add sibling
tree._add_sibling(node_id="child1", new_label="New Sibling")
```
### Renaming Nodes
Users can rename nodes via the edit button:
1. Click the edit icon (visible on hover)
2. Input field appears with current label
3. Press Enter to save (triggers command)
4. Press ESC to cancel (keyboard shortcut)
```python
# Programmatically start rename
tree._start_rename("node-id")
# Save rename
tree._save_rename("node-id", "New Label")
# Cancel rename
tree._cancel_rename()
```
### Deleting Nodes
Users can delete nodes via the delete button:
**Restrictions:**
- Can only delete leaf nodes (no children)
- Attempting to delete a node with children raises an error
- Deleted node is removed from parent's children list
```python
# Programmatically delete node
tree._delete_node("node-id") # Raises ValueError if node has children
```
## Content System
### Node Types and Icons
Assign types to nodes for semantic grouping and custom icon display:
```python
# Define node types
root = TreeNode(label="Project", type="project")
folder = TreeNode(label="src", type="folder")
file = TreeNode(label="main.py", type="python-file")
# Configure icons for types
tree.set_icon_config({
"project": "fluent.folder_open",
"folder": "fluent.folder",
"python-file": "fluent.document_python"
})
```
**Note:** Icon configuration is stored in state and persists within the session.
### Hierarchical Organization
Nodes automatically maintain parent-child relationships:
```python
# Get node's children
node = tree._state.items["node-id"]
child_ids = node.children
# Get node's parent
parent_id = node.parent
# Navigate tree programmatically
for child_id in node.children:
child_node = tree._state.items[child_id]
print(child_node.label)
```
### Finding Root Nodes
Root nodes are nodes without a parent:
```python
root_nodes = [
node_id for node_id, node in tree._state.items.items()
if node.parent is None
]
```
## Advanced Features
### Keyboard Shortcuts
TreeView includes keyboard support for common operations:
| Key | Action |
|-----|--------|
| `ESC` | Cancel rename operation |
Additional shortcuts can be added via the Keyboard component:
```python
from myfasthtml.controls.Keyboard import Keyboard
tree = TreeView(parent=root_instance)
# ESC handler is automatically included for cancel rename
```
### State Management
TreeView maintains persistent state within the session:
| State Property | Type | Description |
|----------------|------|-------------|
| `items` | `dict[str, TreeNode]` | All nodes indexed by ID |
| `opened` | `list[str]` | IDs of expanded nodes |
| `selected` | `str \| None` | Currently selected node ID |
| `editing` | `str \| None` | Node being renamed (if any) |
| `icon_config` | `dict[str, str]` | Type-to-icon mapping |
### Dynamic Updates
TreeView updates are handled via commands that return the updated tree:
```python
# Commands automatically target the tree for HTMX swap
cmd = tree.commands.toggle_node("node-id")
# When executed, returns updated TreeView with new state
```
### CSS Customization
TreeView uses CSS classes for styling:
| Class | Element |
|-------|---------|
| `mf-treeview` | Root container |
| `mf-treenode-container` | Container for node and its children |
| `mf-treenode` | Individual node row |
| `mf-treenode.selected` | Selected node highlight |
| `mf-treenode-label` | Node label text |
| `mf-treenode-input` | Input field during rename |
| `mf-treenode-actions` | Action buttons container (hover) |
You can override these classes to customize appearance:
```css
.mf-treenode.selected {
background-color: #e0f2fe;
border-left: 3px solid #0284c7;
}
.mf-treenode-actions {
opacity: 0;
transition: opacity 0.2s;
}
.mf-treenode:hover .mf-treenode-actions {
opacity: 1;
}
```
## Examples
### Example 1: File System Browser
A file/folder browser with different node types:
```python
from fasthtml.common import *
from myfasthtml.controls.TreeView import TreeView, TreeNode
# Create tree
tree = TreeView(parent=root_instance, _id="file-browser")
# Configure icons
tree.set_icon_config({
"folder": "fluent.folder",
"python": "fluent.document_python",
"text": "fluent.document_text"
})
# Build file structure
root = TreeNode(id="root", label="my-project", type="folder")
tree.add_node(root)
src = TreeNode(id="src", label="src", type="folder")
tree.add_node(src, parent_id="root")
main = TreeNode(label="main.py", type="python")
utils = TreeNode(label="utils.py", type="python")
tree.add_node(main, parent_id="src")
tree.add_node(utils, parent_id="src")
readme = TreeNode(label="README.md", type="text")
tree.add_node(readme, parent_id="root")
# Expand to show structure
tree.expand_all()
return tree
```
### Example 2: Category Management
Managing product categories with inline editing:
```python
from fasthtml.common import *
from myfasthtml.controls.TreeView import TreeView, TreeNode
tree = TreeView(parent=root_instance, _id="categories")
# Root categories
electronics = TreeNode(id="elec", label="Electronics", type="category")
tree.add_node(electronics)
# Subcategories
computers = TreeNode(label="Computers", type="subcategory")
phones = TreeNode(label="Phones", type="subcategory")
tree.add_node(computers, parent_id="elec")
tree.add_node(phones, parent_id="elec")
# Products (leaf nodes)
laptop = TreeNode(label="Laptops", type="product")
desktop = TreeNode(label="Desktops", type="product")
tree.add_node(laptop, parent_id=computers.id)
tree.add_node(desktop, parent_id=computers.id)
tree.expand_all()
return tree
```
### Example 3: Document Outline Editor
Building a document outline with headings:
```python
from fasthtml.common import *
from myfasthtml.controls.TreeView import TreeView, TreeNode
tree = TreeView(parent=root_instance, _id="outline")
# Document structure
doc = TreeNode(id="doc", label="My Document", type="document")
tree.add_node(doc)
# Chapters
ch1 = TreeNode(id="ch1", label="Chapter 1: Introduction", type="heading1")
ch2 = TreeNode(id="ch2", label="Chapter 2: Methods", type="heading1")
tree.add_node(ch1, parent_id="doc")
tree.add_node(ch2, parent_id="doc")
# Sections
sec1_1 = TreeNode(label="1.1 Background", type="heading2")
sec1_2 = TreeNode(label="1.2 Objectives", type="heading2")
tree.add_node(sec1_1, parent_id="ch1")
tree.add_node(sec1_2, parent_id="ch1")
# Subsections
subsec = TreeNode(label="1.1.1 Historical Context", type="heading3")
tree.add_node(subsec, parent_id=sec1_1.id)
tree.expand_all()
return tree
```
### Example 4: Dynamic Tree with Event Handling
Responding to tree events with custom logic:
```python
from fasthtml.common import *
from myfasthtml.controls.TreeView import TreeView, TreeNode
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
tree = TreeView(parent=root_instance, _id="dynamic-tree")
# Initial structure
root = TreeNode(id="root", label="Tasks", type="folder")
tree.add_node(root)
# Function to handle selection
def on_node_selected(node_id):
# Custom logic when node is selected
node = tree._state.items[node_id]
tree._select_node(node_id)
# Update a detail panel elsewhere in the UI
return Div(
H3(f"Selected: {node.label}"),
P(f"Type: {node.type}"),
P(f"Children: {len(node.children)}")
)
# Override select command with custom handler
# (In practice, you'd extend the Commands class or use event callbacks)
tree.expand_all()
return tree
```
---
## Developer Reference
This section contains technical details for developers working on the TreeView component itself.
### State
The TreeView component maintains the following state properties:
| Name | Type | Description | Default |
|------|------|-------------|---------|
| `items` | `dict[str, TreeNode]` | All nodes indexed by ID | `{}` |
| `opened` | `list[str]` | Expanded node IDs | `[]` |
| `selected` | `str \| None` | Selected node ID | `None` |
| `editing` | `str \| None` | Node being renamed | `None` |
| `icon_config` | `dict[str, str]` | Type-to-icon mapping | `{}` |
### Commands
Available commands for programmatic control:
| Name | Description |
|------|-------------|
| `toggle_node(node_id)` | Toggle expand/collapse state |
| `add_child(parent_id)` | Add child node to parent |
| `add_sibling(node_id)` | Add sibling node after target |
| `start_rename(node_id)` | Enter rename mode for node |
| `save_rename(node_id)` | Save renamed node label |
| `cancel_rename()` | Cancel rename operation |
| `delete_node(node_id)` | Delete node (if no children) |
| `select_node(node_id)` | Select a node |
All commands automatically target the TreeView component for HTMX updates.
### Public Methods
| Method | Description |
|--------|-------------|
| `add_node(node, parent_id, insert_index)` | Add a node to the tree |
| `expand_all()` | Expand all nodes with children |
| `set_icon_config(config)` | Configure icons for node types |
| `render()` | Render the complete TreeView |
### TreeNode Dataclass
```python
@dataclass
class TreeNode:
id: str # Unique identifier (auto-generated UUID)
label: str = "" # Display text
type: str = "default" # Node type for icon mapping
parent: Optional[str] = None # Parent node ID
children: list[str] = [] # Child node IDs
```
### High Level Hierarchical Structure
```
Div(id="treeview", cls="mf-treeview")
├── Div(cls="mf-treenode-container", data-node-id="root1")
│ ├── Div(cls="mf-treenode")
│ │ ├── Icon # Toggle chevron
│ │ ├── Span(cls="mf-treenode-label") | Input(cls="mf-treenode-input")
│ │ └── Div(cls="mf-treenode-actions")
│ │ ├── Icon # Add child
│ │ ├── Icon # Rename
│ │ └── Icon # Delete
│ └── Div(cls="mf-treenode-container") # Child nodes (if expanded)
│ └── ...
├── Div(cls="mf-treenode-container", data-node-id="root2")
│ └── ...
└── Keyboard # ESC handler
```
### Element IDs and Attributes
| Attribute | Element | Description |
|-----------|---------|-------------|
| `id` | Root Div | TreeView component ID |
| `data-node-id` | Node container | Node's unique ID |
### Internal Methods
These methods are used internally for rendering and state management:
| Method | Description |
|--------|-------------|
| `_toggle_node(node_id)` | Toggle expand/collapse state |
| `_add_child(parent_id, new_label)` | Add child node implementation |
| `_add_sibling(node_id, new_label)` | Add sibling node implementation |
| `_start_rename(node_id)` | Enter rename mode |
| `_save_rename(node_id, node_label)` | Save renamed node |
| `_cancel_rename()` | Cancel rename operation |
| `_delete_node(node_id)` | Delete node if no children |
| `_select_node(node_id)` | Select a node |
| `_render_action_buttons(node_id)` | Render hover action buttons |
| `_render_node(node_id, level)` | Recursively render node and children |
### Commands Class
The `Commands` nested class provides command factory methods:
| Method | Returns |
|--------|---------|
| `toggle_node(node_id)` | Command to toggle node |
| `add_child(parent_id)` | Command to add child |
| `add_sibling(node_id)` | Command to add sibling |
| `start_rename(node_id)` | Command to start rename |
| `save_rename(node_id)` | Command to save rename |
| `cancel_rename()` | Command to cancel rename |
| `delete_node(node_id)` | Command to delete node |
| `select_node(node_id)` | Command to select node |
All commands are automatically configured with HTMX targeting.
### Integration with Keyboard Component
TreeView includes a Keyboard component for ESC key handling:
```python
Keyboard(self, {"esc": self.commands.cancel_rename()}, _id="-keyboard")
```
This enables users to press ESC to cancel rename operations without clicking.

View File

@@ -0,0 +1,895 @@
# 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. 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)
```
### 6. 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! 🧪