velxio/frontend/src/__tests__/pin-resolver.test.ts

141 lines
4.6 KiB
TypeScript

/**
* Unit tests for the default PinResolver.
*
* These cover the Phase-0 deliverable: a PinResolver that wraps the
* existing getArduinoPin trace + pinManager subscription. Behavior
* must be identical to the legacy path; the abstraction just makes
* the swap to a SPICE-resolved implementation possible in Phase 1.
*
* See project/sim-mixedmode/phase-00-pin-resolver.md
*/
import { describe, it, expect, vi } from 'vitest';
import {
createDefaultPinResolver,
type PinResolverContext,
type PinTracer,
} from '../simulation/PinResolver';
function makeCtx(overrides: Partial<PinResolverContext> = {}): PinResolverContext {
return {
components: [],
boards: [],
wires: [],
ownerBoard: null,
ownerBoardVcc: 5,
subscribeArduinoPin: () => () => {},
readArduinoPin: () => null,
...overrides,
};
}
describe('PinResolver — default impl', () => {
it('reports FLOATING when the pin is not wired to any board pin', () => {
const tracer: PinTracer = () => null;
const r = createDefaultPinResolver('led-1', 'A', makeCtx(), tracer);
expect(r.getCurrentState()).toBe('FLOATING');
expect(r.getCurrentVoltage()).toBeNull();
});
it('reports LOW with voltage 0 when the pin is tied to GND (tracer returns -1)', () => {
const tracer: PinTracer = () => -1;
const r = createDefaultPinResolver('led-1', 'C', makeCtx(), tracer);
expect(r.getCurrentState()).toBe('LOW');
expect(r.getCurrentVoltage()).toBe(0);
});
it('reads HIGH/LOW from readArduinoPin when wired to a GPIO', () => {
const tracer: PinTracer = () => 13;
let pinValue = false;
const ctx = makeCtx({
readArduinoPin: (pin) => (pin === 13 ? pinValue : null),
});
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
pinValue = false;
expect(r.getCurrentState()).toBe('LOW');
expect(r.getCurrentVoltage()).toBe(0);
});
it('synthesises voltage from owner board Vcc on HIGH', () => {
const tracer: PinTracer = () => 13;
const ctx = makeCtx({
ownerBoardVcc: 3.3,
readArduinoPin: () => true,
});
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
expect(r.getCurrentState()).toBe('HIGH');
expect(r.getCurrentVoltage()).toBeCloseTo(3.3);
});
it('subscribes to pin changes via subscribeArduinoPin and fires callbacks', () => {
const tracer: PinTracer = () => 7;
const subscribers: Array<(pin: number, state: boolean) => void> = [];
const unsub = vi.fn();
const ctx = makeCtx({
subscribeArduinoPin: (_pin, cb) => {
subscribers.push(cb);
return unsub;
},
});
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
const onChange = vi.fn();
const cancel = r.onChange(onChange);
// Manually fire as if PinManager delivered an event
subscribers[0]!(7, true);
expect(onChange).toHaveBeenCalledWith('HIGH', 5);
subscribers[0]!(7, false);
expect(onChange).toHaveBeenCalledWith('LOW', 0);
// Caller can unsubscribe cleanly
cancel();
expect(unsub).toHaveBeenCalled();
});
it('returns a no-op unsubscribe when the pin is unwired (tracer returns null)', () => {
const tracer: PinTracer = () => null;
const ctx = makeCtx({
subscribeArduinoPin: vi.fn(),
});
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
const onChange = vi.fn();
const cancel = r.onChange(onChange);
expect(ctx.subscribeArduinoPin).not.toHaveBeenCalled();
expect(typeof cancel).toBe('function');
cancel(); // should not throw
});
it('emits an initial LOW event asynchronously when wired to GND', async () => {
const tracer: PinTracer = () => -1;
const r = createDefaultPinResolver('led-1', 'C', makeCtx(), tracer);
const onChange = vi.fn();
r.onChange(onChange);
// Microtask queue flush — the resolver schedules the LOW event via
// queueMicrotask so subscribers see initial state without blocking
// construction.
await Promise.resolve();
expect(onChange).toHaveBeenCalledWith('LOW', 0);
});
it('updates getCurrentState immediately after a subscribed callback fires', () => {
const tracer: PinTracer = () => 9;
const subscribers: Array<(pin: number, state: boolean) => void> = [];
const ctx = makeCtx({
subscribeArduinoPin: (_pin, cb) => {
subscribers.push(cb);
return () => {};
},
readArduinoPin: () => false,
});
const r = createDefaultPinResolver('led-1', 'A', ctx, tracer);
r.onChange(() => {});
expect(r.getCurrentState()).toBe('LOW');
subscribers[0]!(9, true);
expect(r.getCurrentState()).toBe('HIGH');
expect(r.getCurrentVoltage()).toBe(5);
});
});