package web
import (
"os"
"path/filepath"
"strings"
"testing"
)
// The empty teams page used to say "Anyone can make one" beside no way
// to make one: team.create has always existed on the API, and the web
// had no door to it. A sentence naming an action the UI cannot perform
// is worse than no sentence, so the claim and the control ship together
// or not at all.
func TestTheEmptyTeamsPageOffersTheControlItDescribes(t *testing.T) {
var out strings.Builder
if err := noHooks.template(teamsPage).ExecuteTemplate(&out, "content",
pageData{Explore: "teams", SignedIn: "dan"}); err != nil {
t.Fatal(err)
}
body := out.String()
if !strings.Contains(body, `href="/teams/new"`) {
t.Error("FAIL: a signed-in reader is told teams exist but given no way to make one")
}
// A visitor is not offered a control that would bounce them to a
// sign-in: they are told what the requirement is instead.
var anon strings.Builder
if err := noHooks.template(teamsPage).ExecuteTemplate(&anon, "content",
pageData{Explore: "teams"}); err != nil {
t.Fatal(err)
}
if strings.Contains(anon.String(), `href="/teams/new"`) {
t.Error("FAIL: a signed-out visitor is offered the create control")
}
if !strings.Contains(anon.String(), "signed-in") {
t.Error("FAIL: a signed-out visitor is not told what making a team requires")
}
}
// The Teams tab is hidden while the forge has no teams, so the teams
// page cannot be the only way to reach the form that makes the first
// one. The create menu is the door, and it is open to any signed-in
// account because team.create is.
func TestTheCreateMenuReachesTheTeamForm(t *testing.T) {
var out strings.Builder
// peoplePage, not teamsPage: the teams page emits this link from
// its own empty state, which would pass the assertion without the
// menu entry existing at all.
if err := noHooks.template(peoplePage).Execute(&out, pageData{SignedIn: "dan"}); err != nil {
t.Fatal(err)
}
if !strings.Contains(out.String(), `href="/teams/new"`) {
t.Error("FAIL: a signed-in account cannot reach the team form from the create menu")
}
// Not gated on admin, the way the repository entry is.
var admin strings.Builder
if err := noHooks.template(peoplePage).Execute(&admin,
pageData{SignedIn: "dan", IsAdmin: true}); err != nil {
t.Fatal(err)
}
if !strings.Contains(admin.String(), `href="/teams/new"`) {
t.Error("FAIL: the team entry vanished for an admin")
}
// A signed-out visitor has no create menu at all.
var anon strings.Builder
if err := noHooks.template(peoplePage).Execute(&anon, pageData{}); err != nil {
t.Fatal(err)
}
if strings.Contains(anon.String(), `href="/teams/new"`) {
t.Error("FAIL: a signed-out visitor is offered the team form")
}
}
// The form carries the three fields team.create takes, and says the one
// thing about the handle that cannot be undone later.
func TestTheTeamFormAsksForWhatTeamCreateTakes(t *testing.T) {
var out strings.Builder
if err := noHooks.template(teamNewPage).ExecuteTemplate(&out, "content",
pageData{Title: "New team"}); err != nil {
t.Fatal(err)
}
body := out.String()
for _, field := range []string{`name="name"`, `name="title"`, `name="descr"`} {
if !strings.Contains(body, field) {
t.Errorf("FAIL: the form is missing %s", field)
}
}
if !strings.Contains(body, `action="/teams"`) {
t.Error("FAIL: the form does not post to team.create's route")
}
if !strings.Contains(body, "never changes") {
t.Error("FAIL: the form does not say the handle is permanent")
}
}
// A refused handle comes back as the form with the reason on it, not as
// a refusal card: the reader has a correction to make and needs the
// field to make it in.
func TestARefusedHandleComesBackOnTheForm(t *testing.T) {
var out strings.Builder
if err := noHooks.template(teamNewPage).ExecuteTemplate(&out, "content",
pageData{Title: "New team", Error: `an account already holds "dan"`}); err != nil {
t.Fatal(err)
}
body := out.String()
if !strings.Contains(body, "already holds") {
t.Error("FAIL: the refusal reason is not shown")
}
if !strings.Contains(body, `role="alert"`) {
t.Error("FAIL: the refusal is not announced to a screen reader")
}
if !strings.Contains(body, `name="name"`) {
t.Error("FAIL: the form is gone, so there is nothing to correct")
}
}
// TestWriteTeamNewShot is a LOOKING aid, not a guard: it writes the new
// team form as a standalone page so the field stack, the handle note
// and the refusal state can be seen rather than asserted. Off unless
// QUARRY_SHOT names a file.
func TestWriteTeamNewShot(t *testing.T) {
out := os.Getenv("QUARRY_SHOT")
if out == "" {
t.Skip("QUARRY_SHOT unset")
}
d := pageData{Title: "New team", Explore: "teams", SignedIn: "dmestas"}
page := teamNewPage
if os.Getenv("QUARRY_SHOT_ERR") != "" {
d.Error = `an account already holds "dan"`
}
// The empty teams page is the other half of this change: the claim
// and the control have to be looked at together.
if os.Getenv("QUARRY_SHOT_EMPTY") != "" {
page, d = teamsPage, pageData{Title: "Teams", HideH1: true,
Explore: "teams", SignedIn: os.Getenv("QUARRY_SHOT_WHO"),
FleetTotal: 23, PeopleTotal: 4}
}
// The stylesheet inline, so the file renders standalone.
d.SheetURL = "quarry.css"
var buf strings.Builder
if err := noHooks.template(page).Execute(&buf, d); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(out, []byte(buf.String()), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(filepath.Dir(out), "quarry.css"), []byte(sheet), 0o644); err != nil {
t.Fatal(err)
}
t.Logf("ok: wrote %s (%d bytes)", out, buf.Len())
}