"""Tests for musehub.api.routes.musehub.htmx_helpers. Covers HX-Request detection, HX-Boosted detection, fragment/full routing, HX-Trigger header emission, and HX-Redirect response generation. """ from __future__ import annotations import json from unittest.mock import AsyncMock, MagicMock, patch import pytest from starlette.datastructures import Headers from starlette.responses import Response from starlette.testclient import TestClient from fastapi import Request from musehub.api.routes.musehub.htmx_helpers import ( htmx_fragment_or_full, htmx_redirect, htmx_trigger, is_htmx, is_htmx_boosted, ) from musehub.types.json_types import JSONObject, StrDict def _make_request(headers: StrDict | None = None) -> MagicMock: """Return a mock FastAPI Request with the given headers.""" req = MagicMock() req.headers = Headers(headers=headers or {}) return req def _make_templates(rendered_name: list[str]) -> MagicMock: """Return a mock Jinja2Templates that records the template name used.""" templates = MagicMock() def fake_response(request: Request, name: str, ctx: JSONObject) -> Response: rendered_name.append(name) return Response(content=f"", media_type="text/html") templates.TemplateResponse = fake_response return templates # --------------------------------------------------------------------------- # is_htmx # --------------------------------------------------------------------------- def test_is_htmx_returns_true_with_header() -> None: req = _make_request({"HX-Request": "true"}) assert is_htmx(req) is True def test_is_htmx_returns_false_without_header() -> None: req = _make_request() assert is_htmx(req) is False def test_is_htmx_returns_false_wrong_value() -> None: req = _make_request({"HX-Request": "false"}) assert is_htmx(req) is False def test_is_htmx_returns_false_on_capitalised_value() -> None: """Header value comparison is case-sensitive; 'True' ≠ 'true'.""" req = _make_request({"HX-Request": "True"}) assert is_htmx(req) is False # --------------------------------------------------------------------------- # is_htmx_boosted # --------------------------------------------------------------------------- def test_is_htmx_boosted_with_header() -> None: req = _make_request({"HX-Boosted": "true"}) assert is_htmx_boosted(req) is True def test_is_htmx_boosted_without_header() -> None: req = _make_request() assert is_htmx_boosted(req) is False # --------------------------------------------------------------------------- # htmx_fragment_or_full # --------------------------------------------------------------------------- async def test_htmx_fragment_or_full_returns_fragment_on_htmx_request() -> None: rendered: list[str] = [] req = _make_request({"HX-Request": "true"}) templates = _make_templates(rendered) ctx = {} await htmx_fragment_or_full( req, templates, ctx, full_template="pages/full.html", fragment_template="fragments/part.html", ) assert rendered == ["fragments/part.html"] async def test_htmx_fragment_or_full_returns_full_on_direct_request() -> None: rendered: list[str] = [] req = _make_request() # no HX-Request header templates = _make_templates(rendered) ctx = {} await htmx_fragment_or_full( req, templates, ctx, full_template="pages/full.html", fragment_template="fragments/part.html", ) assert rendered == ["pages/full.html"] async def test_htmx_fragment_or_full_returns_full_when_no_fragment_template() -> None: """Even an HTMX request must get the full page when no fragment_template is given.""" rendered: list[str] = [] req = _make_request({"HX-Request": "true"}) templates = _make_templates(rendered) ctx = {} await htmx_fragment_or_full( req, templates, ctx, full_template="pages/full.html", fragment_template=None, ) assert rendered == ["pages/full.html"] # --------------------------------------------------------------------------- # htmx_trigger # --------------------------------------------------------------------------- def test_htmx_trigger_sets_header_with_detail() -> None: response = Response(content="ok") htmx_trigger(response, "toast", {"message": "Issue closed", "type": "success"}) raw = response.headers["HX-Trigger"] parsed = json.loads(raw) assert parsed == {"toast": {"message": "Issue closed", "type": "success"}} def test_htmx_trigger_sets_header_without_detail() -> None: response = Response(content="ok") htmx_trigger(response, "refresh") raw = response.headers["HX-Trigger"] parsed = json.loads(raw) assert parsed == {"refresh": True} def test_htmx_trigger_sets_header_with_none_detail() -> None: response = Response(content="ok") htmx_trigger(response, "ping", None) raw = response.headers["HX-Trigger"] parsed = json.loads(raw) assert parsed == {"ping": True} # --------------------------------------------------------------------------- # htmx_redirect # --------------------------------------------------------------------------- def test_htmx_redirect_sets_hx_redirect_header() -> None: response = htmx_redirect("/owner/repo") assert response.status_code == 200 assert response.headers["HX-Redirect"] == "/owner/repo"