锁账后写保护与闭期补录留痕;重开须审批并按版本链再结;列表/导出改走真实 API。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
"""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()
|