From af39225688b5dc56dee74fd2729def04510d581f Mon Sep 17 00:00:00 2001 From: David Montero Date: Sun, 31 May 2026 23:23:34 +0200 Subject: [PATCH] refactor(sim): single-board example load rebuilds from scratch Cleaner follow-up to the multi-board residue fix. Instead of removing the extra boards and retyping the surviving one (which left a stale id such as "stm32-bluepill" on what was now an Arduino Uno), the single-board path now tears every board down and adds exactly one fresh board of the target kind. This mirrors the multi-board and board-less paths and guarantees the surviving board's id matches its kind. Drops the now-unused setBoardType/activeBoardId destructures and tightens the boardFilter cast off `any`. Strengthens the regression test to assert the surviving board's id and kind. Co-Authored-By: Claude Opus 4.8 --- .../board-removal-running-reconcile.test.ts | 12 +++- frontend/src/utils/loadExample.ts | 62 ++++++++----------- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/frontend/src/__tests__/board-removal-running-reconcile.test.ts b/frontend/src/__tests__/board-removal-running-reconcile.test.ts index ecdbf467..e2ce47d8 100644 --- a/frontend/src/__tests__/board-removal-running-reconcile.test.ts +++ b/frontend/src/__tests__/board-removal-running-reconcile.test.ts @@ -121,14 +121,20 @@ describe('loadExample — multi-board to single-board leaves no residue (bug 3)' resetStores(); }); - it('a single-board example after a multi-board example ends with exactly one board', async () => { + it('a single-board example after a multi-board example ends with exactly one clean board', async () => { // Multi-board example: STM32 Blue Pill + Arduino Uno. await loadExample(findExample('stm32-uno-gpio-mirror')); expect(useSimulatorStore.getState().boards.length).toBe(2); - // Single-board example must reduce the canvas back to one board. + // Single-board example must reduce the canvas back to one board, freshly + // built — its id must match its kind (no stale "stm32-bluepill" id left + // on what is now an Arduino Uno). await loadExample(findExample('blink-led')); - expect(useSimulatorStore.getState().boards.length).toBe(1); + const after = useSimulatorStore.getState(); + expect(after.boards.length).toBe(1); + expect(after.boards[0].boardKind).toBe('arduino-uno'); + expect(after.boards[0].id).toBe('arduino-uno'); + expect(after.activeBoardId).toBe('arduino-uno'); }); it('residue cleanup also applies when extra boards were added manually', async () => { diff --git a/frontend/src/utils/loadExample.ts b/frontend/src/utils/loadExample.ts index e6c5d95d..07ae175d 100644 --- a/frontend/src/utils/loadExample.ts +++ b/frontend/src/utils/loadExample.ts @@ -88,9 +88,7 @@ export async function loadExample( const { setComponents, setWires, - setBoardType, setBoardLanguageMode, - activeBoardId, boards, addBoard, removeBoard, @@ -186,43 +184,33 @@ export async function loadExample( recalculateAllWirePositions(); } else { // ── Single-board loading ───────────────────────────────────────────── - // Analog-only and digital-only SPICE examples are board-less. Remove every - // existing board so the canvas opens with just the circuit (boards are now - // optional — you can have 0, 1, or many at any time). - const filter = (example as any).boardFilter; + // Tear the canvas down to nothing first, then (unless the example is + // board-less) add exactly one fresh board of the target kind. + // + // Analog-only and digital-only SPICE examples are board-less — they open + // with just the circuit (boards are optional: 0, 1, or many at any time). + // + // Rebuilding from scratch — rather than reusing and retyping a leftover + // board from a previous (possibly multi-board) example — is what keeps + // this prolijo: a single-board example always ends with exactly one + // board whose id matches its kind. The old reuse path left a stale id + // (e.g. "stm32-bluepill" on what was now an Arduino Uno) and any extra + // boards as residue. This mirrors the multi-board path above; the + // setComponents/setWires calls below replace components and wires + // wholesale. + const filter = (example as { boardFilter?: string }).boardFilter; const isBoardless = filter === 'analog' || filter === 'digital'; - if (isBoardless) { - const currentIds = boards.map((b) => b.id); - currentIds.forEach((id) => removeBoard(id)); - } else { + + boards.forEach((b) => removeBoard(b.id)); + + if (!isBoardless) { const targetBoard = example.boardType || 'arduino-uno'; - // A previous MULTI-board example may have left several boards on the - // canvas. A single-board example must end with exactly one board, so - // drop every board past the first before retyping it. Without this - // the extra boards (and their editor file groups) linger as residue - // from the previous example. setComponents/setWires below already - // replace the components and wires wholesale; boards were the one - // piece of state this path never reset. - const existing = useSimulatorStore.getState().boards; - if (existing.length > 1) { - existing.slice(1).forEach((b) => removeBoard(b.id)); - } - // If boards[] is empty (e.g. a previous analog example removed every - // board), setBoardType can't work — it only maps over existing entries. - // Add a fresh board instead. - if (useSimulatorStore.getState().boards.length === 0) { - const newId = addBoard( - targetBoard as BoardKind, - DEFAULT_BOARD_POSITION.x, - DEFAULT_BOARD_POSITION.y, - ); - setActiveBoardId(newId); - } else { - // Reuse the surviving board, but make sure it's the active one - // first — setBoardType retypes whatever board is active. - setActiveBoardId(useSimulatorStore.getState().boards[0].id); - setBoardType(targetBoard); - } + const newId = addBoard( + targetBoard as BoardKind, + DEFAULT_BOARD_POSITION.x, + DEFAULT_BOARD_POSITION.y, + ); + setActiveBoardId(newId); } // ── MicroPython + multi-file payloads ────────────────────────────────