main 7a3b92c

control_test.go RawHistoryFolder

1package tray
2
3import (
4	"errors"
5	"io"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/bnema/zerobus"
11)
12
13func TestParseCommand(t *testing.T) {
14	good := map[string]command{
15		"click left 0 1":                      {verb: "click", button: btnLeft, col: 0, token: 1},
16		"click left 0 4294967295":             {verb: "click", button: btnLeft, token: 1<<32 - 1},
17		"click left 1048576 1":                {verb: "click", button: btnLeft, col: 1 << 20, token: 1},
18		"menu-activate 4294967295 2147483647": {verb: "menu-activate", token: 1<<32 - 1, col: 1<<31 - 1},
19		"click middle 3 42":                   {verb: "click", button: btnMiddle, col: 3, token: 42},
20		"click right 12 7":                    {verb: "click", button: btnRight, col: 12, token: 7},
21		"scroll up 2 0":                       {verb: "scroll", vert: true, delta: -2},
22		"scroll down 1 4":                     {verb: "scroll", vert: true, delta: 1, col: 4},
23		"scroll left 3 1":                     {verb: "scroll", delta: -3, col: 1},
24		"scroll right 5 2":                    {verb: "scroll", delta: 5, col: 2},
25		"hover 4":                             {verb: "hover", col: 4},
26		"leave":                               {verb: "leave"},
27		"menu-activate 9 17":                  {verb: "menu-activate", token: 9, col: 17},
28		"menu-closed 9":                       {verb: "menu-closed", token: 9},
29		"  click  left 1  2 ":                 {verb: "click", button: btnLeft, col: 1, token: 2},
30	}
31	for line, want := range good {
32		got, ok := parseCommand(line)
33		if !ok || got != want {
34			t.Errorf("parseCommand(%q) = %+v, %v; want %+v", line, got, ok, want)
35		}
36	}
37	for _, line := range []string{
38		"", "   ", "garbage", "click", "click left", "click left 0", "click left 0 1 2", "click up 0 1",
39		"click left -1 1", "click left x 1", "click left 0 -3", "click left +1 1", "click left 0 4294967296",
40		"scroll up", "scroll up 0 0", "scroll up -1 0", "scroll up 2", "scroll diagonal 1 0", "scroll up 1000000 0",
41		"hover", "hover x", "hover -1", "hover 1 2", "leave now", "menu-activate 1", "menu-activate 1 -2",
42		"menu-closed", "menu-closed x", "menu-closed 1 2", "CLICK left 0 1",
43		// Signs are refused, and so is anything past 31 bits for columns and steps.
44		"click left 0 +1", "click left +0 1", "click left 0 -0", "scroll up +2 0", "scroll up 2 +0", "hover +1",
45		"menu-activate +1 2", "menu-activate 1 +2", "menu-closed +1", "menu-closed -1",
46		"click left 2147483648 1", "scroll up 1 2147483648", "hover 2147483648", "hover 1048577",
47		"menu-activate 1 2147483648", "click left 0 0x1", "click left 0 1_0", "click left 0 1e3",
48	} {
49		if got, ok := parseCommand(line); ok {
50			t.Errorf("parseCommand(%q) = %+v, want it refused", line, got)
51		}
52	}
53}
54
55// barInput is the pipe the test writes the bar's lines to.
56type barInput struct{ w *io.PipeWriter }
57
58func (b barInput) send(t *testing.T, lines ...string) {
59	t.Helper()
60	for _, l := range lines {
61		if _, err := io.WriteString(b.w, l+"\n"); err != nil {
62			t.Fatal(err)
63		}
64	}
65}
66
67// setup runs a tray with input on a private bus, with two items: steam ("S",
68// column 0) and obsidian ("O", column 2).
69func setup(t *testing.T) (steam, obs *fakeItem, in barInput, out *lines) {
70	t.Helper()
71	addr := privateBus(t)
72	pr, pw := io.Pipe()
73	t.Cleanup(func() { _ = pw.Close() })
74	in = barInput{pw}
75	out, _, _ = startTrayWith(t, addr, pr, func() (*zerobus.Conn, error) { return zerobus.Dial(addr) })
76	waitWatcher(t, addr)
77	steam = newFakeItem(t, addr, "steam", "", [4]byte{255, 30, 60, 200})
78	obs = newFakeItem(t, addr, "obsidian_status_icon_1", "", [4]byte{255, 120, 80, 220})
79	steam.register(t)
80	out.waitFor(t, "steam", func(s string) bool { return strings.Contains(s, "S") })
81	obs.register(t)
82	out.waitFor(t, "both items", func(s string) bool { return strings.Contains(s, "S") && strings.Contains(s, "O") })
83	return steam, obs, in, out
84}
85
86func expectCall(t *testing.T, f *fakeItem, want string) {
87	t.Helper()
88	select {
89	case got := <-f.calls:
90		if got != want {
91			t.Fatalf("item got %s, want %s", got, want)
92		}
93	case <-time.After(5 * time.Second):
94		t.Fatalf("item never got %s", want)
95	}
96}
97
98func expectNoCall(t *testing.T, f *fakeItem) {
99	t.Helper()
100	select {
101	case got := <-f.calls:
102		t.Fatalf("unexpected call %s", got)
103	case <-time.After(150 * time.Millisecond):
104	}
105}
106
107func TestClicksAndScroll(t *testing.T) {
108	steam, obs, in, _ := setup(t)
109
110	in.send(t, "click left 0 1")
111	expectCall(t, steam, "Activate(0,0)")
112	in.send(t, "click middle 0 2")
113	expectCall(t, steam, "SecondaryActivate(0,0)")
114	in.send(t, "click right 0 3")
115	expectCall(t, steam, "ContextMenu(0,0)")
116
117	// The second icon starts at column 2, after one space.
118	in.send(t, "click left 2 4")
119	expectCall(t, obs, "Activate(0,0)")
120	expectNoCall(t, steam)
121
122	in.send(t, "scroll up 2 0")
123	expectCall(t, steam, "Scroll(-2,vertical)")
124	in.send(t, "scroll down 3 0")
125	expectCall(t, steam, "Scroll(3,vertical)")
126	in.send(t, "scroll right 1 2")
127	expectCall(t, obs, "Scroll(1,horizontal)")
128	in.send(t, "scroll left 4 2")
129	expectCall(t, obs, "Scroll(-4,horizontal)")
130}
131
132func TestClickOutsideIconsAndGarbageAreIgnored(t *testing.T) {
133	steam, obs, in, _ := setup(t)
134	in.send(t,
135		"click left 1 1", // the space between the icons
136		"click left 9 2", // past the last icon
137		"garbage",
138		"click left x 3",
139		"click left 0",
140		"click left 0 4 extra",
141		"click left -1 5",
142		"scroll up 0 0",
143		"\x1b[31m not a command",
144		strings.Repeat("click left 0 1 ", 40), // longer than the line limit
145		"hover 0", "leave",                    // accepted by the parser, nothing to do yet
146	)
147	expectNoCall(t, steam)
148	expectNoCall(t, obs)
149	// The loop still works afterwards.
150	in.send(t, "click left 0 9")
151	expectCall(t, steam, "Activate(0,0)")
152}
153
154func TestActivateUnknownMethodFallsBackToContextMenu(t *testing.T) {
155	steam, _, in, _ := setup(t)
156	steam.run(t, func() { steam.unknown = "Activate" })
157	in.send(t, "click left 0 1")
158	expectCall(t, steam, "Activate(0,0)")
159	expectCall(t, steam, "ContextMenu(0,0)")
160}
161
162func TestItemIsMenuLeftClickOpensTheMenu(t *testing.T) {
163	var buf strings.Builder
164	tr := &Tray{opt: Options{Out: &buf}}
165	tr.items = []*item{{loaded: true, icon: "X", dest: ":1.1", path: "/p", isMenu: true, menu: "/m"}}
166	if err := tr.render(true); err != nil {
167		t.Fatal(err)
168	}
169	tg, ok := tr.target(0)
170	if !ok || !tg.IsMenu || tg.Menu != "/m" {
171		t.Fatalf("target = %+v, %v", tg, ok)
172	}
173}
174
175// A frozen application must not block the tray: the call is abandoned after
176// two seconds and the next click, on another item, still goes through.
177func TestFrozenApplicationDoesNotBlockNextClick(t *testing.T) {
178	steam, obs, in, _ := setup(t)
179	steam.run(t, func() { steam.slow, steam.slowFor = "Activate", 4*time.Second })
180	start := time.Now()
181	in.send(t, "click left 0 1", "click left 2 2")
182	expectCall(t, steam, "Activate(0,0)")
183	expectCall(t, obs, "Activate(0,0)")
184	if d := time.Since(start); d < 1500*time.Millisecond || d > 3500*time.Millisecond {
185		t.Fatalf("the second click arrived after %v, want about 2s (the watchdog)", d)
186	}
187	// And again on the dropped item: the connection was redialed.
188	in.send(t, "click middle 2 3")
189	expectCall(t, obs, "SecondaryActivate(0,0)")
190}
191
192// If shutdown begins while the control connection is being dialed, the new
193// connection is closed and not kept.
194func TestConn2ClosesADialThatRacesShutdown(t *testing.T) {
195	addr := privateBus(t)
196	dialing, release := make(chan struct{}), make(chan struct{})
197	var dialed *zerobus.Conn
198	c := &control{dial: func() (*zerobus.Conn, error) {
199		close(dialing)
200		<-release
201		conn, err := zerobus.Dial(addr)
202		dialed = conn
203		return conn, err
204	}}
205	errc := make(chan error, 1)
206	go func() { _, err := c.conn2(); errc <- err }()
207	<-dialing
208	closed := make(chan struct{})
209	go func() { c.close(); close(closed) }()
210	select {
211	case <-closed: // close must not wait for the dial
212	case <-time.After(time.Second):
213		t.Fatal("close waited for the dial")
214	}
215	close(release)
216	if err := <-errc; !errors.Is(err, zerobus.ErrClosed) {
217		t.Fatalf("conn2 = %v, want ErrClosed", err)
218	}
219	if c.conn != nil {
220		t.Fatal("the connection was kept after shutdown")
221	}
222	dialed.NewCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "GetId", "")
223	if _, err := dialed.Call(); err == nil {
224		t.Fatal("the connection dialed during shutdown is still open")
225	}
226}

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 5

Tags 4

↑↓ moveenter openesc close