gabriel / musehub public
test_musehub_ui_analysis_complex_ssr.py python
364 lines 12.0 KB
7f1d07e8 feat: domains, MCP expansion, MIDI player, and production hardening (#8) Gabriel Cardona <cgcardona@gmail.com> 4d ago
1 """Tests for complex analysis page SSR migration (issue #579).
2
3 Verifies that dynamics, emotion, chord-map, contour, and motifs pages
4 render their analysis data server-side — the HTML response must contain
5 the SVG/HTML chart elements without requiring a client-side fetch.
6
7 Covers:
8 - test_dynamics_page_renders_server_side — GET dynamics page, assert <svg or <rect in HTML
9 - test_dynamics_page_shows_velocity_bars — assert SVG velocity bar elements are present
10 - test_emotion_page_renders_svg_scatter — GET emotion page, assert <circle in SVG
11 - test_emotion_page_shows_summary_vector — assert energy/valence/tension/darkness bars in HTML
12 - test_chord_map_page_renders_progression — assert chord symbol in HTML
13 - test_chord_map_page_shows_chord_table — assert chord table present in HTML
14 - test_contour_page_renders_polyline — GET contour page, assert <polyline in HTML
15 - test_contour_page_shows_shape_label — assert shape label in HTML
16 - test_motifs_page_renders_patterns — assert interval pattern element in HTML
17 - test_motifs_page_shows_occurrence_grid — assert occurrences present in HTML
18 - test_complex_analysis_htmx_fragment_path — GET with HX-Request:true returns fragment (emotion)
19 - test_dynamics_htmx_fragment_path — GET dynamics with HX-Request:true returns fragment
20 - test_contour_htmx_fragment_path — GET contour with HX-Request:true returns fragment
21 - test_chord_map_htmx_fragment_path — GET chord-map with HX-Request:true returns fragment
22 - test_motifs_htmx_fragment_path — GET motifs with HX-Request:true returns fragment
23 """
24 from __future__ import annotations
25
26 import pytest
27 from httpx import AsyncClient
28 from sqlalchemy.ext.asyncio import AsyncSession
29
30 from musehub.db.musehub_models import MusehubRepo
31
32 # ---------------------------------------------------------------------------
33 # Helpers
34 # ---------------------------------------------------------------------------
35
36 _ANALYSIS_REF = "deadbeef12345678"
37
38
39 async def _make_repo(db_session: AsyncSession) -> str:
40 """Seed a minimal repo and return its repo_id."""
41 repo = MusehubRepo(
42 name="test-beats",
43 owner="testuser",
44 slug="test-beats",
45 visibility="private",
46 owner_user_id="test-owner",
47 )
48 db_session.add(repo)
49 await db_session.commit()
50 await db_session.refresh(repo)
51 return str(repo.repo_id)
52
53
54 # ---------------------------------------------------------------------------
55 # Dynamics page tests
56 # ---------------------------------------------------------------------------
57
58
59 @pytest.mark.anyio
60 async def test_dynamics_page_renders_server_side(
61 client: AsyncClient,
62 db_session: AsyncSession,
63 ) -> None:
64 """GET dynamics page must contain an SVG element rendered server-side."""
65 await _make_repo(db_session)
66 response = await client.get(
67 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/dynamics"
68 )
69 assert response.status_code == 200
70 assert "text/html" in response.headers["content-type"]
71 body = response.text
72 assert "<svg" in body
73
74
75 @pytest.mark.anyio
76 async def test_dynamics_page_shows_velocity_bars(
77 client: AsyncClient,
78 db_session: AsyncSession,
79 ) -> None:
80 """Dynamics page must render SVG <rect> velocity bars server-side."""
81 await _make_repo(db_session)
82 response = await client.get(
83 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/dynamics"
84 )
85 assert response.status_code == 200
86 body = response.text
87 assert "<rect" in body
88 assert "fill-opacity" in body
89
90
91 @pytest.mark.anyio
92 async def test_dynamics_page_shows_arc_badge(
93 client: AsyncClient,
94 db_session: AsyncSession,
95 ) -> None:
96 """Dynamics page must render arc classification badge server-side."""
97 await _make_repo(db_session)
98 response = await client.get(
99 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/dynamics"
100 )
101 assert response.status_code == 200
102 body = response.text
103 # At least one of the known arc types must appear
104 arc_types = ["flat", "terraced", "crescendo", "decrescendo", "swell", "hairpin"]
105 assert any(arc in body for arc in arc_types)
106
107
108 # ---------------------------------------------------------------------------
109 # Emotion page tests
110 # ---------------------------------------------------------------------------
111
112
113 @pytest.mark.anyio
114 async def test_emotion_page_renders_svg_scatter(
115 client: AsyncClient,
116 db_session: AsyncSession,
117 ) -> None:
118 """GET emotion page must contain SVG <circle> elements for the scatter plot."""
119 await _make_repo(db_session)
120 response = await client.get(
121 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/emotion"
122 )
123 assert response.status_code == 200
124 body = response.text
125 assert "<circle" in body
126 assert "<svg" in body
127
128
129 @pytest.mark.anyio
130 async def test_emotion_page_shows_summary_vector(
131 client: AsyncClient,
132 db_session: AsyncSession,
133 ) -> None:
134 """Emotion page must render energy/valence/tension/darkness bars server-side."""
135 await _make_repo(db_session)
136 response = await client.get(
137 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/emotion"
138 )
139 assert response.status_code == 200
140 body = response.text
141 assert "Energy" in body
142 assert "Valence" in body
143 assert "Tension" in body
144 assert "Darkness" in body
145
146
147 @pytest.mark.anyio
148 async def test_emotion_page_shows_narrative(
149 client: AsyncClient,
150 db_session: AsyncSession,
151 ) -> None:
152 """Emotion page must render the narrative text server-side."""
153 await _make_repo(db_session)
154 response = await client.get(
155 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/emotion"
156 )
157 assert response.status_code == 200
158 body = response.text
159 assert "NARRATIVE" in body
160 assert "TRAJECTORY" in body
161
162
163 # ---------------------------------------------------------------------------
164 # Chord map page tests
165 # ---------------------------------------------------------------------------
166
167
168 @pytest.mark.anyio
169 async def test_chord_map_page_renders_progression(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 ) -> None:
173 """GET chord-map page must contain chord symbols rendered server-side."""
174 await _make_repo(db_session)
175 response = await client.get(
176 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/chord-map"
177 )
178 assert response.status_code == 200
179 body = response.text
180 assert "PROGRESSION TIMELINE" in body
181
182
183 @pytest.mark.anyio
184 async def test_chord_map_page_shows_chord_table(
185 client: AsyncClient,
186 db_session: AsyncSession,
187 ) -> None:
188 """Chord map page must render a chord table with beat and function columns."""
189 await _make_repo(db_session)
190 response = await client.get(
191 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/chord-map"
192 )
193 assert response.status_code == 200
194 body = response.text
195 assert "CHORD TABLE" in body
196 assert "Beat" in body
197 assert "Function" in body
198 assert "Tension" in body
199
200
201 # ---------------------------------------------------------------------------
202 # Contour page tests
203 # ---------------------------------------------------------------------------
204
205
206 @pytest.mark.anyio
207 async def test_contour_page_renders_polyline(
208 client: AsyncClient,
209 db_session: AsyncSession,
210 ) -> None:
211 """GET contour page must contain SVG <polyline> rendered server-side."""
212 await _make_repo(db_session)
213 response = await client.get(
214 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/contour"
215 )
216 assert response.status_code == 200
217 body = response.text
218 assert "<polyline" in body
219 assert "<svg" in body
220
221
222 @pytest.mark.anyio
223 async def test_contour_page_shows_shape_label(
224 client: AsyncClient,
225 db_session: AsyncSession,
226 ) -> None:
227 """Contour page must render the shape label and direction server-side."""
228 await _make_repo(db_session)
229 response = await client.get(
230 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/contour"
231 )
232 assert response.status_code == 200
233 body = response.text
234 assert "Shape" in body
235 assert "Overall Direction" in body
236 assert "Direction Changes" in body
237
238
239 # ---------------------------------------------------------------------------
240 # Motifs page tests
241 # ---------------------------------------------------------------------------
242
243
244 @pytest.mark.anyio
245 async def test_motifs_page_renders_patterns(
246 client: AsyncClient,
247 db_session: AsyncSession,
248 ) -> None:
249 """GET motifs page must contain interval pattern elements rendered server-side."""
250 await _make_repo(db_session)
251 response = await client.get(
252 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/motifs"
253 )
254 assert response.status_code == 200
255 body = response.text
256 assert "INTERVAL PATTERN" in body
257
258
259 @pytest.mark.anyio
260 async def test_motifs_page_shows_occurrence_grid(
261 client: AsyncClient,
262 db_session: AsyncSession,
263 ) -> None:
264 """Motifs page must render occurrence beat markers server-side."""
265 await _make_repo(db_session)
266 response = await client.get(
267 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/motifs"
268 )
269 assert response.status_code == 200
270 body = response.text
271 assert "OCCURRENCES" in body
272
273
274 # ---------------------------------------------------------------------------
275 # HTMX fragment path tests
276 # ---------------------------------------------------------------------------
277
278
279 @pytest.mark.anyio
280 async def test_complex_analysis_htmx_fragment_path(
281 client: AsyncClient,
282 db_session: AsyncSession,
283 ) -> None:
284 """GET emotion page with HX-Request:true returns bare fragment (no <html> wrapper)."""
285 await _make_repo(db_session)
286 response = await client.get(
287 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/emotion",
288 headers={"HX-Request": "true"},
289 )
290 assert response.status_code == 200
291 body = response.text
292 # Fragment must contain chart elements
293 assert "<circle" in body or "Valence" in body
294 # Fragment must NOT contain the full HTML shell
295 assert "<!DOCTYPE html>" not in body
296 assert "<html" not in body
297
298
299 @pytest.mark.anyio
300 async def test_dynamics_htmx_fragment_path(
301 client: AsyncClient,
302 db_session: AsyncSession,
303 ) -> None:
304 """GET dynamics page with HX-Request:true returns bare fragment."""
305 await _make_repo(db_session)
306 response = await client.get(
307 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/dynamics",
308 headers={"HX-Request": "true"},
309 )
310 assert response.status_code == 200
311 body = response.text
312 assert "<rect" in body or "velocity" in body.lower()
313 assert "<!DOCTYPE html>" not in body
314
315
316 @pytest.mark.anyio
317 async def test_contour_htmx_fragment_path(
318 client: AsyncClient,
319 db_session: AsyncSession,
320 ) -> None:
321 """GET contour page with HX-Request:true returns bare fragment."""
322 await _make_repo(db_session)
323 response = await client.get(
324 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/contour",
325 headers={"HX-Request": "true"},
326 )
327 assert response.status_code == 200
328 body = response.text
329 assert "<polyline" in body or "Shape" in body
330 assert "<!DOCTYPE html>" not in body
331
332
333 @pytest.mark.anyio
334 async def test_chord_map_htmx_fragment_path(
335 client: AsyncClient,
336 db_session: AsyncSession,
337 ) -> None:
338 """GET chord-map page with HX-Request:true returns bare fragment."""
339 await _make_repo(db_session)
340 response = await client.get(
341 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/chord-map",
342 headers={"HX-Request": "true"},
343 )
344 assert response.status_code == 200
345 body = response.text
346 assert "PROGRESSION TIMELINE" in body or "Beat" in body
347 assert "<!DOCTYPE html>" not in body
348
349
350 @pytest.mark.anyio
351 async def test_motifs_htmx_fragment_path(
352 client: AsyncClient,
353 db_session: AsyncSession,
354 ) -> None:
355 """GET motifs page with HX-Request:true returns bare fragment."""
356 await _make_repo(db_session)
357 response = await client.get(
358 f"/testuser/test-beats/insights/{_ANALYSIS_REF}/motifs",
359 headers={"HX-Request": "true"},
360 )
361 assert response.status_code == 200
362 body = response.text
363 assert "INTERVAL PATTERN" in body or "occurrences" in body.lower()
364 assert "<!DOCTYPE html>" not in body