refactor: 白板从画笔画板改为文本记事本
原实现是 Canvas 画笔画板,与「文字白板/记事本」需求不符。重做为纯文本实时协作: - model:strokes JSON -> content TEXT + version INT(乐观锁)+ edit_count; schema/dao/service 同步重构,append_stroke/replace_strokes -> update_content。 - WS 协议:stroke -> edit(发完整文本,debounce 400ms);init 下发 content/version。 update 帧广播给他人,cleared 广播给所有人。 - 前端:canvas -> textarea;收到远端 update 用最长公共前后缀算变更区间, 仅替换该区间并保留光标(区间前不动/后平移/内移末尾);清空/复制文本按钮。 - schema.sql 更新 whiteboard 表 DDL;DB 旧表 DROP 重建(开发环境)。 - 测试脚本与 README 同步更新。
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
"""白板 WebSocket 端到端烟测:两客户端实时同步 + 心跳 + 清空。
|
||||
"""白板 WebSocket 端到端烟测(文本记事本):两客户端实时同步 + 心跳 + 清空。
|
||||
|
||||
验证:
|
||||
1. 客户端 A 连入 -> 收到 init
|
||||
1. 客户端 A 连入 -> 收到 init(含 content/version)
|
||||
2. 客户端 B 连入 -> 收到 init
|
||||
3. A 画一笔 -> B 收到 stroke 广播(A 不收自己的)
|
||||
3. A 编辑文本 -> B 收到 update(A 不收自己的)
|
||||
4. 心跳 ping -> pong
|
||||
5. A 清空 -> A、B 都收到 cleared
|
||||
6. 停发心跳的连接会被服务端 reaper 移除(15s,这里只验证 ping/pong 即可,reaper 已在 hub 单测覆盖)
|
||||
6. 持久化:重连后 init 应返回清空后的内容
|
||||
7. edit_count 累计
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
import websockets
|
||||
|
||||
@@ -31,7 +33,6 @@ async def recv_msg(ws, timeout=2.0) -> dict | 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"}))
|
||||
|
||||
@@ -42,18 +43,18 @@ async def main() -> 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 不应收到自己的(排除发送者)
|
||||
# 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, "发送者不应收到自己的 stroke"
|
||||
# B 应收到
|
||||
assert echo is None, "发送者不应收到自己的 update"
|
||||
# B 应收到 update
|
||||
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
|
||||
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"}))
|
||||
@@ -70,20 +71,19 @@ async def main() -> 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 为空(已清空)
|
||||
# 验证持久化:重连后 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, strokes=", init_c.get("strokes") if init_c else None)
|
||||
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["strokes"] == [], "清空后重连应得到空 strokes"
|
||||
assert init_c["content"] == "", "清空后重连应得到空 content"
|
||||
|
||||
# 验证 stroke_count 累计(之前 1 笔 + 1 次清空 = 2)
|
||||
import urllib.request
|
||||
# 验证 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("stroke_count after ops:", meta["stroke_count"])
|
||||
assert meta["stroke_count"] == 2, "1 笔 + 1 清空 = 2 次修改"
|
||||
print("edit_count after ops:", meta["edit_count"])
|
||||
assert meta["edit_count"] == 2, "1 编辑 + 1 清空 = 2 次"
|
||||
|
||||
print("\nWS 端到端全部通过 ✅")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user