"""Theme discovery must be a single source of truth.""" import json from pathlib import Path from app import core, theme_pro, theme_store from test_factory_v2 import factory, site # noqa: F401 -- fixture + helper reused by trade-pro tests 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_trade_pro_registered_and_compatible(): assert 'trade-pro' in core.THEMES result = theme_store.compatibility('trade-pro') assert result['valid'], result['errors'] assert not result['errors'] def test_trade_pro_builds_real_site(factory): sid = site(factory) assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200 selected = factory.put(f'/api/factory/sites/{sid}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}) assert selected.status_code == 200, selected.text result = factory.post(f'/api/sites/{sid}/builds', json={}) assert result.status_code == 200, result.text bid = result.json()['version'] home = factory.get(f'/preview/{sid}/{bid}/') assert home.status_code == 200, home.text[:400] assert 'lang="en"' in home.text assert '>Products<' in home.text # English fallback nav actually rendered assert '>首页<' not in home.text assert '