2b8d9275548bcc9bd161b68f24848b545e1069ef
braney
  Sun Sep 6 14:00:28 2026 -0700
docent: click raw:, and the first two scripts that do anything twice

Every one of the 29 scripts in the suite was a straight line: fresh cart, a few
steps, assert once.  A bug that only exists on the repeat was invisible to that
shape, and the closed-bug pool holds several.  These are the first two that
repeat a gesture.

rm36805  click a TOGA item, dismiss the pop-up, click the same item again.
One click passes on the broken build as happily as on the fixed one,
so the second click is the test.
rm27113  three clicks on the centre of the base-position ruler.  A single
click zooms 3x about the base under the cursor, and the bug drifted
that centre one base left each time.

Both needed a gesture the language could not express.  click: {track, item}
follows the item's own map-box href, which is right when the assertion is about
the hgc PAGE, but hgTracks answers a real click with an ajax DIALOG
(popUpHgcOrHgGene.hgc) and following the href never opens one.  raw: true
presses the mouse where a user presses it and lets the page answer -- a
navigation, a dialog, or a new image in place, whichever arrives, waited for
rather than slept through.  With no item name it is a bare point on the row,
which is the only way to click the ruler at all: the ruler carries no hgc map
boxes for areaXY to snap to.

Two things measured while writing these, both in the script comments.  jQuery UI
HIDES a dialog on close rather than removing it, so the assertion has to be
has: "#hgcDialog:visible" -- without :visible the second half of rm36805 passes
whether or not the pop-up ever comes back.  And one base is invisible at the
13kb windows the rest of the suite uses, where a pixel is fourteen bases, so
rm27113 starts at 100 bases and its three expected windows are arithmetic rather
than three observed strings: every one of them is centred on base 155,806,200.

#37014 is the other repeat-click bug in the pool and is deliberately NOT written.
Its reproducer no longer reaches the code it was about: 9e9ee32a4c8 (#37878)
later excluded crossTissue* tracks from the pop-up path altogether, and the
session's track is crossTissueMapsTissueCellType, so the click now navigates and
no dialog is involved.  Writing it against a different bar chart track would be
pinning the ticket to something it was never about.

refs #38252

diff --git src/hg/utils/docent/docent.js src/hg/utils/docent/docent.js
index 3646de3269b..9f5bc294467 100755
--- src/hg/utils/docent/docent.js
+++ src/hg/utils/docent/docent.js
@@ -1962,40 +1962,62 @@
       case 'shot': await shot(arg); return;                       // shot supplies its own dwell
       case 'pinShot': await pinShot(arg); break;                  // combined figure, off the mp4 timeline
       case 'montage': await montage(arg); break;                  // stills -> one multi-panel PNG
       case 'session': await session(arg); break;                  // the cart itself, as a loadable file
       case 'loadSession': await loadSession(arg); break;          // ... and back in again
       case 'expect': await expectState(arg); break;               // the one verb that can fail a run
       case 'mouseover': await mouseover(arg); return;             // supplies its own dwell (o.hold)
       // escape hatches
       case 'goto': await nav(arg); break;
       case 'click':
         if (arg && typeof arg === 'object' && arg.track) {
           // Click a track item -> follow its map-box link (e.g. the hgc detail page).
           // Named (item:/title:/value:) picks the item by identity; positional
           // (at:/frac:/x:) takes the box nearest that point, for a track whose items
           // cannot be named at all.
+          //
+          // `raw: true` presses the mouse where a user would press it and lets the page do
+          // whatever it does, instead of following the link. That is a different gesture,
+          // not a slower way to reach the same page: an item click on most tracks is
+          // answered by an ajax DIALOG, and the dialog is where a whole class of bug lives
+          // (hgTracks hanging on the SECOND click of the same item, #36805). Following the
+          // href never opens a dialog, so it can never see one. With raw: and no item name
+          // it is a bare point on the row -- the ruler carries no hgc map boxes at all, and
+          // a click on it is the gesture in #27113.
           const named = arg.item ?? arg.title ?? arg.value;
           const it = (named != null)
             ? await itemXY(arg.track, named,
                            arg.title != null && arg.item == null && arg.value == null)
-            : await areaXY(arg.track, arg);
+            : (arg.raw ? await posXY(arg.track, arg) : await areaXY(arg.track, arg));
           await glide(it.x, it.y); await sleep(200);
           // A raw click on the data area is swallowed by hgTracks' drag-select handler, so
-          // follow the item's own map-box link (the hgc detail page) directly.
-          if (it.href) await nav(it.href);
-          else { await page.mouse.click(it.x, it.y); await page.waitForLoadState('load').catch(() => {}); await captureState(); }
+          // by default follow the item's own map-box link (the hgc detail page) directly.
+          if (it.href && !arg.raw) await nav(it.href);
+          else {
+            const was = page.url();
+            await page.mouse.click(it.x, it.y);
+            // Answered in one of three ways depending on what was clicked: a navigation, an
+            // ajax dialog, or a new image swapped in place. Wait for whichever arrives
+            // rather than picking one, and never on a fixed sleep, which would flake on a
+            // slow hgc and waste the time on a fast one.
+            await Promise.race([
+              page.waitForSelector('.ui-dialog:visible', { timeout: 20000 }),
+              page.waitForFunction(u => location.href !== u, was, { timeout: 20000 }),
+            ]).catch(() => {});
+            await page.waitForSelector('#imgTbl', { timeout: 20000 }).catch(() => {});
+            await captureState();
+          }
           if (arg.shot) { await shot(arg.shot); return; }
         } else {
           // Plain selector click. Strip target=_blank first so an external link (e.g. a
           // dbSNP id -> NIH) navigates in THIS tab instead of a popup we can't screenshot,
           // then wait out the navigation so a following shot captures the destination page
           // (a no-op if the click didn't navigate).
           await page.evaluate(s => document.querySelectorAll(s).forEach(e => e.removeAttribute('target')), arg).catch(() => {});
           await clickGlide(arg);
           await page.waitForLoadState('load').catch(() => {});
           await captureState();
         }
         break;
       case 'hover': await glideTo(arg); await page.hover(arg); break;
       case 'wait': await page.waitForSelector(arg, { timeout: 15000 }); break;
       case 'sleep': await sleep(Number(arg)); return;