Skip to content
quarry Artifact [512d62fe40]
Login

Artifact [512d62fe40]

Artifact 512d62fe4029154c0990f92a433feef33e1b86a543073c9807c35e026bce3bd1:


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())
}