"""HEL-230: 提醒管理页通栏三块与三步发送流的结构/响应式契约。""" 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 RemindersPageSourceContractTests(unittest.TestCase): def test_admin_html_order_and_send_flow(self) -> None: html = (WEB / "admin.html").read_text(encoding="utf-8") self.assertIn('data-page="reminders"', html) self.assertIn('id="pending-reminders-card"', html) self.assertIn('id="reminder-history-card"', html) self.assertIn('id="send-reminder-card"', html) self.assertIn('class="send-flow"', html) self.assertIn('class="sf-step"', html) self.assertIn('id="pick-count"', html) self.assertIn('id="reminderForm"', html) self.assertIn('id="reminderCompanyList"', html) self.assertIn('id="reminder-type"', html) self.assertIn('id="reminder-content"', html) self.assertIn('id="reminder-deadline"', html) self.assertIn('id="send-btn"', html) self.assertIn('id="send-hint"', html) self.assertIn('id="reminder-tbody"', html) self.assertIn('id="reminder-tabs"', html) self.assertIn('id="reminder-detail-drawer"', html) self.assertIn("design-system.css?v=10", html) self.assertIn("app.js?v=15", html) pending = html.index('id="pending-reminders-card"') history = html.index('id="reminder-history-card"') send = html.index('id="send-reminder-card"') self.assertLess(pending, history) self.assertLess(history, send) reminders = html[html.index('data-page="reminders"') : html.index("新增公司弹窗")] self.assertNotIn("grid-1-2", reminders) self.assertEqual(reminders.count('class="sf-step"'), 3) self.assertIn("选择接收公司", reminders) self.assertIn("填写提醒内容", reminders) self.assertIn("设定截止并发送", reminders) def test_app_js_keeps_real_reminder_apis(self) -> None: js = (WEB / "app.js").read_text(encoding="utf-8") self.assertIn('label.className = "pick"', js) self.assertIn("function updatePickCount", js) self.assertIn("function syncPendingPicks", js) self.assertIn("/api/admin/reminders/pending", js) self.assertIn("/api/admin/reminders/manual", js) self.assertIn("/api/admin/reminders/send", js) self.assertIn("/api/admin/reminders/scan", js) self.assertIn("function loadAdminRemindersPending", js) self.assertIn("function loadAdminRemindersHistory", js) self.assertIn("function openReminderDetailDrawer", js) self.assertIn("initAdminReminders()", js) self.assertIn("dataset.companyId", js) def test_design_system_send_flow_uses_tokens(self) -> None: css = (WEB / "design-system.css").read_text(encoding="utf-8") self.assertIn(".send-flow", css) self.assertIn(".sf-step", css) self.assertIn(".pick", css) self.assertIn("minmax(0, 1.5fr)", css) block_start = css.index("提醒管理:三步发送流") block = css[block_start:] self.assertIn("var(--border)", block) self.assertIn("var(--accent-soft)", block) self.assertIn("var(--accent)", block) self.assertIn("@media (max-width: 1100px)", block) self.assertNotRegex(block, r"#[0-9a-fA-F]{3,8}") self.assertNotRegex(block, r"rgb\(") self.assertNotIn("box-shadow: 0 0", block) 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 RemindersPageLayoutSmokeTests(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 _open_reminders(self, page, width: int) -> None: page.route("**/app.js**", lambda route: route.abort()) page.set_viewport_size({"width": width, "height": 900}) page.goto(f"{self.base}/admin.html") page.evaluate( """() => { document.querySelectorAll('.app-view').forEach((el) => { el.classList.toggle('is-active', el.dataset.page === 'reminders'); }); const list = document.getElementById('reminderCompanyList'); if (list && !list.children.length) { ['A公司', 'B公司', '郑州金牛建业煤炭有限责任公司'].forEach((name, i) => { const label = document.createElement('label'); label.className = 'pick'; label.innerHTML = '' + name; if (i < 2) label.querySelector('input').checked = true; list.append(label); }); } }""" ) def test_send_flow_columns_and_no_page_overflow(self) -> None: html = (WEB / "admin.html").read_text(encoding="utf-8") self.assertIn("app.js?v=15", html) with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() self._open_reminders(page, 1440) cols_wide = page.evaluate( "() => getComputedStyle(document.querySelector('.send-flow')).gridTemplateColumns" ) self.assertEqual(len(cols_wide.split()), 3, f"桌面端应为三列,实际: {cols_wide}") overflow_wide = page.evaluate( "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" ) self.assertFalse(overflow_wide, "1440px 出现页面级横向溢出") self._open_reminders(page, 900) cols_narrow = page.evaluate( "() => getComputedStyle(document.querySelector('.send-flow')).gridTemplateColumns" ) self.assertEqual(len(cols_narrow.split()), 1, f"窄屏应为单列,实际: {cols_narrow}") for width in (360, 820, 1440): self._open_reminders(page, width) overflow = page.evaluate( "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" ) self.assertFalse(overflow, f"{width}px 出现页面级横向溢出") browser.close() if __name__ == "__main__": unittest.main()