gabriel / musehub public
test_musehub_htmx_helpers.py python
181 lines 5.3 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """Tests for musehub.api.routes.musehub.htmx_helpers.
2
3 Covers HX-Request detection, HX-Boosted detection, fragment/full routing,
4 HX-Trigger header emission, and HX-Redirect response generation.
5 """
6
7 from __future__ import annotations
8
9 import json
10 from unittest.mock import AsyncMock, MagicMock, patch
11
12 import pytest
13 from starlette.datastructures import Headers
14 from starlette.responses import Response
15 from starlette.testclient import TestClient
16
17 from fastapi import Request
18 from musehub.api.routes.musehub.htmx_helpers import (
19 htmx_fragment_or_full,
20 htmx_redirect,
21 htmx_trigger,
22 is_htmx,
23 is_htmx_boosted,
24 )
25 from musehub.types.json_types import JSONObject, StrDict
26
27
28 def _make_request(headers: StrDict | None = None) -> MagicMock:
29 """Return a mock FastAPI Request with the given headers."""
30 req = MagicMock()
31 req.headers = Headers(headers=headers or {})
32 return req
33
34
35 def _make_templates(rendered_name: list[str]) -> MagicMock:
36 """Return a mock Jinja2Templates that records the template name used."""
37
38 templates = MagicMock()
39
40 def fake_response(request: Request, name: str, ctx: JSONObject) -> Response:
41 rendered_name.append(name)
42 return Response(content=f"<rendered:{name}>", media_type="text/html")
43
44 templates.TemplateResponse = fake_response
45 return templates
46
47
48 # ---------------------------------------------------------------------------
49 # is_htmx
50 # ---------------------------------------------------------------------------
51
52
53 def test_is_htmx_returns_true_with_header() -> None:
54 req = _make_request({"HX-Request": "true"})
55 assert is_htmx(req) is True
56
57
58 def test_is_htmx_returns_false_without_header() -> None:
59 req = _make_request()
60 assert is_htmx(req) is False
61
62
63 def test_is_htmx_returns_false_wrong_value() -> None:
64 req = _make_request({"HX-Request": "false"})
65 assert is_htmx(req) is False
66
67
68 def test_is_htmx_returns_false_on_capitalised_value() -> None:
69 """Header value comparison is case-sensitive; 'True' ≠ 'true'."""
70 req = _make_request({"HX-Request": "True"})
71 assert is_htmx(req) is False
72
73
74 # ---------------------------------------------------------------------------
75 # is_htmx_boosted
76 # ---------------------------------------------------------------------------
77
78
79 def test_is_htmx_boosted_with_header() -> None:
80 req = _make_request({"HX-Boosted": "true"})
81 assert is_htmx_boosted(req) is True
82
83
84 def test_is_htmx_boosted_without_header() -> None:
85 req = _make_request()
86 assert is_htmx_boosted(req) is False
87
88
89 # ---------------------------------------------------------------------------
90 # htmx_fragment_or_full
91 # ---------------------------------------------------------------------------
92
93
94 async def test_htmx_fragment_or_full_returns_fragment_on_htmx_request() -> None:
95 rendered: list[str] = []
96 req = _make_request({"HX-Request": "true"})
97 templates = _make_templates(rendered)
98 ctx = {}
99
100 await htmx_fragment_or_full(
101 req, templates, ctx,
102 full_template="pages/full.html",
103 fragment_template="fragments/part.html",
104 )
105
106 assert rendered == ["fragments/part.html"]
107
108
109 async def test_htmx_fragment_or_full_returns_full_on_direct_request() -> None:
110 rendered: list[str] = []
111 req = _make_request() # no HX-Request header
112 templates = _make_templates(rendered)
113 ctx = {}
114
115 await htmx_fragment_or_full(
116 req, templates, ctx,
117 full_template="pages/full.html",
118 fragment_template="fragments/part.html",
119 )
120
121 assert rendered == ["pages/full.html"]
122
123
124 async def test_htmx_fragment_or_full_returns_full_when_no_fragment_template() -> None:
125 """Even an HTMX request must get the full page when no fragment_template is given."""
126 rendered: list[str] = []
127 req = _make_request({"HX-Request": "true"})
128 templates = _make_templates(rendered)
129 ctx = {}
130
131 await htmx_fragment_or_full(
132 req, templates, ctx,
133 full_template="pages/full.html",
134 fragment_template=None,
135 )
136
137 assert rendered == ["pages/full.html"]
138
139
140 # ---------------------------------------------------------------------------
141 # htmx_trigger
142 # ---------------------------------------------------------------------------
143
144
145 def test_htmx_trigger_sets_header_with_detail() -> None:
146 response = Response(content="ok")
147 htmx_trigger(response, "toast", {"message": "Issue closed", "type": "success"})
148
149 raw = response.headers["HX-Trigger"]
150 parsed = json.loads(raw)
151 assert parsed == {"toast": {"message": "Issue closed", "type": "success"}}
152
153
154 def test_htmx_trigger_sets_header_without_detail() -> None:
155 response = Response(content="ok")
156 htmx_trigger(response, "refresh")
157
158 raw = response.headers["HX-Trigger"]
159 parsed = json.loads(raw)
160 assert parsed == {"refresh": True}
161
162
163 def test_htmx_trigger_sets_header_with_none_detail() -> None:
164 response = Response(content="ok")
165 htmx_trigger(response, "ping", None)
166
167 raw = response.headers["HX-Trigger"]
168 parsed = json.loads(raw)
169 assert parsed == {"ping": True}
170
171
172 # ---------------------------------------------------------------------------
173 # htmx_redirect
174 # ---------------------------------------------------------------------------
175
176
177 def test_htmx_redirect_sets_hx_redirect_header() -> None:
178 response = htmx_redirect("/owner/repo")
179
180 assert response.status_code == 200
181 assert response.headers["HX-Redirect"] == "/owner/repo"
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago