Files
astrololo/services/presentation/tests/test_wheeltip.py
T
gitea c1f5bea9f7
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
astrololo: ekrany i eksport jako moduły, katalog z rejestracji (3/5)
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>
2026-08-26 13:50:34 +02:00

92 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Dymki na kosmogramie (PRE-12/PRE-25).
Rysunek niesie <title> przy każdym obiekcie i każdej linii aspektu, ale natywne
dymki przeglądarki przestały się pokazywać po dodaniu powiększania na pełne okno.
Sprawdzone w przeglądarce: elementy z <title> SĄ osiągalne kursorem (19 z 20 trafień
w hit-teście), więc nic ich nie zasłaniało — problem był w samym natywnym dymku.
Test strukturalny, jak przy wheelzoom: bez uruchamiania przeglądarki nie zbadamy
zachowania JS-a. Pilnujemy więc tego, co da się zepsuć po cichu i co ma znaczenie:
kolejności warstw, zdejmowania i ODWIESZANIA <title> oraz wpięcia skryptu.
Zachowanie sprawdzone ręcznie w przeglądarce przy 1280×900, w obu stanach koła.
"""
import pathlib
import re
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
JS = (APP / "static" / "wheeltip.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 / "moduly" / "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 "wheeltip.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_tooltip_sits_above_the_zoom_overlay_but_below_the_progress_window():
"""Warstwy: nakładka powiększonego koła < dymek < okno postępu.
Dymek MUSI być nad nakładką, inaczej po powiększeniu — czyli dokładnie tam,
gdzie zgłoszono problem — byłby niewidoczny. I POD oknem postępu, bo gdy trwa
pisanie horoskopu, log operacji ma zostać na wierzchu."""
tip = re.search(r"\.wheel-tip\s*\{[^}]*z-index:\s*(\d+)", CSS)
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 tip and zoom and progress, "brakuje którejś reguły z-index"
assert int(zoom.group(1)) < int(tip.group(1)) < int(progress.group(1)), \
f"warstwy: nakładka {zoom.group(1)}, dymek {tip.group(1)}, postęp {progress.group(1)}"
def test_tooltip_never_swallows_the_pointer():
"""Dymek chodzi za kursorem, więc bez pointer-events:none zasłaniałby sam
sobie element, nad którym stoi — i migotał."""
rule = re.search(r"\.wheel-tip\s*\{([^}]*)\}", CSS)
assert rule and "pointer-events: none" in rule.group(1)
def test_title_is_detached_only_for_the_hover_and_always_put_back():
"""<title> zostaje w rysunku — to nazwa dostępna dla czytników ekranu.
Na czas najechania odpinamy go, żeby nie było dwóch dymków naraz, ale MUSI
wrócić. Brak odwieszania okradałby czytniki ekranu po pierwszym najechaniu,
i to po cichu — na ekranie nic by nie było widać."""
assert "removeChild" in JS, "dymek natywny nie jest tłumiony"
assert "appendChild(stash.node)" in JS, "brak odwieszania <title>"
for leaving in ("mouseleave", "scroll", "resize"):
assert leaving in JS, f"brak sprzątania przy: {leaving}"
def test_tooltip_reads_the_title_that_is_already_in_the_drawing():
"""Źródłem tekstu jest <title> z SVG, nie druga kopia opisów w JS —
inaczej rozjechałyby się przy pierwszej zmianie treści dymka."""
assert ":scope > title" in JS
assert "textContent" in JS
def test_pdf_path_is_untouched():
"""PDF składa się po stronie serwera, więc żaden dymek go nie dotyczy —
<title> zostają w rysunku i nic w chartwheel.py się nie zmienia."""
wheel = (APP / "chartwheel.py").read_text(encoding="utf-8")
assert "<title>" in wheel
assert "wheeltip" not in wheel