package mujica import ( "slices" "testing" "time" "github.com/egoist/mygo/ui" ) func toastIDs(q *toastQueue, visible bool) []string { var ids []string for _, it := range q.items { if it.visible == visible { ids = append(ids, it.msg.ID) } } return ids } func TestToastQueueLimitsVisible(t *testing.T) { var q toastQueue t0 := time.Unix(0, 0) for _, id := range []string{"a", "b", "c", "d", "e"} { q.push(ToastMessage{ID: id, Message: id}, t0) } if v, w := toastIDs(&q, true), toastIDs(&q, false); !slices.Equal(v, []string{"a", "b", "c"}) || !slices.Equal(w, []string{"d", "e"}) { t.Fatalf("visible %v waiting %v", v, w) } // d starts its 5 s only when it shows. if next := q.expire(t0.Add(ToastDuration)); next != ToastDuration { t.Fatalf("next wake %v", next) } if v := toastIDs(&q, true); !slices.Equal(v, []string{"d", "e"}) { t.Fatalf("after expiry visible %v", v) } q.expire(t0.Add(2*ToastDuration - time.Millisecond)) if len(q.items) != 2 { t.Fatalf("expired early: %v", toastIDs(&q, true)) } q.expire(t0.Add(2 * ToastDuration)) if len(q.items) != 0 { t.Fatalf("left %v", toastIDs(&q, true)) } } func TestToastQueueDedupesByID(t *testing.T) { var q toastQueue t0 := time.Unix(0, 0) q.push(ToastMessage{ID: "sync", Message: "Syncing 1 file"}, t0) q.push(ToastMessage{Message: "Saved"}, t0) q.push(ToastMessage{ID: "sync", Message: "Syncing 2 files"}, t0.Add(4*time.Second)) q.push(ToastMessage{Message: "Saved"}, t0.Add(4*time.Second)) if len(q.items) != 2 || q.items[0].msg.Message != "Syncing 2 files" || q.items[1].msg.ID != "Saved" { t.Fatalf("items %+v", q.items) } // The replaced toast restarted its time. q.expire(t0.Add(6 * time.Second)) if len(q.items) != 2 { t.Fatalf("replacement kept the old time: %d left", len(q.items)) } } func TestToastQueueErrorsStayAndPauseHolds(t *testing.T) { var q toastQueue t0 := time.Unix(0, 0) q.push(ToastMessage{ID: "e", Severity: SeverityError, Message: "Failed"}, t0) q.push(ToastMessage{ID: "w", Severity: SeverityWarning, Message: "Slow", Action: "Details"}, t0) q.pause("w", true, t0.Add(4*time.Second)) if next := q.expire(t0.Add(time.Hour)); next != 0 { t.Fatalf("paused toast still counts down: %v", next) } if !slices.Equal(toastIDs(&q, true), []string{"e", "w"}) { t.Fatalf("visible %v", toastIDs(&q, true)) } q.pause("w", false, t0.Add(time.Hour)) if next := q.expire(t0.Add(time.Hour)); next != time.Second { t.Fatalf("resumed toast left %v, want 1s", next) } q.expire(t0.Add(time.Hour + time.Second)) if !slices.Equal(toastIDs(&q, true), []string{"e"}) { t.Fatalf("visible %v", toastIDs(&q, true)) } } func TestToastQueuePanics(t *testing.T) { for _, m := range []ToastMessage{{}, {Message: "x", Duration: -1}} { func() { defer func() { if recover() == nil { t.Errorf("%+v: no panic", m) } }() var q toastQueue q.push(m, time.Now()) }() } } func toastTester(m ToastMessage, res *ToasterResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { Use(c, Settings{}) if Button(c, "Notify", ButtonOptions{}).Clicked() { ShowToast(c, m) } if r := Toaster(c); r != (ToasterResult{}) { *res = r } }, 600, 400) } func TestToastActionAndClose(t *testing.T) { var res ToasterResult tt := toastTester(ToastMessage{ID: "del", Message: "Note deleted", Action: "Undo"}, &res) if err := tt.Click("Notify"); err != nil { t.Fatal(err) } if !tt.HasText("Note deleted") { t.Fatal("toast not shown") } if err := tt.Click("Undo"); err != nil { t.Fatal(err) } if res.Action != "del" || tt.HasText("Note deleted") { t.Fatalf("after undo %+v, still shown %v", res, tt.HasText("Note deleted")) } res = ToasterResult{} _ = tt.Click("Notify") if err := tt.Click("Dismiss notification"); err != nil { t.Fatal(err) } if res.Closed != "del" || tt.HasText("Note deleted") { t.Fatalf("after close %+v", res) } } func TestToastKeepsFocusAndEscapeCloses(t *testing.T) { var res ToasterResult tt := toastTester(ToastMessage{ID: "s", Message: "Saved"}, &res) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyEnter) if !tt.HasText("Saved") || !tt.Focused("Notify") { t.Fatalf("shown %v, focus kept %v", tt.HasText("Saved"), tt.Focused("Notify")) } if a := tt.Announcements(); !slices.Contains(a, "Saved") { t.Fatalf("announcements %q", a) } tt.Key(0, ui.KeyTab) if !tt.Focused("Dismiss notification") { t.Fatal("Tab does not reach the toast") } tt.Key(0, ui.KeyEscape) if res.Closed != "s" || tt.HasText("Saved") { t.Fatalf("Escape: %+v", res) } } func TestToastExpiresAndHoverPauses(t *testing.T) { var res ToasterResult tt := toastTester(ToastMessage{Message: "Copied", Duration: 40 * time.Millisecond}, &res) _ = tt.Click("Notify") time.Sleep(60 * time.Millisecond) tt.Frame() if tt.HasText("Copied") { t.Fatal("toast outlived its duration") } _ = tt.Click("Notify") r, _ := tt.Find("Copied") tt.Move(r.X+r.W/2, r.Y+r.H/2) time.Sleep(60 * time.Millisecond) tt.Frame() if !tt.HasText("Copied") { t.Fatal("hovered toast expired") } tt.Move(5, 395) time.Sleep(60 * time.Millisecond) tt.Frame() if tt.HasText("Copied") { t.Fatal("toast stayed after the pointer left") } } func TestToastSnapshot(t *testing.T) { snapshot(t, "toast", 480, 300, func(c *ui.Context) { if len(toasts(c).items) == 0 { ShowToast(c, ToastMessage{ID: "1", Message: "Settings saved", Severity: SeveritySuccess}) ShowToast(c, ToastMessage{ID: "2", Message: "Note moved to trash", Action: "Undo"}) ShowToast(c, ToastMessage{ID: "3", Message: "Upload failed: network lost", Severity: SeverityError}) ShowToast(c, ToastMessage{ID: "4", Message: "Queued and not shown"}) } Toaster(c) }) }