Added unit tests for Layout.py
This commit is contained in:
@@ -209,79 +209,301 @@ class TestControlRender:
|
||||
- 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**:
|
||||
- Test only important structural elements and attributes
|
||||
- Use predicates for dynamic/generated values
|
||||
- Don't over-specify tests with irrelevant details
|
||||
- Structure tests in layers (overall structure, then details)
|
||||
3. **Apply the best practices** detailed below
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.1 : Pattern de test en trois étapes (RÈGLE FONDAMENTALE)**
|
||||
|
||||
**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)`
|
||||
|
||||
**Pourquoi :** Ce pattern permet des messages d'erreur clairs et sépare la recherche de l'élément de la validation de sa structure.
|
||||
|
||||
**Exemple :**
|
||||
```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
|
||||
header = find_one(layout.render(), Header(cls=Contains("mf-layout-header")))
|
||||
|
||||
# Étape 2 : Définir la structure attendue
|
||||
expected = Header(
|
||||
Div(id=f"{layout._id}_hl"),
|
||||
Div(id=f"{layout._id}_hr"),
|
||||
)
|
||||
|
||||
# Étape 3 : Comparer
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
#### **COMMENT CHERCHER LES ÉLÉMENTS**
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.2 : Privilégier la recherche par ID**
|
||||
|
||||
**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 :**
|
||||
```python
|
||||
# ✅ BON - recherche par ID
|
||||
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
|
||||
|
||||
# ❌ À ÉVITER - recherche par classe quand un ID existe
|
||||
drawer = find_one(layout.render(), Div(cls=Contains("mf-layout-left-drawer")))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.3 : Utiliser `find_one()` vs `find()` selon le contexte**
|
||||
|
||||
**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
|
||||
|
||||
**Exemples :**
|
||||
```python
|
||||
# ✅ BON - find_one pour structure unique
|
||||
header = find_one(layout.render(), Header(cls=Contains("mf-layout-header")))
|
||||
expected = Header(...)
|
||||
assert matches(header, expected)
|
||||
|
||||
# ✅ BON - find pour compter
|
||||
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**
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.4 : Toujours utiliser `Contains()` pour les attributs `cls` et `style`**
|
||||
|
||||
**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()`.
|
||||
|
||||
**Pourquoi :** Évite les faux négatifs dus à l'ordre des classes/propriétés ou aux espaces.
|
||||
|
||||
**Exemples :**
|
||||
```python
|
||||
# ✅ BON - Contains pour cls (une ou plusieurs classes)
|
||||
expected = Div(cls=Contains("mf-layout-drawer"))
|
||||
expected = Div(cls=Contains("mf-layout-drawer", "mf-layout-left-drawer"))
|
||||
|
||||
# ✅ BON - Contains pour style
|
||||
expected = Div(style=Contains("width: 250px"))
|
||||
|
||||
# ❌ À ÉVITER - test exact des classes
|
||||
expected = Div(cls="mf-layout-drawer mf-layout-left-drawer")
|
||||
|
||||
# ❌ À ÉVITER - test exact du style complet
|
||||
expected = Div(style="width: 250px; overflow: hidden; display: flex;")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.5 : Utiliser `TestIcon()` pour tester la présence d'une icône**
|
||||
|
||||
**Principe :** Utilisez `TestIcon("icon_name")` pour tester la présence d'une icône SVG dans le rendu.
|
||||
|
||||
**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
|
||||
|
||||
**Exemples :**
|
||||
```python
|
||||
from myfasthtml.icons.fluent import panel_right_expand20_regular
|
||||
|
||||
# ✅ BON - Tester une icône spécifique
|
||||
expected = Header(
|
||||
Div(
|
||||
TestIcon("panel_right_expand20_regular"),
|
||||
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")
|
||||
)
|
||||
|
||||
# ❌ À ÉVITER - name="svg"
|
||||
expected = Div(TestIcon("svg")) # ERREUR : causera un échec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.6 : Utiliser `TestScript()` pour tester les scripts JavaScript**
|
||||
|
||||
**Principe :** Utilisez `TestScript(code_fragment)` pour vérifier la présence de code JavaScript. Testez uniquement le fragment important, pas le script complet.
|
||||
|
||||
**Exemple :**
|
||||
```python
|
||||
# ✅ BON - TestScript avec fragment important
|
||||
script = find_one(layout.render(), Script())
|
||||
expected = TestScript(f"initResizer('{layout._id}');")
|
||||
assert matches(script, expected)
|
||||
|
||||
# ❌ À ÉVITER - tester tout le contenu du script
|
||||
expected = Script("(function() { const id = '...'; initResizer(id); })()")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **COMMENT DOCUMENTER LES TESTS**
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.7 : Justifier le choix des éléments testés**
|
||||
|
||||
**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é ?
|
||||
|
||||
**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é**.
|
||||
|
||||
**Exemples :**
|
||||
```python
|
||||
def test_empty_layout_is_rendered(self, layout):
|
||||
"""Test that Layout renders with all main structural sections.
|
||||
|
||||
Why these elements matter:
|
||||
- 6 children: Verifies all main sections are rendered (header, drawers, main, footer, script)
|
||||
- _id: Essential for layout identification and resizer initialization
|
||||
- cls="mf-layout": Root CSS class for layout styling
|
||||
"""
|
||||
expected = Div(...)
|
||||
assert matches(layout.render(), expected)
|
||||
|
||||
def test_left_drawer_is_rendered_when_open(self, layout):
|
||||
"""Test that left drawer renders with correct classes when open.
|
||||
|
||||
Why these elements matter:
|
||||
- _id: Required for targeting drawer in HTMX updates
|
||||
- cls Contains "mf-layout-drawer": Base drawer class for styling
|
||||
- cls Contains "mf-layout-left-drawer": Left-specific drawer positioning
|
||||
- style Contains width: Drawer width must be applied for sizing
|
||||
"""
|
||||
layout._state.left_drawer_open = True
|
||||
drawer = find_one(layout.render(), Div(id=f"{layout._id}_ld"))
|
||||
|
||||
expected = Div(
|
||||
_id=f"{layout._id}_ld",
|
||||
cls=Contains("mf-layout-drawer", "mf-layout-left-drawer"),
|
||||
style=Contains("width: 250px")
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
#### **UTR-11.8 : Tests de comptage avec messages explicites**
|
||||
|
||||
**Principe :** Quand vous comptez des éléments avec `assert len()`, ajoutez TOUJOURS un message explicite qui explique pourquoi ce nombre est attendu.
|
||||
|
||||
**Exemple :**
|
||||
```python
|
||||
# ✅ BON - message explicatif
|
||||
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
|
||||
assert len(resizers) == 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **AUTRES RÈGLES IMPORTANTES**
|
||||
|
||||
---
|
||||
|
||||
**Mandatory render test rules:**
|
||||
|
||||
1. **Test naming**: Use descriptive names like `test_empty_layout_is_rendered()` not `test_layout_renders_with_all_sections()`
|
||||
|
||||
2. **Documentation format**: Every render test MUST have a docstring with:
|
||||
- First line: Brief description of what is being tested
|
||||
- Blank line
|
||||
- "Why these elements matter:" or "Why this test matters:" section
|
||||
- Justification section explaining why tested elements matter (see UTR-11.7)
|
||||
- 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
|
||||
4. **Icon testing**: Use `Div(NotStr(name="icon_name"))` to test SVG icons
|
||||
- Use the exact icon name from the import (e.g., `name="panel_right_expand20_regular"`)
|
||||
- Use `name=""` (empty string) if the import is not explicit
|
||||
- NEVER use `name="svg"` - it will cause test failures
|
||||
5. **Component testing**: Use `TestObject(ComponentClass)` to test presence of components
|
||||
6. **Explanation focus**: In "Why these elements matter", refer to the logical element (e.g., "Svg") not the technical implementation (e.g., "Div(NotStr(...))")
|
||||
|
||||
**Example of proper render test:**
|
||||
```python
|
||||
from myfasthtml.test.matcher import matches, find, Contains, NotStr, TestObject
|
||||
from fasthtml.common import Div, Header, Button
|
||||
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`)
|
||||
|
||||
class TestControlRender:
|
||||
def test_empty_control_is_rendered(self, root_instance):
|
||||
"""Test that control renders with main structural sections.
|
||||
4. **Component testing**: Use `TestObject(ComponentClass)` to test presence of components
|
||||
|
||||
Why these elements matter:
|
||||
- 3 children: Verifies header, body, and footer are all rendered
|
||||
- _id: Essential for HTMX targeting and component identification
|
||||
- cls="control-wrapper": Root CSS class for styling
|
||||
"""
|
||||
control = MyControl(root_instance)
|
||||
5. **Test organization for Controls**: Organize tests into thematic classes:
|
||||
- `TestControlBehaviour`: Tests for control behavior and logic
|
||||
- `TestControlRender`: Tests for control HTML rendering
|
||||
|
||||
expected = Div(
|
||||
Header(),
|
||||
Div(),
|
||||
Div(),
|
||||
_id=control._id,
|
||||
cls="control-wrapper"
|
||||
)
|
||||
6. **Fixture usage**: In `TestControlRender`, use a pytest fixture to create the control instance:
|
||||
```python
|
||||
class TestControlRender:
|
||||
@pytest.fixture
|
||||
def layout(self, root_instance):
|
||||
return Layout(root_instance, app_name="Test App")
|
||||
|
||||
assert matches(control.render(), expected)
|
||||
def test_something(self, layout):
|
||||
# layout is injected automatically
|
||||
```
|
||||
|
||||
def test_header_with_icon_is_rendered(self, root_instance):
|
||||
"""Test that header renders with action icon.
|
||||
---
|
||||
|
||||
Why these elements matter:
|
||||
- Svg: Action icon is essential for user interaction
|
||||
- TestObject(ActionButton): ActionButton component must be present
|
||||
- cls="header-bar": Required CSS class for header styling
|
||||
"""
|
||||
control = MyControl(root_instance)
|
||||
header = control._mk_header()
|
||||
#### **Résumé : Les 8 règles UTR-11**
|
||||
|
||||
expected = Header(
|
||||
Div(NotStr(name="action_icon_20_regular")),
|
||||
TestObject(ActionButton),
|
||||
cls="header-bar"
|
||||
)
|
||||
**Pattern fondamental**
|
||||
- **UTR-11.1** : Pattern en trois étapes (extraire → définir expected → comparer)
|
||||
|
||||
assert matches(header, expected)
|
||||
```
|
||||
**Comment chercher**
|
||||
- **UTR-11.2** : Privilégier recherche par ID
|
||||
- **UTR-11.3** : `find_one()` vs `find()` selon contexte
|
||||
|
||||
**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
|
||||
|
||||
**Comment documenter**
|
||||
- **UTR-11.7** : Justifier le choix des éléments testés
|
||||
- **UTR-11.8** : Messages explicites pour `assert len()`
|
||||
|
||||
---
|
||||
|
||||
**When proposing render tests:**
|
||||
- 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 "Why these elements matter" documentation
|
||||
- Always include justification documentation (see UTR-11.7)
|
||||
|
||||
### UTR-12: Test Workflow
|
||||
|
||||
|
||||
Reference in New Issue
Block a user