| 1 | //go:build browser |
| 2 | |
| 3 | package gallery |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "math" |
| 9 | "net/http" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/chromedp/chromedp" |
| 14 | |
| 15 | "amadan.net/rastrillo/fiesta/internal/harness" |
| 16 | ) |
| 17 | |
| 18 | // realSize reads every showing frame's transform, its laid-out and |
| 19 | // rendered widths, a text field's computed font size where the |
| 20 | // document has one, and the box's inline edges against the document's. |
| 21 | // The field is the first plain one: the headline field is sized on |
| 22 | // purpose at --rst-fs-lg. |
| 23 | const realSize = `JSON.stringify([...document.querySelectorAll(".ds-view")].filter(v => getComputedStyle(v.querySelector(".ds-view__stage")).display !== "none").map(v => { |
| 24 | const box = v.querySelector(".ds-view__box"), f = v.querySelector(".ds-view__frame"), b = box.getBoundingClientRect(), fr = f.getBoundingClientRect(); |
| 25 | const lit = [...v.querySelectorAll(".ds-view__tab")].findIndex(l => getComputedStyle(l).fontWeight === "600"); |
| 26 | let field = null; |
| 27 | try { const i = f.contentDocument.querySelector(':is(input:not([type]), input[type=text], input[type=email], input[type=url]):not(.rst-input--primary, [rst-input~="primary"])'); if (i) field = parseFloat(f.contentDocument.defaultView.getComputedStyle(i).fontSize); } catch (e) {} |
| 28 | return {ID: (v.closest("article, section") || {}).id || "", Lit: lit, Transform: getComputedStyle(f).transform, |
| 29 | Layout: f.offsetWidth, Rendered: fr.width, Field: field, BoxL: b.left, BoxR: b.right, DocW: document.documentElement.clientWidth}; |
| 30 | }))` |
| 31 | |
| 32 | type realReading struct { |
| 33 | ID string |
| 34 | Lit int |
| 35 | Transform string |
| 36 | Layout, Rendered, BoxL, BoxR, DocW float64 |
| 37 | Field *float64 |
| 38 | } |
| 39 | |
| 40 | func readReal(t *testing.T, ctx context.Context, where string) []realReading { |
| 41 | t.Helper() |
| 42 | var rs []realReading |
| 43 | at(t, ctx, realSize, &rs) |
| 44 | if len(rs) == 0 { |
| 45 | t.Fatalf("%s: no frame showing", where) |
| 46 | } |
| 47 | return rs |
| 48 | } |
| 49 | |
| 50 | // TestMobileIsThePageAtItsRealSize: on a phone, Mobile is the page at |
| 51 | // its real size: the frame is laid out at the stage's width up to |
| 52 | // 390px and never scaled, a 16px field is 16px, and the box reaches |
| 53 | // the screen's edges. On a coarse pointer at 390px and 320px and a |
| 54 | // fine one at 390px, in English and Arabic, on a component page and a |
| 55 | // page-class one. The control: on a desktop the Mobile rendering is the |
| 56 | // 390px phone, centred in its stage. |
| 57 | func TestMobileIsThePageAtItsRealSize(t *testing.T) { |
| 58 | coarse := harness.New(t, func(string) http.Handler { return treeHandler(t) }, harness.WithCoarsePointer(), harness.WithScrollbars()) |
| 59 | cctx, ccancel := context.WithTimeout(coarse.Context(), 240*time.Second) |
| 60 | defer ccancel() |
| 61 | fine := harness.New(t, func(string) http.Handler { return treeHandler(t) }, harness.WithScrollbars()) |
| 62 | fctx, fcancel := context.WithTimeout(fine.Context(), 240*time.Second) |
| 63 | defer fcancel() |
| 64 | for _, rig := range []struct { |
| 65 | name string |
| 66 | ctx context.Context |
| 67 | rig *harness.Rig |
| 68 | widths []int64 |
| 69 | }{{"coarse", cctx, coarse, []int64{390, 320}}, {"fine", fctx, fine, []int64{390}}} { |
| 70 | for _, file := range []string{"field.html", "layout-column.html"} { |
| 71 | for _, locale := range []string{"en", "ar"} { |
| 72 | for _, w := range rig.widths { |
| 73 | where := fmt.Sprintf("day/%s %s at %dpx, %s pointer", locale, file, w, rig.name) |
| 74 | openPage(t, rig.ctx, where, pageURL(rig.rig, locale, file), w, 844) |
| 75 | if rig.name == "coarse" { |
| 76 | requireCoarse(t, rig.ctx) |
| 77 | } |
| 78 | eagerly(t, rig.ctx, where) |
| 79 | requireDrawnScrollbar(t, rig.ctx, where) |
| 80 | var sideways float64 |
| 81 | mustRun(t, rig.ctx, chromedp.Evaluate(`document.documentElement.scrollWidth - document.documentElement.clientWidth`, &sideways)) |
| 82 | if sideways > 0.5 { |
| 83 | t.Errorf("%s: the page scrolls %.1fpx sideways", where, sideways) |
| 84 | } |
| 85 | fields := 0 |
| 86 | for _, r := range readReal(t, rig.ctx, where) { |
| 87 | if r.Lit != 1 { |
| 88 | t.Fatalf("%s: %s opens with tab %d lit; on a phone the widget opens on Mobile, and the readings below assume it", where, r.ID, r.Lit) |
| 89 | } |
| 90 | if r.Transform != "matrix(1, 0, 0, 1, 0, 0)" && r.Transform != "none" { |
| 91 | t.Errorf("%s: %s is transformed %s; Mobile is never scaled", where, r.ID, r.Transform) |
| 92 | } |
| 93 | if math.Abs(r.Layout-r.Rendered) > 0.5 || r.Layout > 390 { |
| 94 | t.Errorf("%s: %s lays out at %.1fpx and renders at %.1fpx; want one width, at most 390px", where, r.ID, r.Layout, r.Rendered) |
| 95 | } |
| 96 | if math.Abs(r.BoxL) > 1 || math.Abs(r.BoxR-r.DocW) > 1 { |
| 97 | t.Errorf("%s: %s's box runs %.1f…%.1f in a %.0fpx document; on a phone the stage is the screen's width", where, r.ID, r.BoxL, r.BoxR, r.DocW) |
| 98 | } |
| 99 | if r.Field != nil { |
| 100 | fields++ |
| 101 | if rendered := *r.Field * r.Rendered / r.Layout; math.Abs(*r.Field-16) > 0.1 || math.Abs(rendered-16) > 0.1 { |
| 102 | t.Errorf("%s: %s's text field computes %.2fpx and renders %.2fpx; a phone's field is 16px", where, r.ID, *r.Field, rendered) |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | if file == "field.html" && fields == 0 { |
| 107 | t.Errorf("%s: no text field measured on the field page; the 16px claim was checked against nothing", where) |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | where := "day/en field.html at 1280px, Mobile chosen" |
| 114 | openPage(t, fctx, where, pageURL(fine, "en", "field.html"), 1280, 900) |
| 115 | eagerly(t, fctx, where) |
| 116 | clickAll(t, fctx, where, clickedMobile, "Mobile") |
| 117 | const want = 3 |
| 118 | shown := readReal(t, fctx, where) |
| 119 | if len(shown) < want { |
| 120 | t.Fatalf("%s: %d frames showing, want at least %d to check", where, len(shown), want) |
| 121 | } |
| 122 | for _, r := range shown[:want] { |
| 123 | if r.Layout != 390 { |
| 124 | t.Errorf("%s: %s lays out at %.1fpx, want 390", where, r.ID, r.Layout) |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | // TestDesktopPansFromTheInlineStart: Desktop on a phone pans, and from |
| 130 | // the page's start in either direction: in Arabic the frame is |
| 131 | // anchored at the box's right edge and the box scrolls left. |
| 132 | func TestDesktopPansFromTheInlineStart(t *testing.T) { |
| 133 | rig := harness.New(t, func(string) http.Handler { return treeHandler(t) }) |
| 134 | ctx, cancel := context.WithTimeout(rig.Context(), 180*time.Second) |
| 135 | defer cancel() |
| 136 | for _, c := range []struct { |
| 137 | file string |
| 138 | width float64 |
| 139 | }{{"field.html", 900}, {"layout-column.html", 1200}} { |
| 140 | for _, locale := range []string{"ar", "en"} { |
| 141 | where := fmt.Sprintf("day/%s %s at 390px, Desktop chosen", locale, c.file) |
| 142 | openPage(t, ctx, where, pageURL(rig, locale, c.file), 390, 844) |
| 143 | eagerly(t, ctx, where) |
| 144 | clickAll(t, ctx, where, clickedDesktop, "Desktop") |
| 145 | var rs []struct { |
| 146 | ID string |
| 147 | Checked bool |
| 148 | Layout float64 |
| 149 | Pans bool |
| 150 | Start, End float64 |
| 151 | } |
| 152 | at(t, ctx, `(() => { |
| 153 | const rtl = document.documentElement.dir === "rtl", out = []; |
| 154 | for (const v of document.querySelectorAll(".ds-view")) { |
| 155 | const box = v.querySelector(".ds-view__box"), f = v.querySelector(".ds-view__frame"); |
| 156 | const checked = v.querySelector(".ds-view__tab--d input").checked; |
| 157 | const startEdge = () => { const b = box.getBoundingClientRect(), r = f.getBoundingClientRect(); return rtl ? r.right - b.right : r.left - b.left; }; |
| 158 | const endEdge = () => { const b = box.getBoundingClientRect(), r = f.getBoundingClientRect(); return rtl ? b.left - r.left : r.right - b.right; }; |
| 159 | const atStart = startEdge(); |
| 160 | box.scrollLeft = rtl ? -(box.scrollWidth - box.clientWidth) : box.scrollWidth - box.clientWidth; |
| 161 | const atEnd = endEdge(); |
| 162 | box.scrollLeft = 0; |
| 163 | out.push({ID: (v.closest("article, section") || {}).id || "", Checked: checked, Layout: f.offsetWidth, |
| 164 | Pans: box.scrollWidth > box.clientWidth, Start: atStart, End: atEnd}); |
| 165 | } |
| 166 | return JSON.stringify(out); |
| 167 | })()`, &rs) |
| 168 | for _, r := range rs { |
| 169 | if !r.Checked || r.Layout != c.width { |
| 170 | t.Fatalf("%s: %s has Desktop checked %v and lays out at %.0fpx, want %.0fpx", where, r.ID, r.Checked, r.Layout, c.width) |
| 171 | } |
| 172 | } |
| 173 | for _, r := range rs { |
| 174 | if !r.Pans { |
| 175 | t.Errorf("%s: %s's box does not scroll", where, r.ID) |
| 176 | } |
| 177 | if math.Abs(r.Start) > 1 { |
| 178 | t.Errorf("%s: at rest %s's frame starts %.1fpx from its box's inline start", where, r.ID, r.Start) |
| 179 | } |
| 180 | if math.Abs(r.End) > 1 { |
| 181 | t.Errorf("%s: scrolled to the end, %s's frame ends %.1fpx from its box's inline end", where, r.ID, r.End) |
| 182 | } |
| 183 | } |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | |