main 7a3b92c

tooltip_test.go RawHistoryFolder

1package tray
2
3import (
4	"strings"
5	"testing"
6	"time"
7)
8
9func TestPlainText(t *testing.T) {
10	for in, want := range map[string]string{
11		"":                      "",
12		"plain":                 "plain",
13		"<b>bold</b> text":      "bold text",
14		"a<br>b":                "a\nb",
15		"a<br/>b<BR />c":        "a\nb\nc",
16		"<p style=\"x\">hi</p>": "hi",
17		"Tom &amp; Jerry":       "Tom & Jerry",
18		"&lt;tag&gt; &quot;q&quot; &apos;s&apos;": "<tag> \"q\" 's'",
19		"&#65;&#x42;&#X43;":                       "ABC",
20		"&#0;&#xD800;&#99999999999;":              "&#0;&#xD800;&#99999999999;",
21		"&unknown; &amp":                          "&unknown; &amp",
22		"1 < 2":                                   "1 < 2",
23		"<<b>x</b>":                               "x",
24		"<img src='x>y'>z":                        "y'>z",
25		"\u00e9&#233;":                            "éé",
26	} {
27		if got := plainText(in); got != want {
28			t.Errorf("plainText(%q) = %q, want %q", in, got, want)
29		}
30	}
31	long := plainText(strings.Repeat("é", 1000))
32	if len(long) > maxTipBytes || len(long)%2 != 0 || strings.ContainsRune(long, '\uFFFD') {
33		t.Errorf("long text: %d bytes", len(long))
34	}
35	if got := plainText(strings.Repeat("<b>", 100000) + "x"); got != "x" {
36		t.Errorf("many tags = %q", got)
37	}
38}
39
40// controlLine waits for a control line with the given type and returns its JSON.
41func controlLine(t *testing.T, out *lines, typ string, within time.Duration) string {
42	t.Helper()
43	timeout := time.After(within)
44	for {
45		select {
46		case s, ok := <-out.ch:
47			if !ok {
48				t.Fatalf("output ended while waiting for a %s line", typ)
49			}
50			if j, found := strings.CutPrefix(s, controlPrefix); found && strings.HasSuffix(j, "\x07") &&
51				strings.Contains(j, `"type":"`+typ+`"`) {
52				return strings.TrimSuffix(j, "\x07")
53			}
54		case <-timeout:
55			t.Fatalf("no %s control line within %v", typ, within)
56		}
57	}
58}
59
60// noControlLine fails when a control line arrives within d.
61func noControlLine(t *testing.T, out *lines, d time.Duration) {
62	t.Helper()
63	timeout := time.After(d)
64	for {
65		select {
66		case s, ok := <-out.ch:
67			if ok && strings.HasPrefix(s, controlPrefix) {
68				t.Fatalf("unexpected control line %q", s)
69			}
70		case <-timeout:
71			return
72		}
73	}
74}
75
76func TestTooltipAfterHover(t *testing.T) {
77	steam, _, in, out := setup(t)
78	steam.setTip(t, "Steam", "<b>3</b> friends online<br>&amp; 1 game")
79	// Wait for the refresh the NewToolTip signal triggers.
80	deadline := time.Now().Add(5 * time.Second)
81	var got string
82	for time.Now().Before(deadline) {
83		in.send(t, "hover 0")
84		select {
85		case s := <-out.ch:
86			if strings.HasPrefix(s, controlPrefix) {
87				got = s
88			}
89		case <-time.After(900 * time.Millisecond):
90		}
91		if got != "" {
92			break
93		}
94		in.send(t, "leave")
95	}
96	want := controlPrefix + `{"type":"tooltip","col":0,"width":1,"title":"Steam","body":"3 friends online\n& 1 game"}` + "\x07"
97	if got != want {
98		t.Fatalf("tooltip line = %q\nwant %q", got, want)
99	}
100	// The pointer leaves: the tooltip closes.
101	in.send(t, "leave")
102	if j := controlLine(t, out, "close", time.Second); j != `{"type":"close"}` {
103		t.Fatalf("close line = %q", j)
104	}
105}
106
107func TestTooltipNotSentWhenLeftEarlyOrOnClick(t *testing.T) {
108	steam, obs, in, out := setup(t)
109	steam.run(t, func() { steam.tip = &[2]string{"Steam", "x"} })
110	obs.run(t, func() { obs.title = "Obsidian" })
111	steam.setTip(t, "Steam", "x") // refresh both through the signal path
112	obs.setTip(t, "", "")
113	time.Sleep(200 * time.Millisecond)
114
115	in.send(t, "hover 0")
116	time.Sleep(150 * time.Millisecond)
117	in.send(t, "leave")
118	noControlLine(t, out, 700*time.Millisecond)
119
120	// A click before the delay cancels the tooltip too, without a close line.
121	in.send(t, "hover 0")
122	time.Sleep(100 * time.Millisecond)
123	in.send(t, "click left 0 1")
124	expectCall(t, steam, "Activate(0,0)")
125	noControlLine(t, out, 700*time.Millisecond)
126
127	// Moving between the gap and another icon restarts the delay.
128	in.send(t, "hover 1", "hover 2")
129	start := time.Now()
130	j := controlLine(t, out, "tooltip", 2*time.Second)
131	if d := time.Since(start); d < 400*time.Millisecond {
132		t.Fatalf("the tooltip came after %v, want about 500ms", d)
133	}
134	// An item with no tooltip shows its Title.
135	if !strings.Contains(j, `"title":"Obsidian"`) || !strings.Contains(j, `"col":2`) {
136		t.Fatalf("tooltip = %q", j)
137	}
138	// A click after it was shown closes it.
139	in.send(t, "click left 2 2")
140	controlLine(t, out, "close", time.Second)
141}
142
143func TestTooltipWithNothingToSayIsNotSent(t *testing.T) {
144	_, _, in, out := setup(t) // fake items without ToolTip or Title
145	in.send(t, "hover 0")
146	noControlLine(t, out, 900*time.Millisecond)
147	in.send(t, "leave")
148	noControlLine(t, out, 100*time.Millisecond)
149}

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