Files
TieMeasureFlow/src/frontend/flask_app/tests/js/test_vision_rows.js
T
2026-10-28 22:04:20 +01:00

206 lines
11 KiB
JavaScript

/**
* #114: etichette della tabella quote nel pannello visione (getter `rows`), con plain node.
*
* Run from src/frontend/flask_app: node tests/js/test_vision_rows.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const src = fs.readFileSync(
path.join(__dirname, '..', '..', 'static', 'js', 'vision_execute.js'), 'utf8',
);
// Contesto vm con window e document finti; il file definisce visionExecute() globale
const win = {};
// #120: CustomEvent finto (la pagina emette vision-recorded con window.dispatchEvent)
const ctx = vm.createContext({
window: win,
document: { addEventListener() {}, querySelector() { return null; } },
CustomEvent: class { constructor(type, init) { this.type = type; this.detail = init && init.detail; } },
});
vm.runInContext(src + ';this.visionExecute = visionExecute;', ctx);
let failures = 0;
function check(label, cond, extra) {
if (cond) console.log('ok - ' + label);
else { failures++; console.log('FAIL - ' + label + (extra ? ' :: ' + extra : '')); }
}
win.__visionCfg = {
subtasks: [
{ id: 15, marker_number: 1, description: 'Larghezza', measurement_type: null, unit: 'mm' },
{ id: 16, marker_number: 2, description: '', measurement_type: 'Diametro', unit: 'mm' },
{ id: 17, marker_number: 3, description: null, measurement_type: null },
],
};
const comp = ctx.visionExecute();
comp.result = {
measurements: [
{ subtask_id: 15, value: 1, pass_fail: 'pass' },
{ subtask_id: 16, value: 2, pass_fail: 'pass' },
{ subtask_id: 17, value: 3, pass_fail: 'fail' },
{ subtask_id: 99, value: 4, pass_fail: 'pass' },
],
};
const rows = comp.rows;
check('#114 riga 15: description', rows[0].label === '1. Larghezza', rows[0].label);
check('#114 riga 16: ripiego su measurement_type', rows[1].label === '2. Diametro', rows[1].label);
check('#114 riga 17: ripiego su #id', rows[2].label === '3. #17', rows[2].label);
check('#114 riga 99: subtask sconosciuta', rows[3].label === '#99', rows[3].label);
check('#114 unit riga 17 = mm', rows[2].unit === 'mm', rows[2].unit);
check('#114 unit riga 99 = mm', rows[3].unit === 'mm', rows[3].unit);
// #117: nominale e tolleranze portati dalla quota; fmtTol
win.__visionCfg = {
subtasks: [
{ id: 20, description: 'Largh', unit: 'mm', nominal: 12.5, ltl: 12.4, utl: 12.6, lwl: 12.45, uwl: 12.55 },
{ id: 21, description: 'Diam', unit: 'mm', nominal: 0, ltl: null, utl: 0.05 },
{ id: 22, description: 'Nuda', unit: 'mm' },
],
};
const c2 = ctx.visionExecute();
c2.result = { measurements: [
{ subtask_id: 20, value: 12.5, pass_fail: 'pass' },
{ subtask_id: 21, value: 0, pass_fail: 'pass' },
{ subtask_id: 22, value: 1, pass_fail: 'pass' },
] };
const r2 = c2.rows;
check('#117 riga 20: nominale e tolleranze',
r2[0].nominal === 12.5 && r2[0].ltl === 12.4 && r2[0].utl === 12.6 && r2[0].lwl === 12.45 && r2[0].uwl === 12.55);
check('#117 riga 21: nominale 0 resta 0, ltl null', r2[1].nominal === 0 && r2[1].ltl == null && r2[1].utl === 0.05);
check('#117 riga 22: senza tolleranze sono undefined/null', r2[2].nominal == null && r2[2].utl == null && r2[2].lwl == null);
check('#117 label invariato', r2[0].label === 'Largh', r2[0].label);
check('#117 fmtTol(null) = trattino', c2.fmtTol(null) === '\u2014', c2.fmtTol(null));
check('#117 fmtTol(undefined) = trattino', c2.fmtTol(undefined) === '\u2014');
check('#117 fmtTol(0.05) = 0.050', c2.fmtTol(0.05) === '0.050', c2.fmtTol(0.05));
check('#117 fmtTol(0) = 0.000', c2.fmtTol(0) === '0.000', c2.fmtTol(0));
// #116: righe con key/name/vision_output, etichette in % con un VisionOverlay finto, labelClass
win.__visionCfg = {
subtasks: [
{ id: 30, marker_number: 1, description: 'Larghezza', unit: 'mm', vision_output: 'larg.value' },
{ id: 31, description: '', measurement_type: 'Diametro', unit: 'mm' },
],
};
const c3 = ctx.visionExecute();
c3.result = { overlay: [{ type: 'circle', tool_id: 'larg' }], measurements: [
{ subtask_id: 30, value: 1.5, pass_fail: 'pass' },
{ subtask_id: 31, value: 2, pass_fail: 'fail' },
] };
const r3 = c3.rows;
check('#116 riga: key = subtask_id', r3[0].key === 30 && r3[1].key === 31);
check('#116 riga: name senza marker', r3[0].name === 'Larghezza' && r3[1].name === 'Diametro', r3[0].name + '/' + r3[1].name);
check('#116 riga: vision_output', r3[0].vision_output === 'larg.value' && r3[1].vision_output == null);
check('#116 label invariato (#114)', r3[0].label === '1. Larghezza', r3[0].label);
check('#116 senza VisionOverlay: nessuna etichetta', c3.viewerLabels.length === 0);
let seen = null;
ctx.VisionOverlay = {
labels(rows, overlay) { seen = { rows, overlay }; return [{ key: 30, x: 500, y: 250, text: 'Larghezza 1.500 mm', status: 'pass' }]; },
};
const ls = c3.viewerLabels;
check('#116 una etichetta', ls.length === 1 && ls[0].key === 30 && ls[0].text === 'Larghezza 1.500 mm', JSON.stringify(ls));
// #122: posizione in px logici dell'immagine (la percentuale la calcola <tmf-viewer>), niente style
check('#122 posizione in px logici', ls[0].x === 500 && ls[0].y === 250 && ls[0].style === undefined, JSON.stringify(ls[0]));
check('#116 classe da esito', ls[0].cls === 'bg-green-600 text-white', ls[0].cls);
check('#116 labels riceve rows e overlay', seen.rows.length === 2 && seen.overlay.length === 1);
c3.result = null;
check('#116 senza risultato: nessuna etichetta', c3.viewerLabels.length === 0);
check('#116 labelClass pass', c3.labelClass('pass') === 'bg-green-600 text-white');
check('#116 labelClass warning', c3.labelClass('warning') === 'bg-amber-400 text-black');
check('#116 labelClass fail', c3.labelClass('fail') === 'bg-red-600 text-white');
check('#116 labelClass altro', c3.labelClass('boh') === 'bg-gray-700 text-white' && c3.labelClass(undefined) === 'bg-gray-700 text-white');
// #122: zoom, overlay SVG e Salva sono di <tmf-viewer>: il componente non li ha piu'
check('#122 niente drawOverlay/saveImage/overlayLabels/z', c3.drawOverlay === undefined && c3.saveImage === undefined && c3.overlayLabels === undefined && c3.z === undefined);
check('#122 niente codice canvas', !/getContext|clearRect/.test(src));
// #118: quota non misurata (value null) = NaN, mai Number(null) = 0
check('#118 fmtValue(null) = NaN', c3.fmtValue(null, 'mm') === 'NaN mm', c3.fmtValue(null, 'mm'));
check('#118 fmtValue(undefined) = NaN', c3.fmtValue(undefined, 'mm') === 'NaN mm');
check('#118 fmtValue(0) = 0.000', c3.fmtValue(0, 'mm') === '0.000 mm', c3.fmtValue(0, 'mm'));
check('#118 fmtValue(1.5) = 1.500', c3.fmtValue(1.5, 'mm') === '1.500 mm');
win.__visionCfg = { subtasks: [{ id: 40, description: 'Persa', unit: 'mm', nominal: 5, ltl: 4, utl: 6 }] };
const c4 = ctx.visionExecute();
c4.result = { measurements: [{ subtask_id: 40, value: null, pass_fail: 'fail' }] };
check('#118 riga con value null: valore null, esito fail, tolleranze come prima',
c4.rows[0].value === null && c4.rows[0].status === 'fail' && c4.rows[0].nominal === 5 && c4.rows[0].utl === 6);
// #120: evento vision-recorded dopo un record riuscito, non dopo uno fallito
const events = [];
win.dispatchEvent = (e) => { events.push(e); return true; };
const listeners = {};
win.addEventListener = (t, f) => { (listeners[t] = listeners[t] || []).push(f); };
win.__visionCfg = { cameraAvailable: true, modo: 'impulsiva', subtasks: [{ id: 40, description: 'Persa', unit: 'mm' }] };
const c5 = ctx.visionExecute();
c5.lastBlob = { name: 'x' };
const okMs = [{ subtask_id: 40, value: null, pass_fail: 'fail' }, { subtask_id: 41, value: 2, pass_fail: 'pass' }];
c5.acquire = async function () { this.result = { vision_result_id: 7, measurements: okMs }; };
(async () => {
await c5.record();
const ev = events.filter((e) => e.type === 'vision-recorded');
check('#120 record riuscito: un evento vision-recorded', ev.length === 1, String(ev.length));
check('#120 evento: misure di execute', ev[0].detail.measurements === okMs);
check('#120 record riuscito: saved', c5.saved === true && c5.preview === false);
events.length = 0;
const c6 = ctx.visionExecute();
c6.lastBlob = { name: 'x' };
c6.acquire = async function () { this.error = 'boom'; this.result = null; };
await c6.record();
check('#120 record fallito (errore): nessun evento', events.length === 0 && c6.saved === false);
const c7 = ctx.visionExecute();
c7.lastBlob = { name: 'x' };
c7.acquire = async function () { this.result = { measurements: okMs }; }; // niente vision_result_id
await c7.record();
check('#120 record senza vision_result_id: nessun evento', events.length === 0 && c7.saved === false);
// senza camera il file e' salvato subito da acquire(): stesso evento
events.length = 0;
win.__visionCfg = { cameraAvailable: false, executeUrl: '/e', taskId: 1, subtasks: [] };
const c8 = ctx.visionExecute();
c8.$nextTick = (fn) => fn();
ctx.URL = { createObjectURL: () => 'blob:x', revokeObjectURL() {} };
ctx.FormData = class { append() {} };
ctx.fetch = async () => ({ ok: true, json: async () => ({ vision_result_id: 9, measurements: okMs }) });
await c8.acquire({ name: 'a.png' });
check('#120 senza camera: acquire riuscito emette vision-recorded', events.length === 1 && events[0].type === 'vision-recorded', String(events.length));
// task-result-reset: saved torna false (anche senza camera, init registra l'ascolto)
c5.init();
const h = (listeners['task-result-reset'] || [])[0];
check('#120 init ascolta task-result-reset', typeof h === 'function');
c5.saved = true;
h();
check('#120 task-result-reset: saved = false', c5.saved === false);
// #123: dopo Registra + task-result-reset Registra e' spento sul frame registrato, anteprima spenta, impulsiva = grab
win.__visionCfg = { cameraAvailable: true, modo: 'impulsiva', subtasks: [] };
const c9 = ctx.visionExecute();
let grabs = 0;
c9.grab = async function () { grabs++; };
c9.lastBlob = { name: 'f1' };
c9.acquire = async function () { this.result = { vision_result_id: 11, measurements: okMs }; };
check('#123 prima di Registra: canRecord', c9.canRecord === true);
await c9.record();
check('#123 dopo Registra: canRecord falso (saved)', c9.canRecord === false && c9.recordedBlob === c9.lastBlob);
c9.$nextTick = () => {}; // #123: nel test nessun grab di apertura
c9.init(); // registra l'ascolto di task-result-reset
grabs = 0;
(listeners['task-result-reset'] || []).slice(-1)[0]();
check('#123 reset sul frame registrato: canRecord falso', c9.canRecord === false && c9.saved === false);
check('#123 reset sul frame registrato: niente badge anteprima', c9.preview === false);
check('#123 reset in impulsiva: un solo grab', grabs === 1, String(grabs));
c9.lastBlob = { name: 'f2' };
check('#123 frame nuovo: canRecord vero', c9.canRecord === true);
// #119 #122: saveName = titolo del task (nome proposto da <tmf-viewer save-name>), 'immagine' se manca
win.__visionCfg = { taskName: 'Roto3', subtasks: [] };
check('#122 saveName = nome del task', ctx.visionExecute().saveName === 'Roto3');
win.__visionCfg = { subtasks: [] };
check('#122 saveName di riserva', ctx.visionExecute().saveName === 'immagine');
process.exit(failures ? 1 : 0);
})();