Wizard DXF e riepilogo: overlay SVG, Salva immagine, NaN per quota non misurata
Refs #118 #119 #121 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,16 +84,18 @@ function csvExport() {
|
|||||||
const toleranceMinus = this.hasNumber(nominal) && this.hasNumber(subtask.ltl)
|
const toleranceMinus = this.hasNumber(nominal) && this.hasNumber(subtask.ltl)
|
||||||
? nominal - subtask.ltl : null;
|
? nominal - subtask.ltl : null;
|
||||||
|
|
||||||
|
// #118: quota non misurata (value null) -> NaN nel valore e nella deviazione; le tolleranze nulle restano vuote
|
||||||
|
const notMeasured = m.value === null || m.value === undefined;
|
||||||
const row = [
|
const row = [
|
||||||
m.id || '',
|
m.id || '',
|
||||||
m.subtask_id || '',
|
m.subtask_id || '',
|
||||||
this.escapeCsvValue(subtask.description || ''),
|
this.escapeCsvValue(subtask.description || ''),
|
||||||
this.formatNumber(m.value),
|
notMeasured ? 'NaN' : this.formatNumber(m.value),
|
||||||
this.escapeCsvValue(subtask.unit || 'mm'),
|
this.escapeCsvValue(subtask.unit || 'mm'),
|
||||||
this.formatNumber(nominal),
|
this.formatNumber(nominal),
|
||||||
this.formatNumber(tolerancePlus),
|
this.formatNumber(tolerancePlus),
|
||||||
this.formatNumber(toleranceMinus),
|
this.formatNumber(toleranceMinus),
|
||||||
this.formatNumber(m.deviation),
|
notMeasured ? 'NaN' : this.formatNumber(m.deviation),
|
||||||
m.pass_fail || '',
|
m.pass_fail || '',
|
||||||
this.escapeCsvValue(m.lot_number || ''),
|
this.escapeCsvValue(m.lot_number || ''),
|
||||||
this.escapeCsvValue(m.serial_number || ''),
|
this.escapeCsvValue(m.serial_number || ''),
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
// la scrive e avvisa il componente visionRecipe con l'evento 'dxf-recipe-saved'.
|
// la scrive e avvisa il componente visionRecipe con l'evento 'dxf-recipe-saved'.
|
||||||
// Uso: x-data="dxfRecipeWizard(task.id, csrfFn, msgs)". Dipende da Alpine.js e station_calibration.js.
|
// Uso: x-data="dxfRecipeWizard(task.id, csrfFn, msgs)". Dipende da Alpine.js e station_calibration.js.
|
||||||
function dxfRecipeWizard(taskId, csrf, msgs) {
|
function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||||
// Colori overlay per ruolo, uguali a vision_execute.js (reference, roi, found)
|
// #121: i colori per ruolo stanno in VisionOverlay (vision_overlay.js)
|
||||||
const ROLE_COLORS = { reference: '#2563eb', found: '#16a34a', roi: '#f59e0b' };
|
|
||||||
const VALUE_UNITS = { 'dxf.points_in_pct': '%', 'dxf.distance_avg': 'mm', 'dxf.distance_min': 'mm', 'dxf.distance_max': 'mm' }; // #104
|
const VALUE_UNITS = { 'dxf.points_in_pct': '%', 'dxf.distance_avg': 'mm', 'dxf.distance_min': 'mm', 'dxf.distance_max': 'mm' }; // #104
|
||||||
const VALUE_KEYS = ['dxf.points_in_pct', 'dxf.distance_avg', 'dxf.distance_min', 'dxf.distance_max'];
|
const VALUE_KEYS = ['dxf.points_in_pct', 'dxf.distance_avg', 'dxf.distance_min', 'dxf.distance_max'];
|
||||||
|
|
||||||
@@ -466,6 +465,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
let cx = 0, cy = 0;
|
let cx = 0, cy = 0;
|
||||||
if (vp) { cx = (vp.scrollLeft + vp.clientWidth / 2) / old; cy = (vp.scrollTop + vp.clientHeight / 2) / old; }
|
if (vp) { cx = (vp.scrollLeft + vp.clientWidth / 2) / old; cy = (vp.scrollTop + vp.clientHeight / 2) / old; }
|
||||||
this.zoom = z;
|
this.zoom = z;
|
||||||
|
this.drawOverlay(); // #121: ridisegno al nuovo zoom
|
||||||
if (vp) this.$nextTick(() => { vp.scrollLeft = cx * z - vp.clientWidth / 2; vp.scrollTop = cy * z - vp.clientHeight / 2; });
|
if (vp) this.$nextTick(() => { vp.scrollLeft = cx * z - vp.clientWidth / 2; vp.scrollTop = cy * z - vp.clientHeight / 2; });
|
||||||
},
|
},
|
||||||
// #104: Fit = tutta l'immagine nel viewport (altezza massima, non quella corrente che segue l'immagine)
|
// #104: Fit = tutta l'immagine nel viewport (altezza massima, non quella corrente che segue l'immagine)
|
||||||
@@ -475,6 +475,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
const z = Math.min(8, Math.max(0.05, Math.min(vp.clientWidth / img.naturalWidth, this._maxVpH() / img.naturalHeight)));
|
const z = Math.min(8, Math.max(0.05, Math.min(vp.clientWidth / img.naturalWidth, this._maxVpH() / img.naturalHeight)));
|
||||||
this.zoom = z;
|
this.zoom = z;
|
||||||
this.fitZoom = z;
|
this.fitZoom = z;
|
||||||
|
this.drawOverlay(); // #121: ridisegno al nuovo zoom
|
||||||
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.
|
// #113: zoom per chiave ('img' | 'dxf'), stessa logica di #104 ma con stato in views[k]. Viewport fisso di 240px.
|
||||||
@@ -581,26 +582,15 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
|||||||
{ maximumFractionDigits: 3 }) : '-';
|
{ maximumFractionDigits: 3 }) : '-';
|
||||||
},
|
},
|
||||||
|
|
||||||
// Come vision_execute.js: canvas della dimensione dell'immagine, coordinate in pixel immagine
|
// #121: overlay SVG in pixel immagine (viewBox = dimensioni naturali); tratto a spessore costante a ogni zoom.
|
||||||
|
// Senza VisionOverlay.svg resta vuoto, senza errori.
|
||||||
drawOverlay() {
|
drawOverlay() {
|
||||||
const img = this.$refs.img, cv = this.$refs.overlay;
|
const img = this.$refs.img, sv = this.$refs.overlay;
|
||||||
if (!img || !cv || !img.naturalWidth) return;
|
if (!img || !sv || !img.naturalWidth) return;
|
||||||
cv.width = img.naturalWidth;
|
sv.setAttribute('viewBox', '0 0 ' + img.naturalWidth + ' ' + img.naturalHeight);
|
||||||
cv.height = img.naturalHeight;
|
|
||||||
const g = cv.getContext('2d');
|
|
||||||
g.clearRect(0, 0, cv.width, cv.height);
|
|
||||||
g.lineWidth = Math.max(2, cv.width / 400);
|
|
||||||
g.lineJoin = 'round';
|
|
||||||
const shapes = (this.result && this.result.result && this.result.result.overlay) || [];
|
const shapes = (this.result && this.result.result && this.result.result.overlay) || [];
|
||||||
for (const item of shapes) {
|
const VO = (typeof window !== 'undefined' && window.VisionOverlay) || null;
|
||||||
const rows = item.rows || [], cols = item.cols || [];
|
sv.innerHTML = (VO && typeof VO.svg === 'function') ? VO.svg(shapes, { zoom: this.zoom }) : '';
|
||||||
if (!rows.length || rows.length !== cols.length) continue;
|
|
||||||
g.strokeStyle = ROLE_COLORS[item.role] || '#ef4444';
|
|
||||||
g.beginPath();
|
|
||||||
g.moveTo(cols[0], rows[0]);
|
|
||||||
for (let i = 1; i < rows.length; i++) g.lineTo(cols[i], rows[i]);
|
|
||||||
g.stroke();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -737,6 +737,8 @@
|
|||||||
<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 / 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="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>
|
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="vFit('img')">{{ _('Adatta') }}</button>
|
||||||
|
{# #119: salva il frame grezzo mostrato (PNG, senza disegni) #}
|
||||||
|
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="tmfSaveImage(imagePreviewUrl, 'immagine')" title="{{ _('Salva immagine') }}">{{ _('Salva') }}</button>
|
||||||
<span class="text-xs px-1" x-text="vPct('img')"></span>
|
<span class="text-xs px-1" x-text="vPct('img')"></span>
|
||||||
</div>
|
</div>
|
||||||
<!-- #113: viewport fisso 240px; il wrapper (margin auto) e' largo w*zoom: centrato se piu' piccolo, scorrevole se piu' grande -->
|
<!-- #113: viewport fisso 240px; il wrapper (margin auto) e' largo w*zoom: centrato se piu' piccolo, scorrevole se piu' grande -->
|
||||||
@@ -819,6 +821,8 @@
|
|||||||
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="zoomBy(1 / 1.25)" title="{{ _('Riduci') }}">−</button>
|
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="zoomBy(1 / 1.25)" title="{{ _('Riduci') }}">−</button>
|
||||||
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="zoomBy(1.25)" title="{{ _('Ingrandisci') }}">+</button>
|
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="zoomBy(1.25)" title="{{ _('Ingrandisci') }}">+</button>
|
||||||
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="fit()">{{ _('Adatta') }}</button>
|
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="fit()">{{ _('Adatta') }}</button>
|
||||||
|
{# #119: salva il frame grezzo della Prova (PNG, senza overlay) #}
|
||||||
|
<button type="button" class="px-1.5 text-xs rounded hover:bg-white/20" @click="tmfSaveImage(imageSrc, 'prova')" title="{{ _('Salva immagine') }}">{{ _('Salva') }}</button>
|
||||||
<span class="text-xs px-1" x-text="zoomPct"></span>
|
<span class="text-xs px-1" x-text="zoomPct"></span>
|
||||||
</div>
|
</div>
|
||||||
<!-- #104: pan col puntatore (mouse e dito), cursore grab; altezza che segue l'immagine finche' zoom <= Adatta -->
|
<!-- #104: pan col puntatore (mouse e dito), cursore grab; altezza che segue l'immagine finche' zoom <= Adatta -->
|
||||||
@@ -830,7 +834,8 @@
|
|||||||
<div class="relative" :style="zoomW ? ('width:' + zoomW + 'px;height:' + zoomH + 'px') : 'width:100%'">
|
<div class="relative" :style="zoomW ? ('width:' + zoomW + 'px;height:' + zoomH + 'px') : 'width:100%'">
|
||||||
<img x-ref="img" :src="imageSrc" alt="" class="block" draggable="false" style="width: 100%; height: 100%; user-select: none;"
|
<img x-ref="img" :src="imageSrc" alt="" class="block" draggable="false" style="width: 100%; height: 100%; user-select: none;"
|
||||||
@load="onResultImgLoad()">
|
@load="onResultImgLoad()">
|
||||||
<canvas x-ref="overlay" class="absolute inset-0 w-full h-full pointer-events-none"></canvas>
|
{# #121: overlay SVG (tratto a spessore costante a ogni zoom) al posto del canvas; il viewBox lo imposta drawOverlay #}
|
||||||
|
<svg x-ref="overlay" class="absolute inset-0 w-full h-full pointer-events-none" preserveAspectRatio="none"></svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1516,6 +1521,8 @@
|
|||||||
<script src="{{ url_for('static', filename='js/rich-text.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/rich-text.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/vision_recipe.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/vision_recipe.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/vision_overlay.js') }}"></script>{# #121 #}
|
||||||
|
<script src="{{ url_for('static', filename='js/save_image.js') }}"></script>{# #119 #}
|
||||||
<script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/task_acquisition.js') }}"></script>{# #97 #}
|
<script src="{{ url_for('static', filename='js/task_acquisition.js') }}"></script>{# #97 #}
|
||||||
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||||
|
|||||||
@@ -195,10 +195,12 @@
|
|||||||
{{ "%.3f"|format(m.subtask.nominal) }} {{ m.subtask.unit }}
|
{{ "%.3f"|format(m.subtask.nominal) }} {{ m.subtask.unit }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right measure-value font-semibold" style="color: var(--text-primary);">
|
<td class="text-right measure-value font-semibold" style="color: var(--text-primary);">
|
||||||
{{ "%.3f"|format(m.value) }} {{ m.subtask.unit }}
|
{# #118: quota non misurata (value None) -> NaN, senza unita' #}
|
||||||
|
{% if m.value is none %}NaN{% else %}{{ "%.3f"|format(m.value) }} {{ m.subtask.unit }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right measure-value {% if m.deviation > 0 %}text-measure-fail dark:text-red-400{% elif m.deviation < 0 %}text-measure-pass dark:text-green-400{% else %}{% endif %}">
|
<td class="text-right measure-value {% if m.value is not none and m.deviation is not none and m.deviation > 0 %}text-measure-fail dark:text-red-400{% elif m.value is not none and m.deviation is not none and m.deviation < 0 %}text-measure-pass dark:text-green-400{% else %}{% endif %}">
|
||||||
{{ "%+.3f"|format(m.deviation) }}
|
{# #118: deviazione None, o valore None (il blueprint la mette a 0.0) -> NaN #}
|
||||||
|
{% if m.value is none or m.deviation is none %}NaN{% else %}{{ "%+.3f"|format(m.deviation) }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if m.pass_fail == 'pass' %}
|
{% if m.pass_fail == 'pass' %}
|
||||||
|
|||||||
@@ -153,6 +153,20 @@ const MEASUREMENT = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #118: quota non misurata (value null) -> NaN nel valore e nella deviazione; tolleranze nulle restano vuote
|
||||||
|
{
|
||||||
|
const e = csvExport();
|
||||||
|
const nan = { ...MEASUREMENT, value: null, deviation: null, pass_fail: 'fail',
|
||||||
|
subtask: { ...MEASUREMENT.subtask, utl: null, ltl: null } };
|
||||||
|
const row = e.buildMeasurementCSV([nan]).split('\n')[1].split(';');
|
||||||
|
check('#118 value null -> NaN', row[3] === 'NaN', row.join('|'));
|
||||||
|
check('#118 deviazione null -> NaN', row[8] === 'NaN', row.join('|'));
|
||||||
|
check('#118 tolleranze nulle restano vuote', row[6] === '' && row[7] === '', row.join('|'));
|
||||||
|
check('#118 esito fail', row[9] === 'fail', row.join('|'));
|
||||||
|
const ok = e.buildMeasurementCSV([MEASUREMENT]).split('\n')[1].split(';');
|
||||||
|
check('#118 valore presente invariato', ok[3] !== 'NaN' && ok[8] !== 'NaN', ok.join('|'));
|
||||||
|
}
|
||||||
|
|
||||||
if (failures > 0) {
|
if (failures > 0) {
|
||||||
console.error(failures + ' test falliti su csv-export.js');
|
console.error(failures + ' test falliti su csv-export.js');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -139,6 +139,44 @@ delete global.getComputedStyle;
|
|||||||
await wd.loadDxfPreview();
|
await wd.loadDxfPreview();
|
||||||
check('#113 DXF tolto: vista azzerata', wd.views.dxf.w === 0 && wd.views.dxf.zoom === 1);
|
check('#113 DXF tolto: vista azzerata', wd.views.dxf.w === 0 && wd.views.dxf.zoom === 1);
|
||||||
|
|
||||||
|
// #121: overlay SVG al posto del canvas; ridisegno a ogni cambio di zoom (zoomBy, fit)
|
||||||
|
{
|
||||||
|
const calls = [];
|
||||||
|
global.window = { VisionOverlay: { svg: (shapes, o) => { calls.push({ n: shapes.length, zoom: o.zoom }); return '<path d="M0 0"/>'; } } };
|
||||||
|
const attrs = {};
|
||||||
|
const sv = { innerHTML: '', setAttribute: (k, v) => { attrs[k] = v; } };
|
||||||
|
const mk = () => {
|
||||||
|
const x = make(400);
|
||||||
|
x.$refs = { viewport: { clientWidth: 400, clientHeight: 300, scrollLeft: 0, scrollTop: 0 },
|
||||||
|
img: { naturalWidth: 800, naturalHeight: 600 }, overlay: sv };
|
||||||
|
x.result = { result: { overlay: [{ type: 'contour', role: 'found', rows: [1], cols: [2] }] } };
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
const wo = mk();
|
||||||
|
wo.drawOverlay();
|
||||||
|
check('#121 viewBox = dimensioni naturali', attrs.viewBox === '0 0 800 600', attrs.viewBox);
|
||||||
|
check('#121 innerHTML = VisionOverlay.svg', sv.innerHTML === '<path d="M0 0"/>' && calls[0].n === 1);
|
||||||
|
wo.zoomBy(2);
|
||||||
|
check('#121 zoomBy ridisegna col nuovo zoom', calls[calls.length - 1].zoom === wo.zoom && wo.zoom === 2, JSON.stringify(calls));
|
||||||
|
wo.fit();
|
||||||
|
check('#121 fit ridisegna col nuovo zoom', calls[calls.length - 1].zoom === wo.zoom, JSON.stringify(calls));
|
||||||
|
// senza modulo o senza svg: vuoto, nessun errore
|
||||||
|
global.window = {};
|
||||||
|
sv.innerHTML = 'x';
|
||||||
|
let err = null;
|
||||||
|
try { mk().drawOverlay(); } catch (e) { err = e; }
|
||||||
|
check('#121 senza VisionOverlay: vuoto senza errori', err === null && sv.innerHTML === '', String(err));
|
||||||
|
global.window = { VisionOverlay: {} };
|
||||||
|
sv.innerHTML = 'x';
|
||||||
|
try { mk().drawOverlay(); } catch (e) { err = e; }
|
||||||
|
check('#121 senza svg(): vuoto senza errori', err === null && sv.innerHTML === '', String(err));
|
||||||
|
// immagine non ancora caricata: non tocca nulla
|
||||||
|
const wn = mk(); wn.$refs.img.naturalWidth = 0; sv.innerHTML = 'y';
|
||||||
|
wn.drawOverlay();
|
||||||
|
check('#121 immagine non caricata: nessun disegno', sv.innerHTML === 'y');
|
||||||
|
delete global.window;
|
||||||
|
}
|
||||||
|
|
||||||
if (failures) { console.log(failures + ' test falliti'); process.exit(1); }
|
if (failures) { console.log(failures + ' test falliti'); process.exit(1); }
|
||||||
console.log('tutti i test #113 ok');
|
console.log('tutti i test #113 ok');
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""#118: il riepilogo task mostra NaN per una quota non misurata (value None)."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _station(monkeypatch):
|
||||||
|
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_complete_renders_nan_for_null_value(logged_in_client, mock_api_client):
|
||||||
|
mock_api_client.get.side_effect = [
|
||||||
|
{"id": 1, "code": "REC-001", "name": "Test Recipe", "version": 1},
|
||||||
|
[{"id": 1, "title": "Task 1", "order_index": 0, "subtasks": [
|
||||||
|
{"id": 1, "marker_number": 1, "description": "quota mancante", "nominal": 1.0,
|
||||||
|
"unit": "mm"},
|
||||||
|
{"id": 2, "marker_number": 2, "description": "quota ok", "nominal": 2.0,
|
||||||
|
"unit": "mm"}]}],
|
||||||
|
None,
|
||||||
|
{"items": [
|
||||||
|
{"id": 1, "subtask_id": 1, "value": None, "deviation": None, "pass_fail": "fail",
|
||||||
|
"measured_at": "2026-10-06T08:00:00+00:00"},
|
||||||
|
{"id": 2, "subtask_id": 2, "value": 2.5, "pass_fail": "pass",
|
||||||
|
"measured_at": "2026-10-06T08:00:00+00:00"},
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
resp = logged_in_client.get("/measure/complete/1?version_id=5")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
html = resp.get_data(as_text=True)
|
||||||
|
assert "NaN" in html
|
||||||
|
assert "2.500" in html # la quota misurata resta formattata
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Controllo del template del wizard DXF: Salva in due barre zoom e overlay SVG (#119, #121).
|
||||||
|
|
||||||
|
Legge task_editor.html come testo (come test_vision_panel_fit.py), senza renderlo.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TEMPLATE = Path(__file__).resolve().parent.parent / "templates" / "maker" / "task_editor.html"
|
||||||
|
|
||||||
|
|
||||||
|
def _src() -> str:
|
||||||
|
return TEMPLATE.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _bar_of(src: str, marker: str) -> str:
|
||||||
|
"""Testo della barra zoom che contiene il marker (dal div della barra al primo </div>)."""
|
||||||
|
i = src.index(marker)
|
||||||
|
start = src.rfind('absolute top-2 right-2', 0, i)
|
||||||
|
return src[start:src.index("</div>", i)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_prova_overlay_is_svg_not_canvas():
|
||||||
|
# #121: l'overlay della Prova e' un <svg> con preserveAspectRatio none, non piu' un canvas
|
||||||
|
src = _src()
|
||||||
|
i = src.index('x-ref="overlay"')
|
||||||
|
tag = src[src.rfind("<", 0, i):src.index(">", i) + 1]
|
||||||
|
assert tag.startswith("<svg")
|
||||||
|
assert 'preserveAspectRatio="none"' in tag
|
||||||
|
assert "pointer-events-none" in tag
|
||||||
|
assert "<canvas" not in src[i - 200:i + 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_button_in_image_and_prova_bars_only():
|
||||||
|
# #119: Salva nella barra dell'immagine e in quella della Prova, non in quella del DXF
|
||||||
|
src = _src()
|
||||||
|
img_bar = _bar_of(src, "vFit('img')")
|
||||||
|
prova_bar = _bar_of(src, 'zoomBy(1 / 1.25)')
|
||||||
|
dxf_bar = _bar_of(src, "vFit('dxf')")
|
||||||
|
assert "tmfSaveImage(imagePreviewUrl, 'immagine')" in img_bar
|
||||||
|
assert "tmfSaveImage(imageSrc, 'prova')" in prova_bar
|
||||||
|
assert "tmfSaveImage" not in dxf_bar
|
||||||
|
assert src.count("tmfSaveImage(") == 2
|
||||||
|
# il pulsante sta dopo Adatta e prima della percentuale
|
||||||
|
assert img_bar.index("vFit('img')") < img_bar.index("tmfSaveImage") < img_bar.index("vPct('img')")
|
||||||
|
assert prova_bar.index("fit()") < prova_bar.index("tmfSaveImage") < prova_bar.index("zoomPct")
|
||||||
|
|
||||||
|
|
||||||
|
def test_scripts_loaded_before_wizard():
|
||||||
|
# #119 #121: vision_overlay.js e save_image.js prima di dxf_recipe_wizard.js
|
||||||
|
src = _src()
|
||||||
|
wiz = src.index("js/dxf_recipe_wizard.js")
|
||||||
|
assert src.index("js/vision_overlay.js") < wiz
|
||||||
|
assert src.index("js/save_image.js") < wiz
|
||||||
Reference in New Issue
Block a user