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 r := startMulti(t, nil, left, right)
53 r.mapWindow(t, 1)
54 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
55 r.mapWindow(t, 2)
56 r.mapWindow(t, 3)
57 set := r.key(t, "o", ports.ModAlt)
58 p, ok := scenePreview(outputScene(t, set, "DP-2"), 2)
59 if !ok {
60 t.Fatal("no preview of window 2 on DP-2")
61 }
62 for len(r.commands) > 0 {
63 <-r.commands
64 }
65 r.input <- ports.PointerMotion{X: float64(left.Width + p.Rect.X + p.Rect.W/2), Y: float64(p.Rect.Y + p.Rect.H/2)}
66 r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
67 deadline := time.After(time.Second)
68 for picked := false; !picked; {
69 select {
70 case set := <-r.scenes:
71 s := outputScene(t, set, "DP-2")
72 for _, w := range s.Windows {
73 if w.ID == 2 && w.Preview == 0 && w.Focused && !w.Hidden {
74 picked = true
75 }
76 }
77 case <-deadline:
78 t.Fatal("window 2 not picked")
79 }
80 }
81 noButtonSent(t, r.commands)
82}
83
84func TestOverviewCoreClickOutsidePreviewKeepsOverview(t *testing.T) {
85 r := startMulti(t, nil, right)
86 r.mapWindow(t, 1)
87 r.mapWindow(t, 2)
88 set := r.key(t, "o", ports.ModAlt)
89 for _, w := range set[0].Windows {
90 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 {
91 t.Fatalf("(1, 1) is inside preview %+v", w)
92 }
93 }
94 for len(r.commands) > 0 {
95 <-r.commands
96 }
97 r.input <- ports.PointerMotion{X: 1, Y: 1}
98 r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
99 r.input <- ports.PointerButton{Button: 0x110}
100 // h moves the selection in the overview; it would be a plain key
101 // otherwise, with no scene.
102 r.input <- ports.KeyEvent{Keysym: "h", Pressed: true}
103 select {
104 case s := <-r.scenes:
105 if !hasPreview(s[0]) {
106 t.Fatal("overview closed by a click outside the previews")
107 }
108 case <-time.After(time.Second):
109 t.Fatal("overview closed by a click outside the previews: h published nothing")
110 }
111 noButtonSent(t, r.commands)
112}
113
114func TestOverviewCoreEscapeRestoresFocus(t *testing.T) {
115 r := startMulti(t, nil, right)
116 r.mapWindow(t, 1)
117 r.mapWindow(t, 2)
118 r.mapWindow(t, 3)
119 r.key(t, "o", ports.ModAlt)
120 r.key(t, "h", 0)
121 set := r.key(t, "h", 0)
122 if p, ok := scenePreview(set[0], 1); !ok || !p.Focused {
123 t.Fatalf("preview 1 not selected: %+v", set[0].Windows)
124 }
125 set = r.key(t, "Escape", 0)
126 if hasPreview(set[0]) {
127 t.Fatal("overview still open")
128 }
129 for _, w := range set[0].Windows {
130 if w.Focused != (w.ID == 3) {
131 t.Fatalf("focus: %+v", w)
132 }
133 }
134}
135
136// The default move bind moves the selected preview and keeps the overview
137// open, through the same path as any key.
138func TestOverviewCoreMoveBind(t *testing.T) {
139 r := startMulti(t, nil, right)
140 r.mapWindow(t, 1)
141 r.mapWindow(t, 2)
142 r.mapWindow(t, 3)
143 r.key(t, "o", ports.ModAlt)
144 r.key(t, "h", 0)
145 if set := r.key(t, "Next", ports.ModAlt|ports.ModShift); !hasPreview(set[0]) {
146 t.Fatal("overview closed")
147 }
148 r.key(t, "Escape", 0)
149 // The state channel keeps the latest snapshot: wait for the one after
150 // Escape, back on window 3 of workspace 1. The overview showed window
151 // 2 in the row below; closed, it is off screen on workspace 2.
152 deadline := time.After(time.Second)
153 for {
154 select {
155 case st := <-r.state:
156 if st.Window == nil || st.Window.ID != 3 || st.Outputs[0].Active != 1 {
157 continue
158 }
159 for _, w := range st.Windows {
160 if w.ID == 2 && w.Workspace == 2 && !w.Visible {
161 return
162 }
163 }
164 case <-deadline:
165 t.Fatal("window 2 not on workspace 2 after escape")
166 }
167 }
168}