Skip to content
quarry Artifact [6f46f5aef8]
Login

Artifact [6f46f5aef8]

Artifact 6f46f5aef874ec5734213b681ea8cb9aa56b04440f9226102ecdfc6c83efc87e:


package web

import (
	"regexp"
	"strings"
	"testing"

	"golang.org/x/net/html"
)

// shownText is what a reader sees of a page: text nodes outside code,
// preformatted text and form controls, where a person may type "!12".
func shownText(t *testing.T, doc string) string {
	t.Helper()
	root, err := html.Parse(strings.NewReader(doc))
	if err != nil {
		t.Fatal(err)
	}
	var b strings.Builder
	var walk func(*html.Node)
	walk = func(n *html.Node) {
		if n.Type == html.ElementNode {
			switch n.Data {
			case "code", "pre", "textarea", "input", "script", "style":
				return
			}
		}
		if n.Type == html.TextNode {
			b.WriteString(n.Data + " ")
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			walk(c)
		}
	}
	walk(root)
	return b.String()
}

var shownBang = regexp.MustCompile(`(^|[^\w])![0-9]+`)

// A landing's number is shown as its state icon and the number, named
// "landing N, <state>", and never as "!N": on the listing, the strata
// rungs, the page's heading, the review queue, the timeline's merge and
// landing rows, and a check-in's landed-by line. The bang is what a
// person types. The Accept button carries no number at all.
func TestNoPageShowsALandingAsBangNumber(t *testing.T) {
	var pages = map[string]string{}
	var b strings.Builder
	must := func(err error) {
		t.Helper()
		if err != nil {
			t.Fatal(err)
		}
	}
	row := func(n int, state string) landingRow {
		return landingRow{ID: "aa11bb22cc33", Number: n, Branch: "a", Target: "trunk", State: state, Title: "A"}
	}
	must(noHooks.template(landingListPage).ExecuteTemplate(&b, "l-list-body", pageData{Repo: "quarry", Nav: ticketNav{Repo: "quarry"},
		Landings: []landingRow{row(4, "open"), row(5, "accepted"), row(6, "rejected")}}))
	pages["flat list"] = b.String()
	b.Reset()
	base, top := row(4, "open"), row(5, "open")
	base.Position, base.Depth, top.Position, top.Depth = 1, 2, 2, 2
	must(noHooks.template(landingListPage).ExecuteTemplate(&b, "l-list-body", pageData{Repo: "quarry", Nav: ticketNav{Repo: "quarry"},
		LandingGroups: []landingGroup{{landingRow: base, Segments: []landingRow{base, top}}}}))
	pages["strata"] = b.String()
	for _, own := range []bool{true, false} {
		b.Reset()
		d := landingDetail{landingRow: row(12, "accepted"), CanAccept: true, Mergeable: "clean"}
		must(noHooks.template(landingPage).ExecuteTemplate(&b, "landing-body", pageData{Title: "landing 12 Fix it", Repo: "quarry", Landing: &d, OwnH1: own, HideH1: !own}))
		pages["landing page"] = b.String()
	}
	b.Reset()
	must(noHooks.template(inboxPage).ExecuteTemplate(&b, "content", pageData{Title: "inbox", AttnFilter: "landings",
		LandingQueue: []queueRow{{Repo: "quarry", ID: "7be08128b9420890", Number: 9, Branch: "x", Target: "trunk", Title: "X"}}}))
	pages["review queue"] = b.String()
	b.Reset()
	must(noHooks.template(timelinePage).ExecuteTemplate(&b, "tl-body", pageData{Repo: "quarry", Days: []tlDay{{Date: "2026-09-30", Rows: []tlRow{
		{Kind: "ci", Hash: strings.Repeat("a", 64), Short: "aaaaaaaaaaaa", Landing: strings.Repeat("b", 64), LandingTitle: "B", LandingNumber: 771},
		{Kind: "l", Hash: strings.Repeat("c", 64), Title: "Landing: C", LandingNumber: 4}}}}}))
	pages["timeline"] = b.String()
	b.Reset()
	must(noHooks.template(checkinPage).Execute(&b, pageData{Repo: "alpha", Checkin: checkinDetail{
		Landing: &checkinLanding{ID: strings.Repeat("b", 64), Number: 7, Title: "Ship it", State: "accepted", Role: "merge"}}}))
	pages["check-in"] = b.String()

	for name, doc := range pages {
		if loc := shownBang.FindStringIndex(shownText(t, doc)); loc != nil {
			t.Errorf("%s shows a landing as !N: %q", name, shownText(t, doc)[loc[0]:loc[1]])
		}
		if !strings.Contains(doc, `data-hook="landing-number"`) {
			t.Errorf("%s carries no landing number", name)
		}
	}
	for name, want := range map[string]string{
		"flat list":    `aria-label="landing 5, accepted"`,
		"strata":       `aria-label="landing 4, open"`,
		"landing page": `aria-label="landing 12, accepted"`,
		"review queue": `aria-label="landing 9, open"`,
		"timeline":     `aria-label="landing 771, accepted"`,
		"check-in":     `aria-label="landing 7, accepted"`,
	} {
		if !strings.Contains(pages[name], want) || !strings.Contains(pages[name], `<use href="#i-landing"`) {
			t.Errorf("%s: no state icon labelled %s", name, want)
		}
	}
}

// The decide box says what the button does, with no number in it.
func TestAcceptButtonCarriesNoNumber(t *testing.T) {
	var b strings.Builder
	d := landingDetail{landingRow: landingRow{ID: strings.Repeat("a", 64), Number: 768, Branch: "work", Target: "trunk", State: "open"},
		CanAccept: true, Mergeable: "clean"}
	if d.AcceptState() != "go" {
		t.Fatal("fixture does not reach the accept button")
	}
	if err := noHooks.template(landingPage).ExecuteTemplate(&b, "landing-body", pageData{Repo: "quarry", Landing: &d, OwnH1: true}); err != nil {
		t.Fatal(err)
	}
	m := regexp.MustCompile(`(?s)<button[^>]*data-accept="go".*?</button>`).FindString(b.String())
	if m == "" || regexp.MustCompile(`[0-9]`).MatchString(regexp.MustCompile(`(?s)<[^>]*>`).ReplaceAllString(m, "")) {
		t.Errorf("the accept button shows a number: %s", m)
	}
}

// An Atom title is text: the landing is named in words.
func TestFeedNamesALandingInWords(t *testing.T) {
	head, _ := feedText(timelineEntry{Kind: "l", Comment: "fix", LandingNumber: 771})
	if !strings.HasPrefix(head, "landing 771 ") || strings.Contains(head, "!") {
		t.Errorf("feed title %q", head)
	}
}