v0.1.0 d9218d6

activation_test.go RawHistoryFolder

1package wayland
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/neferwl/internal/ports"
8	"github.com/bnema/purego-libwayland/protocol/xdgactivation"
9	"github.com/bnema/wlturbo"
10)
11
12// tokenProxy receives an xdg_activation_token_v1.done token.
13type tokenProxy struct {
14	wlturbo.BaseProxy
15	tokens chan string
16}
17
18func (p *tokenProxy) Dispatch(e *wlturbo.Event) { p.tokens <- e.String() }
19
20// newToken commits a token request, with a serial and seat when given,
21// and returns the token.
22func newToken(t *testing.T, c *wlturbo.Display, manager uint32, serialSeat ...uint32) string {
23	t.Helper()
24	id := c.AllocateID()
25	p := &tokenProxy{tokens: make(chan string, 1)}
26	p.SetID(id)
27	c.Context().Register(p)
28	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestGetActivationToken, id)
29	if len(serialSeat) == 2 {
30		requestProtocol(t, c, id, xdgactivation.ActivationTokenV1RequestSetSerial, serialSeat[0], serialSeat[1])
31	}
32	requestProtocol(t, c, id, xdgactivation.ActivationTokenV1RequestCommit)
33	for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
34		if err := c.Roundtrip(); err != nil {
35			t.Fatal(err)
36		}
37		select {
38		case tok := <-p.tokens:
39			return tok
40		default:
41		}
42	}
43	t.Fatal("no token")
44	return ""
45}
46
47func activated(t *testing.T, events <-chan ports.ClientEvent, want ports.WindowID) {
48	t.Helper()
49	deadline := time.After(2 * time.Second)
50	for {
51		select {
52		case ev := <-events:
53			if a, ok := ev.(ports.WindowActivate); ok {
54				if a.ID != want {
55					t.Fatalf("activated %d, want %d", a.ID, want)
56				}
57				return
58			}
59		case <-deadline:
60			t.Fatal("no activation")
61		}
62	}
63}
64
65func noActivation(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) {
66	t.Helper()
67	if err := c.Roundtrip(); err != nil {
68		t.Fatal(err)
69	}
70	deadline := time.After(100 * time.Millisecond)
71	for {
72		select {
73		case ev := <-events:
74			if _, ok := ev.(ports.WindowActivate); ok {
75				t.Fatal("activated with an invalid token")
76			}
77		case <-deadline:
78			return
79		}
80	}
81}
82
83func TestActivationNeedsFocusedRequester(t *testing.T) {
84	s, events, commands, dir := lifecycleServer(t)
85	c := protocolClient(t, s, dir)
86	mapWindow := surfaceMapper(t, c, events)
87	first, _, _ := mapWindow()
88	second, surf, _ := mapWindow()
89	manager := bindProtocol(t, c, "xdg_activation_v1")
90
91	// Without focus the token is inert.
92	token := newToken(t, c, manager)
93	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestActivate, token, surf)
94	noActivation(t, c, events)
95
96	// The focused client's token activates, once.
97	commands <- ports.FocusWindow{ID: first.ID}
98	if err := c.Roundtrip(); err != nil {
99		t.Fatal(err)
100	}
101	token = newToken(t, c, manager)
102	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestActivate, token, surf)
103	activated(t, events, second.ID)
104	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestActivate, token, surf)
105	noActivation(t, c, events)
106
107	// Unknown tokens are ignored.
108	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestActivate, "not-a-token", surf)
109	noActivation(t, c, events)
110}
111
112// Another client's token is inert while one client has the focus; the
113// focused client may hand its token to activate another client's window.
114func TestActivationAcrossClients(t *testing.T) {
115	s, events, commands, dir := lifecycleServer(t)
116	a := protocolClient(t, s, dir)
117	winA, _, _ := surfaceMapper(t, a, events)()
118	b := protocolClient(t, s, dir)
119	winB, surfB, _ := surfaceMapper(t, b, events)()
120	managerA := bindProtocol(t, a, "xdg_activation_v1")
121	managerB := bindProtocol(t, b, "xdg_activation_v1")
122	commands <- ports.FocusWindow{ID: winA.ID}
123	if err := a.Roundtrip(); err != nil {
124		t.Fatal(err)
125	}
126	token := newToken(t, b, managerB)
127	requestProtocol(t, b, managerB, xdgactivation.ActivationV1RequestActivate, token, surfB)
128	noActivation(t, b, events)
129	token = newToken(t, a, managerA)
130	requestProtocol(t, b, managerB, xdgactivation.ActivationV1RequestActivate, token, surfB)
131	activated(t, events, winB.ID)
132
133	// A token dies when the focus leaves its client.
134	token = newToken(t, a, managerA)
135	commands <- ports.FocusWindow{ID: winB.ID}
136	waitFocus(t, s, winB.ID)
137	requestProtocol(t, b, managerB, xdgactivation.ActivationV1RequestActivate, token, surfB)
138	noActivation(t, b, events)
139}
140
141// A press serial earns a token only while the press is the latest user
142// action: after a focus change it is stale.
143func TestActivationPressSerial(t *testing.T) {
144	s, events, commands, dir := lifecycleServer(t)
145	c := protocolClient(t, s, dir)
146	w, surf, _ := surfaceMapper(t, c, events)()
147	manager := bindProtocol(t, c, "xdg_activation_v1")
148	seat := bindProtocol(t, c, "wl_seat")
149	registerProtocol(t, c, seat)
150	press := func() uint32 {
151		var serial uint32
152		done := make(chan struct{})
153		s.display.Do(func() {
154			s.serial++
155			s.press, s.pressClient, s.pressAt = s.serial, s.windows[w.ID].xdg.resource.Client(), time.Now()
156			serial = s.serial
157			close(done)
158		})
159		<-done
160		return serial
161	}
162	serial := press()
163	token := newToken(t, c, manager, serial, seat)
164	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestActivate, token, surf)
165	activated(t, events, w.ID)
166	// A wrong serial earns nothing.
167	token = newToken(t, c, manager, serial+100, seat)
168	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestActivate, token, surf)
169	noActivation(t, c, events)
170	// The focus moved after the press: stale.
171	commands <- ports.FocusWindow{ID: w.ID}
172	commands <- ports.FocusWindow{}
173	waitFocus(t, s, 0)
174	token = newToken(t, c, manager, serial, seat)
175	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestActivate, token, surf)
176	noActivation(t, c, events)
177}
178
179func TestActivationTokenCommittedTwice(t *testing.T) {
180	s, _, _, dir := lifecycleServer(t)
181	c := protocolClient(t, s, dir)
182	manager := bindProtocol(t, c, "xdg_activation_v1")
183	id := c.AllocateID()
184	registerProtocol(t, c, id)
185	requestProtocol(t, c, manager, xdgactivation.ActivationV1RequestGetActivationToken, id)
186	requestProtocol(t, c, id, xdgactivation.ActivationTokenV1RequestCommit)
187	requestProtocol(t, c, id, xdgactivation.ActivationTokenV1RequestCommit)
188	expectProtocolError(t, c, id, uint32(xdgactivation.ActivationTokenV1ErrorAlreadyUsed))
189}
190
191// waitFocus waits until the display goroutine has applied a focus change.
192func waitFocus(t *testing.T, s *Server, id ports.WindowID) {
193	t.Helper()
194	for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
195		done := make(chan ports.WindowID, 1)
196		s.display.Do(func() { done <- s.focused })
197		if <-done == id {
198			return
199		}
200	}
201	t.Fatal("focus not applied", id)
202}

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 23

Tags 10

↑↓ moveenter openesc close