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/README.md src/hg/utils/docent/README.md
index 2186b57f549..e68b1a27ba8 100644
--- src/hg/utils/docent/README.md
+++ src/hg/utils/docent/README.md
@@ -1,471 +1,505 @@
# Docent — a language for authoring guided tours of the Genome Browser
A **docent** leads a tour and explains what you are looking at. A Docent script is
that tour, written down: an ordered list of high-level verbs (`go`, `hide`,
`track`, `mouseover`, `convert`, `drag`, `shot`) that drive a real browser against a
real server.
`docent.js` renders one script into three things at once:
- a **silent mp4** of the whole tour,
- a named **PNG still** at every `shot:` marker, and
- a loadable **session file** at every `session:` marker.
So a published figure is literally a frame of the tour, and the two can never drift
apart. A session file goes further: it hands back the state itself, so a reader can open
the view the figure was taken from instead of only looking at it. The surface syntax is YAML — so ordinary editors highlight it and no one has to
learn a new parser — but the language is the verb vocabulary layered on top, not the
serialization. Scripts are named `.docent.yaml` (a bare `.docent` works too).
The verbs deliberately encode *browser mechanics* rather than selectors: `convert:`
knows that the Hide-defaults checkbox reverts when the Assembly menu reloads,
`track: {clinvar: pack}` asks trackDb which containers and checkboxes that implies, and
`mouseover:` knows that a lifted track's DOM id gains a per-run `hub__` prefix. The author writes
intent; the renderer deals with the UI.
## Run
```
PW=/hive/groups/browser/uiTest/pw
PLAYWRIGHT_BROWSERS_PATH=$PW/browsers NODE_PATH=$PW/node_modules \
node docent.js AP1.docent.yaml
```
`docent.js` is not a single file: it requires `targetConf.js` from beside it, which
answers where a run is pointed (`target:` and `DOCENT_TARGET`), which `hg.conf` that
server reads, which hgcentral that names, and which account a `login:` step signs in
with. `tests/preflight.js` requires the same module, which is the point of it being one:
the fixture check has to resolve all four exactly the way the run will. Copy the pair, or
run `docent.js` where it sits.
Needs `playwright`, `js-yaml`, and `ffmpeg`. At UCSC these live in one pinned shared
install at `/hive/groups/browser/uiTest/pw` (`browsers` for Chromium, `node_modules`
for the modules), which every browser-driving test in the tree uses; its `README.md`
records the pinned versions. Point the two variables above anywhere you have them.
Outputs, relative to the script's own directory:
- mp4 → `../.mp4` (override with `mp4:` or a second argument)
- stills → `stills//.png` (override with `stills:`)
- sessions → `sessions//.txt` (override the parent with `sessions:`)
`docent.mk` in this directory has the make rules — include it from a project that keeps
a set of scripts and it rebuilds only the ones whose source changed. See the usage
comment at the top of that file.
## Top of file (all optional)
```yaml
target: genome-test # or rr, hgwdev, hgwbeta, hgwdev-, or a full https://.../cgi-bin
db: hg38 # source assembly
position: chr7:155.8M # starting position (tracked from then on; `go:` sets it too)
reset: true # cartReset first (clean cart + fresh quickLift hub)
size: [1000, 760] # viewport
pix: 850 # browser image width
pace: 1.2 # seconds to dwell after each step
shotHold: 2.2 # extra seconds the video pauses at a shot
trackAnim: false # skip the visible dropdown gesture on `track:` (nav only)
fast: false # true = figures only: no dwells, no cursor animation, no mp4
scale: 1 # >1 = print-resolution stills (see Print resolution)
pinMouseovers: false # record every mouseover for `pinShot:` (see below)
mp4: ../myname.mp4 # override output paths if you want
stills: stills/AP1
sessions: sessions # PARENT of the session dirs; files land in //
sessionUrlBase: https://hgwdev-you.gi.ucsc.edu/~you/docent/sessions # see Sessions
```
`target:` defaults to genome-test, so a script that forgets to say where it runs will
not quietly hit someone's personal sandbox.
## Print resolution
A screen still is about 120 dpi across a journal's 7-inch column — fine on a monitor,
too coarse to print. `scale:` (or `DOCENT_SCALE=k`, or `make hires`) renders **the same
tour with k times the pixels**, which is not the same thing as enlarging the stills
afterwards: a still only ever has the pixels it was drawn with, so every layer is asked
to draw more of them.
```
make hires # every scenario at 3x -> stills.hires//
make hires SCALE=2 # 2x
make hires BASES=BP1 # one scenario
DOCENT_SCALE=3 DOCENT_STILLS=stills.hires DOCENT_FAST=1 node docent.js BP1.docent.yaml
```
What k does:
- **deviceScaleFactor: k**, with the viewport left at its 1x CSS size. The page lays out
exactly as at 1x — same line breaks, same jQuery-dialog width, same tooltip placement —
and all of it rasterizes with k times the pixels.
- **`pix` × k, `textSize` stepped up with it** (hgTracks' size ladder: 6, 8, 10, 12, 14,
18, 24, 34; 3x lands exactly on 24). The server draws a genuinely wider image, and the
bigger font keeps its layout decisions proportional — tick spacing, room for labels,
how features pack into rows. A wider image with an 8px font would be a different
picture, not a bigger one.
- **`zoom: 1/k` on the image table**, handing that wider image the 1x amount of layout
space, so one image pixel lands on one device pixel. Native resolution, no resampling.
- **The tooltip font is pinned back to its 1x size.** hgTracks takes the tooltip's
font-size from the browser text size (`window.browserTextSize` → `hg/js/utils.js`
`addMouseover`), which `textSize × k` has just tripled — and then the device pixel ratio
scales the same text a second time. Left alone, a 3x still gets tooltips 3x too big: the
popups swamp the figure and the last one pinned falls off the crop.
- **Every drawn row asks for k times its height** (`