311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
/**
|
|
* logic-gate-parts.test.ts
|
|
*
|
|
* Tests simulation logic for all six logic gate components:
|
|
* AND, NAND, OR, NOR, XOR, NOT
|
|
*
|
|
* Each gate is tested against its full truth table and for no-pin fallback.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry';
|
|
|
|
// Side-effect import — registers logic gates
|
|
import '../simulation/parts/LogicGateParts';
|
|
|
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
|
|
beforeEach(() => {
|
|
let raf = 0;
|
|
vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => ++raf);
|
|
vi.stubGlobal('cancelAnimationFrame', vi.fn());
|
|
});
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
// ─── Mock factories ───────────────────────────────────────────────────────────
|
|
|
|
function makeElement(): HTMLElement {
|
|
return {
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
} as unknown as HTMLElement;
|
|
}
|
|
|
|
function makeSimulator() {
|
|
const pinManager = {
|
|
onPinChange: vi.fn().mockReturnValue(() => {}),
|
|
triggerPinChange: vi.fn(),
|
|
};
|
|
return {
|
|
pinManager,
|
|
setPinState: vi.fn(),
|
|
cpu: { data: new Uint8Array(512).fill(0), cycles: 0 },
|
|
};
|
|
}
|
|
|
|
/** Pin map helper — resolves named pins to numbers */
|
|
const pinMap =
|
|
(map: Record<string, number>) =>
|
|
(name: string): number | null =>
|
|
name in map ? map[name] : null;
|
|
|
|
/** Simulate changing a pin by invoking the recorded onPinChange callback */
|
|
function firePin(sim: ReturnType<typeof makeSimulator>, callIndex: number, state: boolean) {
|
|
const cb = sim.pinManager.onPinChange.mock.calls[callIndex][1] as (
|
|
pin: number,
|
|
state: boolean,
|
|
) => void;
|
|
const pin = sim.pinManager.onPinChange.mock.calls[callIndex][0] as number;
|
|
cb(pin, state);
|
|
}
|
|
|
|
// ─── Registration ─────────────────────────────────────────────────────────────
|
|
|
|
describe('Logic-gate parts — registration', () => {
|
|
const GATE_IDS = [
|
|
'logic-gate-and',
|
|
'logic-gate-nand',
|
|
'logic-gate-or',
|
|
'logic-gate-nor',
|
|
'logic-gate-xor',
|
|
'logic-gate-not',
|
|
];
|
|
|
|
it('registers all six gate types', () => {
|
|
for (const id of GATE_IDS) {
|
|
expect(PartSimulationRegistry.get(id), `missing: ${id}`).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── AND gate ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('logic-gate-and — truth table', () => {
|
|
it('starts LOW (0,0 → 0)', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-and')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
// Initial call sets Y LOW (false AND false)
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
|
|
it('1,0 → 0', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-and')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
firePin(sim, 0, true); // A = 1
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
|
|
it('0,1 → 0', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-and')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
firePin(sim, 1, true); // B = 1
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
|
|
it('1,1 → 1', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-and')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
firePin(sim, 0, true); // A = 1
|
|
firePin(sim, 1, true); // B = 1
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('returns cleanup function that unsubscribes both pins', () => {
|
|
const sim = makeSimulator();
|
|
const unsub1 = vi.fn();
|
|
const unsub2 = vi.fn();
|
|
sim.pinManager.onPinChange.mockReturnValueOnce(unsub1).mockReturnValueOnce(unsub2);
|
|
const logic = PartSimulationRegistry.get('logic-gate-and')!;
|
|
const cleanup = logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
cleanup();
|
|
expect(unsub1).toHaveBeenCalled();
|
|
expect(unsub2).toHaveBeenCalled();
|
|
});
|
|
|
|
it('no-pin fallback returns no-op', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-and')!;
|
|
const cleanup = logic.attachEvents(makeElement(), sim as any, () => null);
|
|
expect(sim.setPinState).not.toHaveBeenCalled();
|
|
expect(() => cleanup()).not.toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── NAND gate ────────────────────────────────────────────────────────────────
|
|
|
|
describe('logic-gate-nand — truth table', () => {
|
|
function attach() {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-nand')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
return sim;
|
|
}
|
|
|
|
it('0,0 → 1', () => {
|
|
const sim = attach();
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('1,0 → 1', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('1,1 → 0', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
firePin(sim, 1, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
});
|
|
|
|
// ─── OR gate ──────────────────────────────────────────────────────────────────
|
|
|
|
describe('logic-gate-or — truth table', () => {
|
|
function attach() {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-or')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
return sim;
|
|
}
|
|
|
|
it('0,0 → 0', () => {
|
|
const sim = attach();
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
|
|
it('1,0 → 1', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('0,1 → 1', () => {
|
|
const sim = attach();
|
|
firePin(sim, 1, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('1,1 → 1', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
firePin(sim, 1, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
});
|
|
|
|
// ─── NOR gate ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('logic-gate-nor — truth table', () => {
|
|
function attach() {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-nor')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
return sim;
|
|
}
|
|
|
|
it('0,0 → 1', () => {
|
|
const sim = attach();
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('1,0 → 0', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
|
|
it('1,1 → 0', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
firePin(sim, 1, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
});
|
|
|
|
// ─── XOR gate ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('logic-gate-xor — truth table', () => {
|
|
function attach() {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-xor')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, B: 2, Y: 3 }));
|
|
return sim;
|
|
}
|
|
|
|
it('0,0 → 0', () => {
|
|
const sim = attach();
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
|
|
it('1,0 → 1', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('0,1 → 1', () => {
|
|
const sim = attach();
|
|
firePin(sim, 1, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, true);
|
|
});
|
|
|
|
it('1,1 → 0', () => {
|
|
const sim = attach();
|
|
firePin(sim, 0, true);
|
|
firePin(sim, 1, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(3, false);
|
|
});
|
|
});
|
|
|
|
// ─── NOT gate ─────────────────────────────────────────────────────────────────
|
|
|
|
describe('logic-gate-not — truth table', () => {
|
|
it('initial state: A=0 → Y=1', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-not')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, Y: 2 }));
|
|
expect(sim.setPinState).toHaveBeenCalledWith(2, true);
|
|
});
|
|
|
|
it('A=1 → Y=0', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-not')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, Y: 2 }));
|
|
firePin(sim, 0, true);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(2, false);
|
|
});
|
|
|
|
it('A=0 → Y=1 (after toggle)', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-not')!;
|
|
logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, Y: 2 }));
|
|
firePin(sim, 0, true);
|
|
firePin(sim, 0, false);
|
|
expect(sim.setPinState).toHaveBeenLastCalledWith(2, true);
|
|
});
|
|
|
|
it('returns cleanup that unsubscribes A pin', () => {
|
|
const sim = makeSimulator();
|
|
const unsub = vi.fn();
|
|
sim.pinManager.onPinChange.mockReturnValueOnce(unsub);
|
|
const logic = PartSimulationRegistry.get('logic-gate-not')!;
|
|
const cleanup = logic.attachEvents(makeElement(), sim as any, pinMap({ A: 1, Y: 2 }));
|
|
cleanup();
|
|
expect(unsub).toHaveBeenCalled();
|
|
});
|
|
|
|
it('no-pin fallback returns no-op', () => {
|
|
const sim = makeSimulator();
|
|
const logic = PartSimulationRegistry.get('logic-gate-not')!;
|
|
const cleanup = logic.attachEvents(makeElement(), sim as any, () => null);
|
|
expect(sim.setPinState).not.toHaveBeenCalled();
|
|
expect(() => cleanup()).not.toThrow();
|
|
});
|
|
});
|