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}