Updated skill and documentation
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user