1package core_test
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// outputScene returns the scene of the named output in set.
11func outputScene(t *testing.T, set []ports.Scene, name string) ports.Scene {
12 t.Helper()
13 for _, s := range set {
14 if s.Output == name {
15 return s
16 }
17 }
18 t.Fatalf("no scene for %s", name)
19 return ports.Scene{}
20}
21
22// scenePreview returns window id's overview preview in s.
23func scenePreview(s ports.Scene, id ports.WindowID) (ports.SceneWindow, bool) {
24 for _, w := range s.Windows {
25 if w.ID == id && w.Preview > 0 {
26 return w, true
27 }
28 }
29 return ports.SceneWindow{}, false
30}
31
32func hasPreview(s ports.Scene) bool {
33 for _, w := range s.Windows {
34 if w.Preview > 0 {
35 return true
36 }
37 }
38 return false
39}
40
41// noButtonSent fails if core forwarded a click to a client.
42func noButtonSent(t *testing.T, ch chan ports.ClientCommand) {
43 t.Helper()
44 for len(ch) > 0 {
45 if v, ok := (<-ch).(ports.PointerButtonTo); ok {
46 t.Fatalf("click reached a client: %+v", v)
47 }
48 }
49}
50
51func TestOverviewCoreClickPicksOnPointerOutput(t *testing.T) {
52 both(t, func(t *testing.T, animated bool) {
53 overviewClickPicksOnPointerOutput(t, startLanding(t, animated, nil, left, right))
54 })
55}
56
57func overviewClickPicksOnPointerOutput(t *testing.T, r *landRig) {
58 r.mapLanded(t, 1)
59 r.keyLanded(t, "Right", ports.ModAlt|ports.ModCtrl)
60 r.mapLanded(t, 2)
61 r.mapLanded(t, 3)
62 // Settled: a card still fading in takes no click.
63 set := r.keyLanded(t, "o", ports.ModAlt)
64 p, ok := scenePreview(outputScene(t, set, "DP-2"), 2)
65 if !ok {
66 t.Fatal("no preview of window 2 on DP-2")
67 }
68 for len(r.commands) > 0 {
69 <-r.commands
70 }
71 r.input <- ports.PointerMotion{X: float64(left.Width + p.Rect.X + p.Rect.W/2), Y: float64(p.Rect.Y + p.Rect.H/2)}
72 r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
73 deadline := time.After(time.Second)
74 for picked := false; !picked; {
75 select {
76 case set := <-r.scenes:
77 s := outputScene(t, set, "DP-2")
78 for _, w := range s.Windows {
79 if w.ID == 2 && w.Preview == 0 && w.Focused && !w.Hidden {
80 picked = true
81 }
82 }
83 case <-deadline:
84 t.Fatal("window 2 not picked")
85 }
86 }
87 noButtonSent(t, r.commands)
88}
89
90func TestOverviewCoreClickOutsidePreviewKeepsOverview(t *testing.T) {
91 both(t, func(t *testing.T, animated bool) {
92 overviewClickOutsidePreview(t, startLanding(t, animated, nil, right))
93 })
94}
95
96func overviewClickOutsidePreview(t *testing.T, r *landRig) {
97 r.mapWindow(t, 1)
98 r.mapWindow(t, 2)
99 // The cards settle where the overview lays them out: the opening
100 // animation starts them at the windows' own rects.
101 set := r.keyLanded(t, "o", ports.ModAlt)
102 for _, w := range set[0].Windows {
103 if w.Preview > 0 && w.Rect.X <= 1 && w.Rect.Y <= 1 && w.Rect.X+w.Rect.W > 1 && w.Rect.Y+w.Rect.H > 1 {
104 t.Fatalf("(1, 1) is inside preview %+v", w)
105 }
106 }
107 for len(r.commands) > 0 {
108 <-r.commands
109 }
110 r.input <- ports.PointerMotion{X: 1, Y: 1}
111 r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
112 r.input <- ports.PointerButton{Button: 0x110}
113 // h moves the selection in the overview; it would be a plain key
114 // otherwise, with no scene.
115 r.input <- ports.KeyEvent{Keysym: "h", Pressed: true}
116 select {
117 case s := <-r.scenes:
118 if !hasPreview(s[0]) {
119 t.Fatal("overview closed by a click outside the previews")
120 }
121 case <-time.After(time.Second):
122 t.Fatal("overview closed by a click outside the previews: h published nothing")
123 }
124 noButtonSent(t, r.commands)
125}
126
127func TestOverviewCoreEscapeRestoresFocus(t *testing.T) {
128 r := startMulti(t, nil, right)
129 r.mapWindow(t, 1)
130 r.mapWindow(t, 2)
131 r.mapWindow(t, 3)
132 r.key(t, "o", ports.ModAlt)
133 r.key(t, "h", 0)
134 set := r.key(t, "h", 0)
135 if p, ok := scenePreview(set[0], 1); !ok || !p.Focused {
136 t.Fatalf("preview 1 not selected: %+v", set[0].Windows)
137 }
138 set = r.key(t, "Escape", 0)
139 if hasPreview(set[0]) {
140 t.Fatal("overview still open")
141 }
142 for _, w := range set[0].Windows {
143 if w.Focused != (w.ID == 3) {
144 t.Fatalf("focus: %+v", w)
145 }
146 }
147}
148
149// The default move bind moves the selected preview and keeps the overview
150// open, through the same path as any key.
151func TestOverviewCoreMoveBind(t *testing.T) {
152 r := startMulti(t, nil, right)
153 r.mapWindow(t, 1)
154 r.mapWindow(t, 2)
155 r.mapWindow(t, 3)
156 r.key(t, "o", ports.ModAlt)
157 r.key(t, "h", 0)
158 if set := r.key(t, "Next", ports.ModAlt|ports.ModShift); !hasPreview(set[0]) {
159 t.Fatal("overview closed")
160 }
161 r.key(t, "Escape", 0)
162 // The state channel keeps the latest snapshot: wait for the one after
163 // Escape, back on window 3 of workspace 1. The overview showed window
164 // 2 in the row below; closed, it is off screen on workspace 2.
165 deadline := time.After(time.Second)
166 for {
167 select {
168 case st := <-r.state:
169 if st.Window == nil || st.Window.ID != 3 || st.Outputs[0].Active != 1 {
170 continue
171 }
172 for _, w := range st.Windows {
173 if w.ID == 2 && w.Workspace == 2 && !w.Visible {
174 return
175 }
176 }
177 case <-deadline:
178 t.Fatal("window 2 not on workspace 2 after escape")
179 }
180 }
181}