Files
cms-SiteForge/tests/test_theme_registry.py
T
浅念 2a2f943bf4 fix: trade-pro review fixes — drop shadow blocks.css, English presets, 7-item fallback nav
- remove themes/trade-pro/blocks.css (52-byte placeholder shadowed the
  platform 7KB block stylesheet via copy_blocks_css) and drop it from
  manifest styles; Phase 3 brings real component styles
- page-presets.json: all editor/bootstrap copy translated to trade B2B
  English; ids, block types, structure and hrefs identical to industrial
- fallback nav widened to all 7 default sections (Brands/Cases restored)
  so the documented false-positive window only relabels, never drops
  coverage; comment added next to the tp_needs_en_nav set
- test anchors '>Products<' on the built home page

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 18:40:49 +08:00

129 lines
5.9 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
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 '<meta name="theme-color" content="#0b3d91"' in home.text
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