rastrillo / fiesta Public

Every page under budget, every render the same, and the landing sends each language to its own design system

The landing page in a language other than English now links "Design
system" (nav and footer) and "See the design system" to that
language's Overview, /design-system/day/<lang>/; English keeps
/design-system/. landing.go works the URL out from the mount and
Fiesta's default theme, and the template's three links read it. No
new strings.

Tests:

- TestTheLandingLinksTheDesignSystemInItsLanguage (new, internal/www).
  Red first by assertion: before the change all 30 links on the ten
  non-English pages read /design-system/ (for example "/es/: Sistema
  de diseño links /design-system/; want /design-system/day/es/").
  TestEveryLinkInTheSiteResolves passes over the whole new tree; with
  the URL mutated to /design-system/dya/<lang>/ it reported each link
  as a 404.
- TestA11yScansTheShellsCollapsed (ported from Rastrillo onto the
  layout documents, browser). A preservation port: it passed when
  written, 44 scans, no violations. Two mutations, each one rule
  appended to frameStyle and reverted, turned it red by assertion:
  [rst-shell-back] a { color: #bbb } (color-contrast on every content
  page in light, a control no other scan sees) and the back control
  held to 30px (111x30px against the 44x44 check).
- TestEveryPageStaysUnderItsBudget (already passing): 128 KiB per HTML
  file, no debt table, and at least 36 x 100 HTML files in the tree.
  Heaviest today: signal/bn/signin.html at 88,452 bytes of 9,253 HTML
  files. Mutation: 200 KiB of ballast in a meta element at the head of
  pageTemplate, which put 3,097 pages over. A comment does not do it:
  html/template drops comments from a template's text.
- TestRenderIsDeterministic (already passing): two renders at the
  site's mount byte for byte the same, each at least 36 x 100 HTML
  files. Mutation: a "now" template func returning time.Now(), printed
  in pageTemplate's head, made every gallery page differ.

hack/test-weights.txt gains the three tests over 1.5s, measured on
DOCS-T1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3f292290ae765ed27afc997b0a8777188f6efd0d parent 55dc1b8
6 files changed, +482 −7
  • hack/test-weights.txt +3 −0
  • internal/gallery/budget_test.go +129 −0
  • internal/gallery/pages_a11y_browser_test.go +271 −0
  • internal/www/landing.go +17 −4
  • internal/www/landing_test.go +59 −0
  • site/landing/page.html.tmpl +3 −3

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
go: downloading go1.26.10 (linux/arm64)
ok  	amadan.net/rastrillo/fiesta	1.007s
=== 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.03s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.028s
=== history ===
history: 31 commits; release tags: v1.0.0
=== suite ===
--- shard 0 of 48 ---
==> shard 0 of 48: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh root 'internal/gallery@3 cmd/fiesta@2 internal/www@2'
ok  	amadan.net/rastrillo/fiesta	6.963s
ok  	amadan.net/rastrillo/fiesta/check	14.127s
ok  	amadan.net/rastrillo/fiesta/example	0.758s
?   	amadan.net/rastrillo/fiesta/hack/seed/english	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.262s
ok  	amadan.net/rastrillo/fiesta/internal/build	3.332s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.027s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.024s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.661s
ok  	amadan.net/rastrillo/fiesta/internal/site	7.164s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.019s
suite: root took 63s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m20.240000s 0m4.510000s
--- shard 1 of 48 ---
==> shard 1 of 48: make test.internal.gallery~1of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~1of3'
gotest: slice 1 of 3 of ./internal/gallery/ runs 35 of 104 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	91.998s
suite: test.internal.gallery~1of3 took 125s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.470000s 0m2.020000s
--- shard 2 of 48 ---
==> shard 2 of 48: make test.internal.gallery~2of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~2of3'
gotest: slice 2 of 3 of ./internal/gallery/ runs 34 of 104 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	92.936s
suite: test.internal.gallery~2of3 took 125s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.150000s 0m1.950000s
--- shard 3 of 48 ---
==> shard 3 of 48: make test.internal.gallery~3of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~3of3'
gotest: slice 3 of 3 of ./internal/gallery/ runs 35 of 104 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	89.001s
suite: test.internal.gallery~3of3 took 120s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.710000s 0m2.230000s
--- shard 4 of 48 ---
==> shard 4 of 48: make test.cmd.fiesta~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~1of2'
gotest: slice 1 of 2 of ./cmd/fiesta/ runs 7 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	84.085s
suite: test.cmd.fiesta~1of2 took 115s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.850000s 0m2.890000s
--- shard 5 of 48 ---
==> shard 5 of 48: make test.cmd.fiesta~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~2of2'
gotest: slice 2 of 2 of ./cmd/fiesta/ runs 5 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	81.442s
suite: test.cmd.fiesta~2of2 took 112s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.250000s 0m3.330000s
--- shard 6 of 48 ---
==> shard 6 of 48: make test.internal.www~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~1of2'
gotest: slice 1 of 2 of ./internal/www/ runs 8 of 19 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	70.014s
suite: test.internal.www~1of2 took 113s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.010000s 0m2.960000s
--- shard 7 of 48 ---
==> shard 7 of 48: make test.internal.www~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~2of2'
gotest: slice 2 of 2 of ./internal/www/ runs 11 of 19 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	64.862s
suite: test.internal.www~2of2 took 109s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.750000s 0m2.570000s
--- shard 8 of 48 ---
==> shard 8 of 48: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
go: downloading honnef.co/go/tools v0.8.2
go: downloading github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c
go: downloading golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678
go: downloading golang.org/x/tools v0.51.0
go: downloading golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
suite: staticcheck took 79s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.030000s 0m6.040000s
--- shard 9 of 48 ---
==> shard 9 of 48: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
go: downloading golang.org/x/vuln v1.8.0
go: downloading golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518
go: downloading golang.org/x/tools v0.50.0
go: downloading golang.org/x/mod v0.41.0
go: downloading golang.org/x/sync v0.23.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.
suite: govulncheck took 55s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.660000s 0m2.700000s
--- shard 10 of 48 ---
==> shard 10 of 48: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
go: downloading github.com/zricethezav/gitleaks/v8 v8.30.1
go: downloading github.com/spf13/viper v1.19.0
go: downloading github.com/rs/zerolog v1.33.0
go: downloading github.com/spf13/cobra v1.9.1
go: downloading github.com/Masterminds/sprig/v3 v3.3.0
go: downloading github.com/charmbracelet/lipgloss v0.5.0
go: downloading github.com/fatih/semgroup v1.2.0
go: downloading github.com/BobuSumisu/aho-corasick v1.0.3
go: downloading golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
go: downloading github.com/hashicorp/go-version v1.7.0
go: downloading github.com/h2non/filetype v1.1.3
go: downloading github.com/gitleaks/go-gitdiff v0.9.1
go: downloading github.com/mholt/archives v0.1.2
go: downloading golang.org/x/sync v0.11.0
go: downloading github.com/muesli/termenv v0.15.1
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading github.com/mattn/go-runewidth v0.0.14
go: downloading github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
go: downloading github.com/sorairolake/lzip-go v0.3.5
go: downloading github.com/klauspost/compress v1.17.11
go: downloading github.com/STARRY-S/zip v0.2.1
go: downloading github.com/therootcompany/xz v1.0.1
go: downloading github.com/minio/minlz v1.0.0
go: downloading github.com/klauspost/pgzip v1.2.6
go: downloading github.com/nwaples/rardecode/v2 v2.1.0
go: downloading github.com/bodgit/sevenzip v1.6.0
go: downloading github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3
go: downloading github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
go: downloading github.com/pierrec/lz4/v4 v4.1.21
go: downloading github.com/ulikunitz/xz v0.5.12
go: downloading golang.org/x/text v0.22.0
go: downloading dario.cat/mergo v1.0.1
go: downloading github.com/Masterminds/goutils v1.1.1
go: downloading github.com/Masterminds/semver/v3 v3.3.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/huandu/xstrings v1.5.0
go: downloading github.com/mitchellh/copystructure v1.2.0
go: downloading github.com/shopspring/decimal v1.4.0
go: downloading github.com/spf13/cast v1.7.1
go: downloading golang.org/x/crypto v0.35.0
go: downloading github.com/fsnotify/fsnotify v1.8.0
go: downloading github.com/mitchellh/mapstructure v1.5.0
go: downloading github.com/sagikazarmark/slog-shim v0.1.0
go: downloading github.com/spf13/afero v1.12.0
go: downloading github.com/spf13/pflag v1.0.6
go: downloading github.com/mattn/go-colorable v0.1.14
go: downloading github.com/rivo/uniseg v0.2.0
go: downloading github.com/aymanbagabas/go-osc52/v2 v2.0.1
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading golang.org/x/sys v0.30.0
go: downloading github.com/bodgit/plumbing v1.3.0
go: downloading github.com/bodgit/windows v1.0.1
go: downloading github.com/hashicorp/go-multierror v1.1.1
go: downloading go4.org v0.0.0-20230225012048-214862532bf5
go: downloading github.com/subosito/gotenv v1.6.0
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading gopkg.in/ini.v1 v1.67.0
go: downloading github.com/magiconair/properties v1.8.9
go: downloading github.com/pelletier/go-toml/v2 v2.2.3
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7
go: downloading github.com/mitchellh/reflectwalk v1.0.2
go: downloading github.com/hashicorp/errwrap v1.1.0
8:33PM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
8:33PM INF 29 commits scanned.
8:33PM INF scanned ~7766851 bytes (7.77 MB) in 6.49s
8:33PM INF no leaks found
suite: gitleaks took 62s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.220000s 0m4.170000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: ./internal/harness/ runs its 10 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/harness	8.858s
suite: browser.internal.harness took 57s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m15.090000s 0m3.110000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 5 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	85.452s
suite: browser.check~1of13 took 144s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m53.880000s 0m10.420000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 9 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	79.198s
suite: browser.check~2of13 took 137s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.620000s 0m10.170000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 10 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	83.915s
suite: browser.check~3of13 took 143s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m53.050000s 0m11.110000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 11 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	87.043s
suite: browser.check~4of13 took 145s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m57.140000s 0m13.870000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 16 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	59.232s
suite: browser.check~5of13 took 117s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.810000s 0m9.470000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 16 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	69.939s
suite: browser.check~6of13 took 128s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m41.740000s 0m11.720000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 16 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	66.147s
suite: browser.check~7of13 took 124s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m41.230000s 0m10.020000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 17 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	63.554s
suite: browser.check~8of13 took 121s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.750000s 0m11.110000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 18 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	59.623s
suite: browser.check~9of13 took 117s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.080000s 0m12.860000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 17 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	73.693s
suite: browser.check~10of13 took 132s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.920000s 0m14.260000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 18 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	59.970s
suite: browser.check~11of13 took 120s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.590000s 0m9.730000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 18 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	68.903s
suite: browser.check~12of13 took 126s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.590000s 0m11.980000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 18 of 189 browser tests
ok  	amadan.net/rastrillo/fiesta/check	59.609s
suite: browser.check~13of13 took 118s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.390000s 0m11.420000s
--- shard 25 of 48 ---
==> shard 25 of 48: make browser.internal.gallery~1of18
./hack/gotest.sh browser 'internal.gallery~1of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	48.207s
suite: browser.internal.gallery~1of18 took 105s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.250000s 0m5.400000s
--- shard 26 of 48 ---
==> shard 26 of 48: make browser.internal.gallery~2of18
./hack/gotest.sh browser 'internal.gallery~2of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.691s
suite: browser.internal.gallery~2of18 took 122s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.810000s 0m6.120000s
--- shard 27 of 48 ---
==> shard 27 of 48: make browser.internal.gallery~3of18
./hack/gotest.sh browser 'internal.gallery~3of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	74.711s
suite: browser.internal.gallery~3of18 took 130s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.770000s 0m6.910000s
--- shard 28 of 48 ---
==> shard 28 of 48: make browser.internal.gallery~4of18
./hack/gotest.sh browser 'internal.gallery~4of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	74.977s
suite: browser.internal.gallery~4of18 took 133s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.230000s 0m7.670000s
--- shard 29 of 48 ---
==> shard 29 of 48: make browser.internal.gallery~5of18
./hack/gotest.sh browser 'internal.gallery~5of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 5 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	79.800s
suite: browser.internal.gallery~5of18 took 135s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.170000s 0m6.910000s
--- shard 30 of 48 ---
==> shard 30 of 48: make browser.internal.gallery~6of18
./hack/gotest.sh browser 'internal.gallery~6of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 6 of 18 of ./internal/gallery/ runs 6 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.632s
suite: browser.internal.gallery~6of18 took 124s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.440000s 0m6.280000s
--- shard 31 of 48 ---
==> shard 31 of 48: make browser.internal.gallery~7of18
./hack/gotest.sh browser 'internal.gallery~7of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 7 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.387s
suite: browser.internal.gallery~7of18 took 123s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m29.430000s 0m4.890000s
--- shard 32 of 48 ---
==> shard 32 of 48: make browser.internal.gallery~8of18
./hack/gotest.sh browser 'internal.gallery~8of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 8 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	73.179s
suite: browser.internal.gallery~8of18 took 128s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.280000s 0m7.470000s
--- shard 33 of 48 ---
==> shard 33 of 48: make browser.internal.gallery~9of18
./hack/gotest.sh browser 'internal.gallery~9of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 9 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	81.700s
suite: browser.internal.gallery~9of18 took 137s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.080000s 0m9.230000s
--- shard 34 of 48 ---
==> shard 34 of 48: make browser.internal.gallery~10of18
./hack/gotest.sh browser 'internal.gallery~10of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 10 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	72.511s
suite: browser.internal.gallery~10of18 took 129s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m28.470000s 0m4.650000s
--- shard 35 of 48 ---
==> shard 35 of 48: make browser.internal.gallery~11of18
./hack/gotest.sh browser 'internal.gallery~11of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 11 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	74.313s
suite: browser.internal.gallery~11of18 took 131s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m28.000000s 0m4.590000s
--- shard 36 of 48 ---
==> shard 36 of 48: make browser.internal.gallery~12of18
./hack/gotest.sh browser 'internal.gallery~12of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 12 of 18 of ./internal/gallery/ runs 2 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	82.222s
suite: browser.internal.gallery~12of18 took 138s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.040000s 0m7.260000s
--- shard 37 of 48 ---
==> shard 37 of 48: make browser.internal.gallery~13of18
./hack/gotest.sh browser 'internal.gallery~13of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 13 of 18 of ./internal/gallery/ runs 3 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	81.444s
suite: browser.internal.gallery~13of18 took 139s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.550000s 0m7.320000s
--- shard 38 of 48 ---
==> shard 38 of 48: make browser.internal.gallery~14of18
./hack/gotest.sh browser 'internal.gallery~14of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 14 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	83.582s
suite: browser.internal.gallery~14of18 took 143s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.350000s 0m7.920000s
--- shard 39 of 48 ---
==> shard 39 of 48: make browser.internal.gallery~15of18
./hack/gotest.sh browser 'internal.gallery~15of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 15 of 18 of ./internal/gallery/ runs 4 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	83.210s
suite: browser.internal.gallery~15of18 took 141s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m34.120000s 0m7.850000s
--- shard 40 of 48 ---
==> shard 40 of 48: make browser.internal.gallery~16of18
./hack/gotest.sh browser 'internal.gallery~16of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 16 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	76.232s
suite: browser.internal.gallery~16of18 took 132s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.220000s 0m6.320000s
--- shard 41 of 48 ---
==> shard 41 of 48: make browser.internal.gallery~17of18
./hack/gotest.sh browser 'internal.gallery~17of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 17 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	79.937s
suite: browser.internal.gallery~17of18 took 136s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.730000s 0m8.140000s
--- shard 42 of 48 ---
==> shard 42 of 48: make browser.internal.gallery~18of18
./hack/gotest.sh browser 'internal.gallery~18of18'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 18 of 18 of ./internal/gallery/ runs 5 of 83 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	80.959s
suite: browser.internal.gallery~18of18 took 136s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.530000s 0m7.770000s
--- shard 43 of 48 ---
==> shard 43 of 48: make browser.internal.gallery.history
./hack/gotest.sh browser 'internal.gallery.history'
gotest: on ip-172-31-21-140, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: ./internal/gallery/history/ runs its 3 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery/history	55.388s
suite: browser.internal.gallery.history took 102s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.450000s 0m4.460000s
--- shard 44 of 48 ---
==> shard 44 of 48: make browser.internal.www~1of4
./hack/gotest.sh browser 'internal.www~1of4'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	69.334s
suite: browser.internal.www~1of4 took 122s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m49.380000s 0m5.070000s
--- shard 45 of 48 ---
==> shard 45 of 48: make browser.internal.www~2of4
./hack/gotest.sh browser 'internal.www~2of4'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	71.945s
suite: browser.internal.www~2of4 took 122s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m53.280000s 0m4.570000s
--- shard 46 of 48 ---
==> shard 46 of 48: make browser.internal.www~3of4
./hack/gotest.sh browser 'internal.www~3of4'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	71.595s
suite: browser.internal.www~3of4 took 123s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m49.800000s 0m5.040000s
--- shard 47 of 48 ---
==> shard 47 of 48: make browser.internal.www~4of4
./hack/gotest.sh browser 'internal.www~4of4'
gotest: on ip-172-31-21-140, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 4 of ./internal/www/ runs 4 of 16 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	66.542s
suite: browser.internal.www~4of4 took 119s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.310000s 0m4.280000s
diff --git a/hack/test-weights.txt b/hack/test-weights.txt
index b30728c..af558a0 100644
--- a/hack/test-weights.txt
+++ b/hack/test-weights.txt
@@ -97,6 +97,7 @@ browser check TestThePhoneHeaderStripIs56Pixels 2
browser check TestTheTrackColourFollowsTheKnob 2
browser check TestWithoutRelativeColourDayKeepsItsCobalt 2
browser internal/gallery TestPreviewFrameHeightsFitAtThePhonesNarrowest1of12 30
+browser internal/gallery TestA11yScansTheShellsCollapsed 30
browser internal/gallery TestPreviewFrameHeightsFitTheirContent1of3 29
browser internal/gallery TestPreviewFrameHeightsFitTheirContent2of3 28
browser internal/gallery TestPreviewFrameHeightsFitTheirContent3of3 28
@@ -172,8 +173,10 @@ browser internal/www TestTheBuntingFollowsTheTheme 7
browser internal/www TestTheSampleSwitchDrags 7
browser internal/www TestTheStillBuntingMatchesTheMovingOne 7
test internal/gallery TestTheLeakSweepSeesEscapedPunctuation 12
+test internal/gallery TestRenderIsDeterministic 10
test internal/gallery TestEveryLookupIsTraced 8
test internal/gallery TestNoEnglishProseReachesATranslatedPage 7
+test internal/gallery TestEveryPageStaysUnderItsBudget 7
test internal/gallery TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule 4
test internal/gallery TestEveryPageIsAWholeLocalisedDocument 4
test internal/gallery TestEveryProseKeyIsTranslated 4
diff --git a/internal/gallery/budget_test.go b/internal/gallery/budget_test.go
new file mode 100644
index 0000000..dd71bcb
--- /dev/null
+++ b/internal/gallery/budget_test.go
@@ -0,0 +1,129 @@
+package gallery
+
+import (
+ "path"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+
+ "amadan.net/rastrillo/fiesta"
+)
+
+// maxPageBytes is the most one HTML file of the tree may weigh. Fiesta
+// has no debt table: every page is under it, and a page that is not
+// fails by name.
+const maxPageBytes = 128 << 10
+
+// siteMount is where the site serves the tree, so the pages weighed
+// are the pages a reader is sent, URLs and all.
+const siteMount = "/design-system"
+
+// minTreeHTML is the fewest HTML files a whole tree holds: thirty-six
+// directories, three themes by twelve locales, of at least a hundred
+// pages and frame documents each. A render that came back empty, or
+// with a theme or a locale missing, is under it, so neither gate below
+// can pass by reading nothing.
+const minTreeHTML = 36 * 100
+
+// htmlFiles counts the HTML files in a tree.
+func htmlFiles(files map[string][]byte) int {
+ n := 0
+ for name := range files {
+ if strings.HasSuffix(name, ".html") {
+ n++
+ }
+ }
+ return n
+}
+
+// TestEveryPageStaysUnderItsBudget: a page's weight is what a reader
+// waits for, so it is held per page, not in total: every .html file of
+// the tree, pages and frame documents alike, is at most 128 KiB. The
+// tree's total is logged, being worth knowing and worth nothing as a
+// ceiling. Ported from Rastrillo without its debt table, which Fiesta
+// has no entry for.
+//
+// Already passing when written. The mutation that turns it red is
+// 200 KiB of ballast in pageTemplate's head, a meta element's content,
+// which puts every gallery page over. Not a comment: html/template
+// drops comments from a template's text, so a comment weighs nothing.
+func TestEveryPageStaysUnderItsBudget(t *testing.T) {
+ files := render(t, siteMount)
+ if n := htmlFiles(files); n < minTreeHTML {
+ t.Fatalf("the tree holds %d HTML files; a whole one holds at least %d, so this would weigh too little to mean anything", n, minTreeHTML)
+ }
+ var total, heaviest int
+ var heaviestName string
+ var over []string
+ for name, body := range files {
+ total += len(body)
+ if path.Ext(name) != ".html" {
+ continue
+ }
+ if len(body) > heaviest {
+ heaviest, heaviestName = len(body), name
+ }
+ if len(body) > maxPageBytes {
+ over = append(over, name)
+ }
+ }
+ sort.Strings(over)
+ for i, name := range over {
+ if i == 10 {
+ t.Errorf("… and %d more over the %d-byte budget", len(over)-i, maxPageBytes)
+ break
+ }
+ t.Errorf("%s is %d bytes, over its %d-byte budget", name, len(files[name]), maxPageBytes)
+ }
+ t.Logf("heaviest page: %s at %d bytes; whole tree: %d files (%d HTML), %d bytes (%.2f MiB)",
+ heaviestName, heaviest, len(files), htmlFiles(files), total, float64(total)/(1<<20))
+}
+
+// TestRenderIsDeterministic: two renders of the same source are byte
+// for byte the same, file for file. Every map on the way to the output
+// has to be read in a fixed order; without this gate a map's random
+// order would show as a different byte stream on every build of the
+// site, in thousands of files, for no change at all. The first render
+// is the package's shared one at the site's mount, the second a fresh
+// one, and each must be a whole tree.
+//
+// Already passing when written. The mutation that turns it red is a
+// time.Now() in a template, a "now" func printed into pageTemplate's
+// head: every gallery page then differs between the two renders.
+func TestRenderIsDeterministic(t *testing.T) {
+ first := render(t, siteMount)
+ second, err := Render(fiesta.FS(), Options{Mount: siteMount})
+ if err != nil {
+ t.Fatalf("Render: %v", err)
+ }
+ for i, files := range []map[string][]byte{first, second} {
+ if n := htmlFiles(files); n < minTreeHTML {
+ t.Fatalf("render %d holds %d HTML files; a whole tree holds at least %d, so two empty renders would agree", i+1, n, minTreeHTML)
+ }
+ }
+ if reflect.DeepEqual(first, second) {
+ t.Logf("two renders of %d files agree byte for byte", len(first))
+ return
+ }
+ var diffs []string
+ for name, a := range first {
+ b, ok := second[name]
+ switch {
+ case !ok:
+ diffs = append(diffs, "only in the first: "+name)
+ case string(a) != string(b):
+ diffs = append(diffs, "differs: "+name)
+ }
+ }
+ for name := range second {
+ if _, ok := first[name]; !ok {
+ diffs = append(diffs, "only in the second: "+name)
+ }
+ }
+ sort.Strings(diffs)
+ if len(diffs) > 20 {
+ diffs = append(diffs[:20], "…")
+ }
+ t.Fatalf("Render is not deterministic:\n%s", strings.Join(diffs, "\n"))
+}
diff --git a/internal/gallery/pages_a11y_browser_test.go b/internal/gallery/pages_a11y_browser_test.go
index e0a6ed4..11c74cb 100644
--- a/internal/gallery/pages_a11y_browser_test.go
+++ b/internal/gallery/pages_a11y_browser_test.go
@@ -131,3 +131,274 @@ func a11yScansTheGallery(t *testing.T, only string) {
t.Logf("%d scans, %d violations", scans, failed)
rig.Screen("main", "the last page")
}
+
+// shellDemo is one layout example's document in the tree, in theme and
+// English, at the state the manifest opens it on: a shell demo.
+func shellDemo(t *testing.T, theme, name string) string {
+ t.Helper()
+ for _, e := range manifest(t).Examples {
+ if e.Name == name {
+ if !e.Document {
+ t.Fatalf("%s is not a layout document; it has no shell demo", name)
+ }
+ return frameFile(theme, "en", name, e.Default)
+ }
+ }
+ t.Fatalf("the manifest has no %s", name)
+ return ""
+}
+
+// shownJS reports whether the first element matching a selector has a
+// box on screen. The patency check and its control both read it, so
+// the two are the same instrument pointed at two moments. It asks
+// checkVisibility as well as for a box: the content of a closed
+// details still lays out when asked (the engine hides it with
+// content-visibility), so its box alone reads as shown.
+const shownJS = `((sel) => {
+ const el = document.querySelector(sel);
+ if (!el) return false;
+ const r = el.getBoundingClientRect();
+ return el.checkVisibility() && r.width > 0 && r.height > 0;
+})(%q)`
+
+// settleShellJS waits out the document's finite animations and any
+// view transition. The cards fade in and a narrow navigation between
+// two shell documents slides; axe reading either mid-flight measures
+// text at partial opacity, blended colours no reader sees once it has
+// landed. Finite animations only: a spinner's never finishes.
+const settleShellJS = `(async () => {
+ if (document.activeViewTransition) await document.activeViewTransition.finished.catch(() => null);
+ await Promise.all(document.getAnimations()
+ .filter(a => a.effect && a.effect.getComputedTiming().endTime !== Infinity)
+ .map(a => a.finished.catch(() => null)));
+ return true;
+})()`
+
+// targetSizeJS sets whether the engine on the page runs axe's
+// target-size rule. The engine's own run is kept aside the first time,
+// so a scan with the rule is always the engine as shipped.
+const targetSizeJS = `((on) => {
+ if (!window.__axeRun) window.__axeRun = window.axe.run.bind(window.axe);
+ window.axe.run = on ? window.__axeRun : (target, opts) =>
+ window.__axeRun(target, Object.assign({}, opts, {rules: Object.assign({}, opts && opts.rules, {"target-size": {enabled: false}})}));
+ return true;
+})(%t)`
+
+// TestA11yScansTheShellsCollapsed is the scan the others do not cover:
+// every shell demo below its 800px breakpoint.
+//
+// The other scans run the shell documents at 1280, where the topbar's
+// collapse control does not exist at all and the sidebar and console
+// show their rails beside the page. So the views a reader on a phone
+// meets first are views axe would never see. The topbar is scanned
+// with its card open. The sidebar and console are two views of every
+// URL below 800px, so both of their documents are scanned, in every
+// theme and scheme: the index (the rail is the page), with the
+// console's card and the sidebar's profile card open as well, and the
+// content page (the back control and the page, no rail). Then the
+// sidebar's profile card at the rail's foot on a desktop, open with
+// the language menu inside it, in every theme and scheme.
+//
+// It also measures what axe will not: target size. The topbar's
+// summary against WCAG 2.2's 24x24 floor, and the back control against
+// the 44px the touch block promises (390px is inside its width half).
+//
+// Ported from Rastrillo onto Fiesta's layout documents: layout-topbar,
+// layout-sidebar and layout-console are the index documents and the
+// two -page examples the content pages. A scheme is painted as
+// Fiesta's other scans paint it, by data-theme on the root, and each
+// paint is checked to have changed the ground. Every disclosure is
+// checked shut before the click and open after it, so a summary that
+// stopped opening fails rather than being scanned shut and reported
+// clean.
+//
+// A preservation port, already passing when written: Fiesta's shells
+// were clean collapsed. Its two mutations, each one rule appended to
+// frameStyle and reverted: [rst-shell-back] a { color: #bbb } is
+// color-contrast on every content page in light, which no other scan
+// sees, the back control being hidden above 800px; and the back
+// control held to 30px tall fails the 44px check on every content
+// page.
+func TestA11yScansTheShellsCollapsed(t *testing.T) {
+ rig := harness.New(t, func(string) http.Handler { return treeHandler(t) })
+ ctx, cancel := context.WithTimeout(rig.Context(), 15*time.Minute)
+ defer cancel()
+
+ total, scans := 0, 0
+ incomplete := map[string]int{}
+
+ shown := func(where, sel string) bool {
+ t.Helper()
+ var ok bool
+ if err := chromedp.Run(ctx, chromedp.Evaluate(fmt.Sprintf(shownJS, sel), &ok)); err != nil {
+ t.Fatalf("%s: reading whether %q is on screen: %v", where, sel, err)
+ }
+ return ok
+ }
+ load := func(where, file string, w, h int64, ready string) {
+ t.Helper()
+ if err := chromedp.Run(ctx,
+ chromedp.EmulateViewport(w, h),
+ chromedp.Navigate(rig.Origin+href(file)),
+ chromedp.WaitVisible(ready, chromedp.ByQuery),
+ ); err != nil {
+ t.Fatalf("%s: loading: %v", where, err)
+ }
+ }
+ // paint puts the document in scheme, and proves the flip took: the
+ // body's ground under the other scheme differs from its ground
+ // under this one, or a dark scan would be the light scan twice.
+ paint := func(where, scheme string) {
+ t.Helper()
+ other := map[string]string{"light": "dark", "dark": "light"}[scheme]
+ var away, here string
+ const flip = `document.documentElement.setAttribute("data-theme", %q); getComputedStyle(document.body).backgroundColor`
+ mustRun(t, ctx,
+ chromedp.Evaluate(fmt.Sprintf(flip, other), &away),
+ chromedp.Evaluate(fmt.Sprintf(flip, scheme), &here))
+ if away == here {
+ t.Fatalf("%s: the body paints %s under both schemes, so the flip to %s did not take", where, here, scheme)
+ }
+ }
+ // scan runs axe over the whole document in scheme, with or without
+ // the target-size rule.
+ scan := func(where, scheme string, targetSize bool) {
+ t.Helper()
+ var loaded bool
+ mustRun(t, ctx, chromedp.Evaluate(settleShellJS, nil, awaitPromise),
+ chromedp.Evaluate(`typeof window.axe !== "undefined"`, &loaded))
+ if !loaded {
+ mustRun(t, ctx, chromedp.Evaluate(axe.Script(), nil))
+ }
+ mustRun(t, ctx, chromedp.Evaluate(fmt.Sprintf(targetSizeJS, targetSize), nil))
+ paint(where, scheme)
+ res, err := axe.Scan(ctx, "", axe.DefaultTags())
+ if err != nil {
+ t.Fatalf("%s: %v", where, err)
+ }
+ scans++
+ for _, id := range res.Incomplete {
+ incomplete[id]++
+ }
+ total += report(t, where, res.Violations)
+ }
+ // box is a control's size and visible label.
+ box := func(sel string) (w, h int, text string) {
+ t.Helper()
+ var got struct {
+ W, H int
+ Text string
+ }
+ at(t, ctx, fmt.Sprintf(`((sel) => {
+ const s = document.querySelector(sel);
+ const r = s.getBoundingClientRect();
+ return JSON.stringify({W: Math.round(r.width), H: Math.round(r.height), Text: s.innerText.trim()});
+ })(%q)`, sel), &got)
+ return got.W, got.H, got.Text
+ }
+ // open clicks a disclosure and asserts what it reveals, with the
+ // control first: the same reading one click earlier, where the
+ // answer is known. A patency check hard-wired true passes after the
+ // click and fails here.
+ open := func(where, summary, revealed string) {
+ t.Helper()
+ if shown(where, revealed) {
+ t.Errorf("%s: %q is on screen with the disclosure still shut, so it is not behind the disclosure and its being shown proves nothing about the click", where, revealed)
+ }
+ if err := chromedp.Run(ctx, chromedp.Click(summary, chromedp.ByQuery)); err != nil {
+ t.Fatalf("%s: opening: %v", where, err)
+ }
+ if !shown(where, revealed) {
+ t.Fatalf("%s: clicking the disclosure did not bring %q on screen; axe would scan the collapsed document as the disclosed one", where, revealed)
+ }
+ }
+ schemes := []string{"light", "dark"}
+ themes := fiesta.ThemeNames()
+
+ // The topbar: its card, open.
+ for _, scheme := range schemes {
+ where := themes[0] + "/en topbar shell at 390px, card open (" + scheme + ")"
+ load(where, shellDemo(t, themes[0], "layout-topbar"), 390, 780, "[rst-shell-menu] > summary")
+ open(where, "[rst-shell-menu] > summary", "[rst-shell-tail] [rst-shell-nav] a")
+ scan(where, scheme, true)
+ w, h, text := box("[rst-shell-menu] > summary")
+ if w < 24 || h < 24 {
+ t.Errorf("%s: the disclosure's target is %dx%dpx, under WCAG 2.2 SC 2.5.8's 24x24", where, w, h)
+ }
+ if text == "" {
+ t.Errorf("%s: the disclosure has no visible label; the icon beside it is aria-hidden, so the control would have no accessible name at all", where)
+ }
+ }
+
+ // The sidebar and console: both documents, every theme and scheme.
+ for _, shell := range []string{"sidebar", "console"} {
+ for _, theme := range themes {
+ for _, scheme := range schemes {
+ where := theme + "/en " + shell + " index at 390px (" + scheme + ")"
+ load(where, shellDemo(t, theme, "layout-"+shell), 390, 780, "[rst-shell-rail]")
+ // Patency: the document is the index before axe reads it.
+ if rail, main := shown(where, "[rst-shell-rail] [rst-shell-nav] a"), shown(where, "[rst-shell-main]"); !rail || main {
+ t.Fatalf("%s: not the index (rail shown %v, main shown %v); axe would scan the wrong view", where, rail, main)
+ }
+ scan(where, scheme, true)
+ // The whole document with the card open, every rule but
+ // target-size. With the card open everything outside it
+ // lies under the summary's light-dismiss layer, which axe
+ // cannot see (it works out what covers a target from
+ // element boxes): a rail row half under the card reads as
+ // a target cut short when no tap can reach it at all. The
+ // rows' size is measured with the card shut, just above.
+ if shell == "console" {
+ where := theme + "/en console index at 390px, card open (" + scheme + ")"
+ // On the index the account's links are laid out flat
+ // in the card, so a link is what the click reveals.
+ open(where, "[rst-shell-menu] > summary", "[rst-shell-tail] [rst-shell-account] [rst-dropdown-menu] a")
+ scan(where, scheme, false)
+ } else {
+ where := theme + "/en sidebar index at 390px, profile card open (" + scheme + ")"
+ open(where, "[rst-shell-profile] > summary", "[rst-shell-profile] [rst-shell-who] [rst-person-name]")
+ open(where+", language open", "[rst-shell-profile] [rst-locale] > summary", "[rst-shell-profile] [rst-locale] [rst-dropdown-menu] a")
+ scan(where, scheme, false)
+ }
+
+ where = theme + "/en " + shell + " content page at 390px (" + scheme + ")"
+ load(where, shellDemo(t, theme, "layout-"+shell+"-page"), 390, 780, "[rst-shell-back] a")
+ if shown(where, "[rst-shell-rail] [rst-shell-nav] a") {
+ t.Fatalf("%s: the rail's nav is on screen on a content page; axe would scan the index", where)
+ }
+ scan(where, scheme, true)
+ w, h, text := box("[rst-shell-back] a")
+ if w < 44 || h < 44 {
+ t.Errorf("%s: the back control is %dx%dpx; the touch block promises 44x44", where, w, h)
+ }
+ if text == "" {
+ t.Errorf("%s: the back control has no visible label", where)
+ }
+ }
+ }
+ }
+
+ // The sidebar's profile menu on a desktop, the drop-up at the rail's
+ // foot, open with the language inside it, in every theme and scheme.
+ // target-size is off for the same layer as above: the page under
+ // the card is behind it.
+ for _, theme := range themes {
+ for _, scheme := range schemes {
+ where := theme + "/en sidebar content page at 1280px, profile card open (" + scheme + ")"
+ load(where, shellDemo(t, theme, "layout-sidebar-page"), 1280, 720, "[rst-shell-profile] > summary")
+ open(where, "[rst-shell-profile] > summary", "[rst-shell-profile] [rst-shell-who] [rst-person-name]")
+ open(where+", language open", "[rst-shell-profile] [rst-locale] > summary", "[rst-shell-profile] [rst-locale] [rst-dropdown-menu] a")
+ scan(where, scheme, false)
+ }
+ }
+
+ if want := 2 + 2*len(themes)*2*3 + len(themes)*2; scans != want {
+ t.Errorf("%d scans; want %d", scans, want)
+ }
+ var ids []string
+ for id, n := range incomplete {
+ ids = append(ids, fmt.Sprintf("%s x%d", id, n))
+ }
+ sort.Strings(ids)
+ t.Logf("%d scans at 390px and the desktop profile card, %d violations; needs review, axe could not decide: %v", scans, total, ids)
+}
diff --git a/internal/www/landing.go b/internal/www/landing.go
index 11fe172..e7184cc 100644
--- a/internal/www/landing.go
+++ b/internal/www/landing.go
@@ -17,11 +17,13 @@ const landingTemplate = "landing/page.html.tmpl"
// page is what the template sees: the language and its direction for
// the html element, the site's Origin for a URL that has to be fully
-// qualified, the version the footer shows, the still bunting, every
-// language the page is built in for the hreflang links and the
-// language menu, and the catalog behind T.
+// qualified, the version the footer shows, where the design system is
+// in the page's language, the still bunting, every language the page
+// is built in for the hreflang links and the language menu, and the
+// catalog behind T.
type page struct {
Lang, Dir, Origin, Version string
+ DesignSystem string
Bunting template.HTML
Languages []menuLanguage
strings map[string]string
@@ -72,6 +74,17 @@ func pageURL(lang string) string {
return "/" + lang + "/"
}
+// designSystemURL is where the landing page in lang links the design
+// system: its root for English, which is the default theme's English
+// Overview, and the default theme's Overview in lang for every other
+// language, so a reader arrives in the language they were reading.
+func designSystemURL(lang string) string {
+ if lang == "en" {
+ return Mount + "/"
+ }
+ return Mount + "/" + fiesta.ThemeNames()[0] + "/" + lang + "/"
+}
+
// languages is the menu and hreflang list for the page in current, in
// Languages' order, with the current one marked.
func languages(current string) []menuLanguage {
@@ -351,7 +364,7 @@ func landing(siteDir fs.FS, lang string) ([]byte, error) {
return nil, fmt.Errorf("www: %w", err)
}
var buf bytes.Buffer
- if err := tmpl.Execute(&buf, page{Lang: lang, Dir: Dir(lang), Origin: Origin, Version: fiesta.Version, Bunting: stillBunting(buntingShape), Languages: languages(lang), strings: strings}); err != nil {
+ if err := tmpl.Execute(&buf, page{Lang: lang, Dir: Dir(lang), Origin: Origin, Version: fiesta.Version, DesignSystem: designSystemURL(lang), Bunting: stillBunting(buntingShape), Languages: languages(lang), strings: strings}); err != nil {
return nil, fmt.Errorf("www: rendering the landing page in %s: %w", lang, err)
}
return buf.Bytes(), nil
diff --git a/internal/www/landing_test.go b/internal/www/landing_test.go
index 75c0455..a52fe2b 100644
--- a/internal/www/landing_test.go
+++ b/internal/www/landing_test.go
@@ -619,3 +619,62 @@ func TestEveryPageLinksEveryLanguage(t *testing.T) {
}
}
}
+
+// designSystemURL is where the landing page in lang sends a reader who
+// asks for the design system: the design system's own root for
+// English, which is the default theme's English Overview, and that
+// theme's Overview in lang for every other language.
+func designSystemURL(lang string) string {
+ if lang == "en" {
+ return www.Mount + "/"
+ }
+ return www.Mount + "/day/" + lang + "/"
+}
+
+// TestTheLandingLinksTheDesignSystemInItsLanguage: on the landing page
+// in each language, "Design system" in the nav and the footer and "See
+// the design system" under the headline link the design system in the
+// page's own language: /es/ links /design-system/day/es/, and / links
+// /design-system/. The links are found by their words, read from the
+// page's own catalog, so the check names the link a reader sees; and
+// no link on the page goes anywhere else under the design system, so a
+// link left on the English root is caught wherever it sits.
+func TestTheLandingLinksTheDesignSystemInItsLanguage(t *testing.T) {
+ for _, lang := range www.Languages {
+ doc, err := www.Landing(siteSource(t).FS(), lang)
+ if err != nil {
+ t.Fatalf("landing in %s: %v", lang, err)
+ }
+ strs, err := www.Strings(siteSource(t).FS(), lang)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := designSystemURL(lang)
+ found := map[string]int{}
+ walk(parse(t, doc), func(n *xhtml.Node) {
+ if n.Data != "a" {
+ return
+ }
+ href := attr(n, "href")
+ named := false
+ for _, key := range []string{"nav.design", "cta.see"} {
+ if text(n) != strs[key] {
+ continue
+ }
+ named = true
+ found[key]++
+ if href != want {
+ t.Errorf("%s: %q links %q; want %q, the design system in the page's language", pageURL(lang), text(n), href, want)
+ }
+ }
+ if !named && (href == www.Mount || strings.HasPrefix(href, www.Mount+"/")) && href != want {
+ t.Errorf("%s: a link reading %q goes to %q; every link to the design system goes to %q", pageURL(lang), text(n), href, want)
+ }
+ })
+ for key, least := range map[string]int{"nav.design": 2, "cta.see": 1} {
+ if found[key] < least {
+ t.Errorf("%s: %d links read %q (%s); want at least %d", pageURL(lang), found[key], strs[key], key, least)
+ }
+ }
+ }
+}
diff --git a/site/landing/page.html.tmpl b/site/landing/page.html.tmpl
index 974312f..fc780f0 100644
--- a/site/landing/page.html.tmpl
+++ b/site/landing/page.html.tmpl
@@ -35,7 +35,7 @@
<header class="site-top" id="top">
<b class="site-brand" translate="no">Fiesta</b>
<nav class="site-nav" aria-label="{{.T "nav.label"}}">
- <a href="/design-system/">{{.T "nav.design"}}</a>
+ <a href="{{.DesignSystem}}">{{.T "nav.design"}}</a>
<a href="#themes">{{.T "nav.themes"}}</a>
<a href="https://rastrillo.org" translate="no">Rastrillo</a>
<details rst-dropdown rst-locale name="rst-menus" class="site-lang">
@@ -61,7 +61,7 @@
<div class="site-snippet"><code dir="ltr" translate="no">&lt;link rel="stylesheet" href="{{.Origin}}/1/fiesta.css"&gt;</code><button rst-btn="sm" type="button" class="site-copy" hidden data-site-copied="{{.T "copied"}}" data-site-failed="{{.T "copy.failed"}}">{{.T "copy"}}</button></div>
<p class="site-live" role="status"></p>
</div>
- <div class="site-cta" id="actions"><a rst-btn="primary" href="/design-system/">{{.T "cta.see"}}</a><a rst-btn href="#themes">{{.T "cta.themes"}}</a></div>
+ <div class="site-cta" id="actions"><a rst-btn="primary" href="{{.DesignSystem}}">{{.T "cta.see"}}</a><a rst-btn href="#themes">{{.T "cta.themes"}}</a></div>
</div>
<div class="site-stage" id="themes">
@@ -168,7 +168,7 @@
<div class="site-foot-cols">
<div class="site-foot-brand"><b translate="no">Fiesta</b><p>{{.T "foot.tagline"}}</p></div>
<nav aria-labelledby="site-fh-fiesta"><h2 id="site-fh-fiesta" translate="no">Fiesta</h2><ul>
- <li><a href="/design-system/">{{.T "nav.design"}}</a></li>
+ <li><a href="{{.DesignSystem}}">{{.T "nav.design"}}</a></li>
<li><a href="#themes">{{.T "nav.themes"}}</a></li>
<li><a href="#install">{{.T "nav.start"}}</a></li>
<li><a href="https://amadan.net/rastrillo/fiesta">{{.T "foot.source"}}</a></li>