122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
/**
|
||
* Breadboard hole-occupancy rules ("1 pin = 1 cable"):
|
||
* - clicking an occupied hole selects the occupying wire (findWireAtHole);
|
||
* - a new wire end landing in an occupied hole shifts to the nearest free
|
||
* hole of the same 5-hole strip / power rail (resolveFreeHole);
|
||
* - seated component legs (invisible bb wires) count as occupancy but are
|
||
* never selectable.
|
||
* Plus the jumper color policy: rails are red/black, strips draw from the
|
||
* jumper palette deterministically per wire id.
|
||
*/
|
||
import { describe, it, expect } from 'vitest';
|
||
import {
|
||
findWireAtHole,
|
||
holeIsOccupied,
|
||
resolveFreeHole,
|
||
} from '../utils/breadboardOccupancy';
|
||
import {
|
||
railWireColor,
|
||
jumperColorForId,
|
||
WIRE_JUMPER_PALETTE,
|
||
} from '../utils/wireUtils';
|
||
import type { Wire } from '../types/wire';
|
||
|
||
const wire = (
|
||
id: string,
|
||
s: [string, string],
|
||
e: [string, string],
|
||
bb = false,
|
||
): Wire =>
|
||
({
|
||
id,
|
||
start: { componentId: s[0], pinName: s[1], x: 0, y: 0 },
|
||
end: { componentId: e[0], pinName: e[1], x: 0, y: 0 },
|
||
waypoints: [],
|
||
color: '#22c55e',
|
||
...(bb ? { bb: true } : {}),
|
||
}) as unknown as Wire;
|
||
|
||
// Column 21, top bank: holes a-e — one strip.
|
||
const STRIP = ['21t.a', '21t.b', '21t.c', '21t.d', '21t.e'];
|
||
const ALL = [...STRIP, '22t.a', '22t.b', 'bp.1', 'bp.2', 'bn.1'];
|
||
|
||
describe('findWireAtHole', () => {
|
||
it('returns the visible wire whose end sits in the hole', () => {
|
||
const w = wire('w1', ['bb1', '21t.a'], ['bb1', '33t.b']);
|
||
expect(findWireAtHole([w], 'bb1', '21t.a')?.id).toBe('w1');
|
||
expect(findWireAtHole([w], 'bb1', '33t.b')?.id).toBe('w1');
|
||
expect(findWireAtHole([w], 'bb1', '21t.b')).toBeNull();
|
||
});
|
||
|
||
it('never returns invisible seating wires', () => {
|
||
const seat = wire('s1', ['comp1', 'A'], ['bb1', '21t.a'], true);
|
||
expect(findWireAtHole([seat], 'bb1', '21t.a')).toBeNull();
|
||
});
|
||
|
||
it('topmost wire wins when stacked (legacy stacked circuits)', () => {
|
||
const w1 = wire('w1', ['bb1', '21t.a'], ['bb1', '30t.a']);
|
||
const w2 = wire('w2', ['bb1', '21t.a'], ['bb1', '31t.a']);
|
||
expect(findWireAtHole([w1, w2], 'bb1', '21t.a')?.id).toBe('w2');
|
||
});
|
||
});
|
||
|
||
describe('holeIsOccupied', () => {
|
||
it('counts seated legs (bb wires) as occupancy', () => {
|
||
const seat = wire('s1', ['comp1', 'A'], ['bb1', '21t.a'], true);
|
||
expect(holeIsOccupied([seat], 'bb1', '21t.a')).toBe(true);
|
||
expect(holeIsOccupied([seat], 'bb1', '21t.b')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('resolveFreeHole', () => {
|
||
it('keeps a free hole as-is', () => {
|
||
expect(resolveFreeHole('breadboard', 'bb1', '21t.c', [], ALL)).toBe('21t.c');
|
||
});
|
||
|
||
it('shifts to the nearest free hole in the same strip', () => {
|
||
const seat = wire('s1', ['comp1', 'A'], ['bb1', '21t.a'], true);
|
||
expect(resolveFreeHole('breadboard', 'bb1', '21t.a', [seat], ALL)).toBe('21t.b');
|
||
});
|
||
|
||
it('never shifts across strips', () => {
|
||
// Whole 21t strip occupied → falls back to the clicked hole, NOT 22t.
|
||
const wires = STRIP.map((p, i) => wire(`w${i}`, ['bb1', p], ['bb1', '40t.a']));
|
||
expect(resolveFreeHole('breadboard', 'bb1', '21t.a', wires, ALL)).toBe('21t.a');
|
||
});
|
||
|
||
it('shifts within a power rail too', () => {
|
||
const w = wire('w1', ['bb1', 'bp.1'], ['esp32_1', '3V3']);
|
||
expect(resolveFreeHole('breadboard', 'bb1', 'bp.1', [w], ALL)).toBe('bp.2');
|
||
});
|
||
|
||
it('non-breadboard pins pass through', () => {
|
||
const w = wire('w1', ['led1', 'A'], ['bb1', '21t.a']);
|
||
expect(resolveFreeHole('wokwi-led', 'led1', 'A', [w], ['A', 'C'])).toBe('A');
|
||
});
|
||
});
|
||
|
||
describe('wire color policy', () => {
|
||
it('rails are red (+) / black (−)', () => {
|
||
expect(railWireColor('tp.5')).toBe('#cc0000');
|
||
expect(railWireColor('bp.12')).toBe('#cc0000');
|
||
expect(railWireColor('tn.5')).toBe('#000000');
|
||
expect(railWireColor('bn.1')).toBe('#000000');
|
||
expect(railWireColor('21t.a')).toBeNull();
|
||
expect(railWireColor('GND')).toBeNull();
|
||
});
|
||
|
||
it('jumper colors are deterministic per id and inside the palette', () => {
|
||
const c = jumperColorForId('wire_42');
|
||
expect(jumperColorForId('wire_42')).toBe(c);
|
||
expect(WIRE_JUMPER_PALETTE).toContain(c);
|
||
// Different ids spread — at least two distinct colors among a handful.
|
||
const set = new Set(['a', 'b', 'c', 'd', 'e', 'f'].map(jumperColorForId));
|
||
expect(set.size).toBeGreaterThan(1);
|
||
});
|
||
|
||
it('palette reserves red and black for rails', () => {
|
||
expect(WIRE_JUMPER_PALETTE).not.toContain('#cc0000');
|
||
expect(WIRE_JUMPER_PALETTE).not.toContain('#000000');
|
||
});
|
||
});
|