074123ba2fb70164a7aa364f7c8baf21d70f55a5 braney Thu Aug 27 09:04:05 2026 -0700 docent: let expect: check the order rows were drawn in, refs #37892 rows: has been a set test, and exact: true only added "and nothing else", so no assertion in the language could fail because the rows came back in the wrong order. ordered: true adds that check: the named rows have to appear top to bottom in the order given. A row that was not drawn at all is reported once, by rows:, and skipped here rather than failing twice. A failure names the first pair that is inverted. The case that prompted it is #38032, a quickLift target returning its tracks in the order they were lifted rather than the order they have on the source. A tour of that bug passed every check on the broken build. Two tests: ordered asserts the hg38 order, and ordered.xfail names the same two rows backwards and has to fail, since a flag that cannot fail is only a second copy of the set test beside it. diff --git src/hg/utils/docent/docent.js src/hg/utils/docent/docent.js index e48111f3c4f..c8604e635d1 100755 --- src/hg/utils/docent/docent.js +++ src/hg/utils/docent/docent.js @@ -1170,30 +1170,31 @@ await nav(`/cgi-bin/hgTracks?hgS_doLoadUrl=submit&hgS_loadUrlName=${enc(String(from))}`); } await page.waitForSelector('#imgTbl').catch(() => {}); await captureState(); if (o.shot) await shot(o.shot); } // The only verb that can fail a run. Every other verb renders happily whatever it is // handed: a superTrack that came up whole and made an image 7,581 px tall, a subtrack // that never hid, a pinned tooltip that grabbed the neighbouring item, an Apache 414 // page where the view should be. All of those shipped once and all were caught by eye. // Stating the expectation instead stops the run, non-zero, at the step that broke it -- // `make` then fails rather than writing a wrong figure over a right one. // // expect: {rows: [ruler, mane]} these rows were drawn // expect: {rows: [ruler, mane], exact: true} ... and nothing else + // expect: {rows: [ruler, mane], ordered: true} ... in that order, top to bottom // expect: {noRows: [clinvarCnv]} this row was not // expect: {height: 2000} the still is no taller than this ("<1200" etc.) // expect: {tip: "mismatch A->C"} the tooltip now up says this // expect: {text: "...", noText: "..."} the page does / does not contain this // // `warn: true` downgrades a failure to a warning, for a check worth logging but not worth // stopping a build over. async function expectState(arg) { const o = (arg && typeof arg === 'object') ? arg : { text: arg }; const seen = await page.evaluate(() => { const im = document.getElementById('imgTbl'); const tip = document.getElementById('mouseoverContainer'); const up = tip && tip.offsetWidth > 0 && getComputedStyle(tip).display !== 'none' && getComputedStyle(tip).visibility !== 'hidden'; return { @@ -1207,30 +1208,46 @@ // the device pixel ratio -- which is what someone means by "7,581 px tall", and what a // print run makes k times bigger. const height = Math.round(seen.cssHeight * SCALE); // A hub track's row id carries a per-run hub_<n>_ prefix (a quickLift target's rows all // do), so match the plain name by suffix, the way `mouseover:` resolves a track. const drawn = w => seen.rows.some(r => r === w || r.endsWith('_' + w)); const list = v => v == null ? [] : (Array.isArray(v) ? v : [v]).map(String); const bad = []; const want = list(o.rows); const missing = want.filter(w => !drawn(w)); if (missing.length) bad.push(`rows not drawn: ${missing.join(', ')}`); if (o.exact && want.length) { const extra = seen.rows.filter(r => !want.some(w => r === w || r.endsWith('_' + w))); if (extra.length) bad.push(`unexpected rows: ${extra.join(', ')}`); } + // `ordered: true` makes rows: a check on the ORDER they were drawn in as well as on + // which ones were. seen.rows is in document order and hgTracks draws top to bottom, + // so the named rows have to appear at increasing positions. A row that was not drawn + // at all is already reported above, and is skipped here rather than producing a + // second failure saying the same thing. Without this a set test cannot fail for a + // wrong order, which is the whole of a bug like a quickLift target coming back in + // request order rather than source order (#38032). + if (o.ordered) { + const seq = want.map(w => ({ w, at: seen.rows.findIndex(r => r === w || r.endsWith('_' + w)) })) + .filter(e => e.at >= 0); + for (let i = 1; i < seq.length; i++) + if (seq[i].at < seq[i - 1].at) { + bad.push(`rows out of order: ${seq[i - 1].w} should be drawn above ${seq[i].w}`); + break; + } + } const banned = list(o.noRows).filter(w => drawn(w)); if (banned.length) bad.push(`rows that should not be drawn: ${banned.join(', ')}`); if (o.height != null) { // A bare number is a ceiling, which is the check anyone actually wants. const m = /^\s*(<=|>=|<|>|=)?\s*(\d+)\s*$/.exec(String(o.height)); if (!m) bad.push(`height: cannot read "${o.height}"`); else { const n = Number(m[2]), op = m[1] || '<='; const ok = op === '<' ? height < n : op === '>' ? height > n : op === '>=' ? height >= n : op === '=' ? height === n : height <= n; if (!ok) bad.push(`image is ${height}px, wanted ${op}${n}`); } } if (o.tip != null && !seen.tip.includes(String(o.tip))) bad.push(seen.tip ? `tooltip says "${seen.tip}", wanted "${o.tip}"`