package headlessinput import ( "context" "strings" "testing" "testing/synctest" "time" "github.com/bnema/neferwl/internal/adapters/logging" "github.com/bnema/neferwl/internal/adapters/xkb" "github.com/bnema/neferwl/internal/ports" ) func TestScript(t *testing.T) { for _, tt := range []struct { line string names []string codes []uint32 }{ {"type Hi!", []string{"Shift_L", "H", "H", "Shift_L", "i", "i", "Shift_L", "exclam", "exclam", "Shift_L"}, nil}, {"key Shift+A", []string{"Shift_L", "A", "A", "Shift_L"}, []uint32{42, 30, 30, 42}}, {"key Super+Return", []string{"Super_L", "Return", "Return", "Super_L"}, []uint32{125, 28, 28, 125}}, {"hold Super+h 20ms", []string{"Super_L", "h", "h", "Super_L"}, []uint32{125, 35, 35, 125}}, {"hold h", nil, nil}, {"press Super", []string{"Super_L"}, []uint32{125}}, {"release Super", []string{"Super_L"}, []uint32{125}}, {"press Shift", []string{"Shift_L"}, []uint32{42}}, {"press Ctrl", []string{"Control_L"}, []uint32{29}}, {"press Alt", []string{"Alt_L"}, []uint32{56}}, {"press", nil, nil}, {"release Super extra", nil, nil}, {"press unknown", nil, nil}, } { t.Run(tt.line, func(t *testing.T) { km, err := xkb.New(xkb.RMLVO{Layout: "us"}) if err != nil { if strings.Contains(err.Error(), "libxkbcommon.so.0") { t.Skip(err) } t.Fatal(err) } script := make(chan string, 1) script <- tt.line close(script) input := make(chan ports.InputEvent, 32) if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil { t.Fatal(err) } if len(input) != len(tt.names) { t.Fatalf("events=%d want %d", len(input), len(tt.names)) } for i, name := range tt.names { ev := (<-input).(ports.KeyEvent) if ev.Keysym != name { t.Errorf("event %d: %s want %s", i, ev.Keysym, name) } if tt.codes != nil && ev.Keycode != tt.codes[i] { t.Errorf("code %d: %d", i, ev.Keycode) } if tt.line == "key Super+Return" && i == 1 && ev.Mods&ports.ModSuper == 0 { t.Error("Return missing Super") } if tt.line == "key Super+Return" && i == 3 && ev.Mods&ports.ModSuper != 0 { t.Error("Super not released") } } }) } } func TestModifierAcrossMouseActions(t *testing.T) { km, err := xkb.New(xkb.RMLVO{Layout: "us"}) if err != nil { t.Fatal(err) } script := make(chan string, 8) for _, line := range []string{"press Super", "move 500 100", "down right", "move 700 100", "up right", "release Super"} { script <- line } close(script) input := make(chan ports.InputEvent, 16) if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil { t.Fatal(err) } if len(input) != 6 { t.Fatal(len(input)) } press := (<-input).(ports.KeyEvent) if !press.Pressed || press.Mods&ports.ModSuper == 0 { t.Fatal(press) } if _, ok := (<-input).(ports.PointerMotion); !ok { t.Fatal("missing motion") } down := (<-input).(ports.PointerButton) if down.Button != 0x111 || !down.Pressed { t.Fatal(down) } if _, ok := (<-input).(ports.PointerMotion); !ok { t.Fatal("missing drag motion") } up := (<-input).(ports.PointerButton) if up.Button != 0x111 || up.Pressed { t.Fatal(up) } release := (<-input).(ports.KeyEvent) if release.Pressed || release.Mods&ports.ModSuper != 0 { t.Fatal(release) } } // hold keeps the key and its modifiers down for the given duration, then // releases the key before the modifier. func TestHoldKeepsKeyDown(t *testing.T) { synctest.Test(t, func(t *testing.T) { km, err := xkb.New(xkb.RMLVO{Layout: "us"}) if err != nil { t.Skip(err) } script := make(chan string, 1) script <- "hold Super+h 1s" close(script) input := make(chan ports.InputEvent, 8) done := make(chan error, 1) go func() { done <- Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")) }() time.Sleep(900 * time.Millisecond) synctest.Wait() if len(input) != 2 { t.Fatalf("before the hold ends: %d events, want the 2 presses", len(input)) } if err := <-done; err != nil { t.Fatal(err) } for i, want := range []struct { name string pressed bool super bool }{ {"Super_L", true, true}, {"h", true, true}, {"h", false, true}, {"Super_L", false, false}, } { ev := (<-input).(ports.KeyEvent) if ev.Keysym != want.name || ev.Pressed != want.pressed || (ev.Mods&ports.ModSuper != 0) != want.super { 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) } } }) } func TestPointerScript(t *testing.T) { km, err := xkb.New(xkb.RMLVO{Layout: "us"}) if err != nil { t.Skip(err) } script := make(chan string, 8) for _, line := range []string{"move 12.5 45", "click", "down right", "up right", "swipe sideways", "swipe left", "swipe4 up"} { script <- line } close(script) input := make(chan ports.InputEvent, 32) if err := Run(context.Background(), km, nil, script, input, nil, nil, logging.For(context.Background(), "input")); err != nil { t.Fatal(err) } if len(input) != 5+2*(swipeSteps+2) { t.Fatalf("events: %d", len(input)) } m := (<-input).(ports.PointerMotion) if m.X != 12.5 || m.Y != 45 { t.Fatal(m) } for _, want := range []ports.PointerButton{{Button: 0x110, Pressed: true}, {Button: 0x110}, {Button: 0x111, Pressed: true}, {Button: 0x111}} { got := (<-input).(ports.PointerButton) if got.Button != want.Button || got.Pressed != want.Pressed { t.Fatalf("got %+v want %+v", got, want) } } begin := (<-input).(ports.SwipeBegin) if begin.Fingers != 3 { t.Fatal(begin) } last := begin.Time for range swipeSteps { u := (<-input).(ports.SwipeUpdate) if u.DX >= 0 || u.DY != 0 || u.Time <= last { t.Fatalf("update %+v after %v", u, last) } last = u.Time } if e := (<-input).(ports.SwipeEnd); e.Cancelled || e.Time != last { t.Fatal(e) } if b := (<-input).(ports.SwipeBegin); b.Fingers != 4 { t.Fatal(b) } if u := (<-input).(ports.SwipeUpdate); u.DY >= 0 || u.DX != 0 { t.Fatal(u) } } // The cursor is placed in target pixels: after the output transform. func TestMoveCursorTarget(t *testing.T) { km, err := xkb.New(xkb.RMLVO{Layout: "us"}) if err != nil { t.Skip(err) } layouts := make(chan ports.Layout) script := make(chan string) input := make(chan ports.InputEvent, 4) type pos struct { out string x, y float64 } moved := make(chan pos, 1) done := make(chan error, 1) go func() { 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")) }() // Unbuffered: the layout is taken before the move is read. layouts <- ports.Layout{{Info: ports.OutputInfo{Name: "A", Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}} script <- "move 10 20" if got, want := <-moved, (pos{"A", 20, 90}); got != want { t.Fatalf("got %+v want %+v", got, want) } close(script) if err := <-done; err != nil { t.Fatal(err) } }