Compare commits

..

2 Commits

Author SHA1 Message Date
gitea f34016a4a5 feat(prezentacja): wspolne dane formularza miedzy zakladkami + imie i nazwisko (PRE-21)
build / build (push) Successful in 58s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m53s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 38s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
Trzecia wskazowka partnerow. Kazda zakladke (Horoskop / Interpretacje /
Kalendarz) wypelnialo sie od nowa — te same imie, data, godzina, strefa
i miejsce. Teraz raz wpisane dane wedruja za uzytkownikiem, a zmiana w jednej
zakladce przenosi sie na pozostale.

Nowy formsync.js trzyma stan w localStorage. Dlaczego nie sesja na serwerze:
serwer zostaje BEZSTANOWY — zadnego magazynu sesji, zadnych danych urodzeniowych
trzymanych po stronie uslugi (spojne z postawa z LOG-32/PRE-16). Przy okazji
dane synchronizuja sie tez miedzy osobnymi kartami przegladarki, bo zdarzenie
`storage` daje to za darmo.

Dolozone pole „imie i nazwisko" (wszystkie trzy zakladki) — potrzebne do
naglowka raportu PDF (PRE-24). Handlery przyjmuja je i oddaja, wiec nie znika
po przeliczeniu.

Dwie rzeczy, ktore trzeba bylo domknac, zeby to dzialalo naprawde:

1. KOLEJNOSC SKRYPTOW. formsync.js ladowany w <head> z `defer` — skrypty defer
   wykonuja sie w kolejnosci dokumentu, wiec ten zdazy odtworzyc wspolrzedne,
   ZANIM geo.js zbuduje mape. Mapa startuje od razu we wlasciwym miejscu,
   zamiast przeskakiwac po chwili.

2. geo.js ustawia pola z KODU (`.value = ...`), co samo z siebie NIE wywoluje
   zdarzen — bez tego synchronizacja przegapilaby kazdy wybor z mapy, z
   wyszukiwarki i z „Tu i teraz". Dolozony setVal(), ktory jawnie zglasza
   `change`.

Zakladka bez danego pola (np. Sygnifikatory) nie kasuje wartosci zapamietanej
gdzie indziej; uszkodzony wpis w localStorage nie blokuje formularza.

Q-14 rozstrzygniete: lancuch LaTeX->PDF stanie jako OSOBNA USLUGA render —
spojne z izolacja swisseph (LOG-27), obraz produktu zostaje maly.

Weryfikacja na zywym stacku (data+logika+prezentacja) w przegladarce: dane
wpisane w Horoskopie pojawily sie w Interpretacjach; zmiana godziny w
Interpretacjach dotarla do Kalendarza; pola nieobecne na zakladce (zodiak,
system domow) przetrwaly; po POST imie zostalo, a horoskop policzyl sie
normalnie. Testy: 22 nowe strukturalne. Calosc: prezentacja 67 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:12:49 +00:00
gitea 5c82e8bd9f fix(prezentacja): offset wzgledem GMT + nazwa lokalizacji po „Tu i teraz”
Dwie pierwsze wskazowki od partnerow biznesowych; pozostale piec zapisane jako
wymagania (PRE-21..PRE-24) do zrobienia w kolejnych krokach.

PRE-19 — offset wzgledem GMT. Etykieta mowila „Strefa (offset h)”, czyli nie
bylo jasne, wzgledem czego liczymy przesuniecie. Teraz „Offset wzgledem GMT (h)”
z podpowiedzia. Krok juz byl 15-minutowy (0,25 h) — dolozony zakres −12…+14,
zeby nie dalo sie wpisac strefy, ktora nie istnieje. Zmiana w trzech zakladkach,
ktore maja to pole (Horoskop, Interpretacje, Kalendarz).

