Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions tests/browser/interaction.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 34 additions & 22 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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); };
Expand Down Expand Up @@ -877,6 +879,7 @@ function buildLayerFilterButtons() {
function selectNet(netName) {
selectedNet = netName;
selectedFootprintIdx = null;
selectedBefore = null;
highlightedNet = netName;
highlightedFootprints = [];
hoverNets = [];
Expand Down Expand Up @@ -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); };
Expand Down Expand Up @@ -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];
Expand All @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -1316,6 +1328,7 @@ function deselectNet(netName) {

function deselect() {
selectedFootprintIdx = null;
selectedBefore = null;
selectedNet = null;
highlightedFootprints = [];
hoverNets = [];
Expand All @@ -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
Expand Down Expand Up @@ -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" });
Expand Down
Loading