Files
astrololo/services/presentation/tests/test_wheelzoom.py
T
gitea d094a145b1
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4m44s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m43s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
astrololo: generowanie tekstu jako moduł odłączalny (2/5)
Drugi z pięciu kroków budowy trzech produktów. Czysty refaktor — zachowanie
aplikacji się nie zmienia, liczba testów rośnie tylko o nowe.

DLACZEGO WARUNEK W SZABLONIE NIE WYSTARCZA. Dotąd generowanie było chowane przez
`{% if can(request, 'ai') %}`. To sprawia, że funkcji nie WIDAĆ, ale nie że jej
NIE MA: plik szablonu dalej leży w obrazie i dalej zawiera jej nazwy, więc `grep`
po kontenerze pokazuje wszystko, czego warunek nie pokazał na ekranie. Astroklient
ma nie mieć śladu, nie mieć wyłączoną funkcję — więc granica musi być KATALOGIEM.

CO SIĘ PRZENIOSŁO do app/dodatki/: dwa szablony, pięć plików statycznych, trasa
strumienia, cztery metody klienta warstwy logicznej, wpis w katalogu funkcji,
wpisy tras i zasobów, akcje formularza i reguły CSS nazywające funkcję
(`textarea.prompt`).

JAK APLIKACJA O TO PYTA. Neutralny most `app/rozszerzenia.py` odpowiada wyłącznie
na pytanie „czy coś jest podpięte i co wnosi". Sam nie wymienia ani jednej nazwy —
pilnuje tego osobny test, bo most jest w KAŻDYM obrazie. Dlatego też katalog
nazywa się `dodatki`, a nie `ai`: nazwa w instrukcji importu byłaby dokładnie tym
śladem, którego wydzielanie ma się pozbyć.

TRZY MIEJSCA, KTÓRE OKAZAŁY SIĘ TRUDNE:

1. Pola formularza. FastAPI czyta je z SYGNATURY, a wspólny handler nie może
   wymieniać `prompt_budget` ani `llm_provider`. Rozwiązane zależnością: moduł
   deklaruje własne pola u siebie, handler wie tylko, że dostaje słownik.
2. Klient warstwy logicznej jest wspólny, więc metody `prompt`/`horoscope`/
   `llm_models` musiały z niego wyjść. Wspólny klient daje teraz samą drogę
   w dół (`wywolaj`, `pobierz`, `strumien`) — z szyfrowaniem łącza i tokenem
   międzywarstwowym; co nią pojedzie, jest sprawą modułu.
3. Podtytuł ekranu Skompiluj miał wariant „z AI" i wariant bez. Zamiast warunku
   jedno zdanie prawdziwe niezależnie od tego, jakie moduły są w obrazie.

DOCKERFILE. `COPY . .` wnosiło do obrazu także testy — a plik testowy nazywa
funkcje wprost. Teraz wchodzi wyłącznie `app/`.

TEST GRANICY. `test_ai_tylko_w_module.py` przechodzi po wszystkim poza modułem
i szuka siedemnastu słów. Znalazł dwanaście resztek, których nie widziałem:
komentarze w main.py i security.py, przykład `models.js` w komentarzu features.py,
komentarze w compile.js i w trzech arkuszach, oraz regułę `textarea.prompt`,
która przy rozbijaniu CSS trafiła do arkuszy ekranów zamiast do modułu.

Ma też kontrolę pozytywną (te słowa MAJĄ padać w module — inaczej test
przechodziłby, gdyby funkcję wydrążono) i test odłączalności mostu.

SPRAWDZONE NA KOPII BEZ MODUŁU: aplikacja startuje, wszystkie ekrany oddają 200,
katalog ma 13 funkcji zamiast 14, trasy strumienia nie ma, spreparowane
`action=prompt` wraca do akcji domyślnej bez śladu, a `grep` po drzewie nie
znajduje ani jednego ze słów.

Testy: presentation 362, logic 342, data 37, render 41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:23:08 +02:00

84 lines
3.5 KiB
Python

"""Powiększanie kosmogramu na pełne okno (PRE-25).
Test strukturalny, jak przy pozostałych rzeczach żyjących w JS/CSS. Osobno
pilnujemy warstwy nakładki: to jedyna liczba w tym pliku, którą łatwo zmienić
bez zastanowienia i zepsuć coś, czego się nie widzi na pierwszy rzut oka.
"""
import pathlib
import re
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
JS = (APP / "static" / "wheelzoom.js").read_text(encoding="utf-8")
# 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 / "dodatki" / "static").glob("*.css")]))
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
def test_script_is_loaded_only_where_there_is_a_wheel():
"""Koło jest na Horoskopie i na Skompiluj — i tylko tam ten skrypt ma prawo
się pojawić.
Wcześniej ładowany był z base.html, czyli na KAŻDEJ stronie. Konto, które ma
wyłącznie Pliki, pobierało go przy zwykłym wejściu na swój jedyny ekran,
a jego treść wymienia z nazwy zakładki, o których takie konto nie ma prawa
wiedzieć — łącznie z tą, której jeszcze nie ma."""
from pathlib import Path
szablony = Path(__file__).resolve().parents[1] / "app" / "templates"
z_kolem, bez_kola = set(), set()
for f in szablony.glob("*.html"):
(z_kolem if "wheelzoom.js" in f.read_text(encoding="utf-8") else bez_kola).add(f.name)
assert z_kolem == {"chart.html", "compile.html"}, f"ładowany też przez: {z_kolem}"
assert "files.html" in bez_kola and "logowanie.html" in bez_kola
def test_toggles_on_click():
assert "addEventListener('click'" in JS
assert "classList.toggle" in JS
def test_closes_with_escape():
assert "'Escape'" in JS
def test_reachable_from_keyboard():
"""Bez tego jedyny sposób na obejrzenie szczegółów wykresu wymagałby myszy."""
assert "tabindex" in JS
assert "'Enter'" in JS
def test_page_scroll_is_locked_while_zoomed():
"""Inaczej przewijanie „przecieka" na treść pod spodem, której i tak nie widać."""
assert "wheel-zoom-open" in JS
assert re.search(r"body\.wheel-zoom-open\s*\{[^}]*overflow:\s*hidden", CSS)
def test_zoomed_layer_sits_above_map_but_below_progress():
"""Warstwy: kontrolki Leafleta 1000 < kosmogram < okno postępu 1200.
Gdy trwa pisanie horoskopu, log operacji ma zostać na wierzchu."""
zoom = re.search(r"\.wheel-fig\.zoomed\s*\{[^}]*z-index:\s*(\d+)", CSS)
progress = re.search(r"\.progress-overlay\s*\{[^}]*z-index:\s*(\d+)", CSS)
assert zoom and progress, "nie znaleziono warstw w CSS"
assert 1000 < int(zoom.group(1)) < int(progress.group(1))
def test_zoomed_wheel_is_bounded_by_shorter_window_side():
"""Koło jest kwadratowe — ograniczamy je krótszym bokiem okna, inaczej na
szerokim ekranie wystawałoby w pionie."""
block = re.search(r"\.wheel-fig\.zoomed svg\.wheel\s*\{[^}]*\}", CSS)
assert block and "vh" in block.group(0) and "vw" in block.group(0)
def test_cursor_signals_the_affordance():
assert "zoom-in" in CSS and "zoom-out" in CSS
def test_does_nothing_without_a_wheel():
"""Na zakładkach bez kosmogramu skrypt ma po prostu wyjść."""
assert "if (!figures.length) return;" in JS