feat(topology): render temporal graph from database

This commit is contained in:
2026-07-09 22:04:31 +08:00
parent 0dbcd3086e
commit 6ff18623e0
5 changed files with 430 additions and 451 deletions
+83 -37
View File
@@ -1,27 +1,26 @@
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
from werkzeug.serving import make_server
ROOT = Path(__file__).resolve().parents[1]
from topology_api.app import create_app
@contextmanager
def local_server():
handler = functools.partial(SimpleHTTPRequestHandler, directory=str(ROOT))
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
def topology_server():
app = create_app()
app.config.update(TESTING=True)
server = make_server("127.0.0.1", 0, app, threaded=True)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{server.server_port}"
port = server.socket.getsockname()[1]
yield f"http://127.0.0.1:{port}"
finally:
server.shutdown()
server.server_close()
@@ -29,44 +28,91 @@ def local_server():
class GuanghanTopologyE2ETests(unittest.TestCase):
def test_rendered_topology_and_modal_interactions(self) -> None:
def test_historical_graph_and_ghc05_rendering(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",
page = await browser.new_page(
viewport={"width": 1440, "height": 1000}
)
console_errors: list[str] = []
page.on(
"console",
lambda msg: (
console_errors.append(msg.text)
if msg.type == "error"
else None
),
)
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(
async def open_mission(code: str) -> None:
await page.goto(
f"{base_url}/guanghan_topology_demo.html"
f"?mission={code}",
wait_until="networkidle",
)
await page.wait_for_function(
"""code => (
document.querySelector('#page-subtitle')
?.textContent.startsWith(code + ' ·')
)""",
arg=code,
)
await open_mission("BL-01")
self.assertEqual(
await page.locator(".topology-node").count(), 1
)
self.assertEqual(
await page.locator(".connection").count(), 0
)
self.assertEqual(await page.locator(".radiator").count(), 0)
await open_mission("GHC-01")
self.assertEqual(
await page.locator(".topology-node").count(), 2
)
self.assertEqual(
await page.locator(".connection").count(), 0
)
await open_mission("GHC-05")
self.assertEqual(
await page.locator(".topology-node").count(), 14
)
self.assertEqual(
await page.locator(".connection.pressurized").count(), 2
)
self.assertEqual(await page.locator(".radiator").count(), 4)
strokes = await page.locator(
".connection.pressurized"
).evaluate_all(
"(els) => els.map((el) => getComputedStyle(el).stroke)"
)
self.assertEqual(strokes, ["rgb(81, 232, 156)", "rgb(81, 232, 156)"])
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, [])
overflows = await page.locator(
".topology-node"
).evaluate_all(
"""nodes => nodes.flatMap(node => {
const rect = node.querySelector('rect').getBBox();
return [...node.querySelectorAll('text')]
.map(text => ({text: text.textContent,
box: text.getBBox()}))
.filter(item =>
item.box.x < rect.x ||
item.box.x + item.box.width > rect.x + rect.width
);
})"""
)
self.assertEqual(overflows, [])
self.assertEqual(console_errors, [])
await browser.close()
with local_server() as base_url:
with topology_server() as base_url:
asyncio.run(scenario(base_url))