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}