/** * #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 ), 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 : 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 ), '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); })();