`)
rig, ctx := phoneVariantRig(t, map[string]string{
"/": doc, "/rtl": inArabic(t, doc), "/plain": plain, "/plain-rtl": inArabic(t, plain),
})
for _, path := range []string{"/", "/rtl"} {
for _, width := range []int64{390, 320} {
leg := fmt.Sprintf("%s at %d", path, width)
b := readBand(t, ctx, rig.Origin+path, width)
if b.RTL != (path == "/rtl") {
t.Fatalf("%s: the band reads right to left %v; this leg needs %v", leg, b.RTL, path == "/rtl")
}
c := b.Cells
if !sameEdge(c[0].Top, c[1].Top) || !sameEdge(c[2].Top, c[3].Top) {
t.Errorf("%s: the stats' tops are %.1f, %.1f, %.1f, %.1f; stats 1 and 2 must share a line, and 3 and 4", leg, c[0].Top, c[1].Top, c[2].Top, c[3].Top)
}
if c[2].Top < c[0].Bottom-0.5 {
t.Errorf("%s: the second pair starts at %.1f, above the first pair's foot at %.1f", leg, c[2].Top, c[0].Bottom)
}
// The first of a pair is at the inline start: the left in
// English, the right in Arabic.
for _, p := range [][2]int{{0, 1}, {2, 3}} {
first, second := c[p[0]], c[p[1]]
if (!b.RTL && first.Left >= second.Left) || (b.RTL && first.Right <= second.Right) {
t.Errorf("%s: stat %d (%.1f to %.1f) is not at the inline start of stat %d (%.1f to %.1f)", leg, p[0]+1, first.Left, first.Right, p[1]+1, second.Left, second.Right)
}
}
for i, want := range []string{"0px", "1px", "0px", "1px"} {
if c[i].Start != want {
t.Errorf("%s: stat %d's inline-start hairline is %s, want %s", leg, i+1, c[i].Start, want)
}
}
for i, want := range []string{"0px", "0px", "1px", "1px"} {
if c[i].BlockStart != want {
t.Errorf("%s: stat %d's top hairline is %s, want %s", leg, i+1, c[i].BlockStart, want)
}
}
if width != 320 {
continue
}
for i, cell := range c {
if n := cell.Num; n.Left < cell.Left-0.5 || n.Right > cell.Right+0.5 {
t.Errorf("%s: stat %d's figure runs %.1f to %.1f, outside its cell's %.1f to %.1f", leg, i+1, n.Left, n.Right, cell.Left, cell.Right)
}
}
if b.DocScroll > b.DocClient {
t.Errorf("%s: the page is %.0fpx wide in a %.0fpx viewport; it scrolls sideways", leg, b.DocScroll, b.DocClient)
}
}
}
sameAtDesktop(t, ctx, rig.Origin, "/", "/plain", `rst-stats="pairs"`)
sameAtDesktop(t, ctx, rig.Origin, "/rtl", "/plain-rtl", `rst-stats="pairs" in Arabic`)
}
// rowItem is one child of a row as the inline test reads it; Present
// is false for a part the row does not have.
type rowItem struct {
Present bool
Left, Right, Top, Bottom float64
}
// rowReading is the first row on the page: its four parts, how many
// lines the name's link takes, the row's content box (its inline start
// and end, and its top) and column gap, the row's computed display and
// the status's computed grid placement (row start and end, column start
// and end), and the document's scroll and client widths.
type rowReading struct {
Lead, Name, Status, Action rowItem
NameLines int
Start, End, Top, Gap float64
RTL bool
Display, Placement string
DocScroll, DocClient float64
}
const rowJS = `(() => {
const row = document.querySelector("[rst-row]"), cs = getComputedStyle(row), r = row.getBoundingClientRect(), px = (v) => parseFloat(v) || 0;
const item = (sel) => { const el = row.querySelector(":scope > " + sel); if (!el) return {Present: false};
const b = el.getBoundingClientRect(); return {Present: true, Left: b.left, Right: b.right, Top: b.top, Bottom: b.bottom}; };
const words = document.createRange(); words.selectNodeContents(row.querySelector("[rst-row-main] > a"));
const rtl = cs.direction === "rtl", left = r.left + px(cs.borderLeftWidth) + px(cs.paddingLeft), right = r.right - px(cs.borderRightWidth) - px(cs.paddingRight);
return JSON.stringify({Lead: item("[rst-row-lead]"), Name: item("[rst-row-main]"), Status: item("[rst-status]"), Action: item("[rst-row-action]"),
NameLines: new Set([...words.getClientRects()].map((q) => Math.round(q.top))).size,
Start: rtl ? right : left, End: rtl ? left : right, Top: r.top + px(cs.borderTopWidth) + px(cs.paddingTop), Gap: px(cs.columnGap), RTL: rtl,
Display: cs.display, Placement: ((s) => [s.gridRowStart, s.gridRowEnd, s.gridColumnStart, s.gridColumnEnd].join(" / "))(getComputedStyle(row.querySelector(":scope > [rst-status]"))),
DocScroll: document.documentElement.scrollWidth, DocClient: document.documentElement.clientWidth});
})()`
func readRow(t *testing.T, ctx context.Context, url string, width int64) rowReading {
t.Helper()
mustRun(t, ctx, chromedp.EmulateViewport(width, 844), chromedp.Navigate(url), chromedp.WaitReady("[rst-row]", chromedp.ByQuery))
settleUntil(t, ctx, `document.readyState === "complete"`)
var r rowReading
at(t, ctx, rowJS, &r)
if !r.Name.Present || !r.Status.Present {
t.Fatalf("%s: the row has no name or no status to read", url)
}
return r
}
// span is a part's start and end measured along the row from its
// inline start: from the left in English, from the right in Arabic, so
// one ordering holds both directions.
func (r rowReading) span(it rowItem) (start, end float64) {
if r.RTL {
return r.Start - it.Right, r.Start - it.Left
}
return it.Left - r.Start, it.Right - r.Start
}
// width is the row's content width.
func (r rowReading) width() float64 { return max(r.End-r.Start, r.Start-r.End) }
// onFirstLine is whether a part starts at the top of the row's content.
func (r rowReading) onFirstLine(it rowItem) bool { return math.Abs(it.Top-r.Top) <= 1 }
// inlinePlacement is the status's computed grid placement the inline
// value asks for: row 1, and the column the grid's auto-placement picks,
// the implicit one straight after the name's.
const inlinePlacement = "1 / auto / auto / auto"
// placedByTheGrid fails unless the row is a grid and the status is
// placed in it on row 1 with an auto column. The geometry alone cannot
// tell this from a wrapping flex line: in these rows the name shrinks
// first, so a flex line holds every part on the first line too, and
// only drops the action or the status, label by label, once they
// outgrow the row. The grid placement is the design, so it is held.
func placedByTheGrid(t *testing.T, leg string, r rowReading) {
t.Helper()
if r.Display != "grid" {
t.Errorf("%s: the row's display is %s; a row with an inline status is a grid, not a wrapping flex line", leg, r.Display)
}
if r.Placement != inlinePlacement {
t.Errorf("%s: the status's grid placement (row start / end / column start / end) is %s, want %s", leg, r.Placement, inlinePlacement)
}
}
// TestAnInlineStatusStaysOnTheNamesLine: at 390 and 320, in both
// directions, the list-row-action state with its status marked inline
// keeps all four parts on the row's first line, in source order from
// the inline start (lead, name, status, action), and the name's column
// takes the room the other three leave. A name long enough to wrap
// wraps inside its column and the status and action stay up; with no
// lead and no action the status ends at the row's content end. Without
// the value each of those rows puts the status under the name, as 1.0
// does, which is the control that the legs above see the value's work.
// Every marked row is a grid with the status on row 1 in an auto
// column, the placement the design asks for rather than a flex line
// that happens to fit. At 1280 each row, in both directions, paints as
// the same row unmarked, pixel for pixel.
func TestAnInlineStatusStaysOnTheNamesLine(t *testing.T) {
doc := stateDoc(t, "list-row-action", "A short status beside the name")
long := swapped(t, doc, ">Release notes, August", ">Release notes for August, with every change since the spring")
bare := swapped(t, doc, "\n
RN", "")
bare = swapped(t, bare, "\n
Edit", "")
pages := map[string]string{}
for name, d := range map[string]string{"/": doc, "/long": long, "/bare": bare} {
for _, dir := range []string{"", "-rtl"} {
if dir != "" {
d = inArabic(t, d)
}
pages[name+dir] = d
pages[name+dir+"-plain"] = swapped(t, d, `
0 {
if _, before := r.span(parts[i-1].it); start < before-0.5 {
t.Errorf("%s: %s starts %.1fpx along the row, before %s ends at %.1fpx; the line must read lead, name, status, action", leg("/"), p.name, start, parts[i-1].name, before)
}
}
if p.name != "the name" {
room -= end - start + r.Gap
}
}
if start, end := r.span(r.Name); end-start < room-0.5 {
t.Errorf("%s: the name is %.1fpx wide with %.1fpx left by the others; its column must take the room", leg("/"), end-start, room)
}
l := readRow(t, ctx, rig.Origin+"/long"+dir, width)
placedByTheGrid(t, leg("/long"), l)
if l.NameLines < 2 {
t.Errorf("%s: the long name takes %d line; it must wrap inside its column", leg("/long"), l.NameLines)
}
_, nameEnd := l.span(l.Name)
if statusStart, _ := l.span(l.Status); statusStart < nameEnd-0.5 {
t.Errorf("%s: the status starts %.1fpx along, inside the long name's column, which ends at %.1fpx", leg("/long"), statusStart, nameEnd)
}
if !l.onFirstLine(l.Status) || !l.onFirstLine(l.Action) {
t.Errorf("%s: under a long name the status starts at %.1f and the action at %.1f; both must stay on the first line at %.1f", leg("/long"), l.Status.Top, l.Action.Top, l.Top)
}
if l.DocScroll > l.DocClient {
t.Errorf("%s: the page is %.0fpx wide in a %.0fpx viewport; it scrolls sideways", leg("/long"), l.DocScroll, l.DocClient)
}
b := readRow(t, ctx, rig.Origin+"/bare"+dir, width)
if b.Lead.Present || b.Action.Present {
t.Fatalf("%s: the scratch row still has a lead or an action", leg("/bare"))
}
placedByTheGrid(t, leg("/bare"), b)
if _, end := b.span(b.Status); math.Abs(end-b.width()) > 0.5 {
t.Errorf("%s: with no lead and no action the status ends %.1fpx along a row whose content ends at %.1fpx", leg("/bare"), end, b.width())
}
if !b.onFirstLine(b.Status) {
t.Errorf("%s: the status starts at %.1f, off the first line at %.1f", leg("/bare"), b.Status.Top, b.Top)
}
for _, name := range []string{"/", "/long", "/bare"} {
p := readRow(t, ctx, rig.Origin+name+dir+"-plain", width)
if p.Status.Top < p.Name.Bottom-0.5 {
t.Errorf("%s unmarked: the status starts at %.1f, above the name's foot at %.1f; 1.0 puts it under the name", leg(name), p.Status.Top, p.Name.Bottom)
}
}
}
}
for _, name := range []string{"/", "/long", "/bare", "/-rtl", "/long-rtl", "/bare-rtl"} {
sameAtDesktop(t, ctx, rig.Origin, name, name+"-plain", `rst-status="inline" (`+name+`)`)
}
}