rastrillo / fiesta Public

Leave the column shell unpadded, and prove the scheme comes back from the cache

Astra's review of a587915 (task-5-review.md) found two Important and two Minor; the two Important and one Minor are here, the other Minor (the scene cached once per render) is Task 7's, which owns gallery.go.

The column shell was padded. Its body has no wrapper for the frame style's markup rules to name, so it sat in a component's 16px while Rastrillo's standalone shells, rendered by renderShell, carry no padding rule at all. A layout document is a whole page by the manifest's word, so frame now marks a layout's <body> data-gallery-document (Fiesta's own mark) and the frame style gains a third rule, body[data-gallery-document] { padding: 0; }. The first two rules stay as written. TestALayoutDocumentIsMarkedAWholePage holds the mark on every layout frame's body and on no other; TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule counts the third rule once per frame; TestFramePaddingIsAsTheRuleSays reads the column's body at 0px; and TestAShellDemoLaysOutAsAnAppWould now switches the frame style off on every layout document, the column included, rather than exempting it. All four red first against a587915: the document rule 0 times, the column's padding-top 16px, and the gallery's styling moving every box of the column demo.

The Back legs proved nothing: storage stayed Dark throughout and nothing required pageshow.persisted, so the assertions held with the restore handler removed. TestADemoOpenedInItsOwnTabKeepsTheScheme keeps the first-paint reading and the mark and loses its Back legs; TestADemoComesBackFromTheCacheInTheStoredScheme is the cache drive: on the scheme menu's document, leave for the Overview, store Light there, history.back(), require a real cache restore (pageshow.persisted and the same navigation start as before, else fail naming the engine's notRestoredReasons), then Light on the root, color-scheme light, the Light button alone marked; again with the key removed, to System with no attribute, color-scheme "light dark" and System marked; and on the sidebar demo, left by a nav click into its page, Dark to Light on the way back. Red first with frame.js's pageshow handler cut out: every return was a restore and the page came back Dark with Dark marked, by assertion, on all three legs. The drive needs full Chromium, as the spec says every cache test does; it is a function of its own so it can move to internal/gallery/history by cut-and-paste if the fleet's headless shell is what runs internal/gallery.

The robots ordering check read kids[robots-1] before testing robots == 0; the index test comes first now.

Gated with make ci in a detached /var/tmp worktree at a587915 with only these four files copied in; committed with git commit --only on them while the other streams' uncommitted files sat in the worktree.
Human

Prompt

