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}