gabriel / musehub public
test_musehub_ui_issue_list_enhanced.py python
823 lines 24.7 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """Tests for the SSR issue list page — reference HTMX implementation (issue #555).
2
3 Covers server-side rendering, HTMX fragment responses, filters, tabs, and
4 pagination. All assertions target Jinja2-rendered content in the HTML
5 response body, not JavaScript function definitions.
6
7 Test areas:
8 Basic rendering
9 - test_issue_list_page_returns_200
10 - test_issue_list_no_auth_required
11 - test_issue_list_unknown_repo_404
12
13 SSR content — issue data rendered on server
14 - test_issue_list_renders_issue_title_server_side
15 - test_issue_list_filter_form_has_hx_get
16 - test_issue_list_filter_form_has_hx_target
17
18 Open/closed tab counts
19 - test_issue_list_tab_open_has_hx_get
20 - test_issue_list_open_closed_counts_in_tabs
21
22 State filter
23 - test_issue_list_state_filter_closed_shows_closed_only
24
25 Label filter
26 - test_issue_list_label_filter_narrows_issues
27
28 HTMX fragment
29 - test_issue_list_htmx_request_returns_fragment
30 - test_issue_list_fragment_contains_issue_title
31 - test_issue_list_fragment_empty_state_when_no_issues
32
33 Pagination
34 - test_issue_list_pagination_renders_next_link
35
36 Right sidebar
37 - test_issue_list_milestone_progress_in_right_sidebar
38 - test_issue_list_right_sidebar_present
39 - test_issue_list_milestone_progress_heading_present
40 - test_issue_list_milestone_progress_bar_css_present
41 - test_issue_list_milestone_progress_list_present
42 - test_issue_list_labels_summary_heading_present
43 - test_issue_list_labels_summary_list_present
44
45 Filter sidebar
46 - test_issue_list_filter_sidebar_present
47 - test_issue_list_label_chip_container_present
48 - test_issue_list_filter_milestone_select_present
49 - test_issue_list_filter_assignee_select_present
50 - test_issue_list_filter_author_input_present
51 - test_issue_list_sort_radio_group_present
52 - test_issue_list_sort_radio_buttons_present
53
54 Template selector / new-issue flow (minimal JS)
55 - test_issue_list_template_picker_present
56 - test_issue_list_template_grid_present
57 - test_issue_list_template_cards_present
58 - test_issue_list_show_template_picker_js_present
59 - test_issue_list_select_template_js_present
60 - test_issue_list_issue_templates_const_present
61 - test_issue_list_new_issue_btn_calls_template
62 - test_issue_list_templates_back_btn_present
63 - test_issue_list_blank_template_defined
64 - test_issue_list_bug_template_defined
65
66 Bulk toolbar structure
67 - test_issue_list_bulk_toolbar_present
68 - test_issue_list_bulk_count_present
69 - test_issue_list_bulk_label_select_present
70 - test_issue_list_bulk_milestone_select_present
71 - test_issue_list_issue_row_checkbox_present
72 - test_issue_list_toggle_issue_select_js_present
73 - test_issue_list_deselect_all_js_present
74 - test_issue_list_update_bulk_toolbar_js_present
75 - test_issue_list_bulk_close_js_present
76 - test_issue_list_bulk_reopen_js_present
77 - test_issue_list_bulk_assign_label_js_present
78 - test_issue_list_bulk_assign_milestone_js_present
79 """
80 from __future__ import annotations
81
82 import pytest
83 from httpx import AsyncClient
84 from sqlalchemy.ext.asyncio import AsyncSession
85
86 from musehub.db.musehub_models import MusehubIssue, MusehubMilestone, MusehubRepo
87
88
89 # ---------------------------------------------------------------------------
90 # Helpers
91 # ---------------------------------------------------------------------------
92
93
94 async def _make_repo(
95 db: AsyncSession,
96 owner: str = "beatmaker",
97 slug: str = "grooves",
98 ) -> str:
99 """Seed a public repo and return its repo_id string."""
100 repo = MusehubRepo(
101 name=slug,
102 owner=owner,
103 slug=slug,
104 visibility="public",
105 owner_user_id="uid-beatmaker",
106 )
107 db.add(repo)
108 await db.commit()
109 await db.refresh(repo)
110 return str(repo.repo_id)
111
112
113 async def _make_issue(
114 db: AsyncSession,
115 repo_id: str,
116 *,
117 number: int = 1,
118 title: str = "Bass too loud",
119 state: str = "open",
120 labels: list[str] | None = None,
121 author: str = "beatmaker",
122 milestone_id: str | None = None,
123 ) -> MusehubIssue:
124 """Seed an issue and return it."""
125 issue = MusehubIssue(
126 repo_id=repo_id,
127 number=number,
128 title=title,
129 body="Issue body.",
130 state=state,
131 labels=labels or [],
132 author=author,
133 milestone_id=milestone_id,
134 )
135 db.add(issue)
136 await db.commit()
137 await db.refresh(issue)
138 return issue
139
140
141 async def _make_milestone(
142 db: AsyncSession,
143 repo_id: str,
144 *,
145 number: int = 1,
146 title: str = "v1.0",
147 state: str = "open",
148 ) -> MusehubMilestone:
149 """Seed a milestone and return it."""
150 ms = MusehubMilestone(
151 repo_id=repo_id,
152 number=number,
153 title=title,
154 description="Milestone description.",
155 state=state,
156 author="beatmaker",
157 )
158 db.add(ms)
159 await db.commit()
160 await db.refresh(ms)
161 return ms
162
163
164 async def _get_page(
165 client: AsyncClient,
166 owner: str = "beatmaker",
167 slug: str = "grooves",
168 **params: str,
169 ) -> str:
170 """Fetch the issue list page and return its text body."""
171 resp = await client.get(f"/musehub/ui/{owner}/{slug}/issues", params=params)
172 assert resp.status_code == 200
173 return resp.text
174
175
176 # ---------------------------------------------------------------------------
177 # Basic page rendering
178 # ---------------------------------------------------------------------------
179
180
181 @pytest.mark.anyio
182 async def test_issue_list_page_returns_200(
183 client: AsyncClient,
184 db_session: AsyncSession,
185 ) -> None:
186 """GET /musehub/ui/{owner}/{slug}/issues returns 200 HTML."""
187 await _make_repo(db_session)
188 response = await client.get("/musehub/ui/beatmaker/grooves/issues")
189 assert response.status_code == 200
190 assert "text/html" in response.headers["content-type"]
191
192
193 @pytest.mark.anyio
194 async def test_issue_list_no_auth_required(
195 client: AsyncClient,
196 db_session: AsyncSession,
197 ) -> None:
198 """Issue list page renders without a JWT token."""
199 await _make_repo(db_session)
200 response = await client.get("/musehub/ui/beatmaker/grooves/issues")
201 assert response.status_code == 200
202
203
204 @pytest.mark.anyio
205 async def test_issue_list_unknown_repo_404(
206 client: AsyncClient,
207 db_session: AsyncSession,
208 ) -> None:
209 """Unknown owner/slug returns 404."""
210 response = await client.get("/musehub/ui/nobody/norepo/issues")
211 assert response.status_code == 404
212
213
214 # ---------------------------------------------------------------------------
215 # SSR content — issue data is rendered server-side
216 # ---------------------------------------------------------------------------
217
218
219 @pytest.mark.anyio
220 async def test_issue_list_renders_issue_title_server_side(
221 client: AsyncClient,
222 db_session: AsyncSession,
223 ) -> None:
224 """Seeded issue title appears in SSR HTML without JS execution."""
225 repo_id = await _make_repo(db_session)
226 await _make_issue(db_session, repo_id, title="Kick drum too punchy")
227 body = await _get_page(client)
228 assert "Kick drum too punchy" in body
229
230
231 @pytest.mark.anyio
232 async def test_issue_list_filter_form_has_hx_get(
233 client: AsyncClient,
234 db_session: AsyncSession,
235 ) -> None:
236 """Filter form carries hx-get attribute for HTMX partial updates."""
237 await _make_repo(db_session)
238 body = await _get_page(client)
239 assert "hx-get" in body
240
241
242 @pytest.mark.anyio
243 async def test_issue_list_filter_form_has_hx_target(
244 client: AsyncClient,
245 db_session: AsyncSession,
246 ) -> None:
247 """Filter form targets #issue-rows for HTMX swaps."""
248 await _make_repo(db_session)
249 body = await _get_page(client)
250 assert 'hx-target="#issue-rows"' in body or "hx-target='#issue-rows'" in body
251
252
253 # ---------------------------------------------------------------------------
254 # Open/closed tab counts
255 # ---------------------------------------------------------------------------
256
257
258 @pytest.mark.anyio
259 async def test_issue_list_tab_open_has_hx_get(
260 client: AsyncClient,
261 db_session: AsyncSession,
262 ) -> None:
263 """Open tab link carries hx-get for HTMX navigation."""
264 await _make_repo(db_session)
265 body = await _get_page(client)
266 assert "tab-open" in body
267 assert "hx-get" in body
268
269
270 @pytest.mark.anyio
271 async def test_issue_list_open_closed_counts_in_tabs(
272 client: AsyncClient,
273 db_session: AsyncSession,
274 ) -> None:
275 """Tab badges reflect the actual open and closed issue counts from the DB."""
276 repo_id = await _make_repo(db_session)
277 for i in range(3):
278 await _make_issue(db_session, repo_id, number=i + 1, state="open")
279 for i in range(2):
280 await _make_issue(db_session, repo_id, number=i + 4, state="closed")
281 body = await _get_page(client)
282 assert ">3<" in body or ">3 <" in body or "3</span>" in body
283 assert ">2<" in body or ">2 <" in body or "2</span>" in body
284
285
286 # ---------------------------------------------------------------------------
287 # State filter
288 # ---------------------------------------------------------------------------
289
290
291 @pytest.mark.anyio
292 async def test_issue_list_state_filter_closed_shows_closed_only(
293 client: AsyncClient,
294 db_session: AsyncSession,
295 ) -> None:
296 """?state=closed returns only closed issues in the rendered HTML."""
297 repo_id = await _make_repo(db_session)
298 await _make_issue(db_session, repo_id, number=1, title="Open issue", state="open")
299 await _make_issue(db_session, repo_id, number=2, title="Closed issue", state="closed")
300 body = await _get_page(client, state="closed")
301 assert "Closed issue" in body
302 assert "Open issue" not in body
303
304
305 # ---------------------------------------------------------------------------
306 # Label filter
307 # ---------------------------------------------------------------------------
308
309
310 @pytest.mark.anyio
311 async def test_issue_list_label_filter_narrows_issues(
312 client: AsyncClient,
313 db_session: AsyncSession,
314 ) -> None:
315 """?label=bug returns only issues labelled 'bug'."""
316 repo_id = await _make_repo(db_session)
317 await _make_issue(db_session, repo_id, number=1, title="Bug: kick too loud", labels=["bug"])
318 await _make_issue(db_session, repo_id, number=2, title="Feature: add reverb", labels=["feature"])
319 body = await _get_page(client, label="bug")
320 assert "Bug: kick too loud" in body
321 assert "Feature: add reverb" not in body
322
323
324 # ---------------------------------------------------------------------------
325 # HTMX fragment
326 # ---------------------------------------------------------------------------
327
328
329 @pytest.mark.anyio
330 async def test_issue_list_htmx_request_returns_fragment(
331 client: AsyncClient,
332 db_session: AsyncSession,
333 ) -> None:
334 """HX-Request: true returns a bare fragment — no <html> wrapper."""
335 await _make_repo(db_session)
336 resp = await client.get(
337 "/musehub/ui/beatmaker/grooves/issues",
338 headers={"HX-Request": "true"},
339 )
340 assert resp.status_code == 200
341 assert "<html" not in resp.text
342
343
344 @pytest.mark.anyio
345 async def test_issue_list_fragment_contains_issue_title(
346 client: AsyncClient,
347 db_session: AsyncSession,
348 ) -> None:
349 """HTMX fragment contains the seeded issue title."""
350 repo_id = await _make_repo(db_session)
351 await _make_issue(db_session, repo_id, title="Synth pad too bright")
352 resp = await client.get(
353 "/musehub/ui/beatmaker/grooves/issues",
354 headers={"HX-Request": "true"},
355 )
356 assert resp.status_code == 200
357 assert "Synth pad too bright" in resp.text
358
359
360 @pytest.mark.anyio
361 async def test_issue_list_fragment_empty_state_when_no_issues(
362 client: AsyncClient,
363 db_session: AsyncSession,
364 ) -> None:
365 """Fragment returns an empty-state message when no issues match filters."""
366 repo_id = await _make_repo(db_session)
367 await _make_issue(db_session, repo_id, number=1, title="Open issue", state="open")
368 resp = await client.get(
369 "/musehub/ui/beatmaker/grooves/issues",
370 params={"state": "closed"},
371 headers={"HX-Request": "true"},
372 )
373 assert resp.status_code == 200
374 assert "No issues" in resp.text or "no issues" in resp.text.lower()
375
376
377 # ---------------------------------------------------------------------------
378 # Pagination
379 # ---------------------------------------------------------------------------
380
381
382 @pytest.mark.anyio
383 async def test_issue_list_pagination_renders_next_link(
384 client: AsyncClient,
385 db_session: AsyncSession,
386 ) -> None:
387 """When total issues exceed per_page, a Next pagination link appears."""
388 repo_id = await _make_repo(db_session)
389 for i in range(30):
390 await _make_issue(db_session, repo_id, number=i + 1, state="open")
391 body = await _get_page(client, per_page="25")
392 assert "Next" in body or "next" in body.lower()
393
394
395 # ---------------------------------------------------------------------------
396 # Right sidebar
397 # ---------------------------------------------------------------------------
398
399
400 @pytest.mark.anyio
401 async def test_issue_list_milestone_progress_in_right_sidebar(
402 client: AsyncClient,
403 db_session: AsyncSession,
404 ) -> None:
405 """Seeded milestone title appears in the right sidebar progress section."""
406 repo_id = await _make_repo(db_session)
407 await _make_milestone(db_session, repo_id, title="Album Release v1")
408 body = await _get_page(client)
409 assert "Album Release v1" in body
410
411
412 @pytest.mark.anyio
413 async def test_issue_list_right_sidebar_present(
414 client: AsyncClient,
415 db_session: AsyncSession,
416 ) -> None:
417 """sidebar-right element is present in the SSR page."""
418 await _make_repo(db_session)
419 body = await _get_page(client)
420 assert "sidebar-right" in body
421
422
423 @pytest.mark.anyio
424 async def test_issue_list_milestone_progress_heading_present(
425 client: AsyncClient,
426 db_session: AsyncSession,
427 ) -> None:
428 """milestone-progress-heading id is rendered server-side."""
429 await _make_repo(db_session)
430 body = await _get_page(client)
431 assert "milestone-progress-heading" in body
432
433
434 @pytest.mark.anyio
435 async def test_issue_list_milestone_progress_bar_css_present(
436 client: AsyncClient,
437 db_session: AsyncSession,
438 ) -> None:
439 """milestone-progress-bar-fill CSS class is defined in the page."""
440 await _make_repo(db_session)
441 body = await _get_page(client)
442 assert "milestone-progress-bar-fill" in body
443
444
445 @pytest.mark.anyio
446 async def test_issue_list_milestone_progress_list_present(
447 client: AsyncClient,
448 db_session: AsyncSession,
449 ) -> None:
450 """milestone-progress-list element id is present in the page."""
451 await _make_repo(db_session)
452 body = await _get_page(client)
453 assert "milestone-progress-list" in body
454
455
456 @pytest.mark.anyio
457 async def test_issue_list_labels_summary_heading_present(
458 client: AsyncClient,
459 db_session: AsyncSession,
460 ) -> None:
461 """labels-summary-heading id is rendered server-side in the right sidebar."""
462 await _make_repo(db_session)
463 body = await _get_page(client)
464 assert "labels-summary-heading" in body
465
466
467 @pytest.mark.anyio
468 async def test_issue_list_labels_summary_list_present(
469 client: AsyncClient,
470 db_session: AsyncSession,
471 ) -> None:
472 """labels-summary-list id is rendered server-side in the right sidebar."""
473 await _make_repo(db_session)
474 body = await _get_page(client)
475 assert "labels-summary-list" in body
476
477
478 # ---------------------------------------------------------------------------
479 # Filter sidebar elements
480 # ---------------------------------------------------------------------------
481
482
483 @pytest.mark.anyio
484 async def test_issue_list_filter_sidebar_present(
485 client: AsyncClient,
486 db_session: AsyncSession,
487 ) -> None:
488 """filter-sidebar id is rendered server-side."""
489 await _make_repo(db_session)
490 body = await _get_page(client)
491 assert "filter-sidebar" in body
492
493
494 @pytest.mark.anyio
495 async def test_issue_list_label_chip_container_present(
496 client: AsyncClient,
497 db_session: AsyncSession,
498 ) -> None:
499 """label-chip-container id is present in the filter sidebar."""
500 await _make_repo(db_session)
501 body = await _get_page(client)
502 assert "label-chip-container" in body
503
504
505 @pytest.mark.anyio
506 async def test_issue_list_filter_milestone_select_present(
507 client: AsyncClient,
508 db_session: AsyncSession,
509 ) -> None:
510 """filter-milestone <select> element is present."""
511 await _make_repo(db_session)
512 body = await _get_page(client)
513 assert "filter-milestone" in body
514
515
516 @pytest.mark.anyio
517 async def test_issue_list_filter_assignee_select_present(
518 client: AsyncClient,
519 db_session: AsyncSession,
520 ) -> None:
521 """filter-assignee <select> element is present."""
522 await _make_repo(db_session)
523 body = await _get_page(client)
524 assert "filter-assignee" in body
525
526
527 @pytest.mark.anyio
528 async def test_issue_list_filter_author_input_present(
529 client: AsyncClient,
530 db_session: AsyncSession,
531 ) -> None:
532 """filter-author text input is present."""
533 await _make_repo(db_session)
534 body = await _get_page(client)
535 assert "filter-author" in body
536
537
538 @pytest.mark.anyio
539 async def test_issue_list_sort_radio_group_present(
540 client: AsyncClient,
541 db_session: AsyncSession,
542 ) -> None:
543 """sort-radio-group element is present in the filter sidebar."""
544 await _make_repo(db_session)
545 body = await _get_page(client)
546 assert "sort-radio-group" in body
547
548
549 @pytest.mark.anyio
550 async def test_issue_list_sort_radio_buttons_present(
551 client: AsyncClient,
552 db_session: AsyncSession,
553 ) -> None:
554 """Radio inputs with name='sort' are present (SSR-rendered)."""
555 await _make_repo(db_session)
556 body = await _get_page(client)
557 assert 'name="sort"' in body or "name='sort'" in body
558
559
560 # ---------------------------------------------------------------------------
561 # Template selector / new-issue flow (minimal JS retained)
562 # ---------------------------------------------------------------------------
563
564
565 @pytest.mark.anyio
566 async def test_issue_list_template_picker_present(
567 client: AsyncClient,
568 db_session: AsyncSession,
569 ) -> None:
570 """template-picker element is present in the page HTML."""
571 await _make_repo(db_session)
572 body = await _get_page(client)
573 assert "template-picker" in body
574
575
576 @pytest.mark.anyio
577 async def test_issue_list_template_grid_present(
578 client: AsyncClient,
579 db_session: AsyncSession,
580 ) -> None:
581 """template-grid element is rendered server-side."""
582 await _make_repo(db_session)
583 body = await _get_page(client)
584 assert "template-grid" in body
585
586
587 @pytest.mark.anyio
588 async def test_issue_list_template_cards_present(
589 client: AsyncClient,
590 db_session: AsyncSession,
591 ) -> None:
592 """template-card class is present (SSR-rendered template cards)."""
593 await _make_repo(db_session)
594 body = await _get_page(client)
595 assert "template-card" in body
596
597
598 @pytest.mark.anyio
599 async def test_issue_list_show_template_picker_js_present(
600 client: AsyncClient,
601 db_session: AsyncSession,
602 ) -> None:
603 """showTemplatePicker() JS function is present in the page."""
604 await _make_repo(db_session)
605 body = await _get_page(client)
606 assert "showTemplatePicker" in body
607
608
609 @pytest.mark.anyio
610 async def test_issue_list_select_template_js_present(
611 client: AsyncClient,
612 db_session: AsyncSession,
613 ) -> None:
614 """selectTemplate() JS function is present in the page."""
615 await _make_repo(db_session)
616 body = await _get_page(client)
617 assert "selectTemplate" in body
618
619
620 @pytest.mark.anyio
621 async def test_issue_list_issue_templates_const_present(
622 client: AsyncClient,
623 db_session: AsyncSession,
624 ) -> None:
625 """ISSUE_TEMPLATES constant is present in the page JS."""
626 await _make_repo(db_session)
627 body = await _get_page(client)
628 assert "ISSUE_TEMPLATES" in body
629
630
631 @pytest.mark.anyio
632 async def test_issue_list_new_issue_btn_calls_template(
633 client: AsyncClient,
634 db_session: AsyncSession,
635 ) -> None:
636 """new-issue-btn invokes showTemplatePicker."""
637 await _make_repo(db_session)
638 body = await _get_page(client)
639 assert "new-issue-btn" in body
640 assert "showTemplatePicker" in body
641
642
643 @pytest.mark.anyio
644 async def test_issue_list_templates_back_btn_present(
645 client: AsyncClient,
646 db_session: AsyncSession,
647 ) -> None:
648 """← Templates back navigation is present in the new issue flow."""
649 await _make_repo(db_session)
650 body = await _get_page(client)
651 assert "Templates" in body
652
653
654 @pytest.mark.anyio
655 async def test_issue_list_blank_template_defined(
656 client: AsyncClient,
657 db_session: AsyncSession,
658 ) -> None:
659 """'blank' template id is present in ISSUE_TEMPLATES."""
660 await _make_repo(db_session)
661 body = await _get_page(client)
662 assert "'blank'" in body or '"blank"' in body
663
664
665 @pytest.mark.anyio
666 async def test_issue_list_bug_template_defined(
667 client: AsyncClient,
668 db_session: AsyncSession,
669 ) -> None:
670 """'bug' template id is present in ISSUE_TEMPLATES."""
671 await _make_repo(db_session)
672 body = await _get_page(client)
673 assert "'bug'" in body or '"bug"' in body
674
675
676 # ---------------------------------------------------------------------------
677 # Bulk toolbar structure (SSR-rendered, JS-activated)
678 # ---------------------------------------------------------------------------
679
680
681 @pytest.mark.anyio
682 async def test_issue_list_bulk_toolbar_present(
683 client: AsyncClient,
684 db_session: AsyncSession,
685 ) -> None:
686 """bulk-toolbar element is rendered in the page HTML."""
687 await _make_repo(db_session)
688 body = await _get_page(client)
689 assert "bulk-toolbar" in body
690
691
692 @pytest.mark.anyio
693 async def test_issue_list_bulk_count_present(
694 client: AsyncClient,
695 db_session: AsyncSession,
696 ) -> None:
697 """bulk-count element is present."""
698 await _make_repo(db_session)
699 body = await _get_page(client)
700 assert "bulk-count" in body
701
702
703 @pytest.mark.anyio
704 async def test_issue_list_bulk_label_select_present(
705 client: AsyncClient,
706 db_session: AsyncSession,
707 ) -> None:
708 """bulk-label-select element is present."""
709 await _make_repo(db_session)
710 body = await _get_page(client)
711 assert "bulk-label-select" in body
712
713
714 @pytest.mark.anyio
715 async def test_issue_list_bulk_milestone_select_present(
716 client: AsyncClient,
717 db_session: AsyncSession,
718 ) -> None:
719 """bulk-milestone-select element is present."""
720 await _make_repo(db_session)
721 body = await _get_page(client)
722 assert "bulk-milestone-select" in body
723
724
725 @pytest.mark.anyio
726 async def test_issue_list_issue_row_checkbox_present(
727 client: AsyncClient,
728 db_session: AsyncSession,
729 ) -> None:
730 """issue-row-check CSS class is present (checkbox for bulk selection)."""
731 repo_id = await _make_repo(db_session)
732 await _make_issue(db_session, repo_id, title="Has checkbox")
733 body = await _get_page(client)
734 assert "issue-row-check" in body
735
736
737 @pytest.mark.anyio
738 async def test_issue_list_toggle_issue_select_js_present(
739 client: AsyncClient,
740 db_session: AsyncSession,
741 ) -> None:
742 """toggleIssueSelect() JS function is present in the page."""
743 await _make_repo(db_session)
744 body = await _get_page(client)
745 assert "toggleIssueSelect" in body
746
747
748 @pytest.mark.anyio
749 async def test_issue_list_deselect_all_js_present(
750 client: AsyncClient,
751 db_session: AsyncSession,
752 ) -> None:
753 """deselectAll() JS function is present in the page."""
754 await _make_repo(db_session)
755 body = await _get_page(client)
756 assert "deselectAll" in body
757
758
759 @pytest.mark.anyio
760 async def test_issue_list_update_bulk_toolbar_js_present(
761 client: AsyncClient,
762 db_session: AsyncSession,
763 ) -> None:
764 """updateBulkToolbar() JS function is present in the page."""
765 await _make_repo(db_session)
766 body = await _get_page(client)
767 assert "updateBulkToolbar" in body
768
769
770 @pytest.mark.anyio
771 async def test_issue_list_bulk_close_js_present(
772 client: AsyncClient,
773 db_session: AsyncSession,
774 ) -> None:
775 """bulkClose() JS stub is present in the page."""
776 await _make_repo(db_session)
777 body = await _get_page(client)
778 assert "bulkClose" in body
779
780
781 @pytest.mark.anyio
782 async def test_issue_list_bulk_reopen_js_present(
783 client: AsyncClient,
784 db_session: AsyncSession,
785 ) -> None:
786 """bulkReopen() JS stub is present in the page."""
787 await _make_repo(db_session)
788 body = await _get_page(client)
789 assert "bulkReopen" in body
790
791
792 @pytest.mark.anyio
793 async def test_issue_list_bulk_assign_label_js_present(
794 client: AsyncClient,
795 db_session: AsyncSession,
796 ) -> None:
797 """bulkAssignLabel() JS stub is present in the page."""
798 await _make_repo(db_session)
799 body = await _get_page(client)
800 assert "bulkAssignLabel" in body
801
802
803 @pytest.mark.anyio
804 async def test_issue_list_bulk_assign_milestone_js_present(
805 client: AsyncClient,
806 db_session: AsyncSession,
807 ) -> None:
808 """bulkAssignMilestone() JS stub is present in the page."""
809 await _make_repo(db_session)
810 body = await _get_page(client)
811 assert "bulkAssignMilestone" in body
812
813
814 @pytest.mark.anyio
815 async def test_issue_list_full_page_contains_html_wrapper(
816 client: AsyncClient,
817 db_session: AsyncSession,
818 ) -> None:
819 """Direct browser navigation (no HX-Request) returns a full HTML page with <html> tag."""
820 await _make_repo(db_session)
821 resp = await client.get("/musehub/ui/beatmaker/grooves/issues")
822 assert resp.status_code == 200
823 assert "<html" in resp.text