package web
import (
"os"
"regexp"
"strings"
"testing"
)
// A form that opts into the pending state must carry the signal it
// reads, and every busy button must sit inside such a form.
//
// The pending affordance lived on exactly one page. Its CSS was scoped
// to .d-act, which also carries the landing card's layout, its
// ghost-disabled rule and the CI override block — four concerns under
// one selector, so the behaviour could not be reused without dragging
// the layout along. 138 other submit buttons had no pending state at
// all: no feedback on a slow POST, and nothing stopping a second
// press starting a second clone.
//
// The halves must not drift apart. A button with data-class:is-busy
// inside a form with no data-signals:busy silently never spins, and a
// form that declares the signal with no button to read it is dead
// markup that reads as if the page has the affordance.
func TestThePendingStateHasBothItsHalves(t *testing.T) {
src, err := os.ReadFile("web.go")
if err != nil {
t.Fatal(err)
}
form := regexp.MustCompile(`(?s)<form\b[^>]*\bclass="[^"]*\bacting\b[^"]*"[^>]*>`)
forms := form.FindAllString(string(src), -1)
if len(forms) < 5 {
t.Fatalf("FAIL: only %d forms opt into the pending state; the point was to stop "+
"it living on one page", len(forms))
}
for _, f := range forms {
if !strings.Contains(f, "data-signals:busy") {
t.Errorf("FAIL: a form claims the acting class but declares no busy signal, "+
"so its button can never spin:\n %s", f)
}
if !strings.Contains(f, "data-class:busy") {
t.Errorf("FAIL: a form declares the signal but never reflects it, so the other "+
"controls keep taking presses while the server works:\n %s", f)
}
}
}
// The busy label is a THIRD state, not a reuse of the responsive pair.
//
// .b-long and .b-abbr are a pair: the sheet hides the long one under
// 30rem and shows the short one. A short label that borrows .b-long
// renders as an empty outlined pill on a phone — which is what my
// first version of this change did, and what buttonlabel_test.go
// caught. Short labels use .b-idle, which has no narrow variant and
// is hidden only while busy.
func TestAShortIdleLabelDoesNotBorrowTheResponsivePair(t *testing.T) {
// styleSource for the CSS half and the markup half both: the rules
// moved to stylesheet.go, the buttons are still templates in
// web.go, and styleSource is baseHTML plus the sheet, so it holds
// both.
s := styleSource + webSource(t)
if !strings.Contains(s, ".acting button.is-busy .b-idle") {
t.Error("FAIL: .b-idle is not hidden while busy, so the idle label and the " +
"spinner label render together")
}
// Every busy button needs SOME idle label, or it is blank until the
// press and blank is not a control.
button := regexp.MustCompile(`(?s)<button\b[^>]*data-class:is-busy[^>]*>.*?</button>`)
for _, b := range button.FindAllString(s, -1) {
if !strings.Contains(b, "b-busy") {
t.Errorf("FAIL: a button reflects the busy signal but has no busy label:\n %s", trim(b))
}
if !strings.Contains(b, "b-idle") && !strings.Contains(b, "b-long") {
t.Errorf("FAIL: a busy button has no idle label at all:\n %s", trim(b))
}
}
}
func trim(s string) string {
one := strings.Join(strings.Fields(s), " ")
if len(one) > 150 {
return one[:150] + "…"
}
return one
}