Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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"])