fd79513ce2
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym długiem, ale niczego nie blokuje. PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała). KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji, których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią kolejność importów. To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa ekranu, którego nie ma. EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną, więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie różne gwarancje, a eksport wynosi najwięcej treści baz naraz. MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma. Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog generowania przeniesiony z app/dodatki na app/moduly/dodatki. ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny (realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności. Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj", w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które audyt podnosił wcześniej. TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował, dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten obraz, i że w nawigacji nie ma martwych odsyłaczy. Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" — i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia, więc sprawdzenie należy do produktu, nie do mechanizmu. Testy: presentation 368, logic 342, data 37, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
244 lines
11 KiB
Python
244 lines
11 KiB
Python
"""Porównanie systemów domów naraz (PRE-05).
|
|
|
|
JS-a tu nie ma — sprawdzamy strukturalnie: formularz pozwala zaznaczyć kilka
|
|
systemów, klient przekazuje je do logiki, a szablon pokazuje kuspy obok siebie.
|
|
"""
|
|
import pathlib
|
|
|
|
import pytest
|
|
|
|
from tests.skladanie import zloz_kod
|
|
|
|
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
|
CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8")
|
|
COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
|
OPTS = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8") # wspólne opcje
|
|
TABLES = (APP / "templates" / "_result_tables.html").read_text(encoding="utf-8") # wspólne tabele
|
|
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
|
MAIN = zloz_kod()
|
|
|
|
from app.house_systems import HOUSE_SYSTEMS, LIMITED, label as house_label # noqa: E402
|
|
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_both_forms_use_shared_option_partial():
|
|
"""Opcje w jednym pliku — oba formularze go dołączają (koniec duplikacji)."""
|
|
assert '{% include "_form_options.html" %}' in CHART
|
|
assert '{% include "_form_options.html" %}' in COMPILE
|
|
|
|
|
|
def _render(name: str, **ctx) -> str:
|
|
"""Renderuje szablon z tymi samymi globalsami, co produkcja.
|
|
|
|
Szablony budują listę systemów PĘTLĄ po katalogu, więc sprawdzanie ich
|
|
surowego tekstu nic już nie mówi — trzeba je wykonać. Przy okazji test
|
|
łapie literówki w Jinja, których grep po źródle nigdy nie wyłapał."""
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
env = Environment(loader=FileSystemLoader(str(APP / "templates")), autoescape=True)
|
|
env.globals.update(HOUSE_SYSTEMS=HOUSE_SYSTEMS, HOUSE_LIMITED=LIMITED,
|
|
house_label=house_label, static=lambda p: p,
|
|
# Domyślnie renderujemy jak dla konta z PEŁNYMI uprawnieniami —
|
|
# inaczej testy opcji sprawdzałyby pusty formularz. Ograniczenia
|
|
# mają własne testy w test_kontrola_dostepu.py.
|
|
can=lambda request, key: ctx.pop("_denied", None) != key,
|
|
nav_screens=lambda request: [])
|
|
return env.get_template(name).render(**ctx)
|
|
|
|
|
|
def test_form_offers_every_system_the_engine_advertises():
|
|
"""Każdy system z katalogu ma checkbox. Katalog jest jednym źródłem nazw,
|
|
więc dopisanie systemu bez odświeżenia UI zapali się tutaj."""
|
|
html = _render("_form_options.html", form={})
|
|
for value, name in HOUSE_SYSTEMS:
|
|
assert f'name="house_systems" value="{value}"' in html, f"brak checkboxa {value}"
|
|
assert name in html, f"brak nazwy {name}"
|
|
|
|
|
|
def test_form_marks_systems_with_a_domain_limit():
|
|
"""Placidus i Koch nie istnieją za kołem podbiegunowym — UI ma uprzedzać
|
|
ZANIM ktoś policzy horoskop dla Tromsø, nie dopiero w ostrzeżeniu wyniku."""
|
|
html = _render("_form_options.html", form={})
|
|
assert LIMITED, "katalog nie oznacza żadnego systemu jako ograniczonego"
|
|
for value, name in HOUSE_SYSTEMS:
|
|
starred = f"{name} *" in html
|
|
assert starred == (value in LIMITED), f"{value}: gwiazdka={starred}"
|
|
|
|
|
|
def test_handler_accepts_and_passes_house_systems(monkeypatch):
|
|
"""Zaznaczone systemy DOCHODZĄ do warstwy logicznej.
|
|
|
|
Poprzednia wersja szukała napisu `house_systems=house_systems` w main.py
|
|
i pękała, gdy argument zaczął przechodzić przez słownik opcji — mimo że
|
|
zachowanie zostało poprawne. Sprawdzamy więc, co naprawdę wyszło na dół."""
|
|
seen = {}
|
|
|
|
def spy(**kw):
|
|
seen.update(kw)
|
|
return _minimal_chart()
|
|
|
|
c = _client_with_stub_logic(monkeypatch, _minimal_chart())
|
|
from app.main import logic
|
|
|
|
monkeypatch.setattr(logic, "positions", spy)
|
|
r = c.post("/", data={**FORM, "house_systems": ["koch", "campanus"]})
|
|
assert r.status_code == 200, r.text[:300]
|
|
assert seen["house_systems"] == ["koch", "campanus"]
|
|
assert seen["house_system"] == "placidus"
|
|
|
|
|
|
def test_client_forwards_house_systems_to_logic():
|
|
assert '"house_systems": house_systems' in CLIENT
|
|
|
|
|
|
def test_comparison_table_renders_only_for_multiple_systems():
|
|
"""Bez dodatkowych systemów nie pokazujemy pustej tabeli porównania."""
|
|
assert "result.house_systems | length > 1" in TABLES
|
|
assert "Porównanie systemów domów" in TABLES
|
|
|
|
|
|
def test_comparison_table_shows_cusp_per_system():
|
|
"""Tabela iteruje po systemach i po 12 domach — kusp w każdej komórce."""
|
|
assert "for hs in result.house_systems" in TABLES
|
|
assert "hs.cusps[i].in_sign" in TABLES
|
|
|
|
|
|
def test_checkbox_state_survives_submit():
|
|
"""Zaznaczone systemy zostają zaznaczone po przeliczeniu."""
|
|
html = _render("_form_options.html", form={"house_systems": ["koch", "campanus"]})
|
|
for value, _ in HOUSE_SYSTEMS:
|
|
marker = f'value="{value}" checked'
|
|
assert (marker in html) == (value in ("koch", "campanus")), value
|
|
|
|
|
|
def test_fallback_notice_is_shown_prominently():
|
|
"""Po fallbacku kosmogram wygląda bezbłędnie — ostrzeżenie jest jedynym
|
|
sygnałem, że podział domów jest z innego systemu."""
|
|
html = _render("_result_tables.html",
|
|
result={"house_warnings": ["UWAGA: system domów nie ma definicji"]})
|
|
# Klasa nazywa się neutralnie: `house-warning` siedziała też na ekranie
|
|
# logowania i na ekranie plików, czyli nazwa z astrologii lądowała tam,
|
|
# gdzie astrologii ma nie być widać.
|
|
assert "notice-warn" in html
|
|
assert "UWAGA: system domów nie ma definicji" in html
|
|
|
|
|
|
def test_no_notice_when_nothing_was_substituted():
|
|
html = _render("_result_tables.html", result={"house_warnings": []})
|
|
assert "house-warning" not in html
|
|
|
|
|
|
# ── ścieżka PRZEZ handlery, nie obok nich ───────────────────────────────
|
|
# Dwa błędy przeszły przez komplet testów szablonowych, bo żaden nie wywołał
|
|
# POST-a: odwołanie do nieistniejącej zmiennej w handlerze strony głównej oraz
|
|
# użycie parametru formularza, którego w sygnaturze nie było. Oba dają 500 na
|
|
# żywo i oba łapie dopiero prawdziwe żądanie.
|
|
|
|
def _client_with_stub_logic(monkeypatch, chart):
|
|
import os
|
|
|
|
os.environ.pop("APP_PASSWORD", None)
|
|
from starlette.testclient import TestClient
|
|
|
|
from app.main import app, logic
|
|
|
|
monkeypatch.setattr(logic, "positions", lambda **kw: chart)
|
|
return TestClient(app)
|
|
|
|
|
|
def _minimal_chart():
|
|
cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0°00'00''",
|
|
"decimal": float(i * 30), "sign_glyph": "♈"} for i in range(12)]
|
|
ang = {k: {"name": k, "sign": "Aries", "in_sign": "0°00'00''", "decimal": 0.0,
|
|
"sign_glyph": "♈"} for k in ("Asc", "MC", "Dsc", "IC")}
|
|
return {"engine": "test", "positions": [], "cusps": cusps, "angles": ang,
|
|
"sign_glyphs": [{"sign": "Aries", "glyph": "♈"}],
|
|
"house_system": "porphyry", "house_system_requested": "placidus",
|
|
"house_warnings": ["UWAGA: system domów „placidus” nie ma definicji"]}
|
|
|
|
|
|
FORM = {"date": "1984-04-30", "time": "11:20", "tz_offset": "2",
|
|
"lat": "50.06", "lon": "19.94", "house_system": "placidus"}
|
|
|
|
|
|
def test_chart_page_survives_a_real_post(monkeypatch):
|
|
c = _client_with_stub_logic(monkeypatch, _minimal_chart())
|
|
r = c.post("/", data=FORM)
|
|
assert r.status_code == 200, r.text[:400]
|
|
assert "nie ma definicji" in r.text, "ostrzeżenie o fallbacku nie dotarło na stronę"
|
|
|
|
|
|
def test_compile_page_survives_a_real_post(monkeypatch):
|
|
c = _client_with_stub_logic(monkeypatch, _minimal_chart())
|
|
r = c.post("/compile", data=FORM)
|
|
assert r.status_code == 200, r.text[:400]
|
|
|
|
|
|
@pytest.mark.parametrize("orientation", ("asc", "aries"))
|
|
def test_wheel_orientation_reaches_the_renderer(monkeypatch, orientation):
|
|
"""Wybór obrotu koła musi dojść z formularza do rysunku — i wrócić
|
|
zaznaczony, żeby nie gubił się przy przeliczeniu."""
|
|
c = _client_with_stub_logic(monkeypatch, _minimal_chart())
|
|
r = c.post("/", data={**FORM, "wheel_orientation": orientation})
|
|
assert r.status_code == 200, r.text[:400]
|
|
assert f'value="{orientation}" checked' in r.text
|
|
|
|
|
|
# ── układ strony przy komplecie systemów ────────────────────────────────
|
|
# Regresja z pomiaru na żywej stronie: przy 13 zaznaczonych systemach tabela
|
|
# porównania miała 14 kolumn i 1863 px szerokości, a że stała w rodzicu bez
|
|
# overflow-x, ROZPYCHAŁA CAŁY DOKUMENT — 1713 px przy oknie 1280 px, czyli
|
|
# poziomy pasek przewijania na body i rozjechany układ.
|
|
|
|
# Arkusz jest teraz rozbity per uprawnienie (arkusz wspólny + arkusze ekranów),
|
|
# bo jeden plik ze wszystkimi selektorami był spisem funkcji programu dla
|
|
# każdego, kto go pobrał. Reguły szukamy w całości — który arkusz ją niesie,
|
|
# pilnuje osobny test pokrycia.
|
|
CSS = "\n".join(f.read_text(encoding="utf-8")
|
|
for f in sorted([*(APP / "static").glob("*.css"),
|
|
*(APP / "moduly" / "dodatki" / "static").glob("*.css")]))
|
|
|
|
|
|
def test_comparison_table_scrolls_instead_of_widening_the_page():
|
|
"""Tabela ma własny kontener przewijany — przewija się ona, nie strona."""
|
|
html = _render("_result_tables.html", result={
|
|
"house_systems": [{"system": v, "cusps": [
|
|
{"sign_glyph": "♈", "in_sign": "0°00'00''"} for _ in range(12)]}
|
|
for v, _ in HOUSE_SYSTEMS],
|
|
"house_warnings": []})
|
|
assert '<div class="table-scroll">' in html
|
|
before = html.index('class="table-scroll"')
|
|
assert html.index("<table", before) < html.index("</div>", before), \
|
|
"tabela musi być WEWNĄTRZ kontenera przewijanego"
|
|
|
|
|
|
def test_scroll_container_actually_clips_and_pins_the_house_number():
|
|
"""Sam znacznik nie wystarczy — reguła musi istnieć w arkuszu.
|
|
|
|
Geometrii nie da się sprawdzić bez przeglądarki, więc sprawdzamy kontrakt:
|
|
kontener przewija się w poziomie, a pierwsza kolumna zostaje przyklejona
|
|
(bez tego po przewinięciu w prawo nie wiadomo, który to dom)."""
|
|
assert ".table-scroll { overflow-x: auto;" in CSS
|
|
assert ".table-scroll th:first-child, .table-scroll td:first-child {" in CSS
|
|
assert "position: sticky" in CSS
|
|
|
|
|
|
def test_system_checkboxes_use_a_grid_not_a_ragged_flex_wrap():
|
|
"""13 pozycji na flexie zawijało się w poszarpane wiersze o różnej długości."""
|
|
html = _render("_form_options.html", form={})
|
|
assert 'class="opts systems"' in html
|
|
assert ".opts.systems { display: grid;" in CSS
|
|
|
|
|
|
def test_polar_circle_note_is_explained_once_next_to_the_asterisks():
|
|
"""Przypis siedział WEWNĄTRZ <label> selektora i łamał się na dwie linie,
|
|
przez co wiersz siatki rósł z 66 do 108 px i rozjeżdżał się z sąsiednim
|
|
polem. Wyjaśnienie stoi teraz raz, przy checkboxach z gwiazdkami."""
|
|
opts = _render("_form_options.html", form={})
|
|
assert "kołem podbiegunowym" in opts
|
|
for name in ("chart.html", "compile.html"):
|
|
tpl = (APP / "templates" / name).read_text(encoding="utf-8")
|
|
select = tpl[tpl.index("<select name=\"house_system\""):]
|
|
assert "podbiegunow" not in select[:select.index("</label>")], \
|
|
f"{name}: przypis wrócił do etykiety selektora"
|