gabriel / musehub public
test_musehub_jinja2_macros.py python
289 lines 9.3 KB
c2319918 fix(ci): resolve all test failures blocking PR #3 Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """Tests for MuseHub Jinja2 server-side filters and component macros.
2
3 Covers every filter function in jinja2_filters.py and spot-checks macro
4 rendering via a real Jinja2 Environment backed by the on-disk template tree.
5 """
6 from __future__ import annotations
7
8 from datetime import datetime, timedelta, timezone
9 from pathlib import Path
10
11 import pytest
12 from jinja2 import Environment, FileSystemLoader
13
14 from musehub.api.routes.musehub.jinja2_filters import (
15 _fmtdate,
16 _fmtrelative,
17 _label_text_color,
18 _shortsha,
19 register_musehub_filters,
20 )
21
22
23 # ---------------------------------------------------------------------------
24 # Fixtures
25 # ---------------------------------------------------------------------------
26
27
28 @pytest.fixture(scope="module")
29 def jinja_env() -> Environment:
30 """Real Jinja2 Environment pointed at the MuseHub template tree."""
31 template_dir = Path(__file__).parent.parent / "musehub" / "templates"
32 env = Environment(loader=FileSystemLoader(str(template_dir)), autoescape=True)
33 register_musehub_filters(env)
34 return env
35
36
37 # ---------------------------------------------------------------------------
38 # _fmtdate filter
39 # ---------------------------------------------------------------------------
40
41
42 def test_fmtdate_formats_datetime() -> None:
43 result = _fmtdate(datetime(2025, 1, 15, 10, 0, 0))
44 assert result == "Jan 15, 2025"
45
46
47 def test_fmtdate_formats_iso_string() -> None:
48 result = _fmtdate("2025-01-15T10:00:00Z")
49 assert result == "Jan 15, 2025"
50
51
52 def test_fmtdate_none_returns_empty() -> None:
53 assert _fmtdate(None) == ""
54
55
56 def test_fmtdate_iso_string_with_offset() -> None:
57 result = _fmtdate("2025-06-01T08:00:00+00:00")
58 assert result == "Jun 1, 2025"
59
60
61 # ---------------------------------------------------------------------------
62 # _fmtrelative filter
63 # ---------------------------------------------------------------------------
64
65
66 def test_fmtrelative_seconds() -> None:
67 value = datetime.now(timezone.utc) - timedelta(seconds=10)
68 assert _fmtrelative(value) == "just now"
69
70
71 def test_fmtrelative_one_minute() -> None:
72 value = datetime.now(timezone.utc) - timedelta(seconds=90)
73 assert _fmtrelative(value) == "1 minute ago"
74
75
76 def test_fmtrelative_hours() -> None:
77 value = datetime.now(timezone.utc) - timedelta(hours=2)
78 assert _fmtrelative(value) == "2 hours ago"
79
80
81 def test_fmtrelative_one_hour() -> None:
82 value = datetime.now(timezone.utc) - timedelta(hours=1)
83 assert _fmtrelative(value) == "1 hour ago"
84
85
86 def test_fmtrelative_days() -> None:
87 value = datetime.now(timezone.utc) - timedelta(days=3)
88 assert _fmtrelative(value) == "3 days ago"
89
90
91 def test_fmtrelative_one_day() -> None:
92 value = datetime.now(timezone.utc) - timedelta(days=1)
93 assert _fmtrelative(value) == "1 day ago"
94
95
96 def test_fmtrelative_none_returns_empty() -> None:
97 assert _fmtrelative(None) == ""
98
99
100 def test_fmtrelative_iso_string() -> None:
101 value = (datetime.now(timezone.utc) - timedelta(hours=5)).isoformat()
102 result = _fmtrelative(value)
103 assert result == "5 hours ago"
104
105
106 def test_fmtrelative_naive_datetime_treated_as_utc() -> None:
107 """Timezone-naive datetimes are assumed UTC per docstring contract."""
108 value = datetime.utcnow() - timedelta(minutes=30)
109 result = _fmtrelative(value)
110 assert result == "30 minutes ago"
111
112
113 # ---------------------------------------------------------------------------
114 # _shortsha filter
115 # ---------------------------------------------------------------------------
116
117
118 def test_shortsha_returns_8_chars() -> None:
119 assert _shortsha("a1b2c3d4e5f6") == "a1b2c3d4"
120
121
122 def test_shortsha_already_short() -> None:
123 assert _shortsha("abc") == "abc"
124
125
126 def test_shortsha_none_returns_empty() -> None:
127 assert _shortsha(None) == ""
128
129
130 def test_shortsha_empty_string_returns_empty() -> None:
131 assert _shortsha("") == ""
132
133
134 def test_shortsha_exact_8_chars() -> None:
135 assert _shortsha("12345678") == "12345678"
136
137
138 # ---------------------------------------------------------------------------
139 # _label_text_color filter
140 # ---------------------------------------------------------------------------
141
142
143 def test_label_text_color_dark_bg() -> None:
144 assert _label_text_color("#000000") == "#fff"
145
146
147 def test_label_text_color_light_bg() -> None:
148 assert _label_text_color("#ffffff") == "#000"
149
150
151 def test_label_text_color_mid_green() -> None:
152 """Bright green (#3fb950) has high luminance — dark text is more readable."""
153 assert _label_text_color("#3fb950") == "#000"
154
155
156 def test_label_text_color_without_hash() -> None:
157 assert _label_text_color("ffffff") == "#000"
158
159
160 def test_label_text_color_malformed_returns_dark() -> None:
161 assert _label_text_color("#xyz") == "#000"
162
163
164 def test_label_text_color_red() -> None:
165 assert _label_text_color("#ff0000") == "#fff"
166
167
168 # ---------------------------------------------------------------------------
169 # register_musehub_filters — environment registration
170 # ---------------------------------------------------------------------------
171
172
173 def test_jinja2_env_has_fmtdate_filter(jinja_env: Environment) -> None:
174 assert "fmtdate" in jinja_env.filters
175
176
177 def test_jinja2_env_has_fmtrelative_filter(jinja_env: Environment) -> None:
178 assert "fmtrelative" in jinja_env.filters
179
180
181 def test_jinja2_env_has_shortsha_filter(jinja_env: Environment) -> None:
182 assert "shortsha" in jinja_env.filters
183
184
185 def test_jinja2_env_has_label_text_color_filter(jinja_env: Environment) -> None:
186 assert "label_text_color" in jinja_env.filters
187
188
189 def test_fmtdate_filter_via_env(jinja_env: Environment) -> None:
190 tmpl = jinja_env.from_string('{{ "2025-01-15T10:00:00Z" | fmtdate }}')
191 assert tmpl.render() == "Jan 15, 2025"
192
193
194 def test_shortsha_filter_via_env(jinja_env: Environment) -> None:
195 tmpl = jinja_env.from_string('{{ sha | shortsha }}')
196 assert tmpl.render(sha="abcdef1234567890") == "abcdef12"
197
198
199 # ---------------------------------------------------------------------------
200 # Macro rendering — issue_row
201 # ---------------------------------------------------------------------------
202
203
204 class _FakeIssue:
205 issueId = "i-1"
206 number = 42
207 title = "Fix timing issue in drum track"
208 state = "open"
209 labels: list[str] = ["bug", "audio"]
210 createdAt = "2025-01-15T10:00:00Z"
211 created_at = "2025-01-15T10:00:00Z"
212 author = "alice"
213
214
215 def test_issue_row_macro_renders_title(jinja_env: Environment) -> None:
216 tmpl = jinja_env.get_template("musehub/macros/issue.html")
217 macro = tmpl.module.issue_row # type: ignore[attr-defined]
218 html = macro(_FakeIssue(), base_url="/musehub/ui/alice/myrepo")
219 assert "Fix timing issue in drum track" in html
220
221
222 def test_issue_row_macro_renders_issue_number(jinja_env: Environment) -> None:
223 tmpl = jinja_env.get_template("musehub/macros/issue.html")
224 macro = tmpl.module.issue_row # type: ignore[attr-defined]
225 html = macro(_FakeIssue(), base_url="/musehub/ui/alice/myrepo")
226 assert "#42" in html
227
228
229 def test_issue_row_macro_renders_date(jinja_env: Environment) -> None:
230 tmpl = jinja_env.get_template("musehub/macros/issue.html")
231 macro = tmpl.module.issue_row # type: ignore[attr-defined]
232 html = macro(_FakeIssue(), base_url="/musehub/ui/alice/myrepo")
233 assert "Jan 15, 2025" in html
234
235
236 # ---------------------------------------------------------------------------
237 # Macro rendering — pagination
238 # ---------------------------------------------------------------------------
239
240
241 def test_pagination_macro_renders_prev_next(jinja_env: Environment) -> None:
242 tmpl = jinja_env.get_template("musehub/macros/pagination.html")
243 macro = tmpl.module.pagination # type: ignore[attr-defined]
244 html = macro(page=2, total_pages=5, url="/musehub/ui/alice/myrepo/issues")
245 assert "Prev" in html
246 assert "Next" in html
247 assert "Page 2 of 5" in html
248
249
250 def test_pagination_macro_hidden_on_single_page(jinja_env: Environment) -> None:
251 tmpl = jinja_env.get_template("musehub/macros/pagination.html")
252 macro = tmpl.module.pagination # type: ignore[attr-defined]
253 html = macro(page=1, total_pages=1, url="/musehub/ui/alice/myrepo/issues")
254 assert html.strip() == ""
255
256
257 def test_pagination_macro_no_prev_on_first_page(jinja_env: Environment) -> None:
258 tmpl = jinja_env.get_template("musehub/macros/pagination.html")
259 macro = tmpl.module.pagination # type: ignore[attr-defined]
260 html = macro(page=1, total_pages=3, url="/musehub/ui/alice/myrepo/issues")
261 assert "Prev" not in html
262 assert "Next" in html
263
264
265 # ---------------------------------------------------------------------------
266 # Macro rendering — empty_state
267 # ---------------------------------------------------------------------------
268
269
270 def test_empty_state_macro_renders_action(jinja_env: Environment) -> None:
271 tmpl = jinja_env.get_template("musehub/macros/empty_state.html")
272 macro = tmpl.module.empty_state # type: ignore[attr-defined]
273 html = macro(
274 "📭",
275 "No issues yet",
276 "Open an issue to start tracking work.",
277 action_url="/new",
278 action_label="Open an issue",
279 )
280 assert "No issues yet" in html
281 assert "Open an issue" in html
282 assert 'href="/new"' in html
283
284
285 def test_empty_state_macro_no_action_when_url_missing(jinja_env: Environment) -> None:
286 tmpl = jinja_env.get_template("musehub/macros/empty_state.html")
287 macro = tmpl.module.empty_state # type: ignore[attr-defined]
288 html = macro("📭", "No issues yet", "Nothing here.")
289 assert "btn" not in html