rastrillo / fiesta Public

Hold the example file format and the layouts in tests, and cover the one-newline trim and the reversed slot

The exporter enforced the file format when it wrote the documents,
and nothing held it afterwards; examples/README.md calls the
documents hand-owned, and Tasks 9 to 13 read the head and the slot.
Two tests now hold it over the real tree. Each has a control subtest:
a copy of a real document with the doctype dropped, lang changed, the
viewport removed, the theme renamed or the store attribute added must
be caught, and a layout entry given a second state, a second slot or
no slot must be caught, so a pass over the real files is not vacuous.

Already passing on the stub: TestMarkupRefusesAMissingState and
TestSlotRefusesTwoSlots (first Task 5 commit) went green against
stubs that returned an error for everything. They are negative tests
with no honest RED short of a wrong implementation.

Summary

example/example_test.go: TestEveryExampleHasTheStandardHead (doctype, html lang and dir, charset, viewport, title, tokens.css, day.css, and the scheme-store attribute only on scheme-menu/standalone) and TestEveryLayoutHasOneDefaultAndOneSlot, each with a control leg that plants the faults in a copy of a real document and expects them named; Markup's trim of exactly one newline; Slot's reversed markers; an empty srcset candidate is an error rather than a panic.
Agent notes
Task 5 fix round 1, Minors 5, 6 and 7. TestMarkupRefusesAMissingState and TestSlotRefusesTwoSlots, from the first Task 5 commit, were already passing on the stubs: a stub that always errors satisfies a test that wants an error. Recorded here as the test discipline asks; the new TestSlotRefusesReversedMarkers is of the same kind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e3049134a3e7c00fd0b66e0d97fde7557d21323c parent 111e5b6
1 file changed, +195 −1
  • example/example_test.go +195 −1
