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:
@@ -57,6 +57,11 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
_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
|
||||
_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)
|
||||
init() {
|
||||
@@ -200,6 +205,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.imageSrc = file ? URL.createObjectURL(file) : '';
|
||||
this.imagePx = null;
|
||||
this.imageBroken = false;
|
||||
this.vReset('img'); // #113: nuova immagine, vista da rifare
|
||||
this.result = null;
|
||||
this.tested = false; // #104
|
||||
this.resPx = null; // #104
|
||||
@@ -211,10 +217,13 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
onImagePreviewLoad(el) {
|
||||
this.imageBroken = false;
|
||||
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() {
|
||||
this.imageBroken = !!this.imageSrc;
|
||||
this.imagePx = null;
|
||||
this.vReset('img'); // #113
|
||||
},
|
||||
// #102: nome usato dal template (stesso valore dell'URL gia' usato per l'overlay dei risultati)
|
||||
get imagePreviewUrl() { return this.imageSrc; },
|
||||
@@ -247,6 +256,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
async loadDxfPreview() {
|
||||
const seq = ++this._dxfPreviewSeq;
|
||||
this.dxfPreview = null;
|
||||
this.vReset('dxf'); // #113: nuova anteprima in arrivo
|
||||
this.dxfPreviewError = '';
|
||||
this.dxfPreviewBusy = false;
|
||||
if (!this.dxfFile) return; // file tolto: tutto azzerato
|
||||
@@ -260,7 +270,11 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
if (seq !== this._dxfPreviewSeq) return; // file cambiato nel frattempo
|
||||
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) {
|
||||
if (seq !== this._dxfPreviewSeq) return;
|
||||
this.dxfPreviewError = this.msgs.requestFailed || String(e);
|
||||
@@ -463,6 +477,82 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.fitZoom = z;
|
||||
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
|
||||
onWheel(ev) {
|
||||
if (!ev.ctrlKey) return;
|
||||
|
||||
@@ -730,22 +730,58 @@
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 items-start">
|
||||
<div x-show="imagePreviewUrl">
|
||||
<!-- #102: anteprima dell'immagine -->
|
||||
<img x-show="!imageBroken" :src="imagePreviewUrl" alt=""
|
||||
@load="onImagePreviewLoad($event.target)" @error="onImagePreviewError()"
|
||||
class="block w-full rounded border border-[var(--border-color)]"
|
||||
style="height: 240px; object-fit: contain; background: var(--bg-primary);">
|
||||
<!-- #113: barra zoom come quella della Prova (#104), per l'anteprima 'img' -->
|
||||
<div class="relative">
|
||||
<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="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') }}">−</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="imagePreviewSummary" class="mt-1 text-xs text-[var(--text-muted)]" x-text="imagePreviewSummary"></p>
|
||||
</div>
|
||||
<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"
|
||||
class="block w-full rounded border border-[var(--border-color)]"
|
||||
style="height: 240px; background: var(--bg-primary);">
|
||||
<!-- #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>
|
||||
<!-- #113: barra zoom come quella della Prova (#104), per l'anteprima 'dxf' -->
|
||||
<div class="relative">
|
||||
<div class="absolute top-2 right-2 z-10 flex items-center gap-1 rounded px-1 py-0.5"
|
||||
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('dxf', 1 / 1.25)" title="{{ _('Riduci') }}">−</button>
|
||||
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vZoomBy('dxf', 1.25)" title="{{ _('Ingrandisci') }}">+</button>
|
||||
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vFit('dxf')">{{ _('Adatta') }}</button>
|
||||
<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>
|
||||
</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 'onPanDown($event)' in html # #104: pan col puntatore
|
||||
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 "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}"
|
||||
)
|
||||
Reference in New Issue
Block a user