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) {
202 t.Helper()
203 for range 2 {
204 if err := d.c.Roundtrip(); err != nil {
205 t.Fatal(err)
206 }
207 time.Sleep(30 * time.Millisecond)
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, events <-chan ports.ClientEvent) {
370 t.Helper()
371 time.Sleep(50 * time.Millisecond)
372 for {
373 select {
374 case ev := <-events:
375 if _, ok := ev.(T); ok {
376 t.Fatalf("unexpected %T: %+v", ev, ev)
377 }
378 default:
379 return
380 }
381 }
382}
383
384// dndPair is two clients with a drag started from a on a source.
385type dndPair struct {
386 s *Server
387 events chan ports.ClientEvent
388 commands chan ports.ClientCommand
389 a, b *dndClient
390}
391
392func newDndPair(t *testing.T) *dndPair {
393 t.Helper()
394 s, events, commands, dir := keyboardServer(t)
395 p := &dndPair{s: s, events: events, commands: commands}
396 p.a = newDndClient(t, s, dir, events)
397 p.b = newDndClient(t, s, dir, events)
398 return p
399}
400
401// start begins a drag of a source offering text/plain, copy and move.
402func (p *dndPair) start(t *testing.T, icon uint32) uint32 {
403 t.Helper()
404 src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
405 p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
406 return src
407}
408
409// enter makes b the drag target and returns the offer id.
410func (p *dndPair) enter(t *testing.T) uint32 {
411 t.Helper()
412 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6, Time: 10 * time.Millisecond}
413 for _, want := range []dndEvent{{name: "offer", mime: "text/plain"}, {name: "offer", mime: "text/uri-list"}, {name: "source_actions", n: actCopy | actMove}} {
414 if ev := p.b.next(t); ev != want {
415 t.Fatalf("event %+v, want %+v", ev, want)
416 }
417 }
418 ev := p.b.expect(t, "enter")
419 if ev.x != 5 || ev.y != 6 || ev.n == 0 {
420 t.Fatalf("enter %+v", ev)
421 }
422 return ev.n
423}
424
425// A1: a drag from a to b delivers enter, motion and drop with the offer;
426// the action is negotiated; the data reaches b; a is told every step.
427func TestDataDragBetweenClients(t *testing.T) {
428 p := newDndPair(t)
429 p.start(t, 0)
430 if v := dndNextEvent[ports.DataDragStarted](t, p.events); v.Icon != 0 {
431 t.Fatalf("started %+v", v)
432 }
433 offer := p.enter(t)
434
435 p.commands <- ports.DataDragMotion{ID: p.b.window, X: 7, Y: 8, Time: 20 * time.Millisecond}
436 if ev := p.b.expect(t, "motion"); ev.x != 7 || ev.y != 8 {
437 t.Fatalf("motion %+v", ev)
438 }
439
440 // b accepts and prefers move: both sides learn the action.
441 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
442 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
443 if ev := p.a.expect(t, "target"); ev.mime != "text/plain" {
444 t.Fatalf("target %+v", ev)
445 }
446 if ev := p.a.expect(t, "action"); ev.n != actMove {
447 t.Fatalf("action %+v", ev)
448 }
449 if ev := p.b.expect(t, "offer_action"); ev.n != actMove {
450 t.Fatalf("offer action %+v", ev)
451 }
452
453 // Finishing before the drop is a protocol error, so only after it.
454 p.commands <- ports.DataDragDrop{}
455 p.b.expect(t, "drop")
456 p.b.expect(t, "leave")
457 p.a.expect(t, "drop_performed")
458
459 var fds [2]int
460 if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
461 t.Fatal(err)
462 }
463 defer unix.Close(fds[0])
464 if err := wireRequest(p.b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil {
465 t.Fatal(err)
466 }
467 _ = unix.Close(fds[1])
468 if err := p.b.c.Roundtrip(); err != nil {
469 t.Fatal(err)
470 }
471 if ev := p.a.expect(t, "send"); ev.mime != "text/plain" {
472 t.Fatalf("send %+v", ev)
473 }
474 buf := make([]byte, 16)
475 n, err := unix.Read(fds[0], buf)
476 for errors.Is(err, unix.EINTR) {
477 n, err = unix.Read(fds[0], buf)
478 }
479 if err != nil || string(buf[:n]) != "dnd data" {
480 t.Fatalf("received %q, %v", buf[:max(n, 0)], err)
481 }
482
483 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
484 p.a.expect(t, "finished")
485 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
486 p.a.none(t)
487 p.b.none(t)
488 // core ended the drag: wayland does not echo it.
489 dndNoEvent[ports.DataDragEnded](t, p.events)
490}
491
492// The action follows wlroots: the preferred one if both allow it, else the
493// first of copy, move, ask they share, else none.
494func TestChooseAction(t *testing.T) {
495 for _, tc := range []struct{ avail, preferred, want uint32 }{
496 {actCopy | actMove, actMove, actMove},
497 {actCopy | actMove, actAsk, actCopy},
498 {actMove | actAsk, 0, actMove},
499 {actAsk, 0, actAsk},
500 {0, actCopy, 0},
501 } {
502 if got := chooseAction(tc.avail, tc.preferred); got != tc.want {
503 t.Errorf("chooseAction(%d, %d) = %d, want %d", tc.avail, tc.preferred, got, tc.want)
504 }
505 }
506}
507
508// A target and a source sharing no action leave the drop refused.
509func TestDataDragNoCommonAction(t *testing.T) {
510 p := newDndPair(t)
511 src := p.a.source(t, actCopy, "text/plain")
512 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
513 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
514 p.b.expect(t, "offer")
515 p.b.expect(t, "source_actions")
516 offer := p.b.expect(t, "enter").n
517 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
518 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actMove, actMove)
519 p.a.expect(t, "target")
520 p.commands <- ports.DataDragDrop{}
521 p.b.expect(t, "leave")
522 // The refused target no longer accepts.
523 if ev := p.a.expect(t, "target"); ev.mime != "" {
524 t.Fatalf("target %+v", ev)
525 }
526 p.a.expect(t, "cancelled")
527}
528
529// A2: a release over nothing, or over a target that accepted nothing,
530// cancels the source; the target gets leave and no drop.
531func TestDataDragRefusedDrop(t *testing.T) {
532 t.Run("nothing", func(t *testing.T) {
533 p := newDndPair(t)
534 p.start(t, 0)
535 p.commands <- ports.DataDragDrop{}
536 p.a.expect(t, "cancelled")
537 p.b.none(t)
538 })
539 t.Run("not accepted", func(t *testing.T) {
540 p := newDndPair(t)
541 p.start(t, 0)
542 p.enter(t)
543 p.commands <- ports.DataDragDrop{}
544 p.b.expect(t, "leave")
545 p.a.expect(t, "cancelled")
546 p.b.none(t)
547 })
548 t.Run("escape", func(t *testing.T) {
549 p := newDndPair(t)
550 p.start(t, 0)
551 p.enter(t)
552 p.commands <- ports.DataDragCancel{}
553 p.b.expect(t, "leave")
554 p.a.expect(t, "cancelled")
555 dndNoEvent[ports.DataDragEnded](t, p.events)
556 })
557}
558
559// Moving to another surface leaves the first and enters the second; a
560// surface of nobody (ID 0) leaves.
561func TestDataDragLeavesOnFocusChange(t *testing.T) {
562 p := newDndPair(t)
563 p.start(t, 0)
564 p.enter(t)
565 p.commands <- ports.DataDragFocus{}
566 p.b.expect(t, "leave")
567 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 2, Y: 3}
568 for range 3 {
569 p.b.next(t)
570 }
571 if ev := p.b.expect(t, "enter"); ev.x != 2 || ev.y != 3 {
572 t.Fatalf("enter %+v", ev)
573 }
574}
575
576// A4: the source dying mid-drag ends it: the target gets leave and core a
577// DataDragEnded; later commands change nothing.
578func TestDataDragSourceDestroyed(t *testing.T) {
579 p := newDndPair(t)
580 src := p.start(t, 0)
581 p.enter(t)
582 requestProtocol(t, p.a.c, src, wayland.DataSourceRequestDestroy)
583 if err := p.a.c.Roundtrip(); err != nil {
584 t.Fatal(err)
585 }
586 p.b.expect(t, "leave")
587 dndNextEvent[ports.DataDragEnded](t, p.events)
588 p.commands <- ports.DataDragMotion{ID: p.b.window, X: 9, Y: 9}
589 p.commands <- ports.DataDragDrop{}
590 p.b.none(t)
591}
592
593// A drag whose origin client dies ends the same way.
594func TestDataDragOriginDies(t *testing.T) {
595 p := newDndPair(t)
596 p.start(t, 0)
597 p.enter(t)
598 _ = p.a.c.Close()
599 p.b.expect(t, "leave")
600 dndNextEvent[ports.DataDragEnded](t, p.events)
601}
602
603// A6: a start_drag without the current press of the origin client is
604// refused: the source is cancelled and core hears nothing.
605func TestDataDragInvalidStart(t *testing.T) {
606 t.Run("stale serial", func(t *testing.T) {
607 p := newDndPair(t)
608 serial := p.a.press(t, p.commands)
609 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial-1)
610 p.a.expect(t, "cancelled")
611 dndNoEvent[ports.DataDragStarted](t, p.events)
612 })
613 t.Run("other client's press", func(t *testing.T) {
614 p := newDndPair(t)
615 p.a.press(t, p.commands)
616 serial := p.b.press(t, p.commands)
617 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
618 p.a.expect(t, "cancelled")
619 dndNoEvent[ports.DataDragStarted](t, p.events)
620 })
621 t.Run("key serial", func(t *testing.T) {
622 p := newDndPair(t)
623 p.a.press(t, p.commands)
624 p.a.release(t, p.commands)
625 serial := p.a.key(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.events)
629 })
630 t.Run("serial after the release", func(t *testing.T) {
631 p := newDndPair(t)
632 serial := p.a.press(t, p.commands)
633 p.a.release(t, p.commands)
634 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
635 p.a.expect(t, "cancelled")
636 dndNoEvent[ports.DataDragStarted](t, p.events)
637 })
638 t.Run("other surface of the client", func(t *testing.T) {
639 p := newDndPair(t)
640 serial := p.a.press(t, p.commands)
641 other := p.a.surfaceNew(t)
642 p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), other, 0, serial)
643 p.a.expect(t, "cancelled")
644 dndNoEvent[ports.DataDragStarted](t, p.events)
645 })
646 t.Run("press of the first button", func(t *testing.T) {
647 // A second button does not make a new grab: only the first serial.
648 p := newDndPair(t)
649 first := p.a.press(t, p.commands)
650 p.commands <- ports.PointerButtonTo{ID: p.a.window, Button: 0x111, Pressed: true, Time: 2 * time.Millisecond}
651 second := p.a.press2(t)
652 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, second)
653 p.a.expect(t, "cancelled")
654 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, first)
655 // Core is told the grab's button: its release drops.
656 if ev := dndNextEvent[ports.DataDragStarted](t, p.events); ev.Button != 0x110 {
657 t.Fatalf("drag button %#x, want the grab's", ev.Button)
658 }
659 })
660 t.Run("grab release consumed by core", func(t *testing.T) {
661 // A window drag consumes the release: the next grab press
662 // still makes the serial a drag can name.
663 p := newDndPair(t)
664 stale := p.a.press(t, p.commands)
665 serial := p.a.press(t, p.commands)
666 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, stale)
667 p.a.expect(t, "cancelled")
668 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
669 dndNextEvent[ports.DataDragStarted](t, p.events)
670 })
671 t.Run("grab release of a gone window", func(t *testing.T) {
672 p := newDndPair(t)
673 serial := p.a.press(t, p.commands)
674 p.commands <- ports.PointerButtonTo{ID: 9999, Button: 0x110, Time: 2 * time.Millisecond}
675 p.a.key(t, p.commands) // the release is applied once the key is seen
676 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), 0, serial)
677 p.a.expect(t, "cancelled")
678 dndNoEvent[ports.DataDragStarted](t, p.events)
679 })
680 t.Run("used source", func(t *testing.T) {
681 p := newDndPair(t)
682 src := p.start(t, 0)
683 p.commands <- ports.DataDragCancel{}
684 p.a.expect(t, "cancelled")
685 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
686 p.a.expect(t, "cancelled")
687 })
688 t.Run("drag in progress", func(t *testing.T) {
689 p := newDndPair(t)
690 p.start(t, 0)
691 dndNextEvent[ports.DataDragStarted](t, p.events)
692 other := p.a.source(t, actCopy, "text/plain")
693 p.a.startDrag(t, other, 0, p.a.press(t, p.commands))
694 p.a.expect(t, "cancelled")
695 dndNoEvent[ports.DataDragStarted](t, p.events)
696 })
697 t.Run("icon with another role", func(t *testing.T) {
698 p := newDndPair(t)
699 serial := p.a.press(t, p.commands)
700 src := p.a.source(t, actCopy, "text/plain")
701 // The origin toplevel is a role already.
702 requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestStartDrag, src, p.a.surf, p.a.surf, serial)
703 expectProtocolError(t, p.a.c, p.a.device.ID(), uint32(wayland.DataDeviceErrorRole))
704 dndNoEvent[ports.DataDragStarted](t, p.events)
705 })
706}
707
708// A nil source is a drag internal to the origin client: only its own
709// surfaces see it, without offer.
710func TestDataDragWithoutSource(t *testing.T) {
711 p := newDndPair(t)
712 p.a.startDrag(t, 0, 0, p.a.press(t, p.commands))
713 dndNextEvent[ports.DataDragStarted](t, p.events)
714 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
715 p.b.none(t)
716 p.commands <- ports.DataDragFocus{ID: p.a.window, X: 4, Y: 5}
717 if ev := p.a.expect(t, "enter"); ev.n != 0 || ev.x != 4 || ev.y != 5 {
718 t.Fatalf("enter %+v", ev)
719 }
720 p.commands <- ports.DataDragDrop{}
721 p.a.expect(t, "drop")
722}
723
724// Offer and source requests are validated.
725func TestDataDragProtocolErrors(t *testing.T) {
726 t.Run("finish before drop", func(t *testing.T) {
727 p := newDndPair(t)
728 p.start(t, 0)
729 offer := p.enter(t)
730 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestFinish)
731 expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidFinish))
732 })
733 t.Run("offer action mask", func(t *testing.T) {
734 p := newDndPair(t)
735 p.start(t, 0)
736 offer := p.enter(t)
737 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, uint32(8), uint32(0))
738 expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidActionMask))
739 })
740 t.Run("offer preferred action", func(t *testing.T) {
741 p := newDndPair(t)
742 p.start(t, 0)
743 offer := p.enter(t)
744 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actMove)
745 expectProtocolError(t, p.b.c, offer, uint32(wayland.DataOfferErrorInvalidAction))
746 })
747 t.Run("source action mask", func(t *testing.T) {
748 p := newDndPair(t)
749 src := p.a.source(t, 0, "text/plain")
750 requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, uint32(16))
751 expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidActionMask))
752 })
753 t.Run("selection with actions", func(t *testing.T) {
754 p := newDndPair(t)
755 src := p.a.source(t, actCopy, "text/plain")
756 requestProtocol(t, p.a.c, p.a.device.ID(), wayland.DataDeviceRequestSetSelection, src, uint32(0))
757 expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
758 })
759 t.Run("actions after the drag started", func(t *testing.T) {
760 p := newDndPair(t)
761 src := p.start(t, 0)
762 requestProtocol(t, p.a.c, src, wayland.DataSourceRequestSetActions, actCopy)
763 expectProtocolError(t, p.a.c, src, uint32(wayland.DataSourceErrorInvalidSource))
764 })
765}
766
767// A destroyed offer before finish cancels the source.
768func TestDataDragOfferDestroyedBeforeFinish(t *testing.T) {
769 p := newDndPair(t)
770 p.start(t, 0)
771 offer := p.enter(t)
772 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
773 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
774 p.a.expect(t, "target")
775 p.a.expect(t, "action")
776 p.commands <- ports.DataDragDrop{}
777 p.b.expect(t, "offer_action")
778 p.b.expect(t, "drop")
779 p.b.expect(t, "leave")
780 p.a.expect(t, "drop_performed")
781 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
782 p.a.expect(t, "cancelled")
783}
784
785// iconContent reports whether the server still holds content for id.
786func iconContent(t *testing.T, s *Server, id ports.WindowID) bool {
787 t.Helper()
788 var ok bool
789 if !s.display.Do(func() { _, ok = s.latest[id] }) {
790 t.Fatal("display stopped")
791 }
792 return ok
793}
794
795// The icon is a surface with its own window ID: its size and the
796// offsets of its commits reach core, its content is published, and both go
797// when the drag ends.
798func TestDataDragIcon(t *testing.T) {
799 p := newDndPair(t)
800 icon := p.a.surfaceNew(t)
801 buf := p.a.buffer(t, 8, 6)
802 // An offset on an ordinary surface is not used.
803 requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestOffset, int32(5), int32(5))
804 requestProtocol(t, p.a.c, p.a.surf, wayland.SurfaceRequestCommit)
805
806 src := p.a.source(t, actCopy, "text/plain")
807 p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
808 started := dndNextEvent[ports.DataDragStarted](t, p.events)
809 if started.Icon == 0 {
810 t.Fatal("no icon ID")
811 }
812 id := started.Icon
813 if iconContent(t, p.s, id) {
814 t.Fatal("content before the first commit")
815 }
816
817 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
818 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-3), int32(-4))
819 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
820 if err := p.a.c.Roundtrip(); err != nil {
821 t.Fatal(err)
822 }
823 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -3, Y: -4, W: 8, H: 6}) {
824 t.Fatalf("icon %+v", got)
825 }
826 if !iconContent(t, p.s, id) {
827 t.Fatal("no icon content")
828 }
829
830 // Offsets add up from commit to commit.
831 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(2))
832 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
833 if err := p.a.c.Roundtrip(); err != nil {
834 t.Fatal(err)
835 }
836 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -4, Y: -2, W: 8, H: 6}) {
837 t.Fatalf("icon %+v", got)
838 }
839 // The icon never takes input.
840 dndNoEvent[ports.InputRegionChanged](t, p.events)
841
842 p.commands <- ports.DataDragCancel{}
843 p.a.expect(t, "cancelled")
844 if iconContent(t, p.s, id) {
845 t.Fatal("icon content kept after the drag")
846 }
847}
848
849// An icon committed before start_drag shows at once.
850func TestDataDragIconCommittedEarly(t *testing.T) {
851 p := newDndPair(t)
852 icon := p.a.surfaceNew(t)
853 buf := p.a.buffer(t, 4, 4)
854 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
855 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
856 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
857 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
858 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, W: 4, H: 4}) {
859 t.Fatalf("icon %+v", got)
860 }
861 if !iconContent(t, p.s, id) {
862 t.Fatal("no icon content")
863 }
864}
865
866// Before wl_surface v5 the icon offset rides on attach and adds up.
867func TestDataDragIconAttachOffset(t *testing.T) {
868 p := newDndPair(t)
869 comp4 := bindVersion(t, p.a.c, "wl_compositor", 4)
870 icon := p.a.c.AllocateID()
871 requestProtocol(t, p.a.c, comp4, wayland.CompositorRequestCreateSurface, icon)
872 registerProtocol(t, p.a.c, icon)
873 buf := p.a.buffer(t, 4, 4)
874 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
875 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
876 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(-2), int32(-1))
877 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
878 if err := p.a.c.Roundtrip(); err != nil {
879 t.Fatal(err)
880 }
881 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -2, Y: -1, W: 4, H: 4}) {
882 t.Fatalf("icon %+v", got)
883 }
884}
885
886// Destroying the icon mid-drag hides it; the drag goes on.
887func TestDataDragIconDestroyed(t *testing.T) {
888 p := newDndPair(t)
889 icon := p.a.surfaceNew(t)
890 buf := p.a.buffer(t, 8, 6)
891 p.a.startDrag(t, p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list"), icon, p.a.press(t, p.commands))
892 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
893 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
894 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
895 if err := p.a.c.Roundtrip(); err != nil {
896 t.Fatal(err)
897 }
898 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got.W != 8 {
899 t.Fatalf("icon %+v", got)
900 }
901 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestDestroy)
902 if err := p.a.c.Roundtrip(); err != nil {
903 t.Fatal(err)
904 }
905 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id}) {
906 t.Fatalf("icon %+v", got)
907 }
908 if iconContent(t, p.s, id) {
909 t.Fatal("icon content kept")
910 }
911 p.enter(t)
912}
913
914// lockedPair is a pair of clients on a server that can lock the session.
915func lockedPair(t *testing.T) (*dndPair, string) {
916 t.Helper()
917 gate := &sessionsecurity.Gate{}
918 events := make(chan ports.ClientEvent, 128)
919 commands := make(chan ports.ClientCommand, 32)
920 dir := t.TempDir()
921 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"))
922 if err != nil {
923 t.Fatal(err)
924 }
925 ctx, cancel := context.WithCancel(context.Background())
926 done := make(chan error, 1)
927 go func() { done <- s.Run(ctx) }()
928 t.Cleanup(func() {
929 cancel()
930 select {
931 case err := <-done:
932 if err != nil {
933 t.Error(err)
934 }
935 case <-time.After(3 * time.Second):
936 t.Error("server did not stop")
937 }
938 })
939 p := &dndPair{s: s, events: events, commands: commands}
940 p.a = newDndClient(t, s, dir, events)
941 p.b = newDndClient(t, s, dir, events)
942 return p, dir
943}
944
945func lockSession(t *testing.T, s *Server, dir string) {
946 t.Helper()
947 lc := protocolClient(t, s, dir)
948 if _, err := bindLockManager(t, lc).Lock(); err != nil {
949 t.Fatal(err)
950 }
951 if err := lc.Roundtrip(); err != nil {
952 t.Fatal(err)
953 }
954}
955
956// A7: the session lock cancels a drag at once: target leave, source
957// cancelled, icon gone.
958func TestDataDragLockCancels(t *testing.T) {
959 p, dir := lockedPair(t)
960 icon := p.a.surfaceNew(t)
961 buf := p.a.buffer(t, 8, 6)
962 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
963 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
964 src := p.a.source(t, actCopy|actMove, "text/plain", "text/uri-list")
965 p.a.startDrag(t, src, icon, p.a.press(t, p.commands))
966 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
967 p.enter(t)
968
969 lockSession(t, p.s, dir)
970 p.b.expect(t, "leave")
971 p.a.expect(t, "cancelled")
972 if iconContent(t, p.s, id) {
973 t.Fatal("icon content kept")
974 }
975 // A new drag is refused while protected.
976 src2 := p.a.source(t, actCopy, "text/plain")
977 p.a.startDrag(t, src2, 0, uint32(1))
978 p.a.expect(t, "cancelled")
979}
980
981// GO-006: a dropped transfer waiting for its finish dies with the lock, for
982// a v3 and a pre-v3 target alike.
983func TestDataDropLockCancelsPending(t *testing.T) {
984 for _, version := range []uint32{3, 2} {
985 p, dir := lockedPair(t)
986 p.b = newDndClientAt(t, p.s, dir, p.events, version)
987 p.start(t, 0)
988 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 5, Y: 6}
989 p.b.until(t, "enter")
990 offer := p.b.lastOffer(t)
991 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
992 if version >= 3 {
993 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
994 }
995 p.a.until(t, "target")
996 p.commands <- ports.DataDragDrop{}
997 p.a.until(t, "drop_performed")
998 lockSession(t, p.s, dir)
999 p.a.expect(t, "cancelled")
1000 }
1001}
1002
1003// A subsurface of the focused tree is a valid origin.
1004func TestDataDragFromSubsurface(t *testing.T) {
1005 p := newDndPair(t)
1006 serial := p.a.press(t, p.commands)
1007 child := p.a.surfaceNew(t)
1008 subc := bindProtocol(t, p.a.c, "wl_subcompositor")
1009 requestProtocol(t, p.a.c, subc, wayland.SubcompositorRequestGetSubsurface, p.a.c.AllocateID(), child, p.a.surf)
1010 p.a.startDragFrom(t, p.a.source(t, actCopy, "text/plain"), child, 0, serial)
1011 dndNextEvent[ports.DataDragStarted](t, p.events)
1012}
1013
1014// GO-003: destroying the offers of the target withdraws the acceptance: the
1015// release then cancels, with no drop.
1016func TestDataDragOfferDestroyedBeforeRelease(t *testing.T) {
1017 p := newDndPair(t)
1018 p.start(t, 0)
1019 offer := p.enter(t)
1020 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1021 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestSetActions, actCopy, actCopy)
1022 p.a.expect(t, "target")
1023 p.a.expect(t, "action")
1024 requestProtocol(t, p.b.c, offer, wayland.DataOfferRequestDestroy)
1025 if err := p.b.c.Roundtrip(); err != nil {
1026 t.Fatal(err)
1027 }
1028 if ev := p.a.expect(t, "target"); ev.mime != "" {
1029 t.Fatalf("target %+v", ev)
1030 }
1031 if ev := p.a.expect(t, "action"); ev.n != 0 {
1032 t.Fatalf("action %+v", ev)
1033 }
1034 p.commands <- ports.DataDragDrop{}
1035 got := p.b.until(t, "leave")
1036 for _, ev := range got {
1037 if ev.name == "drop" {
1038 t.Fatalf("drop after the offer was destroyed: %+v", got)
1039 }
1040 }
1041 p.a.expect(t, "cancelled")
1042}
1043
1044// GO-004: with ask, the target picks the final action after the drop; the
1045// source hears it just before dnd_finished.
1046func TestDataDragAsk(t *testing.T) {
1047 p := newDndPair(t)
1048 src := p.a.source(t, actCopy|actMove|actAsk, "text/plain")
1049 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1050 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1051 p.b.until(t, "enter")
1052 id := p.b.lastOffer(t)
1053 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1054 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1055 p.a.expect(t, "target")
1056 if ev := p.a.expect(t, "action"); ev.n != actAsk {
1057 t.Fatalf("action %+v", ev)
1058 }
1059 p.commands <- ports.DataDragDrop{}
1060 p.b.until(t, "leave")
1061 p.a.expect(t, "drop_performed")
1062
1063 // Finishing while the action is ask is an error; the choice comes first.
1064 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(2), "text/plain")
1065 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove, actMove)
1066 p.a.expect(t, "target")
1067 p.b.none(t) // no offer.action after the drop
1068 p.a.none(t) // the final action waits for the finish
1069 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestFinish)
1070 if ev := p.a.expect(t, "action"); ev.n != actMove {
1071 t.Fatalf("action %+v", ev)
1072 }
1073 p.a.expect(t, "finished")
1074}
1075
1076// A final preferred action outside the source's is an error.
1077func TestDataDragAskFinalActionNotInSource(t *testing.T) {
1078 p := newDndPair(t)
1079 src := p.a.source(t, actCopy|actAsk, "text/plain")
1080 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1081 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1082 p.b.until(t, "enter")
1083 id := p.b.lastOffer(t)
1084 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1085 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actCopy|actMove|actAsk, actAsk)
1086 p.a.expect(t, "target")
1087 p.a.expect(t, "action")
1088 p.commands <- ports.DataDragDrop{}
1089 p.b.until(t, "leave")
1090 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestSetActions, actMove, actMove)
1091 expectProtocolError(t, p.b.c, id, uint32(wayland.DataOfferErrorInvalidAction))
1092}
1093
1094// GO-005: a pre-v3 target copies: a source that does not allow copy is
1095// cancelled, one that does gets it.
1096func TestDataDragLegacyTarget(t *testing.T) {
1097 setup := func(t *testing.T, actions uint32) (*dndPair, uint32) {
1098 s, events, commands, dir := keyboardServer(t)
1099 p := &dndPair{s: s, events: events, commands: commands}
1100 p.a = newDndClient(t, s, dir, events)
1101 p.b = newDndClientAt(t, s, dir, events, 2)
1102 src := p.a.source(t, actions, "text/plain")
1103 p.a.startDrag(t, src, 0, p.a.press(t, p.commands))
1104 p.commands <- ports.DataDragFocus{ID: p.b.window, X: 1, Y: 1}
1105 p.b.until(t, "enter")
1106 id := p.b.lastOffer(t)
1107 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestAccept, uint32(1), "text/plain")
1108 p.a.expect(t, "target")
1109 return p, id
1110 }
1111 t.Run("move only", func(t *testing.T) {
1112 p, _ := setup(t, actMove)
1113 p.commands <- ports.DataDragDrop{}
1114 got := p.b.until(t, "leave")
1115 for _, ev := range got {
1116 if ev.name == "drop" {
1117 t.Fatalf("drop for a source without copy: %+v", got)
1118 }
1119 }
1120 p.a.until(t, "cancelled")
1121 })
1122 t.Run("copy", func(t *testing.T) {
1123 p, id := setup(t, actCopy|actMove)
1124 p.commands <- ports.DataDragDrop{}
1125 p.b.expect(t, "drop")
1126 p.b.expect(t, "leave")
1127 if ev := p.a.expect(t, "action"); ev.n != actCopy {
1128 t.Fatalf("action %+v", ev)
1129 }
1130 p.a.expect(t, "drop_performed")
1131 // GO-006: destroying the last offer finishes the transfer.
1132 requestProtocol(t, p.b.c, id, wayland.DataOfferRequestDestroy)
1133 p.a.expect(t, "finished")
1134 })
1135}
1136
1137// GO-007: of several offset requests before one commit, the last counts.
1138func TestDataDragIconOffsetReplacedBeforeCommit(t *testing.T) {
1139 p := newDndPair(t)
1140 icon := p.a.surfaceNew(t)
1141 buf := p.a.buffer(t, 8, 6)
1142 p.a.startDrag(t, p.a.source(t, actCopy, "text/plain"), icon, p.a.press(t, p.commands))
1143 id := dndNextEvent[ports.DataDragStarted](t, p.events).Icon
1144 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
1145 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-1), int32(-1))
1146 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestOffset, int32(-5), int32(-7))
1147 requestProtocol(t, p.a.c, icon, wayland.SurfaceRequestCommit)
1148 if err := p.a.c.Roundtrip(); err != nil {
1149 t.Fatal(err)
1150 }
1151 if got := dndNextEvent[ports.DataDragIcon](t, p.events); got != (ports.DataDragIcon{ID: id, X: -5, Y: -7, W: 8, H: 6}) {
1152 t.Fatalf("icon %+v", got)
1153 }
1154}