diff --git a/scripts/calibrazione_pagina.py b/scripts/calibrazione_pagina.py index bed0557..c964c84 100644 --- a/scripts/calibrazione_pagina.py +++ b/scripts/calibrazione_pagina.py @@ -8,6 +8,10 @@ Parla con le rotte dello stesso servizio: /camera/sources, /camera/anteprima, /camera/parametri, /camera/parametri/salva, /camera/calibration, /camera/calibrate. #84: calibrazione densa, un solo dato dell'operatore (lato dello scacco in mm). #87: anteprima, parametri camera, stile TMF. +#91: due tab, «Acquisizione» e «Calibrazione», nella stessa pagina; l'anteprima resta +visibile in entrambe. Calibrazioni salvate con nome in un archivio unico +(/camera/calibrazioni, /camera/calibrazione-attiva); il calcolo non salva piu': +si salva dal calcolo_id con un nome. Dipende da: niente. E' una stringa, cosi' il pacchetto compilato non ha file dati. """ @@ -75,16 +79,45 @@ PAGINA = r""" .parametro .campo input[type=text] { width: 110px; text-align: right; flex: none; } .parametro .unita { font-size: .82rem; color: var(--secondario); min-width: 2.2em; } .parametro .limiti { font-size: .75rem; color: var(--secondario); } + /* #91: tab Acquisizione | Calibrazione */ + .tabs { display: flex; gap: 6px; margin: 0 0 14px; border-bottom: 1px solid var(--bordo); } + .tabs button { background: transparent; color: var(--secondario); border-radius: 6px 6px 0 0; border-bottom: 3px solid transparent; + flex: 0 1 auto; padding: 8px 18px; } + .tabs button[aria-selected=true] { color: var(--testo); background: var(--pannello); border-bottom-color: var(--accento); font-weight: 600; } + /* #91: calibrazione con nome, elenco delle salvate */ + .scorri { overflow-x: auto; margin-top: 8px; } + .elenco { width: 100%; } + .elenco th { text-align: left; font-size: .78rem; font-weight: 600; color: var(--secondario); padding: 2px 12px 4px 0; white-space: nowrap; } + .elenco td { padding: 5px 12px 5px 0; border-top: 1px solid var(--bordo); vertical-align: top; } + .elenco td:first-child { color: var(--testo); } + .elenco .azioni { white-space: nowrap; } + .elenco button { padding: 3px 9px; font-size: .82rem; margin-right: 4px; } + .segno { display: inline-block; font-size: .72rem; padding: 0 6px; border-radius: 9px; background: var(--ok); color: #052e16; margin-left: 6px; } + .segno.diversa { background: var(--avviso); color: #422006; } + .origine label { display: inline-block; margin: 0 14px 4px 0; color: var(--testo); font-size: .92rem; } + input[type=radio] { accent-color: var(--accento); } @media (max-width: 900px) { .griglia { grid-template-columns: minmax(0, 1fr); } .cornice img { max-height: 60vh; } } + @media (max-width: 500px) { /* #91: utilizzabile anche stretta (400 px) */ + main { padding: 8px; } + .scheda { padding: 10px; } + .tabs button { flex: 1 1 0; padding: 8px 6px; } + .parametro .campo input[type=text] { width: 88px; } + }

Acquisizione camera

