Ctrl K
ComunidadGuías…

test_serve_app_js_formulaire_reglages.py

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

Archivo original ↗Preguntar a KiBot
"""Caractérise le formulaire réel dans Node, sans navigateur ni appel réseau."""

from __future__ import annotations

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


REGLAGES = {
    "usb_minutes": 17, "cloud_minutes": 3, "port": 8127,
    "storage_dir": "C:/Vidéos & archives/Blink", "timestamp": True,
    "timezone": "Europe/Paris", "live_protocol": "webrtc",
    "camera_audio_enabled": True, "camera_audio_track": 1,
    "merge_jour": True, "merge_semaine": True, "merge_mois": False,
    "download_auto": True, "initial_setup": False,
    "webhook_token": "jeton-test-abc123",
    "font_size": 40, "font_color": "yellow", "box_opacity": 0.3,
    "clock": "12h",
    "trusted_host": "100.101.194.5",
    "webhook_notif_url": "https://exemple.invalid/notif",
    "header_title": "Maison & garage", "header_icon": "🏠",
    "live_auto_stop_seconds": 300,
    "conservation_clips": 1,
    "conservation_jour": 2,
    "conservation_semaine": 3,
    "conservation_mois": 4,
    "conservation_directs": 5,
    "conservation_photos": 6,
}


