diff --git a/src/frontend/flask_app/tests/js/test_vision_overlay.js b/src/frontend/flask_app/tests/js/test_vision_overlay.js index e514989..63f7f54 100644 --- a/src/frontend/flask_app/tests/js/test_vision_overlay.js +++ b/src/frontend/flask_app/tests/js/test_vision_overlay.js @@ -224,5 +224,141 @@ const rows = fixture.measurements.map((m) => { check('labels: testo name + valore a 3 decimali + unit', f[0].text === 'T 1.500 mm', f[0].text); } +// --- #118: labels, valore null/undefined/non finito -> nessuna etichetta --- +{ + const ov = [{ type: 'segment', role: 'found', tool_id: 't', row1: 10, col1: 10, row2: 10, col2: 20 }]; + const mk = (value) => ({ key: 1, name: 'T', value, unit: 'mm', status: 'fail', vision_output: 't.value' }); + check('labels: value null -> nessuna etichetta (Number(null) e\' 0)', VisionOverlay.labels([mk(null)], ov).length === 0); + check('labels: value undefined -> nessuna', VisionOverlay.labels([mk(undefined)], ov).length === 0); + check('labels: value NaN/Infinity -> nessuna', + VisionOverlay.labels([mk(NaN), mk(Infinity), mk(-Infinity)], ov).length === 0); + check('labels: value 0 resta un valore valido', VisionOverlay.labels([mk(0)], ov).length === 1); + const misto = VisionOverlay.labels([{ ...mk(null), key: 1 }, { ...mk(2.5), key: 2 }], ov); + check('labels: null saltata, le altre righe restano', misto.length === 1 && misto[0].key === 2); +} + +// --- #121: svg(overlay, opts) --- +const count = (str, re) => (str.match(re) || []).length; +const SHAPE_RE = /<(polygon|line|circle|path|polyline) /g; +{ + const out = VisionOverlay.svg(overlay); + // il fixture vero ha 25 forme: 7 segment, 1 contour, 1 points, 4 rect2 roi, 4 arc roi, 4 arc found, 4 circle + check('svg fixture: 25 forme', count(out, SHAPE_RE) === 25, String(count(out, SHAPE_RE))); + check('svg fixture: ogni forma con vector-effect non-scaling-stroke', + count(out, /vector-effect="non-scaling-stroke"/g) === 25); + check('svg fixture: ogni forma fill none', count(out, /fill="none"/g) === 25); + check('svg fixture: 8 roi (4 rect2 + 4 arc) con stroke-dasharray 6 4', count(out, /stroke-dasharray="6 4"/g) === 8, + String(count(out, /stroke-dasharray="6 4"/g))); + check('svg fixture: nessun esterno', !/ VisionOverlay.svg([o], opts); + const roi = one({ type: 'rect2', role: 'roi', row: 50, col: 100, phi_deg: 0, length1: 10, length2: 4 }); + check('svg roi: polygon 4 vertici, colore roi, dash 6 4, spessore lw*0.75', + roi.startsWith(' stroke-width 3', roi4.includes('stroke-width="3"'), roi4); + const fnd = one({ type: 'segment', role: 'found', row1: 1, col1: 2, row2: 3, col2: 4 }); + check('svg found: line verde piena, spessore lw 2 predefinito', + fnd === '', fnd); + check('svg reference blu, ruolo ignoto rosso', + one({ type: 'segment', role: 'reference', row1: 0, col1: 0, row2: 1, col2: 1 }).includes('stroke="#2563eb"') && + one({ type: 'segment', role: 'boh', row1: 0, col1: 0, row2: 1, col2: 1 }).includes('stroke="#ef4444"')); + check('svg: spessore costante, non dipende dallo zoom', + one({ type: 'segment', role: 'found', row1: 0, col1: 0, row2: 1, col2: 1 }, { zoom: 4 }).includes('stroke-width="2"')); + const cir = one({ type: 'circle', role: 'found', row: 5, col: 7, radius: 3 }); + check('svg circle: cx cy r', cir.startsWith(' /d="([^"]+)"/.exec(one({ type: 'arc', role: 'found', ...o }))[1]; + const parse = (d) => d.match(/-?\d+(\.\d+)?/g).map(Number); // [x0, y0, rx, ry, rot, large, sweep, x1, y1] + { + // start 90, extent 90 su centro (40,30) r 12: da (40,18) a (28,30), antiorario (sweep 0), arco piccolo + const d = arcD({ row: 30, col: 40, radius: 12, start_deg: 90, extent_deg: 90 }); + const v = parse(d); + check('svg arc 90+90: M 40 18 A 12 12 0 0 0 28 30', + near(v[0], 40) && near(v[1], 18) && v[2] === 12 && v[3] === 12 && v[4] === 0 && v[5] === 0 && v[6] === 0 && + near(v[7], 28) && near(v[8], 30), d); + } + { + // start 0, extent -90 (orario): da (10,0) a (0,10) a video, sweep 1 + const v = parse(arcD({ row: 0, col: 0, radius: 10, start_deg: 0, extent_deg: -90 })); + check('svg arc extent negativo: sweep 1, fine in basso a video', + near(v[0], 10) && near(v[1], 0) && v[5] === 0 && v[6] === 1 && near(v[7], 0) && near(v[8], 10), JSON.stringify(v)); + } + { + // 270 gradi antiorari da 0: fine a 270 = in basso a video (0, +10) + const v = parse(arcD({ row: 0, col: 0, radius: 10, start_deg: 0, extent_deg: 270 })); + check('svg arc 270: large-arc 1, sweep 0, fine (0,10) a video', + v[5] === 1 && v[6] === 0 && near(v[7], 0) && near(v[8], 10), JSON.stringify(v)); + } + { + const v = parse(arcD({ row: 0, col: 0, radius: 10, start_deg: 0, extent_deg: 360 })); + check('svg arc 360: estensione limitata a 359.99 (large-arc, fine quasi sullo start)', + v[5] === 1 && Math.abs(v[8]) < 0.01 && v[7] > 9.99, JSON.stringify(v)); + } + { + // coincide con draw: punto finale del g.arc del canvas (angolo -rad(start+extent)) + const a = { type: 'arc', role: 'found', row: 30, col: 40, radius: 12, start_deg: 30, extent_deg: 100 }; + const g = fakeCanvas(); + VisionOverlay.draw(g, [a], 1); + const c = onlyCalls(g, 'arc')[0]; + const ex = c[1] + c[3] * Math.cos(c[5]), ey = c[2] + c[3] * Math.sin(c[5]); + const sx = c[1] + c[3] * Math.cos(c[4]), sy = c[2] + c[3] * Math.sin(c[4]); + const v = parse(arcD(a)); + check('svg arc: estremi uguali a quelli di draw su canvas', + Math.abs(v[7] - ex) < 0.01 && Math.abs(v[8] - ey) < 0.01 && Math.abs(v[0] - sx) < 0.01 && Math.abs(v[1] - sy) < 0.01, + JSON.stringify([v, ex, ey])); + } + + // malformati e ignoti: saltati, nessuna eccezione, la forma valida resta + let thrown = null, out = ''; + try { + out = VisionOverlay.svg([ + { type: 'circle', role: 'found', row: 1, col: 1 }, + { type: 'points', role: 'found', rows: [1, 2], cols: [1] }, + { type: 'contour', role: 'found', rows: [], cols: [] }, + { type: 'boh', role: 'found' }, + { type: 'segment', role: 'found', row1: NaN, col1: 0, row2: 1, col2: 1 }, + { type: 'arc', role: 'found', row: 0, col: 0, radius: 1, start_deg: 0 }, + { type: 'rect2', role: 'roi', row: 0, col: 0, phi_deg: 0, length1: Infinity, length2: 1 }, + null, + { type: 'segment', role: 'found', row1: 0, col1: 0, row2: 1, col2: 1 }, + ], { zoom: 1 }); + } catch (e) { thrown = e; } + check('svg malformati: nessuna eccezione, una sola forma valida', + thrown === null && count(out, / stringa vuota', + VisionOverlay.svg(null) === '' && VisionOverlay.svg(undefined) === '' && VisionOverlay.svg([]) === ''); + check('svg: opts non validi -> predefiniti', + VisionOverlay.svg([{ type: 'points', role: 'found', rows: [10], cols: [20] }], { zoom: 0, lw: -1, cross: NaN }) + .includes('d="M 14 10 L 26 10 M 20 4 L 20 16"')); + + // nessuna stringa dei dati nel markup + const evil = '"onload="x'; + const sicuro = VisionOverlay.svg([ + { type: 'segment', role: evil, tool_id: evil, name: evil, row1: 0, col1: 0, row2: 1, col2: 1 }, + { type: 'circle', role: 'found', tool_id: evil, row: 1, col: 1, radius: 2 }, + ]); + check('svg: tool_id e role dei dati non compaiono nel markup', + !/script|onload|alert/.test(sicuro) && count(sicuro, /vector-effect/g) === 2, sicuro); + const strNum = VisionOverlay.svg([{ type: 'segment', role: 'found', row1: '', col1: 0, row2: 1, col2: 1 }]); + check('svg: coordinata stringa -> forma saltata', strNum === ''); +} + console.log('\n' + (total - failures) + '/' + total + ' controlli ok'); process.exit(failures ? 1 : 0);