import { createMockNative, setMockNative, type MockNativeDfu } from './mocks'; import { DfuNitroManager, DfuException } from '../manager'; import { DfuState, DfuError } from '../specs/NativeDfuNitro.nitro'; import type { DfuManagerOptions } from '../manager'; describe('DfuNitroManager', () => { let mockNative: MockNativeDfu; let manager: DfuNitroManager; const baseOptions: DfuManagerOptions = { deviceId: 'AA:BB:CC:DD:EE:FF', filePath: 'file:///tmp/firmware.zip', }; beforeEach(() => { mockNative = createMockNative(); setMockNative(mockNative); manager = new DfuNitroManager(); }); describe('startDfu', () => { it('calls native startDfu with correct options', () => { manager.startDfu(baseOptions); expect(mockNative.startDfu).toHaveBeenCalledTimes(1); const [options] = mockNative.startDfu.mock.calls[0]!; expect(options).toEqual({ deviceId: 'AA:BB:CC:DD:EE:FF', filePath: 'file:///tmp/firmware.zip', }); }); it('strips onStateChange and onProgress from native options', () => { const onStateChange = jest.fn(); const onProgress = jest.fn(); manager.startDfu({ ...baseOptions, onStateChange, onProgress }); const [options] = mockNative.startDfu.mock.calls[0]!; expect(options).not.toHaveProperty('onStateChange'); expect(options).not.toHaveProperty('onProgress'); }); it('passes through all DFU options', () => { const fullOptions: DfuManagerOptions = { ...baseOptions, forceDfu: true, enableUnsafeExperimentalButtonlessDfu: false, forceScanningForNewAddressInLegacyDfu: true, numberOfRetries: 3, packetReceiptNotificationParameter: 12, }; manager.startDfu(fullOptions); const [options] = mockNative.startDfu.mock.calls[0]!; expect(options.forceDfu).toBe(true); expect(options.enableUnsafeExperimentalButtonlessDfu).toBe(false); expect(options.forceScanningForNewAddressInLegacyDfu).toBe(true); expect(options.numberOfRetries).toBe(3); expect(options.packetReceiptNotificationParameter).toBe(12); }); it('resolves when state becomes Completed', async () => { mockNative.startDfu.mockImplementation((_opts, stateCb) => { stateCb(DfuState.Connecting); stateCb(DfuState.Uploading); stateCb(DfuState.Completed); }); await expect(manager.startDfu(baseOptions)).resolves.toBeUndefined(); }); it('rejects with DfuException on error callback', async () => { mockNative.startDfu.mockImplementation((_opts, _stateCb, _progCb, errorCb) => { errorCb(DfuError.FileInvalid, 'Bad firmware file'); }); await expect(manager.startDfu(baseOptions)).rejects.toThrow(DfuException); try { await manager.startDfu(baseOptions); } catch (e) { expect(e).toBeInstanceOf(DfuException); expect((e as DfuException).code).toBe(DfuError.FileInvalid); expect((e as DfuException).message).toBe('Bad firmware file'); } }); it('rejects with DfuException when aborted', async () => { mockNative.startDfu.mockImplementation((_opts, stateCb) => { stateCb(DfuState.Connecting); stateCb(DfuState.Aborted); }); await expect(manager.startDfu(baseOptions)).rejects.toThrow(DfuException); try { await manager.startDfu(baseOptions); } catch (e) { expect((e as DfuException).code).toBe(DfuError.DeviceDisconnected); expect((e as DfuException).message).toBe('DFU was aborted'); } }); it('invokes onStateChange for each state transition', () => { const onStateChange = jest.fn(); const states = [DfuState.Connecting, DfuState.Starting, DfuState.Uploading, DfuState.Completed]; mockNative.startDfu.mockImplementation((_opts, stateCb) => { for (const s of states) stateCb(s); }); manager.startDfu({ ...baseOptions, onStateChange }); expect(onStateChange).toHaveBeenCalledTimes(4); expect(onStateChange.mock.calls.map((c) => c[0])).toEqual(states); }); it('invokes onProgress with structured progress data', () => { const onProgress = jest.fn(); mockNative.startDfu.mockImplementation((_opts, _stateCb, progCb) => { progCb(1, 2, 50, 12.5, 10.0); }); manager.startDfu({ ...baseOptions, onProgress }); expect(onProgress).toHaveBeenCalledWith({ part: 1, totalParts: 2, progress: 50, currentSpeed: 12.5, avgSpeed: 10.0, }); }); it('does not throw when onStateChange/onProgress are omitted', async () => { mockNative.startDfu.mockImplementation((_opts, stateCb, progCb) => { stateCb(DfuState.Uploading); progCb(1, 1, 100, 5, 5); stateCb(DfuState.Completed); }); await expect(manager.startDfu(baseOptions)).resolves.toBeUndefined(); }); }); describe('pause', () => { it('calls native pauseDfu', () => { manager.pause(); expect(mockNative.pauseDfu).toHaveBeenCalledTimes(1); }); }); describe('resume', () => { it('calls native resumeDfu', () => { manager.resume(); expect(mockNative.resumeDfu).toHaveBeenCalledTimes(1); }); }); describe('abort', () => { it('calls native abortDfu', () => { manager.abort(); expect(mockNative.abortDfu).toHaveBeenCalledTimes(1); }); }); }); describe('DfuException', () => { it('has correct name, code, and message', () => { const err = new DfuException(DfuError.DeviceDisconnected, 'Lost connection'); expect(err).toBeInstanceOf(Error); expect(err.name).toBe('DfuException'); expect(err.code).toBe(DfuError.DeviceDisconnected); expect(err.message).toBe('Lost connection'); }); });