// Run with Node >= 20 and an NW.js SDK binary: node test/ui-smoke.cjs /path/to/nw // Uses production library order, views and static mounts without starting the collector. const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const { pathToFileURL } = require('node:url'); const { spawn } = require('node:child_process'); const assert = require('node:assert/strict'); const root = path.resolve(__dirname, '..'); const src = path.join(root, 'src'); const express = require(path.join(src, 'node_modules/express')); const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'carpincho-ui-')); const read = name => fs.readFileSync(path.join(src, name), 'utf8'); const clean = html => html.replace(//g, ''); const app = express(); // Read actual mounts so a stale web route also fails this test. const routes = [...read('gui_web/express.js').matchAll(/app.use\('([^']+)', express.static\(__dirname \+ '([^']+)'\)\)/g)]; app.use(express.static(path.join(src, 'gui_web'))); for (const [, url, dir] of routes) app.use(url, express.static(path.join(src, 'gui_web', dir))); async function probe() { const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const check = (value, message) => { if (!value) throw new Error(message); }; const tests = []; try { await wait(200); check($.fn.jquery.startsWith('3.'), 'Expected jQuery 3'); check(bootstrap.Tooltip.VERSION.startsWith('5.'), 'Expected Bootstrap 5'); for (const tab of document.querySelectorAll('[data-bs-toggle="tab"]')) { tab.click(); await wait(200); check(document.querySelector(tab.dataset.bsTarget)?.classList.contains('active'), 'Tab failed: ' + tab.id); } tests.push('all navigation tabs'); $('#settings').html(window.settingsHTML); // Repeated updates must keep one label and one menu item per option. $('.selectpicker').selectpicker(); await wait(200); for (const select of document.querySelectorAll('#settings select.selectpicker')) { const $select = $(select); $select.append(new Option('Alternativa', '__alternative'), new Option('Prueba dinámica', '__smoke')); actualizarSelects($select); $select.val('__smoke'); for (let i = 0; i < 3; i++) actualizarSelects($select); let wrapper = select.closest('.bootstrap-select'); check(wrapper.querySelector('.filter-option').textContent.trim() === 'Prueba dinámica', 'Render failed: ' + select.id); wrapper.querySelector('button').click(); await wait(50); check(wrapper.querySelector('.dropdown-menu').classList.contains('show'), 'Select did not open: ' + select.id); check([...wrapper.querySelectorAll('[role="option"] .text')].filter(item => item.textContent === 'Prueba dinámica').length === 1, 'Duplicated menu option: ' + select.id); const first = wrapper.querySelector('[role="option"]'); first.click(); check(select.value !== '__smoke', 'Option selection failed: ' + select.id); $select.prop('disabled', true); actualizarSelects($select); wrapper = select.closest('.bootstrap-select'); check(wrapper.querySelector('button').getAttribute('aria-disabled') === 'true', 'Disabled state failed: ' + select.id); wrapper.querySelector('button').click(); check(!wrapper.querySelector('.dropdown-menu').classList.contains('show'), 'Disabled select opened: ' + select.id); $select.selectpicker('destroy'); } tests.push('configuration selects: refresh, value, open, select, disable, destroy'); $('#tip').tooltip('show'); check(!!document.querySelector('.tooltip'), 'Tooltip did not open'); $('#tip').tooltip('dispose'); tests.push('tooltip show/dispose'); const table = await new Promise(resolve => { $('#table').DataTable({dom: 'Bfrtip', buttons: ['copy', 'csv'], pageLength: 1, language: {url: window.languageURL}, initComplete() { resolve(this.api()); }}); }); check(table.rows().count() === 2, 'Missing table rows'); table.search('Beta').draw(); check(table.rows({search: 'applied'}).count() === 1, 'Table search failed'); check(table.buttons.exportData().body[0][0] === 'Beta', 'CSV/copy data failed'); check(document.querySelectorAll('.dt-buttons .btn').length === 2, 'Missing export buttons'); check(!document.querySelector('.dt-buttons .btn-default'), 'Legacy button class'); check(document.querySelector('.dataTables_info').textContent.includes('Mostrando'), 'Spanish language failed'); table.destroy(); tests.push('table initialization, filtering, export data, buttons, translation, destroy'); check(window.failures.length === 0, window.failures.join('\n')); } catch (error) { window.failures.push(error.stack); } require('fs').writeFileSync(window.resultFile, JSON.stringify({versions: process.versions, tests, failures: window.failures}, null, 2)); nw.App.quit(); } async function run(mode, origin) { const folder = path.join(temp, mode); fs.mkdirSync(folder); const html = clean(read(mode + '/index.html')); const base = mode === 'gui' ? pathToFileURL(path.join(src, 'gui/index.html')).href : origin + '/'; // Verify every resource in the entry point and dynamic HTML against its document base. const pages = ['index.html', ...(mode === 'gui' ? ['index_actualizar.html'] : []), ...fs.readdirSync(path.join(src, mode, 'view')).filter(n => n.endsWith('.html')).map(n => 'view/' + n)]; for (const file of pages) { for (const [, resource] of clean(read(mode + '/' + file)).matchAll(/(?:src|href)=["']([^"']+)["']/g)) { if (/^(#|https?:|mailto:|javascript:)/.test(resource)) continue; const url = new URL(resource, base); if (url.protocol === 'file:') assert(fs.existsSync(url), `${file}: missing ${url}`); else assert((await fetch(url)).ok, `${file}: missing ${url}`); } } const styleURL = new URL(mode === 'gui' ? '../gui_common/css/style.css' : 'gui_common/css/style.css', base); for (const [, resource] of read('gui_common/css/style.css').matchAll(/url\(['"]?([^)'"]+)/g)) { const url = new URL(resource, styleURL); if (url.protocol === 'file:') assert(fs.existsSync(url), 'Missing CSS image: ' + url); else assert((await fetch(url)).ok, 'Missing CSS image: ' + url); } const head = [...html.matchAll(/]*>| ${head}
${panes}| Nombre |
|---|
| Alpha |
| Beta |