Ctrl K
ComunidadGuías…

test_serve_app_js_confort_cameras.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Issue #40 : tri naturel des caméras, taille des vignettes, caméras masquées.
Le vrai code de serve_app.js, exécuté par node avec un faux DOM et un faux
localStorage ; les contrôles de structure portent sur serve.py et le CSS."""

from __future__ import annotations

import json
import re
import shutil
import subprocess
import unittest
from pathlib import Path

RACINE = Path(__file__).parent
SOURCE = (RACINE / "serve_app.js").read_text(encoding="utf-8")


def extraire(nom: str) -> str:
    trouve = re.search(rf"^(?:async )?function {nom}\(.*?^\}}", SOURCE, re.DOTALL | re.MULTILINE)
    if trouve is None:
        raise AssertionError(f"fonction {nom}() introuvable dans serve_app.js")
    return trouve.group(0)


FONCTIONS = [
    "restaurerCamerasMasquees", "memoriserCamerasMasquees", "estMasquee",
    "restaurerTailleCartes", "appliquerTailleCartes", "comparerNoms",
    "majCaseMasquees", "toutesLesCameras", "camerasConnues", "camerasPourMasquage",
    "visible", "renderLive", "comparerCamerasDirect",
]

# Constantes et état du bloc « préférences d'affichage » : recopiés du source,
# jamais réécrits ici.
CONSTANTES = "\n".join(
    re.search(rf"^{motif}.*?;$", SOURCE, re.MULTILINE).group(0)
    for motif in (r"const CLE_CAMERAS_MASQUEES", r"const CLE_TAILLE_CARTES",
                  r"const TAILLES_CARTES", r"const TAILLE_CARTES_DEFAUT",
                  r"const _collateurs")
)


def executer(scenario: str, stockage: dict | None = None, systeme: dict | None = None) -> dict:
    node = shutil.which("node")
    if node is None:
        raise unittest.SkipTest("node introuvable")
    script = f"""
