ui: align cylinder controls with service workflow
All checks were successful
ENGIN CI / Build, test and smoke (push) Successful in 43s
All checks were successful
ENGIN CI / Build, test and smoke (push) Successful in 43s
This commit is contained in:
parent
dab5264ed7
commit
9032c49993
@ -39,13 +39,13 @@ rollbacku znajduje się w `docs/PRODUCTION_RUNBOOK.md`.
|
||||
|
||||
## Co zawiera produkt
|
||||
|
||||
- mapa 8-, 12- i 16-cylindrowych silników z diagnozą każdego cylindra,
|
||||
- przegląd silników 8-, 12- i 16-cylindrowych z diagnozą każdego cylindra,
|
||||
- typ usterki: `ok`, `zakoksowany`, `lejacy`, `pompa`, `iglica` lub `unknown`,
|
||||
- nasilenie: `male`, `srednie`, `duze`; dla `ok` i `unknown` zawsze `nie_dotyczy`,
|
||||
- status silnika, najwyższe rzeczywiste severity i kolejka cylindrów do kontroli,
|
||||
- heatmapa odchyleń całego silnika,
|
||||
- status silnika, najwyższe rzeczywiste nasilenie i kolejka cylindrów do kontroli,
|
||||
- mapa odchyleń całego silnika ze stałą, porównywalną skalą,
|
||||
- porównanie widma cylindra z medianą pozostałych cylindrów tej samej jednostki,
|
||||
- trzy najbardziej anomalne pasma, niekalibrowany score modelu oraz rekomendowany następny krok,
|
||||
- priorytet kontroli, trzy najbardziej anomalne pasma oraz rekomendowany następny krok,
|
||||
- pobieranie `predictions.csv` i rozszerzonej diagnostyki,
|
||||
- ścisła walidacja CSV i bezpieczne komunikaty błędów,
|
||||
- lokalne działanie na CPU, bez wysyłania danych do zewnętrznych usług.
|
||||
|
||||
151
app.py
151
app.py
@ -7,7 +7,6 @@ import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
|
||||
@ -36,6 +35,7 @@ MODEL_VERSION = "engin-2026.08.25-1"
|
||||
MODEL_ARTIFACT_DIR = BASE_DIR / "artifacts" / MODEL_VERSION
|
||||
VIEW_OVERVIEW = "Przegląd"
|
||||
VIEW_DETAIL = "Szczegóły cylindra"
|
||||
NO_COMPARISON = 0
|
||||
PLOTLY_CONFIG = {"displayModeBar": False, "displaylogo": False}
|
||||
DIAGNOSTIC_COLUMN_DISPLAY = {
|
||||
"engine_id": "Silnik",
|
||||
@ -122,12 +122,6 @@ def _render_error(error: UserFacingError) -> None:
|
||||
st.caption(f"Kod błędu: `{error.code}`")
|
||||
|
||||
|
||||
def _format_confidence(value: float | int | None) -> str:
|
||||
if value is None or pd.isna(value):
|
||||
return "—"
|
||||
return f"{100 * float(value):.0f}%"
|
||||
|
||||
|
||||
def _diagnostics_for_display(frame: pd.DataFrame) -> pd.DataFrame:
|
||||
display = frame.copy()
|
||||
for column in ("label", "raw_model_label"):
|
||||
@ -151,25 +145,14 @@ def _diagnostics_for_display(frame: pd.DataFrame) -> pd.DataFrame:
|
||||
return display.rename(columns=DIAGNOSTIC_COLUMN_DISPLAY)
|
||||
|
||||
|
||||
def _render_header(live_inference: bool) -> None:
|
||||
if live_inference:
|
||||
mode = "MODEL GOTOWY"
|
||||
verification = "ARTEFAKT ZWERYFIKOWANY"
|
||||
tone = "verified"
|
||||
else:
|
||||
mode = "TRYB DEMONSTRACYJNY"
|
||||
verification = "ZAPISANE WYNIKI"
|
||||
tone = "fallback"
|
||||
def _render_header() -> None:
|
||||
st.markdown(
|
||||
f"""
|
||||
"""
|
||||
<div class="product-header">
|
||||
<div>
|
||||
<div class="eyebrow">AESTEEL · DIAGNOSTYKA WTRYSKU DIESEL</div>
|
||||
<h1>Konsola diagnostyczna ENGIN</h1>
|
||||
<p>Diagnoza cylindra, nasilenie i następny krok.</p>
|
||||
</div>
|
||||
<div class="runtime-badge runtime-{tone}">● {mode}<br><span>{verification}</span></div>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
@ -182,7 +165,7 @@ def _render_summary(summary) -> None:
|
||||
<div><span>STATUS SILNIKA</span><strong>{html.escape(summary.status)}</strong></div>
|
||||
<div><span>NAJWYŻSZE NASILENIE</span><strong>{html.escape(summary.highest_severity_display)}</strong></div>
|
||||
<div><span>WYMAGA UWAGI</span><strong>{summary.attention} / {summary.cylinders}</strong></div>
|
||||
<div><span>ŚR. WYNIK MODELU</span><strong>{_format_confidence(summary.mean_confidence)}</strong></div>
|
||||
<div><span>PIERWSZY DO KONTROLI</span><strong>C{summary.top_cylinder:02d}</strong></div>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
@ -190,9 +173,12 @@ def _render_summary(summary) -> None:
|
||||
|
||||
|
||||
def _render_engine_overview(analysis) -> None:
|
||||
left, right = st.columns(2, gap="large")
|
||||
left, right = st.columns(2, gap="large", vertical_alignment="top")
|
||||
with left:
|
||||
st.markdown("### Mapa odchyleń")
|
||||
st.markdown(
|
||||
'<div class="overview-panel-title">Mapa odchyleń</div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
st.plotly_chart(
|
||||
engine_heatmap(analysis),
|
||||
width="stretch",
|
||||
@ -200,15 +186,17 @@ def _render_engine_overview(analysis) -> None:
|
||||
config=PLOTLY_CONFIG,
|
||||
)
|
||||
with right:
|
||||
st.markdown("### Priorytet kontroli")
|
||||
st.markdown(
|
||||
'<div class="overview-panel-title">Priorytet kontroli</div>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
ranking = rank_cylinders(analysis).head(6).copy()
|
||||
ranking["Cylinder"] = ranking["cylinder"].map(lambda value: f"C{int(value):02d}")
|
||||
ranking["Diagnoza"] = ranking["label_display"]
|
||||
ranking["Nasilenie"] = ranking["severity_display"]
|
||||
ranking["Wynik modelu"] = ranking["label_confidence"].map(_format_confidence)
|
||||
ranking["Priorytet"] = ranking["priority_display"]
|
||||
st.dataframe(
|
||||
ranking[["Cylinder", "Diagnoza", "Nasilenie", "Wynik modelu", "Priorytet"]],
|
||||
ranking[["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"]],
|
||||
hide_index=True,
|
||||
width="stretch",
|
||||
height=360,
|
||||
@ -221,9 +209,11 @@ def _render_cylinder_detail(
|
||||
comparison_cylinders: list[int],
|
||||
) -> None:
|
||||
explanation = explain_cylinder(analysis, cylinder)
|
||||
row = analysis.diagnostics[analysis.diagnostics["cylinder"].eq(cylinder)].iloc[0]
|
||||
color = LABEL_COLORS[explanation.label]
|
||||
severity_confidence = row.get("severity_confidence", np.nan)
|
||||
priority = rank_cylinders(analysis).loc[
|
||||
lambda frame: frame["cylinder"].eq(cylinder), "priority_display"
|
||||
].iloc[0]
|
||||
top_bands = ", ".join(f"{value} kHz" for value in explanation.top_frequencies)
|
||||
st.markdown(
|
||||
f"""
|
||||
<div class="diagnosis-card" style="--diagnosis-color:{color}">
|
||||
@ -233,9 +223,9 @@ def _render_cylinder_detail(
|
||||
<p>{html.escape(explanation.severity_display)}</p>
|
||||
</div>
|
||||
<div class="diagnosis-metrics">
|
||||
<div><span>Wynik diagnozy</span><strong>{_format_confidence(explanation.confidence)}</strong></div>
|
||||
<div><span>Wynik oceny nasilenia</span><strong>{_format_confidence(severity_confidence)}</strong></div>
|
||||
<div><span>Priorytet</span><strong>{html.escape(priority)}</strong></div>
|
||||
<div><span>Średnie odchylenie</span><strong>{explanation.anomaly_score:.1f} mV</strong></div>
|
||||
<div><span>Główne pasma</span><strong>{html.escape(top_bands)}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
""",
|
||||
@ -267,15 +257,74 @@ def _render_cylinder_detail(
|
||||
with next_step:
|
||||
st.markdown("#### Następny krok")
|
||||
st.write(explanation.recommendation)
|
||||
source_label = (
|
||||
"Reguła anomalii"
|
||||
if explanation.decision_source == "ood_override"
|
||||
else "Klasyfikator spektralny"
|
||||
|
||||
|
||||
def _store_selected_cylinder(state_key: str, widget_key: str) -> None:
|
||||
st.session_state[state_key] = int(st.session_state[widget_key])
|
||||
|
||||
|
||||
def _comparison_label(value: int) -> str:
|
||||
if value == NO_COMPARISON:
|
||||
return "Bez porównania"
|
||||
return f"Cylinder {value:02d}"
|
||||
|
||||
|
||||
def _render_cylinder_selectors(
|
||||
analysis,
|
||||
available: list[int],
|
||||
selected_state_key: str,
|
||||
) -> tuple[int, list[int]]:
|
||||
selector_key = f"cylinder_selector_{analysis.engine_id}"
|
||||
selected = int(st.session_state[selected_state_key])
|
||||
if selector_key not in st.session_state or st.session_state[selector_key] not in available:
|
||||
st.session_state[selector_key] = selected
|
||||
|
||||
with st.container(border=True):
|
||||
st.markdown("### Wybór cylindra")
|
||||
columns = st.columns([1.35, 1.0, 1.0, 1.0], gap="medium")
|
||||
with columns[0]:
|
||||
primary = int(
|
||||
st.selectbox(
|
||||
"Cylinder do analizy",
|
||||
available,
|
||||
format_func=lambda value: f"Cylinder {value:02d}",
|
||||
key=selector_key,
|
||||
on_change=_store_selected_cylinder,
|
||||
args=(selected_state_key, selector_key),
|
||||
)
|
||||
st.caption(
|
||||
f"Źródło: {source_label} · wynik modelu nie jest prawdopodobieństwem."
|
||||
)
|
||||
|
||||
comparisons: list[int] = []
|
||||
for slot, column in enumerate(columns[1:], start=1):
|
||||
comparison_key = f"comparison_slot_{analysis.engine_id}_{slot}"
|
||||
options = [
|
||||
NO_COMPARISON,
|
||||
*(
|
||||
cylinder
|
||||
for cylinder in available
|
||||
if cylinder != primary and cylinder not in comparisons
|
||||
),
|
||||
]
|
||||
if (
|
||||
comparison_key not in st.session_state
|
||||
or st.session_state[comparison_key] not in options
|
||||
):
|
||||
st.session_state[comparison_key] = NO_COMPARISON
|
||||
with column:
|
||||
comparison = int(
|
||||
st.selectbox(
|
||||
f"Porównanie {slot}",
|
||||
options,
|
||||
format_func=_comparison_label,
|
||||
key=comparison_key,
|
||||
)
|
||||
)
|
||||
if comparison != NO_COMPARISON:
|
||||
comparisons.append(comparison)
|
||||
|
||||
st.session_state[selected_state_key] = primary
|
||||
return primary, comparisons
|
||||
|
||||
|
||||
def _render_technical(result: DiagnosisResult) -> None:
|
||||
st.markdown("### Walidacja i model")
|
||||
@ -305,7 +354,7 @@ def render_app(dependencies: AppDependencies | None = None) -> None:
|
||||
)
|
||||
_load_css()
|
||||
deps = dependencies or build_dependencies()
|
||||
_render_header(deps.live_inference)
|
||||
_render_header()
|
||||
|
||||
with st.sidebar:
|
||||
st.markdown("## Centrum diagnostyczne")
|
||||
@ -362,14 +411,11 @@ def render_app(dependencies: AppDependencies | None = None) -> None:
|
||||
mime="text/csv",
|
||||
width="stretch",
|
||||
)
|
||||
st.divider()
|
||||
st.caption(f"Wersja modelu: {deps.model_version}")
|
||||
|
||||
analysis = analyze_engine(result, str(engine_id))
|
||||
summary = summarize_engine(analysis)
|
||||
_render_summary(summary)
|
||||
|
||||
session_key = f"selected_cylinder_{analysis.engine_id}"
|
||||
session_key = f"active_cylinder_{analysis.engine_id}"
|
||||
available = analysis.measurements["cylinder"].astype(int).tolist()
|
||||
if session_key not in st.session_state or st.session_state[session_key] not in available:
|
||||
st.session_state[session_key] = summary.top_cylinder
|
||||
@ -389,27 +435,10 @@ def render_app(dependencies: AppDependencies | None = None) -> None:
|
||||
if view == VIEW_OVERVIEW:
|
||||
_render_engine_overview(analysis)
|
||||
elif view == VIEW_DETAIL:
|
||||
selector_col, comparison_col = st.columns([1.0, 2.0], gap="large")
|
||||
with selector_col:
|
||||
selected_from_box = st.selectbox(
|
||||
"Cylinder główny",
|
||||
selected_from_box, additional_cylinders = _render_cylinder_selectors(
|
||||
analysis,
|
||||
available,
|
||||
format_func=lambda value: f"Cylinder {value:02d}",
|
||||
key=session_key,
|
||||
)
|
||||
comparison_options = [
|
||||
cylinder for cylinder in available if cylinder != selected_from_box
|
||||
]
|
||||
with comparison_col:
|
||||
additional_cylinders = st.multiselect(
|
||||
"Porównaj z cylindrami",
|
||||
comparison_options,
|
||||
max_selections=3,
|
||||
format_func=lambda value: f"Cylinder {value:02d}",
|
||||
key=(
|
||||
f"comparison_{analysis.engine_id}_{int(selected_from_box)}"
|
||||
),
|
||||
help="Cylinder główny jest zawsze pokazany; można dodać trzy kolejne.",
|
||||
session_key,
|
||||
)
|
||||
comparison_cylinders = [
|
||||
int(selected_from_box),
|
||||
|
||||
@ -24,10 +24,6 @@
|
||||
[data-testid="stMainBlockContainer"] { padding-top: 2.1rem; max-width: 1500px; }
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
@ -36,23 +32,6 @@
|
||||
.product-header h1 { margin: .2rem 0 .25rem; color: #f3f8fa; font-size: 2rem; line-height: 1.12; letter-spacing: -.035em; }
|
||||
.product-header p { margin: 0; color: var(--muted); }
|
||||
.eyebrow { color: var(--cyan); font-size: .7rem; font-weight: 800; letter-spacing: .17em; }
|
||||
.runtime-badge {
|
||||
min-width: 218px;
|
||||
padding: .7rem .9rem;
|
||||
border: 1px solid rgba(56, 217, 150, .32);
|
||||
border-radius: var(--radius);
|
||||
background: rgba(56, 217, 150, .07);
|
||||
color: var(--green);
|
||||
font: 700 .72rem/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
.runtime-badge span { color: var(--muted); font-size: .64rem; }
|
||||
.runtime-fallback {
|
||||
border-color: rgba(245, 165, 36, .38);
|
||||
background: rgba(245, 165, 36, .08);
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@ -63,7 +42,7 @@
|
||||
margin-bottom: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.status-strip > div { padding: 1rem 1.2rem; border-right: 1px solid var(--line); }
|
||||
.status-strip > div { padding: 1rem 1.2rem; border-right: 1px solid var(--line); text-align: center; }
|
||||
.status-strip > div:last-child { border-right: 0; }
|
||||
.status-strip span, .diagnosis-metrics span { display: block; color: var(--muted); font-size: .66rem; font-weight: 800; letter-spacing: .11em; }
|
||||
.status-strip strong { display: block; margin-top: .2rem; font-size: 1.28rem; color: var(--text); }
|
||||
@ -97,6 +76,24 @@
|
||||
}
|
||||
.diagnosis-metrics strong { display: block; margin-top: .2rem; color: var(--text); font-size: 1.05rem; }
|
||||
|
||||
.overview-panel-title {
|
||||
display: flex;
|
||||
min-height: 2.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-testid="stVerticalBlockBorderWrapper"] {
|
||||
border-color: rgba(40, 183, 217, .32) !important;
|
||||
border-left: 4px solid var(--cyan) !important;
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(90deg, rgba(40, 183, 217, .06), rgba(16, 27, 34, .72));
|
||||
}
|
||||
|
||||
[data-testid="stMetric"] {
|
||||
min-height: 5.2rem;
|
||||
padding: .75rem;
|
||||
@ -140,9 +137,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.product-header { flex-direction: column; }
|
||||
.diagnosis-card { grid-template-columns: 1fr; }
|
||||
.runtime-badge { width: 100%; }
|
||||
.status-strip { grid-template-columns: repeat(2, 1fr); }
|
||||
.diagnosis-metrics { width: 100%; }
|
||||
}
|
||||
|
||||
@ -12,13 +12,13 @@
|
||||
|
||||
Komunikat: „Nie zmuszamy mechanika do czytania 21 wykresów. Najpierw widzi najważniejszy cylinder i pilność.”
|
||||
|
||||
## 1:15–2:20 — explainability
|
||||
## 1:15–2:20 — uzasadnienie diagnozy
|
||||
|
||||
1. Otwórz „Szczegóły cylindra”.
|
||||
2. Wybierz najwyżej sklasyfikowany cylinder.
|
||||
3. Opcjonalnie dodaj drugi cylinder do porównania.
|
||||
4. Porównaj widmo z medianą pozostałych cylindrów tego samego silnika.
|
||||
5. Wskaż zaznaczone pasma, wynik modelu, nasilenie i rekomendowany następny krok.
|
||||
5. Wskaż priorytet, zaznaczone pasma, nasilenie i rekomendowany następny krok.
|
||||
|
||||
Komunikat: „Referencją jest konkretny silnik, nie abstrakcyjna średnia całej floty. Dzięki temu wynik jest łatwiejszy do zweryfikowania przez człowieka.”
|
||||
|
||||
@ -28,7 +28,7 @@ Komunikat: „Referencją jest konkretny silnik, nie abstrakcyjna średnia całe
|
||||
2. Pokaż grupowe makro F1 0.981, trafność nasilenia 0.930 i wynik przy 5% braków.
|
||||
3. Powiedz, że cały silnik jest zawsze w jednym zbiorze walidacyjnym, a model działa na CPU.
|
||||
|
||||
Komunikat: „Wynik nie powstał przez przeciek między cylindrami tego samego silnika. Pipeline jest też testowany na brakach odpowiadających realnym danym warsztatowym.”
|
||||
Komunikat: „Wynik nie powstał przez przeciek między cylindrami tego samego silnika. Proces jest też testowany na brakach odpowiadających realnym danym warsztatowym.”
|
||||
|
||||
## 3:05–3:40 — własny plik i wartość
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ Nie. Status jest deterministyczną regułą biznesową opartą bezpośrednio na
|
||||
|
||||
## Czy score modelu jest prawdopodobieństwem awarii?
|
||||
|
||||
Nie. To niekalibrowany wynik `predict_proba`, używany wyłącznie do względnego porównania decyzji klasyfikatora. Interfejs nazywa go „score”, nie „pewnością”. Dla `unknown` utworzonego przez regułę OOD pozostaje pusty zamiast sztucznie generować wartość 50–99%.
|
||||
Nie. To niekalibrowany wynik `predict_proba`, używany wyłącznie do diagnostyki technicznej. Główny widok mechanika go nie pokazuje; zamiast tego prezentuje diagnozę, nasilenie, priorytet i następny krok. Dla `unknown` utworzonego przez regułę OOD wartość pozostaje pusta.
|
||||
|
||||
## Co robi aplikacja przy błędnym pliku?
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
| Uwaga z przeglądu | Zmiana | Dowód regresji |
|
||||
| --- | --- | --- |
|
||||
| Wybór cylindra niepotrzebnie obciążał przegląd silnika | wybór jest dostępny wyłącznie w widoku szczegółów; przegląd pokazuje tylko stan i priorytety | `test_cylinder_selector_exists_only_in_detail_view` |
|
||||
| Selektor i karta mogły po pierwszym wejściu wskazywać różne cylindry | trwały stan wyboru i stan widżetu mają osobne klucze; test porównuje wartość selektora z nagłówkiem karty | `test_cylinder_selector_exists_only_in_detail_view` |
|
||||
| Wielokrotny wybór pokazywał angielskie i sprzeczne komunikaty | zastąpiono go trzema opcjonalnymi, polskimi polami porównawczymi bez możliwości duplikatów | `test_cylinder_selector_exists_only_in_detail_view` |
|
||||
| Automatyczna skala mapy wyolbrzymiała różnice w zdrowych silnikach | mapa ma stałą skalę ±20 mV i opis diagnozy przy każdym cylindrze | `test_all_chart_factories_return_populated_figures` |
|
||||
| Status `WYMAGA WERYFIKACJI` był nieosiągalny | status zależy bezpośrednio od label/severity; `unknown` bez nazwanej usterki ma dedykowany status | `test_unknown_only_engine_requires_verification` |
|
||||
| Engine Health 0–100 był arbitralny | liczba została usunięta; UI pokazuje najwyższe rzeczywiste severity i porządkowy triage | testy explainability + smoke UI |
|
||||
|
||||
@ -21,12 +21,12 @@ class StreamlitSmokeTests(unittest.TestCase):
|
||||
)
|
||||
self.assertFalse(any("C01" in button.label for button in app.button))
|
||||
self.assertFalse(
|
||||
any(selector.label == "Cylinder główny" for selector in app.selectbox)
|
||||
any(selector.label == "Cylinder do analizy" for selector in app.selectbox)
|
||||
)
|
||||
rendered = "\n".join(markdown.value for markdown in app.markdown)
|
||||
self.assertIn("Konsola diagnostyczna ENGIN", rendered)
|
||||
self.assertIn("ARTEFAKT ZWERYFIKOWANY", rendered)
|
||||
self.assertIn("runtime-verified", rendered)
|
||||
self.assertNotIn("MODEL GOTOWY", rendered)
|
||||
self.assertNotIn("ARTEFAKT ZWERYFIKOWANY", rendered)
|
||||
self.assertNotIn("CPU · DANE LOKALNE", rendered)
|
||||
for english_fragment in (
|
||||
"Diagnostic Console",
|
||||
@ -59,6 +59,13 @@ class StreamlitSmokeTests(unittest.TestCase):
|
||||
diagnostic_columns = set(diagnostic_table.value.columns)
|
||||
self.assertIn("Źródło decyzji", diagnostic_columns)
|
||||
self.assertNotIn("decision_source", diagnostic_columns)
|
||||
overview_table = next(
|
||||
table for table in app.dataframe if "Priorytet" in table.value.columns
|
||||
)
|
||||
self.assertEqual(
|
||||
list(overview_table.value.columns),
|
||||
["Cylinder", "Diagnoza", "Nasilenie", "Priorytet"],
|
||||
)
|
||||
|
||||
def test_upload_mode_has_safe_empty_state(self) -> None:
|
||||
app = AppTest.from_file(str(ROOT / "app.py"), default_timeout=30).run()
|
||||
@ -69,21 +76,35 @@ class StreamlitSmokeTests(unittest.TestCase):
|
||||
def test_cylinder_selector_exists_only_in_detail_view(self) -> None:
|
||||
app = AppTest.from_file(str(ROOT / "app.py"), default_timeout=30).run()
|
||||
self.assertFalse(
|
||||
any(selector.label == "Cylinder główny" for selector in app.selectbox)
|
||||
any(selector.label == "Cylinder do analizy" for selector in app.selectbox)
|
||||
)
|
||||
app.segmented_control[0].set_value("Szczegóły cylindra").run()
|
||||
self.assertEqual(app.segmented_control[0].value, "Szczegóły cylindra")
|
||||
detail_selector = next(
|
||||
selector for selector in app.selectbox if selector.label == "Cylinder główny"
|
||||
selector
|
||||
for selector in app.selectbox
|
||||
if selector.label == "Cylinder do analizy"
|
||||
)
|
||||
rendered = "\n".join(markdown.value for markdown in app.markdown)
|
||||
self.assertIn(f"CYLINDER {int(detail_selector.value):02d}", rendered)
|
||||
detail_selector.set_value(3).run()
|
||||
detail_selector = next(
|
||||
selector for selector in app.selectbox if selector.label == "Cylinder główny"
|
||||
selector
|
||||
for selector in app.selectbox
|
||||
if selector.label == "Cylinder do analizy"
|
||||
)
|
||||
self.assertEqual(detail_selector.value, 3)
|
||||
comparison = app.multiselect[0]
|
||||
self.assertEqual(comparison.label, "Porównaj z cylindrami")
|
||||
comparison.set_value([1, 2, 4]).run()
|
||||
for slot, cylinder in enumerate((1, 2, 4), start=1):
|
||||
comparison = next(
|
||||
selector
|
||||
for selector in app.selectbox
|
||||
if selector.label == f"Porównanie {slot}"
|
||||
)
|
||||
comparison.set_value(cylinder).run()
|
||||
self.assertEqual(len(app.exception), 0)
|
||||
self.assertEqual(len(app.multiselect), 0)
|
||||
rendered = "\n".join(markdown.value for markdown in app.markdown)
|
||||
self.assertIn("CYLINDER 03", rendered)
|
||||
self.assertIn("### Wybór cylindra", rendered)
|
||||
self.assertNotIn("Select all", rendered)
|
||||
self.assertNotIn("You can select up to", rendered)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user