Wizard DXF: zoom, Adatta e pan sulle anteprime immagine e DXF. Refs #113

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 12:35:10 +01:00
parent ca6cd0cab1
commit 45c9e6c3f3
5 changed files with 312 additions and 13 deletions
@@ -57,6 +57,11 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
_srcSeq: 0, // #105: una risposta di /source arrivata dopo una lettura piu' recente si scarta _srcSeq: 0, // #105: una risposta di /source arrivata dopo una lettura piu' recente si scarta
_dxfPickSeq: 0, // #105: sale a ogni scelta manuale del DXF; i file della ricetta arrivati dopo si scartano _dxfPickSeq: 0, // #105: sale a ogni scelta manuale del DXF; i file della ricetta arrivati dopo si scartano
_imgPickSeq: 0, // #105: idem per l'immagine (_setImage) _imgPickSeq: 0, // #105: idem per l'immagine (_setImage)
// #113: zoom delle due anteprime (img = immagine caricata/acquisita, dxf = disegno); w,h = misura del contenuto (px immagine; mm del viewBox per il DXF)
views: { img: { zoom: 1, fit: 1, w: 0, h: 0 }, dxf: { zoom: 1, fit: 1, w: 0, h: 0 } },
viewPanKey: '', // #113: anteprima in trascinamento ('' = nessuna)
_viewPan: null, // #113: {k, x, y, left, top} al pointerdown
_viewRO: {}, // #113: ResizeObserver per chiave, rifanno Adatta se il viewport cambia larghezza
// #105: all'avvio legge se il task ha gia' una ricetta da DXF (serve per l'etichetta del bottone) // #105: all'avvio legge se il task ha gia' una ricetta da DXF (serve per l'etichetta del bottone)
init() { init() {
@@ -200,6 +205,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
this.imageSrc = file ? URL.createObjectURL(file) : ''; this.imageSrc = file ? URL.createObjectURL(file) : '';
this.imagePx = null; this.imagePx = null;
this.imageBroken = false; this.imageBroken = false;
this.vReset('img'); // #113: nuova immagine, vista da rifare
this.result = null; this.result = null;
this.tested = false; // #104 this.tested = false; // #104
this.resPx = null; // #104 this.resPx = null; // #104
@@ -211,10 +217,13 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
onImagePreviewLoad(el) { onImagePreviewLoad(el) {
this.imageBroken = false; this.imageBroken = false;
this.imagePx = el && el.naturalWidth ? { w: el.naturalWidth, h: el.naturalHeight } : null; this.imagePx = el && el.naturalWidth ? { w: el.naturalWidth, h: el.naturalHeight } : null;
if (this.imagePx) this.vSetContent('img', this.imagePx.w, this.imagePx.h); // #113: Adatta al caricamento
else this.vReset('img');
}, },
onImagePreviewError() { onImagePreviewError() {
this.imageBroken = !!this.imageSrc; this.imageBroken = !!this.imageSrc;
this.imagePx = null; this.imagePx = null;
this.vReset('img'); // #113
}, },
// #102: nome usato dal template (stesso valore dell'URL gia' usato per l'overlay dei risultati) // #102: nome usato dal template (stesso valore dell'URL gia' usato per l'overlay dei risultati)
get imagePreviewUrl() { return this.imageSrc; }, get imagePreviewUrl() { return this.imageSrc; },
@@ -247,6 +256,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
async loadDxfPreview() { async loadDxfPreview() {
const seq = ++this._dxfPreviewSeq; const seq = ++this._dxfPreviewSeq;
this.dxfPreview = null; this.dxfPreview = null;
this.vReset('dxf'); // #113: nuova anteprima in arrivo
this.dxfPreviewError = ''; this.dxfPreviewError = '';
this.dxfPreviewBusy = false; this.dxfPreviewBusy = false;
if (!this.dxfFile) return; // file tolto: tutto azzerato if (!this.dxfFile) return; // file tolto: tutto azzerato
@@ -260,7 +270,11 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
const body = await resp.json().catch(() => ({})); const body = await resp.json().catch(() => ({}));
if (seq !== this._dxfPreviewSeq) return; // file cambiato nel frattempo if (seq !== this._dxfPreviewSeq) return; // file cambiato nel frattempo
if (!resp.ok || body.error) this.dxfPreviewError = this._errorText(body); if (!resp.ok || body.error) this.dxfPreviewError = this._errorText(body);
else this.dxfPreview = body; else {
this.dxfPreview = body;
const vb = this.dxfPreviewViewBox().split(' ').map(Number); // #113: misura del viewBox (mm) per lo zoom
this.vSetContent('dxf', vb[2], vb[3]);
}
} catch (e) { } catch (e) {
if (seq !== this._dxfPreviewSeq) return; if (seq !== this._dxfPreviewSeq) return;
this.dxfPreviewError = this.msgs.requestFailed || String(e); this.dxfPreviewError = this.msgs.requestFailed || String(e);
@@ -463,6 +477,82 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
this.fitZoom = z; this.fitZoom = z;
vp.scrollLeft = 0; vp.scrollTop = 0; vp.scrollLeft = 0; vp.scrollTop = 0;
}, },
// #113: zoom per chiave ('img' | 'dxf'), stessa logica di #104 ma con stato in views[k]. Viewport fisso di 240px.
_vpEl(k) { return this.$refs ? this.$refs[k === 'img' ? 'vpImg' : 'vpDxf'] : null; },
_vLimits(k) {
const v = this.views[k];
// img: pixel nativi 5 %..800 %; dxf: da meta' a 50 volte Adatta
return k === 'img' ? [0.05, 8] : [v.fit * 0.5, v.fit * 50];
},
vPct(k) {
const v = this.views[k];
return Math.round((k === 'img' ? v.zoom : (v.fit ? v.zoom / v.fit : 1)) * 100) + ' %';
},
vW(k) { const v = this.views[k]; return v.w * v.zoom; },
vH(k) { const v = this.views[k]; return v.h * v.zoom; },
vCanPan(k) { const v = this.views[k]; return v.w > 0 && v.zoom > v.fit * 1.0001; },
vCursor(k) { return this.vCanPan(k) ? (this.viewPanKey === k ? 'grabbing' : 'grab') : 'default'; },
// #113: nessun contenuto: zoom di nuovo a 1 e misura azzerata (il wrapper sparisce)
vReset(k) { this.views[k] = { zoom: 1, fit: 1, w: 0, h: 0 }; },
// #113: nuovo contenuto: vista da Adatta (appena il viewport ha una larghezza) e osservatore di resize
vSetContent(k, w, h) {
if (!(w > 0) || !(h > 0)) { this.vReset(k); return; }
this.views[k] = { zoom: 1, fit: 1, w, h };
const run = () => { this._vObserve(k); this.vFit(k); };
if (typeof this.$nextTick === 'function') this.$nextTick(run); else run();
},
// #113: se il viewport era nascosto (larghezza 0) o cambia larghezza, rifa Adatta finche' la vista e' ad Adatta
_vObserve(k) {
const vp = this._vpEl(k);
if (!vp || this._viewRO[k] || typeof ResizeObserver === 'undefined') return;
this._viewRO[k] = new ResizeObserver(() => {
const v = this.views[k];
if (v.w > 0 && Math.abs(v.zoom - v.fit) < 1e-9 * Math.max(1, v.fit)) this.vFit(k);
});
this._viewRO[k].observe(vp);
},
vFit(k) {
const vp = this._vpEl(k), v = this.views[k];
if (!vp || !v.w || !v.h || !vp.clientWidth) return; // nascosto o vuoto: niente
let z = Math.min(vp.clientWidth / v.w, 240 / v.h);
if (k === 'img') z = Math.min(8, Math.max(0.05, z));
v.zoom = z;
v.fit = z;
vp.scrollLeft = 0; vp.scrollTop = 0;
},
vZoomBy(k, f) {
const vp = this._vpEl(k), v = this.views[k];
if (!v.w) return;
const old = v.zoom, lim = this._vLimits(k);
const z = Math.min(lim[1], Math.max(lim[0], old * f));
if (z === old) return;
// centro visibile invariato, come zoomBy (#104)
let cx = 0, cy = 0;
if (vp) { cx = (vp.scrollLeft + vp.clientWidth / 2) / old; cy = (vp.scrollTop + vp.clientHeight / 2) / old; }
v.zoom = z;
if (vp && typeof this.$nextTick === 'function') this.$nextTick(() => { vp.scrollLeft = cx * z - vp.clientWidth / 2; vp.scrollTop = cy * z - vp.clientHeight / 2; });
},
vWheel(k, ev) {
if (!ev.ctrlKey) return;
ev.preventDefault();
this.vZoomBy(k, ev.deltaY < 0 ? 1.25 : 1 / 1.25);
},
vPanDown(k, ev) {
const vp = this._vpEl(k);
if (!vp || (ev.pointerType === 'mouse' && ev.button)) return;
if (ev.preventDefault) ev.preventDefault();
this._viewPan = { k, x: ev.clientX, y: ev.clientY, left: vp.scrollLeft, top: vp.scrollTop };
this.viewPanKey = k;
try { vp.setPointerCapture(ev.pointerId); } catch (e) { /* puntatore gia' rilasciato */ }
},
vPanMove(k, ev) {
const vp = this._vpEl(k), p = this._viewPan;
if (!p || p.k !== k || !vp) return;
vp.scrollLeft = p.left - (ev.clientX - p.x);
vp.scrollTop = p.top - (ev.clientY - p.y);
},
vPanEnd() { this._viewPan = null; this.viewPanKey = ''; },
// #104: Ctrl+rotella = zoom // #104: Ctrl+rotella = zoom
onWheel(ev) { onWheel(ev) {
if (!ev.ctrlKey) return; if (!ev.ctrlKey) return;
@@ -730,22 +730,58 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start">
<div x-show="imagePreviewUrl"> <div x-show="imagePreviewUrl">
<!-- #102: anteprima dell'immagine --> <!-- #102: anteprima dell'immagine -->
<img x-show="!imageBroken" :src="imagePreviewUrl" alt="" <!-- #113: barra zoom come quella della Prova (#104), per l'anteprima 'img' -->
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()" <div class="relative">
class="block w-full rounded border border-[var(--border-color)]" <div x-show="!imageBroken && views.img.w" class="absolute top-2 right-2 z-10 flex items-center gap-1 rounded px-1 py-0.5"
style="height: 240px; object-fit: contain; background: var(--bg-primary);"> style="background: rgba(0,0,0,.55); color: #fff;">
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('img', 1 / 1.25)" title="{{ _('Riduci') }}">&minus;</button>
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('img', 1.25)" title="{{ _('Ingrandisci') }}">+</button>
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vFit('img')">{{ _('Adatta') }}</button>
<span class="text-xs px-1" x-text="vPct('img')"></span>
</div>
<!-- #113: viewport fisso 240px; il wrapper (margin auto) e' largo w*zoom: centrato se piu' piccolo, scorrevole se piu' grande -->
<div x-show="!imageBroken" x-ref="vpImg" @wheel="vWheel('img', $event)"
@pointerdown="vPanDown('img', $event)" @pointermove="vPanMove('img', $event)"
@pointerup="vPanEnd()" @pointercancel="vPanEnd()"
class="rounded border border-[var(--border-color)]"
:style="'overflow:auto;touch-action:none;user-select:none;display:flex;height:240px;background:var(--bg-primary);cursor:' + vCursor('img')">
<div style="margin:auto;flex:none;" :style="views.img.w ? ('width:' + vW('img') + 'px;height:' + vH('img') + 'px') : 'width:100%;height:240px'">
<img :src="imagePreviewUrl" alt="" draggable="false"
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()"
class="block" style="width: 100%; height: 100%; object-fit: contain; user-select: none;">
</div>
</div>
</div>
<p x-show="imageBroken" class="text-xs text-amber-600">{{ _('Anteprima non disponibile per questo formato') }}</p> <p x-show="imageBroken" class="text-xs text-amber-600">{{ _('Anteprima non disponibile per questo formato') }}</p>
<p x-show="imagePreviewSummary" class="mt-1 text-xs text-[var(--text-muted)]" x-text="imagePreviewSummary"></p> <p x-show="imagePreviewSummary" class="mt-1 text-xs text-[var(--text-muted)]" x-text="imagePreviewSummary"></p>
</div> </div>
<div x-show="dxfPreview" class="sm:col-start-2">{# #104: resta a destra anche senza anteprima immagine #} <div x-show="dxfPreview" class="sm:col-start-2">{# #104: resta a destra anche senza anteprima immagine #}
<svg x-bind:view-box.camel="dxfPreviewViewBox()" preserveAspectRatio="xMidYMid meet" <!-- #113: barra zoom come quella della Prova (#104), per l'anteprima 'dxf' -->
class="block w-full rounded border border-[var(--border-color)]" <div class="relative">
style="height: 240px; background: var(--bg-primary);"> <div class="absolute top-2 right-2 z-10 flex items-center gap-1 rounded px-1 py-0.5"
<!-- #101: un solo path (x-for/template non funziona nel namespace SVG); view-box.camel perche' il parser HTML abbassa viewBox --> style="background: rgba(0,0,0,.55); color: #fff;">
<path :d="dxfPreviewD()" fill="none" stroke="var(--color-primary, #2563eb)" <button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('dxf', 1 / 1.25)" title="{{ _('Riduci') }}">&minus;</button>
stroke-width="1.5" vector-effect="non-scaling-stroke" <button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('dxf', 1.25)" title="{{ _('Ingrandisci') }}">+</button>
stroke-linejoin="round"></path> <button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vFit('dxf')">{{ _('Adatta') }}</button>
</svg> <span class="text-xs px-1" x-text="vPct('dxf')"></span>
</div>
<!-- #113: stesso viewport dell'immagine; svg 100% del wrapper, viewBox in mm -->
<div x-ref="vpDxf" @wheel="vWheel('dxf', $event)"
@pointerdown="vPanDown('dxf', $event)" @pointermove="vPanMove('dxf', $event)"
@pointerup="vPanEnd()" @pointercancel="vPanEnd()"
class="rounded border border-[var(--border-color)]"
:style="'overflow:auto;touch-action:none;user-select:none;display:flex;height:240px;background:var(--bg-primary);cursor:' + vCursor('dxf')">
<div style="margin:auto;flex:none;" :style="'width:' + vW('dxf') + 'px;height:' + vH('dxf') + 'px'">
<svg x-bind:view-box.camel="dxfPreviewViewBox()" preserveAspectRatio="xMidYMid meet"
class="block rounded" style="width: 100%; height: 100%;">
<!-- #101: un solo path (x-for/template non funziona nel namespace SVG); view-box.camel perche' il parser HTML abbassa viewBox -->
<path :d="dxfPreviewD()" fill="none" stroke="var(--color-primary, #2563eb)"
stroke-width="1.5" vector-effect="non-scaling-stroke"
stroke-linejoin="round"></path>
</svg>
</div>
</div>
</div>
<p class="mt-1 text-xs text-[var(--text-muted)]" x-text="dxfPreviewSummary"></p> <p class="mt-1 text-xs text-[var(--text-muted)]" x-text="dxfPreviewSummary"></p>
</div> </div>
</div> </div>
@@ -0,0 +1,133 @@
/**
* #113: zoom, Adatta e pan sulle anteprime del wizard (immagine e DXF), con plain node.
*
* Run from src/frontend/flask_app: node tests/js/test_dxf_zoom.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
global.document = { documentElement: { lang: 'it' } };
const src = fs.readFileSync(
path.join(__dirname, '..', '..', 'static', 'js', 'dxf_recipe_wizard.js'), 'utf8',
);
eval(src + ';global.dxfRecipeWizard = dxfRecipeWizard;');
let failures = 0;
function check(label, cond, extra) {
if (cond) console.log('ok - ' + label);
else { failures++; console.log('FAIL - ' + label + (extra ? ' :: ' + extra : '')); }
}
const near = (a, b) => Math.abs(a - b) < 1e-9 * Math.max(1, Math.abs(b));
// Wizard con viewport finti e $nextTick sincrono
function make(cw) {
const w = dxfRecipeWizard(1, () => 'tok', {});
const vp = () => ({ clientWidth: cw, clientHeight: 240, scrollLeft: 5, scrollTop: 5 });
w.$refs = { vpImg: vp(), vpDxf: vp() };
w.$nextTick = fn => fn();
return w;
}
// img: viewport 400 x 240, immagine 2856 x 2848
let w = make(400);
w.vSetContent('img', 2856, 2848);
check('#113 img: Adatta = 240/2848', near(w.views.img.zoom, 240 / 2848) && near(w.views.img.fit, 240 / 2848), String(w.views.img.zoom));
check('#113 img: scroll azzerato dopo Adatta', w.$refs.vpImg.scrollLeft === 0 && w.$refs.vpImg.scrollTop === 0);
check('#113 img: percentuale = zoom*100', w.vPct('img') === Math.round(240 / 2848 * 100) + ' %', w.vPct('img'));
const z0 = w.views.img.zoom;
w.vZoomBy('img', 1.25);
check('#113 img: zoomBy moltiplica', near(w.views.img.zoom, z0 * 1.25));
check('#113 img: dopo zoom si puo trascinare', w.vCanPan('img') === true && w.vCursor('img') === 'grab');
w.vZoomBy('img', 1e6);
check('#113 img: limite alto 8', w.views.img.zoom === 8);
w.vZoomBy('img', 1e-9);
check('#113 img: limite basso 0.05', w.views.img.zoom === 0.05);
w.vFit('img');
check('#113 img: Adatta torna al fit', near(w.views.img.zoom, z0) && w.vCanPan('img') === false);
check('#113 img: l\'altra vista non si muove', w.views.dxf.w === 0 && w.views.dxf.zoom === 1);
check('#113 img: wrapper w*zoom', near(w.vW('img'), 2856 * z0) && near(w.vH('img'), 2848 * z0));
// dxf: viewBox 20 x 10 mm, viewport 400 x 240 -> fit = min(20, 24) = 20
w = make(400);
w.vSetContent('dxf', 20, 10);
check('#113 dxf: fit = min(vpW/w, 240/h)', near(w.views.dxf.fit, 20), String(w.views.dxf.fit));
check('#113 dxf: dopo Adatta 100 %', w.vPct('dxf') === '100 %', w.vPct('dxf'));
w.vZoomBy('dxf', 2);
check('#113 dxf: zoom 2x = 200 %', w.vPct('dxf') === '200 %', w.vPct('dxf'));
w.vZoomBy('dxf', 1e9);
check('#113 dxf: si ferma a fit*50', near(w.views.dxf.zoom, 20 * 50) && w.vPct('dxf') === '5000 %', String(w.views.dxf.zoom));
w.vZoomBy('dxf', 1e-9);
check('#113 dxf: si ferma a fit*0.5', near(w.views.dxf.zoom, 10) && w.vPct('dxf') === '50 %', String(w.views.dxf.zoom));
// vFit con viewport largo 0 (nascosto): stato invariato
w = make(0);
w.vSetContent('img', 1000, 500);
check('#113 clientWidth 0: nessun fit, stato invariato', w.views.img.zoom === 1 && w.views.img.fit === 1 && w.views.img.w === 1000);
w.$refs.vpImg.clientWidth = 500;
w.vFit('img');
check('#113 viewport visibile: il fit parte', near(w.views.img.fit, 0.48), String(w.views.img.fit));
// Reset: nuova immagine / DXF tolto
w.vReset('img');
check('#113 reset: contenuto e zoom azzerati', w.views.img.w === 0 && w.views.img.zoom === 1 && w.vCanPan('img') === false);
w.vZoomBy('img', 2);
check('#113 zoomBy senza contenuto: ignorato', w.views.img.zoom === 1);
// Integrazione: onImagePreviewLoad e _setImage
global.URL.createObjectURL = () => 'blob:x';
global.URL.revokeObjectURL = () => {};
w = make(400);
w._setImage({ name: 'a.bmp' }, 'file');
w.onImagePreviewLoad({ naturalWidth: 800, naturalHeight: 240 });
check('#113 onImagePreviewLoad: Adatta all\'arrivo', near(w.views.img.zoom, 0.5) && w.views.img.w === 800, String(w.views.img.zoom));
w._setImage({ name: 'b.bmp' }, 'file');
check('#113 _setImage: vista azzerata', w.views.img.w === 0 && w.views.img.zoom === 1);
w.onImagePreviewLoad({ naturalWidth: 800, naturalHeight: 240 });
w.onImagePreviewError();
check('#113 errore <img>: vista azzerata', w.views.img.w === 0);
// Wheel: solo con Ctrl
w = make(400);
w.vSetContent('dxf', 20, 10);
let prevented = false;
w.vWheel('dxf', { ctrlKey: false, deltaY: -1, preventDefault() { prevented = true; } });
check('#113 rotella senza Ctrl: niente zoom', w.views.dxf.zoom === 20 && !prevented);
w.vWheel('dxf', { ctrlKey: true, deltaY: -1, preventDefault() { prevented = true; } });
check('#113 Ctrl+rotella: zoom avanti', near(w.views.dxf.zoom, 25) && prevented);
// Pan
w = make(400);
w.vSetContent('dxf', 20, 10);
w.vZoomBy('dxf', 2);
const vp = w.$refs.vpDxf;
vp.scrollLeft = 100; vp.scrollTop = 50; vp.setPointerCapture = () => {};
w.vPanDown('dxf', { pointerType: 'mouse', button: 0, clientX: 200, clientY: 100, pointerId: 1 });
w.vPanMove('dxf', { clientX: 150, clientY: 90 });
check('#113 pan: scroll di quanto si muove il puntatore', vp.scrollLeft === 150 && vp.scrollTop === 60, vp.scrollLeft + ',' + vp.scrollTop);
check('#113 pan: cursore grabbing', w.vCursor('dxf') === 'grabbing');
w.vPanEnd();
check('#113 pan finito', w.viewPanKey === '' && w._viewPan === null);
// Integrazione DXF: loadDxfPreview imposta la misura del viewBox e fa Adatta; file tolto azzera
(async () => {
const PREVIEW = {
contorni: [[[0, 0], [20, 0], [20, 10]]],
bbox: { xmin: 0, ymin: 0, xmax: 20, ymax: 10 },
larghezza_mm: 20, altezza_mm: 10, entita: 1, punti: 3,
};
global.FormData = class { append() {} };
global.fetch = async () => ({ ok: true, json: async () => PREVIEW });
const wd = make(400);
wd.dxfFile = { name: 'a.dxf' };
await wd.loadDxfPreview();
const vb = wd.dxfPreviewViewBox().split(' ').map(Number);
check('#113 loadDxfPreview: misura = viewBox', near(wd.views.dxf.w, vb[2]) && near(wd.views.dxf.h, vb[3]), JSON.stringify(wd.views.dxf));
check('#113 loadDxfPreview: Adatta = 100 %', wd.vPct('dxf') === '100 %' && wd.views.dxf.fit > 1);
wd.dxfFile = null;
await wd.loadDxfPreview();
check('#113 DXF tolto: vista azzerata', wd.views.dxf.w === 0 && wd.views.dxf.zoom === 1);
if (failures) { console.log(failures + ' test falliti'); process.exit(1); }
console.log('tutti i test #113 ok');
})();
@@ -357,5 +357,10 @@ class TestPages:
assert "imageSource === 'file'" in html and "zoomBy(" in html and "fit()" in html # #104: mm/px solo da file, zoom e Fit assert "imageSource === 'file'" in html and "zoomBy(" in html and "fit()" in html # #104: mm/px solo da file, zoom e Fit
assert 'onPanDown($event)' in html # #104: pan col puntatore assert 'onPanDown($event)' in html # #104: pan col puntatore
assert "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine assert "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine
# #113: zoom, Adatta e pan anche sulle due anteprime (immagine e DXF), per chiave
assert 'x-ref="vpImg"' in html and 'x-ref="vpDxf"' in html
assert "vFit('img')" in html and "vFit('dxf')" in html
assert "vZoomBy('img', 1.25)" in html and "vZoomBy('dxf', 1.25)" in html
assert "vPanDown('img', $event)" in html and "vWheel('dxf', $event)" in html
assert "existing ? msgs.editLabel : msgs.createLabel" in html and "Modifica ricetta da DXF" in html # #105: etichetta dinamica assert "existing ? msgs.editLabel : msgs.createLabel" in html and "Modifica ricetta da DXF" in html # #105: etichetta dinamica
assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html
@@ -0,0 +1,35 @@
"""Runs the plain-node DXF wizard zoom test (tests/js/test_dxf_zoom.js).
Same thin pytest wrapper pattern as test_dxf_preview_js.py: shells out to
``node`` and asserts the script's own exit code. Skips (does not fail) when
node is not installed.
Covers gitea:Adriano/TieMeasureFlow#113.
"""
import shutil
import subprocess
from pathlib import Path
import pytest
FLASK_APP_DIR = Path(__file__).resolve().parent.parent
JS_TEST = Path(__file__).resolve().parent / "js" / "test_dxf_zoom.js"
def test_dxf_zoom_js_behaviour():
node = shutil.which("node")
if not node:
pytest.skip("node non installato: test JS dello zoom del wizard saltato")
result = subprocess.run(
[node, str(JS_TEST)],
cwd=str(FLASK_APP_DIR),
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, (
f"tests/js/test_dxf_zoom.js fallito (exit {result.returncode})\n"
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)