7a0da7932b
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
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
452 lines
13 KiB
Python
452 lines
13 KiB
Python
"""
|
|
Tests for the ThemeLoader class.
|
|
|
|
Tests cover:
|
|
- parse_frontmatter function
|
|
- validate_css_variables function
|
|
- load_theme function
|
|
- extract_theme_id function
|
|
- get_css_content function
|
|
"""
|
|
|
|
|
|
class TestParseFrontmatter:
|
|
"""Tests for parse_frontmatter method."""
|
|
|
|
def test_parses_valid_toml_frontmatter(self, tmp_path):
|
|
"""Should parse valid TOML frontmatter from CSS."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = """/*---
|
|
id = "test-theme"
|
|
label = "Test Theme"
|
|
icon = "fa-moon"
|
|
type = "dark"
|
|
---*/
|
|
|
|
:root {
|
|
--bg-primary: #1a1a1a;
|
|
}
|
|
"""
|
|
result = loader.parse_frontmatter(css_content)
|
|
|
|
assert result["id"] == "test-theme"
|
|
assert result["label"] == "Test Theme"
|
|
assert result["icon"] == "fa-moon"
|
|
assert result["type"] == "dark"
|
|
|
|
def test_returns_empty_dict_when_no_frontmatter(self, tmp_path):
|
|
"""Should return empty dict when no frontmatter present."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = """:root {
|
|
--bg-primary: #1a1a1a;
|
|
}
|
|
"""
|
|
result = loader.parse_frontmatter(css_content)
|
|
|
|
assert result == {}
|
|
|
|
def test_handles_malformed_toml(self, tmp_path):
|
|
"""Should return empty dict for malformed TOML."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = """/*---
|
|
id = "missing quote
|
|
invalid toml here
|
|
---*/
|
|
|
|
:root {}
|
|
"""
|
|
result = loader.parse_frontmatter(css_content)
|
|
|
|
# Should handle gracefully - returns empty dict
|
|
assert isinstance(result, dict)
|
|
|
|
def test_handles_empty_frontmatter(self, tmp_path):
|
|
"""Should handle empty frontmatter block."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = """/*---
|
|
---*/
|
|
|
|
:root {}
|
|
"""
|
|
result = loader.parse_frontmatter(css_content)
|
|
|
|
assert result == {}
|
|
|
|
|
|
class TestValidateCssVariables:
|
|
"""Tests for validate_css_variables method."""
|
|
|
|
def test_detects_missing_base_variables(self, tmp_path):
|
|
"""Should detect missing base CSS variables."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = """:root {
|
|
--bg-primary: #000;
|
|
}
|
|
"""
|
|
missing_base, missing_rgb = loader.validate_css_variables(
|
|
css_content, "test-theme"
|
|
)
|
|
|
|
# Should detect that many base variables are missing
|
|
assert len(missing_base) > 0
|
|
assert "--bg-secondary" in missing_base or len(missing_base) > 5
|
|
|
|
def test_detects_missing_rgb_variants(self, tmp_path):
|
|
"""Should detect missing RGB variant CSS variables."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
# Has base variables but no RGB variants
|
|
css_content = """:root {
|
|
--bg-primary: #000;
|
|
--bg-secondary: #111;
|
|
--bg-tertiary: #222;
|
|
--text-primary: #fff;
|
|
--text-secondary: #ccc;
|
|
--text-muted: #999;
|
|
--accent-primary: #00f;
|
|
--accent-secondary: #00a;
|
|
--accent-tertiary: #008;
|
|
--border-color: #333;
|
|
--success-color: #0f0;
|
|
--warning-color: #ff0;
|
|
--error-color: #f00;
|
|
}
|
|
"""
|
|
missing_base, missing_rgb = loader.validate_css_variables(
|
|
css_content, "test-theme"
|
|
)
|
|
|
|
# Should detect missing RGB variants
|
|
assert len(missing_rgb) > 0
|
|
|
|
def test_validates_complete_theme(self, tmp_path):
|
|
"""Should return empty lists for complete theme."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
# Complete CSS with all variables
|
|
css_content = """:root {
|
|
--bg-primary: #000;
|
|
--bg-primary-rgb: 0, 0, 0;
|
|
--bg-secondary: #111;
|
|
--bg-secondary-rgb: 17, 17, 17;
|
|
--bg-tertiary: #222;
|
|
--bg-tertiary-rgb: 34, 34, 34;
|
|
--text-primary: #fff;
|
|
--text-primary-rgb: 255, 255, 255;
|
|
--text-secondary: #ccc;
|
|
--text-secondary-rgb: 204, 204, 204;
|
|
--text-muted: #999;
|
|
--text-muted-rgb: 153, 153, 153;
|
|
--accent-primary: #00f;
|
|
--accent-primary-rgb: 0, 0, 255;
|
|
--accent-secondary: #00a;
|
|
--accent-secondary-rgb: 0, 0, 170;
|
|
--accent-tertiary: #008;
|
|
--accent-tertiary-rgb: 0, 0, 136;
|
|
--border-color: #333;
|
|
--border-color-rgb: 51, 51, 51;
|
|
--success-color: #0f0;
|
|
--success-color-rgb: 0, 255, 0;
|
|
--warning-color: #ff0;
|
|
--warning-color-rgb: 255, 255, 0;
|
|
--error-color: #f00;
|
|
--error-color-rgb: 255, 0, 0;
|
|
}
|
|
"""
|
|
missing_base, missing_rgb = loader.validate_css_variables(
|
|
css_content, "test-theme"
|
|
)
|
|
|
|
assert missing_base == []
|
|
assert missing_rgb == []
|
|
|
|
|
|
class TestLoadTheme:
|
|
"""Tests for load_theme method."""
|
|
|
|
def test_loads_theme_from_valid_css_file(self, tmp_path):
|
|
"""Should load theme metadata from valid CSS file."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
css_file = tmp_path / "test-theme.css"
|
|
css_file.write_text("""/*---
|
|
id = "test-theme"
|
|
label = "Test Theme"
|
|
icon = "fa-moon"
|
|
type = "dark"
|
|
---*/
|
|
|
|
[data-theme="test-theme"] {
|
|
--bg-primary: #1a1a1a;
|
|
--bg-primary-rgb: 26, 26, 26;
|
|
--bg-secondary: #222;
|
|
--bg-secondary-rgb: 34, 34, 34;
|
|
--bg-tertiary: #333;
|
|
--bg-tertiary-rgb: 51, 51, 51;
|
|
--text-primary: #fff;
|
|
--text-primary-rgb: 255, 255, 255;
|
|
--text-secondary: #ccc;
|
|
--text-secondary-rgb: 204, 204, 204;
|
|
--text-muted: #999;
|
|
--text-muted-rgb: 153, 153, 153;
|
|
--accent-primary: #00f;
|
|
--accent-primary-rgb: 0, 0, 255;
|
|
--accent-secondary: #00a;
|
|
--accent-secondary-rgb: 0, 0, 170;
|
|
--accent-tertiary: #008;
|
|
--accent-tertiary-rgb: 0, 0, 136;
|
|
--border-color: #333;
|
|
--border-color-rgb: 51, 51, 51;
|
|
--success-color: #0f0;
|
|
--success-color-rgb: 0, 255, 0;
|
|
--warning-color: #ff0;
|
|
--warning-color-rgb: 255, 255, 0;
|
|
--error-color: #f00;
|
|
--error-color-rgb: 255, 0, 0;
|
|
}
|
|
""")
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(css_file)
|
|
|
|
assert theme is not None
|
|
assert theme.id == "test-theme"
|
|
assert theme.label == "Test Theme"
|
|
|
|
def test_returns_none_for_nonexistent_file(self, tmp_path):
|
|
"""Should return None when file doesn't exist."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(tmp_path / "nonexistent.css")
|
|
|
|
assert theme is None
|
|
|
|
def test_auto_generates_metadata_when_missing(self, tmp_path):
|
|
"""Should auto-generate metadata when frontmatter is missing."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
css_file = tmp_path / "my-custom-theme.css"
|
|
css_file.write_text("""[data-theme="my-custom-theme"] {
|
|
--bg-primary: #1a1a1a;
|
|
--bg-primary-rgb: 26, 26, 26;
|
|
--bg-secondary: #222;
|
|
--bg-secondary-rgb: 34, 34, 34;
|
|
--bg-tertiary: #333;
|
|
--bg-tertiary-rgb: 51, 51, 51;
|
|
--text-primary: #fff;
|
|
--text-primary-rgb: 255, 255, 255;
|
|
--text-secondary: #ccc;
|
|
--text-secondary-rgb: 204, 204, 204;
|
|
--text-muted: #999;
|
|
--text-muted-rgb: 153, 153, 153;
|
|
--accent-primary: #00f;
|
|
--accent-primary-rgb: 0, 0, 255;
|
|
--accent-secondary: #00a;
|
|
--accent-secondary-rgb: 0, 0, 170;
|
|
--accent-tertiary: #008;
|
|
--accent-tertiary-rgb: 0, 0, 136;
|
|
--border-color: #333;
|
|
--border-color-rgb: 51, 51, 51;
|
|
--success-color: #0f0;
|
|
--success-color-rgb: 0, 255, 0;
|
|
--warning-color: #ff0;
|
|
--warning-color-rgb: 255, 255, 0;
|
|
--error-color: #f00;
|
|
--error-color-rgb: 255, 0, 0;
|
|
}
|
|
""")
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(css_file)
|
|
|
|
assert theme is not None
|
|
# Should extract ID from CSS selector
|
|
assert theme.id == "my-custom-theme"
|
|
|
|
def test_extracts_theme_id_from_css_selector(self, tmp_path):
|
|
"""Should extract theme ID from CSS selector when not in frontmatter."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
css_file = tmp_path / "file.css"
|
|
css_file.write_text("""/*---
|
|
label = "Custom Theme"
|
|
---*/
|
|
|
|
[data-theme="extracted-id"] {
|
|
--bg-primary: #1a1a1a;
|
|
--bg-primary-rgb: 26, 26, 26;
|
|
--bg-secondary: #222;
|
|
--bg-secondary-rgb: 34, 34, 34;
|
|
--bg-tertiary: #333;
|
|
--bg-tertiary-rgb: 51, 51, 51;
|
|
--text-primary: #fff;
|
|
--text-primary-rgb: 255, 255, 255;
|
|
--text-secondary: #ccc;
|
|
--text-secondary-rgb: 204, 204, 204;
|
|
--text-muted: #999;
|
|
--text-muted-rgb: 153, 153, 153;
|
|
--accent-primary: #00f;
|
|
--accent-primary-rgb: 0, 0, 255;
|
|
--accent-secondary: #00a;
|
|
--accent-secondary-rgb: 0, 0, 170;
|
|
--accent-tertiary: #008;
|
|
--accent-tertiary-rgb: 0, 0, 136;
|
|
--border-color: #333;
|
|
--border-color-rgb: 51, 51, 51;
|
|
--success-color: #0f0;
|
|
--success-color-rgb: 0, 255, 0;
|
|
--warning-color: #ff0;
|
|
--warning-color-rgb: 255, 255, 0;
|
|
--error-color: #f00;
|
|
--error-color-rgb: 255, 0, 0;
|
|
}
|
|
""")
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(css_file)
|
|
|
|
assert theme is not None
|
|
assert theme.id == "extracted-id"
|
|
|
|
|
|
class TestExtractThemeId:
|
|
"""Tests for extract_theme_id method."""
|
|
|
|
def test_extracts_id_from_data_theme_selector(self, tmp_path):
|
|
"""Should extract ID from [data-theme='...'] selector."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = '[data-theme="my-awesome-theme"] { --color: #000; }'
|
|
|
|
result = loader.extract_theme_id(css_content)
|
|
|
|
assert result == "my-awesome-theme"
|
|
|
|
def test_returns_none_when_no_selector(self, tmp_path):
|
|
"""Should return None when no data-theme selector found."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = ":root { --color: #000; }"
|
|
|
|
result = loader.extract_theme_id(css_content)
|
|
|
|
assert result is None
|
|
|
|
def test_handles_single_quotes(self, tmp_path):
|
|
"""Should handle single-quoted selector."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
css_content = "[data-theme='single-quoted'] { --color: #000; }"
|
|
|
|
result = loader.extract_theme_id(css_content)
|
|
|
|
# May or may not match depending on regex - just ensure no crash
|
|
assert result is None or result == "single-quoted"
|
|
|
|
|
|
class TestGetCssContent:
|
|
"""Tests for get_css_content method."""
|
|
|
|
def test_strips_frontmatter_from_content(self, tmp_path):
|
|
"""Should return CSS content without frontmatter."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
css_file = tmp_path / "theme.css"
|
|
css_content = """/*---
|
|
id = "test"
|
|
---*/
|
|
|
|
:root { --bg: #000; }
|
|
"""
|
|
css_file.write_text(css_content)
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(css_file)
|
|
|
|
if theme:
|
|
content = loader.get_css_content(theme)
|
|
assert "/*---" not in content or ":root" in content
|
|
|
|
def test_returns_full_content_when_no_frontmatter(self, tmp_path):
|
|
"""Should return full content when no frontmatter."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
css_file = tmp_path / "theme.css"
|
|
css_content = """:root {
|
|
--bg-primary: #000;
|
|
--bg-primary-rgb: 0, 0, 0;
|
|
--bg-secondary: #111;
|
|
--bg-secondary-rgb: 17, 17, 17;
|
|
--bg-tertiary: #222;
|
|
--bg-tertiary-rgb: 34, 34, 34;
|
|
--text-primary: #fff;
|
|
--text-primary-rgb: 255, 255, 255;
|
|
--text-secondary: #ccc;
|
|
--text-secondary-rgb: 204, 204, 204;
|
|
--text-muted: #999;
|
|
--text-muted-rgb: 153, 153, 153;
|
|
--accent-primary: #00f;
|
|
--accent-primary-rgb: 0, 0, 255;
|
|
--accent-secondary: #00a;
|
|
--accent-secondary-rgb: 0, 0, 170;
|
|
--accent-tertiary: #008;
|
|
--accent-tertiary-rgb: 0, 0, 136;
|
|
--border-color: #333;
|
|
--border-color-rgb: 51, 51, 51;
|
|
--success-color: #0f0;
|
|
--success-color-rgb: 0, 255, 0;
|
|
--warning-color: #ff0;
|
|
--warning-color-rgb: 255, 255, 0;
|
|
--error-color: #f00;
|
|
--error-color-rgb: 255, 0, 0;
|
|
}
|
|
"""
|
|
css_file.write_text(css_content)
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
theme = loader.load_theme(css_file)
|
|
|
|
if theme:
|
|
content = loader.get_css_content(theme)
|
|
assert "--bg-primary: #000" in content
|
|
|
|
|
|
class TestThemeLoaderInit:
|
|
"""Tests for ThemeLoader initialization."""
|
|
|
|
def test_can_create_loader_instance(self, tmp_path):
|
|
"""Should be able to create ThemeLoader instance."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
|
|
assert loader is not None
|
|
|
|
def test_loader_has_required_methods(self, tmp_path):
|
|
"""Loader should have all required methods."""
|
|
from local_deep_research.web.themes.loader import ThemeLoader
|
|
|
|
loader = ThemeLoader(tmp_path)
|
|
|
|
assert hasattr(loader, "parse_frontmatter")
|
|
assert hasattr(loader, "validate_css_variables")
|
|
assert hasattr(loader, "load_theme")
|
|
assert hasattr(loader, "extract_theme_id")
|
|
assert hasattr(loader, "get_css_content")
|