clientgesture_test.go RawHistoryFolder

1package core_test
2
3import (
4	"context"
5	"sync/atomic"
6	"testing"
7	"time"
8
9	"github.com/bnema/neferwl/internal/adapters/config"
10	"github.com/bnema/neferwl/internal/core"
11	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12	"github.com/bnema/neferwl/internal/ports"
13)
14
15// gestureRig is a rig with window 1 mapped on the left output and the
16// pointer on it. The right output is empty: the pointer there has no focus.
17func gestureRig(t *testing.T) *multiRig {
18	t.Helper()
19	r := startMulti(t, nil, left, right)
20	r.mapWindow(t, 1)
21	r.input <- ports.PointerMotion{X: 50, Y: 50}
22	if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
23		t.Fatal(v)
24	}
25	if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) {
26		t.Fatal(v)
27	}
28	return r
29}
30
31// isPointerCommand reports whether a command is one the gesture tests follow:
32// the pointer's and the gestures'.
33func isPointerCommand(cmd ports.ClientCommand) bool {
34	switch cmd.(type) {
35	case ports.PointerFocus, ports.PointerMotionTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
36		return true
37	}
38	return false
39}
40
41// pointerCommand receives the next pointer or gesture command, skipping
42// the configures and focus changes between them.
43func pointerCommand(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand {
44	t.Helper()
45	for {
46		if v := command(t, ch); isPointerCommand(v) {
47			return v
48		}
49	}
50}
51
52// idle fails the test when core sent a command meanwhile. A marker input
53// goes first, so the check waits for core to have handled what came before.
54func (r *multiRig) idle(t *testing.T) {
55	t.Helper()
56	r.input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}
57	if v := pointerCommand(t, r.commands); v != (ports.PointerAxisTo{ID: 1, Axis: ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}}) {
58		t.Fatalf("unexpected command %#v", v)
59	}
60}
61
62func TestPinchGoesToPointerFocus(t *testing.T) {
63	r := gestureRig(t)
64	r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
65	r.input <- ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second}
66	r.input <- ports.PinchEnd{Time: 3 * time.Second}
67	for _, want := range []ports.ClientCommand{
68		ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
69		ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second},
70		ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Time: 3 * time.Second},
71	} {
72		if v := pointerCommand(t, r.commands); v != want {
73			t.Fatalf("got %#v, want %#v", v, want)
74		}
75	}
76}
77
78// A removed touchpad ends its gesture with no time: the client gets the
79// last one, so its clock never runs backwards.
80func TestGestureEndWithoutTime(t *testing.T) {
81	r := gestureRig(t)
82	r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
83	r.input <- ports.PinchUpdate{Scale: 1, Time: 2 * time.Second}
84	r.input <- ports.PinchEnd{Cancelled: true}
85	pointerCommand(t, r.commands)
86	pointerCommand(t, r.commands)
87	if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
88		t.Fatal(v)
89	}
90}
91
92func TestHoldGoesToPointerFocus(t *testing.T) {
93	r := gestureRig(t)
94	r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
95	r.input <- ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}
96	if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
97		t.Fatal(v)
98	}
99	if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: 2 * time.Second}) {
100		t.Fatal(v)
101	}
102}
103
104// Swipes of other finger counts than three and four go to the client; the
105// compositor keeps those.
106func TestSwipeFingerCounts(t *testing.T) {
107	for _, fingers := range []int{2, 5} {
108		r := gestureRig(t)
109		r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
110		// The client gets the accelerated deltas.
111		r.input <- ports.SwipeUpdate{DX: 3, DY: 4, AccelDX: 6, AccelDY: 8, Time: 2 * time.Second}
112		r.input <- ports.SwipeEnd{Time: 3 * time.Second}
113		for _, want := range []ports.ClientCommand{
114			ports.GestureBeginTo{ID: 1, Kind: ports.GestureSwipe, Fingers: fingers, Time: time.Second},
115			ports.GestureUpdateTo{ID: 1, Kind: ports.GestureSwipe, DX: 6, DY: 8, Time: 2 * time.Second},
116			ports.GestureEndTo{ID: 1, Kind: ports.GestureSwipe, Time: 3 * time.Second},
117		} {
118			if v := pointerCommand(t, r.commands); v != want {
119				t.Fatalf("%d fingers: got %#v, want %#v", fingers, v, want)
120			}
121		}
122	}
123	for _, fingers := range []int{3, 4} {
124		r := gestureRig(t)
125		r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
126		r.input <- ports.SwipeUpdate{DX: 40, DY: 0, Time: 2 * time.Second}
127		r.input <- ports.SwipeEnd{Time: 3 * time.Second}
128		r.idle(t)
129	}
130}
131
132// A gesture that began with no window under the pointer reaches nobody,
133// even when the pointer enters a window meanwhile.
134func TestGestureWithoutPointerFocus(t *testing.T) {
135	r := gestureRig(t)
136	r.input <- ports.PointerMotion{X: 300, Y: 50}
137	if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
138		t.Fatal(v)
139	}
140	r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
141	r.input <- ports.PointerMotion{X: 50, Y: 50}
142	if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
143		t.Fatal(v)
144	}
145	if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) {
146		t.Fatal(v)
147	}
148	r.input <- ports.PinchUpdate{Scale: 2, Time: 2 * time.Second}
149	r.input <- ports.PinchEnd{Time: 3 * time.Second}
150	r.idle(t)
151}
152
153// The window losing the pointer focus gets its gesture cancelled before the
154// leave, and nothing of the rest.
155func TestGestureCancelledOnFocusChange(t *testing.T) {
156	r := gestureRig(t)
157	r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
158	if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}) {
159		t.Fatal(v)
160	}
161	r.input <- ports.PinchUpdate{Scale: 1.1, Time: 2 * time.Second}
162	if v := pointerCommand(t, r.commands); v != (ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, Scale: 1.1, Time: 2 * time.Second}) {
163		t.Fatal(v)
164	}
165	r.input <- ports.PointerMotion{X: 300, Y: 50}
166	if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
167		t.Fatal(v)
168	}
169	if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
170		t.Fatal(v)
171	}
172	r.input <- ports.PinchUpdate{Scale: 1.2, Time: 3 * time.Second}
173	r.input <- ports.PinchEnd{Time: 4 * time.Second}
174	r.input <- ports.PointerMotion{X: 50, Y: 50}
175	if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
176		t.Fatal(v)
177	}
178}
179
180// A window that goes away mid-gesture has its gesture cancelled with the
181// pointer focus.
182func TestGestureCancelledOnUnmap(t *testing.T) {
183	r := gestureRig(t)
184	r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
185	if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
186		t.Fatal(v)
187	}
188	r.client <- ports.WindowUnmapped{ID: 1}
189	if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: time.Second}) {
190		t.Fatal(v)
191	}
192	if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
193		t.Fatal(v)
194	}
195}
196
197// A gesture whose end was lost ends cancelled when the next one begins.
198func TestGestureBeginEndsPrevious(t *testing.T) {
199	r := gestureRig(t)
200	r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
201	r.input <- ports.HoldBegin{Fingers: 3, Time: 2 * time.Second}
202	for _, want := range []ports.ClientCommand{
203		ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
204		ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second},
205		ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: 2 * time.Second},
206	} {
207		if v := pointerCommand(t, r.commands); v != want {
208			t.Fatalf("got %#v, want %#v", v, want)
209		}
210	}
211}
212
213// A protected session forwards no gesture to a window; one running when
214// the session locks ends with the pointer focus.
215func TestGesturesRefusedWhileLocked(t *testing.T) {
216	var epoch atomic.Value
217	epoch.Store(ports.SecurityState{})
218	gate := portsmocks.NewMockSessionSecurity(t)
219	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
220	r := &multiRig{
221		client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 32),
222		output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4),
223		commands: make(chan ports.ClientCommand, 256), scenes: make(chan []ports.Scene, 1),
224	}
225	cfg := config.Defaults()
226	c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Constraints: make(chan ports.PointerConstraint, 1)}, core.Options{Security: gate})
227	if err != nil {
228		t.Fatal(err)
229	}
230	ctx, cancel := context.WithCancel(context.Background())
231	done := make(chan error, 1)
232	go func() { done <- c.Run(ctx) }()
233	t.Cleanup(func() { cancel(); <-done })
234	r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 300, Height: 200}}
235	scene(t, r.scenes)
236	r.client <- ports.WindowMapped{ID: 1}
237	scene(t, r.scenes)
238	unlocked := ports.SecurityState{}
239	r.input <- ports.SecurityInput{State: unlocked, Event: ports.PointerMotion{X: 50, Y: 50}}
240	r.input <- ports.SecurityInput{State: unlocked, Event: ports.PinchBegin{Fingers: 2, Time: time.Second}}
241	for _, want := range []ports.ClientCommand{
242		ports.PointerFocus{ID: 1, X: 50, Y: 50},
243		ports.PointerMotionTo{ID: 1, X: 50, Y: 50},
244		ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
245	} {
246		if v := unwrap(t, r.commands, unlocked); v != want {
247			t.Fatalf("got %#v, want %#v", v, want)
248		}
249	}
250
251	// The lock clears the pointer focus, which cancels the gesture in wayland.
252	locked := ports.SecurityState{Generation: 1, Protected: true}
253	epoch.Store(locked)
254	r.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}}}
255	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return s.Security == locked && len(s.Windows) == 1 })
256	r.input <- ports.SecurityInput{State: locked, Event: ports.PointerMotion{X: 50, Y: 50}}
257	r.input <- ports.SecurityInput{State: locked, Event: ports.PinchBegin{Fingers: 2, Time: 2 * time.Second}}
258	r.input <- ports.SecurityInput{State: locked, Event: ports.PinchUpdate{Scale: 2, Time: 3 * time.Second}}
259	r.input <- ports.SecurityInput{State: locked, Event: ports.PinchEnd{Time: 4 * time.Second}}
260	r.input <- ports.SecurityInput{State: locked, Event: ports.HoldBegin{Fingers: 3, Time: 5 * time.Second}}
261	r.input <- ports.SecurityInput{State: locked, Event: ports.HoldEnd{Time: 6 * time.Second}}
262	r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeBegin{Fingers: 2, Time: 7 * time.Second}}
263	r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeEnd{Time: 8 * time.Second}}
264	r.input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "marker", Keycode: 30, Pressed: true}}
265	for {
266		wrapped, ok := receive(t, r.commands).(ports.SecurityCommand)
267		if !ok {
268			t.Fatal("unwrapped command")
269		}
270		switch v := wrapped.Command.(type) {
271		case ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
272			if wrapped.State == locked {
273				t.Fatalf("gesture forwarded while locked: %#v", v)
274			}
275		case ports.ForwardKey:
276			if wrapped.State == locked {
277				return
278			}
279		}
280	}
281}
282
283// unwrap returns the next pointer command of an envelope of the given epoch.
284func unwrap(t *testing.T, ch <-chan ports.ClientCommand, state ports.SecurityState) ports.ClientCommand {
285	t.Helper()
286	for {
287		wrapped, ok := receive(t, ch).(ports.SecurityCommand)
288		if !ok {
289			t.Fatal("unwrapped command")
290		}
291		if wrapped.State != state {
292			t.Fatalf("epoch %+v, want %+v", wrapped.State, state)
293		}
294		if isPointerCommand(wrapped.Command) {
295			return wrapped.Command
296		}
297	}
298}

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 23

Tags 10

↑↓ moveenter openesc close