// Draait de echte Alpine-component uit index.html en toetst de logica van // het invulmenu. Wordt aangeroepen door tests/test_ui_logica.py; los draaien // kan met: node tests/js/menu.test.js const fs = require('fs'); const html = fs.readFileSync(__dirname + '/../../server-up/templates/index.html', 'utf8'); // Alleen de component-functie uitsnijden en uitvoeren. const start = html.indexOf('function app() {'); const eind = html.indexOf('', start); const bron = html.slice(start, eind); global.localStorage = { getItem: () => null, setItem: () => {} }; global.location = { hash: '', search: '' }; global.window = { matchMedia: () => ({matches:false, addEventListener(){}}), addEventListener(){}, innerWidth: 1200 }; global.document = { documentElement: {classList:{add(){},remove(){}}}, addEventListener(){}, querySelector: () => null }; global.navigator = { clipboard: { writeText: async () => {} }, language: 'nl' }; global.crypto = require('crypto').webcrypto; eval(bron); const c = app(); c.t = (k, v) => k; // vertaling niet nodig voor de logica c.bpFieldVisible = () => true; c.rpc = async () => ({ok: true, preview_files: {}, env_preview: ''}); c.toast = () => {}; function veld(naam, stap, extra = {}) { return Object.assign({name: naam, title: naam, step: stap, type: 'str', group: 'Algemeen', required: false, advanced: false}, extra); } let fouten = 0; const check = (naam, waar) => { if (!waar) { fouten++; console.log(' ✖ ' + naam); } else console.log(' ✓ ' + naam); }; // ── Eenvoudige app: alleen basisvelden ────────────────────────────────────── c.install = {format:'boilerplate', loading:false, fields:[ veld('service_name','basis'), veld('port','basis'), veld('appdata_dir','basis'), ], values:{}, step:0}; c.onePage = false; c.showAdvanced = false; let s = c.wizardSteps().map(x => x.id); check('eenvoudige app: basis, verbinden, controleren', JSON.stringify(s) === '["basis","verbinden","controleren"]'); // ── App met geheimen ──────────────────────────────────────────────────────── c.install.fields.push(veld('db_password','geheimen',{required:true, secret:true})); s = c.wizardSteps().map(x => x.id); check('geheimenstap verschijnt', s.includes('geheimen')); // ── Lege stap wordt overgeslagen ──────────────────────────────────────────── check('geen lege instellingenstap', !s.includes('instellingen')); // ── Geavanceerde velden ───────────────────────────────────────────────────── c.install.fields.push(veld('retention','instellingen',{advanced:true})); check('geavanceerd veld verborgen', c.stepFields('instellingen').length === 0); check('stap zonder zichtbare velden verdwijnt', !c.wizardSteps().map(x=>x.id).includes('instellingen')); c.showAdvanced = true; check('geavanceerd veld zichtbaar na aanzetten', c.stepFields('instellingen').length === 1); check('stap komt terug', c.wizardSteps().map(x=>x.id).includes('instellingen')); check('teller klopt', c.advancedCount() === 1); // ── Verplicht veld blokkeert Volgende ─────────────────────────────────────── c.install.step = c.wizardSteps().findIndex(x => x.id === 'geheimen'); check('leeg verplicht veld wordt gemeld', c.stepMissing().length === 1); c.install.values.db_password = 'ietsGeheims'; check('ingevuld: niets meer te melden', c.stepMissing().length === 0); // ── Groepsschakelaar uit: veld telt niet mee ──────────────────────────────── c.install.fields.push(veld('opt_pass','geheimen', {required:true, secret:true, group:'Extra', group_toggle:'gebruik_extra'})); c.install.values.gebruik_extra = false; check('uitgeschakelde groep blokkeert niet', c.stepMissing().length === 0); c.install.values.gebruik_extra = true; check('ingeschakelde groep blokkeert wel', c.stepMissing().length === 1); // ── Navigatie ─────────────────────────────────────────────────────────────── c.install.values.opt_pass = 'x'; c.install.step = 0; check('niet op de laatste stap', !c.opLaatsteStap()); c.install.step = c.wizardSteps().length - 1; check('wel op de laatste stap', c.opLaatsteStap()); check('laatste stap is controleren', c.wizardSteps()[c.install.step].id === 'controleren'); // ── Alles op één pagina ───────────────────────────────────────────────────── c.onePage = true; check('wizard uit bij één pagina', !c.wizardActief()); // ── Groepskoppen ──────────────────────────────────────────────────────────── c.onePage = false; c.install.fields = [veld('a','basis'), veld('b','basis')]; check('één groep: geen kop', c.stepGroups('basis')[0].showHeader === false); c.install.fields.push(veld('c','basis',{group:'Tweede'})); check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader)); // ── Stap Onderdelen ───────────────────────────────────────────────────────── // Een stack als de ARR-stack heeft vijftig schakelbare onderdelen. Die horen in // één eigen stap, gegroepeerd per kopje — en nergens anders nóg een keer. function schakelaar(naam, sectie, aan = false) { return veld('enable_' + naam, 'onderdelen', { type: 'bool', is_group_toggle: true, group_toggle: 'enable_' + naam, group: naam, section: sectie, default: aan}); } c.install = {format:'boilerplate', loading:false, values:{}, step:0, ips:{}, services:[], fields:[ veld('service_name','basis'), schakelaar('sonarr','Bibliotheken'), schakelaar('radarr','Bibliotheken'), schakelaar('prowlarr','Indexers'), veld('port_sonarr','instellingen',{type:'int', group:'Sonarr', group_toggle:'enable_sonarr', advanced:true}), veld('port_radarr','instellingen',{type:'int', group:'Radarr', group_toggle:'enable_radarr', advanced:true}), ]}; c.showAdvanced = true; s = c.wizardSteps().map(x => x.id); check('onderdelen is de eerste stap', s[0] === 'onderdelen'); check('drie schakelaars in de stap', c.stepFields('onderdelen').length === 3); const secties = c.stepSections('onderdelen'); check('twee kopjes', secties.length === 2); check('kopjes kloppen', secties.map(x => x.name).join() === 'Bibliotheken,Indexers'); check('nog niets gekozen', c.gekozenOnderdelen() === 0); c.zetSectie(secties[0], true); check('sectie in één keer aan', c.gekozenOnderdelen() === 2); c.zetSectie(secties[0], false); check('en weer uit', c.gekozenOnderdelen() === 0); // De schakelaar hoorde nergens anders op te duiken. Stond hij ook in de kop van // zijn eigen groep, dan zag je hem twee keer in twee verschillende stappen. c.install.values.enable_sonarr = true; const groepen = c.stepGroups('instellingen'); check('alleen ingeschakelde onderdelen krijgen een kaart', groepen.length === 1); check('kaart is van Sonarr', groepen[0].name === 'Sonarr'); check('geen tweede schakelaar in de groepskop', groepen[0].toggle === null); // ── Eigen IP per container ────────────────────────────────────────────────── check('zonder voorvertoning geen containers', c.ipContainers().length === 0); c.install.services = ['arr-sonarr', 'arr-radarr']; check('containers komen uit de voorvertoning', c.ipContainers().length === 2); c.zetContainerIp('arr-sonarr', true); check('container aangezet', 'arr-sonarr' in c.install.ips); check('andere container blijft op de host', !('arr-radarr' in c.install.ips)); c.install.ips['arr-sonarr'] = '192.168.1.240'; c.zetContainerIp('arr-sonarr', false); check('en weer uitgezet', !('arr-sonarr' in c.install.ips)); console.log(fouten === 0 ? '\nAlle menucontroles geslaagd' : `\n${fouten} FOUTEN`); process.exit(fouten ? 1 : 0);