app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
#!/usr/bin/env node
/* Behavioural smoke test for the dashboard page.
*
* WHY THIS EXISTS. 2.14.0 shipped with api() deleted by an over-greedy regex and 2.14.2
* shipped with shortState() and busySince missing. All three were invisible to 22 byte
* checks, because the markup and the CSS were genuinely correct. They were invisible to
* review. And they were invisible to the user too, because every call site catches its own
* error: the page answered with an empty prompts panel and a single red "dashboard not
* answering" pill while the server was serving 200s in four milliseconds.
*
* A structural audit cannot see a missing function. This runs the REAL page against the
* REAL server and asserts what a person would look at.
*
* Requires jsdom (npm i jsdom@22 inside tools/). No layout engine, so this proves
* BEHAVIOUR, not pixels; widths still have to be measured in a real webview.
*/
const { JSDOM, VirtualConsole } = require('jsdom');
const { spawn, execSync } = require('child_process');
const PORT = process.env.SMOKE_PORT || '8971';
const URL = `http://127.0.0.1:${PORT}/`;
const BIN = process.env.SMOKE_BIN || './target/release/adom-lbr';
// Selectors are the SHARED module's (.anp*), because the panel is no longer built by this
// page. If the module is not mounted these all read zero, which is the failure we want: a
// dashboard that silently drops the panel looks fine in a screenshot of the rest of the page.
const EXPECT = {
sayRows: n => n >= 3 && n <= 6, // newbie-panel: 3 to 6 rows, never zero
iconButtons: n => n >= 6, // two per row, copy and send
ledPills: n => n >= 5, // Binary, Wiki, Identity, Library, 3D viewer, Dock
demoButton: v => v === true,
moduleMounted: v => v === true,
};
(async () => {
// NEVER MEASURE A SERVER YOU DID NOT START. This app's resolve_port deliberately falls
// back to the next free port when its own is taken, so a lingering instance leaves the
// harness talking to a STALE build on the port it expected while the fresh one runs
// somewhere else. That produced a run where every check read zero and the page was in
// fact perfect. Refuse to start on an occupied port, then prove the version.
let occupied = false;
try { await fetch(URL); occupied = true; } catch (e) {}
if (occupied) {
console.log(`[smoke] FAIL port ${PORT} is already answering: something else is serving it.`);
console.log(' kill it, or run with SMOKE_PORT=<free port>. Refusing to measure a server I did not start.');
process.exit(1);
}
const srv = spawn(BIN, ['dashboard', 'serve', '--port', String(PORT), '--foreground'],
{ stdio: 'ignore', detached: true });
const done = code => { try { process.kill(-srv.pid); } catch (e) {} process.exit(code); };
for (let i = 0; i < 40; i++) {
try { await fetch(URL); break; } catch (e) { await new Promise(r => setTimeout(r, 250)); }
}
// Prove the thing answering is the build we just made, not a survivor from an earlier run.
const want = require('fs').readFileSync('VERSION', 'utf8').trim();
try {
const st = await (await fetch(URL + 'api/status')).json();
const bin = (st.leds || []).find(l => l.label === 'Binary');
const got = bin && String(bin.short || '').replace(/^v/, '');
if (got !== want) {
console.log(`[smoke] FAIL served version is ${got}, expected ${want}: measuring the wrong server.`);
done(1); return;
}
console.log(`[smoke] ok serving ${got} on port ${PORT}`);
} catch (e) {
console.log('[smoke] FAIL could not read /api/status to confirm the version: ' + e.message);
done(1); return;
}
const errs = [];
const vc = new VirtualConsole();
vc.on('jsdomError', e => errs.push(String(e.message || e)));
let html = await (await fetch(URL)).text();
// Make the page's own swallowing catches confess, in this COPY only. A caught error is
// still a defect; the user just never gets told about it.
html = html.replace(/\}catch\((\w+)\)\{/g, '}catch($1){ if(typeof console!=="undefined")console.error("SWALLOWED >>> "+($1&&$1.stack?$1.stack:$1));');
vc.on('error', (...a) => { const s = a.join(' '); if (/SWALLOWED/.test(s)) errs.push(s.split('\n')[0]); });
const dom = new JSDOM(html, {
url: URL, runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc,
resources: 'usable', // the page loads newbie-panel.js by <script src>; without this jsdom skips it
beforeParse(w) {
w.fetch = (p, o) => fetch(new w.URL(p, URL).href, o);
w.scrollTo = () => {};
w.matchMedia = w.matchMedia || (q => ({ matches: false, media: q, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){} }));
}
});
const d = dom.window.document;
await new Promise(r => setTimeout(r, 4000));
const got = {
sayRows: d.querySelectorAll('.anp-tbl tbody tr').length,
iconButtons: d.querySelectorAll('.anp-acts .anp-ib').length,
ledPills: d.querySelectorAll('.led-pill').length,
demoButton: !!d.querySelector('.anp-demo'),
moduleMounted: !!(dom.window.AdomNewbiePanel && d.querySelector('.anp')),
};
const ledText = [...d.querySelectorAll('.led-pill')].map(p => p.textContent.replace(/\s+/g, ' ').trim());
let bad = 0;
for (const [k, test] of Object.entries(EXPECT)) {
const ok = test(got[k]);
if (!ok) bad++;
console.log(`[smoke] ${ok ? 'ok ' : 'FAIL'} ${k} = ${got[k]}`);
}
// The specific shape of the 2.14.2 bug: the LED row falls back to one red pill.
if (ledText.length === 1 && /not answering/.test(ledText[0])) {
console.log('[smoke] FAIL LED row is the error fallback: ' + ledText[0]); bad++;
}
const real = errs.filter(e => /ReferenceError|TypeError|SyntaxError/.test(e));
if (real.length) {
bad++;
console.log('[smoke] FAIL javascript errors on load (including ones the page swallows):');
[...new Set(real)].slice(0, 6).forEach(e => console.log(' ' + e.slice(0, 160)));
}
// ---- APPEARANCE, not just behaviour ----------------------------------------------
// The 2.14.0 regex destroyed TWO things. The missing function broke behaviour and was
// caught. The missing STYLESHEET broke only appearance, so the Send-to-KiCad bar, the
// send-evidence panel and the demo floaty rendered as bare labels and unstyled buttons
// for five releases while every gate stayed green. jsdom has no layout engine but it DOES
// resolve the cascade, so a signature declared property per component is checkable here.
// An id-orphan scan was tried first and abandoned: most ids are styled through class or
// descendant rules, so it flagged 70 false positives and would have been switched off.
const LOOKS = [
['#sendbar', 'display', 'flex', 'the Send to KiCad / Fusion bar'],
['#sendbar button', 'borderRadius', /\d/, 'the send buttons'],
['#sendpanel', 'position', 'fixed', 'the send-evidence panel'],
['#tour-card', 'position', 'fixed', 'the demo narration card'],
['#tour-bar', 'borderRadius', '999px', 'the demo play/pause floaty'],
['#sp-console', 'fontFamily', /mono/, 'the streaming console'],
];
let looksBad = 0;
for (const [sel, prop, want, what] of LOOKS) {
const el = d.querySelector(sel);
if (!el) { bad++; looksBad++; console.log(`[smoke] FAIL ${what} (${sel}) is not in the page at all`); continue; }
const got = dom.window.getComputedStyle(el)[prop] || '';
const ok = want instanceof RegExp ? want.test(got) : got === want;
if (!ok) { bad++; looksBad++; console.log(`[smoke] FAIL ${what} is unstyled: ${sel} ${prop} = ${got || '(empty)'}`); }
}
// never print a pass line next to a failure: a summary that says ok while a check
// above it says FAIL is how a broken gate gets read as a working one
if (!looksBad) console.log('[smoke] ok ' + LOOKS.length + ' components carry their styling');
// GRADUATION IS NOT DISMISSAL. The panel folding on its own after enough launches must
// not be recorded as the user dismissing it, or the fold becomes permanent. The module
// reports who moved the panel (newbie-panel 2.1.0 onToggle(open, source)); this asserts
// this app reads it correctly, because the failure is silent and only shows up as a panel
// that never comes back.
try {
const w = dom.window;
const gradKey = Object.keys(w.localStorage).find(k => /graduation/.test(k));
const readGrad = () => gradKey ? w.localStorage.getItem(gradKey) : '';
// `let npanel` at script top level is NOT a window property, so w.npanel is undefined
// and an `if (w.npanel)` guard skips this check in silence, which is the same silent
// pass this whole file exists to abolish. Reach it through eval, in script scope.
const hasHandle = w.eval("typeof npanel !== 'undefined' && !!npanel");
if (!hasHandle) {
bad++;
console.log('[smoke] FAIL no panel handle in script scope: cannot verify graduation vs dismissal');
} else {
w.eval("npanel.close({remember:false})");
const afterApi = readGrad();
w.eval("npanel.open({remember:false})");
const xBtn = d.querySelector('.anp-x');
if (xBtn) xBtn.dispatchEvent(new w.MouseEvent('click', { bubbles: true }));
const afterUser = readGrad();
const apiClean = !/dismissed/.test(afterApi || '');
const userRecorded = /dismissed/.test(afterUser || '');
if (!apiClean) { bad++; console.log('[smoke] FAIL a programmatic close was recorded as a user dismissal'); }
if (!userRecorded) { bad++; console.log('[smoke] FAIL a user close was NOT recorded, so the panel returns after dismissal'); }
if (apiClean && userRecorded) console.log('[smoke] ok graduation close vs user dismissal are distinguished');
// ...and a user REOPEN has to survive a reload. The setup matters: with a FRESH
// profile the policy would keep the panel open anyway, so the check passes for the
// wrong reason and cannot fail. (It did exactly that on its first run, and only the
// negative control exposed it.) Graduate the user first, so the policy actively wants
// the panel folded and only a durable recall can keep it open.
w.eval("gradSave({launches:5})");
const tgl = d.querySelector('#btnHelp');
if (tgl) tgl.dispatchEvent(new w.MouseEvent('click', { bubbles: true }));
const survives = w.eval("(function(){gradRecall=false;return panelOpen();})()");
if (!survives) { bad++; console.log('[smoke] FAIL a user reopen does not survive a reload: graduation re-folds it'); }
else console.log('[smoke] ok a user reopen survives a reload');
}
} catch (e) { console.log('[smoke] note: could not exercise the toggle: ' + e.message); }
// The module proves its own conformance. jsdom has no layout engine, so the geometric
// rows are meaningless here and only the structural verdict is worth reading; the real
// selfCheck has to be run in ah's webview. Reported, never used as the pass condition.
try {
const sc = dom.window.AdomNewbiePanel && dom.window.AdomNewbiePanel.selfCheck();
if (sc) console.log('[smoke] module selfCheck (jsdom, no layout engine): ok=' + sc.ok +
' rows=' + (sc.rows || []).length);
} catch (e) { console.log('[smoke] module selfCheck threw: ' + e.message); }
console.log('[smoke] leds: ' + (ledText.join(' | ') || '(none)'));
console.log(bad ? `[smoke] FAILED ${bad} check(s)` : '[smoke] OK page renders and answers');
dom.window.close();
done(bad ? 1 : 0);
})().catch(e => { console.log('[smoke] FAIL ' + e.message); process.exit(1); });