//go:build browser
// A seg-tabs track made of buttons, for a choice that changes the page
// in place: each button must be the tab a link is, or a page that
// mixes the two, or moves from one to the other, shows two controls.
package check
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/chromedp"
"amadan.net/rastrillo/fiesta"
"amadan.net/rastrillo/fiesta/internal/harness"
)
// segTabsTrack is one track holding the current link, a plain link,
// the pressed button, an unpressed one and a disabled one, so each
// button is read
// beside the link it must look like, under the same rules.
const segTabsTrack = `
`
// segTabsPages serves the track at /// under tokens.css
// and that theme, with no script, so nothing but the stylesheets
// decides what each tab looks like.
func segTabsPages(t *testing.T) http.Handler {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("GET /tokens.css", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/css")
w.Write(fiesta.TokensCSS())
})
mux.HandleFunc("GET /theme/{name}", func(w http.ResponseWriter, r *http.Request) {
css, ok := fiesta.ThemeCSS(strings.TrimSuffix(r.PathValue("name"), ".css"))
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/css")
w.Write(css)
})
mux.HandleFunc("GET /{theme}/{scheme}/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, ``+
`seg-tabs`+
``+
`%s`, r.PathValue("scheme"), r.PathValue("theme"), segTabsTrack)
})
return mux
}
// segTabFace is what a reader sees of a tab: its fill, its text and
// its shape, the box a finger or a pointer has to hit, and the display
// that centres its label in that box on a phone.
var segTabFace = []string{"background-color", "color", "font-family", "font-size", "font-weight", "line-height", "padding", "border-radius", "box-shadow", "block-size", "min-block-size", "min-inline-size", "display", "align-items", "justify-content", "cursor"}
// sameFace reports each property of segTabFace in which button differs
// from link.
func sameFace(t *testing.T, ctx context.Context, where, button, link string) {
t.Helper()
for _, prop := range segTabFace {
if b, l := computed(t, ctx, button, prop), computed(t, ctx, link, prop); b != l {
t.Errorf("%s: %s's %s is %q, %s's is %q; a button in a track must be the tab a link is", where, button, prop, b, link, l)
}
}
}
// TestASegTabButtonLooksLikeASegTabLink: in every theme and scheme,
// with a mouse and on a touch screen, the pressed button is the thumb
// the current link is, and an unpressed button is a plain tab, with
// no border and no fill of the browser's own; hovered, it turns the
// colour a hovered link does. Under forced colours and in print the
// pressed button keeps the current link's system colours and loses
// its lift the same way, or the choice made would vanish there.
func TestASegTabButtonLooksLikeASegTabLink(t *testing.T) {
for _, coarse := range []bool{false, true} {
// A mouse on a desktop meets the desktop floor, a finger on a
// phone the touch floor and its centring: each is a rule the
// buttons must share with the links.
pointer, width := "mouse at 1280", int64(1280)
var opts []harness.Option
if coarse {
pointer, width = "touch at 390", 390
opts = append(opts, harness.WithCoarsePointer())
}
rig := harness.New(t, func(string) http.Handler { return segTabsPages(t) }, opts...)
ctx, cancel := context.WithTimeout(rig.Context(), 90*time.Second)
mustRun(t, ctx, chromedp.EmulateViewport(width, 844))
for _, theme := range fiesta.ThemeNames() {
for _, scheme := range []string{"light", "dark"} {
where := fmt.Sprintf("%s %s, %s", theme, scheme, pointer)
mustRun(t, ctx, chromedp.Navigate(fmt.Sprintf("%s/%s/%s/", rig.Origin, theme, scheme)), chromedp.WaitVisible("#unpressed", chromedp.ByQuery))
requirePointer(t, ctx, coarse)
still(t, ctx)
sameFace(t, ctx, where, "#pressed", "#current")
sameFace(t, ctx, where, "#unpressed", "#link")
if w := computed(t, ctx, "#unpressed", "border-width"); w != "0px" {
t.Errorf("%s: the unpressed button's border is %s wide; the browser's button edge must go", where, w)
}
if bg := computed(t, ctx, "#unpressed", "background-color"); bg != "rgba(0, 0, 0, 0)" {
t.Errorf("%s: the unpressed button is filled %s; only the pressed one wears the thumb", where, bg)
}
rest := computed(t, ctx, "#link", "color")
forceState(t, ctx, "#link", "hover")
forceState(t, ctx, "#unpressed", "hover")
hovered := computed(t, ctx, "#link", "color")
if hovered == rest {
t.Errorf("%s: CONTROL: a hovered link is %s, the colour it rests in; the hover below cannot be told from rest", where, rest)
}
if b := computed(t, ctx, "#unpressed", "color"); b != hovered {
t.Errorf("%s: a hovered unpressed button is %s, a hovered link %s", where, b, hovered)
}
forceState(t, ctx, "#link")
forceState(t, ctx, "#unpressed")
disabledLooksIdle(t, ctx, where, rest)
}
}
cancel()
}
// Forced colours and print, in Day light, where the axis is
// flattened and the current tab is drawn by rules of its own.
rig := harness.New(t, func(string) http.Handler { return segTabsPages(t) })
ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
defer cancel()
for _, medium := range []struct {
name, query string
emulate *emulation.SetEmulatedMediaParams
}{
{"forced colours", "(forced-colors: active)", emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{{Name: "forced-colors", Value: "active"}})},
{"print", "print", emulation.SetEmulatedMedia().WithMedia("print")},
} {
mustRun(t, ctx, chromedp.EmulateViewport(1280, 800), medium.emulate, chromedp.Navigate(rig.Origin+"/day/light/"), chromedp.WaitVisible("#pressed", chromedp.ByQuery))
var on bool
at(t, ctx, `JSON.stringify(matchMedia(`+jsStr(medium.query)+`).matches)`, &on)
if !on {
t.Fatalf("%s: matchMedia(%q) is false; this leg is not the medium it claims", medium.name, medium.query)
}
still(t, ctx)
for _, prop := range []string{"background-color", "color", "box-shadow"} {
if b, l := computed(t, ctx, "#pressed", prop), computed(t, ctx, "#current", prop); b != l {
t.Errorf("%s: the pressed button's %s is %q, the current link's %q", medium.name, prop, b, l)
}
}
mustRun(t, ctx, emulation.SetEmulatedMedia())
}
}
// disabledLooksIdle: a disabled button says it is not listening, as
// Fiesta's other disabled controls do: faint text, the default cursor,
// and no answer to a hover. With the enabled tab's colour, pointer and
// hover it read as a choice that would work, and a click did nothing.
// rest is a plain link's resting colour, which the faint text must not
// be, or the check below could not tell the two apart.
func disabledLooksIdle(t *testing.T, ctx context.Context, where, rest string) {
t.Helper()
var faint string
at(t, ctx, `(() => { const p = document.createElement("span"); p.style.color = "var(--rst-text-faint)"; document.body.append(p); const c = getComputedStyle(p).color; p.remove(); return JSON.stringify(c); })()`, &faint)
faint = canonColours(faint)
if faint == rest {
t.Errorf("%s: CONTROL: the faint text colour is %s, a plain tab's own; a disabled button cannot be told from an enabled one by colour", where, faint)
}
if c := computed(t, ctx, "#disabled", "color"); c != faint {
t.Errorf("%s: a disabled button's text is %s, want the faint text colour %s", where, c, faint)
}
if c := computed(t, ctx, "#disabled", "cursor"); c != "default" {
t.Errorf("%s: a disabled button's cursor is %q, want \"default\": a pointer promises a click that does nothing", where, c)
}
forceState(t, ctx, "#disabled", "hover", "active")
if c := computed(t, ctx, "#disabled", "color"); c != faint {
t.Errorf("%s: a hovered, pressed disabled button turns %s; it must stay %s", where, c, faint)
}
forceState(t, ctx, "#disabled")
}