1package ui23import (4 "strings"5 "testing"67 "gno.land/p/nt/testutils/v0"8 "gno.land/p/nt/uassert/v0"9)1011func TestShortN(t *testing.T) {12 cases := []struct {13 name string14 in string15 head, tail int16 want string17 }{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}3334// ShortN must never make a string longer: that is the property the four35// 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}4344func TestAddr(t *testing.T) {45 a := testutils.TestAddress("alice")46 full := a.String()4748 uassert.Equal(t, "`"+full+"`", AddrFull(a))49 uassert.Equal(t, Short(full), AddrText(a))50 uassert.Equal(t, "`"+Short(full)+"`", Addr(a))5152 // 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}5758// Two different addresses must not collide once shortened, or the display is59// worse than useless.60func TestAddrOf(t *testing.T) {61 a := testutils.TestAddress("alice")62 uassert.Equal(t, Addr(a), AddrOf(a.String()))63}6465func 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] = true71 }72}7374func 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}8384func 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}8990func TestEmpty(t *testing.T) {91 uassert.Equal(t, "*No games yet.*\n", Empty("No games yet."))92}9394func TestPodium(t *testing.T) {95 cases := []struct {96 rank int97 want string98 }{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}110111func 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."))116117 tbl.Row("1", "`g1abc…wxyz`", "42").Row("2", "`g1def…uvwx`", "7")118 uassert.Equal(t, 2, tbl.Len())119120 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}127128// 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}134135// A cell escaped with Cell must survive Table untouched: stacking Cell on an136// 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}144145func 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}150151// Action must produce a markdown link whose target carries the function name152// and the key/value arguments, and must escape the title (which helplink153// 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")160161 esc := Action("a*b", "Fn")162 uassert.True(t, strings.HasPrefix(esc, "[a\\*b]("), "title must be escaped, got: "+esc)163}164165func 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}170171func TestExcerpt(t *testing.T) {172 cases := []struct {173 name string174 in string175 width int176 want string177 }{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}195196// Excerpt must never leave a trailing lone backslash: that is the bug that197// comes from escaping first and cutting second, and it escapes whatever the198// 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 := 0209 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}216Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.