Files
zTools2/tests/manual_whiteboard_ws.py
zikai ff2ad3fcb3 feat: 所有入口统一到 /api/ 前缀
将 zTools2 托管的页面/静态/探针/WebSocket 路由全部从顶级路径迁移到 /api/ 下:
- 页面:/pdf -> /api/pdf、/pdf-admin -> /api/pdf-admin、/upload -> /api/upload、
  /files -> /api/files-page、/wb/{id} -> /api/wb-page/{id}、/wb-admin -> /api/wb-admin
  (页面类加 -page 后缀以规避同名 REST API /api/files、/api/wb/{id})
- 静态资源:/static -> /api/static
- 探针:/health -> /api/health
- WebSocket:/ws/wb/{id} -> /api/ws/wb/{id}
- 前端 HTML 壳与 JS 中的资源/页间链接/WS URL 同步更新
- 手动测试脚本 BASE_WS 同步

这样反代与 vite proxy 只需一条 /api/ 规则即可转发全部入口;
前端 iframe 用同源相对路径 /api/pdf,与环境无关,不再误打到其它环境域名。
README 新增「路由约定」说明。
2026-07-27 15:42:47 +08:00

93 lines
3.5 KiB
Python
Raw Permalink 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含 content/version
2. 客户端 B 连入 -> 收到 init
3. A 编辑文本 -> B 收到 updateA 不收自己的)
4. 心跳 ping -> pong
5. A 清空 -> A、B 都收到 cleared
6. 持久化:重连后 init 应返回清空后的内容
7. edit_count 累计
"""
from __future__ import annotations
import asyncio
import json
import urllib.request
import websockets
BASE_WS = "ws://127.0.0.1:6867/api/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:
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 编辑文本
text = "Hello, this is a shared note.\nSecond line."
await a.send(json.dumps({"type": "edit", "content": text}))
# A 不应收到自己的 update
echo = await recv_msg(a, timeout=1.0)
print("A self-echo (expect None):", echo)
assert echo is None, "发送者不应收到自己的 update"
# B 应收到 update
got = await recv_msg(b)
print("B recv:", got.get("type") if got else None, "content=", repr(got.get("content")) if got else None)
assert got and got["type"] == "update" and got["client_id"] == "A"
assert got["content"] == text
# 心跳
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 应 content 为空(已清空)
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, content=", repr(init_c.get("content")) if init_c else None)
assert init_c and init_c["type"] == "init"
assert init_c["content"] == "", "清空后重连应得到空 content"
# 验证 edit_count 累计1 次编辑 + 1 次清空 = 2
with urllib.request.urlopen(f"http://127.0.0.1:6867/api/wb/{BOARD}") as r:
meta = json.load(r)
print("edit_count after ops:", meta["edit_count"])
assert meta["edit_count"] == 2, "1 编辑 + 1 清空 = 2 次"
print("\nWS 端到端全部通过 ✅")
if __name__ == "__main__":
asyncio.run(main())