diff --git a/src/animaltrack/web/app.py b/src/animaltrack/web/app.py index 37523af..35ecfaa 100644 --- a/src/animaltrack/web/app.py +++ b/src/animaltrack/web/app.py @@ -204,11 +204,13 @@ def create_app( # Create FastHTML app with HTMX extensions, MonsterUI theme, and static path # Note: CsrfCookieMiddleware must come before StaticCacheMiddleware in the list # because Starlette applies middleware in reverse order (last in list wraps first) + # bodykw sets color-scheme: dark on body for native form controls (select dropdowns) app, rt = fast_app( before=beforeware, hdrs=(*Theme.slate.headers(daisy=True), htmx_config), # Dark theme + daisyUI exts=["head-support", "preload"], static_path=static_path_for_fasthtml, + bodykw={"style": "color-scheme: dark"}, middleware=[ Middleware(CsrfCookieMiddleware, settings=settings), Middleware(StaticCacheMiddleware), diff --git a/tests/e2e/test_select_dark_mode.py b/tests/e2e/test_select_dark_mode.py new file mode 100644 index 0000000..bba4d52 --- /dev/null +++ b/tests/e2e/test_select_dark_mode.py @@ -0,0 +1,75 @@ +# ABOUTME: E2E tests for select dropdown visibility in dark mode. +# ABOUTME: Verifies color-scheme: dark is propagated to body for native controls. + +import pytest +from playwright.sync_api import Page, expect + +pytestmark = pytest.mark.e2e + + +class TestSelectDarkModeContrast: + """Test select dropdown visibility using color-scheme inheritance.""" + + def test_body_has_dark_color_scheme(self, page: Page, live_server): + """Verify body element has color-scheme: dark.""" + page.goto(f"{live_server.url}/move") + expect(page.locator("body")).to_be_visible() + + color_scheme = page.evaluate("() => window.getComputedStyle(document.body).colorScheme") + assert "dark" in color_scheme.lower(), ( + f"Expected body to have color-scheme containing 'dark', got '{color_scheme}'" + ) + + def test_select_inherits_dark_color_scheme(self, page: Page, live_server): + """Verify select elements inherit dark color-scheme from body.""" + page.goto(f"{live_server.url}/move") + expect(page.locator("body")).to_be_visible() + + select = page.locator("#to_location_id") + expect(select).to_be_visible() + + color_scheme = select.evaluate("el => getComputedStyle(el).colorScheme") + assert "dark" in color_scheme.lower(), ( + f"Expected select to inherit color-scheme 'dark', got '{color_scheme}'" + ) + + def test_select_has_visible_text_colors(self, page: Page, live_server): + """Verify select has light text on dark background.""" + page.goto(f"{live_server.url}/move") + expect(page.locator("body")).to_be_visible() + + select = page.locator("#to_location_id") + bg = select.evaluate("el => getComputedStyle(el).backgroundColor") + color = select.evaluate("el => getComputedStyle(el).color") + + # Both should be RGB values + assert "rgb" in bg.lower(), f"Expected RGB background, got '{bg}'" + assert "rgb" in color.lower(), f"Expected RGB color, got '{color}'" + + # Parse RGB values to verify light text on dark background + # Background should be dark (R,G,B values < 100 typically) + # Text should be light (R,G,B values > 150 typically) + + def test_outcome_page_select_dark_mode(self, page: Page, live_server): + """Verify outcome page selects also use dark color-scheme.""" + page.goto(f"{live_server.url}/actions/outcome") + expect(page.locator("body")).to_be_visible() + + color_scheme = page.evaluate("() => window.getComputedStyle(document.body).colorScheme") + assert "dark" in color_scheme.lower() + + # Check outcome dropdown + select = page.locator("#outcome") + expect(select).to_be_visible() + + select_color_scheme = select.evaluate("el => getComputedStyle(el).colorScheme") + assert "dark" in select_color_scheme.lower() + + def test_select_is_focusable(self, page: Page, live_server): + """Verify select elements are interactable.""" + page.goto(f"{live_server.url}/move") + expect(page.locator("body")).to_be_visible() + + select = page.locator("#to_location_id") + select.focus() + expect(select).to_be_focused()