1package ui
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/vev/internal/domain"
8 "github.com/stretchr/testify/require"
9)
10
11func TestToastQueue(t *testing.T) {
12 start := time.Unix(100, 0)
13 life := 4 * time.Second
14 toast := func(id string) QueuedToast[string] { return QueuedToast[string]{ID: id, Value: id, Duration: life} }
15 ids := func(ts []QueuedToast[string]) []string {
16 out := make([]string, len(ts))
17 for i, t := range ts {
18 out[i] = t.ID
19 }
20 return out
21 }
22
23 tests := []struct {
24 name string
25 opts ToastQueueOptions
26 push []QueuedToast[string]
27 at time.Duration
28 wantVisible []string
29 wantPending int
30 }{
31 {name: "fills slots newest first", opts: ToastQueueOptions{MaxVisible: 3}, push: []QueuedToast[string]{toast("a"), toast("b")}, wantVisible: []string{"b", "a"}},
32 {name: "overflow waits", opts: ToastQueueOptions{MaxVisible: 2}, push: []QueuedToast[string]{toast("a"), toast("b"), toast("c")}, wantVisible: []string{"b", "a"}, wantPending: 1},
33 {name: "expiry promotes waiting toasts", opts: ToastQueueOptions{MaxVisible: 2}, push: []QueuedToast[string]{toast("a"), toast("b"), toast("c")}, at: life, wantVisible: []string{"c"}},
34 {name: "late poll paces the burst", opts: ToastQueueOptions{MaxVisible: 1}, push: []QueuedToast[string]{toast("a"), toast("b"), toast("c")}, at: 2 * life, wantVisible: []string{"c"}},
35 {name: "same id replaces in place", opts: ToastQueueOptions{MaxVisible: 3}, push: []QueuedToast[string]{toast("a"), toast("b"), toast("a")}, wantVisible: []string{"a", "b"}},
36 {name: "same id replaces waiting", opts: ToastQueueOptions{MaxVisible: 1}, push: []QueuedToast[string]{toast("a"), toast("b"), toast("b")}, wantVisible: []string{"a"}, wantPending: 1},
37 {name: "full queue drops oldest waiting", opts: ToastQueueOptions{MaxVisible: 1, MaxPending: 1}, push: []QueuedToast[string]{toast("a"), toast("b"), toast("c")}, wantVisible: []string{"a"}, wantPending: 1},
38 {name: "persistent toast never expires", opts: ToastQueueOptions{}, push: []QueuedToast[string]{{ID: "p"}}, at: time.Hour, wantVisible: []string{"p"}},
39 }
40 for _, tt := range tests {
41 t.Run(tt.name, func(t *testing.T) {
42 q := NewToastQueue[string](tt.opts)
43 for _, p := range tt.push {
44 q.Push(start, p)
45 }
46 require.Equal(t, tt.wantVisible, nilIfEmpty(ids(q.Visible(start.Add(tt.at)))))
47 require.Equal(t, tt.wantPending, q.Pending())
48 })
49 }
50}
51
52func TestToastQueueDeadlineAndDismiss(t *testing.T) {
53 start := time.Unix(100, 0)
54 q := NewToastQueue[string](ToastQueueOptions{MaxVisible: 1})
55 _, ok := q.NextDeadline()
56 require.False(t, ok)
57
58 q.Push(start, QueuedToast[string]{ID: "a", Duration: time.Second})
59 q.Push(start, QueuedToast[string]{ID: "b", Duration: time.Second})
60 due, ok := q.NextDeadline()
61 require.True(t, ok)
62 require.Equal(t, start.Add(time.Second), due)
63
64 require.True(t, q.Dismiss(start.Add(time.Millisecond), "a"))
65 require.False(t, q.Dismiss(start.Add(time.Millisecond), "missing"))
66 require.Equal(t, "b", q.Visible(start.Add(time.Millisecond))[0].ID)
67 due, _ = q.NextDeadline()
68 require.Equal(t, start.Add(time.Second+time.Millisecond), due, "a promoted toast gets its full lifetime")
69}
70
71func TestToastStackBounds(t *testing.T) {
72 base := domain.Size{Cols: 80, Rows: 12}
73 msg := "hello"
74 stack := func(anchor domain.Anchor, n int) []Toast {
75 out := make([]Toast, n)
76 for i := range out {
77 out[i] = Toast{Message: msg, Anchor: anchor}
78 }
79 return out
80 }
81 tests := []struct {
82 name string
83 toast []Toast
84 wantY []int
85 }{
86 {name: "top grows down", toast: stack(domain.AnchorTopRight, 3), wantY: []int{2, 5, 8}},
87 {name: "bottom grows up", toast: stack(domain.AnchorBottomLeft, 2), wantY: []int{7, 4}},
88 {name: "boxes leaving the frame are dropped", toast: stack(domain.AnchorTopRight, 5), wantY: []int{2, 5, 8}},
89 {name: "empty", toast: nil, wantY: nil},
90 }
91 for _, tt := range tests {
92 t.Run(tt.name, func(t *testing.T) {
93 var got []int
94 for _, r := range ToastStackBounds(base, tt.toast) {
95 got = append(got, r.Y)
96 }
97 require.Equal(t, tt.wantY, got)
98 })
99 }
100}
101
102func nilIfEmpty(s []string) []string {
103 if len(s) == 0 {
104 return nil
105 }
106 return s
107}