class TestsFormulaireReglages(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.node = shutil.which("node")
        if cls.node is None:
            raise unittest.SkipTest("node introuvable")
        source = (Path(__file__).parent / "serve_app.js").read_text(encoding="utf-8")
        debut = source.index("let portActuel = null;")
        fin = source.index('$("filtreButton").onclick', debut)
        fragments = [source[debut:fin]]
        for nom in ("lireJSON", "appliquerDependanceMergeJour", "appliquerDependanceDownloadAuto",
                   "appliquerDependanceTimestamp", "appliquerDependanceAudio"):
            correspondance = re.search(
                rf"^(?:async )?function {nom}\(.*?^\}}", source, re.DOTALL | re.MULTILINE,
            )
            if correspondance is None:
                raise AssertionError(f"fonction {nom}() introuvable")
            fragments.append(correspondance.group(0))
        # Conserve aussi les branchements réels des cases, pas des appels de test
        # aux fonctions internes dont le découpage peut évoluer.
        for identifiant in ("mergeJour", "downloadAuto", "timestamp", "cameraAudioEnabled"):
            correspondance = re.search(
                rf'^\$\("{identifiant}"\)\.onchange = .*?;$', source, re.MULTILINE,
            )
            if correspondance is None:
                raise AssertionError(f"onchange de {identifiant} introuvable")
            fragments.append(correspondance.group(0))
        cls.javascript = "\n".join(fragments)

    def _executer(self, ouvertures=None, actions=None):
        script = r"""
const params = JSON.parse(process.argv[1]);
const evenements = [], captures = [];
let rechargements = 0, reponseCourante, libererGet;
const attenteListes = new Promise(() => {});
const ids = [
  'usbMinutes', 'cloudMinutes', 'port', 'storageDir', 'timestamp', 'timezone',
  'cameraAudioEnabled', 'cameraAudioTrack',
  'liveProtocol', 'mergeJour', 'mergeSemaine', 'mergeMois', 'downloadAuto',
  'initialSetupHint', 'reglagesClose', 'stopButton', 'reglages', 'reglagesButton',
  'webhookUrl', 'webhookRegenerer', 'fontSize', 'fontColor', 'boxOpacity', 'clock',
  'trustedHost', 'webhookNotifUrl', 'headerTitle', 'headerIcon', 'liveAutoStopSeconds', 'redemarrerButton',
  'conservationClips', 'conservationJour', 'conservationSemaine', 'conservationMois', 'conservationDirects', 'conservationPhotos',
  'tabGeneral', 'tabVideo', 'tabAcces', 'tabAlertes', 'tailleCartes', 'barrePagination',
  'panelGeneral', 'panelVideo', 'panelAcces', 'panelAlertes',
];
class Element {
  constructor(id) {
    this.id = id;
    this.value = `avant:${id}`;
    this.checked = false;
    this.disabled = false;
    this.hidden = false;
    this.dataset = {};
    this.ecouteurs = {};
    this._classes = new Set();
    this.classList = {
      add: (nom) => this._classes.add(nom),
      remove: (nom) => this._classes.delete(nom),
      contains: (nom) => this._classes.has(nom),
      toggle: (nom, force) => {
        const present = force === undefined ? !this._classes.has(nom) : !!force;
        this.classList[present ? 'add' : 'remove'](nom);
        return present;
      },
    };
  }
  set value(valeur) { this.valeur = String(valeur); }
  get value() { return this.valeur; }
  addEventListener(type, fonction) { this.ecouteurs[type] = fonction; }
  showModal() {
    evenements.push('showModal');
    captures.push(capturer());
  }
  close() { evenements.push('close'); }
}
const elements = Object.fromEntries(ids.map((id) => [id, new Element(id)]));
globalThis.$ = (id) => {
  if (!elements[id]) throw new Error(`Élément inattendu : ${id}`);
  return elements[id];
};
globalThis.location = {origin: 'http://localhost:1234', reload: () => { rechargements += 1; }};
globalThis.chargerSourdine = () => {
  evenements.push('chargerSourdine');
  return attenteListes;
};
globalThis.chargerSuppressionAuto = () => {
  evenements.push('chargerSuppressionAuto');
  return attenteListes;
};
globalThis.chargerCamerasMasquees = () => {};
globalThis.tailleCartes = 'moyennes';
globalThis.barrePaginationHaute = true;
globalThis.fetch = async (url, options) => {
  if (url !== '/api/reglages' || options !== undefined) {
    throw new Error(`Requête inattendue : ${url}`);
  }
  evenements.push('fetch');
  await new Promise((resolve) => { libererGet = resolve; });
  if (reponseCourante.erreur === 'reseau') throw new TypeError('GET indisponible');
  return {json: async () => {
    evenements.push('json');
    if (reponseCourante.erreur === 'json') throw new SyntaxError('JSON invalide');
    return reponseCourante.reglages;
  }};
};
""" + self.javascript + r"""
function capturer() {
  return {
    portActuel,
    champs: Object.fromEntries(ids.map((id) => [id, {
      value: $(id).value, checked: $(id).checked, disabled: $(id).disabled,
      hidden: $(id).hidden, dataset: {...$(id).dataset},
    }])),
  };
}
(async () => {
  const avantGet = [], apresActions = [];
  for (const ouverture of params.ouvertures) {
    reponseCourante = ouverture;
    const attente = ouverture.bouton ? $('reglagesButton').onclick() :
      ouvrirReglages(ouverture.initiale);
    avantGet.push({etat: capturer(), evenements: [...evenements]});
    libererGet();
    await attente;
  }
  for (const action of params.actions) {
    if (action.type === 'case') {
      $(action.id).checked = action.checked;
      $(action.id).onchange();
      apresActions.push(capturer());
    } else if (action.type === 'cancel') {
      let empeche = false;
      $('reglages').ecouteurs.cancel({preventDefault: () => { empeche = true; }});
      apresActions.push({empeche});
    } else if (action.type === 'fermer') {
      $('reglagesClose').onclick();
    } else if (action.type === 'clic') {
      $(action.id).onclick();
      apresActions.push(capturer());
    } else {
      throw new Error(`Action inconnue : ${action.type}`);
    }
  }
  process.stdout.write(JSON.stringify({
    avantGet, captures, apresActions, evenements, rechargements, final: capturer(),
  }));
})().catch((erreur) => { console.error(erreur); process.exitCode = 1; });
"""
        resultat = subprocess.run(
            [self.node, "-e", script, json.dumps({
                "ouvertures": ouvertures if ouvertures is not None else [{"reglages": REGLAGES}],
                "actions": actions or [],
            })],
            capture_output=True, text=True, encoding="utf-8", check=True, timeout=60,
        )
        self.assertTrue(resultat.stdout, "ouvrirReglages ne doit pas attendre les listes")
        return json.loads(resultat.stdout)

    def test_tous_les_champs_et_protocoles_sont_affiches(self):
        valeurs = {
            "usbMinutes": "usb_minutes", "cloudMinutes": "cloud_minutes", "port": "port",
            "storageDir": "storage_dir", "timezone": "timezone", "liveProtocol": "live_protocol",
            "fontSize": "font_size", "fontColor": "font_color", "boxOpacity": "box_opacity",
            "clock": "clock",
            "trustedHost": "trusted_host", "webhookNotifUrl": "webhook_notif_url",
            "headerTitle": "header_title", "headerIcon": "header_icon",
            "liveAutoStopSeconds": "live_auto_stop_seconds",
            "conservationClips": "conservation_clips",
            "conservationJour": "conservation_jour",
            "conservationSemaine": "conservation_semaine",
            "conservationMois": "conservation_mois",
            "conservationDirects": "conservation_directs",
            "conservationPhotos": "conservation_photos",
        }
        cases = {
            "timestamp": "timestamp", "mergeJour": "merge_jour", "mergeSemaine": "merge_semaine",
            "mergeMois": "merge_mois", "downloadAuto": "download_auto",
        }
        for protocole in ("webrtc", "mse"):
            with self.subTest(protocole=protocole):
                reglages = dict(REGLAGES, live_protocol=protocole)
                resultat = self._executer([{"reglages": reglages}])
                etat = resultat["captures"][0]
                for identifiant, cle in valeurs.items():
                    self.assertEqual(etat["champs"][identifiant]["value"], str(reglages[cle]))
                for identifiant, cle in cases.items():
                    self.assertEqual(etat["champs"][identifiant]["checked"], reglages[cle])
                self.assertEqual(etat["portActuel"], 8127)
                self.assertEqual(
                    etat["champs"]["webhookUrl"]["value"],
                    "http://localhost:1234/webhook/snapshot?camera=NOM_CAMERA&token=jeton-test-abc123",
                )

    def test_ouverture_affiche_l_onglet_general_seul(self):
        resultat = self._executer()
        champs = resultat["captures"][0]["champs"]
        self.assertFalse(champs["panelGeneral"]["hidden"])
        for panneau in ("panelVideo", "panelAcces", "panelAlertes"):
            self.assertTrue(champs[panneau]["hidden"])

    def test_clic_sur_un_onglet_affiche_son_panneau_seul(self):
        for onglet, panneau in (
            ("tabVideo", "panelVideo"), ("tabAcces", "panelAcces"),
            ("tabAlertes", "panelAlertes"), ("tabGeneral", "panelGeneral"),
        ):
            with self.subTest(onglet=onglet):
                resultat = self._executer(actions=[{"type": "clic", "id": onglet}])
                champs = resultat["apresActions"][0]["champs"]
                self.assertFalse(champs[panneau]["hidden"])
                autres = {"panelGeneral", "panelVideo", "panelAcces", "panelAlertes"} - {panneau}
                for autre in autres:
                    self.assertTrue(champs[autre]["hidden"])

    def test_taille_de_police_absente_affiche_un_champ_vide_pas_le_mot_null(self):
        resultat = self._executer([{"reglages": dict(REGLAGES, font_size=None)}])
        self.assertEqual(resultat["captures"][0]["champs"]["fontSize"]["value"], "")

    def test_champs_de_style_grises_quand_l_horodatage_est_decoche(self):
        resultat = self._executer([{
            "reglages": dict(REGLAGES, timestamp=False),
        }], [{"type": "case", "id": "timestamp", "checked": True}])
        premiere = resultat["captures"][0]
        for identifiant in ("fontSize", "fontColor", "boxOpacity", "clock"):
            self.assertTrue(premiere["champs"][identifiant]["disabled"])
        apres_coche = resultat["apresActions"][0]
        for identifiant in ("fontSize", "fontColor", "boxOpacity", "clock"):
            self.assertFalse(apres_coche["champs"][identifiant]["disabled"])

    def test_ouverture_attend_reglages_puis_lance_listes_avant_dialogue(self):
        resultat = self._executer()
        self.assertEqual(resultat["avantGet"][0]["evenements"], ["fetch"])
        self.assertIsNone(resultat["avantGet"][0]["etat"]["portActuel"])
        self.assertEqual(resultat["avantGet"][0]["etat"]["champs"]["usbMinutes"]["value"],
                         "avant:usbMinutes")
        self.assertEqual(resultat["evenements"], [
            "fetch", "json", "chargerSourdine", "chargerSuppressionAuto", "showModal",
        ])
        self.assertEqual(len(resultat["captures"]), 1)

    def test_configuration_initiale_appel_ou_serveur_bloque_echap(self):
        for initiale in (False, True):
            for serveur in (False, True):
                with self.subTest(initiale=initiale, serveur=serveur):
                    resultat = self._executer([{
                        "initiale": initiale, "reglages": dict(REGLAGES, initial_setup=serveur),
                    }], [{"type": "cancel"}])
                    actif = initiale or serveur
                    champs = resultat["final"]["champs"]
                    self.assertEqual(champs["initialSetupHint"]["hidden"], not actif)
                    self.assertEqual(champs["reglagesClose"]["hidden"], actif)
                    self.assertEqual(champs["stopButton"]["hidden"], actif)
                    self.assertEqual(champs["reglages"]["dataset"], {"initialSetup": "1" if actif else "0"})
                    self.assertEqual(resultat["apresActions"], [{"empeche": actif}])

    def test_boutons_ouvrent_mode_ordinaire_et_ferment_dialogue(self):
        resultat = self._executer([
            {"initiale": True, "reglages": REGLAGES},
            {"bouton": True, "reglages": dict(REGLAGES, port=9000)},
        ], [{"type": "cancel"}, {"type": "fermer"}])
        self.assertEqual(resultat["captures"][0]["champs"]["reglages"]["dataset"]["initialSetup"], "1")
        self.assertEqual(resultat["captures"][1]["champs"]["reglages"]["dataset"]["initialSetup"], "0")
        self.assertEqual(resultat["final"]["portActuel"], 9000)
        self.assertEqual(resultat["apresActions"], [{"empeche": False}])
        self.assertEqual(resultat["evenements"][-1], "close")

    def test_sans_fusion_journaliere_semaine_et_mois_sont_decoches(self):
        resultat = self._executer([{
            "reglages": dict(REGLAGES, merge_jour=False, merge_semaine=True, merge_mois=True),
        }], [{"type": "case", "id": "mergeJour", "checked": True}])
        for identifiant in ("mergeSemaine", "mergeMois"):
            self.assertFalse(resultat["captures"][0]["champs"][identifiant]["checked"])
            self.assertTrue(resultat["captures"][0]["champs"][identifiant]["disabled"])
            self.assertFalse(resultat["final"]["champs"][identifiant]["checked"])
            self.assertFalse(resultat["final"]["champs"][identifiant]["disabled"])

    def test_decocher_jour_met_a_jour_dependances_immediatement(self):
        resultat = self._executer(actions=[
            {"type": "case", "id": "mergeJour", "checked": False},
            {"type": "case", "id": "mergeJour", "checked": True},
        ])
        for indice, disabled in enumerate((True, False)):
            for identifiant in ("mergeSemaine", "mergeMois"):
                champ = resultat["apresActions"][indice]["champs"][identifiant]
                self.assertEqual(champ["disabled"], disabled)
                self.assertFalse(champ["checked"])

    def test_sans_telechargement_cadences_grisees_mais_valeurs_conservees(self):
        resultat = self._executer([{"reglages": dict(REGLAGES, download_auto=False)}], [
            {"type": "case", "id": "downloadAuto", "checked": True},
            {"type": "case", "id": "downloadAuto", "checked": False},
        ])
        etats = [resultat["captures"][0]] + resultat["apresActions"]
        for etat, disabled in zip(etats, (True, False, True)):
            for identifiant, valeur in (("usbMinutes", "17"), ("cloudMinutes", "3")):
                self.assertEqual(etat["champs"][identifiant]["disabled"], disabled)
                self.assertEqual(etat["champs"][identifiant]["value"], valeur)
            self.assertTrue(etat["champs"]["mergeJour"]["checked"])
            self.assertTrue(etat["champs"]["mergeSemaine"]["checked"])

    def test_echec_get_conserve_champs_et_port_precedents(self):
        for erreur in ("reseau", "json"):
            with self.subTest(erreur=erreur):
                resultat = self._executer([
                    {"reglages": REGLAGES}, {"erreur": erreur},
                ])
                self.assertEqual(resultat["captures"][0], resultat["captures"][1])
                self.assertEqual(resultat["rechargements"], int(erreur == "json"))
                self.assertEqual(resultat["evenements"][-3:], [
                    "chargerSourdine", "chargerSuppressionAuto", "showModal",
                ])

    def test_echec_get_initial_conserve_protection_du_premier_demarrage(self):
        resultat = self._executer([{"initiale": True, "erreur": "reseau"}], [{"type": "cancel"}])
        self.assertIsNone(resultat["final"]["portActuel"])
        self.assertEqual(resultat["final"]["champs"]["usbMinutes"]["value"], "avant:usbMinutes")
        self.assertTrue(resultat["final"]["champs"]["reglagesClose"]["hidden"])
        self.assertTrue(resultat["final"]["champs"]["stopButton"]["hidden"])
        self.assertEqual(resultat["apresActions"], [{"empeche": True}])


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