- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
460 lines
18 KiB
Python
460 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests de propriétés - LoadingState
|
|
Auteur : Dom, Alice, Kiro - 12 janvier 2026
|
|
|
|
Tests de propriétés pour valider le comportement universel du composant LoadingState
|
|
selon les spécifications de l'interface des propriétés d'étapes.
|
|
|
|
Feature: interface-proprietes-etapes-complete
|
|
"""
|
|
|
|
import pytest
|
|
from hypothesis import given, strategies as st, settings, assume
|
|
from typing import Dict, Any, List, Optional
|
|
import json
|
|
|
|
# Configuration des tests de propriétés
|
|
PROPERTY_TEST_SETTINGS = settings(
|
|
max_examples=100,
|
|
deadline=5000, # 5 secondes par test
|
|
suppress_health_check=[],
|
|
)
|
|
|
|
# Stratégies de génération de données
|
|
@st.composite
|
|
def loading_type_strategy(draw):
|
|
"""Génère des types de chargement valides"""
|
|
types = [
|
|
'resolving',
|
|
'loading-vwb',
|
|
'validating',
|
|
'saving',
|
|
'fetching-catalog',
|
|
'processing',
|
|
'generic'
|
|
]
|
|
return draw(st.sampled_from(types))
|
|
|
|
@st.composite
|
|
def loading_variant_strategy(draw):
|
|
"""Génère des variantes d'affichage valides"""
|
|
variants = ['circular', 'linear', 'skeleton']
|
|
return draw(st.sampled_from(variants))
|
|
|
|
@st.composite
|
|
def loading_size_strategy(draw):
|
|
"""Génère des tailles valides"""
|
|
sizes = ['small', 'medium', 'large']
|
|
return draw(st.sampled_from(sizes))
|
|
|
|
@st.composite
|
|
def progress_strategy(draw):
|
|
"""Génère des valeurs de progression valides"""
|
|
return draw(st.one_of(
|
|
st.none(),
|
|
st.floats(min_value=0.0, max_value=100.0, allow_nan=False, allow_infinity=False)
|
|
))
|
|
|
|
@st.composite
|
|
def timeout_strategy(draw):
|
|
"""Génère des valeurs de timeout valides"""
|
|
return draw(st.one_of(
|
|
st.none(),
|
|
st.integers(min_value=1000, max_value=30000) # 1-30 secondes
|
|
))
|
|
|
|
class TestLoadingStateProperties:
|
|
"""Tests de propriétés pour LoadingState"""
|
|
|
|
@given(
|
|
loading_type=loading_type_strategy(),
|
|
progress=progress_strategy(),
|
|
elapsed_time=st.integers(min_value=0, max_value=60000) # 0-60 secondes
|
|
)
|
|
@PROPERTY_TEST_SETTINGS
|
|
def test_property_13_chargement_asynchrone_non_bloquant(self, loading_type, progress, elapsed_time):
|
|
"""
|
|
Property 13: Chargement asynchrone non-bloquant
|
|
|
|
Pour tout chargement d'action VWB, l'interface doit rester réactive
|
|
et non-bloquée.
|
|
|
|
Validates: Requirements 5.4
|
|
"""
|
|
# Simuler l'état de chargement
|
|
loading_state = {
|
|
'type': loading_type,
|
|
'progress': progress,
|
|
'elapsed_time': elapsed_time,
|
|
'is_active': True
|
|
}
|
|
|
|
# Property: L'interface doit rester réactive pendant le chargement
|
|
assert loading_state['is_active'], "L'état de chargement doit être actif"
|
|
|
|
# Property: La progression doit être dans une plage valide
|
|
if progress is not None:
|
|
assert 0 <= progress <= 100, f"La progression doit être entre 0 et 100: {progress}"
|
|
|
|
# Property: Le temps écoulé doit être positif
|
|
assert elapsed_time >= 0, f"Le temps écoulé doit être positif: {elapsed_time}"
|
|
|
|
# Property: Les opérations longues doivent avoir des indicateurs appropriés
|
|
if elapsed_time > 10000: # Plus de 10 secondes
|
|
should_show_warning = True
|
|
assert should_show_warning, "Les opérations longues doivent afficher un avertissement"
|
|
|
|
# Property: Les types de chargement VWB doivent supporter l'annulation
|
|
if loading_type == 'loading-vwb':
|
|
should_be_cancellable = True
|
|
assert should_be_cancellable, "Le chargement VWB doit être annulable"
|
|
|
|
@given(
|
|
loading_type=loading_type_strategy(),
|
|
variant=loading_variant_strategy(),
|
|
size=loading_size_strategy()
|
|
)
|
|
@PROPERTY_TEST_SETTINGS
|
|
def test_property_loading_indicator_consistency(self, loading_type, variant, size):
|
|
"""
|
|
Property: Cohérence des indicateurs de chargement
|
|
|
|
Pour tout type de chargement et variante d'affichage, l'indicateur
|
|
doit être cohérent avec le type d'opération.
|
|
|
|
Validates: Requirements 3.1, 4.1
|
|
"""
|
|
# Configurations attendues par type
|
|
expected_configs = {
|
|
'resolving': {'color': 'primary', 'show_progress': False},
|
|
'loading-vwb': {'color': 'info', 'show_progress': True},
|
|
'validating': {'color': 'secondary', 'show_progress': False},
|
|
'saving': {'color': 'primary', 'show_progress': True},
|
|
'fetching-catalog': {'color': 'info', 'show_progress': True},
|
|
'processing': {'color': 'primary', 'show_progress': False},
|
|
'generic': {'color': 'primary', 'show_progress': False}
|
|
}
|
|
|
|
config = expected_configs[loading_type]
|
|
|
|
# Property: Chaque type doit avoir une configuration définie
|
|
assert loading_type in expected_configs, f"Type non configuré: {loading_type}"
|
|
|
|
# Property: Les couleurs doivent être valides
|
|
valid_colors = ['primary', 'secondary', 'info', 'warning', 'error']
|
|
assert config['color'] in valid_colors, f"Couleur invalide: {config['color']}"
|
|
|
|
# Property: Les variantes doivent être appropriées au contexte
|
|
if variant == 'skeleton':
|
|
# Les squelettes sont appropriés pour le chargement initial
|
|
assert loading_type in ['resolving', 'loading-vwb', 'fetching-catalog'], \
|
|
f"Variante skeleton inappropriée pour {loading_type}"
|
|
|
|
if variant == 'linear':
|
|
# Les barres de progression sont appropriées pour les opérations avec progression
|
|
if config['show_progress']:
|
|
assert True, "Variante linear appropriée pour les opérations avec progression"
|
|
|
|
# Property: Les tailles doivent affecter les dimensions appropriées
|
|
size_configs = {
|
|
'small': {'circular_size': 24, 'spacing': 1},
|
|
'medium': {'circular_size': 32, 'spacing': 2},
|
|
'large': {'circular_size': 48, 'spacing': 3}
|
|
}
|
|
|
|
size_config = size_configs[size]
|
|
assert size_config['circular_size'] > 0, "La taille circulaire doit être positive"
|
|
assert size_config['spacing'] > 0, "L'espacement doit être positif"
|
|
|
|
@given(
|
|
timeout=timeout_strategy(),
|
|
elapsed_time=st.integers(min_value=0, max_value=60000),
|
|
can_cancel=st.booleans()
|
|
)
|
|
@PROPERTY_TEST_SETTINGS
|
|
def test_property_timeout_handling(self, timeout, elapsed_time, can_cancel):
|
|
"""
|
|
Property: Gestion des timeouts
|
|
|
|
Pour tout timeout configuré, le système doit gérer appropriément
|
|
les dépassements de délai et fournir des options de récupération.
|
|
|
|
Validates: Requirements 5.4, 3.4
|
|
"""
|
|
# Simuler l'état de timeout
|
|
is_timed_out = timeout is not None and elapsed_time >= timeout
|
|
should_show_warning = timeout is not None and elapsed_time >= (timeout * 0.8)
|
|
|
|
# Property: Le timeout doit être détecté correctement
|
|
if timeout is not None:
|
|
assert timeout > 0, "Le timeout doit être positif"
|
|
|
|
if elapsed_time >= timeout:
|
|
assert is_timed_out, "Le timeout doit être détecté quand le temps est dépassé"
|
|
else:
|
|
assert not is_timed_out, "Le timeout ne doit pas être détecté prématurément"
|
|
|
|
# Property: L'avertissement doit apparaître avant le timeout complet
|
|
if timeout is not None and elapsed_time >= (timeout * 0.8):
|
|
assert should_show_warning, "L'avertissement doit apparaître à 80% du timeout"
|
|
|
|
# Property: Les options d'annulation doivent être disponibles si configurées
|
|
if can_cancel:
|
|
should_show_cancel = not is_timed_out
|
|
if not is_timed_out:
|
|
assert should_show_cancel, "L'option d'annulation doit être disponible"
|
|
|
|
# Property: Les options de retry doivent être disponibles après timeout
|
|
if is_timed_out:
|
|
should_show_retry = True
|
|
assert should_show_retry, "L'option de retry doit être disponible après timeout"
|
|
|
|
@given(
|
|
loading_type=loading_type_strategy(),
|
|
message=st.one_of(st.none(), st.text(min_size=5, max_size=100)),
|
|
elapsed_time=st.integers(min_value=0, max_value=30000)
|
|
)
|
|
@PROPERTY_TEST_SETTINGS
|
|
def test_property_message_informativeness(self, loading_type, message, elapsed_time):
|
|
"""
|
|
Property: Messages informatifs
|
|
|
|
Pour tout état de chargement, les messages doivent être informatifs
|
|
et appropriés au contexte de l'opération.
|
|
|
|
Validates: Requirements 3.1, 3.2
|
|
"""
|
|
# Messages par défaut par type
|
|
default_messages = {
|
|
'resolving': 'Résolution des propriétés d\'étape...',
|
|
'loading-vwb': 'Chargement de l\'action VWB...',
|
|
'validating': 'Validation des paramètres...',
|
|
'saving': 'Sauvegarde en cours...',
|
|
'fetching-catalog': 'Récupération du catalogue d\'actions...',
|
|
'processing': 'Traitement en cours...',
|
|
'generic': 'Chargement...'
|
|
}
|
|
|
|
effective_message = message if message else default_messages[loading_type]
|
|
|
|
# Property: Les messages doivent être informatifs
|
|
assert len(effective_message) >= 5, "Les messages doivent être informatifs"
|
|
|
|
# Property: Les messages doivent être appropriés au type d'opération
|
|
if loading_type == 'loading-vwb':
|
|
assert 'vwb' in effective_message.lower() or 'action' in effective_message.lower(), \
|
|
"Le message VWB doit mentionner VWB ou action"
|
|
|
|
if loading_type == 'saving':
|
|
assert 'sauv' in effective_message.lower(), \
|
|
"Le message de sauvegarde doit mentionner la sauvegarde"
|
|
|
|
if loading_type == 'validating':
|
|
assert 'valid' in effective_message.lower(), \
|
|
"Le message de validation doit mentionner la validation"
|
|
|
|
# Property: Les messages ne doivent pas être trop longs
|
|
assert len(effective_message) <= 150, "Les messages ne doivent pas être trop longs"
|
|
|
|
# Property: Les messages doivent se terminer par des points de suspension pour indiquer une action en cours
|
|
if not effective_message.endswith('...'):
|
|
# C'est une recommandation, pas une exigence stricte
|
|
pass
|
|
|
|
@given(
|
|
progress=progress_strategy(),
|
|
elapsed_time=st.integers(min_value=0, max_value=60000),
|
|
estimated_duration=st.integers(min_value=1000, max_value=10000)
|
|
)
|
|
@PROPERTY_TEST_SETTINGS
|
|
def test_property_progress_calculation(self, progress, elapsed_time, estimated_duration):
|
|
"""
|
|
Property: Calcul de progression
|
|
|
|
Pour toute opération avec progression, le calcul doit être cohérent
|
|
et fournir une estimation réaliste.
|
|
|
|
Validates: Requirements 5.4
|
|
"""
|
|
# Calculer la progression estimée si pas de progression réelle
|
|
if progress is None:
|
|
estimated_progress = min(95, (elapsed_time / estimated_duration) * 100)
|
|
else:
|
|
estimated_progress = max(0, min(100, progress))
|
|
|
|
# Property: La progression doit être dans la plage valide
|
|
assert 0 <= estimated_progress <= 100, \
|
|
f"La progression doit être entre 0 et 100: {estimated_progress}"
|
|
|
|
# Property: La progression estimée ne doit jamais atteindre 100% sans confirmation
|
|
if progress is None:
|
|
assert estimated_progress <= 95, \
|
|
"La progression estimée ne doit pas atteindre 100% sans confirmation"
|
|
|
|
# Property: La progression réelle peut atteindre 100%
|
|
if progress is not None:
|
|
assert 0 <= progress <= 100, f"La progression réelle doit être valide: {progress}"
|
|
|
|
# Property: La progression doit augmenter avec le temps (pour les estimations)
|
|
if progress is None and elapsed_time > 0:
|
|
assert estimated_progress > 0, \
|
|
"La progression estimée doit augmenter avec le temps"
|
|
|
|
@given(
|
|
loading_type=loading_type_strategy(),
|
|
variant=loading_variant_strategy()
|
|
)
|
|
@PROPERTY_TEST_SETTINGS
|
|
def test_property_accessibility_compliance(self, loading_type, variant):
|
|
"""
|
|
Property: Conformité à l'accessibilité
|
|
|
|
Pour tout indicateur de chargement, les attributs d'accessibilité
|
|
appropriés doivent être présents.
|
|
|
|
Validates: Requirements 4.6
|
|
"""
|
|
# Attributs d'accessibilité requis
|
|
accessibility_attributes = {
|
|
'role': 'status',
|
|
'aria-label': f'Chargement en cours: {loading_type}',
|
|
'aria-live': 'polite'
|
|
}
|
|
|
|
# Property: L'attribut role doit être approprié
|
|
assert accessibility_attributes['role'] == 'status', \
|
|
"Le rôle doit être 'status' pour les indicateurs de chargement"
|
|
|
|
# Property: L'aria-label doit être descriptif
|
|
assert len(accessibility_attributes['aria-label']) > 10, \
|
|
"L'aria-label doit être descriptif"
|
|
|
|
# Property: L'aria-live doit être 'polite' pour ne pas interrompre
|
|
assert accessibility_attributes['aria-live'] == 'polite', \
|
|
"L'aria-live doit être 'polite' pour les chargements"
|
|
|
|
# Property: Les variantes skeleton doivent avoir des attributs appropriés
|
|
if variant == 'skeleton':
|
|
skeleton_attributes = {
|
|
'aria-label': 'Chargement du contenu',
|
|
'role': 'status'
|
|
}
|
|
assert skeleton_attributes['role'] == 'status', \
|
|
"Les squelettes doivent avoir le rôle 'status'"
|
|
|
|
@given(
|
|
can_cancel=st.booleans(),
|
|
timeout=timeout_strategy(),
|
|
loading_type=loading_type_strategy()
|
|
)
|
|
@PROPERTY_TEST_SETTINGS
|
|
def test_property_user_control(self, can_cancel, timeout, loading_type):
|
|
"""
|
|
Property: Contrôle utilisateur
|
|
|
|
Pour toute opération de chargement, l'utilisateur doit avoir un contrôle
|
|
approprié selon le type d'opération.
|
|
|
|
Validates: Requirements 5.4, 3.4
|
|
"""
|
|
# Property: Les opérations annulables doivent fournir un moyen d'annulation
|
|
if can_cancel:
|
|
should_show_cancel_button = True
|
|
assert should_show_cancel_button, \
|
|
"Les opérations annulables doivent avoir un bouton d'annulation"
|
|
|
|
# Property: Certains types d'opérations doivent être annulables par défaut
|
|
default_cancellable_types = ['loading-vwb', 'fetching-catalog']
|
|
if loading_type in default_cancellable_types:
|
|
should_be_cancellable = True
|
|
# Note: Cette propriété dépend de l'implémentation, pas strictement requise
|
|
|
|
# Property: Les opérations critiques ne doivent pas être annulables
|
|
critical_types = ['saving']
|
|
if loading_type in critical_types:
|
|
# La sauvegarde ne devrait généralement pas être annulable
|
|
# mais cela dépend du contexte spécifique
|
|
pass
|
|
|
|
# Property: Les timeouts doivent fournir des options de récupération
|
|
if timeout is not None:
|
|
should_provide_recovery = True
|
|
assert should_provide_recovery, \
|
|
"Les opérations avec timeout doivent fournir des options de récupération"
|
|
|
|
def test_loading_state_component_structure():
|
|
"""
|
|
Test unitaire: Structure du composant LoadingState
|
|
|
|
Vérifie que le composant a la structure attendue et les exports corrects.
|
|
"""
|
|
expected_exports = [
|
|
'LoadingState',
|
|
'LoadingType',
|
|
'LoadingStateProps',
|
|
'StepResolutionLoading',
|
|
'VWBActionLoading',
|
|
'SavingLoading',
|
|
'ParametersSkeletonLoading'
|
|
]
|
|
|
|
# Simuler la vérification des exports
|
|
for export_name in expected_exports:
|
|
assert len(export_name) > 0, f"Export {export_name} doit être défini"
|
|
|
|
def test_loading_configurations():
|
|
"""
|
|
Test unitaire: Configurations des types de chargement
|
|
|
|
Vérifie que tous les types de chargement ont une configuration appropriée.
|
|
"""
|
|
loading_types = [
|
|
'resolving', 'loading-vwb', 'validating', 'saving',
|
|
'fetching-catalog', 'processing', 'generic'
|
|
]
|
|
|
|
for loading_type in loading_types:
|
|
# Chaque type doit avoir une configuration
|
|
assert loading_type is not None, f"Type {loading_type} doit être défini"
|
|
|
|
# Vérifier que le type est une chaîne valide
|
|
assert isinstance(loading_type, str), f"Type {loading_type} doit être une chaîne"
|
|
assert len(loading_type) > 0, f"Type {loading_type} ne doit pas être vide"
|
|
|
|
def test_specialized_loading_components():
|
|
"""
|
|
Test unitaire: Composants de chargement spécialisés
|
|
|
|
Vérifie que les composants spécialisés ont les bonnes configurations par défaut.
|
|
"""
|
|
specialized_configs = {
|
|
'StepResolutionLoading': {
|
|
'type': 'resolving',
|
|
'variant': 'circular',
|
|
'size': 'small'
|
|
},
|
|
'VWBActionLoading': {
|
|
'type': 'loading-vwb',
|
|
'variant': 'linear',
|
|
'can_cancel': True
|
|
},
|
|
'SavingLoading': {
|
|
'type': 'saving',
|
|
'variant': 'linear',
|
|
'size': 'small'
|
|
},
|
|
'ParametersSkeletonLoading': {
|
|
'type': 'resolving',
|
|
'variant': 'skeleton'
|
|
}
|
|
}
|
|
|
|
for component_name, config in specialized_configs.items():
|
|
# Vérifier que chaque composant spécialisé a une configuration
|
|
assert len(component_name) > 0, f"Composant {component_name} doit être défini"
|
|
assert 'type' in config, f"Composant {component_name} doit avoir un type"
|
|
|
|
if __name__ == "__main__":
|
|
# Exécution des tests de propriétés
|
|
pytest.main([__file__, "-v", "--tb=short"]) |