Files
pointeuse-optimisator/app/tests/test_ui_refonte.py
Jacquin Antoine edf52acdc9 Semaine parfaite : célébration kitsch quand tout boucle à +0h00
La dernière heure saisie qui clôt la semaine à +0h00 pile, sans
aucune demi-journée restante, déclenche une fête assumée : fond disco
en rotation de teintes, pluie d'une fourgonnée de confettis (emojis
et rectangles), trophée bondissant et titre doré défilant. Une seule
fois par semaine et par onglet, fermeture au clic ou après 8 s, et
animation réduite pour prefers-reduced-motion.

Le serveur pose un drapeau data-perfect (solde nul, plus rien à
remplir, heures dues non nulles) sur les soldes re-rendus à chaque
saisie et rafraîchissement ; le client le détecte et célèbre.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-09-11 15:29:46 +02:00

181 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Refonte de l'interface : auto-enregistrement (plus de bouton ✓), menu
d'état unique (congé / férié / astreinte) et panneau aujourd'hui.
Vérifie le rendu de la page semaine et les réponses HTMX (swaps OOB) qui
régénèrent ces composants.
"""
import datetime as dt_module
import sys
# DATA_DIR temporaire est positionné par conftest.py avant l'import de models.
import models # noqa: F401
sys.path.insert(0, ".")
import main
from fastapi.testclient import TestClient
client = TestClient(main.app)
TODAY = "2026-07-17" # vendredi de la semaine 29 (cf. datetime gelé)
class _FrozenDatetime(dt_module.datetime):
@classmethod
def today(cls):
return cls(2026, 7, 17, 10, 30)
def _login(uid: str) -> dict:
import bcrypt
models.save_auth(uid, email=f"{uid}@x.fr",
password_hash=bcrypt.hashpw(b"x" * 12, bcrypt.gensalt()).decode())
r = client.post("/login/password",
data={"email": f"{uid}@x.fr", "password": "x" * 12},
follow_redirects=False)
assert r.status_code == 302, r.text
return {"user_id": r.cookies.get("user_id")}
def test_page_semaine_rend_les_nouveaux_composants(monkeypatch):
monkeypatch.setattr(main, "datetime", _FrozenDatetime)
cookies = _login("u-ui")
r = client.get("/semaine/2026/29", cookies=cookies, follow_redirects=False)
assert r.status_code == 200
# Auto-enregistrement : un témoin par jour, plus de bouton ✓ ni sa colonne
assert "btn-valider" not in r.text
assert "col-save" not in r.text
assert r.text.count('class="autosave"') == 7
assert 'hx-trigger="save-day"' in r.text
# Menu d'état : un par jour, options des cinq bascules
assert r.text.count('class="cg-menu"') == 7
assert "Congé matin" in r.text
assert "Jour férié" in r.text
assert 'cg-opt-chip as">AS</span>' in r.text # option jour d'astreinte
# Panneau aujourd'hui (le jour gelé appartient à la semaine affichée)
assert "today-panel" in r.text
assert "Aujourd'hui ·" in r.text # Jinja laisse l'apostrophe telle quelle
def test_pointage_htmx_rafrachit_panneau_et_compteurs(monkeypatch):
monkeypatch.setattr(main, "datetime", _FrozenDatetime)
cookies = _login("u-ui-pointage")
r = client.post(f"/pointage/{TODAY}", cookies=cookies,
data={"matin_entree": "08:00", "matin_sortie": "12:00"},
headers={"HX-Request": "true"}, follow_redirects=False)
assert r.status_code == 200
assert f'id="calc-{TODAY}-trav"' in r.text # compteurs OOB
assert "today-panel" in r.text # soldes + panneau re-rendus
def test_bascule_congen_regenere_le_menu_d_etat(monkeypatch):
monkeypatch.setattr(main, "datetime", _FrozenDatetime)
cookies = _login("u-ui-conge")
r = client.post(f"/conge/{TODAY}/matin", cookies=cookies,
headers={"HX-Request": "true"}, follow_redirects=False)
assert r.status_code == 200
# Le swap OOB régénère le bouton avec la pastille MA active
assert 'cg-chip st-ma">MA<' in r.text
assert 'id="cg-btn-' + TODAY + '"' in r.text
def test_retour_decale_affiche_le_credit_de_presence(monkeypatch):
"""Retour mercredi 11:00 (event mardi 22:39→23:49) : la colonne Travaillé
crédite les 2h de présence couvertes, avec badge et détail explicites."""
monkeypatch.setattr(main, "datetime", _FrozenDatetime)
cookies = _login("u-ui-credit")
client.post("/astreinte/2026-07-14/toggle", cookies=cookies, follow_redirects=False)
r = client.post("/pointage/2026-07-14", cookies=cookies,
data={"astreinte_debut": ["22:39"], "astreinte_fin": ["23:49"]},
headers={"HX-Request": "true"}, follow_redirects=False)
assert r.status_code == 200
# Le crédit s'affiche sur la ligne du lendemain (jour du retour décalé)
r = client.get("/semaine/2026/29", cookies=cookies, follow_redirects=False)
assert r.status_code == 200
assert 'class="calc-couvert"' in r.text
assert "+2h00 repos" in r.text
assert "matin 09:00→11:00" in r.text # détail des plages couvertes
assert "de présence comptées" in r.text # rappel dans le bloc astreinte
def test_forfait_bonifie_journee_affiche_dans_le_recap(monkeypatch):
"""Forfait 1h00 réglé + AS le samedi 18/07 : le récapitulatif montre le
forfait pondéré par la bonification journée du samedi (×1,5 → 1h30)."""
monkeypatch.setattr(main, "datetime", _FrozenDatetime)
cookies = _login("u-ui-forfait")
r = client.post("/settings/astreinte-forfait", cookies=cookies,
data={"astreinte_forfait": "01:00"}, follow_redirects=False)
assert r.status_code == 303, r.text
client.post("/astreinte/2026-07-18/toggle", cookies=cookies, follow_redirects=False)
r = client.get("/semaine/2026/29", cookies=cookies, follow_redirects=False)
assert r.status_code == 200, r.text
assert "forfait 1h00 → 1h30 pondérées" in r.text # ligne du jour (bloc AS)
assert "forfaits 1h00 → 1h30 pondérés" in r.text # totaux du récapitulatif
assert "considéré réalisé en journée" in r.text # explication au survol
def test_refresh_regenerere_les_cibles_en_fonction_de_lheure(monkeypatch):
"""Le poll /refresh (toutes les 30 s, et au retour sur l'onglet) régénère
les cibles OOB avec l'heure courante : à 14:12, l'après-midi non commencé
du vendredi ancre sa reprise à maintenant — jamais à l'heure désirée 13:30
passée — et la sortie s'ajuste au reste à faire de la semaine."""
class _Frozen14(dt_module.datetime):
@classmethod
def today(cls):
return cls(2026, 7, 17, 14, 12)
monkeypatch.setattr(main, "datetime", _Frozen14)
import calcul
monkeypatch.setattr(calcul, "datetime", _Frozen14)
cookies = _login("u-ui-refresh")
# Lundi-jeudi complets, vendredi matin seulement : reste 4h42,
# seul créneau ouvert = vendredi aprem, reprise ancrée à 14:12.
for d in ("2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"):
client.post(f"/pointage/{d}", cookies=cookies,
data={"matin_entree": "09:00", "matin_sortie": "12:24",
"aprem_entree": "13:30", "aprem_sortie": "17:54"},
headers={"HX-Request": "true"}, follow_redirects=False)
client.post(f"/pointage/{TODAY}", cookies=cookies,
data={"matin_entree": "09:00", "matin_sortie": "12:00"},
headers={"HX-Request": "true"}, follow_redirects=False)
r = client.get(f"/refresh/{TODAY}", cookies=cookies, follow_redirects=False)
assert r.status_code == 200
# Cible du jour régénérée, ancrée à maintenant (14:12), pas à 13:30
assert f'id="calc-{TODAY}-cible"' in r.text
assert "A 14:12 → 19:00" in r.text
# Toutes les cibles de la semaine suivent (swaps OOB), pas seulement le jour
for d in ("2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"):
assert f'id="calc-{d}-cible"' in r.text
# Le panneau aujourd'hui est rafraîchi avec la même sortie cible
assert "today-panel" in r.text
assert "19:00" in r.text
def test_semaine_parfaite_marquee_pour_la_celebration(monkeypatch):
"""La dernière heure saisie qui boucle la semaine à +0h00 pile pose le
drapeau data-perfect (clé semaine) sur les soldes : le client déclenche la
célébration. Semaine incomplète ou solde non nul → pas de drapeau."""
monkeypatch.setattr(main, "datetime", _FrozenDatetime)
cookies = _login("u-ui-perfect")
jours = {"matin_entree": "09:00", "matin_sortie": "12:24",
"aprem_entree": "13:30", "aprem_sortie": "17:54"} # 7h48 pile
for d in ("2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"):
client.post(f"/pointage/{d}", cookies=cookies, data=jours,
headers={"HX-Request": "true"}, follow_redirects=False)
# Semaine incomplète (vendredi vide) : pas de drapeau
r = client.post(f"/pointage/{TODAY}", cookies=cookies,
data={"matin_entree": "09:00", "matin_sortie": "12:24",
"aprem_entree": "13:30", "aprem_sortie": "17:54"},
headers={"HX-Request": "true"}, follow_redirects=False)
assert r.status_code == 200
assert 'data-perfect="2026-W29"' in r.text # 5×7h48 = 39h00 pile : parfait !
# Solde non nul (vendredi écourté) : le drapeau disparaît
r = client.post(f"/pointage/{TODAY}", cookies=cookies,
data={"matin_entree": "09:00", "matin_sortie": "12:24",
"aprem_entree": "13:30", "aprem_sortie": "17:00"},
headers={"HX-Request": "true"}, follow_redirects=False)
assert r.status_code == 200
assert "data-perfect" not in r.text