package main import ( "bufio" "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/egoist/mygo/ui" mujica "mujicaui" ) // mirrorRig serves a one-field view through the web mirror. type mirrorRig struct { t *testing.T m *mirror srv *httptest.Server text string } func newMirrorRig(t *testing.T) *mirrorRig { r := &mirrorRig{t: t} var h http.Handler r.m, h = newMirror(func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) ui.Column(c).Padding(40).Children(func() { mujica.TextInput(c, &r.text, mujica.TextInputOptions{Label: "Ledger"}) }) }) r.srv = httptest.NewServer(h) t.Cleanup(r.srv.Close) return r } func (r *mirrorRig) post(in input) int { r.t.Helper() body, err := json.Marshal(in) if err != nil { r.t.Fatal(err) } res, err := http.Post(r.srv.URL+"/input", "application/json", bytes.NewReader(body)) if err != nil { r.t.Fatal(err) } res.Body.Close() return res.StatusCode } func (r *mirrorRig) get(path string) []byte { r.t.Helper() res, err := http.Get(r.srv.URL + path) if err != nil { r.t.Fatal(err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { r.t.Fatalf("GET %s: %d", path, res.StatusCode) } b, err := io.ReadAll(res.Body) if err != nil { r.t.Fatal(err) } return b } func (r *mirrorRig) value() string { r.m.mu.Lock() defer r.m.mu.Unlock() return r.text } func TestMirrorServesPageAndFirstNotice(t *testing.T) { r := newMirrorRig(t) if page := r.get("/"); !bytes.Contains(page, []byte(" 300*time.Millisecond if !late { c.After(350 * time.Millisecond) } }) time.Sleep(900 * time.Millisecond) m.mu.Lock() defer m.mu.Unlock() if !late { t.Fatal("the view was not rebuilt after its timer with no input") } } func TestMirrorModifiedClickNeedsReleaseInPlace(t *testing.T) { clicks := 0 _, srv := mirrorWith(t, func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) ui.Column(c).Padding(20).Children(func() { if mujica.Button(c, "Seal", mujica.ButtonOptions{}).Clicked() { clicks++ } }) }) shift := []bool{true} postInput(t, srv, input{Type: "down", X: 40, Y: 35, Mods: shift}) postInput(t, srv, input{Type: "up", X: 400, Y: 400, Mods: shift}) postInput(t, srv, input{Type: "down", X: 40, Y: 35, Mods: shift}) postInput(t, srv, input{Type: "up", X: 41, Y: 35, Mods: shift}) if clicks != 1 { t.Fatalf("clicks %d, want 1 (released elsewhere must not click)", clicks) } } func TestMirrorForwardsRepeatedCopy(t *testing.T) { m, _ := mirrorWith(t, func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) }) ch := make(chan []byte, 8) m.mu.Lock() m.subs[ch] = struct{}{} notices := 0 for range 2 { m.tt.SetClipboard("same") m.copied = true if m.capture() { notices++ } } m.mu.Unlock() if notices != 2 { t.Fatalf("repeated copy of the same text notified %d times, want 2", notices) } for range 2 { msg := <-ch if !bytes.Contains(msg, []byte(`"clipboard":"same"`)) { t.Fatalf("notice %s", msg) } } } func TestMirrorMapsNumpad(t *testing.T) { r := newMirrorRig(t) r.post(input{Type: "key", Code: "Tab"}) if code := r.post(input{Type: "key", Code: "Numpad7", Text: "7"}); code != http.StatusNoContent { t.Fatalf("numpad: %d", code) } if got := r.value(); got != "7" { t.Fatalf("value %q", got) } } func TestMirrorForwardsEveryAnnouncement(t *testing.T) { m, srv := mirrorWith(t, func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) ui.Column(c).Padding(20).Children(func() { if mujica.Button(c, "Ring", mujica.ButtonOptions{}).Clicked() { c.Announce("bell rung") } }) }) ch := make(chan []byte, 64) m.mu.Lock() m.subs[ch] = struct{}{} m.mu.Unlock() for range 2 { postInput(t, srv, input{Type: "down", X: 40, Y: 35}) postInput(t, srv, input{Type: "up", X: 40, Y: 35}) } heard := 0 for len(ch) > 0 { if bytes.Contains(<-ch, []byte("bell rung")) { heard++ } } if heard != 2 { t.Fatalf("announcements forwarded %d times, want 2", heard) } } func TestMirrorModifiedClickReleasedJustOutsideDoesNotClick(t *testing.T) { clicks := 0 var box ui.Rect m, srv := mirrorWith(t, func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { b := mujica.Button(c, "Seal", mujica.ButtonOptions{}) box = b.Bounds() if b.Clicked() { clicks++ } }) }) m.mu.Lock() m.tt.Frame() r := box m.mu.Unlock() shift := []bool{true} y := r.Y + r.H/2 postInput(t, srv, input{Type: "down", X: r.X + r.W - 1, Y: y, Mods: shift}) postInput(t, srv, input{Type: "up", X: r.X + r.W + 2, Y: y, Mods: shift}) if clicks != 0 { t.Fatalf("clicks %d after releasing 2 DIP outside", clicks) } } func TestMirrorNotifiesEveryCopyOfEqualText(t *testing.T) { m, srv := mirrorWith(t, func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) ui.Column(c).Padding(20).Children(func() { if mujica.Button(c, "Copy", mujica.ButtonOptions{}).Clicked() { c.WriteClipboard("snippet") } }) }) ch := make(chan []byte, 64) m.mu.Lock() m.subs[ch] = struct{}{} m.mu.Unlock() for range 2 { postInput(t, srv, input{Type: "down", X: 40, Y: 35}) postInput(t, srv, input{Type: "up", X: 40, Y: 35}) } copies := 0 for len(ch) > 0 { if bytes.Contains(<-ch, []byte(`"clipboard":"snippet"`)) { copies++ } } if copies != 2 { t.Fatalf("copies forwarded %d, want 2", copies) } m.mu.Lock() defer m.mu.Unlock() if got := m.tt.Clipboard(); got != "snippet" { t.Fatalf("clipboard %q", got) } }