diff --git a/data/wiki/guanghan_topology_demo.html b/data/wiki/guanghan_topology_demo.html index 5485683..de9a32b 100644 --- a/data/wiki/guanghan_topology_demo.html +++ b/data/wiki/guanghan_topology_demo.html @@ -153,10 +153,93 @@ text-align: center; } + body.modal-open { overflow: hidden; } + + .modal-backdrop { + position: fixed; + inset: 0; + z-index: 20; + display: none; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(2, 7, 14, 0.82); + backdrop-filter: blur(8px); + } + + .modal-backdrop[aria-hidden="false"] { display: flex; } + + .detail-dialog { + position: relative; + width: min(760px, 100%); + max-height: min(820px, calc(100vh - 48px)); + overflow: auto; + padding: 30px; + border: 1px solid #405572; + border-radius: 18px; + outline: none; + background: linear-gradient(145deg, #13233a, #0b1424 72%); + box-shadow: 0 30px 100px rgba(0, 0, 0, 0.64); + } + + .detail-dialog:focus-visible { box-shadow: 0 0 0 3px var(--pressurized), 0 30px 100px rgba(0, 0, 0, 0.64); } + + .modal-close { + position: absolute; + top: 14px; + right: 14px; + width: 40px; + height: 40px; + border: 1px solid #4e627e; + border-radius: 50%; + background: #182a43; + color: var(--text); + font: 500 1.65rem/1 system-ui, sans-serif; + cursor: pointer; + } + + .modal-close:hover, + .modal-close:focus-visible { border-color: var(--pressurized); outline: none; background: #203754; } + + .detail-kicker { + margin: 0 46px 6px 0; + color: var(--pressurized); + font-size: 0.77rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + } + + #detail-title { margin: 0 50px 10px 0; font-size: 1.85rem; } + #detail-role { margin: 0 0 22px; color: #c4d2e4; line-height: 1.7; } + + .detail-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin: 0; + } + + .detail-grid > div { + min-width: 0; + padding: 14px; + border: 1px solid #293c57; + border-radius: 10px; + background: rgba(8, 18, 33, 0.7); + } + + .detail-grid dt { margin-bottom: 7px; color: #7f94b1; font-size: 0.75rem; } + .detail-grid dd { margin: 0; color: #e1ebf8; line-height: 1.55; overflow-wrap: anywhere; } + .detail-grid dd span { display: block; } + .detail-grid dd span + span { margin-top: 4px; } + @media (max-width: 760px) { body { padding: 14px; } .topology-map { min-width: 980px; } .legend { justify-content: flex-start; } + .modal-backdrop { padding: 12px; } + .detail-dialog { padding: 25px 18px 20px; max-height: calc(100vh - 24px); } + .detail-grid { grid-template-columns: 1fr; } } @@ -331,6 +414,23 @@

模块与车辆支持详情查看;走道、管线和散热面板仅用于表达拓扑关系。

+ + diff --git a/tests/test_guanghan_topology_e2e.py b/tests/test_guanghan_topology_e2e.py new file mode 100644 index 0000000..58445d3 --- /dev/null +++ b/tests/test_guanghan_topology_e2e.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import asyncio +import functools +import threading +import unittest +from contextlib import contextmanager +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +from playwright.async_api import async_playwright + + +ROOT = Path(__file__).resolve().parents[1] + + +@contextmanager +def local_server(): + handler = functools.partial(SimpleHTTPRequestHandler, directory=str(ROOT)) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_port}" + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2) + + +class GuanghanTopologyE2ETests(unittest.TestCase): + def test_rendered_topology_and_modal_interactions(self) -> None: + async def scenario(base_url: str) -> None: + async with async_playwright() as pw: + browser = await pw.chromium.launch(headless=True) + page = await browser.new_page(viewport={"width": 1440, "height": 1100}) + errors: list[str] = [] + page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None) + await page.goto( + f"{base_url}/data/wiki/guanghan_topology_demo.html", + wait_until="networkidle", + ) + + self.assertEqual(await page.locator("[data-module]").count(), 14) + self.assertEqual(await page.locator('[data-connection="pressurized"]').count(), 2) + strokes = await page.locator('[data-connection="pressurized"]').evaluate_all( + "(els) => els.map((el) => getComputedStyle(el).stroke)" + ) + self.assertEqual(strokes, ["rgb(81, 232, 156)", "rgb(81, 232, 156)"]) + + for forbidden in ("未来北部十字结构", "未来东部十字结构", "未来西部十字结构"): + self.assertEqual(await page.get_by_text(forbidden, exact=False).count(), 0) + + await page.locator('[data-module="core"]').click() + self.assertEqual(await page.locator("#detail-modal").count(), 1) + self.assertEqual(await page.locator("#detail-modal").get_attribute("aria-hidden"), "false") + self.assertEqual(await page.locator("#detail-title").inner_text(), "广寒核心舱") + await page.keyboard.press("Escape") + self.assertEqual(await page.locator("#detail-modal").get_attribute("aria-hidden"), "true") + + await page.locator('[data-module="habitat-1"]').click() + self.assertIn("运输:高2.7 m", await page.locator("#detail-dimensions").inner_text()) + await page.locator("#detail-modal").click(position={"x": 5, "y": 5}) + self.assertEqual(await page.locator("#detail-modal").get_attribute("aria-hidden"), "true") + + self.assertEqual(errors, []) + await browser.close() + + with local_server() as base_url: + asyncio.run(scenario(base_url)) + + +if __name__ == "__main__": + unittest.main()