From 282ad9b4d7609a4da4461ce5a8d2b34ee93fa449 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Sat, 24 Jan 2026 11:03:34 +0000 Subject: [PATCH] Fix select dropdown dark mode visibility by setting color-scheme on body Browsers need color-scheme: dark on the document (html/body) to properly style native form controls like select dropdown options. Previously, color-scheme was only set on select elements themselves, which didn't propagate to the OS-rendered dropdown options. Added bodykw to fast_app() to set color-scheme: dark on body element. This tells the browser the entire page prefers dark mode, and native controls use dark system colors. Includes E2E tests verifying body and select elements have dark color-scheme. Co-Authored-By: Claude Opus 4.5 --- src/animaltrack/web/app.py | 2 + tests/e2e/test_select_dark_mode.py | 75 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 tests/e2e/test_select_dark_mode.py 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()