Se adecuó el UI
This commit is contained in:
parent
b3d6471018
commit
99264aba3d
48
README.md
48
README.md
@ -148,3 +148,51 @@ Si deseas realizar consultas, sugerencias o simplemente tenes alguna duda sobre
|
|||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
[](https://www.gugler.com.ar)[](http://fcyt.uader.edu.ar/web/)
|
[](https://www.gugler.com.ar)[](http://fcyt.uader.edu.ar/web/)
|
||||||
|
|
||||||
|
|
||||||
|
## Desarrollo y pruebas de la interfaz
|
||||||
|
|
||||||
|
Usar Node.js 20 o posterior (las pruebas se ejecutaron con Node.js 24 y NW.js 0.117.0).
|
||||||
|
La versión de NW.js está en `src/runtime.json`; los launchers, recompilaciones y
|
||||||
|
empaquetadores x64 la leen desde ese archivo. `npm run desktop` obtiene el SDK
|
||||||
|
exacto mediante npm si no está en caché. Para usar un SDK ya instalado, establecer
|
||||||
|
`NW_BINARY` con la ruta completa a su ejecutable. El arranque comprueba la versión.
|
||||||
|
|
||||||
|
Desde `src/`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm ci
|
||||||
|
npm run desktop
|
||||||
|
npm run desktop:dev
|
||||||
|
npm run web
|
||||||
|
```
|
||||||
|
|
||||||
|
`web` respeta la configuración de inicio automático existente. Las siguientes
|
||||||
|
pruebas usan configuración ficticia y archivos temporales; no activan la estación
|
||||||
|
ni publican registros:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run test:config
|
||||||
|
npm run test:ui -- /ruta/al/sdk/nw
|
||||||
|
npm run test:startup -- /ruta/al/sdk/nw
|
||||||
|
```
|
||||||
|
|
||||||
|
Las pruebas gráficas necesitan una sesión de escritorio. `test:ui` comprueba ambos
|
||||||
|
juegos de recursos (escritorio/web), formularios, navegación, guardado, recarga,
|
||||||
|
tablas y estados. `test:startup` abre una copia temporal de la aplicación completa
|
||||||
|
con el recolector desactivado. Los resultados quedan en el directorio temporal
|
||||||
|
indicado por cada comando.
|
||||||
|
|
||||||
|
El formulario único está en `src/gui_common/view/modelo_configuracion.html`.
|
||||||
|
Su modelo, controlador, tablas y estados compartidos están en
|
||||||
|
`src/gui_common/js/`. No se necesita bootstrap-select: todos los selects usan
|
||||||
|
controles nativos. La configuración web se guarda con `POST /GuardarConf`,
|
||||||
|
valida en servidor y responde después de la escritura. El GET de escritura
|
||||||
|
anterior fue retirado.
|
||||||
|
|
||||||
|
Los instaladores se generan con `node empaquetarLinux.js` o
|
||||||
|
`node empaquetarWindows.js` desde la raíz, después de instalar las dependencias
|
||||||
|
de raíz y `src/`. No se ejecutaron empaquetados ni instalaciones del sistema
|
||||||
|
como parte de las pruebas de interfaz. El script ARM histórico incluía Node 14
|
||||||
|
y otro NW.js; ahora se detiene antes de modificar el sistema. Linux ARM puede
|
||||||
|
usar el modo web con Node.js moderno; no hay una GUI ARM validada para este runtime.
|
||||||
|
|||||||
@ -28,7 +28,8 @@
|
|||||||
"desktop": "node src/tools/nw.cjs start",
|
"desktop": "node src/tools/nw.cjs start",
|
||||||
"desktop:dev": "node src/tools/nw.cjs dev",
|
"desktop:dev": "node src/tools/nw.cjs dev",
|
||||||
"test:config": "node --test test/configuracion.test.cjs",
|
"test:config": "node --test test/configuracion.test.cjs",
|
||||||
"test:ui": "node test/ui-smoke.cjs"
|
"test:ui": "node test/ui-smoke.cjs",
|
||||||
|
"test:startup": "node test/ui-startup.cjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
|
|||||||
@ -1,3 +1,13 @@
|
|||||||
|
** Mejoras de interfaz y configuración (Septiembre 27, 2026)
|
||||||
|
+ NW.js 0.117.0 centralizado en runtime.json para desarrollo y empaquetado x64.
|
||||||
|
+ Formulario compartido con controles nativos, validación y aviso de cambios sin guardar.
|
||||||
|
+ Guardado atómico y POST con confirmación; el núcleo recibe la configuración nueva.
|
||||||
|
+ Tablas y controles de estado compartidos entre escritorio y web.
|
||||||
|
+ Diseño adaptable sin zoom automático, con notificaciones ajustadas a pantallas pequeñas.
|
||||||
|
+ Estados separados para conexión, última lectura y publicación, con motivos y acciones.
|
||||||
|
+ Pruebas de configuración, interfaz y arranque aisladas de la estación real.
|
||||||
|
+ El empaquetador ARM histórico queda bloqueado: su Node/NW.js no soportan esta versión.
|
||||||
|
|
||||||
** Versión 2.0.0: (Septiembre 27, 2026)
|
** Versión 2.0.0: (Septiembre 27, 2026)
|
||||||
+ Se actualiza la versión de NWJS → 0.107.0
|
+ Se actualiza la versión de NWJS → 0.107.0
|
||||||
+ Node-hid ahora no necesita nw-gyp, se actualiza la compilación
|
+ Node-hid ahora no necesita nw-gyp, se actualiza la compilación
|
||||||
|
|||||||
12
src/conf/ConfiguracionGeneral.js
Executable file → Normal file
12
src/conf/ConfiguracionGeneral.js
Executable file → Normal file
@ -1,5 +1,4 @@
|
|||||||
// Configuraciones generales de EMA RECOLECTOR
|
// Configuración de EMA Libre
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"ConfGeneral": {
|
"ConfGeneral": {
|
||||||
"Tiempo_de_Recoleccion": 40,
|
"Tiempo_de_Recoleccion": 40,
|
||||||
@ -7,21 +6,20 @@ module.exports = {
|
|||||||
"SistemaMetrico": "SI",
|
"SistemaMetrico": "SI",
|
||||||
"debug": 3,
|
"debug": 3,
|
||||||
"debugTamanio": 10,
|
"debugTamanio": 10,
|
||||||
"NombreEstacion": "-",
|
"NombreEstacion": "Mi estación",
|
||||||
"Modo": "Recolector",
|
"Modo": "Visor",
|
||||||
"Llave_R": "e5303ab3-2a8b-4d42-90ab-1c8367d8521d",
|
"Llave_R": "e5303ab3-2a8b-4d42-90ab-1c8367d8521d",
|
||||||
"inicio_automatico": false,
|
"inicio_automatico": false,
|
||||||
"Audio": true,
|
"Audio": false,
|
||||||
"notificar_cambios": true,
|
"notificar_cambios": true,
|
||||||
"audioVoces": "Spanish_(Latin_America) espeak-ng",
|
"audioVoces": "Spanish_(Latin_America) espeak-ng",
|
||||||
"IP": "192.168.1.220",
|
"IP": "192.168.1.220",
|
||||||
"Puerto": "5000",
|
"Puerto": "5000",
|
||||||
"Ruta": "/datos"
|
"Ruta": "/datos"
|
||||||
|
|
||||||
},
|
},
|
||||||
"ConfEmail": {
|
"ConfEmail": {
|
||||||
"ActivarCorreos": false,
|
"ActivarCorreos": false,
|
||||||
"ReceptorEmail": "-",
|
"ReceptorEmail": "xxx@xx.com",
|
||||||
"Servicio": "Deshabilitado",
|
"Servicio": "Deshabilitado",
|
||||||
"Usuario": "",
|
"Usuario": "",
|
||||||
"Password": "",
|
"Password": "",
|
||||||
|
|||||||
@ -167,7 +167,11 @@ APP = module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Reconfigurar: function () {
|
Reconfigurar: function (nuevaConfiguracion) {
|
||||||
|
// Keep the shared object used by the collector and publishers up to date.
|
||||||
|
if (nuevaConfiguracion) Object.assign(configGeneralEMA, nuevaConfiguracion);
|
||||||
|
APP.Configuracion.Modo = configGeneralEMA.ConfGeneral.Modo;
|
||||||
|
APP.Configuracion.utc = configGeneralEMA.ConfGeneral.UTC;
|
||||||
APP.RegistrarLog('0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.');
|
APP.RegistrarLog('0|CORE|⚙️ RECONFIGURANDO: Se reconfigura el recolectorde EMA LIBRE.');
|
||||||
if (configGeneralEMA.ConfEmail.ActivarCorreos === true) Correo.EnviarEmail(configGeneralEMA,'EMA - CONFIG', '<b>Estimado</b>,<br><br> Le informamos que se modificó la configuración de la estación <b>' + configGeneralEMA.ConfGeneral.NombreEstacion + '</b>.<br><br>Saludos Cordiales.');
|
if (configGeneralEMA.ConfEmail.ActivarCorreos === true) Correo.EnviarEmail(configGeneralEMA,'EMA - CONFIG', '<b>Estimado</b>,<br><br> Le informamos que se modificó la configuración de la estación <b>' + configGeneralEMA.ConfGeneral.NombreEstacion + '</b>.<br><br>Saludos Cordiales.');
|
||||||
APP.Configuracion.TiempoContinuo = configGeneralEMA.ConfGeneral.Tiempo_de_Recoleccion;
|
APP.Configuracion.TiempoContinuo = configGeneralEMA.ConfGeneral.Tiempo_de_Recoleccion;
|
||||||
|
|||||||
@ -930,8 +930,8 @@ function VerHistoricoT(lugar)
|
|||||||
|
|
||||||
async function GuardarConf(origen) {
|
async function GuardarConf(origen) {
|
||||||
const persist = async draft => {
|
const persist = async draft => {
|
||||||
await require('../core/configuracion-store.js').guardarConfiguracion(draft, undefined, origen === 'conf');
|
await require('../core/configuracion-store.js').guardarConfiguracion(draft);
|
||||||
EMA.Reconfigurar();
|
try { EMA.Reconfigurar(draft); } catch (error) { console.warn('Configuración guardada; reiniciá el recolector para aplicarla.'); }
|
||||||
configGeneralEMA = draft;
|
configGeneralEMA = draft;
|
||||||
};
|
};
|
||||||
if (origen === 'conf') return ConfigUI.save(persist);
|
if (origen === 'conf') return ConfigUI.save(persist);
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
// Requiere el módulo NWJS
|
// Requiere el módulo NWJS
|
||||||
const gui = require('nw.gui');
|
const gui = require('nw.gui');
|
||||||
const runtime = require('../../runtime.json');
|
const runtime = require(require('path').join(gui.App.startPath, 'runtime.json'));
|
||||||
if (process.versions.nw !== runtime.nw) {
|
if (process.versions.nw !== runtime.nw) {
|
||||||
window.alert('Esta versión requiere NW.js ' + runtime.nw + '. Iniciá con npm run desktop.');
|
window.alert('Esta versión requiere NW.js ' + runtime.nw + '. Iniciá con npm run desktop.');
|
||||||
gui.App.quit();
|
gui.App.quit();
|
||||||
|
|||||||
@ -54,6 +54,7 @@ if (_IS_NW) {
|
|||||||
gui = require('nw.gui'); // Libreria Grafica
|
gui = require('nw.gui'); // Libreria Grafica
|
||||||
os = require('os'); // Libreria del sistema operativo
|
os = require('os'); // Libreria del sistema operativo
|
||||||
win = gui.Window.get(); // ventana actual
|
win = gui.Window.get(); // ventana actual
|
||||||
|
win.on('close', function () { if (ConfigUI.canLeave()) this.close(true); });
|
||||||
// **********************Iconizado / Menu Superior *********************************************************
|
// **********************Iconizado / Menu Superior *********************************************************
|
||||||
gui.App.clearCache(); // Se limpia cache
|
gui.App.clearCache(); // Se limpia cache
|
||||||
win = gui.Window.get(); // Se obtiene la ventana actual
|
win = gui.Window.get(); // Se obtiene la ventana actual
|
||||||
@ -94,6 +95,7 @@ if (_IS_NW) {
|
|||||||
menu.append(new nw.MenuItem({
|
menu.append(new nw.MenuItem({
|
||||||
label: 'Salir',
|
label: 'Salir',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
if (!ConfigUI.canLeave()) return;
|
||||||
if (typeof EMA !== 'undefined' && typeof EMA.Salir === 'function') EMA.Salir();
|
if (typeof EMA !== 'undefined' && typeof EMA.Salir === 'function') EMA.Salir();
|
||||||
nw.App.closeAllWindows();
|
nw.App.closeAllWindows();
|
||||||
nw.App.quit();
|
nw.App.quit();
|
||||||
|
|||||||
@ -555,3 +555,17 @@ table.dataTable.table-bordered {
|
|||||||
#principal > .container > .row > [class*="col-"] { width: 100%; }
|
#principal > .container > .row > [class*="col-"] { width: 100%; }
|
||||||
.pre-scrollable-m { max-height: none; overflow-y: visible; }
|
.pre-scrollable-m { max-height: none; overflow-y: visible; }
|
||||||
}
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.notifyjs-container {
|
||||||
|
position: fixed !important;
|
||||||
|
inset: auto .75rem .75rem !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: calc(100vw - 1.5rem);
|
||||||
|
}
|
||||||
|
.notifyjs-container > div {
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.notifyjs-arrow { display: none !important; }
|
||||||
|
}
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
};
|
};
|
||||||
function read(config) {
|
function read(config) {
|
||||||
return Object.fromEntries(Object.entries(fields).map(([id, [section, key, type]]) => {
|
return Object.fromEntries(Object.entries(fields).map(([id, [section, key, type]]) => {
|
||||||
const value = config[section]?.[key];
|
let value = config[section]?.[key];
|
||||||
|
if (id === 'Modo' && value === 'Recolector') value = 'Recolector_USB';
|
||||||
return [id, type === 'boolean' ? (value ? 'Activado' : 'Desactivado') : String(value ?? '')];
|
return [id, type === 'boolean' ? (value ? 'Activado' : 'Desactivado') : String(value ?? '')];
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,7 +93,7 @@ const ConfigUI = (() => {
|
|||||||
visibility();
|
visibility();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener('click', event => {
|
window.addEventListener('click', event => {
|
||||||
if (event.target.closest('[data-bs-toggle="tab"], #Wclo') && !canLeave()) { event.preventDefault(); event.stopImmediatePropagation(); }
|
if (event.target.closest('[data-bs-toggle="tab"], #Wclo') && !canLeave()) { event.preventDefault(); event.stopImmediatePropagation(); }
|
||||||
}, true);
|
}, true);
|
||||||
window.addEventListener('beforeunload', event => { if (dirty() || saving) { event.preventDefault(); event.returnValue = ''; } });
|
window.addEventListener('beforeunload', event => { if (dirty() || saving) { event.preventDefault(); event.returnValue = ''; } });
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
const EstadoUI = (() => {
|
const EstadoUI = (() => {
|
||||||
let timer, started = false;
|
let timer, started = false;
|
||||||
function date(value) {
|
function date(value, utc) {
|
||||||
const match = String(value || '').match(/^(\d{2})\/(\d{2})\/(\d{4})[ T](\d{2}):(\d{2}):(\d{2})/);
|
const match = String(value || '').match(/^(\d{2})\/(\d{2})\/(\d{4})[ T](\d{2}):(\d{2}):(\d{2})/);
|
||||||
return match ? new Date(+match[3], +match[2] - 1, +match[1], +match[4], +match[5], +match[6]).getTime() : Date.parse(value);
|
if (!match) return Date.parse(value);
|
||||||
|
if (Number.isFinite(Number(utc))) return Date.UTC(+match[3], +match[2] - 1, +match[1], +match[4], +match[5], +match[6]) - Number(utc) * 3600000;
|
||||||
|
return new Date(+match[3], +match[2] - 1, +match[1], +match[4], +match[5], +match[6]).getTime();
|
||||||
}
|
}
|
||||||
function render(data) {
|
function render(data) {
|
||||||
const target = document.getElementById('estado-estacion');
|
const target = document.getElementById('estado-estacion');
|
||||||
@ -29,11 +31,11 @@ const EstadoUI = (() => {
|
|||||||
reason || (sensorFault ? 'La estación informa un problema de sensores. Revisá alimentación y señal.' : visor ? 'La fecha de lectura indica la vigencia del dato recibido.' : running ? 'El proceso está activo; revisá la última lectura para confirmar recepción.' : 'Iniciá el recolector desde Inicio o revisá la configuración.'),
|
reason || (sensorFault ? 'La estación informa un problema de sensores. Revisá alimentación y señal.' : visor ? 'La fecha de lectura indica la vigencia del dato recibido.' : running ? 'El proceso está activo; revisá la última lectura para confirmar recepción.' : 'Iniciá el recolector desde Inicio o revisá la configuración.'),
|
||||||
reason || sensorFault ? 'warning' : running ? 'success' : 'secondary', reason || !running ? configAction : null);
|
reason || sensorFault ? 'warning' : running ? 'success' : 'secondary', reason || !running ? configAction : null);
|
||||||
const record = data.record;
|
const record = data.record;
|
||||||
const age = Date.now() - date(record?.fecha);
|
const age = Date.now() - date(record?.fecha, data.utc);
|
||||||
const stale = Number.isFinite(age) && age > Math.max(120, Number(data.interval || 60) * 3) * 1000;
|
const stale = Number.isFinite(age) && age > Math.max(120, Number(data.interval || 60) * 3) * 1000;
|
||||||
card('Última lectura', record?.fecha || 'Sin lecturas', stale ? 'La lectura no es reciente. Revisá conexión, sensores y horario de la estación.' : record?.fecha ? 'Último registro recibido.' : 'Esperando el primer registro.', stale ? 'warning' : record?.fecha ? 'success' : 'secondary');
|
card('Última lectura', record?.fecha || 'Sin lecturas', stale ? 'La lectura no es reciente. Revisá conexión, sensores y horario de la estación.' : record?.fecha ? 'Último registro recibido.' : 'Esperando el primer registro.', stale ? 'warning' : record?.fecha ? 'success' : 'secondary');
|
||||||
const publications = Object.values(data.publications || {}).filter(p => p && p.fecha);
|
const publications = Object.values(data.publications || {}).filter(p => p && p.fecha);
|
||||||
publications.sort((a,b) => (date(b.fecha) || 0) - (date(a.fecha) || 0));
|
publications.sort((a,b) => (date(b.fecha, data.utc) || 0) - (date(a.fecha, data.utc) || 0));
|
||||||
const last = publications[0];
|
const last = publications[0];
|
||||||
const failed = publications.find(p => p.estado !== true && !['Esperar', 'No enviado'].includes(p.estado));
|
const failed = publications.find(p => p.estado !== true && !['Esperar', 'No enviado'].includes(p.estado));
|
||||||
card('Última publicación', last ? `${last.red}: ${last.fecha}` : visor ? 'No corresponde al modo visor' : 'Sin publicaciones',
|
card('Última publicación', last ? `${last.red}: ${last.fecha}` : visor ? 'No corresponde al modo visor' : 'Sin publicaciones',
|
||||||
@ -50,7 +52,7 @@ const EstadoUI = (() => {
|
|||||||
if (local) { state = EMA.Estado(); record = EMA.UltimoRegistro(); publications = EMA.UltimoPublicado(); sensors = EMA.EstadoSensores(); }
|
if (local) { state = EMA.Estado(); record = EMA.UltimoRegistro(); publications = EMA.UltimoPublicado(); sensors = EMA.EstadoSensores(); }
|
||||||
else [state, record, publications, sensors] = await Promise.all(['/EMA_Estado','/UltimoRegistro','/UltimoPublicado','/EstadoSensores'].map(url => $.ajax({url, timeout:5000})));
|
else [state, record, publications, sensors] = await Promise.all(['/EMA_Estado','/UltimoRegistro','/UltimoPublicado','/EstadoSensores'].map(url => $.ajax({url, timeout:5000})));
|
||||||
if (config.ConfGeneral.Modo === 'Visor' && typeof UltimoReg !== 'undefined') record = UltimoReg;
|
if (config.ConfGeneral.Modo === 'Visor' && typeof UltimoReg !== 'undefined') record = UltimoReg;
|
||||||
render({mode:config.ConfGeneral.Modo, interval:config.ConfGeneral.Tiempo_de_Recoleccion, state, record, publications, sensors});
|
render({mode:config.ConfGeneral.Modo, interval:config.ConfGeneral.Tiempo_de_Recoleccion, utc:config.ConfGeneral.UTC, state, record, publications, sensors});
|
||||||
} catch (error) {render({error:'No se pudo consultar el estado. Comprobá que el servicio esté disponible.'});}
|
} catch (error) {render({error:'No se pudo consultar el estado. Comprobá que el servicio esté disponible.'});}
|
||||||
if (started) timer = setTimeout(refresh, 10000);
|
if (started) timer = setTimeout(refresh, 10000);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div class="col-centered">
|
<div class="col-centered">
|
||||||
<h1>Configuración General</h1>
|
<h1>Configuración General</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 text-end" style="vertical-align:top;"><button class="btn btn-sm btn-success" onclick="MenuGUI('logs');" role="button">Ver Logs</button> </div>
|
<div class="col-12 text-end" style="vertical-align:top;"><button type="button" class="btn btn-sm btn-success" onclick="MenuGUI('logs');" role="button">Ver Logs</button> </div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6 col-lg-4 col-centered">
|
<div class="col-12 col-md-6 col-lg-4 col-centered">
|
||||||
|
|||||||
18
src/gui_web/configuracion-api.js
Normal file
18
src/gui_web/configuracion-api.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Injectable storage keeps the real HTTP contract testable without station hardware.
|
||||||
|
function registrarConfiguracion(app, {guardar, reconfigurar}) {
|
||||||
|
app.post('/GuardarConf', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await guardar(req.body?.configuracion);
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(error.fields ? 400 : 500).json({ok: false, error: error.fields ? error.message : 'No se pudo guardar la configuración.', fields: error.fields});
|
||||||
|
}
|
||||||
|
// Persistence succeeded even if the running collector cannot reload immediately.
|
||||||
|
try {
|
||||||
|
await reconfigurar(req.body.configuracion);
|
||||||
|
res.json({ok: true});
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ok: true, warning: 'La configuración se guardó. Reiniciá el recolector para aplicarla.'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = {registrarConfiguracion};
|
||||||
@ -920,8 +920,9 @@ function VerHistoricoT(lugar)
|
|||||||
async function GuardarConf(origen) {
|
async function GuardarConf(origen) {
|
||||||
const persist = async draft => {
|
const persist = async draft => {
|
||||||
try {
|
try {
|
||||||
const result = await $.ajax({url: '/GuardarConf', method: 'POST', contentType: 'application/json', data: JSON.stringify({configuracion: draft, validar: origen === 'conf'})});
|
const result = await $.ajax({url: '/GuardarConf', method: 'POST', contentType: 'application/json', data: JSON.stringify({configuracion: draft})});
|
||||||
if (!result.ok) throw new Error('No se pudo guardar la configuración.');
|
if (!result.ok) throw new Error('No se pudo guardar la configuración.');
|
||||||
|
if (result.warning) HABLA(0, result.warning, 'warn');
|
||||||
} catch (request) {
|
} catch (request) {
|
||||||
const error = new Error(request.responseJSON?.error || request.message || 'No se pudo conectar con el servidor. Reintentá el guardado.');
|
const error = new Error(request.responseJSON?.error || request.message || 'No se pudo conectar con el servidor. Reintentá el guardado.');
|
||||||
error.fields = request.responseJSON?.fields;
|
error.fields = request.responseJSON?.fields;
|
||||||
|
|||||||
@ -157,14 +157,9 @@ app.get('/purgarPendiente', function (req, res)
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.post('/GuardarConf', async function (req, res) {
|
require('./configuracion-api.js').registrarConfiguracion(app, {
|
||||||
try {
|
guardar: require('../core/configuracion-store.js').guardarConfiguracion,
|
||||||
await require('../core/configuracion-store.js').guardarConfiguracion(req.body?.configuracion, undefined, req.body?.validar !== false);
|
reconfigurar: config => EMA.Reconfigurar(config)
|
||||||
EMA.Reconfigurar();
|
|
||||||
res.json({ok: true});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(error.fields ? 400 : 500).json({ok: false, error: error.fields ? error.message : 'No se pudo guardar la configuración.', fields: error.fields});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/VerHistorico', function (req, res)
|
app.get('/VerHistorico', function (req, res)
|
||||||
|
|||||||
26824
src/logs/debug.log
26824
src/logs/debug.log
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,8 @@
|
|||||||
"postARM": "node tools/nw.cjs rebuild-arm",
|
"postARM": "node tools/nw.cjs rebuild-arm",
|
||||||
"desktop:dev": "node tools/nw.cjs dev",
|
"desktop:dev": "node tools/nw.cjs dev",
|
||||||
"test:ui": "node ../test/ui-smoke.cjs",
|
"test:ui": "node ../test/ui-smoke.cjs",
|
||||||
"test:config": "node --test ../test/configuracion.test.cjs"
|
"test:config": "node --test ../test/configuracion.test.cjs",
|
||||||
|
"test:startup": "node ../test/ui-startup.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
|||||||
84
test/configuracion.test.cjs
Normal file
84
test/configuracion.test.cjs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
const {test} = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs/promises');
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const model = require('../src/gui_common/js/configuracion-modelo.js');
|
||||||
|
const {guardarConfiguracion} = require('../src/core/configuracion-store.js');
|
||||||
|
const {registrarConfiguracion} = require('../src/gui_web/configuracion-api.js');
|
||||||
|
const express = require('../src/node_modules/express');
|
||||||
|
const fixture = require('./fixtures/configuracion.cjs');
|
||||||
|
|
||||||
|
test('el formulario conserva propiedades ajenas y no modifica el objeto original', () => {
|
||||||
|
const old = fixture(), fields = model.read(old);
|
||||||
|
fields.NombreEstacion = 'Nuevo'; fields.inicio_auto = 'Activado';
|
||||||
|
const next = model.build(old, fields);
|
||||||
|
assert.equal(old.ConfGeneral.NombreEstacion, 'Estación prueba');
|
||||||
|
assert.equal(next.ConfGeneral.inicio_automatico, true);
|
||||||
|
assert.deepEqual(next.Redes, old.Redes);
|
||||||
|
assert.equal(next.ConfGeneral.conservar, old.ConfGeneral.conservar);
|
||||||
|
assert.deepEqual(model.validate(fields), {});
|
||||||
|
});
|
||||||
|
test('validación condicional de visor, IP y SMTP', () => {
|
||||||
|
const fields = model.read(fixture());
|
||||||
|
fields.Modo = 'Visor'; assert(model.validate(fields).Llave_R);
|
||||||
|
fields.Llave_R = 'llave'; assert.deepEqual(model.validate(fields), {});
|
||||||
|
fields.Modo = 'Recolector_IP'; fields.r_puerto = '65536'; fields.r_path = 'datos';
|
||||||
|
assert(model.validate(fields).r_puerto); assert(model.validate(fields).r_path);
|
||||||
|
fields.r_puerto = '65535'; fields.r_path = '/datos'; assert.deepEqual(model.validate(fields), {});
|
||||||
|
fields.Tipo_SMTP = 'Manual'; fields.Usuario_SMTP = 'invalido';
|
||||||
|
const errors = model.validate(fields);
|
||||||
|
assert(errors.Usuario_SMTP && errors.Contrasenia_SMTP && errors.Server_SMTP && errors.Email_receptor);
|
||||||
|
});
|
||||||
|
test('guardar y volver a leer, rechazo sin alterar el archivo y limpieza de temporales', async t => {
|
||||||
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'carpincho-config-'));
|
||||||
|
t.after(() => fs.rm(dir, {recursive: true, force: true}));
|
||||||
|
const file = path.join(dir, 'ConfiguracionGeneral.js');
|
||||||
|
const config = fixture(); config.ConfGeneral.NombreEstacion = 'Prueba " especial';
|
||||||
|
await guardarConfiguracion(config, file);
|
||||||
|
const previous = await fs.readFile(file, 'utf8');
|
||||||
|
const sandbox = {module: {exports: {}}}; vm.runInNewContext(previous, sandbox);
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(sandbox.module.exports)), config);
|
||||||
|
config.ConfGeneral.debugTamanio = -1;
|
||||||
|
await assert.rejects(guardarConfiguracion(config, file));
|
||||||
|
assert.equal(await fs.readFile(file, 'utf8'), previous);
|
||||||
|
await assert.rejects(guardarConfiguracion(fixture(), dir));
|
||||||
|
assert.deepEqual(await fs.readdir(dir), ['ConfiguracionGeneral.js']);
|
||||||
|
});
|
||||||
|
test('HTTP espera la escritura, informa rechazos y permite reintentar', async t => {
|
||||||
|
const app = express(); app.use(express.json());
|
||||||
|
let release, fail = false, reload = 0;
|
||||||
|
registrarConfiguracion(app, {guardar: async config => {
|
||||||
|
assert(config.ConfGeneral);
|
||||||
|
if (fail) throw new Error('disco no disponible');
|
||||||
|
await new Promise(resolve => {release = resolve;});
|
||||||
|
}, reconfigurar: () => {reload++;}});
|
||||||
|
const server = await new Promise(resolve => {const s = app.listen(0,'127.0.0.1',()=>resolve(s));});
|
||||||
|
t.after(() => server.close());
|
||||||
|
const url = `http://127.0.0.1:${server.address().port}/GuardarConf`;
|
||||||
|
const request = () => fetch(url,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({configuracion:fixture()})});
|
||||||
|
let replied = false;
|
||||||
|
const pending = request().then(response => {replied = true; return response.json();});
|
||||||
|
while (!release) await new Promise(resolve => setTimeout(resolve, 5));
|
||||||
|
assert.equal(replied, false); assert.equal(reload, 0);
|
||||||
|
release(); assert.deepEqual(await pending,{ok:true}); assert.equal(reload,1);
|
||||||
|
fail = true; const failed = await request(); assert.equal(failed.status,500); assert.equal((await failed.json()).ok,false);
|
||||||
|
fail = false; release = null; const retry = request();
|
||||||
|
while (!release) await new Promise(resolve => setTimeout(resolve,5));
|
||||||
|
release(); assert.equal((await (await retry).json()).ok,true);
|
||||||
|
});
|
||||||
|
test('la reconfiguración actualiza el objeto compartido y los parámetros del recolector', async () => {
|
||||||
|
const source = await fs.readFile(path.join(__dirname,'../src/core/ema.js'),'utf8');
|
||||||
|
const start = source.indexOf(' Reconfigurar: function');
|
||||||
|
const end = source.indexOf(' Iniciar: function',start);
|
||||||
|
const current = fixture();
|
||||||
|
const context = {configGeneralEMA:current, APP:{Configuracion:{},RegistrarLog() {}},recolector:{conf:{}},Correo:{EnviarEmail(){throw new Error('Unexpected email');}}};
|
||||||
|
vm.runInNewContext('handler = ({'+source.slice(start,end)+'}).Reconfigurar;',context);
|
||||||
|
const next = fixture(); next.ConfGeneral.Tiempo_de_Recoleccion=30;next.ConfGeneral.UTC=2;next.ConfGeneral.Modo='Recolector_IP';
|
||||||
|
context.handler(next);
|
||||||
|
assert.equal(current.ConfGeneral.Tiempo_de_Recoleccion,30);
|
||||||
|
assert.equal(context.APP.Configuracion.TiempoContinuo,30);
|
||||||
|
assert.equal(context.APP.Configuracion.Modo,'Recolector_IP');
|
||||||
|
assert.equal(context.APP.Configuracion.utc,2);
|
||||||
|
});
|
||||||
9
test/fixtures/configuracion.cjs
vendored
Normal file
9
test/fixtures/configuracion.cjs
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = () => ({
|
||||||
|
ConfGeneral: {
|
||||||
|
NombreEstacion: 'Estación prueba', Modo: 'Recolector_USB', Tiempo_de_Recoleccion: 60,
|
||||||
|
debug: 1, debugTamanio: 20, UTC: -3, SistemaMetrico: 'SI', IP: '127.0.0.1', Puerto: '5000', Ruta: '/datos', Llave_R: '',
|
||||||
|
inicio_automatico: false, notificar_cambios: false, Audio: false, audioVoces: '', conservar: 'propiedad ajena al formulario'
|
||||||
|
},
|
||||||
|
ConfEmail: {Servicio: 'Deshabilitado', ActivarCorreos: false, Usuario: '', Password: '', Server: '', Puerto: '587', ReceptorEmail: ''},
|
||||||
|
Redes: [{Nombre: 'Prueba', Publicar: false}]
|
||||||
|
});
|
||||||
@ -13,6 +13,10 @@ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'carpincho-ui-'));
|
|||||||
const read = name => fs.readFileSync(path.join(src, name), 'utf8');
|
const read = name => fs.readFileSync(path.join(src, name), 'utf8');
|
||||||
const clean = html => html.replace(/<!--[\s\S]*?-->/g, '');
|
const clean = html => html.replace(/<!--[\s\S]*?-->/g, '');
|
||||||
const app = express();
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
const testConfigFile = path.join(temp, 'web-config.js');
|
||||||
|
const {guardarConfiguracion} = require('../src/core/configuracion-store.js');
|
||||||
|
require('../src/gui_web/configuracion-api.js').registrarConfiguracion(app, {guardar: config => guardarConfiguracion(config, testConfigFile), reconfigurar() {}});
|
||||||
// Read actual mounts so a stale web route also fails this test.
|
// 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)];
|
const routes = [...read('gui_web/express.js').matchAll(/app.use\('([^']+)', express.static\(__dirname \+ '([^']+)'\)\)/g)];
|
||||||
app.use(express.static(path.join(src, 'gui_web')));
|
app.use(express.static(path.join(src, 'gui_web')));
|
||||||
@ -33,39 +37,59 @@ async function probe() {
|
|||||||
}
|
}
|
||||||
tests.push('all navigation tabs');
|
tests.push('all navigation tabs');
|
||||||
$('#settings').html(window.settingsHTML);
|
$('#settings').html(window.settingsHTML);
|
||||||
// Repeated updates must keep one label and one menu item per option.
|
const fixture = window.configFixture;
|
||||||
$('.selectpicker').selectpicker();
|
const interfaces = {eth0: [{family:'IPv4',internal:false,address:'127.0.0.1'}]};
|
||||||
await wait(200);
|
for (let i = 0; i < 3; i++) {
|
||||||
for (const select of document.querySelectorAll('#settings select.selectpicker')) {
|
ConfigUI.mount(fixture, interfaces, [{name:'Voz de prueba'}]);
|
||||||
const $select = $(select);
|
check(!ConfigUI.dirty(), 'Mount marked the form dirty');
|
||||||
$select.append(new Option('Alternativa', '__alternative'), new Option('Prueba dinámica', '__smoke'));
|
check(document.querySelectorAll('#r_ip option').length === 1, 'Duplicated IP options');
|
||||||
actualizarSelects($select);
|
check(document.querySelectorAll('#vocesAudio option').length === 1, 'Duplicated voice options');
|
||||||
$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');
|
check(!document.querySelector('.bootstrap-select'), 'Plugin is still used');
|
||||||
|
check(document.getElementById('inicio_auto').selectedOptions[0].textContent === 'Desactivado', 'Duplicated selected text');
|
||||||
|
const set = (id, value) => {const e=document.getElementById(id); e.value=value; e.dispatchEvent(new Event('change',{bubbles:true}));};
|
||||||
|
set('Modo','Visor');
|
||||||
|
check(!document.getElementById('LlaveR').hidden && document.getElementById('bloque_por_IP_WIFI').hidden, 'Visor visibility failed');
|
||||||
|
let writes = 0;
|
||||||
|
check(await ConfigUI.save(async () => {writes++;}) === false && writes === 0, 'Invalid visor config saved');
|
||||||
|
check(document.getElementById('Llave_R').getAttribute('aria-invalid') === 'true', 'No field validation message');
|
||||||
|
set('Llave_R','llave-prueba'); set('Modo','Recolector_IP');
|
||||||
|
check(!document.getElementById('bloque_por_IP_WIFI').hidden && document.getElementById('LlaveR').hidden, 'IP visibility failed');
|
||||||
|
set('r_puerto','65536');
|
||||||
|
check(await ConfigUI.save(async () => {writes++;}) === false && writes === 0, 'Invalid port saved');
|
||||||
|
set('r_puerto','5000'); set('Modo','Recolector_USB'); set('NombreEstacion','Nombre nuevo');
|
||||||
|
const confirm = window.confirm;
|
||||||
|
window.confirm = () => false;
|
||||||
|
check(!ConfigUI.canLeave(), 'Unsaved changes did not block navigation');
|
||||||
|
const active = document.querySelector('.nav-link.active');
|
||||||
|
document.getElementById('inicio-tab').click();
|
||||||
|
check(document.querySelector('.nav-link.active') === active, 'Tab changed despite cancel');
|
||||||
|
check(await ConfigUI.save(async () => {throw new Error('Fallo simulado');}) === false, 'Save failure not reported');
|
||||||
|
check(ConfigUI.dirty() && document.getElementById('config-status').textContent.includes('Fallo simulado'), 'Failed save lost dirty state');
|
||||||
|
window.confirm = confirm;
|
||||||
|
const saved = await ConfigUI.save(window.persistConfiguration);
|
||||||
|
check(saved && !ConfigUI.dirty(), 'Successful save did not clear dirty state');
|
||||||
|
const loaded = window.readConfiguration();
|
||||||
|
check(loaded.ConfGeneral.NombreEstacion === 'Nombre nuevo', 'Saved config was not persisted');
|
||||||
|
check(loaded.ConfGeneral.Llave_R === 'llave-prueba', 'Mode switch discarded entered values');
|
||||||
|
ConfigUI.mount(loaded, interfaces, [{name:'Voz de prueba'}]);
|
||||||
|
check(document.getElementById('NombreEstacion').value === 'Nombre nuevo', 'Reload lost saved value');
|
||||||
|
set('NombreEstacion','Cambio descartado'); window.confirm = () => true;
|
||||||
|
check(ConfigUI.canLeave(), 'Discard did not permit navigation'); window.confirm = confirm;
|
||||||
|
ConfigUI.mount(loaded, interfaces, []);
|
||||||
|
tests.push('native selects, repeated mount, mode changes, validation, unsaved changes, failed save, disk save and reload');
|
||||||
|
EstadoUI.render({state:[false,'Sin conexión USB'], mode:'Recolector_USB',record:false,publications:[]});
|
||||||
|
check(document.getElementById('estado-estacion').textContent.includes('Sin conexión USB'), 'Missing connection reason');
|
||||||
|
EstadoUI.render({state:[true,''],record:{fecha:'01/01/2000 00:00:00'},publications:[{red:'Prueba',fecha:'01/01/2000 00:00:00',estado:false,error:'Llave inválida'}]});
|
||||||
|
check(document.getElementById('estado-estacion').textContent.includes('no es reciente'), 'Stale reading not flagged');
|
||||||
|
check(document.getElementById('estado-estacion').textContent.includes('Llave inválida'), 'Publication failure missing');
|
||||||
|
tests.push('separate connection, stale reading and publication error');
|
||||||
$('#tip').tooltip('show');
|
$('#tip').tooltip('show');
|
||||||
check(!!document.querySelector('.tooltip'), 'Tooltip did not open');
|
check(!!document.querySelector('.tooltip'), 'Tooltip did not open');
|
||||||
$('#tip').tooltip('dispose');
|
$('#tip').tooltip('dispose');
|
||||||
tests.push('tooltip show/dispose');
|
tests.push('tooltip show/dispose');
|
||||||
const table = await new Promise(resolve => {
|
const table = await new Promise(resolve => {
|
||||||
$('#table').DataTable({dom: 'Bfrtip', buttons: ['copy', 'csv'], pageLength: 1,
|
$('#table').DataTable({...TablaUI.options(), pageLength: 1,
|
||||||
language: {url: window.languageURL}, initComplete() { resolve(this.api()); }});
|
language: {url: window.languageURL}, initComplete() { resolve(this.api()); }});
|
||||||
});
|
});
|
||||||
check(table.rows().count() === 2, 'Missing table rows');
|
check(table.rows().count() === 2, 'Missing table rows');
|
||||||
@ -79,6 +103,7 @@ async function probe() {
|
|||||||
tests.push('table initialization, filtering, export data, buttons, translation, destroy');
|
tests.push('table initialization, filtering, export data, buttons, translation, destroy');
|
||||||
check(window.failures.length === 0, window.failures.join('\n'));
|
check(window.failures.length === 0, window.failures.join('\n'));
|
||||||
} catch (error) { window.failures.push(error.stack); }
|
} catch (error) { window.failures.push(error.stack); }
|
||||||
|
document.getElementById('settings').replaceChildren();
|
||||||
require('fs').writeFileSync(window.resultFile, JSON.stringify({versions: process.versions, tests, failures: window.failures}, null, 2));
|
require('fs').writeFileSync(window.resultFile, JSON.stringify({versions: process.versions, tests, failures: window.failures}, null, 2));
|
||||||
nw.App.quit();
|
nw.App.quit();
|
||||||
}
|
}
|
||||||
@ -113,18 +138,27 @@ async function run(mode, origin) {
|
|||||||
return `<div class="tab-pane ${id === 'inicio' ? 'active show' : ''}" id="${id}"></div>`;
|
return `<div class="tab-pane ${id === 'inicio' ? 'active show' : ''}" id="${id}"></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
const resultFile = path.join(folder, 'result.json');
|
const resultFile = path.join(folder, 'result.json');
|
||||||
const settings = read(mode + '/view/modelo_configuracion.html').replace(/<script\b[\s\S]*?<\/script>/g, '').replace(/\son\w+="[^"]*"/g, '');
|
const settings = read('gui_common/view/modelo_configuracion.html').replace(/<script\b[\s\S]*?<\/script>/g, '').replace(/\son\w+="[^"]*"/g, '');
|
||||||
const language = new URL(mode === 'gui' ? '../gui_common/css/Spanish.json' : 'css/Spanish.json', base).href;
|
const language = new URL(mode === 'gui' ? '../gui_common/css/Spanish.json' : 'css/Spanish.json', base).href;
|
||||||
|
const configFile = mode === 'gui' ? path.join(folder,'config.js') : testConfigFile;
|
||||||
|
const fixture = require('./fixtures/configuracion.cjs')();
|
||||||
fs.writeFileSync(path.join(folder, 'index.html'), `<!doctype html><meta charset="utf-8"><base href="${base}">
|
fs.writeFileSync(path.join(folder, 'index.html'), `<!doctype html><meta charset="utf-8"><base href="${base}">
|
||||||
<script>window.failures=[];addEventListener('error',e=>failures.push(e.message||'Resource: '+e.target.src),true);addEventListener('unhandledrejection',e=>failures.push(String(e.reason)));</script>
|
<script>window.failures=[];addEventListener('error',e=>failures.push(e.message||'Resource: '+e.target.src),true);addEventListener('unhandledrejection',e=>failures.push(String(e.reason)));</script>
|
||||||
${head}<body class="dia"><ul class="nav nav-tabs">${tabs.join('')}</ul>${panes}<div id="settings"></div>
|
${head}<body class="dia"><ul class="nav nav-tabs">${tabs.join('')}</ul>${panes}<section id="estado-estacion" class="estado-estacion"></section><div id="settings"></div>
|
||||||
<button id="tip" title="Prueba" class="btn btn-primary">Tooltip</button>
|
<button id="tip" title="Prueba" class="btn btn-primary">Tooltip</button>
|
||||||
<table id="table" class="table"><thead><tr><th>Nombre</th></tr></thead><tbody><tr><td>Alpha</td></tr><tr><td>Beta</td></tr></tbody></table>
|
<table id="table" class="table table-bordered"><thead><tr><th>Nombre</th></tr></thead><tbody><tr><td>Alpha</td></tr><tr><td>Beta</td></tr></tbody></table>
|
||||||
<script>window.settingsHTML=${JSON.stringify(settings)};window.languageURL=${JSON.stringify(language)};window.resultFile=${JSON.stringify(resultFile)};addEventListener('load',${probe.toString()});</script>`);
|
<script>
|
||||||
|
window.configFixture=${JSON.stringify(fixture)};
|
||||||
|
window.persistConfiguration=async draft => {
|
||||||
|
if (${JSON.stringify(mode)} === 'gui') await require(${JSON.stringify(path.join(src,'core/configuracion-store.js'))}).guardarConfiguracion(draft, ${JSON.stringify(configFile)});
|
||||||
|
else {const response=await $.ajax({url:'/GuardarConf',method:'POST',contentType:'application/json',data:JSON.stringify({configuracion:draft})});if(!response.ok)throw new Error('Save failed');}
|
||||||
|
};
|
||||||
|
window.readConfiguration=()=>{const sandbox={module:{exports:{}}};require('vm').runInNewContext(require('fs').readFileSync(${JSON.stringify(configFile)},'utf8'),sandbox);return sandbox.module.exports;};
|
||||||
|
window.settingsHTML=${JSON.stringify(settings)};window.languageURL=${JSON.stringify(language)};window.resultFile=${JSON.stringify(resultFile)};addEventListener('load',${probe.toString()});</script>`);
|
||||||
const args = JSON.parse(read('package.json'))['chromium-args'].replace('--disable-logging', '');
|
const args = JSON.parse(read('package.json'))['chromium-args'].replace('--disable-logging', '');
|
||||||
fs.writeFileSync(path.join(folder, 'package.json'), JSON.stringify({name:'carpincho-ui-test',main:'index.html','chromium-args':args,window:{show:false,width:1200,height:900}}));
|
fs.writeFileSync(path.join(folder, 'package.json'), JSON.stringify({name:'carpincho-ui-test',main:'index.html','chromium-args':args,window:{show:false,width:1200,height:900}}));
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const child = spawn(process.argv[2] || 'nw', [folder, '--user-data-dir=' + path.join(folder, 'profile')], {stdio: ['ignore', 'ignore', 'pipe']});
|
const child = spawn(process.argv[2] || process.env.NW_BINARY || 'nw', [folder, '--user-data-dir=' + path.join(folder, 'profile')], {stdio: ['ignore', 'ignore', 'pipe']});
|
||||||
let log = '';
|
let log = '';
|
||||||
child.stderr.on('data', data => {log += data;});
|
child.stderr.on('data', data => {log += data;});
|
||||||
const timer = setTimeout(() => {child.kill(); reject(new Error('NW.js timed out: ' + folder));}, 30000);
|
const timer = setTimeout(() => {child.kill(); reject(new Error('NW.js timed out: ' + folder));}, 30000);
|
||||||
|
|||||||
34
test/ui-startup.cjs
Normal file
34
test/ui-startup.cjs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Full desktop startup test; uses a temporary copy and disables collection/publication.
|
||||||
|
const fs=require('fs'),path=require('path'),os=require('os'),{spawn}=require('child_process');
|
||||||
|
const root=path.resolve(__dirname,'..');
|
||||||
|
const dir=fs.mkdtempSync(path.join(os.tmpdir(),'carpincho-startup-'));
|
||||||
|
fs.cpSync(path.join(root,'src'),dir,{recursive:true,filter:p=>!p.includes('/node_modules')&&!p.includes('/logs')&&!p.endsWith('/ConfiguracionGeneral.js')});
|
||||||
|
fs.symlinkSync(path.join(root,'src/node_modules'),path.join(dir,'node_modules'),'dir');
|
||||||
|
fs.mkdirSync(path.join(dir,'logs'));
|
||||||
|
for(const f of ['debug.log','TanquePendientes.log','registro_diario.log']) fs.writeFileSync(path.join(dir,'logs',f),f==='debug.log'?'':'[]');
|
||||||
|
const conf=require(path.join(root,'test/fixtures/configuracion.cjs'))();conf.ConfPublicador=[];
|
||||||
|
fs.writeFileSync(path.join(dir,'conf/ConfiguracionGeneral.js'),'module.exports='+JSON.stringify(conf));
|
||||||
|
const probe=`<script>
|
||||||
|
window.startupErrors=[];addEventListener('error',e=>startupErrors.push(e.message||e.target.src),true);addEventListener('unhandledrejection',e=>startupErrors.push(String(e.reason)));
|
||||||
|
addEventListener('load',()=>setTimeout(async()=>{
|
||||||
|
try{
|
||||||
|
await MenuGUI('configuracion');await new Promise(r=>setTimeout(r,700));
|
||||||
|
if(!document.getElementById('config-form'))throw new Error('No configuration form');
|
||||||
|
if(document.getElementById('NombreEstacion').value!=='Estación prueba')throw new Error('Configuration not initialized');
|
||||||
|
document.getElementById('NombreEstacion').value='Prueba arranque';
|
||||||
|
document.getElementById('Tiempo_de_Recoleccion').value='30';
|
||||||
|
const saved=await GuardarConf('conf');if(!saved)throw new Error('Save did not succeed');
|
||||||
|
if(EMA.Configuracion.TiempoContinuo!==30)throw new Error('Collector retained old config');
|
||||||
|
await MenuGUI('inicio');await new Promise(r=>setTimeout(r,300));
|
||||||
|
await MenuGUI('configuracion');await new Promise(r=>setTimeout(r,300));
|
||||||
|
if(document.getElementById('NombreEstacion').value!=='Prueba arranque')throw new Error('Reload failed');
|
||||||
|
}catch(e){startupErrors.push(e.stack);}
|
||||||
|
require('fs').writeFileSync(${JSON.stringify(path.join(dir,'result.json'))},JSON.stringify({errors:startupErrors,version:process.versions.nw},null,2));
|
||||||
|
window.confirm=()=>true;if(typeof ConfigUI!=='undefined')ConfigUI.canLeave();nw.App.quit();
|
||||||
|
},2000));</script>`;
|
||||||
|
const html=path.join(dir,'gui/index.html');fs.writeFileSync(html,fs.readFileSync(html,'utf8').replace('<head>','<head>'+probe));
|
||||||
|
const pkgPath=path.join(dir,'package.json');let pkg=JSON.parse(fs.readFileSync(pkgPath));pkg['chromium-args']+=' --enable-logging=stderr';fs.writeFileSync(pkgPath,JSON.stringify(pkg));
|
||||||
|
const nw=process.argv[2]||process.env.NW_BINARY||'nw';
|
||||||
|
const child=spawn(nw,[dir,'--user-data-dir='+path.join(dir,'profile')],{cwd:dir,stdio:['ignore','ignore','pipe']});let log='';child.stderr.on('data',d=>log+=d);const timer=setTimeout(()=>child.kill(),20000);
|
||||||
|
child.on('error',error=>{clearTimeout(timer);console.error(error.message);process.exitCode=1;});
|
||||||
|
child.on('exit',()=>{clearTimeout(timer);fs.writeFileSync(path.join(dir,'stderr.log'),log);console.log(dir);if(fs.existsSync(path.join(dir,'result.json'))) {const result=JSON.parse(fs.readFileSync(path.join(dir,'result.json'),'utf8'));console.log(result);if(result.errors.length)process.exitCode=1;}else {console.log(log.slice(-4500));process.exitCode=1;}});
|
||||||
Loading…
Reference in New Issue
Block a user