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
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>
123 lines
4.9 KiB
Python
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(""", '"'))
|
|
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": []}
|