f5dec15e4d
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m13s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m49s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 29s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 20s
build-render / build (push) Successful in 44s
build / build (push) Successful in 54s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m19s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m59s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
Zasada: co składamy, dodajemy i wyświetlamy, MA trafiać do finalnego raportu
(«Skompiluj») i do PDF-a — nie tylko na stronę /chart. Do tej pory do raportu i
druku szło samo koło; aspektarian (etap 5) i deklinacja/antyscja (etap 6) były
tylko w podglądzie horoskopu.
RAPORT «Skompiluj» (podgląd): handler /compile liczy teraz wszystkie cztery
rysunki, a szablon je pokazuje (koło, aspektarian, deklinacja, antyscja).
PDF (usługa render) — uogólnienie z jednego rysunku na LISTĘ:
- Kontrakt raportu: `figures = [{svg, caption}]` (uporządkowana). `wheel_svg`
zostaje dla zgodności wstecznej jako pojedynczy rysunek.
- `compile.py`: każdy SVG osobno przez rsvg-convert → PDF (fig0…figN); braki/
błędy pomijane (rysunek nie może wywalić raportu, tekst ważniejszy).
- `latex.py build()`: rysunki w sekcji 3 (po danych, przed natalną) w PODANEJ
kolejności, każdy z podpisem. Jedna reguła składania: keepaspectratio z limitem
szerokości i wysokości — kwadratowe (koło, aspektarian) ogranicza wysokość,
szerokie (deklinacja, antyscja) szerokość, bez zniekształceń.
- `compile_pdf` (prezentacja): renderuje komplet w motywie DRUKU i wysyła jako
`figures`.
Testy: render +5 (kolejność, podpisy, zgodność wsteczna, pomijanie pustych),
prezentacja +3 (raport pokazuje komplet, PDF składa komplet). Render 32,
prezentacja 173.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
162 lines
6.2 KiB
Python
162 lines
6.2 KiB
Python
"""Zakładka „Skompiluj" — zbiorczy raport (PRE-23).
|
|
|
|
Raport składa się z trzech kawałków liczonych w różnych miejscach: horoskopu
|
|
(serwer), interpretacji natalnej i predykcji okresowych (magazyn przeglądarki).
|
|
Pilnujemy, żeby zakładka umiała je zebrać, a przede wszystkim — żeby POWIEDZIAŁA,
|
|
czego brakuje. Niekompletny raport, o którym dowiadujesz się dopiero z PDF-a,
|
|
byłby najgorszym wynikiem.
|
|
"""
|
|
import pathlib
|
|
|
|
from starlette.testclient import TestClient
|
|
|
|
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
|
TPL = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
|
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
|
JS = (APP / "static" / "compile.js").read_text(encoding="utf-8")
|
|
NATAL_JS = (APP / "static" / "natal.js").read_text(encoding="utf-8")
|
|
INTERPRET = (APP / "templates" / "interpret.html").read_text(encoding="utf-8")
|
|
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
|
|
|
|
|
def _client():
|
|
import os
|
|
|
|
os.environ.pop("APP_PASSWORD", None) # bez bramki logowania w teście
|
|
from app.main import app
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
# ───────────────────────────────── nawigacja i trasa ─────────────────────
|
|
|
|
def test_tab_is_in_the_menu():
|
|
assert 'href="/compile"' in BASE and "Skompiluj" in BASE
|
|
|
|
|
|
def test_get_renders_empty_form():
|
|
r = _client().get("/compile")
|
|
assert r.status_code == 200
|
|
assert "Złóż podsumowanie" in r.text
|
|
|
|
|
|
def test_routes_exist():
|
|
assert '@app.get("/compile"' in MAIN and '@app.post("/compile"' in MAIN
|
|
|
|
|
|
# ─────────────────────────── kolejność w raporcie ────────────────────────
|
|
|
|
def test_report_order_matches_the_brief():
|
|
"""Prośba partnerów: najpierw imię i nazwisko, potem wprowadzone dane, potem
|
|
RYSUNEK kosmogramu, a dopiero po nim interpretacja natalna i predykcje."""
|
|
person = TPL.index('id="reportPerson"')
|
|
wheel = TPL.index("wheel_svg")
|
|
natal = TPL.index('id="reportNatal"')
|
|
preds = TPL.index('id="reportPredictions"')
|
|
assert person < wheel < natal < preds
|
|
|
|
|
|
def test_person_name_heads_the_report():
|
|
assert 'id="reportPerson"' in TPL
|
|
assert "form.person" in TPL
|
|
|
|
|
|
# ───── wszystkie rysunki w raporcie i w PDF (nie tylko koło) ─────
|
|
# Zasada: co pokazujemy na stronie, ma trafić do podsumowania i do PDF-a.
|
|
|
|
def test_report_shows_every_chart_figure():
|
|
"""Raport «Skompiluj» pokazuje wszystkie rysunki, nie tylko koło."""
|
|
for cls in ("wheel-fig", "aspectarian-fig", "declination-fig", "antiscia-fig"):
|
|
assert cls in TPL, f"brak {cls} w raporcie"
|
|
|
|
|
|
def test_compile_page_renders_all_figures():
|
|
"""Handler /compile liczy wszystkie rysunki do podglądu raportu."""
|
|
for var in ("aspectarian_svg", "declination_svg", "antiscia_svg"):
|
|
assert var in MAIN, f"/compile nie ustawia {var}"
|
|
|
|
|
|
def test_pdf_bundles_all_figures_in_print_theme():
|
|
"""compile_pdf składa KOMPLET rysunków (motyw druku) i wysyła jako `figures`."""
|
|
assert '"figures": figures' in MAIN
|
|
for call in (
|
|
'render(chart, theme="print")',
|
|
'render_aspectarian(chart, theme="print")',
|
|
'render_declination(chart, theme="print")',
|
|
'render_antiscia(chart, theme="print")',
|
|
):
|
|
assert call in MAIN, f"PDF nie składa: {call}"
|
|
|
|
|
|
# ──────────────────────── zbieranie materiału z magazynów ────────────────
|
|
|
|
def test_reads_stores_through_their_api_not_raw_storage():
|
|
"""Format danych ma jednego właściciela — moduł, który je zapisuje.
|
|
Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie."""
|
|
assert "window.astrololoNatal" in JS
|
|
assert "window.astrololoPredictions" in JS
|
|
# chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu)
|
|
assert "localStorage.getItem" not in JS
|
|
assert "localStorage.setItem" not in JS
|
|
|
|
|
|
def test_stores_are_loaded_on_the_tab():
|
|
for src in ("natal.js", "predictions.js", "compile.js"):
|
|
assert src in TPL, f"brak {src} na zakładce"
|
|
|
|
|
|
def test_stores_load_before_the_assembler():
|
|
"""Szukamy TAGÓW skryptów, nie samych nazw — te padają też w komentarzach."""
|
|
def tag(name):
|
|
return TPL.index('src="/static/' + name + '"')
|
|
|
|
assert tag("natal.js") < tag("compile.js")
|
|
assert tag("predictions.js") < tag("compile.js")
|
|
|
|
|
|
# ─────────────────────────── mówi, czego brakuje ─────────────────────────
|
|
|
|
def test_shows_what_is_missing():
|
|
assert 'id="readiness"' in TPL
|
|
assert "rdy-missing" in JS
|
|
for hint in ("Interpretacje", "Kalendarz"):
|
|
assert hint in JS, f"brak podpowiedzi, gdzie uzupełnić: {hint}"
|
|
|
|
|
|
def test_all_three_parts_are_checked():
|
|
for label in ("Policzony horoskop", "Interpretacja natalna", "Predykcje okresowe"):
|
|
assert label in JS
|
|
|
|
|
|
# ───────────────────────────── natalna zapamiętywana ─────────────────────
|
|
|
|
def test_natal_is_captured_on_interpret_tab():
|
|
assert "natal.js" in INTERPRET
|
|
assert 'id="natalNote"' in INTERPRET
|
|
|
|
|
|
def test_natal_store_keeps_a_single_entry():
|
|
"""Natalna jest JEDNA — dotyczy momentu urodzenia, nie okresu. Ponowne
|
|
wygenerowanie ma podmienić, nie dokładać."""
|
|
assert "setItem" in NATAL_JS and "push" not in NATAL_JS
|
|
|
|
|
|
def test_natal_only_stores_natal_profile():
|
|
assert "'natal'" in NATAL_JS
|
|
|
|
|
|
def test_full_storage_is_reported():
|
|
assert "pełny" in NATAL_JS
|
|
|
|
|
|
# ─────────────────────────────── bezpieczeństwo ──────────────────────────
|
|
|
|
def test_ai_text_is_escaped_before_injection():
|
|
"""Treść od modelu wstawiamy do DOM — bez ucieczki byłby to wektor wstrzyknięcia."""
|
|
assert "function esc" in JS
|
|
assert "replace(/</g, '<')" in JS
|
|
|
|
|
|
def test_does_nothing_on_other_tabs():
|
|
assert "if (!document.getElementById('readiness')) return;" in JS
|