Skip to content
quarry Artifact [d82e0c5b8f]
Login

Artifact [d82e0c5b8f]

Artifact d82e0c5b8fa28b3bff6b5432e551e9ccde47bddf7c0de64e336128375024ef6f:


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) {
	src, err := os.ReadFile("web.go")
	if err != nil {
		t.Fatal(err)
	}
	s := string(src)
	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
}