105 lines
4.7 KiB
Python
105 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
from html.parser import HTMLParser
|
|
from pathlib import Path
|
|
|
|
|
|
STATIC_DIR = Path(__file__).resolve().parents[1] / "static"
|
|
|
|
|
|
class IdCollector(HTMLParser):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.ids: list[str] = []
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
self.ids.extend(value for key, value in attrs if key == "id" and value)
|
|
|
|
|
|
class FrontendContractTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.html = (STATIC_DIR / "index.html").read_text(encoding="utf-8")
|
|
cls.script = (STATIC_DIR / "app.js").read_text(encoding="utf-8")
|
|
cls.ui_core = (STATIC_DIR / "ui-core.js").read_text(encoding="utf-8")
|
|
collector = IdCollector()
|
|
collector.feed(cls.html)
|
|
cls.ids = collector.ids
|
|
|
|
def test_html_ids_are_unique(self):
|
|
duplicates = sorted({item for item in self.ids if self.ids.count(item) > 1})
|
|
self.assertEqual(duplicates, [])
|
|
|
|
def test_literal_id_selectors_exist_in_html(self):
|
|
selectors = set(re.findall(r'querySelector\("#([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
|
|
selectors.update(re.findall(r'getElementById\("([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
|
|
selectors.update(re.findall(r'setText\("([A-Za-z][A-Za-z0-9_-]*)"', self.script))
|
|
missing = sorted(selectors - set(self.ids))
|
|
self.assertEqual(missing, [])
|
|
|
|
def test_all_primary_views_have_navigation_entries(self):
|
|
views = set(re.findall(r'id="([A-Za-z][A-Za-z0-9_-]*View|limitPool)" class="workspace-view', self.html))
|
|
navigation = set(re.findall(r'data-view="([A-Za-z][A-Za-z0-9_-]*)"', self.html))
|
|
self.assertEqual(views, navigation)
|
|
self.assertEqual(len(views), 13)
|
|
|
|
def test_public_knowledge_editors_are_hidden_for_non_admins(self):
|
|
self.assertIn('document.querySelector("#reasonForm").hidden = !isAdmin;', self.script)
|
|
self.assertIn('document.querySelector("#sectorPhaseManager").hidden = !isAdmin;', self.script)
|
|
self.assertIn('const canManage = state.user?.role === "admin";', self.script)
|
|
|
|
def test_shared_ui_core_loads_before_application(self):
|
|
self.assertLess(
|
|
self.html.index('<script src="/ui-core.js"'),
|
|
self.html.index('<script src="/app.js"'),
|
|
)
|
|
for function_name in (
|
|
"number", "clamp", "escapeHtml", "formatNumber", "formatTimestamp",
|
|
"displayCompactDate", "todayString", "localDateString", "parseLocalDate",
|
|
):
|
|
self.assertIn(f"function {function_name}(", self.ui_core)
|
|
|
|
def test_business_views_do_not_expose_engineering_source_labels(self):
|
|
prohibited = (
|
|
"Tushare 实时行情",
|
|
"Tushare 日K",
|
|
"SQLite 缓存",
|
|
"演示日K",
|
|
"rt_k 实时截面",
|
|
)
|
|
for label in prohibited:
|
|
self.assertNotIn(label, self.script)
|
|
|
|
def test_stock_hover_preview_always_uses_latest_market_context(self):
|
|
start = self.script.index("async function showStockPreview")
|
|
end = self.script.index("function renderStockPreviewLoading", start)
|
|
preview_loader = self.script[start:end]
|
|
self.assertIn('const cacheKey = `${code}:latest`;', preview_loader)
|
|
self.assertIn('/preview`', preview_loader)
|
|
self.assertNotIn("trade_date", preview_loader)
|
|
self.assertNotIn("elements.tradeDate.value", preview_loader)
|
|
|
|
def test_membership_copy_includes_review_assistant_access(self):
|
|
self.assertIn("复盘助手仅对会员开放", self.html)
|
|
self.assertIn("智能选股、问师、问天、复盘助手等智能功能", self.html)
|
|
self.assertIn("自选股、复盘记录与交易日志", self.html)
|
|
self.assertIn("每日智能分析额度", self.html)
|
|
|
|
def test_review_assistant_uses_the_same_member_gate_pattern(self):
|
|
self.assertIn('id="assistantMemberGate" class="member-gate assistant-member-gate"', self.html)
|
|
self.assertIn('id="assistantMemberContent" class="assistant-member-content"', self.html)
|
|
self.assertIn('elements.assistantDialog.classList.toggle("member-locked", !unlocked);', self.script)
|
|
self.assertIn('button.disabled = !unlocked || state.assistantLoading;', self.script)
|
|
|
|
def test_trade_log_editor_is_dialog_based(self):
|
|
self.assertIn('id="openTradeLogDialog"', self.html)
|
|
self.assertIn('id="tradeLogDialog" class="settings-dialog trade-log-dialog"', self.html)
|
|
self.assertIn('elements.tradeLogDialog.showModal()', self.script)
|
|
self.assertIn('renderTradeLog();\n closeTradeLogDialog();', self.script)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|