Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
- Durante il calcolo l'anteprima si sospende.
-
-
-
-
- nessuna immagine scelta
-
-
+ Calcolando dalla sorgente l'anteprima si sospende e poi riprende.
Risultato del calcolo
+
+
- Da camera, salvando si acquisisce e si calcola di nuovo: tenere la scacchiera ferma.
+
+
-
- Calibrazione salvata
-
-
-
-
-
-
+
+
Calibrazioni salvate
+
-
+
+
@@ -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
+ 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