foreign_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close