acquisitore: elenco sorgenti in cache, calibrata chiesta solo se serve (Refs #92)
Servizio: la ricerca camere di /camera/sources resta 60 s in cache (lock, riscaldamento all'avvio, ?aggiorna=true la forza); le immagini caricate si leggono sempre fresche. Pagina: l'immagine caricata entra nella select senza rileggere l'elenco, pulsante Aggiorna, nessuna richiesta calibrata se la sorgente non ha calibrazione attiva o dopo un 409. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,6 +140,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
<label for="sorgente">Camera o immagine</label>
|
<label for="sorgente">Camera o immagine</label>
|
||||||
<select id="sorgente"></select>
|
<select id="sorgente"></select>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="aggiornaSorgenti" class="secondario" type="button" title="Cerca di nuovo le camere collegate">Aggiorna</button>
|
||||||
<button id="caricaImmagine" class="secondario" type="button">Carica immagine da file...</button>
|
<button id="caricaImmagine" class="secondario" type="button">Carica immagine da file...</button>
|
||||||
<input id="fileImmagine" type="file" accept="image/*" hidden>
|
<input id="fileImmagine" type="file" accept="image/*" hidden>
|
||||||
<button id="rimuoviImmagine" class="secondario" type="button" hidden>Rimuovi immagine</button>
|
<button id="rimuoviImmagine" class="secondario" type="button" hidden>Rimuovi immagine</button>
|
||||||
@@ -312,12 +313,13 @@ async function dettaglio(risposta) {
|
|||||||
// ---- Sorgente
|
// ---- Sorgente
|
||||||
|
|
||||||
// #92: scegli = nome da selezionare (immagine appena caricata); senza, l'ultima scelta ricordata
|
// #92: scegli = nome da selezionare (immagine appena caricata); senza, l'ultima scelta ricordata
|
||||||
async function caricaSorgenti(scegli) {
|
// #92: aggiorna = true forza la ricerca camere del servizio (camera appena collegata); di norma e' in cache
|
||||||
|
async function caricaSorgenti(scegli, aggiorna) {
|
||||||
var sel = $("sorgente");
|
var sel = $("sorgente");
|
||||||
var prima = sel.value;
|
var prima = sel.value;
|
||||||
sel.textContent = "";
|
sel.textContent = "";
|
||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/sources");
|
var r = await fetch("/camera/sources" + (aggiorna ? "?aggiorna=true" : ""));
|
||||||
var j = await r.json();
|
var j = await r.json();
|
||||||
(j.sources || []).forEach(function (s) {
|
(j.sources || []).forEach(function (s) {
|
||||||
var o = document.createElement("option");
|
var o = document.createElement("option");
|
||||||
@@ -341,6 +343,27 @@ function sorgenteImmagine() {
|
|||||||
return sel.selectedIndex >= 0 && sel.options[sel.selectedIndex].dataset.kind === "image";
|
return sel.selectedIndex >= 0 && sel.options[sel.selectedIndex].dataset.kind === "image";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #92: aggiunge (o aggiorna) la voce dell'immagine {source, label, kind} nella select e la seleziona
|
||||||
|
function aggiungiSorgente(j) {
|
||||||
|
var sel = $("sorgente");
|
||||||
|
var o = null;
|
||||||
|
for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === j.source) o = sel.options[i];
|
||||||
|
if (!o) { o = document.createElement("option"); o.value = j.source; sel.appendChild(o); }
|
||||||
|
o.textContent = j.label || j.source;
|
||||||
|
o.dataset.kind = j.kind || "image";
|
||||||
|
sel.value = j.source;
|
||||||
|
cambioSorgente(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #92: «Aggiorna» = ricerca camere nuova (sul servizio puo' durare ~10 s)
|
||||||
|
$("aggiornaSorgenti").onclick = async function () {
|
||||||
|
$("aggiornaSorgenti").disabled = true;
|
||||||
|
dire("msgSorgente", "Ricerca delle camere in corso...", "");
|
||||||
|
await caricaSorgenti(undefined, true);
|
||||||
|
dire("msgSorgente", "Elenco aggiornato.", "ok");
|
||||||
|
$("aggiornaSorgenti").disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
// #92: «Carica immagine da file...» -> POST /camera/immagini, poi la nuova immagine diventa la sorgente
|
// #92: «Carica immagine da file...» -> POST /camera/immagini, poi la nuova immagine diventa la sorgente
|
||||||
$("caricaImmagine").onclick = function () { $("fileImmagine").click(); };
|
$("caricaImmagine").onclick = function () { $("fileImmagine").click(); };
|
||||||
$("fileImmagine").onchange = async function () {
|
$("fileImmagine").onchange = async function () {
|
||||||
@@ -355,7 +378,7 @@ $("fileImmagine").onchange = async function () {
|
|||||||
var r = await fetch("/camera/immagini", { method: "POST", body: f });
|
var r = await fetch("/camera/immagini", { method: "POST", body: f });
|
||||||
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
||||||
var j = await r.json();
|
var j = await r.json();
|
||||||
await caricaSorgenti(j.source);
|
aggiungiSorgente(j); // #92: la ricerca camere puo' durare ~10 s: la voce nuova si aggiunge senza rileggere l'elenco
|
||||||
dire("msgSorgente", "Immagine «" + j.source + "» caricata: premere «Scatta» per vederla.", "ok");
|
dire("msgSorgente", "Immagine «" + j.source + "» caricata: premere «Scatta» per vederla.", "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
||||||
@@ -385,7 +408,10 @@ $("rimuoviImmagine").onclick = async function () {
|
|||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/immagini?nome=" + encodeURIComponent(nome), { method: "DELETE" });
|
var r = await fetch("/camera/immagini?nome=" + encodeURIComponent(nome), { method: "DELETE" });
|
||||||
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
||||||
await caricaSorgenti("");
|
var sel = $("sorgente"); // #92: voce tolta in locale, senza rileggere l'elenco dal servizio
|
||||||
|
sel.removeChild(sel.options[sel.selectedIndex]);
|
||||||
|
if (sel.options.length) sel.selectedIndex = 0;
|
||||||
|
cambioSorgente(true);
|
||||||
dire("msgSorgente", "Immagine «" + nome + "» rimossa.", "ok");
|
dire("msgSorgente", "Immagine «" + nome + "» rimossa.", "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
||||||
@@ -399,6 +425,7 @@ function cambioSorgente(salva) {
|
|||||||
fermaAnteprima();
|
fermaAnteprima();
|
||||||
azzeraIndicatori();
|
azzeraIndicatori();
|
||||||
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
|
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
|
||||||
|
A.calAttiva = null; A.bloccoCal = false; // #92: stato calibrazione della nuova sorgente ancora da leggere
|
||||||
annullaRimuovi();
|
annullaRimuovi();
|
||||||
$("rimuoviImmagine").hidden = !sorgenteImmagine();
|
$("rimuoviImmagine").hidden = !sorgenteImmagine();
|
||||||
dire("msgAnteprima", "");
|
dire("msgAnteprima", "");
|
||||||
@@ -410,8 +437,8 @@ function cambioSorgente(salva) {
|
|||||||
|
|
||||||
// ---- Anteprima (#87)
|
// ---- Anteprima (#87)
|
||||||
|
|
||||||
// #91: ris = {w, h} dell'ultimo frame; #92: urlCal/seqCal = riquadro calibrata, riprendi = ciclo sospeso dalla tab Calibrazione, lato = ultimo lato_max
|
// #91: ris = {w, h} dell'ultimo frame; #92: calAttiva = la sorgente ha una calibrazione attiva (null = non nota), bloccoCal = un 409 ha fermato le richieste calibrata; urlCal/seqCal = riquadro calibrata, riprendi = ciclo sospeso dalla tab Calibrazione, lato = ultimo lato_max
|
||||||
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, riprendi: false, lato: 1280, nitMax: 0, ris: null };
|
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, riprendi: false, lato: 1280, calAttiva: null, bloccoCal: false, nitMax: 0, ris: null };
|
||||||
|
|
||||||
function azzeraIndicatori() {
|
function azzeraIndicatori() {
|
||||||
A.nitMax = 0;
|
A.nitMax = 0;
|
||||||
@@ -464,6 +491,12 @@ function svuotaRiquadri() {
|
|||||||
async function aggiornaCalibrata(lato) {
|
async function aggiornaCalibrata(lato) {
|
||||||
var sorgente = $("sorgente").value;
|
var sorgente = $("sorgente").value;
|
||||||
if (!$("mostraCal").checked || !sorgente) return;
|
if (!$("mostraCal").checked || !sorgente) return;
|
||||||
|
if (A.calAttiva === false) { // #92: lo stato dice che non c'e' calibrazione: niente richiesta al servizio
|
||||||
|
A.seqCal++;
|
||||||
|
messaggioCalibrata("nessuna calibrazione attiva su questa sorgente");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (A.bloccoCal) return; // #92: dopo un 409 (risoluzione diversa, planare) si richiede solo dopo Carica/Scollega o cambio sorgente
|
||||||
var n = ++A.seqCal;
|
var n = ++A.seqCal;
|
||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/anteprima/calibrata", {
|
var r = await fetch("/camera/anteprima/calibrata", {
|
||||||
@@ -472,7 +505,11 @@ async function aggiornaCalibrata(lato) {
|
|||||||
body: JSON.stringify({ source: sorgente, lato_max: lato || A.lato })
|
body: JSON.stringify({ source: sorgente, lato_max: lato || A.lato })
|
||||||
});
|
});
|
||||||
if (n !== A.seqCal) return; // risposta superata da una piu' recente o da un cambio sorgente
|
if (n !== A.seqCal) return; // risposta superata da una piu' recente o da un cambio sorgente
|
||||||
if (!r.ok) { messaggioCalibrata(await dettaglio(r)); return; } // l'anteprima grezza continua
|
if (!r.ok) { // l'anteprima grezza continua
|
||||||
|
if (r.status === 409) A.bloccoCal = true; // #92: inutile richiedere a ogni frame
|
||||||
|
messaggioCalibrata(await dettaglio(r));
|
||||||
|
return;
|
||||||
|
}
|
||||||
var nome = "";
|
var nome = "";
|
||||||
try { nome = decodeURIComponent(r.headers.get("X-Calibrazione") || ""); } catch (e) { /* intestazione non decodificabile: nessun nome */ }
|
try { nome = decodeURIComponent(r.headers.get("X-Calibrazione") || ""); } catch (e) { /* intestazione non decodificabile: nessun nome */ }
|
||||||
var blob = await r.blob();
|
var blob = await r.blob();
|
||||||
@@ -901,10 +938,12 @@ async function aggiornaStato() {
|
|||||||
if (r.status === 404) {
|
if (r.status === 404) {
|
||||||
stato.textContent = "Sorgente non calibrata.";
|
stato.textContent = "Sorgente non calibrata.";
|
||||||
$("statoSorgente").textContent = "non calibrata";
|
$("statoSorgente").textContent = "non calibrata";
|
||||||
|
A.calAttiva = false; // #92
|
||||||
} else if (!r.ok) {
|
} else if (!r.ok) {
|
||||||
stato.textContent = await dettaglio(r);
|
stato.textContent = await dettaglio(r);
|
||||||
} else {
|
} else {
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
|
A.calAttiva = true; // #92
|
||||||
stato.textContent = "";
|
stato.textContent = "";
|
||||||
var titolo = document.createElement("div");
|
var titolo = document.createElement("div");
|
||||||
titolo.textContent = "Calibrazione attiva: " + (d.nome ? "«" + d.nome + "»" : "(senza nome)");
|
titolo.textContent = "Calibrazione attiva: " + (d.nome ? "«" + d.nome + "»" : "(senza nome)");
|
||||||
@@ -1023,8 +1062,9 @@ async function caricaNome(nome) {
|
|||||||
if (sorgente !== $("sorgente").value) return;
|
if (sorgente !== $("sorgente").value) return;
|
||||||
if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; }
|
if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; }
|
||||||
dire("msgElenco", "Calibrazione «" + nome + "» caricata sulla sorgente.", "ok");
|
dire("msgElenco", "Calibrazione «" + nome + "» caricata sulla sorgente.", "ok");
|
||||||
aggiornaStato();
|
A.bloccoCal = false; // #92
|
||||||
aggiornaCalibrata(); // #92: il riquadro calibrata usa subito la nuova calibrazione
|
await aggiornaStato(); // prima lo stato nuovo, poi il riquadro calibrata con la nuova calibrazione
|
||||||
|
aggiornaCalibrata();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
||||||
}
|
}
|
||||||
@@ -1040,8 +1080,9 @@ async function scollega() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
||||||
}
|
}
|
||||||
aggiornaStato();
|
A.bloccoCal = false; // #92
|
||||||
aggiornaCalibrata(); // #92: senza calibrazione il riquadro mostra il 409 del servizio
|
await aggiornaStato();
|
||||||
|
aggiornaCalibrata(); // #92: senza calibrazione il riquadro dice che non c'e'
|
||||||
}
|
}
|
||||||
|
|
||||||
// «Elimina» di una riga: conferma in pagina con un secondo clic entro 4 s (niente finestre del browser)
|
// «Elimina» di una riga: conferma in pagina con un secondo clic entro 4 s (niente finestre del browser)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import os
|
|||||||
import re # #92: nome sicuro per le immagini caricate
|
import re # #92: nome sicuro per le immagini caricate
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time # #92: scadenza della cache delle camere
|
||||||
import urllib.parse # #92: nome calibrazione nell'header X-Calibrazione
|
import urllib.parse # #92: nome calibrazione nell'header X-Calibrazione
|
||||||
import uuid # #91 id dei calcoli in attesa di nome
|
import uuid # #91 id dei calcoli in attesa di nome
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
@@ -110,6 +111,7 @@ from calibrazione_pagina import PAGINA # noqa: E402
|
|||||||
|
|
||||||
_TRIGGER = ("continuo",)
|
_TRIGGER = ("continuo",)
|
||||||
_MAX_CALCOLI = 8 # #91: calcoli tenuti in memoria in attesa di un nome
|
_MAX_CALCOLI = 8 # #91: calcoli tenuti in memoria in attesa di un nome
|
||||||
|
_CACHE_CAMERE_S = 60.0 # #92: durata della cache della ricerca camere
|
||||||
_log = logging.getLogger("camera_service") # #87: errori non bloccanti (settaggi)
|
_log = logging.getLogger("camera_service") # #87: errori non bloccanti (settaggi)
|
||||||
# #87: i quattro parametri regolabili, nell'ordine di presentazione
|
# #87: i quattro parametri regolabili, nell'ordine di presentazione
|
||||||
_PARAMETRI = ("esposizione", "esposizione_auto", "guadagno", "guadagno_auto")
|
_PARAMETRI = ("esposizione", "esposizione_auto", "guadagno", "guadagno_auto")
|
||||||
@@ -169,8 +171,32 @@ def create_app(folder: Path | str | None = None,
|
|||||||
with lock:
|
with lock:
|
||||||
return lock_sorgenti.setdefault(nome, threading.Lock())
|
return lock_sorgenti.setdefault(nome, threading.Lock())
|
||||||
|
|
||||||
|
# #92: la ricerca camere di list_sources dura ~10 s: il risultato (gia' filtrato) resta 60 s in cache
|
||||||
|
cache_camere: dict = {"voci": None, "t": 0.0}
|
||||||
|
lock_ricerca = threading.Lock() # due richieste insieme non lanciano due ricerche
|
||||||
|
|
||||||
|
def _camere(aggiorna: bool = False) -> list:
|
||||||
|
with lock_ricerca:
|
||||||
|
fresca = cache_camere["voci"] is not None and time.monotonic() - cache_camere["t"] < _CACHE_CAMERE_S
|
||||||
|
if fresca and not aggiorna:
|
||||||
|
return cache_camere["voci"]
|
||||||
|
voci = list_sources(None, cameras=True, config=_config())
|
||||||
|
# camera:file (replay) non e' mai configurato in TMF -> fuori dall'elenco
|
||||||
|
cache_camere["voci"] = [{"name": v.name, "kind": v.kind, "label": v.label}
|
||||||
|
for v in voci if v.kind in ("camera", "virtual") and v.name != "camera:file"]
|
||||||
|
cache_camere["t"] = time.monotonic()
|
||||||
|
return cache_camere["voci"]
|
||||||
|
|
||||||
|
def _prima_ricerca() -> None:
|
||||||
|
try:
|
||||||
|
_camere()
|
||||||
|
except Exception: # noqa: BLE001 - e' solo un riscaldamento: la prima richiesta riprova
|
||||||
|
_log.exception("prima ricerca camere fallita")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _vita(_app):
|
async def _vita(_app):
|
||||||
|
# #92: la prima ricerca parte all'avvio, cosi' la prima pagina non aspetta
|
||||||
|
threading.Thread(target=_prima_ricerca, daemon=True, name="ricerca-camere").start()
|
||||||
yield
|
yield
|
||||||
with lock: # alla chiusura rilascia le camere aperte
|
with lock: # alla chiusura rilascia le camere aperte
|
||||||
sorgenti = list(aperte.values())
|
sorgenti = list(aperte.values())
|
||||||
@@ -184,12 +210,9 @@ def create_app(folder: Path | str | None = None,
|
|||||||
app = FastAPI(title="TMF camera service", lifespan=_vita)
|
app = FastAPI(title="TMF camera service", lifespan=_vita)
|
||||||
|
|
||||||
@app.get("/camera/sources")
|
@app.get("/camera/sources")
|
||||||
def camera_sources():
|
def camera_sources(aggiorna: bool = False): # #92: aggiorna=true forza la ricerca (camera appena collegata)
|
||||||
voci = list_sources(None, cameras=True, config=_config())
|
elenco = list(_camere(aggiorna))
|
||||||
# #92: camera:file (replay) non e' mai configurato in TMF -> fuori dall'elenco
|
# #92: in coda le immagini caricate, sempre fresche dalla cartella (costano nulla)
|
||||||
elenco = [{"name": v.name, "kind": v.kind, "label": v.label}
|
|
||||||
for v in voci if v.kind in ("camera", "virtual") and v.name != "camera:file"]
|
|
||||||
# #92: in coda le immagini caricate, che sono sorgenti a pieno titolo
|
|
||||||
for v in list_sources(cartella_immagini, cameras=False):
|
for v in list_sources(cartella_immagini, cameras=False):
|
||||||
elenco.append({"name": v.name, "kind": "image", "label": f"immagine — {v.name}"})
|
elenco.append({"name": v.name, "kind": "image", "label": f"immagine — {v.name}"})
|
||||||
return {"sources": elenco}
|
return {"sources": elenco}
|
||||||
|
|||||||
@@ -106,6 +106,34 @@ def test_sources_con_immagini_e_senza_camera_file(client):
|
|||||||
assert nomi.index("camera:folder") < nomi.index("a.png")
|
assert nomi.index("camera:folder") < nomi.index("a.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_sources_camere_in_cache_e_aggiorna(client):
|
||||||
|
# #92: la ricerca camere (~10 s) si fa una volta e resta 60 s; aggiorna=true la rilancia; le immagini sono sempre fresche
|
||||||
|
g = _modulo(client)
|
||||||
|
originale = g["list_sources"]
|
||||||
|
chiamate = []
|
||||||
|
|
||||||
|
def finto(cartella, cameras=True, **kw):
|
||||||
|
if cameras:
|
||||||
|
chiamate.append(1)
|
||||||
|
return originale(cartella, cameras=cameras, **kw)
|
||||||
|
|
||||||
|
g["list_sources"] = finto
|
||||||
|
try:
|
||||||
|
client.get("/camera/sources") # se la ricerca d'avvio e' ancora in corso aspetta il suo lock
|
||||||
|
base = len(chiamate)
|
||||||
|
_carica(client, "nuova.png", una_scacchiera())
|
||||||
|
voci = client.get("/camera/sources").json()["sources"]
|
||||||
|
client.get("/camera/sources")
|
||||||
|
assert len(chiamate) == base # seconda e terza chiamata: nessuna nuova ricerca
|
||||||
|
assert voci[-1]["name"] == "nuova.png" # ma l'immagine appena caricata c'e'
|
||||||
|
client.get("/camera/sources", params={"aggiorna": "true"})
|
||||||
|
assert len(chiamate) == base + 1 # aggiorna=true la rilancia
|
||||||
|
client.get("/camera/sources")
|
||||||
|
assert len(chiamate) == base + 1 # e il risultato nuovo torna in cache
|
||||||
|
finally:
|
||||||
|
g["list_sources"] = originale
|
||||||
|
|
||||||
|
|
||||||
def test_sources_senza_immagini_non_crea_la_cartella(client):
|
def test_sources_senza_immagini_non_crea_la_cartella(client):
|
||||||
voci = client.get("/camera/sources").json()["sources"]
|
voci = client.get("/camera/sources").json()["sources"]
|
||||||
assert all(v["kind"] != "image" for v in voci)
|
assert all(v["kind"] != "image" for v in voci)
|
||||||
|
|||||||
@@ -112,3 +112,15 @@ def test_calibrata_dopo_anteprima_e_dopo_carica():
|
|||||||
assert PAGINA.count("aggiornaCalibrata();") >= 3
|
assert PAGINA.count("aggiornaCalibrata();") >= 3
|
||||||
assert "messaggioCalibrata(await dettaglio(r))" in PAGINA
|
assert "messaggioCalibrata(await dettaglio(r))" in PAGINA
|
||||||
assert 'decodeURIComponent(r.headers.get("X-Calibrazione")' in PAGINA
|
assert 'decodeURIComponent(r.headers.get("X-Calibrazione")' in PAGINA
|
||||||
|
|
||||||
|
|
||||||
|
def test_elenco_sorgenti_senza_attese_e_calibrata_solo_se_serve():
|
||||||
|
# #92: la voce dell'immagine caricata si aggiunge in pagina (niente rilettura dell'elenco); «Aggiorna» forza la ricerca camere
|
||||||
|
assert "aggiungiSorgente(j);" in PAGINA and "caricaSorgenti(j.source)" not in PAGINA
|
||||||
|
assert 'id="aggiornaSorgenti"' in PAGINA and ">Aggiorna</button>" in PAGINA
|
||||||
|
assert '"?aggiorna=true"' in PAGINA
|
||||||
|
# nessuna calibrazione attiva: il riquadro lo dice senza chiamare il servizio; dopo un 409 non si richiede piu'
|
||||||
|
assert "if (A.calAttiva === false)" in PAGINA
|
||||||
|
assert "nessuna calibrazione attiva su questa sorgente" in PAGINA
|
||||||
|
assert "if (r.status === 409) A.bloccoCal = true;" in PAGINA and "if (A.bloccoCal) return;" in PAGINA
|
||||||
|
assert PAGINA.count("A.bloccoCal = false;") >= 3 # cambio sorgente, Carica, Scollega
|
||||||
|
|||||||
Reference in New Issue
Block a user