v0.4.0 b6be3a8

cursors_test.go RawHistoryFolder

1package app
2
3import (
4	"testing"
5	"time"
6
7	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/stretchr/testify/mock"
10)
11
12const testHideAfter = 5 * time.Second
13
14// idleRig is a cursor router on a clock the test drives. The first arm
15// creates the timer through AfterFunc (exactly once); later arms Reset it.
16type idleRig struct {
17	c     *cursors
18	timer *portsmocks.MockTimer
19	cur   *mockcursor
20	now   time.Time
21	fire  func()
22}
23
24func newIdleRig(t *testing.T) *idleRig {
25	t.Helper()
26	r := &idleRig{now: time.Unix(1000, 0), timer: portsmocks.NewMockTimer(t)}
27	clk := portsmocks.NewMockClock(t)
28	clk.EXPECT().Now().RunAndReturn(func() time.Time { return r.now }).Maybe()
29	clk.EXPECT().AfterFunc(testHideAfter, mock.Anything).RunAndReturn(func(_ time.Duration, f func()) ports.Timer {
30		r.fire = f
31		return r.timer
32	}).Once()
33	r.c = newCursors(clk, testHideAfter)
34	r.cur = newMockcursor(t)
35	r.cur.EXPECT().Hide().Once() // a cursor registered before the first move is hidden
36	r.c.set("A", r.cur)
37	r.cur.EXPECT().Move(1.0, 1.0).Once()
38	r.c.move("A", 1, 1, true)
39	return r
40}
41
42// idleHide lets the delay pass and expects the cursor to hide.
43func (r *idleRig) idleHide() {
44	r.now = r.now.Add(testHideAfter)
45	r.cur.EXPECT().Hide().Once()
46	r.fire()
47}
48
49func TestCursorHidesWhenIdleAndShowsOnMotion(t *testing.T) {
50	r := newIdleRig(t)
51	r.idleHide()
52	r.cur.EXPECT().Move(2.0, 2.0).Once()
53	r.timer.EXPECT().Reset(testHideAfter).Return(false).Once()
54	r.c.move("A", 2, 2, true)
55}
56
57// A timer that fires while the pointer kept moving waits for the time left.
58func TestCursorIdleTimerWaitsForTimeLeft(t *testing.T) {
59	r := newIdleRig(t)
60	r.now = r.now.Add(3 * time.Second)
61	r.cur.EXPECT().Move(2.0, 2.0).Once()
62	r.c.move("A", 2, 2, true)
63	r.now = r.now.Add(2 * time.Second)
64	r.timer.EXPECT().Reset(3 * time.Second).Return(false).Once()
65	r.fire() // no Hide: only 2s since the last motion
66}
67
68// Placing the pointer again after a layout or constraint change is not
69// motion: it moves a visible cursor without delaying its hide, and never
70// shows a hidden one.
71func TestCursorResyncIsNotMotion(t *testing.T) {
72	r := newIdleRig(t)
73	r.now = r.now.Add(3 * time.Second)
74	r.cur.EXPECT().Move(2.0, 2.0).Once()
75	r.c.move("A", 2, 2, false)
76	r.now = r.now.Add(2 * time.Second)
77	r.cur.EXPECT().Hide().Once()
78	r.fire() // 5s after the last motion, only 2s after the resync
79	r.c.move("A", 3, 3, false)
80}
81
82// An output that restarts while the cursor is idle-hidden keeps it hidden.
83func TestCursorStaysHiddenOnOutputRestart(t *testing.T) {
84	r := newIdleRig(t)
85	r.idleHide()
86	next := newMockcursor(t)
87	next.EXPECT().Hide().Once()
88	r.c.set("A", next)
89}
90
91func TestCursorHideAfterReload(t *testing.T) {
92	t.Run("visible", func(t *testing.T) {
93		r := newIdleRig(t)
94		r.timer.EXPECT().Stop().Return(true).Once()
95		r.timer.EXPECT().Reset(10 * time.Second).Return(false).Once()
96		r.c.setHideAfter(10 * time.Second)
97	})
98	t.Run("hidden", func(t *testing.T) {
99		r := newIdleRig(t)
100		r.idleHide()
101		r.timer.EXPECT().Stop().Return(false).Once()
102		r.c.setHideAfter(10 * time.Second) // no re-arm: nothing to hide
103		next := newMockcursor(t)
104		next.EXPECT().Hide().Once()
105		r.c.set("A", next)
106	})
107	t.Run("off", func(t *testing.T) {
108		r := newIdleRig(t)
109		r.timer.EXPECT().Stop().Return(true).Once()
110		r.c.setHideAfter(0)
111		r.now = r.now.Add(time.Minute)
112		r.fire() // a fire already under way hides nothing
113		r.cur.EXPECT().Move(2.0, 2.0).Once()
114		r.c.move("A", 2, 2, true) // and motion arms no timer
115	})
116}
117
118func TestCursorStopCancelsPendingHide(t *testing.T) {
119	r := newIdleRig(t)
120	r.timer.EXPECT().Stop().Return(true).Once()
121	r.c.stop()
122	r.now = r.now.Add(testHideAfter)
123	r.fire() // no Hide
124}

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 25

Tags 10

↑↓ moveenter openesc close