gabriel / musehub public
test_musehub_ui_issue_list_enhanced.py python
745 lines 24.3 KB
Raw
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 3 days 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_right_sidebar_present
38 - test_issue_list_labels_summary_heading_present
39 - test_issue_list_labels_summary_list_present
40
41 Filter sidebar
42 - test_issue_list_filter_sidebar_present
43 - test_issue_list_label_chip_container_present
44 - test_issue_list_filter_assignee_select_present
45 - test_issue_list_filter_author_input_present
46 - test_issue_list_sort_radio_group_present
47 - test_issue_list_sort_radio_buttons_present
48
49 Template selector / new-issue flow (minimal JS)
50 - test_issue_list_template_picker_present
51 - test_issue_list_template_grid_present
52 - test_issue_list_template_cards_present
53 - test_issue_list_show_template_picker_js_present
54 - test_issue_list_select_template_js_present
55 - test_issue_list_issue_templates_const_present
56 - test_issue_list_new_issue_btn_calls_template
57 - test_issue_list_templates_back_btn_present
58 - test_issue_list_blank_template_defined
59 - test_issue_list_bug_template_defined
60
61 Bulk toolbar structure
62 - test_issue_list_bulk_toolbar_present
63 - test_issue_list_bulk_count_present
64 - test_issue_list_bulk_label_select_present
65 - test_issue_list_issue_row_checkbox_present
66 - test_issue_list_toggle_issue_select_js_present
67 - test_issue_list_deselect_all_js_present
68 - test_issue_list_update_bulk_toolbar_js_present
69 - test_issue_list_bulk_close_js_present
70 - test_issue_list_bulk_reopen_js_present
71 - test_issue_list_bulk_assign_label_js_present
72 """
73 from __future__ import annotations
74
75 import pytest
76 from httpx import AsyncClient
77 from sqlalchemy.ext.asyncio import AsyncSession
78
79 from datetime import datetime, timezone
80
81 from muse.core.types import now_utc_iso
82 from musehub.core.genesis import compute_identity_id, compute_issue_id, compute_repo_id
83 from musehub.db.musehub_repo_models import MusehubRepo
84 from musehub.db.musehub_social_models import MusehubIssue
85
86
87 # ---------------------------------------------------------------------------
88 # Helpers
89 # ---------------------------------------------------------------------------
90
91
92 async def _make_repo(
93 db: AsyncSession,
94 owner: str = "beatmaker",
95 slug: str = "grooves",
96 ) -> str:
97 """Seed a public repo and return its repo_id string."""
98 owner_id = compute_identity_id(owner.encode())
99 created_at = datetime.now(tz=timezone.utc)
100 repo = MusehubRepo(
101 repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()),
102 name=slug,
103 owner=owner,
104 slug=slug,
105 visibility="public",
106 owner_user_id=owner_id,
107 created_at=created_at,
108 updated_at=created_at,
109 )
110 db.add(repo)
111 await db.commit()
112 await db.refresh(repo)
113 return str(repo.repo_id)
114
115
116 async def _make_issue(
117 db: AsyncSession,
118 repo_id: str,
119 *,
120 number: int = 1,
121 title: str = "Bass too loud",
122 state: str = "open",
123 labels: list[str] | None = None,
124 author: str = "beatmaker",
125 ) -> MusehubIssue:
126 """Seed an issue and return it."""
127 author_id = compute_identity_id(author.encode())
128 issue = MusehubIssue(
129 issue_id=compute_issue_id(repo_id, author_id, now_utc_iso()),
130 repo_id=repo_id,
131 number=number,
132 title=title,
133 body="Issue body.",
134 state=state,
135 labels=labels or [],
136 author=author,
137 )
138 db.add(issue)
139 await db.commit()
140 await db.refresh(issue)
141 return issue
142
143
144 async def _get_page(
145 client: AsyncClient,
146 owner: str = "beatmaker",
147 slug: str = "grooves",
148 **params: str,
149 ) -> str:
150 """Fetch the issue list page and return its text body."""
151 resp = await client.get(f"/{owner}/{slug}/issues", params=params)
152 assert resp.status_code == 200
153 return resp.text
154
155
156 # ---------------------------------------------------------------------------
157 # Basic page rendering
158 # ---------------------------------------------------------------------------
159
160
161 async def test_issue_list_page_returns_200(
162 client: AsyncClient,
163 db_session: AsyncSession,
164 ) -> None:
165 """GET /{owner}/{slug}/issues returns 200 HTML."""
166 await _make_repo(db_session)
167 response = await client.get("/beatmaker/grooves/issues")
168 assert response.status_code == 200
169 assert "text/html" in response.headers["content-type"]
170
171
172 async def test_issue_list_no_auth_required(
173 client: AsyncClient,
174 db_session: AsyncSession,
175 ) -> None:
176 """Issue list page renders without authentication."""
177 await _make_repo(db_session)
178 response = await client.get("/beatmaker/grooves/issues")
179 assert response.status_code == 200
180
181
182 async def test_issue_list_unknown_repo_404(
183 client: AsyncClient,
184 db_session: AsyncSession,
185 ) -> None:
186 """Unknown owner/slug returns 404."""
187 response = await client.get("/nobody/norepo/issues")
188 assert response.status_code == 404
189
190
191 # ---------------------------------------------------------------------------
192 # SSR content — issue data is rendered server-side
193 # ---------------------------------------------------------------------------
194
195
196 async def test_issue_list_renders_issue_title_server_side(
197 client: AsyncClient,
198 db_session: AsyncSession,
199 ) -> None:
200 """Seeded issue title appears in SSR HTML without JS execution."""
201 repo_id = await _make_repo(db_session)
202 await _make_issue(db_session, repo_id, title="Kick drum too punchy")
203 body = await _get_page(client)
204 assert "Kick drum too punchy" in body
205
206
207 async def test_issue_list_filter_form_has_hx_get(
208 client: AsyncClient,
209 db_session: AsyncSession,
210 ) -> None:
211 """Filter form carries hx-get attribute for HTMX partial updates."""
212 await _make_repo(db_session)
213 body = await _get_page(client)
214 assert "hx-get" in body
215
216
217 async def test_issue_list_filter_form_has_hx_target(
218 client: AsyncClient,
219 db_session: AsyncSession,
220 ) -> None:
221 """Filter form targets #issue-rows for HTMX swaps."""
222 await _make_repo(db_session)
223 body = await _get_page(client)
224 assert 'hx-target="#issue-rows"' in body or "hx-target='#issue-rows'" in body
225
226
227 # ---------------------------------------------------------------------------
228 # Open/closed tab counts
229 # ---------------------------------------------------------------------------
230
231
232 async def test_issue_list_tab_open_has_hx_get(
233 client: AsyncClient,
234 db_session: AsyncSession,
235 ) -> None:
236 """Open tab link carries hx-get for HTMX navigation."""
237 await _make_repo(db_session)
238 body = await _get_page(client)
239 assert "state=open" in body
240 assert "hx-get" in body
241
242
243 async def test_issue_list_open_closed_counts_in_tabs(
244 client: AsyncClient,
245 db_session: AsyncSession,
246 ) -> None:
247 """Tab badges reflect the actual open and closed issue counts from the DB."""
248 repo_id = await _make_repo(db_session)
249 for i in range(3):
250 await _make_issue(db_session, repo_id, number=i + 1, state="open")
251 for i in range(2):
252 await _make_issue(db_session, repo_id, number=i + 4, state="closed")
253 body = await _get_page(client)
254 assert ">3<" in body or ">3 <" in body or "3</span>" in body
255 assert ">2<" in body or ">2 <" in body or "2</span>" in body
256
257
258 # ---------------------------------------------------------------------------
259 # State filter
260 # ---------------------------------------------------------------------------
261
262
263 async def test_issue_list_state_filter_closed_shows_closed_only(
264 client: AsyncClient,
265 db_session: AsyncSession,
266 ) -> None:
267 """?state=closed returns only closed issues in the rendered HTML."""
268 repo_id = await _make_repo(db_session)
269 await _make_issue(db_session, repo_id, number=1, title="UniqueOpenTitle", state="open")
270 await _make_issue(db_session, repo_id, number=2, title="UniqueClosedTitle", state="closed")
271 body = await _get_page(client, state="closed")
272 assert "UniqueClosedTitle" in body
273 assert "UniqueOpenTitle" not in body
274
275
276 # ---------------------------------------------------------------------------
277 # Label filter
278 # ---------------------------------------------------------------------------
279
280
281 async def test_issue_list_label_filter_narrows_issues(
282 client: AsyncClient,
283 db_session: AsyncSession,
284 ) -> None:
285 """?label=bug returns only issues labelled 'bug'."""
286 repo_id = await _make_repo(db_session)
287 await _make_issue(db_session, repo_id, number=1, title="Bug: kick too loud", labels=["bug"])
288 await _make_issue(db_session, repo_id, number=2, title="Feature: add reverb", labels=["feature"])
289 body = await _get_page(client, label="bug")
290 assert "Bug: kick too loud" in body
291 assert "Feature: add reverb" not in body
292
293
294 # ---------------------------------------------------------------------------
295 # HTMX fragment
296 # ---------------------------------------------------------------------------
297
298
299 async def test_issue_list_htmx_request_returns_fragment(
300 client: AsyncClient,
301 db_session: AsyncSession,
302 ) -> None:
303 """HX-Request: true returns a bare fragment — no <html> wrapper."""
304 await _make_repo(db_session)
305 resp = await client.get(
306 "/beatmaker/grooves/issues",
307 headers={"HX-Request": "true"},
308 )
309 assert resp.status_code == 200
310 assert "<html" not in resp.text
311
312
313 async def test_issue_list_fragment_contains_issue_title(
314 client: AsyncClient,
315 db_session: AsyncSession,
316 ) -> None:
317 """HTMX fragment contains the seeded issue title."""
318 repo_id = await _make_repo(db_session)
319 await _make_issue(db_session, repo_id, title="Synth pad too bright")
320 resp = await client.get(
321 "/beatmaker/grooves/issues",
322 headers={"HX-Request": "true"},
323 )
324 assert resp.status_code == 200
325 assert "Synth pad too bright" in resp.text
326
327
328 async def test_issue_list_fragment_empty_state_when_no_issues(
329 client: AsyncClient,
330 db_session: AsyncSession,
331 ) -> None:
332 """Fragment returns an empty-state block when no issues match filters."""
333 repo_id = await _make_repo(db_session)
334 await _make_issue(db_session, repo_id, number=1, title="Open issue", state="open")
335 resp = await client.get(
336 "/beatmaker/grooves/issues",
337 params={"state": "closed"},
338 headers={"HX-Request": "true"},
339 )
340 assert resp.status_code == 200
341 # Template renders isl-empty block for empty state
342 assert "isl-empty" in resp.text
343
344
345 # ---------------------------------------------------------------------------
346 # Pagination
347 # ---------------------------------------------------------------------------
348
349
350 async def test_issue_list_pagination_renders_next_link(
351 client: AsyncClient,
352 db_session: AsyncSession,
353 ) -> None:
354 """When total issues exceed per_page, a Next pagination link appears."""
355 repo_id = await _make_repo(db_session)
356 for i in range(30):
357 await _make_issue(db_session, repo_id, number=i + 1, state="open")
358 body = await _get_page(client, per_page="25")
359 assert "Next" in body or "next" in body.lower()
360
361
362 async def test_issue_list_pagination_next_cursor_is_integer(
363 client: AsyncClient,
364 db_session: AsyncSession,
365 ) -> None:
366 """Next page cursor in the link URL is the issue number (integer), not a timestamp."""
367 repo_id = await _make_repo(db_session)
368 for i in range(30):
369 await _make_issue(db_session, repo_id, number=i + 1, state="open")
370 resp = await client.get("/beatmaker/grooves/issues", params={"limit": "25"})
371 assert resp.status_code == 200
372 import re
373 # Extract cursor value from the Next → link
374 m = re.search(r'cursor=([^&"\']+)', resp.text)
375 assert m, "No cursor found in pagination link"
376 cursor_val = m.group(1)
377 # Must be parseable as an integer (issue number), not a timestamp
378 assert cursor_val.isdigit(), f"Cursor should be an integer issue number, got: {cursor_val!r}"
379
380
381 async def test_issue_list_pagination_second_page_shows_issues(
382 client: AsyncClient,
383 db_session: AsyncSession,
384 ) -> None:
385 """Following the Next cursor returns the second page of issues, not an empty state."""
386 repo_id = await _make_repo(db_session)
387 for i in range(30):
388 await _make_issue(db_session, repo_id, number=i + 1, title=f"Issue {i + 1}", state="open")
389 # Page 1
390 resp1 = await client.get("/beatmaker/grooves/issues", params={"limit": "25"})
391 assert resp1.status_code == 200
392 import re
393 m = re.search(r'cursor=([^&"\']+)', resp1.text)
394 assert m, "No cursor found in page 1 link"
395 cursor = m.group(1)
396 # Page 2
397 resp2 = await client.get("/beatmaker/grooves/issues", params={"limit": "25", "cursor": cursor})
398 assert resp2.status_code == 200
399 assert "isl-empty" not in resp2.text, "Page 2 must not show the empty state"
400 assert "No open issues" not in resp2.text
401
402
403 async def test_issue_list_pagination_timestamp_cursor_falls_back_to_page1(
404 client: AsyncClient,
405 db_session: AsyncSession,
406 ) -> None:
407 """An old timestamp-style cursor (from before the fix) falls back to page 1 gracefully."""
408 repo_id = await _make_repo(db_session)
409 for i in range(5):
410 await _make_issue(db_session, repo_id, number=i + 1, title=f"Issue {i + 1}", state="open")
411 stale_cursor = "2026-05-28T17:54:49.104203+00:00"
412 resp = await client.get("/beatmaker/grooves/issues", params={"cursor": stale_cursor})
413 assert resp.status_code == 200
414 # Should show issues (page 1 fallback), not empty state
415 assert "isl-empty" not in resp.text
416
417
418 # ---------------------------------------------------------------------------
419 # Right sidebar
420 # ---------------------------------------------------------------------------
421
422
423 async def test_issue_list_right_sidebar_present(
424 client: AsyncClient,
425 db_session: AsyncSession,
426 ) -> None:
427 """Right sidebar element is present in the SSR page."""
428 await _make_repo(db_session)
429 body = await _get_page(client)
430 assert "isl-sidebar" in body
431
432
433 async def test_issue_list_labels_summary_heading_present(
434 client: AsyncClient,
435 db_session: AsyncSession,
436 ) -> None:
437 """Labels sidebar section is rendered server-side."""
438 await _make_repo(db_session)
439 body = await _get_page(client)
440 assert "Labels" in body
441
442
443 async def test_issue_list_labels_summary_list_present(
444 client: AsyncClient,
445 db_session: AsyncSession,
446 ) -> None:
447 """Labels sidebar section contains a label list."""
448 await _make_repo(db_session)
449 body = await _get_page(client)
450 assert "Labels" in body
451
452
453 # ---------------------------------------------------------------------------
454 # Filter sidebar elements
455 # ---------------------------------------------------------------------------
456
457
458 async def test_issue_list_filter_sidebar_present(
459 client: AsyncClient,
460 db_session: AsyncSession,
461 ) -> None:
462 """Issue filter form is rendered server-side."""
463 await _make_repo(db_session)
464 body = await _get_page(client)
465 assert 'name="sort"' in body
466
467
468 async def test_issue_list_label_chip_container_present(
469 client: AsyncClient,
470 db_session: AsyncSession,
471 ) -> None:
472 """Label filter select is present in the filter bar."""
473 await _make_repo(db_session)
474 body = await _get_page(client)
475 assert 'name="sort"' in body
476
477
478
479
480 async def test_issue_list_filter_assignee_select_present(
481 client: AsyncClient,
482 db_session: AsyncSession,
483 ) -> None:
484 """Assignee filter <select> appears when assignees exist; sort select always present."""
485 await _make_repo(db_session)
486 body = await _get_page(client)
487 # Sort select is always rendered; assignee select only when data is seeded
488 assert 'name="sort"' in body or "name='sort'" in body
489
490
491 async def test_issue_list_filter_author_input_present(
492 client: AsyncClient,
493 db_session: AsyncSession,
494 ) -> None:
495 """Issue filter form has filter controls (author filter via assignee or label select)."""
496 await _make_repo(db_session)
497 body = await _get_page(client)
498 assert 'name="sort"' in body
499
500
501 async def test_issue_list_sort_radio_group_present(
502 client: AsyncClient,
503 db_session: AsyncSession,
504 ) -> None:
505 """Sort filter <select> element is present (name=sort)."""
506 await _make_repo(db_session)
507 body = await _get_page(client)
508 assert 'name="sort"' in body or "name='sort'" in body
509
510
511 async def test_issue_list_sort_radio_buttons_present(
512 client: AsyncClient,
513 db_session: AsyncSession,
514 ) -> None:
515 """Radio inputs with name='sort' are present (SSR-rendered)."""
516 await _make_repo(db_session)
517 body = await _get_page(client)
518 assert 'name="sort"' in body or "name='sort'" in body
519
520
521 # ---------------------------------------------------------------------------
522 # Template selector / new-issue flow (minimal JS retained)
523 # ---------------------------------------------------------------------------
524
525
526 async def test_issue_list_template_picker_present(
527 client: AsyncClient,
528 db_session: AsyncSession,
529 ) -> None:
530 """template-picker element is present in the page HTML."""
531 await _make_repo(db_session)
532 body = await _get_page(client)
533 assert "template-picker" in body
534
535
536 async def test_issue_list_template_grid_present(
537 client: AsyncClient,
538 db_session: AsyncSession,
539 ) -> None:
540 """Template picker container is rendered server-side."""
541 await _make_repo(db_session)
542 body = await _get_page(client)
543 assert "isl-template-picker" in body
544
545
546 async def test_issue_list_template_cards_present(
547 client: AsyncClient,
548 db_session: AsyncSession,
549 ) -> None:
550 """Template picker card class is present (SSR-rendered template cards)."""
551 await _make_repo(db_session)
552 body = await _get_page(client)
553 assert "isl-tp-card" in body
554
555
556 async def test_issue_list_show_template_picker_js_present(
557 client: AsyncClient,
558 db_session: AsyncSession,
559 ) -> None:
560 """Template picker panel is rendered server-side."""
561 await _make_repo(db_session)
562 body = await _get_page(client)
563 assert "Choose a template" in body
564
565
566 async def test_issue_list_select_template_js_present(
567 client: AsyncClient,
568 db_session: AsyncSession,
569 ) -> None:
570 """Template cards use data-action="select-template" (selectTemplate moved to issue-list.ts)."""
571 await _make_repo(db_session)
572 body = await _get_page(client)
573 assert "select-template" in body
574
575
576 async def test_issue_list_issue_templates_const_present(
577 client: AsyncClient,
578 db_session: AsyncSession,
579 ) -> None:
580 """ISSUE_TEMPLATES is in app.js (TypeScript module); page dispatches issue-list module."""
581 await _make_repo(db_session)
582 body = await _get_page(client)
583 # ISSUE_TEMPLATES moved to app.js; verify page dispatch JSON and template picker HTML
584 assert '"page": "issue-list"' in body
585 assert "template-picker" in body
586
587
588 async def test_issue_list_new_issue_btn_calls_template(
589 client: AsyncClient,
590 db_session: AsyncSession,
591 ) -> None:
592 """New Issue button opens template picker via data-action (showTemplatePicker moved to issue-list.ts)."""
593 await _make_repo(db_session)
594 body = await _get_page(client)
595 assert "New Issue" in body
596 assert "Choose a template" in body
597
598
599 async def test_issue_list_templates_back_btn_present(
600 client: AsyncClient,
601 db_session: AsyncSession,
602 ) -> None:
603 """Template picker is rendered in the new issue flow."""
604 await _make_repo(db_session)
605 body = await _get_page(client)
606 assert "template-picker" in body
607
608
609 async def test_issue_list_blank_template_defined(
610 client: AsyncClient,
611 db_session: AsyncSession,
612 ) -> None:
613 """'blank' template id is present in ISSUE_TEMPLATES."""
614 await _make_repo(db_session)
615 body = await _get_page(client)
616 assert "'blank'" in body or '"blank"' in body
617
618
619 async def test_issue_list_bug_template_defined(
620 client: AsyncClient,
621 db_session: AsyncSession,
622 ) -> None:
623 """'bug' template id is present in ISSUE_TEMPLATES."""
624 await _make_repo(db_session)
625 body = await _get_page(client)
626 assert "'bug'" in body or '"bug"' in body
627
628
629 # ---------------------------------------------------------------------------
630 # Bulk toolbar structure (SSR-rendered, JS-activated)
631 # ---------------------------------------------------------------------------
632
633
634 async def test_issue_list_bulk_toolbar_present(
635 client: AsyncClient,
636 db_session: AsyncSession,
637 ) -> None:
638 """bulk-toolbar element is rendered in the page HTML."""
639 await _make_repo(db_session)
640 body = await _get_page(client)
641 assert "bulk-toolbar" in body
642
643
644 async def test_issue_list_bulk_count_present(
645 client: AsyncClient,
646 db_session: AsyncSession,
647 ) -> None:
648 """bulk-count element is present."""
649 await _make_repo(db_session)
650 body = await _get_page(client)
651 assert "bulk-count" in body
652
653
654 async def test_issue_list_bulk_label_select_present(
655 client: AsyncClient,
656 db_session: AsyncSession,
657 ) -> None:
658 """bulk-label-select element is present."""
659 await _make_repo(db_session)
660 body = await _get_page(client)
661 assert "bulk-label-select" in body
662
663
664 async def test_issue_list_issue_row_checkbox_present(
665 client: AsyncClient,
666 db_session: AsyncSession,
667 ) -> None:
668 """issue-row-check CSS class is present (checkbox for bulk selection)."""
669 repo_id = await _make_repo(db_session)
670 await _make_issue(db_session, repo_id, title="Has checkbox")
671 body = await _get_page(client)
672 assert "issue-row-check" in body
673
674
675 async def test_issue_list_toggle_issue_select_js_present(
676 client: AsyncClient,
677 db_session: AsyncSession,
678 ) -> None:
679 """toggleIssueSelect() is in app.js (TypeScript module); page renders bulk toolbar."""
680 await _make_repo(db_session)
681 body = await _get_page(client)
682 # Function moved to app.js; verify bulk toolbar HTML element is present
683 assert "bulk-toolbar" in body
684
685
686 async def test_issue_list_deselect_all_js_present(
687 client: AsyncClient,
688 db_session: AsyncSession,
689 ) -> None:
690 """Deselect action uses data-bulk-action="deselect" (deselectAll moved to issue-list.ts)."""
691 await _make_repo(db_session)
692 body = await _get_page(client)
693 assert 'data-bulk-action="deselect"' in body
694
695
696 async def test_issue_list_update_bulk_toolbar_js_present(
697 client: AsyncClient,
698 db_session: AsyncSession,
699 ) -> None:
700 """Page renders bulk action buttons (isl-bulk-btn with data-bulk-action attributes)."""
701 await _make_repo(db_session)
702 body = await _get_page(client)
703 assert "isl-bulk-btn" in body
704 assert "data-bulk-action" in body
705
706
707 async def test_issue_list_bulk_close_js_present(
708 client: AsyncClient,
709 db_session: AsyncSession,
710 ) -> None:
711 """Close bulk action uses data-bulk-action="close" (bulkClose moved to issue-list.ts)."""
712 await _make_repo(db_session)
713 body = await _get_page(client)
714 assert 'data-bulk-action="close"' in body
715
716
717 async def test_issue_list_bulk_reopen_js_present(
718 client: AsyncClient,
719 db_session: AsyncSession,
720 ) -> None:
721 """Reopen bulk action uses data-bulk-action="reopen" (bulkReopen moved to issue-list.ts)."""
722 await _make_repo(db_session)
723 body = await _get_page(client)
724 assert 'data-bulk-action="reopen"' in body
725
726
727 async def test_issue_list_bulk_assign_label_js_present(
728 client: AsyncClient,
729 db_session: AsyncSession,
730 ) -> None:
731 """Assign label uses data-bulk-action="assign-label" (bulkAssignLabel moved to issue-list.ts)."""
732 await _make_repo(db_session)
733 body = await _get_page(client)
734 assert 'data-bulk-action="assign-label"' in body
735
736
737 async def test_issue_list_full_page_contains_html_wrapper(
738 client: AsyncClient,
739 db_session: AsyncSession,
740 ) -> None:
741 """Direct browser navigation (no HX-Request) returns a full HTML page with <html> tag."""
742 await _make_repo(db_session)
743 resp = await client.get("/beatmaker/grooves/issues")
744 assert resp.status_code == 200
745 assert "<html" in resp.text
File History 1 commit
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 3 days ago