const magasin = {json.dumps(stockage or {})};
globalThis.localStorage = {{
  getItem: (c) => (c in magasin ? magasin[c] : null),
  setItem: (c, v) => {{ magasin[c] = String(v); }},
}};
const proprietes = {{}};
globalThis.document = {{ documentElement: {{ style: {{ setProperty: (k, v) => {{ proprietes[k] = v; }} }} }} }};
const elements = {{
  hiddenLabel: {{ hidden: true }}, showHidden: {{ checked: false }},
  showOut: {{ checked: true }}, camera: {{ value: "" }}, view: {{ value: "clips" }},
  list: {{ innerHTML: "" }}, count: {{ textContent: "" }},
}};
function $(id) {{ return elements[id]; }}
let _lang = "fr";
let data = {{ clips: [], cameras: [] }}, videos = {{ daily: [] }}, snapshots = [];
let system = {json.dumps(systeme)};
const nomsDirectsActifs = () => [];
const ronde = {{ boite: null }};
const t = (cle) => cle, tf = (cle, p) => cle + JSON.stringify(p), h = (s) => String(s);
const cameraCard = (c) => `<card ${{c.name}}>`;
let rafraichirVignettes = false;
function actualiserVignettes() {{}}
function loadSystem() {{}}
{CONSTANTES}
let camerasMasquees = restaurerCamerasMasquees();
let montrerMasquees = false;
let tailleCartes = restaurerTailleCartes();
{chr(10).join(extraire(nom) for nom in FONCTIONS)}
const sortie = {{}};
{scenario}
console.log(JSON.stringify(sortie));
"""
    resultat = subprocess.run([node, "-"], input=script, capture_output=True, text=True,
                              encoding="utf-8", timeout=60)
    assert resultat.returncode == 0, resultat.stderr
    return json.loads(resultat.stdout)


class TriNaturelDesCameras(unittest.TestCase):
    def test_les_nombres_sont_compares_comme_des_nombres(self):
        r = executer('sortie.tri = ["10-Cave", "2-Escalier", "1-Haustür", "abeille", "Zébra"].sort(comparerNoms);')
        self.assertEqual(r["tri"], ["1-Haustür", "2-Escalier", "10-Cave", "abeille", "Zébra"])

    def test_le_filtre_est_trie_dans_cet_ordre(self):
        r = executer('data.cameras = ["10-Cave", "2-Escalier", "1-Porte"]; sortie.liste = camerasConnues();')
        self.assertEqual(r["liste"], ["1-Porte", "2-Escalier", "10-Cave"])


class CamerasMasquees(unittest.TestCase):
    STOCK = {"blink2video.camerasMasquees": json.dumps(["Grenier", "Cave"])}

    def test_une_camera_masquee_disparait_du_filtre(self):
        r = executer('data.cameras = ["Cave", "Salon", "Grenier"]; sortie.liste = camerasConnues();', self.STOCK)
        self.assertEqual(r["liste"], ["Salon"])

    def test_le_devoilement_temporaire_les_remet_sans_rien_memoriser(self):
        r = executer(
            'data.cameras = ["Cave", "Salon"]; montrerMasquees = true;'
            'sortie.liste = camerasConnues(); sortie.stock = JSON.stringify(magasin);', self.STOCK)
        self.assertEqual(r["liste"], ["Cave", "Salon"])
        self.assertEqual(json.loads(r["stock"]), self.STOCK)

    def test_les_clips_d_une_camera_masquee_ne_sont_pas_visibles(self):
        r = executer(
            'data.clips = [{kind:"clip", camera:"Cave", excluded:false}, {kind:"clip", camera:"Salon", excluded:false}];'
            'sortie.vus = visible().map((c) => c.camera); montrerMasquees = true;'
            'sortie.apres = visible().map((c) => c.camera);', self.STOCK)
        self.assertEqual(r["vus"], ["Salon"])
        self.assertEqual(r["apres"], ["Cave", "Salon"])

    def test_la_liste_des_reglages_garde_les_masquees_et_celles_du_direct(self):
        systeme = {"systems": [{"cameras": [{"name": "Garage"}, {"name": "Salon"}]}]}
        r = executer('data.cameras = ["Salon"]; sortie.liste = camerasPourMasquage();',
                     {"blink2video.camerasMasquees": json.dumps(["Ancienne"])}, systeme)
        self.assertEqual(r["liste"], ["Ancienne", "Garage", "Salon"])

    def test_la_case_de_devoilement_n_existe_que_si_une_camera_est_masquee(self):
        r = executer('majCaseMasquees(); sortie.libre = $("hiddenLabel").hidden;')
        self.assertTrue(r["libre"])
        r = executer('majCaseMasquees(); sortie.libre = $("hiddenLabel").hidden;', self.STOCK)
        self.assertFalse(r["libre"])

    def test_sans_masquee_le_devoilement_retombe(self):
        r = executer('montrerMasquees = true; $("showHidden").checked = true; majCaseMasquees();'
                     'sortie.montrer = montrerMasquees; sortie.coche = $("showHidden").checked;')
        self.assertFalse(r["montrer"])
        self.assertFalse(r["coche"])

    def test_un_stockage_abime_ne_casse_rien(self):
        for valeur in ("pas du json", '{"a": 1}', "null", "[1, 2, null]"):
            with self.subTest(valeur=valeur):
                r = executer('sortie.n = camerasMasquees.size;', {"blink2video.camerasMasquees": valeur})
                self.assertEqual(r["n"], 0)


class VuesDirect(unittest.TestCase):
    SYSTEME = {"error": None, "systems": [
        {"name": "Maison", "key": "m", "armed": True, "module": None, "module_firmware": None,
         "module_serial": None,
         "cameras": [{"name": "10-Cave", "armed": True}, {"name": "2-Salon", "armed": False},
                     {"name": "Grenier", "armed": True}]},
        {"name": "Loft", "key": "l", "armed": False, "module": None, "module_firmware": None,
         "module_serial": None, "cameras": [{"name": "Grenier", "armed": True}]},
    ]}

    def test_le_direct_masque_et_trie_les_cameras(self):
        r = executer('renderLive(); sortie.html = $("list").innerHTML; sortie.compte = $("count").textContent;',
                     {"blink2video.camerasMasquees": json.dumps(["Grenier"])}, self.SYSTEME)
        html = r["html"]
        self.assertLess(html.index("<card 2-Salon>"), html.index("<card 10-Cave>"))
        self.assertNotIn("<card Grenier>", html)
        # Un système dont toutes les caméras sont masquées garde son titre et son
        # armement, et dit pourquoi sa grille est vide.
        self.assertIn("Loft", html)
        self.assertIn("live.toutesMasquees", html)
        self.assertIn('"n":2', r["compte"])
        self.assertIn('"m":1', r["compte"])

    def test_rien_n_est_masque_par_defaut(self):
        r = executer('renderLive(); sortie.html = $("list").innerHTML;', None, self.SYSTEME)
        self.assertEqual(r["html"].count("<card Grenier>"), 2)
        self.assertNotIn("live.toutesMasquees", r["html"])

    def test_les_cameras_hors_ligne_en_fin_de_liste(self):
        # Issue #40 (MCas86) : rien à regarder sur une caméra hors ligne. Chaque
        # groupe garde l'ordre naturel des noms, et le tri reste par système.
        systeme = {"error": None, "systems": [
            {"name": "Maison", "key": "m", "armed": True, "module": None,
             "module_firmware": None, "module_serial": None, "cameras": [
                 {"name": "1-Allée", "offline": True}, {"name": "10-Cave"},
                 {"name": "3-Garage", "offline": True}, {"name": "2-Salon"}]},
            {"name": "Loft", "key": "l", "armed": False, "module": None,
             "module_firmware": None, "module_serial": None,
             "cameras": [{"name": "0-Loft"}]},
        ]}
        r = executer('renderLive(); sortie.html = $("list").innerHTML;', None, systeme)
        ordre = re.findall(r"<card ([^>]+)>", r["html"])
        self.assertEqual(ordre, ["2-Salon", "10-Cave", "1-Allée", "3-Garage", "0-Loft"])


class TailleDesVignettes(unittest.TestCase):
    def test_la_taille_par_defaut_est_celle_d_avant(self):
        r = executer("appliquerTailleCartes(); sortie.p = proprietes;")
        self.assertEqual(r["p"], {"--carte-min": "320px"})

    def test_les_quatre_tailles_et_la_valeur_inconnue(self):
        attendu = {"petites": "220px", "moyennes": "320px", "grandes": "480px", "tres_grandes": "720px"}
        for nom, px in attendu.items():
            with self.subTest(nom=nom):
                r = executer("appliquerTailleCartes(); sortie.p = proprietes;",
                             {"blink2video.tailleCartes": nom})
                self.assertEqual(r["p"]["--carte-min"], px)
        for abime in ("enorme", "constructor", "__proto__", ""):
            with self.subTest(abime=abime):
                r = executer("appliquerTailleCartes(); sortie.p = proprietes;",
                             {"blink2video.tailleCartes": abime})
                self.assertEqual(r["p"]["--carte-min"], "320px")


class StructureDeLaPage(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.html = (RACINE / "serve.py").read_text(encoding="utf-8")
        cls.css = (RACINE / "serve_style.css").read_text(encoding="utf-8")

    def test_les_commandes_existent_dans_la_page(self):
        for identifiant in ("showHidden", "hiddenLabel", "tailleCartes", "camerasMasqueesListe"):
            self.assertEqual(self.html.count(f'id="{identifiant}"'), 1, identifiant)

    def test_la_case_de_devoilement_est_cachee_au_depart(self):
        self.assertRegex(self.html, r'<label id="hiddenLabel" hidden>')

    def test_le_css_lit_la_taille_choisie_et_garde_le_rapport_des_grilles_larges(self):
        self.assertIn("minmax(var(--carte-min, 320px), 1fr)", self.css)
        self.assertIn("calc(var(--carte-min, 320px) * 1.4375)", self.css)

    def test_chaque_nouveau_texte_existe_en_francais_et_en_anglais(self):
        cles = re.findall(r'data-i18n="((?:filtre\.showHidden|reglages\.tailleCartes[\w.]*|reglages\.masquees[\w.]*))"', self.html)
        cles += ["masquees.none", "live.toutesMasquees"]
        self.assertTrue(len(cles) >= 8, cles)
        for cle in cles:
            self.assertEqual(len(re.findall(rf'"{re.escape(cle)}":', SOURCE)), 2,
                             f"{cle} : attendu une fois en français et une fois en anglais")


if __name__ == "__main__":
    unittest.main()