rastrillo / fiesta Public

Close the parked minors: a scenario that must run, an event that must bubble, bounded GETs, a derived size gate, and no hidden release file

Twelve small findings the task reviews parked, none changing css/, js/,
i18n/, icons/ or LICENSE, and no ported assertion.

check: TestAServerOwnedPageIgnoresTheStoredScheme asked only for zero
values, which a scenario missing from the Node report also decodes to;
every scenario now reports ran and the test requires it (red with the
scenario renamed in a scratch copy). The scheme pages count the
rst-scheme events a document listener hears, and the reload drive
wants exactly one after Dark, so the event must bubble (red with
bubbles: false in a scratch fiesta.js). The fixture smoke test and the
depth-fixture check GET through a client with a ten-second timeout and
fail on a read error rather than dropping it (red with a scratch
handler whose Content-Length outruns its body). The scheme menu's light
axe scan emulates a light preference and fails if the page prefers
dark (red with dark emulated). The menus file header names
fields_browser_test.go as the other half and drops a sentence about a
header paragraph that no longer exists; received says it answers 204
with no body and writes the status it is given; the fixture-count
comment says floor, not pin; floorServer.snapshot's comment names
snapshot (and the visited check was seen red, with the loop skipping
the first state: every leg reports the manifest's first state never
served). The prerender recorder is built once rather than per request.
The fifteen fixture-index rows no drive reads carry the reason they
are kept, in index.json and in the seed exporter that writes them, and
the exporter emits TestTheIndexRowsRoundEachRunOfLinks from a slice
rather than a map, so a reseed writes the same file.

internal/build: CheckSize derives the bundles it gates from the themes
the dist carries (each top-level theme file owes bundleName of it), so
a theme added to css/themes/ cannot ship unchecked, and a dist with no
theme fails outright; the error gives both figures in bytes as well as
KiB. TestTheSizeGateFailsAtTheLine was red against the old gate (a
theme with no bundle passed; the message carried no byte figure) and
now also holds a fourth theme and the figures.

internal/site: Release refuses every hidden file but the top-level
stamp whether or not SHA256SUMS lists it; the new subtest, a .secret
listed with the right hash, was red against the old Release (published:
true). cmd/fiesta: claim takes the flag a refusal should name, so
fiesta release says -releases where build, gallery and site say -out;
TestACandidateIsNotAnyChecksummedDirectory was red on the old wording.

