Files
pointeuse-optimisator/app/tests/test_ui_refonte.py
Jacquin Antoine 1b4bf0d488 Forfait d'astreinte : facteur journée affiché, plus de flèche fantôme
La pondération des forfaits suit le facteur journée du jour AS
(semaine ×1, samedi ×1,5, dimanche/férié ×2) : sur des jours de
semaine elle ne change rien, et le récapitulatif affichait
« 0h45 → 0h45 pondérés » — comme si la pondération n'était jamais
appliquée. Le facteur est désormais toujours visible par jour
(« forfait 0h15 ×1 »), et la flèche n'apparaît que quand la
bonification change réellement le total (« ×1 (jours de semaine) »
sinon).

💘 Generated with Crush

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

202 lines
10 KiB
Python
Raw Permalink 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 ×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