7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
509 lines
15 KiB
Python
509 lines
15 KiB
Python
"""
|
|
Unit tests for the theme module components.
|
|
|
|
These tests cover:
|
|
1. TOML frontmatter parsing (loader.py)
|
|
2. ThemeMetadata dataclass (schema.py)
|
|
3. ThemeRegistry functionality (__init__.py)
|
|
4. Invalid theme file handling
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.web.themes import (
|
|
ThemeMetadata,
|
|
ThemeRegistry,
|
|
get_theme_metadata,
|
|
get_themes,
|
|
get_themes_json,
|
|
theme_registry,
|
|
)
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
from local_deep_research.web.themes.schema import (
|
|
REQUIRED_RGB_VARIANTS,
|
|
REQUIRED_VARIABLES,
|
|
)
|
|
|
|
|
|
class TestFrontmatterParsing:
|
|
"""Test TOML frontmatter parsing from CSS files."""
|
|
|
|
@pytest.fixture
|
|
def loader(self, tmp_path):
|
|
"""Create a ThemeLoader with temporary directory."""
|
|
return ThemeLoader(tmp_path)
|
|
|
|
def test_parse_valid_frontmatter(self, loader):
|
|
"""Valid TOML frontmatter should be parsed correctly."""
|
|
css = """/*---
|
|
name = "Test Theme"
|
|
label = "Test Theme"
|
|
icon = "fa-star"
|
|
group = "core"
|
|
type = "dark"
|
|
description = "A test theme"
|
|
---*/
|
|
|
|
[data-theme="test"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
"""
|
|
result = loader.parse_frontmatter(css)
|
|
assert result["name"] == "Test Theme"
|
|
assert result["label"] == "Test Theme"
|
|
assert result["icon"] == "fa-star"
|
|
assert result["group"] == "core"
|
|
assert result["type"] == "dark"
|
|
assert result["description"] == "A test theme"
|
|
|
|
def test_parse_missing_frontmatter(self, loader):
|
|
"""CSS without frontmatter should return empty dict."""
|
|
css = """[data-theme="test"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
"""
|
|
result = loader.parse_frontmatter(css)
|
|
assert result == {}
|
|
|
|
def test_parse_malformed_frontmatter(self, loader):
|
|
"""Malformed TOML should return empty dict without crashing."""
|
|
css = """/*---
|
|
name = "Missing closing quote
|
|
icon = fa-star # missing quotes
|
|
---*/
|
|
|
|
[data-theme="test"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
"""
|
|
result = loader.parse_frontmatter(css)
|
|
assert result == {}
|
|
|
|
def test_parse_empty_frontmatter(self, loader):
|
|
"""Empty frontmatter should return empty dict."""
|
|
css = """/*---
|
|
---*/
|
|
|
|
[data-theme="test"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
"""
|
|
result = loader.parse_frontmatter(css)
|
|
assert result == {}
|
|
|
|
|
|
class TestThemeIdExtraction:
|
|
"""Test theme ID extraction from CSS selectors."""
|
|
|
|
@pytest.fixture
|
|
def loader(self, tmp_path):
|
|
"""Create a ThemeLoader with temporary directory."""
|
|
return ThemeLoader(tmp_path)
|
|
|
|
def test_extract_theme_id(self, loader):
|
|
"""Theme ID should be extracted from data-theme selector."""
|
|
css = """[data-theme="nord"] {
|
|
--bg-primary: #2e3440;
|
|
}
|
|
"""
|
|
result = loader.extract_theme_id(css)
|
|
assert result == "nord"
|
|
|
|
def test_extract_theme_id_with_root(self, loader):
|
|
"""Theme ID should be extracted when combined with :root."""
|
|
css = """:root,
|
|
[data-theme="hashed"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
"""
|
|
result = loader.extract_theme_id(css)
|
|
assert result == "hashed"
|
|
|
|
def test_extract_theme_id_missing(self, loader):
|
|
"""Should return None when no data-theme selector present."""
|
|
css = """:root {
|
|
--bg-primary: #121212;
|
|
}
|
|
"""
|
|
result = loader.extract_theme_id(css)
|
|
assert result is None
|
|
|
|
|
|
class TestCSSVariableValidation:
|
|
"""Test validation of required CSS variables."""
|
|
|
|
@pytest.fixture
|
|
def loader(self, tmp_path):
|
|
"""Create a ThemeLoader with temporary directory."""
|
|
return ThemeLoader(tmp_path)
|
|
|
|
def test_validate_complete_theme(self, loader):
|
|
"""Theme with all required variables should pass validation."""
|
|
# Build CSS with all required variables
|
|
vars_css = "\n".join(
|
|
[f" {var}: #123456;" for var in REQUIRED_VARIABLES]
|
|
)
|
|
rgb_css = "\n".join(
|
|
[f" {var}: 18, 52, 86;" for var in REQUIRED_RGB_VARIANTS]
|
|
)
|
|
css = f"""[data-theme="complete"] {{
|
|
{vars_css}
|
|
{rgb_css}
|
|
}}
|
|
"""
|
|
missing_base, missing_rgb = loader.validate_css_variables(
|
|
css, "complete"
|
|
)
|
|
assert missing_base == []
|
|
assert missing_rgb == []
|
|
|
|
def test_validate_missing_variables(self, loader):
|
|
"""Theme missing required variables should be flagged."""
|
|
css = """[data-theme="incomplete"] {
|
|
--bg-primary: #121212;
|
|
--text-primary: #ffffff;
|
|
}
|
|
"""
|
|
missing_base, missing_rgb = loader.validate_css_variables(
|
|
css, "incomplete"
|
|
)
|
|
assert "--bg-secondary" in missing_base
|
|
assert "--accent-primary" in missing_base
|
|
assert "--bg-primary-rgb" in missing_rgb
|
|
|
|
|
|
class TestThemeLoading:
|
|
"""Test loading themes from files."""
|
|
|
|
@pytest.fixture
|
|
def themes_dir(self, tmp_path):
|
|
"""Create a temporary themes directory with test themes."""
|
|
core_dir = tmp_path / "core"
|
|
core_dir.mkdir()
|
|
|
|
# Create a valid theme file
|
|
valid_theme = core_dir / "test-valid.css"
|
|
valid_theme.write_text("""/*---
|
|
name = "Test Valid"
|
|
label = "Test Valid"
|
|
icon = "fa-check"
|
|
group = "core"
|
|
type = "dark"
|
|
---*/
|
|
|
|
[data-theme="test-valid"] {
|
|
--bg-primary: #121212;
|
|
--bg-secondary: #1e1e1e;
|
|
--bg-tertiary: #2d2d2d;
|
|
--accent-primary: #bb86fc;
|
|
--accent-secondary: #03dac6;
|
|
--accent-tertiary: #cf6679;
|
|
--text-primary: #ffffff;
|
|
--text-secondary: #b3b3b3;
|
|
--text-muted: #666666;
|
|
--border-color: #333333;
|
|
--success-color: #4caf50;
|
|
--warning-color: #ff9800;
|
|
--error-color: #f44336;
|
|
--bg-primary-rgb: 18, 18, 18;
|
|
--bg-secondary-rgb: 30, 30, 30;
|
|
--bg-tertiary-rgb: 45, 45, 45;
|
|
--accent-primary-rgb: 187, 134, 252;
|
|
--accent-secondary-rgb: 3, 218, 198;
|
|
--accent-tertiary-rgb: 207, 102, 121;
|
|
--text-primary-rgb: 255, 255, 255;
|
|
--text-secondary-rgb: 179, 179, 179;
|
|
--text-muted-rgb: 102, 102, 102;
|
|
--border-color-rgb: 51, 51, 51;
|
|
--success-color-rgb: 76, 175, 80;
|
|
--warning-color-rgb: 255, 152, 0;
|
|
--error-color-rgb: 244, 67, 54;
|
|
}
|
|
""")
|
|
|
|
return tmp_path
|
|
|
|
def test_load_valid_theme(self, themes_dir):
|
|
"""Valid theme file should be loaded correctly."""
|
|
loader = ThemeLoader(themes_dir)
|
|
theme = loader.load_theme(themes_dir / "core" / "test-valid.css")
|
|
|
|
assert theme is not None
|
|
assert theme.id == "test-valid"
|
|
assert theme.label == "Test Valid"
|
|
assert theme.icon == "fa-check"
|
|
assert theme.group == "core"
|
|
assert theme.type == "dark"
|
|
|
|
def test_load_nonexistent_file(self, themes_dir):
|
|
"""Loading nonexistent file should return None."""
|
|
loader = ThemeLoader(themes_dir)
|
|
theme = loader.load_theme(themes_dir / "nonexistent.css")
|
|
|
|
assert theme is None
|
|
|
|
def test_load_all_themes_includes_system(self, themes_dir):
|
|
"""load_all_themes should include virtual 'system' theme."""
|
|
loader = ThemeLoader(themes_dir)
|
|
themes = loader.load_all_themes()
|
|
|
|
assert "system" in themes
|
|
assert themes["system"].css_path is None
|
|
assert themes["system"].group == "system"
|
|
|
|
|
|
class TestThemeRegistry:
|
|
"""Test ThemeRegistry singleton and methods."""
|
|
|
|
def test_singleton_pattern(self):
|
|
"""ThemeRegistry should be a singleton."""
|
|
registry1 = ThemeRegistry()
|
|
registry2 = ThemeRegistry()
|
|
assert registry1 is registry2
|
|
|
|
def test_registry_loads_themes(self):
|
|
"""Registry should load themes from themes directory."""
|
|
assert len(theme_registry.themes) > 0
|
|
assert "system" in theme_registry.themes
|
|
|
|
def test_get_theme_ids(self):
|
|
"""get_theme_ids should return sorted list of theme IDs."""
|
|
ids = theme_registry.get_theme_ids()
|
|
assert isinstance(ids, list)
|
|
assert len(ids) > 0
|
|
assert ids == sorted(ids)
|
|
|
|
def test_get_theme_valid(self):
|
|
"""get_theme should return ThemeMetadata for valid ID."""
|
|
theme = theme_registry.get_theme("system")
|
|
assert theme is not None
|
|
assert theme.id == "system"
|
|
|
|
def test_get_theme_invalid(self):
|
|
"""get_theme should return None for invalid ID."""
|
|
theme = theme_registry.get_theme("nonexistent-theme-xyz")
|
|
assert theme is None
|
|
|
|
def test_is_valid_theme(self):
|
|
"""is_valid_theme should correctly validate theme IDs."""
|
|
assert theme_registry.is_valid_theme("system") is True
|
|
assert theme_registry.is_valid_theme("nonexistent") is False
|
|
|
|
def test_get_combined_css(self):
|
|
"""get_combined_css should return valid CSS string."""
|
|
css = theme_registry.get_combined_css()
|
|
assert isinstance(css, str)
|
|
assert "Auto-generated theme CSS" in css
|
|
assert "[data-theme=" in css
|
|
|
|
def test_get_themes_by_group(self):
|
|
"""get_themes_by_group should organize themes correctly."""
|
|
grouped = theme_registry.get_themes_by_group()
|
|
assert isinstance(grouped, dict)
|
|
assert "system" in grouped
|
|
assert len(grouped["system"]) == 1
|
|
assert grouped["system"][0].id == "system"
|
|
|
|
|
|
class TestThemeMetadata:
|
|
"""Test ThemeMetadata dataclass."""
|
|
|
|
def test_to_dict(self):
|
|
"""to_dict should return serializable dictionary."""
|
|
theme = ThemeMetadata(
|
|
id="test",
|
|
label="Test Theme",
|
|
icon="fa-star",
|
|
group="core",
|
|
type="dark",
|
|
description="A test theme",
|
|
)
|
|
result = theme.to_dict()
|
|
|
|
assert result["label"] == "Test Theme"
|
|
assert result["icon"] == "fa-star"
|
|
assert result["group"] == "core"
|
|
assert result["type"] == "dark"
|
|
# id, description, author, css_path should NOT be in to_dict
|
|
assert "id" not in result
|
|
assert "description" not in result
|
|
|
|
def test_default_values(self):
|
|
"""ThemeMetadata should have sensible defaults."""
|
|
theme = ThemeMetadata(
|
|
id="minimal",
|
|
label="Minimal",
|
|
icon="fa-circle",
|
|
group="core",
|
|
)
|
|
assert theme.type == "dark"
|
|
assert theme.description == ""
|
|
assert theme.author == ""
|
|
assert theme.css_path is None
|
|
|
|
|
|
class TestFlaskIntegrationFunctions:
|
|
"""Test Flask integration helper functions."""
|
|
|
|
def test_get_themes(self):
|
|
"""get_themes should return list of theme IDs."""
|
|
themes = get_themes()
|
|
assert isinstance(themes, list)
|
|
assert "system" in themes
|
|
|
|
def test_get_themes_json(self):
|
|
"""get_themes_json should return Markup-safe JSON."""
|
|
json_str = get_themes_json()
|
|
# Should be a valid JSON array
|
|
assert json_str.startswith("[")
|
|
assert json_str.endswith("]")
|
|
assert '"system"' in json_str
|
|
|
|
def test_get_theme_metadata(self):
|
|
"""get_theme_metadata should return Markup-safe JSON object."""
|
|
json_str = get_theme_metadata()
|
|
# Should be a valid JSON object
|
|
assert json_str.startswith("{")
|
|
assert json_str.endswith("}")
|
|
assert '"system"' in json_str
|
|
|
|
|
|
class TestSettingsOptions:
|
|
"""Test settings options generation."""
|
|
|
|
def test_get_settings_options_format(self):
|
|
"""get_settings_options should return correct format."""
|
|
options = theme_registry.get_settings_options()
|
|
|
|
assert isinstance(options, list)
|
|
assert len(options) > 0
|
|
|
|
# Each option should have label and value
|
|
for opt in options:
|
|
assert "label" in opt
|
|
assert "value" in opt
|
|
assert isinstance(opt["label"], str)
|
|
assert isinstance(opt["value"], str)
|
|
|
|
def test_get_settings_options_sorted(self):
|
|
"""get_settings_options should be sorted by label."""
|
|
options = theme_registry.get_settings_options()
|
|
labels = [opt["label"] for opt in options]
|
|
assert labels == sorted(labels)
|
|
|
|
def test_get_grouped_settings_options(self):
|
|
"""get_grouped_settings_options should include group info."""
|
|
options = theme_registry.get_grouped_settings_options()
|
|
|
|
assert isinstance(options, list)
|
|
for opt in options:
|
|
assert "label" in opt
|
|
assert "value" in opt
|
|
assert "group" in opt
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Test edge cases and error handling."""
|
|
|
|
def test_theme_with_special_characters_in_description(self, tmp_path):
|
|
"""Themes with special characters should be handled."""
|
|
core_dir = tmp_path / "core"
|
|
core_dir.mkdir()
|
|
|
|
theme_file = core_dir / "special.css"
|
|
theme_file.write_text("""/*---
|
|
name = "Special \\"Theme\\""
|
|
label = "Special Theme"
|
|
icon = "fa-star"
|
|
group = "core"
|
|
description = "Theme with \\"quotes\\" and 'apostrophes'"
|
|
---*/
|
|
|
|
[data-theme="special"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
""")
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(theme_file)
|
|
|
|
assert theme is not None
|
|
assert "quotes" in theme.description
|
|
|
|
def test_auto_generated_metadata(self, tmp_path):
|
|
"""Themes without frontmatter should get auto-generated metadata."""
|
|
core_dir = tmp_path / "core"
|
|
core_dir.mkdir()
|
|
|
|
theme_file = core_dir / "auto-theme.css"
|
|
theme_file.write_text("""[data-theme="auto-theme"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
""")
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(theme_file)
|
|
|
|
assert theme is not None
|
|
assert theme.id == "auto-theme"
|
|
assert theme.label == "Auto Theme" # Auto-generated from filename
|
|
assert theme.icon == "fa-palette" # Default icon
|
|
assert theme.group == "core" # From parent directory
|
|
|
|
def test_theme_type_validation(self, tmp_path):
|
|
"""Invalid theme type should default to 'dark'."""
|
|
core_dir = tmp_path / "core"
|
|
core_dir.mkdir()
|
|
|
|
theme_file = core_dir / "bad-type.css"
|
|
theme_file.write_text("""/*---
|
|
name = "Bad Type"
|
|
label = "Bad Type"
|
|
icon = "fa-star"
|
|
group = "core"
|
|
type = "invalid"
|
|
---*/
|
|
|
|
[data-theme="bad-type"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
""")
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(theme_file)
|
|
|
|
assert theme is not None
|
|
assert theme.type == "dark" # Should default to dark
|
|
|
|
|
|
class TestCSSContentGeneration:
|
|
"""Test CSS content generation and stripping."""
|
|
|
|
def test_get_css_content_strips_frontmatter(self, tmp_path):
|
|
"""get_css_content should strip TOML frontmatter."""
|
|
core_dir = tmp_path / "core"
|
|
core_dir.mkdir()
|
|
|
|
theme_file = core_dir / "strip-test.css"
|
|
theme_file.write_text("""/*---
|
|
name = "Strip Test"
|
|
label = "Strip Test"
|
|
icon = "fa-star"
|
|
group = "core"
|
|
---*/
|
|
|
|
[data-theme="strip-test"] {
|
|
--bg-primary: #121212;
|
|
}
|
|
""")
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(theme_file)
|
|
css = loader.get_css_content(theme)
|
|
|
|
assert "/*---" not in css
|
|
assert "---*/" not in css
|
|
assert "name = " not in css
|
|
assert "[data-theme=" in css
|
|
assert "--bg-primary" in css
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|