gabriel / musehub public
test_musehub_ui_blame_ssr.py python
204 lines 6.8 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """SSR-specific tests for the MuseHub blame page (issue #566).
2
3 Verifies that blame data is rendered server-side — file path, commit SHA,
4 author, and note rows appear in the initial HTML without a client-side fetch.
5
6 Covers:
7 - test_blame_page_renders_file_path_server_side — path present in SSR HTML
8 - test_blame_page_renders_commit_sha_server_side — short SHA rendered for seeded commit
9 - test_blame_page_renders_author_server_side — commit author rendered in HTML
10 - test_blame_page_renders_note_rows_server_side — blame rows rendered for seeded entries
11 - test_blame_page_unknown_repo_returns_404 — unknown owner/slug → 404
12 - test_blame_page_empty_entries_shows_empty_state — no entries → empty state message
13 - test_blame_page_filter_form_preserves_track — track filter pre-selected from query param
14 - test_blame_page_filter_form_is_htmx_capable — filter form has hx-get attribute
15 """
16 from __future__ import annotations
17
18 from datetime import datetime, timezone
19
20 import pytest
21 from httpx import AsyncClient
22 from sqlalchemy.ext.asyncio import AsyncSession
23
24 from musehub.db.musehub_models import MusehubCommit, MusehubRepo
25
26
27 # ---------------------------------------------------------------------------
28 # Fixtures / helpers
29 # ---------------------------------------------------------------------------
30
31
32 async def _seed_repo(
33 db_session: AsyncSession,
34 *,
35 owner: str = "blameuser",
36 slug: str = "blame-beats",
37 ) -> str:
38 """Seed a public repo and return its repo_id string."""
39 repo = MusehubRepo(
40 name=slug,
41 owner=owner,
42 slug=slug,
43 visibility="public",
44 owner_user_id="00000000-0000-0000-0000-000000000099",
45 )
46 db_session.add(repo)
47 await db_session.commit()
48 await db_session.refresh(repo)
49 return str(repo.repo_id)
50
51
52 async def _seed_commit(
53 db_session: AsyncSession,
54 repo_id: str,
55 *,
56 commit_id: str = "deadbeef12345678",
57 message: str = "Add piano intro",
58 author: str = "blameuser",
59 ) -> None:
60 """Seed a commit so blame entries reference a real author and SHA."""
61 commit = MusehubCommit(
62 repo_id=repo_id,
63 commit_id=commit_id,
64 message=message,
65 author=author,
66 branch="main",
67 timestamp=datetime(2024, 6, 1, 12, 0, 0, tzinfo=timezone.utc),
68 )
69 db_session.add(commit)
70 await db_session.commit()
71
72
73 _OWNER = "blameuser"
74 _SLUG = "blame-beats"
75 _REF = "deadbeef12345678"
76 _PATH = "tracks/piano.mid"
77
78
79 # ---------------------------------------------------------------------------
80 # SSR content tests
81 # ---------------------------------------------------------------------------
82
83
84 @pytest.mark.anyio
85 async def test_blame_page_renders_file_path_server_side(
86 client: AsyncClient,
87 db_session: AsyncSession,
88 ) -> None:
89 """The MIDI file path must appear in the server-rendered HTML."""
90 await _seed_repo(db_session)
91 url = f"/{_OWNER}/{_SLUG}/blame/{_REF}/{_PATH}"
92 response = await client.get(url)
93 assert response.status_code == 200
94 assert "text/html" in response.headers["content-type"]
95 body = response.text
96 # Path rendered in header and filter form action (server-rendered, not a JS shell)
97 assert _PATH in body
98 # Blame content div present — table or empty state rendered without loading placeholder
99 assert "blame-header" in body
100 assert "Loading" not in body
101
102
103 @pytest.mark.anyio
104 async def test_blame_page_renders_commit_sha_server_side(
105 client: AsyncClient,
106 db_session: AsyncSession,
107 ) -> None:
108 """Short commit SHA must appear server-side when a commit is seeded."""
109 repo_id = await _seed_repo(db_session)
110 await _seed_commit(db_session, repo_id)
111 url = f"/{_OWNER}/{_SLUG}/blame/{_REF}/{_PATH}"
112 response = await client.get(url)
113 assert response.status_code == 200
114 body = response.text
115 # Short SHA rendered by the `shortsha` Jinja2 filter (first 8 chars)
116 assert _REF[:8] in body
117
118
119 @pytest.mark.anyio
120 async def test_blame_page_renders_author_server_side(
121 client: AsyncClient,
122 db_session: AsyncSession,
123 ) -> None:
124 """Commit author must appear in the SSR blame table when entries are present."""
125 repo_id = await _seed_repo(db_session)
126 await _seed_commit(db_session, repo_id, author="jazzmaster")
127 url = f"/{_OWNER}/{_SLUG}/blame/{_REF}/{_PATH}"
128 response = await client.get(url)
129 assert response.status_code == 200
130 body = response.text
131 # Author rendered in the blame-author cell when entries exist
132 assert "jazzmaster" in body
133
134
135 @pytest.mark.anyio
136 async def test_blame_page_renders_note_rows_server_side(
137 client: AsyncClient,
138 db_session: AsyncSession,
139 ) -> None:
140 """When blame entries are generated, the SSR table contains note data rows."""
141 repo_id = await _seed_repo(db_session)
142 await _seed_commit(db_session, repo_id)
143 url = f"/{_OWNER}/{_SLUG}/blame/{_REF}/{_PATH}"
144 response = await client.get(url)
145 assert response.status_code == 200
146 body = response.text
147 # Blame table structure is server-rendered — no loading spinner
148 assert "blame-table" in body
149 assert "Loading" not in body
150
151
152 @pytest.mark.anyio
153 async def test_blame_page_unknown_repo_returns_404(
154 client: AsyncClient,
155 db_session: AsyncSession,
156 ) -> None:
157 """Unknown owner/slug must return 404 — the repo lookup fails before rendering."""
158 url = f"/nobody/no-such-repo/blame/{_REF}/{_PATH}"
159 response = await client.get(url)
160 assert response.status_code == 404
161
162
163 @pytest.mark.anyio
164 async def test_blame_page_empty_entries_shows_empty_state(
165 client: AsyncClient,
166 db_session: AsyncSession,
167 ) -> None:
168 """A repo with no commits must render the empty-state message (not a table)."""
169 await _seed_repo(db_session)
170 url = f"/{_OWNER}/{_SLUG}/blame/{_REF}/{_PATH}"
171 response = await client.get(url)
172 assert response.status_code == 200
173 body = response.text
174 # No commits → no blame entries → empty state message rendered server-side
175 assert "blame-empty" in body
176
177
178 @pytest.mark.anyio
179 async def test_blame_page_filter_form_preserves_track(
180 client: AsyncClient,
181 db_session: AsyncSession,
182 ) -> None:
183 """Track filter param must pre-select the correct <option> in the SSR form."""
184 await _seed_repo(db_session)
185 url = f"/{_OWNER}/{_SLUG}/blame/{_REF}/{_PATH}?track=piano"
186 response = await client.get(url)
187 assert response.status_code == 200
188 body = response.text
189 # The server renders the select with the matching option selected
190 assert 'value="piano" selected' in body
191
192
193 @pytest.mark.anyio
194 async def test_blame_page_filter_form_is_htmx_capable(
195 client: AsyncClient,
196 db_session: AsyncSession,
197 ) -> None:
198 """Filter form must carry hx-get attribute so HTMX can swap content inline."""
199 await _seed_repo(db_session)
200 url = f"/{_OWNER}/{_SLUG}/blame/{_REF}/{_PATH}"
201 response = await client.get(url)
202 assert response.status_code == 200
203 body = response.text
204 assert "hx-get" in body