"""HEL-177: 公司端转账往来页面(方案 A)结构与接口契约。""" from __future__ import annotations import re import threading import unittest from functools import partial from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path ROOT = Path(__file__).resolve().parents[1] WEB = ROOT / "web" try: from playwright.sync_api import sync_playwright except ImportError: # pragma: no cover sync_playwright = None class TransfersPageSourceContractTests(unittest.TestCase): def test_company_html_has_nav_overview_and_layers(self) -> None: html = (WEB / "company.html").read_text(encoding="utf-8") self.assertIn('data-view="transfers"', html) self.assertIn("转账往来", html) self.assertIn('id="workspaceTransfersCard"', html) self.assertIn("转账往来概览", html) self.assertIn('data-page="transfers"', html) self.assertIn('id="transfersOverviewLayer"', html) self.assertIn('id="transfersDetailLayer"', html) self.assertIn('id="transferEvidenceDrawer"', html) self.assertIn("期间净变动", html) self.assertNotIn("本公司往来合计", html) self.assertIn("design-system.css?v=6", html) self.assertIn("app.js?v=12", html) # 侧栏顺序:流水管理 → 转账往来 → 往来确认 flows = html.index('data-view="flows"') transfers = html.index('data-view="transfers"') reconcile = html.index('data-view="reconcile"') self.assertLess(flows, transfers) self.assertLess(transfers, reconcile) def test_app_js_wires_real_apis_no_demo_totals(self) -> None: js = (WEB / "app.js").read_text(encoding="utf-8") self.assertIn('transfers: "转账往来"', js) self.assertIn("/api/company/intercompany/summary", js) self.assertIn("/api/company/intercompany/events", js) self.assertIn("/api/company/intercompany/export.csv", js) self.assertIn("/api/company/transfer-events/", js) self.assertIn("function loadTransfersSummary", js) self.assertIn("function openTransfersDetail", js) self.assertIn("function openTransferEvidence", js) self.assertIn("function initTransfers", js) self.assertIn("initTransfers()", js) self.assertIn("期间净变动", js) self.assertIn("has_opening", js) # 不得把「期末余额」写死为无期初时的标签 self.assertNotRegex(js, r'netLabelForWindow[^{]+{[^}]*return "期末余额"') def test_design_system_scopes_info_to_transfers_module(self) -> None: css = (WEB / "design-system.css").read_text(encoding="utf-8") self.assertIn('a[data-view="transfers"].active', css) self.assertIn("var(--info-soft)", css) self.assertIn('[data-page="transfers"] .btn-primary', css) self.assertIn(".xfer-split", css) self.assertIn(".xfer-split-pane.confirmed", css) self.assertIn(".xfer-split-pane.pending", css) # 无新色硬编码 self.assertNotRegex(css, r"\.xfer-split[^\{]*\{[^}]*#[0-9a-fA-F]{3,8}") class TransfersDetailObservationContractTests(unittest.TestCase): def test_server_exposes_batch_fields_on_observations(self) -> None: server = (ROOT / "server.py").read_text(encoding="utf-8") self.assertIn("import_batch_id", server) self.assertIn("original_filename", server) self.assertIn("counterparty_account_masked", server) def _chromium_available() -> bool: try: import ctypes.util return bool(ctypes.util.find_library("atk-1.0")) except Exception: return False @unittest.skipUnless(sync_playwright, "playwright 未安装,跳过布局冒烟") @unittest.skipUnless(_chromium_available(), "系统缺少 chromium 依赖库(如 libatk),跳过布局冒烟") class TransfersPageLayoutSmokeTests(unittest.TestCase): """静态壳:360 / 820 / 1440 无横向溢出。""" @classmethod def setUpClass(cls) -> None: handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) cls.port = cls.httpd.server_address[1] cls.thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True) cls.thread.start() cls.base = f"http://127.0.0.1:{cls.port}" @classmethod def tearDownClass(cls) -> None: cls.httpd.shutdown() cls.httpd.server_close() def test_transfers_shell_no_horizontal_overflow(self) -> None: html = (WEB / "company.html").read_text(encoding="utf-8") with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() for width in (360, 820, 1440): page.set_viewport_size({"width": width, "height": 900}) page.set_content( html.replace('src="app.js?v=12"', 'src=""'), base_url=self.base, ) page.evaluate( """() => { document.querySelectorAll('.app-view').forEach((el) => { el.classList.toggle('is-active', el.dataset.page === 'transfers'); }); const data = document.getElementById('transfersData'); const loading = document.getElementById('transfersLoading'); if (loading) loading.hidden = true; if (data) data.hidden = false; }""" ) overflow = page.evaluate( "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" ) self.assertFalse(overflow, f"{width}px 出现横向溢出") browser.close() if __name__ == "__main__": unittest.main()