Files
williamandClaude Sonnet 5 d8be80ebd1
Build and deploy / test-python (push) Failing after 1m0s
Build and deploy / test-js (push) Failing after 50s
Build and deploy / lint-python (push) Failing after 1m2s
Build and deploy / lint-js (push) Failing after 1m7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 1m1s
Implemente le mini-jeu Mots meles (grille reelle, placement 4 directions)
Quatrieme mini-jeu du support de formation : le createur ecrit 5 a 10
mots (recommandation souple, comme MIN_PAIRS/MIN_CARDS), places par le
serveur dans une grille carree horizontalement, verticalement, ou en
diagonale (deux sens seulement, jamais a l'envers) via un vrai
algorithme de placement avec retry/agrandissement de grille en cas de
conflit. La grille ET la position exacte de chaque mot sont calculees
cote serveur puis embarquees en JSON ; le client valide chaque
selection (glisser ou cliquer-cliquer) par comparaison de coordonnees
exactes, jamais une simple comparaison de texte (qui se tromperait sur
des lettres partagees entre deux mots qui se croisent). Comme
Association/Memory, la grille reste affichee une fois tous les mots
trouves : seul le bouton Recommencer (.docMinigameRestartBar, partage)
apparait.

Verifie via simulation DOM reelle (jsdom) : selection au glisser ET au
clic-clic, mot invalide sans crash, barre de fin qui bascule, panneau
Proprietes (ajout/suppression de mot avec revalidation serveur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 15:31:46 +02:00

123 lines
4.9 KiB
Python

"""Modèle de données du mini-jeu Mots mêlés
(document_engine/labels/mots_config.py) — sans Flask, teste directement
sanitize_mots_config, la construction de la grille
(_build_mots_grid, via render_document_element), et le rendu du plateau."""
import json
from typing import Any
import document_engine as doc_engine
from document_engine.rendering.render_document_element import _build_mots_grid
def test_sanitize_mots_config_on_missing_input_returns_full_defaults() -> None:
assert doc_engine.sanitize_mots_config(None) == {"theme_color": "#ff5f2e", "words": []}
def test_sanitize_mots_config_keeps_valid_words_uppercased() -> None:
raw = {"theme_color": "#123456", "words": ["chat", "chien"]}
config = doc_engine.sanitize_mots_config(raw)
assert config["theme_color"] == "#123456"
assert config["words"] == ["CHAT", "CHIEN"]
def test_sanitize_mots_config_strips_accents_and_non_letters() -> None:
raw = {"words": ["éléphant", "un chat !", "12"]}
config = doc_engine.sanitize_mots_config(raw)
assert config["words"] == ["ELEPHANT", "UNCHAT"]
def test_sanitize_mots_config_drops_words_shorter_than_min_length() -> None:
raw = {"words": ["a", "ok"]}
assert doc_engine.sanitize_mots_config(raw)["words"] == ["OK"]
def test_sanitize_mots_config_deduplicates_words() -> None:
raw = {"words": ["chat", "CHAT", "Chat"]}
assert doc_engine.sanitize_mots_config(raw)["words"] == ["CHAT"]
def test_sanitize_mots_config_caps_words_at_max() -> None:
alphabet = "abcdefghijklmnop"
raw = {"words": [f"mot{alphabet[i]}" for i in range(doc_engine.MAX_WORDS + 3)]}
config = doc_engine.sanitize_mots_config(raw)
assert len(config["words"]) == doc_engine.MAX_WORDS
def test_sanitize_mots_config_ignores_garbage_top_level_input() -> None:
assert doc_engine.sanitize_mots_config("n'importe quoi") == doc_engine.DEFAULT_MOTS_CONFIG
def _mots_element(attributes: dict[str, Any]) -> dict[str, Any]:
return {"id": 1, "kind": "mots", "parent_id": None, "order_index": 0, "attributes": attributes}
def test_render_mots_without_words_shows_only_the_badge() -> None:
html = doc_engine.render_document_element(_mots_element(doc_engine.DEFAULT_MOTS_CONFIG), {})
assert "0 mot" in html
assert "docMotsPlayer" not in html
def test_render_mots_with_words_includes_the_interactive_grid() -> None:
config = doc_engine.sanitize_mots_config({"words": ["chat", "chien", "lapin", "oiseau", "poisson"]})
html = doc_engine.render_document_element(_mots_element(config), {})
assert "docMotsPlayer" in html
assert "data-mots-config=" in html
assert "5 mots" in html
def test_render_mots_escapes_word_text_in_embedded_json() -> None:
# Un mot ne peut de toute façon contenir que des lettres A-Z une fois
# sanitizé (voir sanitize_mots_config) : rien à échapper dans le texte
# du mot lui-même. Le seul contenu utilisateur qui atteint réellement
# l'attribut embarqué est donc déjà sûr par construction — ce test
# vérifie que l'échappement JSON standard reste bien en place (guillemets).
config = doc_engine.sanitize_mots_config({"words": ["chat", "chien"]})
html = doc_engine.render_document_element(_mots_element(config), {})
start = html.find('data-mots-config="') + len('data-mots-config="')
end = html.find('"', start)
embedded = json.loads(html[start:end].replace("&quot;", '"'))
assert {"CHAT", "CHIEN"} <= {w["text"] for w in embedded["words"]}
def _cells_spell(grid: list[list[str]], cells: list[list[int]]) -> str:
return "".join(grid[r][c] for r, c in cells)
def test_build_mots_grid_places_every_word_along_its_own_cells() -> None:
words = ["CHAT", "CHIEN", "OISEAU", "POISSON", "LAPIN"]
built = _build_mots_grid(words)
size = built["size"]
grid = built["grid"]
assert len(grid) == size
assert all(len(row) == size for row in grid)
placed = {w["text"] for w in built["words"]}
assert placed == set(words)
for w in built["words"]:
assert _cells_spell(grid, w["cells"]) == w["text"]
def test_build_mots_grid_only_uses_the_four_allowed_directions() -> None:
words = ["CHAT", "CHIEN", "OISEAU", "POISSON", "LAPIN", "SOURIS"]
built = _build_mots_grid(words)
allowed = {(0, 1), (1, 0), (1, 1), (1, -1)}
for w in built["words"]:
cells = w["cells"]
if len(cells) < 2:
continue
delta_row = cells[1][0] - cells[0][0]
delta_col = cells[1][1] - cells[0][1]
assert (delta_row, delta_col) in allowed
for i in range(len(cells) - 1):
assert cells[i + 1][0] - cells[i][0] == delta_row
assert cells[i + 1][1] - cells[i][1] == delta_col
def test_build_mots_grid_fills_every_remaining_cell_with_a_letter() -> None:
built = _build_mots_grid(["CHAT", "CHIEN"])
assert all(cell for row in built["grid"] for cell in row)
def test_build_mots_grid_on_empty_words_returns_an_empty_grid() -> None:
assert _build_mots_grid([]) == {"size": 0, "grid": [], "words": []}