@@ -0,0 +1,594 @@
|
|||||||
|
# Guanghan GHC-05 Topology Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace the inaccurate Guanghan topology demo with a responsive, interactive single-canvas engineering diagram and correct the related GHC-05 timeline records.
|
||||||
|
|
||||||
|
**Architecture:** Keep the deliverable as one standalone HTML file. Render the topology as an inline SVG with fixed semantic node IDs and connection types, keep module facts in one `MODULE_DETAILS` JavaScript object, and populate one accessible modal from that object. Add Python contract tests for source-level topology facts and a Playwright test for rendered pixels and modal behavior.
|
||||||
|
|
||||||
|
**Tech Stack:** HTML5, CSS, inline SVG, vanilla JavaScript, Python `unittest`, Python `http.server`, Playwright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Create `tests/test_guanghan_topology_demo.py`: static contract tests for node inventory, connection types, future-extension comments, dimensions, and corrected timeline facts.
|
||||||
|
- Create `tests/test_guanghan_topology_e2e.py`: browser test for visible green corridors, hidden future markers, click targets, and modal closing behavior.
|
||||||
|
- Modify `data/wiki/guanghan_topology_demo.html`: complete standalone diagram, centralized details data, and accessible modal.
|
||||||
|
- Modify `user_input/广寒计划/Timeline.md`: remove the nonexistent ring corridor and correct all dependent GHC-05 records.
|
||||||
|
|
||||||
|
### Task 1: Add failing source-contract tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/test_guanghan_topology_demo.py`
|
||||||
|
- Read: `docs/superpowers/specs/2026-07-09-guanghan-topology-design.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the contract test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
HTML_PATH = ROOT / "data" / "wiki" / "guanghan_topology_demo.html"
|
||||||
|
TIMELINE_PATH = ROOT / "user_input" / "广寒计划" / "Timeline.md"
|
||||||
|
|
||||||
|
CLICKABLE_MODULES = {
|
||||||
|
"core",
|
||||||
|
"habitat-1",
|
||||||
|
"habitat-2",
|
||||||
|
"greenhouse",
|
||||||
|
"laboratory",
|
||||||
|
"power-node",
|
||||||
|
"power-north",
|
||||||
|
"power-east",
|
||||||
|
"power-south",
|
||||||
|
"power-west",
|
||||||
|
"wugang-2",
|
||||||
|
"binglun-1",
|
||||||
|
"binglun-2",
|
||||||
|
"binglun-3",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GuanghanTopologySourceTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
cls.html = HTML_PATH.read_text(encoding="utf-8")
|
||||||
|
timeline = TIMELINE_PATH.read_text(encoding="utf-8")
|
||||||
|
cls.ghc05 = timeline.split("GHC-05任务", 1)[1].split("JC-02任务", 1)[0]
|
||||||
|
|
||||||
|
def test_clickable_module_inventory_is_exact(self) -> None:
|
||||||
|
found = set(re.findall(r'data-module="([^"]+)"', self.html))
|
||||||
|
self.assertEqual(found, CLICKABLE_MODULES)
|
||||||
|
|
||||||
|
def test_connection_inventory_matches_approved_topology(self) -> None:
|
||||||
|
self.assertEqual(self.html.count('data-connection="pressurized"'), 2)
|
||||||
|
self.assertIn('data-link="wugang-2--greenhouse"', self.html)
|
||||||
|
self.assertIn('data-link="habitat-1--power-north"', self.html)
|
||||||
|
self.assertNotIn("perimeter-corridor", self.html)
|
||||||
|
|
||||||
|
def test_walkways_and_radiators_are_not_clickable(self) -> None:
|
||||||
|
self.assertNotRegex(
|
||||||
|
self.html,
|
||||||
|
r'<(?:line|path|rect)[^>]+data-connection="pressurized"[^>]+data-module=',
|
||||||
|
)
|
||||||
|
self.assertNotRegex(
|
||||||
|
self.html,
|
||||||
|
r'<(?:g|rect)[^>]+class="[^"]*radiator[^"]*"[^>]+data-module=',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_future_extensions_exist_only_as_source_comments(self) -> None:
|
||||||
|
comments = "\n".join(re.findall(r"<!--(.*?)-->", self.html, flags=re.S))
|
||||||
|
visible_source = re.sub(r"<!--.*?-->", "", self.html, flags=re.S)
|
||||||
|
for direction in ("north", "east", "west"):
|
||||||
|
self.assertIn(f"FUTURE_EXPANSION_ANCHOR:{direction}", comments)
|
||||||
|
self.assertNotIn("未来北部十字结构", visible_source)
|
||||||
|
self.assertNotIn("未来东部十字结构", visible_source)
|
||||||
|
self.assertNotIn("未来西部十字结构", visible_source)
|
||||||
|
|
||||||
|
def test_every_clickable_module_has_dimensions_field(self) -> None:
|
||||||
|
for module_id in CLICKABLE_MODULES:
|
||||||
|
self.assertRegex(
|
||||||
|
self.html,
|
||||||
|
rf'"{re.escape(module_id)}"\s*:\s*\{{.*?dimensions\s*:',
|
||||||
|
msg=f"{module_id} is missing dimensions",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_timeline_contains_only_the_two_real_corridors(self) -> None:
|
||||||
|
for incorrect in (
|
||||||
|
"4段加压走道",
|
||||||
|
"居住舱#1↔居住舱#2",
|
||||||
|
"居住舱#2↔实验舱",
|
||||||
|
"实验舱↔温室",
|
||||||
|
"温室↔居住舱#1",
|
||||||
|
"环形加压通路",
|
||||||
|
"8个对接面",
|
||||||
|
"4段加压走道48小时",
|
||||||
|
):
|
||||||
|
self.assertNotIn(incorrect, self.ghc05)
|
||||||
|
self.assertIn("居住舱#1远端对接口", self.ghc05)
|
||||||
|
self.assertIn("电力模块北部", self.ghc05)
|
||||||
|
self.assertIn("温室远端对接口", self.ghc05)
|
||||||
|
self.assertIn("吴刚#2", self.ghc05)
|
||||||
|
self.assertIn("从温室模块远端接口断开", self.ghc05)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test and verify the expected failures**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest tests.test_guanghan_topology_demo -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: failures for missing `data-module`/`data-connection` contracts, absent future anchor comments, incomplete dimensions data, and the incorrect ring-corridor timeline text.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit the failing contract**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add tests/test_guanghan_topology_demo.py
|
||||||
|
git commit -m "test: define Guanghan topology contract" -m "Co-Authored-By: Codex <noreply@openai.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Correct the GHC-05 timeline record
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `user_input/广寒计划/Timeline.md:270-296`
|
||||||
|
- Test: `tests/test_guanghan_topology_demo.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rewrite the affected GHC-05 entries without changing dates or EVA numbers**
|
||||||
|
|
||||||
|
Apply these factual replacements:
|
||||||
|
|
||||||
|
```text
|
||||||
|
2024-12-25 EVA-7:
|
||||||
|
卸下居住舱#2、实验舱,以及用于“居住舱#1↔电力模块北部”和“温室↔吴刚#2”的两段加压走道组件。
|
||||||
|
|
||||||
|
2024-12-28 EVA-10:
|
||||||
|
居住舱#1安装到核心舱南侧;其远端通过加压走道连接电力模块北部。
|
||||||
|
|
||||||
|
2024-12-31 EVA-13:
|
||||||
|
吴刚#2通过加压走道连接温室远端对接口;对该走道执行结构锁紧与50 psi氦气检漏。
|
||||||
|
|
||||||
|
2025-01-01 EVA-14:
|
||||||
|
复核居住舱#1↔电力模块北部与温室↔吴刚#2两条加压走道的密封、照明和舱门联锁。
|
||||||
|
|
||||||
|
2025-01-02 EVA-15:
|
||||||
|
将“4段加压走道48小时密封保持确认”改为“两段加压走道48小时密封保持确认”。
|
||||||
|
|
||||||
|
2025-01-18 EVA-18:
|
||||||
|
将“从吴刚#2侧断开吴刚#2与广寒核心舱”改为“从吴刚#2侧断开吴刚#2与温室模块远端接口”。
|
||||||
|
```
|
||||||
|
|
||||||
|
Preserve the existing mass, dimensions, power, crew, and mission-operation facts around these sentences.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the timeline contract**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest tests.test_guanghan_topology_demo.GuanghanTopologySourceTests.test_timeline_contains_only_the_two_real_corridors -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `OK`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Review the focused diff**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git diff -- user_input/广寒计划/Timeline.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: only the six affected GHC-05 entries change; dates, EVA numbering, and unrelated facts remain intact.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit the timeline correction**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add user_input/广寒计划/Timeline.md
|
||||||
|
git commit -m "docs: 修正广寒基地加压走道记录" -m "Co-Authored-By: Codex <noreply@openai.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Build the semantic single-canvas topology
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `data/wiki/guanghan_topology_demo.html`
|
||||||
|
- Test: `tests/test_guanghan_topology_demo.py`
|
||||||
|
- Reference: `user_input/广寒计划/Timeline.md`
|
||||||
|
- Reference: `data/wiki/guanghan_ghc01_zh.html`
|
||||||
|
- Reference: `data/wiki/guanghan_ghc03_zh.html`
|
||||||
|
- Reference: `data/wiki/guanghan_ghc05_zh.html`
|
||||||
|
- Reference: `data/wiki/wugang_lunar_lander_zh.html`
|
||||||
|
- Reference: `data/wiki/guanghan_bl01_zh.html`
|
||||||
|
- Reference: `data/wiki/guanghan_bl02_zh.html`
|
||||||
|
- Reference: `data/wiki/guanghan_bl03_zh.html`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the old CSS grid with an inline SVG contract**
|
||||||
|
|
||||||
|
Use this exact semantic pattern for every visible node and connection:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<svg class="topology-map" viewBox="0 0 1500 1700" role="img"
|
||||||
|
aria-labelledby="topology-title topology-desc">
|
||||||
|
<title id="topology-title">广寒基地 GHC-05 任务后拓扑</title>
|
||||||
|
<desc id="topology-desc">核心舱十字结构向南连接电力模块十字结构。</desc>
|
||||||
|
|
||||||
|
<!-- FUTURE_EXPANSION_ANCHOR:north
|
||||||
|
Reserve canvas above habitat-2 for a future cross node and multiple Wugang ports. -->
|
||||||
|
<!-- FUTURE_EXPANSION_ANCHOR:east
|
||||||
|
Reserve canvas east of laboratory for a future cross node. -->
|
||||||
|
<!-- FUTURE_EXPANSION_ANCHOR:west
|
||||||
|
Reserve canvas west of greenhouse for a future cross node and rover ports. -->
|
||||||
|
|
||||||
|
<line class="connection structural" data-connection="structural"
|
||||||
|
x1="750" y1="350" x2="750" y2="460"/>
|
||||||
|
<line class="connection pressurized" data-connection="pressurized"
|
||||||
|
data-link="wugang-2--greenhouse"
|
||||||
|
x1="220" y1="590" x2="350" y2="590"/>
|
||||||
|
<line class="connection pressurized" data-connection="pressurized"
|
||||||
|
data-link="habitat-1--power-north"
|
||||||
|
x1="750" y1="800" x2="750" y2="940"/>
|
||||||
|
|
||||||
|
<g class="topology-node module-node core-node" data-module="core"
|
||||||
|
role="button" tabindex="0" aria-label="查看广寒核心舱详情">
|
||||||
|
<rect x="630" y="480" width="240" height="220" rx="18"/>
|
||||||
|
<text x="750" y="565">广寒核心舱</text>
|
||||||
|
<text class="node-meta" x="750" y="605">四向安装节点</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
Create all 14 `data-module` groups listed in the source-contract test. Position them as approved:
|
||||||
|
|
||||||
|
- Core cross: habitat-2 north, laboratory east, habitat-1 south, greenhouse west.
|
||||||
|
- Wugang-2 beyond greenhouse on the west green corridor.
|
||||||
|
- Power cross below habitat-1: north rack/corridor, east nuclear-fuel/centrifuge, south reactor, west ISRU/mining/storage.
|
||||||
|
- Binglun-1 west, Binglun-2 south, Binglun-3 east, each on purple dashed utility lines.
|
||||||
|
|
||||||
|
Place one non-interactive `.radiator` group above each power branch, offset away from the green north corridor. Do not put `data-module` on radiator groups.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add responsive engineering-map styling**
|
||||||
|
|
||||||
|
Use CSS variables and explicit line styles:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--bg: #070d18;
|
||||||
|
--panel: #0d1728;
|
||||||
|
--structural: #8fa1b8;
|
||||||
|
--pressurized: #51e89c;
|
||||||
|
--utility: #b178e6;
|
||||||
|
--core: #ffad42;
|
||||||
|
--habitat: #52baff;
|
||||||
|
--power: #d5b642;
|
||||||
|
--vehicle: #c28cdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-shell { overflow: auto; border: 1px solid #26344a; border-radius: 16px; }
|
||||||
|
.topology-map { display: block; width: 100%; min-width: 980px; height: auto; background: var(--bg); }
|
||||||
|
.connection { fill: none; vector-effect: non-scaling-stroke; }
|
||||||
|
.structural { stroke: var(--structural); stroke-width: 5; }
|
||||||
|
.pressurized { stroke: var(--pressurized); stroke-width: 8; }
|
||||||
|
.utility { stroke: var(--utility); stroke-width: 4; stroke-dasharray: 12 9; }
|
||||||
|
.topology-node { cursor: pointer; outline: none; }
|
||||||
|
.topology-node:hover rect,
|
||||||
|
.topology-node:focus-visible rect { filter: brightness(1.24); stroke-width: 5; }
|
||||||
|
.radiator { pointer-events: none; }
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
body { padding: 14px; }
|
||||||
|
.topology-map { min-width: 900px; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not apply SVG filters to zero-width/zero-height corridor lines; the approved mockup proved Chromium can clip those lines even when computed styles report a green stroke.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the centralized module facts**
|
||||||
|
|
||||||
|
Define all entries with this exact shape:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const MODULE_DETAILS = {
|
||||||
|
"habitat-1": {
|
||||||
|
name: "居住舱#1",
|
||||||
|
type: "居住舱段",
|
||||||
|
location: "广寒核心舱南部安装口",
|
||||||
|
role: "长期驻留与乘员生活空间,提供4个折叠铺位和2个睡袋挂点。",
|
||||||
|
metrics: ["质量:12.2 t", "居住配置:4铺位 + 2睡袋"],
|
||||||
|
dimensions: ["运输:高2.7 m × 长6.2 m × 宽5.0 m", "展开:高3.2 m × 长6.2 m × 宽7.6 m"],
|
||||||
|
status: "已展开并投入使用",
|
||||||
|
connections: ["北:广寒核心舱(结构直连)", "南:电力模块北部(加压走道)"],
|
||||||
|
mission: "GHC-05 · 2024-12-28 EVA-10完成安装"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Populate the other 13 entries from the reference pages and corrected timeline. Use `"资料未记录"` for unavailable dimensions rather than inventing values. In particular, preserve these confirmed dimensions:
|
||||||
|
|
||||||
|
- Greenhouse transport: `高2.8 m × 长8.7 m × 宽5.0 m`; deployed: `高3.2 m × 长8.7 m × 宽7.4 m`.
|
||||||
|
- Habitat-1 transport/deployed values shown above.
|
||||||
|
- Habitat-2 deployed: `高3.2 m × 长6.2 m × 宽7.6 m`; if transport dimensions are not explicitly recorded, show `资料未记录`.
|
||||||
|
- Laboratory transport: `高2.7 m × 长7.5 m × 宽5.0 m`; deployed: `高3.2 m × 长7.5 m × 宽7.5 m`.
|
||||||
|
- Core dry mass: `28.2 t`; unavailable physical dimensions: `资料未记录`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the static contract**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest tests.test_guanghan_topology_demo -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests pass except no browser behavior is asserted yet.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the semantic topology**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add data/wiki/guanghan_topology_demo.html tests/test_guanghan_topology_demo.py
|
||||||
|
git commit -m "feat: rebuild Guanghan topology diagram" -m "Co-Authored-By: Codex <noreply@openai.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Add failing browser tests for modal behavior and rendered corridors
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/test_guanghan_topology_e2e.py`
|
||||||
|
- Modify: `data/wiki/guanghan_topology_demo.html`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the browser test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
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").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()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the E2E test and verify it fails**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest tests.test_guanghan_topology_e2e -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fail because the accessible modal contract or event handlers are not yet complete.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the accessible modal**
|
||||||
|
|
||||||
|
Add this modal structure after the map:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="modal-backdrop" id="detail-modal" aria-hidden="true">
|
||||||
|
<section class="detail-dialog" role="dialog" aria-modal="true"
|
||||||
|
aria-labelledby="detail-title" tabindex="-1">
|
||||||
|
<button class="modal-close" type="button" aria-label="关闭详情">×</button>
|
||||||
|
<p class="detail-kicker" id="detail-type"></p>
|
||||||
|
<h2 id="detail-title"></h2>
|
||||||
|
<p id="detail-role"></p>
|
||||||
|
<dl class="detail-grid">
|
||||||
|
<div><dt>安装位置</dt><dd id="detail-location"></dd></div>
|
||||||
|
<div><dt>运行状态</dt><dd id="detail-status"></dd></div>
|
||||||
|
<div><dt>关键参数</dt><dd id="detail-metrics"></dd></div>
|
||||||
|
<div><dt>尺寸</dt><dd id="detail-dimensions"></dd></div>
|
||||||
|
<div><dt>连接关系</dt><dd id="detail-connections"></dd></div>
|
||||||
|
<div><dt>任务节点</dt><dd id="detail-mission"></dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use these functions and handlers:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const modal = document.querySelector("#detail-modal");
|
||||||
|
const dialog = modal.querySelector(".detail-dialog");
|
||||||
|
const closeButton = modal.querySelector(".modal-close");
|
||||||
|
let opener = null;
|
||||||
|
|
||||||
|
function lines(items) {
|
||||||
|
return items.map((item) => `<span>${item}</span>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetails(moduleId, trigger) {
|
||||||
|
const detail = MODULE_DETAILS[moduleId];
|
||||||
|
if (!detail) return;
|
||||||
|
opener = trigger;
|
||||||
|
document.querySelector("#detail-type").textContent = detail.type;
|
||||||
|
document.querySelector("#detail-title").textContent = detail.name;
|
||||||
|
document.querySelector("#detail-role").textContent = detail.role;
|
||||||
|
document.querySelector("#detail-location").textContent = detail.location;
|
||||||
|
document.querySelector("#detail-status").textContent = detail.status;
|
||||||
|
document.querySelector("#detail-metrics").innerHTML = lines(detail.metrics);
|
||||||
|
document.querySelector("#detail-dimensions").innerHTML = lines(detail.dimensions);
|
||||||
|
document.querySelector("#detail-connections").innerHTML = lines(detail.connections);
|
||||||
|
document.querySelector("#detail-mission").textContent = detail.mission;
|
||||||
|
modal.setAttribute("aria-hidden", "false");
|
||||||
|
document.body.classList.add("modal-open");
|
||||||
|
dialog.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetails() {
|
||||||
|
modal.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.classList.remove("modal-open");
|
||||||
|
opener?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-module]").forEach((node) => {
|
||||||
|
node.addEventListener("click", () => openDetails(node.dataset.module, node));
|
||||||
|
node.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
openDetails(node.dataset.module, node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
closeButton.addEventListener("click", closeDetails);
|
||||||
|
modal.addEventListener("click", (event) => {
|
||||||
|
if (event.target === modal) closeDetails();
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape" && modal.getAttribute("aria-hidden") === "false") closeDetails();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Render detail values as text except for arrays passed through the fixed `lines()` helper; all array entries are authored local constants, not user input.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run E2E and full source tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest tests.test_guanghan_topology_demo tests.test_guanghan_topology_e2e -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests report `OK`, with no browser console errors.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit interaction behavior**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add data/wiki/guanghan_topology_demo.html tests/test_guanghan_topology_e2e.py
|
||||||
|
git commit -m "feat: add Guanghan module detail modal" -m "Co-Authored-By: Codex <noreply@openai.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Browser visual verification and final polish
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify if needed: `data/wiki/guanghan_topology_demo.html`
|
||||||
|
- Verify: `user_input/广寒计划/Timeline.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Serve the workspace locally**
|
||||||
|
|
||||||
|
Run from the project root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m http.server 8765 --bind 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8765/data/wiki/guanghan_topology_demo.html
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Capture and inspect a 1440×1100 desktop screenshot**
|
||||||
|
|
||||||
|
Verify in the screenshot:
|
||||||
|
|
||||||
|
- Core is the four-way center of habitat-2, laboratory, habitat-1, and greenhouse.
|
||||||
|
- Both green corridors are visible as actual pixels, with no inline corridor labels.
|
||||||
|
- Power north radiator does not cover the habitat-1 corridor.
|
||||||
|
- Power cross is fully expanded.
|
||||||
|
- Binglun-1/2/3 appear west/south/east.
|
||||||
|
- No future expansion label or placeholder is visible.
|
||||||
|
- The top legend appears exactly once.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Capture and inspect representative modal screenshots**
|
||||||
|
|
||||||
|
Open and inspect:
|
||||||
|
|
||||||
|
- Habitat-1: transport and deployed dimensions are visible.
|
||||||
|
- Power west: ISRU, mining, and ore/fuel storage appear together.
|
||||||
|
- Wugang-2: greenhouse distal-port connection is stated.
|
||||||
|
|
||||||
|
Verify close button, backdrop close, `Esc`, and keyboard focus.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run final verification**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest tests.test_guanghan_topology_demo tests.test_guanghan_topology_e2e -v
|
||||||
|
git diff --check
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: tests `OK`; no whitespace errors; only intended task files differ from the starting worktree.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit any screenshot-driven polish**
|
||||||
|
|
||||||
|
If visual inspection required code changes:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git add data/wiki/guanghan_topology_demo.html
|
||||||
|
git commit -m "style: polish Guanghan topology layout" -m "Co-Authored-By: Codex <noreply@openai.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
If no code changes were needed, do not create an empty commit.
|
||||||
Reference in New Issue
Block a user