package web
import (
"strings"
"testing"
)
// A thread's posts are a named template, so the live stream renders
// the same markup the page did. A second renderer for the live half is
// how two views of one thread drift apart.
func TestTheThreadsPostsAreOneTemplate(t *testing.T) {
data := pageData{
Repo: "alpha", ForumRoot: "abc123",
ForumPosts: []forumPostRow{
{Hash: "aaaaaaaaaaaa1111", User: "lou", HTML: "<p>first</p>", Date: "2026-09-16T09:00:00Z"},
{Hash: "bbbbbbbbbbbb2222", User: "mia", HTML: "<p>a reply</p>", Date: "2026-09-16T09:05:00Z"},
},
}
var frag strings.Builder
if err := noHooks.template(forumThreadPage).ExecuteTemplate(&frag, "forum-posts", data); err != nil {
t.Fatal(err)
}
got := frag.String()
// The patch target is on the fragment, or a patch has nowhere to
// land.
if !strings.Contains(got, `id="f-posts"`) {
t.Errorf("FAIL: the fragment carries no patch target")
}
for _, want := range []string{"first", "a reply", `id="p-aaaaaaaaaaaa"`} {
if !strings.Contains(got, want) {
t.Errorf("FAIL: the fragment is missing %q", want)
}
}
// And the whole page contains the same fragment, rather than a
// second rendering of the posts.
var page strings.Builder
if err := noHooks.template(forumThreadPage).ExecuteTemplate(&page, "content", data); err != nil {
t.Fatal(err)
}
if !strings.Contains(page.String(), got) {
t.Errorf("FAIL: the page does not render the same posts fragment")
}
}
// The data-init opens the stream, and sits on a WRAPPER rather than on
// the element the patch replaces: a patch morphs its target to match
// the fragment, which carries no data-init, so an attribute on the
// target is deleted by the first patch along with the connection.
func TestTheThreadOpensItsStreamFromAWrapper(t *testing.T) {
data := pageData{Repo: "alpha", ForumRoot: "abc123",
ForumStream: "/r/alpha/forum.sse/abc123",
ForumPosts: []forumPostRow{{Hash: "aaaaaaaaaaaa1111", User: "lou", HTML: "<p>hi</p>"}}}
var page strings.Builder
if err := noHooks.template(forumThreadPage).ExecuteTemplate(&page, "content", data); err != nil {
t.Fatal(err)
}
got := page.String()
if !strings.Contains(got, `data-init="@get('/r/alpha/forum.sse/abc123')"`) {
t.Errorf("FAIL: the thread does not open its stream")
}
// The attribute is BEFORE the target, not on it.
init := strings.Index(got, "data-init=")
target := strings.Index(got, `id="f-posts"`)
if init < 0 || target < 0 || init > target {
t.Errorf("FAIL: data-init is not on a wrapper outside the patched element")
}
// With no change plane there is no stream, and no attribute
// pointing at one.
data.ForumStream = ""
var cold strings.Builder
if err := noHooks.template(forumThreadPage).ExecuteTemplate(&cold, "content", data); err != nil {
t.Fatal(err)
}
if strings.Contains(cold.String(), "data-init") {
t.Errorf("FAIL: a page with no stream still opens one")
}
}