From 9b86c816c11d58502179de2e5221234252fd66df Mon Sep 17 00:00:00 2001 From: ciegovolador Date: Thu, 11 Jun 2026 03:29:44 -0300 Subject: [PATCH] fix(sim): keep PwmCallback 2-arg compatible via arity dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the earlier approach of widening the existing PWM-callback assertions to accept the new timeMs arg — that masked a contract change rather than fixing it. Instead, updatePwm now hands the optional timeMs only to listeners that declare a 3rd parameter (cb.length >= 3) — i.e. the buzzer, which needs the precise onset time. Plain (pin, dutyCycle) listeners, and the existing toHaveBeenCalledWith(pin, dutyCycle) tests, see an unchanged 2-arg call, so the original PwmCallback contract is preserved. Add a PinManager test locking the dispatch: a 2-param listener stays 2-arg; a 3-param listener receives timeMs. Co-Authored-By: Claude Opus 4.8 --- frontend/src/__tests__/AVRSimulator.test.ts | 4 +-- frontend/src/__tests__/PinManager.test.ts | 34 +++++++++++++++++-- .../src/__tests__/attiny85-simulation.test.ts | 4 +-- frontend/src/__tests__/mega-emulation.test.ts | 6 ++-- frontend/src/simulation/PinManager.ts | 8 ++++- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/frontend/src/__tests__/AVRSimulator.test.ts b/frontend/src/__tests__/AVRSimulator.test.ts index f88b9a44..f98cfd42 100644 --- a/frontend/src/__tests__/AVRSimulator.test.ts +++ b/frontend/src/__tests__/AVRSimulator.test.ts @@ -168,7 +168,7 @@ describe('AVRSimulator — PWM OCR monitoring', () => { sim.start(); sim.stop(); - expect(pwmCb).toHaveBeenCalledWith(9, 128 / 255, expect.anything()); // 3rd arg = sim timeMs + expect(pwmCb).toHaveBeenCalledWith(9, 128 / 255); }); it('PWM covers all six Arduino PWM pins', () => { @@ -202,7 +202,7 @@ describe('AVRSimulator — PWM OCR monitoring', () => { PWM_MAP.forEach(({ pin }, i) => { const expected = ((i + 1) * 25) / 255; - expect(cbs[pin]).toHaveBeenCalledWith(pin, expected, expect.anything()); // 3rd arg = sim timeMs + expect(cbs[pin]).toHaveBeenCalledWith(pin, expected); }); }); }); diff --git a/frontend/src/__tests__/PinManager.test.ts b/frontend/src/__tests__/PinManager.test.ts index b98cca16..a5cc5695 100644 --- a/frontend/src/__tests__/PinManager.test.ts +++ b/frontend/src/__tests__/PinManager.test.ts @@ -124,7 +124,7 @@ describe('PinManager — PWM duty cycle', () => { const cb = vi.fn(); pm.onPwmChange(9, cb); pm.updatePwm(9, 0.5); - expect(cb).toHaveBeenCalledWith(9, 0.5, undefined); // 3rd arg = optional timeMs (not passed here) + expect(cb).toHaveBeenCalledWith(9, 0.5); }); it('stores the latest PWM value', () => { @@ -150,9 +150,39 @@ describe('PinManager — PWM duty cycle', () => { pwmPins.forEach((pin, i) => { const dc = (i + 1) / 6; pm.updatePwm(pin, dc); - expect(callbacks[i]).toHaveBeenCalledWith(pin, dc, undefined); // optional timeMs not passed + expect(callbacks[i]).toHaveBeenCalledWith(pin, dc); }); }); + + // The optional timeMs (precise simulated onset time, used by the buzzer for + // sample-accurate audio) must NOT widen the public PwmCallback contract: + // listeners that declare only (pin, dutyCycle) keep getting a 2-arg call, + // while a listener that declares a 3rd parameter receives timeMs. This guards + // the arity-based dispatch the buzzer relies on. Regular functions are used + // (not vi.fn) because the dispatch keys off Function.length, and a 3-param + // listener must report length 3. + it('hands timeMs only to listeners that declare a 3rd parameter', () => { + let twoArgCount = -1; + let threeArgCount = -1; + let threeArgTime: number | undefined; + function twoArg(this: unknown, _pin: number, _dc: number) { + // eslint-disable-next-line prefer-rest-params + twoArgCount = arguments.length; + } + function threeArg(this: unknown, _pin: number, _dc: number, t?: number) { + // eslint-disable-next-line prefer-rest-params + threeArgCount = arguments.length; + threeArgTime = t; + } + pm.onPwmChange(7, twoArg); + pm.onPwmChange(7, threeArg); + + pm.updatePwm(7, 0.5, 123); + + expect(twoArgCount).toBe(2); // original 2-arg contract preserved — no trailing timeMs + expect(threeArgCount).toBe(3); + expect(threeArgTime).toBe(123); // 3-arg listener (the buzzer) gets the precise time + }); }); // ─── Analog voltage API ────────────────────────────────────────────────────── diff --git a/frontend/src/__tests__/attiny85-simulation.test.ts b/frontend/src/__tests__/attiny85-simulation.test.ts index 2c66826b..5843c765 100644 --- a/frontend/src/__tests__/attiny85-simulation.test.ts +++ b/frontend/src/__tests__/attiny85-simulation.test.ts @@ -283,7 +283,7 @@ describe('ATtiny85 — PWM monitoring', () => { sim.start(); sim.stop(); - expect(pwmCb).toHaveBeenCalledWith(1, 128 / 255, expect.anything()); // 3rd arg = sim timeMs + expect(pwmCb).toHaveBeenCalledWith(1, 128 / 255); }); it('PinManager receives PWM update on pin 0 when OCR0A (0x56) is written', () => { @@ -300,7 +300,7 @@ describe('ATtiny85 — PWM monitoring', () => { sim.start(); sim.stop(); - expect(pwmCb).toHaveBeenCalledWith(0, 64 / 255, expect.anything()); // 3rd arg = sim timeMs + expect(pwmCb).toHaveBeenCalledWith(0, 64 / 255); }); it('ATtiny85 PWM covers 4 pins (OCR0A/OCR0B/OCR1A/OCR1B)', () => { diff --git a/frontend/src/__tests__/mega-emulation.test.ts b/frontend/src/__tests__/mega-emulation.test.ts index 4f373576..0752eeda 100644 --- a/frontend/src/__tests__/mega-emulation.test.ts +++ b/frontend/src/__tests__/mega-emulation.test.ts @@ -301,7 +301,7 @@ describe('AVRSimulator Mega — PWM OCR mapping differs from Uno', () => { // Call pollPwmRegisters directly — avoids RAF dependency in unit tests (sim as any).pollPwmRegisters(); - expect(cb).toHaveBeenCalledWith(13, 128 / 255, expect.anything()); // 3rd arg = sim timeMs + expect(cb).toHaveBeenCalledWith(13, 128 / 255); sim.stop(); }); @@ -318,7 +318,7 @@ describe('AVRSimulator Mega — PWM OCR mapping differs from Uno', () => { (sim as any).pollPwmRegisters(); - expect(cb).toHaveBeenCalledWith(5, 200 / 255, expect.anything()); // 3rd arg = sim timeMs + expect(cb).toHaveBeenCalledWith(5, 200 / 255); sim.stop(); }); @@ -335,7 +335,7 @@ describe('AVRSimulator Mega — PWM OCR mapping differs from Uno', () => { (sim as any).pollPwmRegisters(); - expect(cb).toHaveBeenCalledWith(6, 100 / 255, expect.anything()); // 3rd arg = sim timeMs + expect(cb).toHaveBeenCalledWith(6, 100 / 255); sim.stop(); }); }); diff --git a/frontend/src/simulation/PinManager.ts b/frontend/src/simulation/PinManager.ts index 180ef9f4..15a68714 100644 --- a/frontend/src/simulation/PinManager.ts +++ b/frontend/src/simulation/PinManager.ts @@ -201,7 +201,13 @@ export class PinManager { if (dutyCycle > 0) this.outputPins.add(pin); const callbacks = this.pwmListeners.get(pin); if (callbacks) { - callbacks.forEach((cb) => cb(pin, dutyCycle, timeMs)); + // Backward-compatible dispatch: the original PwmCallback contract is + // (pin, dutyCycle). Only listeners that actually declare a 3rd parameter + // (the buzzer, which needs the precise onset time for sample-accurate + // audio) receive timeMs. Plain 2-arg listeners — and the existing tests + // that assert toHaveBeenCalledWith(pin, dutyCycle) — see an unchanged + // 2-arg call instead of a spurious trailing arg. + callbacks.forEach((cb) => (cb.length >= 3 ? cb(pin, dutyCycle, timeMs) : cb(pin, dutyCycle))); } }