rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3package history
4
5import (
6 "context"
7 "encoding/json"
8 "net/http"
9 "strconv"
10 "testing"
11 "time"
12
13 "github.com/chromedp/chromedp"
14
15 "amadan.net/rastrillo/fiesta"
16 "amadan.net/rastrillo/fiesta/example"
17 "amadan.net/rastrillo/fiesta/internal/harness"
18)
19
20// A shell demo opened in a tab of its own, left and come back to from
21// the back/forward cache, in the scheme stored while it was away.
22
23// frameDoc is the path of one state's frame document within the tree.
24func frameDoc(theme, locale, name, state string) string {
25 return theme + "/" + locale + "/frames/" + name + "/" + state + ".html"
26}
27
28// defaultState is the manifest's default state for name.
29func defaultState(t *testing.T, name string) string {
30 t.Helper()
31 m, err := example.Load(fiesta.FS())
32 if err != nil {
33 t.Fatal(err)
34 }
35 state, ok := m.Default(name)
36 if !ok || state == "" {
37 t.Fatalf("the manifest has no default state for %s", name)
38 }
39 return state
40}
41
42func run(t *testing.T, ctx context.Context, actions ...chromedp.Action) {
43 t.Helper()
44 if err := chromedp.Run(ctx, actions...); err != nil {
45 t.Fatal(err)
46 }
47}
48
49// readJSON evaluates a JS expression returning a JSON string into v.
50func readJSON(t *testing.T, ctx context.Context, js string, v any) {
51 t.Helper()
52 var raw string
53 if err := chromedp.Run(ctx, chromedp.Evaluate(js, &raw)); err != nil {
54 t.Fatalf("evaluating: %v\n%s", err, js)
55 }
56 if err := json.Unmarshal([]byte(raw), v); err != nil {
57 t.Fatalf("decoding %q: %v", raw, err)
58 }
59}
60
61// schemeMarkJS reports the scheme menu's buttons and which is marked.
62const schemeMarkJS = `JSON.stringify([...document.querySelectorAll("[rst-scheme] button")].map(b => ({
63 V: b.form && b.form.elements.scheme ? b.form.elements.scheme.value : "",
64 Cur: b.getAttribute("aria-current") || ""
65})))`
66
67// schemeOf reads the root's scheme attribute and computed color-scheme.
68const schemeOf = `JSON.stringify({Theme: document.documentElement.getAttribute("data-theme") || "", Scheme: getComputedStyle(document.documentElement).colorScheme})`
69
70// TestADemoComesBackFromTheCacheInTheStoredScheme: a demo opened in a
71// tab of its own, left for the gallery, whose stored scheme changes
72// while it is away, comes back from the back/forward cache in the new
73// scheme with its menu marked: Dark to Light, then Light to System, on
74// the scheme menu's document; and Dark to Light on the sidebar demo,
75// left by a click into its page. The control first, every time: the
76// return really was a cache restore (pageshow.persisted, and the same
77// navigation start as before), since a document reloaded instead would
78// pass on start-up code alone and prove nothing about the pageshow
79// handler. Back is history.back() from the page itself, polled for:
80// chromedp's NavigateBack waits for a load a restored page never fires,
81// and its node waits never resolve after a history navigation
82// (check/menus_browser_test.go).
83//
84// Full Chromium only, which is why it is in this package: the headless
85// shell never restores a page from the cache (hack/gotest.sh's
86// FULL_CHROMIUM note), and on it this fails naming the engine's reason
87// rather than passing on a reload.
88func TestADemoComesBackFromTheCacheInTheStoredScheme(t *testing.T) {
89 rig := harness.New(t, func(string) http.Handler { return treeHandler(t) })
90 ctx, cancel := context.WithTimeout(rig.Context(), 3*time.Minute)
91 defer cancel()
92 theme := fiesta.ThemeNames()[0]
93 index := indexHref(theme, "en")
94 menu := frameDoc(theme, "en", "scheme-menu", defaultState(t, "scheme-menu"))
95 sidebar := frameDoc(theme, "en", "layout-sidebar", defaultState(t, "layout-sidebar"))
96 page := frameDoc(theme, "en", "layout-sidebar-page", defaultState(t, "layout-sidebar-page"))
97
98 run(t, ctx,
99 addInit(`addEventListener("pageshow", function (e) { window.__restored = e.persisted; });`),
100 chromedp.EmulateViewport(1280, 900),
101 chromedp.Navigate(rig.Origin+index),
102 chromedp.WaitReady("body"),
103 chromedp.Evaluate(`localStorage.setItem("fiesta-scheme", "dark"); "ok"`, new(string)),
104 )
105
106 readRoot := func(where, wantTheme, wantScheme string) {
107 t.Helper()
108 until(t, ctx, where, `document.readyState === "complete"`)
109 var got struct{ Theme, Scheme string }
110 readJSON(t, ctx, schemeOf, &got)
111 if got.Theme != wantTheme {
112 t.Errorf("%s: <html> carries data-theme=%q, want %q", where, got.Theme, wantTheme)
113 }
114 if got.Scheme != wantScheme {
115 t.Errorf("%s: color-scheme computes %q, want %q", where, got.Scheme, wantScheme)
116 }
117 }
118 readMark := func(where, scheme string) {
119 t.Helper()
120 var buttons []struct{ V, Cur string }
121 readJSON(t, ctx, schemeMarkJS, &buttons)
122 if len(buttons) == 0 {
123 t.Fatalf("%s: no scheme menu button; the mark would be read off nothing", where)
124 }
125 for _, b := range buttons {
126 if (b.V == scheme) != (b.Cur == "true") {
127 t.Errorf("%s: the %s button is aria-current=%q; %s alone is marked", where, b.V, b.Cur, scheme)
128 }
129 }
130 }
131 // away leaves the document (leave is the statement that does), waits
132 // to be at awayAt, changes the stored scheme there (store), goes
133 // back, and requires the return to be a cache restore of the
134 // document whose path ends in back.
135 away := func(where, leave, awayAt, store, back string) {
136 t.Helper()
137 var started float64
138 run(t, ctx, chromedp.Evaluate(`performance.getEntriesByType("navigation")[0].startTime + performance.timeOrigin`, &started))
139 run(t, ctx, chromedp.Evaluate(`window.__restored = undefined; `+leave+`; true`, nil))
140 until(t, ctx, where+", away", `location.pathname.endsWith("`+awayAt+`") && document.readyState === "complete"`)
141 run(t, ctx, chromedp.Evaluate(store+`; history.back(); true`, nil))
142 until(t, ctx, where+", back", `location.pathname.endsWith("`+back+`") && window.__restored !== undefined`)
143 var raw string
144 run(t, ctx, chromedp.Evaluate(`JSON.stringify({Restored: window.__restored === true,
145 Started: performance.getEntriesByType("navigation")[0].startTime + performance.timeOrigin,
146 Why: JSON.stringify((performance.getEntriesByType("navigation")[0] || {}).notRestoredReasons || null)})`, &raw))
147 var r struct {
148 Restored bool
149 Started float64
150 Why string
151 }
152 if err := json.Unmarshal([]byte(raw), &r); err != nil {
153 t.Fatalf("%s: decoding %q: %v", where, raw, err)
154 }
155 if !r.Restored || r.Started != started {
156 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",
157 where, r.Restored, strconv.FormatFloat(started, 'f', 0, 64), strconv.FormatFloat(r.Started, 'f', 0, 64), r.Why)
158 }
159 }
160
161 // The scheme menu's document: Dark, then Light on the way back, then
162 // System on the way back.
163 run(t, ctx, chromedp.Navigate(rig.Origin+mountPrefix+menu), chromedp.WaitReady("body"))
164 readRoot("the menu document", "dark", "dark")
165 readMark("the menu document", "dark")
166 toIndex := `location.assign("` + index + `")`
167 menuPath := "/" + menu
168 away("Dark to Light", toIndex, "/index.html", `localStorage.setItem("fiesta-scheme", "light")`, menuPath)
169 readRoot("the menu document back from the cache with Light stored", "light", "light")
170 readMark("the menu document back from the cache with Light stored", "light")
171 away("Light to System", toIndex, "/index.html", `localStorage.removeItem("fiesta-scheme")`, menuPath)
172 readRoot("the menu document back from the cache with nothing stored", "", "light dark")
173 readMark("the menu document back from the cache with nothing stored", "system")
174
175 // The shell demo: Dark, into its page by the nav, Light on the way back.
176 run(t, ctx,
177 chromedp.Evaluate(`localStorage.setItem("fiesta-scheme", "dark"); "ok"`, new(string)),
178 chromedp.Navigate(rig.Origin+mountPrefix+sidebar),
179 chromedp.WaitReady("body"),
180 )
181 readRoot("the sidebar demo", "dark", "dark")
182 away("the sidebar demo, Dark to Light", `document.querySelector("[rst-shell-nav] a").click()`, "/"+page, `localStorage.setItem("fiesta-scheme", "light")`, "/"+sidebar)
183 readRoot("the sidebar demo back from the cache with Light stored", "light", "light")
184
185 // A fresh navigation before the screen: after a history
186 // navigation chromedp's node waits never resolve, so the flush of
187 // the page's problems is taken on a document loaded anew.
188 run(t, ctx, chromedp.Navigate(rig.Origin+mountPrefix+menu), chromedp.WaitReady("body"))
189 rig.Screen("body", "the scheme menu's document in Light")
190}
191