gabriel / musehub public
test_musehub_ui_milestones_ssr.py python
236 lines 8.2 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """SSR tests for Muse Hub milestones UI pages — issue #558.
2
3 Validates that milestone data is rendered server-side into HTML (not deferred
4 to client JS) and that HTMX fragment requests return bare HTML without the
5 full page shell.
6
7 Covers GET /musehub/ui/{owner}/{repo_slug}/milestones:
8 - test_milestones_list_renders_title_server_side — milestone title in HTML
9 - test_milestones_list_progress_bar_has_correct_width — width:75% for 3/4 closed
10 - test_milestones_list_htmx_state_switch_returns_fragment — HX-Request → bare fragment
11
12 Covers GET /musehub/ui/{owner}/{repo_slug}/milestones/{number}:
13 - test_milestone_detail_renders_milestone_title — title in HTML server-side
14 - test_milestone_detail_shows_linked_issues — issue title in HTML
15 - test_milestone_detail_issue_state_filter_open — ?state=closed shows only closed
16 - test_milestone_detail_unknown_number_404 — unknown number → 404
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 (
25 MusehubIssue,
26 MusehubMilestone,
27 MusehubRepo,
28 )
29
30
31 # ---------------------------------------------------------------------------
32 # Seed helpers
33 # ---------------------------------------------------------------------------
34
35
36 async def _make_repo(db: AsyncSession, owner: str = "artist", slug: str = "ssr-album") -> str:
37 """Seed a public repo and return its repo_id string."""
38 repo = MusehubRepo(
39 name=slug,
40 owner=owner,
41 slug=slug,
42 visibility="public",
43 owner_user_id="uid-ssr-artist",
44 )
45 db.add(repo)
46 await db.commit()
47 await db.refresh(repo)
48 return str(repo.repo_id)
49
50
51 async def _make_milestone(
52 db: AsyncSession,
53 repo_id: str,
54 *,
55 number: int = 1,
56 title: str = "SSR Milestone",
57 description: str = "SSR milestone description",
58 state: str = "open",
59 ) -> MusehubMilestone:
60 """Seed a milestone and return the ORM object."""
61 ms = MusehubMilestone(
62 repo_id=repo_id,
63 number=number,
64 title=title,
65 description=description,
66 state=state,
67 author="artist",
68 )
69 db.add(ms)
70 await db.commit()
71 await db.refresh(ms)
72 return ms
73
74
75 async def _make_issue(
76 db: AsyncSession,
77 repo_id: str,
78 *,
79 number: int = 1,
80 title: str = "Linked issue",
81 state: str = "open",
82 milestone_id: str | None = None,
83 ) -> MusehubIssue:
84 """Seed an issue and return the ORM object."""
85 issue = MusehubIssue(
86 repo_id=repo_id,
87 number=number,
88 title=title,
89 body="Issue body.",
90 state=state,
91 labels=["mix"],
92 author="artist",
93 milestone_id=milestone_id,
94 )
95 db.add(issue)
96 await db.commit()
97 await db.refresh(issue)
98 return issue
99
100
101 # ---------------------------------------------------------------------------
102 # Milestones list SSR tests
103 # ---------------------------------------------------------------------------
104
105
106 @pytest.mark.anyio
107 async def test_milestones_list_renders_title_server_side(
108 client: AsyncClient,
109 db_session: AsyncSession,
110 ) -> None:
111 """Milestone title is rendered into the HTML response server-side (not via JS)."""
112 repo_id = await _make_repo(db_session)
113 await _make_milestone(db_session, repo_id, title="Album Release Milestone")
114 response = await client.get(
115 "/musehub/ui/artist/ssr-album/milestones?state=all"
116 )
117 assert response.status_code == 200
118 assert "text/html" in response.headers["content-type"]
119 # Title must appear in the HTML without requiring client-side JS execution.
120 assert "Album Release Milestone" in response.text
121
122
123 @pytest.mark.anyio
124 async def test_milestones_list_progress_bar_has_correct_width(
125 client: AsyncClient,
126 db_session: AsyncSession,
127 ) -> None:
128 """Progress bar fill width reflects closed/total ratio (3 closed, 1 open → 75%)."""
129 repo_id = await _make_repo(db_session)
130 ms = await _make_milestone(db_session, repo_id, title="Progress Test")
131 mid = str(ms.milestone_id)
132
133 # Seed 3 closed + 1 open issue linked to the milestone.
134 await _make_issue(db_session, repo_id, number=1, state="closed", milestone_id=mid)
135 await _make_issue(db_session, repo_id, number=2, state="closed", milestone_id=mid)
136 await _make_issue(db_session, repo_id, number=3, state="closed", milestone_id=mid)
137 await _make_issue(db_session, repo_id, number=4, state="open", milestone_id=mid)
138
139 response = await client.get("/musehub/ui/artist/ssr-album/milestones?state=all")
140 assert response.status_code == 200
141 # Fragment renders the inline style; int(75.0) == 75 → "width:75%"
142 assert "width:75%" in response.text
143
144
145 @pytest.mark.anyio
146 async def test_milestones_list_htmx_state_switch_returns_fragment(
147 client: AsyncClient,
148 db_session: AsyncSession,
149 ) -> None:
150 """HX-Request: true returns a bare HTML fragment without the full page shell."""
151 repo_id = await _make_repo(db_session)
152 ms = await _make_milestone(db_session, repo_id, state="closed", title="Closed Milestone")
153 _ = ms # milestone exists so the closed tab has content
154
155 response = await client.get(
156 "/musehub/ui/artist/ssr-album/milestones?state=closed",
157 headers={"HX-Request": "true"},
158 )
159 assert response.status_code == 200
160 # Fragment must NOT contain the full HTML shell.
161 assert "<html" not in response.text
162 assert "<head" not in response.text
163 # Fragment should contain milestone content or empty-state markup.
164 assert "milestone" in response.text.lower() or "No milestones" in response.text
165
166
167 # ---------------------------------------------------------------------------
168 # Milestone detail SSR tests
169 # ---------------------------------------------------------------------------
170
171
172 @pytest.mark.anyio
173 async def test_milestone_detail_renders_milestone_title(
174 client: AsyncClient,
175 db_session: AsyncSession,
176 ) -> None:
177 """Milestone title is in the response HTML server-side (not behind JS)."""
178 repo_id = await _make_repo(db_session)
179 await _make_milestone(db_session, repo_id, number=1, title="SSR Detail Title")
180 response = await client.get("/musehub/ui/artist/ssr-album/milestones/1")
181 assert response.status_code == 200
182 assert "text/html" in response.headers["content-type"]
183 assert "SSR Detail Title" in response.text
184
185
186 @pytest.mark.anyio
187 async def test_milestone_detail_shows_linked_issues(
188 client: AsyncClient,
189 db_session: AsyncSession,
190 ) -> None:
191 """Issues assigned to the milestone appear in the HTML without JS execution."""
192 repo_id = await _make_repo(db_session)
193 ms = await _make_milestone(db_session, repo_id, number=1, title="Linked Issues Test")
194 await _make_issue(
195 db_session,
196 repo_id,
197 number=1,
198 title="Bass groove needs more swing",
199 milestone_id=str(ms.milestone_id),
200 )
201 response = await client.get("/musehub/ui/artist/ssr-album/milestones/1")
202 assert response.status_code == 200
203 assert "Bass groove needs more swing" in response.text
204
205
206 @pytest.mark.anyio
207 async def test_milestone_detail_issue_state_filter_closed(
208 client: AsyncClient,
209 db_session: AsyncSession,
210 ) -> None:
211 """?state=closed shows only closed linked issues."""
212 repo_id = await _make_repo(db_session)
213 ms = await _make_milestone(db_session, repo_id, number=1, title="Filter Test")
214 mid = str(ms.milestone_id)
215 await _make_issue(
216 db_session, repo_id, number=1, title="Open issue title", state="open", milestone_id=mid
217 )
218 await _make_issue(
219 db_session, repo_id, number=2, title="Closed issue title", state="closed", milestone_id=mid
220 )
221 response = await client.get("/musehub/ui/artist/ssr-album/milestones/1?state=closed")
222 assert response.status_code == 200
223 body = response.text
224 assert "Closed issue title" in body
225 assert "Open issue title" not in body
226
227
228 @pytest.mark.anyio
229 async def test_milestone_detail_unknown_number_404(
230 client: AsyncClient,
231 db_session: AsyncSession,
232 ) -> None:
233 """Non-existent milestone number returns 404."""
234 await _make_repo(db_session)
235 response = await client.get("/musehub/ui/artist/ssr-album/milestones/9999")
236 assert response.status_code == 404