velxio/frontend/src/__tests__/board-removal-running-recon...

144 lines
5.4 KiB
TypeScript

// @vitest-environment jsdom
/**
* Regression tests for three reported circuit bugs:
*
* 1. "Deleting a board STARTS the simulation, leaving the circuit
* unresponsive until stopped."
* 2. "Selecting New Project sometimes STARTS the simulation."
* 3. "Load a multi-board example, then a single-board example — the old
* boards stay behind as residue."
*
* Root cause for (1) and (2): the flat `running` flag mirrors the ACTIVE
* board's run state. `removeBoard` reassigned `activeBoardId` but never
* re-derived `running`, so deleting the running/active board left it stale
* at `true`. The UI then looked "running" and SimulatorCanvas's auto-start
* effect (which treats `running` as a master switch for remote boards) span
* a sibling board up. New Project hits the same path — it removes every
* board in a loop. The fix re-derives `running` from the new active board
* inside removeBoard.
*
* Root cause for (3): loadExample's single-board path called setBoardType
* when boards already existed but never dropped the EXTRA boards a previous
* multi-board example had added. The fix removes every board past the first
* before retyping.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { useElectricalStore } from '../store/useElectricalStore';
import { loadExample } from '../utils/loadExample';
import { exampleProjects } from '../data/examples';
function resetStores() {
const sim = useSimulatorStore.getState();
for (const id of sim.boards.map((b) => b.id)) sim.removeBoard(id);
useElectricalStore.getState().setPaused(false);
}
function findExample(id: string) {
const e = exampleProjects.find((x) => x.id === id);
if (!e) throw new Error(`Example not found: ${id}`);
return e;
}
/** Mark a board (and, if active, the flat mirror) as running — without
* spinning up a real simulator/bridge. */
function markRunning(boardId: string) {
useSimulatorStore.setState((s) => ({
running: s.activeBoardId === boardId ? true : s.running,
boards: s.boards.map((b) => (b.id === boardId ? { ...b, running: true } : b)),
}));
}
describe('removeBoard — running-flag reconciliation (bugs 1 & 2)', () => {
beforeEach(() => {
resetStores();
});
it('deleting the running ACTIVE board clears the global running flag', () => {
const { addBoard, setActiveBoardId, removeBoard } = useSimulatorStore.getState();
const a = addBoard('arduino-uno', 0, 0);
const b = addBoard('arduino-nano', 300, 0);
setActiveBoardId(a);
markRunning(a);
expect(useSimulatorStore.getState().running).toBe(true);
removeBoard(a);
const s = useSimulatorStore.getState();
expect(s.activeBoardId).toBe(b);
// The new active board (b) is NOT running, so the flat flag must follow.
expect(s.running).toBe(false);
});
it('deleting the only (running) board leaves running=false with no boards', () => {
const { addBoard, setActiveBoardId, removeBoard } = useSimulatorStore.getState();
const a = addBoard('arduino-uno', 0, 0);
setActiveBoardId(a);
markRunning(a);
removeBoard(a);
const s = useSimulatorStore.getState();
expect(s.boards).toHaveLength(0);
expect(s.activeBoardId).toBeNull();
expect(s.running).toBe(false);
});
it('deleting a NON-active board does not disturb the active board mirror', () => {
const { addBoard, setActiveBoardId, removeBoard } = useSimulatorStore.getState();
const a = addBoard('arduino-uno', 0, 0);
const b = addBoard('arduino-nano', 300, 0);
setActiveBoardId(a);
markRunning(a);
removeBoard(b); // remove the inactive one
const s = useSimulatorStore.getState();
expect(s.activeBoardId).toBe(a);
expect(s.running).toBe(true); // active board still running
});
it('New Project teardown (remove every board in a loop) ends with running=false', () => {
const { addBoard, setActiveBoardId, removeBoard } = useSimulatorStore.getState();
const a = addBoard('arduino-uno', 0, 0);
addBoard('arduino-nano', 300, 0);
setActiveBoardId(a);
markRunning(a);
// Mirror desktop menu.ts::newProject(): drop every board.
for (const board of [...useSimulatorStore.getState().boards]) {
removeBoard(board.id);
}
const s = useSimulatorStore.getState();
expect(s.boards).toHaveLength(0);
expect(s.running).toBe(false);
});
});
describe('loadExample — multi-board to single-board leaves no residue (bug 3)', () => {
beforeEach(() => {
resetStores();
});
it('a single-board example after a multi-board example ends with exactly one 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.
await loadExample(findExample('blink-led'));
expect(useSimulatorStore.getState().boards.length).toBe(1);
});
it('residue cleanup also applies when extra boards were added manually', async () => {
const { addBoard } = useSimulatorStore.getState();
addBoard('arduino-nano', 300, 0);
addBoard('arduino-mega', 600, 0);
expect(useSimulatorStore.getState().boards.length).toBeGreaterThan(1);
await loadExample(findExample('blink-led'));
expect(useSimulatorStore.getState().boards.length).toBe(1);
});
});