Files
rpa_vision_v3/tests/property/test_loading_state_properties_12jan2026.py
Dom a27b74cf22 v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- 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>
2026-01-29 11:23:51 +01:00

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"])