v0.1.0 47aaa0f

ime_test.go RawHistoryFolder

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}

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 1

Tags 12

↑↓ moveenter openesc close