from __future__ import annotations import json import re import shutil import subprocess import unittest from pathlib import Path class HarnaisClips(unittest.TestCase): """Extrait de serve_app.js ce qui peint la liste des clips et l'exécute sous node.""" @classmethod def setUpClass(cls): cls.node = shutil.which("node") if cls.node is None: raise unittest.SkipTest("node introuvable") source = Path(__file__).with_name("serve_app.js").read_text(encoding="utf-8") fragments = [] for nom in ("visible", "duration", "restaurerTaillePageClips", "navigationClips", "changerPageClips", "renderClips", "card", "load", "appliquerFiltre", "suppressionProposee", "casesPage", "barreSelectionPage", "majSelectionPage", "stagerPage", "stagerExclusion", "stagerSuppression"): match = re.search(rf"^(?:async )?function {nom}\(.*?^\}}", source, re.MULTILINE | re.DOTALL) if match is None: raise AssertionError(f"fonction introuvable : {nom}") fragments.append(match.group(0)) cls.code = "\n".join(fragments) cls.constants = re.search(r"^const CLIPS_PAR_PAGE = .*?^let clipsParPage = .*?;", source, re.MULTILINE | re.DOTALL).group(0) cls.click = re.search(r'^\$\("list"\).addEventListener\("click", .*?^\}\);', source, re.MULTILINE | re.DOTALL).group(0) cls.submit = re.search(r'^\$\("list"\).addEventListener\("submit", .*?^\}\);', source, re.MULTILINE | re.DOTALL).group(0) cls.change = re.search(r'^\$\("list"\).addEventListener\("change", .*?^\}\);', source, re.MULTILINE | re.DOTALL).group(0) def executer(self, actions="", total=1605, stockage=None, stockage_indisponible=False): script = """ (async () => { const transitions = []; const lifecycle = []; const ordre = []; const stockage = STOCKAGE; const localStorage = { getItem(k) { if (INDISPONIBLE) throw Error('storage'); return stockage[k] ?? null; }, setItem(k,v) { if (INDISPONIBLE) throw Error('storage'); stockage[k]=v; } }; const boites = {view:{value:'clips'},camera:{value:''},showOut:{checked:true}, filtre:{close(){}},count:{},filtreCompte:{},list:{innerHTML:'',scrollIntoView(){}, querySelector(){return null;},querySelectorAll(){return [];},contains(){return true;}, addEventListener(n,f){this[n]=f;}}}; const $ = id => boites[id]; const h = v => String(v).replaceAll('&','&').replaceAll('<','<').replaceAll('"','"'); const t = k => k; const tf = (k,v) => k + ':' + JSON.stringify(v); const avecJeton = v => v; let data = {filtered:false,total_known:{clip:1605},suppressionAuto:[],clips:[]}; for(let i=0;i { lifecycle.push('cleanup'); ordre.push('cleanup'); }; const preparerLecteursClips = () => { lifecycle.push('prepare'); ordre.push('prepare'); }; const _filtrePersiste = null; let plageClips = {preset:'all'}, plageEnAttente = null; const paramsPourPlage = () => ''; const fetch = async url => url.startsWith('/api/clips') ? structuredClone(data) : []; const lireJSON = async answer => answer; const estMasquee = () => false; const comparerNoms = (a, b) => String(a).localeCompare(String(b)); const fill = () => {}, camerasConnues = () => [], sauvegarderFiltre = () => {}; const majBoutonAppliquer = () => {}, chargerSnapshots = () => {}; const render = () => renderClips(); """.replace("TOTAL", str(total)).replace("STOCKAGE", json.dumps(stockage or {})).replace("INDISPONIBLE", str(stockage_indisponible).lower()) + self.constants + "\n" + self.code + "\n" + self.click + "\n" + self.submit + "\n" + self.change + "\n" + actions + """ renderClips(); console.log(JSON.stringify({html:$('list').innerHTML,page:pageClips,size:clipsParPage,transitions,lifecycle,ordre,stockage, extra:globalThis.lireExtra ? globalThis.lireExtra() : null, selected:data.clips.filter(c=>c.excludedStaged||c.supprimerStaged).map(c=>c.identity)})); })().catch(e=>{console.error(e);process.exitCode=1;}); """ r = subprocess.run([self.node, "-e", script], text=True, encoding="utf-8", capture_output=True, timeout=20, check=True) return json.loads(r.stdout) def identities(self, sortie): return re.findall(r'src="/media/clip/([^" ]+)"', sortie["html"]) class TestsPaginationClips(HarnaisClips): def test_premiere_page_bornee_et_ordre_conserve(self): sortie = self.executer() self.assertEqual(self.identities(sortie), [f"clip-{i}" for i in range(50)]) self.assertEqual(sortie["html"].count('
'), 2) self.assertEqual(masquee["html"].count('