702253f387
Co-Authored-By: Claude Code <noreply@anthropic.com>
107 lines
4.8 KiB
Python
107 lines
4.8 KiB
Python
"""Theme discovery must be a single source of truth."""
|
|
import json
|
|
from pathlib import Path
|
|
from app import core, theme_pro, theme_store
|
|
|
|
ROOT = Path(core.__file__).resolve().parent.parent
|
|
|
|
def test_discover_returns_builtin_themes_with_manifests():
|
|
ids = core.discover_themes()
|
|
for tid in ('automotive', 'industrial', 'corporate', 'minimal'):
|
|
assert tid in ids
|
|
for tid in ids:
|
|
manifest = json.loads((ROOT / 'themes' / tid / 'manifest.json').read_text(encoding='utf-8'))
|
|
assert manifest.get('id') == tid
|
|
|
|
def test_three_whitelists_agree():
|
|
# Tripwire: the three names must stay the same object. Fails only if someone
|
|
# reintroduces a hardcoded copy of the list anywhere.
|
|
assert set(core.THEMES) == set(theme_pro.THEMES) == set(theme_store.BUILTIN)
|
|
assert isinstance(core.THEMES, tuple)
|
|
|
|
def test_discover_skips_broken_theme_dirs(tmp_path, monkeypatch):
|
|
themes = tmp_path / 'themes'
|
|
good = themes / 'good-one'; good.mkdir(parents=True)
|
|
(good / 'manifest.json').write_text(json.dumps({'id': 'good-one'}), encoding='utf-8')
|
|
broken = themes / 'broken'; broken.mkdir()
|
|
(broken / 'manifest.json').write_text('{not json', encoding='utf-8')
|
|
mismatch = themes / 'mismatch'; mismatch.mkdir()
|
|
(mismatch / 'manifest.json').write_text(json.dumps({'id': 'other-name'}), encoding='utf-8')
|
|
no_manifest = themes / 'no-manifest'; no_manifest.mkdir()
|
|
underscore = themes / '_shared'; underscore.mkdir()
|
|
monkeypatch.setattr(core, 'ROOT', tmp_path)
|
|
assert core.discover_themes() == ('good-one',)
|
|
|
|
def test_discover_fails_fast_when_themes_dir_missing(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(core, 'ROOT', tmp_path)
|
|
import pytest
|
|
with pytest.raises(RuntimeError, match='themes directory missing'):
|
|
core.discover_themes()
|
|
|
|
def test_env_for_theme_prefers_theme_dir_then_shared_root():
|
|
from app import theme_pro
|
|
env = theme_pro.env_for('industrial')
|
|
paths = [str(p).replace('\\', '/') for p in env.loader.searchpath]
|
|
assert paths[0].endswith('themes/industrial')
|
|
assert paths[1].endswith('themes')
|
|
# 共享宏库必须仍可解析(回落路径)
|
|
assert env.get_template('_professional_components.html') is not None
|
|
|
|
def test_env_for_is_cached_per_theme():
|
|
from app import theme_pro
|
|
assert theme_pro.env_for('industrial') is theme_pro.env_for('industrial')
|
|
|
|
def test_env_for_pro_template_loads_unprefixed():
|
|
from app import theme_pro
|
|
assert theme_pro.env_for('industrial').get_template('pro.html') is not None
|
|
|
|
def test_env_for_rejects_unknown_theme():
|
|
import pytest
|
|
from app import theme_pro
|
|
with pytest.raises(ValueError):
|
|
theme_pro.env_for('../../etc')
|
|
with pytest.raises(ValueError):
|
|
theme_pro.env_for('no-such-theme')
|
|
|
|
def test_env_for_theme_macro_shadows_shared_root(tmp_path, monkeypatch):
|
|
# The core mechanism: a theme-local _professional_components.html wins over
|
|
# the shared one at themes/ root.
|
|
from app import theme_pro
|
|
themes = tmp_path / 'themes'
|
|
for sub, marker in (('industrial', 'theme-local'), ('_root_macros', None)):
|
|
d = themes / sub
|
|
d.mkdir(parents=True)
|
|
(themes / '_professional_components.html').write_text('SHARED', encoding='utf-8')
|
|
(themes / 'industrial' / '_professional_components.html').write_text('THEME-LOCAL', encoding='utf-8')
|
|
monkeypatch.setattr(theme_pro, 'ROOT', tmp_path)
|
|
monkeypatch.setattr(theme_pro, '_ENVS', {})
|
|
tpl = theme_pro.env_for('industrial').get_template('_professional_components.html')
|
|
assert tpl.render().strip() == 'THEME-LOCAL'
|
|
|
|
def test_env_for_degraded_branch_not_cached_and_single_path(tmp_path, monkeypatch):
|
|
from app import theme_pro
|
|
themes = tmp_path / 'themes'
|
|
themes.mkdir(parents=True)
|
|
monkeypatch.setattr(theme_pro, 'ROOT', tmp_path)
|
|
monkeypatch.setattr(theme_pro, '_ENVS', {})
|
|
core.THEMES = ('industrial',) # whitelist a theme whose dir does not exist
|
|
try:
|
|
env = theme_pro.env_for('industrial')
|
|
assert [str(p) for p in env.loader.searchpath] == [str(themes)]
|
|
assert 'industrial' not in theme_pro._ENVS
|
|
finally:
|
|
core.THEMES = core.discover_themes()
|
|
|
|
def test_theme_blocks_css_overrides_platform(tmp_path):
|
|
from app import builder
|
|
dist = tmp_path / 'assets'; dist.mkdir()
|
|
theme_dir = tmp_path / 'theme'; theme_dir.mkdir()
|
|
(theme_dir / 'blocks.css').write_text('/* theme version */', encoding='utf-8')
|
|
builder.copy_blocks_css(theme_dir, dist)
|
|
assert (dist / 'blocks.css').read_text(encoding='utf-8') == '/* theme version */'
|
|
# 无主题版时回落平台版(精确断言内容一致)
|
|
dist2 = tmp_path / 'assets2'; dist2.mkdir()
|
|
builder.copy_blocks_css(tmp_path / 'nonexistent', dist2)
|
|
platform = (builder.ROOT / 'app' / 'static' / 'blocks.css').read_text(encoding='utf-8')
|
|
assert (dist2 / 'blocks.css').read_text(encoding='utf-8') == platform
|