| 1 | //go:build browser |
| 2 | |
| 3 | package check |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "math" |
| 9 | "regexp" |
| 10 | "strconv" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/chromedp/cdproto/input" |
| 16 | "github.com/chromedp/chromedp" |
| 17 | |
| 18 | "amadan.net/rastrillo/fiesta" |
| 19 | "amadan.net/rastrillo/fiesta/internal/harness" |
| 20 | ) |
| 21 | |
| 22 | // layers splits a computed background-image into its top-level layers. |
| 23 | func layers(v string) []string { |
| 24 | var out []string |
| 25 | depth, start := 0, 0 |
| 26 | for i, r := range v { |
| 27 | switch r { |
| 28 | case '(': |
| 29 | depth++ |
| 30 | case ')': |
| 31 | depth-- |
| 32 | case ',': |
| 33 | if depth == 0 { |
| 34 | out = append(out, strings.TrimSpace(v[start:i])) |
| 35 | start = i + 1 |
| 36 | } |
| 37 | } |
| 38 | } |
| 39 | return append(out, strings.TrimSpace(v[start:])) |
| 40 | } |
| 41 | |
| 42 | func isGrey(rgb string) bool { |
| 43 | var r, g, b float64 |
| 44 | if _, err := fmt.Sscanf(rgb, "rgb(%f, %f, %f)", &r, &g, &b); err != nil { |
| 45 | return false |
| 46 | } |
| 47 | return math.Abs(r-g) < 8 && math.Abs(g-b) < 8 && math.Abs(r-b) < 8 |
| 48 | } |
| 49 | |
| 50 | // TestEveryButtonVariantHasItsOwnHoverAndPress is the C11 table read back |
| 51 | // from the browser, frozen first (C14): rest, hover and press differ for |
| 52 | // every variant in both spellings; a filled hover keeps its fill and |
| 53 | // never turns grey (the mockup regression); the sheen is on only while |
| 54 | // hovered and not pressed; nothing hovers or presses while disabled or |
| 55 | // busy; the class and attribute spellings compute the same. |
| 56 | func TestEveryButtonVariantHasItsOwnHoverAndPress(t *testing.T) { |
| 57 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 58 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 59 | defer cancel() |
| 60 | open(t, ctx, rig, "/", false) |
| 61 | still(t, ctx) |
| 62 | type face struct{ bg, img, border, color, shadow string } |
| 63 | read := func(id string) face { |
| 64 | return face{computed(t, ctx, id, "background-color"), computed(t, ctx, id, "background-image"), computed(t, ctx, id, "border-top-color"), computed(t, ctx, id, "color"), computed(t, ctx, id, "box-shadow")} |
| 65 | } |
| 66 | for _, id := range []string{"#b-plain", "#b-primary", "#b-danger", "#b-ghost", "#b-link"} { |
| 67 | rest := read(id) |
| 68 | forceState(t, ctx, id, "hover") |
| 69 | hover := read(id) |
| 70 | forceState(t, ctx, id, "hover", "active") |
| 71 | press := read(id) |
| 72 | forceState(t, ctx, id) |
| 73 | if rest == hover { |
| 74 | t.Errorf("%s: hover is the rest face %+v", id, rest) |
| 75 | } |
| 76 | if hover == press { |
| 77 | t.Errorf("%s: press is the hover face %+v; a pressed face replaces the hovered face (C11)", id, hover) |
| 78 | } |
| 79 | if id == "#b-primary" || id == "#b-danger" || id == "#b-link" { |
| 80 | if hover.bg == rest.bg { |
| 81 | t.Errorf("%s: hover keeps the rest fill %s; every state reads its own token", id, rest.bg) |
| 82 | } |
| 83 | if isGrey(hover.bg) { |
| 84 | t.Errorf("%s: hovered fill %s is grey; a filled button's hover keeps its fill", id, hover.bg) |
| 85 | } |
| 86 | if !strings.Contains(hover.img, "radial-gradient(120px") { |
| 87 | t.Errorf("%s: hover has no 120px sheen: %q", id, hover.img) |
| 88 | } |
| 89 | } |
| 90 | if id == "#b-plain" && !strings.Contains(hover.img, "radial-gradient(90px") { |
| 91 | t.Errorf("#b-plain: hover has no 90px sheen: %q", hover.img) |
| 92 | } |
| 93 | if strings.Contains(press.img, "radial-gradient(90px") || strings.Contains(press.img, "radial-gradient(120px") { |
| 94 | t.Errorf("%s: the sheen is still on while pressed: %q", id, press.img) |
| 95 | } |
| 96 | } |
| 97 | for _, pair := range [][2]string{{"#b-plain", "#b-plain-c"}, {"#b-primary", "#b-primary-c"}} { |
| 98 | for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}} { |
| 99 | forceState(t, ctx, pair[0], states...) |
| 100 | forceState(t, ctx, pair[1], states...) |
| 101 | if a, b := read(pair[0]), read(pair[1]); a != b { |
| 102 | t.Errorf("%s and %s differ in state %v: %+v vs %+v; both spellings are one rule (C12)", pair[0], pair[1], states, a, b) |
| 103 | } |
| 104 | forceState(t, ctx, pair[0]) |
| 105 | forceState(t, ctx, pair[1]) |
| 106 | } |
| 107 | } |
| 108 | // A disabled or busy button hovers like any other (today's |
| 109 | // behaviour in every theme) and never presses: the press rule |
| 110 | // excludes it, and a browser never makes a disabled button :active. |
| 111 | // The sheen is not compared: it is keyed on :not(:active), which a |
| 112 | // forced state can set on a disabled button where a pointer cannot. |
| 113 | for _, id := range []string{"#b-off", "#b-busy"} { |
| 114 | forceState(t, ctx, id, "hover") |
| 115 | hover := read(id) |
| 116 | forceState(t, ctx, id, "hover", "active") |
| 117 | got := read(id) |
| 118 | forceState(t, ctx, id) |
| 119 | hover.img, got.img = "", "" |
| 120 | if got != hover { |
| 121 | t.Errorf("%s pressed while hovered: %+v -> %+v; a disabled or busy button has no press", id, hover, got) |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | // cssColour spells a theme token the way Chromium serialises a computed |
| 127 | // colour: a hex as rgb(r, g, b), an rgba() with its alpha as the |
| 128 | // shortest decimal, transparent as rgba(0, 0, 0, 0). channelsOf and |
| 129 | // alphaOf are contrast_face_test.go's. |
| 130 | func cssColour(t *testing.T, v string) string { |
| 131 | t.Helper() |
| 132 | v = strings.TrimSpace(v) |
| 133 | if v == "transparent" { |
| 134 | return "rgba(0, 0, 0, 0)" |
| 135 | } |
| 136 | c, a := channelsOf(t, v), alphaOf(t, v) |
| 137 | if a >= 1 { |
| 138 | return fmt.Sprintf("rgb(%d, %d, %d)", int(c[0]), int(c[1]), int(c[2])) |
| 139 | } |
| 140 | return fmt.Sprintf("rgba(%d, %d, %d, %s)", int(c[0]), int(c[1]), int(c[2]), strconv.FormatFloat(a, 'f', -1, 64)) |
| 141 | } |
| 142 | |
| 143 | // expectedStack is the four-layer background-image the C11 table says |
| 144 | // a button paints, in Chromium's computed serialisation (measured, not |
| 145 | // guessed: a 70px radial at the default position serialises without |
| 146 | // the position, a colour stop without a position as the colour alone, |
| 147 | // calc(1 * 0.16) as 0.16). sheen is "" for none. |
| 148 | func expectedStack(sheen, spotAlpha, washAlpha, face string) string { |
| 149 | if sheen == "" { |
| 150 | sheen = "none" |
| 151 | } |
| 152 | return sheen + ", radial-gradient(70px, rgba(0, 0, 0, " + spotAlpha + "), rgba(0, 0, 0, 0) 75%), linear-gradient(rgba(0, 0, 0, " + washAlpha + ") 0px, rgba(0, 0, 0, " + washAlpha + ") 0px), " + face |
| 153 | } |
| 154 | |
| 155 | func alphaStr(a float64) string { return strconv.FormatFloat(a, 'f', -1, 64) } |
| 156 | |
| 157 | // TestEveryButtonPaintsItsWholeStack: for primary, danger |
| 158 | // and plain, at rest, hovered and pressed, in both schemes, the resolved |
| 159 | // background-image is exactly the four layers the table composes from |
| 160 | // day's tokens, the background-colour is the state's fill and the |
| 161 | // colour its label, so a dropped declaration (a shorter list, a face |
| 162 | // that is not last) or a disconnected token (a colour that is not the |
| 163 | // theme's) is caught in every cell the contrast arithmetic assumes. The |
| 164 | // negative control severs the danger face in dark, hovered, and proves |
| 165 | // the check fails there. |
| 166 | func TestEveryButtonPaintsItsWholeStack(t *testing.T) { |
| 167 | for _, scheme := range []string{"light", "dark"} { |
| 168 | t.Run(scheme, func(t *testing.T) { |
| 169 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 170 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 171 | defer cancel() |
| 172 | open(t, ctx, rig, "/", false) |
| 173 | mustRun(t, ctx, chromedp.Evaluate(`document.documentElement.setAttribute("data-theme", "`+scheme+`"), true`, nil)) |
| 174 | still(t, ctx) |
| 175 | tok := resolvedTokens(t, "day")[scheme] |
| 176 | wash, spot := pressNumber(t, "day", scheme, "--rst-press-wash"), pressNumber(t, "day", scheme, "--rst-press-spot") |
| 177 | washP, spotP := pressNumber(t, "day", scheme, "--rst-press-wash-plain"), pressNumber(t, "day", scheme, "--rst-press-spot-plain") |
| 178 | face := "linear-gradient(" + cssColour(t, "rgba(255, 255, 255, "+tok["--rst-depth-face"]+")") + ", " + cssColour(t, "rgba(0, 0, 0, "+tok["--rst-depth-face"]+")") + ")" |
| 179 | sheenFill := "radial-gradient(120px at 50% -40%, " + cssColour(t, tok["--rst-sheen-fill"]) + ", rgba(0, 0, 0, 0) 70%)" |
| 180 | sheenPlain := "radial-gradient(90px at 50% -40%, " + cssColour(t, tok["--rst-sheen-plain"]) + ", rgba(0, 0, 0, 0) 70%)" |
| 181 | type cell struct { |
| 182 | sel string |
| 183 | states []string |
| 184 | stack, bg, fg string |
| 185 | } |
| 186 | cells := []cell{ |
| 187 | {"#b-primary", nil, expectedStack("", "0", "0", face), cssColour(t, tok["--rst-accent-fill"]), cssColour(t, tok["--rst-on-accent-fill"])}, |
| 188 | {"#b-primary", []string{"hover"}, expectedStack(sheenFill, "0", "0", face), cssColour(t, tok["--rst-accent-fill-hover"]), cssColour(t, tok["--rst-on-accent-fill"])}, |
| 189 | {"#b-primary", []string{"hover", "active"}, expectedStack("", alphaStr(spot), alphaStr(wash), face), cssColour(t, tok["--rst-accent-fill-hover"]), cssColour(t, tok["--rst-on-accent-fill"])}, |
| 190 | {"#b-danger", nil, expectedStack("", "0", "0", face), cssColour(t, tok["--rst-danger-fill"]), cssColour(t, tok["--rst-on-danger-fill"])}, |
| 191 | {"#b-danger", []string{"hover"}, expectedStack(sheenFill, "0", "0", face), cssColour(t, tok["--rst-danger-fill-hover"]), cssColour(t, tok["--rst-on-danger-fill-hover"])}, |
| 192 | {"#b-danger", []string{"hover", "active"}, expectedStack("", alphaStr(spot), alphaStr(wash), face), cssColour(t, tok["--rst-danger-fill-hover"]), cssColour(t, tok["--rst-on-danger-fill-hover"])}, |
| 193 | {"#b-plain", nil, expectedStack("", "0", "0", "none"), cssColour(t, tok["--rst-control-face"]), cssColour(t, tok["--rst-text"])}, |
| 194 | {"#b-plain", []string{"hover"}, expectedStack(sheenPlain, "0", "0", "none"), cssColour(t, tok["--rst-control-face-hover"]), cssColour(t, tok["--rst-control-text-hover"])}, |
| 195 | {"#b-plain", []string{"hover", "active"}, expectedStack("", alphaStr(spotP), alphaStr(washP), "none"), cssColour(t, tok["--rst-control-face-hover"]), cssColour(t, tok["--rst-control-text-hover"])}, |
| 196 | // A ghost hovers with no sheen (the C11 ghost row): the plain |
| 197 | // sheen rule is :hover:not(:active) at (0,3,0), and a ghost |
| 198 | // rule at (0,2,0) once lost to it and painted the accent. |
| 199 | {"#b-ghost", []string{"hover"}, expectedStack("", "0", "0", "none"), cssColour(t, tok["--rst-ghost-face-hover"]), cssColour(t, tok["--rst-text"])}, |
| 200 | } |
| 201 | for _, c := range cells { |
| 202 | forceState(t, ctx, c.sel, c.states...) |
| 203 | got, bg, fg := computed(t, ctx, c.sel, "background-image"), computed(t, ctx, c.sel, "background-color"), computed(t, ctx, c.sel, "color") |
| 204 | forceState(t, ctx, c.sel) |
| 205 | if norm(got) != norm(c.stack) { |
| 206 | t.Errorf("%s %v stack:\n got %s\n want %s", c.sel, c.states, got, c.stack) |
| 207 | } |
| 208 | if bg != c.bg || fg != c.fg { |
| 209 | t.Errorf("%s %v is %s on %s, want %s on %s", c.sel, c.states, fg, bg, c.fg, c.bg) |
| 210 | } |
| 211 | } |
| 212 | if scheme == "dark" { |
| 213 | // The negative control: sever the danger face and a cell the |
| 214 | // loop above accepted must now fail. |
| 215 | mustRun(t, ctx, chromedp.Evaluate(`(() => { const s = document.createElement("style"); s.textContent = "[rst-btn~=danger] { --rst-btn-face: none; }"; document.head.append(s); return true; })()`, nil)) |
| 216 | forceState(t, ctx, "#b-danger", "hover") |
| 217 | cut := computed(t, ctx, "#b-danger", "background-image") |
| 218 | forceState(t, ctx, "#b-danger") |
| 219 | if norm(cut) == norm(expectedStack(sheenFill, "0", "0", face)) { |
| 220 | t.Fatal("severing the danger face left its hovered stack unchanged; this check would not catch a dropped declaration") |
| 221 | } |
| 222 | } |
| 223 | }) |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | // norm collapses whitespace differences in a serialised gradient. |
| 228 | func norm(s string) string { return strings.Join(strings.Fields(s), " ") } |
| 229 | |
| 230 | // TestAPressIsInstantAndSlowToLeave is the press's drive: --rst-press is 1 and |
| 231 | // the button sits 1px lower while held, with the same box; on release |
| 232 | // the fade is a 0.7s transition on the spec's curve, and 250ms into it |
| 233 | // the value is what the curve gives (about 0.223); under reduced motion |
| 234 | // it is 0 at once and the button never moved. A real pointer, not a |
| 235 | // forced state: the fade is a transition, and a forced pseudo-state |
| 236 | // does not run one on removal. Not frozen, for the same reason. The |
| 237 | // transition is read through getAnimations(), paused and scrubbed to |
| 238 | // 250ms, not sampled by the clock: a sleep-then-read had about 110ms |
| 239 | // of slack on a slow runner (0.11 at 350ms, under the old floor of |
| 240 | // 0.1), and the scrub still catches a snap (no transition), a stick, |
| 241 | // and a wrong duration or curve. |
| 242 | func TestAPressIsInstantAndSlowToLeave(t *testing.T) { |
| 243 | for _, reduce := range []bool{false, true} { |
| 244 | t.Run(fmt.Sprintf("reduced motion %v", reduce), func(t *testing.T) { |
| 245 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 246 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 247 | defer cancel() |
| 248 | open(t, ctx, rig, "/", reduce) |
| 249 | x, y, w, h := rect(t, ctx, "#b-primary") |
| 250 | cx, cy := x+w/2, y+h/2 |
| 251 | mustRun(t, ctx, chromedp.MouseEvent(input.MousePressed, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 252 | if got := computed(t, ctx, "#b-primary", "--rst-press"); got != "1" { |
| 253 | t.Fatalf("held: --rst-press is %q, want 1", got) |
| 254 | } |
| 255 | if got := computed(t, ctx, "#b-primary", "translate"); (got == "0px 1px") == reduce { |
| 256 | t.Errorf("held: translate is %q (reduced motion %v)", got, reduce) |
| 257 | } |
| 258 | if _, _, w2, h2 := rect(t, ctx, "#b-primary"); w2 != w || h2 != h { |
| 259 | t.Errorf("the pressed button's box changed from %vx%v to %vx%v; a press must cause no layout shift", w, h, w2, h2) |
| 260 | } |
| 261 | mustRun(t, ctx, chromedp.MouseEvent(input.MouseReleased, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 262 | if reduce { |
| 263 | if got := computed(t, ctx, "#b-primary", "--rst-press"); got != "0" { |
| 264 | t.Errorf("released under reduced motion: --rst-press is %q, want 0 at once", got) |
| 265 | } |
| 266 | return |
| 267 | } |
| 268 | var fade struct { |
| 269 | Found bool |
| 270 | Duration float64 |
| 271 | Easing string |
| 272 | Value string |
| 273 | } |
| 274 | at(t, ctx, `(() => { const b = document.querySelector("#b-primary"); const a = b.getAnimations().find(x => x.transitionProperty === "--rst-press"); if (!a) return JSON.stringify({Found: false}); const tm = a.effect.getTiming(); a.pause(); a.currentTime = 250; return JSON.stringify({Found: true, Duration: tm.duration, Easing: tm.easing, Value: getComputedStyle(b).getPropertyValue("--rst-press")}); })()`, &fade) |
| 275 | if !fade.Found { |
| 276 | t.Fatal("released: no transition is running on --rst-press; the press snapped instead of fading") |
| 277 | } |
| 278 | if fade.Duration != 700 || fade.Easing != "cubic-bezier(0.2, 0.6, 0.3, 1)" { |
| 279 | t.Errorf("the fade runs %vms on %q, want 700ms on cubic-bezier(0.2, 0.6, 0.3, 1)", fade.Duration, fade.Easing) |
| 280 | } |
| 281 | var v float64 |
| 282 | if _, err := fmt.Sscanf(fade.Value, "%f", &v); err != nil { |
| 283 | t.Fatalf("--rst-press at 250ms is %q: %v", fade.Value, err) |
| 284 | } |
| 285 | if math.Abs(v-0.223) > 0.005 { |
| 286 | t.Errorf("250ms into the fade --rst-press is %v, want about 0.223: the curve from 1 to 0 over 0.7s gives that, a stuck or linear fade does not", v) |
| 287 | } |
| 288 | }) |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | // TestAPressInsideTheFadeSnapsBackToOne (review focus 2). |
| 293 | func TestAPressInsideTheFadeSnapsBackToOne(t *testing.T) { |
| 294 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 295 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 296 | defer cancel() |
| 297 | open(t, ctx, rig, "/", false) |
| 298 | x, y, w, h := rect(t, ctx, "#b-plain") |
| 299 | cx, cy := x+w/2, y+h/2 |
| 300 | mustRun(t, ctx, chromedp.MouseEvent(input.MousePressed, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1)), |
| 301 | chromedp.MouseEvent(input.MouseReleased, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1)), |
| 302 | chromedp.Sleep(200*time.Millisecond), |
| 303 | chromedp.MouseEvent(input.MousePressed, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 304 | if got := computed(t, ctx, "#b-plain", "--rst-press"); got != "1" { |
| 305 | t.Errorf("pressed again mid-fade: --rst-press is %q, want 1 at once", got) |
| 306 | } |
| 307 | mustRun(t, ctx, chromedp.MouseEvent(input.MouseReleased, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 308 | } |
| 309 | |
| 310 | // TestAGhostsReleaseShadowEases (#104 item 6): the ghost's press shadow |
| 311 | // rides --rst-press, so on release it eases over the same 0.7s curve |
| 312 | // as the face. Held, the ghost's own :active shadow is a 1px-down, 3px |
| 313 | // inset of the contact colour; 250ms into the release, --rst-press is |
| 314 | // about 0.223 and the shadow is that fraction of it, still inset, one |
| 315 | // layer. A base-rule shadow in its place (the rest list, not inset) is |
| 316 | // what the guarded line prevents, and a snap to nothing is what the |
| 317 | // transition prevents. Scrubbed, not sampled, as the press drive is. |
| 318 | func TestAGhostsReleaseShadowEases(t *testing.T) { |
| 319 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 320 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 321 | defer cancel() |
| 322 | open(t, ctx, rig, "/", false) |
| 323 | x, y, w, h := rect(t, ctx, "#b-ghost") |
| 324 | cx, cy := x+w/2, y+h/2 |
| 325 | mustRun(t, ctx, chromedp.MouseEvent(input.MousePressed, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 326 | held := computed(t, ctx, "#b-ghost", "box-shadow") |
| 327 | if !strings.HasSuffix(held, " 0px 1px 3px 0px inset") || strings.Contains(held, "inset, ") { |
| 328 | t.Fatalf("held: the ghost's shadow is %q, want one inset layer 0px 1px 3px", held) |
| 329 | } |
| 330 | mustRun(t, ctx, chromedp.MouseEvent(input.MouseReleased, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 331 | var fade struct { |
| 332 | Found bool |
| 333 | Press string |
| 334 | Shadow string |
| 335 | } |
| 336 | at(t, ctx, `(() => { const b = document.querySelector("#b-ghost"); const a = b.getAnimations().find(x => x.transitionProperty === "--rst-press"); if (!a) return JSON.stringify({Found: false}); a.pause(); a.currentTime = 250; const cs = getComputedStyle(b); return JSON.stringify({Found: true, Press: cs.getPropertyValue("--rst-press"), Shadow: cs.boxShadow}); })()`, &fade) |
| 337 | if !fade.Found { |
| 338 | t.Fatal("released: no transition is running on --rst-press; the ghost's shadow snapped instead of easing") |
| 339 | } |
| 340 | var press, sy, blur float64 |
| 341 | fmt.Sscanf(fade.Press, "%f", &press) |
| 342 | m := regexp.MustCompile(`\) 0px ([\d.]+)px ([\d.]+)px 0px inset$`).FindStringSubmatch(fade.Shadow) |
| 343 | if m == nil || strings.Contains(fade.Shadow, "inset, ") { |
| 344 | t.Fatalf("250ms into the release the ghost's shadow is %q (--rst-press %s); want one inset layer riding the press", fade.Shadow, fade.Press) |
| 345 | } |
| 346 | fmt.Sscanf(m[1], "%f", &sy) |
| 347 | fmt.Sscanf(m[2], "%f", &blur) |
| 348 | if math.Abs(sy-press) > 0.02 || math.Abs(blur-3*press) > 0.05 { |
| 349 | t.Errorf("250ms into the release the ghost's shadow is 0 %vpx %vpx with --rst-press %v; want the press's 1px and 3px scaled by it", sy, blur, press) |
| 350 | } |
| 351 | if press < 0.1 || press > 0.4 { |
| 352 | t.Errorf("250ms into the release --rst-press is %v, want about 0.223; the shadow was read off the wrong point of the curve", press) |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | // TestTheFaceStillPaintsWithoutARegisteredPress (#104 item 6): the |
| 357 | // button's face and the ghost's shadow read --rst-press through a 0 |
| 358 | // fallback, because an engine without @property has no initial value |
| 359 | // for it, and an unset var() inside a gradient or a calc() invalidates |
| 360 | // the whole declaration: four layers of face gone, shadow gone. The |
| 361 | // drive unregisters the property by serving tokens.css without its |
| 362 | // @property block, then checks every variant still paints its face at |
| 363 | // rest and pressed, and the ghost its shadow while pressed. Reverting |
| 364 | // a fallback on either guarded line turns that declaration to none. |
| 365 | func TestTheFaceStillPaintsWithoutARegisteredPress(t *testing.T) { |
| 366 | const registration = `@property --rst-press { syntax: "<number>"; inherits: false; initial-value: 0; }` |
| 367 | rig := pressFallbackRig(t, func(css string) string { |
| 368 | if strings.Count(css, registration) != 1 { |
| 369 | t.Fatalf("tokens.css no longer registers --rst-press as this test knows it:\n%s", registration) |
| 370 | } |
| 371 | return strings.Replace(css, registration, "/* --rst-press unregistered by the test */", 1) |
| 372 | }, harness.WithHover()) |
| 373 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 374 | defer cancel() |
| 375 | open(t, ctx, rig, "/", false) |
| 376 | // The control: unregistered, the property has no computed value at |
| 377 | // rest. Registered, it reads 0. |
| 378 | if got := computed(t, ctx, "#b-plain", "--rst-press"); got != "" { |
| 379 | t.Fatalf("--rst-press computes to %q at rest; the property is still registered, so the drive would prove nothing", got) |
| 380 | } |
| 381 | for _, id := range []string{"#b-plain", "#b-primary", "#b-danger", "#b-ghost", "#b-link"} { |
| 382 | if got := computed(t, ctx, id, "background-image"); got == "none" || len(layers(got)) != 4 { |
| 383 | t.Errorf("%s at rest: background-image is %q, want four layers; the face is gone without the registered press", id, got) |
| 384 | } |
| 385 | } |
| 386 | x, y, w, h := rect(t, ctx, "#b-ghost") |
| 387 | cx, cy := x+w/2, y+h/2 |
| 388 | mustRun(t, ctx, chromedp.MouseEvent(input.MousePressed, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 389 | if got := computed(t, ctx, "#b-ghost", "box-shadow"); got == "none" || !strings.HasSuffix(got, " 0px 1px 3px 0px inset") { |
| 390 | t.Errorf("the ghost pressed: box-shadow is %q, want its 0px 1px 3px inset; the shadow is gone without the registered press", got) |
| 391 | } |
| 392 | if got := computed(t, ctx, "#b-ghost", "background-image"); got == "none" || len(layers(got)) != 4 { |
| 393 | t.Errorf("the ghost pressed: background-image is %q, want four layers", got) |
| 394 | } |
| 395 | mustRun(t, ctx, chromedp.MouseEvent(input.MouseReleased, cx, cy, chromedp.ButtonLeft, chromedp.ClickCount(1))) |
| 396 | // Released with no registered property there is no transition to |
| 397 | // ease; the shadow goes to its zero-sized rest, and must not go to |
| 398 | // none. |
| 399 | if got := computed(t, ctx, "#b-ghost", "box-shadow"); got == "none" || !strings.HasSuffix(got, " 0px 0px 0px 0px inset") { |
| 400 | t.Errorf("the ghost released: box-shadow is %q, want its zero inset; the shadow is gone without the registered press", got) |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | // TestHoverTintsPaintWithoutRegisteredNumbers (from rastrillo's day |
| 405 | // follow-ups review): the row, tile and rail hover tints read --rst-hover and |
| 406 | // --rst-pressed, and in an engine without @property (Firefox before |
| 407 | // 128) an unset var() invalidates the whole declaration at computed |
| 408 | // value time: the hover rule sets --rst-hover itself, but --rst-pressed |
| 409 | // was set only while pressed, so a plain hover lost its background |
| 410 | // (rows and rails their tint, the tile its surface) and a release lost |
| 411 | // it again. Both registrations are cut from the served tokens.css; the |
| 412 | // hover rule must then set every number it reads, and rest, hover, |
| 413 | // press and release must each paint the registered design's settled |
| 414 | // values. Frozen, so a settled value is what is read. |
| 415 | func TestHoverTintsPaintWithoutRegisteredNumbers(t *testing.T) { |
| 416 | registrations := []string{ |
| 417 | `@property --rst-hover { syntax: "<percentage>"; inherits: false; initial-value: 0%; }`, |
| 418 | `@property --rst-pressed { syntax: "<percentage>"; inherits: false; initial-value: 0%; }`, |
| 419 | } |
| 420 | rig := pressFallbackRig(t, func(css string) string { |
| 421 | for _, r := range registrations { |
| 422 | if strings.Count(css, r) != 1 { |
| 423 | t.Fatalf("tokens.css no longer registers the hover numbers as this test knows them:\n%s", r) |
| 424 | } |
| 425 | css = strings.Replace(css, r, "/* unregistered by the test */", 1) |
| 426 | } |
| 427 | return css |
| 428 | }, harness.WithHover()) |
| 429 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 430 | defer cancel() |
| 431 | open(t, ctx, rig, "/", false) |
| 432 | still(t, ctx) |
| 433 | // The control: unregistered, the numbers have no computed value at |
| 434 | // rest. Registered, they read 0%. |
| 435 | if h, p := computed(t, ctx, "#row-a", "--rst-hover"), computed(t, ctx, "#row-a", "--rst-pressed"); h != "" || p != "" { |
| 436 | t.Fatalf("--rst-hover is %q and --rst-pressed %q at rest; the properties are still registered, so the drive would prove nothing", h, p) |
| 437 | } |
| 438 | const hoverTint, pressTint = "rgb(245, 247, 253)", "rgb(236, 241, 252)" |
| 439 | for _, c := range []struct{ sel, press, rest string }{ |
| 440 | {"#row-a", "#link-a", "rgba(0, 0, 0, 0)"}, |
| 441 | {"#tile", "#tile-link", "rgb(255, 255, 255)"}, |
| 442 | {"#nav-x", "#nav-x", "rgba(0, 0, 0, 0)"}, |
| 443 | } { |
| 444 | read := func() (string, string) { |
| 445 | return computed(t, ctx, c.sel, "background-color"), computed(t, ctx, c.sel, "box-shadow") |
| 446 | } |
| 447 | if bg, _ := read(); bg != c.rest { |
| 448 | t.Errorf("%s at rest is %s, want %s", c.sel, bg, c.rest) |
| 449 | } |
| 450 | forceState(t, ctx, c.sel, "hover") |
| 451 | if bg, sh := read(); bg != hoverTint || !strings.Contains(sh, "inset") { |
| 452 | t.Errorf("%s hovered is %s with box-shadow %q; want the hover tint %s with its inset lift. Without @property an unset number blanks the declaration", c.sel, bg, sh, hoverTint) |
| 453 | } |
| 454 | if c.press == c.sel { |
| 455 | forceState(t, ctx, c.sel, "hover", "active") |
| 456 | } else { |
| 457 | forceState(t, ctx, c.press, "active") |
| 458 | } |
| 459 | if bg, sh := read(); bg != pressTint || sh == "none" { |
| 460 | t.Errorf("%s pressed is %s with box-shadow %q; want the press tint %s with a shadow", c.sel, bg, sh, pressTint) |
| 461 | } |
| 462 | if c.press == c.sel { |
| 463 | forceState(t, ctx, c.sel, "hover") |
| 464 | } else { |
| 465 | forceState(t, ctx, c.press) |
| 466 | } |
| 467 | if bg, sh := read(); bg != hoverTint || !strings.Contains(sh, "inset") { |
| 468 | t.Errorf("%s released, still hovered, is %s with box-shadow %q; want the hover tint %s again", c.sel, bg, sh, hoverTint) |
| 469 | } |
| 470 | forceState(t, ctx, c.sel) |
| 471 | if bg, _ := read(); bg != c.rest { |
| 472 | t.Errorf("%s back at rest is %s, want %s", c.sel, bg, c.rest) |
| 473 | } |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | // TestFocusOutlinesSurviveLiftAndPress (C14, review focus 4): the solid |
| 478 | // 2px outline is computed on a focused button that is also hovered and |
| 479 | // pressed, on a focused link in a hovered row, and on a focused hovered |
| 480 | // nav item. One forceState call per element with every state it needs. |
| 481 | // It passes before the press and the lift exist (the outline is the |
| 482 | // page's today): it is here so no later state can take it away. |
| 483 | func TestFocusOutlinesSurviveLiftAndPress(t *testing.T) { |
| 484 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 485 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 486 | defer cancel() |
| 487 | open(t, ctx, rig, "/", false) |
| 488 | still(t, ctx) |
| 489 | forceState(t, ctx, "#b-primary", "hover", "active", "focus", "focus-visible") |
| 490 | if st, w := computed(t, ctx, "#b-primary", "outline-style"), computed(t, ctx, "#b-primary", "outline-width"); st != "solid" || w != "2px" { |
| 491 | t.Errorf("a focused, hovered, pressed button has outline %s %s, want solid 2px", st, w) |
| 492 | } |
| 493 | forceState(t, ctx, "#b-primary") |
| 494 | forceState(t, ctx, "#row-a", "hover") |
| 495 | forceState(t, ctx, "#link-a", "focus", "focus-visible") |
| 496 | var ring string |
| 497 | mustRun(t, ctx, chromedp.Evaluate(`(() => { const s = getComputedStyle(document.querySelector("#link-a"), "::after"); return s.outlineStyle + " " + s.outlineWidth; })()`, &ring)) |
| 498 | if ring != "solid 2px" { |
| 499 | t.Errorf("a focused link in a hovered row draws %q on its overlay, want solid 2px", ring) |
| 500 | } |
| 501 | forceState(t, ctx, "#row-a") |
| 502 | forceState(t, ctx, "#link-a") |
| 503 | forceState(t, ctx, "#nav-x", "hover", "focus", "focus-visible") |
| 504 | if st, w := computed(t, ctx, "#nav-x", "outline-style"), computed(t, ctx, "#nav-x", "outline-width"); st != "solid" || w != "2px" { |
| 505 | t.Errorf("a focused hovered nav item has outline %s %s, want solid 2px", st, w) |
| 506 | } |
| 507 | forceState(t, ctx, "#nav-x") |
| 508 | } |
| 509 | |
| 510 | // TestCardsRestOnThePageWithNoScript: with no light.js on the page, a |
| 511 | // box, a stats band, a list card and a data card each compute the two |
| 512 | // ambient layers lit from straight above, a 0px rim radius (the static |
| 513 | // design), and --rst-card-lit 0. Depth adds no height: the same box is |
| 514 | // measured again with its decorative tokens set flat on the element, |
| 515 | // which changes shadows and gradients and nothing in layout. |
| 516 | func TestCardsRestOnThePageWithNoScript(t *testing.T) { |
| 517 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 518 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 519 | defer cancel() |
| 520 | open(t, ctx, rig, "/", false) |
| 521 | still(t, ctx) |
| 522 | for _, id := range []string{"#box", "#stats", "#list", "#card"} { |
| 523 | if got := computed(t, ctx, id, "--rst-card-lit"); got != "0" { |
| 524 | t.Errorf("%s: --rst-card-lit is %q with no script, want 0", id, got) |
| 525 | } |
| 526 | shadow := computed(t, ctx, id, "box-shadow") |
| 527 | if !strings.HasPrefix(shadow, "rgba(16, 24, 40, 0.06) 0px 1px 1px 0px, rgba(16, 24, 40, 0.06) 0px 1.5px 3px 0px") { |
| 528 | t.Errorf("%s: box-shadow %q, want the two ambient layers lit from straight above", id, shadow) |
| 529 | } |
| 530 | if img := computed(t, ctx, id, "background-image"); !strings.Contains(img, "radial-gradient(0px") { |
| 531 | t.Errorf("%s: the rim gradient is %q; at lit 0 its radius is 0px", id, img) |
| 532 | } |
| 533 | } |
| 534 | var heights struct{ Lit, Flat float64 } |
| 535 | at(t, ctx, `(() => { const b = document.getElementById("box"); const lit = b.getBoundingClientRect().height; b.style.cssText = "--rst-depth-ambient: transparent; --rst-light-rim: var(--rst-line); --rst-card-lit: 1"; const flat = b.getBoundingClientRect().height; b.style.cssText = ""; return JSON.stringify({Lit: lit, Flat: flat}); })()`, &heights) |
| 536 | if heights.Lit != heights.Flat { |
| 537 | t.Errorf("a box is %vpx tall with depth and %vpx without; depth added height", heights.Lit, heights.Flat) |
| 538 | } |
| 539 | } |
| 540 | |
| 541 | // TestEveryNavSurfaceHasItsStates is the table above read back from |
| 542 | // the browser, frozen: the sidebar's and console's current item is a |
| 543 | // surface thumb with a ring and a contact shadow on a rail that sits on |
| 544 | // the page background (C15), on an index-view root at desktop width |
| 545 | // too, where the rail sits beside the page; a hovered item takes the |
| 546 | // hover tint and a pressed one the press tint, in both rails and the |
| 547 | // topbar's phone card; a pressed tab or chip is its hovered self; every |
| 548 | // surface has its tap highlight off. |
| 549 | func TestEveryNavSurfaceHasItsStates(t *testing.T) { |
| 550 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 551 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 552 | defer cancel() |
| 553 | open(t, ctx, rig, "/", false) |
| 554 | still(t, ctx) |
| 555 | for _, c := range []struct{ rail, cur, other string }{{"#sidebar [rst-shell-rail]", "#nav-y", "#nav-x"}, {"#console [rst-shell-rail]", "#cnav-y", "#cnav-x"}, {"#isidebar [rst-shell-rail]", "#inav-y", "#inav-x"}} { |
| 556 | if bg, rail := computed(t, ctx, c.cur, "background-color"), computed(t, ctx, c.rail, "background-color"); bg != "rgb(255, 255, 255)" || rail != "rgb(251, 251, 252)" { |
| 557 | t.Errorf("%s: current item %s on a rail of %s; want the surface thumb on the page background", c.cur, bg, rail) |
| 558 | } |
| 559 | if sh := computed(t, ctx, c.cur, "box-shadow"); !strings.Contains(sh, "inset") || strings.Count(sh, "rgb") < 3 { |
| 560 | t.Errorf("%s: box-shadow %q, want the highlight, the ring and the contact shadow", c.cur, sh) |
| 561 | } |
| 562 | rest := computed(t, ctx, c.other, "background-color") |
| 563 | forceState(t, ctx, c.other, "hover") |
| 564 | hover := computed(t, ctx, c.other, "background-color") |
| 565 | forceState(t, ctx, c.other, "hover", "active") |
| 566 | press := computed(t, ctx, c.other, "background-color") |
| 567 | forceState(t, ctx, c.other) |
| 568 | if rest == hover || hover != "rgb(245, 247, 253)" || press != "rgb(236, 241, 252)" { |
| 569 | t.Errorf("%s rest %s hover %s press %s; want rgb(245, 247, 253) and rgb(236, 241, 252)", c.other, rest, hover, press) |
| 570 | } |
| 571 | } |
| 572 | // The topbar's desktop links keep the underline; the phone card is |
| 573 | // measured on its own rig at the end. |
| 574 | if got := computed(t, ctx, "#tnav-y", "border-bottom-color"); got != "rgb(31, 86, 214)" { |
| 575 | t.Errorf("topbar current link underline is %s, want the accent", got) |
| 576 | } |
| 577 | // The tab and chip hovers are top-level rules, outside any hover |
| 578 | // query, so they read the same on a rig with or without a pointer. |
| 579 | for _, sel := range []string{"#tab-1", "#page-1"} { |
| 580 | forceState(t, ctx, sel, "hover") |
| 581 | hover := computed(t, ctx, sel, "background-color") |
| 582 | forceState(t, ctx, sel, "hover", "active") |
| 583 | if got := computed(t, ctx, sel, "background-color"); got != hover { |
| 584 | t.Errorf("%s pressed is %s, hovered %s; tabs and chips have no press of their own", sel, got, hover) |
| 585 | } |
| 586 | forceState(t, ctx, sel) |
| 587 | } |
| 588 | forceState(t, ctx, "#nav-x", "active") |
| 589 | if got := computed(t, ctx, "#nav-x", "background-color"); got != "rgb(236, 241, 252)" { |
| 590 | t.Errorf("nav item tapped (active, no hover) is %s, want the tap tint rgb(236, 241, 252)", got) |
| 591 | } |
| 592 | forceState(t, ctx, "#nav-x") |
| 593 | for _, sel := range []string{"#nav-x", "#cnav-x", "#tnav-x", "#tab-1", "#page-1"} { |
| 594 | if got := computed(t, ctx, sel, "-webkit-tap-highlight-color"); got != "rgba(0, 0, 0, 0)" { |
| 595 | t.Errorf("%s tap highlight is %s, want transparent", sel, got) |
| 596 | } |
| 597 | } |
| 598 | // Chromium serialises a zero letter-spacing as "normal", whatever |
| 599 | // length the theme wrote, so both spellings of none are accepted; |
| 600 | // the old 0.06em reads 0.69px and fails either way. |
| 601 | if size, track := computed(t, ctx, "[rst-stat-label]", "letter-spacing"), computed(t, ctx, "[rst-stat-label]", "text-transform"); (size != "0px" && size != "normal") || track != "none" { |
| 602 | t.Errorf("stat label letter-spacing %s text-transform %s; want 0px and none (C15)", size, track) |
| 603 | } |
| 604 | // The phone card, on a rig opened at the phone width: after a second |
| 605 | // EmulateViewport this Chromium reports (hover: none) and (pointer: |
| 606 | // none), so a card read on the rig above would see only the tap |
| 607 | // rule. One viewport call before navigation keeps the mouse, and the |
| 608 | // query is checked first (harness/rig.go says to). Day hovers to its |
| 609 | // card tint and presses to the tap tint, and its current link wears |
| 610 | // the thumb's three shadow layers; plain and signal, swapped in on |
| 611 | // the same page, paint nothing on hover or press, as they did before |
| 612 | // the card had a hover. |
| 613 | narrow := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 614 | nctx, ncancel := context.WithTimeout(narrow.Context(), 60*time.Second) |
| 615 | defer ncancel() |
| 616 | mustRun(t, nctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(narrow.Origin+"/"), chromedp.WaitVisible("#field", chromedp.ByQuery)) |
| 617 | still(t, nctx) |
| 618 | var hov bool |
| 619 | at(t, nctx, `JSON.stringify(matchMedia("(hover: hover)").matches)`, &hov) |
| 620 | if !hov { |
| 621 | t.Fatal("(hover: hover) is false on the narrow rig; the card's hover rules cannot be read here") |
| 622 | } |
| 623 | cardFace := func(states ...string) string { |
| 624 | forceState(t, nctx, "#tnav-x", states...) |
| 625 | got := computed(t, nctx, "#tnav-x", "background-color") |
| 626 | forceState(t, nctx, "#tnav-x") |
| 627 | return got |
| 628 | } |
| 629 | if hover, press := cardFace("hover"), cardFace("hover", "active"); hover != "rgb(245, 247, 253)" || press != "rgb(236, 241, 252)" { |
| 630 | t.Errorf("topbar phone-card link hover %s press %s; want rgb(245, 247, 253) and rgb(236, 241, 252)", hover, press) |
| 631 | } |
| 632 | if bg, sh := computed(t, nctx, "#tnav-y", "background-color"), computed(t, nctx, "#tnav-y", "box-shadow"); bg != "rgb(255, 255, 255)" || !strings.Contains(sh, "inset") || strings.Count(sh, "rgb") < 3 { |
| 633 | t.Errorf("topbar phone-card current link is %s with box-shadow %q; want the surface thumb with the highlight, the ring and the contact shadow", bg, sh) |
| 634 | } |
| 635 | // The phone index's rows, on the same rig: they tint and nothing |
| 636 | // else. The rail items' state rules match these rows too and the |
| 637 | // index block outweighs them only by repeating the root's name, so |
| 638 | // these cells are what holds that ladder: weaken it (drop the |
| 639 | // doubled root) and the rail's ring and sink leak back as a shadow |
| 640 | // on a hovered or pressed row. Hover is the hover tint, press and tap |
| 641 | // the press tint (today's accent-soft in plain and signal, where the |
| 642 | // tap tint is transparent and would have blanked a tapped row), and a |
| 643 | // current row rests as any row, with no thumb. |
| 644 | face := func(sel string, states ...string) (bg, shadow string) { |
| 645 | forceState(t, nctx, sel, states...) |
| 646 | bg, shadow = computed(t, nctx, sel, "background-color"), computed(t, nctx, sel, "box-shadow") |
| 647 | forceState(t, nctx, sel) |
| 648 | return bg, shadow |
| 649 | } |
| 650 | indexRows := func(theme, hoverTint, pressTint string) { |
| 651 | for _, sel := range []string{"#inav-x", "#inav-y"} { |
| 652 | for _, c := range []struct { |
| 653 | states []string |
| 654 | want string |
| 655 | }{{[]string{"hover"}, hoverTint}, {[]string{"hover", "active"}, pressTint}, {[]string{"active"}, pressTint}} { |
| 656 | if bg, shadow := face(sel, c.states...); bg != c.want || shadow != "none" { |
| 657 | t.Errorf("%s: index row %s %v is %s with box-shadow %q; want %s and none (tint only)", theme, sel, c.states, bg, shadow, c.want) |
| 658 | } |
| 659 | } |
| 660 | } |
| 661 | restX, _ := face("#inav-x") |
| 662 | if bg, shadow := face("#inav-y"); bg != restX || shadow != "none" || computed(t, nctx, "#inav-y", "font-weight") != "450" { |
| 663 | t.Errorf("%s: the current index row rests as %s with box-shadow %q and weight %s; want a plain row (%s, none, 450), the index has no thumb", theme, bg, shadow, computed(t, nctx, "#inav-y", "font-weight"), restX) |
| 664 | } |
| 665 | } |
| 666 | indexRows("day", "rgb(245, 247, 253)", "rgb(236, 241, 252)") |
| 667 | for _, c := range []struct{ theme, cardHover, hoverTint, pressTint string }{{"plain", "rgba(0, 0, 0, 0)", "rgb(237, 237, 237)", "rgb(237, 237, 237)"}, {"signal", "rgba(0, 0, 0, 0)", "rgb(233, 238, 255)", "rgb(233, 238, 255)"}} { |
| 668 | css, _ := fiesta.ThemeCSS(c.theme) |
| 669 | mustRun(t, nctx, chromedp.Evaluate(`(() => { const l = document.querySelector('link[href="/theme.css"]'); if (l) l.remove(); for (const s of document.querySelectorAll("style[data-swap]")) s.remove(); const s = document.createElement("style"); s.dataset.swap = "1"; s.textContent = `+jsStr(string(css))+`; document.head.append(s); return true; })()`, nil)) |
| 670 | if hover, press := cardFace("hover"), cardFace("hover", "active"); hover != c.cardHover || press != c.cardHover { |
| 671 | t.Errorf("%s: topbar phone-card link hover %s press %s; want transparent, as before the card had a hover", c.theme, hover, press) |
| 672 | } |
| 673 | indexRows(c.theme, c.hoverTint, c.pressTint) |
| 674 | } |
| 675 | } |
| 676 | |
| 677 | // TestFieldsSitIntoThePageAndFocusKeepsTheOutline (spec §3): |
| 678 | // an input has a 1px inset ambient shadow and a --rst-line-strong |
| 679 | // border at rest; focused, its border is the accent, it gains the 3px |
| 680 | // halo, and the solid 2px outline is still there. The textarea does |
| 681 | // the same, and the search box does it on :focus-within, the field |
| 682 | // box carrying the indicator for its input. |
| 683 | func TestFieldsSitIntoThePageAndFocusKeepsTheOutline(t *testing.T) { |
| 684 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 685 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 686 | defer cancel() |
| 687 | open(t, ctx, rig, "/", false) |
| 688 | still(t, ctx) |
| 689 | for _, f := range []struct{ box, focus string }{{"#field", "#field"}, {"#area", "#area"}, {"#search", "#search-in"}} { |
| 690 | // day's light --rst-line-strong is #868691 (a review decision: the sign-in |
| 691 | // border needed it darker than the #8a8a95 first proposed). |
| 692 | if sh, border := computed(t, ctx, f.box, "box-shadow"), computed(t, ctx, f.box, "border-top-color"); !strings.HasPrefix(sh, "rgba(16, 24, 40, 0.06) 0px 1px 1px 0px inset") || border != "rgb(134, 134, 145)" { |
| 693 | t.Errorf("%s at rest: box-shadow %q border %s; want the inset ambient shadow and --rst-line-strong", f.box, sh, border) |
| 694 | } |
| 695 | forceState(t, ctx, f.focus, "focus", "focus-visible") |
| 696 | sh, border, outline := computed(t, ctx, f.box, "box-shadow"), computed(t, ctx, f.box, "border-top-color"), computed(t, ctx, f.box, "outline-style")+" "+computed(t, ctx, f.box, "outline-width") |
| 697 | if border != "rgb(31, 86, 214)" || !strings.Contains(sh, "0px 0px 0px 3px") || outline != "solid 2px" { |
| 698 | t.Errorf("focused %s: border %s box-shadow %q outline %s; want the accent border, the 3px halo and the solid 2px outline", f.box, border, sh, outline) |
| 699 | } |
| 700 | forceState(t, ctx, f.focus) |
| 701 | } |
| 702 | } |
| 703 | |