4bdfb673cc
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m2s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m56s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 34s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 23s
build / build (push) Successful in 1m40s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m12s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m52s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 32s
Testy / Kontrola składni wszystkich warstw (push) Successful in 19s
„Logika dwóch lokalizacji": dotąd offset GMT był ręcznym polem (PRE-19) — trzeba
było go znać i samemu pamiętać o czasie letnim. Teraz liczymy go z lokalizacji.
Sedno wymagania: strefę ustalamy RAZ i trzymamy jako stałą liczbę, żeby drobna
zmiana współrzędnych nie przerzuciła DST i nie „przeskoczyła" Ascendenta na
sąsiedni znak. „Większa miejscowość z bazy" okazuje się zbędna — strefa IANA jest
i tak regionalna, więc wioska daje tę samą strefę co pobliskie miasto.
Jak:
- `timezone.py`: współrzędne → strefa IANA (tzfpy, OFFLINE — bez sieci), a z niej
offset DLA DATY URODZENIA. `zoneinfo`/`tzdata` znają reguły historyczne i DST:
Kraków 1984 to +1h zimą, +2h latem; Katmandu +5:45. Degraduje się do None (brak
biblioteki / punkt bez strefy / zła data) — wtedy zostaje ręczny offset.
- Endpoint `GET /timezone?lat&lon&date&time` → {tz, offset, dst, label}. 404, gdy
nie da się ustalić.
- `geo.js`: po wyborze miejsca (mapa / wyszukiwarka / „Tu i teraz") oraz przy
zmianie DATY (bo DST zależy od pory roku) pobiera offset i wypełnia pole
tz_offset, pokazując wykrytą strefę („Wykryto: Europe/Warsaw · +2:00 (czas
letni)"). Pole zostaje edytowalne. Na wejściu podpowiada tylko gdy offset
wygląda na nieustawiony — nie nadpisuje wartości ręcznie wpisanej i wysłanej.
Zależności (lekkie, offline): tzfpy (wheel Rust) + tzdata (dla zoneinfo w slim-obrazie).
Weryfikacja: żywy serwer — Kraków 1984-06 → +2:00 (czas letni), 1984-01 → +1:00,
Katmandu → +5:45. Testy: +12 strefa (moduł + endpoint), +5 JS. Prezentacja 187.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
5.1 KiB
Python
125 lines
5.1 KiB
Python
"""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
|
|
|
|
|
|
# ---- strefa czasowa z lokalizacji (PRE-03) ----
|
|
|
|
def test_setcoords_resolves_timezone():
|
|
"""Nowe współrzędne (mapa/wyszukiwarka) mają odświeżyć strefę i offset."""
|
|
src = _geo_source()
|
|
body = src[src.index("function setCoords"):src.index("\n }", src.index("function setCoords"))]
|
|
assert "resolveTz()" in body
|
|
|
|
|
|
def test_now_button_resolves_timezone():
|
|
"""„Tu i teraz" to nowe miejsce → nowa strefa."""
|
|
assert "resolveTz()" in _handler_body()
|
|
|
|
|
|
def test_date_change_recomputes_timezone():
|
|
"""Czas letni zależy od daty — zmiana daty musi przeliczyć offset."""
|
|
src = _geo_source()
|
|
assert "dateEl.addEventListener('change', resolveTz)" in src
|
|
|
|
|
|
def test_timezone_hint_does_not_clobber_manual_offset_on_load():
|
|
"""Na wejściu podpowiadamy strefę TYLKO gdy offset wygląda na nieustawiony —
|
|
inaczej nadpisalibyśmy wartość ręcznie wpisaną i wysłaną przez użytkownika."""
|
|
src = _geo_source()
|
|
assert "parseFloat(tzEl.value) === 0" in src
|
|
|
|
|
|
def test_timezone_fills_offset_field_and_shows_zone():
|
|
src = _geo_source()
|
|
start = src.index("function resolveTz")
|
|
body = src[start:src.index("\n }", start)]
|
|
assert "/timezone?lat=" in body
|
|
assert "setVal(tzEl, d.offset)" in body # wypełnia pole offsetu
|
|
assert "d.label" in body # pokazuje wykrytą strefę
|