1//go:build linux
2
3package wayland
4
5import (
6 "context"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/bnema/nefergui/internal/harness"
15 "github.com/bnema/wlturbo/protocol/core"
16 "github.com/bnema/wlturbo/protocol/xdgshell"
17 "github.com/bnema/wlturbo/wl"
18 "golang.org/x/sys/unix"
19)
20
21// The child uses a plain shm toplevel so the test does not depend on a GPU
22// renderer. The parent runs the pinned compositor with its real input script.
23func TestHarnessInput(t *testing.T) {
24 if os.Getenv("NEFERGUI_HARNESS") != "1" {
25 t.Skip("set NEFERGUI_HARNESS=1 for NeferWL input injection")
26 }
27 script := filepath.Join(t.TempDir(), "input.txt")
28 if err := os.WriteFile(script, []byte("sleep 2s\nmove 100 100\nsleep 200ms\nclick left\nkey a\nsleep 500ms\n"), 0600); err != nil {
29 t.Fatal(err)
30 }
31 root := filepath.Join(t.TempDir(), "artifacts")
32 result, code := harness.Run(context.Background(), harness.Options{Size: "400x300", Scale: "1", Layout: "us", Background: "#111111", Out: root, AllowUnpinned: os.Getenv("NEFERGUI_NEFERWL") != "", Input: script, Timeout: 25 * time.Second, ReadyTimeout: 8 * time.Second, Client: []string{os.Args[0], "-test.run=^TestHarnessInputClient$", "-test.v"}})
33 if code != harness.Pass {
34 t.Fatalf("harness code=%d status=%s error=%s artifacts=%s", code, result.Status, result.Error, root)
35 }
36 data, err := os.ReadFile(result.Artifacts["client_stdout"])
37 if err != nil {
38 t.Fatal(err)
39 }
40 for _, want := range []string{"INPUT motion", "INPUT press", "INPUT release", "INPUT key a"} {
41 if !strings.Contains(string(data), want) {
42 t.Fatalf("missing %s in client output:\n%s", want, data)
43 }
44 }
45}
46
47func TestHarnessInputClient(t *testing.T) {
48 if os.Getenv("NEFERGUI_DEBUG_DIR") == "" {
49 t.Skip("harness child only")
50 }
51 d, err := wl.Connect("")
52 if err != nil {
53 t.Fatal(err)
54 }
55 defer d.Close()
56 if err = d.Roundtrip(); err != nil {
57 t.Fatal(err)
58 }
59 ctx := d.Context()
60 compositor := core.NewCompositor(ctx)
61 if _, err = d.Registry().BindNegotiated(core.CompositorInterface, 6, compositor); err != nil {
62 t.Fatal(err)
63 }
64 shell := xdgshell.NewXdgWmBase(ctx)
65 if _, err = d.Registry().BindNegotiated(xdgshell.XdgWmBaseInterface, 6, shell); err != nil {
66 t.Fatal(err)
67 }
68 shm := core.NewShm(ctx)
69 if _, err = d.Registry().BindNegotiated(core.ShmInterface, 1, shm); err != nil {
70 t.Fatal(err)
71 }
72 surface, err := compositor.CreateSurface()
73 if err != nil {
74 t.Fatal(err)
75 }
76 xdg, err := shell.GetXdgSurface(surface)
77 if err != nil {
78 t.Fatal(err)
79 }
80 if _, err = xdg.GetToplevel(); err != nil {
81 t.Fatal(err)
82 }
83 configured := false
84 xdg.OnConfigure(func(serial uint32) {
85 if e := xdg.AckConfigure(serial); e != nil {
86 t.Error(e)
87 }
88 configured = true
89 })
90 if err = surface.Commit(); err != nil {
91 t.Fatal(err)
92 }
93 for !configured {
94 if err = d.Dispatch(); err != nil {
95 t.Fatal(err)
96 }
97 }
98 fd, err := unix.MemfdCreate("nefergui-harness-input", unix.MFD_CLOEXEC)
99 if err != nil {
100 t.Fatal(err)
101 }
102 if err = unix.Ftruncate(fd, 400*300*4); err != nil {
103 t.Fatal(err)
104 }
105 pool, err := shm.CreatePool(fd, 400*300*4) // consumes fd on success
106 if err != nil {
107 _ = unix.Close(fd)
108 t.Fatal(err)
109 }
110 buffer, err := pool.CreateBuffer(0, 400, 300, 400*4, 0) // wl_shm XRGB8888
111 if err != nil {
112 t.Fatal(err)
113 }
114 if err = surface.Attach(buffer, 0, 0); err != nil {
115 t.Fatal(err)
116 }
117 if err = surface.DamageBuffer(0, 0, 400, 300); err != nil {
118 t.Fatal(err)
119 }
120 if err = surface.Commit(); err != nil {
121 t.Fatal(err)
122 }
123 w := &Window{Display: d, Surface: surface, events: make(chan Event, 128), readerStop: make(chan struct{})}
124 if err = w.BindSeat(); err != nil {
125 t.Fatal(err)
126 }
127 adapter, err := NewInputAdapter("en_US.UTF-8")
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer adapter.Close()
132 deadline := time.After(6 * time.Second)
133 seen := map[string]bool{}
134 for !(seen["motion"] && seen["press"] && seen["release"] && seen["key a"]) {
135 select {
136 case <-deadline:
137 t.Fatalf("timed out waiting for injected input: %v", seen)
138 default:
139 }
140 if err = d.Dispatch(); err != nil {
141 t.Fatal(err)
142 }
143 for len(w.events) > 0 {
144 ev := <-w.events
145 switch ev.Kind {
146 case SeatCapabilities, SeatRemoved:
147 if err = w.ApplySeat(ev); err != nil {
148 t.Fatal(err)
149 }
150 case InputFrame, InputKeymap, InputFocusIn, InputFocusOut, InputKey, InputModifiers, InputRepeatInfo:
151 inputs, e := adapter.ApplyInput(ev, time.Now())
152 if e != nil {
153 t.Fatal(e)
154 }
155 for _, in := range inputs {
156 kind := in.Kind
157 if kind == "key" && in.Key.Pressed && in.Key.Text == "a" {
158 kind = "key a"
159 }
160 if kind == "motion" || kind == "press" || kind == "release" || kind == "key a" {
161 seen[kind] = true
162 fmt.Println("INPUT", kind)
163 }
164 }
165 }
166 }
167 }
168}