`)}
for _, leg := range []struct {
name string
w, h int64
coarse bool
}{{"1280x900 mouse", 1280, 900, false}, {"390x844 touch", 390, 844, true}} {
rig := sizingRig(t, leg.coarse, page)
ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
var got map[string]float64
var raw string
if err := chromedp.Run(ctx, chromedp.EmulateViewport(leg.w, leg.h), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitReady("#button", chromedp.ByQuery),
chromedp.Evaluate(`JSON.stringify(Object.fromEntries(["heading", "prose", "prose-link", "button"].map(id => [id, document.querySelector("#" + id + " > svg").getBoundingClientRect().height])))`, &raw)); err != nil {
cancel()
t.Fatal(err)
}
cancel()
if err := json.Unmarshal([]byte(raw), &got); err != nil {
t.Fatal(err)
}
for id, want := range map[string]float64{"heading": 24, "prose": 12, "prose-link": 12, "button": 16} {
if got[id] != want {
t.Errorf("%s: the icon in #%s is %vpx, want %v", leg.name, id, got[id], want)
}
}
}
}
// assertCovered is what makes "every control passed" mean something,
// in two halves. Every rendered control on the closed page was measured
// (none skipped by a bug in measure itself), and for every inventory row
// on this page at least one element matching its Probe was measured
// (measureFn marks each with data-measured), so an idiom cannot drop out
// of the drive because its only fixture went away. Each named fixture
// control is also required by id in TestTextControlsAreSixteenPixels.
func assertCovered(t *testing.T, ctx context.Context, where, page string, wide bool) {
t.Helper()
var missed []string
if err := chromedp.Run(ctx, chromedp.Evaluate(`[...document.querySelectorAll('a[href], button, summary, input:not([type=hidden]), select, textarea')]
.filter(e => !e.hasAttribute("data-measured") && !e.closest("[data-sizing-not-an-idiom], [inert]") && e.checkVisibility({visibilityProperty: true}) && getComputedStyle(e).clipPath !== "inset(50%)" && e.getBoundingClientRect().width >= 2)
.filter(e => !(e.matches("input[type=checkbox], input[type=radio]") && e.closest("label") && e.closest("label").hasAttribute("data-measured-area")))
.map(e => e.outerHTML.slice(0, 80))`, &missed)); err != nil {
t.Fatalf("%s: listing unmeasured controls: %v", where, err)
}
if len(missed) > 0 {
t.Errorf("%s: %d rendered controls were never measured: %v", where, len(missed), missed)
}
for _, e := range tapInventory {
sel, entryPage := e.Probe, ""
switch {
case strings.HasPrefix(sel, "elsewhere:"):
continue
case strings.HasPrefix(sel, "narrow:"):
if wide {
continue
}
sel = strings.TrimPrefix(sel, "narrow:")
case strings.HasPrefix(sel, "modal:"):
sel, entryPage = strings.TrimPrefix(sel, "modal:"), "modal"
}
if entryPage != page {
continue
}
var n int
if err := chromedp.Run(ctx, chromedp.Evaluate(fmt.Sprintf(`[...document.querySelectorAll(%q)].filter(e => e.hasAttribute("data-measured")).length`, sel), &n)); err != nil {
t.Fatalf("%s: counting %s: %v", where, sel, err)
}
if n == 0 {
t.Errorf("%s: no %s (%s) was measured; the idiom dropped out of the drive", where, sel, e.Idiom)
}
}
}
// fixtureCounts are controls the sizing fixture itself owns, with how
// many of each must have been measured. Declared here, not read off the
// page: a control that disappears from the fixture is absent from the
// DOM too, so only a number written down independently can notice it,
// even while another control of the same idiom survives.
var fixtureCounts = []struct {
Sel string
N int
}{
{`[data-extra="buttons"] [rst-btn]`, 3},
{`[data-extra="locale"] [rst-locale] button`, 3},
{`[data-extra="combobox"] [role=option]`, 12},
{`[data-extra="legacy-drawer"] [rst-shell-nav] a`, 2},
{`[data-extra="small-parents"] :is(input, textarea)`, 7}, // the bare input is the app's own, not measured
// Previous and 1 are spans (disabled, current); 9 and Next are links
// at every width. 2, beside the current page, is a link only a wide
// strip shows, so it is measured where it is drawn and not counted.
{`[data-partial="pagination"] [rst-pagination] a:not([rst-pagination-wide])`, 2},
{`[data-partial="bulk-bar"] [rst-dropdown-menu] button`, 2},
{`[data-partial="seg-tabs"] a`, 2},
{`[data-partial="seg-tabs"] [rst-seg-tabs] > button`, 2},
{`[data-partial="side-nav"] [rst-side-nav] a`, 2},
{`[data-partial="dropdown"] [rst-dropdown-menu] a`, 1},
{`[data-extra="short-labels"] [rst-bulkbar-escalate]`, 1},
{`[data-extra="short-labels"] a[rst-person]`, 1},
{`[data-extra="signin-providers"] [rst-signin-providers] > a`, 1},
}
func assertFixtureCounts(t *testing.T, ctx context.Context, where string) {
t.Helper()
for _, c := range fixtureCounts {
var n int
if err := chromedp.Run(ctx, chromedp.Evaluate(fmt.Sprintf(`[...document.querySelectorAll(%q)].filter(e => e.hasAttribute("data-measured")).length`, c.Sel), &n)); err != nil {
t.Fatalf("%s: counting %s: %v", where, c.Sel, err)
}
if n != c.N {
t.Errorf("%s: %d of %s were measured, want exactly %d", where, n, c.Sel, c.N)
}
}
}
// TestEveryTapTargetIsAtLeast44Pixels is §10.1's target half, at 390
// and 1024 with a coarse pointer and at 600 with a mouse, then the modal
// on a page of its own. The 1280 mouse leg is the desktop floor: the
// same drive, every control at least 32×32, so a control that is only
// floored on touch (a 26px kebab, a 24px clear link) fails there. Every
// leg also holds the icons inside controls to 16px and a caret's to 12.
func TestEveryTapTargetIsAtLeast44Pixels(t *testing.T) {
pages := map[string]string{
"/": sizingDoc("sizing", sizingFixture(t)),
"/modal": sizingDoc("modal", modalSample(t, "TestEveryTapTargetIsAtLeast44Pixels")),
}
for _, leg := range []struct {
name string
w, h int64
coarse bool
floor float64
}{
{"390x844 touch", 390, 844, true, 44},
{"1024x768 touch", 1024, 768, true, 44},
{"600x800 mouse", 600, 800, false, 44},
{"1280x900 mouse", 1280, 900, false, 32},
} {
t.Run(leg.name, func(t *testing.T) {
rig := sizingRig(t, leg.coarse, pages)
ctx, cancel := context.WithTimeout(rig.Context(), 240*time.Second)
defer cancel()
if err := chromedp.Run(ctx, chromedp.EmulateViewport(leg.w, leg.h), chromedp.Navigate(rig.Origin+"/")); err != nil {
t.Fatal(err)
}
requirePointer(t, ctx, leg.coarse)
assertIcons(t, ctx, leg.name)
n := measureEverything(t, ctx, leg.name, leg.floor)
t.Logf("%s: %d controls measured", leg.name, n)
assertCovered(t, ctx, leg.name, "", leg.w >= 800)
assertFixtureCounts(t, ctx, leg.name)
if err := chromedp.Run(ctx, chromedp.Navigate(rig.Origin+"/modal")); err != nil {
t.Fatal(err)
}
got := readTargets(t, ctx, targetsJS)
assertTargetsAt(t, leg.name+", the modal", got, leg.floor)
assertCovered(t, ctx, leg.name+", the modal", "modal", leg.w >= 800)
})
}
}
// TestTheKebabOverflowsIntoTheGapOnALiteralColumn is §2.3's claim for
// an app that keeps --rst-cols' literal 32px: on a wide touch screen the
// 44px kebab is justify-self: end, so it spills 12px toward the inline
// start, into the 0.85rem gap, and never over the cell beside it.
func TestTheKebabOverflowsIntoTheGapOnALiteralColumn(t *testing.T) {
page := sizingDoc("literal", `