6f3f3596f3dc61a8a738717b9701e69a86b5bb0b
braney
Mon Sep 21 17:24:33 2026 -0700
docent: wait: {gone:} for a selector to leave, and rm38257 stops racing itself
rm38257 went red in the 2026-09-21 nightly with the #38257 fix still live on
genome-test. The script was waiting on the wrong half of the click.
hgHubConnect.js switches the tab synchronously inside the click dispatch, while
topLinks.js closes the Account popup from a setTimeout(..., 0). So the tab is
the half that settles FIRST, and waiting for it returned a tick early: the
expect after it read a page that still had the popup on it. Measured with a
MutationObserver over the click, the tab goes active at t=43ms and the popup
goes on the next task.
wait: now also takes {gone: <selector>}, which waits for a selector to leave the
DOM, so a script can wait on the vanishing half of an answer. rm38257 uses it.
Six runs each way on genome-test, same build, only the script differing: 6 of 6
green with the new wait:, 3 of 6 red with the old one, failing at the same step
with the same message the nightly printed. The whole directory is green, 98 of
98.
refs #37892, refs #38252
diff --git src/hg/utils/docent/docent.js src/hg/utils/docent/docent.js
index bfd84537ada..84fc05298e5 100755
--- src/hg/utils/docent/docent.js
+++ src/hg/utils/docent/docent.js
@@ -2416,31 +2416,46 @@
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 'wait': {
+ // A selector to wait FOR, or {gone: <sel>} for one to wait OUT. Both directions are
+ // needed because a script that asserts what a click did has to wait on the half of the
+ // answer that settles LAST, and that is not always the half that appears. #38257 is the
+ // case that forced it: hgHubConnect.js switches the tab inside the click dispatch, while
+ // topLinks.js closes the Account popup from a setTimeout(..., 0), so waiting for the tab
+ // returns a tick early and the popup is still on the page. That is a race the script
+ // loses about half the time, and it reads as the fix having come undone.
+ if (arg && typeof arg === 'object') {
+ if (!arg.gone) throw new Error('wait: an object argument takes gone: <selector>');
+ await page.waitForSelector(arg.gone, { state: 'detached', timeout: 15000 });
+ } else {
+ await page.waitForSelector(arg, { timeout: 15000 });
+ }
+ break;
+ }
case 'sleep': await sleep(Number(arg)); return;
default: console.warn('unknown verb:', verb);
}
await sleep(PACE);
}
if (SCALE > 1)
console.log(`scale: ${SCALE}x -- pix=${PIX}, textSize=${TEXTSIZE}, dpr=${SCALE} at ${VW}x${VH}, `
+ `stills only (no mp4) -> ${STILLDIR}`);
if (doc.reset) await page.goto(absurl('/cgi-bin/cartReset?skipLs=1'), { waitUntil: 'domcontentloaded' });
const steps = doc.steps || [];
const timing = [];
for (let i = 0; i < steps.length; i++) {
const s = norm(steps[i]);
const t0 = Date.now();