"""Tests for Fenris theme presets (issue #80). Covers: - Three valid presets: Amber, Nord, High Contrast - Amber is the default with amber graph role - Theme roles for graph rendering (allocated, unallocated, gap, zero, partial) - Status semantic colours/glyphs/text always win over theme - Global action reachability and focus contrast in every preset """ import pytest import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from fenris.themes import ( THEMES, THEME_NAMES, get_theme, get_graph_colors, STATUS_COLORS, ) # --------------------------------------------------------------------------- # Theme registry tests # --------------------------------------------------------------------------- class TestThemeRegistry: """All three presets are registered with correct names.""" def test_three_themes_registered(self): """Exactly three themes exist.""" assert len(THEMES) == 3 def test_theme_names(self): """Theme names are amber, nord, high_contrast.""" assert THEME_NAMES == {"amber", "nord", "high_contrast"} def test_get_theme_valid(self): """get_theme returns a Theme for each valid name.""" from textual.theme import Theme for name in THEME_NAMES: theme = get_theme(name) assert isinstance(theme, Theme) def test_get_theme_invalid_returns_amber(self): """Unknown theme name returns the amber theme.""" theme = get_theme("nonexistent") assert theme.name == "fenris-amber" # --------------------------------------------------------------------------- # Amber theme tests (default, amber graph role) # --------------------------------------------------------------------------- class TestAmberTheme: """Amber is the default theme with warm tones.""" def test_amber_is_dark(self): """Amber is a dark theme.""" theme = get_theme("amber") assert theme.dark is True def test_amber_primary_is_amber(self): """Primary colour is warm amber.""" theme = get_theme("amber") assert "d4a017" in theme.primary.lower() or "amber" in theme.primary.lower() def test_amber_has_graph_colors(self): """Amber defines all required graph role variables.""" colors = get_graph_colors("amber") assert "allocated" in colors assert "unallocated" in colors assert "gap" in colors assert "zero" in colors assert "partial" in colors # --------------------------------------------------------------------------- # Nord theme tests # --------------------------------------------------------------------------- class TestNordTheme: """Nord uses the polar night palette.""" def test_nord_is_dark(self): """Nord is a dark theme.""" theme = get_theme("nord") assert theme.dark is True def test_nord_has_graph_colors(self): """Nord defines all required graph role variables.""" colors = get_graph_colors("nord") assert "allocated" in colors assert "unallocated" in colors assert "gap" in colors assert "zero" in colors assert "partial" in colors # --------------------------------------------------------------------------- # High Contrast theme tests # --------------------------------------------------------------------------- class TestHighContrastTheme: """High Contrast for maximum readability.""" def test_high_contrast_is_dark(self): """High contrast is a dark theme.""" theme = get_theme("high_contrast") assert theme.dark is True def test_high_contrast_foreground_is_white(self): """Foreground is pure white for maximum contrast.""" theme = get_theme("high_contract") if False else get_theme("high_contrast") assert theme.foreground is not None def test_high_contrast_has_graph_colors(self): """High contrast defines all required graph role variables.""" colors = get_graph_colors("high_contrast") assert "allocated" in colors assert "unallocated" in colors assert "gap" in colors assert "zero" in colors assert "partial" in colors # --------------------------------------------------------------------------- # Status semantic colours override theme (AC80-1) # --------------------------------------------------------------------------- class TestStatusSemanticOverride: """Status semantic colours/glyphs/text always win over theme styling.""" def test_status_colors_defined(self): """STATUS_COLORS maps each StatusState to a fixed colour.""" from fenris.status_composition import StatusState for state in StatusState: assert state.value in STATUS_COLORS def test_status_colors_are_not_theme_dependent(self): """Status colours are the same regardless of theme.""" from fenris.status_composition import StatusState # These are the canonical status colours — they must not change with theme assert STATUS_COLORS[StatusState.MONITORING.value] == "green" assert STATUS_COLORS[StatusState.ERROR.value] == "red" assert STATUS_COLORS[StatusState.PAUSED.value] == "yellow" assert STATUS_COLORS[StatusState.INTERRUPTED.value] == "red" assert STATUS_COLORS[StatusState.STALE.value] == "red" assert STATUS_COLORS[StatusState.WAITING.value] == "yellow" assert STATUS_COLORS[StatusState.UNKNOWN.value] == "dim" assert STATUS_COLORS[StatusState.COLLECTING.value] == "green" # --------------------------------------------------------------------------- # Graph colour role tests (AC80-5) # --------------------------------------------------------------------------- class TestGraphColorRoles: """Theme roles for graph rendering expose distinct colours per preset.""" def test_all_themes_define_same_roles(self): """Every theme has the same set of graph colour roles.""" roles = None for name in THEME_NAMES: colors = get_graph_colors(name) if roles is None: roles = set(colors.keys()) else: assert set(colors.keys()) == roles def test_graph_roles_are_distinct_across_presets(self): """Different themes produce different graph colour values.""" amber = get_graph_colors("amber") nord = get_graph_colors("nord") hc = get_graph_colors("high_contrast") # At least one role should differ between themes assert amber["allocated"] != nord["allocated"] or amber["allocated"] != hc["allocated"] def test_zero_role_is_dim(self): """Zero-usage bars use a dim/subtle colour in all themes.""" for name in THEME_NAMES: colors = get_graph_colors(name) # Zero should be distinct from allocated assert colors["zero"] != colors["allocated"]