app
Adom Chip Fetcher
Public Made by Adomby adom
Your whole parts library — manufacturer-grade chip CAD (symbol, footprint, 3D) one tap from your EDA tool.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
// Ralph-loop self-test for the chip-fetcher dashboard. Drives the REAL UI in
// headless Chromium at a phone viewport, exercises the source carat + zoom, and
// asserts + screenshots so regressions are caught here, not on John's phone.
//
// node chip-fetcher/selftest.cjs # full run
// Screenshots → /tmp/selftest/*.png
const puppeteer = require('/home/adom/project/node_modules/puppeteer-core');
const fs = require('fs');
const CHROME = '/home/adom/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome';
const BASE = 'http://127.0.0.1:8786/';
const OUT = '/tmp/selftest';
const MPN = process.argv[2] || 'BMI270';
const PROJ = process.argv[3] || 'Multi-source demo 2 (BMI270)';
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
fs.mkdirSync(OUT, { recursive: true });
const checks = [];
const check = (name, ok, detail = '') => { checks.push({ name, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); };
(async () => {
const browser = await puppeteer.launch({ executablePath: CHROME, headless: 'new', args: ['--no-sandbox', '--disable-gpu'] });
const page = await browser.newPage();
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2, isMobile: true, hasTouch: true });
page.on('pageerror', e => console.log('PAGEERROR', e.message));
const url = `${BASE}?project=${encodeURIComponent(PROJ)}&chip=${encodeURIComponent(MPN)}`;
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
await page.waitForSelector('.card', { timeout: 15000 });
await sleep(1200);
await page.screenshot({ path: `${OUT}/1-card.png` });
// The deep-linked chip is present + filtered.
const cards = await page.$$eval('.card', els => els.map(e => e.getAttribute('data-mpn')));
check('deep-link filters to the chip', cards.length === 1 && cards[0] === MPN, `cards=${JSON.stringify(cards)}`);
// Footprint carat: list sources + the canonical (current) FP thumbnail must NOT
// be the ds2sf one (the bug John found).
const fpInfo = await page.evaluate(() => {
const grp = [...document.querySelectorAll('.thumb-src-group')].find(g => g.querySelector('.thumb[data-thumb-kind="footprint"]'));
if (!grp) return { err: 'no footprint group' };
const chip = grp.querySelector('.src-chip-lab')?.textContent;
const img = grp.querySelector('.thumb img');
const opts = [...grp.querySelectorAll('.src-opt')].map(o => o.querySelector('.src-opt-lab').textContent);
return { chip, imgSrc: img?.src, opts };
});
check('FP carat lists multiple sources', (fpInfo.opts || []).length >= 3, `opts=${JSON.stringify(fpInfo.opts)}`);
check('FP tile defaults to a vendor source (not ds2sf)', /SnapEDA|Ultra|Component/.test(fpInfo.chip || ''), `chip=${fpInfo.chip}`);
// ds2sf with no pads must NOT appear as a footprint source.
check('ds2sf not listed as a FP source when it has no footprint', !(fpInfo.opts || []).some(o => /ds2sf/i.test(o)), `opts=${JSON.stringify(fpInfo.opts)}`);
// "Open in" menu (detail panel) exposes every tool for the chip.
const tools = await page.$$eval('.openin-row .openbtn', els => els.map(e => e.textContent));
check('"Open in" lists adom-symbol/footprint/lbr/3D', ['adom-symbol', 'adom-footprint', 'adom-lbr', 'adom-3D'].every(t => tools.includes(t)), `tools=${JSON.stringify(tools)}`);
check('"Open in" includes chipsmith', tools.includes('chipsmith'));
check('"Open in" includes ds2sf extraction', tools.includes('ds2sf extraction'));
// Detail panel is tabbed; Actions is the default (Files must NOT dominate).
const tabs = await page.evaluate(() => {
const t = [...document.querySelectorAll('.dtab')].map(x => x.textContent.trim());
const onTab = document.querySelector('.dtab.on')?.textContent.trim();
const filesPanelVisible = document.querySelector('.dpanel[data-panel="files"]')?.classList.contains('on');
return { t, onTab, filesPanelVisible };
});
check('detail panel is tabbed (Actions/Details/Files)', tabs.t.some(x => x.startsWith('Actions')) && tabs.t.some(x => x.startsWith('Files')), `tabs=${JSON.stringify(tabs.t)}`);
check('Actions is the default tab (Files not dominating)', /Actions/.test(tabs.onTab || '') && !tabs.filesPanelVisible, `onTab=${tabs.onTab}`);
await page.evaluate(() => document.querySelector('.openin-row')?.scrollIntoView());
await page.screenshot({ path: `${OUT}/4-detail-tabs.png` });
// The canonical FP thumbnail must actually load (not broken) and not be ds2sf-synth.
const fpThumb = await page.evaluate(async () => {
const grp = [...document.querySelectorAll('.thumb-src-group')].find(g => g.querySelector('.thumb[data-thumb-kind="footprint"]'));
const img = grp.querySelector('.thumb img');
const ok = img.complete && img.naturalWidth > 0;
let body = '';
try { body = await (await fetch(img.src)).text(); } catch {}
return { ok, hasDs2sf: /ds2sf|SYNTHESI/i.test(body) };
});
check('canonical FP thumbnail loads (not broken)', fpThumb.ok);
check('canonical FP thumbnail is NOT ds2sf-synthesized', !fpThumb.hasDs2sf);
// Switch the FP source via the carat → label sticks + tile re-renders.
await page.evaluate(() => {
const grp = [...document.querySelectorAll('.thumb-src-group')].find(g => g.querySelector('.thumb[data-thumb-kind="footprint"]'));
grp.querySelector('.src-chip').click();
});
await sleep(300);
const switched = await page.evaluate(() => {
const pop = [...document.querySelectorAll('.src-popover')].find(p => p.offsetParent !== null);
const ul = [...pop.querySelectorAll('.src-opt')].find(o => /Ultra/.test(o.textContent));
ul.click();
const grp = ul.closest('.thumb-src-group');
return grp.querySelector('.src-chip-lab').textContent;
});
await sleep(400);
check('picking a source updates the carat label (sticks)', /Ultra/.test(switched), `label=${switched}`);
await page.screenshot({ path: `${OUT}/2-source-switched.png` });
// Zoom: tap the zoom button → full-screen overlay with a visible X.
await page.evaluate(() => { document.querySelector('.card .zoom-btn').click(); });
await sleep(700);
const zoom = await page.evaluate(() => {
const bd = document.querySelector('.card-zoom-backdrop');
const x = document.querySelector('.card-zoom-x');
const wrap = document.querySelector('.card-zoomed-wrapper');
const fills = wrap ? wrap.getBoundingClientRect().width / window.innerWidth : 0;
return { hasBackdrop: !!bd, fullscreen: bd?.classList.contains('fullscreen'), hasX: !!x && getComputedStyle(x).display !== 'none', widthFrac: fills };
});
check('zoom opens an overlay', zoom.hasBackdrop);
check('zoom is full-screen on phone', zoom.fullscreen && zoom.widthFrac > 0.85, `widthFrac=${zoom.widthFrac.toFixed(2)}`);
check('zoom has a visible X close button', zoom.hasX);
// The zoom focus view exposes Open-in + Send-to (reachable on a phone).
const zoomActions = await page.evaluate(() => ({
open: [...document.querySelectorAll('.card-zoomed-wrapper .openin-row .openbtn')].map(b => b.textContent),
send: [...document.querySelectorAll('.card-zoomed-wrapper .sendto-row .sendbtn')].map(b => b.textContent),
}));
check('zoom view has Open-in actions', zoomActions.open.includes('adom-symbol') && zoomActions.open.includes('chipsmith'), `open=${JSON.stringify(zoomActions.open)}`);
check('zoom view has Send-to actions', zoomActions.send.length === 4, `send=${JSON.stringify(zoomActions.send)}`);
await page.screenshot({ path: `${OUT}/3-zoom.png` });
// Tapping "Open in adom-symbol" from the zoom actually opens a NEW TAB
// (window.open) — the phone-correct method.
const before = (await browser.pages()).length;
await page.evaluate(() => [...document.querySelectorAll('.card-zoomed-wrapper .openin-row .openbtn')].find(b => b.textContent === 'adom-symbol').click());
await sleep(2500);
const after = (await browser.pages()).length;
check('opening adom-symbol spawns a new browser tab (window.open)', after > before, `pages ${before}→${after}`);
const toast = await page.evaluate(() => document.getElementById('cf-toast')?.textContent || '');
check('user is told the open method', /new browser tab|window\.open|Hydrogen/i.test(toast), `toast="${toast.trim()}"`);
// X closes it.
await page.evaluate(() => document.querySelector('.card-zoom-x').click());
await sleep(500);
const closed = await page.$('.card-zoom-backdrop');
check('X closes the zoom (back to index)', !closed);
await browser.close();
const fails = checks.filter(c => !c.ok);
console.log(`\n${checks.length - fails.length}/${checks.length} checks passed.`);
fs.writeFileSync(`${OUT}/result.json`, JSON.stringify(checks, null, 2));
process.exit(fails.length ? 1 : 0);
})().catch(e => { console.error('SELFTEST ERROR', e); process.exit(2); });