from __future__ import annotations import re import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] INDEX = (ROOT / "admin" / "index.html").read_text(encoding="utf-8") STYLES = (ROOT / "admin" / "styles.css").read_text(encoding="utf-8") APP = (ROOT / "admin" / "app.js").read_text(encoding="utf-8") HTTPAPP = (ROOT / "datahub" / "httpapp.py").read_text(encoding="utf-8") class ConsoleShellTests(unittest.TestCase): """The console shell must carry the site-session gate, not its own login.""" def test_independent_login_and_password_change_are_gone(self) -> None: for removed in ("login-form", "change-form", "login-view", "change-view", 'value="hub_admin"'): self.assertNotIn(removed, INDEX, f"{removed} 属于已废弃的独立账号体系") self.assertNotIn("/admin/api/login", APP) self.assertNotIn("/admin/api/change-password", APP) def test_gate_offers_a_way_back_to_the_review_site(self) -> None: for element in ("gate-view", "gate-title", "gate-desc", "gate-login", "gate-retry"): self.assertIn(element, INDEX) self.assertIn("/admin/api/session", APP) self.assertIn("login_url", APP) def test_nav_exposes_the_pages_this_console_now_owns(self) -> None: for page in ("overview", "sources", "models", "members", "lineage"): self.assertIn(f'data-nav="{page}"', INDEX) # 路由白名单必须与导航一致,否则点了导航会回落到总览 routed = re.search(r"return \[([^\]]+)\]\.includes\(h\)", APP) assert routed is not None for page in ("overview", "sources", "models", "members", "lineage"): self.assertIn(f"'{page}'", routed.group(1)) class ThemeTokenTests(unittest.TestCase): """Day/night is one token set with two value sets — never stacked overrides.""" def test_both_themes_define_the_same_tokens(self) -> None: night = _token_block(':root,\n:root[data-theme="night"]') day = _token_block(':root[data-theme="day"]') self.assertTrue(night) self.assertEqual( sorted(night), sorted(day), "日间主题必须覆盖同一组变量名,缺一个就会漏出夜间色", ) def test_no_hardcoded_colours_escape_the_token_set(self) -> None: # 颜色只要写死在 JS 或组件样式里,切主题就会有一块保持夜间色。 self.assertEqual([], re.findall(r"#[0-9a-fA-F]{6}", APP)) after_tokens = STYLES.split("/* ---------- ambient background", 1)[1] self.assertEqual([], re.findall(r"#[0-9a-fA-F]{6}", after_tokens)) def test_no_dark_literal_paint_survives_outside_the_token_blocks(self) -> None: """A literal dark rgba() would stay dark in day mode — tokens only. Accent tints are allowed: they are low-opacity washes of the four status hues and read correctly on either background. """ accent = {(34, 211, 238), (52, 211, 153), (251, 191, 36), (248, 113, 113)} offenders = [] body = STYLES.split("/* ---------- ambient background", 1)[1] for line in body.splitlines(): for match in re.finditer(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)", line): rgb = tuple(int(match.group(index)) for index in (1, 2, 3)) if rgb in accent or sum(rgb) >= 250: continue offenders.append(line.strip()[:80]) self.assertEqual([], offenders, "深色字面值必须收敛成 token,否则日间主题会漏出夜间底色") def test_theme_choice_survives_a_reload(self) -> None: self.assertIn("localStorage.getItem('datahub-theme')", APP) self.assertIn("localStorage.setItem('datahub-theme'", APP) self.assertIn('data-theme="night"', INDEX) def test_svg_colours_go_through_style_so_tokens_apply(self) -> None: # var() 在 SVG 呈现属性里支持不稳,必须写进 style 才吃得到主题变量。 for attribute in ('fill="var(', 'stroke="var(', 'stop-color="var('): self.assertNotIn(attribute, APP, f"{attribute} 应改写为 style 声明") class NarrowScreenTests(unittest.TestCase): def test_narrow_layout_stacks_inputs_and_buttons(self) -> None: self.assertIn("@media (max-width: 1100px)", STYLES) narrow = STYLES.split("@media (max-width: 1100px)", 1)[1] self.assertIn(".field-row { grid-template-columns: minmax(0, 1fr); }", narrow) # 凭证行与模型行在窄屏都要竖排,按钮才不会和输入框抢同一行 self.assertIn(".cred-line { flex-direction: column;", narrow) self.assertIn(".model-row { flex-direction: column;", narrow) class ConsoleEndpointTests(unittest.TestCase): def test_every_endpoint_the_page_calls_is_routed(self) -> None: called = { path.split("?")[0] for path in re.findall(r"api\('(/admin/api/[^']+)'", APP) } self.assertTrue(called) for path in called: if path.startswith("/admin/api/sources/"): continue self.assertIn(f'"{path}"', HTTPAPP, f"{path} 前端在调,后端没路由") def test_write_endpoints_are_reached_with_the_csrf_header(self) -> None: self.assertIn("X-CSRF-Token", APP) self.assertIn("check_csrf", HTTPAPP) def _token_block(selector: str) -> list[str]: start = STYLES.index(selector) body = STYLES[start:].split("}", 1)[0] return re.findall(r"(--[a-z0-9-]+):", body) if __name__ == "__main__": unittest.main() class StylesheetIntegrityTests(unittest.TestCase): """HEL-560 改造中曾误把样式表尾部整段截断,抽屉/弹层/toast 全部失样, 页面照样能跑、单测照样绿。这里把"每个仍在用的组件都得有样式"钉死。""" def test_every_component_the_page_renders_still_has_its_own_rules(self) -> None: for selector in ( ".auth-wrap", ".drawer", ".drawer-mask", ".drawer-hd", ".drawer-tab", ".modal-mask", ".modal-box", ".modal-actions", ".opbtn", ".empty-hint", ".toast", ".row-fail", ".row-off", ".spark-end", ".cred-box", ".model-row", ".vend-manual", ".gate-panel", ".dtable", ".invite-code", ".table-foot", ".pbtn", ".field", ".form-hint", ): self.assertIn(f"{selector} ", STYLES, f"{selector} 的样式丢了") def test_stylesheet_braces_stay_balanced(self) -> None: body = STYLES[STYLES.index("/* ================= index.css"):] self.assertEqual(body.count("{"), body.count("}"))