1//go:build linux
2
3package wayland
4
5import (
6 "encoding/binary"
7 "strings"
8 "testing"
9 "time"
10 "unicode/utf8"
11
12 "github.com/bnema/wlturbo"
13 "github.com/bnema/wlturbo/protocol/core"
14 "github.com/bnema/wlturbo/protocol/textinput"
15)
16
17func TestSurroundingUTF8Window(t *testing.T) {
18 s := strings.Repeat("é", 3000)
19 for _, pos := range []int{0, 1, 2000, 3999, len(s)} {
20 text, cursor, anchor := surroundingWindow(s, pos, pos)
21 if len(text) > 4000 || !strings.Contains(s, text) || cursor < 0 || cursor > len(text) || anchor != cursor {
22 t.Fatalf("window at %d: len=%d cursor=%d", pos, len(text), cursor)
23 }
24 }
25}
26func imeStringFrame(id uint32, opcode uint16, s string, tail ...uint32) []byte {
27 data := append([]byte(s), 0)
28 padded := make([]byte, (len(data)+3)&^3)
29 copy(padded, data)
30 frame := inputFrame(id, opcode, uint32(len(data)))
31 frame = append(frame, padded...)
32 for _, v := range tail {
33 b := make([]byte, 4)
34 binary.LittleEndian.PutUint32(b, v)
35 frame = append(frame, b...)
36 }
37 binary.LittleEndian.PutUint32(frame[4:], uint32(len(frame))<<16|uint32(opcode))
38 return frame
39}
40func TestIMEBatchSerialOverSocketpair(t *testing.T) {
41 ca, server := inputPair(t)
42 d, err := wlturbo.ConnectFromConn(ca)
43 if err != nil {
44 t.Fatal(err)
45 }
46 defer d.Close()
47 inputRequest(t, server)
48 manager := textinput.NewTextInputManagerV3(d.Context())
49 if err = d.Registry().Bind(1, textinput.TextInputManagerV3Interface, 2, manager); err != nil {
50 t.Fatal(err)
51 }
52 inputRequest(t, server)
53 seat := core.NewSeat(d.Context())
54 seat.SetID(20)
55 d.Context().Register(seat)
56 surface := core.NewSurface(d.Context())
57 surface.SetID(30)
58 d.Context().Register(surface)
59 input, err := manager.GetTextInput(seat)
60 if err != nil {
61 t.Fatal(err)
62 }
63 inputRequest(t, server)
64 events := make(chan IMEEvent, 10)
65 m := &IME{input: input, surface: surface.ID(), post: func(e IMEEvent) { events <- e }}
66 input.OnEnter(func(id uint32) { m.post(IMEEvent{Kind: "enter", Surface: id}) })
67 input.OnPreeditString(func(s string, b, e int32) { m.post(IMEEvent{Kind: "preedit", Text: s, Begin: b, End: e}) })
68 input.OnCommitString(func(s string) { m.post(IMEEvent{Kind: "commit", Text: s}) })
69 input.OnDeleteSurroundingText(func(b, a uint32) { m.post(IMEEvent{Kind: "delete", Before: b, After: a}) })
70 input.OnDone(func(serial uint32) { m.post(IMEEvent{Kind: "done", Serial: serial}) })
71 m.Enable() // requests before surface enter are remembered
72 inputSend(t, server, inputFrame(input.ID(), 0, surface.ID()))
73 if err = d.Dispatch(); err != nil {
74 t.Fatal(err)
75 }
76 m.ApplyIME(<-events)
77 if m.commits != 1 || !m.enabled {
78 t.Fatalf("enable %d %v", m.commits, m.enabled)
79 }
80 inputRequest(t, server) // enable; commit might be in the same read
81 for _, msg := range [][]byte{
82 imeStringFrame(input.ID(), 2, "é", 0, 2),
83 imeStringFrame(input.ID(), 3, "x"),
84 inputFrame(input.ID(), 4, 1, 0),
85 inputFrame(input.ID(), 5, 0), // mismatched serial MUST still apply edit
86 } {
87 inputSend(t, server, msg)
88 if err = d.Dispatch(); err != nil {
89 t.Fatal(err)
90 }
91 }
92 for i := 0; i < 3; i++ {
93 if _, ok := m.ApplyIME(<-events); ok {
94 t.Fatal("partial batch delivered")
95 }
96 }
97 batch, ok := m.ApplyIME(<-events)
98 if !ok || batch.Preedit != "é" || batch.Commit != "x" || batch.DeleteBefore != 1 || m.synced {
99 t.Fatalf("batch %+v ok=%v synced=%v", batch, ok, m.synced)
100 }
101 m.ContentType(true, false)
102 if !m.pendingContent {
103 t.Fatal("mismatched done changed protocol state")
104 }
105 m.ApplyIME(IMEEvent{Kind: "done", Serial: 1})
106 if !m.synced || m.commits <= 1 {
107 t.Fatal("pending state not flushed after matching done")
108 }
109}
110
111func TestSurroundingWindowRuneBoundaries(t *testing.T) {
112 s := strings.Repeat("é", 3500)
113 for _, pos := range []int{0, 1, 3999, len(s) - 1, len(s)} {
114 window, cursor, anchor := surroundingWindow(s, pos, len(s))
115 if len(window) > 4000 || !utf8.ValidString(window) || !utf8.ValidString(window[:cursor]) || !utf8.ValidString(window[:anchor]) {
116 t.Fatalf("invalid window at %d: len=%d cursor=%d anchor=%d", pos, len(window), cursor, anchor)
117 }
118 }
119}
120
121func TestIMEPasswordContentWire(t *testing.T) {
122 ca, server := inputPair(t)
123 d, err := wlturbo.ConnectFromConn(ca)
124 if err != nil {
125 t.Fatal(err)
126 }
127 defer d.Close()
128 inputRequest(t, server)
129 input := textinput.NewTextInputV3(d.Context())
130 input.SetID(20)
131 d.Context().Register(input)
132 m := &IME{input: input, entered: true, enabled: true, synced: true}
133 m.ContentType(true, true)
134 buf := make([]byte, 128)
135 _ = server.SetReadDeadline(time.Now().Add(time.Second))
136 n, err := server.Read(buf)
137 if err != nil {
138 t.Fatal(err)
139 }
140 if n < 16 || binary.LittleEndian.Uint32(buf[:4]) != input.ID() || binary.LittleEndian.Uint16(buf[4:]) != 5 {
141 t.Fatalf("unexpected content request: %x", buf[:n])
142 }
143 hint := binary.LittleEndian.Uint32(buf[8:12])
144 purpose := binary.LittleEndian.Uint32(buf[12:16])
145 if hint != textinput.CONTENT_HINT_SENSITIVE_DATA|textinput.CONTENT_HINT_HIDDEN_TEXT || purpose != textinput.CONTENT_PURPOSE_PASSWORD {
146 t.Fatalf("password hint=%d purpose=%d", hint, purpose)
147 }
148}
149
150func TestIMEInitialStateOneCommit(t *testing.T) {
151 ca, server := inputPair(t)
152 d, err := wlturbo.ConnectFromConn(ca)
153 if err != nil {
154 t.Fatal(err)
155 }
156 defer d.Close()
157 inputRequest(t, server)
158 input := textinput.NewTextInputV3(d.Context())
159 input.SetID(20)
160 d.Context().Register(input)
161 m := &IME{input: input, entered: true}
162 m.Surrounding("hello", 5, 5)
163 m.ContentType(true, false)
164 m.CursorRect(1, 2, 3, 4)
165 m.Enable()
166 if m.Err != nil || m.commits != 1 {
167 t.Fatalf("enable: %v commits=%d", m.Err, m.commits)
168 }
169 buf := make([]byte, 256)
170 _ = server.SetReadDeadline(time.Now().Add(time.Second))
171 n, err := server.Read(buf)
172 if err != nil {
173 t.Fatal(err)
174 }
175 var ops []uint16
176 for offset := 0; offset < n; {
177 size := int(binary.LittleEndian.Uint16(buf[offset+6:]))
178 if size < 8 || offset+size > n {
179 t.Fatalf("invalid frame: %x", buf[:n])
180 }
181 ops = append(ops, binary.LittleEndian.Uint16(buf[offset+4:]))
182 offset += size
183 }
184 if len(ops) != 5 || ops[0] != 1 || ops[1] != 5 || ops[2] != 3 || ops[3] != 6 || ops[4] != 7 {
185 t.Fatalf("request order %v", ops)
186 }
187}