756582322f8a53fef19e8c24ba902e353ba3623f
chmalee
  Thu Aug 27 14:38:34 2026 -0700
uiTest: shared browser UI test harness, plus an hgTracks example, refs #38188

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/hg/utils/uiTest/tests/t02-needs.js src/hg/utils/uiTest/tests/t02-needs.js
new file mode 100644
index 00000000000..d52c75141ab
--- /dev/null
+++ src/hg/utils/uiTest/tests/t02-needs.js
@@ -0,0 +1,56 @@
+// What a test's `needs` block decides, before any browser launches: run, skip
+// with a reason, or refuse outright.
+//
+// The one that matters most is the last: a test that writes to a server must
+// never run against production, and that is not skippable. Skipping it would
+// mean a green run that quietly did nothing, which is how somebody eventually
+// uploads a qa123456 hub to genome.ucsc.edu.
+
+'use strict';
+
+const t = require('./assert');
+const { resolveNeeds } = require('../lib/run');
+const { EXIT } = require('../lib/env');
+
+const dev = {
+    target: 'hgwdev', isRr: false, canWrite: true, canHgsql: true,
+    account: { name: 'qa', user: 'someoneQa' },
+    accountReason: '', hgsqlReason: '', confFile: '/home/someone/.hg.uiTest.conf',
+};
+const bare = {
+    target: 'genome-test', isRr: false, canWrite: true, canHgsql: false,
+    account: null,
+    accountReason: 'no conf file at /home/someone/.hg.uiTest.conf',
+    hgsqlReason: 'can.hgsql is not set in /home/someone/.hg.uiTest.conf',
+    confFile: '/home/someone/.hg.uiTest.conf',
+};
+const rr = { ...dev, target: 'rr', isRr: true, account: null, accountReason: 'production' };
+
+t.is(resolveNeeds(undefined, bare), null, 'a test with no needs block always runs');
+t.is(resolveNeeds({}, bare), null, 'an empty needs block always runs');
+t.is(resolveNeeds({ login: false, hgsql: false, write: false }, bare), null,
+    'a needs block that asks for nothing always runs');
+
+t.is(resolveNeeds({ login: true }, dev), null, 'needs.login runs when an account is configured');
+t.is(resolveNeeds({ login: true }, bare), bare.accountReason,
+    'needs.login skips with the reason when there is no account');
+
+t.is(resolveNeeds({ hgsql: true }, dev), null, 'needs.hgsql runs where hgsql is available');
+t.is(resolveNeeds({ hgsql: true }, bare), bare.hgsqlReason,
+    'needs.hgsql skips off a machine with hgsql');
+
+t.is(resolveNeeds({ write: true }, dev), null, 'needs.write runs where can.write is set');
+t.ok(/can\.write is not set/.test(resolveNeeds({ write: true }, { ...dev, canWrite: false })),
+    'needs.write skips where can.write is off');
+
+const e = t.throws(() => resolveNeeds({ write: true }, rr), /production/,
+    'needs.write against rr is refused outright');
+t.is(e && e.exitCode, EXIT.CONFIG, 'and it exits 2, a configuration error');
+
+// The refusal comes first, so a machine that also has can.write off still gets
+// the loud answer rather than a quiet skip.
+const e2 = t.throws(() => resolveNeeds({ write: true }, { ...rr, canWrite: false }), /production/,
+    'the rr refusal beats every skip');
+t.is(e2 && e2.exitCode, EXIT.CONFIG, 'and it is still exit 2');
+
+t.done('t02-needs');