1package indicator
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/nefergui"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/bnema/nefercap/internal/ports"
14)
15
16func TestAppendElapsed(t *testing.T) {
17 for secs, want := range map[int64]string{
18 0: "00:00", 5: "00:05", 65: "01:05", 3599: "59:59", 3600: "1:00:00", 36061: "10:01:01",
19 } {
20 assert.Equal(t, want, string(appendElapsed(nil, secs)), "%d", secs)
21 }
22}
23
24func TestRefreshFollowsTheReadingAndCachesSecond(t *testing.T) {
25 start := time.Unix(100, 0)
26 h := newHUD(start, make(chan struct{}, 1), "")
27 assert.Equal(t, "● REC 00:00", h.refresh(start))
28 assert.Equal(t, "● REC 00:00", h.refresh(start.Add(999*time.Millisecond)))
29 assert.Equal(t, "● REC 01:01", h.refresh(start.Add(61*time.Second)))
30 assert.Equal(t, "● REC 1:00:00", h.refresh(start.Add(time.Hour)))
31 assert.Equal(t, "● REC 00:00", h.refresh(start.Add(-time.Second)), "a reading before start never shows negative time")
32}
33
34func TestTargetLabel(t *testing.T) {
35 start := time.Unix(100, 0)
36 at := start.Add(5 * time.Second)
37 for label, want := range map[string]string{
38 "": "● REC 00:05",
39 "DP-1": "● REC 00:05 · DP-1",
40 "web": "● REC 00:05 · web",
41 "123456789012": "● REC 00:05 · 123456789012",
42 "1234567890123": "● REC 00:05 · 12345678901…",
43 "espace de travail": "● REC 00:05 · espace de t…",
44 "a\nb\x00c": "● REC 00:05 · abc",
45 "\n\t": "● REC 00:05",
46 } {
47 assert.Equal(t, want, newHUD(start, make(chan struct{}, 1), label).refresh(at), "%q", label)
48 }
49 // The longest line still fits the 228 px text area (about 29 cells).
50 long := newHUD(start, make(chan struct{}, 1), "wwwwwwwwwwwwwwwwww").refresh(start.Add(time.Hour))
51 assert.LessOrEqual(t, len([]rune(long)), 29)
52}
53
54func TestTargetLabelIsSingleLinePrintable(t *testing.T) {
55 for name, label := range map[string]string{
56 "line separator": "a\u2028b",
57 "paragraph separator": "a\u2029b",
58 "bidi override": "a\u202eb",
59 "bidi isolate": "a\u2066b\u2069",
60 "bidi mark": "a\u200fb\u061c",
61 "nbsp": "a\u00a0b",
62 "zero width": "a\u200bb",
63 "private use": "a\ue000b",
64 "next line": "a\u0085b",
65 "unassigned": "a\U0010ffffb",
66 } {
67 got := suffixFor(label)
68 assert.Equal(t, " · ab", got, name)
69 }
70 assert.Equal(t, " · café 東京", suffixFor("café 東京"), "printable non-ASCII stays")
71 assert.Equal(t, "", suffixFor("\u202e\u2028"))
72}
73
74func TestSuffixLongInput(t *testing.T) {
75 label := strings.Repeat("\x00界", 100000)
76 assert.Equal(t, " · 界界界界界界界界界界界…", suffixFor(label))
77 assert.Equal(t, " · 123456789012", suffixFor("123456789012"+strings.Repeat("\n", 100000)))
78}
79
80var suffixSink string
81
82func TestSuffixAllocations(t *testing.T) {
83 label := strings.Repeat("\x00界", 100000)
84 allocs := testing.AllocsPerRun(100, func() { suffixSink = suffixFor(label) })
85 t.Logf("long suffix: %.0f allocs/run", allocs)
86 if allocs > 1 {
87 t.Fatalf("suffix allocated %.0f times, want only the returned string", allocs)
88 }
89}
90
91func BenchmarkSuffixFor(b *testing.B) {
92 for name, label := range map[string]string{
93 "short": "workspace",
94 "long": strings.Repeat("\x00界", 100000),
95 } {
96 b.Run(name, func(b *testing.B) {
97 b.ReportAllocs()
98 for b.Loop() {
99 suffixSink = suffixFor(label)
100 }
101 })
102 }
103}
104
105func TestRefreshAllocations(t *testing.T) {
106 if raceEnabled {
107 t.Skip("the race detector adds allocations")
108 }
109 start := time.Unix(100, 0)
110 h := newHUD(start, make(chan struct{}, 1), "workspace-one")
111 at := start.Add(7 * time.Second)
112 h.refresh(at)
113 assert.Equal(t, 0.0, testing.AllocsPerRun(100, func() { h.refresh(at) }))
114}
115
116func TestRequestStopNeverBlocks(t *testing.T) {
117 stop := make(chan struct{}, 1)
118 h := newHUD(time.Now(), stop, "")
119 h.requestStop()
120 h.requestStop() // channel full: the second request is dropped, not blocked
121 assert.Len(t, stop, 1)
122}
123
124func TestLayerConfigTakesNoFocusAndOnlyStopInput(t *testing.T) {
125 c := layerConfig("DP-1", ports.Region{})
126 assert.Equal(t, nefergui.KeyboardNone, c.Keyboard)
127 assert.Equal(t, nefergui.LayerOverlay, c.Level)
128 assert.Equal(t, nefergui.AnchorTop, c.Anchors)
129 assert.Equal(t, int32(-1), c.ExclusiveZone)
130 require.Len(t, c.InputRects, 1)
131 r := c.InputRects[0]
132 assert.Equal(t, nefergui.Rect{X: Width - stopWidth, Width: stopWidth, Height: Height}, r)
133 assert.LessOrEqual(t, r.X+r.Width, int32(Width))
134}
135
136func TestLayerConfigCentresOnTheRecordedFrame(t *testing.T) {
137 c := layerConfig("DP-1", ports.Region{X: 100, Y: 50, Width: 1000, Height: 600})
138 assert.Equal(t, nefergui.AnchorTop|nefergui.AnchorLeft, c.Anchors)
139 assert.Equal(t, [4]int32{50 + topMargin, 0, 0, 100 + (1000-Width)/2}, c.Margin)
140 assert.Equal(t, nefergui.KeyboardNone, c.Keyboard)
141 // A frame narrower than the HUD starts at its left edge.
142 c = layerConfig("DP-1", ports.Region{X: 7, Width: 100, Height: 100})
143 assert.Equal(t, int32(7), c.Margin[3])
144 // No frame: the whole output, centred by the compositor.
145 c = layerConfig("DP-1", ports.Region{})
146 assert.Equal(t, nefergui.AnchorTop, c.Anchors)
147 assert.Equal(t, [4]int32{topMargin, 0, 0, 0}, c.Margin)
148}
149
150func TestHUDReadabilityAndStopGeometry(t *testing.T) {
151 assert.Equal(t, 384, Width)
152 assert.Equal(t, 40, Height)
153 assert.Equal(t, 90, stopWidth)
154 assert.Contains(t, styleSheet, "font-size: 1rem")
155 assert.Contains(t, styleSheet, "width: 5.625rem")
156 assert.Contains(t, styleSheet, "line-height: 2.5rem")
157 assert.Contains(t, styleSheet, "padding: 0; line-height:")
158 assert.Equal(t, nefergui.Rect{X: 294, Y: 0, Width: 90, Height: 40}, stopRect())
159}
160
161func BenchmarkRefreshUnchanged(b *testing.B) {
162 start := time.Unix(100, 0)
163 h := newHUD(start, make(chan struct{}, 1), "workspace")
164 h.refresh(start)
165 b.ReportAllocs()
166 for b.Loop() {
167 h.refresh(start)
168 }
169}
170
171func TestRunRejectsBadSetup(t *testing.T) {
172 stop := make(chan struct{}, 1)
173 ok := func(context.Context, nefergui.WaylandSurface) error { return nil }
174 out := ports.Output{ID: 1, Name: "DP-1"}
175
176 assert.ErrorIs(t, New(nil, stop).Run(context.Background(), out), ErrNoAuthorize)
177 assert.ErrorIs(t, New(ok, nil).Run(context.Background(), out), ErrNoStop)
178 assert.NotErrorIs(t, New(ok, nil).Run(context.Background(), out), ErrNoAuthorize)
179 assert.ErrorIs(t, New(ok, stop).Run(context.Background(), ports.Output{ID: 1}), ports.ErrOutputNotFound)
180 ctx, cancel := context.WithCancel(context.Background())
181 cancel()
182 assert.ErrorIs(t, New(ok, stop).Run(ctx, out), context.Canceled)
183}