detached be48d93

test(core): deterministic mid-swipe overview test, shared transition helpers, quitting headless scripts

TestOverviewOpenedMidSwipeDropsIt receives the lift's own scene and then
settles on the stepped clock instead of polling real time. settledOf,
settleShown, configuresOf and drainConfigures live in one file
(settledPlacement was a copy of settledOf). The headless click-focus
variants end their script with the quit bind, so the run stops at ~2.6 s
instead of the 5 s timeout.
commit
be48d93f5eddc32181280474e5e9f2ea9104c3be
author
brice · · 2026-10-05 02:08 UTC
parent
1d8bbbe
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +91 -86
M internal/app/app_test.go +3 -1
@@ -268,7 +268,9 @@ func headlessPointerClickFocus(t *testing.T, animated bool) {
268268 var halt sync.Once
269269 stopCollector := func() { halt.Do(func() { close(stop); <-collected }) }
270270 t.Cleanup(stopCollector)
271- err := Run(context.Background(), Options{Backend: "headless", NoXwayland: true, Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n")), Timeout: 5 * time.Second, testScenes: scenes})
271+ // The script ends with the default quit bind: the run stops when it
272+ // is done, the timeout is only a backstop.
273+ err := Run(context.Background(), Options{Backend: "headless", NoXwayland: true, Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\nkey Ctrl+Alt+BackSpace\n")), Timeout: 5 * time.Second, testScenes: scenes})
272274 if err != nil {
273275 if strings.Contains(strings.ToLower(err.Error()), "vulkan") {
274276 t.Skipf("Vulkan unavailable: %v", err)
@@ -268,7 +268,9 @@ func headlessPointerClickFocus(t *testing.T, animated bool) {
268 var halt sync.Once268 var halt sync.Once
269 stopCollector := func() { halt.Do(func() { close(stop); <-collected }) }269 stopCollector := func() { halt.Do(func() { close(stop); <-collected }) }
270 t.Cleanup(stopCollector)270 t.Cleanup(stopCollector)
271- err := Run(context.Background(), Options{Backend: "headless", NoXwayland: true, Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\n")), Timeout: 5 * time.Second, testScenes: scenes})271+ // The script ends with the default quit bind: the run stops when it
272+ // is done, the timeout is only a backstop.
273+ err := Run(context.Background(), Options{Backend: "headless", NoXwayland: true, Config: cfg, Script: io.NopCloser(strings.NewReader("sleep 1s\nkey Super+Return\nsleep 1s\nmove 600 300\nclick\nsleep 500ms\nkey Ctrl+Alt+BackSpace\n")), Timeout: 5 * time.Second, testScenes: scenes})
272 if err != nil {274 if err != nil {
273 if strings.Contains(strings.ToLower(err.Error()), "vulkan") {275 if strings.Contains(strings.ToLower(err.Error()), "vulkan") {
274 t.Skipf("Vulkan unavailable: %v", err)276 t.Skipf("Vulkan unavailable: %v", err)
M internal/core/swipe_test.go +23 -22
@@ -772,38 +772,39 @@ func TestOverviewOpenedMidSwipeDropsIt(t *testing.T) {
772772 r.move(t, -40, 0)
773773 r.move(t, -40, 0)
774774 r.key(t, "o", ports.ModAlt)
775+ // s is the latest scene.
776+ var s ports.Scene
775777 if closeFirst {
776- r.key(t, "Escape", 0)
778+ s = r.key(t, "Escape", 0)[0]
777779 }
778780 for range 20 {
779781 r.at += 8 * time.Millisecond
780782 r.input <- ports.SwipeUpdate{DX: -80, Time: r.at}
781783 }
784+ // The updates publish nothing (the overview gesture, or a dropped
785+ // swipe); the lift always publishes, so its scene is received
786+ // before the next event: core is idle when the escape and the
787+ // flips arrive, each waits for its own scene.
782788 r.input <- ports.SwipeEnd{Time: r.at}
789+ s = scene(t, r.scenes)
783790 if !closeFirst {
784- r.input <- ports.KeyEvent{Keysym: "Escape", Pressed: true}
785- }
786- // Past any spring: the view shows where it settled. The escape's
787- // own springs (the cards zooming back) start when it is handled,
788- // which may follow a first flip: flip until the scene is settled.
789- var s ports.Scene
790- for deadline := time.Now().Add(5 * time.Second); ; {
791- r.advance(5 * time.Second)
792- r.frames <- ports.OutputFrame{Output: wide.Name}
793- select {
794- case set := <-r.scenes:
795- s = set[0]
796- case <-time.After(20 * time.Millisecond):
797- }
798- if len(s.Windows) > 0 && !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool {
799- // Neither a card nor a window still zooming back.
800- return w.Preview > 0 || w.Zoom > 0 || w.Fade > 0
801- }) {
791+ s = r.key(t, "Escape", 0)[0]
792+ }
793+ // Past any spring: the escape's own springs (the cards zooming
794+ // back) may chain after the swipe's landing, so settle until
795+ // none runs.
796+ for range 4 {
797+ set, ok := r.settleAll(t, wide.Name)
798+ if !ok {
802799 break
803800 }
804- if time.Now().After(deadline) {
805- t.Fatalf("closeFirst=%v: the scene never settled: %+v", closeFirst, s.Windows)
806- }
801+ s = set[0]
802+ }
803+ if len(s.Windows) == 0 || slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool {
804+ // Neither a card nor a window still zooming back.
805+ return w.Preview > 0 || w.Zoom > 0 || w.Fade > 0
806+ }) {
807+ t.Fatalf("closeFirst=%v: the scene never settled: %+v", closeFirst, s.Windows)
807808 }
808809 for _, id := range []ports.WindowID{1, 2, 3} {
809810 before, _ := rectOf(settled, id)
@@ -772,38 +772,39 @@ func TestOverviewOpenedMidSwipeDropsIt(t *testing.T) {
772 r.move(t, -40, 0)772 r.move(t, -40, 0)
773 r.move(t, -40, 0)773 r.move(t, -40, 0)
774 r.key(t, "o", ports.ModAlt)774 r.key(t, "o", ports.ModAlt)
775+ // s is the latest scene.
776+ var s ports.Scene
775 if closeFirst {777 if closeFirst {
776- r.key(t, "Escape", 0)778+ s = r.key(t, "Escape", 0)[0]
777 }779 }
778 for range 20 {780 for range 20 {
779 r.at += 8 * time.Millisecond781 r.at += 8 * time.Millisecond
780 r.input <- ports.SwipeUpdate{DX: -80, Time: r.at}782 r.input <- ports.SwipeUpdate{DX: -80, Time: r.at}
781 }783 }
784+ // The updates publish nothing (the overview gesture, or a dropped
785+ // swipe); the lift always publishes, so its scene is received
786+ // before the next event: core is idle when the escape and the
787+ // flips arrive, each waits for its own scene.
782 r.input <- ports.SwipeEnd{Time: r.at}788 r.input <- ports.SwipeEnd{Time: r.at}
789+ s = scene(t, r.scenes)
783 if !closeFirst {790 if !closeFirst {
784- r.input <- ports.KeyEvent{Keysym: "Escape", Pressed: true}791+ s = r.key(t, "Escape", 0)[0]
785- }792+ }
786- // Past any spring: the view shows where it settled. The escape's793+ // Past any spring: the escape's own springs (the cards zooming
787- // own springs (the cards zooming back) start when it is handled,794+ // back) may chain after the swipe's landing, so settle until
788- // which may follow a first flip: flip until the scene is settled.795+ // none runs.
789- var s ports.Scene796+ for range 4 {
790- for deadline := time.Now().Add(5 * time.Second); ; {797+ set, ok := r.settleAll(t, wide.Name)
791- r.advance(5 * time.Second)798+ if !ok {
792- r.frames <- ports.OutputFrame{Output: wide.Name}
793- select {
794- case set := <-r.scenes:
795- s = set[0]
796- case <-time.After(20 * time.Millisecond):
797- }
798- if len(s.Windows) > 0 && !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool {
799- // Neither a card nor a window still zooming back.
800- return w.Preview > 0 || w.Zoom > 0 || w.Fade > 0
801- }) {
802 break799 break
803 }800 }
804- if time.Now().After(deadline) {801+ s = set[0]
805- t.Fatalf("closeFirst=%v: the scene never settled: %+v", closeFirst, s.Windows)802+ }
806- }803+ if len(s.Windows) == 0 || slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool {
804+ // Neither a card nor a window still zooming back.
805+ return w.Preview > 0 || w.Zoom > 0 || w.Fade > 0
806+ }) {
807+ t.Fatalf("closeFirst=%v: the scene never settled: %+v", closeFirst, s.Windows)
807 }808 }
808 for _, id := range []ports.WindowID{1, 2, 3} {809 for _, id := range []ports.WindowID{1, 2, 3} {
809 before, _ := rectOf(settled, id)810 before, _ := rectOf(settled, id)
M internal/core/transition_chain_internal_test.go +0 -6
@@ -10,12 +10,6 @@ import (
1010 "github.com/bnema/neferwl/internal/ports"
1111 )
1212
13-// settleShown publishes so the screens' shown layouts are current.
14-func settleShown(t *testing.T, c *Core) {
15- t.Helper()
16- indicatorScene(t, c)
17-}
18-
1913 // A bind that stops a spring while it acts (Monitor.Focus) must not lose its
2014 // speed: the transition that follows starts from what the snapshot recorded.
2115 func TestChainedActionsKeepVelocity(t *testing.T) {
@@ -10,12 +10,6 @@ import (
10 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
11 )11 )
12 12
13-// settleShown publishes so the screens' shown layouts are current.
14-func settleShown(t *testing.T, c *Core) {
15- t.Helper()
16- indicatorScene(t, c)
17-}
18-
19 // A bind that stops a spring while it acts (Monitor.Focus) must not lose its13 // A bind that stops a spring while it acts (Monitor.Focus) must not lose its
20 // speed: the transition that follows starts from what the snapshot recorded.14 // speed: the transition that follows starts from what the snapshot recorded.
21 func TestChainedActionsKeepVelocity(t *testing.T) {15 func TestChainedActionsKeepVelocity(t *testing.T) {
M internal/core/transition_fade_internal_test.go +0 -15
@@ -18,21 +18,6 @@ func fadeCore(t *testing.T) (*Core, *indicatorClock, chan ports.ClientCommand) {
1818 return c, ic, cmds
1919 }
2020
21-// drainConfigures returns the IDs configured since the last drain.
22-func drainConfigures(cmds chan ports.ClientCommand) []WindowID {
23- var ids []WindowID
24- for {
25- select {
26- case cmd := <-cmds:
27- if v, ok := cmd.(ports.ConfigureWindow); ok {
28- ids = append(ids, v.ID)
29- }
30- default:
31- return ids
32- }
33- }
34-}
35-
3621 func sceneWindow(t *testing.T, s ports.Scene, id WindowID) ports.SceneWindow {
3722 t.Helper()
3823 i := slices.IndexFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == id })
@@ -18,21 +18,6 @@ func fadeCore(t *testing.T) (*Core, *indicatorClock, chan ports.ClientCommand) {
18 return c, ic, cmds18 return c, ic, cmds
19 }19 }
20 20
21-// drainConfigures returns the IDs configured since the last drain.
22-func drainConfigures(cmds chan ports.ClientCommand) []WindowID {
23- var ids []WindowID
24- for {
25- select {
26- case cmd := <-cmds:
27- if v, ok := cmd.(ports.ConfigureWindow); ok {
28- ids = append(ids, v.ID)
29- }
30- default:
31- return ids
32- }
33- }
34-}
35-
36 func sceneWindow(t *testing.T, s ports.Scene, id WindowID) ports.SceneWindow {21 func sceneWindow(t *testing.T, s ports.Scene, id WindowID) ports.SceneWindow {
37 t.Helper()22 t.Helper()
38 i := slices.IndexFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == id })23 i := slices.IndexFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == id })
A internal/core/transition_helpers_internal_test.go +56 -0
@@ -0,0 +1,56 @@
1+package core
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// Helpers shared by the transition tests (transition_*_internal_test.go).
11+
12+// settleShown publishes so the screens' shown layouts are current.
13+func settleShown(t *testing.T, c *Core) {
14+ t.Helper()
15+ indicatorScene(t, c)
16+}
17+
18+// settledOf is the settled placement of window id on sc.
19+func settledOf(t *testing.T, sc *screen, id WindowID) Placement {
20+ t.Helper()
21+ i := slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == id })
22+ if i < 0 {
23+ t.Fatalf("window %d not in the settled layout %+v", id, sc.settledLayout)
24+ }
25+ return sc.settledLayout[i]
26+}
27+
28+// configuresOf returns the configures sent since the last drain, by window.
29+func configuresOf(cmds chan ports.ClientCommand) map[WindowID][]ports.ConfigureWindow {
30+ out := map[WindowID][]ports.ConfigureWindow{}
31+ for {
32+ select {
33+ case cmd := <-cmds:
34+ if v, ok := cmd.(ports.ConfigureWindow); ok {
35+ out[v.ID] = append(out[v.ID], v)
36+ }
37+ default:
38+ return out
39+ }
40+ }
41+}
42+
43+// drainConfigures returns the IDs configured since the last drain, in order.
44+func drainConfigures(cmds chan ports.ClientCommand) []WindowID {
45+ var ids []WindowID
46+ for {
47+ select {
48+ case cmd := <-cmds:
49+ if v, ok := cmd.(ports.ConfigureWindow); ok {
50+ ids = append(ids, v.ID)
51+ }
52+ default:
53+ return ids
54+ }
55+ }
56+}
@@ -0,0 +1,56 @@
1+package core
2+
3+import (
4+ "slices"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// Helpers shared by the transition tests (transition_*_internal_test.go).
11+
12+// settleShown publishes so the screens' shown layouts are current.
13+func settleShown(t *testing.T, c *Core) {
14+ t.Helper()
15+ indicatorScene(t, c)
16+}
17+
18+// settledOf is the settled placement of window id on sc.
19+func settledOf(t *testing.T, sc *screen, id WindowID) Placement {
20+ t.Helper()
21+ i := slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == id })
22+ if i < 0 {
23+ t.Fatalf("window %d not in the settled layout %+v", id, sc.settledLayout)
24+ }
25+ return sc.settledLayout[i]
26+}
27+
28+// configuresOf returns the configures sent since the last drain, by window.
29+func configuresOf(cmds chan ports.ClientCommand) map[WindowID][]ports.ConfigureWindow {
30+ out := map[WindowID][]ports.ConfigureWindow{}
31+ for {
32+ select {
33+ case cmd := <-cmds:
34+ if v, ok := cmd.(ports.ConfigureWindow); ok {
35+ out[v.ID] = append(out[v.ID], v)
36+ }
37+ default:
38+ return out
39+ }
40+ }
41+}
42+
43+// drainConfigures returns the IDs configured since the last drain, in order.
44+func drainConfigures(cmds chan ports.ClientCommand) []WindowID {
45+ var ids []WindowID
46+ for {
47+ select {
48+ case cmd := <-cmds:
49+ if v, ok := cmd.(ports.ConfigureWindow); ok {
50+ ids = append(ids, v.ID)
51+ }
52+ default:
53+ return ids
54+ }
55+ }
56+}
M internal/core/transition_map_internal_test.go +0 -9
@@ -9,15 +9,6 @@ import (
99 "github.com/bnema/neferwl/internal/ports"
1010 )
1111
12-func settledOf(t *testing.T, sc *screen, id WindowID) Placement {
13- t.Helper()
14- i := slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == id })
15- if i < 0 {
16- t.Fatalf("window %d not in the settled layout %+v", id, sc.settledLayout)
17- }
18- return sc.settledLayout[i]
19-}
20-
2112 // A tiled map: the new window fades in from 90 % of its settled rect, its
2213 // neighbour slides from where it was, the client is configured once, to
2314 // the final size, and everything settles exactly.
@@ -9,15 +9,6 @@ import (
9 "github.com/bnema/neferwl/internal/ports"9 "github.com/bnema/neferwl/internal/ports"
10 )10 )
11 11
12-func settledOf(t *testing.T, sc *screen, id WindowID) Placement {
13- t.Helper()
14- i := slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == id })
15- if i < 0 {
16- t.Fatalf("window %d not in the settled layout %+v", id, sc.settledLayout)
17- }
18- return sc.settledLayout[i]
19-}
20-
21 // A tiled map: the new window fades in from 90 % of its settled rect, its12 // A tiled map: the new window fades in from 90 % of its settled rect, its
22 // neighbour slides from where it was, the client is configured once, to13 // neighbour slides from where it was, the client is configured once, to
23 // the final size, and everything settles exactly.14 // the final size, and everything settles exactly.
M internal/core/transition_stash_internal_test.go +9 -33
@@ -30,21 +30,6 @@ func stashRig(t *testing.T, on bool) (*Core, *indicatorClock, *screen, chan port
3030 return c, ic, sc, cmds
3131 }
3232
33-// configuresOf drains the commands into the configures sent per window.
34-func configuresOf(cmds chan ports.ClientCommand) map[WindowID][]ports.ConfigureWindow {
35- out := map[WindowID][]ports.ConfigureWindow{}
36- for {
37- select {
38- case cmd := <-cmds:
39- if v, ok := cmd.(ports.ConfigureWindow); ok {
40- out[v.ID] = append(out[v.ID], v)
41- }
42- default:
43- return out
44- }
45- }
46-}
47-
4833 // act is a bind: snapshot, action, transition.
4934 func act(c *Core, ic *indicatorClock, a Action) {
5035 shots := c.snapshot(ic.now)
@@ -52,15 +37,6 @@ func act(c *Core, ic *indicatorClock, a Action) {
5237 c.transition(shots, ic.now)
5338 }
5439
55-func settledPlacement(t *testing.T, sc *screen, id WindowID) Placement {
56- t.Helper()
57- i := slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == id })
58- if i < 0 {
59- t.Fatalf("window %d not in the settled layout", id)
60- }
61- return sc.settledLayout[i]
62-}
63-
6440 // Showing the stash fades in and grows every window it brings on screen;
6541 // the windows it keeps off screen stay out; the settled layout is the
6642 // final one at once, with no extra configure.
@@ -79,7 +55,7 @@ func TestStashShowAppears(t *testing.T) {
7955 act(c, ic, ActionToggleStashVisible)
8056 s := indicatorScene(t, c)
8157 for _, id := range []WindowID{2, 3} {
82- p := settledPlacement(t, sc, id)
58+ p := settledOf(t, sc, id)
8359 w := sceneWindow(t, s, id)
8460 if w.Hidden || w.Fade != 1 || w.Rect != scaledRect(p.Rect, appearScale) || w.Zoom < 0.85 || w.Zoom > 0.95 {
8561 t.Fatalf("window %d first frame %+v, want invisible at 90 %% of %+v", id, w, p.Rect)
@@ -94,7 +70,7 @@ func TestStashShowAppears(t *testing.T) {
9470 if len(cfgs[id]) != 1 {
9571 t.Fatalf("window %d configured %d times, want once: %+v", id, len(cfgs[id]), cfgs[id])
9672 }
97- want := c.clientRect(settledPlacement(t, sc, id))
73+ want := c.clientRect(settledOf(t, sc, id))
9874 if v := cfgs[id][0]; !v.Visible || v.Width != want.W || v.Height != want.H {
9975 t.Fatalf("window %d configure %+v, want %dx%d (the settled client rect)", id, v, want.W, want.H)
10076 }
@@ -109,7 +85,7 @@ func TestStashShowAppears(t *testing.T) {
10985 }
11086 s = frame(t, c, ic, 5*time.Second)
11187 for _, id := range []WindowID{2, 3} {
112- p := settledPlacement(t, sc, id)
88+ p := settledOf(t, sc, id)
11389 if w := sceneWindow(t, s, id); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect {
11490 t.Fatalf("window %d settled %+v, want %+v", id, w, p.Rect)
11591 }
@@ -128,7 +104,7 @@ func TestStashHideLeaves(t *testing.T) {
128104 c, ic, sc, cmds := stashRig(t, true)
129105 left := map[WindowID]Placement{}
130106 for _, id := range []WindowID{2, 3} {
131- left[id] = settledPlacement(t, sc, id)
107+ left[id] = settledOf(t, sc, id)
132108 }
133109 act(c, ic, ActionToggleStashVisible)
134110 s := indicatorScene(t, c)
@@ -266,7 +242,7 @@ func TestOverviewPileScalesFromStash(t *testing.T) {
266242 s = indicatorScene(t, c)
267243 for _, id := range []WindowID{2, 3} {
268244 w := sceneWindow(t, s, id)
269- p := settledPlacement(t, sc, id)
245+ p := settledOf(t, sc, id)
270246 if w.Hidden || w.Preview != overviewCardZoom || w.Rect != from[id] || w.Zoom != 1 {
271247 t.Fatalf("window %d first frame %+v, want its stash rect %+v at content scale 1 (card %+v)", id, w, from[id], p.Rect)
272248 }
@@ -275,7 +251,7 @@ func TestOverviewPileScalesFromStash(t *testing.T) {
275251 s = frame(t, c, ic, 40*time.Millisecond)
276252 for _, id := range []WindowID{2, 3} {
277253 w := sceneWindow(t, s, id)
278- p := settledPlacement(t, sc, id)
254+ p := settledOf(t, sc, id)
279255 if !(w.Rect.W < from[id].W && w.Rect.W > p.Rect.W) || !(w.Zoom > overviewCardZoom && w.Zoom < 1) {
280256 t.Fatalf("window %d mid-way %+v, want between %+v and %+v", id, w, from[id], p.Rect)
281257 }
@@ -284,7 +260,7 @@ func TestOverviewPileScalesFromStash(t *testing.T) {
284260 s = frame(t, c, ic, 5*time.Second)
285261 for _, id := range []WindowID{2, 3} {
286262 w := sceneWindow(t, s, id)
287- p := settledPlacement(t, sc, id)
263+ p := settledOf(t, sc, id)
288264 if w.Rect != p.Rect || w.Zoom != 0 || w.Fade != 0 || w.Preview != overviewCardZoom {
289265 t.Fatalf("window %d settled %+v, want the pile card %+v", id, w, p)
290266 }
@@ -399,7 +375,7 @@ func TestStashShowDuringHideDoesNotJump(t *testing.T) {
399375 t.Fatalf("fade %v then %v, want it to clear", w.Fade, n.Fade)
400376 }
401377 s = frame(t, c, ic, 5*time.Second)
402- p := settledPlacement(t, sc, 2)
378+ p := settledOf(t, sc, 2)
403379 if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect || c.animating() {
404380 t.Fatalf("settled %+v, want %+v", w, p.Rect)
405381 }
@@ -414,7 +390,7 @@ func TestStashNavigationLeavesFromSlide(t *testing.T) {
414390 indicatorScene(t, c)
415391 s := frame(t, c, ic, 30*time.Millisecond)
416392 last := sceneWindow(t, s, 2)
417- settled := settledPlacement(t, sc, 2)
393+ settled := settledOf(t, sc, 2)
418394 if last.Rect == settled.Rect {
419395 t.Fatal("setup: window 2 already at rest")
420396 }
@@ -30,21 +30,6 @@ func stashRig(t *testing.T, on bool) (*Core, *indicatorClock, *screen, chan port
30 return c, ic, sc, cmds30 return c, ic, sc, cmds
31 }31 }
32 32
33-// configuresOf drains the commands into the configures sent per window.
34-func configuresOf(cmds chan ports.ClientCommand) map[WindowID][]ports.ConfigureWindow {
35- out := map[WindowID][]ports.ConfigureWindow{}
36- for {
37- select {
38- case cmd := <-cmds:
39- if v, ok := cmd.(ports.ConfigureWindow); ok {
40- out[v.ID] = append(out[v.ID], v)
41- }
42- default:
43- return out
44- }
45- }
46-}
47-
48 // act is a bind: snapshot, action, transition.33 // act is a bind: snapshot, action, transition.
49 func act(c *Core, ic *indicatorClock, a Action) {34 func act(c *Core, ic *indicatorClock, a Action) {
50 shots := c.snapshot(ic.now)35 shots := c.snapshot(ic.now)
@@ -52,15 +37,6 @@ func act(c *Core, ic *indicatorClock, a Action) {
52 c.transition(shots, ic.now)37 c.transition(shots, ic.now)
53 }38 }
54 39
55-func settledPlacement(t *testing.T, sc *screen, id WindowID) Placement {
56- t.Helper()
57- i := slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == id })
58- if i < 0 {
59- t.Fatalf("window %d not in the settled layout", id)
60- }
61- return sc.settledLayout[i]
62-}
63-
64 // Showing the stash fades in and grows every window it brings on screen;40 // Showing the stash fades in and grows every window it brings on screen;
65 // the windows it keeps off screen stay out; the settled layout is the41 // the windows it keeps off screen stay out; the settled layout is the
66 // final one at once, with no extra configure.42 // final one at once, with no extra configure.
@@ -79,7 +55,7 @@ func TestStashShowAppears(t *testing.T) {
79 act(c, ic, ActionToggleStashVisible)55 act(c, ic, ActionToggleStashVisible)
80 s := indicatorScene(t, c)56 s := indicatorScene(t, c)
81 for _, id := range []WindowID{2, 3} {57 for _, id := range []WindowID{2, 3} {
82- p := settledPlacement(t, sc, id)58+ p := settledOf(t, sc, id)
83 w := sceneWindow(t, s, id)59 w := sceneWindow(t, s, id)
84 if w.Hidden || w.Fade != 1 || w.Rect != scaledRect(p.Rect, appearScale) || w.Zoom < 0.85 || w.Zoom > 0.95 {60 if w.Hidden || w.Fade != 1 || w.Rect != scaledRect(p.Rect, appearScale) || w.Zoom < 0.85 || w.Zoom > 0.95 {
85 t.Fatalf("window %d first frame %+v, want invisible at 90 %% of %+v", id, w, p.Rect)61 t.Fatalf("window %d first frame %+v, want invisible at 90 %% of %+v", id, w, p.Rect)
@@ -94,7 +70,7 @@ func TestStashShowAppears(t *testing.T) {
94 if len(cfgs[id]) != 1 {70 if len(cfgs[id]) != 1 {
95 t.Fatalf("window %d configured %d times, want once: %+v", id, len(cfgs[id]), cfgs[id])71 t.Fatalf("window %d configured %d times, want once: %+v", id, len(cfgs[id]), cfgs[id])
96 }72 }
97- want := c.clientRect(settledPlacement(t, sc, id))73+ want := c.clientRect(settledOf(t, sc, id))
98 if v := cfgs[id][0]; !v.Visible || v.Width != want.W || v.Height != want.H {74 if v := cfgs[id][0]; !v.Visible || v.Width != want.W || v.Height != want.H {
99 t.Fatalf("window %d configure %+v, want %dx%d (the settled client rect)", id, v, want.W, want.H)75 t.Fatalf("window %d configure %+v, want %dx%d (the settled client rect)", id, v, want.W, want.H)
100 }76 }
@@ -109,7 +85,7 @@ func TestStashShowAppears(t *testing.T) {
109 }85 }
110 s = frame(t, c, ic, 5*time.Second)86 s = frame(t, c, ic, 5*time.Second)
111 for _, id := range []WindowID{2, 3} {87 for _, id := range []WindowID{2, 3} {
112- p := settledPlacement(t, sc, id)88+ p := settledOf(t, sc, id)
113 if w := sceneWindow(t, s, id); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect {89 if w := sceneWindow(t, s, id); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect {
114 t.Fatalf("window %d settled %+v, want %+v", id, w, p.Rect)90 t.Fatalf("window %d settled %+v, want %+v", id, w, p.Rect)
115 }91 }
@@ -128,7 +104,7 @@ func TestStashHideLeaves(t *testing.T) {
128 c, ic, sc, cmds := stashRig(t, true)104 c, ic, sc, cmds := stashRig(t, true)
129 left := map[WindowID]Placement{}105 left := map[WindowID]Placement{}
130 for _, id := range []WindowID{2, 3} {106 for _, id := range []WindowID{2, 3} {
131- left[id] = settledPlacement(t, sc, id)107+ left[id] = settledOf(t, sc, id)
132 }108 }
133 act(c, ic, ActionToggleStashVisible)109 act(c, ic, ActionToggleStashVisible)
134 s := indicatorScene(t, c)110 s := indicatorScene(t, c)
@@ -266,7 +242,7 @@ func TestOverviewPileScalesFromStash(t *testing.T) {
266 s = indicatorScene(t, c)242 s = indicatorScene(t, c)
267 for _, id := range []WindowID{2, 3} {243 for _, id := range []WindowID{2, 3} {
268 w := sceneWindow(t, s, id)244 w := sceneWindow(t, s, id)
269- p := settledPlacement(t, sc, id)245+ p := settledOf(t, sc, id)
270 if w.Hidden || w.Preview != overviewCardZoom || w.Rect != from[id] || w.Zoom != 1 {246 if w.Hidden || w.Preview != overviewCardZoom || w.Rect != from[id] || w.Zoom != 1 {
271 t.Fatalf("window %d first frame %+v, want its stash rect %+v at content scale 1 (card %+v)", id, w, from[id], p.Rect)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)
272 }248 }
@@ -275,7 +251,7 @@ func TestOverviewPileScalesFromStash(t *testing.T) {
275 s = frame(t, c, ic, 40*time.Millisecond)251 s = frame(t, c, ic, 40*time.Millisecond)
276 for _, id := range []WindowID{2, 3} {252 for _, id := range []WindowID{2, 3} {
277 w := sceneWindow(t, s, id)253 w := sceneWindow(t, s, id)
278- p := settledPlacement(t, sc, id)254+ p := settledOf(t, sc, id)
279 if !(w.Rect.W < from[id].W && w.Rect.W > p.Rect.W) || !(w.Zoom > overviewCardZoom && w.Zoom < 1) {255 if !(w.Rect.W < from[id].W && w.Rect.W > p.Rect.W) || !(w.Zoom > overviewCardZoom && w.Zoom < 1) {
280 t.Fatalf("window %d mid-way %+v, want between %+v and %+v", id, w, from[id], p.Rect)256 t.Fatalf("window %d mid-way %+v, want between %+v and %+v", id, w, from[id], p.Rect)
281 }257 }
@@ -284,7 +260,7 @@ func TestOverviewPileScalesFromStash(t *testing.T) {
284 s = frame(t, c, ic, 5*time.Second)260 s = frame(t, c, ic, 5*time.Second)
285 for _, id := range []WindowID{2, 3} {261 for _, id := range []WindowID{2, 3} {
286 w := sceneWindow(t, s, id)262 w := sceneWindow(t, s, id)
287- p := settledPlacement(t, sc, id)263+ p := settledOf(t, sc, id)
288 if w.Rect != p.Rect || w.Zoom != 0 || w.Fade != 0 || w.Preview != overviewCardZoom {264 if w.Rect != p.Rect || w.Zoom != 0 || w.Fade != 0 || w.Preview != overviewCardZoom {
289 t.Fatalf("window %d settled %+v, want the pile card %+v", id, w, p)265 t.Fatalf("window %d settled %+v, want the pile card %+v", id, w, p)
290 }266 }
@@ -399,7 +375,7 @@ func TestStashShowDuringHideDoesNotJump(t *testing.T) {
399 t.Fatalf("fade %v then %v, want it to clear", w.Fade, n.Fade)375 t.Fatalf("fade %v then %v, want it to clear", w.Fade, n.Fade)
400 }376 }
401 s = frame(t, c, ic, 5*time.Second)377 s = frame(t, c, ic, 5*time.Second)
402- p := settledPlacement(t, sc, 2)378+ p := settledOf(t, sc, 2)
403 if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect || c.animating() {379 if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect || c.animating() {
404 t.Fatalf("settled %+v, want %+v", w, p.Rect)380 t.Fatalf("settled %+v, want %+v", w, p.Rect)
405 }381 }
@@ -414,7 +390,7 @@ func TestStashNavigationLeavesFromSlide(t *testing.T) {
414 indicatorScene(t, c)390 indicatorScene(t, c)
415 s := frame(t, c, ic, 30*time.Millisecond)391 s := frame(t, c, ic, 30*time.Millisecond)
416 last := sceneWindow(t, s, 2)392 last := sceneWindow(t, s, 2)
417- settled := settledPlacement(t, sc, 2)393+ settled := settledOf(t, sc, 2)
418 if last.Rect == settled.Rect {394 if last.Rect == settled.Rect {
419 t.Fatal("setup: window 2 already at rest")395 t.Fatal("setup: window 2 already at rest")
420 }396 }

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 23

Tags 10

↑↓ moveenter openesc close