1package reactions23import (4 "strconv"5 "strings"6 "testing"78 rx "gno.land/p/moul/reactions/v0"9 "gno.land/p/nt/testutils/v0"10 "gno.land/p/nt/uassert/v0"11)1213const homePage = "gno.land/r/moul/home"1415var (16 alice = testutils.TestAddress("alice")17 bob = testutils.TestAddress("bob")18)1920// reset clears realm state. Realm globals live for the whole test binary, and21// the Example runs after every Test, so everything that renders starts here.22func reset() { board = rx.NewBoard() }2324func TestReactAndUnreact(cur realm, t *testing.T) {25 reset()2627 testing.SetRealm(testing.NewUserRealm(alice))28 React(cross(cur), homePage, rx.KeyUp)29 testing.SetRealm(testing.NewUserRealm(bob))30 React(cross(cur), homePage, rx.KeyFire)3132 uassert.Equal(t, 2, Count(homePage))33 uassert.Equal(t, 1, CountOf(homePage, rx.KeyUp))34 uassert.Equal(t, rx.KeyUp, ReactionOf(homePage, alice))35 uassert.Equal(t, 1, Pages())3637 key, who, _ := Last(homePage)38 uassert.Equal(t, rx.KeyFire, key)39 uassert.Equal(t, bob, who)4041 testing.SetRealm(testing.NewUserRealm(alice))42 Unreact(cross(cur), homePage)43 uassert.Equal(t, 1, Count(homePage))44 uassert.Equal(t, "", ReactionOf(homePage, alice))45}4647func TestReadsOnAnUntouchedPageAreZero(t *testing.T) {48 reset()49 uassert.Equal(t, 0, Count("gno.land/r/nobody/here"))50 uassert.Equal(t, 0, CountOf("gno.land/r/nobody/here", rx.KeyUp))51 uassert.Equal(t, "", ReactionOf("gno.land/r/nobody/here", alice))52 key, _, _ := Last("gno.land/r/nobody/here")53 uassert.Equal(t, "", key)54}5556func TestReactRefusals(cur realm, t *testing.T) {57 reset()58 testing.SetRealm(testing.NewUserRealm(alice))59 uassert.AbortsWithMessage(t, cur, "reactions: no such reaction: poop", func() {60 React(cross(cur), homePage, "poop")61 })6263 testing.SetRealm(testing.NewUserRealm(alice))64 uassert.AbortsWithMessage(t, cur, "reactions: not a page key: not a path", func() {65 React(cross(cur), "not a path", rx.KeyUp)66 })6768 uassert.Equal(t, 0, Pages(), "a refused reaction creates no page")69}7071// TestUnreactAbortsRatherThanNoOp is the deliberate asymmetry: a misspelled72// page key on Unreact has to be visible, or a reader thinks they removed73// something they did not.74func TestUnreactAbortsRatherThanNoOp(cur realm, t *testing.T) {75 reset()76 testing.SetRealm(testing.NewUserRealm(alice))77 uassert.AbortsWithMessage(t, cur, "reactions: nothing to remove on "+homePage, func() {78 Unreact(cross(cur), homePage)79 })80}8182// TestRenderBlockForIsWhatTheBlockLinksTo pins the property a host realm83// depends on: every link in the embedded block points at THIS realm, not at84// the realm that embedded it, and carries the page key as an argument.85func TestRenderBlockForIsWhatTheBlockLinksTo(t *testing.T) {86 reset()87 got := RenderBlockFor(homePage)88 uassert.True(t, strings.Contains(got, "(/r/moul/reactions/v0$help&func=React&"),89 "the txlink targets this realm")90 uassert.True(t, strings.Contains(got, "page=gno.land%2Fr%2Fmoul%2Fhome"),91 "and carries the host page as the parameter")92 uassert.False(t, strings.Contains(got, "/r/moul/home$help"),93 "never the host realm, which has no React to call")94}9596// TestRenderBlockKeysOnTheCaller is the borrowed-read rule this realm's whole97// ergonomics rest on: RenderBlock has no cur realm parameter, so gno opens no98// frame for it and unsafe.CurrentRealm() reports whoever called.99//100// In-package there is no other realm to call from, so what this pins is the101// half that can be pinned here: RenderBlock agrees with RenderBlockFor when102// the caller IS this realm. The cross-realm half is measured in AGENTS.md103// (2026-09-22) and is the same mechanism r/moul/config's TopBlock uses.104func TestRenderBlockKeysOnTheCaller(t *testing.T) {105 reset()106 uassert.Equal(t, RenderBlockFor(realmPath), RenderBlock())107 uassert.NotEqual(t, RenderBlockFor(homePage), RenderBlock(),108 "and a different page is a different block")109}110111func TestRenderPageView(cur realm, t *testing.T) {112 reset()113 testing.SetRealm(testing.NewUserRealm(alice))114 React(cross(cur), homePage, rx.KeyUp)115116 got := Render(homePage)117 uassert.True(t, strings.HasPrefix(got, "# gno\\.land/r/moul/home\n"),118 "the heading is the page key, escaped: ui.Inline backslashes the dot")119 uassert.True(t, strings.Contains(got, "Last reaction: \U0001F44D by "))120 uassert.True(t, strings.Contains(got, "Remove my reaction"))121 uassert.True(t, strings.Contains(got, "[The board](/r/moul/reactions/v0)"))122}123124func TestRenderRejectsAPathThatIsNotAPageKey(t *testing.T) {125 reset()126 got := Render("[click](http://e.vil)")127 uassert.True(t, strings.HasPrefix(got, "# Not a page\n"))128 uassert.False(t, strings.Contains(got, "](http://e.vil)"),129 "a render path is escaped before it is shown back")130}131132// TestRenderHomeShowsTheBoard uses uassert rather than an Example because the133// page contains consecutive blank lines, which gno collapses inside an134// // Output: block.135func TestRenderHomeShowsTheBoard(cur realm, t *testing.T) {136 reset()137 uassert.True(t, strings.Contains(Render(""), "*No page has been reacted to yet.*"))138139 testing.SetRealm(testing.NewUserRealm(alice))140 React(cross(cur), homePage, rx.KeyParty)141142 got := Render("")143 uassert.True(t, strings.Contains(got,144 "| [gno\\.land/r/moul/home](/r/moul/reactions/v0:gno.land/r/moul/home) | 1 |"),145 "the cell text is escaped, the URL is not: they are different contexts")146 uassert.True(t, strings.Contains(got, "import \"gno.land/r/moul/reactions/v0\""),147 "the homepage is the getting-started doc, and the snippet is copy-pasteable")148 for _, k := range rx.Keys() {149 uassert.True(t, strings.Contains(got, "| `"+k+"` | "+rx.Emoji(k)+" |"), k)150 }151}152153// TestBoardIndexIsCapped: the number of pages is unbounded, the index is not.154func TestBoardIndexIsCapped(cur realm, t *testing.T) {155 reset()156 for i := 0; i < maxListed+3; i++ {157 testing.SetRealm(testing.NewUserRealm(alice))158 // Zero-padded, because avl keys sort as strings and ufmt has no width159 // flags: "p9" would otherwise sort between "p10" and "p11".160 n := strconv.Itoa(i)161 if len(n) < 2 {162 n = "0" + n163 }164 React(cross(cur), "gno.land/r/spam/p"+n, rx.KeyUp)165 }166 uassert.Equal(t, maxListed+3, Pages())167168 got := Render("")169 uassert.Equal(t, maxListed, strings.Count(got, "(/r/moul/reactions/v0:gno.land/r/spam/"))170 uassert.True(t, strings.Contains(got, "28 pages in total, 25 shown."))171}172Count(page string) int
CountOf(page string, key string) int
Last(page string) (key string, who string, height int64)
Pages() int
React(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, page string, key string)
ReactionOf(page string, who string) string
Render(path string) string
RenderBlock() string
RenderBlockFor(page string) string
Unreact(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, page string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.