package web
import (
"strings"
"testing"
)
func aTeamSettingsPage(stream string) pageData {
td := &teamDetail{
Team: teamRow{Name: "grove", Title: "Grove"},
CanManage: true,
Repos: []teamRepo{{Repo: "alpha", Added: "2026-09-01T00:00:00Z"}},
Offered: []teamOfferRow{{Repo: "beta", OfferedBy: "lou",
Offered: "2026-09-15T00:00:00Z", Expires: "2026-09-22T00:00:00Z"}},
MayRemove: []string{"alpha"},
}
data := teamSettingsData(td, noHooks.marks == nil, false, nil)
data.TeamStream = stream
return data
}
// The roster a changed team sends is the roster the page draws,
// rendered by the same template. Two views of one team cannot drift.
func TestAChangedTeamSendsItsRoster(t *testing.T) {
data := aTeamSettingsPage("")
got := teamRosterPart(&noHooks, data)
for _, want := range []string{
"event: datastar-patch-elements",
"data: selector #t-repos",
// Inner, because the wrapper carries the id the patch targets.
"data: mode inner",
"alpha",
"beta",
} {
if !strings.Contains(got, want) {
t.Errorf("FAIL: the patch is missing %q", want)
} else {
t.Logf("ok: the patch carries %q", want)
}
}
// It is comparable, which is what lets the stream stay quiet when
// nothing moved.
if teamRosterPart(&noHooks, data) != got {
t.Error("FAIL: the same team renders differently twice")
}
// And the page renders that same fragment rather than a second
// copy of the roster.
var page strings.Builder
if err := noHooks.template(teamSettingsPage).ExecuteTemplate(&page, "content", data); err != nil {
t.Fatal(err)
}
var frag strings.Builder
if err := noHooks.template(teamSettingsPage).ExecuteTemplate(&frag, "team-repos", data); err != nil {
t.Fatal(err)
}
if !strings.Contains(page.String(), frag.String()) {
t.Error("FAIL: the page does not render the same roster fragment")
} else {
t.Log("ok: page and stream render one template")
}
}
// The page opens its stream from a wrapper, and says nothing when
// there is no stream to open.
func TestATeamOpensItsStreamFromAWrapper(t *testing.T) {
var page strings.Builder
if err := noHooks.template(teamSettingsPage).ExecuteTemplate(&page,
"content", aTeamSettingsPage("/t/grove/settings.sse")); err != nil {
t.Fatal(err)
}
got := page.String()
if !strings.Contains(got, `data-init="@get('/t/grove/settings.sse')"`) {
t.Fatal("FAIL: the team settings page does not open its stream")
}
// Outside the patched region, not on it: a patch morphs its target
// and would take the attribute — and the connection — with it.
init := strings.Index(got, "data-init=")
target := strings.Index(got, `id="t-repos"`)
if target < 0 || init > target {
t.Errorf("FAIL: data-init is not outside #t-repos (init %d, target %d)", init, target)
} else {
t.Log("ok: data-init sits outside the patched element")
}
var quiet strings.Builder
if err := noHooks.template(teamSettingsPage).ExecuteTemplate(&quiet, "content", aTeamSettingsPage("")); err != nil {
t.Fatal(err)
}
if strings.Contains(quiet.String(), "data-init=") {
t.Error("FAIL: the page opens a stream it was given no address for")
} else {
t.Log("ok: no stream, no data-init")
}
}
// The forms stay OUT of the patched region. A patch morphs what it
// lands on, so streaming the name, the description or the image picker
// would take the letters out from under somebody part-way through
// typing them — to tell them about a repository.
func TestTheTeamsFormsAreNotStreamed(t *testing.T) {
var frag strings.Builder
if err := noHooks.template(teamSettingsPage).ExecuteTemplate(&frag,
"team-repos", aTeamSettingsPage("")); err != nil {
t.Fatal(err)
}
got := frag.String()
for _, hook := range []string{`id="t-title"`, `id="t-descr"`, `data-hook="avatar-edit"`} {
if strings.Contains(got, hook) {
t.Errorf("FAIL: %s is inside the patched region", hook)
} else {
t.Logf("ok: %s is not streamed", hook)
}
}
// The roster itself is in there, or the region is the wrong one.
if !strings.Contains(got, "alpha") {
t.Error("FAIL: the roster is not in the patched region")
}
}