gabriel / musehub public
test_musehub_ui_proposal_ssr.py python
214 lines 8.0 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 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 async def test_proposal_list_renders_title_server_side(
86 client: AsyncClient,
87 db_session: AsyncSession,
88 ) -> None:
89 """Proposal title is rendered into the HTML response server-side without client JS."""
90 repo_id = await _make_repo(db_session)
91 await _make_proposal(db_session, repo_id, title="Funk bridge with wah pedal")
92 response = await client.get("/proposaldev/proposal-ssr-album/proposals")
93 assert response.status_code == 200
94 assert "text/html" in response.headers["content-type"]
95 assert "Funk bridge with wah pedal" in response.text
96
97
98 async def test_proposal_list_open_closed_counts_in_tabs(
99 client: AsyncClient,
100 db_session: AsyncSession,
101 ) -> None:
102 """State tabs display SSR-computed open/merged/closed counts."""
103 repo_id = await _make_repo(db_session)
104 await _make_proposal(db_session, repo_id, proposal_number=1, title="Open proposal 1", state="open")
105 await _make_proposal(db_session, repo_id, proposal_number=2, title="Open proposal 2", state="open")
106 await _make_proposal(db_session, repo_id, proposal_number=3, title="Merged proposal", state="merged")
107 response = await client.get("/proposaldev/proposal-ssr-album/proposals")
108 assert response.status_code == 200
109 body = response.text
110 # Tab counts for open and merged must appear as server-rendered numbers.
111 assert "2" in body # open_count
112 assert "1" in body # merged_count
113
114
115 async def test_proposal_list_htmx_fragment_on_tab_switch(
116 client: AsyncClient,
117 db_session: AsyncSession,
118 ) -> None:
119 """HX-Request: true with state=merged returns a bare HTML fragment."""
120 repo_id = await _make_repo(db_session)
121 await _make_proposal(db_session, repo_id, title="Merged feature", state="merged")
122 response = await client.get(
123 "/proposaldev/proposal-ssr-album/proposals?state=merged",
124 headers={"HX-Request": "true"},
125 )
126 assert response.status_code == 200
127 body = response.text
128 # Fragment must NOT contain the full HTML page shell.
129 assert "<html" not in body
130 assert "<head" not in body
131 # Proposal title must appear in the fragment.
132 assert "Merged feature" in body
133
134
135 # ---------------------------------------------------------------------------
136 # Proposal detail SSR tests
137 # ---------------------------------------------------------------------------
138
139
140 async def test_proposal_detail_renders_title_server_side(
141 client: AsyncClient,
142 db_session: AsyncSession,
143 ) -> None:
144 """Proposal title and branch info appear in the detail page HTML server-side."""
145 repo_id = await _make_repo(db_session)
146 proposal = await _make_proposal(
147 db_session, repo_id, title="Add jazz chord voicings", from_branch="feat/jazz"
148 )
149 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
150 assert response.status_code == 200
151 assert "text/html" in response.headers["content-type"]
152 assert "Add jazz chord voicings" in response.text
153
154
155 async def test_proposal_detail_renders_diff_stats(
156 client: AsyncClient,
157 db_session: AsyncSession,
158 ) -> None:
159 """Branch names (from_branch / to_branch) appear in the detail page HTML."""
160 repo_id = await _make_repo(db_session)
161 proposal = await _make_proposal(
162 db_session,
163 repo_id,
164 title="Bass groove proposal",
165 from_branch="feat/bass-groove",
166 to_branch="dev",
167 )
168 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
169 assert response.status_code == 200
170 body = response.text
171 # Both branch names must appear in the server-rendered HTML.
172 assert "feat/bass-groove" in body
173 assert "dev" in body
174
175
176 async def test_proposal_detail_merge_button_has_hx_post(
177 client: AsyncClient,
178 db_session: AsyncSession,
179 ) -> None:
180 """An open proposal detail page includes a merge button with an hx-post attribute."""
181 repo_id = await _make_repo(db_session)
182 proposal = await _make_proposal(db_session, repo_id, title="Merge-ready proposal", state="open")
183 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
184 assert response.status_code == 200
185 body = response.text
186 # The merge card must have at least one HTMX POST trigger.
187 assert "hx-post" in body
188 assert "merge" in body.lower()
189
190
191 async def test_proposal_detail_merge_button_disabled_when_not_mergeable(
192 client: AsyncClient,
193 db_session: AsyncSession,
194 ) -> None:
195 """A closed or merged proposal does not show the merge button."""
196 repo_id = await _make_repo(db_session)
197 proposal = await _make_proposal(db_session, repo_id, title="Already Merged proposal", state="merged")
198 response = await client.get(f"/proposaldev/proposal-ssr-album/proposals/{proposal.proposal_id}")
199 assert response.status_code == 200
200 body = response.text
201 # Merged/closed proposals must not render the merge action form.
202 assert "Merge merge proposal" not in body
203
204
205 async def test_proposal_detail_unknown_number_404(
206 client: AsyncClient,
207 db_session: AsyncSession,
208 ) -> None:
209 """A request for a non-existent proposal id returns HTTP 404."""
210 await _make_repo(db_session)
211 response = await client.get(
212 "/proposaldev/proposal-ssr-album/proposals/nonexistent-proposal-uuid"
213 )
214 assert response.status_code == 404
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago