4d991e3416f4ac769408dfd2f65d8fdb0f7fae9b
braney
  Wed Sep 16 12:36:41 2026 -0700
docent: a test for hgCollection, the one CGI nothing reached, refs #37892

hgCollection shares visibility logic with hgTracks by COPY rather than by call.
hg/hgCollection/hgCollection.c carries its own isParentVisible() at line 269, a
verbatim copy of the one in hg/lib/trackHub.c.  Nine scripts in tests/regress
caught the trackHub.c copy on the #37547 cart-visibility branch; nothing caught
this one, and it was found by grep afterwards.  This is the test that would have.

The copy's reach is narrower than it first looks, and the script says so: its only
caller is checkForVisible(), which builds the "Visible Tracks" folder at the top
of the builder's source tree.  It is NOT on the save path, so the bug drops a
track you have on in the browser out of that shortcut folder and leaves a saved
collection alone.

The assertion sits on the FOLDER'S OWN jsTree CLASS, which turned out to be a
cleaner statement of the bug than counting what is inside it.  addVisibleTracks
writes `,children:true` into the node only when checkForVisible() found
something, so the folder renders jstree-open when the cart has visible tracks and
jstree-leaf when it does not.  The leaves inside it are then named as well.

Three things the live page settled, each of which had been a guess:

* The folder arrives OPEN.  Clicking it CLOSES it.  The first draft clicked.
* A node's id is the track name, and the same id is used for that track under
its group folder, so both selectors are scoped to li#visible.  Unscoped, they
would also match the copy under Regulation the moment anyone opened it.
* wgEncodeBroadHistoneGm12878H3k4me1StdSig does not follow its siblings'
naming.  It is named in the assertion for that reason: a rename shows up here
rather than quietly halving what this checks.

The track cannot be chosen freely.  It has to be a leaf whose container is hidden
by default, or the test passes on a broken build (the rule in regress/README.txt:
a top-level track is added as a stub whatever the cart says, and a default-visible
child has its container in the list already).  It also has to be one hgCollection
will show at all -- trackCanBeAdded() at line 141 keeps only wig, bigWig and
bedGraph leaves, which is why this names a cell-line leaf and not the multiWig
container above it.  wgEncodeRegMarkH3k4me1H1hesc is both.  The noHas: names
wgEncodeRegMarkH3k27acH1hesc, whose container IS visible by default: it says the
hideKids reached hgCollection, so the has: is not passing because the whole
superTrack came up on its own.

The wait had to be restructured, and the reason generalises.  Waiting for the
folder's CHILDREN meant that a build with the bug -- where the folder is empty --
timed out after 15 seconds with a Playwright message instead of failing at the
expect: with what it wanted.  Waiting for either terminal state (jstree-open or
jstree-leaf) fixes it: measured on genome-test the folder settles about 30ms after
load whichever way it goes, and jstree-open lands about 11ms before the children,
which is why the child check gets its own wait after the class has been asserted.

Measured both ways.  The same script with the track hidden fails in about a
second:

step 9 (expect) failed: nothing matches "li#visible.jstree-open";
1 element(s) match "li#visible.jstree-leaf", wanted none

README.txt describes it, drops hgCollection from "Still to write", and records
what that section should now hold instead: a THIRD copy of isParentVisible(), in
hg/lib/hgFind.c at line 2957, feeding isTrackVisible() at 2977.  It decides
whether a search result lands under "Visible Tracks" or "Hidden Tracks" on
hgSearch, it is the same bug in the same shape, and it needs no login.

18 of 18 pass in tests/, and preflight resolves all four fixtures.

diff --git src/hg/utils/docent/tests/README.txt src/hg/utils/docent/tests/README.txt
index ce7cce1fa77..08df45d7358 100644
--- src/hg/utils/docent/tests/README.txt
+++ src/hg/utils/docent/tests/README.txt
@@ -87,30 +87,38 @@
 
   selftest      session: -> expect: -> loadSession:, on hg38 at SHH. Saves the cart,
                 changes the view, restores it from the local file, checks rows both times.
   heavysession  the same three steps as selftest, on a Recommended Track Set: 34 rows in,
                 saved, moved away, loaded back, `exact: true` on both halves. selftest
                 round-trips two rows, which barely reaches outIfNotPresent() in hgSession
                 -- the function that writes a trackDb default for every track that is
                 deliberately NOT in the cart, and the one a broken save-and-reload path
                 shows up in. A path that dropped four rows left selftest green.
   firstrequest  a track turned on has to be drawn by the request that turned it on, with
                 no `go:`, `open:` or `convert:` in between. The bug it exists for lags by
                 exactly one request, so any script that navigates before asserting reads a
                 correct image and passes. It names wgEncodeRegMarkH3k4me1 for the reason
                 in its header: a top-level track or a default-visible child would pass on
                 the broken build too.
