Files
MujicaUI/toast_test.go
T
2026-10-05 12:11:45 -04:00

194 lines
5.6 KiB
Go

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)
})
}