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/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.PointerFocus{ID: 0}
87 if got := nextCursor(t, cursors); got != (ports.CursorChange{}) {
88 t.Fatalf("leave: %+v, want the arrow", got)
89 }
90}
91
92func TestCursorShapeNames(t *testing.T) {
93 for shape, want := range map[cursorshape.WpCursorShapeDeviceV1Shape]string{
94 cursorshape.WpCursorShapeDeviceV1ShapeDefault: "default",
95 cursorshape.WpCursorShapeDeviceV1ShapeNwseResize: "nwse-resize",
96 cursorshape.WpCursorShapeDeviceV1ShapeZoomOut: "zoom-out",
97 cursorshape.WpCursorShapeDeviceV1ShapeAllResize: "all-resize",
98 cursorshape.WpCursorShapeDeviceV1ShapeVerticalText: "vertical-text",
99 } {
100 if got := cursorShapes[shape]; got != want {
101 t.Errorf("shape %d = %q, want %q", shape, got, want)
102 }
103 }
104}
105
106func TestCursorShapeInvalid(t *testing.T) {
107 s, _, _, _, dir := cursorServer(t)
108 c := protocolClient(t, s, dir)
109 seat := bindProtocol(t, c, "wl_seat")
110 registerProtocol(t, c, seat)
111 pointer := c.AllocateID()
112 registerProtocol(t, c, pointer)
113 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
114 manager := bindProtocol(t, c, "wp_cursor_shape_manager_v1")
115 device := c.AllocateID()
116 registerProtocol(t, c, device)
117 requestProtocol(t, c, manager, cursorshape.WpCursorShapeManagerV1RequestGetPointer, device, pointer)
118 // Version 1 ends at zoom_out.
119 requestProtocol(t, c, device, cursorshape.WpCursorShapeDeviceV1RequestSetShape, uint32(1), uint32(cursorshape.WpCursorShapeDeviceV1ShapeDndAsk))
120 expectProtocolError(t, c, device, uint32(cursorshape.WpCursorShapeDeviceV1ErrorInvalidShape))
121}
122
123func TestSetCursorSurface(t *testing.T) {
124 s, events, commands, cursors, 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 w := toplevelMapper(t, c, events)()
132 commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
133 nextCursor(t, cursors)
134
135 comp := bindProtocol(t, c, "wl_compositor")
136 shm := bindProtocol(t, c, "wl_shm")
137 registerProtocol(t, c, shm)
138 fd, err := unix.MemfdCreate("cursor", unix.MFD_CLOEXEC)
139 if err != nil {
140 t.Fatal(err)
141 }
142 defer unix.Close(fd)
143 // 2x1 ARGB: opaque blue, then transparent; stride padded to 12.
144 if _, err := unix.Pwrite(fd, []byte{255, 0, 0, 255, 0, 0, 0, 0, 9, 9, 9, 9}, 0); err != nil {
145 t.Fatal(err)
146 }
147 pool, buf := c.AllocateID(), c.AllocateID()
148 registerProtocol(t, c, pool)
149 registerProtocol(t, c, buf)
150 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(12)); err != nil {
151 t.Fatal(err)
152 }
153 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(2), int32(1), int32(12), uint32(wayland.ShmFormatArgb8888))
154 surf := c.AllocateID()
155 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
156 registerProtocol(t, c, surf)
157
158 // A cursor surface without a buffer hides the cursor.
159 requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), surf, int32(1), int32(0))
160 if err := c.Roundtrip(); err != nil {
161 t.Fatal(err)
162 }
163 if got := nextCursor(t, cursors); !got.Hidden {
164 t.Fatalf("empty surface: %+v", got)
165 }
166 // A commit updates the image.
167 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
168 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
169 if err := c.Roundtrip(); err != nil {
170 t.Fatal(err)
171 }
172 got := nextCursor(t, cursors)
173 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}) {
174 t.Fatalf("image: %+v %+v", got, got.Image)
175 }
176 // A null surface hides the cursor.
177 requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), uint32(0), int32(0), int32(0))
178 if err := c.Roundtrip(); err != nil {
179 t.Fatal(err)
180 }
181 if got := nextCursor(t, cursors); !got.Hidden {
182 t.Fatalf("null surface: %+v", got)
183 }
184}
185
186func TestSetCursorRoleError(t *testing.T) {
187 s, events, _, _, dir := cursorServer(t)
188 c := protocolClient(t, s, dir)
189 seat := bindProtocol(t, c, "wl_seat")
190 registerProtocol(t, c, seat)
191 pointer := c.AllocateID()
192 registerProtocol(t, c, pointer)
193 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
194 toplevelMapper(t, c, events)()
195 // Surface 3 of the mapper's connection is the toplevel's wl_surface:
196 // find it through the server instead of guessing ids.
197 var surf uint32
198 s.display.Do(func() {
199 for _, st := range s.surfaces {
200 if st.kind == roleXDG {
201 surf = st.wl.ID()
202 }
203 }
204 })
205 requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), surf, int32(0), int32(0))
206 expectProtocolError(t, c, pointer, uint32(wayland.PointerErrorRole))
207}