166 lines
7.4 KiB
JavaScript
166 lines
7.4 KiB
JavaScript
// Rendert de echte installatiemodal en appkaart met Alpine in een DOM en klikt
|
|
// de bedieningen daadwerkelijk aan. Vangt bindingsfouten die je met losse
|
|
// logica niet ziet: een ontbrekende eigenschap in een x-for-item liet hier het
|
|
// boolean-attribuut disabled staan. Alleen de gerenderde DOM laat vervolgens
|
|
// zien dat de knop geen klikevent meer ontvangt.
|
|
const fs = require('fs'), path = require('path');
|
|
|
|
// jsdom en Alpine zijn geen onderdeel van dit project; zonder die twee slaat
|
|
// deze test zichzelf over (afsluitcode 2). Installeren kan met:
|
|
// cd tests/js && npm install jsdom alpinejs
|
|
let JSDOM, alpinePad;
|
|
try {
|
|
({ JSDOM } = require('jsdom'));
|
|
alpinePad = require.resolve('alpinejs/dist/cdn.js');
|
|
} catch (e) {
|
|
console.log('overgeslagen: jsdom of alpinejs ontbreekt (npm install in tests/js)');
|
|
process.exit(2);
|
|
}
|
|
const WORTEL = path.resolve(__dirname, '..', '..');
|
|
const html = fs.readFileSync(WORTEL + '/server-up/templates/index.html', 'utf8');
|
|
|
|
const dom = new JSDOM('<!doctype html><html><body></body></html>',
|
|
{ runScripts: 'dangerously', pretendToBeVisual: true, url: 'http://localhost/' });
|
|
const w = dom.window;
|
|
w.fetch = async () => ({ ok: true, json: async () => ({ ok: true }) });
|
|
|
|
const start = html.indexOf('function app() {');
|
|
w.eval(html.slice(start, html.indexOf('</script>', start)));
|
|
|
|
// De component vóór Alpine klaarzetten: erna is het een proxy die dit weigert.
|
|
const echt = w.app;
|
|
w.app = function () {
|
|
const c = echt();
|
|
c.t = k => k;
|
|
c.bpFieldVisible = () => true;
|
|
c.toast = () => {};
|
|
c.rpc = async () => ({ ok: true, preview_files: { 'compose.yaml': 'services: {}' }, env_preview: '' });
|
|
c.connectTargets = () => [];
|
|
c.pickIp = () => {};
|
|
c.netList = [];
|
|
c.sharedNetwork = 'serverup';
|
|
c.page = 'stacks';
|
|
c.stacks = [{
|
|
name: 'mijn-app', has_compose: true, running: true, containers: [],
|
|
categories: [], depends_missing: [], editable: true, connected: false
|
|
}];
|
|
c.modal = 'install';
|
|
c.install = {
|
|
format: 'boilerplate', loading: false, stack: 'test', repo_id: 'r', instance: 'test',
|
|
values: { service_name: 'test', port: 8080 },
|
|
fields: [
|
|
{name:'service_name',title:'Naam',step:'basis',type:'str',group:'Algemeen',required:true,advanced:false},
|
|
{name:'port',title:'Poort',step:'basis',type:'int',group:'Algemeen',required:true,advanced:false},
|
|
{name:'db_password',title:'Wachtwoord',step:'geheimen',type:'str',group:'Algemeen',required:true,secret:true,advanced:false},
|
|
],
|
|
step: 0, preview_files: {}, composeText: '', envText: '', editing: false, tab: 'compose',
|
|
mode: 'install', connect_apps: true, network: '', ip: '', envVars: [], images: [],
|
|
envOverrides: {}, imageTags: {},
|
|
};
|
|
return c;
|
|
};
|
|
|
|
const m = html.match(/<div x-show="modal==='install'"[\s\S]*?\n <\/div>\n/);
|
|
const rm = html.match(/<!-- Remove confirm -->[\s\S]*?<!-- Editor -->/);
|
|
const stacks = html.match(/<section x-show="page === 'stacks'"[\s\S]*?<\/section>/);
|
|
w.document.body.innerHTML = `<div x-data="app()">${stacks[0]}${m[0]}${rm[0]}</div>`;
|
|
w.eval(fs.readFileSync(alpinePad, 'utf8'));
|
|
|
|
const wacht = ms => new Promise(r => setTimeout(r, ms));
|
|
(async () => {
|
|
await wacht(100);
|
|
const comp = w.Alpine.$data(w.document.querySelector('[x-data]'));
|
|
const knoppen = [...w.document.querySelectorAll('.modal-foot button')];
|
|
console.log('knoppen in de voet:');
|
|
knoppen.forEach(b => console.log(
|
|
` "${b.textContent.trim().slice(0,14)}" zichtbaar=${b.style.display !== 'none'} disabled=${b.disabled}`));
|
|
|
|
let fouten = 0;
|
|
const check = (naam, waar) => { console.log((waar ? ' ✓ ' : ' ✖ ') + naam); if (!waar) fouten++; };
|
|
check('velden worden gerenderd', w.document.querySelectorAll('.modal-body input').length > 0);
|
|
check('niets ontbreekt op stap 1', comp.stepMissing().length === 0);
|
|
const volgende = knoppen.find(b => b.textContent.includes('next'));
|
|
check('Volgende staat in de DOM', !!volgende);
|
|
if (!volgende) process.exit(1);
|
|
|
|
// Alpine haalt een boolean-attribuut alleen weg bij null, undefined of false.
|
|
// Een lege lijst gaf dus .length === 0, en dat zette disabled juist AAN.
|
|
check('Volgende is niet uitgeschakeld met alles ingevuld', volgende.disabled === false);
|
|
|
|
volgende.click();
|
|
await wacht(120);
|
|
check('klikken gaat naar de volgende stap', comp.install.step === 1);
|
|
|
|
// Leeg verplicht veld hoort wél te blokkeren.
|
|
comp.install.step = 2;
|
|
await wacht(60);
|
|
const geheimen = comp.wizardSteps()[2];
|
|
check('stap 3 is de geheimenstap', geheimen && geheimen.id === 'geheimen');
|
|
check('leeg wachtwoord blokkeert', comp.stepMissing().length === 1);
|
|
await wacht(60);
|
|
check('knop is dan uitgeschakeld', volgende.disabled === true);
|
|
|
|
comp.install.values.db_password = 'ietsGeheims';
|
|
await wacht(80);
|
|
check('knop weer bruikbaar na invullen', volgende.disabled === false);
|
|
|
|
// De verwijderknop testen we via de echte Alpine @click-binding. De losse
|
|
// menu-test roept doRemove() direct aan en mist fouten in de DOM.
|
|
let verwijderVerzoek = null;
|
|
let verwijderd = false;
|
|
comp.rpc = async (url, body) => {
|
|
verwijderVerzoek = {url, body};
|
|
return {job_id: 'remove-1'};
|
|
};
|
|
comp.pollJob = (id, klaar) => klaar('done');
|
|
comp.loadStacks = async () => { verwijderd = true; };
|
|
comp.toast = () => {};
|
|
const kaartknop = [...w.document.querySelectorAll('button')].find(
|
|
b => b.getAttribute('@click') === 'confirmRemove(s.name)');
|
|
check('verwijderknop op de appkaart is gerenderd', !!kaartknop);
|
|
if (!kaartknop) {
|
|
console.log('stacks:', JSON.stringify(comp.stacks));
|
|
console.log('filtered:', JSON.stringify(comp.filteredStacks));
|
|
console.log('click-bindings:', [...w.document.querySelectorAll('button')]
|
|
.map(b => b.getAttribute('@click')).filter(Boolean).join(' | '));
|
|
process.exit(1);
|
|
}
|
|
kaartknop.click();
|
|
await wacht(60);
|
|
const verwijderModal = [...w.document.querySelectorAll('.modal')].find(
|
|
e => e.getAttribute('x-show') === "modal==='remove'");
|
|
check('verwijdermodal opent met de juiste app',
|
|
comp.modal === 'remove' && comp.removeTarget === 'mijn-app');
|
|
const veilige = [...verwijderModal.querySelectorAll('button')].find(
|
|
b => b.getAttribute('@click') === "doRemove('full')");
|
|
check('verwijderknop is actief', veilige && !veilige.disabled);
|
|
veilige.click();
|
|
await wacht(80);
|
|
check('klik verstuurt de verwijderaanvraag', verwijderVerzoek &&
|
|
verwijderVerzoek.url === '/api/stacks/mijn-app/remove' &&
|
|
verwijderVerzoek.body.mode === 'full');
|
|
check('voltooide verwijdering herlaadt de apps', verwijderd);
|
|
|
|
// Op een telefoon loopt verwijderen via een functie in het x-for-actiemenu.
|
|
comp.modal = null;
|
|
comp.removeTarget = '';
|
|
const menuActies = [...w.document.querySelectorAll('button')].filter(
|
|
b => b.getAttribute('@click') === "a.doe(); stackMenu = ''");
|
|
comp.stackMenu = 'mijn-app';
|
|
await wacht(40);
|
|
const mobielVerwijderen = menuActies.find(
|
|
b => (w.Alpine.$data(b).a || {}).id === 'remove');
|
|
check('mobiele verwijderactie is gerenderd', !!mobielVerwijderen);
|
|
if (!mobielVerwijderen) process.exit(1);
|
|
check('mobiele verwijderactie is actief',
|
|
mobielVerwijderen && !mobielVerwijderen.disabled);
|
|
mobielVerwijderen.click();
|
|
await wacht(60);
|
|
if (comp.modal !== 'remove')
|
|
console.log('mobiele status:', comp.modal, comp.removeTarget, comp.stackMenu);
|
|
check('mobiele verwijderactie opent de modal',
|
|
comp.modal === 'remove' && comp.removeTarget === 'mijn-app');
|
|
|
|
console.log(fouten === 0 ? '\nAlle DOM-controles geslaagd' : `\n${fouten} FOUTEN`);
|
|
process.exit(fouten ? 1 : 0);
|
|
})();
|