Pannello visione operatore: barra senza input file con camera, Salva, Registra conta le quote, svg al posto del canvas, NaN

Refs #118 #119 #120 #121

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 21:20:21 +01:00
parent 4485803122
commit 58baed19e2
6 changed files with 287 additions and 51 deletions
@@ -13,7 +13,12 @@ const src = fs.readFileSync(
);
// Contesto vm con window e document finti; il file definisce visionExecute() globale
const win = {};
const ctx = vm.createContext({ window: win, document: { addEventListener() {}, querySelector() { return null; } } });
// #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;
@@ -108,15 +113,97 @@ check('#116 labelClass warning', c3.labelClass('warning') === 'bg-amber-400 text
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');
// drawOverlay usa VisionOverlay.draw con lw = max(2, w/400)
let drawn = null;
ctx.VisionOverlay = { draw(g, overlay, lw) { drawn = { g, overlay, lw }; return { drawn: 1, skipped: 0 }; } };
const gfake = { clearRect() {}, set lineWidth(v) {}, set lineJoin(v) {} };
const cv = { width: 0, height: 0, getContext() { return gfake; } };
c3.$refs = { img: { naturalWidth: 2000, naturalHeight: 1000 }, overlay: cv };
// #121: drawOverlay imposta il viewBox naturale e riempie l'svg con VisionOverlay.svg(overlay, {zoom})
let svgArgs = null;
ctx.VisionOverlay = { svg(overlay, opts) { svgArgs = { overlay, opts }; return '<circle cx="1" cy="2" r="3"/>'; } };
const attrs = {};
const sv = { innerHTML: 'vecchio', setAttribute(k, v) { attrs[k] = v; } };
c3.$refs = { img: { naturalWidth: 2000, naturalHeight: 1000 }, overlay: sv };
c3.result = { overlay: [{ type: 'circle' }] };
c3.z.zoom = 0.5;
c3.drawOverlay();
check('#121 viewBox = dimensioni naturali', attrs.viewBox === '0 0 2000 1000', attrs.viewBox);
check('#121 innerHTML = VisionOverlay.svg', sv.innerHTML === '<circle cx="1" cy="2" r="3"/>', sv.innerHTML);
check('#121 svg riceve overlay e zoom', svgArgs.overlay.length === 1 && svgArgs.opts.zoom === 0.5, JSON.stringify(svgArgs.opts));
c3.result = null;
c3.drawOverlay();
check('#121 senza risultato: svg vuoto', sv.innerHTML === '');
ctx.VisionOverlay = {};
c3.result = { overlay: [{ type: 'circle' }] };
c3.drawOverlay();
check('#116 drawOverlay chiama VisionOverlay.draw', drawn && drawn.g === gfake && drawn.overlay.length === 1 && drawn.lw === 5, JSON.stringify(drawn && drawn.lw));
check('#116 canvas alla risoluzione naturale', cv.width === 2000 && cv.height === 1000);
check('#121 senza VisionOverlay.svg: svg vuoto, nessun errore', sv.innerHTML === '');
check('#121 niente codice canvas', !/getContext|clearRect|\.width\s*=/.test(src.slice(src.indexOf('drawOverlay() {'))));
process.exit(failures ? 1 : 0);
// #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);
// #119: saveImage -> tmfSaveImage(src dell'immagine mostrata, nome del task)
let saveArgs = null;
ctx.tmfSaveImage = (src, name) => { saveArgs = { src, name }; return Promise.resolve(true); };
win.__visionCfg = { taskName: 'Roto3', subtasks: [] };
const c9 = ctx.visionExecute();
c9.imageSrc = 'blob:abc';
const r9 = await c9.saveImage();
check('#119 saveImage: tmfSaveImage(src, nome task)', r9 === true && saveArgs.src === 'blob:abc' && saveArgs.name === 'Roto3', JSON.stringify(saveArgs));
c9.imageSrc = '';
saveArgs = null;
check('#119 saveImage senza immagine: niente', (await c9.saveImage()) === false && saveArgs === null);
process.exit(failures ? 1 : 0);
})();