MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s
- Support ephemeral cursor_moved and client_left events in docroom Hub - Keep active in-memory cursor presence map per document room - Synchronize current presence state to newly connected WebSocket clients - Allow read-only viewers to share cursor positions without document mutation rights - Throttle frontend cursor move emissions to 100ms with instant blur cleanup - Render active collaborator presence avatar pills in document header - Display user-colored block indicator and non-destructive caret overlay with name tag
86 lines
1.8 KiB
Go
86 lines
1.8 KiB
Go
package ws
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDocRoomCursorPresenceAndEchoProtection(t *testing.T) {
|
|
hub := NewHub()
|
|
room := hub.GetOrCreateDocRoom("doc_test_presence")
|
|
|
|
c1 := &DocWSClient{
|
|
hub: hub,
|
|
room: room,
|
|
userID: "user_1",
|
|
clientID: "cli_1",
|
|
canWrite: true,
|
|
send: make(chan []byte, 10),
|
|
}
|
|
c2 := &DocWSClient{
|
|
hub: hub,
|
|
room: room,
|
|
userID: "user_2",
|
|
clientID: "cli_2",
|
|
canWrite: false, // read-only viewer
|
|
send: make(chan []byte, 10),
|
|
}
|
|
|
|
room.mu.Lock()
|
|
room.wsClients[c1] = true
|
|
room.wsClients[c2] = true
|
|
room.mu.Unlock()
|
|
|
|
// 1. Send cursor_moved from c1
|
|
selStart := 5
|
|
selEnd := 5
|
|
room.HandleEvent(DocSyncEvent{
|
|
Type: "cursor_moved",
|
|
DocID: "doc_test_presence",
|
|
ClientID: "cli_1",
|
|
UserID: "user_1",
|
|
UserName: "Alice",
|
|
UserColor: "#3b82f6",
|
|
BlockID: "blk_123",
|
|
SelectionStart: &selStart,
|
|
SelectionEnd: &selEnd,
|
|
}, c1)
|
|
|
|
// c1 should NOT have received its own echo
|
|
select {
|
|
case <-c1.send:
|
|
t.Fatal("c1 received echo of its own cursor_moved event!")
|
|
default:
|
|
}
|
|
|
|
// c2 SHOULD receive the event
|
|
select {
|
|
case msg := <-c2.send:
|
|
if len(msg) == 0 {
|
|
t.Fatal("empty message received by c2")
|
|
}
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("timed out waiting for cursor_moved event on c2")
|
|
}
|
|
|
|
// Room should have registered c1's presence
|
|
room.mu.Lock()
|
|
cp, exists := room.activeCursors["cli_1"]
|
|
room.mu.Unlock()
|
|
|
|
if !exists || cp == nil {
|
|
t.Fatal("expected active cursor for cli_1 in room")
|
|
}
|
|
if cp.UserName != "Alice" || cp.BlockID != "blk_123" {
|
|
t.Fatalf("unexpected cursor data: %+v", cp)
|
|
}
|
|
|
|
// Ephemeral cursor events must NOT mark the room dirty for disk flush
|
|
room.mu.Lock()
|
|
dirty := room.dirty
|
|
room.mu.Unlock()
|
|
if dirty {
|
|
t.Fatal("cursor_moved should not mark room dirty")
|
|
}
|
|
}
|