#!/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; };
// `querySelectorAll('[data-champ]')` rend vraiment les controles du dernier rendu : le
// GUI s'en sert pour valider avant d'enregistrer, et un stub qui rend toujours [] ferait
// passer cette validation sur ZERO champ — le banc mesurerait alors moins que rien.
Elem.prototype.querySelectorAll = function (sel) {
if (!/data-champ/.test(String(sel))) { return []; }
var html = this.innerHTML || '';
var out = [], re = /<(input|select)([^>]*?)data-champ="([^"]*)"([^>]*)>/g, m;
while ((m = re.exec(html)) !== null) {
var attrs = m[2] + m[4];
var v = /\svalue="([^"]*)"/.exec(attrs);
out.push({__champ: m[3], value: v ? v[1] : '',
getAttribute: function (n) { return n === 'data-champ' ? this.__champ : null; },
classList: {add: function () {}, remove: function () {}, toggle: function () {}},
focus: function () {}});
}
return out;
};
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; },
// `#detail [data-champ]` : le GUI interroge le DOCUMENT, pas l'element. Un stub qui
// rendait [] ici faisait croire que le formulaire n'etait pas marque — et la
// sauvegarde des serveurs refusait, silencieusement, dans le banc seul.
querySelectorAll: function (sel) {
var m = /^#([\w-]+)\s+(.+)$/.exec(String(sel));
if (m && __elems[m[1]]) { return __elems[m[1]].querySelectorAll(m[2]); }
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);
var __postsCaptures = {};
// `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, opts) {
if (opts && opts.method === 'POST') { __postsCaptures[String(url)] = JSON.parse(opts.body); }
// Un POST rend la MEME charge que l'inventaire : le GUI recharge apres sauvegarde, et
// lui rendre {} le ferait repartir d'un plan vide — un faux vert, pas un test.
var corps = (String(url).indexOf('/api/inventaire') === 0 || (opts && opts.method === 'POST'))
? __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)]));
});
"""
# LE BANC D'UNE CONSOLE DE SITE. On ne rejoue pas les neuf vues : on regarde CE QUE
# la vue d'accueil dessine, parce que c'est la qu'un site ne montrait rien.
APRES_SITE = r"""
charger().then(function () {
var sortie = {erreurs: [], grille: '', detail: '', detailChoisi: '', hotes: 0};
try {
sortie.hotes = (typeof hotes !== 'undefined' && hotes) ? hotes.length : 0;
setVue('serveurs');
sortie.grille = document.getElementById('grilles').innerHTML;
sortie.detail = document.getElementById('detail').innerHTML;
if (sortie.hotes) { choisirMachine(0); sortie.detailChoisi = document.getElementById('detail').innerHTML; }
} catch (e) { sortie.erreurs.push(String((e && e.stack) || e)); }
console.log('__RESULTAT__' + JSON.stringify(sortie));
}).catch(function (e) {
console.log('__RESULTAT__' + JSON.stringify({erreurs: ['charger() : ' + ((e && e.stack) || e)]}));
});
"""
class TestFormeDesCharges(unittest.TestCase):
"""Les deux charges que la page consomme ont-elles la MEME FORME ?
`ConsoleLocataire` et `ConsoleSite` servent la meme page : ce qu'elle sait lire d'un
cote, elle doit pouvoir le lire de l'autre. Depuis le decoupage en classes, c'est le
TRONC qui assemble — une seule `charge()` pour les deux — donc cette garde ne devrait
plus jamais se declencher. On la garde parce qu'une branche peut surcharger `charge()`
un jour, et parce qu'un test qui ne se declenche plus est precisement celui qui
prouve que la refonte tient.
Le 2026-09-20, `serveurs` valait `[]` d'un cote et `{}` de l'autre. Les deux sont
« vides », et la page ne les lit pas pareil : `(data.serveurs || []).map(...)` trouve
une table — truthy, sans `.map` — et `charger()` LEVE. La console d'un site mourait
avant de dessiner, et l'erreur accusait une methode manquante plutot qu'une forme.
Un vide doit avoir la FORME de ce qu'il remplace. Ce test ne demande pas que les deux
charges portent les memes VALEURS — un site n'a pas de plan de locataire — seulement
qu'elles portent les memes TYPES. Ce test n'a pas besoin de node.
"""
# Ce que les deux charges ne partagent legitimement pas, et pourquoi.
ECARTS_ADMIS = {
"contexte": "ajoute par la route GET pour un locataire, deja present pour un site",
"intrants": "un site n'a pas de plan de locataire dont tirer des intrants",
}
def test_les_deux_charges_portent_les_memes_types(self):
if not g.FICHIER_UNDERLAY.exists():
self.skipTest("aucune fabric montee : pas de charge de site a comparer")
site = g.ConsoleSite(g._depot_hebergeur(), True).charge()
tenant = g.ConsoleLocataire(g.INSTANCE, g.INVENTAIRE_DEFAUT, True).charge()
ecarts = []
for clef in sorted(set(tenant) & set(site)):
if clef in self.ECARTS_ADMIS:
continue
a, b = type(tenant[clef]).__name__, type(site[clef]).__name__
if a != b:
ecarts.append(f"{clef} : locataire={a}, site={b}")
self.assertEqual(ecarts, [],
"des clefs changent de type selon la console :\n - "
+ "\n - ".join(ecarts))
def test_un_ecart_admis_doit_encore_exister(self):
"""Une exemption qui survit a son objet est un mensonge tranquille."""
tenant = g.inventaire_api(g.INVENTAIRE_DEFAUT)
for clef in self.ECARTS_ADMIS:
self.assertTrue(clef in tenant or clef == "contexte",
f"{clef} n'existe plus : son exemption n'a plus d'objet")
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"", html, re.S)
assert blocs, "aucun bloc