| 1 | //go:build browser |
| 2 | |
| 3 | package gallery |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "fmt" |
| 9 | "net/http" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/chromedp/chromedp" |
| 14 | |
| 15 | "amadan.net/rastrillo/fiesta" |
| 16 | "amadan.net/rastrillo/fiesta/internal/harness" |
| 17 | ) |
| 18 | |
| 19 | // The shell demos are the layout examples' frame documents opened full |
| 20 | // width, and they are the shell as an app gets it. Rastrillo's two |
| 21 | // drives on its shell demos, ported onto the layout documents. |
| 22 | |
| 23 | // layoutDemos is every layout document's path in the tree, in day and |
| 24 | // English: the shell demos. |
| 25 | func layoutDemos(t *testing.T) []string { |
| 26 | t.Helper() |
| 27 | var out []string |
| 28 | for _, e := range manifest(t).Examples { |
| 29 | if e.Document { |
| 30 | out = append(out, frameFile(fiesta.ThemeNames()[0], "en", e.Name, e.Default)) |
| 31 | } |
| 32 | } |
| 33 | if len(out) == 0 { |
| 34 | t.Fatal("the manifest has no layout example; the drives below would read nothing") |
| 35 | } |
| 36 | return out |
| 37 | } |
| 38 | |
| 39 | // mouseRig is a browser with a mouse, serving the tree. |
| 40 | func mouseRig(t *testing.T) *harness.Rig { |
| 41 | t.Helper() |
| 42 | return harness.New(t, func(string) http.Handler { return treeHandler(t) }) |
| 43 | } |
| 44 | |
| 45 | // TestTheWayBackFromAShellDemoIsATarget: the way back from a shell demo |
| 46 | // is a link in the shell's foot, and it is a target like any other: at |
| 47 | // least --rst-target tall, 32px with a mouse and 44px on a phone. Read |
| 48 | // on every shell demo that has a foot, with a mouse at 1280px and a |
| 49 | // coarse pointer at 390px; a demo whose foot link is not on screen is |
| 50 | // counted, so a selector that stopped matching cannot pass by measuring |
| 51 | // nothing. The link is found by where it goes, the Overview, so a foot |
| 52 | // link left dead at "#" measures nothing and fails. |
| 53 | func TestTheWayBackFromAShellDemoIsATarget(t *testing.T) { |
| 54 | const read = `(() => [...document.querySelectorAll("[rst-shell-foot] a, [rst-stage-foot] a")] |
| 55 | .filter(a => a.getAttribute("href").endsWith("/index.html") && a.checkVisibility()) |
| 56 | .map(a => ({H: a.getBoundingClientRect().height, |
| 57 | Floor: parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--rst-target")) * parseFloat(getComputedStyle(document.documentElement).fontSize)})))()` |
| 58 | for _, c := range []struct { |
| 59 | name string |
| 60 | rig func(*testing.T) *harness.Rig |
| 61 | w, h int64 |
| 62 | min float64 |
| 63 | }{ |
| 64 | {"a mouse at 1280px", mouseRig, 1280, 800, 32}, |
| 65 | {"a coarse pointer at 390px", func(t *testing.T) *harness.Rig { return phoneRig(t) }, 390, 844, 44}, |
| 66 | } { |
| 67 | rig := c.rig(t) |
| 68 | ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second) |
| 69 | measured := 0 |
| 70 | for _, demo := range layoutDemos(t) { |
| 71 | where := fmt.Sprintf("%s with %s", demo, c.name) |
| 72 | var raw string |
| 73 | if err := chromedp.Run(ctx, chromedp.EmulateViewport(c.w, c.h), |
| 74 | chromedp.Navigate(rig.Origin+href(demo)), |
| 75 | chromedp.WaitReady("body"), |
| 76 | chromedp.Evaluate(`JSON.stringify(`+read+`)`, &raw)); err != nil { |
| 77 | cancel() |
| 78 | t.Fatalf("%s: %v", where, err) |
| 79 | } |
| 80 | var links []struct{ H, Floor float64 } |
| 81 | if err := json.Unmarshal([]byte(raw), &links); err != nil { |
| 82 | cancel() |
| 83 | t.Fatalf("%s: decoding %s: %v", where, raw, err) |
| 84 | } |
| 85 | for _, l := range links { |
| 86 | measured++ |
| 87 | if l.Floor < c.min { |
| 88 | t.Errorf("%s: --rst-target is %.0fpx, want at least %.0f", where, l.Floor, c.min) |
| 89 | } |
| 90 | if l.H < l.Floor-0.5 { |
| 91 | t.Errorf("%s: the way back is %.1fpx tall, under the %.0fpx target", where, l.H, l.Floor) |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | cancel() |
| 96 | if measured == 0 { |
| 97 | t.Errorf("with %s no shell demo showed its way back, so this measured nothing", c.name) |
| 98 | } |
| 99 | t.Logf("%s: %d foot links measured", c.name, measured) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | // TestAShellDemoLaysOutAsAnAppWould: a shell demo is the shell as an |
| 104 | // app gets it, so nothing of the gallery's own styling may move it. |
| 105 | // Rastrillo's demos once linked gallery.css and its bar layout took |
| 106 | // hold of the sidebar demo. Fiesta's frame documents link no gallery |
| 107 | // stylesheet; what they do carry of the gallery's is the frame style |
| 108 | // in their head, whose rules are meant to leave a shell exactly as it |
| 109 | // is. Every element's box is read as served and again with every |
| 110 | // gallery stylesheet and the frame style switched off; the two must |
| 111 | // agree, on every layout document, the column shell included, whose |
| 112 | // body has no wrapper for a markup rule to name and is filled by the |
| 113 | // document mark instead. At 1280 and 900 with a mouse, either side of |
| 114 | // the gallery bar's two-row band, and at 390 with a coarse pointer. |
| 115 | // |
| 116 | // Red first on the column shell, whose body sat in 16px of padding |
| 117 | // before the document mark. Its mutation, run and reverted: dropping |
| 118 | // "> [rst-shell-sidebar]" from the fill rule's :has() list, which gave |
| 119 | // the sidebar demo 1rem of padding and moved every box in it once the |
| 120 | // style was switched off, reported here per element. |
| 121 | func TestAShellDemoLaysOutAsAnAppWould(t *testing.T) { |
| 122 | const read = `(() => { |
| 123 | const boxes = () => [...document.querySelectorAll("body *")].map(e => { |
| 124 | const b = e.getBoundingClientRect(); |
| 125 | return (e.getAttribute("id") || e.tagName.toLowerCase()) + " " + [b.left, b.top, b.width, b.height].map(n => Math.round(n)).join(","); |
| 126 | }); |
| 127 | const served = boxes(); |
| 128 | const gallery = [...document.styleSheets].filter(s => (s.href && /gallery[^/]*\.css/.test(s.href)) || (!s.href && s.ownerNode && s.ownerNode.tagName === "STYLE")); |
| 129 | gallery.forEach(s => { s.disabled = true; }); |
| 130 | const bare = boxes(); |
| 131 | gallery.forEach(s => { s.disabled = false; }); |
| 132 | const diff = []; |
| 133 | served.forEach((s, i) => { if (s !== bare[i]) diff.push(s + " (without the gallery's styling: " + bare[i] + ")"); }); |
| 134 | return JSON.stringify({Main: !!document.querySelector("main"), Sheets: gallery.length, Diff: diff.slice(0, 5)}); |
| 135 | })()` |
| 136 | for _, c := range []struct { |
| 137 | name string |
| 138 | rig func(*testing.T) *harness.Rig |
| 139 | w, h int64 |
| 140 | }{ |
| 141 | {"a mouse at 1280px", mouseRig, 1280, 800}, |
| 142 | {"a mouse at 900px", mouseRig, 900, 800}, |
| 143 | {"a coarse pointer at 390px", func(t *testing.T) *harness.Rig { return phoneRig(t) }, 390, 844}, |
| 144 | } { |
| 145 | rig := c.rig(t) |
| 146 | ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second) |
| 147 | for _, demo := range layoutDemos(t) { |
| 148 | where := fmt.Sprintf("%s with %s", demo, c.name) |
| 149 | var raw string |
| 150 | if err := chromedp.Run(ctx, chromedp.EmulateViewport(c.w, c.h), |
| 151 | chromedp.Navigate(rig.Origin+href(demo)), |
| 152 | chromedp.WaitReady("body"), |
| 153 | chromedp.Evaluate(read, &raw)); err != nil { |
| 154 | cancel() |
| 155 | t.Fatalf("%s: %v", where, err) |
| 156 | } |
| 157 | var got struct { |
| 158 | Main bool |
| 159 | Sheets int |
| 160 | Diff []string |
| 161 | } |
| 162 | if err := json.Unmarshal([]byte(raw), &got); err != nil { |
| 163 | cancel() |
| 164 | t.Fatalf("%s: decoding %s: %v", where, raw, err) |
| 165 | } |
| 166 | if !got.Main { |
| 167 | t.Errorf("%s: no main element; the demo is not the page this test knows", where) |
| 168 | } |
| 169 | if got.Sheets == 0 { |
| 170 | t.Errorf("%s: no gallery styling was switched off, so the two readings are the same reading", where) |
| 171 | } |
| 172 | for _, d := range got.Diff { |
| 173 | t.Errorf("%s: the gallery's styling moves %s", where, d) |
| 174 | } |
| 175 | } |
| 176 | cancel() |
| 177 | t.Logf("%s: %d demos read with the frame style switched off", c.name, len(layoutDemos(t))) |
| 178 | } |
| 179 | } |
| 180 | |