1package wayland
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/go-wayland-bindings/server/cursorshape"
9 "github.com/bnema/go-wayland-bindings/server/wayland"
10 "github.com/bnema/neferwl/internal/adapters/logging"
11 "github.com/bnema/neferwl/internal/ports"
12 "golang.org/x/sys/unix"
13)
14
15func cursorServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.CursorChange, string) {
16 t.Helper()
17 dir := t.TempDir()
18 events := make(chan ports.ClientEvent, 16)
19 commands := make(chan ports.ClientCommand, 16)
20 cursors := make(chan ports.CursorChange, 16)
21 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Cursors: cursors}, logging.For(context.Background(), "wayland"))
22 if err != nil {
23 t.Fatal(err)
24 }
25 ctx, cancel := context.WithCancel(context.Background())
26 done := make(chan error, 1)
27 go func() { done <- s.Run(ctx) }()
28 t.Cleanup(func() { cancel(); <-done })
29 return s, events, commands, cursors, dir
30}
31
32// nextCursor returns the latest cursor change sent within a short wait.
33func nextCursor(t *testing.T, cursors <-chan ports.CursorChange) ports.CursorChange {
34 t.Helper()
35 var c ports.CursorChange
36 select {
37 case c = <-cursors:
38 case <-time.After(2 * time.Second):
39 t.Fatal("no cursor change")
40 }
41 for {
42 select {
43 case c = <-cursors:
44 case <-time.After(50 * time.Millisecond):
45 return c
46 }
47 }
48}
49
50func TestCursorShapeFollowsPointerFocus(t *testing.T) {
51 s, events, commands, cursors, dir := cursorServer(t)
52 c := protocolClient(t, s, dir)
53 seat := bindProtocol(t, c, "wl_seat")
54 registerProtocol(t, c, seat)
55 pointer := c.AllocateID()
56 registerProtocol(t, c, pointer)
57 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
58 manager := bindProtocol(t, c, "wp_cursor_shape_manager_v1")
59 device := c.AllocateID()
60 registerProtocol(t, c, device)
61 requestProtocol(t, c, manager, cursorshape.WpCursorShapeManagerV1RequestGetPointer, device, pointer)
62 w := toplevelMapper(t, c, events)()
63
64 // Without pointer focus the request is ignored.
65 requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(1), uint32(cursorshape.WpCursorShapeDeviceV1ShapeText))
66 if err := c.Roundtrip(); err != nil {
67 t.Fatal(err)
68 }
69 select {
70 case got := <-cursors:
71 t.Fatalf("cursor change without focus: %+v", got)
72 case <-time.After(50 * time.Millisecond):
73 }
74
75 commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
76 if got := nextCursor(t, cursors); got != (ports.CursorChange{}) {
77 t.Fatalf("enter: %+v, want the arrow", got)
78 }
79 requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(2), uint32(cursorshape.WpCursorShapeDeviceV1ShapePointer))
80 if err := c.Roundtrip(); err != nil {
81 t.Fatal(err)
82 }
83 if got := nextCursor(t, cursors); got.Shape != "pointer" {
84 t.Fatalf("shape: %+v", got)
85 }
86 commands <- ports.CursorOverride{Shape: "ew-resize"}
87 if got := nextCursor(t, cursors); got != (ports.CursorChange{Shape: "ew-resize"}) {
88 t.Fatalf("override: %+v", got)
89 }
90 requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(3), uint32(cursorshape.WpCursorShapeDeviceV1ShapeText))
91 if err := c.Roundtrip(); err != nil {
92 t.Fatal(err)
93 }
94 select {
95 case got := <-cursors:
96 t.Fatalf("client cursor forwarded during override: %+v", got)
97 case <-time.After(50 * time.Millisecond):
98 }
99 commands <- ports.CursorOverride{}
100 if got := nextCursor(t, cursors); got != (ports.CursorChange{Shape: "text"}) {
101 t.Fatalf("restored client cursor: %+v", got)
102 }
103 commands <- ports.PointerFocus{ID: 0}
104 if got := nextCursor(t, cursors); got != (ports.CursorChange{}) {
105 t.Fatalf("leave: %+v, want the arrow", got)
106 }
107}
108
109func TestCursorShapeNames(t *testing.T) {
110 for shape, want := range map[cursorshape.WpCursorShapeDeviceV1Shape]string{
111 cursorshape.WpCursorShapeDeviceV1ShapeDefault: "default",
112 cursorshape.WpCursorShapeDeviceV1ShapeNwseResize: "nwse-resize",
113 cursorshape.WpCursorShapeDeviceV1ShapeZoomOut: "zoom-out",
114 cursorshape.WpCursorShapeDeviceV1ShapeAllResize: "all-resize",
115 cursorshape.WpCursorShapeDeviceV1ShapeVerticalText: "vertical-text",
116 } {
117 if got := cursorShapes[shape]; got != want {
118 t.Errorf("shape %d = %q, want %q", shape, got, want)
119 }
120 }
121}
122
123func TestCursorShapeInvalid(t *testing.T) {
124 s, _, _, _, dir := cursorServer(t)
125 c := protocolClient(t, s, dir)
126 seat := bindProtocol(t, c, "wl_seat")
127 registerProtocol(t, c, seat)
128 pointer := c.AllocateID()
129 registerProtocol(t, c, pointer)
130 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
131 manager := bindProtocol(t, c, "wp_cursor_shape_manager_v1")
132 device := c.AllocateID()
133 registerProtocol(t, c, device)
134 requestProtocol(t, c, manager, cursorshape.WpCursorShapeManagerV1RequestGetPointer, device, pointer)
135 // Version 1 ends at zoom_out.
136 requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(1), uint32(cursorshape.WpCursorShapeDeviceV1ShapeDndAsk))
137 expectProtocolError(t, c, device, uint32(cursorshape.WpCursorShapeDeviceV1ErrorInvalidShape))
138}
139
140func TestSetCursorSurface(t *testing.T) {
141 s, events, commands, cursors, dir := cursorServer(t)
142 c := protocolClient(t, s, dir)
143 seat := bindProtocol(t, c, "wl_seat")
144 registerProtocol(t, c, seat)
145 pointer := c.AllocateID()
146 registerProtocol(t, c, pointer)
147 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
148 w := toplevelMapper(t, c, events)()
149 commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
150 nextCursor(t, cursors)
151
152 comp := bindProtocol(t, c, "wl_compositor")
153 shm := bindProtocol(t, c, "wl_shm")
154 registerProtocol(t, c, shm)
155 fd, err := unix.MemfdCreate("cursor", unix.MFD_CLOEXEC)
156 if err != nil {
157 t.Fatal(err)
158 }
159 defer unix.Close(fd)
160 // 2x1 ARGB: opaque blue, then transparent; stride padded to 12.
161 if _, err := unix.Pwrite(fd, []byte{255, 0, 0, 255, 0, 0, 0, 0, 9, 9, 9, 9}, 0); err != nil {
162 t.Fatal(err)
163 }
164 pool, buf := c.AllocateID(), c.AllocateID()
165 registerProtocol(t, c, pool)
166 registerProtocol(t, c, buf)
167 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(12)); err != nil {
168 t.Fatal(err)
169 }
170 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(2), int32(1), int32(12), uint32(wayland.ShmFormatArgb8888))
171 surf := c.AllocateID()
172 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
173 registerProtocol(t, c, surf)
174
175 // A cursor surface without a buffer hides the cursor.
176 requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), surf, int32(1), int32(0))
177 if err := c.Roundtrip(); err != nil {
178 t.Fatal(err)
179 }
180 if got := nextCursor(t, cursors); !got.Hidden {
181 t.Fatalf("empty surface: %+v", got)
182 }
183 // A commit updates the image.
184 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
185 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
186 if err := c.Roundtrip(); err != nil {
187 t.Fatal(err)
188 }
189 got := nextCursor(t, cursors)
190 if got.Image == nil || got.Scale != 1 || got.Image.W != 2 || got.Image.H != 1 || got.Image.HotX != 1 || string(got.Image.Pixels) != string([]byte{255, 0, 0, 255, 0, 0, 0, 0}) {
191 t.Fatalf("image: %+v %+v", got, got.Image)
192 }
193 // A null surface hides the cursor.
194 requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), uint32(0), int32(0), int32(0))
195 if err := c.Roundtrip(); err != nil {
196 t.Fatal(err)
197 }
198 if got := nextCursor(t, cursors); !got.Hidden {
199 t.Fatalf("null surface: %+v", got)
200 }
201}
202
203func TestSetCursorRoleError(t *testing.T) {
204 s, events, _, _, dir := cursorServer(t)
205 c := protocolClient(t, s, dir)
206 seat := bindProtocol(t, c, "wl_seat")
207 registerProtocol(t, c, seat)
208 pointer := c.AllocateID()
209 registerProtocol(t, c, pointer)
210 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
211 toplevelMapper(t, c, events)()
212 // Surface 3 of the mapper's connection is the toplevel's wl_surface:
213 // find it through the server instead of guessing ids.
214 var surf uint32
215 s.display.Do(func() {
216 for _, st := range s.surfaces {
217 if st.kind == roleXDG {
218 surf = st.wl.ID()
219 }
220 }
221 })
222 requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), surf, int32(0), int32(0))
223 expectProtocolError(t, c, pointer, uint32(wayland.PointerErrorRole))
224}
225
226func TestCursorOverrideProtection(t *testing.T) {
227 s, _, commands, cursors, _ := cursorServer(t)
228 state := installSecurity(t, s)
229 commands <- ports.CursorOverride{Shape: "ns-resize"}
230 if got := nextCursor(t, cursors); got.Shape != "ns-resize" {
231 t.Fatalf("override: %+v", got)
232 }
233 protect(t, s, state, 1, true)
234 if !s.display.Do(func() {
235 s.resetProtectionInput()
236 s.apply(ports.SecurityCommand{State: *state.Load(), Command: ports.CursorOverride{Shape: "ew-resize"}})
237 if s.cursorOverride != "" {
238 t.Errorf("protected override: %q", s.cursorOverride)
239 }
240 }) {
241 t.Fatal("display stopped")
242 }
243 select {
244 case got := <-cursors:
245 t.Fatalf("protected cursor forwarded: %+v", got)
246 case <-time.After(50 * time.Millisecond):
247 }
248 protect(t, s, state, 2, false)
249 if !s.display.Do(func() { s.setCursor(ports.CursorChange{Hidden: true}) }) {
250 t.Fatal("display stopped")
251 }
252 if got := nextCursor(t, cursors); got != (ports.CursorChange{Hidden: true}) {
253 t.Fatalf("override survived protection: %+v", got)
254 }
255}