Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}