+  collection    hgCollection, which no other script here or in regress/ reaches. It shares
+                visibility logic with hgTracks by COPY rather than by call:
+                hg/hgCollection/hgCollection.c carries its own isParentVisible(), a
+                verbatim copy of the one in hg/lib/trackHub.c, and it decides what goes in
+                the builder's "Visible Tracks" folder. Needs a login, so it is the one
+                script here that uses `login:`. Asserts on the folder's own jsTree class
+                first -- open when checkForVisible() found something, leaf when it did not
+                -- and then on the leaves inside it.
   composite     clinvar with clinvarCnv hidden: the two-request split (#37953). One
                 request would leave clinvarCnv_sel=1 and the CNV row drawn.
   views         hideKids on the VIEW that holds the subtrack, with the sibling views
                 hidden by name. Also covers the `_sel` checkbox, since the subtrack is
                 `parent wgEncodeRegDnaseSignal off`, and pins the superTrack side effect
                 below.
   views.xfail   the same thing aimed at the COMPOSITE instead, which loses the row.
                 Expected to fail.
   supertrack    varsInPubs hideKids + one member: `exact: true`, because a test that only
                 checked the member was present would pass with all six drawn.
   urllen        {cCREs: hideKids} must not become the 1701-variable, 42,020-character GET
                 that Apache answered with 414. Checks `noText: "Too Long"`, since a 414
                 renders as a perfectly good page; the derive baseline pins it at 3.
   customtrack   addCustomTrack: with inline BED, tabs and newlines surviving the trip.
   scale         a 3x run draws the same rows as a 1x one.
@@ -160,42 +168,37 @@
 --------------
 
   mouseover:      by item: on stacked items, and the timing case where a neighbour's
                   tooltip is still up on arrival
   pinShot:        several tooltips in one figure, cursors drawn
   convert:        quickLift onto a GenArk haplotype, hideDefaults re-checked -- note a
                   session taken after it cannot be checked in, see #38046
   drag:           each of then: zoom / highlight / cancel
   addHub:,
   addPublicHub:   the two hub attach paths (a stable hub URL is the hard part)
   montage:        panel order, lettering, a named shot that was never taken
   goShow:         the suggestion menu, including a `pick:` that matches nothing
   loadSession:    the three remote forms -- only the local-file form is covered
   the YAML lint   `{item:name}` with no space warns and drops the argument. This needs a
                   test that reads stderr, which the harness does not do yet.
-  hgCollection    no script here or in regress/ reaches that CGI at all, and it shares
-                  visibility logic with hgTracks by COPY rather than by call:
-                  hg/hgCollection/hgCollection.c carries its own isParentVisible(), a
-                  verbatim copy of the one in hg/lib/trackHub.c. The copy in trackHub.c
-                  was caught by nine scripts in regress/ on the #37547 branch; the copy in
-                  hgCollection.c was found by grep afterwards, and would have dropped a
-                  container's children out of a saved collection in the same silent way.
-                  A test needs a `collection:` verb: the page puts tracks into a
-                  collection by dragging between two jsTrees, and `drag:` is the
-                  genomic drag-select on the track image, not that. Its buttons are
-                  #newCollection, #doNewCollection, #saveCollections and #discardChanges,
-                  which is enough to open and save one but not to put a track in it.
+  hgFind's copy   there is a THIRD copy of isParentVisible(), in hg/lib/hgFind.c at line
+                  2957, feeding isTrackVisible() at 2977. It decides category->visibility,
+                  which is what puts a search result under "Visible Tracks" rather than
+                  "Hidden Tracks" on hgSearch (hgSearch.c:174, js/hgSearch.js:44). Same
+                  bug, same shape, and no login needed. wgEncodeGencodeBasicV49 is a
+                  searchable subtrack three levels under a hidden container, which is the
+                  right shape for it.
 
 A test that needs a stable server-side fixture (a hub, a custom track) should carry it
 in the script rather than assume something on disk.
 
 The one fixture that cannot be carried anywhere is a login. hgCollection refuses a
 visitor who is not signed in, and the login cookie is checked against a salted hash, so
 a script that needs that page uses the `login:` step, and the step reads an account from
 ~/.docentLogin. That file is one [section] per HGCENTRAL DATABASE, because an account is
 a row in gbMembers in one of them: genome-test, hgwdev, every sandbox and every ticket
 park read hgcentraltest and share one account, while hgwbeta and the RR are separate sets
 of accounts. Which central a server reads is read from its hg.conf rather than guessed
 from the host -- a sandbox can point itself somewhere else, and two on hgwdev do today.
 `make preflight` says which account and which central it resolved for the server being
 driven, and refuses a file that is readable by group or other. No password is ever
 printed and none can be written in a script. ../README.md under `login` has the format.