"""HEL-351: 夜间控件、一次性初始密码交付、缩放滚动条契约。""" from __future__ import annotations import os 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 def _prepare_chrome_libs() -> Path | None: candidates = [ROOT / ".chrome-libs" / "lib"] for lib_dir in candidates: if (lib_dir / "libatk-1.0.so.0").exists(): current = os.environ.get("LD_LIBRARY_PATH", "") prefix = str(lib_dir) if prefix not in current.split(":"): os.environ["LD_LIBRARY_PATH"] = ( f"{prefix}:{current}" if current else prefix ) return lib_dir return None def _chromium_available() -> bool: if not sync_playwright: return False _prepare_chrome_libs() try: with sync_playwright() as p: browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) browser.close() return True except Exception: return False class Hel351SourceContractTests(unittest.TestCase): def test_night_select_and_scrollbar_use_tokens(self) -> None: css = (WEB / "design-system.css").read_text(encoding="utf-8") self.assertIn("color-scheme: light", css) self.assertIn("color-scheme: dark", css) self.assertIn("scrollbar-color: var(--gold) var(--surface-2)", css) self.assertIn("html[data-theme=\"night\"] ::-webkit-scrollbar-thumb", css) self.assertIn("background-color: var(--gold)", css) self.assertIn(".select option", css) self.assertIn("background-color: var(--surface)", css) self.assertIn("html[data-theme=\"night\"] .ds-dp-day.is-selected", css) self.assertIn("color: var(--gold)", css) self.assertNotIn("overflow: hidden; /* HEL-351", css) def test_tabs_and_table_split_overflow_axes(self) -> None: css = (WEB / "design-system.css").read_text(encoding="utf-8") tabs = css[css.index(".tabs {") : css.index(".tabs button")] self.assertIn("overflow-x: auto", tabs) self.assertIn("overflow-y: hidden", tabs) wrap = css[css.index(".table-wrap {") : css.index(".table-wrap.dash-master-scroll")] self.assertIn("overflow-x: auto", wrap) self.assertIn("overflow-y: hidden", wrap) self.assertIn(".table-wrap.dash-master-scroll", css) self.assertIn("overflow-y: auto", css[css.index(".table-wrap.dash-master-scroll") :][:180]) def test_admin_delivers_once_password_window(self) -> None: html = (WEB / "admin.html").read_text(encoding="utf-8") self.assertIn('id="credentialDialog"', html) self.assertIn('id="cred-pass"', html) self.assertIn('id="cred-copy-pass"', html) self.assertIn("datepicker.js?v=1", html) self.assertNotIn("初始密码由管理员统一发放", html) js = (WEB / "app.js").read_text(encoding="utf-8") self.assertIn("function showOnceCredentials(", js) self.assertIn("function wipeCredentials(", js) self.assertNotIn("初始密码已生成(仅此一次显示):${result.initial_password}", js) self.assertNotIn("临时密码已生成(仅此一次):${result.initial_password}", js) self.assertIn('openModal("credentialDialog")', js) def test_datepicker_follows_trigger_and_flips(self) -> None: js = (WEB / "datepicker.js").read_text(encoding="utf-8") css = (WEB / "design-system.css").read_text(encoding="utf-8") self.assertIn("visualViewport", js) self.assertIn("getBoundingClientRect", js) self.assertIn("rect.top - gap - height", js) self.assertIn('addEventListener("scroll", position, true)', js) block = css[css.index(".ds-datepicker {") : css.index(".ds-datepicker[hidden]")] self.assertIn("position: fixed", block) self.assertIn("z-index: var(--z-tooltip)", block) @unittest.skipUnless(sync_playwright, "playwright 未安装,跳过浏览器探针") @unittest.skipUnless(_chromium_available(), "chromium 无法启动,跳过浏览器探针") class Hel351BrowserProbeTests(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 _open(self, page, *, night: bool, width: int = 1440, zoom: float = 1) -> None: page.set_viewport_size({"width": width, "height": 900}) theme_attr = 'data-theme="night"' if night else 'data-theme="day"' page.set_content( f"""