1package ui23import (4 "context"5 "io"6 "slices"7 "strings"8 "sync"9 "testing"10 "time"1112 tea "charm.land/bubbletea/v2"13 "github.com/charmbracelet/x/ansi"1415 "src.rest/cat/012/internal/room"16 "src.rest/cat/012/internal/sheet"17)1819// watched is a program's model that notes when a frame first shows a20// text: a session watching another's edit arrive.21type watched struct {22 *Shared23 mu sync.Mutex24 want string25 seen chan time.Time26}2728func (w *watched) Update(msg tea.Msg) (tea.Model, tea.Cmd) {29 _, cmd := w.Shared.Update(msg)30 return w, cmd31}3233func (w *watched) View() tea.View {34 v := w.Shared.View()35 w.mu.Lock()36 defer w.mu.Unlock()37 if w.want != "" && strings.Contains(ansi.Strip(v.Content), w.want) {38 w.want = ""39 w.seen <- time.Now()40 }41 return v42}4344// expect has the next frame showing text noted.45func (w *watched) expect(text string) {46 w.mu.Lock()47 defer w.mu.Unlock()48 w.want = text49}5051// program runs a served session in a room in a real Bubble Tea program,52// with no terminal, until the test ends.53func program(t *testing.T, r *servedRooms, user, name string) (*watched, *tea.Program) {54 m := New(sheet.New(), "")55 m.Serve(r.root, []string{"TERM=xterm-256color"})56 m.ShareRooms(r.reg, user)57 m.OpenOnStart(name)58 w := &watched{Shared: InRooms(Guard(m)), seen: make(chan time.Time, 1)}59 p := tea.NewProgram(w, tea.WithInput(nil), tea.WithOutput(io.Discard), tea.WithoutSignals(), tea.WithWindowSize(80, 24), tea.WithFPS(FrameRate))60 ctx, cancel := context.WithCancel(context.Background())61 w.Attach(ctx, p)62 done := make(chan struct{})63 go func() {64 p.Run()65 close(done)66 }()67 t.Cleanup(func() {68 p.Quit()69 <-done70 cancel()71 })72 return w, p73}7475// An edit in one session reaches another's screen within a frame: the76// other's model has taken it and drawn a frame showing it before a77// frame's time has passed, so the next frame its terminal gets holds it.78func TestSharedEditWithinAFrame(t *testing.T) {79 if testing.Short() {80 t.Skip("times frames")81 }82 unlock := benchLock(t)83 defer unlock()84 r := newRooms(t, room.Edit)85 ann := r.open("ann", "@fast")86 bob, _ := program(t, r, "bob", "@fast")87 deadline := time.Now().Add(5 * time.Second)88 for shared := false; !shared && time.Now().Before(deadline); {89 ann.sh.turn(func() { shared = ann.m.shared() })90 time.Sleep(time.Millisecond)91 }92 var took []time.Duration93 for i := range 30 {94 text := "v" + strings.Repeat("x", i%5) + string(rune('a'+i%26))95 ann.press(text)96 bob.expect(text)97 start := time.Now()98 ann.press("<enter>")99 select {100 case at := <-bob.seen:101 took = append(took, at.Sub(start))102 case <-time.After(2 * time.Second):103 t.Fatalf("bob never showed %q", text)104 }105 ann.press("<up>") // the same cell again, on bob's screen106 }107 slices.Sort(took)108 median := took[len(took)/2]109 t.Logf("an edit reached the other session in %v (median), %v at worst", median, took[len(took)-1])110 if median > FrameInterval {111 t.Errorf("median %v, more than a frame (%v)", median, FrameInterval)112 }113}114115// Sessions typing at once in one room, each in its own program: every116// entry lands, and the race detector finds nothing shared unguarded.117func TestSharedSessionsTypeAtOnce(t *testing.T) {118 r := newRooms(t, room.Edit)119 var ps []*tea.Program120 for _, user := range []string{"ann", "bob", "cy"} {121 _, p := program(t, r, user, "@busy")122 ps = append(ps, p)123 }124 time.Sleep(50 * time.Millisecond) // everyone's in125 var wg sync.WaitGroup126 for i, p := range ps {127 wg.Add(1)128 go func() {129 defer wg.Done()130 for k := range i + 1 { // each to its own column131 p.Send(tea.KeyPressMsg{Code: tea.KeyRight})132 _ = k133 }134 for row := range 20 {135 for _, c := range "n" + string(rune('0'+row%10)) {136 p.Send(tea.KeyPressMsg{Code: c, Text: string(c)})137 }138 p.Send(tea.KeyPressMsg{Code: tea.KeyEnter})139 }140 }()141 }142 wg.Wait()143 // Look as one more participant would, in a turn of its own.144 look, _ := r.reg.Join("@busy", &peerLink{name: "look", kick: make(chan struct{}, 1)}, nil)145 deadline := time.Now().Add(5 * time.Second)146 for {147 n := 0148 look.Do(func(w *sheet.Workbook) { n = w.Sheets()[0].Len() })149 if n == 60 {150 break151 }152 if time.Now().After(deadline) {153 t.Fatalf("%d of 60 entries landed", n)154 }155 time.Sleep(10 * time.Millisecond)156 }157}