"""HEL-269: 月结面板、重开审批、审计记录页结构与三档宽度。""" from __future__ import annotations 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 PeriodClosePageContractTests(unittest.TestCase): def test_admin_has_closing_reopen_and_audit_surfaces(self) -> None: html = (WEB / "admin.html").read_text(encoding="utf-8") self.assertIn('id="closingPanel"', html) self.assertIn('id="periodTimeline"', html) self.assertIn('data-page="period-audit"', html) self.assertIn('data-view="period-audit"', html) self.assertIn("重开审批", html) self.assertIn('id="reopenQueue"', html) self.assertIn('id="reopenRequestDialog"', html) self.assertIn('id="reopenDecideDialog"', html) self.assertIn("btn-warn", html) settings = html.index('data-view="settings"') audit_nav = html.index('data-view="period-audit"') reminders = html.index('data-view="reminders"') self.assertLess(settings, audit_nav) self.assertLess(audit_nav, reminders) css = (WEB / "design-system.css").read_text(encoding="utf-8") self.assertIn(".btn-warn", css) self.assertIn(".pill-lock", css) self.assertIn(".tl-cell.locked", css) self.assertIn(".diff-grid", css) self.assertIn(".empty-icon", css) js = (WEB / "app.js").read_text(encoding="utf-8") self.assertIn("/api/admin/period-closes", js) self.assertIn("/api/admin/period-reopens", js) self.assertIn("/api/admin/period-audit", js) self.assertIn("/api/flows", js) self.assertIn("/api/company/manual-records", js) 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 PeriodCloseLayoutSmokeTests(unittest.TestCase): @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_settings_and_audit_no_horizontal_overflow(self) -> None: with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.route("**/app.js**", lambda route: route.abort()) for width in (360, 820, 1440): page.set_viewport_size({"width": width, "height": 900}) page.goto(f"{self.base}/admin.html") for view in ("settings", "period-audit"): page.evaluate( """(view) => { document.querySelectorAll('.app-view').forEach((el) => { el.classList.toggle('is-active', el.dataset.page === view); }); }""", view, ) overflow = page.evaluate( "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" ) self.assertFalse(overflow, f"{width}px {view} 出现横向溢出") browser.close() if __name__ == "__main__": unittest.main()