Astra reviewed a587915: Approved with fixes. Exempt the column shell from the frame padding at render time (Rastrillo's renderShell has no padding rule) and make the layout test compare the column with the frame style disabled; make the cache-return test prove restoration (require pageshow.persisted, change storage while away, assert the restored scheme and the menu marks, including a return to System), red with the pageshow handler removed; check robots > 0 before kids[robots-1]. The scene cache goes to Task 7.

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

Summary

frame marks a layout document's <body> data-gallery-document and the frame style gains body[data-gallery-document] { padding: 0; }, so the column shell is unpadded like every shell; TestALayoutDocumentIsMarkedAWholePage, the third rule counted, the column in TestFramePaddingIsAsTheRuleSays, and TestAShellDemoLaysOutAsAnAppWould with the frame style off on every layout. TestADemoComesBackFromTheCacheInTheStoredScheme requires a real back/forward cache restore and asserts Dark to Light, Light to System (root, color-scheme, marks) on the menu document and Dark to Light on the sidebar demo; the old Back legs are gone. The robots ordering check tests the index first.
Agent notes
The cache drive needs full Chromium (the headless shell never restores from the cache); it is one self-contained function so it can move to internal/gallery/history if the fleet's shell runs internal/gallery. Red-first was shown in the gate worktree with frame.js's pageshow handler cut out: every return restored, and the page came back Dark with Dark marked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 992b61e650623108fa52fd5a4b29627ab26341a4 parent 5937d5b
4 files changed, +230 −79
  • internal/gallery/footlink_browser_test.go +20 −31
  • internal/gallery/frame.go +21 −8
  • internal/gallery/frame_test.go +56 −7
  • internal/gallery/stage_browser_test.go +133 −33

CI failed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.135s
=== 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.023s
=== 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.260s
ok  	amadan.net/rastrillo/fiesta/check	6.127s
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	110.780s
ok  	amadan.net/rastrillo/fiesta/example	0.086s
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.096s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.607s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.028s
ok  	amadan.net/rastrillo/fiesta/internal/gallery	94.960s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.006s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.427s
ok  	amadan.net/rastrillo/fiesta/internal/site	3.485s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.006s
ok  	amadan.net/rastrillo/fiesta/internal/www	91.588s
--- 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' .
1:51AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
1:51AM INF 1 commits scanned.
1:51AM INF scanned ~6226349 bytes (6.23 MB) in 5.12s
1:51AM INF no leaks found
--- 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.438s
--- 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	254.374s
--- 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	272.900s
--- 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	276.859s
--- 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 104 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	149.146s
--- 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 32 of 104 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	131.970s
--- 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 35 of 104 tests
--- FAIL: TestTheSchemeMenuRepaintsTheFrames (180.95s)
    a11y_browser_test.go:469: field: 7 frames loaded and painted
    a11y_browser_test.go:489: context deadline exceeded
FAIL
FAIL	amadan.net/rastrillo/fiesta/internal/gallery	245.215s
FAIL
make[1]: *** [Makefile:190: browser.internal.gallery~3of4] Error 1
make: *** [Makefile:64: suite] Error 2

amadan: step suite shard 10 of 15 failed: exit status 2
--- 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 36 of 104 tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	117.949s
--- 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	25.738s
--- 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	157.934s
--- 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	141.945s

amadan: job failed at step suite shard 10 of 15 — its output is above, not at the tail of this log
diff --git a/internal/gallery/footlink_browser_test.go b/internal/gallery/footlink_browser_test.go
index b43d7b0..184ccba 100644
--- a/internal/gallery/footlink_browser_test.go
+++ b/internal/gallery/footlink_browser_test.go
@@ -105,37 +105,33 @@ func TestTheWayBackFromAShellDemoIsATarget(t *testing.T) {
// Rastrillo's demos once linked gallery.css and its bar layout took
// hold of the sidebar demo. Fiesta's frame documents link no gallery
// stylesheet; what they do carry of the gallery's is the frame style
-// in their head, whose fill rule is meant to leave a shell exactly as
-// it is. Every element's box is read as served and again with every
-// gallery stylesheet switched off, and, on a document the fill rule
-// names (a body that is or holds a shell or the stage), the frame
-// style too; the two must agree. The column shell has no wrapper for
-// a rule to name and takes a component's padding, as Rastrillo's rule
-// pads it, so it is read with the frame style in place, against the
-// gallery stylesheets alone. At 1280 and 900 with a mouse, either side
-// of the gallery bar's two-row band, and at 390 with a coarse pointer.
+// in their head, whose rules are meant to leave a shell exactly as it
+// is. Every element's box is read as served and again with every
+// gallery stylesheet and the frame style switched off; the two must
+// agree, on every layout document, the column shell included, whose
+// body has no wrapper for a markup rule to name and is filled by the
+// document mark instead. At 1280 and 900 with a mouse, either side of
+// the gallery bar's two-row band, and at 390 with a coarse pointer.
//
-// Already passing when written: the fill rule left every shell at
-// padding 0 from the first render. Its mutation, run and reverted:
-// dropping "> [rst-shell-sidebar]" from the fill rule's :has() list,
-// which gave the sidebar demo 1rem of padding and moved every box in
-// it once the style was switched off, reported here per element.
+// Red first on the column shell, whose body sat in 16px of padding
+// before the document mark. Its mutation, run and reverted: dropping
+// "> [rst-shell-sidebar]" from the fill rule's :has() list, which gave
+// the sidebar demo 1rem of padding and moved every box in it once the
+// style was switched off, reported here per element.
func TestAShellDemoLaysOutAsAnAppWould(t *testing.T) {
const read = `(() => {
const boxes = () => [...document.querySelectorAll("body *")].map(e => {
const b = e.getBoundingClientRect();
return (e.getAttribute("id") || e.tagName.toLowerCase()) + " " + [b.left, b.top, b.width, b.height].map(n => Math.round(n)).join(",");
});
- const shell = document.body.matches("[rst-stage], [rst-shell-topbar], [rst-shell-sidebar], [rst-shell-console]")
- || !!document.body.querySelector(":scope > [rst-shell-topbar], :scope > [rst-shell-sidebar], :scope > [rst-shell-console], :scope > [rst-backdrop], :scope > [rst-stage]");
const served = boxes();
- const gallery = [...document.styleSheets].filter(s => (s.href && /gallery[^/]*\.css/.test(s.href)) || (shell && !s.href && s.ownerNode && s.ownerNode.tagName === "STYLE"));
+ const gallery = [...document.styleSheets].filter(s => (s.href && /gallery[^/]*\.css/.test(s.href)) || (!s.href && s.ownerNode && s.ownerNode.tagName === "STYLE"));
gallery.forEach(s => { s.disabled = true; });
const bare = boxes();
gallery.forEach(s => { s.disabled = false; });
const diff = [];
served.forEach((s, i) => { if (s !== bare[i]) diff.push(s + " (without the gallery's styling: " + bare[i] + ")"); });
- return JSON.stringify({Main: !!document.querySelector("main"), Shell: shell, Sheets: gallery.length, Diff: diff.slice(0, 5)});
+ return JSON.stringify({Main: !!document.querySelector("main"), Sheets: gallery.length, Diff: diff.slice(0, 5)});
})()`
for _, c := range []struct {
name string
@@ -148,7 +144,6 @@ func TestAShellDemoLaysOutAsAnAppWould(t *testing.T) {
} {
rig := c.rig(t)
ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second)
- shells := 0
for _, demo := range layoutDemos(t) {
where := fmt.Sprintf("%s with %s", demo, c.name)
var raw string
@@ -160,9 +155,9 @@ func TestAShellDemoLaysOutAsAnAppWould(t *testing.T) {
t.Fatalf("%s: %v", where, err)
}
var got struct {
- Main, Shell bool
- Sheets int
- Diff []string
+ Main bool
+ Sheets int
+ Diff []string
}
if err := json.Unmarshal([]byte(raw), &got); err != nil {
cancel()
@@ -171,20 +166,14 @@ func TestAShellDemoLaysOutAsAnAppWould(t *testing.T) {
if !got.Main {
t.Errorf("%s: no main element; the demo is not the page this test knows", where)
}
- if got.Shell {
- shells++
- if got.Sheets == 0 {
- t.Errorf("%s: no gallery styling was switched off, so the two readings are the same reading", where)
- }
+ if got.Sheets == 0 {
+ t.Errorf("%s: no gallery styling was switched off, so the two readings are the same reading", where)
}
for _, d := range got.Diff {
t.Errorf("%s: the gallery's styling moves %s", where, d)
}
}
cancel()
- if shells == 0 {
- t.Errorf("with %s no demo's body was a shell or the stage, so the frame style was never switched off", c.name)
- }
- t.Logf("%s: %d demos read, %d of them with the frame style switched off", c.name, len(layoutDemos(t)), shells)
+ t.Logf("%s: %d demos read with the frame style switched off", c.name, len(layoutDemos(t)))
}
}
diff --git a/internal/gallery/frame.go b/internal/gallery/frame.go
index e25ab99..ec8df1a 100644
--- a/internal/gallery/frame.go
+++ b/internal/gallery/frame.go
@@ -54,9 +54,17 @@ var (
// padding under one is also a scrollbar that can never be got rid of.
// The layout-stage example's body is itself the stage, so :is() names
// the body; a wrapped sign-in's stage is the body's child, so :has()
-// names the children.
+// names the children. A layout document is a whole page by the
+// manifest's word, whatever its markup offers a selector: the column
+// shell has no wrapper to name, and Rastrillo's standalone shells are
+// rendered with no padding rule at all, so frame marks a layout's body
+// data-gallery-document (Fiesta's own mark) and the third rule fills it.
const frameStyle = `<style>body { padding: 1rem; }
-body:is([rst-stage], [rst-shell-topbar], [rst-shell-sidebar], [rst-shell-console]), body:has(> [rst-shell-topbar], > [rst-shell-sidebar], > [rst-shell-console], > [rst-backdrop], > [rst-stage]) { padding: 0; }</style>`
+body:is([rst-stage], [rst-shell-topbar], [rst-shell-sidebar], [rst-shell-console]), body:has(> [rst-shell-topbar], > [rst-shell-sidebar], > [rst-shell-console], > [rst-backdrop], > [rst-stage]) { padding: 0; }
+body[data-gallery-document] { padding: 0; }</style>`
+
+// documentMark is what a layout document's <body> carries.
+const documentMark = ` data-gallery-document`
// frame turns one example document into a frame document: the
// gallery's hashed assets in place of the relative links, the page's
@@ -64,10 +72,11 @@ body:is([rst-stage], [rst-shell-topbar], [rst-shell-sidebar], [rst-shell-console
// meta after the viewport's, the frame style last in the head, the
// scheme store on <html>, the page's language and direction on <html>
// when it is not English, the sign-in card on its stage, a layout's
-// links live where the interface says, and every other href and action
-// in the body pointed at "#". The example's title and everything else
-// in it are kept. e is the example the document belongs to: whether it
-// is a layout decides whether its links come alive.
+// body marked a whole page and its links live where the interface
+// says, and every other href and action in the body pointed at "#".
+// The example's title and everything else in it are kept. e is the
+// example the document belongs to: whether it is a layout decides
+// whether its body is a page and its links come alive.
func (s *site) frame(doc, theme, locale string, e example.Entry) (string, error) {
head := headTag.FindStringIndex(doc)
body := bodyTag.FindStringIndex(doc)
@@ -81,7 +90,7 @@ func (s *site) frame(doc, theme, locale string, e example.Entry) (string, error)
return "", errors.New("no stylesheet in the head to put frame.js before")
}
at[0] += head[1]
- top := doc[:at[0]] + `<script src="` + s.asset("frame.js") + `"></script>` + "\n" + doc[at[0]:body[1]]
+ top := doc[:at[0]] + `<script src="` + s.asset("frame.js") + `"></script>` + "\n" + doc[at[0]:body[0]]
top = assetLink.ReplaceAllStringFunc(top, func(m string) string {
sub := assetLink.FindStringSubmatch(m)
file := sub[2] + "/" + sub[3]
@@ -120,6 +129,10 @@ func (s *site) frame(doc, theme, locale string, e example.Entry) (string, error)
return "", errors.New("no </head> to end with the frame style")
}
top = top[:shut] + frameStyle + "\n" + top[shut:]
+ open := doc[body[0]:body[1]]
+ if e.Document {
+ open = open[:len(open)-1] + documentMark + ">"
+ }
rest := doc[body[1]:]
end := strings.LastIndex(rest, "</body>")
@@ -154,7 +167,7 @@ func (s *site) frame(doc, theme, locale string, e example.Entry) (string, error)
return sub[1] + `="#"`
})
})
- return top + inner + tail, nil
+ return top + open + inner + tail, nil
}
// pictures points every sample picture at its hashed copy under
diff --git a/internal/gallery/frame_test.go b/internal/gallery/frame_test.go
index 0994bc5..8971a35 100644
--- a/internal/gallery/frame_test.go
+++ b/internal/gallery/frame_test.go
@@ -16,11 +16,13 @@ import (
// app it stands for, and the attribute that lets a document opened in
// a tab of its own remember the reader's scheme.
-// The two rules every frame's style carries, as written: a component
-// sample gets breathing room, a whole-page sample fills the frame.
+// The rules every frame's style carries, as written: a component
+// sample gets breathing room, a whole-page sample fills the frame, by
+// its markup or by the manifest's word.
const (
- paddingRule = `body { padding: 1rem; }`
- fillRule = `body:is([rst-stage], [rst-shell-topbar], [rst-shell-sidebar], [rst-shell-console]), body:has(> [rst-shell-topbar], > [rst-shell-sidebar], > [rst-shell-console], > [rst-backdrop], > [rst-stage]) { padding: 0; }`
+ paddingRule = `body { padding: 1rem; }`
+ fillRule = `body:is([rst-stage], [rst-shell-topbar], [rst-shell-sidebar], [rst-shell-console]), body:has(> [rst-shell-topbar], > [rst-shell-sidebar], > [rst-shell-console], > [rst-backdrop], > [rst-stage]) { padding: 0; }`
+ documentRule = `body[data-gallery-document] { padding: 0; }`
)
// frameDocs is every frame document of the tree, by path.
@@ -52,8 +54,8 @@ func elementChildren(n node) []node {
// TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule: every frame
// document has one robots meta saying noindex, right after the
// viewport meta, and one style, the last element of its head, with the
-// padding rule and the fill rule each exactly once, both selectors
-// present as written.
+// padding rule, the fill rule and the document rule each exactly once,
+// every selector present as written.
func TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule(t *testing.T) {
frames := frameDocs(t, render(t, ""))
for name, body := range frames {
@@ -81,7 +83,9 @@ func TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule(t *testing.T) {
}
if robots < 0 {
t.Errorf("%s carries no robots meta", name)
- } else if prev := kids[robots-1]; robots == 0 || prev.n.Data != "meta" || func() bool { v, _ := prev.attr("name"); return v != "viewport" }() {
+ } else if robots == 0 {
+ t.Errorf("%s: the robots meta is the head's first element; it follows the viewport meta", name)
+ } else if prev := kids[robots-1]; prev.n.Data != "meta" || func() bool { v, _ := prev.attr("name"); return v != "viewport" }() {
t.Errorf("%s: the robots meta does not follow the viewport meta", name)
}
if last := kids[len(kids)-1]; last.n.Data != "style" {
@@ -94,10 +98,55 @@ func TestEveryFrameDocumentCarriesNoindexAndItsPaddingRule(t *testing.T) {
if n := strings.Count(s, fillRule); n != 1 {
t.Errorf("%s carries the fill rule %d times, want once", name, n)
}
+ if n := strings.Count(s, documentRule); n != 1 {
+ t.Errorf("%s carries the document rule %d times, want once", name, n)
+ }
}
t.Logf("%d frame documents checked", len(frames))
}
+// TestALayoutDocumentIsMarkedAWholePage: the body of every layout
+// example's frame carries data-gallery-document, which the document
+// rule fills to the frame's edge, and no other frame's body does. The
+// column shell is the case the markup rules cannot reach: its body has
+// no wrapper, so without the mark it would sit in a component's padding.
+func TestALayoutDocumentIsMarkedAWholePage(t *testing.T) {
+ frames := frameDocs(t, render(t, ""))
+ m := manifest(t)
+ marked, layouts := 0, 0
+ for name, body := range frames {
+ parts := strings.Split(name, "/")
+ e, ok := m.Entry(parts[len(parts)-2])
+ if !ok {
+ t.Fatalf("%s is no example's frame", name)
+ }
+ bodyEl, ok := parse(t, body).first(func(n node) bool { return n.n.Data == "body" })
+ if !ok {
+ t.Errorf("%s has no <body>", name)
+ continue
+ }
+ has := hasAttr(bodyEl, "data-gallery-document")
+ if e.Document {
+ layouts++
+ if has {
+ marked++
+ } else {
+ t.Errorf("%s is a layout document whose body is not marked data-gallery-document", name)
+ }
+ } else if has {
+ t.Errorf("%s is not a layout document and its body is marked data-gallery-document", name)
+ }
+ }
+ if layouts == 0 {
+ t.Fatal("no frame belongs to a layout example; the check above read nothing")
+ }
+ column := "day/en/frames/layout-column/" + mustEntry(t, "layout-column").Default + ".html"
+ if _, ok := frames[column]; !ok {
+ t.Fatalf("no %s", column)
+ }
+ t.Logf("%d of %d layout frames marked", marked, layouts)
+}
+
// sceneOf lifts the stage's scene from the layout-stage example's own
// document, independently of the render, so the test and the code
// cannot agree on a wrong scene.
diff --git a/internal/gallery/stage_browser_test.go b/internal/gallery/stage_browser_test.go
index c09ae9a..4a26f15 100644
--- a/internal/gallery/stage_browser_test.go
+++ b/internal/gallery/stage_browser_test.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"math"
"net/http"
+ "strconv"
"strings"
"testing"
"time"
@@ -179,9 +180,10 @@ func TestTheSocialSignInButtonsStackAndCarryTheirMarks(t *testing.T) {
// TestFramePaddingIsAsTheRuleSays: the computed padding of a frame
// document's body is 1rem on a component's frame and nothing on a
-// shell's, the stage's, the modal's and a staged sign-in's, read in
-// the browser rather than off the bytes, since the rule is a selector
-// and a selector is only proven by what it selects.
+// shell's (the column's included, whose body no markup rule reaches),
+// the stage's, the modal's and a staged sign-in's, read in the browser
+// rather than off the bytes, since the rule is a selector and a
+// selector is only proven by what it selects.
func TestFramePaddingIsAsTheRuleSays(t *testing.T) {
rig := harness.New(t, func(string) http.Handler { return treeHandler(t) })
ctx, cancel := context.WithTimeout(rig.Context(), 2*time.Minute)
@@ -193,6 +195,7 @@ func TestFramePaddingIsAsTheRuleSays(t *testing.T) {
{"button", mustDefault(t, "button"), "16px"},
{"layout-stage", mustDefault(t, "layout-stage"), "0px"},
{"layout-sidebar", mustDefault(t, "layout-sidebar"), "0px"},
+ {"layout-column", mustDefault(t, "layout-column"), "0px"},
{"modal", mustDefault(t, "modal"), "0px"},
{"signin", "asking-for-an-address", "0px"},
} {
@@ -252,14 +255,13 @@ const schemeMarkJS = `JSON.stringify([...document.querySelectorAll("[rst-scheme]
// TestADemoOpenedInItsOwnTabKeepsTheScheme: a returning visitor with
// Dark stored opens a shell demo in a tab of its own, with the theme
-// stylesheet slow to arrive: data-theme="dark" is on <html> at the
-// first animation frame, color-scheme computes dark once styled, and
-// the one frame document that holds a scheme menu marks Dark rather
-// than the System the markup ships with; after going into the shell's
-// page and Back, and after leaving the menu's document and coming
-// Back, the scheme and the mark are still dark. fiesta.js restores a
-// stored scheme only when it is in the head without defer, and these
-// documents load it deferred, so the restore is frame.js's.
+// stylesheet slow to arrive: data-theme="dark" is on <html> when the
+// body is inserted and at the first animation frame, color-scheme
+// computes dark once styled, and the one frame document that holds a
+// scheme menu marks Dark rather than the System the markup ships with.
+// fiesta.js restores a stored scheme only when it is in the head
+// without defer, and these documents load it deferred, so the restore
+// is frame.js's. The way back from the cache is the next drive's.
func TestADemoOpenedInItsOwnTabKeepsTheScheme(t *testing.T) {
rig := harness.New(t, func(string) http.Handler { return slowTheme{newTree(t, nil), 400 * time.Millisecond} })
ctx, cancel := context.WithTimeout(rig.Context(), 3*time.Minute)
@@ -326,36 +328,134 @@ func TestADemoOpenedInItsOwnTabKeepsTheScheme(t *testing.T) {
t.Logf("the sidebar demo: data-theme=%q at <body>, %q at the first animation frame with color-scheme %q", atBody, first.Theme, first.Scheme)
readRoot("the sidebar demo once loaded")
- // Into the shell's page by its nav, and Back. The page is asked to
- // go back on a tick and the new document polled for: chromedp's
- // NavigateBack waits for a load this engine does not always report,
- // and its node waits never resolve after a history navigation
- // (check/menus_browser_test.go).
- mustRun(t, ctx,
- chromedp.Click(`[rst-shell-nav] a`, chromedp.ByQuery),
- chromedp.WaitReady(`[rst-shell-sidebar="page"]`, chromedp.ByQuery),
- )
- readRoot("the sidebar's page")
- mustRun(t, ctx, chromedp.Evaluate(`setTimeout(function () { history.back(); }, 0); true`, nil))
- until(t, ctx, "the sidebar demo after Back", `!!document.querySelector('[rst-shell-sidebar="index"]')`)
- readRoot("the sidebar demo after Back")
-
// The one frame document with a scheme menu: the stored scheme is
- // marked, not the one the markup ships with; and still after Back.
+ // marked, not the one the markup ships with.
mustRun(t, ctx, chromedp.Navigate(rig.Origin+href(menu)), chromedp.WaitReady("body"))
readRoot("the scheme menu's document")
readMark("the scheme menu's document")
+ rig.Screen("body", "the scheme menu's document in Dark")
+}
+
+// schemeOf reads the root's scheme attribute and computed color-scheme.
+const schemeOf = `JSON.stringify({Theme: document.documentElement.getAttribute("data-theme") || "", Scheme: getComputedStyle(document.documentElement).colorScheme})`
+
+// TestADemoComesBackFromTheCacheInTheStoredScheme: a demo opened in a
+// tab of its own, left for the gallery, whose stored scheme changes
+// while it is away, comes back from the back/forward cache in the new
+// scheme with its menu marked: Dark to Light, then Light to System, on
+// the scheme menu's document; and Dark to Light on the sidebar demo,
+// left by a click into its page. The control first, every time: the
+// return really was a cache restore (pageshow.persisted, and the same
+// navigation start as before), since a document reloaded instead would
+// pass on start-up code alone and prove nothing about the pageshow
+// handler. Back is history.back() from the page itself, polled for:
+// chromedp's NavigateBack waits for a load a restored page never fires,
+// and its node waits never resolve after a history navigation
+// (check/menus_browser_test.go).
+//
+// Full Chromium only: the headless shell never restores a page from
+// the cache (hack/gotest.sh's FULL_CHROMIUM note), and on it this fails
+// naming the engine's reason rather than passing on a reload.
+func TestADemoComesBackFromTheCacheInTheStoredScheme(t *testing.T) {
+ rig := harness.New(t, func(string) http.Handler { return newTree(t, nil) })
+ ctx, cancel := context.WithTimeout(rig.Context(), 3*time.Minute)
+ defer cancel()
+ theme := fiesta.ThemeNames()[0]
+ index := href(theme + "/en/index.html")
+ menu := frameFile(theme, "en", "scheme-menu", mustDefault(t, "scheme-menu"))
+ sidebar := frameFile(theme, "en", "layout-sidebar", mustDefault(t, "layout-sidebar"))
+ page := frameFile(theme, "en", "layout-sidebar-page", mustDefault(t, "layout-sidebar-page"))
+
mustRun(t, ctx,
- chromedp.Navigate(rig.Origin+href(theme+"/en/index.html")),
+ addInit(`addEventListener("pageshow", function (e) { window.__restored = e.persisted; });`),
+ chromedp.EmulateViewport(1280, 900),
+ chromedp.Navigate(rig.Origin+index),
chromedp.WaitReady("body"),
- chromedp.Evaluate(`setTimeout(function () { history.back(); }, 0); true`, nil),
+ chromedp.Evaluate(`localStorage.setItem("fiesta-scheme", "dark"); "ok"`, new(string)),
)
- until(t, ctx, "the scheme menu's document after Back", `!!document.querySelector("[rst-scheme]")`)
- readRoot("the scheme menu's document after Back")
- readMark("the scheme menu's document after Back")
+
+ readRoot := func(where, wantTheme, wantScheme string) {
+ t.Helper()
+ until(t, ctx, where, `document.readyState === "complete"`)
+ var got struct{ Theme, Scheme string }
+ at(t, ctx, schemeOf, &got)
+ if got.Theme != wantTheme {
+ t.Errorf("%s: <html> carries data-theme=%q, want %q", where, got.Theme, wantTheme)
+ }
+ if got.Scheme != wantScheme {
+ t.Errorf("%s: color-scheme computes %q, want %q", where, got.Scheme, wantScheme)
+ }
+ }
+ readMark := func(where, scheme string) {
+ t.Helper()
+ var buttons []struct{ V, Cur string }
+ at(t, ctx, schemeMarkJS, &buttons)
+ if len(buttons) == 0 {
+ t.Fatalf("%s: no scheme menu button; the mark would be read off nothing", where)
+ }
+ for _, b := range buttons {
+ if (b.V == scheme) != (b.Cur == "true") {
+ t.Errorf("%s: the %s button is aria-current=%q; %s alone is marked", where, b.V, b.Cur, scheme)
+ }
+ }
+ }
+ // away leaves the document (leave is the statement that does), waits
+ // to be at awayAt, changes the stored scheme there (store), goes
+ // back, and requires the return to be a cache restore of the
+ // document whose path ends in back.
+ away := func(where, leave, awayAt, store, back string) {
+ t.Helper()
+ var started float64
+ mustRun(t, ctx, chromedp.Evaluate(`performance.getEntriesByType("navigation")[0].startTime + performance.timeOrigin`, &started))
+ mustRun(t, ctx, chromedp.Evaluate(`window.__restored = undefined; `+leave+`; true`, nil))
+ until(t, ctx, where+", away", `location.pathname.endsWith("`+awayAt+`") && document.readyState === "complete"`)
+ mustRun(t, ctx, chromedp.Evaluate(store+`; history.back(); true`, nil))
+ until(t, ctx, where+", back", `location.pathname.endsWith("`+back+`") && window.__restored !== undefined`)
+ var raw string
+ mustRun(t, ctx, chromedp.Evaluate(`JSON.stringify({Restored: window.__restored === true,
+ Started: performance.getEntriesByType("navigation")[0].startTime + performance.timeOrigin,
+ Why: JSON.stringify((performance.getEntriesByType("navigation")[0] || {}).notRestoredReasons || null)})`, &raw))
+ var r struct {
+ Restored bool
+ Started float64
+ Why string
+ }
+ if err := json.Unmarshal([]byte(raw), &r); err != nil {
+ t.Fatalf("%s: decoding %q: %v", where, raw, err)
+ }
+ if !r.Restored || r.Started != started {
+ t.Fatalf("%s: the return was not a back/forward cache restore (persisted %v, navigation %s then %s; the engine says %s); the engine must be full Chromium and the tree handler must not send Cache-Control: no-store",
+ where, r.Restored, strconv.FormatFloat(started, 'f', 0, 64), strconv.FormatFloat(r.Started, 'f', 0, 64), r.Why)
+ }
+ }
+
+ // The scheme menu's document: Dark, then Light on the way back, then
+ // System on the way back.
+ mustRun(t, ctx, chromedp.Navigate(rig.Origin+href(menu)), chromedp.WaitReady("body"))
+ readRoot("the menu document", "dark", "dark")
+ readMark("the menu document", "dark")
+ toIndex := `location.assign("` + index + `")`
+ menuPath := "/" + menu
+ away("Dark to Light", toIndex, "/index.html", `localStorage.setItem("fiesta-scheme", "light")`, menuPath)
+ readRoot("the menu document back from the cache with Light stored", "light", "light")
+ readMark("the menu document back from the cache with Light stored", "light")
+ away("Light to System", toIndex, "/index.html", `localStorage.removeItem("fiesta-scheme")`, menuPath)
+ readRoot("the menu document back from the cache with nothing stored", "", "light dark")
+ readMark("the menu document back from the cache with nothing stored", "system")
+
+ // The shell demo: Dark, into its page by the nav, Light on the way back.
+ mustRun(t, ctx,
+ chromedp.Evaluate(`localStorage.setItem("fiesta-scheme", "dark"); "ok"`, new(string)),
+ chromedp.Navigate(rig.Origin+href(sidebar)),
+ chromedp.WaitReady("body"),
+ )
+ readRoot("the sidebar demo", "dark", "dark")
+ away("the sidebar demo, Dark to Light", `document.querySelector("[rst-shell-nav] a").click()`, "/"+page, `localStorage.setItem("fiesta-scheme", "light")`, "/"+sidebar)
+ readRoot("the sidebar demo back from the cache with Light stored", "light", "light")
+
// A fresh navigation before the screen: after a history
// navigation chromedp's node waits never resolve, so the flush of
- // the page's problems is taken on the document loaded anew.
+ // the page's problems is taken on a document loaded anew.
mustRun(t, ctx, chromedp.Navigate(rig.Origin+href(menu)), chromedp.WaitReady("body"))
- rig.Screen("body", "the scheme menu's document in Dark")
+ rig.Screen("body", "the scheme menu's document in Light")
}