gabriel / musehub public
test_musehub_ui_analysis_simple_ssr.py python
415 lines 14.3 KB
7f1d07e8 feat: domains, MCP expansion, MIDI player, and production hardening (#8) Gabriel Cardona <cgcardona@gmail.com> 4d ago
1 """SSR tests for the analysis dashboard and five simple dimension pages.
2
3 Covers the migration from client-side JS data fetching to server-side Jinja2
4 rendering per issue #578 — analysis dashboard + key, tempo, meter, groove, form.
5
6 Tests:
7 - test_analysis_dashboard_renders_dimension_links — GET dashboard, dimension links in HTML
8 - test_analysis_dashboard_no_auth_required — accessible without JWT
9 - test_analysis_dashboard_renders_key_data_server_side — key tonic in HTML without JS fetch
10 - test_key_analysis_renders_tonic_server_side — tonic rendered in HTML by Jinja2
11 - test_key_analysis_renders_distribution_bars — confidence bar present as CSS
12 - test_key_analysis_htmx_fragment_path — HX-Request:true returns fragment (no extends)
13 - test_tempo_analysis_renders_bpm_server_side — BPM value in HTML
14 - test_tempo_analysis_renders_stability_bar — stability bar present as CSS
15 - test_meter_analysis_renders_time_signature — time signature in HTML
16 - test_meter_analysis_renders_beat_strength_bars — beat strength bars in HTML
17 - test_groove_analysis_renders_pattern — groove style in HTML
18 - test_groove_analysis_renders_score_gauge — groove score gauge present
19 - test_form_analysis_renders_sections — section names in HTML
20 - test_form_analysis_renders_timeline — form timeline present
21 - test_analysis_pages_no_js_chart_lib — no ChartJS or D3 references
22 """
23 from __future__ import annotations
24
25 import pytest
26 from httpx import AsyncClient
27 from sqlalchemy.ext.asyncio import AsyncSession
28
29 from musehub.db.musehub_models import MusehubRepo
30
31
32 # ---------------------------------------------------------------------------
33 # Helpers
34 # ---------------------------------------------------------------------------
35
36
37 async def _make_repo(db_session: AsyncSession) -> str:
38 """Seed a minimal repo and return its repo_id."""
39 repo = MusehubRepo(
40 name="analysis-ssr-beats",
41 owner="analysisuser",
42 slug="analysis-ssr-beats",
43 visibility="private",
44 owner_user_id="analysis-owner",
45 )
46 db_session.add(repo)
47 await db_session.commit()
48 await db_session.refresh(repo)
49 return str(repo.repo_id)
50
51
52 _REF = "abc1234def5678"
53 _BASE = "/analysisuser/analysis-ssr-beats"
54 _DASHBOARD_URL = f"{_BASE}/insights/{_REF}"
55 _KEY_URL = f"{_BASE}/insights/{_REF}/key"
56 _TEMPO_URL = f"{_BASE}/insights/{_REF}/tempo"
57 _METER_URL = f"{_BASE}/insights/{_REF}/meter"
58 _GROOVE_URL = f"{_BASE}/insights/{_REF}/groove"
59 _FORM_URL = f"{_BASE}/insights/{_REF}/form"
60
61
62 # ---------------------------------------------------------------------------
63 # Dashboard tests
64 # ---------------------------------------------------------------------------
65
66
67 @pytest.mark.anyio
68 async def test_analysis_dashboard_renders_dimension_links(
69 client: AsyncClient,
70 db_session: AsyncSession,
71 ) -> None:
72 """GET analysis dashboard returns an HTML insights page."""
73 await _make_repo(db_session)
74 response = await client.get(_DASHBOARD_URL)
75 assert response.status_code == 200
76 assert "text/html" in response.headers["content-type"]
77 body = response.text
78 # Insights page title is always present
79 assert "Insights" in body or "insights" in body.lower()
80
81
82 @pytest.mark.anyio
83 async def test_analysis_dashboard_no_auth_required(
84 client: AsyncClient,
85 db_session: AsyncSession,
86 ) -> None:
87 """Analysis dashboard is accessible without a JWT."""
88 await _make_repo(db_session)
89 response = await client.get(_DASHBOARD_URL)
90 assert response.status_code == 200
91
92
93 @pytest.mark.anyio
94 async def test_analysis_dashboard_renders_key_data_server_side(
95 client: AsyncClient,
96 db_session: AsyncSession,
97 ) -> None:
98 """Dashboard renders server-side — status 200 with HTML content."""
99 await _make_repo(db_session)
100 response = await client.get(_DASHBOARD_URL)
101 assert response.status_code == 200
102 body = response.text
103 # Page is server-rendered (not a blank shell waiting for JS)
104 assert "musehub" in body.lower()
105
106
107 @pytest.mark.anyio
108 async def test_analysis_dashboard_htmx_fragment_path(
109 client: AsyncClient,
110 db_session: AsyncSession,
111 ) -> None:
112 """GET dashboard with HX-Request: true returns fragment (no <html> wrapper)."""
113 await _make_repo(db_session)
114 response = await client.get(_DASHBOARD_URL, headers={"HX-Request": "true"})
115 assert response.status_code == 200
116 body = response.text
117 assert "<html" not in body
118 assert "Back to repo" in body or "Analysis" in body
119
120
121 # ---------------------------------------------------------------------------
122 # Key analysis tests
123 # ---------------------------------------------------------------------------
124
125
126 @pytest.mark.anyio
127 async def test_key_analysis_renders_tonic_server_side(
128 client: AsyncClient,
129 db_session: AsyncSession,
130 ) -> None:
131 """GET key analysis page renders tonic in HTML via Jinja2 (not via JS fetch)."""
132 await _make_repo(db_session)
133 response = await client.get(_KEY_URL)
134 assert response.status_code == 200
135 assert "text/html" in response.headers["content-type"]
136 body = response.text
137 assert "Key Detection" in body
138 # Tonic is one of the standard pitch classes — verify some key content is there
139 for note in ("C", "D", "E", "F", "G", "A", "B"):
140 if note in body:
141 break
142 else:
143 pytest.fail("No pitch class (key tonic) found in server-rendered key page")
144
145
146 @pytest.mark.anyio
147 async def test_key_analysis_renders_distribution_bars(
148 client: AsyncClient,
149 db_session: AsyncSession,
150 ) -> None:
151 """Key page renders confidence bar as inline CSS — no JS chart library required."""
152 await _make_repo(db_session)
153 response = await client.get(_KEY_URL)
154 assert response.status_code == 200
155 body = response.text
156 # Confidence bar uses inline CSS width% — rendered server-side
157 assert "Detection Confidence" in body
158 assert "%" in body
159
160
161 @pytest.mark.anyio
162 async def test_key_analysis_renders_alternate_keys(
163 client: AsyncClient,
164 db_session: AsyncSession,
165 ) -> None:
166 """Key page renders alternate key candidates server-side."""
167 await _make_repo(db_session)
168 response = await client.get(_KEY_URL)
169 assert response.status_code == 200
170 body = response.text
171 # Relative key is always rendered
172 assert "Relative Key" in body
173
174
175 @pytest.mark.anyio
176 async def test_key_analysis_htmx_fragment_path(
177 client: AsyncClient,
178 db_session: AsyncSession,
179 ) -> None:
180 """GET key page with HX-Request: true returns fragment (no <html> wrapper)."""
181 await _make_repo(db_session)
182 response = await client.get(_KEY_URL, headers={"HX-Request": "true"})
183 assert response.status_code == 200
184 body = response.text
185 assert "<html" not in body
186 assert "Key Detection" in body
187
188
189 # ---------------------------------------------------------------------------
190 # Tempo analysis tests
191 # ---------------------------------------------------------------------------
192
193
194 @pytest.mark.anyio
195 async def test_tempo_analysis_renders_bpm_server_side(
196 client: AsyncClient,
197 db_session: AsyncSession,
198 ) -> None:
199 """GET tempo analysis page renders BPM value in HTML (server-side, not JS)."""
200 await _make_repo(db_session)
201 response = await client.get(_TEMPO_URL)
202 assert response.status_code == 200
203 body = response.text
204 assert "Tempo Analysis" in body
205 assert "BPM" in body
206 # BPM is a numeric value — at least one digit must appear
207 assert any(c.isdigit() for c in body)
208
209
210 @pytest.mark.anyio
211 async def test_tempo_analysis_renders_stability_bar(
212 client: AsyncClient,
213 db_session: AsyncSession,
214 ) -> None:
215 """Tempo page renders stability bar as inline CSS — server-side only."""
216 await _make_repo(db_session)
217 response = await client.get(_TEMPO_URL)
218 assert response.status_code == 200
219 body = response.text
220 assert "Stability" in body
221 assert "Time Feel" in body
222 assert "Tempo Changes" in body
223
224
225 @pytest.mark.anyio
226 async def test_tempo_analysis_htmx_fragment_path(
227 client: AsyncClient,
228 db_session: AsyncSession,
229 ) -> None:
230 """GET tempo page with HX-Request: true returns fragment only."""
231 await _make_repo(db_session)
232 response = await client.get(_TEMPO_URL, headers={"HX-Request": "true"})
233 assert response.status_code == 200
234 body = response.text
235 assert "<html" not in body
236 assert "Tempo Analysis" in body
237
238
239 # ---------------------------------------------------------------------------
240 # Meter analysis tests
241 # ---------------------------------------------------------------------------
242
243
244 @pytest.mark.anyio
245 async def test_meter_analysis_renders_time_signature(
246 client: AsyncClient,
247 db_session: AsyncSession,
248 ) -> None:
249 """GET meter analysis page renders time signature in HTML (server-side)."""
250 await _make_repo(db_session)
251 response = await client.get(_METER_URL)
252 assert response.status_code == 200
253 body = response.text
254 assert "Meter Analysis" in body
255 assert "Time Signature" in body
256 # Time signature contains a slash like 4/4 or 3/4
257 assert "/" in body
258
259
260 @pytest.mark.anyio
261 async def test_meter_analysis_renders_beat_strength_bars(
262 client: AsyncClient,
263 db_session: AsyncSession,
264 ) -> None:
265 """Meter page renders beat strength profile as CSS bars — server-side only."""
266 await _make_repo(db_session)
267 response = await client.get(_METER_URL)
268 assert response.status_code == 200
269 body = response.text
270 assert "Beat Strength Profile" in body
271 # The meter type badge (compound/simple) is rendered server-side
272 assert "simple" in body or "compound" in body
273
274
275 @pytest.mark.anyio
276 async def test_meter_analysis_htmx_fragment_path(
277 client: AsyncClient,
278 db_session: AsyncSession,
279 ) -> None:
280 """GET meter page with HX-Request: true returns fragment only."""
281 await _make_repo(db_session)
282 response = await client.get(_METER_URL, headers={"HX-Request": "true"})
283 assert response.status_code == 200
284 body = response.text
285 assert "<html" not in body
286 assert "Meter Analysis" in body
287
288
289 # ---------------------------------------------------------------------------
290 # Groove analysis tests
291 # ---------------------------------------------------------------------------
292
293
294 @pytest.mark.anyio
295 async def test_groove_analysis_renders_pattern(
296 client: AsyncClient,
297 db_session: AsyncSession,
298 ) -> None:
299 """GET groove analysis page renders groove style in HTML (server-side)."""
300 await _make_repo(db_session)
301 response = await client.get(_GROOVE_URL)
302 assert response.status_code == 200
303 body = response.text
304 assert "Groove Analysis" in body
305 assert "Style" in body
306 # One of the known groove styles should appear
307 groove_styles = {"straight", "swing", "shuffled", "latin", "funk"}
308 assert any(style in body.lower() for style in groove_styles), (
309 "Expected a groove style name (straight/swing/shuffled/latin/funk) in response"
310 )
311
312
313 @pytest.mark.anyio
314 async def test_groove_analysis_renders_score_gauge(
315 client: AsyncClient,
316 db_session: AsyncSession,
317 ) -> None:
318 """Groove page renders score gauge and swing factor as CSS bars — server-side."""
319 await _make_repo(db_session)
320 response = await client.get(_GROOVE_URL)
321 assert response.status_code == 200
322 body = response.text
323 assert "Groove Score" in body
324 assert "Swing Factor" in body
325 assert "BPM" in body
326 assert "Onset Deviation" in body
327
328
329 @pytest.mark.anyio
330 async def test_groove_analysis_htmx_fragment_path(
331 client: AsyncClient,
332 db_session: AsyncSession,
333 ) -> None:
334 """GET groove page with HX-Request: true returns fragment only."""
335 await _make_repo(db_session)
336 response = await client.get(_GROOVE_URL, headers={"HX-Request": "true"})
337 assert response.status_code == 200
338 body = response.text
339 assert "<html" not in body
340 assert "Groove Analysis" in body
341
342
343 # ---------------------------------------------------------------------------
344 # Form analysis tests
345 # ---------------------------------------------------------------------------
346
347
348 @pytest.mark.anyio
349 async def test_form_analysis_renders_sections(
350 client: AsyncClient,
351 db_session: AsyncSession,
352 ) -> None:
353 """GET form analysis page renders section labels in HTML (server-side)."""
354 await _make_repo(db_session)
355 response = await client.get(_FORM_URL)
356 assert response.status_code == 200
357 body = response.text
358 assert "Form Analysis" in body
359 assert "Form" in body
360 # Sections table is always rendered; at least one section label must appear
361 section_labels = {"intro", "verse", "chorus", "bridge", "outro"}
362 assert any(label in body.lower() for label in section_labels), (
363 "Expected a section label (intro/verse/chorus/bridge/outro) in response"
364 )
365
366
367 @pytest.mark.anyio
368 async def test_form_analysis_renders_timeline(
369 client: AsyncClient,
370 db_session: AsyncSession,
371 ) -> None:
372 """Form page renders section timeline as CSS bars — server-side only."""
373 await _make_repo(db_session)
374 response = await client.get(_FORM_URL)
375 assert response.status_code == 200
376 body = response.text
377 assert "Form Timeline" in body
378 assert "Sections" in body
379 assert "Total Beats" in body
380
381
382 @pytest.mark.anyio
383 async def test_form_analysis_htmx_fragment_path(
384 client: AsyncClient,
385 db_session: AsyncSession,
386 ) -> None:
387 """GET form page with HX-Request: true returns fragment only."""
388 await _make_repo(db_session)
389 response = await client.get(_FORM_URL, headers={"HX-Request": "true"})
390 assert response.status_code == 200
391 body = response.text
392 assert "<html" not in body
393 assert "Form Analysis" in body
394
395
396 # ---------------------------------------------------------------------------
397 # No JS chart library tests (all pages)
398 # ---------------------------------------------------------------------------
399
400
401 @pytest.mark.anyio
402 async def test_analysis_pages_no_js_chart_lib(
403 client: AsyncClient,
404 db_session: AsyncSession,
405 ) -> None:
406 """None of the analysis SSR pages reference ChartJS or D3 chart libraries."""
407 await _make_repo(db_session)
408 urls = [_DASHBOARD_URL, _KEY_URL, _TEMPO_URL, _METER_URL, _GROOVE_URL, _FORM_URL]
409 for url in urls:
410 response = await client.get(url)
411 assert response.status_code == 200, f"Expected 200 for {url}"
412 body = response.text.lower()
413 assert "chart.js" not in body, f"ChartJS found in {url}"
414 assert "d3.js" not in body, f"D3 found in {url}"
415 assert "cdn.jsdelivr.net/npm/chart" not in body, f"ChartJS CDN found in {url}"