178 lines
8.3 KiB
Python
178 lines
8.3 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Le GUI REND-IL ? Chaque vue est dessinee pour de vrai, dans un DOM simule.
|
||
|
|
|
||
|
|
CE QUE `verifier_gui.py` NE POUVAIT PAS VOIR (2026-09-08). Il passe le JS a
|
||
|
|
`node --check` : la syntaxe, donc, et rien d'autre. Le formulaire genere des bases
|
||
|
|
lisait `data.schema` — or il n'existe aucun `data` global dans cette page ; c'est une
|
||
|
|
const locale de `charger()`. Syntaxe parfaite, `ReferenceError` a l'ouverture. Pire dans
|
||
|
|
`sauvegarderBases`, ou `const data` est declare PLUS BAS dans la meme fonction : zone
|
||
|
|
morte temporelle, l'enregistrement jetait avant meme d'envoyer.
|
||
|
|
|
||
|
|
Le defaut a survecu a une verification sous node parce que le banc APPELAIT la fonction
|
||
|
|
en lui passant `data` — il reproduisait la fonction, pas sa PORTEE. Une garantie qu'on
|
||
|
|
n'a jamais vue echouer n'est pas une garantie.
|
||
|
|
|
||
|
|
Ce banc-ci charge le JS entier, tel que la page le recoit, dans un DOM simule, appelle
|
||
|
|
`charger()` sur la VRAIE reponse de l'API, puis dessine CHAQUE vue. Toute reference a un
|
||
|
|
identifiant qui n'existe pas leve, et le test echoue.
|
||
|
|
|
||
|
|
Ce qu'il ne fait PAS : juger l'apparence. Il dit que ca rend, pas que c'est beau.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import re
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
RACINE = Path(__file__).resolve().parents[2]
|
||
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||
|
|
|
||
|
|
import inventory_gui as g # noqa: E402
|
||
|
|
|
||
|
|
# Un DOM juste assez reel : tout element repond, retient son innerHTML, et n'invente
|
||
|
|
# rien. Un `getElementById` qui rendrait `null` masquerait les fautes qu'on cherche.
|
||
|
|
BANC = r"""
|
||
|
|
'use strict';
|
||
|
|
function Elem(id) {
|
||
|
|
this.id = id; this.innerHTML = ''; this.textContent = ''; this.value = '';
|
||
|
|
this.hidden = false; this.disabled = false; this.title = ''; this.className = '';
|
||
|
|
this.style = {}; this.dataset = {}; this.children = [];
|
||
|
|
this.classList = {add(){}, remove(){}, toggle(){}, contains(){ return false; }};
|
||
|
|
}
|
||
|
|
Elem.prototype.appendChild = function (e) { this.children.push(e); return e; };
|
||
|
|
Elem.prototype.removeChild = function (e) { return e; };
|
||
|
|
Elem.prototype.setAttribute = function () {};
|
||
|
|
Elem.prototype.getAttribute = function () { return ''; };
|
||
|
|
Elem.prototype.addEventListener = function () {};
|
||
|
|
Elem.prototype.querySelector = function () { return null; };
|
||
|
|
Elem.prototype.querySelectorAll = function () { return []; };
|
||
|
|
Elem.prototype.focus = function () {};
|
||
|
|
Elem.prototype.remove = function () {};
|
||
|
|
Elem.prototype.closest = function () { return null; };
|
||
|
|
Elem.prototype.scrollIntoView = function () {};
|
||
|
|
|
||
|
|
var __elems = {};
|
||
|
|
var document = {
|
||
|
|
getElementById: function (id) { return __elems[id] || (__elems[id] = new Elem(id)); },
|
||
|
|
createElement: function () { return new Elem('cree'); },
|
||
|
|
querySelector: function () { return null; },
|
||
|
|
querySelectorAll: function () { return []; },
|
||
|
|
addEventListener: function () {},
|
||
|
|
body: new Elem('body'),
|
||
|
|
documentElement: new Elem('html'),
|
||
|
|
};
|
||
|
|
var window = {addEventListener: function () {}, location: {href: '', reload: function () {}},
|
||
|
|
matchMedia: function () { return {matches: false, addEventListener: function () {}}; }};
|
||
|
|
var navigator = {clipboard: {writeText: function () { return Promise.resolve(); }}};
|
||
|
|
function MutationObserver(f) { this.observe = function () {}; this.disconnect = function () {}; }
|
||
|
|
function requestAnimationFrame(f) { return 0; }
|
||
|
|
function cancelAnimationFrame() {}
|
||
|
|
function EventSource() { this.addEventListener = function () {}; this.close = function () {}; }
|
||
|
|
function alert() {} function confirm() { return false; } function prompt() { return null; }
|
||
|
|
var localStorage = {getItem: function () { return null; }, setItem: function () {}, removeItem: function () {}};
|
||
|
|
var __PAYLOAD__ = JSON.parse(process.env.PAYLOAD);
|
||
|
|
// `fetch` rend la VRAIE reponse de /api/inventaire ; les autres routes rendent un objet
|
||
|
|
// vide plutot que de jeter, pour que les sondes de fond ne masquent pas l'erreur cherchee.
|
||
|
|
function fetch(url) {
|
||
|
|
var corps = String(url).indexOf('/api/inventaire') === 0 ? __PAYLOAD__ : {};
|
||
|
|
return Promise.resolve({ok: true, status: 200, json: function () { return Promise.resolve(corps); },
|
||
|
|
text: function () { return Promise.resolve(''); }});
|
||
|
|
}
|
||
|
|
var __erreurs = [];
|
||
|
|
function __essayer(quoi, f) { try { f(); } catch (e) { __erreurs.push(quoi + ' : ' + e); } }
|
||
|
|
"""
|
||
|
|
|
||
|
|
APRES = r"""
|
||
|
|
// --- L'EPREUVE -----------------------------------------------------------------
|
||
|
|
charger().then(function () {
|
||
|
|
['serveurs', 'applications', 'bases', 'domaines', 'nomenclature',
|
||
|
|
'integrations', 'flux', 'couches', 'reseau'].forEach(function (v) {
|
||
|
|
__essayer('setVue(' + v + ')', function () { setVue(v); });
|
||
|
|
});
|
||
|
|
// Les vues a maitre-detail ne montrent leur formulaire QU'UNE FOIS une entree
|
||
|
|
// choisie : dessiner la grille seule ne prouve rien du formulaire genere.
|
||
|
|
__essayer('bases + selection', function () {
|
||
|
|
setVue('bases');
|
||
|
|
if (typeof bdApplis !== 'undefined' && bdApplis.length) { choisirBase('bd:0'); }
|
||
|
|
if (typeof bdServeurs !== 'undefined' && bdServeurs.length) { choisirBase('srv:0'); }
|
||
|
|
});
|
||
|
|
__essayer('nomenclature + selection', function () {
|
||
|
|
setVue('nomenclature');
|
||
|
|
var f = Object.keys(nomEdit.fonctions || {});
|
||
|
|
var c = Object.keys(nomEdit.categories || {});
|
||
|
|
if (f.length) { choisirNom('fon:' + f[0]); }
|
||
|
|
if (c.length) { choisirNom('cat:' + c[0]); }
|
||
|
|
});
|
||
|
|
__essayer('domaines + selection', function () { setVue('domaines'); if (domaines.length) choisirDomaine(0); });
|
||
|
|
__essayer('applications + selection', function () { setVue('applications'); if (applications.length) ouvrirApp(applications[0].id); });
|
||
|
|
console.log('__RESULTAT__' + JSON.stringify(__erreurs));
|
||
|
|
}).catch(function (e) {
|
||
|
|
console.log('__RESULTAT__' + JSON.stringify(['charger() : ' + (e && e.stack || e)]));
|
||
|
|
});
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
class TestRenduGui(unittest.TestCase):
|
||
|
|
"""Chaque vue du GUI se dessine sans lever."""
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def setUpClass(cls):
|
||
|
|
if not shutil.which("node"):
|
||
|
|
raise unittest.SkipTest("node absent")
|
||
|
|
html = g.HTML.replace("__JETON__", "TEST")
|
||
|
|
blocs = re.findall(r"<script>(.*?)</script>", html, re.S)
|
||
|
|
assert blocs, "aucun bloc <script>"
|
||
|
|
cls.js = BANC + "\n".join(blocs) + APRES
|
||
|
|
|
||
|
|
def _jouer(self, js: str, payload: dict) -> list:
|
||
|
|
with tempfile.NamedTemporaryFile("w", suffix=".js", delete=False, encoding="utf-8") as f:
|
||
|
|
f.write(js)
|
||
|
|
chemin = f.name
|
||
|
|
try:
|
||
|
|
r = subprocess.run(["node", chemin], capture_output=True, text=True, timeout=120,
|
||
|
|
env={**__import__("os").environ, "PAYLOAD": json.dumps(payload)})
|
||
|
|
finally:
|
||
|
|
Path(chemin).unlink(missing_ok=True)
|
||
|
|
marque = "__RESULTAT__"
|
||
|
|
for ligne in r.stdout.splitlines():
|
||
|
|
if ligne.startswith(marque):
|
||
|
|
return json.loads(ligne[len(marque):])
|
||
|
|
self.fail(f"le banc n'a rien rendu.\nstdout: {r.stdout[-2000:]}\nstderr: {r.stderr[-2000:]}")
|
||
|
|
|
||
|
|
def test_toutes_les_vues_rendent(self):
|
||
|
|
"""La vraie reponse de l'API, les neuf vues, zero exception."""
|
||
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
||
|
|
erreurs = self._jouer(self.js, payload)
|
||
|
|
self.assertEqual(erreurs, [], "des vues ont leve :\n - " + "\n - ".join(erreurs))
|
||
|
|
|
||
|
|
def test_controle_negatif_reference_absente(self):
|
||
|
|
"""LE CONTROLE. Une lecture d'identifiant inexistant DOIT faire echouer le banc.
|
||
|
|
|
||
|
|
Sans lui, ce test passerait aussi bien sur un banc qui n'execute rien. On rejoue
|
||
|
|
exactement la faute du 2026-09-08 : le formulaire des bases lit un global qui
|
||
|
|
n'existe pas.
|
||
|
|
"""
|
||
|
|
casse = self.js.replace("const reg = schemaPlan[nomRegistre] || null;",
|
||
|
|
"const reg = (donneesQuiNexistentPas.schema || {})[nomRegistre] || null;")
|
||
|
|
self.assertNotEqual(casse, self.js, "le motif du controle negatif n'existe plus")
|
||
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
||
|
|
erreurs = self._jouer(casse, payload)
|
||
|
|
self.assertTrue(any("donneesQuiNexistentPas" in e for e in erreurs),
|
||
|
|
f"le banc n'a pas vu la reference absente : {erreurs}")
|
||
|
|
|
||
|
|
def test_le_schema_est_bien_servi(self):
|
||
|
|
"""Le formulaire ne peut rien generer si l'API n'envoie pas le schema."""
|
||
|
|
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
||
|
|
self.assertIn("schema", payload, "l'API ne sert pas le schema du plan")
|
||
|
|
self.assertIn("nomenclature", payload["schema"])
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|