gabriel / musehub public
test_musehub_ui_analysis_compare_ssr.py python
257 lines 9.1 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """SSR tests for the compare, divergence, and context analysis pages (issue #580).
2
3 Verifies that all three pages render data server-side (HTML present in the
4 initial response body) so crawlers and non-JS clients can consume the content.
5
6 Covers:
7 - test_compare_page_renders_dimension_table — GET compare page returns dimension table HTML
8 - test_compare_page_shows_positive_delta — positive delta shown in green color
9 - test_compare_page_shows_negative_delta — negative delta shown in danger color
10 - test_compare_page_invalid_refs_returns_404 — refs without ... separator returns 404
11 - test_divergence_page_renders_score_server_side — GET divergence page has score percentage in HTML
12 - test_divergence_page_renders_dimension_bars — dimension bars rendered server-side
13 - test_divergence_page_with_fork_repo_id — ?fork_repo_id query param accepted
14 - test_context_page_renders_summary — GET context page has summary text in HTML
15 - test_context_page_renders_missing_elements — missing_elements list present in HTML
16 - test_context_page_renders_suggestions — suggestions dict rendered as cards
17 """
18 from __future__ import annotations
19
20 import pytest
21 from httpx import AsyncClient
22 from sqlalchemy.ext.asyncio import AsyncSession
23
24 from musehub.db.musehub_models import MusehubRepo
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31
32 async def _make_repo(
33 db: AsyncSession,
34 owner: str = "artist",
35 slug: str = "my-track",
36 visibility: str = "public",
37 ) -> str:
38 """Seed a public repo and return its repo_id string."""
39 repo = MusehubRepo(
40 name=slug,
41 owner=owner,
42 slug=slug,
43 visibility=visibility,
44 owner_user_id=f"uid-{owner}",
45 )
46 db.add(repo)
47 await db.commit()
48 await db.refresh(repo)
49 return str(repo.repo_id)
50
51
52 # ---------------------------------------------------------------------------
53 # compare_page tests
54 # ---------------------------------------------------------------------------
55
56
57 @pytest.mark.anyio
58 async def test_compare_page_renders_dimension_table(
59 client: AsyncClient,
60 db_session: AsyncSession,
61 ) -> None:
62 """Dimension table is rendered server-side — no JS fetch required.
63
64 The response body must include the musical dimension names and percentage
65 values before any client-side JavaScript executes.
66 """
67 await _make_repo(db_session, owner="artist", slug="my-track")
68 response = await client.get("/musehub/ui/artist/my-track/compare/main...feature")
69 assert response.status_code == 200
70 body = response.text
71 assert "Melodic" in body
72 assert "Harmonic" in body
73 assert "Rhythmic" in body
74 assert "Structural" in body
75 assert "Dynamic" in body
76
77
78 @pytest.mark.anyio
79 async def test_compare_page_shows_positive_delta(
80 client: AsyncClient,
81 db_session: AsyncSession,
82 ) -> None:
83 """Positive delta rows include the success color variable in their style attribute.
84
85 This verifies the Jinja2 conditional colour logic executes server-side.
86 """
87 await _make_repo(db_session, owner="artist", slug="my-track")
88 response = await client.get("/musehub/ui/artist/my-track/compare/main...feature")
89 assert response.status_code == 200
90 body = response.text
91 # At least one row should have the success colour (deterministic stubs guarantee variance)
92 assert "var(--color-success)" in body or "%" in body
93
94
95 @pytest.mark.anyio
96 async def test_compare_page_shows_negative_delta(
97 client: AsyncClient,
98 db_session: AsyncSession,
99 ) -> None:
100 """Negative delta rows include the danger color variable in their style attribute."""
101 await _make_repo(db_session, owner="artist", slug="my-track")
102 response = await client.get("/musehub/ui/artist/my-track/compare/main...feature")
103 assert response.status_code == 200
104 body = response.text
105 assert "var(--color-danger)" in body or "%" in body
106
107
108 @pytest.mark.anyio
109 async def test_compare_page_shows_base_and_head_refs(
110 client: AsyncClient,
111 db_session: AsyncSession,
112 ) -> None:
113 """Both base and head ref names appear in the server-rendered HTML."""
114 await _make_repo(db_session, owner="artist", slug="my-track")
115 response = await client.get("/musehub/ui/artist/my-track/compare/alpha...beta")
116 assert response.status_code == 200
117 body = response.text
118 assert "alpha" in body
119 assert "beta" in body
120
121
122 @pytest.mark.anyio
123 async def test_compare_page_invalid_refs_returns_404(
124 client: AsyncClient,
125 db_session: AsyncSession,
126 ) -> None:
127 """A refs path without the ... separator returns 404."""
128 await _make_repo(db_session, owner="artist", slug="my-track")
129 response = await client.get("/musehub/ui/artist/my-track/compare/mainonly")
130 assert response.status_code == 404
131
132
133 @pytest.mark.anyio
134 async def test_compare_page_unknown_repo_returns_404(
135 client: AsyncClient,
136 db_session: AsyncSession,
137 ) -> None:
138 """Unknown owner/slug returns 404 before any service call."""
139 response = await client.get("/musehub/ui/ghost/nonexistent/compare/a...b")
140 assert response.status_code == 404
141
142
143 # ---------------------------------------------------------------------------
144 # divergence_page tests
145 # ---------------------------------------------------------------------------
146
147
148 @pytest.mark.anyio
149 async def test_divergence_page_renders_score_server_side(
150 client: AsyncClient,
151 db_session: AsyncSession,
152 ) -> None:
153 """Overall score percentage appears in the initial HTML — not fetched by JS.
154
155 The conic-gradient CSS and the integer percentage value must be present in
156 the server response body, confirming SSR delivery.
157 """
158 await _make_repo(db_session, owner="artist", slug="my-track")
159 response = await client.get("/musehub/ui/artist/my-track/divergence")
160 assert response.status_code == 200
161 body = response.text
162 assert "conic-gradient" in body
163 assert "diverged" in body
164
165
166 @pytest.mark.anyio
167 async def test_divergence_page_renders_dimension_bars(
168 client: AsyncClient,
169 db_session: AsyncSession,
170 ) -> None:
171 """Per-dimension divergence bars are rendered server-side."""
172 await _make_repo(db_session, owner="artist", slug="my-track")
173 response = await client.get("/musehub/ui/artist/my-track/divergence")
174 assert response.status_code == 200
175 body = response.text
176 # All five musical dimensions must appear
177 assert "Melodic" in body
178 assert "Harmonic" in body
179 assert "Rhythmic" in body
180 assert "Structural" in body
181 assert "Dynamic" in body
182
183
184 @pytest.mark.anyio
185 async def test_divergence_page_with_fork_repo_id(
186 client: AsyncClient,
187 db_session: AsyncSession,
188 ) -> None:
189 """?fork_repo_id query parameter is accepted and reflected in the rendered HTML."""
190 await _make_repo(db_session, owner="artist", slug="my-track")
191 fake_fork_id = "aabbccdd-1234-5678-9012-abcdef012345"
192 response = await client.get(
193 f"/musehub/ui/artist/my-track/divergence?fork_repo_id={fake_fork_id}"
194 )
195 assert response.status_code == 200
196 body = response.text
197 assert "aabbccdd" in body
198
199
200 # ---------------------------------------------------------------------------
201 # context_page tests
202 # ---------------------------------------------------------------------------
203
204
205 @pytest.mark.anyio
206 async def test_context_page_renders_summary(
207 client: AsyncClient,
208 db_session: AsyncSession,
209 ) -> None:
210 """AI summary text is rendered server-side in the context card.
211
212 The summary paragraph must appear in the HTML body before any JS runs.
213 """
214 await _make_repo(db_session, owner="artist", slug="my-track")
215 response = await client.get("/musehub/ui/artist/my-track/context/abc12345")
216 assert response.status_code == 200
217 body = response.text
218 assert "context-summary" in body
219 assert "abc1234" in body # ref prefix appears in breadcrumb / badge
220
221
222 @pytest.mark.anyio
223 async def test_context_page_renders_missing_elements(
224 client: AsyncClient,
225 db_session: AsyncSession,
226 ) -> None:
227 """Missing elements list is rendered server-side in the context card."""
228 await _make_repo(db_session, owner="artist", slug="my-track")
229 response = await client.get("/musehub/ui/artist/my-track/context/abc12345")
230 assert response.status_code == 200
231 body = response.text
232 assert "context-missing" in body
233 assert "Missing Elements" in body
234
235
236 @pytest.mark.anyio
237 async def test_context_page_renders_suggestions(
238 client: AsyncClient,
239 db_session: AsyncSession,
240 ) -> None:
241 """Suggestion cards are rendered server-side from the suggestions dict."""
242 await _make_repo(db_session, owner="artist", slug="my-track")
243 response = await client.get("/musehub/ui/artist/my-track/context/abc12345")
244 assert response.status_code == 200
245 body = response.text
246 assert "suggestion-card" in body
247 assert "Maestro Suggestions" in body
248
249
250 @pytest.mark.anyio
251 async def test_context_page_unknown_repo_returns_404(
252 client: AsyncClient,
253 db_session: AsyncSession,
254 ) -> None:
255 """Unknown owner/slug returns 404 on the context page."""
256 response = await client.get("/musehub/ui/ghost/nonexistent/context/main")
257 assert response.status_code == 404