/** * PachaFamily - Test Engine 🦙 * Moteur de tests automatisés (E2E) - Version Anti-Flakiness & QA Exhaustive */ const PachaTestEngine = { arena: document.getElementById("test-arena"), reportBox: document.getElementById("test-report"), doc: null, // ========================================== // 🛠️ HELPERS (BLINDÉS POUR LE QA) // ========================================== wait: (ms) => new Promise((r) => setTimeout(r, ms)), log: function (msg, icon = "ℹ️") { this.reportBox.value += `[${new Date().toLocaleTimeString()}] ${icon} ${msg}\n`; this.reportBox.scrollTop = this.reportBox.scrollHeight; }, assert: function (cond, msgPass, msgFail) { this.log(cond ? msgPass : msgFail || msgPass, cond ? "✅" : "❌"); }, // 📡 Intercepte les erreurs de l'application dans l'iframe pour les afficher dans le rapport injectIframeErrorCatcher: function () { if (!this.arena.contentWindow) return; this.arena.contentWindow.onerror = (msg, url, line) => { this.log(`Erreur App (Ligne ${line}): ${msg}`, "🔥"); }; const origError = this.arena.contentWindow.console.error; this.arena.contentWindow.console.error = (...args) => { this.log(`Console Error: ${args.join(" ")}`, "🔥"); origError.apply(this.arena.contentWindow.console, args); }; }, // 🛡️ Attend le rechargement de la page de manière stricte actionAndWaitForReload: async function (actionFn, timeout = 5000) { return new Promise((resolve, reject) => { let reloaded = false; this.arena.onload = () => { reloaded = true; this.doc = this.arena.contentWindow.document; this.arena.contentWindow.confirm = () => true; this.arena.contentWindow.alert = () => true; this.injectIframeErrorCatcher(); resolve(); }; // Exécute l'action et gère le rejet si elle plante Promise.resolve(actionFn()).catch((err) => reject(err)); setTimeout(() => { if (!reloaded) { this.log( "⚠️ Le rechargement n'a pas eu lieu. Suite du test...", "⏱️", ); resolve(); } }, timeout); }); }, load: async function (url) { this.log(`Chargement de la page : ${url}`, "🔄"); return new Promise((resolve) => { this.arena.onload = () => { this.doc = this.arena.contentWindow.document; this.arena.contentWindow.confirm = () => true; this.arena.contentWindow.alert = () => true; this.injectIframeErrorCatcher(); resolve(); }; this.arena.src = url; }); }, get: function (sel) { return this.doc.querySelector(sel); }, // 🖱️ Clic strict : Si l'élément n'existe pas, on DÉCLENCHE UNE ERREUR FATALE click: async function (sel, delay = 500) { const el = typeof sel === "string" ? this.get(sel) : sel; if (!el) { throw new Error( `Clic impossible : l'élément '${typeof sel === "string" ? sel : "DOM Object"}' est introuvable.`, ); } this.arena.contentWindow.confirm = () => true; this.arena.contentWindow.alert = () => true; el.click(); await this.wait(delay); return true; }, fill: function (sel, val) { const el = this.get(sel); if (!el) throw new Error(`Remplissage impossible : '${sel}' introuvable.`); el.value = val; }, select: async function (sel, val, delay = 300) { const el = this.get(sel); if (!el) throw new Error(`Sélection impossible : '${sel}' introuvable.`); el.value = val; el.dispatchEvent(new Event("change")); await this.wait(delay); }, findInTable: function (text) { return Array.from(this.doc.querySelectorAll("td")).find((el) => el.textContent.includes(text), ); }, // ========================================== // 🧪 SCÉNARIO EXHAUSTIF : SUIVI MENSUEL (MODALES & CRUD) // ========================================== testBudgetSuiviExhaustive: async function () { this.reportBox.value = ""; this.log("=== 🚀 DÉBUT EXHAUSTIF : SUIVI MENSUEL ===", "INFO"); await this.load("budget.php?tab=suivi"); await this.wait(1000); // --- 1. GESTION DE L'ÉTAT (CLÔTURE / RÉOUVERTURE) --- this.log("🔍 Test 1 : État du mois (Clôture / Réouverture)"); let btnReopenInitial = this.get( 'form input[value="reopen_month"] ~ button[type="submit"]', ); if (btnReopenInitial) { this.log( "🔒 Mois actuellement fermé. Réouverture forcée pour les tests...", "WARN", ); await this.actionAndWaitForReload( async () => await this.click(btnReopenInitial), ); await this.wait(1500); // ⏱️ Attente de la 2e redirection JS du PHP this.assert( this.get('form input[value="close_month"]') !== null, "Mois rouvert.", ); } // --- 2. UI TOGGLES --- this.log("🔍 Test 2 : Toggle 'Charges à venir'"); const toggleBtn = this.get( "button[onclick*=\"toggleDiv('pendingDetailsList')\"]", ); if (toggleBtn) { await this.click(toggleBtn); const pendingList = this.get("#pendingDetailsList"); this.assert( pendingList && pendingList.style.display !== "none", "Liste des charges déployée.", ); } // --- 3. MODALE CSV --- this.log("🔍 Test 3 : Modale Import CSV"); const btnCsv = this.get('button[onclick*="importCsvModal"]'); if (btnCsv) { await this.click(btnCsv); const csvModal = this.get("#importCsvModal"); this.assert( csvModal && (csvModal.classList.contains("is-open") || csvModal.style.display === "flex"), "Modale CSV ouverte.", ); // Fermeture const closeCsvBtn = csvModal.querySelector( ".pf-modal-close, button[onclick*='closeSuiviModal']", ); if (closeCsvBtn) await this.click(closeCsvBtn); this.assert( !csvModal.classList.contains("is-open") && csvModal.style.display !== "flex", "Modale CSV fermée.", ); } // --- 4. MODALE SNAPSHOT (UPDATE BALANCE) --- this.log("🔍 Test 4 : Modale Snapshot (Solde Bancaire)"); const btnSnapshot = this.get('button[onclick*="snapshotModal"]'); if (btnSnapshot) { await this.click(btnSnapshot); this.assert(this.get("#snapshotModal"), "Modale Snapshot ouverte."); this.fill('#snapshotModal input[name="snapshot_amount"]', "1337.00"); this.log("Soumission du nouveau solde...", "INFO"); await this.actionAndWaitForReload(async () => { await this.click('#snapshotModal button[type="submit"]'); }); const pageText = this.doc.body.innerText; this.assert( pageText.includes("1 337"), "Solde mis à jour et formaté à 1 337 €.", ); } // --- 5. CRUD DÉPENSE (AJOUT / ÉDITION / SUPPRESSION) --- this.log("🔍 Test 5 : CRUD Dépense Manuelle"); const btnAdd = this.get(".btn-add-item"); if (btnAdd) { // 5A. AJOUT this.log("Création d'une nouvelle dépense..."); await this.click(btnAdd); await this.select("#modalCatSelect", "Autres"); this.fill("#modalAmount", "77.77"); this.fill("#modalLabelInput", "E2E_QA_TEST_EXPENSE"); await this.actionAndWaitForReload(async () => { await this.click('#manualExpenseModal button[type="submit"]'); }); let newExpense = this.findInTable("E2E_QA_TEST_EXPENSE"); this.assert( newExpense !== undefined, "Dépense 'E2E_QA_TEST_EXPENSE' trouvée dans le DOM.", ); if (newExpense) { // 5B. ÉDITION this.log("✏️ Édition de la dépense..."); const row = newExpense.closest("tr"); // 🛡️ NOUVEAU : Sécurité anti-crash if (!row) { throw new Error( "L'élément trouvé n'est pas dans une ligne de tableau (