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>
18 files changed,
+223
−79
check/coverage_browser_test.go+3 −1check/fixture_test.go+3 −2check/menus_browser_test.go+8 −9check/node/fiesta_node.mjs+1 −0check/prerender_browser_test.go+2 −1check/rig_browser_test.go+12 −4check/scheme_browser_test.go+21 −5check/scripts_test.go+6 −0check/testdata/fixtures/index.json+15 −15cmd/fiesta/claim.go+8 −8cmd/fiesta/main.go+2 −2cmd/fiesta/release_foreign_test.go+4 −0hack/seed/_rastrillo/ui_fiestaseed_test.go+21 −13internal/build/build.go+30 −10internal/build/build_test.go+51 −7internal/gallery/a11y_browser_test.go+10 −1internal/site/site.go+6 −1internal/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' .
[90m10:33PM[0m [32mINF[0m [1mUnknown SCM platform. Use --platform to include links in findings.[0m [36mhost=[0mamadan.net
[90m10:33PM[0m [32mINF[0m [1m1 commits scanned.[0m
[90m10:33PM[0m [32mINF[0m [1mscanned ~4554990 bytes (4.55 MB) in 3.12s[0m
[90m10:33PM[0m [32mINF[0m [1mno leaks found[0m
--- 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 |