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 & Jerry": "Tom & Jerry",
18 "<tag> "q" 's'": "<tag> \"q\" 's'",
19 "ABC": "ABC",
20 "���": "���",
21 "&unknown; &": "&unknown; &",
22 "1 < 2": "1 < 2",
23 "<<b>x</b>": "x",
24 "<img src='x>y'>z": "y'>z",
25 "\u00e9é": "éé",
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>& 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}