+ +
+ + +
+
@@ -132,7 +165,7 @@ PAGINA = r"""
-
+

Sorgente

@@ -151,43 +184,51 @@ PAGINA = r"""
+
+ +
@@ -275,6 +316,7 @@ function cambioSorgente(salva) { fermaAnteprima(); azzeraIndicatori(); dire("msgAnteprima", ""); + dire("msgElenco", ""); // #91 aggiornaStato(); caricaParametri(); if (era) avviaAnteprima(); @@ -282,7 +324,7 @@ function cambioSorgente(salva) { // ---- Anteprima (#87) -var A = { attiva: false, gen: 0, inVolo: false, url: null, nitMax: 0 }; +var A = { attiva: false, gen: 0, inVolo: false, url: null, nitMax: 0, ris: null }; // #91: ris = {w, h} dell'ultimo frame function azzeraIndicatori() { A.nitMax = 0; @@ -290,6 +332,8 @@ function azzeraIndicatori() { $("saturati").className = "valore"; $("barraNitidezza").style.width = "0"; $("raddrizzato").textContent = " "; + A.ris = null; // #91: risoluzione del frame non piu' nota + segnaRisoluzioni(); } function pulsantiAnteprima() { @@ -322,7 +366,12 @@ function aggiornaIndicatori(h) { } if (isFinite(media)) $("media").textContent = fmt(media, 1); var w = h.get("X-Larghezza"), a = h.get("X-Altezza"); - if (w && a) $("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0); + if (w && a) { + $("dimensioni").textContent = fmt(w, 0) + " x " + fmt(a, 0); + // #91: risoluzione nota del frame, per segnare le calibrazioni che non combaciano + var nw = parseInt(w, 10), na = parseInt(a, 10); + if (!A.ris || A.ris.w !== nw || A.ris.h !== na) { A.ris = { w: nw, h: na }; segnaRisoluzioni(); } + } var r = h.get("X-Raddrizzato"); $("raddrizzato").textContent = r === "si" ? "raddrizzata dalla calibrazione" : r === "no" ? "non raddrizzata" : " "; } @@ -611,6 +660,29 @@ $("salvaParametri").onclick = async function () { } }; +// ---- Tab (#91): «Acquisizione» | «Calibrazione»; l'anteprima sta fuori dai pannelli e resta sempre visibile + +function scegliTab(nome, salva) { + var cal = nome === "cal"; + $("tabAcq").setAttribute("aria-selected", cal ? "false" : "true"); + $("tabCal").setAttribute("aria-selected", cal ? "true" : "false"); + $("tabAcq").tabIndex = cal ? -1 : 0; + $("tabCal").tabIndex = cal ? 0 : -1; + $("pannelloAcq").hidden = cal; + $("pannelloCal").hidden = !cal; + if (salva) { memoria("tmf.acq.tab", nome); if (cal) caricaCalibrazioni(); } // l'elenco si rilegge entrando +} +$("tabAcq").onclick = function () { scegliTab("acq", true); }; +$("tabCal").onclick = function () { scegliTab("cal", true); }; +[$("tabAcq"), $("tabCal")].forEach(function (b) { // frecce sinistra/destra fra le tab + b.onkeydown = function (e) { + if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return; + var cal = b.id === "tabAcq"; + scegliTab(cal ? "cal" : "acq", true); + $(cal ? "tabCal" : "tabAcq").focus(); + }; +}); + // ---- Calibrazione (logica di #83 e #84; #87: lato precompilato, data italiana, conferma in pagina) function righe(d) { @@ -644,37 +716,6 @@ function tabella(el, d) { }); } -async function aggiornaStato() { - var sorgente = $("sorgente").value; - var stato = $("stato"); - annullaConferma(); - $("elimina").disabled = true; - if (!sorgente) { stato.textContent = "-"; $("statoSorgente").textContent = "-"; return; } - try { - var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente)); - if (sorgente !== $("sorgente").value) return; // nel frattempo si e' cambiata sorgente - if (r.status === 404) { - stato.textContent = "Sorgente non calibrata."; - $("statoSorgente").textContent = "non calibrata"; - return; - } - if (!r.ok) { stato.textContent = await dettaglio(r); return; } - var d = await r.json(); - stato.textContent = ""; - var t = document.createElement("table"); - tabella(t, d); - var quando = document.createElement("div"); - quando.textContent = "Calibrata il " + dataItaliana(d.calibrated_at); - stato.appendChild(t); - stato.appendChild(quando); - $("elimina").disabled = false; - $("statoSorgente").textContent = "calibrata: " + fmt(d.px_per_mm, 2) + " px/mm"; - if (d.square_mm) $("quadrato").value = fmtCasella(d.square_mm); // #87: parte dal lato gia' usato - } catch (e) { - stato.textContent = "Servizio non raggiungibile."; - } -} - // avvisi sulla qualita' (solo calibrazione densa): copertura bassa o residuo alto function avvisi(d) { var a = []; @@ -687,78 +728,292 @@ function avvisi(d) { return a.join(" "); } -async function calcola(conFile, salva) { +// ---- Calibrazioni con nome (#91): archivio unico, una attiva per sorgente + +var C = { elenco: [], attiva: null, celle: [], gen: 0, confermaNome: null, confermaBtn: null, confermaTimer: null }; +var S = { nome: null, timer: null }; // secondo clic su «Sovrascrivi» entro 4 s +var JSON_H = { "Content-Type": "application/json" }; + +function descriviOrigine(o) { return String(o || "-").replace(":", " "); } + +// stato della calibrazione attiva sulla sorgente selezionata (come prima di #91, piu' il nome) +async function aggiornaStato() { var sorgente = $("sorgente").value; - if (!sorgente) { messaggio("Scegliere una sorgente.", "errore"); return; } + var stato = $("stato"); + annullaConferma(); + $("calSorgente").textContent = sorgente ? $("sorgente").options[$("sorgente").selectedIndex].textContent : "-"; // #91 + if (!sorgente) { stato.textContent = "-"; $("statoSorgente").textContent = "-"; caricaCalibrazioni(); return; } + try { + var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente)); + if (sorgente !== $("sorgente").value) return; // nel frattempo si e' cambiata sorgente + if (r.status === 404) { + stato.textContent = "Sorgente non calibrata."; + $("statoSorgente").textContent = "non calibrata"; + } else if (!r.ok) { + stato.textContent = await dettaglio(r); + } else { + var d = await r.json(); + stato.textContent = ""; + var titolo = document.createElement("div"); + titolo.textContent = "Calibrazione attiva: " + (d.nome ? "«" + d.nome + "»" : "(senza nome)"); + titolo.style.fontWeight = "600"; + var t = document.createElement("table"); + tabella(t, d); + var quando = document.createElement("div"); + quando.textContent = "Calibrata il " + dataItaliana(d.calibrated_at); + stato.appendChild(titolo); + stato.appendChild(t); + stato.appendChild(quando); + $("statoSorgente").textContent = "calibrata: " + fmt(d.px_per_mm, 2) + " px/mm"; + if (d.square_mm) $("quadrato").value = fmtCasella(d.square_mm); // #87: parte dal lato gia' usato + } + } catch (e) { + stato.textContent = "Servizio non raggiungibile."; + } + caricaCalibrazioni(); +} + +// #91: elenco unico delle calibrazioni salvate; "attiva" e' quella della sorgente selezionata +async function caricaCalibrazioni() { + var sorgente = $("sorgente").value; + var g = ++C.gen; + try { + var r = await fetch("/camera/calibrazioni" + (sorgente ? "?source=" + encodeURIComponent(sorgente) : "")); + if (g !== C.gen) return; + if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; } + var j = await r.json(); + if (g !== C.gen) return; + C.elenco = j.calibrazioni || []; + C.attiva = j.attiva || null; + disegnaElenco(); + } catch (e) { + if (g === C.gen) dire("msgElenco", "Servizio non raggiungibile.", "errore"); + } +} + +function cella(tr, testo) { + var td = document.createElement("td"); + td.textContent = testo; + tr.appendChild(td); + return td; +} + +function bottone(testo, classe, fn) { + var b = document.createElement("button"); + b.textContent = testo; + b.className = classe || ""; + b.onclick = fn; + return b; +} + +function disegnaElenco() { + var t = $("calibrazioni"); + annullaConferma(); + t.textContent = ""; + C.celle = []; + if (!C.elenco.length) { + var trVuota = document.createElement("tr"); + cella(trVuota, "Nessuna calibrazione salvata."); + t.appendChild(trVuota); + return; + } + var testa = document.createElement("tr"); + ["Nome", "Risoluzione", "px/mm", "Metodo", "Data", "Origine", ""].forEach(function (h) { + var th = document.createElement("th"); + th.textContent = h; + testa.appendChild(th); + }); + t.appendChild(testa); + C.elenco.forEach(function (c) { + var tr = document.createElement("tr"); + var tdNome = cella(tr, c.nome); + var attiva = C.attiva !== null && c.nome === C.attiva; + if (attiva) { + var s1 = document.createElement("span"); + s1.className = "segno"; + s1.textContent = "attiva"; + tdNome.appendChild(s1); + } + var s2 = document.createElement("span"); // «risoluzione diversa» dal frame della sorgente (si accende in segnaRisoluzioni) + s2.className = "segno diversa"; + s2.textContent = "risoluzione diversa"; + s2.hidden = true; + tdNome.appendChild(s2); + C.celle.push({ w: Number(c.image_width), h: Number(c.image_height), span: s2 }); + cella(tr, c.image_width && c.image_height ? fmt(c.image_width, 0) + "×" + fmt(c.image_height, 0) : "-"); + cella(tr, fmt(c.px_per_mm, 3)); + cella(tr, c.metodo === "densa" ? "densa" : "planare"); + cella(tr, c.calibrated_at ? dataItaliana(c.calibrated_at) : "-"); + cella(tr, descriviOrigine(c.origine)); + var az = document.createElement("td"); + az.className = "azioni"; + var bc = bottone("Carica", "", function () { caricaNome(c.nome); }); + bc.disabled = !$("sorgente").value; + az.appendChild(bc); + if (attiva) az.appendChild(bottone("Scollega", "secondario", scollega)); + az.appendChild(bottone("Elimina", "secondario", function (e) { eliminaNome(c.nome, e.currentTarget); })); + tr.appendChild(az); + t.appendChild(tr); + }); + segnaRisoluzioni(); +} + +// #91: «risoluzione diversa» se il frame noto della sorgente non combacia con quello della calibrazione +function segnaRisoluzioni() { + (C.celle || []).forEach(function (c) { + c.span.hidden = !(A.ris && c.w > 0 && c.h > 0 && (A.ris.w !== c.w || A.ris.h !== c.h)); + }); +} + +// «Carica»: la calibrazione diventa quella attiva della sorgente. Un errore (es. risoluzione diversa) non ferma l'anteprima. +async function caricaNome(nome) { + var sorgente = $("sorgente").value; + if (!sorgente) { dire("msgElenco", "Scegliere una sorgente nella tab Acquisizione.", "errore"); return; } + dire("msgElenco", "Caricamento di «" + nome + "»...", ""); + try { + var r = await fetch("/camera/calibrazione-attiva", { method: "PUT", headers: JSON_H, body: JSON.stringify({ source: sorgente, nome: nome }) }); + if (sorgente !== $("sorgente").value) return; + if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; } + dire("msgElenco", "Calibrazione «" + nome + "» caricata sulla sorgente.", "ok"); + $("raddrizzato").textContent = $("grezzo").checked ? "non raddrizzata" : "raddrizzata dalla calibrazione"; // il prossimo frame conferma + aggiornaStato(); + } catch (e) { + dire("msgElenco", "Servizio non raggiungibile.", "errore"); + } +} + +// «Scollega»: la sorgente torna senza calibrazione, la libreria resta +async function scollega() { + var sorgente = $("sorgente").value; + if (!sorgente) return; + try { + var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" }); + dire("msgElenco", r.ok ? "Calibrazione scollegata dalla sorgente (resta fra le salvate)." : await dettaglio(r), r.ok ? "ok" : "errore"); + if (r.ok) $("raddrizzato").textContent = "non raddrizzata"; + } catch (e) { + dire("msgElenco", "Servizio non raggiungibile.", "errore"); + } + aggiornaStato(); +} + +// «Elimina» di una riga: conferma in pagina con un secondo clic entro 4 s (niente finestre del browser) +function annullaConferma() { + clearTimeout(C.confermaTimer); + C.confermaTimer = null; + if (C.confermaBtn) C.confermaBtn.textContent = "Elimina"; + C.confermaBtn = null; + C.confermaNome = null; +} +async function eliminaNome(nome, btn) { + if (C.confermaNome !== nome) { + annullaConferma(); + C.confermaNome = nome; + C.confermaBtn = btn; + btn.textContent = "Confermi? Clic ancora"; + C.confermaTimer = setTimeout(annullaConferma, 4000); + return; + } + annullaConferma(); + try { + var r = await fetch("/camera/calibrazioni?nome=" + encodeURIComponent(nome), { method: "DELETE" }); + dire("msgElenco", r.ok ? "Calibrazione «" + nome + "» eliminata." : await dettaglio(r), r.ok ? "ok" : "errore"); + } catch (e) { + dire("msgElenco", "Servizio non raggiungibile.", "errore"); + } + aggiornaStato(); +} + +// #91: il calcolo non salva mai; il servizio tiene il risultato e ne da' il calcolo_id per «Salva» +async function calcola() { + var daFile = $("origFile").checked; + var sorgente = $("sorgente").value; + if (!daFile && !sorgente) { messaggio("Scegliere una sorgente nella tab Acquisizione.", "errore"); return; } var lato = leggiNumero($("quadrato").value); if (!(lato > 0)) { messaggio("Il lato dello scacco deve essere un numero maggiore di zero.", "errore"); return; } var f = new FormData(); - f.append("source", sorgente); f.append("square_mm", String(lato)); // al servizio sempre con il punto - f.append("save", salva ? "true" : "false"); - if (conFile) { + if (daFile) { var file = $("file").files[0]; if (!file) { messaggio("Scegliere un file immagine.", "errore"); return; } f.append("image", file); + } else { + f.append("source", sorgente); } - var eraAttiva = A.attiva; // #87: l'anteprima si sospende e poi riprende + var eraAttiva = !daFile && A.attiva; // #87: da sorgente l'anteprima si sospende e poi riprende if (eraAttiva) fermaAnteprima(); messaggio("Calcolo in corso...", ""); - ["acquisisci", "carica", "salva"].forEach(function (i) { $(i).disabled = true; }); + ["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; }); try { var r = await fetch("/camera/calibrate", { method: "POST", body: f }); if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; } var d = await r.json(); - ultimo = { conFile: conFile, dati: d }; + ultimo = { dati: d }; tabella($("tabella"), d); $("overlay").src = "data:image/png;base64," + d.overlay_png; $("avviso").textContent = avvisi(d); + $("origineCalcolo").textContent = "Origine: " + descriviOrigine(d.origine); $("risultato").hidden = false; - messaggio(d.saved ? "Calibrazione salvata." : "Calibrazione riuscita: controllare e salvare.", "ok"); - if (d.saved) aggiornaStato(); + annullaSovrascrivi(); + dire("msgSalva", ""); + messaggio("Calcolo riuscito: dare un nome e salvare.", "ok"); } catch (e) { messaggio("Servizio non raggiungibile.", "errore"); } finally { - ["acquisisci", "carica", "salva"].forEach(function (i) { $(i).disabled = false; }); + $("acquisisci").disabled = false; + $("salva").disabled = false; if (eraAttiva) avviaAnteprima(); } } -$("acquisisci").onclick = function () { calcola(false, false); }; -$("carica").onclick = function () { calcola(true, false); }; -// «Salva» rifa il calcolo con save=true sulla stessa origine (altra acquisizione se da camera) -$("salva").onclick = function () { calcola(ultimo ? ultimo.conFile : false, true); }; +// «Salva» con nome; se il nome c'e' gia' il pulsante chiede un secondo clic per sovrascrivere +function annullaSovrascrivi() { + clearTimeout(S.timer); + S.timer = null; + S.nome = null; + $("salva").textContent = "Salva"; +} +async function salvaNome() { + if (!ultimo || !ultimo.dati.calcolo_id) { dire("msgSalva", "Nessun calcolo da salvare: calcolare prima.", "errore"); return; } + var nome = $("nomeCal").value.trim(); + if (!nome) { dire("msgSalva", "Scrivere un nome per la calibrazione.", "errore"); return; } + if (nome.length > 60) { dire("msgSalva", "Il nome può avere al massimo 60 caratteri.", "errore"); return; } + var sovrascrivi = S.timer !== null && S.nome === nome.toLowerCase(); // secondo clic sullo stesso nome + annullaSovrascrivi(); + $("salva").disabled = true; + try { + var r = await fetch("/camera/calibrazioni", { + method: "POST", headers: JSON_H, + body: JSON.stringify({ calcolo_id: ultimo.dati.calcolo_id, nome: nome, sovrascrivi: sovrascrivi }) + }); + if (r.status === 409) { + S.nome = nome.toLowerCase(); + $("salva").textContent = "Sovrascrivi «" + nome + "»? Clic ancora"; + S.timer = setTimeout(annullaSovrascrivi, 4000); + dire("msgSalva", await dettaglio(r), "errore"); + return; + } + if (!r.ok) { dire("msgSalva", await dettaglio(r), "errore"); return; } + dire("msgSalva", "Calibrazione «" + nome + "» salvata. Per usarla su una sorgente: «Carica» nell'elenco.", "ok"); + caricaCalibrazioni(); + } catch (e) { + dire("msgSalva", "Servizio non raggiungibile.", "errore"); + } finally { + $("salva").disabled = false; + } +} + +$("acquisisci").onclick = calcola; +$("salva").onclick = salvaNome; +$("nomeCal").onkeydown = function (e) { if (e.key === "Enter") salvaNome(); }; // #87: il pulsante del file e' in italiano, l'input nativo resta nascosto $("scegliFile").onclick = function () { $("file").click(); }; $("file").onchange = function () { $("nomeFile").textContent = $("file").files[0] ? $("file").files[0].name : "nessuna immagine scelta"; -}; - -// #87: «Elimina calibrazione» chiede conferma in pagina con un secondo clic (niente finestre del browser) -var confermaTimer = null; -function annullaConferma() { - clearTimeout(confermaTimer); - confermaTimer = null; - $("elimina").textContent = "Elimina calibrazione"; -} -$("elimina").onclick = async function () { - if (!confermaTimer) { - $("elimina").textContent = "Confermi? Clic ancora per eliminare"; - confermaTimer = setTimeout(annullaConferma, 4000); - return; - } - annullaConferma(); - var sorgente = $("sorgente").value; - try { - var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" }); - messaggio(r.ok ? "Calibrazione eliminata." : await dettaglio(r), r.ok ? "ok" : "errore"); - } catch (e) { - messaggio("Servizio non raggiungibile.", "errore"); - } - aggiornaStato(); + if ($("file").files[0]) $("origFile").checked = true; // #91: scelto un file, l'origine diventa il file }; $("sorgente").onchange = function () { cambioSorgente(true); }; +scegliTab(memoria("tmf.acq.tab") === "cal" ? "cal" : "acq", false); // #91: tab ricordata caricaSorgenti(); diff --git a/scripts/tests/test_pagina_acquisizione.py b/scripts/tests/test_pagina_acquisizione.py index 052ce57..247a1f7 100644 --- a/scripts/tests/test_pagina_acquisizione.py +++ b/scripts/tests/test_pagina_acquisizione.py @@ -32,7 +32,7 @@ def test_id_principali(): attesi = {"sorgente", "avvia", "ferma", "scatta", "grezzo", "mirinoCasella", "immagine", "nitidezza", "nitidezzaMax", "saturati", "media", "dimensioni", "parametri", "salvaParametri", "quadrato", "acquisisci", "scegliFile", "file", - "carica", "salva", "elimina", "stato", "statoSorgente"} + "salva", "stato", "statoSorgente"} # #91: carica/elimina sono ora per riga, nell'elenco assert attesi <= ids, attesi - ids # ogni id usato dal JS con $("...") esiste nel markup usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA)) diff --git a/scripts/tests/test_pagina_tab.py b/scripts/tests/test_pagina_tab.py new file mode 100644 index 0000000..63ed98a --- /dev/null +++ b/scripts/tests/test_pagina_tab.py @@ -0,0 +1,75 @@ +"""Test della pagina a due tab (#91): contenuto statico di PAGINA, senza servizio.""" + +import importlib.util +import re +from pathlib import Path + +_FILE = Path(__file__).resolve().parents[1] / "calibrazione_pagina.py" +_spec = importlib.util.spec_from_file_location("calibrazione_pagina_tab", _FILE) +_mod = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_mod) +PAGINA = _mod.PAGINA + + +def test_due_tab_con_ruolo_e_selezione(): + assert re.search(r'" in PAGINA and ">Calibrazione" in PAGINA + assert 'role="tablist"' in PAGINA and 'role="tabpanel"' in PAGINA + assert 'aria-selected", cal ?' in PAGINA # la selezione cambia da JS + assert '"tmf.acq.tab"' in PAGINA # tab scelta ricordata (via memoria(), protetta) + + +def test_anteprima_fuori_dai_pannelli(): + # l'anteprima sta nella colonna sinistra, prima dei due pannelli: visibile in entrambe le tab + i_ant = PAGINA.index('id="sezioneAnteprima"') + i_acq = PAGINA.index('id="pannelloAcq"') + i_cal = PAGINA.index('id="pannelloCal"') + assert i_ant < i_acq < i_cal + # Sorgente e Parametri nel pannello Acquisizione, la calibrazione in quello Calibrazione + assert i_acq < PAGINA.index('id="sezioneSorgente"') < PAGINA.index('id="sezioneParametri"') < i_cal + assert PAGINA.index('id="sezioneCalibrazione"') > i_cal + + +def test_id_del_pannello_calibrazione(): + ids = set(re.findall(r'id="([^"]+)"', PAGINA)) + attesi = {"calSorgente", "origSorgente", "origFile", "scegliFile", "file", "quadrato", "acquisisci", + "nomeCal", "salva", "msgSalva", "calibrazioni", "msgElenco", "stato", "risultato", "overlay"} + assert attesi <= ids, attesi - ids + usati = set(re.findall(r'\$\("([^"]+)"\)', PAGINA)) + assert usati <= ids, usati - ids + assert len(re.findall(r'id="([^"]+)"', PAGINA)) == len(ids) # nessun id doppio + + +def test_testi_del_pannello(): + for testo in ("Sorgente selezionata", "Immagine da file", "Scegli immagine...", "Calcola", + "Calibrazioni salvate", "si cambia nella tab Acquisizione", "risoluzione diversa", + "Carica", "Elimina", "Scollega", "Sovrascrivi «", "Clic ancora", "Nome"): + assert testo in PAGINA, testo + + +def test_rotte_del_contratto(): + for rotta in ("/camera/calibrazioni", "/camera/calibrazione-attiva", "/camera/calibrate", + "/camera/calibration?source="): + assert '"' + rotta in PAGINA, rotta + for parola in ("calcolo_id", "sovrascrivi", "method: \"PUT\"", "method: \"DELETE\"", "X-Larghezza", "X-Altezza"): + assert parola in PAGINA, parola + # il calcolo non salva piu': il campo save sparisce + assert 'append("save"' not in PAGINA + + +def test_nessuna_finestra_del_browser_e_offline(): + for vietato in ("alert(", "confirm(", "prompt(", "http://", "https://"): + assert vietato not in PAGINA, vietato + + +def test_acquisizione_di_87_90_conservata(): + # anteprima, indicatori, mirino, grezzo, parametri camera e ciclo a 100 ms restano + for parola in ("Parametri camera", "Immagine grezza (senza correzione)", "Mirino centrale", + "Math.max(100 - (Date.now() - t0), 0)", "/camera/anteprima", "/camera/parametri"): + assert parola in PAGINA, parola + + +def test_pagina_stretta(): + assert "@media (max-width: 500px)" in PAGINA + assert "overflow-x: auto" in PAGINA # l'elenco scorre di lato invece di rompere la pagina