toast_test.go RawHistoryFolder

1package ui
2
3import (
4	"fmt"
5	"testing"
6
7	renderer "github.com/bnema/vev-vt"
8	"github.com/bnema/vev/internal/domain"
9)
10
11func TestTruncateText(t *testing.T) {
12	tests := []struct {
13		name     string
14		text     string
15		maxWidth int
16		want     string
17	}{
18		{"no truncation needed", "hello", 10, "hello"},
19		{"truncation with ellipsis", "hello world", 8, "hello w…"},
20		{"maxWidth zero", "hello", 0, ""},
21		// The ellipsis itself is one cell wide, so any non-positive maxWidth is
22		// also "smaller than the ellipsis" and collapses to the same empty result.
23		{"maxWidth smaller than ellipsis", "hello world", -1, ""},
24		{"wide runes", "日本語のテキスト", 5, "日本…"},
25	}
26	for _, tt := range tests {
27		t.Run(tt.name, func(t *testing.T) {
28			if got := TruncateText(tt.text, tt.maxWidth); got != tt.want {
29				t.Fatalf("TruncateText(%q, %d) = %q, want %q", tt.text, tt.maxWidth, got, tt.want)
30			}
31		})
32	}
33}
34
35func TestToastBoundsAnchorsMarginsAndTinyFrames(t *testing.T) {
36	message := "Reconnection attempts…"
37	width := textWidth(message) + 4
38	if width != 26 {
39		t.Fatalf("test message width = %d, want 26", width)
40	}
41
42	tests := []struct {
43		name  string
44		base  domain.Size
45		toast Toast
46		want  domain.Rect
47	}{
48		{"top left", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorTopLeft}, domain.Rect{X: 2, Y: 2, Width: 26, Height: 3}},
49		{"top", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorTop}, domain.Rect{X: 27, Y: 2, Width: 26, Height: 3}},
50		{"top right", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorTopRight}, domain.Rect{X: 52, Y: 2, Width: 26, Height: 3}},
51		{"left", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorLeft}, domain.Rect{X: 2, Y: 10, Width: 26, Height: 3}},
52		{"center", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorCenter}, domain.Rect{X: 27, Y: 10, Width: 26, Height: 3}},
53		{"right", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorRight}, domain.Rect{X: 52, Y: 10, Width: 26, Height: 3}},
54		{"bottom left", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorBottomLeft}, domain.Rect{X: 2, Y: 19, Width: 26, Height: 3}},
55		{"bottom", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorBottom}, domain.Rect{X: 27, Y: 19, Width: 26, Height: 3}},
56		{"bottom right", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message, Anchor: domain.AnchorBottomRight}, domain.Rect{X: 52, Y: 19, Width: 26, Height: 3}},
57		{"omitted anchor centers", domain.Size{Cols: 80, Rows: 24}, Toast{Message: message}, domain.Rect{X: 27, Y: 10, Width: 26, Height: 3}},
58		{"zero frame", domain.Size{}, Toast{Message: "tiny"}, domain.Rect{}},
59		{"one by one clamps", domain.Size{Cols: 1, Rows: 1}, Toast{Message: "tiny"}, domain.Rect{Width: 1, Height: 1}},
60		{"narrow frame clamps width and margin", domain.Size{Cols: 8, Rows: 2}, Toast{Message: "long message", Anchor: domain.AnchorBottomRight}, domain.Rect{Width: 8, Height: 2}},
61	}
62
63	for _, tt := range tests {
64		t.Run(tt.name, func(t *testing.T) {
65			if got := ToastBounds(tt.base, tt.toast); got != tt.want {
66				t.Fatalf("ToastBounds() = %+v, want %+v", got, tt.want)
67			}
68		})
69	}
70}
71
72func TestDrawToastDrawsBorderedToastAndTruncates(t *testing.T) {
73	styles := ToastStyles{Text: renderer.Style{Foreground: 3, Background: -1}, Box: renderer.Style{Bold: true, Foreground: 4, Background: -1}}
74
75	t.Run("bordered centered toast preserves exterior", func(t *testing.T) {
76		f := renderer.NewFrame(20, 7)
77		exterior := renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()}
78		FillRect(f, domain.Rect{Width: 20, Height: 7}, exterior)
79
80		DrawToast(f, Toast{Message: "Hi", Anchor: domain.AnchorCenter}, styles)
81		bounds := ToastBounds(domain.Size{Cols: 20, Rows: 7}, Toast{Message: "Hi", Anchor: domain.AnchorCenter})
82		if bounds != (domain.Rect{X: 7, Y: 2, Width: 6, Height: 3}) {
83			t.Fatalf("bounds = %+v, want centered 6x3 box", bounds)
84		}
85		assertRune(t, f, bounds.X, bounds.Y, '┌')
86		assertRune(t, f, bounds.X+bounds.Width-1, bounds.Y, '┐')
87		assertRune(t, f, bounds.X, bounds.Y+bounds.Height-1, '└')
88		assertRune(t, f, bounds.X+bounds.Width-1, bounds.Y+bounds.Height-1, '┘')
89		assertRune(t, f, bounds.X+1, bounds.Y, '─')
90		assertRune(t, f, bounds.X, bounds.Y+1, '│')
91		assertRune(t, f, bounds.X+2, bounds.Y+1, 'H')
92		assertRune(t, f, bounds.X+3, bounds.Y+1, 'i')
93		assertCell(t, f, 0, 0, exterior)
94		assertCell(t, f, 19, 6, exterior)
95	})
96
97	t.Run("narrow toast truncates with ellipsis", func(t *testing.T) {
98		f := renderer.NewFrame(8, 3)
99		DrawToast(f, Toast{Message: "abcdef", Anchor: domain.AnchorCenter, MaxWidth: 8}, styles)
100		assertRune(t, f, 0, 0, '┌')
101		assertRune(t, f, 7, 0, '┐')
102		assertRune(t, f, 2, 1, 'a')
103		assertRune(t, f, 3, 1, 'b')
104		assertRune(t, f, 4, 1, 'c')
105		assertRune(t, f, 5, 1, '…')
106	})
107}
108
109func TestDrawToastHandlesTinyFrames(t *testing.T) {
110	styles := ToastStyles{Text: renderer.DefaultStyle(), Box: renderer.DefaultStyle()}
111	for _, size := range []domain.Size{{}, {Cols: 1, Rows: 1}, {Cols: 2, Rows: 1}, {Cols: 1, Rows: 2}} {
112		t.Run(fmt.Sprintf("%dx%d", size.Cols, size.Rows), func(t *testing.T) {
113			f := renderer.NewFrame(size.Cols, size.Rows)
114			DrawToast(f, Toast{Message: "abcdef", Anchor: domain.AnchorCenter}, styles)
115		})
116	}
117}

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