//! HTMX integration tests: verify that HTMX-aware routes return the expected //! HTML fragments, headers, and status codes. use crate::harness::TestHarness; use serde_json::Value; use uuid::Uuid; // ============================================================================= // Dashboard Tabs // ============================================================================= #[tokio::test] async fn dashboard_tabs_return_html_fragments() { let mut h = TestHarness::new().await; let _user_id = h.signup("tabuser", "tab@example.com", "password123").await; let tabs = ["details", "payments", "projects", "creator"]; for tab in tabs { let resp = h .client .htmx_get(&format!("/dashboard/tabs/{}", tab)) .await; assert_eq!( resp.status, 200, "Dashboard tab '{}' should return 200, got {}", tab, resp.status ); // Each tab returns an HTML fragment (not a full page with ) assert!( !resp.text.contains("("SELECT id FROM items WHERE project_id = $1") .bind(project_id.parse::().unwrap()) .fetch_all(&h.db) .await .unwrap(); for iid in &items_list { h.client .put_form(&format!("/api/items/{}", iid), "is_public=true") .await; } // HTMX GET with item_type filter let resp = h.client.htmx_get("/discover/results?item_type=audio").await; assert_eq!(resp.status, 200, "Filtered discover should return 200"); // Should contain results HTML assert!( resp.text.contains("results-container") || resp.text.contains("results-table"), "Filtered discover results should contain HTML structure" ); } #[tokio::test] async fn discover_pagination() { let mut h = TestHarness::new().await; // Request page 2 -- even with no data, should return valid pagination HTML let resp = h.client.htmx_get("/discover/results?page=2").await; assert_eq!(resp.status, 200, "Discover page 2 should return 200"); // Should contain the pagination area and results structure assert!( resp.text.contains("results-container") || resp.text.contains("results-table"), "Paginated discover results should contain HTML structure" ); // Should contain the page info assert!( resp.text.contains("Showing"), "Paginated results should contain 'Showing' text" ); } #[tokio::test] async fn discover_huge_page_does_not_overflow() { let mut h = TestHarness::new().await; // `(page - 1) * DISCOVER_PAGE_SIZE` used to be computed in u32; a page this // large overflows u32 (and, with overflow-checks on in the test profile, // panicked into a 500). The offset/label math is now i64-widened and // saturating, so a hostile page just yields an empty, well-formed page. let resp = h.client.htmx_get("/discover/results?page=200000000").await; assert_eq!(resp.status, 200, "huge ?page must not overflow into a 500: {}", resp.text); } // ============================================================================= // Inline Editing // ============================================================================= #[tokio::test] async fn edit_row_returns_form() { let mut h = TestHarness::new().await; let item_id = h.create_creator_with_item("htmxuser", "audio", 500).await.item_id; let resp = h .client .htmx_get(&format!("/dashboard/item/{}/edit-row", item_id)) .await; assert_eq!(resp.status, 200, "Edit row should return 200"); // Should contain form elements assert!( resp.text.contains("edit-row"), "Edit row should contain edit-row class" ); assert!( resp.text.contains("name=\"title\""), "Edit row should contain title input" ); } #[tokio::test] async fn item_update_returns_json() { let mut h = TestHarness::new().await; let item_id = h.create_creator_with_item("htmxuser", "audio", 500).await.item_id; // The update_item handler returns JSON for non-HTMX requests let resp = h .client .put_form( &format!("/api/items/{}", item_id), "title=Updated+Title", ) .await; assert_eq!( resp.status, 200, "Item update should return 200, got {} {}", resp.status, resp.text ); let body: Value = resp.json(); assert_eq!(body["title"], "Updated Title"); } #[tokio::test] async fn item_update_nonexistent_returns_error() { let mut h = TestHarness::new().await; let _ = h.create_creator_with_item("htmxuser", "audio", 500).await; // Try to update a non-existent item let fake_id = Uuid::new_v4(); let resp = h .client .htmx_put_form( &format!("/api/items/{}", fake_id), "title=Nope", ) .await; assert_eq!( resp.status, 404, "Updating non-existent item should return 404, got {} {}", resp.status, resp.text ); } // ============================================================================= // Tag Operations // ============================================================================= #[tokio::test] async fn add_tag_returns_html() { let mut h = TestHarness::new().await; let item_id = h.create_creator_with_item("htmxuser", "audio", 500).await.item_id; // Insert a leaf tag (depth >= 3) directly in the database let tag_id = Uuid::new_v4(); sqlx::query("INSERT INTO tags (id, name, slug, sort_order, path) VALUES ($1, $2, $3, 0, $4)") .bind(tag_id) .bind("TestTag") .bind("audio.genre.testtag") .bind("audio.genre.testtag") .execute(&h.db) .await .expect("Failed to insert tag"); // HTMX POST to add the tag let resp = h .client .htmx_post_form( &format!("/api/items/{}/tags", item_id), &format!("tag_id={}", tag_id), ) .await; assert_eq!(resp.status, 200, "Add tag should return 200, got {} {}", resp.status, resp.text); // Should return rendered TagTemplate HTML assert!( resp.text.contains("tag"), "Add tag response should contain tag markup" ); assert!( resp.text.contains("TestTag"), "Add tag response should contain the tag name" ); } #[tokio::test] async fn tag_suggestions_returns_html() { let mut h = TestHarness::new().await; let item_id = h.create_creator_with_item("htmxuser", "audio", 500).await.item_id; // The tags table may already have seeded tags. Request suggestions for the // item -- the handler matches tags based on item title/description/type. // It returns either an HTML fragment with suggestions or empty HTML. let resp = h .client .htmx_get(&format!("/api/items/{}/tag-suggestions", item_id)) .await; assert_eq!( resp.status, 200, "Tag suggestions should return 200, got {}", resp.status ); // Response is valid HTML (possibly empty if no tags match) } // ============================================================================= // Delete + Toast // ============================================================================= #[tokio::test] async fn delete_item_returns_toast() { let mut h = TestHarness::new().await; let item_id = h.create_creator_with_item("htmxuser", "audio", 500).await.item_id; let resp = h .client .htmx_delete(&format!("/api/items/{}", item_id)) .await; assert!( resp.status.is_success(), "Delete item should succeed, got {} {}", resp.status, resp.text ); // delete_item always returns HX-Trigger with showToast (no HTMX check needed) let trigger = resp.header("HX-Trigger").expect("Should have HX-Trigger header"); assert!( trigger.contains("showToast"), "HX-Trigger should contain showToast, got: {}", trigger ); assert!( trigger.contains("success"), "Toast should be success type, got: {}", trigger ); } #[tokio::test] async fn delete_link_returns_toast() { let mut h = TestHarness::new().await; let _user_id = h.signup("linkdel", "linkdel@example.com", "password123").await; // Create a link first via HTMX POST let resp = h .client .htmx_post_form( "/api/links", "url=https%3A%2F%2Fexample.com&title=My+Link", ) .await; assert!( resp.status.is_success(), "Create link should succeed, got {} {}", resp.status, resp.text ); // The HTMX response is HTML (link_row), extract the link ID from data-id attribute let link_id = resp .text .split("data-id=\"") .nth(1) .and_then(|s| s.split('"').next()) .expect("Link row should have data-id attribute"); // Delete with HTMX let resp = h .client .htmx_delete(&format!("/api/links/{}", link_id)) .await; assert!( resp.status.is_success(), "Delete link should succeed, got {} {}", resp.status, resp.text ); let trigger = resp.header("HX-Trigger").expect("Should have HX-Trigger header"); assert!( trigger.contains("showToast"), "HX-Trigger should contain showToast, got: {}", trigger ); assert!( trigger.contains("Link removed"), "Toast message should say 'Link removed', got: {}", trigger ); } // ============================================================================= // Form Loading // ============================================================================= #[tokio::test] async fn old_modal_form_routes_return_404() { let mut h = TestHarness::new().await; let _user_id = h.create_creator("formuser").await; // Old modal form routes removed in favour of creation wizards let resp = h.client.htmx_get("/dashboard/new-project-form").await; assert_eq!(resp.status, 404, "Old project form route should be gone"); let resp = h .client .htmx_get("/dashboard/project/anything/new-item-form") .await; assert_eq!(resp.status, 404, "Old item form route should be gone"); }