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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

render_test.gno

8.73 Kb · 255 lines
  1package connect4_v4
  2
  3import (
  4	"encoding/base64"
  5	"strconv"
  6	"strings"
  7	"testing"
  8	"time"
  9
 10	"gno.land/p/nt/testutils/v0"
 11	"gno.land/p/nt/uassert/v0"
 12)
 13
 14func TestLeaderboardTopTen(t *testing.T) {
 15	reset()
 16	for i := 1; i <= 12; i++ {
 17		topWins = bump(topWins, testutils.TestAddress("p"+strconv.Itoa(i)), int64(i))
 18	}
 19	out := leaderboard(topWins, func(v int64) string { return strconv.FormatInt(v, 10) })
 20	uassert.True(t, strings.HasPrefix(strings.Split(out, "\n")[2], "| 1 | "+testutils.TestAddress("p12").String()+" | 12 |"))
 21	uassert.True(t, strings.Contains(out, "| 10 | "+testutils.TestAddress("p3").String()+" | 3 |"))
 22	uassert.False(t, strings.Contains(out, testutils.TestAddress("p2").String()))
 23}
 24
 25func TestGamePageShowsActionsForEveryState(cur realm, t *testing.T) {
 26	reset()
 27	contains := func(out string, want ...string) {
 28		for _, w := range want {
 29			uassert.True(t, strings.Contains(out, w), "missing "+w+" in:\n"+out)
 30		}
 31	}
 32
 33	testing.SetRealm(testing.NewUserRealm(alice))
 34	pay(stake)
 35	id := Offer(cross(cur), "", 10, commit(), fee)
 36	page := "game/" + strconv.Itoa(id)
 37	contains(Render(page), "func=Accept", ".send=10000000ugnot", "func=Cancel", "Open offer")
 38
 39	g := mustGet(id)
 40	g.ExpiresAt = time.Now().Add(-time.Second)
 41	out := Render(page)
 42	contains(out, "expired", "func=Cancel")
 43	uassert.False(t, strings.Contains(out, "func=Accept"))
 44
 45	g, p := accepted(cur)
 46	page = "game/" + strconv.Itoa(g.ID)
 47	contains(Render(page), "Waiting for", "func=Reveal", "func=Resign")
 48
 49	testing.SetRealm(testing.NewUserRealm(alice))
 50	pay(0)
 51	revealBoth(cur, g, p)
 52	contains(Render(page), "func=Play", "column=7", "func=Resign", "Turn:")
 53
 54	testing.SetRealm(testing.NewUserRealm(alice))
 55	pay(0)
 56	Resign(cross(cur), g.ID)
 57	contains(Render(page), "Winner", bob.String(), gnot(2*stake-fee))
 58
 59	for status, want := range map[Status]string{
 60		Draw:      "both stakes refunded",
 61		Void:      "both stakes refunded",
 62		Cancelled: "refunded to the creator",
 63	} {
 64		g.Status = status
 65		contains(Render(page), want)
 66	}
 67}
 68
 69func TestLobbyLinksEveryGameToItsPage(cur realm, t *testing.T) {
 70	reset()
 71	testing.SetRealm(testing.NewUserRealm(alice))
 72	pay(stake)
 73	id := Offer(cross(cur), "", 10, commit(), fee)
 74	g, _ := accepted(cur)
 75	// Render as if deployed elsewhere: links must follow the realm's real path.
 76	testing.SetRealm(testing.NewCodeRealm("gno.land/r/nym-mikecito001/connect4"))
 77	out := Render("")
 78	base := "(/r/nym-mikecito001/connect4:"
 79	uassert.True(t, strings.Contains(out, base+"game/"+strconv.Itoa(id)+")"), out)
 80	uassert.True(t, strings.Contains(out, base+"game/"+strconv.Itoa(g.ID)+")"), out)
 81	uassert.True(t, strings.Contains(out, base+"new)"), out)
 82	uassert.False(t, strings.Contains(out, "samcrew"), "links must not hardcode the realm path")
 83}
 84
 85func TestNewOfferPage(t *testing.T) {
 86	reset()
 87	c := sha256Hex("some four random words")
 88
 89	out := Render("new")
 90	uassert.True(t, strings.Contains(out, `<gno-form path="new">`))
 91	uassert.False(t, strings.Contains(out, "func=Offer"))
 92
 93	out = Render("new?stake=12.5&validFor=10&opponent=" + bob.String() + "&commitment=" + c)
 94	for _, want := range []string{"func=Offer", ".send=12500000ugnot", "validFor=10", "opponent=" + bob.String(), "commitment=" + c, "12.5 GNOT"} {
 95		uassert.True(t, strings.Contains(out, want), "missing "+want+" in:\n"+out)
 96	}
 97
 98	out = Render("new?stake=3&commitment=" + c) // validFor defaults, opponent optional
 99	uassert.True(t, strings.Contains(out, ".send=3000000ugnot"))
100	uassert.True(t, strings.Contains(out, "validFor=15"))
101
102	for _, q := range []string{
103		"stake=%3Cscript%3E&validFor=10&commitment=" + c, // not a number
104		"stake=0.5&validFor=10&commitment=" + c,          // below minimum
105		"stake=1.0000001&validFor=10&commitment=" + c,    // more than 6 decimals
106		"stake=5&validFor=61&commitment=" + c,
107		"stake=5&validFor=10&opponent=%3Cscript%3E&commitment=" + c,
108		"stake=5&validFor=10&commitment=%3Cscript%3E",
109		"stake=5&validFor=10",
110	} {
111		out := Render("new?" + q)
112		uassert.False(t, strings.Contains(out, "func=Offer"), q)
113		uassert.True(t, strings.Contains(out, "Fix the following"), q)
114		uassert.False(t, strings.Contains(out, "<script>"), "form input echoed: "+q)
115	}
116}
117
118// svgs returns the decoded SVG of every data-URI image in out, and the
119// markdown that immediately wraps each image ("[" link or "" for none).
120func svgs(t *testing.T, out string) (images []string, linked []bool) {
121	parts := strings.Split(out, "(data:image/svg+xml;base64,")
122	for i, p := range parts[1:] {
123		end := strings.Index(p, ")")
124		raw, err := base64.StdEncoding.DecodeString(p[:end])
125		if err != nil {
126			t.Fatalf("image %d is not base64: %v", i, err)
127		}
128		images = append(images, string(raw))
129		before := parts[i]
130		img := strings.LastIndex(before, "![")
131		linked = append(linked, img > 0 && before[img-1] == '[')
132	}
133	return images, linked
134}
135
136func TestBoardIsClickableSVGColumns(cur realm, t *testing.T) {
137	reset()
138	g, p := accepted(cur)
139	page := "game/" + strconv.Itoa(g.ID)
140
141	// Reveal phase: the board is one image holding all 7 columns, not a link.
142	out := Render(page)
143	images, linked := svgs(t, out)
144	uassert.Equal(t, 1, len(images))
145	uassert.Equal(t, 7, strings.Count(images[0], `class="col"`))
146	uassert.False(t, linked[0], "no clickable column before the reveal")
147	uassert.True(t, strings.Contains(out, "![board: column 1:"), "alt text")
148	uassert.False(t, strings.Contains(out, "func=Play"))
149
150	testing.SetRealm(testing.NewUserRealm(alice))
151	pay(0)
152	revealBoth(cur, g, p)
153	first := g.Turn
154	move(cur, g, 3) // first mover drops in column 3
155
156	out = Render(page)
157	for c := 1; c <= 7; c++ {
158		uassert.True(t, strings.Contains(out, "](/r/samcrew/connect4$help&func=Play&column="+strconv.Itoa(c)+"&id=") ||
159			strings.Contains(out, "$help&func=Play&column="+strconv.Itoa(c)+"&id="), "column link "+strconv.Itoa(c))
160	}
161	images, linked = svgs(t, out)
162	boardImgs := 0
163	for i, img := range images {
164		if strings.Contains(img, `class="col"`) {
165			boardImgs++
166			uassert.True(t, linked[i], "board column must be a link while playing")
167		}
168	}
169	uassert.Equal(t, 7, boardImgs)
170	col3 := images[boardIndex(images, 3)]
171	uassert.True(t, strings.Contains(col3, `fill="`+[...]string{"", red, yellow}[first]+`"`), col3)
172	uassert.True(t, strings.Contains(col3, `stroke="`+lastMoveStroke+`"`), "last move ring")
173	uassert.True(t, strings.Contains(out, "![column 3: "+[...]string{"", "red", "yellow"}[first]), "alt text")
174
175	// Win with four stacked in column 1: the four cells get the win ring.
176	for _, c := range []int{1, 2, 1, 2, 1, 2} {
177		move(cur, g, c)
178	}
179	move(cur, g, 1)
180	uassert.Equal(t, "won", g.Status.String())
181	out = Render(page)
182	images, _ = svgs(t, out)
183	uassert.Equal(t, 1, len(images))
184	uassert.Equal(t, 4, strings.Count(images[0], `stroke="`+winStroke+`"`), images[0])
185	uassert.False(t, strings.Contains(out, "func=Play"))
186}
187
188// boardIndex returns the index in images of board column c (1-based).
189func boardIndex(images []string, c int) int {
190	n := 0
191	for i, img := range images {
192		if strings.Contains(img, `class="col"`) {
193			n++
194			if n == c {
195				return i
196			}
197		}
198	}
199	return -1
200}
201
202func TestLeaderboardsUpdateOnSettlement(cur realm, t *testing.T) {
203	reset()
204	g := started(cur)
205	testing.SetRealm(testing.NewUserRealm(alice))
206	pay(0)
207	Resign(cross(cur), g.ID)
208	uassert.Equal(t, 1, len(topWins))
209	uassert.Equal(t, bob, topWins[0].Addr)
210	uassert.Equal(t, stake-fee, topGnot[0].Score)
211	uassert.True(t, strings.Contains(Render(""), "| 1 | "+bob.String()+" | 1 |"))
212}
213
214func TestLobbyListsAtMostLobbyRows(cur realm, t *testing.T) {
215	reset()
216	for i := 0; i < lobbyRows+3; i++ {
217		testing.SetRealm(testing.NewUserRealm(alice))
218		pay(stake)
219		Offer(cross(cur), "", 10, commit(), fee)
220	}
221	out := Render("")
222	uassert.Equal(t, lobbyRows, strings.Count(out, "func=Accept"))
223	uassert.True(t, strings.Contains(out, "Showing the first 50 games."))
224}
225
226func TestLobbySkipsExpiredOffersAndShowsEachFee(cur realm, t *testing.T) {
227	reset()
228	// Expired offers nobody cancelled don't use up the row limit.
229	for i := 0; i < lobbyRows; i++ {
230		testing.SetRealm(testing.NewUserRealm(alice))
231		pay(stake)
232		Offer(cross(cur), "", 1, commit(), fee)
233	}
234	testing.SkipHeights(13) // 65s: all expired
235	g := started(cur)
236	expireClock()
237	out := Render("")
238	uassert.True(t, strings.Contains(out, gameLink(pageURL(""), g.ID)), out)
239	uassert.True(t, strings.Contains(out, "clock ran out"), "an expired clock is not shown as negative time")
240	uassert.False(t, strings.Contains(out, "Showing the first"))
241
242	// Each offer shows the fee it was snapshotted with, not the current one.
243	testing.SetRealm(testing.NewUserRealm(admin))
244	pay(0)
245	SetFee(cross(cur), 300_000)
246	testing.SetRealm(testing.NewUserRealm(alice))
247	pay(stake)
248	Offer(cross(cur), "", 30, commit(), fee)
249	testing.SetRealm(testing.NewUserRealm(admin))
250	pay(0)
251	SetFee(cross(cur), 100_000)
252	out = Render("")
253	uassert.True(t, strings.Contains(out, "| 10 GNOT | 0.3 GNOT |"), out)
254	uassert.True(t, strings.Contains(out, "(0.1 GNOT for new offers)"))
255}