gabriel / musehub public
test_musehub_ui_issue_list_enhanced.py python
828 lines 25.0 KB
c0f0b481 release: merge dev → main (#5) 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 "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 "/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 """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 in app.css; page renders milestone sidebar."""
440 await _make_repo(db_session)
441 body = await _get_page(client)
442 # Class moved to app.css (SCSS refactor); verify the milestone sidebar renders instead
443 assert "milestone-progress-heading" 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 """milestone-progress-list element id is present in the page."""
452 await _make_repo(db_session)
453 body = await _get_page(client)
454 assert "milestone-progress-list" 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-summary-heading id is rendered server-side in the right sidebar."""
463 await _make_repo(db_session)
464 body = await _get_page(client)
465 assert "labels-summary-heading" 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-summary-list id is rendered server-side in the right sidebar."""
474 await _make_repo(db_session)
475 body = await _get_page(client)
476 assert "labels-summary-list" 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 """filter-sidebar id is rendered server-side."""
490 await _make_repo(db_session)
491 body = await _get_page(client)
492 assert "filter-sidebar" 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-chip-container id is present in the filter sidebar."""
501 await _make_repo(db_session)
502 body = await _get_page(client)
503 assert "label-chip-container" 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 """filter-milestone <select> element is present."""
512 await _make_repo(db_session)
513 body = await _get_page(client)
514 assert "filter-milestone" in body
515
516
517 @pytest.mark.anyio
518 async def test_issue_list_filter_assignee_select_present(
519 client: AsyncClient,
520 db_session: AsyncSession,
521 ) -> None:
522 """filter-assignee <select> element is present."""
523 await _make_repo(db_session)
524 body = await _get_page(client)
525 assert "filter-assignee" in body
526
527
528 @pytest.mark.anyio
529 async def test_issue_list_filter_author_input_present(
530 client: AsyncClient,
531 db_session: AsyncSession,
532 ) -> None:
533 """filter-author text input is present."""
534 await _make_repo(db_session)
535 body = await _get_page(client)
536 assert "filter-author" in body
537
538
539 @pytest.mark.anyio
540 async def test_issue_list_sort_radio_group_present(
541 client: AsyncClient,
542 db_session: AsyncSession,
543 ) -> None:
544 """sort-radio-group element is present in the filter sidebar."""
545 await _make_repo(db_session)
546 body = await _get_page(client)
547 assert "sort-radio-group" in body
548
549
550 @pytest.mark.anyio
551 async def test_issue_list_sort_radio_buttons_present(
552 client: AsyncClient,
553 db_session: AsyncSession,
554 ) -> None:
555 """Radio inputs with name='sort' are present (SSR-rendered)."""
556 await _make_repo(db_session)
557 body = await _get_page(client)
558 assert 'name="sort"' in body or "name='sort'" in body
559
560
561 # ---------------------------------------------------------------------------
562 # Template selector / new-issue flow (minimal JS retained)
563 # ---------------------------------------------------------------------------
564
565
566 @pytest.mark.anyio
567 async def test_issue_list_template_picker_present(
568 client: AsyncClient,
569 db_session: AsyncSession,
570 ) -> None:
571 """template-picker element is present in the page HTML."""
572 await _make_repo(db_session)
573 body = await _get_page(client)
574 assert "template-picker" in body
575
576
577 @pytest.mark.anyio
578 async def test_issue_list_template_grid_present(
579 client: AsyncClient,
580 db_session: AsyncSession,
581 ) -> None:
582 """template-grid element is rendered server-side."""
583 await _make_repo(db_session)
584 body = await _get_page(client)
585 assert "template-grid" in body
586
587
588 @pytest.mark.anyio
589 async def test_issue_list_template_cards_present(
590 client: AsyncClient,
591 db_session: AsyncSession,
592 ) -> None:
593 """template-card class is present (SSR-rendered template cards)."""
594 await _make_repo(db_session)
595 body = await _get_page(client)
596 assert "template-card" in body
597
598
599 @pytest.mark.anyio
600 async def test_issue_list_show_template_picker_js_present(
601 client: AsyncClient,
602 db_session: AsyncSession,
603 ) -> None:
604 """showTemplatePicker() JS function is present in the page."""
605 await _make_repo(db_session)
606 body = await _get_page(client)
607 assert "showTemplatePicker" in body
608
609
610 @pytest.mark.anyio
611 async def test_issue_list_select_template_js_present(
612 client: AsyncClient,
613 db_session: AsyncSession,
614 ) -> None:
615 """selectTemplate() JS function is present in the page."""
616 await _make_repo(db_session)
617 body = await _get_page(client)
618 assert "selectTemplate" in body
619
620
621 @pytest.mark.anyio
622 async def test_issue_list_issue_templates_const_present(
623 client: AsyncClient,
624 db_session: AsyncSession,
625 ) -> None:
626 """ISSUE_TEMPLATES is in app.js (TypeScript module); page dispatches issue-list module."""
627 await _make_repo(db_session)
628 body = await _get_page(client)
629 # ISSUE_TEMPLATES moved to app.js; verify page dispatch JSON and template picker HTML
630 assert '"page": "issue-list"' in body
631 assert "template-picker" in body
632
633
634 @pytest.mark.anyio
635 async def test_issue_list_new_issue_btn_calls_template(
636 client: AsyncClient,
637 db_session: AsyncSession,
638 ) -> None:
639 """new-issue-btn invokes showTemplatePicker."""
640 await _make_repo(db_session)
641 body = await _get_page(client)
642 assert "new-issue-btn" in body
643 assert "showTemplatePicker" in body
644
645
646 @pytest.mark.anyio
647 async def test_issue_list_templates_back_btn_present(
648 client: AsyncClient,
649 db_session: AsyncSession,
650 ) -> None:
651 """← Templates back navigation is present in the new issue flow."""
652 await _make_repo(db_session)
653 body = await _get_page(client)
654 assert "Templates" in body
655
656
657 @pytest.mark.anyio
658 async def test_issue_list_blank_template_defined(
659 client: AsyncClient,
660 db_session: AsyncSession,
661 ) -> None:
662 """'blank' template id is present in ISSUE_TEMPLATES."""
663 await _make_repo(db_session)
664 body = await _get_page(client)
665 assert "'blank'" in body or '"blank"' in body
666
667
668 @pytest.mark.anyio
669 async def test_issue_list_bug_template_defined(
670 client: AsyncClient,
671 db_session: AsyncSession,
672 ) -> None:
673 """'bug' template id is present in ISSUE_TEMPLATES."""
674 await _make_repo(db_session)
675 body = await _get_page(client)
676 assert "'bug'" in body or '"bug"' in body
677
678
679 # ---------------------------------------------------------------------------
680 # Bulk toolbar structure (SSR-rendered, JS-activated)
681 # ---------------------------------------------------------------------------
682
683
684 @pytest.mark.anyio
685 async def test_issue_list_bulk_toolbar_present(
686 client: AsyncClient,
687 db_session: AsyncSession,
688 ) -> None:
689 """bulk-toolbar element is rendered in the page HTML."""
690 await _make_repo(db_session)
691 body = await _get_page(client)
692 assert "bulk-toolbar" in body
693
694
695 @pytest.mark.anyio
696 async def test_issue_list_bulk_count_present(
697 client: AsyncClient,
698 db_session: AsyncSession,
699 ) -> None:
700 """bulk-count element is present."""
701 await _make_repo(db_session)
702 body = await _get_page(client)
703 assert "bulk-count" in body
704
705
706 @pytest.mark.anyio
707 async def test_issue_list_bulk_label_select_present(
708 client: AsyncClient,
709 db_session: AsyncSession,
710 ) -> None:
711 """bulk-label-select element is present."""
712 await _make_repo(db_session)
713 body = await _get_page(client)
714 assert "bulk-label-select" in body
715
716
717 @pytest.mark.anyio
718 async def test_issue_list_bulk_milestone_select_present(
719 client: AsyncClient,
720 db_session: AsyncSession,
721 ) -> None:
722 """bulk-milestone-select element is present."""
723 await _make_repo(db_session)
724 body = await _get_page(client)
725 assert "bulk-milestone-select" in body
726
727
728 @pytest.mark.anyio
729 async def test_issue_list_issue_row_checkbox_present(
730 client: AsyncClient,
731 db_session: AsyncSession,
732 ) -> None:
733 """issue-row-check CSS class is present (checkbox for bulk selection)."""
734 repo_id = await _make_repo(db_session)
735 await _make_issue(db_session, repo_id, title="Has checkbox")
736 body = await _get_page(client)
737 assert "issue-row-check" in body
738
739
740 @pytest.mark.anyio
741 async def test_issue_list_toggle_issue_select_js_present(
742 client: AsyncClient,
743 db_session: AsyncSession,
744 ) -> None:
745 """toggleIssueSelect() is in app.js (TypeScript module); page renders bulk toolbar."""
746 await _make_repo(db_session)
747 body = await _get_page(client)
748 # Function moved to app.js; verify bulk toolbar HTML element is present
749 assert "bulk-toolbar" in body
750
751
752 @pytest.mark.anyio
753 async def test_issue_list_deselect_all_js_present(
754 client: AsyncClient,
755 db_session: AsyncSession,
756 ) -> None:
757 """deselectAll() JS function is present in the page."""
758 await _make_repo(db_session)
759 body = await _get_page(client)
760 assert "deselectAll" in body
761
762
763 @pytest.mark.anyio
764 async def test_issue_list_update_bulk_toolbar_js_present(
765 client: AsyncClient,
766 db_session: AsyncSession,
767 ) -> None:
768 """updateBulkToolbar() is in app.js (TypeScript module); page renders bulk action buttons."""
769 await _make_repo(db_session)
770 body = await _get_page(client)
771 # Function moved to app.js; verify bulk action buttons are in the HTML
772 assert "bulk-action-btn" in body
773
774
775 @pytest.mark.anyio
776 async def test_issue_list_bulk_close_js_present(
777 client: AsyncClient,
778 db_session: AsyncSession,
779 ) -> None:
780 """bulkClose() JS stub is present in the page."""
781 await _make_repo(db_session)
782 body = await _get_page(client)
783 assert "bulkClose" in body
784
785
786 @pytest.mark.anyio
787 async def test_issue_list_bulk_reopen_js_present(
788 client: AsyncClient,
789 db_session: AsyncSession,
790 ) -> None:
791 """bulkReopen() JS stub is present in the page."""
792 await _make_repo(db_session)
793 body = await _get_page(client)
794 assert "bulkReopen" in body
795
796
797 @pytest.mark.anyio
798 async def test_issue_list_bulk_assign_label_js_present(
799 client: AsyncClient,
800 db_session: AsyncSession,
801 ) -> None:
802 """bulkAssignLabel() JS stub is present in the page."""
803 await _make_repo(db_session)
804 body = await _get_page(client)
805 assert "bulkAssignLabel" in body
806
807
808 @pytest.mark.anyio
809 async def test_issue_list_bulk_assign_milestone_js_present(
810 client: AsyncClient,
811 db_session: AsyncSession,
812 ) -> None:
813 """bulkAssignMilestone() JS stub is present in the page."""
814 await _make_repo(db_session)
815 body = await _get_page(client)
816 assert "bulkAssignMilestone" in body
817
818
819 @pytest.mark.anyio
820 async def test_issue_list_full_page_contains_html_wrapper(
821 client: AsyncClient,
822 db_session: AsyncSession,
823 ) -> None:
824 """Direct browser navigation (no HX-Request) returns a full HTML page with <html> tag."""
825 await _make_repo(db_session)
826 resp = await client.get("/beatmaker/grooves/issues")
827 assert resp.status_code == 200
828 assert "<html" in resp.text