v0.4.0 b6be3a8

transition_stash_internal_test.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"slices"
6	"testing"
7	"time"
8
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// stashRig is indicatorCore with animations on and a stash of windows 4, 3
13// and 2 (2 selected, 3 peeking on its left) over the tile 1, settled. The
14// peek veil is 0.5.
15func stashRig(t *testing.T, on bool) (*Core, *indicatorClock, *screen, chan ports.ClientCommand) {
16	t.Helper()
17	c, ic, cmds := fadeCore(t)
18	c.cfg.Animations.On = on
19	c.cfg.Stash.Dim = 0.5
20	sc := c.cur()
21	for id := WindowID(1); id <= 4; id++ {
22		sc.mon.AddWindow(id)
23	}
24	for _, id := range []WindowID{4, 3, 2} {
25		sc.mon.Current().FocusID(id)
26		c.applyAction(ActionToggleWindowStash)
27	}
28	settleShown(t, c)
29	drainConfigures(cmds)
30	return c, ic, sc, cmds
31}
32
33// act is a bind: snapshot, action, transition.
34func act(c *Core, ic *indicatorClock, a Action) {
35	shots := c.snapshot(ic.now)
36	c.applyAction(a)
37	c.transition(shots, ic.now)
38}
39
40// Showing the stash fades in and grows every window it brings on screen;
41// the windows it keeps off screen stay out; the settled layout is the
42// final one at once, with no extra configure.
43func TestStashShowAppears(t *testing.T) {
44	c, ic, sc, cmds := stashRig(t, true)
45	act(c, ic, ActionToggleStashVisible)
46	settleShown(t, c)
47	if !sc.mon.Current().stashHidden {
48		t.Fatal("setup: the stash is shown")
49	}
50	ic.now = ic.now.Add(5 * time.Second)
51	c.animate(ic.now, nil)
52	settleShown(t, c)
53	drainConfigures(cmds)
54
55	act(c, ic, ActionToggleStashVisible)
56	s := indicatorScene(t, c)
57	for _, id := range []WindowID{2, 3} {
58		p := settledOf(t, sc, id)
59		w := sceneWindow(t, s, id)
60		if w.Hidden || w.Fade != 1 || w.Rect != scaledRect(p.Rect, appearScale) || w.Zoom < 0.85 || w.Zoom > 0.95 {
61			t.Fatalf("window %d first frame %+v, want invisible at 90 %% of %+v", id, w, p.Rect)
62		}
63	}
64	if w := sceneWindow(t, s, 4); !w.Hidden || w.Fade != 0 {
65		t.Fatalf("window 4 stays off screen: %+v", w)
66	}
67	// The action configures what it changes once, at the settled size.
68	cfgs := configuresOf(cmds)
69	for _, id := range []WindowID{2, 3} {
70		if len(cfgs[id]) != 1 {
71			t.Fatalf("window %d configured %d times, want once: %+v", id, len(cfgs[id]), cfgs[id])
72		}
73		want := c.clientRect(settledOf(t, sc, id))
74		if v := cfgs[id][0]; !v.Visible || v.Width != want.W || v.Height != want.H {
75			t.Fatalf("window %d configure %+v, want %dx%d (the settled client rect)", id, v, want.W, want.H)
76		}
77	}
78
79	s = frame(t, c, ic, 40*time.Millisecond)
80	for _, id := range []WindowID{2, 3} {
81		w := sceneWindow(t, s, id)
82		if !(w.Fade > 0 && w.Fade < 1) || !(w.Zoom > 0.9 && w.Zoom < 1) {
83			t.Fatalf("window %d mid-way fade %v zoom %v, want strictly between", id, w.Fade, w.Zoom)
84		}
85	}
86	s = frame(t, c, ic, 5*time.Second)
87	for _, id := range []WindowID{2, 3} {
88		p := settledOf(t, sc, id)
89		if w := sceneWindow(t, s, id); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect {
90			t.Fatalf("window %d settled %+v, want %+v", id, w, p.Rect)
91		}
92	}
93	if c.animating() || len(sc.rects) != 0 {
94		t.Fatalf("still animating: %d rects", len(sc.rects))
95	}
96	if ids := drainConfigures(cmds); len(ids) != 0 {
97		t.Fatalf("the animation reconfigured %v", ids)
98	}
99}
100
101// Hiding the stash keeps its windows drawn while they fade out and shrink,
102// out of reach of input and focus, until they settle.
103func TestStashHideLeaves(t *testing.T) {
104	c, ic, sc, cmds := stashRig(t, true)
105	left := map[WindowID]Placement{}
106	for _, id := range []WindowID{2, 3} {
107		left[id] = settledOf(t, sc, id)
108	}
109	act(c, ic, ActionToggleStashVisible)
110	s := indicatorScene(t, c)
111	if len(sc.settledLayout) != len(sc.shown) {
112		t.Fatalf("settled %d shown %d", len(sc.settledLayout), len(sc.shown))
113	}
114	for _, id := range []WindowID{2, 3} {
115		w := sceneWindow(t, s, id)
116		if w.Hidden || w.Fade != 0 || w.Rect != left[id].Rect {
117			t.Fatalf("window %d first frame %+v, want drawn opaque at %+v", id, w, left[id].Rect)
118		}
119		if _, _, ok := c.windowRect(id); ok {
120			t.Fatalf("window %d is still on screen for input", id)
121		}
122	}
123	// The selected window is under the tile: its centre hits the tile.
124	r := left[2].Rect
125	if id, _, _ := c.hit(float64(r.X+r.W/2), float64(r.Y+r.H/2)); id == 2 || id == 3 {
126		t.Fatalf("hit %d on a leaving window", id)
127	}
128	if id, _ := sc.mon.Focused(); id == 2 || id == 3 {
129		t.Fatalf("focused %d", id)
130	}
131	if ids := drainConfigures(cmds); slices.Contains(ids, 2) || slices.Contains(ids, 3) {
132		t.Fatalf("a leaving window was configured: %v", ids)
133	}
134
135	s = frame(t, c, ic, 40*time.Millisecond)
136	for _, id := range []WindowID{2, 3} {
137		if w := sceneWindow(t, s, id); !(w.Fade > 0 && w.Fade < 1) || !(w.Rect.W < left[id].Rect.W) {
138			t.Fatalf("window %d mid-way %+v", id, w)
139		}
140	}
141	s = frame(t, c, ic, 5*time.Second)
142	for _, id := range []WindowID{2, 3} {
143		if w := sceneWindow(t, s, id); !w.Hidden {
144			t.Fatalf("window %d still drawn after the settle: %+v", id, w)
145		}
146	}
147	if c.animating() || len(sc.rects) != 0 {
148		t.Fatalf("still animating: %d rects", len(sc.rects))
149	}
150	// The hide's own configure comes once the fade is over, from the size
151	// the window had (not from nothing).
152	var hidden []ports.ConfigureWindow
153	for {
154		select {
155		case cmd := <-cmds:
156			if v, ok := cmd.(ports.ConfigureWindow); ok && v.ID == 2 {
157				hidden = append(hidden, v)
158			}
159			continue
160		default:
161		}
162		break
163	}
164	if len(hidden) != 1 || hidden[0].Visible || hidden[0].Activated {
165		t.Fatalf("configures of window 2 after the hide: %+v", hidden)
166	}
167}
168
169// With animations off both are instant.
170func TestStashToggleOffIsInstant(t *testing.T) {
171	c, ic, sc, _ := stashRig(t, false)
172	act(c, ic, ActionToggleStashVisible)
173	s := indicatorScene(t, c)
174	if w := sceneWindow(t, s, 2); !w.Hidden || len(sc.rects) != 0 || c.animating() {
175		t.Fatalf("hide with animations off: %+v, %d rects", w, len(sc.rects))
176	}
177	act(c, ic, ActionToggleStashVisible)
178	s = indicatorScene(t, c)
179	if w := sceneWindow(t, s, 2); w.Hidden || w.Fade != 0 || w.Zoom != 0 || len(sc.rects) != 0 || c.animating() {
180		t.Fatalf("show with animations off: %+v, %d rects", w, len(sc.rects))
181	}
182}
183
184// Navigating in the stash animates the peek veil: the window that leaves the
185// centre darkens from 0, the one that reaches it clears from the veil.
186func TestStashNavigationAnimatesPeekDim(t *testing.T) {
187	c, ic, sc, _ := stashRig(t, true)
188	if w := sceneWindow(t, indicatorScene(t, c), 3); w.Dim != 0.5 {
189		t.Fatalf("setup: peek dim %v", w.Dim)
190	}
191	act(c, ic, ActionFocusColumnLeft) // 3 selected, 2 peeks on its right
192	s := indicatorScene(t, c)
193	if w := sceneWindow(t, s, 3); w.Dim != 0.5 || w.Focused != true {
194		t.Fatalf("window 3 right after: %+v, want the veil it had", w)
195	}
196	if w := sceneWindow(t, s, 2); w.Dim != 0 {
197		t.Fatalf("window 2 right after: dim %v, want 0 (it was selected)", w.Dim)
198	}
199	s = frame(t, c, ic, 40*time.Millisecond)
200	if w := sceneWindow(t, s, 3); !(w.Dim > 0 && w.Dim < 0.5) {
201		t.Fatalf("window 3 mid-way dim %v, want strictly between 0 and 0.5", w.Dim)
202	}
203	if w := sceneWindow(t, s, 2); !(w.Dim > 0 && w.Dim < 0.5) {
204		t.Fatalf("window 2 mid-way dim %v, want strictly between 0 and 0.5", w.Dim)
205	}
206	s = frame(t, c, ic, 5*time.Second)
207	if w := sceneWindow(t, s, 3); w.Dim != 0 {
208		t.Fatalf("window 3 settled dim %v, want 0", w.Dim)
209	}
210	if w := sceneWindow(t, s, 2); w.Dim != 0.5 {
211		t.Fatalf("window 2 settled dim %v, want 0.5", w.Dim)
212	}
213	if c.animating() || len(sc.rects) != 0 {
214		t.Fatalf("still animating: %d rects", len(sc.rects))
215	}
216}
217
218// A navigation that chains on a running one starts the veil where it was.
219func TestStashNavigationChainsPeekDim(t *testing.T) {
220	c, ic, _, _ := stashRig(t, true)
221	act(c, ic, ActionFocusColumnLeft)
222	indicatorScene(t, c)
223	ic.now = ic.now.Add(30 * time.Millisecond)
224	c.animate(ic.now, nil)
225	mid := sceneWindow(t, indicatorScene(t, c), 3).Dim
226	act(c, ic, ActionFocusColumnRight) // 3 goes back to a peek
227	if got := sceneWindow(t, indicatorScene(t, c), 3).Dim; got < mid-0.02 || got > mid+0.02 {
228		t.Fatalf("window 3 veil %v after the chained action, want the %v it had", got, mid)
229	}
230}
231
232// The overview pile: the stash cards shrink from the stash windows' rects
233// and settle on the pile layout (C1's rules).
234func TestOverviewPileScalesFromStash(t *testing.T) {
235	c, ic, sc, _ := stashRig(t, true)
236	from := map[WindowID]ports.Rect{}
237	s := indicatorScene(t, c)
238	for _, id := range []WindowID{2, 3} {
239		from[id] = sceneWindow(t, s, id).Rect
240	}
241	toggleOverview(c, sc, ic.now)
242	s = indicatorScene(t, c)
243	for _, id := range []WindowID{2, 3} {
244		w := sceneWindow(t, s, id)
245		p := settledOf(t, sc, id)
246		if w.Hidden || w.Preview != overviewCardZoom || w.Rect != from[id] || w.Zoom != 1 {
247			t.Fatalf("window %d first frame %+v, want its stash rect %+v at content scale 1 (card %+v)", id, w, from[id], p.Rect)
248		}
249		checkContent(t, w, from[id], "first frame")
250	}
251	s = frame(t, c, ic, 40*time.Millisecond)
252	for _, id := range []WindowID{2, 3} {
253		w := sceneWindow(t, s, id)
254		p := settledOf(t, sc, id)
255		if !(w.Rect.W < from[id].W && w.Rect.W > p.Rect.W) || !(w.Zoom > overviewCardZoom && w.Zoom < 1) {
256			t.Fatalf("window %d mid-way %+v, want between %+v and %+v", id, w, from[id], p.Rect)
257		}
258		checkContent(t, w, from[id], "mid-way")
259	}
260	s = frame(t, c, ic, 5*time.Second)
261	for _, id := range []WindowID{2, 3} {
262		w := sceneWindow(t, s, id)
263		p := settledOf(t, sc, id)
264		if w.Rect != p.Rect || w.Zoom != 0 || w.Fade != 0 || w.Preview != overviewCardZoom {
265			t.Fatalf("window %d settled %+v, want the pile card %+v", id, w, p)
266		}
267	}
268}
269
270// A navigation that brings a window to a margin makes it appear, one that
271// sends a peek off screen makes it leave; the others only change their veil
272// and rect.
273func TestStashNavigationAppearsAndLeaves(t *testing.T) {
274	c, ic, sc, cmds := stashRig(t, true)
275	w := sc.mon.Current()
276	w.AddWindow(5)
277	w.stashFocus = false                   // the new tile has the focus, not the stash
278	c.applyAction(ActionToggleWindowStash) // stash 4 3 2 5: 5 selected, 2 peeks
279	if len(w.Stash) != 4 || w.stashAt != 3 {
280		t.Fatalf("setup: stash %+v at %d", w.Stash, w.stashAt)
281	}
282	settleShown(t, c)
283	act(c, ic, ActionFocusColumnLeft) // 2 selected, 3 comes in, 5 stays a peek
284	s := indicatorScene(t, c)
285	if w := sceneWindow(t, s, 3); w.Hidden || w.Fade != 1 {
286		t.Fatalf("window 3 comes to the margin: %+v, want invisible at first", w)
287	}
288	if w := sceneWindow(t, s, 5); w.Hidden || w.Fade != 0 {
289		t.Fatalf("window 5 stays a peek: %+v", w)
290	}
291	settledAll := frame(t, c, ic, 5*time.Second)
292	if w := sceneWindow(t, settledAll, 3); w.Fade != 0 || w.Hidden {
293		t.Fatalf("window 3 settled %+v", w)
294	}
295	drainConfigures(cmds)
296	act(c, ic, ActionFocusColumnLeft) // 3 selected, 4 comes in, 5 goes
297	s = indicatorScene(t, c)
298	if w := sceneWindow(t, s, 4); w.Hidden || w.Fade != 1 {
299		t.Fatalf("window 4 comes to the margin: %+v", w)
300	}
301	if w := sceneWindow(t, s, 5); w.Hidden || w.Fade != 0 || w.Rect.W == 0 {
302		t.Fatalf("window 5 leaves from its peek rect: %+v", w)
303	}
304	s = frame(t, c, ic, 5*time.Second)
305	if w := sceneWindow(t, s, 5); !w.Hidden {
306		t.Fatalf("window 5 still drawn after the settle: %+v", w)
307	}
308	if c.animating() {
309		t.Fatal("still animating")
310	}
311}
312
313// near fails the test unless a and b differ by less than eps.
314func near(t *testing.T, what string, a, b, eps float64) {
315	t.Helper()
316	if math.Abs(a-b) > eps {
317		t.Fatalf("%s jumped: %v then %v", what, a, b)
318	}
319}
320
321// A hide during the show's fade-in continues from what was drawn: no jump
322// in fade or rect, then it settles gone.
323func TestStashHideDuringShowDoesNotJump(t *testing.T) {
324	c, ic, sc, _ := stashRig(t, true)
325	act(c, ic, ActionToggleStashVisible)
326	indicatorScene(t, c)
327	ic.now = ic.now.Add(5 * time.Second)
328	c.animate(ic.now, nil)
329	indicatorScene(t, c)
330	act(c, ic, ActionToggleStashVisible) // show
331	indicatorScene(t, c)
332	s := frame(t, c, ic, 30*time.Millisecond)
333	last := sceneWindow(t, s, 2)
334	if !(last.Fade > 0 && last.Fade < 1) {
335		t.Fatalf("setup: fade %v", last.Fade)
336	}
337	act(c, ic, ActionToggleStashVisible) // hide, same instant
338	w := sceneWindow(t, indicatorScene(t, c), 2)
339	if w.Hidden {
340		t.Fatalf("window 2 vanished: %+v", w)
341	}
342	near(t, "fade", last.Fade, w.Fade, 0.02)
343	near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
344	near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
345	near(t, "zoom", contentScale(last), contentScale(w), 0.03)
346	s = frame(t, c, ic, 5*time.Second)
347	if w := sceneWindow(t, s, 2); !w.Hidden || c.animating() || len(sc.rects) != 0 {
348		t.Fatalf("after the settle: %+v, %d rects", w, len(sc.rects))
349	}
350}
351
352// A show during the hide's fade-out continues from what was drawn.
353func TestStashShowDuringHideDoesNotJump(t *testing.T) {
354	c, ic, sc, _ := stashRig(t, true)
355	act(c, ic, ActionToggleStashVisible) // hide
356	indicatorScene(t, c)
357	s := frame(t, c, ic, 30*time.Millisecond)
358	last := sceneWindow(t, s, 2)
359	if !(last.Fade > 0 && last.Fade < 1) {
360		t.Fatalf("setup: fade %v", last.Fade)
361	}
362	act(c, ic, ActionToggleStashVisible) // show
363	w := sceneWindow(t, indicatorScene(t, c), 2)
364	if w.Hidden {
365		t.Fatalf("window 2 hidden: %+v", w)
366	}
367	near(t, "fade", last.Fade, w.Fade, 0.02)
368	near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
369	near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
370	near(t, "zoom", contentScale(last), contentScale(w), 0.03)
371	// The entrance now runs back to opaque (the fade's speed carries over:
372	// it may still rise a few frames before it turns).
373	s = frame(t, c, ic, 150*time.Millisecond)
374	if n := sceneWindow(t, s, 2); !(n.Fade < w.Fade) {
375		t.Fatalf("fade %v then %v, want it to clear", w.Fade, n.Fade)
376	}
377	s = frame(t, c, ic, 5*time.Second)
378	p := settledOf(t, sc, 2)
379	if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect || c.animating() {
380		t.Fatalf("settled %+v, want %+v", w, p.Rect)
381	}
382}
383
384// A peek sliding to a margin that a fast second navigation sends off screen
385// leaves from where it was drawn, mid-slide, not from its settled rect.
386func TestStashNavigationLeavesFromSlide(t *testing.T) {
387	c, ic, sc, _ := stashRig(t, true)
388	// 2 selected, 3 peeks left. Left: 3 centred, 2 peeks right, 4 enters.
389	act(c, ic, ActionFocusColumnLeft)
390	indicatorScene(t, c)
391	s := frame(t, c, ic, 30*time.Millisecond)
392	last := sceneWindow(t, s, 2)
393	settled := settledOf(t, sc, 2)
394	if last.Rect == settled.Rect {
395		t.Fatal("setup: window 2 already at rest")
396	}
397	// Left again: 4 centred, 3 peeks right, 2 goes off.
398	act(c, ic, ActionFocusColumnLeft)
399	w := sceneWindow(t, indicatorScene(t, c), 2)
400	if w.Hidden {
401		t.Fatalf("window 2 vanished: %+v", w)
402	}
403	near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
404	near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
405	near(t, "dim", last.Dim, w.Dim, 0.03)
406	if s = frame(t, c, ic, 5*time.Second); !sceneWindow(t, s, 2).Hidden {
407		t.Fatal("window 2 still drawn after the settle")
408	}
409}
410
411// Leaving entries land in the same order whatever the map's: a hidden stash
412// float (replaced in place) and absent tiles and floats (ID order, tiles
413// before the first float, floats last).
414func TestWithLeavingOrderIsDeterministic(t *testing.T) {
415	c, ic := indicatorCore(t)
416	c.cfg.Animations.On = true
417	sc := c.cur()
418	w := sc.mon.Current()
419	for id := WindowID(1); id <= 4; id++ {
420		sc.mon.AddWindow(id)
421	}
422	w.FocusID(4)
423	c.applyAction(ActionToggleWindowStash) // 4 stashed, shown
424	w.AddFloating(5, 20, 10)
425	settleShown(t, c)
426	var left []Placement
427	for _, p := range sc.settledLayout {
428		if p.ID == 2 || p.ID == 3 || p.ID == 4 || p.ID == 5 {
429			left = append(left, p)
430		}
431	}
432	if len(left) != 4 {
433		t.Fatalf("setup: %d placements", len(left))
434	}
435	// 2 and 3 close (absent tiles), 5 closes (absent float), 4 hides.
436	sc.mon.RemoveWindow(2)
437	sc.mon.RemoveWindow(3)
438	sc.mon.RemoveWindow(5)
439	c.applyAction(ActionToggleStashVisible)
440	for _, p := range left {
441		c.leaveFrom(sc, p, nil, ic.now)
442	}
443	var want []WindowID
444	for i := range 40 {
445		c.refreshShown()
446		var ids []WindowID
447		for _, p := range sc.settledLayout {
448			ids = append(ids, p.ID)
449		}
450		if i == 0 {
451			want = ids
452			if n := len(ids); n < 4 {
453				t.Fatalf("layout %v", ids)
454			}
455			i2, i3 := slices.Index(ids, 2), slices.Index(ids, 3)
456			i4, i5 := slices.Index(ids, 4), slices.Index(ids, 5)
457			// Absent tiles by ID, then the leaving stash float (in its
458			// slot), the absent float last.
459			if !(i2 >= 0 && i2 < i3 && i3 < i4 && i4 < i5 && i5 == len(ids)-1) {
460				t.Fatalf("order %v: want tiles 2, 3, then the stash float 4, then 5", ids)
461			}
462			if k := slices.Index(ids, 4); !sc.settledLayout[k].Leaving {
463				t.Fatal("the hidden stash window is not replaced in place")
464			}
465			continue
466		}
467		if !slices.Equal(ids, want) {
468			t.Fatalf("run %d: order %v, first run %v", i, ids, want)
469		}
470	}
471}

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