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
|
||||
)
|
||||
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)
|
||||
strokes = await page.locator(
|
||||
".connection.pressurized"
|
||||
'.connection[data-type="pressurized_passage"]'
|
||||
).evaluate_all(
|
||||
"(els) => els.map((el) => getComputedStyle(el).stroke)"
|
||||
)
|
||||
@@ -220,5 +220,135 @@ class GuanghanTopologyE2ETests(unittest.TestCase):
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user