8ef61a1285
Refs #123 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
206 lines
11 KiB
JavaScript
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);
|
|
})();
|