maker: anteprima del DXF nel wizard «Crea ricetta da DXF» (Refs #101)
Proxy Flask /maker/api/vision/dxf-preview (piano B), stato e funzioni dxfPreview* nel wizard, riquadro SVG con L x H sotto il campo .dxf, traduzioni e test (proxy, JS con node, pagina). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* #101: anteprima DXF del wizard «Crea ricetta da DXF», con plain node.
|
||||
* Copre dxfPreviewPaths, dxfPreviewViewBox, dxfPreviewSummary e lo scarto
|
||||
* di una risposta arrivata dopo un cambio di file.
|
||||
*
|
||||
* Run from src/frontend/flask_app: node tests/js/test_dxf_preview.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 PREVIEW = {
|
||||
contorni: [[[0, -5], [12.3, -5], [12.3, 0], [0, 0], [0, -5]], [[1, -1]]],
|
||||
bbox: { xmin: 0, ymin: -5.1, xmax: 12.3, ymax: 0 },
|
||||
larghezza_mm: 12.3, altezza_mm: 5.1, entita: 2, punti: 6,
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const w = dxfRecipeWizard(1, () => 'tok', { dxfEntities: 'entità' });
|
||||
|
||||
check('senza anteprima: nessun path', w.dxfPreviewPaths().length === 0);
|
||||
check('senza anteprima: summary vuoto', w.dxfPreviewSummary === '');
|
||||
|
||||
w.dxfPreview = PREVIEW;
|
||||
const paths = w.dxfPreviewPaths();
|
||||
check('un contorno con >1 punti diventa una polyline (quello da 1 punto no)', paths.length === 1);
|
||||
check('formato points', paths[0] === '0,-5 12.3,-5 12.3,0 0,0 0,-5', paths[0]);
|
||||
|
||||
const vb = w.dxfPreviewViewBox().split(' ').map(Number);
|
||||
const m = Math.max(0.5, 0.05 * 12.3);
|
||||
check('viewBox con margine 5%', Math.abs(vb[0] + m) < 1e-3 && Math.abs(vb[2] - (12.3 + 2 * m)) < 1e-3, vb.join(' '));
|
||||
|
||||
// Disegno minuscolo: margine minimo 0.5 mm
|
||||
w.dxfPreview = { ...PREVIEW, bbox: { xmin: 0, ymin: 0, xmax: 1, ymax: 1 } };
|
||||
check('margine minimo 0.5 mm', Math.abs(w.dxfPreviewViewBox().split(' ').map(Number)[0] + 0.5) < 1e-3);
|
||||
|
||||
w.dxfPreview = PREVIEW;
|
||||
const s = w.dxfPreviewSummary;
|
||||
check('summary L × H mm · N entità', s.includes('×') && s.includes('mm') && s.endsWith('2 entità')
|
||||
&& /12[,.]30/.test(s) && /5[,.]10/.test(s), s);
|
||||
|
||||
// Risposta tardiva scartata: due scelte di file, la prima risponde per ultima
|
||||
const resolvers = [];
|
||||
global.fetch = () => new Promise(res => resolvers.push(res));
|
||||
global.FormData = class { append() {} };
|
||||
const w2 = dxfRecipeWizard(1, () => 'tok', {});
|
||||
const reply = (body, ok = true) => ({ ok, json: async () => body });
|
||||
w2.dxfFile = { name: 'a.dxf' };
|
||||
const p1 = w2.loadDxfPreview();
|
||||
w2.dxfFile = { name: 'b.dxf' };
|
||||
const p2 = w2.loadDxfPreview();
|
||||
resolvers[1](reply(PREVIEW));
|
||||
await p2;
|
||||
resolvers[0](reply({ ...PREVIEW, entita: 99 }));
|
||||
await p1;
|
||||
check('la risposta vecchia non sovrascrive la nuova', w2.dxfPreview && w2.dxfPreview.entita === 2);
|
||||
check('busy spento', w2.dxfPreviewBusy === false);
|
||||
|
||||
// File tolto: tutto azzerato
|
||||
w2.dxfFile = null;
|
||||
await w2.loadDxfPreview();
|
||||
check('file tolto azzera anteprima ed errore', w2.dxfPreview === null && w2.dxfPreviewError === '');
|
||||
|
||||
// Errore 422 con detail
|
||||
global.fetch = async () => reply({ detail: 'DXF illeggibile: x' }, false);
|
||||
w2.dxfFile = { name: 'c.dxf' };
|
||||
await w2.loadDxfPreview();
|
||||
check('errore col detail', w2.dxfPreviewError === 'DXF illeggibile: x' && w2.dxfPreview === null, w2.dxfPreviewError);
|
||||
|
||||
if (failures) { console.log(failures + ' fallimenti'); process.exit(1); }
|
||||
console.log('tutto ok');
|
||||
})();
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Runs the plain-node DXF preview behaviour test (tests/js/test_dxf_preview.js).
|
||||
|
||||
Same thin pytest wrapper pattern as test_numpad_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#101.
|
||||
"""
|
||||
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_preview.js"
|
||||
|
||||
|
||||
def test_dxf_preview_js_behaviour():
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node non installato: test JS di dxf_recipe_wizard.js 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_preview.js fallito (exit {result.returncode})\n"
|
||||
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
||||
)
|
||||
@@ -210,6 +210,49 @@ class TestDxfCompareProxy:
|
||||
mock_api_client.post.assert_not_called()
|
||||
|
||||
|
||||
class TestDxfPreviewProxy:
|
||||
"""#101: anteprima del DXF scelto nel wizard (proxy verso il backend)."""
|
||||
URL = "/maker/api/vision/dxf-preview"
|
||||
OK = {
|
||||
"contorni": [[[0.0, -5.0], [10.0, -5.0], [10.0, 0.0], [0.0, 0.0]]],
|
||||
"bbox": {"xmin": 0.0, "ymin": -5.0, "xmax": 10.0, "ymax": 0.0},
|
||||
"larghezza_mm": 10.0, "altezza_mm": 5.0, "entita": 1, "punti": 4,
|
||||
}
|
||||
|
||||
def _post(self, client, with_file=True):
|
||||
data = {"dxf": (io.BytesIO(b"0\nEOF"), "d.dxf")} if with_file else {}
|
||||
return client.post(self.URL, data=data, content_type="multipart/form-data")
|
||||
|
||||
def test_forwards_file(self, logged_in_client, mock_api_client):
|
||||
mock_api_client.post.return_value = self.OK
|
||||
resp = self._post(logged_in_client)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()["larghezza_mm"] == 10.0
|
||||
args, kwargs = mock_api_client.post.call_args
|
||||
assert args[0] == "/api/vision/dxf-preview"
|
||||
assert set(kwargs["files"]) == {"dxf"}
|
||||
assert kwargs["files"]["dxf"][0] == "d.dxf"
|
||||
assert kwargs["files"]["dxf"][1] == b"0\nEOF"
|
||||
|
||||
def test_missing_file_is_400(self, logged_in_client, mock_api_client):
|
||||
resp = self._post(logged_in_client, with_file=False)
|
||||
assert resp.status_code == 400
|
||||
mock_api_client.post.assert_not_called()
|
||||
|
||||
def test_backend_422_propagates(self, logged_in_client, mock_api_client):
|
||||
mock_api_client.post.return_value = {
|
||||
"error": True, "status_code": 422, "detail": "DXF illeggibile: x"}
|
||||
resp = self._post(logged_in_client)
|
||||
assert resp.status_code == 422
|
||||
assert resp.get_json()["detail"] == "DXF illeggibile: x"
|
||||
|
||||
def test_requires_maker_role(self, logged_in_client, mock_api_client):
|
||||
with logged_in_client.session_transaction() as sess:
|
||||
sess["user"] = {**sess["user"], "roles": ["MeasurementTec"]}
|
||||
assert self._post(logged_in_client).status_code == 403
|
||||
mock_api_client.post.assert_not_called()
|
||||
|
||||
|
||||
class TestPages:
|
||||
def test_stations_page_has_calibration_cell(self, logged_in_client):
|
||||
with patch("blueprints.admin.api_client") as admin_api:
|
||||
@@ -236,4 +279,5 @@ class TestPages:
|
||||
assert "dxfRecipeWizard(" in html
|
||||
assert "@dxf-recipe-saved" in html
|
||||
assert 'x-model="mmPx"' in html # #98: campo mm/px a mano
|
||||
assert "dxfPreviewPaths()" in html # #101: anteprima SVG del DXF
|
||||
assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html
|
||||
|
||||
Reference in New Issue
Block a user