gabriel / musehub public
test_musehub_ui_proposal_ssr.py python
222 lines 8.1 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """SSR tests for MuseHub proposal list + proposal detail pages — issue #569.
2
3 Validates that proposal data is rendered server-side into HTML (not deferred to client
4 JS) and that HTMX fragment requests return bare HTML without the full page shell.
5
6 Covers GET /{owner}/{repo_slug}/proposals:
7 - test_proposal_list_renders_title_server_side — proposal title appears in HTML
8 - test_proposal_list_open_closed_counts_in_tabs — tab counts reflect seeded proposals
9 - test_proposal_list_htmx_fragment_on_tab_switch — HX-Request: true → fragment
10
11 Covers GET /{owner}/{repo_slug}/proposals/{proposal_id}:
12 - test_proposal_detail_renders_title_server_side — proposal title in HTML server-side
13 - test_proposal_detail_renders_diff_stats — branch info in HTML
14 - test_proposal_detail_merge_button_has_hx_post — merge button has hx-post
15 - test_proposal_detail_merge_button_disabled_when_not_mergeable — closed proposal → no merge button
16 - test_proposal_detail_unknown_number_404 — non-existent proposal_id → 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 MusehubProposal, MusehubRepo
25
26
27 # ---------------------------------------------------------------------------
28 # Seed helpers
29 # ---------------------------------------------------------------------------
30
31
32 async def _make_repo(
33 db: AsyncSession,
34 owner: str = "proposaldev",
35 slug: str = "proposal-ssr-album",
36 ) -> 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-proposal-ssr-dev",
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_proposal(
52 db: AsyncSession,
53 repo_id: str,
54 *,
55 proposal_number: int = 1,
56 title: str = "Add bossa nova bridge",
57 body: str = "Adds a new bossa nova bridge section.",
58 state: str = "open",
59 from_branch: str = "feat/bossa-nova",
60 to_branch: str = "main",
61 author: str = "beatmaker",
62 ) -> MusehubProposal:
63 """Seed a proposal and return the ORM object."""
64 proposal = MusehubProposal(
65 repo_id=repo_id,
66 proposal_number=proposal_number,
67 title=title,
68 body=body,
69 state=state,
70 from_branch=from_branch,
71 to_branch=to_branch,
72 author=author,
73 )
74 db.add(proposal)
75 await db.commit()
76 await db.refresh(proposal)
77 return proposal
78
79
80 # ---------------------------------------------------------------------------
81 # Proposal list SSR tests
82 # ---------------------------------------------------------------------------
83
84
85 @pytest.mark.anyio
86 async def test_proposal_list_renders_title_server_side(
87 client: AsyncClient,
88 db_session: AsyncSession,
89 ) -> None:
90 """Proposal title is rendered into the HTML response server-side without client JS."""
91 repo_id = await _make_repo(db_session)
92 await _make_proposal(db_session, repo_id, title="Funk bridge with wah pedal")
93 response = await client.get("/proposaldev/proposal-ssr-album/proposals")
94 assert response.status_code == 200
95 assert "text/html" in response.headers["content-type"]
96 assert "Funk bridge with wah pedal" in response.text
97
98
99 @pytest.mark.anyio
100 async def test_proposal_list_open_closed_counts_in_tabs(
101 client: AsyncClient,
102 db_session: AsyncSession,
103 ) -> None:
104 """State tabs display SSR-computed open/merged/closed counts."""
105 repo_id = await _make_repo(db_session)
106 await _make_proposal(db_session, repo_id, proposal_number=1, title="Open proposal 1", state="open")
107 await _make_proposal(db_session, repo_id, proposal_number=2, title="Open proposal 2", state="open")
108 await _make_proposal(db_session, repo_id, proposal_number=3, title="Merged proposal", state="merged")
109 response = await client.get("/proposaldev/proposal-ssr-album/proposals")
110 assert response.status_code == 200
111 body = response.text
112 # Tab counts for open and merged must appear as server-rendered numbers.
113 assert "2" in body # open_count
114 assert "1" in body # merged_count
115
116
117 @pytest.mark.anyio
118 async def test_proposal_list_htmx_fragment_on_tab_switch(
119 client: AsyncClient,
120 db_session: AsyncSession,
121 ) -> None:
122 """HX-Request: true with state=merged returns a bare HTML fragment."""
123 repo_id = await _make_repo(db_session)
124 await _make_proposal(db_session, repo_id, title="Merged feature", state="merged")
125 response = await client.get(
126 "/proposaldev/proposal-ssr-album/proposals?state=merged",
127 headers={"HX-Request": "true"},
128 )
129 assert response.status_code == 200
130 body = response.text
131 # Fragment must NOT contain the full HTML page shell.
132 assert "<html" not in body
133 assert "<head" not in body
134 # Proposal title must appear in the fragment.
135 assert "Merged feature" in body
136
137
138 # ---------------------------------------------------------------------------
139 # Proposal detail SSR tests
140 # ---------------------------------------------------------------------------
141
142
143 @pytest.mark.anyio
144 async def test_proposal_detail_renders_title_server_side(
145 client: AsyncClient,
146 db_session: AsyncSession,
147 ) -> None:
148 """Proposal title and branch info appear in the detail page HTML server-side."""
149 repo_id = await _make_repo(db_session)
150 proposal = await _make_proposal(
151 db_session, repo_id, title="Add jazz chord voicings", from_branch="feat/jazz"
152 )
153 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
154 assert response.status_code == 200
155 assert "text/html" in response.headers["content-type"]
156 assert "Add jazz chord voicings" in response.text
157
158
159 @pytest.mark.anyio
160 async def test_proposal_detail_renders_diff_stats(
161 client: AsyncClient,
162 db_session: AsyncSession,
163 ) -> None:
164 """Branch names (from_branch / to_branch) appear in the detail page HTML."""
165 repo_id = await _make_repo(db_session)
166 proposal = await _make_proposal(
167 db_session,
168 repo_id,
169 title="Bass groove proposal",
170 from_branch="feat/bass-groove",
171 to_branch="dev",
172 )
173 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
174 assert response.status_code == 200
175 body = response.text
176 # Both branch names must appear in the server-rendered HTML.
177 assert "feat/bass-groove" in body
178 assert "dev" in body
179
180
181 @pytest.mark.anyio
182 async def test_proposal_detail_merge_button_has_hx_post(
183 client: AsyncClient,
184 db_session: AsyncSession,
185 ) -> None:
186 """An open proposal detail page includes a merge button with an hx-post attribute."""
187 repo_id = await _make_repo(db_session)
188 proposal = await _make_proposal(db_session, repo_id, title="Merge-ready proposal", state="open")
189 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
190 assert response.status_code == 200
191 body = response.text
192 # The merge card must have at least one HTMX POST trigger.
193 assert "hx-post" in body
194 assert "merge" in body.lower()
195
196
197 @pytest.mark.anyio
198 async def test_proposal_detail_merge_button_disabled_when_not_mergeable(
199 client: AsyncClient,
200 db_session: AsyncSession,
201 ) -> None:
202 """A closed or merged proposal does not show the merge button."""
203 repo_id = await _make_repo(db_session)
204 proposal = await _make_proposal(db_session, repo_id, title="Already Merged proposal", state="merged")
205 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
206 assert response.status_code == 200
207 body = response.text
208 # Merged/closed proposals must not render the merge action form.
209 assert "Merge merge proposal" not in body
210
211
212 @pytest.mark.anyio
213 async def test_proposal_detail_unknown_number_404(
214 client: AsyncClient,
215 db_session: AsyncSession,
216 ) -> None:
217 """A request for a non-existent proposal id returns HTTP 404."""
218 await _make_repo(db_session)
219 response = await client.get(
220 "/proposaldev/proposal-ssr-album/proposals/nonexistent-proposal-uuid"
221 )
222 assert response.status_code == 404
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago