v0.4.0 b6be3a8

transition_map_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// A tiled map: the new window fades in from 90 % of its settled rect, its
13// neighbour slides from where it was, the client is configured once, to
14// the final size, and everything settles exactly.
15func TestMapTiledAppearsAndReflows(t *testing.T) {
16	c, ic, cmds := fadeCore(t)
17	c.cfg.Animations.On = true
18	sc := c.cur()
19	c.mapWindow(ports.WindowMapped{ID: 1})
20	indicatorScene(t, c)
21	c.animate(ic.now.Add(5*time.Second), nil)
22	ic.now = ic.now.Add(5 * time.Second)
23	indicatorScene(t, c)
24	if len(sc.rects) != 0 {
25		t.Fatalf("motions left after the first window settled: %d", len(sc.rects))
26	}
27	drainConfigures(cmds)
28	old1 := sceneWindow(t, indicatorScene(t, c), 1).Rect
29
30	c.mapWindow(ports.WindowMapped{ID: 2})
31	s := indicatorScene(t, c)
32	settled2 := settledOf(t, sc, 2)
33	w2 := sceneWindow(t, s, 2)
34	if w2.Fade != 1 || w2.Hidden || w2.Rect != scaledRect(settled2.Rect, appearScale) || math.Abs(w2.Zoom-appearScale) > 0.02 || w2.Preview != 0 {
35		t.Fatalf("new window right after the map: %+v, settled %+v", w2, settled2.Rect)
36	}
37	// The neighbour is drawn where it was, and moves to its new half.
38	if got := sceneWindow(t, s, 1).Rect; got != old1 {
39		t.Fatalf("neighbour right after the map at %+v, want the old %+v", got, old1)
40	}
41	settled1 := settledOf(t, sc, 1)
42	if settled1.Rect == old1 {
43		t.Fatal("the new column did not move the neighbour: the case checks nothing")
44	}
45	// One configure per window, to the settled size.
46	got := drainConfigures(cmds)
47	slices.Sort(got)
48	if !slices.Equal(got, []WindowID{1, 2}) {
49		t.Fatalf("configures %v, want one each for 1 and 2", got)
50	}
51
52	ic.now = ic.now.Add(40 * time.Millisecond)
53	c.animate(ic.now, nil)
54	s = indicatorScene(t, c)
55	w2 = sceneWindow(t, s, 2)
56	if !(w2.Fade > 0 && w2.Fade < 1) || !(w2.Zoom > appearScale && w2.Zoom < 1) || !(w2.Rect.W > scaledRect(settled2.Rect, appearScale).W && w2.Rect.W < settled2.Rect.W) {
57		t.Fatalf("first frame of the new window: %+v", w2)
58	}
59	if r := sceneWindow(t, s, 1).Rect; !(r.W < old1.W && r.W > settled1.Rect.W) {
60		t.Fatalf("neighbour on the first frame %+v, want between %+v and %+v", r, old1, settled1.Rect)
61	}
62	if ids := drainConfigures(cmds); len(ids) != 0 {
63		t.Fatalf("frames reconfigured %v", ids)
64	}
65
66	ic.now = ic.now.Add(5 * time.Second)
67	c.animate(ic.now, nil)
68	s = indicatorScene(t, c)
69	if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != settled2.Rect {
70		t.Fatalf("new window settled: %+v, want %+v", w, settled2.Rect)
71	}
72	if w := sceneWindow(t, s, 1); w.Rect != settled1.Rect {
73		t.Fatalf("neighbour settled at %+v, want %+v", w.Rect, settled1.Rect)
74	}
75	if len(sc.rects) != 0 || c.animating() {
76		t.Fatal("still animating after the settle")
77	}
78}
79
80// A floating toplevel appears too.
81func TestMapFloatingAppears(t *testing.T) {
82	c, ic, _ := fadeCore(t)
83	c.cfg.Animations.On = true
84	sc := c.cur()
85	c.mapWindow(ports.WindowMapped{ID: 1})
86	indicatorScene(t, c)
87	c.mapWindow(ports.WindowMapped{ID: 2, Floating: true, Width: 80, Height: 60})
88	s := indicatorScene(t, c)
89	settled := settledOf(t, sc, 2)
90	w := sceneWindow(t, s, 2)
91	if !w.Floating && !settled.Floating {
92		t.Fatalf("window 2 is not a float: %+v", settled)
93	}
94	if w.Fade != 1 || w.Rect != scaledRect(settled.Rect, appearScale) {
95		t.Fatalf("float right after the map: %+v, settled %+v", w, settled.Rect)
96	}
97	ic.now = ic.now.Add(5 * time.Second)
98	c.animate(ic.now, nil)
99	if w := sceneWindow(t, indicatorScene(t, c), 2); w.Fade != 0 || w.Rect != settled.Rect {
100		t.Fatalf("float settled: %+v", w)
101	}
102}
103
104// With animations off a map is instant: no motion, no fade.
105func TestMapOffIsInstant(t *testing.T) {
106	c, _, _ := fadeCore(t)
107	sc := c.cur()
108	c.mapWindow(ports.WindowMapped{ID: 1})
109	indicatorScene(t, c)
110	c.mapWindow(ports.WindowMapped{ID: 2})
111	s := indicatorScene(t, c)
112	if len(sc.rects) != 0 || c.animating() {
113		t.Fatal("a map started a motion with animations off")
114	}
115	for _, w := range s.Windows {
116		if w.Fade != 0 || w.Zoom != 0 || w.Rect != settledOf(t, sc, w.ID).Rect {
117			t.Fatalf("window %d not at its settled rect: %+v", w.ID, w)
118		}
119	}
120}
121
122// A window that is not shown tiled or floating on screen does not appear:
123// fullscreen, hidden behind a fullscreen game, with a session lock or with
124// the overview open. Neighbours stay put as well in the last two.
125func TestMapExceptions(t *testing.T) {
126	t.Run("fullscreen", func(t *testing.T) {
127		c, ic, _ := fadeCore(t)
128		c.cfg.Animations.On = true
129		sc := c.cur()
130		c.mapWindow(ports.WindowMapped{ID: 1})
131		c.stopAnimations() // the first window's own entrance
132		sc.mon.SetFullscreen(1, true)
133		c.appearMapped(1, ic.now)
134		indicatorScene(t, c)
135		if len(sc.rects) != 0 || c.animating() {
136			t.Fatalf("a fullscreen window appeared: %+v", sc.rects)
137		}
138	})
139	t.Run("behind a fullscreen game", func(t *testing.T) {
140		c, _, _ := fadeCore(t)
141		c.cfg.Animations.On = true
142		sc := c.cur()
143		sc.mon.Current().Overflow = OverflowFixed
144		c.mapWindow(ports.WindowMapped{ID: 1})
145		c.stopAnimations() // the first window's own entrance
146		sc.mon.SetFullscreen(1, true)
147		indicatorScene(t, c)
148		c.mapWindow(ports.WindowMapped{ID: 2})
149		if s := indicatorScene(t, c); len(sc.rects) != 0 || sceneWindow(t, s, 2).Fade != 0 {
150			t.Fatalf("a window mapped behind a fullscreen one appeared: %+v", sc.rects)
151		}
152	})
153	t.Run("session lock", func(t *testing.T) {
154		c, _, _ := fadeCore(t)
155		c.cfg.Animations.On = true
156		sc := c.cur()
157		c.mapWindow(ports.WindowMapped{ID: 1})
158		c.stopAnimations() // the first window's own entrance
159		c.security = ports.SecurityState{Generation: 1, Protected: true}
160		c.mapWindow(ports.WindowMapped{ID: 2})
161		if len(sc.rects) != 0 || c.animating() {
162			t.Fatalf("a window mapped under a session lock animated: %+v", sc.rects)
163		}
164	})
165	t.Run("overview open", func(t *testing.T) {
166		c, _, _ := fadeCore(t)
167		c.cfg.Animations.On = true
168		sc := c.cur()
169		c.mapWindow(ports.WindowMapped{ID: 1})
170		c.stopAnimations() // the first window's own entrance
171		sc.mon.ToggleOverview()
172		c.mapWindow(ports.WindowMapped{ID: 2})
173		if len(sc.rects) != 0 {
174			t.Fatalf("a window mapped in the overview animated: %+v", sc.rects)
175		}
176	})
177	t.Run("map again", func(t *testing.T) {
178		c, ic, _ := fadeCore(t)
179		c.cfg.Animations.On = true
180		sc := c.cur()
181		c.mapWindow(ports.WindowMapped{ID: 1})
182		c.stopAnimations() // the first window's own entrance
183		indicatorScene(t, c)
184		ic.now = ic.now.Add(5 * time.Second)
185		c.mapWindow(ports.WindowMapped{ID: 1})
186		c.stopAnimations() // the first window's own entrance
187		if len(sc.rects) != 0 {
188			t.Fatalf("a repeated map restarted the entrance: %+v", sc.rects)
189		}
190	})
191}
192
193// A float maps before its first commit: Width and Height are 0, so core lays
194// it out at a default size, and the real one comes in WindowResized. The
195// entrance must restart from 90 % of the real size, not stay distorted.
196func TestMapFloatEntranceFollowsItsFirstCommit(t *testing.T) {
197	c, ic, _ := fadeCore(t)
198	c.cfg.Animations.On = true
199	sc := c.cur()
200	c.mapWindow(ports.WindowMapped{ID: 1})
201	c.stopAnimations()
202	c.mapWindow(ports.WindowMapped{ID: 2, Floating: true})
203	indicatorScene(t, c)
204	guess := settledOf(t, sc, 2).Rect
205
206	c.resizeFloating(ports.WindowResized{ID: 2, Width: 60, Height: 20})
207	s := indicatorScene(t, c)
208	real := settledOf(t, sc, 2).Rect
209	if real == guess || real.W == real.H*guess.W/guess.H {
210		t.Fatalf("the resize did not change the settled shape: %+v then %+v", guess, real)
211	}
212	w := sceneWindow(t, s, 2)
213	want := scaledRect(real, appearScale)
214	near := func(a, b int) bool { return a-b <= 1 && b-a <= 1 }
215	if w.Fade != 1 || !near(w.Rect.X, want.X) || !near(w.Rect.Y, want.Y) || !near(w.Rect.W, want.W) || !near(w.Rect.H, want.H) {
216		t.Fatalf("first frame %+v, want 90 %% of the real %+v = %+v", w, real, want)
217	}
218	if math.Abs(w.Zoom-appearScale) > 0.02 {
219		t.Fatalf("zoom %v, want ~0.9 (aspect kept)", w.Zoom)
220	}
221
222	// Mid-way the frame keeps the real aspect, the zoom follows its width.
223	ic.now = ic.now.Add(40 * time.Millisecond)
224	c.animate(ic.now, nil)
225	w = sceneWindow(t, indicatorScene(t, c), 2)
226	ratio, wantRatio := float64(w.Rect.W)/float64(w.Rect.H), float64(real.W)/float64(real.H)
227	if math.Abs(ratio-wantRatio)/wantRatio > 0.06 || !(w.Rect.W > want.W && w.Rect.W < real.W) || math.Abs(w.Zoom-float64(w.Rect.W)/float64(real.W)) > 0.01 {
228		t.Fatalf("mid-way %+v: aspect %.3f want %.3f", w, ratio, wantRatio)
229	}
230
231	ic.now = ic.now.Add(5 * time.Second)
232	c.animate(ic.now, nil)
233	if w := sceneWindow(t, indicatorScene(t, c), 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != real {
234		t.Fatalf("settled %+v, want %+v", w, real)
235	}
236}
237
238// A resize with no entrance running only moves the settled rect.
239func TestResizeFloatWithoutEntranceKeepsNoMotion(t *testing.T) {
240	c, _, _ := fadeCore(t)
241	c.cfg.Animations.On = true
242	sc := c.cur()
243	c.mapWindow(ports.WindowMapped{ID: 1, Floating: true})
244	c.stopAnimations()
245	c.resizeFloating(ports.WindowResized{ID: 1, Width: 60, Height: 20})
246	if len(sc.rects) != 0 {
247		t.Fatalf("a resize started a motion: %+v", sc.rects)
248	}
249}

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