gabriel / musehub public
test_musehub_ui_issue_list_enhanced.py python
830 lines 25.6 KB
9236d457 fix: remove all inline scripts, restore strict script-src CSP (#51) Gabriel Cardona <cgcardona@gmail.com> 23h 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 block 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 # Template renders isl-empty block for empty state
375 assert "isl-empty" in resp.text
376
377
378 # ---------------------------------------------------------------------------
379 # Pagination
380 # ---------------------------------------------------------------------------
381
382
383 @pytest.mark.anyio
384 async def test_issue_list_pagination_renders_next_link(
385 client: AsyncClient,
386 db_session: AsyncSession,
387 ) -> None:
388 """When total issues exceed per_page, a Next pagination link appears."""
389 repo_id = await _make_repo(db_session)
390 for i in range(30):
391 await _make_issue(db_session, repo_id, number=i + 1, state="open")
392 body = await _get_page(client, per_page="25")
393 assert "Next" in body or "next" in body.lower()
394
395
396 # ---------------------------------------------------------------------------
397 # Right sidebar
398 # ---------------------------------------------------------------------------
399
400
401 @pytest.mark.anyio
402 async def test_issue_list_milestone_progress_in_right_sidebar(
403 client: AsyncClient,
404 db_session: AsyncSession,
405 ) -> None:
406 """Seeded milestone title appears in the right sidebar progress section."""
407 repo_id = await _make_repo(db_session)
408 await _make_milestone(db_session, repo_id, title="Album Release v1")
409 body = await _get_page(client)
410 assert "Album Release v1" in body
411
412
413 @pytest.mark.anyio
414 async def test_issue_list_right_sidebar_present(
415 client: AsyncClient,
416 db_session: AsyncSession,
417 ) -> None:
418 """Right sidebar element is present in the SSR page."""
419 await _make_repo(db_session)
420 body = await _get_page(client)
421 assert "isl-sidebar" in body
422
423
424 @pytest.mark.anyio
425 async def test_issue_list_milestone_progress_heading_present(
426 client: AsyncClient,
427 db_session: AsyncSession,
428 ) -> None:
429 """Milestones sidebar section is rendered server-side."""
430 await _make_repo(db_session)
431 body = await _get_page(client)
432 assert "Milestones" in body
433
434
435 @pytest.mark.anyio
436 async def test_issue_list_milestone_progress_bar_css_present(
437 client: AsyncClient,
438 db_session: AsyncSession,
439 ) -> None:
440 """milestone-progress-bar-fill CSS class is in app.css; page renders milestone sidebar."""
441 await _make_repo(db_session)
442 body = await _get_page(client)
443 assert "Milestones" in body
444
445
446 @pytest.mark.anyio
447 async def test_issue_list_milestone_progress_list_present(
448 client: AsyncClient,
449 db_session: AsyncSession,
450 ) -> None:
451 """Milestones sidebar section contains milestone progress bars."""
452 await _make_repo(db_session)
453 body = await _get_page(client)
454 assert "Milestones" in body
455
456
457 @pytest.mark.anyio
458 async def test_issue_list_labels_summary_heading_present(
459 client: AsyncClient,
460 db_session: AsyncSession,
461 ) -> None:
462 """Labels sidebar section is rendered server-side."""
463 await _make_repo(db_session)
464 body = await _get_page(client)
465 assert "Labels" in body
466
467
468 @pytest.mark.anyio
469 async def test_issue_list_labels_summary_list_present(
470 client: AsyncClient,
471 db_session: AsyncSession,
472 ) -> None:
473 """Labels sidebar section contains a label list."""
474 await _make_repo(db_session)
475 body = await _get_page(client)
476 assert "Labels" in body
477
478
479 # ---------------------------------------------------------------------------
480 # Filter sidebar elements
481 # ---------------------------------------------------------------------------
482
483
484 @pytest.mark.anyio
485 async def test_issue_list_filter_sidebar_present(
486 client: AsyncClient,
487 db_session: AsyncSession,
488 ) -> None:
489 """Issue filter form is rendered server-side."""
490 await _make_repo(db_session)
491 body = await _get_page(client)
492 assert "isl-filters" in body
493
494
495 @pytest.mark.anyio
496 async def test_issue_list_label_chip_container_present(
497 client: AsyncClient,
498 db_session: AsyncSession,
499 ) -> None:
500 """Label filter select is present in the filter bar."""
501 await _make_repo(db_session)
502 body = await _get_page(client)
503 assert "isl-filter-select" in body
504
505
506 @pytest.mark.anyio
507 async def test_issue_list_filter_milestone_select_present(
508 client: AsyncClient,
509 db_session: AsyncSession,
510 ) -> None:
511 """Milestone filter <select> appears when milestones exist; filter form always present."""
512 await _make_repo(db_session)
513 body = await _get_page(client)
514 # Filter form is always present; milestone select only when data is seeded
515 assert "issue-filter-form" in body
516
517
518 @pytest.mark.anyio
519 async def test_issue_list_filter_assignee_select_present(
520 client: AsyncClient,
521 db_session: AsyncSession,
522 ) -> None:
523 """Assignee filter <select> appears when assignees exist; sort select always present."""
524 await _make_repo(db_session)
525 body = await _get_page(client)
526 # Sort select is always rendered; assignee select only when data is seeded
527 assert 'name="sort"' in body or "name='sort'" in body
528
529
530 @pytest.mark.anyio
531 async def test_issue_list_filter_author_input_present(
532 client: AsyncClient,
533 db_session: AsyncSession,
534 ) -> None:
535 """Issue filter form has filter controls (author filter via assignee or label select)."""
536 await _make_repo(db_session)
537 body = await _get_page(client)
538 assert "issue-filter-form" in body
539
540
541 @pytest.mark.anyio
542 async def test_issue_list_sort_radio_group_present(
543 client: AsyncClient,
544 db_session: AsyncSession,
545 ) -> None:
546 """Sort filter <select> element is present (name=sort)."""
547 await _make_repo(db_session)
548 body = await _get_page(client)
549 assert 'name="sort"' in body or "name='sort'" in body
550
551
552 @pytest.mark.anyio
553 async def test_issue_list_sort_radio_buttons_present(
554 client: AsyncClient,
555 db_session: AsyncSession,
556 ) -> None:
557 """Radio inputs with name='sort' are present (SSR-rendered)."""
558 await _make_repo(db_session)
559 body = await _get_page(client)
560 assert 'name="sort"' in body or "name='sort'" in body
561
562
563 # ---------------------------------------------------------------------------
564 # Template selector / new-issue flow (minimal JS retained)
565 # ---------------------------------------------------------------------------
566
567
568 @pytest.mark.anyio
569 async def test_issue_list_template_picker_present(
570 client: AsyncClient,
571 db_session: AsyncSession,
572 ) -> None:
573 """template-picker element is present in the page HTML."""
574 await _make_repo(db_session)
575 body = await _get_page(client)
576 assert "template-picker" in body
577
578
579 @pytest.mark.anyio
580 async def test_issue_list_template_grid_present(
581 client: AsyncClient,
582 db_session: AsyncSession,
583 ) -> None:
584 """Template picker container is rendered server-side."""
585 await _make_repo(db_session)
586 body = await _get_page(client)
587 assert "isl-template-picker" in body
588
589
590 @pytest.mark.anyio
591 async def test_issue_list_template_cards_present(
592 client: AsyncClient,
593 db_session: AsyncSession,
594 ) -> None:
595 """Template picker card class is present (SSR-rendered template cards)."""
596 await _make_repo(db_session)
597 body = await _get_page(client)
598 assert "isl-tp-card" in body
599
600
601 @pytest.mark.anyio
602 async def test_issue_list_show_template_picker_js_present(
603 client: AsyncClient,
604 db_session: AsyncSession,
605 ) -> None:
606 """Template picker is triggered by a data-action button (showTemplatePicker moved to issue-list.ts)."""
607 await _make_repo(db_session)
608 body = await _get_page(client)
609 assert "show-template-picker" in body
610
611
612 @pytest.mark.anyio
613 async def test_issue_list_select_template_js_present(
614 client: AsyncClient,
615 db_session: AsyncSession,
616 ) -> None:
617 """Template cards use data-action="select-template" (selectTemplate moved to issue-list.ts)."""
618 await _make_repo(db_session)
619 body = await _get_page(client)
620 assert "select-template" in body
621
622
623 @pytest.mark.anyio
624 async def test_issue_list_issue_templates_const_present(
625 client: AsyncClient,
626 db_session: AsyncSession,
627 ) -> None:
628 """ISSUE_TEMPLATES is in app.js (TypeScript module); page dispatches issue-list module."""
629 await _make_repo(db_session)
630 body = await _get_page(client)
631 # ISSUE_TEMPLATES moved to app.js; verify page dispatch JSON and template picker HTML
632 assert '"page": "issue-list"' in body
633 assert "template-picker" in body
634
635
636 @pytest.mark.anyio
637 async def test_issue_list_new_issue_btn_calls_template(
638 client: AsyncClient,
639 db_session: AsyncSession,
640 ) -> None:
641 """New Issue button opens template picker via data-action (showTemplatePicker moved to issue-list.ts)."""
642 await _make_repo(db_session)
643 body = await _get_page(client)
644 assert "New Issue" in body
645 assert "show-template-picker" in body
646
647
648 @pytest.mark.anyio
649 async def test_issue_list_templates_back_btn_present(
650 client: AsyncClient,
651 db_session: AsyncSession,
652 ) -> None:
653 """Template picker is rendered in the new issue flow."""
654 await _make_repo(db_session)
655 body = await _get_page(client)
656 assert "template-picker" in body
657
658
659 @pytest.mark.anyio
660 async def test_issue_list_blank_template_defined(
661 client: AsyncClient,
662 db_session: AsyncSession,
663 ) -> None:
664 """'blank' template id is present in ISSUE_TEMPLATES."""
665 await _make_repo(db_session)
666 body = await _get_page(client)
667 assert "'blank'" in body or '"blank"' in body
668
669
670 @pytest.mark.anyio
671 async def test_issue_list_bug_template_defined(
672 client: AsyncClient,
673 db_session: AsyncSession,
674 ) -> None:
675 """'bug' template id is present in ISSUE_TEMPLATES."""
676 await _make_repo(db_session)
677 body = await _get_page(client)
678 assert "'bug'" in body or '"bug"' in body
679
680
681 # ---------------------------------------------------------------------------
682 # Bulk toolbar structure (SSR-rendered, JS-activated)
683 # ---------------------------------------------------------------------------
684
685
686 @pytest.mark.anyio
687 async def test_issue_list_bulk_toolbar_present(
688 client: AsyncClient,
689 db_session: AsyncSession,
690 ) -> None:
691 """bulk-toolbar element is rendered in the page HTML."""
692 await _make_repo(db_session)
693 body = await _get_page(client)
694 assert "bulk-toolbar" in body
695
696
697 @pytest.mark.anyio
698 async def test_issue_list_bulk_count_present(
699 client: AsyncClient,
700 db_session: AsyncSession,
701 ) -> None:
702 """bulk-count element is present."""
703 await _make_repo(db_session)
704 body = await _get_page(client)
705 assert "bulk-count" in body
706
707
708 @pytest.mark.anyio
709 async def test_issue_list_bulk_label_select_present(
710 client: AsyncClient,
711 db_session: AsyncSession,
712 ) -> None:
713 """bulk-label-select element is present."""
714 await _make_repo(db_session)
715 body = await _get_page(client)
716 assert "bulk-label-select" in body
717
718
719 @pytest.mark.anyio
720 async def test_issue_list_bulk_milestone_select_present(
721 client: AsyncClient,
722 db_session: AsyncSession,
723 ) -> None:
724 """bulk-milestone-select element is present."""
725 await _make_repo(db_session)
726 body = await _get_page(client)
727 assert "bulk-milestone-select" in body
728
729
730 @pytest.mark.anyio
731 async def test_issue_list_issue_row_checkbox_present(
732 client: AsyncClient,
733 db_session: AsyncSession,
734 ) -> None:
735 """issue-row-check CSS class is present (checkbox for bulk selection)."""
736 repo_id = await _make_repo(db_session)
737 await _make_issue(db_session, repo_id, title="Has checkbox")
738 body = await _get_page(client)
739 assert "issue-row-check" in body
740
741
742 @pytest.mark.anyio
743 async def test_issue_list_toggle_issue_select_js_present(
744 client: AsyncClient,
745 db_session: AsyncSession,
746 ) -> None:
747 """toggleIssueSelect() is in app.js (TypeScript module); page renders bulk toolbar."""
748 await _make_repo(db_session)
749 body = await _get_page(client)
750 # Function moved to app.js; verify bulk toolbar HTML element is present
751 assert "bulk-toolbar" in body
752
753
754 @pytest.mark.anyio
755 async def test_issue_list_deselect_all_js_present(
756 client: AsyncClient,
757 db_session: AsyncSession,
758 ) -> None:
759 """Deselect action uses data-bulk-action="deselect" (deselectAll moved to issue-list.ts)."""
760 await _make_repo(db_session)
761 body = await _get_page(client)
762 assert 'data-bulk-action="deselect"' in body
763
764
765 @pytest.mark.anyio
766 async def test_issue_list_update_bulk_toolbar_js_present(
767 client: AsyncClient,
768 db_session: AsyncSession,
769 ) -> None:
770 """Page renders bulk action buttons (isl-bulk-btn with data-bulk-action attributes)."""
771 await _make_repo(db_session)
772 body = await _get_page(client)
773 assert "isl-bulk-btn" in body
774 assert "data-bulk-action" in body
775
776
777 @pytest.mark.anyio
778 async def test_issue_list_bulk_close_js_present(
779 client: AsyncClient,
780 db_session: AsyncSession,
781 ) -> None:
782 """Close bulk action uses data-bulk-action="close" (bulkClose moved to issue-list.ts)."""
783 await _make_repo(db_session)
784 body = await _get_page(client)
785 assert 'data-bulk-action="close"' in body
786
787
788 @pytest.mark.anyio
789 async def test_issue_list_bulk_reopen_js_present(
790 client: AsyncClient,
791 db_session: AsyncSession,
792 ) -> None:
793 """Reopen bulk action uses data-bulk-action="reopen" (bulkReopen moved to issue-list.ts)."""
794 await _make_repo(db_session)
795 body = await _get_page(client)
796 assert 'data-bulk-action="reopen"' in body
797
798
799 @pytest.mark.anyio
800 async def test_issue_list_bulk_assign_label_js_present(
801 client: AsyncClient,
802 db_session: AsyncSession,
803 ) -> None:
804 """Assign label uses data-bulk-action="assign-label" (bulkAssignLabel moved to issue-list.ts)."""
805 await _make_repo(db_session)
806 body = await _get_page(client)
807 assert 'data-bulk-action="assign-label"' in body
808
809
810 @pytest.mark.anyio
811 async def test_issue_list_bulk_assign_milestone_js_present(
812 client: AsyncClient,
813 db_session: AsyncSession,
814 ) -> None:
815 """Assign milestone uses data-bulk-action="assign-milestone" (bulkAssignMilestone moved to issue-list.ts)."""
816 await _make_repo(db_session)
817 body = await _get_page(client)
818 assert 'data-bulk-action="assign-milestone"' in body
819
820
821 @pytest.mark.anyio
822 async def test_issue_list_full_page_contains_html_wrapper(
823 client: AsyncClient,
824 db_session: AsyncSession,
825 ) -> None:
826 """Direct browser navigation (no HX-Request) returns a full HTML page with <html> tag."""
827 await _make_repo(db_session)
828 resp = await client.get("/beatmaker/grooves/issues")
829 assert resp.status_code == 200
830 assert "<html" in resp.text