ui_test.gno
7.45 Kb · 215 lines
1package ui
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9)
10
11func TestShortN(t *testing.T) {
12 cases := []struct {
13 name string
14 in string
15 head, tail int
16 want string
17 }{
18 {"empty", "", 8, 4, ""},
19 {"shorter than the rule", "abc", 8, 4, "abc"},
20 {"exactly at the threshold", "0123456789abc", 8, 4, "0123456789abc"},
21 {"one over the threshold", "0123456789abcd", 8, 4, "01234567…abcd"},
22 {"gno address", "g1manfred47kzduec920z88wfr64ylksmdcedlf5", 8, 4, "g1manfre…dlf5"},
23 {"head only", "0123456789abcd", 6, 0, "012345…"},
24 {"tail only", "0123456789abcd", 0, 4, "…abcd"},
25 {"negative clamps to zero", "0123456789abcd", -3, -3, "…"},
26 {"multibyte is not split", "éééééééééééééé", 2, 2, "éé…éé"},
27 }
28 for _, tc := range cases {
29 got := ShortN(tc.in, tc.head, tc.tail)
30 uassert.Equal(t, tc.want, got, tc.name)
31 }
32}
33
34// ShortN must never make a string longer: that is the property the four
35// divergent shortAddr implementations disagreed on.
36func TestShortNeverGrows(t *testing.T) {
37 inputs := []string{"", "a", "abcdefghijkl", "0123456789abc", "0123456789abcd", strings.Repeat("x", 100)}
38 for _, in := range inputs {
39 got := Short(in)
40 uassert.True(t, len([]rune(got)) <= len([]rune(in)), "Short("+in+") grew the input")
41 }
42}
43
44func TestAddr(t *testing.T) {
45 a := testutils.TestAddress("alice")
46 full := a.String()
47
48 uassert.Equal(t, "`"+full+"`", AddrFull(a))
49 uassert.Equal(t, Short(full), AddrText(a))
50 uassert.Equal(t, "`"+Short(full)+"`", Addr(a))
51
52 // The shortened form keeps the recognisable head and the distinguishing tail.
53 uassert.True(t, strings.HasPrefix(AddrText(a), full[:Head]), "head is kept")
54 uassert.True(t, strings.HasSuffix(AddrText(a), full[len(full)-Tail:]), "tail is kept")
55 uassert.True(t, strings.Contains(AddrText(a), Ellipsis), "ellipsis is present")
56}
57
58// Two different addresses must not collide once shortened, or the display is
59// worse than useless.
60func TestAddrOf(t *testing.T) {
61 a := testutils.TestAddress("alice")
62 uassert.Equal(t, Addr(a), AddrOf(a.String()))
63}
64
65func TestAddrDistinguishes(t *testing.T) {
66 seen := map[string]bool{}
67 for _, name := range []string{"alice", "bob", "carol", "dave", "eve"} {
68 s := AddrText(testutils.TestAddress(name))
69 uassert.False(t, seen[s], "shortened address collision on "+name)
70 seen[s] = true
71 }
72}
73
74func TestInlineEscapes(t *testing.T) {
75 // The seven pairs the hand-rolled escapeInline copies covered.
76 for _, meta := range []string{"*", "_", "[", "]", "`", "\\"} {
77 got := Inline("a" + meta + "b")
78 uassert.True(t, strings.Contains(got, "\\"+meta), "Inline did not escape "+meta)
79 }
80 // And the one they all missed: a newline lets user text escape its line.
81 uassert.False(t, strings.Contains(Inline("a\nb"), "\n"), "Inline must fold newlines")
82}
83
84func TestCellEscapesPipe(t *testing.T) {
85 got := Cell("a|b")
86 uassert.False(t, strings.Contains(got, " |"), "a bare pipe would open a new column")
87 uassert.True(t, strings.Contains(got, "\\|"), "pipe must be escaped")
88}
89
90func TestEmpty(t *testing.T) {
91 uassert.Equal(t, "*No games yet.*\n", Empty("No games yet."))
92}
93
94func TestPodium(t *testing.T) {
95 cases := []struct {
96 rank int
97 want string
98 }{
99 {0, "\U0001F947"},
100 {1, "\U0001F948"},
101 {2, "\U0001F949"},
102 {3, ""},
103 {99, ""},
104 {-1, ""},
105 }
106 for _, tc := range cases {
107 uassert.Equal(t, tc.want, Podium(tc.rank))
108 }
109}
110
111func TestTable(t *testing.T) {
112 tbl := NewTable("#", "who", "score")
113 uassert.Equal(t, 0, tbl.Len())
114 uassert.Equal(t, "", tbl.String())
115 uassert.Equal(t, "*nobody yet.*\n", tbl.OrEmpty("nobody yet."))
116
117 tbl.Row("1", "`g1abc…wxyz`", "42").Row("2", "`g1def…uvwx`", "7")
118 uassert.Equal(t, 2, tbl.Len())
119
120 want := "| # | who | score |\n" +
121 "| --- | --- | --- |\n" +
122 "| 1 | `g1abc…wxyz` | 42 |\n" +
123 "| 2 | `g1def…uvwx` | 7 |\n"
124 uassert.Equal(t, want, tbl.String())
125 uassert.Equal(t, want, tbl.OrEmpty("nobody yet."))
126}
127
128// A short row is padded rather than producing a table the renderer misreads.
129func TestTableShortRowIsPadded(t *testing.T) {
130 tbl := NewTable("a", "b", "c")
131 tbl.Row("1")
132 uassert.Equal(t, "| a | b | c |\n| --- | --- | --- |\n| 1 | | |\n", tbl.String())
133}
134
135// A cell escaped with Cell must survive Table untouched: stacking Cell on an
136// escaper that also rewrites "|" produced "a\\|b" in the guestbook port.
137func TestTableDoesNotDoubleEscape(t *testing.T) {
138 tbl := NewTable("msg")
139 tbl.Row(Cell("a|b"))
140 got := tbl.String()
141 uassert.True(t, strings.Contains(got, `a\|b`), "expected the GFM pipe escape, got: "+got)
142 uassert.False(t, strings.Contains(got, "|"), "cell was escaped twice: "+got)
143}
144
145func TestJoin(t *testing.T) {
146 uassert.Equal(t, "a\n\nb", Join("\n\n", "a", "", "b"))
147 uassert.Equal(t, "", Join("\n\n", "", ""))
148 uassert.Equal(t, "solo", Join("\n\n", "solo"))
149}
150
151// Action must produce a markdown link whose target carries the function name
152// and the key/value arguments, and must escape the title (which helplink
153// itself does not).
154func TestAction(t *testing.T) {
155 got := Action("Play cell 4", "Move", "gameID", "7", "cell", "4")
156 uassert.True(t, strings.HasPrefix(got, "[Play cell 4]("), "expected a markdown link, got: "+got)
157 uassert.True(t, strings.Contains(got, "Move"), "link must name the function")
158 uassert.True(t, strings.Contains(got, "gameID=7"), "link must carry the args")
159 uassert.True(t, strings.Contains(got, "cell=4"), "link must carry the args")
160
161 esc := Action("a*b", "Fn")
162 uassert.True(t, strings.HasPrefix(esc, "[a\\*b]("), "title must be escaped, got: "+esc)
163}
164
165func TestActionIn(t *testing.T) {
166 got := ActionIn("gno.land/r/moul/gns/v0", "Say hi", "Hello", "name", "moul")
167 uassert.True(t, strings.Contains(got, "/r/moul/gns/v0"), "must target the named realm, got: "+got)
168 uassert.True(t, strings.Contains(got, "Hello"), "must name the function")
169}
170
171func TestExcerpt(t *testing.T) {
172 cases := []struct {
173 name string
174 in string
175 width int
176 want string
177 }{
178 {"empty", "", 10, ""},
179 {"shorter than the width", "hello", 10, "hello"},
180 {"one over is kept whole, as ShortN does", "0123456789a", 10, "0123456789a"},
181 {"two over is cut", "0123456789ab", 10, "0123456789…"},
182 {"zero width", "hello", 0, "…"},
183 {"negative width clamps to zero", "hello", -3, "…"},
184 // The cut counts runes. A byte slice at 10 would split the 11th 'é'
185 // and put invalid UTF-8 on the page.
186 {"multibyte is not split", strings.Repeat("é", 20), 10, strings.Repeat("é", 10) + "…"},
187 // The escaping half: what is kept is escaped, the ellipsis is not.
188 {"escapes what it keeps", "[link](url) and more", 11, "\\[link\\]\\(url\\)…"},
189 {"newline cannot escape the line", "one\ntwo", 10, "one two"},
190 }
191 for _, tc := range cases {
192 uassert.Equal(t, tc.want, Excerpt(tc.in, tc.width), tc.name)
193 }
194}
195
196// Excerpt must never leave a trailing lone backslash: that is the bug that
197// comes from escaping first and cutting second, and it escapes whatever the
198// caller appends after the excerpt.
199func TestExcerptNeverStrandsABackslash(t *testing.T) {
200 // Each of these ends, at some width, on a character the escaper backslashes.
201 for _, in := range []string{
202 "....................", "********************", "[[[[[[[[[[[[[[[[[[[[",
203 "!!!!!!!!!!!!!!!!!!!!", "<<<<<<<<<<<<<<<<<<<<", "--------------------",
204 } {
205 for width := 0; width <= 15; width++ {
206 got := Excerpt(in, width)
207 body := strings.TrimSuffix(got, Ellipsis)
208 trailing := 0
209 for i := len(body) - 1; i >= 0 && body[i] == '\\'; i-- {
210 trailing++
211 }
212 uassert.Equal(t, 0, trailing%2, "odd run of trailing backslashes in Excerpt("+in+")")
213 }
214 }
215}