1package wayland
2
3import (
4 "fmt"
5 "slices"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/inputmethod"
11 "github.com/bnema/go-wayland-bindings/server/textinput"
12 "github.com/bnema/go-wayland-bindings/server/wayland"
13 "github.com/bnema/go-wayland-bindings/server/xdgshell"
14 "github.com/bnema/neferwl/internal/ports"
15 "github.com/bnema/wlturbo"
16 "golang.org/x/sys/unix"
17)
18
19// imeLog records events of one object as "name arg arg" strings, from
20// the event signatures: u, i, o (IDs), s (strings) and h (fds, closed).
21type imeLog struct {
22 wlturbo.BaseProxy
23 events []string
24 sigs []struct{ name, args string }
25}
26
27func (l *imeLog) Dispatch(e *wlturbo.Event) {
28 if int(e.Opcode) >= len(l.sigs) {
29 l.events = append(l.events, fmt.Sprint("unknown ", e.Opcode))
30 return
31 }
32 sig := l.sigs[e.Opcode]
33 parts := []string{sig.name}
34 for _, a := range sig.args {
35 switch a {
36 case 'u', 'o':
37 parts = append(parts, fmt.Sprint(e.Uint32()))
38 case 'i':
39 parts = append(parts, fmt.Sprint(e.Int32()))
40 case 's':
41 parts = append(parts, fmt.Sprintf("%q", e.String()))
42 case 'h':
43 if fd := int(e.Fd()); fd > 0 {
44 unix.Close(fd)
45 }
46 parts = append(parts, "fd")
47 }
48 }
49 l.events = append(l.events, strings.Join(parts, " "))
50}
51
52// take returns the recorded events and forgets them.
53func (l *imeLog) take() []string {
54 ev := l.events
55 l.events = nil
56 return ev
57}
58
59func newIMELog(c *wlturbo.Display, id uint32, sigs ...string) *imeLog {
60 l := &imeLog{}
61 for _, s := range sigs {
62 name, args, _ := strings.Cut(s, ":")
63 l.sigs = append(l.sigs, struct{ name, args string }{name, args})
64 }
65 l.SetID(id)
66 registerWireProxy(c, l)
67 return l
68}
69
70var (
71 textInputEvents = []string{"enter:o", "leave:o", "preedit_string:sii", "commit_string:s", "delete_surrounding_text:uu", "done:u"}
72 inputMethodEvents = []string{"activate", "deactivate", "surrounding_text:suu", "text_change_cause:u", "content_type:uu", "done", "unavailable"}
73 grabEvents = []string{"keymap:uhu", "key:uuuu", "modifiers:uuuuu", "repeat_info:ii"}
74)
75
76// textApp is a focused client window with a text input.
77type textApp struct {
78 c *wlturbo.Display
79 ti, surf uint32
80 top uint32
81 window ports.WindowID
82 log *imeLog
83 keys *eventsProxy
84}
85
86func newTextApp(t *testing.T, s *Server, events chan ports.ClientEvent, commands chan ports.ClientCommand, dir string) *textApp {
87 t.Helper()
88 c := protocolClient(t, s, dir)
89 seat := bindProtocol(t, c, "wl_seat")
90 registerProtocol(t, c, seat)
91 keys := &eventsProxy{}
92 keys.SetID(c.AllocateID())
93 registerWireProxy(c, keys)
94 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keys.ID())
95 w, surf, xdg := surfaceMapper(t, c, events)()
96 manager := bindProtocol(t, c, "zwp_text_input_manager_v3")
97 registerProtocol(t, c, manager)
98 ti := c.AllocateID()
99 log := newIMELog(c, ti, textInputEvents...)
100 requestProtocol(t, c, manager, textinput.ZwpTextInputManagerV3RequestGetTextInput, ti, seat)
101 commands <- ports.FocusWindow{ID: w.ID}
102 waitFocus(t, s, w.ID)
103 roundtrip(t, c)
104 // surfaceMapper allocated the toplevel right after the xdg_surface.
105 return &textApp{c: c, ti: ti, surf: surf, top: xdg + 1, window: w.ID, log: log, keys: keys}
106}
107
108func (a *textApp) request(t *testing.T, op uint32, args ...any) {
109 t.Helper()
110 requestProtocol(t, a.c, a.ti, op, args...)
111}
112
113// enable commits an enabled text input with surrounding text and content type.
114func (a *textApp) enable(t *testing.T) {
115 t.Helper()
116 a.request(t, textinput.ZwpTextInputV3RequestEnable)
117 a.request(t, textinput.ZwpTextInputV3RequestSetSurroundingText, "hello", int32(5), int32(5))
118 a.request(t, textinput.ZwpTextInputV3RequestSetContentType, uint32(textinput.ZwpTextInputV3ContentHintSpellcheck), uint32(textinput.ZwpTextInputV3ContentPurposeNormal))
119 a.request(t, textinput.ZwpTextInputV3RequestCommit)
120}
121
122// imeApp is an input method client.
123type imeApp struct {
124 c *wlturbo.Display
125 im uint32
126 log *imeLog
127}
128
129func newIMEApp(t *testing.T, s *Server, dir string) *imeApp {
130 t.Helper()
131 c := protocolClient(t, s, dir)
132 seat := bindProtocol(t, c, "wl_seat")
133 registerProtocol(t, c, seat)
134 manager := bindProtocol(t, c, "zwp_input_method_manager_v2")
135 registerProtocol(t, c, manager)
136 im := c.AllocateID()
137 log := newIMELog(c, im, inputMethodEvents...)
138 requestProtocol(t, c, manager, inputmethod.ZwpInputMethodManagerV2RequestGetInputMethod, seat, im)
139 roundtrip(t, c)
140 return &imeApp{c: c, im: im, log: log}
141}
142
143func (m *imeApp) request(t *testing.T, op uint32, args ...any) {
144 t.Helper()
145 requestProtocol(t, m.c, m.im, op, args...)
146}
147
148func (m *imeApp) grab(t *testing.T) (uint32, *imeLog) {
149 t.Helper()
150 g := m.c.AllocateID()
151 log := newIMELog(m.c, g, grabEvents...)
152 m.request(t, inputmethod.ZwpInputMethodV2RequestGrabKeyboard, g)
153 return g, log
154}
155
156// settle lets the server handle every connection's requests, in order.
157func settle(t *testing.T, cs ...*wlturbo.Display) {
158 t.Helper()
159 roundtrip(t, cs...)
160 roundtrip(t, cs...)
161}
162
163func expectEvents(t *testing.T, what string, got []string, want ...string) {
164 t.Helper()
165 if !slices.Equal(got, want) {
166 t.Fatalf("%s events:\n got %q\nwant %q", what, got, want)
167 }
168}
169
170// Text input focus follows keyboard focus across clients and window close.
171func TestTextInputFollowsFocus(t *testing.T) {
172 s, events, commands, dir := keyboardServer(t)
173 a := newTextApp(t, s, events, commands, dir)
174 expectEvents(t, "a", a.log.take(), fmt.Sprint("enter ", a.surf))
175 b := newTextApp(t, s, events, commands, dir)
176 settle(t, a.c, b.c)
177 expectEvents(t, "a", a.log.take(), fmt.Sprint("leave ", a.surf))
178 expectEvents(t, "b", b.log.take(), fmt.Sprint("enter ", b.surf))
179
180 commands <- ports.FocusWindow{ID: a.window}
181 waitFocus(t, s, a.window)
182 settle(t, a.c, b.c)
183 expectEvents(t, "a", a.log.take(), fmt.Sprint("enter ", a.surf))
184 expectEvents(t, "b", b.log.take(), fmt.Sprint("leave ", b.surf))
185
186 // Closing the focused window leaves.
187 requestProtocol(t, a.c, a.top, xdgshell.ToplevelRequestDestroy)
188 settle(t, a.c)
189 waitFocus(t, s, 0)
190 settle(t, a.c)
191 expectEvents(t, "a", a.log.take(), fmt.Sprint("leave ", a.surf))
192}
193
194// Enable activates the input method with the client state; its preedit,
195// commit and delete reach the client in order with the commit count.
196func TestTextInputIMERoundTrip(t *testing.T) {
197 s, events, commands, dir := keyboardServer(t)
198 ime := newIMEApp(t, s, dir)
199 app := newTextApp(t, s, events, commands, dir)
200 app.log.take()
201 app.enable(t)
202 settle(t, app.c, ime.c)
203 expectEvents(t, "ime", ime.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
204
205 ime.request(t, inputmethod.ZwpInputMethodV2RequestSetPreeditString, "nihon", int32(5), int32(5))
206 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
207 settle(t, ime.c, app.c)
208 expectEvents(t, "app", app.log.take(), `preedit_string "nihon" 5 5`, "done 1")
209
210 ime.request(t, inputmethod.ZwpInputMethodV2RequestDeleteSurroundingText, uint32(1), uint32(0))
211 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "日本")
212 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
213 settle(t, ime.c, app.c)
214 expectEvents(t, "app", app.log.take(), `commit_string "日本"`, "delete_surrounding_text 1 0", "done 1")
215
216 // State updates reach the input method; done carries the latest
217 // client commit count even when the input method is behind.
218 app.request(t, textinput.ZwpTextInputV3RequestSetSurroundingText, "hello日本", int32(11), int32(11))
219 app.request(t, textinput.ZwpTextInputV3RequestSetTextChangeCause, uint32(textinput.ZwpTextInputV3ChangeCauseOther))
220 app.request(t, textinput.ZwpTextInputV3RequestCommit)
221 app.request(t, textinput.ZwpTextInputV3RequestCommit)
222 settle(t, app.c, ime.c)
223 expectEvents(t, "ime", ime.log.take(), `surrounding_text "hello日本" 11 11`, "text_change_cause 1", "content_type 2 0", "done", `surrounding_text "hello日本" 11 11`, "text_change_cause 0", "content_type 2 0", "done")
224 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "x")
225 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
226 settle(t, ime.c, app.c)
227 expectEvents(t, "app", app.log.take(), `commit_string "x"`, "done 3")
228
229 app.request(t, textinput.ZwpTextInputV3RequestDisable)
230 app.request(t, textinput.ZwpTextInputV3RequestCommit)
231 settle(t, app.c, ime.c)
232 expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
233 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "lost")
234 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(4))
235 settle(t, ime.c, app.c)
236 expectEvents(t, "app", app.log.take())
237}
238
239// Focus loss deactivates; a second text input cannot take over; state
240// past the spec limit is ignored.
241func TestTextInputFocusLossAndLimits(t *testing.T) {
242 s, events, commands, dir := keyboardServer(t)
243 ime := newIMEApp(t, s, dir)
244 app := newTextApp(t, s, events, commands, dir)
245 app.enable(t)
246 settle(t, app.c, ime.c)
247 ime.log.take()
248
249 app.request(t, textinput.ZwpTextInputV3RequestSetSurroundingText, strings.Repeat("a", maxIMEText+1), int32(0), int32(0))
250 app.request(t, textinput.ZwpTextInputV3RequestCommit)
251 settle(t, app.c, ime.c)
252 expectEvents(t, "ime", ime.log.take(), `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
253
254 other := newTextApp(t, s, events, commands, dir)
255 settle(t, app.c, ime.c)
256 expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
257 // After leave, app's requests are ignored until the next enter.
258 app.enable(t)
259 settle(t, app.c, ime.c)
260 expectEvents(t, "ime", ime.log.take())
261 other.enable(t)
262 settle(t, other.c, ime.c)
263 expectEvents(t, "ime", ime.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
264}
265
266// Only one input method at a time; the slot frees when it disconnects,
267// and the next one is activated for the enabled text input.
268func TestInputMethodSingleSlot(t *testing.T) {
269 s, events, commands, dir := keyboardServer(t)
270 first := newIMEApp(t, s, dir)
271 second := newIMEApp(t, s, dir)
272 expectEvents(t, "second", second.log.take(), "unavailable")
273 // An inert input method's requests do nothing.
274 second.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "x")
275 second.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(0))
276 app := newTextApp(t, s, events, commands, dir)
277 app.enable(t)
278 settle(t, second.c, app.c, first.c)
279 app.log.take()
280 _ = first.c.Close()
281 deadline := time.Now().Add(2 * time.Second)
282 for len(app.log.events) == 0 && time.Now().Before(deadline) {
283 roundtrip(t, app.c)
284 time.Sleep(5 * time.Millisecond)
285 }
286 // The preedit is cleared by an empty done.
287 expectEvents(t, "app", app.log.take(), "done 1")
288 third := newIMEApp(t, s, dir)
289 expectEvents(t, "third", third.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
290}
291
292// until roundtrips cs until ok holds: commands reach the display
293// goroutine on their own channel, not in order with requests.
294func until(t *testing.T, what string, ok func() bool, cs ...*wlturbo.Display) {
295 t.Helper()
296 for deadline := time.Now().Add(2 * time.Second); !ok(); time.Sleep(5 * time.Millisecond) {
297 if time.Now().After(deadline) {
298 t.Fatal(what)
299 }
300 roundtrip(t, cs...)
301 }
302}
303
304// A keyboard grab takes the focused client's keys until released; a key
305// the client saw pressed is released to the client.
306func TestInputMethodKeyboardGrab(t *testing.T) {
307 s, events, commands, dir := keyboardServer(t)
308 app := newTextApp(t, s, events, commands, dir)
309 ime := newIMEApp(t, s, dir)
310 app.keys.opcodes = nil
311 key := func(code uint32, pressed bool) {
312 commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, TimeMsec: 7}}
313 }
314 k := uint16(wayland.KeyboardEventKey)
315 key(31, true) // held by the client before the grab
316 until(t, "client did not get the key before the grab", func() bool { return len(app.keys.opcodes) == 1 }, app.c)
317 g, grab := ime.grab(t)
318 settle(t, ime.c)
319 got := grab.take()
320 if len(got) != 3 || got[0] != "keymap 1 fd "+fmt.Sprint(len(keymapText(t))+1) || got[1] != "repeat_info 25 600" || !strings.HasPrefix(got[2], "modifiers ") {
321 t.Fatalf("grab setup %q", got)
322 }
323 key(30, true)
324 key(30, false)
325 key(31, false)
326 until(t, "client did not get the release of 31", func() bool { return len(app.keys.opcodes) == 2 }, app.c)
327 settle(t, ime.c)
328 if len(grab.events) != 2 || !strings.HasSuffix(grab.events[0], " 7 30 1") || !strings.HasSuffix(grab.events[1], " 7 30 0") {
329 t.Fatalf("grab keys %q", grab.events)
330 }
331 if !slices.Equal(app.keys.opcodes, []uint16{k, k}) {
332 t.Fatalf("client keys %v, want the press and release of 31", app.keys.opcodes)
333 }
334 // 33 is pressed through the grab and released after it ends: the
335 // client never sees it.
336 key(33, true)
337 until(t, "grab did not get 33", func() bool { return len(grab.events) == 3 }, ime.c)
338 requestProtocol(t, ime.c, g, inputmethod.ZwpInputMethodKeyboardGrabV2RequestRelease)
339 settle(t, ime.c)
340 app.keys.opcodes = nil
341 key(33, false)
342 key(32, true)
343 until(t, "key after release did not reach the client", func() bool { return slices.Contains(app.keys.opcodes, k) }, app.c)
344 settle(t, ime.c)
345 if len(grab.events) != 3 {
346 t.Fatalf("released grab got %q", grab.events)
347 }
348 keys := 0
349 for _, op := range app.keys.opcodes {
350 if op == k {
351 keys++
352 }
353 }
354 if keys != 1 {
355 t.Fatalf("client keys %v, want only the press of 32", app.keys.opcodes)
356 }
357}
358
359// Disconnects and destroyed surfaces in active states do not break the
360// seat: keys return to the client, the input method is deactivated.
361func TestTextInputTeardown(t *testing.T) {
362 s, events, commands, dir := keyboardServer(t)
363 ime := newIMEApp(t, s, dir)
364 app := newTextApp(t, s, events, commands, dir)
365 app.enable(t)
366 ime.grab(t)
367 settle(t, app.c, ime.c)
368 ime.log.take()
369
370 // The focused surface dies with an active text input.
371 requestProtocol(t, app.c, app.top, xdgshell.ToplevelRequestDestroy)
372 requestProtocol(t, app.c, app.surf, wayland.SurfaceRequestDestroy)
373 settle(t, app.c, ime.c)
374 unmapped(t, events, app.window)
375 waitFocus(t, s, 0)
376 settle(t, ime.c)
377 expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
378
379 other := newTextApp(t, s, events, commands, dir)
380 other.enable(t)
381 settle(t, other.c, ime.c)
382 ime.log.take()
383 _ = other.c.Close()
384 deadline := time.Now().Add(2 * time.Second)
385 for len(ime.log.events) == 0 && time.Now().Before(deadline) {
386 roundtrip(t, ime.c)
387 time.Sleep(5 * time.Millisecond)
388 }
389 expectEvents(t, "ime", ime.log.take(), "deactivate", "done")
390 unmapped(t, events, other.window)
391
392 // With the grabbing input method gone, keys reach the client again.
393 last := newTextApp(t, s, events, commands, dir)
394 _ = ime.c.Close()
395 last.keys.opcodes = nil
396 deadline = time.Now().Add(2 * time.Second)
397 for !slices.Contains(last.keys.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) {
398 commands <- ports.ForwardKey{ID: last.window, Key: ports.KeyEvent{Keycode: 30, Pressed: true}}
399 commands <- ports.ForwardKey{ID: last.window, Key: ports.KeyEvent{Keycode: 30}}
400 roundtrip(t, last.c)
401 time.Sleep(10 * time.Millisecond)
402 }
403 if !slices.Contains(last.keys.opcodes, uint16(wayland.KeyboardEventKey)) {
404 t.Fatal("keys did not return to the client")
405 }
406}
407
408// An input popup gets the text cursor rectangle; a surface with a role
409// cannot become one.
410func TestInputPopupSurface(t *testing.T) {
411 s, events, commands, dir := keyboardServer(t)
412 ime := newIMEApp(t, s, dir)
413 app := newTextApp(t, s, events, commands, dir)
414 comp := bindProtocol(t, ime.c, "wl_compositor")
415 surf, popup := ime.c.AllocateID(), ime.c.AllocateID()
416 registerProtocol(t, ime.c, surf)
417 requestProtocol(t, ime.c, comp, wayland.CompositorRequestCreateSurface, surf)
418 rects := newIMELog(ime.c, popup, "text_input_rectangle:iiii")
419 ime.request(t, inputmethod.ZwpInputMethodV2RequestGetInputPopupSurface, popup, surf)
420 app.request(t, textinput.ZwpTextInputV3RequestEnable)
421 app.request(t, textinput.ZwpTextInputV3RequestSetCursorRectangle, int32(10), int32(20), int32(2), int32(16))
422 app.request(t, textinput.ZwpTextInputV3RequestCommit)
423 settle(t, ime.c, app.c, ime.c)
424 expectEvents(t, "popup", rects.take(), "text_input_rectangle 10 20 2 16")
425
426 again := ime.c.AllocateID()
427 registerProtocol(t, ime.c, again)
428 ime.request(t, inputmethod.ZwpInputMethodV2RequestGetInputPopupSurface, again, surf)
429 expectProtocolError(t, ime.c, ime.im, uint32(inputmethod.ZwpInputMethodV2ErrorRole))
430}
431
432// tagged forwards a text input's events to a shared log, prefixed, so
433// the order across objects shows.
434type tagged struct {
435 wlturbo.BaseProxy
436 tag string
437 into *imeLog
438}
439
440func (p *tagged) Dispatch(e *wlturbo.Event) {
441 p.into.Dispatch(e)
442 last := &p.into.events[len(p.into.events)-1]
443 *last = p.tag + " " + *last
444}
445
446// On a focus change every leave comes before any enter, whatever the
447// order the text inputs were created in.
448func TestTextInputLeaveBeforeEnter(t *testing.T) {
449 s, events, commands, dir := keyboardServer(t)
450 c := protocolClient(t, s, dir)
451 seat := bindProtocol(t, c, "wl_seat")
452 registerProtocol(t, c, seat)
453 mapWindow := surfaceMapper(t, c, events)
454 a, aSurf, _ := mapWindow()
455 b, bSurf, _ := mapWindow()
456 manager := bindProtocol(t, c, "zwp_text_input_manager_v3")
457 registerProtocol(t, c, manager)
458 log := newIMELog(c, 0, textInputEvents...)
459 for _, tag := range []string{"t1", "t2"} {
460 p := &tagged{tag: tag, into: log}
461 p.SetID(c.AllocateID())
462 registerWireProxy(c, p)
463 requestProtocol(t, c, manager, textinput.ZwpTextInputManagerV3RequestGetTextInput, p.ID(), seat)
464 }
465 commands <- ports.FocusWindow{ID: a.ID}
466 waitFocus(t, s, a.ID)
467 settle(t, c)
468 log.take()
469 commands <- ports.FocusWindow{ID: b.ID}
470 waitFocus(t, s, b.ID)
471 settle(t, c)
472 expectEvents(t, "focus", log.take(), fmt.Sprint("t1 leave ", aSurf), fmt.Sprint("t2 leave ", aSurf), fmt.Sprint("t1 enter ", bSurf), fmt.Sprint("t2 enter ", bSurf))
473}
474
475// A commit sent for a previous activation does not reach the next text
476// input; one for the current activation does.
477func TestInputMethodCommitBeforeActivationDropped(t *testing.T) {
478 s, events, commands, dir := keyboardServer(t)
479 ime := newIMEApp(t, s, dir)
480 first := newTextApp(t, s, events, commands, dir)
481 first.enable(t)
482 settle(t, first.c, ime.c)
483 expectEvents(t, "ime", ime.log.take(), "activate", `surrounding_text "hello" 5 5`, "text_change_cause 0", "content_type 2 0", "done")
484 second := newTextApp(t, s, events, commands, dir)
485 second.log.take()
486 second.enable(t)
487 settle(t, second.c, ime.c)
488 if got := ime.log.take(); len(got) != 7 || got[0] != "deactivate" || got[2] != "activate" {
489 t.Fatalf("ime events %q", got)
490 }
491 // Serial 1 is first's activation; second's is 3.
492 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "late")
493 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(1))
494 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommitString, "ok")
495 ime.request(t, inputmethod.ZwpInputMethodV2RequestCommit, uint32(3))
496 settle(t, ime.c, second.c)
497 expectEvents(t, "second", second.log.take(), `commit_string "ok"`, "done 1")
498}
499
500// An unavailable input method's popup request has no effect: the surface
501// keeps no role.
502func TestInertInputMethodPopupIgnored(t *testing.T) {
503 s, _, _, dir := keyboardServer(t)
504 newIMEApp(t, s, dir)
505 inert := newIMEApp(t, s, dir)
506 inert.log.take()
507 comp := bindProtocol(t, inert.c, "wl_compositor")
508 surf := inert.c.AllocateID()
509 registerProtocol(t, inert.c, surf)
510 requestProtocol(t, inert.c, comp, wayland.CompositorRequestCreateSurface, surf)
511 for range 2 {
512 popup := inert.c.AllocateID()
513 registerProtocol(t, inert.c, popup)
514 inert.request(t, inputmethod.ZwpInputMethodV2RequestGetInputPopupSurface, popup, surf)
515 }
516 settle(t, inert.c)
517 expectEvents(t, "inert", inert.log.take())
518}