test(topology): verify database-driven presentation rendering
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
# 广寒基地拓扑图表现层数据库化验证报告
|
||||||
|
|
||||||
|
日期:2026-07-09
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
页面已经切换为 B1 表现层数据库驱动:节点样式、连接样式、散热器样式和图例来自 `layout.presentation`。
|
||||||
|
|
||||||
|
## 自动化测试
|
||||||
|
|
||||||
|
命令:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe -m unittest tests.test_topology_api tests.test_guanghan_topology_demo tests.test_guanghan_topology_e2e -v
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:**27 tests OK**
|
||||||
|
|
||||||
|
- API 测试:14 passed(含 presentation payload 验证、DB 风格变更反映)
|
||||||
|
- 源码测试:8 passed(含硬编码 CSS 清除、legend 动态渲染)
|
||||||
|
- E2E 测试:5 passed(含 DB 风格变更浏览器反映、历史快照、legend 验证)
|
||||||
|
|
||||||
|
## 服务验证
|
||||||
|
|
||||||
|
`http://localhost:8080/health` 返回 200。
|
||||||
|
|
||||||
|
## 人工核查
|
||||||
|
|
||||||
|
- GHC-05 空间结构保持不变。
|
||||||
|
- 两条加压走道仍为数据库默认绿色实线(`#51e89c`)。
|
||||||
|
- 散热器不覆盖加压走道和模块主体。
|
||||||
|
- 图例由 API 返回并渲染(`renderLegend()` 从 `layout.presentation.legendItems` 动态生成)。
|
||||||
|
- 临时修改数据库连接线颜色后,浏览器渲染随之变化(E2E 验证 `#00ffaa`)。
|
||||||
|
- 临时修改数据库核心舱填充色后,浏览器渲染随之变化(E2E 验证 `#123456`)。
|
||||||
|
|
||||||
|
## 数据库表
|
||||||
|
|
||||||
|
新增 5 张表现层表:
|
||||||
|
|
||||||
|
| 表 | 用途 |
|
||||||
|
|----|------|
|
||||||
|
| `diagram_themes` | 主题元数据(默认 `guanghan_dark_engineering_v1`) |
|
||||||
|
| `diagram_node_styles` | 节点样式(5 种:node-card, core-card, power-card, power-node-card, vehicle-card) |
|
||||||
|
| `diagram_edge_styles` | 连接样式(6 种:structural_mount, pressurized_passage, fuel, power, cooling, docking) |
|
||||||
|
| `diagram_decoration_styles` | 装饰样式(1 种:radiator) |
|
||||||
|
| `diagram_legend_items` | 图例项(4 项:结构直连, 加压走道, 燃料/供电, 大型折叠散热面板) |
|
||||||
@@ -81,11 +81,11 @@ class GuanghanTopologyE2ETests(unittest.TestCase):
|
|||||||
await page.locator(".topology-node").count(), 14
|
await page.locator(".topology-node").count(), 14
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
await page.locator(".connection.pressurized").count(), 2
|
await page.locator('.connection[data-type="pressurized_passage"]').count(), 2
|
||||||
)
|
)
|
||||||
self.assertEqual(await page.locator(".radiator").count(), 4)
|
self.assertEqual(await page.locator(".radiator").count(), 4)
|
||||||
strokes = await page.locator(
|
strokes = await page.locator(
|
||||||
".connection.pressurized"
|
'.connection[data-type="pressurized_passage"]'
|
||||||
).evaluate_all(
|
).evaluate_all(
|
||||||
"(els) => els.map((el) => getComputedStyle(el).stroke)"
|
"(els) => els.map((el) => getComputedStyle(el).stroke)"
|
||||||
)
|
)
|
||||||
@@ -220,5 +220,135 @@ class GuanghanTopologyE2ETests(unittest.TestCase):
|
|||||||
asyncio.run(scenario(base_url))
|
asyncio.run(scenario(base_url))
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_edge_style_changes_browser_rendering(self) -> None:
|
||||||
|
async def scenario(base_url: str) -> None:
|
||||||
|
from topology_api.db import get_writer_connection
|
||||||
|
|
||||||
|
with get_writer_connection() as conn:
|
||||||
|
conn.execute("SET search_path TO topology, public")
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT stroke_color
|
||||||
|
FROM diagram_edge_styles
|
||||||
|
WHERE connection_type = 'pressurized_passage'
|
||||||
|
""")
|
||||||
|
original_color = cur.fetchone()["stroke_color"]
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE diagram_edge_styles
|
||||||
|
SET stroke_color = '#00ffaa'
|
||||||
|
WHERE connection_type = 'pressurized_passage'
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
try:
|
||||||
|
async with async_playwright() as pw:
|
||||||
|
browser = await pw.chromium.launch(headless=True)
|
||||||
|
page = await browser.new_page(
|
||||||
|
viewport={"width": 1440, "height": 1000}
|
||||||
|
)
|
||||||
|
await page.goto(
|
||||||
|
f"{base_url}/guanghan_topology_demo.html"
|
||||||
|
"?mission=GHC-05",
|
||||||
|
wait_until="networkidle",
|
||||||
|
)
|
||||||
|
await page.wait_for_function(
|
||||||
|
"""() => document.querySelectorAll(
|
||||||
|
'.connection[data-type="pressurized_passage"]'
|
||||||
|
).length === 2"""
|
||||||
|
)
|
||||||
|
strokes = await page.locator(
|
||||||
|
'.connection[data-type="pressurized_passage"]'
|
||||||
|
).evaluate_all(
|
||||||
|
"(els) => els.map((el) => getComputedStyle(el).stroke)"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
strokes,
|
||||||
|
["rgb(0, 255, 170)", "rgb(0, 255, 170)"],
|
||||||
|
)
|
||||||
|
await browser.close()
|
||||||
|
finally:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE diagram_edge_styles
|
||||||
|
SET stroke_color = %(original)s
|
||||||
|
WHERE connection_type = 'pressurized_passage'
|
||||||
|
""", {"original": original_color})
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
with topology_server() as base_url:
|
||||||
|
asyncio.run(scenario(base_url))
|
||||||
|
|
||||||
|
def test_database_node_style_changes_browser_rendering(self) -> None:
|
||||||
|
async def scenario(base_url: str) -> None:
|
||||||
|
from topology_api.db import get_writer_connection
|
||||||
|
|
||||||
|
with get_writer_connection() as conn:
|
||||||
|
conn.execute("SET search_path TO topology, public")
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT fill_color
|
||||||
|
FROM diagram_node_styles
|
||||||
|
WHERE style_key = 'core-card'
|
||||||
|
""")
|
||||||
|
original_fill = cur.fetchone()["fill_color"]
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE diagram_node_styles
|
||||||
|
SET fill_color = '#123456'
|
||||||
|
WHERE style_key = 'core-card'
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
try:
|
||||||
|
async with async_playwright() as pw:
|
||||||
|
browser = await pw.chromium.launch(headless=True)
|
||||||
|
page = await browser.new_page(
|
||||||
|
viewport={"width": 1440, "height": 1000}
|
||||||
|
)
|
||||||
|
await page.goto(
|
||||||
|
f"{base_url}/guanghan_topology_demo.html"
|
||||||
|
"?mission=GHC-05",
|
||||||
|
wait_until="networkidle",
|
||||||
|
)
|
||||||
|
core_fill = await page.locator(
|
||||||
|
'.topology-node[data-node-key="core_cabin"] rect'
|
||||||
|
).evaluate(
|
||||||
|
"(el) => getComputedStyle(el).fill"
|
||||||
|
)
|
||||||
|
self.assertEqual(core_fill, "rgb(18, 52, 86)")
|
||||||
|
await browser.close()
|
||||||
|
finally:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE diagram_node_styles
|
||||||
|
SET fill_color = %(original)s
|
||||||
|
WHERE style_key = 'core-card'
|
||||||
|
""", {"original": original_fill})
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
with topology_server() as base_url:
|
||||||
|
asyncio.run(scenario(base_url))
|
||||||
|
|
||||||
|
def test_legend_renders_from_database_presentation(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": 1000}
|
||||||
|
)
|
||||||
|
await page.goto(
|
||||||
|
f"{base_url}/guanghan_topology_demo.html?mission=GHC-05",
|
||||||
|
wait_until="networkidle",
|
||||||
|
)
|
||||||
|
labels = await page.locator("#legend .legend-item").evaluate_all(
|
||||||
|
"(items) => items.map(item => item.textContent.trim())"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
labels,
|
||||||
|
["结构直连", "加压走道", "燃料 / 供电", "大型折叠散热面板"],
|
||||||
|
)
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
with topology_server() as base_url:
|
||||||
|
asyncio.run(scenario(base_url))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user