import asyncio import re import unittest from pathlib import Path from jinja2 import ChainableUndefined, Environment from core.template import TemplateRepository ROOT = Path(__file__).parent.parent TEMPLATE_ROOT = ROOT / "templates" SPECIAL_PAGE_STYLES = { "baizhan", "chengbeng", "chengjiu", "fubenjilu", "helps", "jiaoyihang", "juesheliaotian", "richangyuche", "wujia", "xingxiashijian", "zhanji", "zili", "zilipaixing", } class TemplateRepositoryTests(unittest.TestCase): def setUp(self): self.repository = TemplateRepository(TEMPLATE_ROOT) def test_all_pages_compose_to_valid_jinja_documents(self): pages = sorted((TEMPLATE_ROOT / "pages").glob("*.html")) page_styles = sorted((TEMPLATE_ROOT / "styles" / "pages").glob("*.css")) self.assertEqual(46, len(pages)) self.assertEqual( SPECIAL_PAGE_STYLES, {style.stem for style in page_styles}, ) environment = Environment(undefined=ChainableUndefined) empty_context = { "data": {}, "icons": {"img": {}, "sect": {}, "serendipity": {}}, "items": [], "summary": {}, } for page in pages: with self.subTest(page=page.name): fragment = page.read_text(encoding="utf-8") for document_tag in ("!DOCTYPE", "html", "head", "style", "body"): self.assertIsNone( re.search(rf"<{document_tag}(?:\s|>)", fragment, re.IGNORECASE) ) template = asyncio.run(self.repository.get(page.name)) self.assertIn("", template) self.assertIn("", template) self.assertNotIn("", template) self.assertNotIn("/*__PAGE_CSS__*/", template) self.assertNotIn("/* @component", template) self.assertNotIn("/* @endcomponent */", template) rendered = environment.from_string(template).render(empty_context) self.assertIn("", rendered) def test_repeated_page_patterns_use_shared_components(self): components = (TEMPLATE_ROOT / "styles" / "components.css").read_text( encoding="utf-8" ) for selector in ( ".data-table", ".data-cell--rank", ".card-grid", ".ability-card", ".collection-card", ".object-card", ): self.assertIn(selector, components) expected_components = { "bilei.html": "data-table", "rank_role.html": "data-cell--rank", "jineng.html": "ability-card", "juesheqiyu.html": "collection-card", "qiwu.html": "object-card", } for name, component in expected_components.items(): with self.subTest(page=name): fragment = (TEMPLATE_ROOT / "pages" / name).read_text( encoding="utf-8" ) self.assertIn(component, fragment) page_style = TEMPLATE_ROOT / "styles" / "pages" / ( f"{Path(name).stem}.css" ) self.assertFalse(page_style.exists()) def test_image_templates_do_not_define_responsive_breakpoints(self): for style in sorted((TEMPLATE_ROOT / "styles").rglob("*.css")): with self.subTest(style=style.relative_to(TEMPLATE_ROOT)): content = style.read_text(encoding="utf-8") self.assertIsNone( re.search(r"@media\s*(?:screen\s+and\s+)?\([^)]*width", content) ) base = (TEMPLATE_ROOT / "styles" / "base.css").read_text(encoding="utf-8") self.assertIn("min-width: calc(var(--jx3-page-width) + 94px)", base) self.assertIn("width: var(--jx3-page-width)", base) self.assertNotIn("100vw", base) def test_only_declared_component_blocks_are_injected(self): table_page = asyncio.run(self.repository.get("bilei.html")) self.assertIn(".data-table--spaced", table_page) self.assertNotIn(".ability-card__head", table_page) self.assertNotIn(".object-card__footer", table_page) ability_page = asyncio.run(self.repository.get("jineng.html")) self.assertIn(".card-grid", ability_page) self.assertIn(".ability-card__head", ability_page) self.assertNotIn(".data-table--spaced", ability_page) self.assertNotIn(".object-card__footer", ability_page) object_page = asyncio.run(self.repository.get("qiwu.html")) self.assertIn(".object-card__footer", object_page) self.assertNotIn(".ability-card__head", object_page) def test_wujia_groups_follow_the_api_names_instead_of_list_positions(self): """万宝楼分组在真实返回中位于列表末尾,且在售组名为“在售期”。""" template = asyncio.run(self.repository.get("wujia.html")) rendered = Environment(undefined=ChainableUndefined).from_string(template).render( { "icons": {"img": {}, "sect": {}, "serendipity": {}}, "name": "浮屠明音礼盒", "alias": "", "view": "", "desc": "", "category": "外观礼盒", "date": "2018-08-24", "retail": 888, "list": [ {"name": "电信区", "list": []}, {"name": "双线区", "list": []}, {"name": "无界区", "list": []}, { "name": "梦江南", "list": [ { "date": "QUERY-MARKER", "server": "梦江南", "value": 4700, "sale": 4, } ], }, {"name": "公示期", "list": []}, { "name": "在售期", "list": [ { "date": "SALE-MARKER", "zone": "电信区", "server": "梦江南", "value": 5188, } ], }, ], } ) sale_start = rendered.index('
') query_start = rendered.index('
') next_card = rendered.index('
', query_start) sale_section = rendered[sale_start:query_start] query_section = rendered[query_start:next_card] self.assertIn("SALE-MARKER", sale_section) self.assertNotIn("QUERY-MARKER", sale_section) self.assertIn("查询区服:梦江南", re.sub(r"\s+", "", query_section)) self.assertIn("QUERY-MARKER", query_section) self.assertNotIn("SALE-MARKER", query_section) def test_known_dark_theme_text_uses_shared_color_tokens(self): expected = { "jiaoyihang.css": (".item-name", ".price", ".sample"), "juesheliaotian.css": (".role-col", ".message-col"), "zilipaixing.css": (".force", ".role-name-col", ".server-col"), } for name, selectors in expected.items(): with self.subTest(style=name): content = (TEMPLATE_ROOT / "styles" / "pages" / name).read_text( encoding="utf-8" ) for selector in selectors: block = re.search( rf"{re.escape(selector)}\s*\{{(?P.*?)\}}", content, re.DOTALL, ) self.assertIsNotNone(block) self.assertIn("var(--jx3-", block.group("body")) def test_rejects_unknown_component_declaration(self): components = (TEMPLATE_ROOT / "styles" / "components.css").read_text( encoding="utf-8" ) with self.assertRaises(ValueError): self.repository._select_component_styles(components, {"missing"}) def test_cache_returns_same_template_instance(self): first = asyncio.run(self.repository.get("helps.html")) shared_assets = self.repository._shared_assets second = asyncio.run(self.repository.get("helps.html")) self.assertIs(first, second) asyncio.run(self.repository.get("bilei.html")) self.assertIs(shared_assets, self.repository._shared_assets) self.repository.clear() self.assertIsNone(self.repository._shared_assets) def test_rejects_path_traversal(self): with self.assertRaises(ValueError): asyncio.run(self.repository.get("../helps.html")) def test_help_page_catalogue_structure(self): fragment = (TEMPLATE_ROOT / "pages" / "helps.html").read_text( encoding="utf-8" ) categories = re.findall( r'
([^<]+)
', fragment ) features = re.findall(r'
(.*?)', fragment) self.assertEqual( [ "活动情报", "名剑相关", "排行榜单", "物价交易", "阵营战场", "角色名片", "奇遇相关", "百战记录", "角色信息", "资历相关", "心法配装", "游戏社区", "百度贴吧", "系统工具", "杂项功能", "避雷功能", "推送功能", ], categories, ) self.assertEqual(69, len(features)) self.assertEqual(105, len(commands)) self.assertEqual(105, len(set(commands))) self.assertIn("共 17 个功能分类 · 69 项功能 · 105 条指令", fragment) if __name__ == "__main__": unittest.main()