diff --git a/tests/browser/interaction.spec.js b/tests/browser/interaction.spec.js index 2fcba23..60ee738 100644 --- a/tests/browser/interaction.spec.js +++ b/tests/browser/interaction.spec.js @@ -379,6 +379,151 @@ test('clicking a part row again unpins and deselects it', async ({ page }) => { await expect(page.locator('#detail-pane')).toBeHidden(); }); +test('clicking a part row again gives the selection back', async ({ page }) => { + await load(page); + const x = 0; + const y = 1; + await page.locator('#tab-components').click(); + const rowFor = (i) => page.locator(`#comp-tbody .comp-row[data-idx="${i}"]`); + await rowFor(x).click(); + await settle(page); + await rowFor(y).click(); + await settle(page); + expect((await state(page)).selectedFootprintIdx).toBe(y); + + await rowFor(y).click(); + await settle(page); + expect((await state(page)).selectedFootprintIdx).toBe(x); + expect(await page.evaluate((y) => !!pinnedComponents[y], y)).toBe(false); + await expect(rowFor(x)).toHaveClass(/selected/); + await expect(page.locator('#detail-pane .detail-ref')).toHaveText(footprints[x].ref); +}); + +test('clicking a part on the board again gives the selection back', async ({ page }) => { + await load(page); + const x = footprints.findIndex((fp) => fp.layer === 'F'); + await page.locator('#tab-components').click(); + await page.locator(`#comp-tbody .comp-row[data-idx="${x}"]`).click(); + await settle(page); + await page.keyboard.press('f'); + await waitIdle(page); + // match() runs in the page, so it compares with the page's selected part, x. + const spot = await canvasSpot(page, (hit) => 'footprint' in hit && hit.footprint !== selectedFootprintIdx); + const y = spot.hit.footprint; + + await clickCanvas(page, spot); + expect((await state(page)).selectedFootprintIdx).toBe(y); + await clickCanvas(page, spot); + expect((await state(page)).selectedFootprintIdx).toBe(x); + expect(await page.evaluate((y) => !!pinnedComponents[y], y)).toBe(false); + await expect(page.locator('#detail-pane .detail-ref')).toHaveText(footprints[x].ref); +}); + +// A part X on another net is selected, then a part Y is clicked in the net pane. +async function selectPartThenNetPanePart(page) { + const net = frontNets[0]; + await selectNetFromList(page, net); + const x = footprints.findIndex((fp, i) => !netToFootprints[net].has(i)); + expect(x).toBeGreaterThanOrEqual(0); + await page.locator('#tab-components').click(); + await page.locator(`#comp-tbody .comp-row[data-idx="${x}"]`).click(); + await settle(page); + await page.locator('#tab-nets').click(); + + const yRef = await page.locator('#net-results .net-comp-row .net-comp-ref').first().textContent(); + const y = footprints.findIndex((fp) => fp.ref === yRef); + await page.locator('#net-results .net-comp-row').first().click(); + await settle(page); + return { x, y }; +} + +test('a part clicked in the net pane becomes the selected part', async ({ page }) => { + await load(page); + const { x, y } = await selectPartThenNetPanePart(page); + + expect((await state(page)).selectedFootprintIdx).toBe(y); + expect(await page.evaluate(([x, y]) => [!!pinnedComponents[x], !!pinnedComponents[y]], [x, y])) + .toEqual([true, true]); + await expect(page.locator('#net-comp-detail .detail-ref')).toHaveText(footprints[y].ref); + await expect(page.locator('#detail-pane .detail-ref')).toHaveText(footprints[y].ref); + await expect(page.locator(`#comp-tbody .comp-row[data-idx="${y}"]`)).toHaveClass(/selected/); + expect(await page.evaluate(() => location.hash)).not.toContain('focus='); +}); + +test('a part clicked again in the net pane gives the selection back', async ({ page }) => { + await load(page); + const { x, y } = await selectPartThenNetPanePart(page); + await page.locator('#net-results .net-comp-row', { + has: page.locator('.net-comp-ref', { hasText: new RegExp('^' + escapeRe(footprints[y].ref) + '$') }), + }).click(); + await settle(page); + + expect((await state(page)).selectedFootprintIdx).toBe(x); + expect(await page.evaluate((y) => !!pinnedComponents[y], y)).toBe(false); + await expect(page.locator('#net-comp-detail .detail-ref')).toHaveText(footprints[x].ref); + await expect(page.locator('#detail-pane .detail-ref')).toHaveText(footprints[x].ref); + await expect(page.locator(`#comp-tbody .comp-row[data-idx="${x}"]`)).toHaveClass(/selected/); + expect(await page.evaluate(() => location.hash)) + .not.toMatch(new RegExp('comp=' + escapeRe(footprints[y].ref) + '(&|$)')); +}); + +test('a part taken off in the net pane that is not selected leaves the selection', async ({ page }) => { + await load(page); + const { y } = await selectPartThenNetPanePart(page); + const rowFor = (ref) => page.locator('#net-results .net-comp-row', { + has: page.locator('.net-comp-ref', { hasText: new RegExp('^' + escapeRe(ref) + '$') }), + }); + const refs = page.locator('#net-results .net-comp-row .net-comp-ref'); + expect(await refs.count()).toBeGreaterThan(1); + const zRef = await refs.nth(1).textContent(); + const z = footprints.findIndex((fp) => fp.ref === zRef); + expect(z).not.toBe(y); + await rowFor(zRef).click(); + await settle(page); + await rowFor(footprints[y].ref).click(); + await settle(page); + + expect((await state(page)).selectedFootprintIdx).toBe(z); + await expect(page.locator('#net-comp-detail .detail-ref')).toHaveText(zRef); + await expect(page.locator('#detail-pane .detail-ref')).toHaveText(zRef); + + await rowFor(zRef).click(); + await settle(page); + expect((await state(page)).selectedFootprintIdx).toBeNull(); +}); + +test('a part shown in the net pane is hidden there when its pin comes off elsewhere', async ({ page }) => { + await load(page); + const { x, y } = await selectPartThenNetPanePart(page); + const w = footprints.findIndex((fp, i) => i !== x && i !== y); + await page.locator('#tab-components').click(); + await page.locator(`#comp-tbody .comp-row[data-idx="${w}"]`).click(); + await settle(page); + await expect(page.locator('#net-comp-detail .detail-ref')).toHaveText(footprints[y].ref); + await page.locator(`#comp-tbody .comp-row[data-idx="${y}"]`).click(); + await settle(page); + + expect((await state(page)).selectedFootprintIdx).toBe(w); + expect(await page.evaluate((y) => !!pinnedComponents[y], y)).toBe(false); + await page.locator('#tab-nets').click(); + await expect(page.locator('#net-results')).toBeVisible(); + await expect(page.locator('#net-comp-detail')).toBeHidden(); +}); + +test('a part clicked again in the net pane with nothing before it deselects', async ({ page }) => { + await load(page); + await selectNetFromList(page, frontNets[0]); + const row = page.locator('#net-results .net-comp-row').first(); + await row.click(); + await settle(page); + await page.locator('#net-results .net-comp-row').first().click(); + await settle(page); + + expect((await state(page)).selectedFootprintIdx).toBeNull(); + await expect(page.locator('#net-comp-detail')).toBeHidden(); + await expect(page.locator('#detail-pane')).toBeHidden(); +}); + // #1: the nets stayed in the breadcrumbs, but the canvas showed only the // component. Hovering a row did not stash highlightedNetPath, so leaving the // row cleared it. diff --git a/web/app.js b/web/app.js index 06c9546..851bdbb 100644 --- a/web/app.js +++ b/web/app.js @@ -22,6 +22,8 @@ var componentByRef = {}; // UPPER ref → fpIdx // Selection state var selectedFootprintIdx = null; +var selectedBefore = null; // part selected before the last click took over +var netPaneDetailIdx = null; // part shown in the net pane detail var selectedNet = null; var highlightedFootprints = []; var highlightedNet = null; @@ -535,6 +537,8 @@ function togglePinComponent(fpIdx) { if (pinnedComponents[fpIdx]) { delete pinnedComponents[fpIdx]; unregisterSelection('comp', fpIdx); + // A part that loses its pin is not shown in the net pane detail. + if (netPaneDetailIdx === fpIdx) renderDetailPane(null, false); } else { registerSelection('comp', fpIdx); pinnedComponents[fpIdx] = getSelectionColor('comp', fpIdx); @@ -636,9 +640,7 @@ function populateComponentList() { tr.addEventListener("click", function(e) { var idx = parseInt(this.dataset.idx); hoverClear(); // the pin shows the part from here on - togglePinComponent(idx); - if (pinnedComponents[idx]) selectFootprint(idx, true); - else deselectFootprint(idx); + clickFootprint(idx, true); }); tr.addEventListener("mouseenter", (function(idx) { return function() { hoverFootprint(idx); }; @@ -877,6 +879,7 @@ function buildLayerFilterButtons() { function selectNet(netName) { selectedNet = netName; selectedFootprintIdx = null; + selectedBefore = null; highlightedNet = netName; highlightedFootprints = []; hoverNets = []; @@ -957,21 +960,7 @@ function populateNetResults(netName) { row.addEventListener("click", function(e) { hoverClear(); - togglePinComponent(fpIdx); - if (!pinnedComponents[fpIdx]) { - renderDetailPane(null, false); - deselectFootprint(fpIdx); - return; - } - renderDetailPane(fpIdx, false); - updateCompListSelection(fpIdx); - redrawAllIfDone(); - var targetLayer = pcbdata.footprints[fpIdx].layer; - var canvasdict = targetLayer === "B" ? allcanvas.back : allcanvas.front; - if (settings.canvaslayout !== "FB" && settings.canvaslayout !== targetLayer) { - setCanvasLayout(targetLayer); - } - zoomToFootprint(fpIdx, canvasdict); + if (clickFootprint(fpIdx, true)) renderDetailPane(selectedFootprintIdx, false); }); row.addEventListener("mouseenter", (function(idx) { return function() { hoverFootprint(idx); }; @@ -1038,6 +1027,7 @@ function populateNetResults(netName) { function selectFootprint(fpIdx, zoomTo) { selectedFootprintIdx = fpIdx; + selectedBefore = null; // Canvas color comes from pinnedComponents; highlightedFootprints is hover-only. var fp = pcbdata.footprints[fpIdx]; @@ -1059,10 +1049,31 @@ function selectFootprint(fpIdx, zoomTo) { } } +// A click on a part in the list, the net pane or the canvas. The first click +// pins the part and selects it. A second click on the selected part takes its +// pin off and selects the part before it again, if that part is still pinned. +// A click on a pinned part that is not selected only takes its pin off. +// Returns true when the click changed the selected part. +function clickFootprint(fpIdx, zoomTo) { + togglePinComponent(fpIdx); + if (pinnedComponents[fpIdx]) { + var before = selectedFootprintIdx; + selectFootprint(fpIdx, zoomTo); + selectedBefore = before !== fpIdx ? before : null; + return true; + } + if (selectedFootprintIdx !== fpIdx) return false; + var back = selectedBefore; + if (back !== null && pinnedComponents[back]) selectFootprint(back, false); + else deselectFootprint(fpIdx); + return true; +} + // A part clicked again after its pin came off is no longer the selected part. function deselectFootprint(fpIdx) { if (selectedFootprintIdx !== fpIdx) return; selectedFootprintIdx = null; + selectedBefore = null; renderDetailPane(null); updateCompListSelection(null); updateHashFromSelection(); @@ -1083,6 +1094,7 @@ function renderDetailPane(fpIdx, showPads) { if (showPads === undefined) showPads = true; // When showPads is false we're in net context — render into the net panel's detail area var paneId = showPads ? "detail-pane" : "net-comp-detail"; + if (!showPads) netPaneDetailIdx = fpIdx; var pane = document.getElementById(paneId); var handle = document.getElementById("detail-resize-handle"); if (fpIdx === null) { @@ -1316,6 +1328,7 @@ function deselectNet(netName) { function deselect() { selectedFootprintIdx = null; + selectedBefore = null; selectedNet = null; highlightedFootprints = []; hoverNets = []; @@ -1326,6 +1339,7 @@ function deselect() { linkMarkers = []; renderBreadcrumbs(); renderDetailPane(null); + renderDetailPane(null, false); document.querySelectorAll(".comp-row.selected").forEach(r => r.classList.remove("selected")); document.querySelectorAll(".net-comp-row.selected").forEach(r => r.classList.remove("selected")); // Return to search panel @@ -1382,12 +1396,10 @@ function onNetClickedFromCanvas(netName) { } function onFootprintClickedFromCanvas(fpIdx) { - togglePinComponent(fpIdx); - if (!pinnedComponents[fpIdx]) { deselectFootprint(fpIdx); return; } - selectFootprint(fpIdx, false); + if (!clickFootprint(fpIdx, false) || selectedFootprintIdx === null) return; // Switch to components tab to show the selection switchTab("components"); - updateCompListSelection(fpIdx); + updateCompListSelection(selectedFootprintIdx); // Scroll comp list to selected var row = document.querySelector("#comp-tbody .comp-row.selected"); if (row) row.scrollIntoView({ block: "nearest" });