internal/gallery: the axe drive reads the body's ground after each
scheme flip and fails if light and dark paint the same, so a dark scan
is a dark page (red with day's dark block renamed in a scratch copy).
Human

Prompt

Pre-release cleanup, part B: fix the parked minors, items 1 to 12, with a scratch red run for each test that gains an assertion.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

Twelve parked review minors closed across check, internal/build, internal/site, cmd/fiesta, internal/gallery and the seed exporter; each new assertion has a recorded red run; no release-feeding file changed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 764bfcce6d5ddd7fa81fbee230b39486bc60d416 parent 2adaddb
18 files changed, +223 −79
  • check/coverage_browser_test.go +3 −1
  • check/fixture_test.go +3 −2
  • check/menus_browser_test.go +8 −9
  • check/node/fiesta_node.mjs +1 −0
  • check/prerender_browser_test.go +2 −1
  • check/rig_browser_test.go +12 −4
  • check/scheme_browser_test.go +21 −5
  • check/scripts_test.go +6 −0
  • check/testdata/fixtures/index.json +15 −15
  • cmd/fiesta/claim.go +8 −8
  • cmd/fiesta/main.go +2 −2
  • cmd/fiesta/release_foreign_test.go +4 −0
  • hack/seed/_rastrillo/ui_fiestaseed_test.go +21 −13
  • internal/build/build.go +30 −10
  • internal/build/build_test.go +51 −7
  • internal/gallery/a11y_browser_test.go +10 −1
  • internal/site/site.go +6 −1
  • internal/site/site_test.go +20 −0

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.107s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.022s
=== suite ===
--- shard 0 of 10 ---
==> shard 0 of 10: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 go test -count=1 ./...
ok  	amadan.net/rastrillo/fiesta	0.809s
ok  	amadan.net/rastrillo/fiesta/check	5.191s
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	5.088s
ok  	amadan.net/rastrillo/fiesta/example	0.356s
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.143s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.866s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.011s
ok  	amadan.net/rastrillo/fiesta/internal/gallery	2.970s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.030s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.681s
ok  	amadan.net/rastrillo/fiesta/internal/site	0.123s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.020s
--- shard 1 of 10 ---
==> shard 1 of 10: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
--- shard 2 of 10 ---
==> shard 2 of 10: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
--- shard 3 of 10 ---
==> shard 3 of 10: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
10:33PM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
10:33PM INF 1 commits scanned.
10:33PM INF scanned ~4554990 bytes (4.55 MB) in 3.12s
10:33PM INF no leaks found
--- shard 4 of 10 ---
==> shard 4 of 10: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
ok  	amadan.net/rastrillo/fiesta/internal/harness	10.731s
--- shard 5 of 10 ---
==> shard 5 of 10: make browser.check~1of3
./hack/gotest.sh browser 'check~1of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 3 of ./check/ runs 92 of 276 tests
ok  	amadan.net/rastrillo/fiesta/check	199.307s
--- shard 6 of 10 ---
==> shard 6 of 10: make browser.check~2of3
./hack/gotest.sh browser 'check~2of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 3 of ./check/ runs 92 of 276 tests
ok  	amadan.net/rastrillo/fiesta/check	203.664s
--- shard 7 of 10 ---
==> shard 7 of 10: make browser.check~3of3
./hack/gotest.sh browser 'check~3of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 3 of 3 of ./check/ runs 92 of 276 tests
ok  	amadan.net/rastrillo/fiesta/check	202.900s
--- shard 8 of 10 ---
==> shard 8 of 10: make browser.internal.gallery~1of2
./hack/gotest.sh browser 'internal.gallery~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/gallery/ runs 1 of 13 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	79.460s
--- shard 9 of 10 ---
==> shard 9 of 10: make browser.internal.gallery~2of2
./hack/gotest.sh browser 'internal.gallery~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/gallery/ runs 12 of 13 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	23.548s
diff --git a/check/coverage_browser_test.go b/check/coverage_browser_test.go
index 695b9c6..d51904a 100644
--- a/check/coverage_browser_test.go
+++ b/check/coverage_browser_test.go
@@ -43,7 +43,9 @@ type floorServer struct {
served map[string]int
}
-// servedSince returns how many times each path was served, as a copy.
+// snapshot returns how many times each path has been served so far,
+// as a copy, so a leg can subtract the count before it from the count
+// after it.
func (s *floorServer) snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
diff --git a/check/fixture_test.go b/check/fixture_test.go
index 9cf6b63..8ca4937 100644
--- a/check/fixture_test.go
+++ b/check/fixture_test.go
@@ -104,8 +104,9 @@ func fixtureExample(t *testing.T, file, builder, kase string) string {
// TestEveryFixtureRowResolves: every row that names a path names a
// file that exists and is not empty, and the rows without a path are
-// the example references, which name an example. The count is pinned
-// so an index that lost its rows could not pass by listing nothing.
+// the example references, which name an example. The count is floored
+// at 100 so an index that lost its rows could not pass by listing
+// nothing.
func TestEveryFixtureRowResolves(t *testing.T) {
rows := fixtureIndex(t)
if len(rows) < 100 {
diff --git a/check/menus_browser_test.go b/check/menus_browser_test.go
index aca062c..0c251d6 100644
--- a/check/menus_browser_test.go
+++ b/check/menus_browser_test.go
@@ -3,9 +3,8 @@
// The menu idioms, the modal panel, the busy rule and the name cell in
// a browser: the A half of rastrillo's ui/browser_test.go at the seed commit,
// each drive reading its page through fixture so the page it drives
-// is the page the rastrillo drive drove. The select and date journeys that
-// made up the file's other half are ported beside the scripts they
-// drive.
+// is the page the rastrillo drive drove. The select and date journeys
+// that made up the file's other half are fields_browser_test.go.
package check
import (
@@ -29,9 +28,11 @@ import (
"amadan.net/rastrillo/fiesta/internal/harness"
)
-// received answers a POST on path with a page that says so, and hands
-// the form's one field (or, with field empty, the whole encoded form)
-// to a buffered channel, so a drive can check what the server got.
+// received answers a POST on path with status, and hands the form's
+// one field (or, with field empty, the whole encoded form) to a
+// buffered channel, so a drive can check what the server got. A 204
+// goes out with no body, as the busy drive's server answered; any
+// other status carries a page that says the post was received.
func received(mux *http.ServeMux, path, field string, got chan string, status int) {
mux.HandleFunc("POST "+path, func(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
@@ -48,6 +49,7 @@ func received(mux *http.ServeMux, path, field string, got chan string, status in
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(status)
fmt.Fprint(w, `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>ok</title></head><body><p id="done">received</p></body></html>`)
})
}
@@ -84,9 +86,6 @@ func groupPage(t *testing.T) (http.Handler, chan string) {
// heading and Enter commits the wrong option — or nothing;
// - the opt-out is read as a truthy attribute (it is present, after
// all) and the select enhances anyway.
-//
-// It runs on the same terms as the drives above; see the file header
-// for what issue #86 turned out to be.
func TestGroupedSelectRendersItsGroups(t *testing.T) {
mux, submitted := groupPage(t)
rig := harness.New(t, func(string) http.Handler { return mux })
diff --git a/check/node/fiesta_node.mjs b/check/node/fiesta_node.mjs
index fa19598..a076ea4 100644
--- a/check/node/fiesta_node.mjs
+++ b/check/node/fiesta_node.mjs
@@ -116,6 +116,7 @@ function page({ store = false, stored = null, broken = false, twice = false } =
open: menu.open,
prevented: false,
error: "",
+ ran: true,
...extra,
};
},
diff --git a/check/prerender_browser_test.go b/check/prerender_browser_test.go
index 4e1d602..b9ed743 100644
--- a/check/prerender_browser_test.go
+++ b/check/prerender_browser_test.go
@@ -57,13 +57,14 @@ func prerenderSite(t *testing.T, hits chan<- string) func(string) http.Handler {
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Write([]byte(speculationRules))
})
+ recorded := recordDocuments(mux, hits)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// On every response rather than sniffed for HTML: browsers
// act on it for documents and ignore it elsewhere. A
// structured-field list of one string, which is the
// header's form.
w.Header().Set("Speculation-Rules", `"`+speculationRulesPath+`"`)
- recordDocuments(mux, hits).ServeHTTP(w, r)
+ recorded.ServeHTTP(w, r)
})
}
}
diff --git a/check/rig_browser_test.go b/check/rig_browser_test.go
index df780d5..8cb327a 100644
--- a/check/rig_browser_test.go
+++ b/check/rig_browser_test.go
@@ -594,13 +594,17 @@ func TestTheDepthFixtureServesItsStylesheets(t *testing.T) {
ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
defer cancel()
+ client := &http.Client{Timeout: 10 * time.Second}
for _, path := range []string{"/tokens.css", "/theme.css"} {
- res, err := http.Get(rig.Origin + path)
+ res, err := client.Get(rig.Origin + path)
if err != nil {
t.Fatalf("GET %s: %v", path, err)
}
- body, _ := io.ReadAll(res.Body)
+ body, err := io.ReadAll(res.Body)
res.Body.Close()
+ if err != nil {
+ t.Fatalf("reading GET %s: %v", path, err)
+ }
if res.StatusCode != http.StatusOK {
t.Errorf("GET %s answered %d, want 200", path, res.StatusCode)
}
@@ -635,13 +639,17 @@ func TestTheFixturePageLoadsEveryAsset(t *testing.T) {
if len(assets) != 8 {
t.Fatalf("the page links %d assets, want 8 (two stylesheets, six scripts): %q", len(assets), assets)
}
+ client := &http.Client{Timeout: 10 * time.Second}
for _, url := range assets {
- res, err := http.Get(url)
+ res, err := client.Get(url)
if err != nil {
t.Fatalf("GET %s: %v", url, err)
}
- body, _ := io.ReadAll(res.Body)
+ body, err := io.ReadAll(res.Body)
res.Body.Close()
+ if err != nil {
+ t.Fatalf("reading GET %s: %v", url, err)
+ }
if res.StatusCode != http.StatusOK {
t.Errorf("GET %s answered %d, want 200", url, res.StatusCode)
}
diff --git a/check/scheme_browser_test.go b/check/scheme_browser_test.go
index f8274d2..6a2faba 100644
--- a/check/scheme_browser_test.go
+++ b/check/scheme_browser_test.go
@@ -40,7 +40,9 @@ const schemeMenu = `<details rst-dropdown rst-scheme name="rst-menus">
// schemeDoc is a page with the stylesheet and a theme, the menu, and
// the scripts head asks for. store puts data-rst-scheme-store="local"
-// on <html>, where fiesta.js reads it before anything else runs.
+// on <html>, where fiesta.js reads it before anything else runs. The
+// body's own script also counts the rst-scheme events a listener on
+// the document hears, which is how an app's cookie writer listens.
func schemeDoc(store bool, head string) string {
attr := ""
if store {
@@ -50,7 +52,8 @@ func schemeDoc(store bool, head string) string {
`<meta name="viewport" content="width=device-width"><title>scheme</title>` +
`<link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/theme.css">` + head +
`</head><body><div rst-page>` + schemeMenu + `</div>` +
- `<script>window.__early = document.documentElement.getAttribute("data-theme") || "";</script></body></html>`
+ `<script>window.__early = document.documentElement.getAttribute("data-theme") || "";` +
+ `document.addEventListener("rst-scheme", () => { window.__heard = (window.__heard || 0) + 1; });</script></body></html>`
}
// schemePages is what driveSchemeRemember and the drives beside it
@@ -76,6 +79,7 @@ type schemeState struct {
Open bool // the menu is still open
Mark bool // window.__mark survived: no navigation happened
Early string // data-theme as the body's inline script saw it
+ Heard int // rst-scheme events a document listener heard
}
const schemeStateJS = `(() => {
@@ -90,7 +94,8 @@ const schemeStateJS = `(() => {
Current: cur ? cur.form.elements.scheme.value : "",
Open: m.open,
Mark: window.__mark === 1,
- Early: window.__early
+ Early: window.__early,
+ Heard: window.__heard || 0
});
})()`
@@ -134,6 +139,9 @@ func driveSchemeRemember(t *testing.T, rig *harness.Rig) {
if got.Theme != "dark" || got.Current != "dark" || got.Stored != "dark" || got.Cookie != "" || got.Open || !got.Mark {
t.Fatalf("after choosing Dark in place: %+v", got)
}
+ if got.Heard != 1 {
+ t.Fatalf("a listener on the document heard %d rst-scheme events after Dark, want 1; the event is dispatched on the form and must bubble", got.Heard)
+ }
mustRun(t, ctx, chromedp.Reload(), chromedp.WaitVisible(`[rst-scheme]`, chromedp.ByQuery))
at(t, ctx, schemeStateJS, &got)
@@ -274,12 +282,20 @@ func axeClean(t *testing.T, ctx context.Context, where string) {
// choice sets data-theme="dark" on the root and navigates nowhere (the
// mark chooseScheme leaves on the window survives it). The root is
// read first: without data-rst-scheme-store="local" this would be a
-// server-owned page, and the drive could not tell the two apart.
+// server-owned page, and the drive could not tell the two apart. The
+// first scan is light because a light preference is emulated and
+// checked, not because the browser happens to default to one.
func TestTheSchemeMenuPassesAxeInBothSchemes(t *testing.T) {
rig := sizingRig(t, false, schemeMenuPages(t))
ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second)
defer cancel()
- mustRun(t, ctx, chromedp.EmulateViewport(1280, 900), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible(`[rst-scheme]`, chromedp.ByQuery))
+ light := emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{{Name: "prefers-color-scheme", Value: "light"}})
+ mustRun(t, ctx, light, chromedp.EmulateViewport(1280, 900), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible(`[rst-scheme]`, chromedp.ByQuery))
+ var dark bool
+ mustRun(t, ctx, chromedp.Evaluate(`matchMedia("(prefers-color-scheme: dark)").matches`, &dark))
+ if dark {
+ t.Fatal("the page prefers dark, so the light scan would scan dark")
+ }
var store string
mustRun(t, ctx, chromedp.Evaluate(`document.documentElement.getAttribute("data-rst-scheme-store") || ""`, &store))
diff --git a/check/scripts_test.go b/check/scripts_test.go
index 8d7de63..28649d2 100644
--- a/check/scripts_test.go
+++ b/check/scripts_test.go
@@ -196,6 +196,7 @@ type schemeRun struct {
Current string // the scheme whose button is aria-current
Open bool // the menu is still open
Error string // what the submit threw, "" for nothing
+ Ran bool // the scenario wrote this report at all
}
// fiestaReport is fiesta_node.mjs's whole report, a scenario per field.
@@ -309,6 +310,11 @@ func TestTheStoredSchemeIsAppliedOnLoad(t *testing.T) {
// System.
func TestAServerOwnedPageIgnoresTheStoredScheme(t *testing.T) {
got := fiestaNode(t).ServerIgnoresStored
+ // Every field this test wants is a zero value, which is also what
+ // a scenario that went missing from the report decodes to.
+ if !got.Ran {
+ t.Fatal("the serverIgnoresStored scenario is not in fiesta_node.mjs's report")
+ }
if got.Theme != "" {
t.Errorf("a server-owned page took data-theme %q from storage; want none", got.Theme)
}
diff --git a/check/testdata/fixtures/index.json b/check/testdata/fixtures/index.json
index 693d15b..4f5494a 100644
--- a/check/testdata/fixtures/index.json
+++ b/check/testdata/fixtures/index.json
@@ -411,7 +411,7 @@
"case": "column",
"path": "index_browser_test/shellLayoutPage-column.html",
"kind": "document",
- "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content"
+ "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content; read by no drive: the helper's own defaults, kept for comparison with the layout examples"
},
{
"file": "index_browser_test.go",
@@ -419,7 +419,7 @@
"case": "topbar",
"path": "index_browser_test/shellLayoutPage-topbar.html",
"kind": "document",
- "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content"
+ "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content; read by no drive: the helper's own defaults, kept for comparison with the layout examples"
},
{
"file": "index_browser_test.go",
@@ -427,7 +427,7 @@
"case": "sidebar",
"path": "index_browser_test/shellLayoutPage-sidebar.html",
"kind": "document",
- "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content"
+ "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content; read by no drive: the helper's own defaults, kept for comparison with the layout examples"
},
{
"file": "index_browser_test.go",
@@ -435,7 +435,7 @@
"case": "console",
"path": "index_browser_test/shellLayoutPage-console.html",
"kind": "document",
- "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content"
+ "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content; read by no drive: the helper's own defaults, kept for comparison with the layout examples"
},
{
"file": "index_browser_test.go",
@@ -443,7 +443,7 @@
"case": "stage",
"path": "index_browser_test/shellLayoutPage-stage.html",
"kind": "document",
- "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content"
+ "note": "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content; read by no drive: the helper's own defaults, kept for comparison with the layout examples"
},
{
"file": "index_browser_test.go",
@@ -779,7 +779,7 @@
"case": "sidebar",
"path": "index_browser_test/TestAFragmentLinkLandsBelowTheBackStrip-sidebar.html",
"kind": "document",
- "note": "/page"
+ "note": "/page; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "index_browser_test.go",
@@ -787,7 +787,7 @@
"case": "console",
"path": "index_browser_test/TestAFragmentLinkLandsBelowTheBackStrip-console.html",
"kind": "document",
- "note": "/page"
+ "note": "/page; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "index_browser_test.go",
@@ -795,7 +795,7 @@
"case": "sidebar-index",
"path": "index_browser_test/TestThePhoneHeaderStripIs56Pixels-sidebar-index.html",
"kind": "document",
- "note": "/"
+ "note": "/; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "index_browser_test.go",
@@ -803,7 +803,7 @@
"case": "sidebar-page",
"path": "index_browser_test/TestThePhoneHeaderStripIs56Pixels-sidebar-page.html",
"kind": "document",
- "note": "/page"
+ "note": "/page; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "index_browser_test.go",
@@ -811,7 +811,7 @@
"case": "console-index",
"path": "index_browser_test/TestThePhoneHeaderStripIs56Pixels-console-index.html",
"kind": "document",
- "note": "/"
+ "note": "/; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "index_browser_test.go",
@@ -819,7 +819,7 @@
"case": "console-page",
"path": "index_browser_test/TestThePhoneHeaderStripIs56Pixels-console-page.html",
"kind": "document",
- "note": "/page"
+ "note": "/page; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "console_shell_browser_test.go",
@@ -867,7 +867,7 @@
"case": "console-ltr",
"path": "console_shell_browser_test/consolePageFrom-console-ltr.html",
"kind": "document",
- "note": "consolePageFrom with the console layout and no extra defines; consolePage and legacyConsolePage are its callers; the control twin is this page with \u003cstyle\u003econsoleControlCSS\u003c/style\u003e injected before \u003c/head\u003e"
+ "note": "consolePageFrom with the console layout and no extra defines; consolePage and legacyConsolePage are its callers; the control twin is this page with \u003cstyle\u003econsoleControlCSS\u003c/style\u003e injected before \u003c/head\u003e; read by no drive: the helper's own defaults, kept for comparison with the layout examples"
},
{
"file": "profile_browser_test.go",
@@ -1003,7 +1003,7 @@
"case": "modal",
"path": "card_browser_test/topbarPageWith-modal.html",
"kind": "document",
- "note": "topbarPageWith(t, \"ltr\", Styleguide()[\"modal\"]); TestAModalCoversTheTopbarOnAPhone's topbar leg"
+ "note": "topbarPageWith(t, \"ltr\", Styleguide()[\"modal\"]); TestAModalCoversTheTopbarOnAPhone's topbar leg; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "card_browser_test.go",
@@ -1011,7 +1011,7 @@
"case": "sidebar",
"path": "card_browser_test/TestAModalCoversTheTopbarOnAPhone-sidebar.html",
"kind": "document",
- "note": "the sidebar content page with the modal as its content; the topbar leg is topbarPageWith-modal"
+ "note": "the sidebar content page with the modal as its content; the topbar leg is topbarPageWith-modal; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "card_browser_test.go",
@@ -1019,7 +1019,7 @@
"case": "console",
"path": "card_browser_test/TestAModalCoversTheTopbarOnAPhone-console.html",
"kind": "document",
- "note": "the console content page with the modal as its content; the topbar leg is topbarPageWith-modal"
+ "note": "the console content page with the modal as its content; the topbar leg is topbarPageWith-modal; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
},
{
"file": "card_browser_test.go",
diff --git a/cmd/fiesta/claim.go b/cmd/fiesta/claim.go
index c05bd0c..d5821dc 100644
--- a/cmd/fiesta/claim.go
+++ b/cmd/fiesta/claim.go
@@ -84,7 +84,7 @@ func stampBody(command string) string {
// leaves a directory the command still owns and will clean out on the
// next run. It is not atomic: a failed run leaves a partial tree
// behind, and the only thing saying so is the non-zero exit status.
-func claim(root, command string) error {
+func claim(root, command, flag string) error {
if real, err := filepath.EvalSymlinks(root); err == nil {
root = real
} else if !os.IsNotExist(err) {
@@ -103,17 +103,17 @@ func claim(root, command string) error {
body, err := os.ReadFile(stamp)
if err != nil {
return fmt.Errorf("refusing to empty %s: it is not empty and holds no %s stamp, "+
- "so %s has not written it before. Empty it yourself, or point -out somewhere else", root, stampName, command)
+ "so %s has not written it before. Empty it yourself, or point %s somewhere else", root, stampName, command, flag)
}
if by := writerOf(body); by != command {
if by == "" {
return fmt.Errorf("refusing to empty %s: its %s stamp names no command, so it may be something else's. "+
- "Empty it yourself, or point -out somewhere else", root, stamp)
+ "Empty it yourself, or point %s somewhere else", root, stamp, flag)
}
return fmt.Errorf("refusing to empty %s: its %s stamp says it is written by %s, and %s would empty that command's output. "+
- "Point -out somewhere else", root, stamp, by, command)
+ "Point %s somewhere else", root, stamp, by, command, flag)
}
- if err := foreignBelow(root, command); err != nil {
+ if err := foreignBelow(root, command, flag); err != nil {
return err
}
for _, e := range entries {
@@ -132,18 +132,18 @@ func claim(root, command string) error {
// is not the command's own output: a stamp other than root's, or a
// symbolic link. It only reads; nothing is removed until it returns
// nil. An unreadable directory is an error, and so a refusal.
-func foreignBelow(root, command string) error {
+func foreignBelow(root, command, flag string) error {
return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("reading %s: %w", root, err)
}
if d.Type()&fs.ModeSymlink != 0 {
return fmt.Errorf("refusing to empty %s: %s is a symbolic link, which %s never writes, so it is not this command's output. "+
- "Remove it yourself, or point -out somewhere else", root, p, command)
+ "Remove it yourself, or point %s somewhere else", root, p, command, flag)
}
if !d.IsDir() && d.Name() == stampName && filepath.Dir(p) != root {
return fmt.Errorf("refusing to empty %s: %s is a stamp %s did not write, so another command's output is inside. "+
- "Empty it yourself, or point -out somewhere else", root, p, command)
+ "Empty it yourself, or point %s somewhere else", root, p, command, flag)
}
return nil
})
diff --git a/cmd/fiesta/main.go b/cmd/fiesta/main.go
index 08019ac..053a511 100644
--- a/cmd/fiesta/main.go
+++ b/cmd/fiesta/main.go
@@ -283,7 +283,7 @@ func release(releases, version string, candidate bool) error {
"A fix is the next version", root, tag)
}
}
- if err := claim(root, "fiesta release"); err != nil {
+ if err := claim(root, "fiesta release", "-releases"); err != nil {
return err
}
return writeFiles(root, dist)
@@ -337,7 +337,7 @@ func writeTree(root, command string, files map[string][]byte) error {
if err != nil {
return fmt.Errorf("resolving -out: %w", err)
}
- if err := claim(root, command); err != nil {
+ if err := claim(root, command, "-out"); err != nil {
return err
}
return writeFiles(root, files)
diff --git a/cmd/fiesta/release_foreign_test.go b/cmd/fiesta/release_foreign_test.go
index dc73252..02fb9ef 100644
--- a/cmd/fiesta/release_foreign_test.go
+++ b/cmd/fiesta/release_foreign_test.go
@@ -52,6 +52,10 @@ func TestACandidateIsNotAnyChecksummedDirectory(t *testing.T) {
if !strings.Contains(err.Error(), stampName) {
t.Errorf("the refusal does not name the missing %s stamp: %v", stampName, err)
}
+ // The way out it names is release's own flag: release has no -out.
+ if !strings.Contains(err.Error(), "-releases") || strings.Contains(err.Error(), "-out") {
+ t.Errorf("the refusal does not point at -releases, the flag release takes: %v", err)
+ }
for name, want := range files {
got, err := os.ReadFile(filepath.Join(dir, name))
if err != nil || string(got) != string(want) {
diff --git a/hack/seed/_rastrillo/ui_fiestaseed_test.go b/hack/seed/_rastrillo/ui_fiestaseed_test.go
index 22fdfdc..380f895 100644
--- a/hack/seed/_rastrillo/ui_fiestaseed_test.go
+++ b/hack/seed/_rastrillo/ui_fiestaseed_test.go
@@ -270,9 +270,15 @@ func TestFiestaSeed(t *testing.T) {
), "")
// index_browser_test.go: shellLayoutPage with the defines each
- // drive gives it.
+ // drive gives it. The rows no ported drive reads say so in their
+ // note: the drives that ran on them now run on the layout examples,
+ // and the pages stay as what rastrillo rendered, for comparison.
+ const (
+ unreadExample = "; read by no drive: the ported test runs on the layout example, and this is the page rastrillo's drive rendered, kept for comparison"
+ unreadDefault = "; read by no drive: the helper's own defaults, kept for comparison with the layout examples"
+ )
for _, shell := range LayoutNames() {
- w.page("index_browser_test.go", "shellLayoutPage", shell, shellLayoutPage(t, layout(shell), "ltr"), "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content")
+ w.page("index_browser_test.go", "shellLayoutPage", shell, shellLayoutPage(t, layout(shell), "ltr"), "the helper's own defaults: brand Harbour, nav-invoices/orders/team, #rail-locale, the Invoices content"+unreadDefault)
}
for _, shell := range []string{"sidebar", "console"} {
for _, dir := range []string{"ltr", "rtl"} {
@@ -289,12 +295,14 @@ func TestFiestaSeed(t *testing.T) {
}
w.add(fixtureEntry{File: "index_browser_test.go", Builder: "TestAnOldSidebarLayoutKeepsItsDrawer", Case: "", Path: "../legacy/sidebar.html", Kind: "document",
Note: "shellLayoutPage(t, legacyLayout(t, \"sidebar\"), \"ltr\") with no extra defines: the seeded legacy sidebar"})
- for kase, nav := range map[string]string{
- "group-first": `<p rst-shell-group>Sales</p><a href="/a">A</a><a href="/b">B</a><p rst-shell-group>Settings</p><a href="/c">C</a>`,
- "one": `<a href="/a">A</a>`,
- "no-groups": `<a href="/a">A</a><a href="/b">B</a><a href="/c" aria-current="page">C</a>`,
+ // A slice, not a map: the rows land in index.json in this order,
+ // and a reseed must write the same file.
+ for _, c := range []struct{ kase, nav string }{
+ {"group-first", `<p rst-shell-group>Sales</p><a href="/a">A</a><a href="/b">B</a><p rst-shell-group>Settings</p><a href="/c">C</a>`},
+ {"one", `<a href="/a">A</a>`},
+ {"no-groups", `<a href="/a">A</a><a href="/b">B</a><a href="/c" aria-current="page">C</a>`},
} {
- w.page("index_browser_test.go", "TestTheIndexRowsRoundEachRunOfLinks", kase, shellLayoutPage(t, layout("sidebar"), "ltr", viewIndex, `{{define "nav"}}`+nav+`{{end}}`), "/"+kase)
+ w.page("index_browser_test.go", "TestTheIndexRowsRoundEachRunOfLinks", c.kase, shellLayoutPage(t, layout("sidebar"), "ltr", viewIndex, `{{define "nav"}}`+c.nav+`{{end}}`), "/"+c.kase)
}
{
short := `<a id="nav-invoices" href="/invoices">Invoices</a><a id="nav-orders" href="/orders">Orders</a>`
@@ -316,13 +324,13 @@ func TestFiestaSeed(t *testing.T) {
{
content := `{{define "content"}}<h1>Invoices</h1><div style="block-size: 150vh"></div><h2 id="target">Target</h2><div style="block-size: 150vh"></div>{{end}}`
for _, shell := range []string{"sidebar", "console"} {
- w.page("index_browser_test.go", "TestAFragmentLinkLandsBelowTheBackStrip", shell, shellLayoutPage(t, layout(shell), "ltr", content), "/page")
+ w.page("index_browser_test.go", "TestAFragmentLinkLandsBelowTheBackStrip", shell, shellLayoutPage(t, layout(shell), "ltr", content), "/page"+unreadExample)
}
}
for _, shell := range []string{"sidebar", "console"} {
src := layout(shell)
- w.page("index_browser_test.go", "TestThePhoneHeaderStripIs56Pixels", shell+"-index", shellLayoutPage(t, src, "ltr", append(profileDefs("Harbour"), viewIndex)...), "/")
- w.page("index_browser_test.go", "TestThePhoneHeaderStripIs56Pixels", shell+"-page", shellLayoutPage(t, src, "ltr", profileDefs("Harbour")...), "/page")
+ w.page("index_browser_test.go", "TestThePhoneHeaderStripIs56Pixels", shell+"-index", shellLayoutPage(t, src, "ltr", append(profileDefs("Harbour"), viewIndex)...), "/"+unreadExample)
+ w.page("index_browser_test.go", "TestThePhoneHeaderStripIs56Pixels", shell+"-page", shellLayoutPage(t, src, "ltr", profileDefs("Harbour")...), "/page"+unreadExample)
}
// console_shell_browser_test.go. consolePage returns the page and a
@@ -341,7 +349,7 @@ func TestFiestaSeed(t *testing.T) {
page, _ = legacyConsolePage(t, postsNav3, "ltr")
w.page("console_shell_browser_test.go", "legacyConsolePage", "ltr", page, "legacyConsolePage(t, <Posts, Drafts, Settings>, \"ltr\"): the legacy console with consolePageFrom's own blocks"+controlNote)
page, _ = consolePageFrom(t, layout("console"), postsNav3, "ltr")
- w.page("console_shell_browser_test.go", "consolePageFrom", "console-ltr", page, "consolePageFrom with the console layout and no extra defines; consolePage and legacyConsolePage are its callers"+controlNote)
+ w.page("console_shell_browser_test.go", "consolePageFrom", "console-ltr", page, "consolePageFrom with the console layout and no extra defines; consolePage and legacyConsolePage are its callers"+controlNote+unreadDefault)
}
// profile_browser_test.go.
@@ -370,10 +378,10 @@ func TestFiestaSeed(t *testing.T) {
// card_browser_test.go.
w.page("card_browser_test.go", "topbarPage", "ltr", topbarPage(t, "ltr"), "topbarPage(t, \"ltr\"): topbarPageWith and the drives' standard content")
w.page("card_browser_test.go", "topbarPage", "rtl", topbarPage(t, "rtl"), "topbarPage(t, \"rtl\")")
- w.page("card_browser_test.go", "topbarPageWith", "modal", topbarPageWith(t, "ltr", Styleguide()["modal"]), "topbarPageWith(t, \"ltr\", Styleguide()[\"modal\"]); TestAModalCoversTheTopbarOnAPhone's topbar leg")
+ w.page("card_browser_test.go", "topbarPageWith", "modal", topbarPageWith(t, "ltr", Styleguide()["modal"]), "topbarPageWith(t, \"ltr\", Styleguide()[\"modal\"]); TestAModalCoversTheTopbarOnAPhone's topbar leg"+unreadExample)
for _, shell := range []string{"sidebar", "console"} {
w.page("card_browser_test.go", "TestAModalCoversTheTopbarOnAPhone", shell, shellLayoutPage(t, layout(shell), "ltr", upInvoices,
- `{{define "content"}}`+Styleguide()["modal"]+`{{end}}`), "the "+shell+" content page with the modal as its content; the topbar leg is topbarPageWith-modal")
+ `{{define "content"}}`+Styleguide()["modal"]+`{{end}}`), "the "+shell+" content page with the modal as its content; the topbar leg is topbarPageWith-modal"+unreadExample)
}
for _, dir := range []string{"ltr", "rtl"} {
w.page("card_browser_test.go", "TestTheConsoleBarIsTheSameCard", dir, shellLayoutPage(t, layout("console"), dir, `{{define "account"}}<a id="acct-profile" href="/go/profile">Profile</a>{{end}}`,
diff --git a/internal/build/build.go b/internal/build/build.go
index e034442..708d11e 100644
--- a/internal/build/build.go
+++ b/internal/build/build.go
@@ -25,7 +25,7 @@ func Banner(version string) string {
// OneLinkLimit is the most a one-file theme may weigh gzipped: 25 KiB.
// It is what a static page that links fiesta.css alone pays, so it is
-// the figure the size gate holds the three bundles under.
+// the figure the size gate holds every bundle under.
const OneLinkLimit = 25 * 1024
// Gzipped is the length of b compressed at gzip level 6, the level a
@@ -49,9 +49,23 @@ func bundleName(theme string) string {
return "fiesta-" + theme + ".css"
}
-// oneLink is the three one-file themes the size gate holds to the
-// limit: the bundles a static page links alone.
-var oneLink = []string{bundleName(defaultTheme), bundleName("plain"), bundleName("signal")}
+// bundlesIn is the one-file theme the dist owes for every theme it
+// carries: each top-level .css that is neither tokens.css, shell.css
+// nor a bundle is a theme's own file, and its bundle is bundleName of
+// it. Derived from the dist rather than listed, so a theme added to
+// css/themes/ cannot ship without its bundle passing the gate.
+func bundlesIn(dist map[string][]byte) []string {
+ var names []string
+ for name := range dist {
+ if strings.Contains(name, "/") || !strings.HasSuffix(name, ".css") ||
+ name == "tokens.css" || name == "shell.css" || name == bundleName(defaultTheme) || strings.HasPrefix(name, "fiesta-") {
+ continue
+ }
+ names = append(names, bundleName(strings.TrimSuffix(name, ".css")))
+ }
+ sort.Strings(names)
+ return names
+}
// Dist builds the release from src, which needs LICENSE, css/, js/,
// icons/ and i18n/ and is read for nothing else, keyed by path
@@ -203,18 +217,24 @@ func iconChildren(svg []byte) ([]byte, error) {
return bytes.TrimSpace(s[open+1 : len(s)-len("</svg>")]), nil
}
-// CheckSize is the size gate: every one-file theme in dist gzips to
-// under OneLinkLimit, or the error names the first that does not and
-// gives both figures. A bundle the dist does not hold fails it too;
-// a gate that passed on absence would be no gate.
+// CheckSize is the size gate: every theme the dist carries has its
+// one-file bundle, and each gzips to under OneLinkLimit, or the error
+// names the first that does not and gives both figures, in bytes and
+// in KiB. A bundle the dist does not hold fails it too, and so does a
+// dist with no theme at all; a gate that passed on absence would be
+// no gate.
func CheckSize(dist map[string][]byte) error {
- for _, name := range oneLink {
+ bundles := bundlesIn(dist)
+ if len(bundles) == 0 {
+ return fmt.Errorf("the dist carries no theme, so there is no one-file theme to hold to the limit")
+ }
+ for _, name := range bundles {
b, ok := dist[name]
if !ok {
return fmt.Errorf("%s is not in the dist", name)
}
if g := Gzipped(b); g >= OneLinkLimit {
- return fmt.Errorf("%s is %.1f KiB gzipped; the one-link limit is %.1f KiB", name, float64(g)/1024, float64(OneLinkLimit)/1024)
+ return fmt.Errorf("%s is %d bytes gzipped (%.1f KiB); the one-link limit is %d (%.1f KiB)", name, g, float64(g)/1024, OneLinkLimit, float64(OneLinkLimit)/1024)
}
}
return nil
diff --git a/internal/build/build_test.go b/internal/build/build_test.go
index 4f6efe3..16be96c 100644
--- a/internal/build/build_test.go
+++ b/internal/build/build_test.go
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"encoding/xml"
+ "fmt"
"io/fs"
"math/rand"
"sort"
@@ -158,9 +159,24 @@ func incompressible(t *testing.T, target int) []byte {
return nil
}
+// gatedDist is a dist of the three themes' own files, each bundle
+// holding the bytes given: the shape CheckSize reads its bundle list
+// from.
+func gatedDist(bundle []byte) map[string][]byte {
+ d := map[string][]byte{}
+ for name, theme := range bundles {
+ d[theme+".css"] = []byte(":root{}")
+ d[name] = bundle
+ }
+ return d
+}
+
// TestTheSizeGateFailsAtTheLine holds CheckSize to its line, one
// bundle at a time: a dist whose three bundles gzip to 25,599 bytes
-// passes, and one with any bundle at 25,600 fails, naming the file.
+// passes, and one with any bundle at 25,600 fails, naming the file
+// and giving both figures. The bundles the gate holds are the themes
+// the dist carries, so a theme file with no bundle fails it by the
+// bundle's name, and a dist with no theme fails it outright.
func TestTheSizeGateFailsAtTheLine(t *testing.T) {
under := incompressible(t, OneLinkLimit-1)
over := incompressible(t, OneLinkLimit)
@@ -171,10 +187,7 @@ func TestTheSizeGateFailsAtTheLine(t *testing.T) {
t.Fatalf("the over bundle gzips to %d, want %d", g, OneLinkLimit)
}
for _, name := range bundleNames() {
- d := map[string][]byte{}
- for _, n := range bundleNames() {
- d[n] = under
- }
+ d := gatedDist(under)
if err := CheckSize(d); err != nil {
t.Errorf("%s at %d gzipped bytes failed the gate: %v", name, OneLinkLimit-1, err)
}
@@ -187,10 +200,41 @@ func TestTheSizeGateFailsAtTheLine(t *testing.T) {
if !strings.Contains(err.Error(), name) {
t.Errorf("the gate's error does not name %s: %v", name, err)
}
- if !strings.Contains(err.Error(), "25.0 KiB") {
- t.Errorf("the gate's error does not give its figures in KiB to one decimal: %v", err)
+ }
+
+ // The figures: the measured size and the limit, each in bytes and
+ // in KiB, with a bundle far enough over the line that the two
+ // differ in both units.
+ wide := incompressible(t, OneLinkLimit+500)
+ d := gatedDist(under)
+ d["fiesta.css"] = wide
+ err := CheckSize(d)
+ if err == nil {
+ t.Fatalf("fiesta.css at %d gzipped bytes passed the gate", OneLinkLimit+500)
+ }
+ for _, want := range []string{fmt.Sprintf("%d bytes gzipped (25.5 KiB)", OneLinkLimit+500), fmt.Sprintf("limit is %d (25.0 KiB)", OneLinkLimit)} {
+ if !strings.Contains(err.Error(), want) {
+ t.Errorf("the gate's error does not say %q: %v", want, err)
}
}
+
+ // A fourth theme is gated by its bundle, present or not.
+ d = gatedDist(under)
+ d["extra.css"] = []byte(":root{}")
+ if err := CheckSize(d); err == nil || !strings.Contains(err.Error(), "fiesta-extra.css") {
+ t.Errorf("a theme with no bundle passed the gate, or the error does not name fiesta-extra.css: %v", err)
+ }
+ d["fiesta-extra.css"] = over
+ if err := CheckSize(d); err == nil || !strings.Contains(err.Error(), "fiesta-extra.css") {
+ t.Errorf("a fourth theme's bundle at the line passed the gate, or the error does not name it: %v", err)
+ }
+ d["fiesta-extra.css"] = under
+ if err := CheckSize(d); err != nil {
+ t.Errorf("a fourth theme's bundle under the line failed the gate: %v", err)
+ }
+ if err := CheckSize(map[string][]byte{"fiesta.css": under}); err == nil {
+ t.Error("a dist with no theme file passed the gate")
+ }
}
// TestTheBundlesAreTokensThenThemeStripped: each one-file theme is the
diff --git a/internal/gallery/a11y_browser_test.go b/internal/gallery/a11y_browser_test.go
index ee1aebf..73b2444 100644
--- a/internal/gallery/a11y_browser_test.go
+++ b/internal/gallery/a11y_browser_test.go
@@ -244,8 +244,14 @@ func TestEveryExamplePassesAxe(t *testing.T) {
chromedp.Navigate(rig.Origin+href(frameFile(leg.theme, leg.rtl, e.Name, s.ID))),
chromedp.WaitReady("body"),
)
+ // The body's ground is read back after each flip, and the
+ // two must differ: a dark scan of a page still painted
+ // light would be the light scan twice.
+ ground := map[string]string{}
for _, scheme := range []string{"light", "dark"} {
- mustRun(t, ctx, chromedp.Evaluate(fmt.Sprintf(`document.documentElement.setAttribute("data-theme", %q); getComputedStyle(document.body).backgroundColor`, scheme), nil))
+ var bg string
+ mustRun(t, ctx, chromedp.Evaluate(fmt.Sprintf(`document.documentElement.setAttribute("data-theme", %q); getComputedStyle(document.body).backgroundColor`, scheme), &bg))
+ ground[scheme] = bg
res, err := axe.Scan(ctx, "", axe.DefaultTags())
if err != nil {
t.Fatalf("%s %s: %v", where, scheme, err)
@@ -256,6 +262,9 @@ func TestEveryExamplePassesAxe(t *testing.T) {
}
failed += report(t, where+" "+scheme, res.Violations)
}
+ if ground["light"] == ground["dark"] {
+ t.Fatalf("%s: the body paints %s under both schemes, so the flip to dark did not take", where, ground["light"])
+ }
}
}
var ids []string
diff --git a/internal/site/site.go b/internal/site/site.go
index c579f84..d5c7f5b 100644
--- a/internal/site/site.go
+++ b/internal/site/site.go
@@ -210,7 +210,9 @@ func Build(releases fs.FS, pages fs.FS) (map[string][]byte, error) {
// The one file not held to the sums is StampName at the top of the
// release, fiesta release's own ownership mark, which is skipped and
// never returned. Any other hidden file, and a stamp anywhere deeper,
-// is unlisted and refused like any stray.
+// is refused by name whether or not SHA256SUMS lists it: fiesta
+// release writes none, so a listed one was put there by hand, and a
+// hidden file is never published.
func Release(dir fs.FS) (map[string][]byte, error) {
if fi, err := fs.Lstat(dir, sumsName); err != nil {
return nil, fmt.Errorf("%s: %w", sumsName, err)
@@ -240,6 +242,9 @@ func Release(dir fs.FS) (map[string][]byte, error) {
if p == StampName {
return nil
}
+ if hidden(p) {
+ return fmt.Errorf("%s is hidden, and a hidden file is never published, so it is not a release file", p)
+ }
if !d.Type().IsRegular() {
return fmt.Errorf("%s is not a regular file, so it is not a release file", p)
}
diff --git a/internal/site/site_test.go b/internal/site/site_test.go
index 9f54493..14352c8 100644
--- a/internal/site/site_test.go
+++ b/internal/site/site_test.go
@@ -315,6 +315,26 @@ func TestTheStampIsSkippedAndNeverPublished(t *testing.T) {
}
})
}
+ // A hidden file that a hand-edited SHA256SUMS lists, with the right
+ // hash, is still refused: fiesta release never lists one, and
+ // hidden means never published.
+ t.Run("a listed hidden file", func(t *testing.T) {
+ files := release("1.0.0")
+ files[".secret"] = []byte("not for the site\n")
+ files["SHA256SUMS"] = sumsOf(files)
+ releases := fstest.MapFS{}
+ for name, data := range files {
+ releases["1.0.0/"+name] = &fstest.MapFile{Data: data}
+ }
+ out, err := site.Build(releases, pagesOf(nil))
+ if err == nil {
+ _, published := out["1.0.0/.secret"]
+ t.Fatalf("Build accepted a release whose SHA256SUMS lists .secret (published: %v)", published)
+ }
+ if !strings.Contains(err.Error(), ".secret") {
+ t.Errorf("the error does not name .secret: %v", err)
+ }
+ })
}
// TestHiddenPagesAreRefused: a hidden file or directory anywhere under