<tmf-viewer>: visualizzatore immagine standard, tmfSaveBlob e tmfSvgToPngBlob
Refs #122 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* #122: static/js/image_viewer.js (<tmf-viewer>) con plain node: matematica pura (TmfViewerMath) e
|
||||
* registrazione del Custom Element una sola volta. Il DOM vero si prova in Chrome (fixtures/image_viewer_demo.html).
|
||||
*
|
||||
* Il modulo si carica senza window/document/customElements.
|
||||
* Run from src/frontend/flask_app: node tests/js/test_image_viewer.js
|
||||
*/
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
|
||||
const MOD = path.join(__dirname, '..', '..', 'static', 'js', 'image_viewer.js');
|
||||
|
||||
let failures = 0;
|
||||
let total = 0;
|
||||
function check(label, cond, extra) {
|
||||
total++;
|
||||
if (cond) console.log('ok - ' + label);
|
||||
else { failures++; console.log('FAIL - ' + label + (extra ? ' :: ' + extra : '')); }
|
||||
}
|
||||
const near = (a, b) => Math.abs(a - b) < 1e-9;
|
||||
|
||||
// --- caricamento in node senza globali del browser ---
|
||||
check('node senza window/document/customElements/HTMLElement',
|
||||
typeof window === 'undefined' && typeof document === 'undefined' &&
|
||||
typeof customElements === 'undefined' && typeof HTMLElement === 'undefined');
|
||||
const { TmfViewerMath: M } = require(MOD);
|
||||
check('esporta TmfViewerMath con le quattro funzioni',
|
||||
M && ['fitZoom', 'clampZoom', 'zoomAround', 'pct'].every((k) => typeof M[k] === 'function'));
|
||||
|
||||
// --- fitZoom ---
|
||||
check('fitZoom: box largo, immagine alta -> limita l\'altezza', near(M.fitZoom(800, 400, 1000, 1000), 0.4));
|
||||
check('fitZoom: box alto, immagine larga -> limita la larghezza', near(M.fitZoom(400, 800, 1000, 500), 0.4));
|
||||
check('fitZoom: immagine piu\' piccola del box -> zoom > 1', near(M.fitZoom(800, 600, 400, 300), 2));
|
||||
check('fitZoom: box esatto -> 1', near(M.fitZoom(640, 480, 640, 480), 1));
|
||||
check('fitZoom: boxH Infinity (fit-height) -> solo larghezza', near(M.fitZoom(500, Infinity, 1000, 4000), 0.5));
|
||||
check('fitZoom: 0 se box nascosto o immagine senza dimensioni',
|
||||
M.fitZoom(0, 400, 10, 10) === 0 && M.fitZoom(400, 0, 10, 10) === 0 &&
|
||||
M.fitZoom(400, 400, 0, 10) === 0 && M.fitZoom(400, 400, 10, 0) === 0);
|
||||
check('fitZoom: 0 con NaN, negativi, undefined',
|
||||
M.fitZoom(NaN, 400, 10, 10) === 0 && M.fitZoom(-5, 400, 10, 10) === 0 && M.fitZoom(400, 400, undefined, 10) === 0 &&
|
||||
M.fitZoom(400, 400, 10, null) === 0);
|
||||
|
||||
// --- clampZoom ---
|
||||
check('clampZoom: dentro i limiti resta uguale', near(M.clampZoom(1, 0.5), 1));
|
||||
check('clampZoom: sotto fit*0.5 -> fit*0.5', near(M.clampZoom(0.01, 0.5), 0.25));
|
||||
check('clampZoom: sopra 8 -> 8', near(M.clampZoom(20, 0.5), 8));
|
||||
check('clampZoom: fit sopra 8 alza il massimo a fit', near(M.clampZoom(20, 12), 12) && near(M.clampZoom(11, 12), 11));
|
||||
check('clampZoom: limiti esatti', near(M.clampZoom(0.25, 0.5), 0.25) && near(M.clampZoom(8, 0.5), 8));
|
||||
|
||||
// --- zoomAround ---
|
||||
{
|
||||
// il punto del contenuto sotto (px,py) prima e dopo lo zoom deve essere lo stesso
|
||||
const z0 = 0.5, z1 = 1.25, sl = 120, st = 40, px = 300, py = 200;
|
||||
const r = M.zoomAround(z0, z1, sl, st, px, py);
|
||||
const before = { x: (sl + px) / z0, y: (st + py) / z0 }; // coordinate logiche sotto il puntatore
|
||||
const after = { x: (r.scrollLeft + px) / z1, y: (r.scrollTop + py) / z1 };
|
||||
check('zoomAround: il punto sotto il puntatore resta fermo', near(before.x, after.x) && near(before.y, after.y),
|
||||
JSON.stringify([before, after]));
|
||||
const same = M.zoomAround(2, 2, 50, 60, 10, 10);
|
||||
check('zoomAround: z1 == z0 non sposta lo scroll', near(same.scrollLeft, 50) && near(same.scrollTop, 60));
|
||||
const c = M.zoomAround(1, 2, 0, 0, 100, 50);
|
||||
check('zoomAround: da scroll 0 raddoppiando (100,50) -> scroll (100,50)', near(c.scrollLeft, 100) && near(c.scrollTop, 50));
|
||||
const o = M.zoomAround(2, 1, 100, 100, 0, 0);
|
||||
check('zoomAround: puntatore all\'origine, riduzione -> scroll dimezzato', near(o.scrollLeft, 50) && near(o.scrollTop, 50));
|
||||
}
|
||||
|
||||
// --- pct ---
|
||||
check('pct: 1 -> "100 %"', M.pct(1) === '100 %', M.pct(1));
|
||||
check('pct: arrotonda 0.4567 -> "46 %"', M.pct(0.4567) === '46 %', M.pct(0.4567));
|
||||
check('pct: 8 -> "800 %", 0 -> "0 %"', M.pct(8) === '800 %' && M.pct(0) === '0 %');
|
||||
|
||||
// --- define una sola volta, anche caricando due volte ---
|
||||
{
|
||||
const defined = {};
|
||||
let defineCalls = 0;
|
||||
globalThis.HTMLElement = class {};
|
||||
globalThis.customElements = {
|
||||
get: (n) => defined[n],
|
||||
define: (n, c) => { defineCalls++; defined[n] = c; },
|
||||
};
|
||||
delete require.cache[require.resolve(MOD)];
|
||||
require(MOD);
|
||||
delete require.cache[require.resolve(MOD)];
|
||||
require(MOD);
|
||||
check('customElements.define("tmf-viewer") una sola volta con due caricamenti',
|
||||
defineCalls === 1 && typeof defined['tmf-viewer'] === 'function', 'chiamate: ' + defineCalls);
|
||||
const C = defined['tmf-viewer'];
|
||||
check('osserva src, width, height, fit-height, no-save, save-name',
|
||||
['src', 'width', 'height', 'fit-height', 'no-save', 'save-name'].every((a) => C.observedAttributes.includes(a)));
|
||||
check('espone fit, zoomBy, overlay, labels, zoom sul prototipo',
|
||||
typeof C.prototype.fit === 'function' && typeof C.prototype.zoomBy === 'function' &&
|
||||
['overlay', 'labels', 'zoom'].every((k) => Object.getOwnPropertyDescriptor(C.prototype, k)));
|
||||
delete globalThis.customElements;
|
||||
delete globalThis.HTMLElement;
|
||||
}
|
||||
|
||||
console.log('\n' + (total - failures) + '/' + total + ' controlli ok');
|
||||
process.exit(failures ? 1 : 0);
|
||||
Reference in New Issue
Block a user