gabriel / musehub public
test_musehub_ui_issue_list_enhanced.py python
742 lines 24.1 KB
Raw
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 35 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_empty_label_param_shows_all_issues(
363 client: AsyncClient,
364 db_session: AsyncSession,
365 ) -> None:
366 """IL_01 — label='' (empty string) must behave the same as no label filter.
367
368 Regression: the Next button preserves label= in the URL when no label is
369 selected. An empty string must not be forwarded to list_issues as a literal
370 label filter — it matches zero issues and produces a false empty page.
371 """
372 repo_id = await _make_repo(db_session)
373 await _make_issue(db_session, repo_id, number=1, title="Alpha issue", labels=["bug"])
374 await _make_issue(db_session, repo_id, number=2, title="Beta issue", labels=["enhancement"])
375 # label="" (empty string) — what the Next button generates when no filter is active
376 body = await _get_page(client, label="")
377 assert "Alpha issue" in body
378 assert "Beta issue" in body
379
380
381 async def test_issue_list_pagination_next_page_works_with_empty_label(
382 client: AsyncClient,
383 db_session: AsyncSession,
384 ) -> None:
385 """IL_02 — the second page must contain issues when label= is empty.
386
387 Regression: navigating to the next page with label= in the URL returned
388 'No open issues' because the empty string was passed as a literal label
389 filter to list_issues, yielding zero results.
390 """
391 repo_id = await _make_repo(db_session)
392 for i in range(30):
393 await _make_issue(db_session, repo_id, number=i + 1, title=f"Issue {i + 1}", state="open")
394 # Get page 1 to find the next_cursor
395 resp1 = await client.get("/beatmaker/grooves/issues", params={"limit": "25", "label": ""})
396 assert resp1.status_code == 200
397 body1 = resp1.text
398 # Page 1 should have a Next link
399 assert "Next" in body1 or "cursor=" in body1
400 # Extract cursor from the Next link href
401 import re
402 match = re.search(r'cursor=([^&"\']+)', body1)
403 assert match, "No cursor found in page 1 — Next link missing"
404 cursor = match.group(1)
405 # Page 2 with label="" must return issues, not an empty state
406 resp2 = await client.get(
407 "/beatmaker/grooves/issues",
408 params={"limit": "25", "label": "", "cursor": cursor},
409 )
410 assert resp2.status_code == 200
411 assert "isl-empty" not in resp2.text
412 assert "Issue " in resp2.text
413
414
415 # ---------------------------------------------------------------------------
416 # Right sidebar
417 # ---------------------------------------------------------------------------
418
419
420 async def test_issue_list_right_sidebar_present(
421 client: AsyncClient,
422 db_session: AsyncSession,
423 ) -> None:
424 """Right sidebar element is present in the SSR page."""
425 await _make_repo(db_session)
426 body = await _get_page(client)
427 assert "isl-sidebar" in body
428
429
430 async def test_issue_list_labels_summary_heading_present(
431 client: AsyncClient,
432 db_session: AsyncSession,
433 ) -> None:
434 """Labels sidebar section is rendered server-side."""
435 await _make_repo(db_session)
436 body = await _get_page(client)
437 assert "Labels" in body
438
439
440 async def test_issue_list_labels_summary_list_present(
441 client: AsyncClient,
442 db_session: AsyncSession,
443 ) -> None:
444 """Labels sidebar section contains a label list."""
445 await _make_repo(db_session)
446 body = await _get_page(client)
447 assert "Labels" in body
448
449
450 # ---------------------------------------------------------------------------
451 # Filter sidebar elements
452 # ---------------------------------------------------------------------------
453
454
455 async def test_issue_list_filter_sidebar_present(
456 client: AsyncClient,
457 db_session: AsyncSession,
458 ) -> None:
459 """Issue filter form is rendered server-side."""
460 await _make_repo(db_session)
461 body = await _get_page(client)
462 assert 'name="sort"' in body
463
464
465 async def test_issue_list_label_chip_container_present(
466 client: AsyncClient,
467 db_session: AsyncSession,
468 ) -> None:
469 """Label filter select is present in the filter bar."""
470 await _make_repo(db_session)
471 body = await _get_page(client)
472 assert 'name="sort"' in body
473
474
475
476
477 async def test_issue_list_filter_assignee_select_present(
478 client: AsyncClient,
479 db_session: AsyncSession,
480 ) -> None:
481 """Assignee filter <select> appears when assignees exist; sort select always present."""
482 await _make_repo(db_session)
483 body = await _get_page(client)
484 # Sort select is always rendered; assignee select only when data is seeded
485 assert 'name="sort"' in body or "name='sort'" in body
486
487
488 async def test_issue_list_filter_author_input_present(
489 client: AsyncClient,
490 db_session: AsyncSession,
491 ) -> None:
492 """Issue filter form has filter controls (author filter via assignee or label select)."""
493 await _make_repo(db_session)
494 body = await _get_page(client)
495 assert 'name="sort"' in body
496
497
498 async def test_issue_list_sort_radio_group_present(
499 client: AsyncClient,
500 db_session: AsyncSession,
501 ) -> None:
502 """Sort filter <select> element is present (name=sort)."""
503 await _make_repo(db_session)
504 body = await _get_page(client)
505 assert 'name="sort"' in body or "name='sort'" in body
506
507
508 async def test_issue_list_sort_radio_buttons_present(
509 client: AsyncClient,
510 db_session: AsyncSession,
511 ) -> None:
512 """Radio inputs with name='sort' are present (SSR-rendered)."""
513 await _make_repo(db_session)
514 body = await _get_page(client)
515 assert 'name="sort"' in body or "name='sort'" in body
516
517
518 # ---------------------------------------------------------------------------
519 # Template selector / new-issue flow (minimal JS retained)
520 # ---------------------------------------------------------------------------
521
522
523 async def test_issue_list_template_picker_present(
524 client: AsyncClient,
525 db_session: AsyncSession,
526 ) -> None:
527 """template-picker element is present in the page HTML."""
528 await _make_repo(db_session)
529 body = await _get_page(client)
530 assert "template-picker" in body
531
532
533 async def test_issue_list_template_grid_present(
534 client: AsyncClient,
535 db_session: AsyncSession,
536 ) -> None:
537 """Template picker container is rendered server-side."""
538 await _make_repo(db_session)
539 body = await _get_page(client)
540 assert "isl-template-picker" in body
541
542
543 async def test_issue_list_template_cards_present(
544 client: AsyncClient,
545 db_session: AsyncSession,
546 ) -> None:
547 """Template picker card class is present (SSR-rendered template cards)."""
548 await _make_repo(db_session)
549 body = await _get_page(client)
550 assert "isl-tp-card" in body
551
552
553 async def test_issue_list_show_template_picker_js_present(
554 client: AsyncClient,
555 db_session: AsyncSession,
556 ) -> None:
557 """Template picker panel is rendered server-side."""
558 await _make_repo(db_session)
559 body = await _get_page(client)
560 assert "Choose a template" in body
561
562
563 async def test_issue_list_select_template_js_present(
564 client: AsyncClient,
565 db_session: AsyncSession,
566 ) -> None:
567 """Template cards use data-action="select-template" (selectTemplate moved to issue-list.ts)."""
568 await _make_repo(db_session)
569 body = await _get_page(client)
570 assert "select-template" in body
571
572
573 async def test_issue_list_issue_templates_const_present(
574 client: AsyncClient,
575 db_session: AsyncSession,
576 ) -> None:
577 """ISSUE_TEMPLATES is in app.js (TypeScript module); page dispatches issue-list module."""
578 await _make_repo(db_session)
579 body = await _get_page(client)
580 # ISSUE_TEMPLATES moved to app.js; verify page dispatch JSON and template picker HTML
581 assert '"page": "issue-list"' in body
582 assert "template-picker" in body
583
584
585 async def test_issue_list_new_issue_btn_calls_template(
586 client: AsyncClient,
587 db_session: AsyncSession,
588 ) -> None:
589 """New Issue button opens template picker via data-action (showTemplatePicker moved to issue-list.ts)."""
590 await _make_repo(db_session)
591 body = await _get_page(client)
592 assert "New Issue" in body
593 assert "Choose a template" in body
594
595
596 async def test_issue_list_templates_back_btn_present(
597 client: AsyncClient,
598 db_session: AsyncSession,
599 ) -> None:
600 """Template picker is rendered in the new issue flow."""
601 await _make_repo(db_session)
602 body = await _get_page(client)
603 assert "template-picker" in body
604
605
606 async def test_issue_list_blank_template_defined(
607 client: AsyncClient,
608 db_session: AsyncSession,
609 ) -> None:
610 """'blank' template id is present in ISSUE_TEMPLATES."""
611 await _make_repo(db_session)
612 body = await _get_page(client)
613 assert "'blank'" in body or '"blank"' in body
614
615
616 async def test_issue_list_bug_template_defined(
617 client: AsyncClient,
618 db_session: AsyncSession,
619 ) -> None:
620 """'bug' template id is present in ISSUE_TEMPLATES."""
621 await _make_repo(db_session)
622 body = await _get_page(client)
623 assert "'bug'" in body or '"bug"' in body
624
625
626 # ---------------------------------------------------------------------------
627 # Bulk toolbar structure (SSR-rendered, JS-activated)
628 # ---------------------------------------------------------------------------
629
630
631 async def test_issue_list_bulk_toolbar_present(
632 client: AsyncClient,
633 db_session: AsyncSession,
634 ) -> None:
635 """bulk-toolbar element is rendered in the page HTML."""
636 await _make_repo(db_session)
637 body = await _get_page(client)
638 assert "bulk-toolbar" in body
639
640
641 async def test_issue_list_bulk_count_present(
642 client: AsyncClient,
643 db_session: AsyncSession,
644 ) -> None:
645 """bulk-count element is present."""
646 await _make_repo(db_session)
647 body = await _get_page(client)
648 assert "bulk-count" in body
649
650
651 async def test_issue_list_bulk_label_select_present(
652 client: AsyncClient,
653 db_session: AsyncSession,
654 ) -> None:
655 """bulk-label-select element is present."""
656 await _make_repo(db_session)
657 body = await _get_page(client)
658 assert "bulk-label-select" in body
659
660
661 async def test_issue_list_issue_row_checkbox_present(
662 client: AsyncClient,
663 db_session: AsyncSession,
664 ) -> None:
665 """issue-row-check CSS class is present (checkbox for bulk selection)."""
666 repo_id = await _make_repo(db_session)
667 await _make_issue(db_session, repo_id, title="Has checkbox")
668 body = await _get_page(client)
669 assert "issue-row-check" in body
670
671
672 async def test_issue_list_toggle_issue_select_js_present(
673 client: AsyncClient,
674 db_session: AsyncSession,
675 ) -> None:
676 """toggleIssueSelect() is in app.js (TypeScript module); page renders bulk toolbar."""
677 await _make_repo(db_session)
678 body = await _get_page(client)
679 # Function moved to app.js; verify bulk toolbar HTML element is present
680 assert "bulk-toolbar" in body
681
682
683 async def test_issue_list_deselect_all_js_present(
684 client: AsyncClient,
685 db_session: AsyncSession,
686 ) -> None:
687 """Deselect action uses data-bulk-action="deselect" (deselectAll moved to issue-list.ts)."""
688 await _make_repo(db_session)
689 body = await _get_page(client)
690 assert 'data-bulk-action="deselect"' in body
691
692
693 async def test_issue_list_update_bulk_toolbar_js_present(
694 client: AsyncClient,
695 db_session: AsyncSession,
696 ) -> None:
697 """Page renders bulk action buttons (isl-bulk-btn with data-bulk-action attributes)."""
698 await _make_repo(db_session)
699 body = await _get_page(client)
700 assert "isl-bulk-btn" in body
701 assert "data-bulk-action" in body
702
703
704 async def test_issue_list_bulk_close_js_present(
705 client: AsyncClient,
706 db_session: AsyncSession,
707 ) -> None:
708 """Close bulk action uses data-bulk-action="close" (bulkClose moved to issue-list.ts)."""
709 await _make_repo(db_session)
710 body = await _get_page(client)
711 assert 'data-bulk-action="close"' in body
712
713
714 async def test_issue_list_bulk_reopen_js_present(
715 client: AsyncClient,
716 db_session: AsyncSession,
717 ) -> None:
718 """Reopen bulk action uses data-bulk-action="reopen" (bulkReopen moved to issue-list.ts)."""
719 await _make_repo(db_session)
720 body = await _get_page(client)
721 assert 'data-bulk-action="reopen"' in body
722
723
724 async def test_issue_list_bulk_assign_label_js_present(
725 client: AsyncClient,
726 db_session: AsyncSession,
727 ) -> None:
728 """Assign label uses data-bulk-action="assign-label" (bulkAssignLabel moved to issue-list.ts)."""
729 await _make_repo(db_session)
730 body = await _get_page(client)
731 assert 'data-bulk-action="assign-label"' in body
732
733
734 async def test_issue_list_full_page_contains_html_wrapper(
735 client: AsyncClient,
736 db_session: AsyncSession,
737 ) -> None:
738 """Direct browser navigation (no HX-Request) returns a full HTML page with <html> tag."""
739 await _make_repo(db_session)
740 resp = await client.get("/beatmaker/grooves/issues")
741 assert resp.status_code == 200
742 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 35 days ago