diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 5b1f02d8f..7753e1d0e 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,5 +1,13 @@ ## 12.0.0 +* **BREAKING** This release introduces a significant architectural change to the `health` plugin by removing the `singleton` pattern. + * **Dependency Injection for `DeviceInfoPlugin`**: + - The `Health` class is no longer a singleton. + - The `Health()` factory constructor is removed. + - The `Health` class now accepts an (optional) `DeviceInfoPlugin` dependency through its constructor, this change was introduced to provide easy mocking of the `DeviceInfo` class during unit tests. + - This architectural change means that, for the application to work correctly, the `Health` class *MUST* be initialized correctly as a global instance. + * **Impact**: + - For most users, **no immediate code changes are required** but it is paramount to initialize the `Health` class as a global instance (i.e. do not call `Health()` every time but rather define an instance `final health = Health();`). * **BREAKING** (Android) Remove automatic permission request of `DISTANCE_DELTA` and `TOTAL_CALORIES_BURNED` data types when requesting permission for `WORKOUT` health data type. * For `WORKOUT`s that require above permissions, now those need to be requested manually. * Fix [#984](https://github.com/cph-cachet/flutter-plugins/issues/984) - PR [#1055](https://github.com/cph-cachet/flutter-plugins/pull/1055) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index e86985d6c..f67f860a0 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -7,6 +7,9 @@ import 'package:health_example/util.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:carp_serializable/carp_serializable.dart'; +// Global Health instance +final health = Health(); + void main() => runApp(HealthApp()); class HealthApp extends StatefulWidget { @@ -85,15 +88,15 @@ class _HealthAppState extends State { @override void initState() { // configure the health plugin before use and check the Health Connect status - Health().configure(); - Health().getHealthConnectSdkStatus(); + health.configure(); + health.getHealthConnectSdkStatus(); super.initState(); } /// Install Google Health Connect on this phone. Future installHealthConnect() async => - await Health().installHealthConnect(); + await health.installHealthConnect(); /// Authorize, i.e. get permissions to access relevant health data. Future authorize() async { @@ -107,7 +110,7 @@ class _HealthAppState extends State { // Check if we have health permissions bool? hasPermissions = - await Health().hasPermissions(types, permissions: permissions); + await health.hasPermissions(types, permissions: permissions); // hasPermissions = false because the hasPermission cannot disclose if WRITE access exists. // Hence, we have to request with WRITE as well. @@ -117,8 +120,8 @@ class _HealthAppState extends State { if (!hasPermissions) { // requesting access to the data types before reading them try { - authorized = await Health() - .requestAuthorization(types, permissions: permissions); + authorized = + await health.requestAuthorization(types, permissions: permissions); } catch (error) { debugPrint("Exception in authorize: $error"); } @@ -132,7 +135,7 @@ class _HealthAppState extends State { Future getHealthConnectSdkStatus() async { assert(Platform.isAndroid, "This is only available on Android"); - final status = await Health().getHealthConnectSdkStatus(); + final status = await health.getHealthConnectSdkStatus(); setState(() { _contentHealthConnectStatus = @@ -154,7 +157,7 @@ class _HealthAppState extends State { try { // fetch health data - List healthData = await Health().getHealthDataFromTypes( + List healthData = await health.getHealthDataFromTypes( types: types, startTime: yesterday, endTime: now, @@ -175,7 +178,7 @@ class _HealthAppState extends State { } // filter out duplicates - _healthDataList = Health().removeDuplicates(_healthDataList); + _healthDataList = health.removeDuplicates(_healthDataList); for (var data in _healthDataList) { debugPrint(toJsonString(data)); @@ -201,91 +204,91 @@ class _HealthAppState extends State { bool success = true; // misc. health data examples using the writeHealthData() method - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 1.925, type: HealthDataType.HEIGHT, startTime: earlier, endTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 90, type: HealthDataType.WEIGHT, startTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 90, type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 90, type: HealthDataType.STEPS, startTime: earlier, endTime: now, recordingMethod: RecordingMethod.manual); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 200, type: HealthDataType.ACTIVE_ENERGY_BURNED, startTime: earlier, endTime: now, ); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 70, type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now); if (Platform.isIOS) { - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 30, type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, startTime: earlier, endTime: now); } else { - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 30, type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, startTime: earlier, endTime: now); } - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 37, type: HealthDataType.BODY_TEMPERATURE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 105, type: HealthDataType.BLOOD_GLUCOSE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 1.8, type: HealthDataType.WATER, startTime: earlier, endTime: now); // different types of sleep - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_REM, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_ASLEEP, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_AWAKE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 0.0, type: HealthDataType.SLEEP_DEEP, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 22, type: HealthDataType.LEAN_BODY_MASS, startTime: earlier, @@ -293,12 +296,12 @@ class _HealthAppState extends State { ); // specialized write methods - success &= await Health().writeBloodOxygen( + success &= await health.writeBloodOxygen( saturation: 98, startTime: earlier, endTime: now, ); - success &= await Health().writeWorkoutData( + success &= await health.writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, title: "Random workout name that shows up in Health Connect", start: now.subtract(const Duration(minutes: 15)), @@ -306,12 +309,12 @@ class _HealthAppState extends State { totalDistance: 2430, totalEnergyBurned: 400, ); - success &= await Health().writeBloodPressure( + success &= await health.writeBloodPressure( systolic: 90, diastolic: 80, startTime: now, ); - success &= await Health().writeMeal( + success &= await health.writeMeal( mealType: MealType.SNACK, startTime: earlier, endTime: now, @@ -363,7 +366,7 @@ class _HealthAppState extends State { // const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]; // const leftEarSensitivities = [49.0, 54.0, 89.0, 52.0, 77.0, 35.0]; // const rightEarSensitivities = [76.0, 66.0, 90.0, 22.0, 85.0, 44.5]; - // success &= await Health().writeAudiogram( + // success &= await health.writeAudiogram( // frequencies, // leftEarSensitivities, // rightEarSensitivities, @@ -375,7 +378,7 @@ class _HealthAppState extends State { // }, // ); - success &= await Health().writeMenstruationFlow( + success &= await health.writeMenstruationFlow( flow: MenstrualFlow.medium, isStartOfCycle: true, startTime: earlier, @@ -383,8 +386,8 @@ class _HealthAppState extends State { ); // Available on iOS 16.0+ only - if (Platform.isIOS) { - success &= await Health().writeHealthData( + if (Platform.isIOS) { + success &= await health.writeHealthData( value: 22, type: HealthDataType.WATER_TEMPERATURE, startTime: earlier, @@ -392,7 +395,7 @@ class _HealthAppState extends State { recordingMethod: RecordingMethod.manual ); - success &= await Health().writeHealthData( + success &= await health.writeHealthData( value: 55, type: HealthDataType.UNDERWATER_DEPTH, startTime: earlier, @@ -413,7 +416,7 @@ class _HealthAppState extends State { bool success = true; for (HealthDataType type in types) { - success &= await Health().delete( + success &= await health.delete( type: type, startTime: earlier, endTime: now, @@ -434,15 +437,15 @@ class _HealthAppState extends State { final midnight = DateTime(now.year, now.month, now.day); bool stepsPermission = - await Health().hasPermissions([HealthDataType.STEPS]) ?? false; + await health.hasPermissions([HealthDataType.STEPS]) ?? false; if (!stepsPermission) { stepsPermission = - await Health().requestAuthorization([HealthDataType.STEPS]); + await health.requestAuthorization([HealthDataType.STEPS]); } if (stepsPermission) { try { - steps = await Health().getTotalStepsInInterval(midnight, now, + steps = await health.getTotalStepsInInterval(midnight, now, includeManualEntry: !recordingMethodsToFilter.contains(RecordingMethod.manual)); } catch (error) { @@ -468,7 +471,7 @@ class _HealthAppState extends State { bool success = false; try { - await Health().revokePermissions(); + await health.revokePermissions(); success = true; } catch (error) { debugPrint("Exception in revokeAccess: $error"); @@ -503,7 +506,7 @@ class _HealthAppState extends State { child: const Text("Check Health Connect Status", style: TextStyle(color: Colors.white))), if (Platform.isAndroid && - Health().healthConnectSdkStatus != + health.healthConnectSdkStatus != HealthConnectSdkStatus.sdkAvailable) TextButton( onPressed: installHealthConnect, @@ -513,7 +516,7 @@ class _HealthAppState extends State { style: TextStyle(color: Colors.white))), if (Platform.isIOS || Platform.isAndroid && - Health().healthConnectSdkStatus == + health.healthConnectSdkStatus == HealthConnectSdkStatus.sdkAvailable) Wrap(spacing: 10, children: [ TextButton( diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 8df652e4e..06b76bb52 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -34,19 +34,15 @@ part of '../health.dart'; /// or getter methods. Otherwise, the plugin will throw an exception. class Health { static const MethodChannel _channel = MethodChannel('flutter_health'); - static final _instance = Health._(); String? _deviceId; - final _deviceInfo = DeviceInfoPlugin(); - HealthConnectSdkStatus _healthConnectSdkStatus = - HealthConnectSdkStatus.sdkUnavailable; + final DeviceInfoPlugin _deviceInfo; + HealthConnectSdkStatus _healthConnectSdkStatus = + HealthConnectSdkStatus.sdkUnavailable; - Health._() { - _registerFromJsonFunctions(); - } - - /// The singleton [Health] instance. - factory Health() => _instance; + Health({DeviceInfoPlugin? deviceInfo}) : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() { + _registerFromJsonFunctions(); + } /// The latest status on availability of Health Connect SDK on this phone. HealthConnectSdkStatus get healthConnectSdkStatus => _healthConnectSdkStatus; diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index 8a1cc79f5..16c5823af 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -26,6 +26,7 @@ dev_dependencies: # dart run build_runner build --delete-conflicting-outputs build_runner: any json_serializable: any + mocktail: ^1.0.4 flutter: plugin: diff --git a/packages/health/test/health_test.dart b/packages/health/test/health_test.dart index 8b1378917..dc7c88cbc 100644 --- a/packages/health/test/health_test.dart +++ b/packages/health/test/health_test.dart @@ -1 +1,337 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:health/health.dart'; +import 'package:carp_serializable/carp_serializable.dart'; +import 'mocks/device_info_mock.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('HealthDataPoint fromJson Tests', () { + + //Instantiate Health class with the Mock + final health = Health(deviceInfo: MockDeviceInfoPlugin()); + setUpAll(() async { + await health.configure(); + }); + test('Test WorkoutHealthValue', () async { + var entry = { + "uuid": "A91A2F10-3D7B-486A-B140-5ADCD3C9C6D0", + "value": { + "__type": "WorkoutHealthValue", + "workoutActivityType": "AMERICAN_FOOTBALL", + "totalEnergyBurned": 100, + "totalEnergyBurnedUnit": "KILOCALORIE", + "totalDistance": 2000, + "totalDistanceUnit": "METER" + }, + "type": "WORKOUT", + "unit": "NO_UNIT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "756B1A7A-C972-4BDB-9748-0D4749CF299C", + "sourceId": "com.apple.Health", + "sourceName": "Salud", + "recordingMethod": "manual", + "workoutSummary": { + "workoutType": "AMERICAN_FOOTBALL", + "totalDistance": 2000, + "totalEnergyBurned": 100, + "totalSteps": 0 + } + }; + + var hdp = HealthDataPoint.fromJson(entry); + + expect(hdp.uuid, "A91A2F10-3D7B-486A-B140-5ADCD3C9C6D0"); + expect(hdp.type, HealthDataType.WORKOUT); + expect(hdp.unit, HealthDataUnit.NO_UNIT); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "756B1A7A-C972-4BDB-9748-0D4749CF299C"); + expect(hdp.sourceId, "com.apple.Health"); + expect(hdp.sourceName, "Salud"); + expect(hdp.recordingMethod, RecordingMethod.manual); + + expect(hdp.value, isA()); + expect((hdp.value as WorkoutHealthValue).workoutActivityType, + HealthWorkoutActivityType.AMERICAN_FOOTBALL); + expect((hdp.value as WorkoutHealthValue).totalEnergyBurned, 100); + expect((hdp.value as WorkoutHealthValue).totalEnergyBurnedUnit, + HealthDataUnit.KILOCALORIE); + expect((hdp.value as WorkoutHealthValue).totalDistance, 2000); + expect((hdp.value as WorkoutHealthValue).totalDistanceUnit, + HealthDataUnit.METER); + + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + + + }); + test('Test NumericHealthValue', () { + final json = { + "uuid": "some-uuid-1", + "value": {"__type": "NumericHealthValue", "numericValue": 123.45}, + "type": "HEART_RATE", + "unit": "COUNT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "automatic" + }; + + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-1"); + expect(hdp.type, HealthDataType.HEART_RATE); + expect(hdp.unit, HealthDataUnit.COUNT); + expect(hdp.sourcePlatform, HealthPlatformType.googleHealthConnect); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.automatic); + + expect(hdp.value, isA()); + expect((hdp.value as NumericHealthValue).numericValue, 123.45); + + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test AudiogramHealthValue', () { + final json = { + "uuid": "some-uuid-2", + "value": { + "__type": "AudiogramHealthValue", + "frequencies": [1000.0, 2000.0, 3000.0], + "leftEarSensitivities": [20.0, 25.0, 30.0], + "rightEarSensitivities": [15.0, 20.0, 25.0] + }, + "type": "AUDIOGRAM", + "unit": "DECIBEL_HEARING_LEVEL", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "manual" + }; + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-2"); + expect(hdp.type, HealthDataType.AUDIOGRAM); + expect(hdp.unit, HealthDataUnit.DECIBEL_HEARING_LEVEL); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.manual); + expect(hdp.value, isA()); + + final audiogramValue = hdp.value as AudiogramHealthValue; + expect(audiogramValue.frequencies, [1000.0, 2000.0, 3000.0]); + expect(audiogramValue.leftEarSensitivities, [20.0, 25.0, 30.0]); + expect(audiogramValue.rightEarSensitivities, [15.0, 20.0, 25.0]); + + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test ElectrocardiogramHealthValue', () { + final json = { + "uuid": "some-uuid-3", + "value": { + "__type": "ElectrocardiogramHealthValue", + "voltageValues": [ + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.1, + "timeSinceSampleStart": 0.01 + }, + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.2, + "timeSinceSampleStart": 0.02 + }, + { + "__type": "ElectrocardiogramVoltageValue", + "voltage": 0.3, + "timeSinceSampleStart": 0.03 + } + ], + }, + "type": "ELECTROCARDIOGRAM", + "unit": "VOLT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "appleHealth", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "active" + }; + + final hdp = HealthDataPoint.fromJson(json); + + expect(hdp.uuid, "some-uuid-3"); + expect(hdp.type, HealthDataType.ELECTROCARDIOGRAM); + expect(hdp.unit, HealthDataUnit.VOLT); + expect(hdp.sourcePlatform, HealthPlatformType.appleHealth); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.active); + expect(hdp.value, isA()); + + final ecgValue = hdp.value as ElectrocardiogramHealthValue; + expect(ecgValue.voltageValues.length, 3); + expect(ecgValue.voltageValues[0], isA()); + expect(ecgValue.voltageValues[0].voltage, 0.1); + expect(ecgValue.voltageValues[0].timeSinceSampleStart, 0.01); + expect(ecgValue.voltageValues[1].voltage, 0.2); + expect(ecgValue.voltageValues[1].timeSinceSampleStart, 0.02); + expect(ecgValue.voltageValues[2].voltage, 0.3); + expect(ecgValue.voltageValues[2].timeSinceSampleStart, 0.03); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test NutritionHealthValue', () { + final json = { + "uuid": "some-uuid-4", + "value": { + "__type": "NutritionHealthValue", + "calories": 500.0, + "carbs": 60.0, + "protein": 20.0, + "fat": 30.0, + "caffeine": 100.0, + "vitaminA": 20.0, + "b1Thiamine": 20.0, + "b2Riboflavin": 20.0, + "b3Niacin": 20.0, + "b5PantothenicAcid": 20.0, + "b6Pyridoxine": 20.0, + "b7Biotin": 20.0, + "b9Folate": 20.0, + "b12Cobalamin": 20.0, + "vitaminC": 20.0, + "vitaminD": 20.0, + "vitaminE": 20.0, + "vitaminK": 20.0, + "calcium": 20.0, + "cholesterol": 20.0, + "chloride": 20.0, + "chromium": 20.0, + "copper": 20.0, + "fatUnsaturated": 20.0, + "fatMonounsaturated": 20.0, + "fatPolyunsaturated": 20.0, + "fatSaturated": 20.0, + "fatTransMonoenoic": 20.0, + "fiber": 20.0, + "iodine": 20.0, + "iron": 20.0, + "magnesium": 20.0, + "manganese": 20.0, + "molybdenum": 20.0, + "phosphorus": 20.0, + "potassium": 20.0, + "selenium": 20.0, + "sodium": 20.0, + "sugar": 20.0, + "water": 20.0, + "zinc": 20.0 + }, + "type": "NUTRITION", + "unit": "NO_UNIT", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "manual" + }; + + final hdp = HealthDataPoint.fromJson(json); + expect(hdp.uuid, "some-uuid-4"); + expect(hdp.type, HealthDataType.NUTRITION); + expect(hdp.unit, HealthDataUnit.NO_UNIT); + expect(hdp.sourcePlatform, HealthPlatformType.googleHealthConnect); + expect(hdp.sourceDeviceId, "some-device-id"); + expect(hdp.sourceId, "some-source-id"); + expect(hdp.sourceName, "some-source-name"); + expect(hdp.recordingMethod, RecordingMethod.manual); + expect(hdp.value, isA()); + + final nutritionValue = hdp.value as NutritionHealthValue; + expect(nutritionValue.calories, 500.0); + expect(nutritionValue.carbs, 60.0); + expect(nutritionValue.protein, 20.0); + expect(nutritionValue.fat, 30.0); + expect(nutritionValue.caffeine, 100.0); + expect(nutritionValue.vitaminA, 20.0); + expect(nutritionValue.b1Thiamine, 20.0); + expect(nutritionValue.b2Riboflavin, 20.0); + expect(nutritionValue.b3Niacin, 20.0); + expect(nutritionValue.b5PantothenicAcid, 20.0); + expect(nutritionValue.b6Pyridoxine, 20.0); + expect(nutritionValue.b7Biotin, 20.0); + expect(nutritionValue.b9Folate, 20.0); + expect(nutritionValue.b12Cobalamin, 20.0); + expect(nutritionValue.vitaminC, 20.0); + expect(nutritionValue.vitaminD, 20.0); + expect(nutritionValue.vitaminE, 20.0); + expect(nutritionValue.vitaminK, 20.0); + expect(nutritionValue.calcium, 20.0); + expect(nutritionValue.cholesterol, 20.0); + expect(nutritionValue.chloride, 20.0); + expect(nutritionValue.chromium, 20.0); + expect(nutritionValue.copper, 20.0); + expect(nutritionValue.fatUnsaturated, 20.0); + expect(nutritionValue.fatMonounsaturated, 20.0); + expect(nutritionValue.fatPolyunsaturated, 20.0); + expect(nutritionValue.fatSaturated, 20.0); + expect(nutritionValue.fatTransMonoenoic, 20.0); + expect(nutritionValue.fiber, 20.0); + expect(nutritionValue.iodine, 20.0); + expect(nutritionValue.iron, 20.0); + expect(nutritionValue.magnesium, 20.0); + expect(nutritionValue.manganese, 20.0); + expect(nutritionValue.molybdenum, 20.0); + expect(nutritionValue.phosphorus, 20.0); + expect(nutritionValue.potassium, 20.0); + expect(nutritionValue.selenium, 20.0); + expect(nutritionValue.sodium, 20.0); + expect(nutritionValue.sugar, 20.0); + expect(nutritionValue.water, 20.0); + expect(nutritionValue.zinc, 20.0); + // debugPrint(toJsonString(hdp)); + expect(toJsonString(hdp), isA()); + }); + test('Test HealthValue error handling', () { + final json = { + "uuid": "some-uuid-error", + "value": { + "__type": "UnknownHealthValue", // This should throw an error + "numericValue": 123.45 + }, + "type": "HEART_RATE", + "unit": "COUNT_PER_MINUTE", + "dateFrom": "2024-09-24T17:34:00.000", + "dateTo": "2024-09-24T17:57:00.000", + "sourcePlatform": "googleHealthConnect", + "sourceDeviceId": "some-device-id", + "sourceId": "some-source-id", + "sourceName": "some-source-name", + "recordingMethod": "automatic" + }; + expect( + () => HealthDataPoint.fromJson(json), + throwsA( + isA())); //Expect SerializationException + }); + }); +} diff --git a/packages/health/test/mocks/device_info_mock.dart b/packages/health/test/mocks/device_info_mock.dart new file mode 100644 index 000000000..019a36012 --- /dev/null +++ b/packages/health/test/mocks/device_info_mock.dart @@ -0,0 +1,60 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +class MockDeviceInfoPlugin extends Mock implements DeviceInfoPlugin { + @override + Future get androidInfo => + Future.value(AndroidDeviceInfo.fromMap({ + 'id': 'mock-android-id', + 'version': { + 'baseOS': 'mock-baseOS', + 'codename': 'mock-codename', + 'incremental': 'mock-incremental', + 'previewSdkInt': 23, + 'release': 'mock-release', + 'sdkInt': 30, + 'securityPatch': 'mock-securityPatch', + }, + 'board': 'mock-board', + 'bootloader': 'mock-bootloader', + 'brand': 'mock-brand', + 'device': 'mock-device', + 'display': 'mock-display', + 'fingerprint': 'mock-fingerprint', + 'hardware': 'mock-hardware', + 'host': 'mock-host', + 'manufacturer': 'mock-manufacturer', + 'model': 'mock-model', + 'product': 'mock-product', + 'supported32BitAbis': [], + 'supported64BitAbis': [], + 'supportedAbis': [], + 'tags': 'mock-tags', + 'type': 'mock-type', + 'isPhysicalDevice': true, + 'systemFeatures': [], + 'serialNumber': 'mock-serial', + 'isLowRamDevice': false, + })); + + + @override + Future get iosInfo => Future.value(IosDeviceInfo.fromMap({ + 'name': 'mock-ios-name', + 'systemName': 'mock-ios-systemName', + 'systemVersion': '16.0', + 'model': 'mock-ios-model', + 'modelName': 'mock-ios-modelName', + 'localizedModel': 'mock-ios-localizedModel', + 'identifierForVendor': 'mock-ios-id', + 'isPhysicalDevice': true, + 'isiOSAppOnMac': false, + 'utsname': { + 'sysname': 'mock-ios-sysname', + 'nodename': 'mock-ios-nodename', + 'release': 'mock-ios-release', + 'version': 'mock-ios-version', + 'machine': 'mock-ios-machine', + }, + })); +} \ No newline at end of file