All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m10s
De knop Volgende deed niets. Alpine haalt een boolean-attribuut alleen weg bij null, undefined of false; het getal 0 zet het juist AAN. Mijn :disabled="stepMissing().length" gaf bij een ingevuld formulier 0 en zette de knop dus permanent uit. De logica klopte, de binding niet — met losse logica niet te vinden, dus er draait nu een test die de echte modal met Alpine in een DOM rendert en erop klikt. Tijdzones: - Nieuwe instelling TIMEZONE, standaard die van de server (TZ, /etc/timezone, /etc/localtime). In alle 77 sjablonen stond Europe/Amsterdam hard ingevuld. - Apps zonder tijdzoneveld krijgen TZ er automatisch bij; die draaiden in UTC. Poorten: - _bezette_poorten() telt nu ook mee waar de host zelf op luistert, gelezen uit /proc/net/tcp in een hulpcontainer met --network host. Een nginx buiten Docker zag Server Up eerder niet. - GET /api/ports/check geeft vrij/bezet plus een alternatief, en meldt of de host echt gecontroleerd kon worden. Netwerken: - APP_NETWORKS: meerdere gedeelde bridge-netwerken in plaats van één waar élke app aan hing. Per app kies je welke; aanmaken kan vanuit het invulmenu, met eigen naam of een voorstel (su-<appnaam>). - add_shared_network() accepteert een lijst; één naam blijft werken. - GET /api/networks/<n>/ip-check controleert subnet, bereik, Docker-toewijzing en de ARP-tabel van de host (vangt een fysiek apparaat met DHCP-adres) en stelt een vrij adres voor. Pangolin (nieuw, core/pangolin.py): - Publieke URL per app via een tunnel, zonder poorten open te zetten. - De API veranderde in 1.9 (resource hing onder een site, staat nu los); beide routes worden geprobeerd en bij een fout krijg je beide meldingen. - API-sleutel is write-only. Getest tegen een nagebouwde API, niet tegen een echte server. Nieuw: docs/pangolin.md; docs/netwerken.md uitgebreid. 2369 tests groen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
101 lines
4.4 KiB
JavaScript
101 lines
4.4 KiB
JavaScript
// Rendert de echte install-modal met Alpine in een DOM en klikt erop.
|
|
// Vangt bindingsfouten die je met losse logica niet ziet: :disabled="0"
|
|
// schakelt een knop juist UIT, want Alpine haalt een boolean-attribuut
|
|
// alleen weg bij null, undefined of false.
|
|
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.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/);
|
|
w.document.body.innerHTML = `<div x-data="app()">${m[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);
|
|
|
|
console.log(fouten === 0 ? '\nAlle DOM-controles geslaagd' : `\n${fouten} FOUTEN`);
|
|
process.exit(fouten ? 1 : 0);
|
|
})();
|