package web
import (
"bytes"
"craftdesign.group/quarry/internal/oltp"
"strings"
"testing"
"craftdesign.group/quarry/internal/fault"
)
// The fleet family's nav follows the same tab convention as the repo
// and explore bars: underline tabs whose ink carries a
// view-transition-name. One nav reaches all four surfaces, so none of
// them can ship unreachable again — /fleet/ops did, twice.
func TestFleetNavWearsTheSharedTabs(t *testing.T) {
for _, tc := range []struct{ tab, here string }{
{"stores", `href="/fleet" class="here"`},
{"ops", `href="/fleet/ops" class="here"`},
{"errors", `href="/fleet/exceptions" class="here"`},
} {
var buf bytes.Buffer
if err := exceptionsPage.ExecuteTemplate(&buf, "fleet-nav", pageData{FleetTab: tc.tab}); err != nil {
t.Fatal(err)
}
html := buf.String()
if !strings.Contains(html, `class="fleet-tabs"`) {
t.Fatalf("fleet-nav is not the tabs convention:\n%s", html)
}
if !strings.Contains(html, tc.here) || !strings.Contains(html, `class="ink"`) {
t.Errorf("tab %s: no underline on its own surface:\n%s", tc.tab, html)
}
}
}
// Every fleet surface renders THROUGH the shared nav. The nav lives in
// the LAYOUT now, in the fleet's own bar, the way a repository's tabs
// do — so the whole page is what carries it, not the content block.
func TestExceptionsPageCarriesTheFleetNav(t *testing.T) {
var buf bytes.Buffer
// The same page data the handler builds.
if err := exceptionsPage.Execute(&buf, pageData{Title: "Exceptions",
HideH1: true, FleetTab: "errors"}); err != nil {
t.Fatal(err)
}
html := buf.String()
if !strings.Contains(html, `class="fleet-tabs"`) {
t.Fatalf("no fleet tabs on the exceptions page:\n%.400s", html)
}
if strings.Contains(html, `seg-box" aria-label="fleet"`) {
t.Fatal("the fleet nav still renders as a seg-box")
}
// The bar names the place once; the sub-page does not repeat it in
// a visible heading, but still says it for a screen reader.
if strings.Contains(html, "<h1>Exceptions</h1>") {
t.Error("the exceptions page still draws its own visible h1")
}
if !strings.Contains(html, `<h1 class="sr-only">Exceptions</h1>`) {
t.Error("the page lost its heading entirely; it must keep one for screen readers")
}
if !strings.Contains(html, `class="repo" href="/fleet"`) {
t.Error("the fleet bar does not name the place")
}
}
// The fleet settings page renders. Its save button sits inside a
// {{with .Visibility}}, where the dot is the panel and not the page —
// reaching for the page's own SavedPanel from in there failed at
// EXECUTION time, so the whole page served a template error while
// compiling and every test passed.
func TestFleetSettingsPageRenders(t *testing.T) {
var buf bytes.Buffer
err := fleetSettingsPage.Execute(&buf, pageData{Title: "fleet settings",
OwnH1: true, FleetTab: "settings", SavedPanel: "fleet",
Visibility: &visibilityPanel{Current: "private",
Options: []oltp.Visibility{{Name: "private", Blurb: "only members"}}}})
if err != nil {
t.Fatalf("fleet settings page does not render: %v", err)
}
if !strings.Contains(buf.String(), `class="fleet-tabs"`) {
t.Error("the settings page is not wearing the fleet bar")
}
}
// A row has to be actionable standing alone: what broke, what it costs,
// what to do, and the specifics as data rather than prose an agent
// would have to parse back out of a sentence.
func TestExceptionRowCarriesWhatAReaderNeedsToAct(t *testing.T) {
var buf bytes.Buffer
err := exceptionsPage.Execute(&buf, pageData{Title: "Exceptions", FleetTab: "errors",
ErrorTotal: 40, ErrorDistinct: 1, ServerErrors: []errlogRow{{
Message: "ingest lane is full", Surface: "ingest", Path: "ingest alpha",
Repo: "alpha", Count: 40, Build: "1.2.3", Req: "9f",
Why: "check-ins wait to be indexed; nothing is lost",
Fix: "raise the lane's limit or add a node",
Fields: []fault.Field{{Key: "queued", Value: "32"}, {Key: "shed", Value: "833"}},
}}})
if err != nil {
t.Fatal(err)
}
html := buf.String()
for _, want := range []string{`class="exc-why"`, `class="exc-cost"`, `class="exc-fix"`,
`class="exc-fields"`, "<dt>queued</dt>", "<dd><code>32</code></dd>", "1.2.3"} {
if !strings.Contains(html, want) {
t.Errorf("the row is missing %s", want)
}
}
}
// The header used to print the length of its own slice as the number of
// signatures, so a journal holding thirty faults reported twenty-five
// and gave no sign it had hidden five.
func TestExceptionsHeaderAdmitsWhatItIsNotShowing(t *testing.T) {
render := func(shown, distinct int) string {
rows := make([]errlogRow, shown)
for i := range rows {
rows[i] = errlogRow{Message: "x", Surface: "web", Count: 1}
}
var buf bytes.Buffer
if err := exceptionsPage.Execute(&buf, pageData{Title: "Exceptions", FleetTab: "errors",
ServerErrors: rows, ErrorDistinct: distinct, ErrorTotal: 99}); err != nil {
t.Fatal(err)
}
head := buf.String()
i := strings.Index(head, `<p class="tl-meta-inline">`)
return head[i:min(i+140, len(head))]
}
if got := render(25, 30); !strings.Contains(got, "25 of 30") {
t.Errorf("a truncated list does not say so: %s", got)
}
if got := render(30, 30); strings.Contains(got, " of ") {
t.Errorf("a complete list claims to be truncated: %s", got)
}
}
// The row says how often lately, not only how often ever — and draws
// the shape, so a burst that ended and a fault still occurring stop
// looking like the same big number.
func TestExceptionRowSaysHowOftenLately(t *testing.T) {
steady := errlogRow{Message: "x", Surface: "web", Count: 833}
steady.Rate = make([]int, 24)
for i := range steady.Rate {
steady.Rate[i] = 45
}
if got := steady.Lately(); got != "45/hour" {
t.Errorf("rate reads %q", got)
}
if steady.Spark() == "" {
t.Error("a fault with occurrences draws no shape")
}
// A handful over a day is a number, not a rate: "0.2/hour" tells a
// reader nothing they can act on.
rare := errlogRow{Count: 3, Rate: make([]int, 24)}
rare.Rate[3], rare.Rate[9] = 1, 2
if got := rare.Lately(); got != "3 in 24h" {
t.Errorf("a rare fault reads %q", got)
}
// Nothing recent: no rate and no sparkline at all, rather than a
// flatline that looks like data.
quiet := errlogRow{Count: 150, Rate: make([]int, 24)}
if quiet.Lately() != "" || quiet.Spark() != "" {
t.Error("a fault with no recent occurrence still claims a rate")
}
if (errlogRow{Count: 1}).Lately() != "" {
t.Error("a row with no rate data invented one")
}
}