1package wayland
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/wayland"
8 "github.com/bnema/go-wayland-bindings/server/xdgforeign"
9 xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/wlturbo"
12 "golang.org/x/sys/unix"
13)
14
15// handleProxy receives zxdg_exported_v2.handle.
16type handleProxy struct {
17 wlturbo.BaseProxy
18 handles chan string
19}
20
21func (p *handleProxy) Dispatch(e *wlturbo.Event) { p.handles <- e.String() }
22
23// destroyedProxy receives zxdg_imported_v2.destroyed.
24type destroyedProxy struct {
25 wlturbo.BaseProxy
26 destroyed chan struct{}
27}
28
29func (p *destroyedProxy) Dispatch(*wlturbo.Event) { p.destroyed <- struct{}{} }
30
31// exportToplevel exports surf and returns the exported object and handle.
32func exportToplevel(t *testing.T, c *wlturbo.Display, surf uint32) (uint32, string) {
33 t.Helper()
34 exporter := bindProtocol(t, c, "zxdg_exporter_v2")
35 id := c.AllocateID()
36 p := &handleProxy{handles: make(chan string, 1)}
37 p.SetID(id)
38 registerWireProxy(c, p)
39 requestProtocol(t, c, exporter, xdgforeign.ZxdgExporterV2RequestExportToplevel, id, surf)
40 if err := c.Roundtrip(); err != nil {
41 t.Fatal(err)
42 }
43 select {
44 case h := <-p.handles:
45 return id, h
46 default:
47 t.Fatal("no handle")
48 return 0, ""
49 }
50}
51
52// importToplevel imports handle; the proxy reports destroyed.
53func importToplevel(t *testing.T, c *wlturbo.Display, handle string) (uint32, *destroyedProxy) {
54 t.Helper()
55 importer := bindProtocol(t, c, "zxdg_importer_v2")
56 id := c.AllocateID()
57 p := &destroyedProxy{destroyed: make(chan struct{}, 1)}
58 p.SetID(id)
59 registerWireProxy(c, p)
60 requestProtocol(t, c, importer, xdgforeign.ZxdgImporterV2RequestImportToplevel, id, handle)
61 if err := c.Roundtrip(); err != nil {
62 t.Fatal(err)
63 }
64 return id, p
65}
66
67// childToplevel maps a toplevel of c whose parent is set by parent, called
68// before its first buffer (as a portal dialog does).
69func childToplevel(t *testing.T, c *wlturbo.Display, parent func(surf uint32)) uint32 {
70 t.Helper()
71 comp := bindProtocol(t, c, "wl_compositor")
72 wm := bindProtocol(t, c, "xdg_wm_base")
73 shm := bindProtocol(t, c, "wl_shm")
74 registerProtocol(t, c, shm)
75 fd, err := unix.MemfdCreate("dialog-buffer", 0)
76 if err != nil {
77 t.Fatal(err)
78 }
79 defer unix.Close(fd)
80 if err := unix.Ftruncate(fd, 4); err != nil {
81 t.Fatal(err)
82 }
83 pool, buffer := c.AllocateID(), c.AllocateID()
84 registerProtocol(t, c, pool)
85 registerProtocol(t, c, buffer)
86 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
87 t.Fatal(err)
88 }
89 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
90 surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()
91 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
92 registerProtocol(t, c, surf)
93 requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
94 serials := make(chan uint32, 1)
95 xp := &configureProxy{serial: serials}
96 xp.SetID(xdg)
97 registerWireProxy(c, xp)
98 registerProtocol(t, c, top)
99 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
100 parent(surf)
101 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
102 if err := c.Roundtrip(); err != nil {
103 t.Fatal(err)
104 }
105 select {
106 case serial := <-serials:
107 requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
108 default:
109 t.Fatal("missing configure")
110 }
111 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
112 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
113 if err := c.Roundtrip(); err != nil {
114 t.Fatal(err)
115 }
116 return surf
117}
118
119// A portal backend parents its dialog to another client's window through
120// xdg-foreign: the dialog maps floating, with that window as its parent.
121func TestForeignParentsDialog(t *testing.T) {
122 s, events, _, dir := lifecycleServer(t)
123 app := protocolClient(t, s, dir)
124 appWin, appSurf, _ := surfaceMapper(t, app, events)()
125 _, handle := exportToplevel(t, app, appSurf)
126
127 portal := protocolClient(t, s, dir)
128 imported, _ := importToplevel(t, portal, handle)
129 childToplevel(t, portal, func(surf uint32) {
130 requestProtocol(t, portal, imported, xdgforeign.ZxdgImportedV2RequestSetParentOf, surf)
131 })
132 dialog := mapped(t, events, 2*time.Second)
133 if !dialog.Floating || dialog.Parent != appWin.ID {
134 t.Fatalf("dialog %+v, want floating over %d", dialog, appWin.ID)
135 }
136
137 // Destroying the import ends the relationship: core hears it.
138 requestProtocol(t, portal, imported, xdgforeign.ZxdgImportedV2RequestDestroy)
139 if err := portal.Roundtrip(); err != nil {
140 t.Fatal(err)
141 }
142 for deadline := time.After(2 * time.Second); ; {
143 select {
144 case ev := <-events:
145 if p, ok := ev.(ports.WindowParent); ok {
146 if p.ID != dialog.ID || p.Parent != 0 {
147 t.Fatalf("parent change %+v", p)
148 }
149 return
150 }
151 case <-deadline:
152 t.Fatal("no parent change")
153 }
154 }
155}
156
157// An unknown handle is destroyed at once; revoking an export destroys its
158// imports and drops the parent they set.
159func TestForeignInvalidHandles(t *testing.T) {
160 s, events, _, dir := lifecycleServer(t)
161 portal := protocolClient(t, s, dir)
162 _, unknown := importToplevel(t, portal, "not-a-handle")
163 select {
164 case <-unknown.destroyed:
165 default:
166 t.Fatal("unknown handle not destroyed")
167 }
168
169 app := protocolClient(t, s, dir)
170 _, appSurf, _ := surfaceMapper(t, app, events)()
171 exported, handle := exportToplevel(t, app, appSurf)
172 imported, p := importToplevel(t, portal, handle)
173 requestProtocol(t, app, exported, xdgforeign.ZxdgExportedV2RequestDestroy)
174 if err := app.Roundtrip(); err != nil {
175 t.Fatal(err)
176 }
177 if err := portal.Roundtrip(); err != nil {
178 t.Fatal(err)
179 }
180 select {
181 case <-p.destroyed:
182 default:
183 t.Fatal("import not destroyed with its export")
184 }
185 childToplevel(t, portal, func(surf uint32) {
186 requestProtocol(t, portal, imported, xdgforeign.ZxdgImportedV2RequestSetParentOf, surf)
187 })
188 if dialog := mapped(t, events, 2*time.Second); dialog.Floating || dialog.Parent != 0 {
189 t.Fatalf("revoked parent still applied: %+v", dialog)
190 }
191}
192
193// Only toplevels can be exported.
194func TestForeignExportNeedsToplevel(t *testing.T) {
195 s, _, _, dir := lifecycleServer(t)
196 c := protocolClient(t, s, dir)
197 comp := bindProtocol(t, c, "wl_compositor")
198 surf := c.AllocateID()
199 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
200 exporter := bindProtocol(t, c, "zxdg_exporter_v2")
201 id := c.AllocateID()
202 registerProtocol(t, c, id)
203 requestProtocol(t, c, exporter, xdgforeign.ZxdgExporterV2RequestExportToplevel, id, surf)
204 expectProtocolError(t, c, exporter, uint32(xdgforeign.ZxdgExporterV2ErrorInvalidSurface))
205}