feat: refonte UX page /matches — révision carte par carte
- Remplace la table trop large par une carte de révision centrée - Une paire à la fois : noms wrappés, score, prix moyens - Valider/Rejeter via fetch() sans rechargement de page - Passage automatique à la paire suivante après chaque action - Compteurs mis à jour en temps réel (en attente/validées/rejetées) - Message de fin avec lien vers /compare quand tout est traité - Ajout tests pytest pour la page /matches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -268,3 +268,118 @@ def test_api_product_history_not_found(client_with_data):
|
||||
"""GET /api/product/<inconnu>/history retourne 404."""
|
||||
resp = client_with_data.get("/api/product/ProduitInexistant/history")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests /matches — DB vide et avec données
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_matches_page_empty_200(client):
|
||||
"""/matches accessible même si la base est vide."""
|
||||
resp = client.get("/matches")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_matches_page_shows_no_pending(client):
|
||||
"""/matches sans données affiche un message indiquant qu'il n'y a rien à valider."""
|
||||
resp = client.get("/matches")
|
||||
assert resp.status_code == 200
|
||||
# Le template affiche soit "Aucune paire" soit un message d'invitation
|
||||
assert "match" in resp.text.lower() or "paire" in resp.text.lower()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_path_with_match(db_path: Path) -> Path:
|
||||
"""Base avec 1 paire fuzzy pending dans product_matches."""
|
||||
conn = schema.get_connection(db_path)
|
||||
try:
|
||||
with conn:
|
||||
conn.execute(
|
||||
"INSERT INTO product_matches "
|
||||
"(name_picnic, name_leclerc, score, status, created_at) "
|
||||
"VALUES ('lait demi-écrémé', 'lait demi ecreme', 92.0, 'pending', '2026-01-01T00:00:00')"
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
return db_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client_with_match(db_path_with_match: Path):
|
||||
"""TestClient avec une paire fuzzy pending."""
|
||||
with patch("tickettracker.config.DB_PATH", db_path_with_match):
|
||||
yield TestClient(app)
|
||||
|
||||
|
||||
def test_matches_page_shows_pending(client_with_match):
|
||||
"""/matches affiche la paire pending."""
|
||||
resp = client_with_match.get("/matches")
|
||||
assert resp.status_code == 200
|
||||
assert "lait demi" in resp.text.lower()
|
||||
|
||||
|
||||
def test_api_match_validate_200(client_with_match, db_path_with_match):
|
||||
"""POST /api/match/1/validate retourne 200 et met à jour le statut."""
|
||||
resp = client_with_match.post("/api/match/1/validate")
|
||||
assert resp.status_code == 200
|
||||
# Vérification en base
|
||||
conn = schema.get_connection(db_path_with_match)
|
||||
status = conn.execute("SELECT status FROM product_matches WHERE id=1").fetchone()["status"]
|
||||
conn.close()
|
||||
assert status == "validated"
|
||||
|
||||
|
||||
def test_api_match_reject_200(client_with_match, db_path_with_match):
|
||||
"""POST /api/match/1/reject retourne 200 et met à jour le statut."""
|
||||
resp = client_with_match.post("/api/match/1/reject")
|
||||
assert resp.status_code == 200
|
||||
conn = schema.get_connection(db_path_with_match)
|
||||
status = conn.execute("SELECT status FROM product_matches WHERE id=1").fetchone()["status"]
|
||||
conn.close()
|
||||
assert status == "rejected"
|
||||
|
||||
|
||||
def test_api_match_validate_not_found(client):
|
||||
"""POST /api/match/999/validate retourne 404."""
|
||||
resp = client.post("/api/match/999/validate")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_api_match_reject_not_found(client):
|
||||
"""POST /api/match/999/reject retourne 404."""
|
||||
resp = client.post("/api/match/999/reject")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_api_compare_includes_fuzzy_match(db_path_with_data: Path):
|
||||
"""GET /api/compare retourne les fuzzy matches validés dans les résultats."""
|
||||
# Insérer un fuzzy match validé
|
||||
conn = schema.get_connection(db_path_with_data)
|
||||
try:
|
||||
with conn:
|
||||
# Normaliser les articles pour avoir les données dans price_history
|
||||
conn.execute(
|
||||
"UPDATE items SET name_normalized = 'lait demi-écrémé' "
|
||||
"WHERE name_raw = 'Lait demi-écremé'"
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE items SET name_normalized = 'lait demi ecreme' "
|
||||
"WHERE name_raw = 'LAIT DEMI ECREME'"
|
||||
)
|
||||
# Insérer un fuzzy match validé liant les deux noms
|
||||
conn.execute(
|
||||
"INSERT INTO product_matches "
|
||||
"(name_picnic, name_leclerc, score, status, created_at) "
|
||||
"VALUES ('lait demi-écrémé', 'lait demi ecreme', 92.0, 'validated', '2026-01-01T00:00:00')"
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
with patch("tickettracker.config.DB_PATH", db_path_with_data):
|
||||
test_client = TestClient(app)
|
||||
resp = test_client.get("/api/compare")
|
||||
|
||||
assert resp.status_code == 200
|
||||
products = resp.json()
|
||||
match_types = [p["match_type"] for p in products]
|
||||
assert "fuzzy" in match_types
|
||||
|
||||
Reference in New Issue
Block a user