Test dell'overlay SVG e delle etichette senza valore

Refs #121 #118

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 21:17:29 +01:00
parent 5723f055c4
commit df67a32a82
@@ -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); 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 <svg> esterno', !/<svg/.test(out));
check('svg fixture: nessun NaN/undefined/null nel markup', !/NaN|undefined|null|Infinity/.test(out));
check('svg fixture: numeri con al piu\' 2 decimali', !/\d\.\d{3,}/.test(out));
}
{
const one = (o, opts) => 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('<polygon ') && roi.includes('points="110,46 110,54 90,54 90,46"') && roi.includes('stroke="#f59e0b"') &&
roi.includes('stroke-dasharray="6 4"') && roi.includes('stroke-width="1.5"'), roi);
const roi4 = one({ type: 'rect2', role: 'roi', row: 50, col: 100, phi_deg: 0, length1: 10, length2: 4 }, { lw: 4 });
check('svg roi: lw 4 -> 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 === '<line x1="2" y1="1" x2="4" y2="3" fill="none" stroke="#16a34a" stroke-width="2" vector-effect="non-scaling-stroke"/>', 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('<circle cx="7" cy="5" r="3" '), cir);
const con = one({ type: 'contour', role: 'found', rows: [1, 2, 3], cols: [4, 5, 6] });
check('svg contour: polyline aperta', con.startsWith('<polyline points="4,1 5,2 6,3" ') && !/ Z/.test(con), con);
// points: croce di semi-braccio cross/zoom
const p1 = one({ type: 'points', role: 'found', rows: [10], cols: [20] }, { zoom: 1 });
check('svg points zoom 1: semi-braccio 6', p1.includes('d="M 14 10 L 26 10 M 20 4 L 20 16"'), p1);
const p2 = one({ type: 'points', role: 'found', rows: [10], cols: [20] }, { zoom: 2 });
check('svg points zoom 2: semi-braccio 3', p2.includes('d="M 17 10 L 23 10 M 20 7 L 20 13"'), p2);
const p3 = one({ type: 'points', role: 'found', rows: [10, 30], cols: [20, 40] }, { zoom: 1, cross: 2 });
check('svg points: cross personalizzato, 2 croci in un solo path',
count(p3, /<path /g) === 1 && count(p3, /M /g) === 4 && p3.includes('M 18 10 L 22 10'), p3);
// arc: estremi calcolati (y verso il basso, angoli antiorari)
const arcD = (o) => /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, /<line /g) === 1 && count(out, /vector-effect/g) === 1, String(thrown) + out);
check('svg: overlay non array o vuoto -> 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 = '<script>alert(1)</script>"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: '<b>', col1: 0, row2: 1, col2: 1 }]);
check('svg: coordinata stringa -> forma saltata', strNum === '');
}
console.log('\n' + (total - failures) + '/' + total + ' controlli ok'); console.log('\n' + (total - failures) + '/' + total + ' controlli ok');
process.exit(failures ? 1 : 0); process.exit(failures ? 1 : 0);