Files
zTools2/tests/manual_whiteboard_ws.py
zikai 7188c62d3a refactor: 白板路由改 /wb 前缀,消除 HTML 与 REST 同路径冲突
问题:GET /whiteboard/{board_id} 同时被 REST(返回 JSON)与 main.py 的 HTML 页面
注册,FastAPI 按注册顺序匹配到 REST,导致浏览器访问拿到 JSON 而非前端页面。

改为按职责分命名空间,避免冲突:
- HTML 页面:/wb/{id}(main.py)、/wb-admin(main.py,Basic Auth)
- 公开 REST:GET /api/wb/{id}(前端 init 拉取初始笔画)
- WS:/ws/wb/{id}(实时同步 + 心跳)
- 管理 REST:GET /api/admin/wb、DELETE /api/admin/wb/{id}(Basic Auth)

前端 whiteboard.js / whiteboard_admin.js、测试脚本、README 路径同步更新。
旧 /whiteboard/* 路径不再注册(404)。
2026-07-21 14:51:33 +00:00

93 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""白板 WebSocket 端到端烟测:两客户端实时同步 + 心跳 + 清空。
验证:
1. 客户端 A 连入 -> 收到 init
2. 客户端 B 连入 -> 收到 init
3. A 画一笔 -> B 收到 stroke 广播A 不收自己的)
4. 心跳 ping -> pong
5. A 清空 -> A、B 都收到 cleared
6. 停发心跳的连接会被服务端 reaper 移除15s这里只验证 ping/pong 即可reaper 已在 hub 单测覆盖)
"""
from __future__ import annotations
import asyncio
import json
import websockets
BASE_WS = "ws://127.0.0.1:6867/ws/wb"
BOARD = "e2etest"
async def recv_msg(ws, timeout=2.0) -> dict | None:
try:
raw = await asyncio.wait_for(ws.recv(), timeout=timeout)
return json.loads(raw)
except asyncio.TimeoutError:
return None
async def main() -> None:
async with websockets.connect(f"{BASE_WS}/{BOARD}") as a, \
websockets.connect(f"{BASE_WS}/{BOARD}") as b:
# hello
await a.send(json.dumps({"type": "hello", "client_id": "A"}))
await b.send(json.dumps({"type": "hello", "client_id": "B"}))
init_a = await recv_msg(a)
init_b = await recv_msg(b)
print("A init:", init_a.get("type") if init_a else None)
print("B init:", init_b.get("type") if init_b else None)
assert init_a and init_a["type"] == "init"
assert init_b and init_b["type"] == "init"
# A 画一笔
stroke = {"points": [[10, 10], [20, 20]], "color": "#1565c0", "width": 3}
await a.send(json.dumps({"type": "stroke", "stroke": stroke}))
# A 不应收到自己的(排除发送者)
echo = await recv_msg(a, timeout=1.0)
print("A self-echo (expect None):", echo)
assert echo is None, "发送者不应收到自己的 stroke"
# B 应收到
got = await recv_msg(b)
print("B recv:", got.get("type") if got else None, "client_id=", got.get("client_id") if got else None)
assert got and got["type"] == "stroke" and got["client_id"] == "A"
assert got["stroke"] == stroke
# 心跳
await a.send(json.dumps({"type": "ping"}))
pong = await recv_msg(a)
print("A pong:", pong.get("type") if pong else None)
assert pong and pong["type"] == "pong"
# 清空 -> 两端都收 cleared
await b.send(json.dumps({"type": "clear"}))
cleared_b = await recv_msg(b)
cleared_a = await recv_msg(a)
print("B cleared:", cleared_b.get("type") if cleared_b else None,
"A cleared:", cleared_a.get("type") if cleared_a else None)
assert cleared_b and cleared_b["type"] == "cleared" and cleared_b["client_id"] == "B"
assert cleared_a and cleared_a["type"] == "cleared" and cleared_a["client_id"] == "B"
# 验证持久化:重连后 init 应 strokes 为空(已清空)
async with websockets.connect(f"{BASE_WS}/{BOARD}") as c:
await c.send(json.dumps({"type": "hello", "client_id": "C"}))
init_c = await recv_msg(c)
print("C init after clear, strokes=", init_c.get("strokes") if init_c else None)
assert init_c and init_c["type"] == "init"
assert init_c["strokes"] == [], "清空后重连应得到空 strokes"
# 验证 stroke_count 累计(之前 1 笔 + 1 次清空 = 2
import urllib.request
with urllib.request.urlopen(f"http://127.0.0.1:6867/api/wb/{BOARD}") as r:
meta = json.load(r)
print("stroke_count after ops:", meta["stroke_count"])
assert meta["stroke_count"] == 2, "1 笔 + 1 清空 = 2 次修改"
print("\nWS 端到端全部通过 ✅")
if __name__ == "__main__":
asyncio.run(main())