Updated skill and documentation

This commit is contained in:
2026-02-27 17:31:19 +01:00
parent c07b75ee72
commit efbc5a59ff
2 changed files with 597 additions and 312 deletions

View File

@@ -8,11 +8,13 @@ disable-model-invocation: false
# Unit Tester Mode # Unit Tester Mode
You are now in **Unit Tester Mode** - specialized mode for writing unit tests for existing code in the MyFastHtml project. You are now in **Unit Tester Mode** - specialized mode for writing unit tests for existing code in the MyFastHtml
project.
## Primary Objective ## Primary Objective
Write comprehensive unit tests for existing code by: Write comprehensive unit tests for existing code by:
1. Analyzing the code to understand its behavior 1. Analyzing the code to understand its behavior
2. Identifying test cases (success paths and edge cases) 2. Identifying test cases (success paths and edge cases)
3. Proposing test plan for validation 3. Proposing test plan for validation
@@ -20,46 +22,104 @@ Write comprehensive unit tests for existing code by:
## Unit Test Rules (UTR) ## Unit Test Rules (UTR)
### UTR-1: Test Analysis Before Implementation ### UTR-1: Communication Language
- **Conversations**: French or English (match user's language)
- **Code, documentation, comments**: English only
- Before writing tests, **list all planned tests with explanations**
- Wait for validation before implementing tests
### UTR-2: Test Analysis Before Implementation
Before writing any tests: Before writing any tests:
1. **Check for existing tests first** - Look for corresponding test file (e.g., `src/foo/bar.py``tests/foo/test_bar.py`)
1. **Check for existing tests first** - Look for corresponding test file (e.g., `src/foo/bar.py`
`tests/foo/test_bar.py`)
2. **Analyze the code thoroughly** - Read and understand the implementation 2. **Analyze the code thoroughly** - Read and understand the implementation
3. **If tests exist**: Identify what's already covered and what's missing 3. **If tests exist**: Identify what's already covered and what's missing
4. **If tests don't exist**: Identify all test scenarios (success and failure cases) 4. **If tests don't exist**: Identify all test scenarios (success and failure cases)
5. **Present test plan** - Describe what each test will verify (new tests only if file exists) 5. **Present test plan** - Describe what each test will verify (new tests only if file exists)
6. **Wait for validation** - Only proceed after explicit approval 6. **Wait for validation** - Only proceed after explicit approval
### UTR-2: Test Naming Conventions ### UTR-3: Ask Questions One at a Time
**Ask questions to clarify understanding:**
- Ask questions **one at a time**
- Wait for complete answer before asking the next question
- Indicate progress: "Question 1/5" if multiple questions are needed
- Never assume behavior - always verify understanding
### UTR-4: Code Standards
**Follow PEP 8** conventions strictly:
- Variable and function names: `snake_case`
- Explicit, descriptive naming
- **No emojis in code**
**Documentation**:
- Use Google or NumPy docstring format
- Every test should have a clear docstring explaining what it verifies
- Include type hints where applicable
### UTR-5: Test Naming Conventions
- **Passing tests**: `test_i_can_xxx` - Tests that should succeed - **Passing tests**: `test_i_can_xxx` - Tests that should succeed
- **Failing tests**: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions - **Failing tests**: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions
**Example:** **Example:**
```python ```python
def test_i_can_create_command_with_valid_name(): def test_i_can_create_command_with_valid_name():
"""Test that a command can be created with a valid name.""" """Test that a command can be created with a valid name."""
cmd = Command("valid_name", "description", lambda: None) cmd = Command("valid_name", "description", lambda: None)
assert cmd.name == "valid_name" assert cmd.name == "valid_name"
def test_i_cannot_create_command_with_empty_name(): def test_i_cannot_create_command_with_empty_name():
"""Test that creating a command with empty name raises ValueError.""" """Test that creating a command with empty name raises ValueError."""
with pytest.raises(ValueError): with pytest.raises(ValueError):
Command("", "description", lambda: None) Command("", "description", lambda: None)
``` ```
### UTR-3: Use Functions, Not Classes (Default) ### UTR-6: Test File Organization
- Use **functions** for tests by default **File paths:**
- Only use classes when inheritance or grouping is required (see UTR-10)
- Before writing tests, **list all planned tests with explanations**
- Wait for validation before implementing tests
### UTR-4: Do NOT Test Python Built-ins - Always specify the full file path when creating test files
- Mirror source structure: `src/myfasthtml/core/commands.py``tests/core/test_commands.py`
### UTR-7: What to choose, functions or classes during tests?
- Use **functions** when tests are validating the same concern
- Use **classes** when grouping is required, for example
- behavior tests and rendering tests can be grouped into TestControlBehaviour and TestControlRender classes
- CRUD operation can be grouped into TestCreate, TestRead, TestUpdate, TestDelete classes
- When code documentation in the class to test explicitly shows the concerns
example
```python
# ------------------------------------------------------------------
# Data initialisation
# ------------------------------------------------------------------
```
or
```python
# ------------------------------------------------------------------
# Formula management
# ------------------------------------------------------------------
```
- Never mix functions and classes in the same test file
### UTR-8: Do NOT Test Python Built-ins
**Do NOT test Python's built-in functionality.** **Do NOT test Python's built-in functionality.**
**Bad example - Testing Python list behavior:** **Bad example - Testing Python list behavior:**
```python ```python
def test_i_can_add_child_to_node(self): def test_i_can_add_child_to_node(self):
"""Test that we can add a child ID to the children list.""" """Test that we can add a child ID to the children list."""
@@ -74,6 +134,7 @@ def test_i_can_add_child_to_node(self):
This test validates that Python's `list.append()` works correctly, which is not our responsibility. This test validates that Python's `list.append()` works correctly, which is not our responsibility.
**Good example - Testing business logic:** **Good example - Testing business logic:**
```python ```python
def test_i_can_add_child_node(self, root_instance): def test_i_can_add_child_node(self, root_instance):
"""Test adding a child node to a parent.""" """Test adding a child node to a parent."""
@@ -92,14 +153,16 @@ def test_i_can_add_child_node(self, root_instance):
This test validates the `add_node()` method's logic: state management, relationship creation, bidirectional linking. This test validates the `add_node()` method's logic: state management, relationship creation, bidirectional linking.
**Other examples of what NOT to test:** **Other examples of what NOT to test:**
- Setting/getting attributes: `obj.value = 5; assert obj.value == 5` - Setting/getting attributes: `obj.value = 5; assert obj.value == 5`
- Dictionary operations: `d["key"] = "value"; assert "key" in d` - Dictionary operations: `d["key"] = "value"; assert "key" in d`
- String concatenation: `result = "hello" + "world"; assert result == "helloworld"` - String concatenation: `result = "hello" + "world"; assert result == "helloworld"`
- Type checking: `assert isinstance(obj, MyClass)` (unless type validation is part of your logic) - Type checking: `assert isinstance(obj, MyClass)` (unless type validation is part of your logic)
### UTR-5: Test Business Logic Only ### UTR-9: Test Business Logic Only
**What TO test:** **What TO test:**
- Your business logic and algorithms - Your business logic and algorithms
- Your validation rules - Your validation rules
- Your state transformations - Your state transformations
@@ -107,65 +170,35 @@ This test validates the `add_node()` method's logic: state management, relations
- Your error handling for invalid inputs - Your error handling for invalid inputs
- Your side effects (database updates, command registration, etc.) - Your side effects (database updates, command registration, etc.)
### UTR-6: Test Coverage Requirements ### UTR-10: Test Coverage Requirements
For each code element, consider testing: For each code element, consider testing:
**Functions/Methods:** **Functions/Methods:**
- Valid inputs (typical use cases) - Valid inputs (typical use cases)
- Edge cases (empty values, None, boundaries) - Edge cases (empty values, None, boundaries)
- Error conditions (invalid inputs, exceptions) - Error conditions (invalid inputs, exceptions)
- Return values and side effects - Return values and side effects
**Classes:** **Classes:**
- Initialization (default values, custom values) - Initialization (default values, custom values)
- State management (attributes, properties) - State management (attributes, properties)
- Methods (all public methods) - Methods (all public methods)
- Integration (interactions with other classes) - Integration (interactions with other classes)
**Components (Controls):** **Components (Controls):**
- Creation and initialization - Creation and initialization
- State changes - State changes
- Commands and their effects - Commands and their effects
- Rendering (if applicable) - Rendering (if applicable)
- Edge cases and error conditions - Edge cases and error conditions
### UTR-7: Ask Questions One at a Time ---
**Ask questions to clarify understanding:** ### UTR-11: IMPORTANT ! Required Reading for Control Render Tests
- Ask questions **one at a time**
- Wait for complete answer before asking the next question
- Indicate progress: "Question 1/5" if multiple questions are needed
- Never assume behavior - always verify understanding
### UTR-8: Communication Language
**Conversations**: French or English (match user's language)
**Code, documentation, comments**: English only
### UTR-9: Code Standards
**Follow PEP 8** conventions strictly:
- Variable and function names: `snake_case`
- Explicit, descriptive naming
- **No emojis in code**
**Documentation**:
- Use Google or NumPy docstring format
- Every test should have a clear docstring explaining what it verifies
- Include type hints where applicable
### UTR-10: Test File Organization
**File paths:**
- Always specify the full file path when creating test files
- Mirror source structure: `src/myfasthtml/core/commands.py``tests/core/test_commands.py`
**Example:**
```
✅ Creating: tests/core/test_new_feature.py
✅ Modifying: tests/controls/test_treeview.py
```
**Test organization for Controls:** **Test organization for Controls:**
@@ -185,6 +218,7 @@ class TestControlBehaviour:
# Test state changes, data updates, etc. # Test state changes, data updates, etc.
pass pass
class TestControlRender: class TestControlRender:
"""Tests for control HTML rendering.""" """Tests for control HTML rendering."""
@@ -200,16 +234,14 @@ class TestControlRender:
``` ```
**Why separate behaviour and render tests:** **Why separate behaviour and render tests:**
- **Behaviour tests**: Focus on logic, state management, commands, and interactions - **Behaviour tests**: Focus on logic, state management, commands, and interactions
- **Render tests**: Focus on HTML structure, attributes, and visual representation - **Render tests**: Focus on HTML structure, attributes, and visual representation
- **Clarity**: Makes it clear what aspect of the control is being tested - **Clarity**: Makes it clear what aspect of the control is being tested
- **Maintenance**: Easier to locate and update tests when behaviour or rendering changes - **Maintenance**: Easier to locate and update tests when behaviour or rendering changes
**Note:** This organization applies **only to controls** (components with rendering capabilities). For other classes (core logic, utilities, etc.), use simple function-based tests or organize by feature/edge cases as needed. **Note:** This organization applies **only to controls** (components with rendering capabilities). For other classes (
core logic, utilities, etc.), use simple function-based tests or organize by feature/edge cases as needed.
### UTR-11: Required Reading for Control Render Tests
---
#### **UTR-11.0: Read the matcher documentation (MANDATORY PREREQUISITE)** #### **UTR-11.0: Read the matcher documentation (MANDATORY PREREQUISITE)**
@@ -218,6 +250,7 @@ class TestControlRender:
**Mandatory reading:** `docs/testing_rendered_components.md` **Mandatory reading:** `docs/testing_rendered_components.md`
**What you must master:** **What you must master:**
- **`matches(actual, expected)`** - How to validate that an element matches your expectations - **`matches(actual, expected)`** - How to validate that an element matches your expectations
- **`find(ft, expected)`** - How to search for elements within an HTML tree - **`find(ft, expected)`** - How to search for elements within an HTML tree
- **Predicates** - How to test patterns instead of exact values: - **Predicates** - How to test patterns instead of exact values:
@@ -225,26 +258,26 @@ class TestControlRender:
- `Empty()`, `NoChildren()`, `AttributeForbidden()` for children - `Empty()`, `NoChildren()`, `AttributeForbidden()` for children
- **Error messages** - How to read `^^^` markers to understand differences - **Error messages** - How to read `^^^` markers to understand differences
- **Key principle** - Test only what matters, ignore the rest - **Key principle** - Test only what matters, ignore the rest
- **MyFastHtml test helpers** - `TestObject`, `TestLabel`, `TestIcon`, `TestIconNotStr`, `TestCommand`, `TestScript`
**Without this reading, you cannot write correct render tests.** **Without this reading, you cannot write correct render tests.**
--- ---
### **TEST FILE STRUCTURE**
---
#### **UTR-11.1: Always start with a global structure test (FUNDAMENTAL RULE)** #### **UTR-11.1: Always start with a global structure test (FUNDAMENTAL RULE)**
**Principle:** The **first render test** must ALWAYS verify the global HTML structure of the component. This is the test that helps readers understand the general architecture. **Principle:** The **first render test** must ALWAYS verify the global HTML structure of the component. This is the test
that helps readers understand the general architecture.
**Why:** **Why:**
- Gives immediate overview of the structure - Gives immediate overview of the structure
- Facilitates understanding for new contributors - Facilitates understanding for new contributors
- Quickly detects major structural changes - Quickly detects major structural changes
- Serves as living documentation of HTML architecture - Serves as living documentation of HTML architecture
**Test format:** **Test format:**
```python ```python
def test_i_can_render_component_with_no_data(self, component): def test_i_can_render_component_with_no_data(self, component):
"""Test that Component renders with correct global structure.""" """Test that Component renders with correct global structure."""
@@ -259,27 +292,62 @@ def test_i_can_render_component_with_no_data(self, component):
``` ```
**Notes:** **Notes:**
- Simple test with only IDs of main sections - Simple test with only IDs of main sections
- Inline comments to identify each section - Inline comments to identify each section
- No detailed verification of attributes (classes, content, etc.) - No detailed verification of attributes (classes, content, etc.)
- This test must be the first in the `TestComponentRender` class - This test must be the first in the `TestComponentRender` class
**Naming exception:** This specific test does NOT follow the `test_i_can_xxx` pattern (UTR-5). Use a descriptive name like `test_empty_layout_is_rendered()` instead. All other render tests follow UTR-5 normally.
**Test order:** **Test order:**
1. **First test:** Global structure (UTR-11.1) 1. **First test:** Global structure (UTR-11.1)
2. **Following tests:** Details of each section (UTR-11.2 to UTR-11.11) 2. **Following tests:** Details of each section (UTR-11.2 to UTR-11.14)
--- ---
#### **UTR-11.2: Break down complex tests into explicit steps** #### **UTR-11.2: Three-step pattern for simple tests**
**Principle:** When a test verifies multiple levels of HTML nesting, break it down into numbered steps with explicit comments. **Principle:** For tests not requiring multi-level decomposition, use the standard three-step pattern.
**The three steps:**
1. **Extract the element to test** with `find_one()` or `find()` from the global render
2. **Define the expected structure** with `expected = ...`
3. **Compare** with `assert matches(element, expected)`
**Example:**
```python
def test_header_has_two_sides(self, layout):
"""Test that there is a left and right header section."""
# Step 1: Extract the element to test
header = find_one(layout.render(), Header(cls=Contains("mf-layout-header")))
# Step 2: Define the expected structure
expected = Header(
Div(id=f"{layout._id}_hl"),
Div(id=f"{layout._id}_hr"),
)
# Step 3: Compare
assert matches(header, expected)
```
#### **UTR-11.3: Break down complex tests into explicit steps**
**Principle:** When a test verifies multiple levels of HTML nesting, break it down into numbered steps with explicit
comments.
**Why:** **Why:**
- Facilitates debugging (you know exactly which step fails) - Facilitates debugging (you know exactly which step fails)
- Improves test readability - Improves test readability
- Allows validating structure level by level - Allows validating structure level by level
**Example:** **Example:**
```python ```python
def test_content_wrapper_when_tab_active(self, tabs_manager): def test_content_wrapper_when_tab_active(self, tabs_manager):
"""Test that content wrapper shows active tab content.""" """Test that content wrapper shows active tab content."""
@@ -304,42 +372,12 @@ def test_content_wrapper_when_tab_active(self, tabs_manager):
``` ```
**Pattern:** **Pattern:**
- Step 1: Global structure with empty `Div()` + comment for children tested after
- Step 1: Global structure with `Div()` + comment for children tested after
- Step 2+: Extraction with `find_one()` + detailed validation - Step 2+: Extraction with `find_one()` + detailed validation
--- ---
#### **UTR-11.3: Three-step pattern for simple tests**
**Principle:** For tests not requiring multi-level decomposition, use the standard three-step pattern.
**The three steps:**
1. **Extract the element to test** with `find_one()` or `find()` from the global render
2. **Define the expected structure** with `expected = ...`
3. **Compare** with `assert matches(element, expected)`
**Example:**
```python
def test_header_has_two_sides(self, layout):
"""Test that there is a left and right header section."""
# Step 1: Extract the element to test
header = find_one(layout.render(), Header(cls=Contains("mf-layout-header")))
# Step 2: Define the expected structure
expected = Header(
Div(id=f"{layout._id}_hl"),
Div(id=f"{layout._id}_hr"),
)
# Step 3: Compare
assert matches(header, expected)
```
---
### **HOW TO SEARCH FOR ELEMENTS**
---
#### **UTR-11.4: Prefer searching by ID** #### **UTR-11.4: Prefer searching by ID**
@@ -348,6 +386,7 @@ def test_header_has_two_sides(self, layout):
**Why:** More robust, faster, and targeted (an ID is unique). **Why:** More robust, faster, and targeted (an ID is unique).
**Example:** **Example:**
```python ```python
# ✅ GOOD - search by ID # ✅ GOOD - search by ID
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld")) drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
@@ -361,10 +400,12 @@ drawer = find_one(layout.render(), Div(cls=Contains("mf-layout-left-drawer")))
#### **UTR-11.5: Use `find_one()` vs `find()` based on context** #### **UTR-11.5: Use `find_one()` vs `find()` based on context**
**Principle:** **Principle:**
- `find_one()`: When you search for a unique element and want to test its complete structure - `find_one()`: When you search for a unique element and want to test its complete structure
- `find()`: When you search for multiple elements or want to count/verify their presence - `find()`: When you search for multiple elements or want to count/verify their presence
**Examples:** **Examples:**
```python ```python
# ✅ GOOD - find_one for unique structure # ✅ GOOD - find_one for unique structure
header = find_one(layout.render(), Header(cls=Contains("mf-layout-header"))) header = find_one(layout.render(), Header(cls=Contains("mf-layout-header")))
@@ -378,19 +419,17 @@ assert len(resizers) == 1, "Left drawer should contain exactly one resizer eleme
--- ---
### **HOW TO SPECIFY EXPECTED STRUCTURE**
---
#### **UTR-11.6: Always use `Contains()` for `cls` and `style` attributes** #### **UTR-11.6: Always use `Contains()` for `cls` and `style` attributes**
**Principle:** **Principle:**
- For `cls`: CSS classes can be in any order. Test only important classes with `Contains()`. - For `cls`: CSS classes can be in any order. Test only important classes with `Contains()`.
- For `style`: CSS properties can be in any order. Test only important properties with `Contains()`. - For `style`: CSS properties can be in any order. Test only important properties with `Contains()`.
**Why:** Avoids false negatives due to class/property order or spacing. **Why:** Avoids false negatives due to class/property order or spacing.
**Examples:** **Examples:**
```python ```python
# ✅ GOOD - Contains for cls (one or more classes) # ✅ GOOD - Contains for cls (one or more classes)
expected = Div(cls=Contains("mf-layout-drawer")) expected = Div(cls=Contains("mf-layout-drawer"))
@@ -410,67 +449,24 @@ expected = Div(style="width: 250px; overflow: hidden; display: flex;")
#### **UTR-11.7: Use `TestIcon()` or `TestIconNotStr()` to test icon presence** #### **UTR-11.7: Use `TestIcon()` or `TestIconNotStr()` to test icon presence**
**Principle:** Use `TestIcon()` or `TestIconNotStr()` depending on how the icon is integrated in the code. **Principle:** Use `TestIcon()` or `TestIconNotStr()` depending on how the icon is integrated in the code. See `docs/testing_rendered_components.md` section 7 for full documentation.
**Difference between the two:** **How to choose — read the source code first:**
- **`TestIcon("icon_name")`**: Searches for the pattern `<div><NotStr .../></div>` (icon wrapped in a Div)
- **`TestIconNotStr("icon_name")`**: Searches only for `<NotStr .../>` (icon alone, without wrapper)
**How to choose:**
1. **Read the source code** to see how the icon is rendered
2. If `mk.icon()` wraps the icon in a Div → use `TestIcon()` (default `wrapper="div"`)
3. If `mk.label(..., icon=...)` wraps the icon in a Span → use `TestIcon(..., wrapper="span")`
4. If the icon is directly included without wrapper → use `TestIconNotStr()`
**The `wrapper` parameter:**
Different `mk` helpers use different wrappers for icons:
| Helper method | Wrapper element | TestIcon usage | | Helper method | Wrapper element | TestIcon usage |
|---------------|-----------------|----------------| |----------------------------------|-----------------|------------------------------------|
| `mk.icon(my_icon)` | `<div>` | `TestIcon("name")` | | `mk.icon(my_icon)` | `<div>` | `TestIcon("name")` |
| `mk.label("Text", icon=my_icon)` | `<span>` | `TestIcon("name", wrapper="span")` | | `mk.label("Text", icon=my_icon)` | `<span>` | `TestIcon("name", wrapper="span")` |
| Direct: `Div(my_icon)` | none | `TestIconNotStr("name")` | | Direct: `Div(my_icon)` | none | `TestIconNotStr("name")` |
**The `name` parameter:** **The `name` parameter:**
- **Exact name**: Use the exact import name (e.g., `TestIcon("panel_right_expand20_regular")`) to validate a specific icon - **Exact name**: Use the exact import name (e.g., `TestIcon("panel_right_expand20_regular")`) to validate a specific icon
- **`name=""`** (empty string): Validates **any icon** - **`name=""`** (empty string): Validates **any icon**
**Examples:**
```python
# Example 1: Icon via mk.icon() - wrapper is Div (default)
# Source code: mk.icon(panel_right_expand20_regular, size=20)
# Rendered: <div><svg .../></div>
expected = Header(
Div(
TestIcon("panel_right_expand20_regular"), # ✅ wrapper="div" (default)
cls=Contains("flex", "gap-1")
)
)
# Example 2: Icon via mk.label() - wrapper is Span
# Source code: mk.label("Back", icon=chevron_left20_regular, command=...)
# Rendered: <label><span><svg .../></span><span>Back</span></label>
back_icon = find_one(details, TestIcon("chevron_left20_regular", wrapper="span")) # ✅ wrapper="span"
# Example 3: Direct icon (used without helper)
# Source code: Span(dismiss_circle16_regular, cls="icon")
# Rendered: <span><svg .../></span>
expected = Span(
TestIconNotStr("dismiss_circle16_regular"), # ✅ Without wrapper
cls=Contains("icon")
)
# Example 4: Verify any wrapped icon
expected = Div(
TestIcon(""), # Accepts any wrapped icon
cls=Contains("icon-wrapper")
)
```
**Debugging tip:** **Debugging tip:**
If your test fails with `TestIcon()`: If your test fails with `TestIcon()`:
1. Check if the wrapper is `<span>` instead of `<div>` → try `wrapper="span"` 1. Check if the wrapper is `<span>` instead of `<div>` → try `wrapper="span"`
2. Check if there's no wrapper at all → try `TestIconNotStr()` 2. Check if there's no wrapper at all → try `TestIconNotStr()`
3. The error message will show you the actual structure 3. The error message will show you the actual structure
@@ -479,9 +475,8 @@ If your test fails with `TestIcon()`:
#### **UTR-11.8: Use `TestScript()` to test JavaScript scripts** #### **UTR-11.8: Use `TestScript()` to test JavaScript scripts**
**Principle:** Use `TestScript(code_fragment)` to verify JavaScript code presence. Test only the important fragment, not the complete script. **Principle:** Use `TestScript(code_fragment)` to verify JavaScript code presence. Test only the important fragment, not the complete script. See `docs/testing_rendered_components.md` section 9 for full documentation.
**Example:**
```python ```python
# ✅ GOOD - TestScript with important fragment # ✅ GOOD - TestScript with important fragment
script = find_one(layout.render(), Script()) script = find_one(layout.render(), Script())
@@ -496,17 +491,20 @@ expected = Script("(function() { const id = '...'; initResizer(id); })()")
#### **UTR-11.9: Remove default `enctype` attribute when searching for Form elements** #### **UTR-11.9: Remove default `enctype` attribute when searching for Form elements**
**Principle:** FastHTML's `Form()` component automatically adds `enctype="multipart/form-data"` as a default attribute. When using `find()` or `find_one()` to search for a Form, you must remove this attribute from the expected pattern. **Principle:** FastHTML's `Form()` component automatically adds `enctype="multipart/form-data"` as a default attribute.
When using `find()` or `find_one()` to search for a Form, you must remove this attribute from the expected pattern.
**Why:** The actual Form in your component may not have this attribute, causing the match to fail. **Why:** The actual Form in your component may not have this attribute, causing the match to fail.
**Problem:** **Problem:**
```python ```python
# ❌ FAILS - Form() has default enctype that may not exist in actual form # ❌ FAILS - Form() has default enctype that may not exist in actual form
form = find_one(details, Form()) # AssertionError: Found 0 elements form = find_one(details, Form()) # AssertionError: Found 0 elements
``` ```
**Solution:** **Solution:**
```python ```python
# ✅ WORKS - Remove the default enctype attribute # ✅ WORKS - Remove the default enctype attribute
expected_form = Form() expected_form = Form()
@@ -515,6 +513,7 @@ form = find_one(details, expected_form)
``` ```
**Complete example:** **Complete example:**
```python ```python
def test_column_details_contains_form(self, component): def test_column_details_contains_form(self, component):
"""Test that column details contains a form with required fields.""" """Test that column details contains a form with required fields."""
@@ -532,21 +531,21 @@ def test_column_details_contains_form(self, component):
assert title_input is not None assert title_input is not None
``` ```
**Note:** This is a FastHTML-specific behavior. Always check for similar default attributes when tests fail unexpectedly with "Found 0 elements". **Note:** This is a FastHTML-specific behavior. Always check for similar default attributes when tests fail unexpectedly
with "Found 0 elements".
--- ---
### **HOW TO DOCUMENT TESTS** #### **UTR-11.10: Test Documentation - Justify the choice of tested elements**
--- **Principle:** In the test documentation section (after the description docstring), explain **why each tested element or
attribute was chosen**. What makes it important for the functionality?
#### **UTR-11.10: Justify the choice of tested elements** **What matters:** Not the exact wording ("Why these elements matter" vs "Why this test matters"), but **the explanation
of why what is tested is relevant**.
**Principle:** In the test documentation section (after the description docstring), explain **why each tested element or attribute was chosen**. What makes it important for the functionality?
**What matters:** Not the exact wording ("Why these elements matter" vs "Why this test matters"), but **the explanation of why what is tested is relevant**.
**Examples:** **Examples:**
```python ```python
def test_empty_layout_is_rendered(self, layout): def test_empty_layout_is_rendered(self, layout):
"""Test that Layout renders with all main structural sections. """Test that Layout renders with all main structural sections.
@@ -559,6 +558,7 @@ def test_empty_layout_is_rendered(self, layout):
expected = Div(...) expected = Div(...)
assert matches(layout.render(), expected) assert matches(layout.render(), expected)
def test_left_drawer_is_rendered_when_open(self, layout): def test_left_drawer_is_rendered_when_open(self, layout):
"""Test that left drawer renders with correct classes when open. """Test that left drawer renders with correct classes when open.
@@ -581,6 +581,7 @@ def test_left_drawer_is_rendered_when_open(self, layout):
``` ```
**Key points:** **Key points:**
- Explain why the attribute/element is important (functionality, HTMX, styling, etc.) - Explain why the attribute/element is important (functionality, HTMX, styling, etc.)
- No need to follow rigid wording - No need to follow rigid wording
- What matters is the **justification of the choice**, not the format - What matters is the **justification of the choice**, not the format
@@ -589,9 +590,11 @@ def test_left_drawer_is_rendered_when_open(self, layout):
#### **UTR-11.11: Count tests with explicit messages** #### **UTR-11.11: Count tests with explicit messages**
**Principle:** When you count elements with `assert len()`, ALWAYS add an explicit message explaining why this number is expected. **Principle:** When you count elements with `assert len()`, ALWAYS add an explicit message explaining why this number is
expected.
**Example:** **Example:**
```python ```python
# ✅ GOOD - explanatory message # ✅ GOOD - explanatory message
resizers = find(drawer, Div(cls=Contains("mf-resizer-left"))) resizers = find(drawer, Div(cls=Contains("mf-resizer-left")))
@@ -606,72 +609,97 @@ assert len(resizers) == 1
--- ---
### **OTHER IMPORTANT RULES** #### **UTR-11.12: No inline comments in expected structures**
**Principle:** Do NOT add comments on each line of the expected structure. The only exception is the global structure test (UTR-11.1) where inline comments identify each section.
```python
# ✅ GOOD - no inline noise
expected = Div(
Div(id=f"{layout._id}-header"),
Div(id=f"{layout._id}-content"),
Div(id=f"{layout._id}-footer"),
)
# ❌ AVOID - inline comments everywhere
expected = Div(
Div(id=f"{layout._id}-header"), # header
Div(id=f"{layout._id}-content"), # content
Div(id=f"{layout._id}-footer"), # footer
)
```
--- ---
**Mandatory render test rules:** #### **UTR-11.13: Use `TestObject(ComponentClass)` to test component presence**
1. **Test naming**: Use descriptive names like `test_empty_layout_is_rendered()` not `test_layout_renders_with_all_sections()` **Principle:** When a component renders another component as a child, use `TestObject(ComponentClass)` to verify its presence without coupling to its internal rendering.
2. **Documentation format**: Every render test MUST have a docstring with: ```python
- First line: Brief description of what is being tested from myfasthtml.test.matcher import matches, find, TestObject
- Blank line from myfasthtml.controls.Search import Search
- Justification section explaining why tested elements matter (see UTR-11.10)
- List of important elements/attributes being tested with explanations (in English)
3. **No inline comments**: Do NOT add comments on each line of the expected structure (except for structural clarification in global layout tests like `# left drawer`) # ✅ GOOD - test presence without coupling to internal rendering
expected = Div(
TestObject(Search),
id=f"{toolbar._id}"
)
assert matches(toolbar.render(), expected)
```
4. **Component testing**: Use `TestObject(ComponentClass)` to test presence of components ---
5. **Test organization for Controls**: Organize tests into thematic classes: #### **UTR-11.14: Use pytest fixtures in `TestControlRender`**
- `TestControlBehaviour`: Tests for control behavior and logic
- `TestControlRender`: Tests for control HTML rendering
6. **Fixture usage**: In `TestControlRender`, use a pytest fixture to create the control instance: **Principle:** In `TestControlRender`, always use a pytest fixture to create the control instance. This avoids duplicating setup code across tests.
```python
class TestControlRender: ```python
class TestControlRender:
@pytest.fixture @pytest.fixture
def layout(self, root_instance): def layout(self, root_instance):
return Layout(root_instance, app_name="Test App") return Layout(root_instance, app_name="Test App")
def test_something(self, layout): def test_empty_layout_is_rendered(self, layout):
# layout is injected automatically # layout is injected automatically by pytest
``` assert matches(layout.render(), ...)
```
--- ---
#### **Summary: The 12 UTR-11 sub-rules** #### **Summary: The 15 UTR-11 sub-rules**
**Prerequisite** **Prerequisite**
- **UTR-11.0**: ⭐⭐⭐ Read `docs/testing_rendered_components.md` (MANDATORY) - **UTR-11.0**: ⭐⭐⭐ Read `docs/testing_rendered_components.md` (MANDATORY)
**Test file structure** **Test file structure**
- **UTR-11.1**: ⭐ Always start with a global structure test (FIRST TEST)
- **UTR-11.2**: Break down complex tests into numbered steps - **UTR-11.1**: ⭐ Always start with a global structure test (FIRST TEST, naming exception to UTR-5)
- **UTR-11.3**: Three-step pattern for simple tests - **UTR-11.2**: Three-step pattern for simple tests
- **UTR-11.3**: Break down complex tests into numbered steps
**How to search** **How to search**
- **UTR-11.4**: Prefer search by ID - **UTR-11.4**: Prefer search by ID
- **UTR-11.5**: `find_one()` vs `find()` based on context - **UTR-11.5**: `find_one()` vs `find()` based on context
**How to specify** **How to specify**
- **UTR-11.6**: Always `Contains()` for `cls` and `style` - **UTR-11.6**: Always `Contains()` for `cls` and `style`
- **UTR-11.7**: `TestIcon()` or `TestIconNotStr()` to test icon presence - **UTR-11.7**: `TestIcon()` or `TestIconNotStr()` to test icon presence
- **UTR-11.8**: `TestScript()` for JavaScript - **UTR-11.8**: `TestScript()` for JavaScript
- **UTR-11.9**: Remove default `enctype` from `Form()` patterns - **UTR-11.9**: Remove default `enctype` from `Form()` patterns
- **UTR-11.13**: `TestObject(ComponentClass)` to test component presence
**How to document** **How to document**
- **UTR-11.10**: Justify the choice of tested elements - **UTR-11.10**: Justify the choice of tested elements
- **UTR-11.11**: Explicit messages for `assert len()` - **UTR-11.11**: Explicit messages for `assert len()`
--- **Code style**
**When proposing render tests:** - **UTR-11.12**: No inline comments in expected structures
- Reference specific patterns from the documentation - **UTR-11.14**: Use pytest fixtures in `TestControlRender`
- Explain why you chose to test certain elements and not others
- Justify the use of predicates vs exact values
- Always include justification documentation (see UTR-11.10)
--- ---
@@ -682,6 +710,7 @@ assert len(resizers) == 1
**Why:** Prevents writing tests based on incorrect assumptions about behavior. **Why:** Prevents writing tests based on incorrect assumptions about behavior.
**Example:** **Example:**
``` ```
Test: "content_is_cached_after_first_retrieval" Test: "content_is_cached_after_first_retrieval"
Flow: create_tab() → _add_or_update_tab() → state.ns_tabs_content[tab_id] = component Flow: create_tab() → _add_or_update_tab() → state.ns_tabs_content[tab_id] = component
@@ -689,6 +718,7 @@ Conclusion: Cache is already filled after create_tab, test would be redundant
``` ```
**Process:** **Process:**
1. Identify the method being tested 1. Identify the method being tested
2. Trace all method calls it makes 2. Trace all method calls it makes
3. Identify state changes at each step 3. Identify state changes at each step
@@ -704,6 +734,7 @@ Conclusion: Cache is already filled after create_tab, test would be redundant
**Why:** More robust, clearer error messages, consistent with render test patterns. **Why:** More robust, clearer error messages, consistent with render test patterns.
**Examples:** **Examples:**
```python ```python
# ❌ FRAGILE - string matching # ❌ FRAGILE - string matching
result = component._dynamic_get_content("nonexistent_id") result = component._dynamic_get_content("nonexistent_id")
@@ -721,11 +752,13 @@ assert matches(result, Div('Tab not found.'))
**Rule:** FastHTML elements use HTML attribute names, not Python parameter names. **Rule:** FastHTML elements use HTML attribute names, not Python parameter names.
**Key differences:** **Key differences:**
- Use `attrs.get('class')` not `attrs.get('cls')` - Use `attrs.get('class')` not `attrs.get('cls')`
- Use `attrs.get('id')` for the ID - Use `attrs.get('id')` for the ID
- Prefer `matches()` with predicates to avoid direct attribute access - Prefer `matches()` with predicates to avoid direct attribute access
**Examples:** **Examples:**
```python ```python
# ❌ WRONG - Python parameter name # ❌ WRONG - Python parameter name
classes = element.attrs.get('cls', '') # Returns None or '' classes = element.attrs.get('cls', '') # Returns None or ''
@@ -760,18 +793,21 @@ assert matches(element, expected)
**Rule:** When proposing a test plan, systematically identify tests that can be parameterized and propose them as such. **Rule:** When proposing a test plan, systematically identify tests that can be parameterized and propose them as such.
**When to parameterize:** **When to parameterize:**
- Tests that follow the same pattern with different input values - Tests that follow the same pattern with different input values
- Tests that verify the same behavior for different sides/directions (left/right, up/down) - Tests that verify the same behavior for different sides/directions (left/right, up/down)
- Tests that check the same logic with different states (visible/hidden, enabled/disabled) - Tests that check the same logic with different states (visible/hidden, enabled/disabled)
- Tests that validate the same method with different valid inputs - Tests that validate the same method with different valid inputs
**How to identify candidates:** **How to identify candidates:**
1. Look for tests with similar names differing only by a value (e.g., `test_left_panel_...` and `test_right_panel_...`) 1. Look for tests with similar names differing only by a value (e.g., `test_left_panel_...` and `test_right_panel_...`)
2. Look for tests that have identical structure but different parameters 2. Look for tests that have identical structure but different parameters
3. Look for combinatorial scenarios (side × state combinations) 3. Look for combinatorial scenarios (side × state combinations)
**How to propose:** **How to propose:**
In your test plan, explicitly show: In your test plan, explicitly show:
1. The individual tests that would be written without parameterization 1. The individual tests that would be written without parameterization
2. The parameterized version with all test cases 2. The parameterized version with all test cases
3. The reduction in test count 3. The reduction in test count
@@ -798,6 +834,7 @@ def test_i_can_toggle_panel_visibility(...)
``` ```
**Benefits:** **Benefits:**
- Reduces code duplication - Reduces code duplication
- Makes it easier to add new test cases - Makes it easier to add new test cases
- Improves maintainability - Improves maintainability
@@ -808,6 +845,7 @@ def test_i_can_toggle_panel_visibility(...)
## Managing Rules ## Managing Rules
To disable a specific rule, the user can say: To disable a specific rule, the user can say:
- "Disable UTR-4" (do not apply the rule about testing Python built-ins) - "Disable UTR-4" (do not apply the rule about testing Python built-ins)
- "Enable UTR-4" (re-enable a previously disabled rule) - "Enable UTR-4" (re-enable a previously disabled rule)

View File

@@ -522,7 +522,254 @@ expected = Input(
matches(actual, expected) matches(actual, expected)
``` ```
### 5. Combining matches() and find()
### 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. **Goal**: First find elements, then validate them in detail.
@@ -574,7 +821,7 @@ for card in cards:
matches(card, expected_card) matches(card, expected_card)
``` ```
### 6. Testing Edge Cases ### 11. Testing Edge Cases
**Testing empty elements:** **Testing empty elements:**