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
202 lines
10 KiB
Python
202 lines
10 KiB
Python
"""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
|