b4e78d426dcdb47a20f1979025d5a0969ea79ac4
braney
  Thu Sep 17 17:07:49 2026 -0700
docent: a box: check for where an element sits, and lists for text:/noText:, refs #37892

`expect:` could say what was in a page and never where it was on the screen.
has:/noHas: take a CSS selector, which describes the tree; color: reads pixels
but only inside a track's row.  #38251 moved the narrow-window menu icon out of
the blue bar with every selector still matching and every word of the page still
there, and nothing in the language could ask about it.

box: takes `inside:` (every edge within another element's box, with `tolerance:`
px of slack), `clear:` (no overlap with anything a selector matches, `gap:` for a
minimum separation) and `height:`/`width:`.  Every element `sel:` matches has to
satisfy every clause, so {sel: "ul.nice-menu > li", inside: "#main-menu-whole"}
reads as "every menu item is in the bar".  Boxes are read in document
coordinates; an element with no box at all is skipped rather than treated as a
zero-sized box at the origin, which would sit "inside" anything.  A failure
prints the measurement:

#topRightLinks is not inside #main-menu-whole: 114px above it
-- it is at 666,0 34x32, #main-menu-whole at 0,114 1000x32

The image height: and box's own height:/width: now share one cmpSize(), so the
two cannot drift into different comparison grammars.

text: and noText: take a list, the way rows:, has: and noHas: always did.  They
have to: a list handed to a check that stringifies its argument fails OPEN --
["a", "b"] becomes "a,b", which no page contains, so it passes on anything, and
passes silently.  Six scripts in one batch were written that way and all six
looked green.

pagechecks and its .xfail twin cover both, the xfail with one entry aimed wrongly
and one aimed rightly in each list, so it fails only if every entry is really
looked at on its own.

diff --git src/hg/utils/docent/tests/pagechecks.docent.yaml src/hg/utils/docent/tests/pagechecks.docent.yaml
index e1837901d12..6b0d498ecb4 100644
--- src/hg/utils/docent/tests/pagechecks.docent.yaml
+++ src/hg/utils/docent/tests/pagechecks.docent.yaml
@@ -1,56 +1,80 @@
 # The three `expect:` checks that look at the page rather than at the track image, plus
 # the positional form of `click:`. All four were added for the regression suite next door
 # (tests/regress), and tests/README.txt's rule is that a verb we touch and find untested
 # belongs here.
 #
 #   url: / noHas:   a substring check on the CURRENT ADDRESS. Some things are visible
 #                   nowhere else -- which CGI a click reached, and what a form put in a
 #                   query string. #36387's fix strips zero-width characters out of a
 #                   search term before the position box submits it, and the character is
 #                   invisible in the rendered page, so the URL is the only evidence.
 #   has: / noHas:   a CSS selector matches / matches nothing. For a bug whose whole
 #                   signature is WHERE something sits: #37785 drew the same rows, the same
 #                   height and the same pixels either way, and only the row its center
 #                   label's image map hung off changed.
 #   click: {track:, frac:}   the item box nearest a point, and its hgc link. The only way
 #                   in for a track whose items cannot be named -- a `type bigBed 3` gets
 #                   an EMPTY i= in its hgc href and the same title on every box.
+#   box:            WHERE an element sits on the screen, which a selector cannot say at
+#                   all. #38251 moved the narrow-window menu icon out of the blue bar and
+#                   slid it across the menu items with every selector still matching.
+#   text:/noText:   as LISTS. They took one string until 2026-09-17, and a list handed to
+#                   a check that stringifies its argument fails OPEN: ["a", "b"] becomes
+#                   "a,b", which no page contains, so it passes on anything and passes
+#                   silently. Six scripts next door were written that way and looked green.
 #
 # pagechecks.xfail.docent.yaml is the other half: the same view with each of the four
 # aimed the wrong way, because a check that cannot fail is not a check.
 target: genome-test
 db: hg38
 position: chr7:155799529-155812871
 reset: true
 fast: true
 steps:
   - go: chr7:155799529-155812871
   - hide: all
   - track: {mane: pack}
 
   # url: and noUrl: on a plain tracks view. `go:` navigates hgTracks directly, so the
   # address carries the CGI and the position and no hgSearch.
   - expect:
       url: "/hgTracks"
       noUrl: "/hgSearch"
       rows: [ruler, mane]
 
   # has: and noHas:. hgTracks puts each center label in a MAP named map_center_<track>,
   # inside the data cell of the row it belongs to -- which is the structure #37785 broke.
   # The ruler has no center label of its own, so that pair is the negative case.
   - expect:
       has:
         - "#td_data_mane map[name=map_center_mane]"
         - "#imgTbl tr#tr_mane"
       noHas:
         - "#td_data_ruler map[name=map_center_mane]"
         - "map[name=map_center_noSuchTrack]"
 
+  # box:. The blue menu bar is the same on every page and is the structure #38251 broke:
+  # the icon belongs at the right-hand end of the bar, clear of the menu items, and the bar
+  # stays one row. The three clauses are each exercised once -- inside:, clear: with a gap:,
+  # and the two sizes.
+  - expect:
+      box:
+        - {sel: "#topRightLinks", inside: "#main-menu-whole"}
+        - {sel: "#topRightLinks", clear: "#main-menu ul.nice-menu > li > a", gap: 8}
+        - {sel: "#main-menu-whole", height: "<=40", width: ">=1000"}
+
+  # text: and noText: given lists. Every string has to be checked on its own: joined into
+  # one string these would be "hide all,default tracks,chr7:..." and match nothing at all.
+  # Three of each, so a check that only ever looked at the first entry would be caught too.
+  - expect:
+      text: ["chr7:155,799,529-155,812,871", "Visible Tracks", "Mapping and Sequencing"]
+      noText: ["Warning/Error", "Too Long", "Unknown database"]
+
   # The positional click. frac: 0.5 is the middle of the row; mane HAS named items here,
   # so this is not the case that needs it, but it is the case where the result can be
   # checked -- the details page has to be the one for a MANE transcript.
   - click: {track: mane, frac: 0.5}
   - expect:
       url: "/hgc"
       text: "Position:"
       noText: "Warning/Error"