gabriel / musehub public
test_musehub_ui_issue_list_enhanced.py python
827 lines 24.9 KB
04faf0e3 feat: supercharge all repo pages, enforce separation of concerns Gabriel Cardona <cgcardona@gmail.com> 5d 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"/{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 /{owner}/{slug}/issues returns 200 HTML."""
187 await _make_repo(db_session)
188 response = await client.get("/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("/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("/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 "state=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="UniqueOpenTitle", state="open")
299 await _make_issue(db_session, repo_id, number=2, title="UniqueClosedTitle", state="closed")
300 body = await _get_page(client, state="closed")
301 assert "UniqueClosedTitle" in body
302 assert "UniqueOpenTitle" 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 "/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 "/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 "/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 """Milestones sidebar section is rendered server-side."""
429 await _make_repo(db_session)
430 body = await _get_page(client)
431 assert "Milestones" 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 in app.css; page renders milestone sidebar."""
440 await _make_repo(db_session)
441 body = await _get_page(client)
442 assert "Milestones" 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 """Milestones sidebar section contains milestone progress bars."""
451 await _make_repo(db_session)
452 body = await _get_page(client)
453 assert "Milestones" 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 sidebar section is rendered server-side."""
462 await _make_repo(db_session)
463 body = await _get_page(client)
464 assert "Labels" 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 sidebar section contains a label list."""
473 await _make_repo(db_session)
474 body = await _get_page(client)
475 assert "Labels" 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 is in app.js (TypeScript module); page dispatches issue-list module."""
626 await _make_repo(db_session)
627 body = await _get_page(client)
628 # ISSUE_TEMPLATES moved to app.js; verify page dispatch JSON and template picker HTML
629 assert '"page": "issue-list"' in body
630 assert "template-picker" in body
631
632
633 @pytest.mark.anyio
634 async def test_issue_list_new_issue_btn_calls_template(
635 client: AsyncClient,
636 db_session: AsyncSession,
637 ) -> None:
638 """New Issue button invokes showTemplatePicker."""
639 await _make_repo(db_session)
640 body = await _get_page(client)
641 assert "New Issue" in body
642 assert "showTemplatePicker" in body
643
644
645 @pytest.mark.anyio
646 async def test_issue_list_templates_back_btn_present(
647 client: AsyncClient,
648 db_session: AsyncSession,
649 ) -> None:
650 """Template picker is rendered in the new issue flow."""
651 await _make_repo(db_session)
652 body = await _get_page(client)
653 assert "template-picker" in body
654
655
656 @pytest.mark.anyio
657 async def test_issue_list_blank_template_defined(
658 client: AsyncClient,
659 db_session: AsyncSession,
660 ) -> None:
661 """'blank' template id is present in ISSUE_TEMPLATES."""
662 await _make_repo(db_session)
663 body = await _get_page(client)
664 assert "'blank'" in body or '"blank"' in body
665
666
667 @pytest.mark.anyio
668 async def test_issue_list_bug_template_defined(
669 client: AsyncClient,
670 db_session: AsyncSession,
671 ) -> None:
672 """'bug' template id is present in ISSUE_TEMPLATES."""
673 await _make_repo(db_session)
674 body = await _get_page(client)
675 assert "'bug'" in body or '"bug"' in body
676
677
678 # ---------------------------------------------------------------------------
679 # Bulk toolbar structure (SSR-rendered, JS-activated)
680 # ---------------------------------------------------------------------------
681
682
683 @pytest.mark.anyio
684 async def test_issue_list_bulk_toolbar_present(
685 client: AsyncClient,
686 db_session: AsyncSession,
687 ) -> None:
688 """bulk-toolbar element is rendered in the page HTML."""
689 await _make_repo(db_session)
690 body = await _get_page(client)
691 assert "bulk-toolbar" in body
692
693
694 @pytest.mark.anyio
695 async def test_issue_list_bulk_count_present(
696 client: AsyncClient,
697 db_session: AsyncSession,
698 ) -> None:
699 """bulk-count element is present."""
700 await _make_repo(db_session)
701 body = await _get_page(client)
702 assert "bulk-count" in body
703
704
705 @pytest.mark.anyio
706 async def test_issue_list_bulk_label_select_present(
707 client: AsyncClient,
708 db_session: AsyncSession,
709 ) -> None:
710 """bulk-label-select element is present."""
711 await _make_repo(db_session)
712 body = await _get_page(client)
713 assert "bulk-label-select" in body
714
715
716 @pytest.mark.anyio
717 async def test_issue_list_bulk_milestone_select_present(
718 client: AsyncClient,
719 db_session: AsyncSession,
720 ) -> None:
721 """bulk-milestone-select element is present."""
722 await _make_repo(db_session)
723 body = await _get_page(client)
724 assert "bulk-milestone-select" in body
725
726
727 @pytest.mark.anyio
728 async def test_issue_list_issue_row_checkbox_present(
729 client: AsyncClient,
730 db_session: AsyncSession,
731 ) -> None:
732 """issue-row-check CSS class is present (checkbox for bulk selection)."""
733 repo_id = await _make_repo(db_session)
734 await _make_issue(db_session, repo_id, title="Has checkbox")
735 body = await _get_page(client)
736 assert "issue-row-check" in body
737
738
739 @pytest.mark.anyio
740 async def test_issue_list_toggle_issue_select_js_present(
741 client: AsyncClient,
742 db_session: AsyncSession,
743 ) -> None:
744 """toggleIssueSelect() is in app.js (TypeScript module); page renders bulk toolbar."""
745 await _make_repo(db_session)
746 body = await _get_page(client)
747 # Function moved to app.js; verify bulk toolbar HTML element is present
748 assert "bulk-toolbar" in body
749
750
751 @pytest.mark.anyio
752 async def test_issue_list_deselect_all_js_present(
753 client: AsyncClient,
754 db_session: AsyncSession,
755 ) -> None:
756 """deselectAll() JS function is present in the page."""
757 await _make_repo(db_session)
758 body = await _get_page(client)
759 assert "deselectAll" in body
760
761
762 @pytest.mark.anyio
763 async def test_issue_list_update_bulk_toolbar_js_present(
764 client: AsyncClient,
765 db_session: AsyncSession,
766 ) -> None:
767 """updateBulkToolbar() is in app.js (TypeScript module); page renders bulk action buttons."""
768 await _make_repo(db_session)
769 body = await _get_page(client)
770 # Function moved to app.js; verify bulk action buttons are in the HTML
771 assert "bulk-action-btn" in body
772
773
774 @pytest.mark.anyio
775 async def test_issue_list_bulk_close_js_present(
776 client: AsyncClient,
777 db_session: AsyncSession,
778 ) -> None:
779 """bulkClose() JS stub is present in the page."""
780 await _make_repo(db_session)
781 body = await _get_page(client)
782 assert "bulkClose" in body
783
784
785 @pytest.mark.anyio
786 async def test_issue_list_bulk_reopen_js_present(
787 client: AsyncClient,
788 db_session: AsyncSession,
789 ) -> None:
790 """bulkReopen() JS stub is present in the page."""
791 await _make_repo(db_session)
792 body = await _get_page(client)
793 assert "bulkReopen" in body
794
795
796 @pytest.mark.anyio
797 async def test_issue_list_bulk_assign_label_js_present(
798 client: AsyncClient,
799 db_session: AsyncSession,
800 ) -> None:
801 """bulkAssignLabel() JS stub is present in the page."""
802 await _make_repo(db_session)
803 body = await _get_page(client)
804 assert "bulkAssignLabel" in body
805
806
807 @pytest.mark.anyio
808 async def test_issue_list_bulk_assign_milestone_js_present(
809 client: AsyncClient,
810 db_session: AsyncSession,
811 ) -> None:
812 """bulkAssignMilestone() JS stub is present in the page."""
813 await _make_repo(db_session)
814 body = await _get_page(client)
815 assert "bulkAssignMilestone" in body
816
817
818 @pytest.mark.anyio
819 async def test_issue_list_full_page_contains_html_wrapper(
820 client: AsyncClient,
821 db_session: AsyncSession,
822 ) -> None:
823 """Direct browser navigation (no HX-Request) returns a full HTML page with <html> tag."""
824 await _make_repo(db_session)
825 resp = await client.get("/beatmaker/grooves/issues")
826 assert resp.status_code == 200
827 assert "<html" in resp.text