| """ |
| tests/test_ui_renderers.py — Unit tests for app.py UI renderer functions. |
| |
| Covers: render_quip, render_deadlines_html, render_footer, |
| render_panic_meter, render_mascot, select_result_state (Plan 03). |
| |
| Run: BUREAUCAT_NO_MODEL=1 .venv/bin/python -m pytest tests/test_ui_renderers.py -x -q |
| """ |
|
|
| import os |
| import sys |
|
|
| |
| os.environ["BUREAUCAT_NO_MODEL"] = "1" |
|
|
| |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| import app |
|
|
|
|
| |
| |
| |
|
|
| class TestRenderQuip: |
| def test_empty_string_returns_empty(self): |
| assert app.render_quip("") == "" |
|
|
| def test_whitespace_only_returns_empty(self): |
| assert app.render_quip(" ") == "" |
|
|
| def test_none_like_empty(self): |
| |
| assert app.render_quip("") == "" |
|
|
| def test_quip_contains_prefix_exactly_once(self): |
| result = app.render_quip("witty remark") |
| assert result.count("Bureaucat says:") == 1 |
|
|
| def test_quip_contains_prefix_exactly_once_with_existing_prefix(self): |
| |
| result = app.render_quip("Skatteverket wants your money") |
| assert result.count("Bureaucat says:") == 1 |
|
|
| def test_quip_contains_quip_text(self): |
| result = app.render_quip("witty") |
| assert "witty" in result |
|
|
| def test_quip_no_double_prefix(self): |
| |
| result = app.render_quip("the cat is watching") |
| count = result.count("Bureaucat says:") |
| assert count == 1, f"Expected exactly 1 prefix occurrence, got {count}: {result!r}" |
|
|
| def test_quip_is_markdown_not_raw_html(self): |
| |
| result = app.render_quip("test quip") |
| assert "Bureaucat says:" in result |
| assert "test quip" in result |
|
|
|
|
| |
| |
| |
|
|
| class TestRenderDeadlinesHtml: |
| def test_value_line_contains_mark_tag(self): |
| result = app.render_deadlines_html("- 15 juni 2026 — last day") |
| assert "<mark" in result |
|
|
| def test_value_line_contains_verbatim_value(self): |
| result = app.render_deadlines_html("- 15 juni 2026 — last day") |
| assert "15 juni 2026" in result |
|
|
| def test_value_line_contains_amber_background(self): |
| result = app.render_deadlines_html("- 15 juni 2026 — last day") |
| assert "#FFF3CD" in result |
|
|
| def test_none_found_has_no_mark(self): |
| result = app.render_deadlines_html("None found.") |
| assert "<mark" not in result |
|
|
| def test_none_found_case_insensitive(self): |
| result = app.render_deadlines_html("NONE FOUND.") |
| assert "<mark" not in result |
|
|
| def test_empty_string_has_no_mark(self): |
| result = app.render_deadlines_html("") |
| assert "<mark" not in result |
|
|
| def test_multiple_items_all_wrapped(self): |
| deadlines = "- 15 juni 2026 — last day\n- 1 234 kr — belopp" |
| result = app.render_deadlines_html(deadlines) |
| assert result.count("<mark") == 2 |
|
|
| def test_mark_pill_color_values(self): |
| result = app.render_deadlines_html("- 2026-06-15 — deadline") |
| |
| assert "#FFF3CD" in result |
| assert "#8B6914" in result |
|
|
| def test_html_escaping_in_value(self): |
| |
| result = app.render_deadlines_html("- <script>alert(1)</script> — test") |
| assert "<script>" not in result |
|
|
| def test_uses_li_elements(self): |
| result = app.render_deadlines_html("- 2026-06-15 — deadline") |
| assert "<li" in result |
|
|
|
|
| |
| |
| |
|
|
| class TestRenderFooter: |
| def test_contains_privacy_note(self): |
| result = app.render_footer() |
| assert "Nothing is stored after your session ends." in result |
|
|
| def test_contains_legal_disclaimer(self): |
| result = app.render_footer() |
| assert "does not give legal advice" in result |
|
|
| def test_contains_trust_01_full_copy(self): |
| result = app.render_footer() |
| |
| assert "Everything runs on a small model inside this Space." in result |
| assert "Nothing is sent to external APIs." in result |
| assert "Nothing is stored after your session ends." in result |
|
|
| def test_contains_trust_05_full_copy(self): |
| result = app.render_footer() |
| |
| assert "Bureaucat explains letters" in result |
| assert "does not give legal advice" in result |
| assert "consult a qualified professional" in result |
|
|
| def test_returns_string(self): |
| result = app.render_footer() |
| assert isinstance(result, str) |
| assert len(result) > 0 |
|
|
|
|
| |
| |
| |
|
|
| class TestRenderPanicMeter: |
| def test_severity_5_contains_label(self): |
| result = app.render_panic_meter(5) |
| assert "5 — Act immediately" in result |
|
|
| def test_severity_5_contains_color(self): |
| result = app.render_panic_meter(5) |
| assert "#C0392B" in result |
|
|
| def test_severity_5_contains_denominator(self): |
| result = app.render_panic_meter(5) |
| |
| assert "/5" in result |
| assert "severity-5" in result |
| assert "width" not in result |
|
|
| def test_severity_1_contains_label(self): |
| result = app.render_panic_meter(1) |
| assert "1 — Informational" in result |
|
|
| def test_severity_1_is_badge_not_bar(self): |
| result = app.render_panic_meter(1) |
| assert "panic-badge" in result |
| assert "severity-1" in result |
|
|
| def test_severity_1_contains_color(self): |
| result = app.render_panic_meter(1) |
| assert "#27AE60" in result |
|
|
| def test_severity_none_does_not_raise(self): |
| result = app.render_panic_meter(None) |
| assert isinstance(result, str) |
|
|
| def test_severity_none_contains_unavailable_label(self): |
| result = app.render_panic_meter(None) |
| assert "Result unavailable — try re-uploading" in result |
|
|
| def test_severity_none_contains_gray(self): |
| result = app.render_panic_meter(None) |
| assert "#9E9E9E" in result |
|
|
| def test_severity_2_label_and_color(self): |
| result = app.render_panic_meter(2) |
| assert "2 — Low concern" in result |
| assert "#8BC34A" in result |
| assert "/5" in result |
|
|
| def test_severity_3_label_and_color(self): |
| result = app.render_panic_meter(3) |
| assert "3 — Action needed" in result |
| assert "#F39C12" in result |
| assert "/5" in result |
|
|
| def test_severity_4_label_and_color(self): |
| result = app.render_panic_meter(4) |
| assert "4 — Urgent" in result |
| assert "#E67E22" in result |
| assert "/5" in result |
|
|
| def test_every_level_has_integer_and_word(self): |
| """Accessibility: color alone must not carry meaning (UI-SPEC line 92).""" |
| for sev in (1, 2, 3, 4, 5): |
| result = app.render_panic_meter(sev) |
| assert str(sev) in result, f"Integer {sev} missing from output" |
| |
| assert " — " in result, f"Word label separator missing at severity {sev}" |
|
|
|
|
| |
| |
| |
|
|
| def _make_result(deadlines: str): |
| """Helper: construct a minimal StructuredResult with given deadlines text.""" |
| return app.StructuredResult( |
| transcription="", |
| quip="", |
| tldr="", |
| why="", |
| actions="", |
| deadlines=deadlines, |
| severity=None, |
| raw="", |
| ) |
|
|
|
|
| class TestSelectResultState: |
| def test_money_amount_returns_money(self): |
| result = _make_result("- 4 250 kr — kvarskatt") |
| assert app.select_result_state(result) == "money" |
|
|
| def test_date_only_returns_deadline(self): |
| result = _make_result("- 15 juni 2026 — sista dag") |
| assert app.select_result_state(result) == "deadline" |
|
|
| def test_none_found_returns_allclear(self): |
| result = _make_result("None found.") |
| assert app.select_result_state(result) == "allclear" |
|
|
| def test_none_found_case_insensitive(self): |
| result = _make_result("NONE FOUND.") |
| assert app.select_result_state(result) == "allclear" |
|
|
| def test_money_wins_over_deadline(self): |
| """Money takes priority even when a date is also present.""" |
| result = _make_result("- 15 juni 2026 — datum\n- 4 250 kr — belopp") |
| assert app.select_result_state(result) == "money" |
|
|
| def test_sek_amount_returns_money(self): |
| result = _make_result("- 1000 SEK — belopp") |
| assert app.select_result_state(result) == "money" |
|
|
| def test_iso_date_returns_deadline(self): |
| result = _make_result("- 2026-06-15 — deadline date") |
| assert app.select_result_state(result) == "deadline" |
|
|
| def test_empty_deadlines_returns_allclear(self): |
| result = _make_result("") |
| assert app.select_result_state(result) == "allclear" |
|
|
| def test_no_money_no_date_returns_allclear(self): |
| result = _make_result("- Some reference: 123456789") |
| assert app.select_result_state(result) == "allclear" |
|
|
|
|
| |
| |
| |
|
|
| class TestRenderMascot: |
| def test_verifying_contains_correct_png(self): |
| result = app.render_mascot("verifying") |
| assert "assets/mascot/verifying.png" in result |
|
|
| def test_verifying_contains_class(self): |
| result = app.render_mascot("verifying") |
| assert "mascot-verifying" in result |
|
|
| def test_verifying_contains_mascot_img_class(self): |
| result = app.render_mascot("verifying") |
| assert "mascot-img" in result |
|
|
| def test_unknown_state_falls_back_to_idle(self): |
| result = app.render_mascot("bogus") |
| assert "assets/mascot/idle.png" in result |
|
|
| def test_idle_state_renders(self): |
| result = app.render_mascot("idle") |
| assert "assets/mascot/idle.png" in result |
| assert "mascot-idle" in result |
|
|
| def test_money_state_renders(self): |
| result = app.render_mascot("money") |
| assert "assets/mascot/money.png" in result |
| assert "mascot-money" in result |
|
|
| def test_allclear_state_renders(self): |
| result = app.render_mascot("allclear") |
| assert "assets/mascot/allclear.png" in result |
| assert "mascot-allclear" in result |
|
|
| def test_deadline_state_renders(self): |
| result = app.render_mascot("deadline") |
| assert "assets/mascot/deadline.png" in result |
| assert "mascot-deadline" in result |
|
|
| def test_reading_state_renders(self): |
| result = app.render_mascot("reading") |
| assert "assets/mascot/reading.png" in result |
| assert "mascot-reading" in result |
|
|
| def test_confused_and_wrong_document_in_assets(self): |
| """Phase 3 wires confused and wrong_document error states into MASCOT_ASSETS.""" |
| assert "confused" in app.MASCOT_ASSETS |
| assert "wrong_document" in app.MASCOT_ASSETS |
| assert "confused.png" in app.MASCOT_ASSETS["confused"] |
| assert "wrong_document.png" in app.MASCOT_ASSETS["wrong_document"] |
|
|
|
|
| |
| |
| |
|
|
| class TestAnimationCssHtml: |
| def test_contains_keyframe_bob(self): |
| assert "@keyframes bob" in app.ANIMATION_CSS_HTML |
|
|
| def test_contains_keyframe_sway(self): |
| assert "@keyframes sway" in app.ANIMATION_CSS_HTML |
|
|
| def test_contains_keyframe_blink(self): |
| assert "@keyframes blink" in app.ANIMATION_CSS_HTML |
|
|
| def test_contains_keyframe_pop(self): |
| assert "@keyframes pop" in app.ANIMATION_CSS_HTML |
|
|
| def test_contains_keyframe_shake(self): |
| assert "@keyframes shake" in app.ANIMATION_CSS_HTML |
|
|
| def test_contains_mascot_img_transition(self): |
| assert "mascot-img" in app.ANIMATION_CSS_HTML |
| assert "opacity 0.2s ease" in app.ANIMATION_CSS_HTML |
|
|
| def test_no_javascript(self): |
| """CSS-only requirement (D2-10): no <script> tags in animation block.""" |
| assert "<script" not in app.ANIMATION_CSS_HTML.lower() |
|
|