Files
aiscr-qgis-amcr-viewer/tests/smoke_test.py
T
david-spacil 46fd8da045 Pamatovat filtry ve formuláři a přidat jejich reset (#84) (#85)
* Pamatovat filtry ve formuláři a přidat jejich reset (#84)

Filtrační dialog si do konce běhu QGIS pamatuje poslední filtry
potvrzené tlačítkem OK, zvlášť pro akce, lokality a samostatné
nálezy. Storno ani odmítnuté obrácené rozmezí uložený stav nemění.

- tlačítko „Obnovit výchozí“ vrátí formulář do výchozího stavu
  (bbox zapnutý, PIAN se třemi úrovněmi); uložený stav změní až OK
- tlačítko ✕ u každého výběru vymaže jen tento filtr
- upozornění nahoře hlásí obnovené filtry a jejich počet
- kódy, které po aktualizaci heslářů zmizely, se zahodí; popisky
  se skládají z aktuálních heslářů
- výchozí hodnoty jsou definované na jednom místě, PIAN už není
  natvrdo v setup_picker()
- smoke test, README a changelog v2.2.0

OpenSpec: openspec/changes/add-filter-memory-and-reset/
Připraveno s pomocí AI (implementace subagent, revize a ověření
Claude), ručně zkontrolovat v QGIS.

* Křížek vrací filtr do výchozího stavu, oznámení bez ikony (#84)

Úpravy po ručním testu v QGIS:
- ✕ u výběru vrací filtr do jeho výchozího stavu; u PIAN – přesnost
  obnoví tři předvolené úrovně, místo aby výběr vymazal. Tlačítko je
  aktivní, jen když se výběr od výchozího liší. PIAN bez omezení jde
  dál nastavit odškrtnutím všech úrovní ve výběru.
- oznámení o obnovených filtrech bez úvodního „ℹ“

Spec, design, tasks, README, changelog a smoke test upraveny.
Připraveno s pomocí AI.

* Archivovat OpenSpec změnu add-filter-memory-and-reset (#84)

Ruční test v QGIS (akce) zapsán k úkolu 4.3, změna archivována
do openspec/changes/archive/2026-10-02-add-filter-memory-and-reset/.
Připraveno s pomocí AI.

* Upřesnit záznam ručního testu u úkolu 4.3 (#84)

Vzhled i funkce ověřeny u všech tří typů dat, poslední oprava
(✕ u PIAN, oznámení) jen u akcí.
Připraveno s pomocí AI.
2026-10-02 19:04:53 +02:00

668 lines
23 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Smoke test: loads the plugin inside a real QGIS and exercises the parts
that differ between Qt5 and Qt6.
It is deliberately offline – no request ever leaves the machine, so the
test says nothing about the AMCR API, only about the plugin loading and
its widgets being constructible.
Run it from the repository root:
python3 tests/smoke_test.py
QGIS must be importable (inside the qgis/qgis Docker image it already is).
The exit code is 0 when everything passed, 1 otherwise.
"""
import os
import sys
import traceback
import requests
# Offscreen, otherwise the dialogs need an X server
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
selhani = []
def zkouska(nazev, funkce):
"""Runs one check and keeps going even when it raises."""
try:
detail = funkce()
except Exception:
selhani.append(nazev)
print(f" FAIL {nazev}")
print(traceback.format_exc().rstrip())
else:
print(f" OK {nazev}" + (f" – {detail}" if detail else ""))
from qgis.core import ( # noqa: E402
Qgis,
QgsApplication,
QgsTask,
QgsWkbTypes,
)
from qgis.PyQt import QtCore # noqa: E402
from qgis.PyQt.QtCore import QDate # noqa: E402
print(f"QGIS {Qgis.QGIS_VERSION.split('-')[0]} | Qt {QtCore.QT_VERSION_STR} "
f"| PyQt {QtCore.PYQT_VERSION_STR}")
QgsApplication.setPrefixPath(os.environ.get("QGIS_PREFIX_PATH", "/usr"), True)
qgs = QgsApplication([], True)
qgs.initQgis()
import amcr_viewer.amcr_codelists # noqa: E402,F401
import amcr_viewer.amcr_dialog as dialog # noqa: E402
import amcr_viewer.amcr_tools # noqa: E402,F401
import amcr_viewer.amcr_viewer # noqa: E402,F401
print(" OK import všech modulů pluginu")
def enumy():
"""
The scoped enum forms must exist. Unscoped aliases still resolve in
QGIS 4.2, so a plain import proves nothing – these are read explicitly.
"""
return (f"QgsTask.Flag.CanCancel={int(QgsTask.Flag.CanCancel)}, "
f"PointGeometry={int(QgsWkbTypes.GeometryType.PointGeometry)}, "
f"MessageLevel.Info={int(Qgis.MessageLevel.Info)}")
def uloha():
ukol = dialog.UpdateCodelistsTask("smoke")
assert ukol.canCancel() is True
return "canCancel=True"
def dialogy():
# A modal warning would block the offscreen run forever
dialog.QMessageBox.warning = staticmethod(lambda *a, **k: None)
popis = []
for typ in ("akce", "lokalita", "samostatny_nalez"):
okno = dialog.AmcrFilterDialog(typ)
okno.show()
QgsApplication.processEvents()
popis.append(f"{typ}: {len(okno.date_ranges)} rozmezí")
okno.close()
return ", ".join(popis)
def filtr_datumu():
"""
A half-filled range must be completed with the sentinel. The API
rejects a one-sided range, so this is the part worth guarding.
The expected value is written out on purpose – comparing against
dialog.DATE_OPEN_TO would only prove the module agrees with itself.
"""
okno = dialog.AmcrFilterDialog("samostatny_nalez")
pole, _, od, _do = okno.date_ranges[0]
od.setDate(QDate(2016, 1, 1))
hodnota = okno.get_filters()[pole]
assert hodnota == "2016-01-01,9999-12-31", hodnota
# A range left completely empty must add no filter at all
prazdne = dialog.AmcrFilterDialog("samostatny_nalez")
pole_prazdne = prazdne.date_ranges[0][0]
assert pole_prazdne not in prazdne.get_filters()
prazdne.close()
okno.close()
return hodnota
class FalesnaSession:
"""Offline stand-in for requests.Session: returns canned JSON
bodies for GET /api/user/islogged and counts the requests."""
def __init__(self, tela):
# tela: a list of (body, exception) pairs – one per GET call,
# consumed in order; None body means raise the exception
self.tela = list(tela)
self.get_volani = 0
def get(self, url, timeout=0):
self.get_volani += 1
polozka = self.tela.pop(0)
# A bare dict is a plain body; (None, exception) means raise
if isinstance(polozka, tuple):
tela, vyjimka = polozka
else:
tela, vyjimka = polozka, None
if tela is None and vyjimka is not None:
raise vyjimka
class Odpoved:
def __init__(self, tela):
self.telo = tela
self.text = str(tela)
def json(self):
if isinstance(self.telo, Exception):
raise self.telo
return self.telo
return Odpoved(tela)
def prihlasovaci_stav():
"""
_ensure_logged_in with a fake session and monkeypatched login /
credentials / _get_session – everything stays offline.
Each case: (name, expected status, islogged bodies of the current
session, islogged bodies after re-login, fake login result,
stored credentials, expected number of islogged GETs).
"""
pripady = [
# Valid session – no re-login, no extra request
("platná session", "logged_in",
[{"remaining": 3500}], [], None, ("", ""), 1),
# nologged + successful re-login, verified again
("expired + re-login", "relogged",
[{"error": "nologged"}], [{"remaining": 1800}],
"session", ("uzivatel", "heslo"), 1),
# nologged + failed re-login
("expired + selhaný re-login", "fallback",
[{"error": "nologged"}], [], None, ("uzivatel", "heslo"), 1),
# nologged + no stored credentials
("expired bez údajů", "fallback",
[{"error": "nologged"}], [], None, ("", ""), 1),
# No session and no credentials – no request at all
("anonym bez údajů", "anonymous",
[], [], None, ("", ""), 0),
# Network error during the check
("chyba sítě", "unknown",
[(None, requests.exceptions.ConnectionError("probe"))],
[], None, ("", ""), 1),
# 200 but invalid JSON
("neplatný JSON", "unknown",
[(None, ValueError("Invalid JSON"))],
[], None, ("", ""), 1),
]
tools = amcr_viewer.amcr_tools
puvodni = (tools.login_to_api, tools._get_session,
dialog.LoginDialog.__dict__["get_credentials"])
try:
return _prihlasovaci_stav(pripady, tools)
finally:
# Leave the modules as they were found, even when a case fails
(tools.login_to_api, tools._get_session,
dialog.LoginDialog.get_credentials) = puvodni
tools.AMCR_SESSION = None
def _prihlasovaci_stav(pripady, tools):
"""Runs the cases of prihlasovaci_stav()."""
vysledky = []
for (nazev, ocekavano, tela, tela_po_loginu, login_vysledek,
kredity, get_volani) in pripady:
# The current session (None = _get_session returns None);
# a successful fake re-login produces a new fake session
session = FalesnaSession(tela) if tela else None
login_hodnota = FalesnaSession(tela_po_loginu) \
if login_vysledek else None
tools.AMCR_SESSION = session
def fake_login(hodnota):
# Like the real login_to_api: stores the session globally
def login(uzivatel, heslo):
if hodnota is not None:
tools.AMCR_SESSION = hodnota
return hodnota
return login
tools.login_to_api = fake_login(login_hodnota)
tools._get_session = (lambda s: lambda: s)(session) \
if session else (lambda: None)
dialog.LoginDialog.get_credentials = staticmethod(
(lambda k: lambda: k)(kredity)
)
stav = tools._ensure_logged_in()
assert stav == ocekavano, f"{nazev}: {stav} != {ocekavano}"
# The old session object must have been used for the checks
if session is not None:
assert session.get_volani == get_volani, \
f"{nazev}: {session.get_volani} != {get_volani}"
# The returned fake login session must become the global one
# and its login state must have been verified as well
if ocekavano == "relogged":
assert login_hodnota is not None
assert tools.AMCR_SESSION is login_hodnota
assert login_hodnota.get_volani == 1, \
f"{nazev}: nová session nebyla ověřena"
vysledky.append(f"{nazev} → {stav}")
return ", ".join(vysledky)
def odhlaseni():
"""logout_from_api with a fake session – offline."""
tools = amcr_viewer.amcr_tools
puvodni = tools.AMCR_SESSION
try:
# Logged-in session: one GET to /logout, session dropped
session = FalesnaSession([{"msg": "logged out"}])
session_get = session.get
urls = []
def get(url, timeout=0):
urls.append(url)
odpoved = session_get(url, timeout)
odpoved.raise_for_status = lambda: None
return odpoved
session.get = get
tools.AMCR_SESSION = session
assert tools.logout_from_api() is True
assert tools.AMCR_SESSION is None
assert urls and urls[0].endswith("/api/user/logout"), urls
# Network error: session still dropped locally
chyba = FalesnaSession(
[(None, requests.exceptions.ConnectionError("probe"))]
)
tools.AMCR_SESSION = chyba
assert tools.logout_from_api() is False
assert tools.AMCR_SESSION is None
assert chyba.get_volani == 1
# No session: nothing to do, no request
assert tools.logout_from_api() is True
finally:
tools.AMCR_SESSION = puvodni
return "odhlášení, chyba sítě → zahozeno lokálně, bez session → nic"
def vaha_komponent():
"""
_component_entries: the weight is 1/n of the components that pass
the predicate, so the weights of one documentation unit sum to 1
even with a period/area filter active.
The cases come from the spec of the fix (issue #55); the sums are
compared with a tolerance because 1/3 weights add up to 0.999…
"""
tools = amcr_viewer.amcr_tools
def komponenta(ident, areal=None, obdobi=None):
return {
"ident_cely": ident,
"komponenta_areal": ({"id": areal} if areal else None),
"komponenta_obdobi": ({"id": obdobi} if obdobi else None),
}
dj_meta = {"dj_id": "X-M-000001"}
# 4 components, no filter: 4 features, each 0.25
komps = [komponenta(f"K{i}") for i in range(4)]
zaznamy = tools._component_entries(dj_meta, komps, lambda k: True)
assert len(zaznamy) == 4, len(zaznamy)
assert all(z["vaha"] == 0.25 for z in zaznamy), \
[z["vaha"] for z in zaznamy]
# Period filter keeps 1 of 4: single feature with weight 1
komps = [
komponenta("K0", obdobi="neolit"),
komponenta("K1"), komponenta("K2"), komponenta("K3"),
]
zaznamy = tools._component_entries(
dj_meta, komps, lambda k: k["komponenta_obdobi"] is not None
)
assert len(zaznamy) == 1, len(zaznamy)
assert zaznamy[0]["vaha"] == 1.0, zaznamy[0]["vaha"]
# Filter keeps 2 of 3: 2 features, each 0.5, sum 1 within tolerance
komps = [
komponenta("K0", obdobi="neolit"), komponenta("K1", obdobi="bronz"),
komponenta("K2"),
]
zaznamy = tools._component_entries(
dj_meta, komps, lambda k: k["komponenta_obdobi"] is not None
)
assert len(zaznamy) == 2, len(zaznamy)
assert all(z["vaha"] == 0.5 for z in zaznamy), \
[z["vaha"] for z in zaznamy]
assert abs(sum(z["vaha"] for z in zaznamy) - 1) < 1e-9
# Sum with tolerance also for an indivisible split (1/3)
komps = [komponenta(f"K{i}") for i in range(3)]
zaznamy = tools._component_entries(dj_meta, komps, lambda k: True)
assert abs(sum(z["vaha"] for z in zaznamy) - 1) < 1e-9
# No components: one entry with empty component fields, weight 1
zaznamy = tools._component_entries(dj_meta, [], lambda k: True)
assert len(zaznamy) == 1, zaznamy
assert zaznamy[0]["vaha"] == 1, zaznamy[0]["vaha"]
assert zaznamy[0]["komponenta_id"] == ""
# The shared DJ metadata and component fields travel along
komps = [komponenta("K0", areal="sidelni", obdobi="neolit")]
zaznamy = tools._component_entries(dj_meta, komps, lambda k: True)
assert zaznamy[0]["dj_id"] == "X-M-000001"
assert zaznamy[0]["komponenta_id"] == "K0"
return "4×0.25; 1/4 → 1.0; 2/3 → 2×0.5; prázdné → 1"
def vychozi_filtry():
"""
A fresh dialog starts from the defaults: the map-extent restriction
checked, PIAN – přesnost pre-selected for akce and lokalita, and
nothing (not even PIAN) sent for samostatny_nalez.
"""
_stubuj_varovani()
try:
pian = ["HES-000861", "HES-000862", "HES-000863"]
for typ, ma_pian in (("akce", True), ("lokalita", True),
("samostatny_nalez", False)):
okno = dialog.AmcrFilterDialog(typ)
assert okno.get_bbox() == "true", typ
assert okno.get_komponenty() == "false", typ
if ma_pian:
assert okno.selection_cache["pian_presnost"] == pian, typ
assert okno.get_filters()["f_pian_presnost"] == pian, typ
else:
assert "f_pian_presnost" not in okno.get_filters(), typ
okno.close()
return ("PIAN 861/862/863 u akcí a lokalit, u nálezů bez "
"omezení, bbox zapnutý")
finally:
dialog._REMEMBERED_STATE.clear()
def _stubuj_varovani():
# A modal warning would block the offscreen run forever
dialog.QMessageBox.warning = staticmethod(lambda *a, **k: None)
def _kody(codelist, n=1):
"""First n real codes of a codelist (label -> code dict)."""
return [code for code in codelist.values() if code][:n]
def _napln(okno, typ):
"""Fills a dialog with a non-default state for round-trip tests."""
okno._set_picker("kraj", _kody(dialog.KRAJE))
okno._set_picker("obdobi", _kody(dialog.OBDOBI, 2))
okno.chk_bbox.setChecked(False)
if typ == "akce":
okno.chk_posevidence.setChecked(True)
okno.chk_proj_akce.setChecked(True)
okno._set_picker("typ_akce", _kody(dialog.TYP_AKCE))
okno.date_ranges[0][2].setDate(QDate(2016, 1, 1))
okno.date_ranges[0][3].setDate(QDate(2017, 12, 31))
if typ == "lokalita":
okno.chk_komponenty.setChecked(True)
okno._set_picker("typ_lokality", _kody(dialog.TYP_LOKALITY))
if typ == "samostatny_nalez":
okno._set_picker("druh_nalezu", _kody(dialog.DRUH_NALEZU))
okno.date_ranges[0][3].setDate(QDate(2020, 6, 30))
def pamet_snapshotu():
"""
Snapshot -> apply on a fresh dialog keeps get_filters(),
get_bbox() and get_komponenty() identical; a code unknown to the
current codelist is dropped on the way.
"""
_stubuj_varovani()
try:
for typ in ("akce", "lokalita", "samostatny_nalez"):
okno = dialog.AmcrFilterDialog(typ)
_napln(okno, typ)
stav = okno.get_filters()
snapshot = okno._snapshot()
okno.close()
obnovene = dialog.AmcrFilterDialog(typ)
obnovene._apply_state(snapshot)
assert obnovene._snapshot() == snapshot, typ
assert obnovene.get_filters() == stav, typ
assert obnovene.get_bbox() == okno.get_bbox(), typ
assert obnovene.get_komponenty() == okno.get_komponenty(), typ
obnovene.close()
# Unknown code: dropped, not sent
okno = dialog.AmcrFilterDialog("akce")
kraj = _kody(dialog.KRAJE)
okno._set_picker("kraj", kraj + ["XX-NEEXISTUJE"])
assert okno.selection_cache["kraj"] == kraj
assert okno.get_filters()["f_kraj"] == kraj
okno.close()
# The same through a remembered state, as after a codelist
# update removed the code: dropped on opening, the picker text
# is the current codelist label
stitek = next(k for k, v in dialog.KRAJE.items() if v == kraj[0])
stav = dialog.AmcrFilterDialog("akce")._snapshot()
stav["codes"]["kraj"] = kraj + ["XX-NEEXISTUJE"]
dialog._REMEMBERED_STATE["akce"] = stav
okno = dialog.AmcrFilterDialog("akce")
assert okno.get_filters()["f_kraj"] == kraj
assert okno.pickers["kraj"][1].text() == stitek
okno.close()
return "round-trip všech tří typů, neznámý kód zahozen"
finally:
dialog._REMEMBERED_STATE.clear()
def pamet_potvrzeni():
"""
OK remembers the form state for the next opening of the same data
type; Cancel and a refused reversed date range do not; another
data type starts from the defaults.
"""
_stubuj_varovani()
try:
okno = dialog.AmcrFilterDialog("akce")
_napln(okno, "akce")
potvrzene = okno.get_filters()
okno.accept()
okno.close()
# OK -> reopen restores the same filters
znovu = dialog.AmcrFilterDialog("akce")
assert znovu.get_filters() == potvrzene
assert znovu.get_bbox() == "false"
# Another data type starts from the defaults
lokalita = dialog.AmcrFilterDialog("lokalita")
assert "f_kraj" not in lokalita.get_filters()
assert lokalita.get_bbox() == "true"
lokalita.close()
# Cancel keeps the remembered state
znovu._set_picker("kraj", [])
znovu.chk_bbox.setChecked(True)
znovu.reject()
znovu.close()
po_zruseni = dialog.AmcrFilterDialog("akce")
assert po_zruseni.get_filters() == potvrzene
# A reversed range is refused and the state stays unchanged
zapamatovano = dialog._REMEMBERED_STATE["akce"]
po_zruseni.date_ranges[0][2].setDate(QDate(2018, 1, 1))
po_zruseni.date_ranges[0][3].setDate(QDate(2017, 1, 1))
po_zruseni.accept()
assert po_zruseni.result() == 0, "obrácené rozmezí přijato"
assert dialog._REMEMBERED_STATE["akce"] == zapamatovano
po_zruseni.close()
return ("OK obnoví, Cancel i obrácené rozmezí ne, jiný typ "
"od výchozích")
finally:
dialog._REMEMBERED_STATE.clear()
def obnoveni_vychozich():
"""
Obnovit výchozí resets the form only: reset + OK behaves like a
fresh dialog, reset + Cancel keeps the remembered state.
"""
_stubuj_varovani()
try:
# Default output, captured before anything is remembered
okno = dialog.AmcrFilterDialog("akce")
vychozi = okno.get_filters()
vychozi_bbox = okno.get_bbox()
vychozi_komponenty = okno.get_komponenty()
okno.close()
# A remembered non-default state
okno = dialog.AmcrFilterDialog("akce")
_napln(okno, "akce")
potvrzene = okno.get_filters()
okno.accept()
okno.close()
# Reset + Cancel: reopening shows the remembered state
okno = dialog.AmcrFilterDialog("akce")
okno.action_reset()
assert okno.get_filters() == vychozi
okno.reject()
okno.close()
okno = dialog.AmcrFilterDialog("akce")
assert okno.get_filters() == potvrzene
okno.close()
# Reset + OK: the sent filters and the next opening are default
okno = dialog.AmcrFilterDialog("akce")
okno.action_reset()
assert okno.get_filters() == vychozi
assert okno.get_bbox() == vychozi_bbox
assert okno.get_komponenty() == vychozi_komponenty
okno.accept()
okno.close()
okno = dialog.AmcrFilterDialog("akce")
assert okno.get_filters() == vychozi
okno.close()
return "reset + OK = čerstvý dialog, reset + Cancel zachová"
finally:
dialog._REMEMBERED_STATE.clear()
def vymazani_vyberu():
"""
The ✕ button returns only its own filter to its default (empty,
or the three pre-selected levels for PIAN – přesnost) and is
disabled while the filter already is at that default.
"""
_stubuj_varovani()
try:
okno = dialog.AmcrFilterDialog("lokalita")
okno._set_picker("kraj", _kody(dialog.KRAJE))
okno._set_picker("obdobi", _kody(dialog.OBDOBI, 2))
pred = okno.get_filters()
assert "f_kraj" in pred and "f_obdobi" in pred
okno.pickers["kraj"][2].click()
assert okno.selection_cache["kraj"] == []
po = okno.get_filters()
assert "f_kraj" not in po
assert po["f_obdobi"] == pred["f_obdobi"]
assert not okno.pickers["kraj"][2].isEnabled()
okno.close()
okno = dialog.AmcrFilterDialog("akce")
assert "f_pian_presnost" in okno.get_filters()
# A fresh dialog is at the default, so ✕ is disabled
assert not okno.pickers["pian_presnost"][2].isEnabled()
# A reordered default list still counts as the default
vychozi = dialog.DEFAULT_CODES["pian_presnost"]
okno._set_picker("pian_presnost", list(reversed(vychozi)))
assert not okno.pickers["pian_presnost"][2].isEnabled()
assert "f_pian_presnost" in okno.get_filters()
# Emptied PIAN means no restriction and enables ✕
okno._set_picker("pian_presnost", [])
assert "f_pian_presnost" not in okno.get_filters()
assert okno.pickers["pian_presnost"][2].isEnabled()
# ✕ restores the three default levels and disables itself
okno.pickers["pian_presnost"][2].click()
assert sorted(okno.get_filters()["f_pian_presnost"]) == sorted(
vychozi
)
assert not okno.pickers["pian_presnost"][2].isEnabled()
okno.close()
return ("✕ vrací filtr na výchozí hodnotu, PIAN na tři "
"úrovně, jinak prázdné")
finally:
dialog._REMEMBERED_STATE.clear()
def upozorneni_obnovy():
"""
The notice is hidden for a fresh dialog and for a remembered
default state; with two filters set it is shown with the count 2
and hidden again by Obnovit výchozí.
"""
_stubuj_varovani()
try:
okno = dialog.AmcrFilterDialog("akce")
assert okno.lbl_notice.isHidden()
okno.close()
# A remembered default state is not worth a notice
okno = dialog.AmcrFilterDialog("akce")
okno.accept()
okno.close()
okno = dialog.AmcrFilterDialog("akce")
assert okno.lbl_notice.isHidden()
okno.close()
# Two non-default filters -> a notice with the count 2
okno = dialog.AmcrFilterDialog("akce")
okno._set_picker("kraj", _kody(dialog.KRAJE))
okno._set_picker("obdobi", _kody(dialog.OBDOBI))
okno.accept()
okno.close()
okno = dialog.AmcrFilterDialog("akce")
assert not okno.lbl_notice.isHidden()
assert okno.lbl_notice.text().startswith("Načteny filtry")
assert "aktivní filtry: 2" in okno.lbl_notice.text()
okno.action_reset()
assert okno.lbl_notice.isHidden()
okno.close()
return "skryté pro výchozí, viditelné s počtem 2, reset skryje"
finally:
dialog._REMEMBERED_STATE.clear()
zkouska("scoped enumy", enumy)
zkouska("UpdateCodelistsTask", uloha)
zkouska("filtrační dialogy", dialogy)
zkouska("filtr podle data", filtr_datumu)
zkouska("výchozí filtry", vychozi_filtry)
zkouska("paměť snapshotu", pamet_snapshotu)
zkouska("paměť potvrzení", pamet_potvrzeni)
zkouska("obnovení výchozích", obnoveni_vychozich)
zkouska("vymazání výběru", vymazani_vyberu)
zkouska("upozornění obnovy", upozorneni_obnovy)
zkouska("stav přihlášení", prihlasovaci_stav)
zkouska("odhlášení", odhlaseni)
zkouska("váha komponent", vaha_komponent)
qgs.exitQgis()
if selhani:
print(f"\nNEPROŠLO: {', '.join(selhani)}")
sys.exit(1)
print("\nVše prošlo")