1package headlessinput
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "testing/synctest"
8 "time"
9
10 "github.com/bnema/neferwl/internal/adapters/logging"
11 "github.com/bnema/neferwl/internal/adapters/xkb"
12 "github.com/bnema/neferwl/internal/ports"
13)
14
15func TestScript(t *testing.T) {
16 for _, tt := range []struct {
17 line string
18 names []string
19 codes []uint32
20 }{
21 {"type Hi!", []string{"Shift_L", "H", "H", "Shift_L", "i", "i", "Shift_L", "exclam", "exclam", "Shift_L"}, nil},
22 {"key Shift+A", []string{"Shift_L", "A", "A", "Shift_L"}, []uint32{42, 30, 30, 42}},
23 {"key Super+Return", []string{"Super_L", "Return", "Return", "Super_L"}, []uint32{125, 28, 28, 125}},
24 {"hold Super+h 20ms", []string{"Super_L", "h", "h", "Super_L"}, []uint32{125, 35, 35, 125}},
25 {"hold h", nil, nil},
26 {"press Super", []string{"Super_L"}, []uint32{125}},
27 {"release Super", []string{"Super_L"}, []uint32{125}},
28 {"press Shift", []string{"Shift_L"}, []uint32{42}},
29 {"press Ctrl", []string{"Control_L"}, []uint32{29}},
30 {"press Alt", []string{"Alt_L"}, []uint32{56}},
31 {"press", nil, nil},
32 {"release Super extra", nil, nil},
33 {"press unknown", nil, nil},
34 } {
35 t.Run(tt.line, func(t *testing.T) {
36 km, err := xkb.New(xkb.RMLVO{Layout: "us"})
37 if err != nil {
38 if strings.Contains(err.Error(), "libxkbcommon.so.0") {
39 t.Skip(err)
40 }
41 t.Fatal(err)
42 }
43 script := make(chan string, 1)
44 script <- tt.line
45 close(script)
46 input := make(chan ports.InputEvent, 32)
47 if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
48 t.Fatal(err)
49 }
50 if len(input) != len(tt.names) {
51 t.Fatalf("events=%d want %d", len(input), len(tt.names))
52 }
53 for i, name := range tt.names {
54 ev := (<-input).(ports.KeyEvent)
55 if ev.Keysym != name {
56 t.Errorf("event %d: %s want %s", i, ev.Keysym, name)
57 }
58 if tt.codes != nil && ev.Keycode != tt.codes[i] {
59 t.Errorf("code %d: %d", i, ev.Keycode)
60 }
61 if tt.line == "key Super+Return" && i == 1 && ev.Mods&ports.ModSuper == 0 {
62 t.Error("Return missing Super")
63 }
64 if tt.line == "key Super+Return" && i == 3 && ev.Mods&ports.ModSuper != 0 {
65 t.Error("Super not released")
66 }
67 }
68 })
69 }
70}
71
72func TestModifierAcrossMouseActions(t *testing.T) {
73 km, err := xkb.New(xkb.RMLVO{Layout: "us"})
74 if err != nil {
75 t.Fatal(err)
76 }
77 script := make(chan string, 8)
78 for _, line := range []string{"press Super", "move 500 100", "down right", "move 700 100", "up right", "release Super"} {
79 script <- line
80 }
81 close(script)
82 input := make(chan ports.InputEvent, 16)
83 if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
84 t.Fatal(err)
85 }
86 if len(input) != 6 {
87 t.Fatal(len(input))
88 }
89 press := (<-input).(ports.KeyEvent)
90 if !press.Pressed || press.Mods&ports.ModSuper == 0 {
91 t.Fatal(press)
92 }
93 if _, ok := (<-input).(ports.PointerMotion); !ok {
94 t.Fatal("missing motion")
95 }
96 down := (<-input).(ports.PointerButton)
97 if down.Button != 0x111 || !down.Pressed {
98 t.Fatal(down)
99 }
100 if _, ok := (<-input).(ports.PointerMotion); !ok {
101 t.Fatal("missing drag motion")
102 }
103 up := (<-input).(ports.PointerButton)
104 if up.Button != 0x111 || up.Pressed {
105 t.Fatal(up)
106 }
107 release := (<-input).(ports.KeyEvent)
108 if release.Pressed || release.Mods&ports.ModSuper != 0 {
109 t.Fatal(release)
110 }
111}
112
113// hold keeps the key and its modifiers down for the given duration, then
114// releases the key before the modifier.
115func TestHoldKeepsKeyDown(t *testing.T) {
116 synctest.Test(t, func(t *testing.T) {
117 km, err := xkb.New(xkb.RMLVO{Layout: "us"})
118 if err != nil {
119 t.Skip(err)
120 }
121 script := make(chan string, 1)
122 script <- "hold Super+h 1s"
123 close(script)
124 input := make(chan ports.InputEvent, 8)
125 done := make(chan error, 1)
126 go func() {
127 done <- Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input"))
128 }()
129 time.Sleep(900 * time.Millisecond)
130 synctest.Wait()
131 if len(input) != 2 {
132 t.Fatalf("before the hold ends: %d events, want the 2 presses", len(input))
133 }
134 if err := <-done; err != nil {
135 t.Fatal(err)
136 }
137 for i, want := range []struct {
138 name string
139 pressed bool
140 super bool
141 }{
142 {"Super_L", true, true},
143 {"h", true, true},
144 {"h", false, true},
145 {"Super_L", false, false},
146 } {
147 ev := (<-input).(ports.KeyEvent)
148 if ev.Keysym != want.name || ev.Pressed != want.pressed || (ev.Mods&ports.ModSuper != 0) != want.super {
149 t.Errorf("event %d: %s pressed=%v mods=%v, want %s pressed=%v super=%v", i, ev.Keysym, ev.Pressed, ev.Mods, want.name, want.pressed, want.super)
150 }
151 }
152 })
153}
154
155func TestPointerScript(t *testing.T) {
156 km, err := xkb.New(xkb.RMLVO{Layout: "us"})
157 if err != nil {
158 t.Skip(err)
159 }
160 script := make(chan string, 8)
161 for _, line := range []string{"move 12.5 45", "click", "down right", "up right", "swipe sideways", "swipe left", "swipe4 up"} {
162 script <- line
163 }
164 close(script)
165 input := make(chan ports.InputEvent, 32)
166 if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil {
167 t.Fatal(err)
168 }
169 if len(input) != 5+2*(swipeSteps+2) {
170 t.Fatalf("events: %d", len(input))
171 }
172 m := (<-input).(ports.PointerMotion)
173 if m.X != 12.5 || m.Y != 45 {
174 t.Fatal(m)
175 }
176 for _, want := range []ports.PointerButton{{Button: 0x110, Pressed: true}, {Button: 0x110}, {Button: 0x111, Pressed: true}, {Button: 0x111}} {
177 got := (<-input).(ports.PointerButton)
178 if got.Button != want.Button || got.Pressed != want.Pressed {
179 t.Fatalf("got %+v want %+v", got, want)
180 }
181 }
182 begin := (<-input).(ports.SwipeBegin)
183 if begin.Fingers != 3 {
184 t.Fatal(begin)
185 }
186 last := begin.Time
187 for range swipeSteps {
188 u := (<-input).(ports.SwipeUpdate)
189 if u.DX >= 0 || u.DY != 0 || u.Time <= last {
190 t.Fatalf("update %+v after %v", u, last)
191 }
192 last = u.Time
193 }
194 if e := (<-input).(ports.SwipeEnd); e.Cancelled || e.Time != last {
195 t.Fatal(e)
196 }
197 if b := (<-input).(ports.SwipeBegin); b.Fingers != 4 {
198 t.Fatal(b)
199 }
200 if u := (<-input).(ports.SwipeUpdate); u.DY >= 0 || u.DX != 0 {
201 t.Fatal(u)
202 }
203}
204
205// The cursor is placed in target pixels: after the output transform.
206func TestMoveCursorTarget(t *testing.T) {
207 km, err := xkb.New(xkb.RMLVO{Layout: "us"})
208 if err != nil {
209 t.Skip(err)
210 }
211 layouts := make(chan ports.Layout)
212 script := make(chan string)
213 input := make(chan ports.InputEvent, 4)
214 type pos struct {
215 out string
216 x, y float64
217 }
218 moved := make(chan pos, 1)
219 done := make(chan error, 1)
220 go func() {
221 done <- Run(context.Background(), km, nil, script, input, layouts, func(o string, x, y float64) { moved <- pos{o, x, y} }, logging.For(context.Background(), "input"))
222 }()
223 // Unbuffered: the layout is taken before the move is read.
224 layouts <- ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}}
225 script <- "move 10 20"
226 if got, want := <-moved, (pos{"A", 20, 90}); got != want {
227 t.Fatalf("got %+v want %+v", got, want)
228 }
229 close(script)
230 if err := <-done; err != nil {
231 t.Fatal(err)
232 }
233}