diff --git a/example/example_test.go b/example/example_test.go
index 70f19d9..8d6a8d4 100644
--- a/example/example_test.go
+++ b/example/example_test.go
@@ -3,8 +3,10 @@ package example_test
import (
"io/fs"
"path"
+ "regexp"
"slices"
"sort"
+ "strconv"
"strings"
"testing"
"testing/fstest"
@@ -47,6 +49,17 @@ func TestMarkupReturnsTheBodyAsWritten(t *testing.T) {
if got != synthBody {
t.Fatalf("Markup returned\n%s\nwant\n%s", got, synthBody)
}
+ // Exactly one newline is trimmed at each end: a body that ends in a
+ // blank line keeps it.
+ fsys := synthFS()
+ fsys["examples/field/blank.html"] = &fstest.MapFile{Data: []byte(synthDoc(synthBody + "\n"))}
+ got, err = example.Markup(fsys, "field", "blank")
+ if err != nil {
+ t.Fatalf("Markup: %v", err)
+ }
+ if got != synthBody+"\n" {
+ t.Fatalf("Markup trimmed more than one trailing newline:\n%q", got[len(got)-20:])
+ }
}
func TestMarkupRefusesAMissingState(t *testing.T) {
@@ -80,6 +93,13 @@ func TestSlotRefusesTwoSlots(t *testing.T) {
}
}
+func TestSlotRefusesReversedMarkers(t *testing.T) {
+ doc := "<main><!-- /fiesta:slot -->a<!-- fiesta:slot --></main>"
+ if _, err := example.Slot(doc, "c"); err == nil {
+ t.Fatal("Slot on a document whose slot closes before it opens returned no error")
+ }
+}
+
// ── The real examples ────────────────────────────────────────────────
// tree is the embedded design system, which carries examples/ from
@@ -268,7 +288,12 @@ func TestExamplesLinkOnlyFiestasOwnFiles(t *testing.T) {
continue
}
for _, cand := range strings.Split(v, ",") {
- file := strings.Fields(cand)[0]
+ fields := strings.Fields(cand)
+ if len(fields) == 0 {
+ t.Errorf("%s: the body's %s=%q has an empty candidate", where, name, v)
+ continue
+ }
+ file := fields[0]
if !strings.HasPrefix(file, "../assets/") {
t.Errorf("%s: the body's %s=%q is not under ../assets/", where, name, v)
continue
@@ -381,6 +406,175 @@ func TestEveryStateIsValidOnItsOwn(t *testing.T) {
})
}
+// headProblems lists what is wrong with one document's standard head:
+// the doctype, <html lang="en" dir="ltr"> (with the scheme store
+// attribute only where store says so), the charset, the viewport, a
+// non-empty <title>, and the two stylesheets.
+func headProblems(t *testing.T, doc string, store bool) []string {
+ t.Helper()
+ var problems []string
+ if !strings.HasPrefix(doc, "<!doctype html>\n") {
+ problems = append(problems, "no <!doctype html> first")
+ }
+ var sawHTML, charset, viewport, title, tokens, day bool
+ walk(t, doc, func(tok html.Token, inHead bool) {
+ switch {
+ case tok.Data == "html":
+ sawHTML = true
+ if v, _ := attr(tok, "lang"); v != "en" {
+ problems = append(problems, "<html> lang is "+strconv.Quote(v)+", want en")
+ }
+ if v, _ := attr(tok, "dir"); v != "ltr" {
+ problems = append(problems, "<html> dir is "+strconv.Quote(v)+", want ltr")
+ }
+ if v, ok := attr(tok, "data-rst-scheme-store"); ok != store || (ok && v != "local") {
+ problems = append(problems, "<html> data-rst-scheme-store is "+strconv.Quote(v)+", present "+strconv.FormatBool(ok)+", want present "+strconv.FormatBool(store))
+ }
+ case !inHead:
+ case tok.Data == "meta":
+ if v, ok := attr(tok, "charset"); ok && strings.EqualFold(v, "utf-8") {
+ charset = true
+ }
+ if v, _ := attr(tok, "name"); v == "viewport" {
+ viewport = true
+ }
+ case tok.Data == "link":
+ v, _ := attr(tok, "href")
+ tokens = tokens || v == "../../css/tokens.css"
+ day = day || v == "../../css/themes/day.css"
+ }
+ })
+ if m := titlePattern.FindStringSubmatch(doc); m != nil && strings.TrimSpace(m[1]) != "" {
+ title = true
+ }
+ for _, c := range []struct {
+ ok bool
+ what string
+ }{{sawHTML, "no <html> tag"}, {charset, "no <meta charset=\"utf-8\"> in the head"}, {viewport, "no viewport meta in the head"},
+ {title, "no non-empty <title>"}, {tokens, "the head does not link ../../css/tokens.css"}, {day, "the head does not link ../../css/themes/day.css"}} {
+ if !c.ok {
+ problems = append(problems, c.what)
+ }
+ }
+ return problems
+}
+
+var titlePattern = regexp.MustCompile(`(?s)<title>(.*?)</title>`)
+
+// TestEveryExampleHasTheStandardHead holds the example file format
+// the exporter wrote and the README now calls hand-owned: a hand edit
+// that drops the viewport or the theme fails here. The control leg
+// plants each fault in a copy of a real document and expects the
+// checker to name it, so the pass below is not vacuous.
+func TestEveryExampleHasTheStandardHead(t *testing.T) {
+ fsys, m := manifest(t)
+ t.Run("control", func(t *testing.T) {
+ doc, err := example.Document(fsys, m.Examples[0].Name, m.Examples[0].Default)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, c := range []struct {
+ name, from, to, want string
+ }{
+ {"doctype", "<!doctype html>\n", "", "no <!doctype html> first"},
+ {"lang", `lang="en"`, `lang="es"`, `<html> lang is "es", want en`},
+ {"viewport", `<meta name="viewport" content="width=device-width">`, "", "no viewport meta in the head"},
+ {"theme", `href="../../css/themes/day.css"`, `href="../../css/themes/night.css"`, "the head does not link ../../css/themes/day.css"},
+ {"store", `dir="ltr"`, `dir="ltr" data-rst-scheme-store="local"`, `<html> data-rst-scheme-store is "local", present true, want present false`},
+ } {
+ mutated := strings.Replace(doc, c.from, c.to, 1)
+ if mutated == doc {
+ t.Fatalf("%s: the mutation changed nothing; the control proves nothing", c.name)
+ }
+ if got := headProblems(t, mutated, false); !slices.Contains(got, c.want) {
+ t.Errorf("%s: the checker reported %v, want it to include %q", c.name, got, c.want)
+ }
+ }
+ if got := headProblems(t, doc, false); len(got) != 0 {
+ t.Errorf("the unmutated document reports %v", got)
+ }
+ })
+ everyDocument(t, func(e example.Entry, s example.State, doc string) {
+ store := e.Name == "scheme-menu" && s.ID == "standalone"
+ for _, p := range headProblems(t, doc, store) {
+ t.Errorf("%s/%s: %s", e.Name, s.ID, p)
+ }
+ })
+}
+
+// layoutProblems lists what is wrong with a layout entry and its one
+// document: it has exactly one state, default, which is its Default,
+// and the document has exactly one slot. A non-layout document has no
+// slot at all.
+func layoutProblems(e example.Entry, doc string) []string {
+ var problems []string
+ if !e.Document {
+ if strings.Contains(doc, "<!-- fiesta:slot -->") || strings.Contains(doc, "<!-- /fiesta:slot -->") {
+ problems = append(problems, "a non-layout document carries a slot marker")
+ }
+ return problems
+ }
+ if len(e.States) != 1 || e.States[0].ID != "default" {
+ var ids []string
+ for _, s := range e.States {
+ ids = append(ids, s.ID)
+ }
+ problems = append(problems, "a layout must have the one state default, has "+strings.Join(ids, ", "))
+ }
+ if e.Default != "default" {
+ problems = append(problems, "a layout's default must be default, is "+e.Default)
+ }
+ if _, err := example.Slot(doc, "<p>x</p>"); err != nil {
+ problems = append(problems, err.Error())
+ }
+ return problems
+}
+
+func TestEveryLayoutHasOneDefaultAndOneSlot(t *testing.T) {
+ fsys, m := manifest(t)
+ t.Run("control", func(t *testing.T) {
+ e, ok := m.Entry("layout-sidebar")
+ if !ok || !e.Document {
+ t.Fatal("no layout-sidebar layout to mutate")
+ }
+ doc, err := example.Document(fsys, e.Name, "default")
+ if err != nil {
+ t.Fatal(err)
+ }
+ two := e
+ two.States = append(append([]example.State(nil), e.States...), example.State{ID: "wide"})
+ if got := layoutProblems(two, doc); len(got) == 0 {
+ t.Error("an entry with two states passed")
+ }
+ if got := layoutProblems(e, strings.Replace(doc, "<!-- fiesta:slot -->", "<!-- fiesta:slot --><!-- fiesta:slot -->", 1)); len(got) == 0 {
+ t.Error("a document with two slots passed")
+ }
+ if got := layoutProblems(e, strings.ReplaceAll(doc, "<!-- fiesta:slot -->", "")); len(got) == 0 {
+ t.Error("a document with no slot passed")
+ }
+ plain := e
+ plain.Document = false
+ if got := layoutProblems(plain, doc); len(got) == 0 {
+ t.Error("a non-layout document with a slot passed")
+ }
+ if got := layoutProblems(e, doc); len(got) != 0 {
+ t.Errorf("the unmutated layout reports %v", got)
+ }
+ })
+ layouts := 0
+ everyDocument(t, func(e example.Entry, s example.State, doc string) {
+ if e.Document {
+ layouts++
+ }
+ for _, p := range layoutProblems(e, doc) {
+ t.Errorf("%s/%s: %s", e.Name, s.ID, p)
+ }
+ })
+ if layouts != 7 {
+ t.Errorf("%d layout documents, want 7", layouts)
+ }
+}
+
func TestTheStandaloneSchemeStateOwnsItsStorage(t *testing.T) {
fsys, m := manifest(t)
def, ok := m.Default("scheme-menu")