import { createInitialMeasurementMachine, transitionMeasurement, } from '../measurement/measurementMachine'; import type { MeasurementEvent, MeasurementMachineSnapshot, } from '../measurement/measurementTypes'; import {makeProcessingResult} from './syntheticMeasurements'; const send = ( snapshot: MeasurementMachineSnapshot, ...events: readonly MeasurementEvent[] ): MeasurementMachineSnapshot => events.reduce(transitionMeasurement, snapshot); describe('measurement machine', () => { test('runs the complete camera-only lifecycle', () => { const result = makeProcessingResult(); const snapshot = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'CAMERA_READY'}, {type: 'CONTACT_CHANGED', contactState: 'valid'}, {type: 'STABILIZATION_COMPLETE'}, {type: 'ACQUISITION_COMPLETE'}, {type: 'PROCESSING_SUCCEEDED', result}, ); expect(snapshot.value).toBe('completed'); expect(snapshot.context.result).toEqual(result); expect(snapshot.context.attempt).toBe(1); }); test('continues when the optional HealthKit request fails', () => { const snapshot = send( createInitialMeasurementMachine(), {type: 'START', requestHealthKit: true}, {type: 'CAMERA_PERMISSION_GRANTED'}, ); expect(snapshot.value).toBe('requestingHealthPermission'); const afterFailure = transitionMeasurement(snapshot, { type: 'HEALTH_PERMISSION_RESOLVED', status: 'requestFailed', }); expect(afterFailure.value).toBe('preparingCamera'); expect(afterFailure.context.healthPermission).toBe('requestFailed'); }); test('a rejected camera permission retry returns through idle before requesting again', () => { const denied = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_DENIED'}, ); expect(denied.value).toBe('error'); const retrying = transitionMeasurement(denied, {type: 'RETRY'}); expect(retrying.value).toBe('idle'); expect(retrying.context.attempt).toBe(1); const requestingAgain = transitionMeasurement(retrying, {type: 'START'}); expect(requestingAgain.value).toBe('requestingCameraPermission'); expect(requestingAgain.context.attempt).toBe(2); }); test.each(['waitingForFinger', 'measuring'] as const)( 'routes a camera failure from %s to the global error state', activeState => { let active = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'CAMERA_READY'}, ); if (activeState === 'measuring') { active = send( active, {type: 'CONTACT_CHANGED', contactState: 'valid'}, {type: 'STABILIZATION_COMPLETE'}, ); } const failed = transitionMeasurement(active, { type: 'FAIL', failure: { code: 'cameraRuntime', message: 'Camera runtime failure', recoverable: true, }, }); expect(failed.value).toBe('error'); expect(failed.context.failure?.code).toBe('cameraRuntime'); }, ); test('keeps stabilization and acquisition running when contact fluctuates', () => { const measuring = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'CAMERA_READY'}, {type: 'CONTACT_CHANGED', contactState: 'valid'}, {type: 'STABILIZATION_COMPLETE'}, ); expect(measuring.value).toBe('measuring'); const waiting = transitionMeasurement(measuring, { type: 'CONTACT_CHANGED', contactState: 'unstable', }); expect(waiting.value).toBe('measuring'); const stillMeasuring = transitionMeasurement(waiting, { type: 'CONTACT_CHANGED', contactState: 'valid', }); expect(stillMeasuring.value).toBe('measuring'); }); test('allows stabilization to complete even when the latest frame is unstable', () => { const stabilizing = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'CAMERA_READY'}, {type: 'CONTACT_CHANGED', contactState: 'valid'}, {type: 'CONTACT_CHANGED', contactState: 'unstable'}, ); expect(stabilizing.value).toBe('stabilizing'); expect( transitionMeasurement(stabilizing, {type: 'STABILIZATION_COMPLETE'}).value, ).toBe('measuring'); }); test.each(['serious', 'critical'] as const)( 'stops immediately for a %s thermal state', thermalState => { const measuring = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'CAMERA_READY'}, ); const stopped = transitionMeasurement(measuring, { type: 'THERMAL_STATE_CHANGED', thermalState, }); expect(stopped.value).toBe('thermalStop'); expect(stopped.context.failure?.code).toBe( thermalState === 'critical' ? 'thermalCritical' : 'thermalSerious', ); expect(stopped.context.failure?.message).toMatch(/iPhone|chaud/); }, ); test('cannot reset past an unsafe thermal state', () => { const stopped = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'THERMAL_STATE_CHANGED', thermalState: 'serious'}, ); const reset = transitionMeasurement(stopped, {type: 'RESET'}); expect(reset.value).toBe('thermalStop'); const cooled = transitionMeasurement(reset, { type: 'THERMAL_STATE_CHANGED', thermalState: 'fair', }); expect(transitionMeasurement(cooled, {type: 'RETRY'}).value).toBe( 'preparingCamera', ); }); test('never completes an invalid native result', () => { const invalidResult = makeProcessingResult({ valid: false, invalidReason: 'Signal insuffisant', quality: { ...makeProcessingResult().quality, score: 0.4, level: 'invalid', }, }); const snapshot = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'CAMERA_READY'}, {type: 'CONTACT_CHANGED', contactState: 'valid'}, {type: 'STABILIZATION_COMPLETE'}, {type: 'ACQUISITION_COMPLETE'}, {type: 'PROCESSING_SUCCEEDED', result: invalidResult}, ); expect(snapshot.value).toBe('invalid'); expect(snapshot.context.result?.valid).toBe(false); }); test('keeps a conservative BPM when PRV quality is poor', () => { const partialResult = makeProcessingResult({ valid: true, bpm: { ...makeProcessingResult().bpm, bpm: 68, }, quality: { ...makeProcessingResult().quality, score: 0.52, level: 'poor', }, prv: { ...makeProcessingResult().prv, reliable: false, }, }); const snapshot = send( createInitialMeasurementMachine(), {type: 'START'}, {type: 'CAMERA_PERMISSION_GRANTED'}, {type: 'CAMERA_READY'}, {type: 'CONTACT_CHANGED', contactState: 'valid'}, {type: 'STABILIZATION_COMPLETE'}, {type: 'ACQUISITION_COMPLETE'}, {type: 'PROCESSING_SUCCEEDED', result: partialResult}, ); expect(snapshot.value).toBe('completed'); expect(snapshot.context.result?.bpm.bpm).toBe(68); expect(snapshot.context.result?.prv.reliable).toBe(false); }); });