// Functional DOM test of the bulk-delete checkbox UI. // Loads the REAL index.html into jsdom, stubs fetch, and drives the actual // render + selection functions. Proves: checkboxes render, select-all works, // the bar appears only when a selection exists, and the payload is right. const fs = require('fs'); const path = require('path'); const { JSDOM } = require(path.join('/tmp/uitest/node_modules/jsdom')); const html = fs.readFileSync('/opt/gemastik18-final/panel/static/index.html', 'utf8'); // Fake 4 teams, mirroring the real API shape. const FAKE_TEAMS = { teams: [ { index: 1, label: 'Max', status: 'running', ports: {}, domain: 'max.attackdefense.imrnes.team' }, { index: 2, label: 'Aryma', status: 'running', ports: {}, domain: 'aryma.attackdefense.imrnes.team' }, { index: 3, label: 'Ken', status: 'running', ports: {}, domain: 'ken.attackdefense.imrnes.team' }, ], }; const calls = []; const dom = new JSDOM(html, { runScripts: 'dangerously', url: 'https://attackdefense.imrnes.team/' }); dom.window.fetch = async (url, opts) => { calls.push({ url: String(url), method: (opts && opts.method) || 'GET', body: opts && opts.body }); const u = String(url); if (u.endsWith('/api/teams') && !(opts && opts.method)) { return { ok: true, json: async () => FAKE_TEAMS }; } if (u.includes('/api/teams/bulk-delete/') && !(opts && opts.method)) { return { ok: true, json: async () => ({ state: 'done', done: [{ team: 2 }], errors: {} }) }; } return { ok: true, json: async () => ({ ok: true }) }; }; dom.window.confirm = () => true; const w = dom.window, d = w.document; // jsdom keeps top-level `let` out of window, so read it through eval in the page. const sel_set = () => w.eval('TEAM_SELECTED'); const call = (fn) => w.eval(`${fn}()`); let pass = 0, fail = 0; const ok = (name, cond, extra) => { if (cond) { pass++; console.log(' PASS ' + name); } else { fail++; console.log(' FAIL ' + name + (extra ? ' -> ' + extra : '')); } }; (async () => { // Drive the real page init: it calls loadTeams on DOMContentLoaded. await w.loadTeams(); const boxes = () => Array.from(d.querySelectorAll('input[id^="teamSel"]')); ok('one checkbox per team rendered', boxes().length === 3, 'got ' + boxes().length); ok('checkbox id encodes the team index', boxes()[0] && boxes()[0].id === 'teamSel1', boxes()[0] && boxes()[0].id); ok('checkbox onchange passes the index', boxes()[0] && /toggleTeamSelect\(1,/.test(boxes()[0].getAttribute('onchange') || ''), boxes()[0] && boxes()[0].getAttribute('onchange')); // Bar hidden with no selection. const bar = d.getElementById('bulkDelBar'); ok('bar exists', !!bar); ok('bar hidden when nothing selected', bar && bar.style.display === 'none', bar && bar.style.display); // Tick team 2 -> bar appears, count updates. boxes()[1].checked = true; boxes()[1].dispatchEvent(new w.Event('change', { bubbles: true })); ok('bar shows after first tick', bar.style.display !== 'none', bar.style.display); ok('TEAM_SELECTED holds team2', sel_set().has(2) && sel_set().size === 1, 'size=' + sel_set().size); ok('bar mentions 1 team', /1\b/.test(bar.textContent), JSON.stringify(bar.textContent.trim().slice(0, 60))); // Select all -> every team ticked. The button drives selectAllTeams(true). const allBtn = Array.from(d.querySelectorAll('button')) .find(b => /selectAllTeams\(true\)/.test(b.getAttribute('onclick') || '')); ok('select-all button exists', !!allBtn); allBtn.click(); ok('select-all ticks every box', boxes().every(b => b.checked)); ok('TEAM_SELECTED has all 3', sel_set().size === 3, 'size=' + sel_set().size); ok('bar shows 3 teams', /3/.test(bar.textContent)); // Bulk delete issues ONE POST with the selected indices. calls.length = 0; const goBtn = Array.from(d.querySelectorAll('#bulkDelBar button')) .find(b => /bulkDeleteTeams\(\)/.test(b.getAttribute('onclick') || '')); ok('bulk delete button appears in the bar', !!goBtn); goBtn.click(); await new Promise(r => setTimeout(r, 400)); const post = calls.find(c => c.url.includes('bulk-delete') && c.method === 'POST'); ok('bulk delete POSTs once', !!post, JSON.stringify(calls.map(c => c.method + ' ' + c.url))); if (post) { const payload = JSON.parse(post.body); ok('payload carries all 3 indices', JSON.stringify(payload.indices) === '[1,2,3]', JSON.stringify(payload)); } // Clear selection hides the bar again. call('clearTeamSelection'); ok('clear empties selection', sel_set().size === 0); ok('bar hidden after clear', bar.style.display === 'none', bar.style.display); ok('checkboxes cleared too', boxes().every(b => !b.checked)); console.log(`\n ${pass} passed, ${fail} failed`); process.exit(fail === 0 ? 0 : 1); })();