"""Message d'aide localisant les documents non livrés (P1-5) + ouverture dossier. Pur : pas de display. ``failure_hint`` formate un texte ; ``open_in_file_manager`` dispatch vers la bonne commande OS (monkeypatchée). """ from __future__ import annotations from pathlib import Path import pytest import gui_v6.fsutil as fsutil from gui_v6.processing_runner import RunSummary from gui_v6.tabs.tab_usage import failure_hint def test_no_hint_when_all_ok(): s = RunSummary(total=3, succeeded=3, failed=0) assert failure_hint(s, Path("/out")) is None def test_hint_when_failures_mentions_output_dir(): s = RunSummary(total=3, succeeded=2, failed=1) hint = failure_hint(s, Path("/out/anonymise")) assert hint is not None assert "/out/anonymise" in hint # Honnêteté : préciser que les échecs ne sont PAS anonymisés. assert "pas" in hint.lower() def test_hint_when_stopped(): s = RunSummary(total=3, succeeded=1, failed=0, stopped=True) assert failure_hint(s, Path("/out")) is not None def test_no_hint_without_output_dir(): s = RunSummary(total=1, succeeded=0, failed=1) assert failure_hint(s, None) is None def test_open_in_file_manager_dispatches(monkeypatch): calls = {} monkeypatch.setattr(fsutil.sys, "platform", "linux") monkeypatch.setattr(fsutil.subprocess, "Popen", lambda args, **k: calls.setdefault("args", args)) fsutil.open_in_file_manager(Path("/out")) assert calls["args"][0] == "xdg-open" assert calls["args"][1] == "/out" def test_no_claim_written_when_zero_succeeded(): """0 succès : ne pas prétendre qu'un dossier contient des documents écrits.""" s = RunSummary(total=2, succeeded=0, failed=2) hint = failure_hint(s, Path("/out")) assert hint is not None assert "écrits dans" not in hint assert "Aucun document" in hint def test_hint_with_path_when_some_succeeded(): """≥1 succès : localiser le dossier de sortie effectif.""" s = RunSummary(total=3, succeeded=2, failed=1) hint = failure_hint(s, Path("/out")) assert hint is not None assert "/out" in hint assert "écrits dans" in hint # -- garde anti-régression du bug Critical (empilement de widgets) ----------- @pytest.fixture def usage_tab(): """``UsageTab`` headless (Xvfb) — skip propre si pas de display.""" pytest.importorskip("customtkinter") try: import customtkinter as ctk from gui_v6.tabs.tab_usage import UsageTab root = ctk.CTk() root.withdraw() tab = UsageTab(root) except Exception as exc: # pas de display Tk pytest.skip(f"display Tk indisponible: {exc}") try: yield tab finally: try: root.destroy() except Exception: pass def test_show_failure_hint_does_not_accumulate(usage_tab): """Bug Critical : deux runs en échec ne doivent pas empiler de hints sous ``_rsec`` (handle ``_hint_row`` détruit inconditionnellement).""" usage_tab._last_output_dir = Path("/out") summary = RunSummary(total=3, succeeded=2, failed=1) usage_tab._show_failure_hint(summary) count_after_first = len(usage_tab._rsec.winfo_children()) usage_tab._show_failure_hint(summary) count_after_second = len(usage_tab._rsec.winfo_children()) assert count_after_second == count_after_first def test_show_failure_hint_clears_stale_hint(usage_tab): """Un run en échec suivi d'un run nominal ne doit pas laisser de hint périmé.""" usage_tab._last_output_dir = Path("/out") usage_tab._show_failure_hint(RunSummary(total=3, succeeded=2, failed=1)) with_hint = len(usage_tab._rsec.winfo_children()) usage_tab._show_failure_hint(RunSummary(total=3, succeeded=3, failed=0)) without_hint = len(usage_tab._rsec.winfo_children()) assert without_hint == with_hint - 1