1package wayland
2
3import (
4 "context"
5 "errors"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/wayland"
11 "github.com/bnema/neferwl/internal/adapters/logging"
12 "github.com/bnema/neferwl/internal/adapters/sessionsecurity"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/wlturbo"
15 "golang.org/x/sys/unix"
16)
17
18const (
19 actCopy = uint32(wayland.DataDeviceManagerDndActionCopy)
20 actMove = uint32(wayland.DataDeviceManagerDndActionMove)
21 actAsk = uint32(wayland.DataDeviceManagerDndActionAsk)
22)
23
24// dndEvent is one data device, offer or source event a test client saw.
25type dndEvent struct {
26 name string
27 mime string
28 x, y float64
29 n uint32
30}
31
32type dndDeviceEvents struct {
33 wlturbo.BaseProxy
34 c *wlturbo.Display
35 log chan dndEvent
36 // offer is the id of the latest data_offer.
37 offer atomic.Uint32
38}
39
40func (p *dndDeviceEvents) Dispatch(e *wlturbo.Event) {
41 switch e.Opcode {
42 case uint16(wayland.DataDeviceEventDataOffer):
43 o := &dndOfferEvents{log: p.log}
44 o.SetID(e.Uint32())
45 p.offer.Store(o.ID())
46 registerWireProxy(p.c, o)
47 case uint16(wayland.DataDeviceEventEnter):
48 serial, _ := e.Uint32(), e.Uint32()
49 x, y := e.Fixed().Float64(), e.Fixed().Float64()
50 p.log <- dndEvent{name: "enter", x: x, y: y, n: e.Uint32() + serial*0}
51 case uint16(wayland.DataDeviceEventLeave):
52 p.log <- dndEvent{name: "leave"}
53 case uint16(wayland.DataDeviceEventMotion):
54 _ = e.Uint32()
55 p.log <- dndEvent{name: "motion", x: e.Fixed().Float64(), y: e.Fixed().Float64()}
56 case uint16(wayland.DataDeviceEventDrop):
57 p.log <- dndEvent{name: "drop"}
58 }
59}
60
61type dndOfferEvents struct {
62 wlturbo.BaseProxy
63 log chan dndEvent
64}
65
66func (p *dndOfferEvents) Dispatch(e *wlturbo.Event) {
67 switch e.Opcode {
68 case uint16(wayland.DataOfferEventOffer):
69 p.log <- dndEvent{name: "offer", mime: e.String()}
70 case uint16(wayland.DataOfferEventSourceActions):
71 p.log <- dndEvent{name: "source_actions", n: e.Uint32()}
72 case uint16(wayland.DataOfferEventAction):
73 p.log <- dndEvent{name: "offer_action", n: e.Uint32()}
74 }
75}
76
77type dndSourceEvents struct {
78 wlturbo.BaseProxy
79 log chan dndEvent
80}
81
82func (p *dndSourceEvents) Dispatch(e *wlturbo.Event) {
83 switch e.Opcode {
84 case uint16(wayland.DataSourceEventTarget):
85 p.log <- dndEvent{name: "target", mime: e.String()}
86 case uint16(wayland.DataSourceEventSend):
87 mime := e.String()
88 fd := int(e.Fd())
89 _, _ = unix.Write(fd, []byte("dnd data"))
90 _ = unix.Close(fd)
91 p.log <- dndEvent{name: "send", mime: mime}
92 case uint16(wayland.DataSourceEventCancelled):
93 p.log <- dndEvent{name: "cancelled"}
94 case uint16(wayland.DataSourceEventDndDropPerformed):
95 p.log <- dndEvent{name: "drop_performed"}
96 case uint16(wayland.DataSourceEventDndFinished):
97 p.log <- dndEvent{name: "finished"}
98 case uint16(wayland.DataSourceEventAction):
99 p.log <- dndEvent{name: "action", n: e.Uint32()}
100 }
101}
102
103// dndClient is a client with a toplevel, a pointer and a data device.
104type dndClient struct {
105 keys chan uint32
106 c *wlturbo.Display
107 manager uint32
108 device *dndDeviceEvents
109 serials chan uint32
110 comp uint32
111 shm uint32
112 surf uint32
113 window ports.WindowID
114 log chan dndEvent
115}
116
117// keySerials records the serial of each key event.
118type keySerials struct {
119 wlturbo.BaseProxy
120 serials chan uint32
121}
122
123func (p *keySerials) Dispatch(e *wlturbo.Event) {
124 if e.Opcode == uint16(wayland.KeyboardEventKey) {
125 p.serials <- e.Uint32()
126 }
127}
128
129func newDndClient(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent) *dndClient {
130 t.Helper()
131 return newDndClientAt(t, s, dir, events, 3)
132}
133
134// newDndClientAt is newDndClient with wl_data_device_manager at version.
135func newDndClientAt(t *testing.T, s *Server, dir string, events <-chan ports.ClientEvent, version uint32) *dndClient {
136 t.Helper()
137 c := protocolClient(t, s, dir)
138 d := &dndClient{c: c, log: make(chan dndEvent, 64), serials: make(chan uint32, 8), keys: make(chan uint32, 8)}
139 kb := &keySerials{serials: d.keys}
140 seat := bindVersion(t, c, "wl_seat", 8)
141 registerProtocol(t, c, seat)
142 ptr := &pressSerials{serials: d.serials}
143 ptr.SetID(c.AllocateID())
144 registerWireProxy(c, ptr)
145 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, ptr.ID())
146 kb.SetID(c.AllocateID())
147 registerWireProxy(c, kb)
148 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, kb.ID())
149 d.manager = bindVersion(t, c, "wl_data_device_manager", version)
150 d.device = &dndDeviceEvents{c: c, log: d.log}
151 d.device.SetID(c.AllocateID())
152 registerWireProxy(c, d.device)
153 requestProtocol(t, c, d.manager, wayland.DataDeviceManagerRequestGetDataDevice, d.device.ID(), seat)
154 d.comp = bindVersion(t, c, "wl_compositor", 5)
155 d.shm = bindProtocol(t, c, "wl_shm")
156 registerProtocol(t, c, d.shm)
157 w, surf, _ := surfaceMapperAt(t, c, events, 5)()
158 d.window, d.surf = w.ID, surf
159 return d
160}
161
162// lastOffer is the id of the latest offer the client got.
163func (d *dndClient) lastOffer(t *testing.T) uint32 {
164 t.Helper()
165 id := d.device.offer.Load()
166 if id == 0 {
167 t.Fatal("no offer")
168 }
169 return id
170}
171
172// next returns the next event the client sees.
173func (d *dndClient) next(t *testing.T) dndEvent {
174 t.Helper()
175 deadline := time.After(2 * time.Second)
176 for {
177 if err := d.c.Roundtrip(); err != nil {
178 t.Fatal(err)
179 }
180 select {
181 case ev := <-d.log:
182 return ev
183 case <-deadline:
184 t.Fatal("no event")
185 case <-time.After(5 * time.Millisecond):
186 }
187 }
188}
189
190// expect waits for the next event and checks its name.
191func (d *dndClient) expect(t *testing.T, name string) dndEvent {
192 t.Helper()
193 ev := d.next(t)
194 if ev.name != name {
195 t.Fatalf("event %+v, want %s", ev, name)
196 }
197 return ev
198}
199
200// none checks that no event arrives.
201func (d *dndClient) none(t *testing.T, commands chan<- ports.ClientCommand) {
202 t.Helper()
203 // Observing a key sent through the command queue proves earlier
204 // compositor commands ran. The roundtrip then drains wire events.
205 d.key(t, commands)
206 if err := d.c.Roundtrip(); err != nil {
207 t.Fatal(err)
208 }
209 select {
210 case ev := <-d.log:
211 t.Fatalf("unexpected event %+v", ev)
212 default:
213 }
214}
215
216func (d *dndClient) source(t *testing.T, actions uint32, mimes ...string) uint32 {
217 t.Helper()
218 src := &dndSourceEvents{log: d.log}
219 src.SetID(d.c.AllocateID())
220 registerWireProxy(d.c, src)
221 requestProtocol(t, d.c, d.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID())
222 for _, m := range mimes {
223 requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestOffer, m)
224 }
225 if actions != 0 {
226 requestProtocol(t, d.c, src.ID(), wayland.DataSourceRequestSetActions, actions)
227 }
228 return src.ID()
229}
230
231// press makes the client's window press a button and returns the serial.
232func (d *dndClient) press(t *testing.T, commands chan<- ports.ClientCommand) uint32 {
233 t.Helper()
234 commands <- ports.PointerFocus{ID: d.window, X: 1, Y: 1}
235 commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Pressed: true, Grab: true, Time: time.Millisecond}
236 deadline := time.After(2 * time.Second)
237 for {
238 if err := d.c.Roundtrip(); err != nil {
239 t.Fatal(err)
240 }
241 select {
242 case serial := <-d.serials:
243 return serial
244 case <-deadline:
245 t.Fatal("no button")
246 case <-time.After(5 * time.Millisecond):
247 }
248 }
249}
250
251// press2 returns the next pointer button serial.
252func (d *dndClient) press2(t *testing.T) uint32 {
253 t.Helper()
254 deadline := time.After(2 * time.Second)
255 for {
256 if err := d.c.Roundtrip(); err != nil {
257 t.Fatal(err)
258 }
259 select {
260 case serial := <-d.serials:
261 return serial
262 case <-deadline:
263 t.Fatal("no button")
264 case <-time.After(5 * time.Millisecond):
265 }
266 }
267}
268
269// release releases the button press took and waits for the client to see it.
270func (d *dndClient) release(t *testing.T, commands chan<- ports.ClientCommand) {
271 t.Helper()
272 commands <- ports.PointerButtonTo{ID: d.window, Button: 0x110, Time: 2 * time.Millisecond}
273 d.press2(t)
274}
275
276// key presses a key on the client's focused window and returns the serial.
277func (d *dndClient) key(t *testing.T, commands chan<- ports.ClientCommand) uint32 {
278 t.Helper()
279 commands <- ports.FocusWindow{ID: d.window}
280 commands <- ports.ForwardKey{ID: d.window, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}}
281 deadline := time.After(2 * time.Second)
282 for {
283 if err := d.c.Roundtrip(); err != nil {
284 t.Fatal(err)
285 }
286 select {
287 case serial := <-d.keys:
288 return serial
289 case <-deadline:
290 t.Fatal("no key")
291 case <-time.After(5 * time.Millisecond):
292 }
293 }
294}
295
296// events returns what the client saw up to and including name.
297func (d *dndClient) until(t *testing.T, name string) []dndEvent {
298 t.Helper()
299 var got []dndEvent
300 for {
301 ev := d.next(t)
302 got = append(got, ev)
303 if ev.name == name {
304 return got
305 }
306 }
307}
308
309func (d *dndClient) startDrag(t *testing.T, src, icon, serial uint32) {
310 t.Helper()
311 d.startDragFrom(t, src, d.surf, icon, serial)
312}
313
314// startDragFrom is startDrag with an origin surface.
315func (d *dndClient) startDragFrom(t *testing.T, src, origin, icon, serial uint32) {
316 t.Helper()
317 requestProtocol(t, d.c, d.device.ID(), wayland.DataDeviceRequestStartDrag, src, origin, icon, serial)
318 if err := d.c.Roundtrip(); err != nil {
319 t.Fatal(err)
320 }
321}
322
323// buffer makes a w×h ARGB shm buffer.
324func (d *dndClient) buffer(t *testing.T, w, h int) uint32 {
325 t.Helper()
326 fd, err := unix.MemfdCreate("dnd-icon", 0)
327 if err != nil {
328 t.Fatal(err)
329 }
330 defer unix.Close(fd)
331 if err := unix.Ftruncate(fd, int64(w*h*4)); err != nil {
332 t.Fatal(err)
333 }
334 pool, buf := d.c.AllocateID(), d.c.AllocateID()
335 registerProtocol(t, d.c, pool)
336 registerProtocol(t, d.c, buf)
337 if err := wireRequest(d.c, d.shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(w*h*4)); err != nil {
338 t.Fatal(err)
339 }
340 requestProtocol(t, d.c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
341 return buf
342}
343
344func (d *dndClient) surfaceNew(t *testing.T) uint32 {
345 t.Helper()
346 id := d.c.AllocateID()
347 requestProtocol(t, d.c, d.comp, wayland.CompositorRequestCreateSurface, id)
348 registerProtocol(t, d.c, id)
349 return id
350}
351
352// dndNextEvent returns the next client event of type T, skipping others.
353func dndNextEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T {
354 t.Helper()
355 deadline := time.After(2 * time.Second)
356 for {
357 select {
358 case ev := <-events:
359 if v, ok := ev.(T); ok {
360 return v
361 }
362 case <-deadline:
363 t.Fatalf("no %T event", *new(T))
364 }
365 }
366}
367
368// dndNoEvent checks that no client event of type T is pending.
369func dndNoEvent[T ports.ClientEvent](t *testing.T, s *Server, events <-chan ports.ClientEvent) {
370 t.Helper()
371 // Callers have observed completion of the triggering request/command.
372 // Put a marker behind its emitted events, including the forwarding queue.
373 marker := ports.WindowResized{ID: ^ports.WindowID(0)}
374 if !s.display.Do(func() { s.emit(marker) }) {
375 t.Fatal("display stopped")
376 }
377 deadline := time.After(2 * time.Second)
378 for {
379 select {
380 case ev := <-events:
381 if v, ok := ev.(ports.WindowResized); ok && v.ID == marker.ID {
382 return
383 }
384 if _, ok := ev.(T); ok {
385 t.Fatalf("unexpected %T: %+v", ev, ev)
386 }
387 case <-deadline:
388 t.Fatal("no event barrier")
389 }
390 }
391}
392
393// dndPair is two clients with a drag started from a on a source.
394type dndPair struct {
395 s *Server
396 events chan ports.ClientEvent
397 commands chan ports.ClientCommand
398 a, b *dndClient
399}
400
401func newDndPair(t *testing.T) *dndPair {
402 t.Helper()
403 s, events, commands, dir := keyboardServer(t)
404 p := &dndPair{s: s, events: events, commands: commands}
405 p.a = newDndClient(t, s, dir, events)
406 p.b = newDndClient(t, s, dir, events)
407 return p
408}
409
410// start begins a drag of a source offering text/plain, copy and move.
411func (p *dndPair) start(t *testing.T, icon uint32) uint32 {
412 t.Helper()
413 src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
414 p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
415 return src
416}
417
418// enter makes b the drag target and returns the offer id.
419func (p *dndPair) enter(t *testing.T) uint32 {
420 t.Helper()
421 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond}
422 for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} {
423 if ev := p.b.next(t); ev != want {
424 t.Fatalf("event %+v, want %+v", ev, want)
425 }
426 }
427 ev := p.b.expect(t, "enter")
428 if ev.x != 5 || ev.y != 6 || ev.n == 0 {
429 t.Fatalf("enter %+v", ev)
430 }
431 return ev.n
432}
433
434// A1: a drag from a to b delivers enter, motion and drop with the offer;
435// the action is negotiated; the data reaches b; a is told every step.
436func TestDataDragBetweenClients(t *testing.T) {
437 p := newDndPair(t)
438 p.start(t, 0)
439 if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 {
440 t.Fatalf("started %+v", v)
441 }
442 offer := p.enter(t)
443
444 p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond}
445 if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 {
446 t.Fatalf("motion %+v", ev)
447 }
448
449 // b accepts and prefers move: both sides learn the action.
450 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
451 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
452 if ev := p.a.expect(t, "target"); ev.mime != "text/plain" {
453 t.Fatalf("target %+v", ev)
454 }
455 if ev := p.a.expect(t, "action"); ev.n != actMove {
456 t.Fatalf("action %+v", ev)
457 }
458 if ev := p.b.expect(t, "offer_action"); ev.n != actMove {
459 t.Fatalf("offer action %+v", ev)
460 }
461
462 // Finishing before the drop is a protocol error, so only after it.
463 p.commands <- ports.DataDragDrop{}
464 p.b.expect(t, "drop")
465 p.b.expect(t, "leave")
466 p.a.expect(t, "drop_performed")
467
468 var fds [2]int
469 if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
470 t.Fatal(err)
471 }
472 defer unix.Close(fds[0])
473 if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil {
474 t.Fatal(err)
475 }
476 _ = unix.Close(fds[1])
477 if err := p.b.c.Roundtrip(); err != nil {
478 t.Fatal(err)
479 }
480 if ev := p.a.expect(t, "send"); ev.mime != "text/plain" {
481 t.Fatalf("send %+v", ev)
482 }
483 buf := make([]byte, 16)
484 n, err := unix.Read(fds[0], buf)
485 for errors.Is(err, unix.EINTR) {
486 n, err = unix.Read(fds[0], buf)
487 }
488 if err != nil || string(buf[:n]) != "dnd data" {
489 t.Fatalf("received %q, %v", buf[:max(n, 0)], err)
490 }
491
492 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
493 p.a.expect(t, "finished")
494 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
495 p.a.none(t, p.commands)
496 p.b.none(t, p.commands)
497 // core ended the drag: wayland does not echo it.
498 dndNoEvent[ports.DataDragEnded](t, p.s, p.events)
499}
500
501// The action follows wlroots: the preferred one if both allow it, else the
502// first of copy, move, ask they share, else none.
503func TestChooseAction(t *testing.T) {
504 for _, tc := range []struct{ avail, preferred, want uint32 }{
505 {actCopy | actMove, actMove, actMove},
506 {actCopy | actMove, actAsk, actCopy},
507 {actMove | actAsk, 0, actMove},
508 {actAsk, 0, actAsk},
509 {0, actCopy, 0},
510 } {
511 if got := chooseAction(tc.avail, tc.preferred); got != tc.want {
512 t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want)
513 }
514 }
515}
516
517// A target and a source sharing no action leave the drop refused.
518func TestDataDragNoCommonAction(t *testing.T) {
519 p := newDndPair(t)
520 src := p.a.source(t, actCopy, "text/plain")
521 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
522 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
523 p.b.expect(t, "offer")
524 p.b.expect(t, "source_actions")
525 offer := p.b.expect(t, "enter").n
526 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
527 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove)
528 p.a.expect(t, "target")
529 p.commands <- ports.DataDragDrop{}
530 p.b.expect(t, "leave")
531 // The refused target no longer accepts.
532 if ev := p.a.expect(t, "target"); ev.mime != "" {
533 t.Fatalf("target %+v", ev)
534 }
535 p.a.expect(t, "cancelled")
536}
537
538// A2: a release over nothing, or over a target that accepted nothing,
539// cancels the source; the target gets leave and no drop.
540func TestDataDragRefusedDrop(t *testing.T) {
541 t.Run("nothing", func(t *testing.T) {
542 p := newDndPair(t)
543 p.start(t, 0)
544 p.commands <- ports.DataDragDrop{}
545 p.a.expect(t, "cancelled")
546 p.b.none(t, p.commands)
547 })
548 t.Run("not accepted", func(t *testing.T) {
549 p := newDndPair(t)
550 p.start(t, 0)
551 p.enter(t)
552 p.commands <- ports.DataDragDrop{}
553 p.b.expect(t, "leave")
554 p.a.expect(t, "cancelled")
555 p.b.none(t, p.commands)
556 })
557 t.Run("escape", func(t *testing.T) {
558 p := newDndPair(t)
559 p.start(t, 0)
560 p.enter(t)
561 p.commands <- ports.DataDragCancel{}
562 p.b.expect(t, "leave")
563 p.a.expect(t, "cancelled")
564 dndNoEvent[ports.DataDragEnded](t, p.s, p.events)
565 })
566}
567
568// Moving to another surface leaves the first and enters the second; a
569// surface of nobody (ID 0) leaves.
570func TestDataDragLeavesOnFocusChange(t *testing.T) {
571 p := newDndPair(t)
572 p.start(t, 0)
573 p.enter(t)
574 p.commands <- ports.DataDragFocus{}
575 p.b.expect(t, "leave")
576 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3}
577 for range 3 {
578 p.b.next(t)
579 }
580 if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 {
581 t.Fatalf("enter %+v", ev)
582 }
583}
584
585// A4: the source dying mid-drag ends it: the target gets leave and core a
586// DataDragEnded; later commands change nothing.
587func TestDataDragSourceDestroyed(t *testing.T) {
588 p := newDndPair(t)
589 src := p.start(t, 0)
590 p.enter(t)
591 requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy)
592 if err := p.a.c.Roundtrip(); err != nil {
593 t.Fatal(err)
594 }
595 p.b.expect(t, "leave")
596 dndNextEvent[ports.DataDragEnded](t, p.events)
597 p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9}
598 p.commands <- ports.DataDragDrop{}
599 p.b.none(t, p.commands)
600}
601
602// A drag whose origin client dies ends the same way.
603func TestDataDragOriginDies(t *testing.T) {
604 p := newDndPair(t)
605 p.start(t, 0)
606 p.enter(t)
607 _ = p.a.c.Close()
608 p.b.expect(t, "leave")
609 dndNextEvent[ports.DataDragEnded](t, p.events)
610}
611
612// A6: a start_drag without the current press of the origin client is
613// refused: the source is cancelled and core hears nothing.
614func TestDataDragInvalidStart(t *testing.T) {
615 t.Run("stale serial", func(t *testing.T) {
616 p := newDndPair(t)
617 serial := p.a.press(t, p.commands)
618 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1)
619 p.a.expect(t, "cancelled")
620 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
621 })
622 t.Run("other client's press", func(t *testing.T) {
623 p := newDndPair(t)
624 p.a.press(t, p.commands)
625 serial := p.b.press(t, p.commands)
626 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
627 p.a.expect(t, "cancelled")
628 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
629 })
630 t.Run("key serial", func(t *testing.T) {
631 p := newDndPair(t)
632 p.a.press(t, p.commands)
633 p.a.release(t, p.commands)
634 serial := p.a.key(t, p.commands)
635 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
636 p.a.expect(t, "cancelled")
637 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
638 })
639 t.Run("serial after the release", func(t *testing.T) {
640 p := newDndPair(t)
641 serial := p.a.press(t, p.commands)
642 p.a.release(t, p.commands)
643 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
644 p.a.expect(t, "cancelled")
645 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
646 })
647 t.Run("other surface of the client", func(t *testing.T) {
648 p := newDndPair(t)
649 serial := p.a.press(t, p.commands)
650 other := p.a.surfaceNew(t)
651 p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial)
652 p.a.expect(t, "cancelled")
653 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
654 })
655 t.Run("press of the first button", func(t *testing.T) {
656 // A second button does not make a new grab: only the first serial.
657 p := newDndPair(t)
658 first := p.a.press(t, p.commands)
659 p.commands <- ports.PointerButtonTo{ID: p.a.window, Button: 0x111, Pressed: true, Time: 2 * time.Millisecond}
660 second := p.a.press2(t)
661 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second)
662 p.a.expect(t, "cancelled")
663 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first)
664 // Core is told the grab's button: its release drops.
665 if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 {
666 t.Fatalf("drag button %#x, want the grab's", ev.Button)
667 }
668 })
669 t.Run("grab release consumed by core", func(t *testing.T) {
670 // A window drag consumes the release: the next grab press
671 // still makes the serial a drag can name.
672 p := newDndPair(t)
673 stale := p.a.press(t, p.commands)
674 serial := p.a.press(t, p.commands)
675 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale)
676 p.a.expect(t, "cancelled")
677 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
678 dndNextEvent[ports.DataDragStarted](t, p.events)
679 })
680 t.Run("grab release of a gone window", func(t *testing.T) {
681 p := newDndPair(t)
682 serial := p.a.press(t, p.commands)
683 p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond}
684 p.a.key(t, p.commands) // the release is applied once the key is seen
685 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
686 p.a.expect(t, "cancelled")
687 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
688 })
689 t.Run("used source", func(t *testing.T) {
690 p := newDndPair(t)
691 src := p.start(t, 0)
692 p.commands <- ports.DataDragCancel{}
693 p.a.expect(t, "cancelled")
694 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
695 p.a.expect(t, "cancelled")
696 })
697 t.Run("drag in progress", func(t *testing.T) {
698 p := newDndPair(t)
699 p.start(t, 0)
700 dndNextEvent[ports.DataDragStarted](t, p.events)
701 other := p.a.source(t, actCopy, "text/plain")
702 p.a.startDrag(t, other, 0, p.a.press(t, p.commands))
703 p.a.expect(t, "cancelled")
704 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
705 })
706 t.Run("icon with another role", func(t *testing.T) {
707 p := newDndPair(t)
708 serial := p.a.press(t, p.commands)
709 src := p.a.source(t, actCopy, "text/plain")
710 // The origin toplevel is a role already.
711 requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial)
712 expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole))
713 dndNoEvent[ports.DataDragStarted](t, p.s, p.events)
714 })
715}
716
717// A nil source is a drag internal to the origin client: only its own
718// surfaces see it, without offer.
719func TestDataDragWithoutSource(t *testing.T) {
720 p := newDndPair(t)
721 p.a.startDrag(t, 0, 0, p.a.press(t, p.commands))
722 dndNextEvent[ports.DataDragStarted](t, p.events)
723 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
724 p.b.none(t, p.commands)
725 p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5}
726 if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 {
727 t.Fatalf("enter %+v", ev)
728 }
729 p.commands <- ports.DataDragDrop{}
730 p.a.expect(t, "drop")
731}
732
733// Offer and source requests are validated.
734func TestDataDragProtocolErrors(t *testing.T) {
735 t.Run("finish before drop", func(t *testing.T) {
736 p := newDndPair(t)
737 p.start(t, 0)
738 offer := p.enter(t)
739 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
740 expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish))
741 })
742 t.Run("offer action mask", func(t *testing.T) {
743 p := newDndPair(t)
744 p.start(t, 0)
745 offer := p.enter(t)
746 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0))
747 expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask))
748 })
749 t.Run("offer preferred action", func(t *testing.T) {
750 p := newDndPair(t)
751 p.start(t, 0)
752 offer := p.enter(t)
753 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove)
754 expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction))
755 })
756 t.Run("source action mask", func(t *testing.T) {
757 p := newDndPair(t)
758 src := p.a.source(t, 0, "text/plain")
759 requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16))
760 expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask))
761 })
762 t.Run("selection with actions", func(t *testing.T) {
763 p := newDndPair(t)
764 src := p.a.source(t, actCopy, "text/plain")
765 requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0))
766 expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
767 })
768 t.Run("actions after the drag started", func(t *testing.T) {
769 p := newDndPair(t)
770 src := p.start(t, 0)
771 requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy)
772 expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
773 })
774}
775
776// A destroyed offer before finish cancels the source.
777func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) {
778 p := newDndPair(t)
779 p.start(t, 0)
780 offer := p.enter(t)
781 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
782 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
783 p.a.expect(t, "target")
784 p.a.expect(t, "action")
785 p.commands <- ports.DataDragDrop{}
786 p.b.expect(t, "offer_action")
787 p.b.expect(t, "drop")
788 p.b.expect(t, "leave")
789 p.a.expect(t, "drop_performed")
790 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
791 p.a.expect(t, "cancelled")
792}
793
794// iconContent reports whether the server still holds content for id.
795func iconContent(t *testing.T, s *Server, id ports.WindowID) bool {
796 t.Helper()
797 var ok bool
798 if !s.display.Do(func() { _, ok = s.latest[id] }) {
799 t.Fatal("display stopped")
800 }
801 return ok
802}
803
804// The icon is a surface with its own window ID: its size and the
805// offsets of its commits reach core, its content is published, and both go
806// when the drag ends.
807func TestDataDragIcon(t *testing.T) {
808 p := newDndPair(t)
809 icon := p.a.surfaceNew(t)
810 buf := p.a.buffer(t, 8, 6)
811 // An offset on an ordinary surface is not used.
812 requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5))
813 requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit)
814
815 src := p.a.source(t, actCopy, "text/plain")
816 p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
817 started := dndNextEvent[ports.DataDragStarted](t, p.events)
818 if started.Icon == 0 {
819 t.Fatal("no icon ID")
820 }
821 id := started.Icon
822 if iconContent(t, p.s, id) {
823 t.Fatal("content before the first commit")
824 }
825
826 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
827 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4))
828 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
829 if err := p.a.c.Roundtrip(); err != nil {
830 t.Fatal(err)
831 }
832 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) {
833 t.Fatalf("icon %+v", got)
834 }
835 if !iconContent(t, p.s, id) {
836 t.Fatal("no icon content")
837 }
838
839 // Offsets add up from commit to commit.
840 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2))
841 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
842 if err := p.a.c.Roundtrip(); err != nil {
843 t.Fatal(err)
844 }
845 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) {
846 t.Fatalf("icon %+v", got)
847 }
848 // The icon never takes input.
849 dndNoEvent[ports.InputRegionChanged](t, p.s, p.events)
850
851 p.commands <- ports.DataDragCancel{}
852 p.a.expect(t, "cancelled")
853 if iconContent(t, p.s, id) {
854 t.Fatal("icon content kept after the drag")
855 }
856}
857
858// An icon committed before start_drag shows at once.
859func TestDataDragIconCommittedEarly(t *testing.T) {
860 p := newDndPair(t)
861 icon := p.a.surfaceNew(t)
862 buf := p.a.buffer(t, 4, 4)
863 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
864 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
865 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
866 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
867 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) {
868 t.Fatalf("icon %+v", got)
869 }
870 if !iconContent(t, p.s, id) {
871 t.Fatal("no icon content")
872 }
873}
874
875// Before wl_surface v5 the icon offset rides on attach and adds up.
876func TestDataDragIconAttachOffset(t *testing.T) {
877 p := newDndPair(t)
878 comp4 := bindVersion(t, p.a.c, "wl_compositor", 4)
879 icon := p.a.c.AllocateID()
880 requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon)
881 registerProtocol(t, p.a.c, icon)
882 buf := p.a.buffer(t, 4, 4)
883 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
884 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
885 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1))
886 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
887 if err := p.a.c.Roundtrip(); err != nil {
888 t.Fatal(err)
889 }
890 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) {
891 t.Fatalf("icon %+v", got)
892 }
893}
894
895// Destroying the icon mid-drag hides it; the drag goes on.
896func TestDataDragIconDestroyed(t *testing.T) {
897 p := newDndPair(t)
898 icon := p.a.surfaceNew(t)
899 buf := p.a.buffer(t, 8, 6)
900 p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands))
901 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
902 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
903 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
904 if err := p.a.c.Roundtrip(); err != nil {
905 t.Fatal(err)
906 }
907 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 {
908 t.Fatalf("icon %+v", got)
909 }
910 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy)
911 if err := p.a.c.Roundtrip(); err != nil {
912 t.Fatal(err)
913 }
914 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) {
915 t.Fatalf("icon %+v", got)
916 }
917 if iconContent(t, p.s, id) {
918 t.Fatal("icon content kept")
919 }
920 p.enter(t)
921}
922
923// lockedPair is a pair of clients on a server that can lock the session.
924func lockedPair(t *testing.T) (*dndPair, string) {
925 t.Helper()
926 gate := &sessionsecurity.Gate{}
927 events := make(chan ports.ClientEvent, 128)
928 commands := make(chan ports.ClientCommand, 32)
929 dir := t.TempDir()
930 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Security: gate, Keymap: keymapText(t)}, Channels{Events: events, Commands: commands, SecurityChanges: make(chan ports.SecurityState, 16), SecurityEvents: make(chan ports.SecurityBackendEvent, 16)}, logging.For(context.Background(), "wayland"))
931 if err != nil {
932 t.Fatal(err)
933 }
934 ctx, cancel := context.WithCancel(context.Background())
935 done := make(chan error, 1)
936 go func() { done <- s.Run(ctx) }()
937 t.Cleanup(func() {
938 cancel()
939 select {
940 case err := <-done:
941 if err != nil {
942 t.Error(err)
943 }
944 case <-time.After(3 * time.Second):
945 t.Error("server did not stop")
946 }
947 })
948 p := &dndPair{s: s, events: events, commands: commands}
949 p.a = newDndClient(t, s, dir, events)
950 p.b = newDndClient(t, s, dir, events)
951 return p, dir
952}
953
954func lockSession(t *testing.T, s *Server, dir string) {
955 t.Helper()
956 lc := protocolClient(t, s, dir)
957 if _, err := bindLockManager(t, lc).Lock(); err != nil {
958 t.Fatal(err)
959 }
960 if err := lc.Roundtrip(); err != nil {
961 t.Fatal(err)
962 }
963}
964
965// A7: the session lock cancels a drag at once: target leave, source
966// cancelled, icon gone.
967func TestDataDragLockCancels(t *testing.T) {
968 p, dir := lockedPair(t)
969 icon := p.a.surfaceNew(t)
970 buf := p.a.buffer(t, 8, 6)
971 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
972 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
973 src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
974 p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
975 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
976 p.enter(t)
977
978 lockSession(t, p.s, dir)
979 p.b.expect(t, "leave")
980 p.a.expect(t, "cancelled")
981 if iconContent(t, p.s, id) {
982 t.Fatal("icon content kept")
983 }
984 // A new drag is refused while protected.
985 src2 := p.a.source(t, actCopy, "text/plain")
986 p.a.startDrag(t, src2, 0, uint32(1))
987 p.a.expect(t, "cancelled")
988}
989
990// GO-006: a dropped transfer waiting for its finish dies with the lock, for
991// a v3 and a pre-v3 target alike.
992func TestDataDropLockCancelsPending(t *testing.T) {
993 for _, version := range []uint32{3, 2} {
994 p, dir := lockedPair(t)
995 p.b = newDndClientAt(t, p.s, dir, p.events, version)
996 p.start(t, 0)
997 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6}
998 p.b.until(t, "enter")
999 offer := p.b.lastOffer(t)
1000 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1001 if version >= 3 {
1002 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1003 }
1004 p.a.until(t, "target")
1005 p.commands <- ports.DataDragDrop{}
1006 p.a.until(t, "drop_performed")
1007 lockSession(t, p.s, dir)
1008 p.a.expect(t, "cancelled")
1009 }
1010}
1011
1012// A subsurface of the focused tree is a valid origin.
1013func TestDataDragFromSubsurface(t *testing.T) {
1014 p := newDndPair(t)
1015 serial := p.a.press(t, p.commands)
1016 child := p.a.surfaceNew(t)
1017 subc := bindProtocol(t, p.a.c, "wl_subcompositor")
1018 requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf)
1019 p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial)
1020 dndNextEvent[ports.DataDragStarted](t, p.events)
1021}
1022
1023// GO-003: destroying the offers of the target withdraws the acceptance: the
1024// release then cancels, with no drop.
1025func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) {
1026 p := newDndPair(t)
1027 p.start(t, 0)
1028 offer := p.enter(t)
1029 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1030 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1031 p.a.expect(t, "target")
1032 p.a.expect(t, "action")
1033 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
1034 if err := p.b.c.Roundtrip(); err != nil {
1035 t.Fatal(err)
1036 }
1037 if ev := p.a.expect(t, "target"); ev.mime != "" {
1038 t.Fatalf("target %+v", ev)
1039 }
1040 if ev := p.a.expect(t, "action"); ev.n != 0 {
1041 t.Fatalf("action %+v", ev)
1042 }
1043 p.commands <- ports.DataDragDrop{}
1044 got := p.b.until(t, "leave")
1045 for _, ev := range got {
1046 if ev.name == "drop" {
1047 t.Fatalf("drop after the offer was destroyed: %+v", got)
1048 }
1049 }
1050 p.a.expect(t, "cancelled")
1051}
1052
1053// GO-004: with ask, the target picks the final action after the drop; the
1054// source hears it just before dnd_finished.
1055func TestDataDragAsk(t *testing.T) {
1056 p := newDndPair(t)
1057 src := p.a.source(t, actCopy|actMove|actAsk, "text/plain")
1058 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1059 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1060 p.b.until(t, "enter")
1061 id := p.b.lastOffer(t)
1062 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1063 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1064 p.a.expect(t, "target")
1065 if ev := p.a.expect(t, "action"); ev.n != actAsk {
1066 t.Fatalf("action %+v", ev)
1067 }
1068 p.commands <- ports.DataDragDrop{}
1069 p.b.until(t, "leave")
1070 p.a.expect(t, "drop_performed")
1071
1072 // Finishing while the action is ask is an error; the choice comes first.
1073 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain")
1074 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1075 p.a.expect(t, "target")
1076 p.b.none(t, p.commands) // no offer.action after the drop
1077 p.a.none(t, p.commands) // the final action waits for the finish
1078 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish)
1079 if ev := p.a.expect(t, "action"); ev.n != actMove {
1080 t.Fatalf("action %+v", ev)
1081 }
1082 p.a.expect(t, "finished")
1083}
1084
1085// A final preferred action outside the source's is an error.
1086func TestDataDragAskFinalActionNotInSource(t *testing.T) {
1087 p := newDndPair(t)
1088 src := p.a.source(t, actCopy|actAsk, "text/plain")
1089 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1090 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1091 p.b.until(t, "enter")
1092 id := p.b.lastOffer(t)
1093 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1094 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1095 p.a.expect(t, "target")
1096 p.a.expect(t, "action")
1097 p.commands <- ports.DataDragDrop{}
1098 p.b.until(t, "leave")
1099 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove)
1100 expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction))
1101}
1102
1103// GO-005: a pre-v3 target copies: a source that does not allow copy is
1104// cancelled, one that does gets it.
1105func TestDataDragLegacyTarget(t *testing.T) {
1106 setup := func(t *testing.T, actions uint32) (*dndPair, uint32) {
1107 s, events, commands, dir := keyboardServer(t)
1108 p := &dndPair{s: s, events: events, commands: commands}
1109 p.a = newDndClient(t, s, dir, events)
1110 p.b = newDndClientAt(t, s, dir, events, 2)
1111 src := p.a.source(t, actions, "text/plain")
1112 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1113 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1114 p.b.until(t, "enter")
1115 id := p.b.lastOffer(t)
1116 // Legacy accept is feedback only: deliberately never send it.
1117 return p, id
1118 }
1119 t.Run("move only", func(t *testing.T) {
1120 p, _ := setup(t, actMove)
1121 p.commands <- ports.DataDragDrop{}
1122 got := p.b.until(t, "leave")
1123 for _, ev := range got {
1124 if ev.name == "drop" {
1125 t.Fatalf("drop for a source without copy: %+v", got)
1126 }
1127 }
1128 p.a.until(t, "cancelled")
1129 })
1130 t.Run("copy", func(t *testing.T) {
1131 p, id := setup(t, actCopy|actMove)
1132 p.commands <- ports.DataDragDrop{}
1133 p.b.expect(t, "drop")
1134 p.b.expect(t, "leave")
1135 if ev := p.a.expect(t, "action"); ev.n != actCopy {
1136 t.Fatalf("action %+v", ev)
1137 }
1138 p.a.expect(t, "drop_performed")
1139 // GO-006: destroying the last offer finishes the transfer.
1140 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy)
1141 p.a.expect(t, "finished")
1142 })
1143}
1144
1145// GO-007: of several offset requests before one commit, the last counts.
1146func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) {
1147 p := newDndPair(t)
1148 icon := p.a.surfaceNew(t)
1149 buf := p.a.buffer(t, 8, 6)
1150 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
1151 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
1152 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1153 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1))
1154 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7))
1155 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
1156 if err := p.a.c.Roundtrip(); err != nil {
1157 t.Fatal(err)
1158 }
1159 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) {
1160 t.Fatalf("icon %+v", got)
1161 }
1162}
1163
1164func TestDataDragLastTargetDeviceReleased(t *testing.T) {
1165 p := newDndPair(t)
1166 p.start(t, 0)
1167 offer := p.enter(t)
1168 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1169 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1170 p.a.expect(t, "target")
1171 p.a.expect(t, "action")
1172 p.b.expect(t, "offer_action")
1173 requestProtocol(t, p.b.c, p.b.device.ID(), wayland.DataDeviceRequestRelease)
1174 if err := p.b.c.Roundtrip(); err != nil {
1175 t.Fatal(err)
1176 }
1177 p.commands <- ports.DataDragDrop{}
1178 if ev := p.a.expect(t, "target"); ev.mime != "" {
1179 t.Fatalf("target %+v", ev)
1180 }
1181 if ev := p.a.expect(t, "action"); ev.n != 0 {
1182 t.Fatalf("action %+v", ev)
1183 }
1184 p.a.expect(t, "cancelled")
1185 // The offer was deliberately kept alive; it must not keep a pending drop.
1186 if !p.s.display.Do(func() {
1187 if len(p.s.dndDrops) != 0 {
1188 t.Error("pending drop without a device")
1189 }
1190 }) {
1191 t.Fatal("display stopped")
1192 }
1193}
1194
1195func TestDataDragMultipleOffersShareAction(t *testing.T) {
1196 for _, name := range []string{"finish", "acceptor destroyed"} {
1197 t.Run(name, func(t *testing.T) {
1198 p := newDndPair(t)
1199 seat := bindVersion(t, p.b.c, "wl_seat", 8)
1200 second := &dndDeviceEvents{c: p.b.c, log: make(chan dndEvent, 64)}
1201 second.SetID(p.b.c.AllocateID())
1202 registerWireProxy(p.b.c, second)
1203 requestProtocol(t, p.b.c, p.b.manager, wayland.DataDeviceManagerRequestGetDataDevice, second.ID(), seat)
1204 if err := p.b.c.Roundtrip(); err != nil {
1205 t.Fatal(err)
1206 }
1207 p.start(t, 0)
1208 first := p.enter(t)
1209 other := &dndClient{c: p.b.c, device: second, log: second.log}
1210 other.until(t, "enter")
1211 two := other.lastOffer(t)
1212 requestProtocol(t, p.b.c, first, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1213 requestProtocol(t, p.b.c, first, wayland.DataOfferRequestSetActions, actCopy|actMove, actCopy)
1214 p.a.expect(t, "target")
1215 p.a.expect(t, "action")
1216 for _, d := range []*dndClient{p.b, other} {
1217 if ev := d.expect(t, "offer_action"); ev.n != actCopy {
1218 t.Fatalf("copy: %+v", ev)
1219 }
1220 }
1221 requestProtocol(t, p.b.c, two, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1222 if ev := p.a.expect(t, "action"); ev.n != actMove {
1223 t.Fatalf("source action %+v", ev)
1224 }
1225 for _, d := range []*dndClient{p.b, other} {
1226 if ev := d.expect(t, "offer_action"); ev.n != actMove {
1227 t.Fatalf("move: %+v", ev)
1228 }
1229 }
1230 if name == "finish" {
1231 p.commands <- ports.DataDragDrop{}
1232 for _, d := range []*dndClient{p.b, other} {
1233 d.expect(t, "drop")
1234 d.expect(t, "leave")
1235 }
1236 p.a.expect(t, "drop_performed")
1237 // Either offer finishes the same negotiated move transfer.
1238 requestProtocol(t, p.b.c, two, wayland.DataOfferRequestFinish)
1239 p.a.expect(t, "finished")
1240 return
1241 }
1242 requestProtocol(t, p.b.c, first, wayland.DataOfferRequestDestroy)
1243 if err := p.b.c.Roundtrip(); err != nil {
1244 t.Fatal(err)
1245 }
1246 if ev := p.a.expect(t, "target"); ev.mime != "" {
1247 t.Fatalf("stale acceptance %+v", ev)
1248 }
1249 if ev := p.a.expect(t, "action"); ev.n != 0 {
1250 t.Fatalf("stale action %+v", ev)
1251 }
1252 if ev := other.expect(t, "offer_action"); ev.n != 0 {
1253 t.Fatalf("remaining offer %+v", ev)
1254 }
1255 p.commands <- ports.DataDragDrop{}
1256 p.a.expect(t, "cancelled")
1257 other.expect(t, "leave")
1258 })
1259 }
1260}