v0.2.0 a558f49

indicator_test.go RawHistoryFolder

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}

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 2

Tags 9

↑↓ moveenter openesc close