PRE-20 — po „Tu i teraz” wspolrzedne i pineska skakaly na biezace polozenie, ale
w polu tekstowym zostawala STARA, wczesniej wpisana nazwa. Formularz pokazywal
jedno miejsce, a liczyl dla innego — cicha pomylka, nic sie nie wywalalo.
Handler zdarzenia astrololo:coords odswieza teraz nazwe przez reverseName.

Przy okazji druga strona tego samego bledu: reverseName czysci pole ZANIM
wysle zapytanie. Gdyby /reverse nie odpowiedzialo (brak sieci), zostalaby stara
nazwa — lepiej puste pole i poprawne wspolrzedne niz nazwa, ktora klamie.

Wymagania: PRE-19/20 (zrobione), PRE-21 wspolne dane miedzy zakladkami wraz
z polem imie i nazwisko, PRE-22 wiele predykcji okresowych w pamieci sesji,
PRE-23 zakladka „Skompiluj”, PRE-24 raport PDF przez LaTeX. Dolozone pytanie
otwarte Q-14 o lancuch LaTeX→PDF (gdzie postawic TeX Live, silnik unicode owy
pod glify, konwersja SVG) — decyzja wplywa na deploy i rozmiar obrazow.

Testy: 12 nowych, strukturalnych na zrodle (JS-a nie uruchomimy, a obie regresje
sa ciche). Sprawdzone sabotazem — po cofnieciu kazdej poprawki czerwienieja.
Calosc: prezentacja 45 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:12:49 +00:00
10 changed files with 340 additions and 13 deletions
Binary file not shown.
+6 -3
View File
@@ -71,6 +71,7 @@ def chart_form(request: Request):
@app.post("/", response_class=HTMLResponse)
def chart_compute(
request: Request,
person: str = Form(""), # imie i nazwisko — do naglowka raportu (PRE-21/24)
date: str = Form(...),
time: str = Form(...),
tz_offset: float = Form(0.0),
@@ -81,7 +82,7 @@ def chart_compute(
zodiac: str = Form("tropical"),
tables: bool = Form(False),
):
form = {"date": date, "time": time, "tz_offset": tz_offset,
form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset,
"lat": lat, "lon": lon, "house_system": house_system, "stations": stations,
"zodiac": zodiac, "tables": tables}
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
@@ -137,6 +138,7 @@ def interpret_form(request: Request):
@app.post("/interpret", response_class=HTMLResponse)
def interpret_run(
request: Request,
person: str = Form(""), # imie i nazwisko — do naglowka raportu (PRE-21/24)
date: str = Form(...),
time: str = Form(...),
tz_offset: float = Form(0.0),
@@ -148,7 +150,7 @@ def interpret_run(
llm_provider: str = Form("local"),
llm_model: str = Form(""),
):
form = {"date": date, "time": time, "tz_offset": tz_offset,
form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset,
"lat": lat, "lon": lon, "group": group, "prompt_budget": prompt_budget,
"llm_provider": llm_provider, "llm_model": llm_model}
ctx: dict = {"form": form, "result": None, "error": None, "moment": None,
@@ -188,6 +190,7 @@ def timeline_form(request: Request):
@app.post("/timeline", response_class=HTMLResponse)
def timeline_run(
request: Request,
person: str = Form(""), # imie i nazwisko — do naglowka raportu (PRE-21/24)
date: str = Form(...),
time: str = Form(...),
tz_offset: float = Form(0.0),
@@ -200,7 +203,7 @@ def timeline_run(
llm_provider: str = Form("local"),
llm_model: str = Form(""),
):
form = {"date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon,
form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon,
"from_date": from_date, "to_date": to_date, "prompt_budget": prompt_budget,
"llm_provider": llm_provider, "llm_model": llm_model}
ctx: dict = {"form": form, "result": None, "error": None, "moment": None,
@@ -0,0 +1,98 @@
// Wspólne dane formularza między zakładkami (PRE-21).
//
// Problem: każdą zakładkę (Horoskop / Interpretacje / Kalendarz) wypełniało się
// od nowa — te same imię, data, godzina, strefa i miejsce. Teraz raz wpisane
// dane wędrują za użytkownikiem.
//
// Dlaczego localStorage, a nie sesja na serwerze: serwer zostaje BEZSTANOWY
// (żadnego magazynu sesji, żadnych danych urodzeniowych trzymanych po stronie
// usługi), a przy okazji dane synchronizują się też między osobnymi kartami
// przeglądarki — zdarzenie `storage` daje to za darmo.
//
// UWAGA na kolejność: ten skrypt ładujemy w <head> z `defer`, więc wykonuje się
// PO sparsowaniu dokumentu, ale PRZED geo.js (skrypty defer idą w kolejności
// dokumentu). Dzięki temu mapa startuje już z odtworzonymi współrzędnymi i nie
// trzeba jej potem przestawiać.
(function () {
'use strict';
var KEY = 'astrololo.form.v1';
// Pola wspólne dla zakładek. Nazwa pola formularza -> jak je znaleźć.
// `geoSearch` (nazwa miejsca) nie ma atrybutu name — szukamy po id.
var FIELDS = [
{ key: 'person', sel: 'input[name=person]' },
{ key: 'date', sel: 'input[name=date]' },
{ key: 'time', sel: 'input[name=time]' },
{ key: 'tz_offset', sel: 'input[name=tz_offset]' },
{ key: 'lat', sel: 'input[name=lat]' },
{ key: 'lon', sel: 'input[name=lon]' },
{ key: 'house_system', sel: 'select[name=house_system]' },
{ key: 'zodiac', sel: 'select[name=zodiac]' },
{ key: 'place', sel: '#geoSearch' }
];
function load() {
try {
return JSON.parse(localStorage.getItem(KEY) || '{}') || {};
} catch (e) {
return {}; // uszkodzony wpis nie może zablokować strony
}
}
function save(state) {
try {
localStorage.setItem(KEY, JSON.stringify(state));
} catch (e) {
/* tryb prywatny albo brak miejsca — trudno, formularz ma działać dalej */
}
}
function apply(state) {
FIELDS.forEach(function (f) {
var el = document.querySelector(f.sel);
if (!el) return;
var v = state[f.key];
if (v === undefined || v === null || v === '') return;
if (el.value !== v) el.value = v;
});
}
function collect() {
var state = load();
FIELDS.forEach(function (f) {
var el = document.querySelector(f.sel);
if (!el) return; // pola nieobecnego na tej zakładce nie kasujemy
if (el.value === '' || el.value === null) return;
state[f.key] = el.value;
});
return state;
}
// 1) odtwórz to, co użytkownik wpisał gdzie indziej
apply(load());
// 2) zapisuj każdą zmianę (input dla wpisywania, change dla list i kalendarzy)
FIELDS.forEach(function (f) {
var el = document.querySelector(f.sel);
if (!el) return;
['input', 'change'].forEach(function (ev) {
el.addEventListener(ev, function () { save(collect()); });
});
});
// 3) zapisz stan po wysłaniu formularza — po przeładowaniu strony pola mają
// pokazywać to, co faktycznie policzyliśmy
document.addEventListener('submit', function () { save(collect()); }, true);
// 4) druga otwarta karta zmieniła dane -> dociągnij je tutaj na żywo
window.addEventListener('storage', function (e) {
if (e.key === KEY) apply(load());
});
// 5) geo.js ustawia lat/lon z mapy poza polami formularza — po takiej zmianie
// też chcemy zapamiętać (zdarzenie leci z „Tu i teraz" oraz z mapy)
document.addEventListener('astrololo:coords', function () {
setTimeout(function () { save(collect()); }, 0);
});
})();
+23 -4
View File
@@ -38,18 +38,32 @@
// Kontener mógł być renderowany zanim ustaliła się jego szerokość — poprawka.
setTimeout(function () { map.invalidateSize(); }, 60);
// Ustawienie `.value` z kodu NIE wywołuje zdarzeń, więc synchronizacja
// między zakładkami (formsync.js) przegapiłaby zmiany z mapy. Zgłaszamy je
// jawnie przez `change`.
function setVal(el, value) {
if (!el) return;
el.value = value;
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function setCoords(lat, lon, fromMap) {
latEl.value = lat.toFixed(4);
lonEl.value = lon.toFixed(4);
setVal(latEl, lat.toFixed(4));
setVal(lonEl, lon.toFixed(4));
marker.setLatLng([lat, lon]);
if (!fromMap) map.setView([lat, lon], Math.max(map.getZoom(), 12));
if (fromMap) reverseName(lat, lon);
}
function reverseName(lat, lon) {
// Czyścimy pole OD RAZU. Dopóki nie znamy nazwy nowego punktu, stara jest
// po prostu nieprawdziwa — pokazywałaby inne miejsce niż współrzędne obok.
// Dotyczy też sytuacji, gdy /reverse nie odpowie (brak sieci): lepiej puste
// pole i poprawne współrzędne niż nazwa, która kłamie.
if (searchEl) setVal(searchEl, '');
fetch('/reverse?lat=' + lat + '&lon=' + lon)
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) { if (d && d.name) searchEl.value = d.name; })
.then(function (d) { if (d && d.name && searchEl) setVal(searchEl, d.name); })
.catch(function () {});
}
@@ -68,7 +82,7 @@
li.textContent = it.name;
li.addEventListener('click', function () {
setCoords(it.lat, it.lon, false);
searchEl.value = it.name;
setVal(searchEl, it.name);
resultsEl.hidden = true;
});
resultsEl.appendChild(li);
@@ -108,9 +122,14 @@
});
// ---- synchronizacja z „Tu i teraz" ----
// Poza przesunięciem pineski MUSIMY odświeżyć pole tekstowe: po „Tu i teraz"
// zostawała w nim stara, ręcznie wpisana nazwa, więc formularz pokazywał jedno
// miejsce, a współrzędne wskazywały już inne. reverseName pyta o nazwę punktu,
// na który faktycznie ustawiła się mapa.
document.addEventListener('astrololo:coords', function () {
marker.setLatLng([curLat(), curLon()]);
map.setView([curLat(), curLon()], Math.max(map.getZoom(), 12));
reverseName(curLat(), curLon());
});
});
})();
@@ -5,6 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>astrololo · {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/static/styles.css">
{# Wspólne dane formularza między zakładkami (PRE-21). W <head> z `defer`
CELOWO: skrypty defer wykonują się w kolejności dokumentu, więc ten zdąży
odtworzyć współrzędne, ZANIM geo.js zbuduje mapę — mapa startuje od razu
we właściwym miejscu, bez przestawiania. #}
<script defer src="/static/formsync.js"></script>
</head>
<body>
<main>
@@ -6,6 +6,12 @@
<p class="sub">Wpisz dane momentu i miejsca — program policzy pozycje obiektów, osie i domy (silnik efemeryd warstwy logicznej).</p>
<form method="post" action="/">
<div class="grid">
<label title="Trafia do nagłówka raportu PDF (PRE-24). Nie jest nigdzie wysyłane poza aplikację ani zapisywane na serwerze.">Imię i nazwisko
<input type="text" name="person" autocomplete="name" placeholder="np. Jan Kowalski"
value="{{ form.person or '' }}">
</label>
</div>
<div class="grid">
<label>Data
<input type="date" name="date" value="{{ form.date or '' }}" required>
@@ -13,8 +19,9 @@
<label>Godzina (lokalna)
<input type="time" name="time" value="{{ form.time or '' }}" required>
</label>
<label>Strefa (offset h)
<input type="number" name="tz_offset" step="0.25" value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
<label title="Przesunięcie czasu lokalnego względem GMT/UTC. Krok 15 minut (0,25 h) — bo część stref ma przesunięcie o pół lub ćwierć godziny, np. Indie +5:30, Nepal +5:45.">Offset względem GMT (h)
<input type="number" name="tz_offset" step="0.25" min="-12" max="14"
value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
</label>
</div>
<div class="grid">
@@ -6,6 +6,12 @@
<p class="sub">Program policzy horoskop i wyszuka w bazie interpretacje pasujące do obliczeń (pierwsza wersja: planeta w swoim znaku).</p>
<form method="post" action="/interpret">
<div class="grid">
<label title="Trafia do nagłówka raportu PDF (PRE-24). Nie jest nigdzie wysyłane poza aplikację ani zapisywane na serwerze.">Imię i nazwisko
<input type="text" name="person" autocomplete="name" placeholder="np. Jan Kowalski"
value="{{ form.person or '' }}">
</label>
</div>
<div class="grid">
<label>Data
<input type="date" name="date" value="{{ form.date or '' }}" required>
@@ -13,8 +19,9 @@
<label>Godzina (lokalna)
<input type="time" name="time" value="{{ form.time or '' }}" required>
</label>
<label>Strefa (offset h)
<input type="number" name="tz_offset" step="0.25" value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
<label title="Przesunięcie czasu lokalnego względem GMT/UTC. Krok 15 minut (0,25 h) — bo część stref ma przesunięcie o pół lub ćwierć godziny, np. Indie +5:30, Nepal +5:45.">Offset względem GMT (h)
<input type="number" name="tz_offset" step="0.25" min="-12" max="14"
value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
</label>
<label>Szerokość (lat)
<input type="number" name="lat" step="0.0001" value="{{ form.lat if form.lat is not none else 0 }}">
@@ -6,6 +6,12 @@
<p class="sub">Zbiorcza oś czasu technik predykcyjnych (profekcje, solariusze, dyrekcje solar-arc) z interpretacjami z bazy dla dat.</p>
<form method="post" action="/timeline">
<div class="grid">
<label title="Trafia do nagłówka raportu PDF (PRE-24). Nie jest nigdzie wysyłane poza aplikację ani zapisywane na serwerze.">Imię i nazwisko
<input type="text" name="person" autocomplete="name" placeholder="np. Jan Kowalski"
value="{{ form.person or '' }}">
</label>
</div>
<div class="grid">
<label>Data urodzenia
<input type="date" name="date" value="{{ form.date or '' }}" required>
@@ -13,8 +19,9 @@
<label>Godzina (lokalna)
<input type="time" name="time" value="{{ form.time or '' }}" required>
</label>
<label>Strefa (offset h)
<input type="number" name="tz_offset" step="0.25" value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
<label title="Przesunięcie czasu lokalnego względem GMT/UTC. Krok 15 minut (0,25 h) — bo część stref ma przesunięcie o pół lub ćwierć godziny, np. Indie +5:30, Nepal +5:45.">Offset względem GMT (h)
<input type="number" name="tz_offset" step="0.25" min="-12" max="14"
value="{{ form.tz_offset if form.tz_offset is not none else 0 }}">
</label>
<label>Szerokość (lat)
<input type="number" name="lat" step="0.0001" value="{{ form.lat if form.lat is not none else 0 }}">
@@ -0,0 +1,93 @@
"""Wspólne dane formularza między zakładkami + pole imienia (PRE-21).
Znów test strukturalny na źródle: synchronizacja żyje w JS, a jej awaria jest
CICHA — formularz po prostu przestaje pamiętać dane i użytkownik wpisuje je od
nowa. Pilnujemy więc reguł, które łatwo zgubić przy kolejnych zmianach.
"""
import pathlib
import pytest
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
TEMPLATES = APP / "templates"
SYNC_JS = (APP / "static" / "formsync.js").read_text(encoding="utf-8")
GEO_JS = (APP / "static" / "geo.js").read_text(encoding="utf-8")
BASE_HTML = (TEMPLATES / "base.html").read_text(encoding="utf-8")
MAIN_PY = (APP / "main.py").read_text(encoding="utf-8")
WITH_FORM = ("chart.html", "interpret.html", "timeline.html")
# Pola, które mają wędrować między zakładkami. Gdy dojdzie nowe wspólne pole,
# ten wykaz ma się nie rozjechać z formsync.js.
SHARED = ("person", "date", "time", "tz_offset", "lat", "lon",
"house_system", "zodiac", "place")
# ─────────────────────────────── pole imienia ────────────────────────────
@pytest.mark.parametrize("name", WITH_FORM)
def test_person_field_present_on_every_form(name):
"""Imię i nazwisko trafia do nagłówka raportu PDF (PRE-24) — musi dać się
je wpisać z każdej zakładki, bo dane są wspólne."""
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert 'name="person"' in html, f"{name}: brak pola imienia"
@pytest.mark.parametrize("name", WITH_FORM)
def test_person_value_survives_submit(name):
"""Po wysłaniu formularza pole ma pokazywać to, co wpisano — inaczej znikałoby
przy każdym przeliczeniu."""
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert "form.person" in html, f"{name}: wartość imienia nie wraca po POST"
def test_handlers_accept_and_echo_person():
"""Trzy handlery renderujące formularz przyjmują `person` i wkładają go
z powrotem do `form`."""
assert MAIN_PY.count("person: str = Form") == 3
assert MAIN_PY.count('"person": person') == 3
# ─────────────────────── synchronizacja między zakładkami ────────────────
@pytest.mark.parametrize("key", SHARED)
def test_every_shared_field_is_synced(key):
assert f"'{key}'" in SYNC_JS, f"pole {key} nie jest synchronizowane"
def test_sync_script_loaded_before_map_script():
"""KOLEJNOŚĆ MA ZNACZENIE: formsync w <head> z `defer` wykonuje się przed
geo.js (skrypty defer idą w kolejności dokumentu), więc mapa startuje już
z odtworzonymi współrzędnymi zamiast skakać po chwili."""
assert "formsync.js" in BASE_HTML, "formsync.js nie jest ładowany globalnie"
assert "defer" in BASE_HTML.split("formsync.js")[0].rsplit("<script", 1)[-1] + "defer"
def test_sync_reacts_to_other_browser_tab():
"""Zmiana w drugiej otwartej karcie ma dociągnąć się tutaj (zdarzenie storage)."""
assert "'storage'" in SYNC_JS
def test_sync_persists_on_submit():
assert "'submit'" in SYNC_JS
def test_corrupt_storage_does_not_break_the_page():
"""Uszkodzony wpis w localStorage nie może zablokować formularza."""
assert "catch" in SYNC_JS and "JSON.parse" in SYNC_JS
def test_map_changes_are_reported_to_sync():
"""geo.js ustawia pola z KODU (`.value = ...`), co samo z siebie nie wywołuje
zdarzeń — bez jawnego `change` synchronizacja przegapiłaby wybór z mapy."""
assert "function setVal" in GEO_JS
assert "dispatchEvent(new Event('change'" in GEO_JS
# wszystkie miejsca ustawiające pola idą przez setVal
assert "latEl.value =" not in GEO_JS.replace("el.value = value", "")
assert "searchEl.value = d.name" not in GEO_JS
def test_sync_does_not_wipe_fields_missing_on_a_tab():
"""Zakładka bez danego pola (np. Sygnifikatory) nie może wyczyścić wartości
zapamiętanej z innej zakładki."""
assert "if (!el) return;" in SYNC_JS
@@ -0,0 +1,88 @@
"""Poprawki zgłoszone przez partnerów: offset względem GMT i lokalizacja po
kliknięciu „Tu i teraz" (PRE-19, PRE-20).
JS-a tu nie uruchomimy, więc pilnujemy tego strukturalnie — tak samo jak
niezmienników klienta HTTP. Obie regresje są ciche (nic się nie wywala, tylko
formularz pokazuje nieprawdę), więc test na źródle jest tu wart więcej niż zwykle.
"""
import pathlib
import pytest
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
TEMPLATES = APP / "templates"
GEO_JS = APP / "static" / "geo.js"
WITH_OFFSET = ("chart.html", "interpret.html", "timeline.html")
# ───────────────────────────── PRE-19: offset względem GMT ───────────────
@pytest.mark.parametrize("name", WITH_OFFSET)
def test_offset_label_says_relative_to_gmt(name):
"""Etykieta musi mówić, WZGLĘDEM CZEGO liczymy przesunięcie."""
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert "Offset względem GMT" in html, f"{name}: etykieta nie mówi o GMT"
@pytest.mark.parametrize("name", WITH_OFFSET)
def test_offset_step_is_quarter_hour(name):
"""Krok 15 minut = 0,25 h. Strefy pół- i ćwierćgodzinne (Indie +5:30,
Nepal +5:45) są realne — przy kroku 1 h nie dało się ich ustawić."""
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert 'name="tz_offset" step="0.25"' in html, f"{name}: krok inny niż 15 minut"
@pytest.mark.parametrize("name", WITH_OFFSET)
def test_offset_range_covers_real_zones(name):
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert 'min="-12"' in html and 'max="14"' in html, f"{name}: brak sensownego zakresu stref"
# ──────────────────── PRE-20: „Tu i teraz" odświeża nazwę miejsca ────────
def _geo_source() -> str:
return GEO_JS.read_text(encoding="utf-8")
def _handler_body() -> str:
"""Ciało handlera zdarzenia astrololo:coords (wysyłanego przez „Tu i teraz").
Szukamy KONKRETNIE `addEventListener`, nie samej nazwy zdarzenia — ta pada
także w komentarzu nagłówkowym pliku i wtedy wycięlibyśmy komentarz zamiast
kodu (test przechodziłby albo padał z niewłaściwego powodu).
"""
src = _geo_source()
start = src.index("addEventListener('astrololo:coords'")
end = src.index("});", start)
return src[start:end]
def test_now_button_handler_refreshes_place_name():
"""REGRESJA: po „Tu i teraz" pineska i współrzędne skakały na nowe miejsce,
a w polu tekstowym zostawała stara nazwa — formularz pokazywał jedno miejsce,
a liczył dla innego."""
assert "reverseName(" in _handler_body(), (
"handler astrololo:coords nie odświeża nazwy miejsca (reverseName)"
)
def test_reverse_name_clears_field_before_lookup():
"""Nazwa jest czyszczona ZANIM przyjdzie odpowiedź. Gdyby /reverse nie
odpowiedziało (brak sieci), zostałaby stara nazwa — czyli pole kłamałoby
o lokalizacji. Lepiej puste pole niż nieprawdziwe."""
src = _geo_source()
start = src.index("function reverseName")
body = src[start:src.index("\n }", start)]
# sposób zapisu może się zmienić (bezpośrednie .value albo setVal, które
# dodatkowo zgłasza `change`) — pilnujemy KOLEJNOŚCI, nie składni
clear_at = max(body.find("searchEl.value = ''"), body.find("setVal(searchEl, '')"))
fetch_at = body.find("fetch(")
assert clear_at != -1, "reverseName nie czyści pola nazwy"
assert clear_at < fetch_at, "pole czyszczone dopiero po zapytaniu — stara nazwa zdąży zostać"
def test_now_button_still_syncs_map():
"""Odświeżenie nazwy nie może zastąpić przesunięcia pineski i widoku mapy."""
body = _handler_body()
assert "marker.setLatLng" in body and "map.setView" in body