"""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": []}