toast_queue_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close