"""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' 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 ×1,5 → 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_forfait_semaine_affiche_le_facteur_1_sans_ponderation_fantome(monkeypatch): """Forfait sur des jours AS de semaine : le facteur journée vaut ×1, la pondération ne change donc rien. L'affichage doit le dire explicitement (« forfait 0h15 ×1 », « forfaits 0h45 ×1 (jours de semaine) ») au lieu d'une flèche « → pondérés » qui suggère une pondération jamais appliquée.""" monkeypatch.setattr(main, "datetime", _FrozenDatetime) cookies = _login("u-ui-forfait-x1") r = client.post("/settings/astreinte-forfait", cookies=cookies, data={"astreinte_forfait": "00:15"}, follow_redirects=False) assert r.status_code == 303, r.text for d in ("2026-07-13", "2026-07-15", "2026-07-16"): # lun, mer, jeu (mardi 14/07 = férié ×2) client.post(f"/astreinte/{d}/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 r.text.count("forfait 0h15 ×1") == 3 # lun, mar, mer : facteur visible assert "forfaits 0h45 ×1 (jours de semaine)" in r.text # totaux : pas de flèche fantôme assert "(+ 0h45 de forfaits ×1)" in r.text # grand total idem assert "forfaits 0h45 →" not in r.text # aucune pondération affichée à tort 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