Preview heights measured open as well as at rest, and enforced
The two height drives now read the Overview's demo (key demo) beside every example page, and read each box twice: with its content at rest, and with each thing a reader opens in it open in turn (a dropdown, a row menu, the language and scheme menus, the enhanced select's list, the date and time fields' calendar or clock). For the open reading every box is made 1600px tall first, since a menu's cap is in dvh and dvh in a frame is the frame; a menu anchored to its button is read where it lies, what scrolls inside a list as the list, and the calendar's sheet on a narrow frame as its own content and margins. Each enhanced field is closed again with Escape, and a frame not back to its closed height fails the drive. The narrow drive grants its 48px only to a frame holding a sidebar shell, as Rastrillo's does; the wide drive keeps it for every frame at rest and grants none to what opens. heights.go is the twelve-locale sweep at 1500px (Desktop and Mobile) and at a 320px viewport, measured on this branch with the shell, the frame padding, the stage and the demo in. The menus, the select and the date and time fields get the room Rastrillo reserves by hand (locale-menu 420, the calendar fields 430, field-time 440, field-select 400, the dropdowns 200); demo is 760, Mobile 950. Documents 100dvh tall (the layouts, the staged sign-in screens, shell-sidebar) read equal to their box, so the sweep's +20 on them is not taken: their numbers stay. The 320px raises are the same in all twelve locales and go into previewMobileHeights; previewMobileLocaleHeights stays empty until the samples are translated and swept again. heightsEnforced is true: a short box fails the gate. New behaviour, red by assertion first: both drives enforced against the table before this commit report 46 shortfalls at 1500px in day/en, every one an open menu or panel (list-bar's filter menu needs 187px in a box of 110, row-menu 183 in 90, locale-menu 400 in 90, field-date 406 in 190); at 320px, with the grace gone for components, callout/warning (216 in 200), page-header (196 in 187) and button (385 in 370) too. Green after, enforced: 153 frames and 28 opened at 1500px, 612 frames and 112 opened at 320px over en, ar, ru and ga, none short, each self-check read short. hack/test-weights.txt carries the gallery package's measured weights, so the four slices deal the two height drives, the axe scans and the demo's drives apart; the role count is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+365
−148
hack/test-weights.txt+34 −4internal/gallery/heights.go+103 −94internal/gallery/heights_browser_test.go+228 −50
CI passed — run details
CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok amadan.net/rastrillo/fiesta 0.110s
=== 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 15 ---
==> shard 0 of 15: 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 1.368s
ok amadan.net/rastrillo/fiesta/check 5.401s
ok amadan.net/rastrillo/fiesta/cmd/fiesta 88.001s
ok amadan.net/rastrillo/fiesta/example 0.215s
ok amadan.net/rastrillo/fiesta/internal/axe 0.076s
ok amadan.net/rastrillo/fiesta/internal/build 1.664s
ok amadan.net/rastrillo/fiesta/internal/codeview 0.008s
ok amadan.net/rastrillo/fiesta/internal/gallery 84.109s
? amadan.net/rastrillo/fiesta/internal/harness [no test files]
ok amadan.net/rastrillo/fiesta/internal/markup 0.016s
ok amadan.net/rastrillo/fiesta/internal/nodetest 0.353s
ok amadan.net/rastrillo/fiesta/internal/site 2.962s
ok amadan.net/rastrillo/fiesta/internal/tokens 0.008s
ok amadan.net/rastrillo/fiesta/internal/www 75.360s
--- shard 1 of 15 ---
==> shard 1 of 15: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
--- shard 2 of 15 ---
==> shard 2 of 15: 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 15 ---
==> shard 3 of 15: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
[90m5:39AM[0m [32mINF[0m [1mUnknown SCM platform. Use --platform to include links in findings.[0m [36mhost=[0mamadan.net
[90m5:39AM[0m [32mINF[0m [1m1 commits scanned.[0m
[90m5:39AM[0m [32mINF[0m [1mscanned ~6488473 bytes (6.49 MB) in 4.44s[0m
[90m5:39AM[0m [32mINF[0m [1mno leaks found[0m
--- shard 4 of 15 ---
==> shard 4 of 15: 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 13.208s
--- shard 5 of 15 ---
==> shard 5 of 15: 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 95 of 286 tests
ok amadan.net/rastrillo/fiesta/check 225.360s
--- shard 6 of 15 ---
==> shard 6 of 15: 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 95 of 286 tests
ok amadan.net/rastrillo/fiesta/check 241.117s
--- shard 7 of 15 ---
==> shard 7 of 15: 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 96 of 286 tests
ok amadan.net/rastrillo/fiesta/check 244.721s
--- shard 8 of 15 ---
==> shard 8 of 15: make browser.internal.gallery~1of4
./hack/gotest.sh browser 'internal.gallery~1of4'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 4 of ./internal/gallery/ runs 1 of 145 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 314.062s
--- shard 9 of 15 ---
==> shard 9 of 15: make browser.internal.gallery~2of4
./hack/gotest.sh browser 'internal.gallery~2of4'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 4 of ./internal/gallery/ runs 47 of 145 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 195.688s
--- shard 10 of 15 ---
==> shard 10 of 15: make browser.internal.gallery~3of4
./hack/gotest.sh browser 'internal.gallery~3of4'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 3 of 4 of ./internal/gallery/ runs 49 of 145 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 214.435s
--- shard 11 of 15 ---
==> shard 11 of 15: make browser.internal.gallery~4of4
./hack/gotest.sh browser 'internal.gallery~4of4'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 4 of 4 of ./internal/gallery/ runs 48 of 145 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 205.712s
--- shard 12 of 15 ---
==> shard 12 of 15: make browser.internal.gallery.history
./hack/gotest.sh browser 'internal.gallery.history'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
ok amadan.net/rastrillo/fiesta/internal/gallery/history 34.559s
--- shard 13 of 15 ---
==> shard 13 of 15: make browser.internal.www~1of2
./hack/gotest.sh browser 'internal.www~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/www/ runs 17 of 34 tests
ok amadan.net/rastrillo/fiesta/internal/www 130.975s
--- shard 14 of 15 ---
==> shard 14 of 15: make browser.internal.www~2of2
./hack/gotest.sh browser 'internal.www~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/www/ runs 17 of 34 tests
ok amadan.net/rastrillo/fiesta/internal/www 116.991s
Intent
diff --git a/hack/test-weights.txt b/hack/test-weights.txt| index 59142a1..e7d0578 100644 |
| --- a/hack/test-weights.txt |
| +++ b/hack/test-weights.txt |
| @@ -66,8 +66,38 @@ browser check TestTheCalendarDocksAndItsDaysAreTaps 2 |
| browser check TestTheCardFitsTheSmallestPhone 2 |
| browser check TestTheConsoleDegradesTheWayItSaysItDoesWithoutHas 2 |
| browser check TestTheTrackColourFollowsTheKnob 2 |
| -# Measured 2026-10-27 on docs-t1, one run of internal/gallery with the |
| -# headless shell. |
| -browser internal/gallery TestEveryExamplePassesAxe 56 |
| +# Measured 2026-10-29 on docs-t1, one run of internal/gallery with |
| +# /usr/bin/chromium, every test at 2s or more. |
| +browser internal/gallery TestPreviewFrameHeightsFitAtThePhonesNarrowest 290 |
| +browser internal/gallery TestPreviewFrameHeightsFitTheirContent 83 |
| +browser internal/gallery TestEveryExamplePassesAxe 55 |
| +browser internal/gallery TestTheHTMLTabRendersExactlyLikeThePreview 49 |
| +browser internal/gallery TestA11yScansTheGallery 34 |
| +browser internal/gallery TestThePreviewDefaultIsMonotoneInStageWidth 12 |
| +browser internal/gallery TestThePreviewWidgetIsUsableOnAPhone 10 |
| +browser internal/gallery TestAFrameStopsLookingForItsNextDocument 8 |
| +browser internal/gallery TestTheBarsLanguageMenuStaysOnScreen 8 |
| +browser internal/gallery TestEveryLookupIsTraced 7 |
| browser internal/gallery TestTheGalleryHasNoHorizontalScrollAt320 6 |
| -browser internal/gallery TestAFrameSubmitsNothing 2 |
| +browser internal/gallery TestA11yScansThePhoneViews 5 |
| +browser internal/gallery TestAMovedSectionsOldLinkReachesIt 5 |
| +browser internal/gallery TestMobileIsThePageAtItsRealSize 5 |
| +browser internal/gallery TestA11yScansTheCodePanels 4 |
| +browser internal/gallery TestPositionSurvivesASwitch 4 |
| +browser internal/gallery TestTheDisplaySettingsMenuOnAPhone 4 |
| +browser internal/gallery TestThePinnedBarFitsItsReservation 4 |
| +browser internal/gallery TestAFrameHidesWhileItNavigates 3 |
| +browser internal/gallery TestAFrameNeverShowsTheOtherSchemeWhileItLoads 3 |
| +browser internal/gallery TestAFrameShowsNothingUntilItsDocumentIsStyled 3 |
| +browser internal/gallery TestDesktopPansFromTheInlineStart 3 |
| +browser internal/gallery TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule 3 |
| +browser internal/gallery TestFocusIsNeverUnderThePinnedBar 3 |
| +browser internal/gallery TestNothingPaintsOverTheBarOrTheBackStrip 3 |
| +browser internal/gallery TestPreviewWidgetDrivesTheWholeJourney 3 |
| +browser internal/gallery TestTheAssetDeclarationDrivesTheTree 3 |
| +browser internal/gallery TestTheCopyButtonCopiesAnnouncesAndFailsSafely 3 |
| +browser internal/gallery TestTheDemoIsFourDocumentsInTheShippedShell 3 |
| +browser internal/gallery TestThePasswordScreenCarriesItsWarning 3 |
| +browser internal/gallery TestAShellDemoLaysOutAsAnAppWould 2 |
| +browser internal/gallery TestAStoredSchemeIsOnTheGalleryPageBeforeFirstPaint 2 |
| +browser internal/gallery TestThePhoneIndexAndTheWayBack 2 |
diff --git a/internal/gallery/heights.go b/internal/gallery/heights.go| index 2efcb16..9e45460 100644 |
| --- a/internal/gallery/heights.go |
| +++ b/internal/gallery/heights.go |
| @@ -14,43 +14,49 @@ import ( |
| // the whole table. Keyed <example>/<state>, <example>/row for a |
| // grouped widget, and demo for the demo application. |
| // |
| +// A box holds what a reader opens in it as well as what is there at |
| +// rest: a menu, the enhanced select's list, the date and time fields' |
| +// calendar or clock. The drives open each in turn in a box made tall |
| +// enough that a menu's dvh cap is not the frame, and the number here is |
| +// the larger of the two readings, so a box sized for the closed state |
| +// is a short box. Rastrillo reserves the same room by hand. |
| +// |
| // Seeded once by the widget task to prove the measurement; calibrated |
| -// by the integration task once the shell, the frame padding and the |
| -// stage are in, which is when the drives start failing a short box, |
| -// and again after the samples are translated. |
| +// once the shell, the frame padding, the stage and the demo were all |
| +// in, which is when the drives started failing a short box; and to be |
| +// swept again after the samples are translated. |
| // previewHeight is what an example gets when the table has nothing to |
| // say about it. |
| const previewHeight = 220 |
| // previewHeights is each key's Desktop height, in CSS pixels at its |
| -// class's virtual width. A box deliberately taller than its content at |
| -// rest is the one a reader opens something in: a field whose script |
| -// opens a panel, a menu, the modal. |
| +// class's virtual width: the tallest of its content at rest and with |
| +// each thing in it open, over the twelve locales, with slack. |
| var previewHeights = map[string]int{ |
| - "actor/a-person": 80, |
| - "actor/an-agent-acting-for-a-person": 80, |
| - "api-tokens/four-tokens-two-of-them-live": 930, |
| - "api-tokens/just-created-the-secret-shown-once": 1060, |
| - "api-tokens/no-tokens-yet": 670, |
| - "back-nav/back-to-a-record": 100, |
| - "badge/row": 100, |
| - "box/default": 220, |
| - "bulk-bar/three-rows-selected": 110, |
| - "button/default": 270, |
| - "callout/info": 140, |
| - "callout/negative": 160, |
| - "callout/negative-announced-as-an-alert": 160, |
| - "callout/positive": 160, |
| - "callout/warning": 160, |
| - "choice-field/pick-any": 280, |
| - "choice-field/pick-one": 280, |
| - "confirm-form/destructive": 170, |
| - "confirm-form/ordinary-default-cancel-wording": 170, |
| - "detail-list/four-facts-one-machine-ish": 230, |
| - "demo": 700, |
| - "dropdown/a-filter-menu-with-one-choice-applied": 90, |
| - "dropdown/menu-idiom": 120, |
| + "actor/a-person": 80, |
| + "actor/an-agent-acting-for-a-person": 80, |
| + "api-tokens/four-tokens-two-of-them-live": 930, |
| + "api-tokens/just-created-the-secret-shown-once": 1060, |
| + "api-tokens/no-tokens-yet": 670, |
| + "back-nav/back-to-a-record": 100, |
| + "badge/row": 100, |
| + "box/default": 220, |
| + "bulk-bar/three-rows-selected": 210, |
| + "button/default": 270, |
| + "callout/info": 140, |
| + "callout/negative": 160, |
| + "callout/negative-announced-as-an-alert": 160, |
| + "callout/positive": 160, |
| + "callout/warning": 160, |
| + "choice-field/pick-any": 280, |
| + "choice-field/pick-one": 280, |
| + "confirm-form/destructive": 170, |
| + "confirm-form/ordinary-default-cancel-wording": 170, |
| + "demo": 760, |
| + "detail-list/four-facts-one-machine-ish": 230, |
| + "dropdown/a-filter-menu-with-one-choice-applied": 200, |
| + "dropdown/menu-idiom": 200, |
| "empty-state/sentence-only": 160, |
| "empty-state/with-a-post-call-to-action": 240, |
| "error-page/403-forbidden": 330, |
| @@ -62,24 +68,24 @@ var previewHeights = map[string]int{ |
| "field-check/disabled": 160, |
| "field-check/off": 160, |
| "field-check/on": 160, |
| - "field-date/bounded": 190, |
| - "field-date/enhanced-input-type-tomorrow-or-in-3-weeks": 210, |
| + "field-date/bounded": 430, |
| + "field-date/enhanced-input-type-tomorrow-or-in-3-weeks": 430, |
| "field-date/plain-the-bare-native-input": 190, |
| - "field-date/required-with-an-error": 210, |
| - "field-daterange/date-and-time-seeded": 210, |
| - "field-daterange/dates-only-with-an-error-on-the-end": 240, |
| - "field-datetime/bounded": 190, |
| - "field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks": 210, |
| + "field-date/required-with-an-error": 430, |
| + "field-daterange/date-and-time-seeded": 460, |
| + "field-daterange/dates-only-with-an-error-on-the-end": 460, |
| + "field-datetime/bounded": 430, |
| + "field-datetime/enhanced-input-type-tomorrow-or-in-3-weeks": 430, |
| "field-datetime/plain-the-bare-native-input": 190, |
| - "field-datetime/required-with-an-error": 210, |
| + "field-datetime/required-with-an-error": 430, |
| "field-money/bare": 190, |
| "field-money/with-a-hint": 210, |
| "field-money/with-an-error": 210, |
| - "field-select/hand-written-with-optgroups": 190, |
| + "field-select/hand-written-with-optgroups": 400, |
| "field-select/required-with-an-error": 210, |
| "field-select/three-options-a-native-select": 180, |
| "field-select/twelve-options-plain-the-enhancement-opted-out": 180, |
| - "field-select/twelve-options-the-filterable-combobox": 210, |
| + "field-select/twelve-options-the-filterable-combobox": 400, |
| "field-text/bare": 190, |
| "field-text/required": 190, |
| "field-text/the-headline-field": 190, |
| @@ -89,10 +95,10 @@ var previewHeights = map[string]int{ |
| "field-textarea/required": 230, |
| "field-textarea/with-a-hint": 260, |
| "field-textarea/with-an-error": 260, |
| - "field-time/bounded": 190, |
| - "field-time/enhanced-input-type-half-seven-or-noon": 210, |
| + "field-time/bounded": 440, |
| + "field-time/enhanced-input-type-half-seven-or-noon": 440, |
| "field-time/plain-the-bare-native-input": 190, |
| - "field-time/required-with-an-error": 210, |
| + "field-time/required-with-an-error": 440, |
| "field-url/bare": 190, |
| "field-url/with-a-hint": 210, |
| "field-url/with-an-error": 210, |
| @@ -134,53 +140,53 @@ var previewHeights = map[string]int{ |
| "list-bar-search/empty-accessible-name-from-the-catalog": 120, |
| "list-bar-search/with-a-query": 120, |
| "list-bar-search/with-a-query-on-page-4": 120, |
| - "list-bar/search-with-a-filter-dropdown": 110, |
| + "list-bar/search-with-a-filter-dropdown": 210, |
| "list-grid/default": 280, |
| "list-row-action/lead-marker-status-and-an-action": 120, |
| "list-row-action/name-and-meta-line-only": 120, |
| "list-search-submit/default-label": 130, |
| - "locale-menu/twelve-languages-current-highlighted": 90, |
| + "locale-menu/twelve-languages-current-highlighted": 420, |
| "media-grid/screenshots-a-logo-and-a-banner": 720, |
| "media-grid/select-mode-one-tile-selected": 400, |
| - "meter/row": 100, |
| - "modal/default": 170, |
| - "notice/a-saved-message": 120, |
| - "page-header/title-only": 120, |
| - "page-header/title-subhead-and-a-primary-action": 150, |
| - "pagination/first-page-default-accessible-name": 120, |
| - "pagination/middle-of-a-long-list": 120, |
| - "person/large-the-show-header-size": 100, |
| - "person/name-and-email": 90, |
| - "person/no-initial": 90, |
| - "row-menu/links-a-form-and-a-destructive-item": 90, |
| - "row-menu/links-only-for-a-list-inside-a-selection-form": 90, |
| - "scheme-menu/standalone": 90, |
| - "scheme-menu/three-choices-dark-current": 90, |
| - "seg-tabs/two-sections": 110, |
| - "selbox/default": 90, |
| - "shell-sidebar/default": 260, |
| - "shell-topbar/default": 210, |
| - "signin/a-passkey": 470, |
| - "signin/after-the-link-is-sent": 390, |
| - "signin/after-the-link-is-sent-in-another-browser": 390, |
| - "signin/an-address-that-does-not-look-right": 390, |
| - "signin/asking-for-an-address": 420, |
| - "signin/coming-back-with-an-email-link": 390, |
| - "signin/coming-back-with-keymail": 390, |
| - "signin/email-and-password": 390, |
| - "signin/google-apple-and-the-rest": 310, |
| - "signin/on-the-way-to-keymail": 390, |
| - "signin/when-keymail-could-not-confirm-it": 410, |
| - "signin/when-keymail-was-offered-and-a-link-went-out": 390, |
| - "stat-band/default": 190, |
| - "stat/a-shortened-number": 140, |
| - "stat/a-smaller-number": 140, |
| - "stat/the-big-number": 190, |
| - "stat/when-going-down-is-good": 180, |
| - "stat/when-going-up-is-bad": 180, |
| - "stat/when-you-cannot-say": 180, |
| - "status-pill/row": 100, |
| - "tblock/default": 270, |
| + "meter/row": 100, |
| + "modal/default": 170, |
| + "notice/a-saved-message": 120, |
| + "page-header/title-only": 120, |
| + "page-header/title-subhead-and-a-primary-action": 150, |
| + "pagination/first-page-default-accessible-name": 120, |
| + "pagination/middle-of-a-long-list": 120, |
| + "person/large-the-show-header-size": 100, |
| + "person/name-and-email": 90, |
| + "person/no-initial": 90, |
| + "row-menu/links-a-form-and-a-destructive-item": 210, |
| + "row-menu/links-only-for-a-list-inside-a-selection-form": 180, |
| + "scheme-menu/standalone": 200, |
| + "scheme-menu/three-choices-dark-current": 200, |
| + "seg-tabs/two-sections": 110, |
| + "selbox/default": 90, |
| + "shell-sidebar/default": 260, |
| + "shell-topbar/default": 210, |
| + "signin/a-passkey": 470, |
| + "signin/after-the-link-is-sent": 390, |
| + "signin/after-the-link-is-sent-in-another-browser": 390, |
| + "signin/an-address-that-does-not-look-right": 390, |
| + "signin/asking-for-an-address": 420, |
| + "signin/coming-back-with-an-email-link": 390, |
| + "signin/coming-back-with-keymail": 390, |
| + "signin/email-and-password": 390, |
| + "signin/google-apple-and-the-rest": 310, |
| + "signin/on-the-way-to-keymail": 390, |
| + "signin/when-keymail-could-not-confirm-it": 410, |
| + "signin/when-keymail-was-offered-and-a-link-went-out": 390, |
| + "stat-band/default": 190, |
| + "stat/a-shortened-number": 140, |
| + "stat/a-smaller-number": 140, |
| + "stat/the-big-number": 190, |
| + "stat/when-going-down-is-good": 180, |
| + "stat/when-going-up-is-bad": 180, |
| + "stat/when-you-cannot-say": 180, |
| + "status-pill/row": 100, |
| + "tblock/default": 270, |
| } |
| // previewMobileHeights overrides the 1.25× factor for the examples |
| @@ -193,23 +199,26 @@ var previewHeights = map[string]int{ |
| var previewMobileHeights = map[string]int{ |
| "api-tokens/four-tokens-two-of-them-live": 1850, |
| "api-tokens/just-created-the-secret-shown-once": 2080, |
| - "badge/row": 170, |
| - "button/default": 370, |
| - "field-daterange/date-and-time-seeded": 300, |
| - "field-daterange/dates-only-with-an-error-on-the-end": 330, |
| + "badge/row": 170, |
| + "button/default": 410, |
| + "callout/negative": 240, |
| + "callout/warning": 240, |
| + "format-abbr/default": 220, |
| "format-numbers/default": 220, |
| "layout-column/default": 980, |
| "layout-console-page/default": 1130, |
| "layout-sidebar-page/default": 1000, |
| "layout-stage/default": 640, |
| "layout-topbar/default": 1070, |
| - "list-bar/search-with-a-filter-dropdown": 180, |
| - "list-row-action/lead-marker-status-and-an-action": 200, |
| - "media-grid/screenshots-a-logo-and-a-banner": 1560, |
| - "media-grid/select-mode-one-tile-selected": 810, |
| - "meter/row": 370, |
| - "signin/a-passkey": 730, |
| - "signin/after-the-link-is-sent": 600, |
| + "list-bar/search-with-a-filter-dropdown": 310, |
| + "list-row-action/lead-marker-status-and-an-action": 240, |
| + "media-grid/screenshots-a-logo-and-a-banner": 1590, |
| + "media-grid/select-mode-one-tile-selected": 850, |
| + "meter/row": 370, |
| + "page-header/title-subhead-and-a-primary-action": 220, |
| + "pagination/middle-of-a-long-list": 180, |
| + "signin/a-passkey": 730, |
| + "signin/after-the-link-is-sent": 600, |
| "signin/after-the-link-is-sent-in-another-browser": 570, |
| "signin/an-address-that-does-not-look-right": 610, |
| "signin/asking-for-an-address": 660, |
diff --git a/internal/gallery/heights_browser_test.go b/internal/gallery/heights_browser_test.go| index f0a9964..c78c687 100644 |
| --- a/internal/gallery/heights_browser_test.go |
| +++ b/internal/gallery/heights_browser_test.go |
| @@ -24,14 +24,13 @@ import ( |
| // phone. A frame smaller than its content is a sample a reader has to |
| // scroll inside a box the size of a paragraph. |
| // |
| -// heightsEnforced is the switch between reporting and gating. Until |
| -// the shell, the frame padding and the stage have all landed, what a |
| -// frame measures is still moving, so a shortfall is logged and the |
| -// drives fail only when they measured nothing or when the self-check |
| -// fails: a box made 100px short of its content by hand has to be |
| -// reported short, which proves the measurement. The integration task |
| -// calibrates the table and flips this. |
| -const heightsEnforced = false |
| +// heightsEnforced is the switch between reporting and gating. The |
| +// table is calibrated now the shell, the frame padding, the stage and |
| +// the demo are all in, so a box short of its content, at rest or with |
| +// what opens in it open, fails the drive by name. The self-check stays: |
| +// a box made 100px short of its content by hand has to be reported |
| +// short, which proves the measurement. |
| +const heightsEnforced = true |
| // sweepFull is the opt-in for the whole table: every locale rather than |
| // the routine few, and the table printed as Go source. |
| @@ -65,25 +64,39 @@ func narrowLocales() []string { |
| return []string{"en", "ar", "ru", "ga"} |
| } |
| -// measureHeights reads every frame on the page, keyed as heights.go |
| -// keys them (<example>/<state>, read off the frame's src): what its |
| -// document needs in its own pixels, and what its box gives it, which |
| -// is --ds-h or --ds-hm resolved, since the frame takes its height back |
| -// off the box in virtual pixels. |
| +// frameKey is the JS that keys a frame as heights.go keys it: the |
| +// <example>/<state> its src names under frames/, demo for the demo |
| +// application, or "?" for anything else. |
| +const frameKey = `const frameKey = f => { |
| + const src = f.getAttribute("src") || ""; |
| + const m = /\/frames\/([^/]+)\/([^/]+)\.html$/.exec(src); |
| + if (m) return m[1] + "/" + m[2]; |
| + return /\/demo\.html$/.test(src) ? "demo" : "?"; |
| +};` |
| + |
| +// measureHeights reads every frame on the page, keyed by frameKey: what |
| +// its document needs in its own pixels, what its box gives it, which is |
| +// --ds-h or --ds-hm resolved, since the frame takes its height back off |
| +// the box in virtual pixels, and whether it frames a sidebar shell, |
| +// whose rail is 100dvh tall. |
| const measureHeights = `(() => { |
| + ` + frameKey + ` |
| const out = {}; |
| for (const f of document.querySelectorAll(".ds-view__frame")) { |
| - const m = /\/frames\/([^/]+)\/([^/]+)\.html$/.exec(f.getAttribute("src") || ""); |
| - const key = m ? m[1] + "/" + m[2] : "?"; |
| + const key = frameKey(f); |
| let d = null; |
| try { d = f.contentDocument; } catch (e) {} |
| const need = d && d.body ? Math.ceil(Math.max(d.body.getBoundingClientRect().height, d.body.scrollHeight)) : -1; |
| - out[key] = {Need: need, Box: Math.round(parseFloat(getComputedStyle(f).height))}; |
| + const shell = !!(d && d.querySelector("[rst-shell-sidebar]")); |
| + out[key] = {Need: need, Box: Math.round(parseFloat(getComputedStyle(f).height)), Shell: shell}; |
| } |
| return JSON.stringify(out); |
| })()` |
| -type heightReading struct{ Need, Box int } |
| +type heightReading struct { |
| + Need, Box int |
| + Shell bool |
| +} |
| // readHeights decodes one measurement, failing on what makes it |
| // worthless: no frame, or a frame with no document in it. |
| @@ -99,18 +112,125 @@ func readHeights(t *testing.T, ctx context.Context, where string) map[string]hei |
| t.Fatalf("%s %s: the frame has no document in it", where, key) |
| } |
| if strings.HasPrefix(key, "?") { |
| - t.Fatalf("%s: a frame's src names no document under frames/", where) |
| + t.Fatalf("%s: a frame's src names no document under frames/ and is not the demo", where) |
| + } |
| + } |
| + return got |
| +} |
| + |
| +// measureOpen is what a box has to hold for what a reader opens in it: |
| +// a menu (a dropdown, a row menu, the language and scheme menus), the |
| +// enhanced select's list, the date and time fields' calendar or clock. |
| +// Every widget's box is first made 1600px tall, because dvh inside a |
| +// frame is the frame, and tokens.css caps a menu at |
| +// min(20rem, max(100dvh - 6rem, 8rem)): measured in its own box, a |
| +// menu would shrink to fit and always read as fitting. Then each thing |
| +// that opens is opened in turn, the bottom of everything visible in the |
| +// document read, the body's own padding added under it, and the thing |
| +// closed again; the boxes get their heights back at the end. A frame |
| +// whose document is a whole page (a shell, the stage, a backdrop) is |
| +// 100dvh tall and is left out: its menus open inside it. |
| +const measureOpen = `(async (prop) => { |
| + ` + frameKey + ` |
| + const settle = (w) => new Promise(r => w.requestAnimationFrame(() => w.requestAnimationFrame(() => setTimeout(r, 30)))); |
| + const views = [...document.querySelectorAll(".ds-view")]; |
| + const kept = views.map(v => v.style.getPropertyValue(prop)); |
| + views.forEach(v => v.style.setProperty(prop, "1600px")); |
| + await settle(window); |
| + const out = {}; |
| + for (const f of document.querySelectorAll(".ds-view__frame")) { |
| + const d = f.contentDocument; |
| + if (!d || !d.body || d.querySelector("[rst-shell-sidebar], [rst-shell-topbar], [rst-shell-console], [rst-stage], [rst-backdrop]")) continue; |
| + const w = d.defaultView; |
| + const opens = []; |
| + for (const det of d.querySelectorAll("details[rst-dropdown], details[rst-row-menu]")) { |
| + if (!det.parentElement.closest("details")) opens.push(["menu", det]); |
| + } |
| + for (const input of d.querySelectorAll("[rst-combo] input[role=combobox]")) opens.push(["select", input]); |
| + for (const pick of d.querySelectorAll("[rst-dtp] [rst-dtp-pick]")) opens.push(["picker", pick]); |
| + if (opens.length === 0) continue; |
| + await settle(w); |
| + const pad = parseFloat(w.getComputedStyle(d.body).paddingBottom) || 0; |
| + let need = 0, what = ""; |
| + const closed = Math.ceil(d.body.getBoundingClientRect().height); |
| + for (const [kind, el] of opens) { |
| + if (kind === "menu") el.open = true; |
| + else if (kind === "select") { el.focus(); el.click(); } |
| + else el.click(); |
| + await settle(w); |
| + // A sheet pinned to the frame's foot (the calendar on a narrow |
| + // frame: fixed, and not anchored to anything) is as tall as its |
| + // content up to the frame's height, so what it asks of the box is |
| + // its own content and its margins, and what lies inside it is not |
| + // read again. A menu anchored to its button is fixed too, and is |
| + // read where it lies, as is everything else; but what scrolls |
| + // inside a menu or a list is read as the box that clips it. |
| + const all = [...d.body.querySelectorAll("*")]; |
| + const sheets = all.filter(e => { |
| + const m = w.getComputedStyle(e); |
| + return m.position === "fixed" && (!m.positionArea || m.positionArea === "none") && (!e.checkVisibility || e.checkVisibility()); |
| + }); |
| + const clips = all.filter(e => /auto|scroll|hidden|clip/.test(w.getComputedStyle(e).overflowY)); |
| + let bottom = d.body.getBoundingClientRect().bottom - pad, by = "body"; |
| + for (const e of all) { |
| + if (e.checkVisibility && !e.checkVisibility()) continue; |
| + if (sheets.some(x => x.contains(e)) || clips.some(c => c !== e && c.contains(e))) continue; |
| + const r = e.getBoundingClientRect(); |
| + if (r.height > 0 && r.bottom > bottom) { bottom = r.bottom; by = e.tagName.toLowerCase() + [...e.attributes].filter(a => a.name.startsWith("rst-")).map(a => "[" + a.name + "]").join(""); } |
| + } |
| + let n = Math.ceil(bottom + pad + w.scrollY); |
| + for (const x of sheets) { |
| + const sheet = Math.ceil(x.scrollHeight + 2 * (parseFloat(w.getComputedStyle(x).bottom) || 0)); |
| + if (sheet > n) { n = sheet; by = "a sheet"; } |
| + } |
| + if (n > need) { need = n; what = kind + " (" + by + ")"; } |
| + if (kind === "menu") el.open = false; |
| + else { |
| + const input = el.closest("[rst-dtp], [rst-combo]").querySelector("input[role=combobox]"); |
| + input.dispatchEvent(new KeyboardEvent("keydown", {key: "Escape", bubbles: true, cancelable: true})); |
| + input.blur(); |
| + } |
| + await settle(w); |
| + } |
| + const after = Math.ceil(d.body.getBoundingClientRect().height); |
| + out[frameKey(f)] = {Need: need, What: what, Closed: closed, After: after}; |
| + } |
| + views.forEach((v, i) => kept[i] ? v.style.setProperty(prop, kept[i]) : v.style.removeProperty(prop)); |
| + await settle(window); |
| + return JSON.stringify(out); |
| +})` |
| + |
| +type openReading struct { |
| + Need int |
| + What string |
| + Closed, After int |
| +} |
| + |
| +// readOpen measures what each frame's box must hold with its menus and |
| +// panels open, at the view prop names (--ds-h for Desktop, --ds-hm for |
| +// Mobile), and fails if a frame's document is not back to its closed |
| +// height afterwards: a panel left open would be read as content by the |
| +// next measurement. |
| +func readOpen(t *testing.T, ctx context.Context, where, prop string) map[string]openReading { |
| + t.Helper() |
| + var got map[string]openReading |
| + at(t, ctx, fmt.Sprintf("%s(%q)", measureOpen, prop), &got, awaitPromise) |
| + for key, o := range got { |
| + if o.After > o.Closed+1 { |
| + t.Fatalf("%s %s: %dpx before its %s was opened and %dpx after it was closed; something was left open", where, key, o.Closed, o.What, o.After) |
| } |
| } |
| return got |
| } |
| // shortfall reports one frame's box against its content: an error when |
| -// the heights are enforced, a line in the log until then. The slack |
| -// Rastrillo's drive grants, 48px, covers a sidebar shell's 100dvh rail. |
| -func shortfall(t *testing.T, where, key string, r heightReading) bool { |
| +// the heights are enforced, a line in the log until then. grace is the |
| +// slack the box is allowed under its content: Rastrillo's 48px for a |
| +// sidebar shell's 100dvh rail, which no box can fit, and 0 for the rest |
| +// where a drive grants it to shells alone. |
| +func shortfall(t *testing.T, where, key string, r heightReading, grace int) bool { |
| t.Helper() |
| - if r.Need <= r.Box+48 { |
| + if r.Need <= r.Box+grace { |
| if r.Box > r.Need*4 && r.Box-r.Need > 120 { |
| t.Logf("%s %s: %dpx of frame for %dpx of document; deliberate headroom, or a number to bring down", where, key, r.Box, r.Need) |
| } |
| @@ -125,6 +245,13 @@ func shortfall(t *testing.T, where, key string, r heightReading) bool { |
| return true |
| } |
| +// openShortfall reports one frame's box against what it must hold open: |
| +// no grace, since a menu cut short is a menu a reader cannot use. |
| +func openShortfall(t *testing.T, where, key string, open openReading, box int) bool { |
| + t.Helper() |
| + return shortfall(t, where+" with its "+open.What+" open", key, heightReading{Need: open.Need, Box: box}, 0) |
| +} |
| + |
| // selfCheck proves the measurement can see a short box: every widget's |
| // prop (--ds-h or --ds-hm) is set 100px under what its document needs, |
| // and the reading must then report the shortfall. |
| @@ -141,10 +268,11 @@ func selfCheck(t *testing.T, ctx context.Context, where, prop string, before map |
| } |
| } |
| -// examplePages is every example's page file, in manifest order. |
| -func examplePages(t *testing.T) []string { |
| +// heightPages is every page that frames something: the Overview, for |
| +// the demo application, then every example's page in manifest order. |
| +func heightPages(t *testing.T) []string { |
| t.Helper() |
| - var out []string |
| + out := []string{"index.html"} |
| for _, e := range manifest(t).Examples { |
| out = append(out, e.Name+".html") |
| } |
| @@ -218,32 +346,47 @@ func printTable(t *testing.T, desk, mob map[string]map[string]int) { |
| t.Logf("the table as measured:\n%s", b.String()) |
| } |
| -// TestPreviewFrameHeightsFitTheirContent: on every example page of |
| -// day/en (every locale under FIESTA_SWEEP=full), at a 1500px window, |
| -// every frame's document against its box at Desktop and, with Mobile |
| -// chosen, against --ds-hm. A stage frame counts like any other: its |
| -// document is 100dvh tall, so one that fits reads equal to its box. |
| +// TestPreviewFrameHeightsFitTheirContent: on the Overview and every |
| +// example page of day/en (every locale under FIESTA_SWEEP=full), at a |
| +// 1500px window, every frame's document against its box at Desktop |
| +// and, with Mobile chosen, against --ds-hm; then, at each, what the box |
| +// must hold with each of its menus and panels open. A stage frame |
| +// counts like any other: its document is 100dvh tall, so one that fits |
| +// reads equal to its box. The wide drive grants every frame |
| +// Rastrillo's 48px for content at rest, and none for what opens. |
| func TestPreviewFrameHeightsFitTheirContent(t *testing.T) { |
| rig := harness.New(t, func(string) http.Handler { return treeHandler(t) }) |
| deadline := 12 * time.Minute |
| if sweepFull() { |
| - deadline = 18 * time.Minute |
| + deadline = 28 * time.Minute |
| } |
| ctx, cancel := context.WithTimeout(rig.Context(), deadline) |
| defer cancel() |
| desk, mob := map[string]map[string]int{}, map[string]map[string]int{} |
| - measured, short := 0, 0 |
| + measured, opened, short := 0, 0, 0 |
| + keep := func(m map[string]int, key string, n int) { |
| + if n > m[key] { |
| + m[key] = n |
| + } |
| + } |
| for _, locale := range wideLocales() { |
| desk[locale], mob[locale] = map[string]int{}, map[string]int{} |
| - for _, file := range examplePages(t) { |
| + for _, file := range heightPages(t) { |
| where := fmt.Sprintf("%s/%s at 1500px", locale, file) |
| openPage(t, ctx, where, pageURL(rig, locale, file), 1500, 1000) |
| eagerly(t, ctx, where) |
| desktop := readHeights(t, ctx, where+" Desktop") |
| for key, r := range desktop { |
| measured++ |
| - desk[locale][key] = r.Need |
| - if shortfall(t, where+" Desktop", key, r) { |
| + keep(desk[locale], key, r.Need) |
| + if shortfall(t, where+" Desktop", key, r, 48) { |
| + short++ |
| + } |
| + } |
| + for key, o := range readOpen(t, ctx, where+" Desktop", "--ds-h") { |
| + opened++ |
| + keep(desk[locale], key, o.Need) |
| + if openShortfall(t, where+" Desktop", key, o, desktop[key].Box) { |
| short++ |
| } |
| } |
| @@ -251,8 +394,14 @@ func TestPreviewFrameHeightsFitTheirContent(t *testing.T) { |
| mobileSettle(t, ctx, where) |
| mobile := readHeights(t, ctx, where+" Mobile") |
| for key, r := range mobile { |
| - mob[locale][key] = r.Need |
| - if shortfall(t, where+" Mobile", key, r) { |
| + keep(mob[locale], key, r.Need) |
| + if shortfall(t, where+" Mobile", key, r, 48) { |
| + short++ |
| + } |
| + } |
| + for key, o := range readOpen(t, ctx, where+" Mobile", "--ds-hm") { |
| + keep(mob[locale], key, o.Need) |
| + if openShortfall(t, where+" Mobile", key, o, mobile[key].Box) { |
| short++ |
| } |
| } |
| @@ -263,10 +412,13 @@ func TestPreviewFrameHeightsFitTheirContent(t *testing.T) { |
| } |
| } |
| } |
| - if measured == 0 { |
| - t.Fatal("no frame was measured") |
| + if measured == 0 || opened == 0 { |
| + t.Fatalf("%d frames measured and %d opened; the sweep read too little", measured, opened) |
| + } |
| + if _, ok := desk["en"]["demo"]; !ok { |
| + t.Fatal("the Overview's demo was not measured") |
| } |
| - t.Logf("%d frames measured over %d locale(s); %d short (enforced: %v)", measured, len(desk), short, heightsEnforced) |
| + t.Logf("%d frames measured and %d opened over %d locale(s); %d short (enforced: %v)", measured, opened, len(desk), short, heightsEnforced) |
| if sweepFull() { |
| printTable(t, desk, mob) |
| } |
| @@ -275,20 +427,23 @@ func TestPreviewFrameHeightsFitTheirContent(t *testing.T) { |
| // TestPreviewFrameHeightsFitAtThePhonesNarrowest: the same with Mobile |
| // chosen at a 320px viewport, where a Mobile frame lays out at the |
| // stage's width and text that fits a line at 390px wraps, over en, ar, |
| -// ru and ga (all twelve under the sweep). The control: every frame is |
| -// laid out under 390px, at its stage's width. |
| +// ru and ga (all twelve under the sweep), closed and open. Only a frame |
| +// holding a sidebar shell gets the 48px of grace, as Rastrillo's |
| +// narrow drive grants it: the rail is 100dvh tall and no box can fit |
| +// it; every other frame gets none. The control: every frame is laid |
| +// out under 390px, at its stage's width. |
| func TestPreviewFrameHeightsFitAtThePhonesNarrowest(t *testing.T) { |
| rig := harness.New(t, func(string) http.Handler { return treeHandler(t) }) |
| deadline := 15 * time.Minute |
| if sweepFull() { |
| - deadline = 19 * time.Minute |
| + deadline = 28 * time.Minute |
| } |
| ctx, cancel := context.WithTimeout(rig.Context(), deadline) |
| defer cancel() |
| - measured, short := 0, 0 |
| + measured, opened, short, shells := 0, 0, 0, 0 |
| var raise []string |
| for _, locale := range narrowLocales() { |
| - for _, file := range examplePages(t) { |
| + for _, file := range heightPages(t) { |
| where := fmt.Sprintf("%s/%s at 320px", locale, file) |
| openPage(t, ctx, where, pageURL(rig, locale, file), 320, 844) |
| eagerly(t, ctx, where) |
| @@ -301,11 +456,34 @@ func TestPreviewFrameHeightsFitAtThePhonesNarrowest(t *testing.T) { |
| t.Fatalf("%s: frames %v are not at the stage's narrow width; the readings would be of the 390px frame", where, wide) |
| } |
| got := readHeights(t, ctx, where) |
| + need := map[string]int{} |
| for key, r := range got { |
| measured++ |
| - if shortfall(t, where, key, r) { |
| + grace := 0 |
| + if r.Shell { |
| + grace, shells = 48, shells+1 |
| + } |
| + need[key] = r.Need |
| + if shortfall(t, where, key, r, grace) { |
| + short++ |
| + } |
| + } |
| + for key, o := range readOpen(t, ctx, where, "--ds-hm") { |
| + opened++ |
| + if o.Need > need[key] { |
| + need[key] = o.Need |
| + } |
| + if openShortfall(t, where, key, o, got[key].Box) { |
| short++ |
| - raise = append(raise, fmt.Sprintf("%s %q: %d,", locale, key, (r.Need+20+9)/10*10)) |
| + } |
| + } |
| + for key, n := range need { |
| + grace := 0 |
| + if got[key].Shell { |
| + grace = 48 |
| + } |
| + if n > got[key].Box+grace { |
| + raise = append(raise, fmt.Sprintf("%s %q: %d,", locale, key, (n+20+9)/10*10)) |
| } |
| } |
| if file == "button.html" && locale == "en" { |
| @@ -313,11 +491,11 @@ func TestPreviewFrameHeightsFitAtThePhonesNarrowest(t *testing.T) { |
| } |
| } |
| } |
| - if measured == 0 { |
| - t.Fatal("no frame was measured") |
| + if measured == 0 || opened == 0 || shells == 0 { |
| + t.Fatalf("%d frames measured, %d opened, %d of them sidebar shells; the sweep read too little to say the grace went where it should", measured, opened, shells) |
| } |
| sort.Strings(raise) |
| - t.Logf("%d frames measured at 320px over %d locale(s); %d short (enforced: %v)", measured, len(narrowLocales()), short, heightsEnforced) |
| + t.Logf("%d frames measured and %d opened at 320px over %d locale(s), %d sidebar shells given grace; %d short (enforced: %v)", measured, opened, len(narrowLocales()), shells, short, heightsEnforced) |
| if len(raise) > 0 { |
| t.Logf("Mobile entries to raise, as locale key: height:\n\t%s", strings.Join(raise, "\n\t")) |
| } |