工作台概览小卡 + 侧栏入口;总览/对方汇总/明细/原始流水抽屉接入 HEL-175/176 接口;无期初时展示期间净变动;模块内用 info 品蓝。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
139 lines
5.8 KiB
Python
139 lines
5.8 KiB
Python
"""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()
|