gabriel / musehub public
test_musehub_ui_commits_enhanced.py python
403 lines 14.5 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """Regression tests for the enhanced commits list page.
2
3 Covers the four feature areas added to commits_list_page():
4
5 Filter bar
6 - test_commits_enhanced_filter_bar_present — filter-bar HTML element present
7 - test_commits_enhanced_author_dropdown_present — author <select> with 'All authors' default
8 - test_commits_enhanced_date_picker_inputs_present — dateFrom / dateTo date inputs present
9 - test_commits_enhanced_search_input_present — message search <input> present
10 - test_commits_enhanced_tag_filter_input_present — tag filter <input> present
11
12 Server-side filtering
13 - test_commits_enhanced_author_filter_narrows_results — ?author= returns only that author's commits
14 - test_commits_enhanced_author_filter_excludes_others — commits by other authors absent
15 - test_commits_enhanced_search_filter_matches_message — ?q= matches substring in commit message
16 - test_commits_enhanced_search_filter_excludes_others — non-matching commits absent
17 - test_commits_enhanced_date_from_filter — ?dateFrom= excludes older commits
18 - test_commits_enhanced_tag_filter_matches_tag — ?tag=emotion:funky matches message substring
19
20 Compare mode
21 - test_commits_enhanced_compare_toggle_btn_present — compare-toggle-btn button present
22 - test_commits_enhanced_compare_strip_present — compare-strip container present
23 - test_commits_enhanced_compare_check_inputs_present — compare-check checkboxes per row
24 - test_commits_enhanced_compare_js_function — toggleCompareMode() JS function present
25
26 Metadata badges (client-side JS)
27 - test_commits_enhanced_meta_badges_container_present — meta-badges span present per row
28 - test_commits_enhanced_badge_js_extract_function — extractBadges() JS function present
29 - test_commits_enhanced_chip_css_classes_present — chip-tempo / chip-key / chip-emotion CSS defined
30
31 Mini-lane
32 - test_commits_enhanced_dag_merge_arm_present — dag-merge-arm element on merge commits
33 - test_commits_enhanced_mini_lane_dag_col_present — dag-col column present
34
35 Pagination with active filters
36 - test_commits_enhanced_pagination_preserves_filters — page links carry active filter params
37 """
38 from __future__ import annotations
39
40 import uuid
41 from datetime import datetime, timezone
42
43 import pytest
44 from httpx import AsyncClient
45 from sqlalchemy.ext.asyncio import AsyncSession
46
47 from musehub.db.musehub_models import MusehubBranch, MusehubCommit, MusehubRepo
48
49 # ── Constants ─────────────────────────────────────────────────────────────────
50
51 _OWNER = "enhancedowner"
52 _SLUG = "enhanced-commits"
53
54 _SHA_ALICE_1 = "a1" + "0" * 38
55 _SHA_ALICE_2 = "a2" + "0" * 38
56 _SHA_BOB_1 = "b1" + "0" * 38
57 _SHA_MERGE = "cc" + "0" * 38
58
59 # ── Seed helpers ──────────────────────────────────────────────────────────────
60
61
62 async def _seed_repo(db: AsyncSession) -> str:
63 """Seed a public repo with 4 commits from 2 authors and return repo_id."""
64 repo = MusehubRepo(
65 repo_id=str(uuid.uuid4()),
66 name=_SLUG,
67 owner=_OWNER,
68 slug=_SLUG,
69 visibility="public",
70 owner_user_id=str(uuid.uuid4()),
71 )
72 db.add(repo)
73 await db.flush()
74 repo_id = str(repo.repo_id)
75
76 branch = MusehubBranch(repo_id=repo_id, name="main", head_commit_id=_SHA_MERGE)
77 db.add(branch)
78
79 # Alice: two commits with music metadata in messages
80 db.add(MusehubCommit(
81 commit_id=_SHA_ALICE_1,
82 repo_id=repo_id,
83 branch="main",
84 parent_ids=[],
85 message="Add walking bass line 120 BPM Cm emotion:funky",
86 author="alice",
87 timestamp=datetime(2026, 1, 10, tzinfo=timezone.utc),
88 ))
89 db.add(MusehubCommit(
90 commit_id=_SHA_ALICE_2,
91 repo_id=repo_id,
92 branch="main",
93 parent_ids=[_SHA_ALICE_1],
94 message="Refine rhodes chord voicings stage:chorus",
95 author="alice",
96 timestamp=datetime(2026, 2, 15, tzinfo=timezone.utc),
97 ))
98 # Bob: one commit
99 db.add(MusehubCommit(
100 commit_id=_SHA_BOB_1,
101 repo_id=repo_id,
102 branch="main",
103 parent_ids=[_SHA_ALICE_2],
104 message="Add jazz drums groove 90 BPM Gm",
105 author="bob",
106 timestamp=datetime(2026, 3, 1, tzinfo=timezone.utc),
107 ))
108 # Merge commit
109 db.add(MusehubCommit(
110 commit_id=_SHA_MERGE,
111 repo_id=repo_id,
112 branch="main",
113 parent_ids=[_SHA_ALICE_2, _SHA_BOB_1],
114 message="Merge feat/drums into main",
115 author="alice",
116 timestamp=datetime(2026, 3, 2, tzinfo=timezone.utc),
117 ))
118
119 await db.commit()
120 return repo_id
121
122
123 def _url(path: str = "") -> str:
124 return f"/musehub/ui/{_OWNER}/{_SLUG}/commits{path}"
125
126
127 # ── Filter bar HTML ───────────────────────────────────────────────────────────
128
129
130 @pytest.mark.anyio
131 async def test_commits_enhanced_filter_bar_present(
132 client: AsyncClient, db_session: AsyncSession
133 ) -> None:
134 """filter-bar container is rendered on the commits list page."""
135 await _seed_repo(db_session)
136 resp = await client.get(_url())
137 assert resp.status_code == 200
138 assert "filter-bar" in resp.text
139
140
141 @pytest.mark.anyio
142 async def test_commits_enhanced_author_dropdown_present(
143 client: AsyncClient, db_session: AsyncSession
144 ) -> None:
145 """Author <select> dropdown is present and includes 'All authors' option."""
146 await _seed_repo(db_session)
147 resp = await client.get(_url())
148 assert resp.status_code == 200
149 assert "All authors" in resp.text
150 # Both authors appear as options
151 assert "alice" in resp.text
152 assert "bob" in resp.text
153
154
155 @pytest.mark.anyio
156 async def test_commits_enhanced_date_picker_inputs_present(
157 client: AsyncClient, db_session: AsyncSession
158 ) -> None:
159 """dateFrom and dateTo date inputs are present in the filter bar."""
160 await _seed_repo(db_session)
161 resp = await client.get(_url())
162 assert resp.status_code == 200
163 assert 'type="date"' in resp.text
164 assert "dateFrom" in resp.text
165 assert "dateTo" in resp.text
166
167
168 @pytest.mark.anyio
169 async def test_commits_enhanced_search_input_present(
170 client: AsyncClient, db_session: AsyncSession
171 ) -> None:
172 """Full-text message search input is present in the filter bar."""
173 await _seed_repo(db_session)
174 resp = await client.get(_url())
175 assert resp.status_code == 200
176 assert 'name="q"' in resp.text
177 assert "keyword in message" in resp.text
178
179
180 @pytest.mark.anyio
181 async def test_commits_enhanced_tag_filter_input_present(
182 client: AsyncClient, db_session: AsyncSession
183 ) -> None:
184 """Tag filter input is present in the filter bar."""
185 await _seed_repo(db_session)
186 resp = await client.get(_url())
187 assert resp.status_code == 200
188 assert 'name="tag"' in resp.text
189 assert "emotion:" in resp.text # placeholder hint text
190
191
192 # ── Server-side filtering ─────────────────────────────────────────────────────
193
194
195 @pytest.mark.anyio
196 async def test_commits_enhanced_author_filter_narrows_results(
197 client: AsyncClient, db_session: AsyncSession
198 ) -> None:
199 """?author=bob returns only bob's commits."""
200 await _seed_repo(db_session)
201 resp = await client.get(_url() + "?author=bob")
202 assert resp.status_code == 200
203 assert _SHA_BOB_1[:8] in resp.text
204
205
206 @pytest.mark.anyio
207 async def test_commits_enhanced_author_filter_excludes_others(
208 client: AsyncClient, db_session: AsyncSession
209 ) -> None:
210 """Commits by authors other than the filtered author do not appear."""
211 await _seed_repo(db_session)
212 resp = await client.get(_url() + "?author=bob")
213 assert resp.status_code == 200
214 # Alice's commit SHA should not appear
215 assert _SHA_ALICE_1[:8] not in resp.text
216
217
218 @pytest.mark.anyio
219 async def test_commits_enhanced_search_filter_matches_message(
220 client: AsyncClient, db_session: AsyncSession
221 ) -> None:
222 """?q=walking+bass returns the commit containing that substring."""
223 await _seed_repo(db_session)
224 resp = await client.get(_url() + "?q=walking+bass")
225 assert resp.status_code == 200
226 assert _SHA_ALICE_1[:8] in resp.text
227
228
229 @pytest.mark.anyio
230 async def test_commits_enhanced_search_filter_excludes_others(
231 client: AsyncClient, db_session: AsyncSession
232 ) -> None:
233 """?q= excludes commits that do not match the search term."""
234 await _seed_repo(db_session)
235 resp = await client.get(_url() + "?q=walking+bass")
236 assert resp.status_code == 200
237 # Bob's drums commit should not appear
238 assert _SHA_BOB_1[:8] not in resp.text
239
240
241 @pytest.mark.anyio
242 async def test_commits_enhanced_date_from_filter(
243 client: AsyncClient, db_session: AsyncSession
244 ) -> None:
245 """?dateFrom=2026-03-01 excludes commits before that date."""
246 await _seed_repo(db_session)
247 resp = await client.get(_url() + "?dateFrom=2026-03-01")
248 assert resp.status_code == 200
249 # Only Bob (2026-03-01) and merge (2026-03-02) should appear
250 assert _SHA_BOB_1[:8] in resp.text
251 assert _SHA_MERGE[:8] in resp.text
252 # Alice's January commit should not appear
253 assert _SHA_ALICE_1[:8] not in resp.text
254
255
256 @pytest.mark.anyio
257 async def test_commits_enhanced_tag_filter_matches_tag(
258 client: AsyncClient, db_session: AsyncSession
259 ) -> None:
260 """?tag=emotion:funky matches the commit containing that tag string."""
261 await _seed_repo(db_session)
262 resp = await client.get(_url() + "?tag=emotion%3Afunky")
263 assert resp.status_code == 200
264 assert _SHA_ALICE_1[:8] in resp.text
265 # The commit without the tag should not appear
266 assert _SHA_BOB_1[:8] not in resp.text
267
268
269 # ── Compare mode ──────────────────────────────────────────────────────────────
270
271
272 @pytest.mark.anyio
273 async def test_commits_enhanced_compare_toggle_btn_present(
274 client: AsyncClient, db_session: AsyncSession
275 ) -> None:
276 """Compare toggle button is present in the toolbar."""
277 await _seed_repo(db_session)
278 resp = await client.get(_url())
279 assert resp.status_code == 200
280 assert "compare-toggle-btn" in resp.text
281
282
283 @pytest.mark.anyio
284 async def test_commits_enhanced_compare_strip_present(
285 client: AsyncClient, db_session: AsyncSession
286 ) -> None:
287 """compare-strip container is present (initially hidden via CSS/JS)."""
288 await _seed_repo(db_session)
289 resp = await client.get(_url())
290 assert resp.status_code == 200
291 assert "compare-strip" in resp.text
292 assert "compare-link" in resp.text
293
294
295 @pytest.mark.anyio
296 async def test_commits_enhanced_compare_check_inputs_present(
297 client: AsyncClient, db_session: AsyncSession
298 ) -> None:
299 """Per-row compare checkboxes are rendered for each commit."""
300 await _seed_repo(db_session)
301 resp = await client.get(_url())
302 assert resp.status_code == 200
303 assert "compare-check" in resp.text
304 assert "compare-col" in resp.text
305
306
307 @pytest.mark.anyio
308 async def test_commits_enhanced_compare_js_function(
309 client: AsyncClient, db_session: AsyncSession
310 ) -> None:
311 """toggleCompareMode() and onCompareCheck() JS functions are defined."""
312 await _seed_repo(db_session)
313 resp = await client.get(_url())
314 assert resp.status_code == 200
315 assert "toggleCompareMode" in resp.text
316 assert "onCompareCheck" in resp.text
317 assert "updateCompareStrip" in resp.text
318
319
320 # ── Metadata badge JS ─────────────────────────────────────────────────────────
321
322
323 @pytest.mark.anyio
324 async def test_commits_enhanced_meta_badges_container_present(
325 client: AsyncClient, db_session: AsyncSession
326 ) -> None:
327 """meta-badges span is rendered inside each commit row."""
328 await _seed_repo(db_session)
329 resp = await client.get(_url())
330 assert resp.status_code == 200
331 assert "meta-badges" in resp.text
332
333
334 @pytest.mark.anyio
335 async def test_commits_enhanced_badge_js_extract_function(
336 client: AsyncClient, db_session: AsyncSession
337 ) -> None:
338 """extractBadges() and renderBadges() JS functions are defined."""
339 await _seed_repo(db_session)
340 resp = await client.get(_url())
341 assert resp.status_code == 200
342 assert "extractBadges" in resp.text
343 assert "renderBadges" in resp.text
344 assert "TEMPO_RE" in resp.text
345 assert "EMOTION_RE" in resp.text
346
347
348 @pytest.mark.anyio
349 async def test_commits_enhanced_chip_css_classes_present(
350 client: AsyncClient, db_session: AsyncSession
351 ) -> None:
352 """CSS classes for metadata chips are defined in the page <style>."""
353 await _seed_repo(db_session)
354 resp = await client.get(_url())
355 assert resp.status_code == 200
356 assert "chip-tempo" in resp.text
357 assert "chip-key" in resp.text
358 assert "chip-emotion" in resp.text
359 assert "chip-stage" in resp.text
360 assert "chip-instr" in resp.text
361
362
363 # ── Mini-lane DAG ─────────────────────────────────────────────────────────────
364
365
366 @pytest.mark.anyio
367 async def test_commits_enhanced_dag_merge_arm_present(
368 client: AsyncClient, db_session: AsyncSession
369 ) -> None:
370 """dag-merge-arm element is rendered for merge commits."""
371 await _seed_repo(db_session)
372 resp = await client.get(_url())
373 assert resp.status_code == 200
374 assert "dag-merge-arm" in resp.text
375
376
377 @pytest.mark.anyio
378 async def test_commits_enhanced_mini_lane_dag_col_present(
379 client: AsyncClient, db_session: AsyncSession
380 ) -> None:
381 """dag-col column is rendered for every commit row."""
382 await _seed_repo(db_session)
383 resp = await client.get(_url())
384 assert resp.status_code == 200
385 assert "dag-col" in resp.text
386 assert "dag-node" in resp.text
387
388
389 # ── Pagination preserves filters ──────────────────────────────────────────────
390
391
392 @pytest.mark.anyio
393 async def test_commits_enhanced_pagination_preserves_filters(
394 client: AsyncClient, db_session: AsyncSession
395 ) -> None:
396 """Pagination links forward active filter params so state persists across pages."""
397 await _seed_repo(db_session)
398 resp = await client.get(_url() + "?author=alice&per_page=1&page=1")
399 assert resp.status_code == 200
400 body = resp.text
401 # Older link should carry author=alice
402 assert "author=alice" in body
403 assert "page=2" in body