mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-03 14:13:26 +00:00
feat(web): Auto-refresh dashboard and clean up HTMX inheritance (#49)
This commit is contained in:
@@ -195,6 +195,27 @@ class TestHTMXSidebarLoading:
|
||||
assert "radarr" in content
|
||||
assert "jellyfin" in content
|
||||
|
||||
def test_dashboard_content_persists_after_sidebar_loads(
|
||||
self, page: Page, server_url: str
|
||||
) -> None:
|
||||
"""Dashboard content must remain visible after HTMX loads sidebar.
|
||||
|
||||
Regression test: conflicting hx-select attributes on the nav element
|
||||
were causing the dashboard to disappear when sidebar loaded.
|
||||
"""
|
||||
page.goto(server_url)
|
||||
|
||||
# Dashboard content should be visible immediately (server-rendered)
|
||||
stats = page.locator("#stats-cards")
|
||||
assert stats.is_visible()
|
||||
|
||||
# Wait for sidebar to fully load via HTMX
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
|
||||
# Dashboard content must STILL be visible after sidebar loads
|
||||
assert stats.is_visible(), "Dashboard disappeared after sidebar loaded"
|
||||
assert page.locator("#stats-cards .card").count() >= 4
|
||||
|
||||
def test_sidebar_shows_running_status(self, page: Page, server_url: str) -> None:
|
||||
"""Sidebar shows running/stopped status indicators for services."""
|
||||
page.goto(server_url)
|
||||
@@ -736,3 +757,146 @@ class TestKeyboardShortcuts:
|
||||
"document.querySelector('#save-config-btn')?.textContent?.includes('Saved')",
|
||||
timeout=5000,
|
||||
)
|
||||
|
||||
|
||||
class TestContentStability:
|
||||
"""Test that HTMX operations don't accidentally destroy other page content.
|
||||
|
||||
These tests verify that when one element updates, other elements remain stable.
|
||||
This catches bugs where HTMX attributes (hx-select, hx-swap-oob, etc.) are
|
||||
misconfigured and cause unintended side effects.
|
||||
"""
|
||||
|
||||
def test_all_dashboard_sections_visible_after_full_load(
|
||||
self, page: Page, server_url: str
|
||||
) -> None:
|
||||
"""All dashboard sections remain visible after HTMX completes loading."""
|
||||
page.goto(server_url)
|
||||
|
||||
# Wait for all HTMX requests to complete
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# All major dashboard sections must be visible
|
||||
assert page.locator("#stats-cards").is_visible(), "Stats cards missing"
|
||||
assert page.locator("#stats-cards .card").count() >= 4, "Stats incomplete"
|
||||
assert page.locator("#pending-operations").is_visible(), "Pending ops missing"
|
||||
assert page.locator("#services-by-host").is_visible(), "Services by host missing"
|
||||
assert page.locator("#sidebar-services").is_visible(), "Sidebar missing"
|
||||
|
||||
def test_sidebar_persists_after_navigation_and_back(self, page: Page, server_url: str) -> None:
|
||||
"""Sidebar content persists through navigation cycle."""
|
||||
page.goto(server_url)
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
|
||||
# Remember sidebar state
|
||||
initial_count = page.locator("#sidebar-services li").count()
|
||||
assert initial_count == 4
|
||||
|
||||
# Navigate away
|
||||
page.locator("#sidebar-services a", has_text="plex").click()
|
||||
page.wait_for_url("**/service/plex", timeout=5000)
|
||||
|
||||
# Sidebar should still be there with same content
|
||||
assert page.locator("#sidebar-services").is_visible()
|
||||
assert page.locator("#sidebar-services li").count() == initial_count
|
||||
|
||||
# Navigate back
|
||||
page.go_back()
|
||||
page.wait_for_url(server_url, timeout=5000)
|
||||
|
||||
# Sidebar still intact
|
||||
assert page.locator("#sidebar-services").is_visible()
|
||||
assert page.locator("#sidebar-services li").count() == initial_count
|
||||
|
||||
def test_dashboard_sections_persist_after_save(self, page: Page, server_url: str) -> None:
|
||||
"""Dashboard sections remain after save triggers cf:refresh event."""
|
||||
page.goto(server_url)
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
|
||||
# Capture initial state - all must be visible
|
||||
assert page.locator("#stats-cards").is_visible()
|
||||
assert page.locator("#pending-operations").is_visible()
|
||||
assert page.locator("#services-by-host").is_visible()
|
||||
|
||||
# Trigger save (which dispatches cf:refresh)
|
||||
page.locator("#save-config-btn").click()
|
||||
page.wait_for_function(
|
||||
"document.querySelector('#save-config-btn')?.textContent?.includes('Saved')",
|
||||
timeout=5000,
|
||||
)
|
||||
|
||||
# Wait for refresh requests to complete
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# All sections must still be visible
|
||||
assert page.locator("#stats-cards").is_visible(), "Stats disappeared after save"
|
||||
assert page.locator("#pending-operations").is_visible(), "Pending disappeared"
|
||||
assert page.locator("#services-by-host").is_visible(), "Services disappeared"
|
||||
assert page.locator("#sidebar-services").is_visible(), "Sidebar disappeared"
|
||||
|
||||
def test_filter_state_not_affected_by_other_htmx_requests(
|
||||
self, page: Page, server_url: str
|
||||
) -> None:
|
||||
"""Sidebar filter state persists during other HTMX activity."""
|
||||
page.goto(server_url)
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
|
||||
# Apply a filter
|
||||
filter_input = page.locator("#sidebar-filter")
|
||||
filter_input.fill("plex")
|
||||
filter_input.dispatch_event("keyup")
|
||||
|
||||
# Verify filter is applied
|
||||
assert page.locator("#sidebar-services li:not([hidden])").count() == 1
|
||||
|
||||
# Trigger a save (causes cf:refresh on multiple elements)
|
||||
page.locator("#save-config-btn").click()
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
# Filter input should still have our text
|
||||
# (Note: sidebar reloads so filter clears - this tests the sidebar reload works)
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
assert page.locator("#sidebar-services").is_visible()
|
||||
|
||||
def test_main_content_not_affected_by_sidebar_refresh(
|
||||
self, page: Page, server_url: str
|
||||
) -> None:
|
||||
"""Main content area stays intact when sidebar refreshes."""
|
||||
page.goto(server_url)
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
|
||||
# Get main content text
|
||||
main_content = page.locator("#main-content")
|
||||
initial_text = main_content.inner_text()
|
||||
assert "Compose Farm" in initial_text
|
||||
|
||||
# Trigger cf:refresh (which refreshes sidebar)
|
||||
page.evaluate("document.body.dispatchEvent(new CustomEvent('cf:refresh'))")
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
# Main content should be unchanged (same page, just refreshed partials)
|
||||
assert "Compose Farm" in main_content.inner_text()
|
||||
assert page.locator("#stats-cards").is_visible()
|
||||
|
||||
def test_no_duplicate_elements_after_multiple_refreshes(
|
||||
self, page: Page, server_url: str
|
||||
) -> None:
|
||||
"""Multiple refresh cycles don't create duplicate elements."""
|
||||
page.goto(server_url)
|
||||
page.wait_for_selector("#sidebar-services", timeout=5000)
|
||||
|
||||
# Count initial elements
|
||||
initial_stat_count = page.locator("#stats-cards .card").count()
|
||||
initial_service_count = page.locator("#sidebar-services li").count()
|
||||
|
||||
# Trigger multiple refreshes
|
||||
for _ in range(3):
|
||||
page.evaluate("document.body.dispatchEvent(new CustomEvent('cf:refresh'))")
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# Counts should be same (no duplicates created)
|
||||
assert page.locator("#stats-cards .card").count() == initial_stat_count
|
||||
assert page.locator("#sidebar-services li").count() == initial_service_count
|
||||
|
||||
Reference in New Issue
Block a user