"""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" from tests.skladanie import zloz, zloz_kod # Szablon z rozwiniętymi wstawkami modułów — patrz tests/skladanie.py. TPL = zloz("compile.html") BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8") JS = (APP / "static" / "compile.js").read_text(encoding="utf-8") # Skrypty generowania mieszkają w module `app/ai/` — katalogu, którego # w obrazie bez tej funkcji po prostu nie ma. NATAL_JS = (APP / "moduly" / "dodatki" / "static" / "natal.js").read_text(encoding="utf-8") INTERPRET = zloz("interpret.html") # Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py. MAIN = zloz_kod() 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(): """Zakładka jest w KATALOGU FUNKCJI — stamtąd buduje się nawigacja. Menu nie jest już listą wpisaną w base.html: powstaje z uprawnień konta (PRE-26), więc szukanie w szablonie sztywnego linku nic by nie mówiło. Że nawigacja faktycznie renderuje katalog, sprawdza test_kontrola_dostepu.""" from app import features tab = features.BY_KEY["compile"] assert tab in features.SCREENS assert tab.href and tab.label 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(monkeypatch): """compile_pdf składa KOMPLET rysunków w motywie DRUKU. Sprawdzamy przez WYWOŁANIE trasy, nie przez szukanie tekstu w main.py: poprzednia wersja greppowała `render(chart, theme="print")` i pękała przy każdym dopisaniu argumentu, mimo że zachowanie zostawało poprawne.""" seen: dict[str, str] = {} from app import chartwheel from app.clients.render_client import RenderClient from app.main import logic for name in ("render", "render_aspectarian", "render_declination", "render_antiscia"): original = getattr(chartwheel, name) def spy(chart, theme="screen", _n=name, _o=original, **kw): seen[_n] = theme return _o(chart, theme=theme, **kw) monkeypatch.setattr(chartwheel, name, spy) monkeypatch.setattr(logic, "positions", lambda **kw: _pdf_sample_chart()) monkeypatch.setattr(RenderClient, "pdf", lambda self, report: b"%PDF-1.4 stub") r = _client().post("/compile/pdf", json={"person": "Jan", "data": { "date": "1984-04-30", "time": "11:20", "tz_offset": 2, "lat": 50.06, "lon": 19.94}}) assert r.status_code == 200, r.text[:300] assert set(seen) == {"render", "render_aspectarian", "render_declination", "render_antiscia"}, seen assert set(seen.values()) == {"print"}, seen def _pdf_sample_chart() -> dict: cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0", "decimal": float(i * 30), "sign_glyph": "♈"} for i in range(12)] ang = {k: {"name": k, "sign": "Aries", "in_sign": "0", "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": "equal"} # ──────────────────────── 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.kosmogramNatal" in JS assert "window.kosmogramPredictions" 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. Statyka jedzie przez helper cache-bustingu (PRE-26): src="{{ static('X') }}".""" def tag(name): return TPL.index("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(/