reactions_test.gno
8.55 Kb · 245 lines
1package reactions
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9)
10
11const (
12 realmPath = "gno.land/r/test/reactions/v0"
13 homePage = "gno.land/r/moul/home"
14)
15
16var (
17 alice = testutils.TestAddress("alice")
18 bob = testutils.TestAddress("bob")
19 carol = testutils.TestAddress("carol")
20)
21
22func TestValidPage(t *testing.T) {
23 cases := []struct {
24 page string
25 want bool
26 }{
27 {"gno.land/r/moul/home", true},
28 {"gno.land/p/moul/kit/ui/v0", true},
29 {"gno.land/r/moul/x/daily/b58demo", true},
30 {"a/b", true},
31 {"gno.land/r/you/blog:hello-world", true}, // one page per article
32 {"", false},
33 {"nolslash", false},
34 {"/leading", false},
35 {"trailing/", false},
36 {"double//slash", false},
37 {"gno.land/r/MOUL/home", false}, // uppercase is not a path here
38 {"gno.land/r/moul/home ", false}, // a space is not path-shaped
39 {"gno.land/r/[x](http://e.vil)", false}, // a markdown link cannot be a key
40 {"gno.land/r/moul/home", false}, // bidi override
41 {strings.Repeat("a/", MaxPageLen) + "b", false}, // over the cap
42 {strings.Repeat("a", MaxPageLen-2) + "/b", true}, // exactly at the cap
43 {strings.Repeat("a", MaxPageLen-1) + "/b", false}, // one byte over
44 }
45 for _, tc := range cases {
46 uassert.Equal(t, tc.want, ValidPage(tc.page), tc.page)
47 }
48}
49
50func TestPaletteIsClosed(t *testing.T) {
51 uassert.Equal(t, 6, len(Keys()))
52 for _, k := range Keys() {
53 uassert.True(t, Valid(k), k)
54 uassert.NotEqual(t, "", Emoji(k), k)
55 }
56 uassert.False(t, Valid("poop"))
57 uassert.Equal(t, "", Emoji("poop"))
58
59 // Keys returns a copy: reordering it must not reorder the palette.
60 ks := Keys()
61 ks[0] = "clobbered"
62 uassert.Equal(t, KeyUp, Keys()[0])
63}
64
65// TestNilPageReadsAsEmpty is what lets a host realm render the block without
66// ever checking whether the page exists.
67func TestNilPageReadsAsEmpty(t *testing.T) {
68 var p *Page
69 uassert.Equal(t, 0, p.Count(KeyUp))
70 uassert.Equal(t, 0, p.Total())
71 uassert.Equal(t, "", p.Of(alice))
72 key, who, at := p.Last()
73 uassert.Equal(t, "", key)
74 uassert.Equal(t, address(""), who)
75 uassert.Equal(t, int64(0), at)
76}
77
78func TestReactCountsAddressesNotClicks(t *testing.T) {
79 b := NewBoard()
80
81 uassert.True(t, b.React(homePage, alice, KeyUp, 100))
82 uassert.True(t, b.React(homePage, alice, KeyUp, 101))
83 uassert.True(t, b.React(homePage, alice, KeyUp, 102))
84
85 p := b.Page(homePage)
86 uassert.Equal(t, 1, p.Count(KeyUp), "three clicks from one address is one reaction")
87 uassert.Equal(t, 1, p.Total())
88
89 // The repeat still counts as the last event, because it happened.
90 _, _, at := p.Last()
91 uassert.Equal(t, int64(102), at)
92}
93
94func TestChangingReactionMovesTheCount(t *testing.T) {
95 b := NewBoard()
96 b.React(homePage, alice, KeyUp, 100)
97 b.React(homePage, bob, KeyUp, 101)
98 b.React(homePage, alice, KeyFire, 102)
99
100 p := b.Page(homePage)
101 uassert.Equal(t, 1, p.Count(KeyUp))
102 uassert.Equal(t, 1, p.Count(KeyFire))
103 uassert.Equal(t, 2, p.Total(), "changing a reaction never changes the head count")
104 uassert.Equal(t, KeyFire, p.Of(alice))
105 uassert.Equal(t, KeyUp, p.Of(bob))
106}
107
108func TestUnreact(t *testing.T) {
109 b := NewBoard()
110 b.React(homePage, alice, KeyUp, 100)
111
112 uassert.False(t, b.Unreact("gno.land/r/never/touched", alice), "no page")
113 uassert.False(t, b.Unreact(homePage, bob), "no reaction of bob's")
114 uassert.True(t, b.Unreact(homePage, alice))
115 uassert.False(t, b.Unreact(homePage, alice), "removing twice is not an error, just false")
116
117 p := b.Page(homePage)
118 uassert.Equal(t, 0, p.Count(KeyUp), "an emptied bucket leaves no residue")
119 uassert.Equal(t, 0, p.Total())
120
121 // Last is deliberately not rewound: it records what happened.
122 key, who, _ := p.Last()
123 uassert.Equal(t, KeyUp, key)
124 uassert.Equal(t, alice, who)
125}
126
127func TestReactRejects(t *testing.T) {
128 b := NewBoard()
129 uassert.False(t, b.React("not a path", alice, KeyUp, 1), "bad page")
130 uassert.False(t, b.React(homePage, alice, "poop", 1), "key outside the palette")
131 uassert.Equal(t, 0, b.Pages(), "a rejected reaction creates no page")
132}
133
134func TestPagesAreIndependentAndOrdered(t *testing.T) {
135 b := NewBoard()
136 b.React("gno.land/r/moul/home", alice, KeyUp, 1)
137 b.React("gno.land/r/moul/blog", bob, KeyFire, 2)
138 b.React("gno.land/r/moul/blog", carol, KeyFire, 3)
139
140 uassert.Equal(t, 2, b.Pages())
141 uassert.Equal(t, 1, b.Page("gno.land/r/moul/home").Total())
142 uassert.Equal(t, 2, b.Page("gno.land/r/moul/blog").Total())
143
144 var seen []string
145 b.Each(func(page string, p *Page) bool {
146 seen = append(seen, page)
147 return false
148 })
149 uassert.Equal(t, "gno.land/r/moul/blog,gno.land/r/moul/home", strings.Join(seen, ","),
150 "Each walks in key order, so a Render built from it is stable")
151}
152
153func TestEachStopsOnTrue(t *testing.T) {
154 b := NewBoard()
155 b.React("gno.land/r/a/one", alice, KeyUp, 1)
156 b.React("gno.land/r/b/two", alice, KeyUp, 2)
157
158 n := 0
159 b.Each(func(page string, p *Page) bool {
160 n++
161 return true
162 })
163 uassert.Equal(t, 1, n)
164}
165
166func TestRealmAndPageURL(t *testing.T) {
167 uassert.Equal(t, "/r/test/reactions/v0", RealmURL(realmPath))
168 uassert.Equal(t, "", RealmURL("nodomain"))
169 uassert.Equal(t, "/r/test/reactions/v0:gno.land/r/moul/home",
170 PageURL(realmPath, homePage))
171 uassert.Equal(t, "", PageURL("nodomain", homePage))
172}
173
174// TestBlockShape pins the embed byte for byte. It is a Test and not an Example
175// because the block contains a blank line, and gno collapses consecutive blank
176// lines inside an // Output: block.
177func TestBlockShape(t *testing.T) {
178 b := NewBoard()
179
180 empty := "[\U0001F44D](/r/test/reactions/v0$help&func=React&key=up&page=gno.land%2Fr%2Fmoul%2Fhome) " +
181 "[\U0001F49C](/r/test/reactions/v0$help&func=React&key=heart&page=gno.land%2Fr%2Fmoul%2Fhome) " +
182 "[\U0001F525](/r/test/reactions/v0$help&func=React&key=fire&page=gno.land%2Fr%2Fmoul%2Fhome) " +
183 "[\U0001F389](/r/test/reactions/v0$help&func=React&key=party&page=gno.land%2Fr%2Fmoul%2Fhome) " +
184 "[\U0001F680](/r/test/reactions/v0$help&func=React&key=rocket&page=gno.land%2Fr%2Fmoul%2Fhome) " +
185 "[\U0001F440](/r/test/reactions/v0$help&func=React&key=eyes&page=gno.land%2Fr%2Fmoul%2Fhome)" +
186 "\n\n*no reactions yet*\n"
187 uassert.Equal(t, empty, Block(realmPath, homePage, b.Page(homePage)),
188 "a page nobody has reacted to still renders the whole strip")
189
190 b.React(homePage, alice, KeyUp, 100)
191 b.React(homePage, bob, KeyUp, 101)
192 b.React(homePage, carol, KeyFire, 102)
193
194 got := Block(realmPath, homePage, b.Page(homePage))
195 uassert.True(t, strings.Contains(got, "[\U0001F44D 2]"), "the count rides in the link title")
196 uassert.True(t, strings.Contains(got, "[\U0001F525 1]"))
197 uassert.True(t, strings.Contains(got, "[\U0001F389]"), "an empty bucket shows no zero")
198 uassert.True(t, strings.Contains(got, "last \U0001F525 by "))
199 uassert.True(t, strings.Contains(got, "[3 reactions](/r/test/reactions/v0:gno.land/r/moul/home)"))
200 uassert.True(t, strings.HasSuffix(got, "*\n"), "one trailing newline, so it drops into a Render")
201}
202
203func TestSummaryPluralises(t *testing.T) {
204 b := NewBoard()
205 b.React(homePage, alice, KeyUp, 100)
206 uassert.True(t, strings.Contains(Summary(realmPath, homePage, b.Page(homePage)), "[1 reaction]"))
207
208 b.React(homePage, bob, KeyUp, 101)
209 uassert.True(t, strings.Contains(Summary(realmPath, homePage, b.Page(homePage)), "[2 reactions]"))
210}
211
212// TestEmojiSurviveTheEscaper is the reason the palette holds no U+FE0F and no
213// zero-width joiner: ui.ActionIn escapes the link title, and that escaper
214// strips exactly those. A glyph that does not survive it renders as a
215// monochrome box on every page that embeds the block.
216func TestEmojiSurviveTheEscaper(t *testing.T) {
217 b := NewBoard()
218 for _, k := range Keys() {
219 got := Block(realmPath, homePage, b.Page(homePage))
220 uassert.True(t, strings.Contains(got, "["+Emoji(k)+"]"), k)
221 }
222}
223
224// TestSummaryGoesEmptyWhenEveryoneLeaves is the interaction between the two
225// halves of Last: it is deliberately not rewound by Unreact, so the summary has
226// to decide for itself whether there is anything left to summarise. Gating on
227// the last key alone would print "last 👍 by g1… · 0 reactions" above an empty
228// strip, which contradicts itself and keeps attributing a reaction its author
229// took back.
230func TestSummaryGoesEmptyWhenEveryoneLeaves(t *testing.T) {
231 b := NewBoard()
232 b.React(homePage, alice, KeyUp, 100)
233 b.Unreact(homePage, alice)
234
235 p := b.Page(homePage)
236 uassert.Equal(t, 0, p.Total(), "the tally is empty")
237
238 key, _, _ := p.Last()
239 uassert.Equal(t, KeyUp, key, "and Last still records the event, by design")
240
241 uassert.Equal(t, "no reactions yet", Summary(realmPath, homePage, p),
242 "so the summary shows the empty state rather than a withdrawn reaction")
243
244 uassert.True(t, strings.HasSuffix(Block(realmPath, homePage, p), "*no reactions yet*\n"))
245}