diff --git a/tests/browser/pad-names.spec.js b/tests/browser/pad-names.spec.js new file mode 100644 index 0000000..1f95701 --- /dev/null +++ b/tests/browser/pad-names.spec.js @@ -0,0 +1,90 @@ +// Pad labels in the canvas tooltip and the detail pane's pad table. +// netdaq-small has a KiCad pad name on every pad, so a pad shows as the pin +// the netlist names (U24:11 is "pin 11"). ciaa-acc has no names, so its pads +// keep the old "pad N", N being the place in the footprint's pad list. + +const { test, expect } = require('@playwright/test'); +const fs = require('fs'); +const path = require('path'); + +const BOARDS = { + named: { + page: 'file://' + path.join(__dirname, 'board.html'), + fixture: path.join(__dirname, '..', 'fixtures', 'netdaq-small.json'), + }, + unnamed: { + page: 'file://' + path.join(__dirname, 'ciaa-acc.html'), + fixture: path.join(__dirname, '..', 'fixtures', 'ciaa-acc.json'), + }, +}; + +function footprint(board, ref) { + const data = JSON.parse(fs.readFileSync(BOARDS[board].fixture, 'utf8')); + return data.pcbdata.footprints.find((fp) => fp.ref === ref); +} + +async function load(page, board, ref) { + await page.goto('about:blank'); + await page.goto(BOARDS[board].page + `#comp=${ref}&side=F&zoom=selected`); + await page.waitForFunction(() => window.__pcbaTest && window.__pcbaTest.ready(), null, { timeout: 60000 }); + await page.waitForFunction(() => window.__pcbaTest.idle(), null, { timeout: 60000 }); +} + +// Move the pointer onto a front board point and return the tooltip text. +async function hover(page, [x, y]) { + const v = await page.evaluate(() => window.__pcbaTest.viewBox('F')); + const box = await page.locator('#frontcanvas').boundingBox(); + const fx = 0.5 + (x - v.cx) / v.w; + const fy = 0.5 + (y - v.cy) / v.h; + await page.mouse.move(box.x + box.width * fx, box.y + box.height * fy); + const tooltip = page.locator('#canvas-tooltip'); + await expect(tooltip).toBeVisible(); + return tooltip.innerText(); +} + +async function padTableNumbers(page) { + return page.locator('#detail-pane .pad-table tr td:first-child').allInnerTexts(); +} + +test('named board: hovering a pad shows its pin name, not its place in the list', async ({ page }) => { + const fp = footprint('named', 'U24'); + // ibom sorts pads by name as text, so the third pad is pin 11. + expect(fp.pads[2].name).toBe('11'); + await load(page, 'named', 'U24'); + + const text = await hover(page, fp.pads[2].pos); + expect(text).toContain('U24'); + expect(text).toContain('pin 11'); + expect(text).not.toContain('pad 3'); + + const pin1 = fp.pads.find((p) => p.pin1); + expect(await hover(page, pin1.pos)).toContain('pin ' + pin1.name); + + expect(await padTableNumbers(page)).toEqual(fp.pads.map((p) => p.name)); +}); + +test('unnamed board: hovering a pad still shows pad N', async ({ page }) => { + test.setTimeout(120000); + const fp = footprint('unnamed', 'U46'); + expect(fp.pads.every((p) => p.name === undefined)).toBe(true); + expect(fp.pads[3].pin1).toBeFalsy(); + await load(page, 'unnamed', 'U46'); + + const text = await hover(page, fp.pads[3].pos); + expect(text).toContain('U46'); + expect(text).toContain('pad 4'); + expect(text).not.toContain('pin 4'); + + expect(await padTableNumbers(page)).toEqual(fp.pads.map((p, i) => String(i + 1))); +}); + +test('the tooltip shows a pad name as text, not markup', async ({ page }) => { + await load(page, 'named', 'U24'); + const fp = footprint('named', 'U24'); + await page.evaluate(() => { + const fp = pcbdata.footprints.find((f) => f.ref === 'U24'); + fp.pads[2].name = 'X'; + }); + const text = await hover(page, fp.pads[2].pos); + expect(text).toContain('pin X'); +}); diff --git a/web/app.js b/web/app.js index 572e4a4..1563bbe 100644 --- a/web/app.js +++ b/web/app.js @@ -991,7 +991,8 @@ function renderDetailPane(fpIdx, showPads) { var tr = document.createElement("tr"); var tdNum = document.createElement("td"); - tdNum.textContent = i + 1; + // The pad name when the export has one, else the place in the list. + tdNum.textContent = padName(pad) || (i + 1); tr.appendChild(tdNum); var tdNet = document.createElement("td"); diff --git a/web/render.js b/web/render.js index 97766de..c15a272 100644 --- a/web/render.js +++ b/web/render.js @@ -1062,6 +1062,22 @@ function canvasHitScan(side, x, y) { return null; } +// What the viewer calls pad j of a footprint. A pad name from the CAD export +// ("11", "A1", "EP") is the pin as the netlist names it (REFDES:PIN), so it +// reads "pin 11". Exports without names (stock ibom, Allegro) only have the +// pad's place in the footprint's list, which is not the pin number, so that +// reads "pad N". +// The pad's name, or "" when it has none. Same test as the MCP tools, so +// both agree on which pads have a pin name. +function padName(pad) { + return typeof pad.name === "string" ? pad.name : ""; +} + +function padLabel(pad, j) { + if (padName(pad)) return "pin " + padName(pad); + return pad.pin1 ? "pin 1" : ("pad " + (j + 1)); +} + function padHitScan(layer, x, y) { if (!layerShows(layer, "pads")) return null; for (var i = 0; i < pcbdata.footprints.length; i++) { @@ -1069,8 +1085,7 @@ function padHitScan(layer, x, y) { for (var j = 0; j < fp.pads.length; j++) { var pad = fp.pads[j]; if (pad.layers.includes(layer) && pointWithinPad(x, y, pad)) { - var label = pad.pin1 ? "pin 1" : ("pad " + (j + 1)); - return { fpIdx: i, padLabel: label, net: pad.net }; + return { fpIdx: i, padLabel: padLabel(pad, j), net: pad.net }; } } } @@ -1266,7 +1281,7 @@ function handleMouseMove(e, layerdict) { var comp = pcbdata.components[padHit.fpIdx]; var line1 = fp.ref + (comp && comp.val ? " \u2014 " + comp.val : ""); var line2 = padHit.padLabel + (padHit.net ? " \u2014 " + padHit.net : ""); - tooltip.innerHTML = "" + line1 + "
" + line2 + ""; + tooltip.innerHTML = "" + escapeHtml(line1) + "
" + escapeHtml(line2) + ""; tooltip.style.display = "block"; tooltip.style.left = tipX + "px"; tooltip.style.top = tipY + "px";