1package core
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// pulseCore is indicatorCore with the focus pulse on and two tiled windows,
11// 2 focused, its pulse settled and over.
12func pulseCore(t *testing.T) (*Core, *indicatorClock) {
13 t.Helper()
14 c, ic := indicatorCore(t)
15 c.cfg.Focus.Animation = ports.FocusAnimationPulse
16 c.cfg.Focus.Strength = 0.05
17 c.cur().mon.AddWindow(1)
18 c.cur().mon.AddWindow(2)
19 indicatorScene(t, c)
20 ic.now = ic.now.Add(pulseSettle)
21 c.pulseTick()
22 ic.now = ic.now.Add(pulseCooldown)
23 indicatorScene(t, c)
24 return c, ic
25}
26
27func pulseOf(s ports.Scene, id WindowID) float64 {
28 for _, w := range s.Windows {
29 if w.ID == id {
30 return w.FocusEffect
31 }
32 }
33 return -1
34}
35
36func TestFocusPulseAfterSettle(t *testing.T) {
37 c, ic := pulseCore(t)
38 c.cur().mon.Current().FocusID(1)
39 if s := indicatorScene(t, c); pulseOf(s, 1) != 0 {
40 t.Fatalf("pulse before settle: %v", pulseOf(s, 1))
41 }
42 ic.now = ic.now.Add(pulseSettle)
43 if !c.pulseTick() {
44 t.Fatal("no pulse after settle")
45 }
46 if !c.animating() {
47 t.Fatal("a running pulse asks for no frames")
48 }
49 ic.now = ic.now.Add(pulseRise)
50 s := indicatorScene(t, c)
51 if p := pulseOf(s, 1); p < 0.05*0.99 || p > 0.05 {
52 t.Fatalf("peak %v, want the configured 0.05", p)
53 }
54 if pulseOf(s, 2) != 0 {
55 t.Fatal("unfocused window pulses")
56 }
57 ic.now = ic.now.Add(pulseFall)
58 if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || c.animating() {
59 t.Fatalf("pulse left after its end: %v", pulseOf(s, 1))
60 }
61}
62
63// Switching faster than pulseSettle never pulses: only the last focus counts.
64func TestFocusPulseDebounced(t *testing.T) {
65 c, ic := pulseCore(t)
66 for range 4 {
67 c.cur().mon.Current().FocusID(1)
68 indicatorScene(t, c)
69 ic.now = ic.now.Add(pulseSettle / 3)
70 c.cur().mon.Current().FocusID(2)
71 indicatorScene(t, c)
72 ic.now = ic.now.Add(pulseSettle / 3)
73 }
74 // A stale timer of an earlier focus fires: the focus is too recent.
75 if c.pulseTick() {
76 t.Fatal("pulse while switching")
77 }
78 if c.pulse.timerC == nil {
79 t.Fatal("no timer for the last focus")
80 }
81 ic.now = ic.now.Add(pulseSettle)
82 // Window 2 pulsed in pulseCore over a second ago: it may pulse again.
83 if !c.pulseTick() || c.pulse.id != 2 {
84 t.Fatalf("last focus did not pulse: %d", c.pulse.id)
85 }
86}
87
88// A focus change stops the running pulse; coming back within the cooldown
89// does not pulse the same window again.
90func TestFocusPulseStopsAndCoolsDown(t *testing.T) {
91 c, ic := pulseCore(t)
92 c.cur().mon.Current().FocusID(1)
93 indicatorScene(t, c)
94 ic.now = ic.now.Add(pulseSettle)
95 c.pulseTick()
96 ic.now = ic.now.Add(pulseRise / 2)
97 c.cur().mon.Current().FocusID(2)
98 if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || pulseOf(s, 2) != 0 {
99 t.Fatal("pulse survived a focus change")
100 }
101 c.cur().mon.Current().FocusID(1)
102 indicatorScene(t, c)
103 ic.now = ic.now.Add(pulseSettle)
104 if c.pulseTick() {
105 t.Fatal("same window pulsed within the cooldown")
106 }
107}
108
109// A window focused as it maps does not pulse; focusing it again later does.
110func TestFocusPulseSkipsNewWindow(t *testing.T) {
111 c, ic := pulseCore(t)
112 c.windows.mapped(ports.WindowMapped{ID: 3}, ic.now)
113 c.cur().mon.AddWindow(3)
114 indicatorScene(t, c)
115 if c.pulse.timerC != nil {
116 t.Fatal("opening a window arms the pulse")
117 }
118 ic.now = ic.now.Add(pulseSettle)
119 if c.pulseTick() || c.animating() {
120 t.Fatal("a new window pulses")
121 }
122 c.cur().mon.Current().FocusID(1)
123 indicatorScene(t, c)
124 c.cur().mon.Current().FocusID(3)
125 indicatorScene(t, c)
126 ic.now = ic.now.Add(pulseSettle)
127 if !c.pulseTick() || c.pulse.id != 3 {
128 t.Fatal("an existing window did not pulse")
129 }
130}
131
132// A window that mapped earlier without the focus pulses when focused.
133func TestFocusPulseWindowMappedEarlier(t *testing.T) {
134 c, ic := pulseCore(t)
135 c.windows.mapped(ports.WindowMapped{ID: 1}, ic.now.Add(-time.Minute))
136 c.cur().mon.Current().FocusID(1)
137 indicatorScene(t, c)
138 ic.now = ic.now.Add(pulseSettle)
139 if !c.pulseTick() || c.pulse.id != 1 {
140 t.Fatal("a window mapped earlier did not pulse")
141 }
142}
143
144func TestFocusPulseSkipsFullscreen(t *testing.T) {
145 c, ic := pulseCore(t)
146 c.cur().mon.Current().FocusID(1)
147 c.cur().mon.ToggleFullscreen()
148 indicatorScene(t, c)
149 ic.now = ic.now.Add(pulseSettle)
150 if c.pulseTick() || c.animating() {
151 t.Fatal("fullscreen window asks for pulse frames")
152 }
153 // Fullscreen entered mid-pulse ends it.
154 c.cur().mon.ToggleFullscreen()
155 c.cur().mon.Current().FocusID(2)
156 indicatorScene(t, c)
157 ic.now = ic.now.Add(pulseSettle)
158 if !c.pulseTick() {
159 t.Fatal("no pulse on the tiled window")
160 }
161 c.cur().mon.ToggleFullscreen()
162 if s := indicatorScene(t, c); pulseOf(s, 2) != 0 || c.animating() {
163 t.Fatal("pulse survived fullscreen")
164 }
165}
166
167// A popup grab or layer taking the keyboard stops the pulse; the window
168// getting it back is not a new focus and does not pulse again.
169func TestFocusPulseIgnoresKeyboardDetours(t *testing.T) {
170 c, ic := pulseCore(t)
171 c.cur().mon.Current().FocusID(1)
172 indicatorScene(t, c)
173 ic.now = ic.now.Add(pulseSettle)
174 c.pulseTick()
175 // The keyboard goes elsewhere (held false), then comes back.
176 c.pulseFocus(1, false)
177 if c.animating() {
178 t.Fatal("pulse kept while the keyboard is elsewhere")
179 }
180 ic.now = ic.now.Add(2 * pulseCooldown)
181 c.pulseFocus(1, true)
182 if c.pulse.timerC != nil {
183 t.Fatal("returning keyboard counts as a new focus")
184 }
185}
186
187func TestFocusPulseOff(t *testing.T) {
188 c, ic := pulseCore(t)
189 c.cfg.Focus.Animation = ports.FocusAnimationOff
190 c.cur().mon.Current().FocusID(1)
191 indicatorScene(t, c)
192 if c.pulse.timerC != nil {
193 t.Fatal("timer armed with the pulse off")
194 }
195 ic.now = ic.now.Add(pulseSettle)
196 if c.pulseTick() || c.animating() {
197 t.Fatal("pulse with focus.animation off")
198 }
199 // Turning it back on does not pulse the window already focused.
200 c.cfg.Focus.Animation = ports.FocusAnimationPulse
201 indicatorScene(t, c)
202 if c.pulse.timerC != nil {
203 t.Fatal("enabling the pulse pulses the focused window")
204 }
205}