gabriel / musehub public
test_musehub_ui_new_repo_ssr.py python
209 lines 7.2 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """SSR tests for the Muse Hub new repo creation wizard (issue #562).
2
3 Validates that the wizard form is rendered server-side via Jinja2 — license
4 options, form inputs, and HTMX attributes appear in the raw HTML response
5 without requiring JavaScript execution. Also validates that the availability
6 check endpoint returns an HTML fragment when called by HTMX.
7
8 Covers:
9 - test_new_repo_page_renders_license_options_server_side — license <option> in HTML
10 - test_new_repo_page_has_hx_get_on_name_input — name input has hx-get attribute
11 - test_new_repo_page_has_visibility_inputs — Public/Private radio inputs in HTML
12 - test_new_repo_page_form_renders_without_js — form element in SSR HTML
13 - test_new_repo_page_has_hx_indicator_on_name_input — hx-indicator attribute present
14 - test_new_repo_page_has_name_check_indicator_span — indicator span exists in HTML
15 - test_new_repo_name_check_htmx_returns_available_html — HTMX → HTML span "Available"
16 - test_new_repo_name_check_htmx_returns_taken_html — HTMX → HTML span "taken"
17 - test_new_repo_name_check_json_path_unchanged — no HX-Request → JSON response
18 """
19 from __future__ import annotations
20
21 import pytest
22 from httpx import AsyncClient
23 from sqlalchemy.ext.asyncio import AsyncSession
24
25 from musehub.db.musehub_models import MusehubRepo
26
27
28 # ---------------------------------------------------------------------------
29 # Seed helper
30 # ---------------------------------------------------------------------------
31
32
33 async def _seed_repo(
34 db: AsyncSession,
35 owner: str = "existingowner",
36 slug: str = "taken-repo",
37 ) -> None:
38 """Seed a public repo so the slug appears as taken in check requests."""
39 repo = MusehubRepo(
40 name=slug,
41 owner=owner,
42 slug=slug,
43 visibility="public",
44 owner_user_id="seed-uid",
45 )
46 db.add(repo)
47 await db.commit()
48
49
50 # ---------------------------------------------------------------------------
51 # Tests — SSR form verification
52 # ---------------------------------------------------------------------------
53
54
55 @pytest.mark.anyio
56 async def test_new_repo_page_renders_license_options_server_side(
57 client: AsyncClient,
58 db_session: AsyncSession,
59 ) -> None:
60 """License <option> elements are server-rendered in the HTML response.
61
62 Confirms the license dropdown is Jinja2-rendered from the ``licenses``
63 context variable, not built by client-side JavaScript.
64 """
65 response = await client.get("/musehub/ui/new")
66 assert response.status_code == 200
67 body = response.text
68 # CC BY license option must appear as a real <option> tag in the raw HTML
69 assert "<option" in body
70 assert "CC BY" in body
71
72
73 @pytest.mark.anyio
74 async def test_new_repo_page_has_hx_get_on_name_input(
75 client: AsyncClient,
76 db_session: AsyncSession,
77 ) -> None:
78 """The repository name input has an hx-get attribute pointing to /new/check.
79
80 This drives the live HTMX availability check without client JS polling.
81 """
82 response = await client.get("/musehub/ui/new")
83 assert response.status_code == 200
84 body = response.text
85 assert "hx-get" in body
86 assert "/musehub/ui/new/check" in body
87
88
89 @pytest.mark.anyio
90 async def test_new_repo_page_has_visibility_inputs(
91 client: AsyncClient,
92 db_session: AsyncSession,
93 ) -> None:
94 """Public and Private visibility radio inputs are in the server-rendered HTML."""
95 response = await client.get("/musehub/ui/new")
96 assert response.status_code == 200
97 body = response.text
98 assert 'value="public"' in body
99 assert 'value="private"' in body
100
101
102 @pytest.mark.anyio
103 async def test_new_repo_page_form_renders_without_js(
104 client: AsyncClient,
105 db_session: AsyncSession,
106 ) -> None:
107 """A <form> element is present in the SSR HTML — no JS required to show it.
108
109 The old JS-shell pattern rendered the form via innerHTML inside load().
110 SSR means the form tag appears in the raw server response.
111 """
112 response = await client.get("/musehub/ui/new")
113 assert response.status_code == 200
114 assert "<form" in response.text
115
116
117 @pytest.mark.anyio
118 async def test_new_repo_page_has_hx_indicator_on_name_input(
119 client: AsyncClient,
120 db_session: AsyncSession,
121 ) -> None:
122 """The name input has hx-indicator pointing at #name-check-indicator.
123
124 This gives users a loading spinner while the debounced availability check
125 request is in-flight (issue #704).
126 """
127 response = await client.get("/musehub/ui/new")
128 assert response.status_code == 200
129 body = response.text
130 assert 'hx-indicator="#name-check-indicator"' in body
131
132
133 @pytest.mark.anyio
134 async def test_new_repo_page_has_name_check_indicator_span(
135 client: AsyncClient,
136 db_session: AsyncSession,
137 ) -> None:
138 """A span with id=name-check-indicator and class=htmx-indicator is rendered.
139
140 HTMX toggles opacity on this element while the availability check is
141 in-flight, giving the user visual feedback without any custom JavaScript
142 (issue #704).
143 """
144 response = await client.get("/musehub/ui/new")
145 assert response.status_code == 200
146 body = response.text
147 assert 'id="name-check-indicator"' in body
148 assert 'class="htmx-indicator"' in body
149
150
151 # ---------------------------------------------------------------------------
152 # Tests — /new/check availability endpoint
153 # ---------------------------------------------------------------------------
154
155
156 @pytest.mark.anyio
157 async def test_new_repo_name_check_htmx_returns_available_html(
158 client: AsyncClient,
159 db_session: AsyncSession,
160 ) -> None:
161 """GET /new/check with HX-Request header returns an HTML availability span.
162
163 The span is swapped into #name-check by HTMX — no JS needed.
164 """
165 response = await client.get(
166 "/musehub/ui/new/check",
167 params={"owner": "newowner", "slug": "unique-name-xyz-123"},
168 headers={"HX-Request": "true"},
169 )
170 assert response.status_code == 200
171 assert "text/html" in response.headers["content-type"]
172 assert "<span" in response.text
173 assert "Available" in response.text
174
175
176 @pytest.mark.anyio
177 async def test_new_repo_name_check_htmx_returns_taken_html(
178 client: AsyncClient,
179 db_session: AsyncSession,
180 ) -> None:
181 """GET /new/check for an existing slug returns a "taken" HTML span."""
182 await _seed_repo(db_session, owner="existingowner", slug="taken-repo")
183 response = await client.get(
184 "/musehub/ui/new/check",
185 params={"owner": "existingowner", "slug": "taken-repo"},
186 headers={"HX-Request": "true"},
187 )
188 assert response.status_code == 200
189 assert "text/html" in response.headers["content-type"]
190 body = response.text
191 assert "<span" in body
192 assert "taken" in body.lower() or "✗" in body
193
194
195 @pytest.mark.anyio
196 async def test_new_repo_name_check_json_path_unchanged(
197 client: AsyncClient,
198 db_session: AsyncSession,
199 ) -> None:
200 """GET /new/check without HX-Request header returns JSON — backward-compat path."""
201 response = await client.get(
202 "/musehub/ui/new/check",
203 params={"owner": "anyowner", "slug": "any-slug-999"},
204 )
205 assert response.status_code == 200
206 assert response.headers["content-type"].startswith("application/json")
207 data = response.json()
208 assert "available" in data
209 assert isinstance(data["available"], bool)