TabsManager.py Added unit tests + documentation

This commit is contained in:
2025-12-07 15:42:48 +01:00
parent 05067515d6
commit fde2e85c92
11 changed files with 4375 additions and 317 deletions

View File

@@ -201,190 +201,283 @@ class TestControlRender:
### UTR-11: Required Reading for Control Render Tests
**Before writing ANY render tests for Controls, you MUST:**
---
1. **Read the matcher documentation**: `docs/testing_rendered_components.md`
2. **Understand the key concepts**:
- How `matches()` and `find()` work
- When to use predicates (Contains, StartsWith, AnyValue, etc.)
- How to test only what matters (not every detail)
- How to read error messages with `^^^` markers
3. **Apply the best practices** detailed below
#### **UTR-11.0: Read the matcher documentation (MANDATORY PREREQUISITE)**
**Principle:** Before writing any render tests, you MUST read and understand the complete matcher documentation.
**Mandatory reading:** `docs/testing_rendered_components.md`
**What you must master:**
- **`matches(actual, expected)`** - How to validate that an element matches your expectations
- **`find(ft, expected)`** - How to search for elements within an HTML tree
- **Predicates** - How to test patterns instead of exact values:
- `Contains()`, `StartsWith()`, `DoesNotContain()`, `AnyValue()` for attributes
- `Empty()`, `NoChildren()`, `AttributeForbidden()` for children
- **Error messages** - How to read `^^^` markers to understand differences
- **Key principle** - Test only what matters, ignore the rest
**Without this reading, you cannot write correct render tests.**
---
#### **UTR-11.1 : Pattern de test en trois étapes (RÈGLE FONDAMENTALE)**
### **TEST FILE STRUCTURE**
**Principe :** C'est le pattern par défaut à appliquer pour tous les tests de rendu. Les autres règles sont des compléments à ce pattern.
---
**Les trois étapes :**
1. **Extraire l'élément à tester** avec `find_one()` ou `find()` à partir du rendu global
2. **Définir la structure attendue** avec `expected = ...`
3. **Comparer** avec `assert matches(element, expected)`
#### **UTR-11.1: Always start with a global structure test (FUNDAMENTAL RULE)**
**Pourquoi :** Ce pattern permet des messages d'erreur clairs et sépare la recherche de l'élément de la validation de sa structure.
**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.
**Exemple :**
**Why:**
- Gives immediate overview of the structure
- Facilitates understanding for new contributors
- Quickly detects major structural changes
- Serves as living documentation of HTML architecture
**Test format:**
```python
def test_i_can_render_component_with_no_data(self, component):
"""Test that Component renders with correct global structure."""
html = component.render()
expected = Div(
Div(id=f"{component.get_id()}-controller"), # controller
Div(id=f"{component.get_id()}-header"), # header
Div(id=f"{component.get_id()}-content"), # content
id=component.get_id(),
)
assert matches(html, expected)
```
**Notes:**
- Simple test with only IDs of main sections
- Inline comments to identify each section
- No detailed verification of attributes (classes, content, etc.)
- This test must be the first in the `TestComponentRender` class
**Test order:**
1. **First test:** Global structure (UTR-11.1)
2. **Following tests:** Details of each section (UTR-11.2 to UTR-11.10)
---
#### **UTR-11.2: 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:**
- Facilitates debugging (you know exactly which step fails)
- Improves test readability
- Allows validating structure level by level
**Example:**
```python
def test_content_wrapper_when_tab_active(self, tabs_manager):
"""Test that content wrapper shows active tab content."""
tab_id = tabs_manager.create_tab("Tab1", Div("My Content"))
wrapper = tabs_manager._mk_tab_content_wrapper()
# Step 1: Validate wrapper global structure
expected = Div(
Div(), # tab content, tested in step 2
id=f"{tabs_manager.get_id()}-content-wrapper",
cls=Contains("mf-tab-content-wrapper"),
)
assert matches(wrapper, expected)
# Step 2: Extract and validate specific content
tab_content = find_one(wrapper, Div(id=f"{tabs_manager.get_id()}-{tab_id}-content"))
expected = Div(
Div("My Content"), # <= actual content
cls=Contains("mf-tab-content"),
)
assert matches(tab_content, expected)
```
**Pattern:**
- Step 1: Global structure with empty `Div()` + comment for children tested after
- 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
# ✅ BON - Pattern en trois étapes
def test_header_has_two_sides(self, layout):
"""Test that there is a left and right header section."""
# Étape 1 : Extraire l'élément à tester
# Step 1: Extract the element to test
header = find_one(layout.render(), Header(cls=Contains("mf-layout-header")))
# Étape 2 : Définir la structure attendue
# Step 2: Define the expected structure
expected = Header(
Div(id=f"{layout._id}_hl"),
Div(id=f"{layout._id}_hr"),
)
# Étape 3 : Comparer
# Step 3: Compare
assert matches(header, expected)
# ❌ À ÉVITER - Tout imbriqué en une ligne
def test_header_has_two_sides(self, layout):
assert matches(
find_one(layout.render(), Header(cls=Contains("mf-layout-header"))),
Header(Div(id=f"{layout._id}_hl"), Div(id=f"{layout._id}_hr"))
)
```
**Note :** Cette règle s'applique à presque tous les tests. Les autres règles ci-dessous complètent ce pattern fondamental.
---
### **HOW TO SEARCH FOR ELEMENTS**
---
#### **COMMENT CHERCHER LES ÉLÉMENTS**
#### **UTR-11.4: Prefer searching by ID**
---
**Principle:** Always search for an element by its `id` when it has one, rather than by class or other attribute.
#### **UTR-11.2 : Privilégier la recherche par ID**
**Why:** More robust, faster, and targeted (an ID is unique).
**Principe :** Toujours chercher un élément par son `id` quand il en a un, plutôt que par classe ou autre attribut.
**Pourquoi :** Plus robuste, plus rapide, et ciblé (un ID est unique).
**Exemple :**
**Example:**
```python
# ✅ BON - recherche par ID
# ✅ GOOD - search by ID
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
# ❌ À ÉVITER - recherche par classe quand un ID existe
# ❌ AVOID - search by class when an ID exists
drawer = find_one(layout.render(), Div(cls=Contains("mf-layout-left-drawer")))
```
---
#### **UTR-11.3 : Utiliser `find_one()` vs `find()` selon le contexte**
#### **UTR-11.5: Use `find_one()` vs `find()` based on context**
**Principe :**
- `find_one()` : Quand vous cherchez un élément unique et voulez tester sa structure complète
- `find()` : Quand vous cherchez plusieurs éléments ou voulez compter/vérifier leur présence
**Principle:**
- `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
**Exemples :**
**Examples:**
```python
# ✅ BON - find_one pour structure unique
# ✅ GOOD - find_one for unique structure
header = find_one(layout.render(), Header(cls=Contains("mf-layout-header")))
expected = Header(...)
assert matches(header, expected)
# ✅ BON - find pour compter
# ✅ GOOD - find for counting
resizers = find(drawer, Div(cls=Contains("mf-resizer-left")))
assert len(resizers) == 1, "Left drawer should contain exactly one resizer element"
```
---
#### **COMMENT SPÉCIFIER LA STRUCTURE ATTENDUE**
### **HOW TO SPECIFY EXPECTED STRUCTURE**
---
#### **UTR-11.4 : Toujours utiliser `Contains()` pour les attributs `cls` et `style`**
#### **UTR-11.6: Always use `Contains()` for `cls` and `style` attributes**
**Principe :**
- Pour `cls` : Les classes CSS peuvent être dans n'importe quel ordre. Testez uniquement les classes importantes avec `Contains()`.
- Pour `style` : Les propriétés CSS peuvent être dans n'importe quel ordre. Testez uniquement les propriétés importantes avec `Contains()`.
**Principle:**
- 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()`.
**Pourquoi :** Évite les faux négatifs dus à l'ordre des classes/propriétés ou aux espaces.
**Why:** Avoids false negatives due to class/property order or spacing.
**Exemples :**
**Examples:**
```python
# ✅ BON - Contains pour cls (une ou plusieurs classes)
# ✅ GOOD - Contains for cls (one or more classes)
expected = Div(cls=Contains("mf-layout-drawer"))
expected = Div(cls=Contains("mf-layout-drawer", "mf-layout-left-drawer"))
# ✅ BON - Contains pour style
# ✅ GOOD - Contains for style
expected = Div(style=Contains("width: 250px"))
# ❌ À ÉVITER - test exact des classes
# ❌ AVOID - exact class test
expected = Div(cls="mf-layout-drawer mf-layout-left-drawer")
# ❌ À ÉVITER - test exact du style complet
# ❌ AVOID - exact complete style test
expected = Div(style="width: 250px; overflow: hidden; display: flex;")
```
---
#### **UTR-11.5 : Utiliser `TestIcon()` pour tester la présence d'une icône**
#### **UTR-11.7: Use `TestIcon()` or `TestIconNotStr()` to test icon presence**
**Principe :** Utilisez `TestIcon("icon_name")` pour tester la présence d'une icône SVG dans le rendu.
**Principle:** Use `TestIcon()` or `TestIconNotStr()` depending on how the icon is integrated in the code.
**Le paramètre `name` :**
- **Nom exact** : Utilisez le nom exact de l'import (ex: `TestIcon("panel_right_expand20_regular")`) pour valider une icône spécifique
- **`name=""`** (chaîne vide) : Valide **n'importe quelle icône**. Le test sera passant dès que la structure affichant une icône sera trouvée, peu importe laquelle.
- **JAMAIS `name="svg"`** : Cela causera des échecs de test
**Difference between the two:**
- **`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()` or equivalent wraps the icon in a Div → use `TestIcon()`
3. If the icon is directly included without wrapper → use `TestIconNotStr()`
**The `name` parameter:**
- **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**
**Examples:**
**Exemples :**
```python
from myfasthtml.icons.fluent import panel_right_expand20_regular
# ✅ BON - Tester une icône spécifique
# Example 1: Wrapped icon (typically with mk.icon())
# Source code: mk.icon(panel_right_expand20_regular, size=20)
# Rendered: <div><NotStr .../></div>
expected = Header(
Div(
TestIcon("panel_right_expand20_regular"),
TestIcon("panel_right_expand20_regular"), # ✅ With wrapper
cls=Contains("flex", "gap-1")
)
)
# ✅ BON - Tester la présence de n'importe quelle icône
expected = Div(
TestIcon(""), # Accepte n'importe quelle icône
cls=Contains("icon-wrapper")
# Example 2: Direct icon (used without helper)
# Source code: Span(dismiss_circle16_regular, cls="icon")
# Rendered: <span><NotStr .../></span>
expected = Span(
TestIconNotStr("dismiss_circle16_regular"), # ✅ Without wrapper
cls=Contains("icon")
)
# ❌ À ÉVITER - name="svg"
expected = Div(TestIcon("svg")) # ERREUR : causera un échec
# Example 3: Verify any wrapped icon
expected = Div(
TestIcon(""), # Accepts any wrapped icon
cls=Contains("icon-wrapper")
)
```
**Debugging tip:**
If your test fails with `TestIcon()`, try `TestIconNotStr()` and vice-versa. The error message will show you the actual structure.
---
#### **UTR-11.6 : Utiliser `TestScript()` pour tester les scripts JavaScript**
#### **UTR-11.8: Use `TestScript()` to test JavaScript scripts**
**Principe :** Utilisez `TestScript(code_fragment)` pour vérifier la présence de code JavaScript. Testez uniquement le fragment important, pas le script complet.
**Principle:** Use `TestScript(code_fragment)` to verify JavaScript code presence. Test only the important fragment, not the complete script.
**Exemple :**
**Example:**
```python
# ✅ BON - TestScript avec fragment important
# ✅ GOOD - TestScript with important fragment
script = find_one(layout.render(), Script())
expected = TestScript(f"initResizer('{layout._id}');")
assert matches(script, expected)
# ❌ À ÉVITER - tester tout le contenu du script
# ❌ AVOID - testing all script content
expected = Script("(function() { const id = '...'; initResizer(id); })()")
```
---
#### **COMMENT DOCUMENTER LES TESTS**
### **HOW TO DOCUMENT TESTS**
---
#### **UTR-11.7 : Justifier le choix des éléments testés**
#### **UTR-11.9: Justify the choice of tested elements**
**Principe :** Dans la section de documentation du test (après le docstring de description), expliquez **pourquoi chaque élément ou attribut testé a été choisi**. Qu'est-ce qui le rend important pour la fonctionnalité ?
**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?
**Ce qui compte :** Pas la formulation exacte ("Why these elements matter" vs "Why this test matters"), mais **l'explication de la pertinence de ce qui est testé**.
**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**.
**Exemples :**
**Examples:**
```python
def test_empty_layout_is_rendered(self, layout):
"""Test that Layout renders with all main structural sections.
@@ -418,33 +511,33 @@ def test_left_drawer_is_rendered_when_open(self, layout):
assert matches(drawer, expected)
```
**Points clés :**
- Expliquez pourquoi l'attribut/élément est important (fonctionnalité, HTMX, styling, etc.)
- Pas besoin de suivre une formulation rigide
- L'important est la **justification du choix**, pas le format
**Key points:**
- Explain why the attribute/element is important (functionality, HTMX, styling, etc.)
- No need to follow rigid wording
- What matters is the **justification of the choice**, not the format
---
#### **UTR-11.8 : Tests de comptage avec messages explicites**
#### **UTR-11.10: Count tests with explicit messages**
**Principe :** Quand vous comptez des éléments avec `assert len()`, ajoutez TOUJOURS un message explicite qui explique pourquoi ce nombre est attendu.
**Principle:** When you count elements with `assert len()`, ALWAYS add an explicit message explaining why this number is expected.
**Exemple :**
**Example:**
```python
# ✅ BON - message explicatif
# ✅ GOOD - explanatory message
resizers = find(drawer, Div(cls=Contains("mf-resizer-left")))
assert len(resizers) == 1, "Left drawer should contain exactly one resizer element"
dividers = find(content, Div(cls="divider"))
assert len(dividers) >= 1, "Groups should be separated by dividers"
# ❌ À ÉVITER - pas de message
# ❌ AVOID - no message
assert len(resizers) == 1
```
---
#### **AUTRES RÈGLES IMPORTANTES**
### **OTHER IMPORTANT RULES**
---
@@ -455,7 +548,7 @@ assert len(resizers) == 1
2. **Documentation format**: Every render test MUST have a docstring with:
- First line: Brief description of what is being tested
- Blank line
- Justification section explaining why tested elements matter (see UTR-11.7)
- Justification section explaining why tested elements matter (see UTR-11.9)
- 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`)
@@ -479,23 +572,28 @@ assert len(resizers) == 1
---
#### **Résumé : Les 8 règles UTR-11**
#### **Summary: The 11 UTR-11 sub-rules**
**Pattern fondamental**
- **UTR-11.1** : Pattern en trois étapes (extraire → définir expected → comparer)
**Prerequisite**
- **UTR-11.0**: ⭐⭐⭐ Read `docs/testing_rendered_components.md` (MANDATORY)
**Comment chercher**
- **UTR-11.2** : Privilégier recherche par ID
- **UTR-11.3** : `find_one()` vs `find()` selon contexte
**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.3**: Three-step pattern for simple tests
**Comment spécifier**
- **UTR-11.4** : Toujours `Contains()` pour `cls` et `style`
- **UTR-11.5** : `TestIcon()` pour tester la présence d'icônes
- **UTR-11.6** : `TestScript()` pour JavaScript
**How to search**
- **UTR-11.4**: Prefer search by ID
- **UTR-11.5**: `find_one()` vs `find()` based on context
**Comment documenter**
- **UTR-11.7** : Justifier le choix des éléments testés
- **UTR-11.8** : Messages explicites pour `assert len()`
**How to specify**
- **UTR-11.6**: Always `Contains()` for `cls` and `style`
- **UTR-11.7**: `TestIcon()` or `TestIconNotStr()` to test icon presence
- **UTR-11.8**: `TestScript()` for JavaScript
**How to document**
- **UTR-11.9**: Justify the choice of tested elements
- **UTR-11.10**: Explicit messages for `assert len()`
---
@@ -503,19 +601,89 @@ assert len(resizers) == 1
- Reference specific patterns from the documentation
- 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.7)
- Always include justification documentation (see UTR-11.9)
### UTR-12: Test Workflow
---
### UTR-12: Analyze Execution Flow Before Writing Tests
**Rule:** Before writing a test, trace the complete execution flow to understand side effects.
**Why:** Prevents writing tests based on incorrect assumptions about behavior.
**Example:**
```
Test: "content_is_cached_after_first_retrieval"
Flow: create_tab() → _add_or_update_tab() → state.ns_tabs_content[tab_id] = component
Conclusion: Cache is already filled after create_tab, test would be redundant
```
**Process:**
1. Identify the method being tested
2. Trace all method calls it makes
3. Identify state changes at each step
4. Verify your assumptions about what the test should validate
5. Only then write the test
---
### UTR-13: Prefer matches() for Content Verification
**Rule:** Even in behavior tests, use `matches()` to verify HTML content rather than `assert "text" in str(element)`.
**Why:** More robust, clearer error messages, consistent with render test patterns.
**Examples:**
```python
# ❌ FRAGILE - string matching
result = component._dynamic_get_content("nonexistent_id")
assert "Tab not found" in str(result)
# ✅ ROBUST - structural matching
result = component._dynamic_get_content("nonexistent_id")
assert matches(result, Div('Tab not found.'))
```
---
### UTR-14: Know FastHTML Attribute Names
**Rule:** FastHTML elements use HTML attribute names, not Python parameter names.
**Key differences:**
- Use `attrs.get('class')` not `attrs.get('cls')`
- Use `attrs.get('id')` for the ID
- Prefer `matches()` with predicates to avoid direct attribute access
**Examples:**
```python
# ❌ WRONG - Python parameter name
classes = element.attrs.get('cls', '') # Returns None or ''
# ✅ CORRECT - HTML attribute name
classes = element.attrs.get('class', '') # Returns actual classes
# ✅ BETTER - Use predicates with matches()
expected = Div(cls=Contains("active"))
assert matches(element, expected)
```
---
### UTR-15: Test Workflow
1. **Receive code to test** - User provides file path or code section
2. **Check existing tests** - Look for corresponding test file and read it if it exists
3. **Analyze code** - Read and understand implementation
4. **Gap analysis** - If tests exist, identify what's missing; otherwise identify all scenarios
5. **Propose test plan** - List new/missing tests with brief explanations
6. **Wait for approval** - User validates the test plan
7. **Implement tests** - Write all approved tests
8. **Verify** - Ensure tests follow naming conventions and structure
9. **Ask before running** - Do NOT automatically run tests with pytest. Ask user first if they want to run the tests.
4. **Trace execution flow** - Apply UTR-12 to understand side effects
5. **Gap analysis** - If tests exist, identify what's missing; otherwise identify all scenarios
6. **Propose test plan** - List new/missing tests with brief explanations
7. **Wait for approval** - User validates the test plan
8. **Implement tests** - Write all approved tests
9. **Verify** - Ensure tests follow naming conventions and structure
10. **Ask before running** - Do NOT automatically run tests with pytest. Ask user first if they want to run the tests.
---
## Managing Rules