gabriel / musehub public
test_musehub_ui_labels_ssr.py python
184 lines 6.2 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """SSR tests for Muse Hub labels management UI — issue #557.
2
3 Verifies that the labels page renders data server-side in Jinja2 templates
4 without requiring JavaScript execution. Tests assert on HTML content directly
5 returned by the server, not on JavaScript rendering logic.
6
7 Covers GET /musehub/ui/{owner}/{repo_slug}/labels:
8 - test_labels_page_renders_label_name_server_side
9 - test_labels_page_shows_issue_count_server_side
10 - test_labels_page_fragment_on_htmx_request
11
12 Covers POST mutations with HX-Request header:
13 - test_labels_htmx_create_returns_fragment
14 - test_labels_htmx_delete_returns_fragment
15 - test_labels_htmx_reset_returns_10_defaults
16 """
17 from __future__ import annotations
18
19 import pytest
20 from httpx import AsyncClient
21 from sqlalchemy.ext.asyncio import AsyncSession
22
23 from musehub.db.musehub_label_models import MusehubLabel
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 = "label_ssr_artist",
35 slug: str = "label-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-label-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_label(
52 db: AsyncSession,
53 repo_id: str,
54 *,
55 name: str = "bug",
56 color: str = "#d73a4a",
57 description: str | None = "Something isn't working",
58 ) -> MusehubLabel:
59 """Seed a label and return the ORM instance."""
60 label = MusehubLabel(
61 repo_id=repo_id,
62 name=name,
63 color=color,
64 description=description,
65 )
66 db.add(label)
67 await db.commit()
68 await db.refresh(label)
69 return label
70
71
72 # ---------------------------------------------------------------------------
73 # GET — SSR assertions
74 # ---------------------------------------------------------------------------
75
76
77 @pytest.mark.anyio
78 async def test_labels_page_renders_label_name_server_side(
79 client: AsyncClient,
80 db_session: AsyncSession,
81 ) -> None:
82 """Label name is present in the HTML returned by the server, not injected by JS."""
83 repo_id = await _make_repo(db_session)
84 await _make_label(db_session, repo_id, name="needs-arrangement")
85 response = await client.get("/musehub/ui/label_ssr_artist/label-ssr-album/labels")
86 assert response.status_code == 200
87 assert "text/html" in response.headers["content-type"]
88 assert "needs-arrangement" in response.text
89
90
91 @pytest.mark.anyio
92 async def test_labels_page_shows_issue_count_server_side(
93 client: AsyncClient,
94 db_session: AsyncSession,
95 ) -> None:
96 """Issue count is rendered in the label list HTML by the server."""
97 repo_id = await _make_repo(db_session)
98 await _make_label(db_session, repo_id, name="enhancement", color="#a2eeef")
99 response = await client.get("/musehub/ui/label_ssr_artist/label-ssr-album/labels")
100 assert response.status_code == 200
101 # Label row with zero issues: "0 issues" should appear
102 assert "0 issues" in response.text or "issues" in response.text
103
104
105 @pytest.mark.anyio
106 async def test_labels_page_fragment_on_htmx_request(
107 client: AsyncClient,
108 db_session: AsyncSession,
109 ) -> None:
110 """HX-Request: true causes the handler to return only the fragment — no <html> shell."""
111 repo_id = await _make_repo(db_session)
112 await _make_label(db_session, repo_id, name="htmx-fragment-label")
113 response = await client.get(
114 "/musehub/ui/label_ssr_artist/label-ssr-album/labels",
115 headers={"HX-Request": "true"},
116 )
117 assert response.status_code == 200
118 assert "<html" not in response.text
119 assert "htmx-fragment-label" in response.text
120
121
122 # ---------------------------------------------------------------------------
123 # POST mutations — HTMX fragment returns
124 # ---------------------------------------------------------------------------
125
126
127 @pytest.mark.anyio
128 async def test_labels_htmx_create_returns_fragment(
129 client: AsyncClient,
130 db_session: AsyncSession,
131 auth_headers: dict[str, str],
132 ) -> None:
133 """POST create with HX-Request returns updated label list fragment.
134
135 Sends JSON body (handled by _parse_label_create_body) with HX-Request header.
136 """
137 await _make_repo(db_session)
138 htmx_headers = {**auth_headers, "HX-Request": "true"}
139 response = await client.post(
140 "/musehub/ui/label_ssr_artist/label-ssr-album/labels",
141 json={"name": "htmx-created-label", "color": "#ff0000"},
142 headers=htmx_headers,
143 )
144 assert response.status_code == 201
145 assert "<html" not in response.text
146 assert "htmx-created-label" in response.text
147
148
149 @pytest.mark.anyio
150 async def test_labels_htmx_delete_returns_fragment(
151 client: AsyncClient,
152 db_session: AsyncSession,
153 auth_headers: dict[str, str],
154 ) -> None:
155 """POST delete with HX-Request returns fragment without the deleted label."""
156 repo_id = await _make_repo(db_session)
157 label = await _make_label(db_session, repo_id, name="to-be-deleted")
158 htmx_headers = {**auth_headers, "HX-Request": "true"}
159 response = await client.post(
160 f"/musehub/ui/label_ssr_artist/label-ssr-album/labels/{label.id}/delete",
161 headers=htmx_headers,
162 )
163 assert response.status_code == 200
164 assert "<html" not in response.text
165 assert "to-be-deleted" not in response.text
166
167
168 @pytest.mark.anyio
169 async def test_labels_htmx_reset_returns_10_defaults(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 auth_headers: dict[str, str],
173 ) -> None:
174 """POST reset with HX-Request returns fragment containing all 10 default labels."""
175 await _make_repo(db_session)
176 htmx_headers = {**auth_headers, "HX-Request": "true"}
177 response = await client.post(
178 "/musehub/ui/label_ssr_artist/label-ssr-album/labels/reset",
179 headers=htmx_headers,
180 )
181 assert response.status_code == 200
182 assert "<html" not in response.text
183 # 10 default labels — count the unique per-row IDs to avoid matching class="label-row-actions"
184 assert response.text.count('id="label-row-') == 10