From bcacd836d2379c813991eeac5508a92d5f1aaa08 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 18 Apr 2023 12:37:18 +0200 Subject: [PATCH 1/9] ability to mock services --- lib/src/services/api_provider.dart | 10 ++++++++-- lib/src/services/data_repository.dart | 16 +++++++++++----- lib/src/services/intent_repository.dart | 13 ++++++++++--- lib/src/services/notification_provider.dart | 13 ++++++++++--- lib/src/services/timer_repository.dart | 9 +++++++-- lib/src/services/user_repository.dart | 14 ++++++++++---- 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/lib/src/services/api_provider.dart b/lib/src/services/api_provider.dart index 6c8705ed..6cc941a7 100644 --- a/lib/src/services/api_provider.dart +++ b/lib/src/services/api_provider.dart @@ -1,7 +1,13 @@ part of 'services.dart'; class ApiProvider { - factory ApiProvider() => _apiProvider; + factory ApiProvider() => _apiProvider ??= ApiProvider._(); + + // coverage:ignore-start + @visibleForTesting + factory ApiProvider.mocked(ApiProvider mock) => _apiProvider ??= mock; + // coverage:ignore-end + ApiProvider._() { final auth = UserRepository().currentAppAuthentication; @@ -37,7 +43,7 @@ class ApiProvider { miscApi = ncCookbookApi.getMiscApi(); tagsApi = ncCookbookApi.getTagsApi(); } - static final ApiProvider _apiProvider = ApiProvider._(); + static ApiProvider? _apiProvider; late NcCookbookApi ncCookbookApi; late RecipesApi recipeApi; diff --git a/lib/src/services/data_repository.dart b/lib/src/services/data_repository.dart index 9e689b1f..ed6c14da 100644 --- a/lib/src/services/data_repository.dart +++ b/lib/src/services/data_repository.dart @@ -1,16 +1,22 @@ part of 'services.dart'; class DataRepository { - factory DataRepository() => _dataRepository; + factory DataRepository() => _dataRepository ??= const DataRepository._(); - DataRepository._(); + // coverage:ignore-start + @visibleForTesting + factory DataRepository.mocked(DataRepository mock) => + _dataRepository ??= mock; + // coverage:ignore-end + + const DataRepository._(); // Singleton - static final DataRepository _dataRepository = DataRepository._(); + static DataRepository? _dataRepository; // Provider List - final ApiProvider api = ApiProvider(); + static final api = ApiProvider(); - final NextcloudMetadataApi _nextcloudMetadataApi = NextcloudMetadataApi(); + static final _nextcloudMetadataApi = NextcloudMetadataApi(); // Data static final String categoryAll = translate('categories.all_categories'); diff --git a/lib/src/services/intent_repository.dart b/lib/src/services/intent_repository.dart index 9b5d4967..61b33c74 100644 --- a/lib/src/services/intent_repository.dart +++ b/lib/src/services/intent_repository.dart @@ -1,11 +1,18 @@ part of 'services.dart'; class IntentRepository { - factory IntentRepository() => _intentRepository; + factory IntentRepository() => + _intentRepository ??= const IntentRepository._(); - IntentRepository._(); + // coverage:ignore-start + @visibleForTesting + factory IntentRepository.mocked(IntentRepository mock) => + _intentRepository ??= mock; + // coverage:ignore-end + + const IntentRepository._(); // Singleton Pattern - static final IntentRepository _intentRepository = IntentRepository._(); + static IntentRepository? _intentRepository; static final _navigationKey = GlobalKey(); static const platform = MethodChannel('app.channel.shared.data'); diff --git a/lib/src/services/notification_provider.dart b/lib/src/services/notification_provider.dart index 2d8998e0..3e3e8b72 100644 --- a/lib/src/services/notification_provider.dart +++ b/lib/src/services/notification_provider.dart @@ -17,11 +17,18 @@ final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); class NotificationService { - factory NotificationService() => _notificationService; + + factory NotificationService() => + _notificationService ??= NotificationService._(); + + // coverage:ignore-start + @visibleForTesting + factory NotificationService.mocked(NotificationService mock) => + _notificationService ??= mock; + // coverage:ignore-end NotificationService._(); - static final NotificationService _notificationService = - NotificationService._(); + static NotificationService? _notificationService; int curId = 0; Future init() async { diff --git a/lib/src/services/timer_repository.dart b/lib/src/services/timer_repository.dart index f38ac067..c132bb68 100644 --- a/lib/src/services/timer_repository.dart +++ b/lib/src/services/timer_repository.dart @@ -1,10 +1,15 @@ part of 'services.dart'; class TimerList { - factory TimerList() => _instance; + factory TimerList() => _instance ??= TimerList._(); + + // coverage:ignore-start + @visibleForTesting + factory TimerList.mocked(TimerList mock) => _instance ??= mock; + // coverage:ignore-end TimerList._() : _timers = []; - static final TimerList _instance = TimerList._(); + static TimerList? _instance; final List _timers; List get timers => _timers; diff --git a/lib/src/services/user_repository.dart b/lib/src/services/user_repository.dart index c2158533..554d6238 100644 --- a/lib/src/services/user_repository.dart +++ b/lib/src/services/user_repository.dart @@ -1,13 +1,19 @@ part of 'services.dart'; class UserRepository { - factory UserRepository() => _userRepository; + factory UserRepository() => _userRepository ??= const UserRepository._(); - UserRepository._(); + // coverage:ignore-start + @visibleForTesting + factory UserRepository.mocked(UserRepository mock) => + _userRepository ??= mock; + // coverage:ignore-end + + const UserRepository._(); // Singleton - static final UserRepository _userRepository = UserRepository._(); + static UserRepository? _userRepository; - AuthenticationProvider authenticationProvider = AuthenticationProvider(); + static final authenticationProvider = AuthenticationProvider(); Future authenticate( String serverUrl, From a3514503d5324fb553bb897b82512e2b92cfa67f Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 18 Apr 2023 11:17:51 +0200 Subject: [PATCH 2/9] test timer repository --- pubspec.yaml | 1 + test/services/timer_repository_test.dart | 36 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 test/services/timer_repository_test.dart diff --git a/pubspec.yaml b/pubspec.yaml index ac3e3bc3..49a23979 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,6 +109,7 @@ dev_dependencies: sdk: flutter lint: ^2.0.0 + mocktail: ^0.3.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/services/timer_repository_test.dart b/test/services/timer_repository_test.dart new file mode 100644 index 00000000..71d61d13 --- /dev/null +++ b/test/services/timer_repository_test.dart @@ -0,0 +1,36 @@ +// ignore_for_file: discarded_futures + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; + +class NotificationServiceMock extends Mock implements NotificationService {} + +// ignore: avoid_implementing_value_types +class FakeTimer extends Fake implements Timer {} + +void main() { + final notificationService = NotificationServiceMock(); + final timer = FakeTimer(); + + setUpAll(() => NotificationService.mocked(notificationService)); + + group(TimerList, () { + test('add timer', () { + when(() => notificationService.start(timer)).thenAnswer((_) async {}); + + TimerList().add(timer); + verify(() => notificationService.start(timer)).called(1); + expect(TimerList().timers, contains(timer)); + }); + + test('remove timer', () { + when(() => notificationService.cancel(timer)).thenAnswer((_) async {}); + + TimerList().remove(timer); + verify(() => notificationService.cancel(timer)).called(1); + expect(TimerList().timers, isNot(contains(timer))); + }); + }); +} From fb21b443d70bcabb1644b7e6d623d0a0622b8c3c Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Wed, 5 Apr 2023 13:20:30 +0200 Subject: [PATCH 3/9] restructure tests --- test/{models => }/app_authentication_test.dart | 0 test/{services => }/timer_repository_test.dart | 0 test/{models => }/timer_test.dart | 0 test/{util => }/url_validator_test.dart | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename test/{models => }/app_authentication_test.dart (100%) rename test/{services => }/timer_repository_test.dart (100%) rename test/{models => }/timer_test.dart (100%) rename test/{util => }/url_validator_test.dart (100%) diff --git a/test/models/app_authentication_test.dart b/test/app_authentication_test.dart similarity index 100% rename from test/models/app_authentication_test.dart rename to test/app_authentication_test.dart diff --git a/test/services/timer_repository_test.dart b/test/timer_repository_test.dart similarity index 100% rename from test/services/timer_repository_test.dart rename to test/timer_repository_test.dart diff --git a/test/models/timer_test.dart b/test/timer_test.dart similarity index 100% rename from test/models/timer_test.dart rename to test/timer_test.dart diff --git a/test/util/url_validator_test.dart b/test/url_validator_test.dart similarity index 100% rename from test/util/url_validator_test.dart rename to test/url_validator_test.dart From e69d3f558b43c1becc58c379ce8f93e1ac099d77 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Mon, 17 Apr 2023 08:34:13 +0200 Subject: [PATCH 4/9] test nutrition extension --- lib/src/models/recipe.dart | 52 +++++++++++++-------------- lib/src/screens/recipe_screen.dart | 3 +- test/recipe_extension_test.dart | 57 ++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 test/recipe_extension_test.dart diff --git a/lib/src/models/recipe.dart b/lib/src/models/recipe.dart index 733edb9e..e794d683 100644 --- a/lib/src/models/recipe.dart +++ b/lib/src/models/recipe.dart @@ -1,43 +1,43 @@ import 'package:nc_cookbook_api/nc_cookbook_api.dart'; -extension RecipeExtension on Recipe { - Map get nutritionList { +extension NutritionExtension on Nutrition { + Map get asMap { final items = {}; - if (nutrition.calories != null) { - items['calories'] = nutrition.calories!; + if (calories != null) { + items['calories'] = calories!; } - if (nutrition.carbohydrateContent != null) { - items['carbohydrateContent'] = nutrition.carbohydrateContent!; + if (carbohydrateContent != null) { + items['carbohydrateContent'] = carbohydrateContent!; } - if (nutrition.cholesterolContent != null) { - items['cholesterolContent'] = nutrition.cholesterolContent!; + if (cholesterolContent != null) { + items['cholesterolContent'] = cholesterolContent!; } - if (nutrition.fatContent != null) { - items['fatContent'] = nutrition.fatContent!; + if (fatContent != null) { + items['fatContent'] = fatContent!; } - if (nutrition.fiberContent != null) { - items['fiberContent'] = nutrition.fiberContent!; + if (fiberContent != null) { + items['fiberContent'] = fiberContent!; } - if (nutrition.proteinContent != null) { - items['proteinContent'] = nutrition.proteinContent!; + if (proteinContent != null) { + items['proteinContent'] = proteinContent!; } - if (nutrition.saturatedFatContent != null) { - items['saturatedFatContent'] = nutrition.saturatedFatContent!; + if (saturatedFatContent != null) { + items['saturatedFatContent'] = saturatedFatContent!; } - if (nutrition.servingSize != null) { - items['servingSize'] = nutrition.servingSize!; + if (servingSize != null) { + items['servingSize'] = servingSize!; } - if (nutrition.sodiumContent != null) { - items['sodiumContent'] = nutrition.sodiumContent!; + if (sodiumContent != null) { + items['sodiumContent'] = sodiumContent!; } - if (nutrition.sugarContent != null) { - items['sugarContent'] = nutrition.sugarContent!; + if (sugarContent != null) { + items['sugarContent'] = sugarContent!; } - if (nutrition.transFatContent != null) { - items['transFatContent'] = nutrition.transFatContent!; + if (transFatContent != null) { + items['transFatContent'] = transFatContent!; } - if (nutrition.unsaturatedFatContent != null) { - items['unsaturatedFatContent'] = nutrition.unsaturatedFatContent!; + if (unsaturatedFatContent != null) { + items['unsaturatedFatContent'] = unsaturatedFatContent!; } return items; diff --git a/lib/src/screens/recipe_screen.dart b/lib/src/screens/recipe_screen.dart index 755832ac..05d27daf 100644 --- a/lib/src/screens/recipe_screen.dart +++ b/lib/src/screens/recipe_screen.dart @@ -132,7 +132,8 @@ class _RecipeScreenBodyState extends State { @override Widget build(BuildContext context) { final list = [ - if (recipe.nutritionList.isNotEmpty) NutritionList(recipe.nutritionList), + if (recipe.nutrition.asMap.isNotEmpty) + NutritionList(recipe.nutrition.asMap), if (recipe.recipeIngredient.isNotEmpty) IngredientList(recipe), InstructionList(recipe), ]; diff --git a/test/recipe_extension_test.dart b/test/recipe_extension_test.dart new file mode 100644 index 00000000..6f66b631 --- /dev/null +++ b/test/recipe_extension_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/recipe.dart'; +import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; + +class NotificationServiceMock extends Mock implements NotificationService {} + +// ignore: avoid_implementing_value_types +class FakeTimer extends Fake implements Timer {} + +void main() { + const calories = 'Test calories'; + const carbohydrateContent = 'Testcarbohydrat eContent'; + const cholesterolContent = 'Testcholestero lContent'; + const fatContent = 'Testfa tContent'; + const fiberContent = 'Testfibe rContent'; + const proteinContent = 'Testprotei nContent'; + const saturatedFatContent = 'TestsaturatedFa tContent'; + const servingSize = 'Testser vingSize'; + const sodiumContent = 'Testsodiu mContent'; + const sugarContent = 'Testsuga rContent'; + const transFatContent = 'TesttransFa tContent'; + const unsaturatedFatContent = 'TestunsaturatedFa tContent'; + + final nutritionMap = Nutrition( + (b) => b + ..calories = calories + ..carbohydrateContent = carbohydrateContent + ..cholesterolContent = cholesterolContent + ..fatContent = fatContent + ..fiberContent = fiberContent + ..proteinContent = proteinContent + ..saturatedFatContent = saturatedFatContent + ..servingSize = servingSize + ..sodiumContent = sodiumContent + ..sugarContent = sugarContent + ..transFatContent = transFatContent + ..unsaturatedFatContent = unsaturatedFatContent, + ).asMap; + + test('NutritionExtension', () { + expect(nutritionMap['calories'], calories); + expect(nutritionMap['carbohydrateContent'], carbohydrateContent); + expect(nutritionMap['cholesterolContent'], cholesterolContent); + expect(nutritionMap['fatContent'], fatContent); + expect(nutritionMap['fiberContent'], fiberContent); + expect(nutritionMap['proteinContent'], proteinContent); + expect(nutritionMap['saturatedFatContent'], saturatedFatContent); + expect(nutritionMap['servingSize'], servingSize); + expect(nutritionMap['sodiumContent'], sodiumContent); + expect(nutritionMap['sugarContent'], sugarContent); + expect(nutritionMap['transFatContent'], transFatContent); + expect(nutritionMap['unsaturatedFatContent'], unsaturatedFatContent); + }); +} From 87c171234ceece34f8e60a69ede2418fd6ada3da Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 18 Apr 2023 12:34:00 +0200 Subject: [PATCH 5/9] test duration_utils --- test/duration_utils_test.dart | 32 +++++++++++++++++++++++++++ test/helpers/translation_helpers.dart | 12 ++++++++++ 2 files changed, 44 insertions(+) create mode 100644 test/duration_utils_test.dart create mode 100644 test/helpers/translation_helpers.dart diff --git a/test/duration_utils_test.dart b/test/duration_utils_test.dart new file mode 100644 index 00000000..229af785 --- /dev/null +++ b/test/duration_utils_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:nextcloud_cookbook_flutter/src/util/duration_utils.dart'; + +import 'helpers/translation_helpers.dart'; + +void main() { + const hours = 1; + const minutes = 5; + const seconds = 30; + const duration = Duration(hours: hours, minutes: minutes, seconds: seconds); + + setUpAll(setupL10n); + + group('DurationExtension', () { + test('translatedString', () { + final translated = duration.translatedString; + + expect(translated, '$hours Hours : $minutes Minutes'); + }); + test('formatMinutes', () { + final formatted = duration.formatMinutes(); + + expect(formatted, '$hours:0$minutes'); + }); + + test('formatSeconds', () { + final formatted = duration.formatSeconds(); + + expect(formatted, '0$hours:0$minutes:$seconds'); + }); + }); +} diff --git a/test/helpers/translation_helpers.dart b/test/helpers/translation_helpers.dart new file mode 100644 index 00000000..1ad00ab4 --- /dev/null +++ b/test/helpers/translation_helpers.dart @@ -0,0 +1,12 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_translate/flutter_translate.dart'; + +void setupL10n() { + final translations = jsonDecode( + File('assets/i18n/en.json').readAsStringSync(), + ) as Map; + + Localization.load(translations); +} From 31a30e4f45c706c0e598ca57ff70eef5503ab86f Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 18 Apr 2023 13:49:17 +0200 Subject: [PATCH 6/9] test data_repository --- lib/src/services/data_repository.dart | 13 +-- lib/src/services/services.dart | 1 - test/data_repository_test.dart | 129 ++++++++++++++++++++++++++ test/mocks/mocks.dart | 44 +++++++++ 4 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 test/data_repository_test.dart create mode 100644 test/mocks/mocks.dart diff --git a/lib/src/services/data_repository.dart b/lib/src/services/data_repository.dart index ed6c14da..cf21aa38 100644 --- a/lib/src/services/data_repository.dart +++ b/lib/src/services/data_repository.dart @@ -114,16 +114,11 @@ class DataRepository { } Future _fetchCategoryMainRecipe(Category category) async { - try { - final categoryRecipes = await fetchRecipesShort(category: category.name); - if (categoryRecipes != null && categoryRecipes.isNotEmpty) { - return categoryRecipes.first; - } - } catch (e) { - log('Could not load main recipe of Category!'); - rethrow; - } + final categoryRecipes = await fetchRecipesShort(category: category.name); + if (categoryRecipes != null && categoryRecipes.isNotEmpty) { + return categoryRecipes.first; + } return null; } diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart index 82830441..82899659 100644 --- a/lib/src/services/services.dart +++ b/lib/src/services/services.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; import 'package:dio/dio.dart' as dio; import 'package:dio/dio.dart'; diff --git a/test/data_repository_test.dart b/test/data_repository_test.dart new file mode 100644 index 00000000..80ce2f5a --- /dev/null +++ b/test/data_repository_test.dart @@ -0,0 +1,129 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; + +import 'helpers/translation_helpers.dart'; +import 'mocks/mocks.dart'; + +void main() { + final api = ApiProvider.mocked(ApiMock()); + + setUpAll(setupL10n); + + group(DataRepository, () { + test('fetchRecipesShort', () async { + when(() => api.recipeApi.listRecipes()).thenAnswer( + (_) async => ResponseMock>(data: BuiltList()), + ); + when( + () => + api.categoryApi.recipesInCategory(category: any(named: 'category')), + ).thenAnswer( + (_) async => ResponseMock>(data: BuiltList()), + ); + + final allRecipes = await DataRepository() + .fetchRecipesShort(category: DataRepository.categoryAll); + + expect(allRecipes, isEmpty); + verify(() => api.recipeApi.listRecipes()).called(1); + + final uncategorized = await DataRepository() + .fetchRecipesShort(category: DataRepository.categoryUncategorized); + + expect(uncategorized, isEmpty); + verify( + () => api.categoryApi.recipesInCategory(category: '_'), + ).called(1); + + final recipes = + await DataRepository().fetchRecipesShort(category: 'category'); + + expect(recipes, isEmpty); + verify( + () => api.categoryApi.recipesInCategory(category: 'category'), + ).called(1); + }); + + test('fetchRecipe', () async { + when(() => api.recipeApi.recipeDetails(id: any(named: 'id'))).thenAnswer( + (_) async => ResponseMock(data: FakeRecipe()), + ); + + final recipe = await DataRepository().fetchRecipe('id'); + + expect(recipe, isA()); + verify(() => api.recipeApi.recipeDetails(id: 'id')).called(1); + }); + + test('updateRecipe', () async { + final recipe = FakeRecipe(); + when( + () => api.recipeApi.updateRecipe(id: recipe.id, recipe: recipe), + ).thenAnswer( + (invocation) async => ResponseMock(data: recipe.id), + ); + + final id = await DataRepository().updateRecipe(recipe); + + expect(id, recipe.id); + verify(() => api.recipeApi.updateRecipe(id: recipe.id, recipe: recipe)) + .called(1); + }); + + test('createRecipe', () async { + final recipe = FakeRecipe(); + when( + () => api.recipeApi.newRecipe(recipe: recipe), + ).thenAnswer( + (invocation) async => ResponseMock(data: recipe.id), + ); + + final id = await DataRepository().createRecipe(recipe); + + expect(id, recipe.id); + verify(() => api.recipeApi.newRecipe(recipe: recipe)).called(1); + }); + + test('deleteRecipe', () async { + final recipe = FakeRecipe(); + when( + () => api.recipeApi.deleteRecipe(id: recipe.id), + ).thenAnswer( + (invocation) async => ResponseMock(data: recipe.id), + ); + + final id = await DataRepository().deleteRecipe(recipe); + + expect(id, recipe.id); + verify(() => api.recipeApi.deleteRecipe(id: recipe.id)).called(1); + }); + + test('importRecipe', () async { + final url = UrlBuilder()..url = 'testUrl'; + when( + () => api.recipeApi.callImport(url: url.build()), + ).thenAnswer( + (invocation) async => ResponseMock(), + ); + + await DataRepository().importRecipe(url.url!); + + verify(() => api.recipeApi.callImport(url: url.build())).called(1); + }); + + test('fetchCategories', () async {}); + + test('fetchCategoryMainRecipes', () async {}); + + test('fetchAllRecipes', () async {}); + + test('getUserAvatarUrl', () async {}); + + test('getMatchingCategoryNames', () async {}); + + test('fetchImage', () async {}); + }); +} diff --git a/test/mocks/mocks.dart b/test/mocks/mocks.dart new file mode 100644 index 00000000..c515dbdb --- /dev/null +++ b/test/mocks/mocks.dart @@ -0,0 +1,44 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; +import 'package:nextcloud_cookbook_flutter/src/services/services.dart'; + +class ApiMock extends Mock implements ApiProvider { + final _recipesApi = RecipesApiMock(); + final _categoriesApi = CategoriesApiMock(); + final _miscApi = MiscApiMock(); + final _tagsApi = TagsApiMock(); + + @override + RecipesApi get recipeApi => _recipesApi; + @override + CategoriesApi get categoryApi => _categoriesApi; + @override + MiscApi get miscApi => _miscApi; + @override + TagsApi get tagsApi => _tagsApi; +} + +class RecipesApiMock extends Mock implements RecipesApi {} + +class CategoriesApiMock extends Mock implements CategoriesApi {} + +class MiscApiMock extends Mock implements MiscApi {} + +class TagsApiMock extends Mock implements TagsApi {} + +class ResponseMock extends Mock implements Response { + ResponseMock({this.data}); + + @override + T? data; +} + +class FakeRecipeStub extends Fake implements RecipeStub {} + +class FakeRecipe extends Fake implements Recipe { + @override + String get id => 'some ID'; +} + +class FakeUrl extends Fake implements Url {} From 98c2f23ae618c2c9fac1544b4e93397026bf6ff6 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 18 Apr 2023 13:31:11 +0200 Subject: [PATCH 7/9] make AnimatedList an Iterable --- lib/src/models/animated_list.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/models/animated_list.dart b/lib/src/models/animated_list.dart index e479a4dd..720ac9e2 100644 --- a/lib/src/models/animated_list.dart +++ b/lib/src/models/animated_list.dart @@ -8,7 +8,7 @@ typedef RemovedItemBuilder = Widget Function( Animation animation, ); -abstract class AnimatedListModel { +abstract class AnimatedListModel extends Iterable { AnimatedListModel({ required this.listKey, required this.removedItemBuilder, @@ -54,13 +54,16 @@ abstract class AnimatedListModel { } } + @override bool get isNotEmpty => _items.isNotEmpty; + @override int get length => _items.length; E operator [](int index) => _items[index]; - int indexOf(E item) => _items.indexOf(item); + @override + Iterator get iterator => _items.iterator; } class AnimatedTimerList extends AnimatedListModel { From efffabb02ce606e3f6b394834b5887d68b59ddff Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 18 Apr 2023 13:34:08 +0200 Subject: [PATCH 8/9] ignore lines from coverage --- lib/src/models/app_authentication.dart | 2 ++ lib/src/models/image_response.dart | 2 ++ lib/src/services/authentication_provider.dart | 2 ++ lib/src/services/net/nextcloud_metadata_api.dart | 2 ++ lib/src/util/theme_data.dart | 2 ++ 5 files changed, 10 insertions(+) diff --git a/lib/src/models/app_authentication.dart b/lib/src/models/app_authentication.dart index bae84a47..d61afe83 100644 --- a/lib/src/models/app_authentication.dart +++ b/lib/src/models/app_authentication.dart @@ -21,12 +21,14 @@ class AppAuthentication extends Equatable { ..responseType = ResponseType.plain; if (isSelfSignedCertificate) { + // coverage:ignore-start authenticatedClient.httpClientAdapter = IOHttpClientAdapter( onHttpClientCreate: (client) { client.badCertificateCallback = (cert, host, port) => true; return client; }, ); + // coverage:ignore-end } } diff --git a/lib/src/models/image_response.dart b/lib/src/models/image_response.dart index cdf8ab54..b000bdd2 100644 --- a/lib/src/models/image_response.dart +++ b/lib/src/models/image_response.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; +// coverage:ignore-start class ImageResponse { const ImageResponse({ required this.data, @@ -8,3 +9,4 @@ class ImageResponse { final Uint8List data; final bool isSvg; } +// coverage:ignore-end diff --git a/lib/src/services/authentication_provider.dart b/lib/src/services/authentication_provider.dart index 4f7c47cf..1d8ffe18 100644 --- a/lib/src/services/authentication_provider.dart +++ b/lib/src/services/authentication_provider.dart @@ -1,5 +1,6 @@ part of 'services.dart'; +// coverage:ignore-start class AuthenticationProvider { final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); final String _appAuthenticationKey = 'appAuthentication'; @@ -241,3 +242,4 @@ class AuthenticationProvider { await _secureStorage.delete(key: _appAuthenticationKey); } } +// coverage:ignore-end diff --git a/lib/src/services/net/nextcloud_metadata_api.dart b/lib/src/services/net/nextcloud_metadata_api.dart index f34bfb31..0998117b 100644 --- a/lib/src/services/net/nextcloud_metadata_api.dart +++ b/lib/src/services/net/nextcloud_metadata_api.dart @@ -1,5 +1,6 @@ part of '../services.dart'; +// coverage:ignore-start class NextcloudMetadataApi { factory NextcloudMetadataApi() => NextcloudMetadataApi._( UserRepository().currentAppAuthentication, @@ -11,3 +12,4 @@ class NextcloudMetadataApi { String getUserAvatarUrl() => '${_appAuthentication.server}/avatar/${_appAuthentication.loginName}/80'; } +// coverage:ignore-end diff --git a/lib/src/util/theme_data.dart b/lib/src/util/theme_data.dart index 38c34a6f..1c73b2d0 100644 --- a/lib/src/util/theme_data.dart +++ b/lib/src/util/theme_data.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +// coverage:ignore-start class AppTheme { const AppTheme._(); @@ -90,3 +91,4 @@ class SnackBarThemes extends ThemeExtension { @override String toString() => 'SnackBarThemes(colorScheme: $colorScheme)'; } +// coverage:ignore-end From 22229804317ff27dd478ad9b410c1dda514290eb Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 21 Apr 2023 15:36:41 +0200 Subject: [PATCH 9/9] test Timer model --- lib/src/models/timer.dart | 22 +++++++---- lib/src/models/timer.g.dart | 4 +- test/timer_test.dart | 76 +++++++++++++++++++++++++++++++------ 3 files changed, 81 insertions(+), 21 deletions(-) diff --git a/lib/src/models/timer.dart b/lib/src/models/timer.dart index 5548347d..13d5ec6b 100644 --- a/lib/src/models/timer.dart +++ b/lib/src/models/timer.dart @@ -56,8 +56,8 @@ class Timer { } @visibleForTesting @JsonKey( - toJson: _recipeToJson, - fromJson: _recipeFromJson, + toJson: recipeToJson, + fromJson: recipeFromJson, ) final Recipe? recipe; final DateTime done; @@ -114,10 +114,18 @@ class Timer { duration, recipeId, ); + // coverage:ignore-start + @override + String toString() => + 'Timer(done: $done, id: $id, title: $title, body: $body, duration: $duration, recipeId: $recipeId)'; + // coverage:ignore-end } -Recipe _recipeFromJson(String data) => - standardSerializers.fromJson(Recipe.serializer, data)!; - -String? _recipeToJson(Object? data) => - data != null ? standardSerializers.toJson(Recipe.serializer, data) : null; +@visibleForTesting +Recipe recipeFromJson(Map? data) => + standardSerializers.deserializeWith(Recipe.serializer, data)!; +@visibleForTesting +Map? recipeToJson(Object? data) => data != null + ? standardSerializers.serializeWith(Recipe.serializer, data) + as Map? + : null; diff --git a/lib/src/models/timer.g.dart b/lib/src/models/timer.g.dart index b2a9cd98..9d12fa3a 100644 --- a/lib/src/models/timer.g.dart +++ b/lib/src/models/timer.g.dart @@ -7,13 +7,13 @@ part of 'timer.dart'; // ************************************************************************** Timer _$TimerFromJson(Map json) => Timer.restore( - _recipeFromJson(json['recipe'] as String), + recipeFromJson(json['recipe'] as Map?), DateTime.parse(json['done'] as String), json['id'] as int?, ); Map _$TimerToJson(Timer instance) => { - 'recipe': _recipeToJson(instance.recipe), + 'recipe': recipeToJson(instance.recipe), 'done': instance.done.toIso8601String(), 'id': instance.id, }; diff --git a/test/timer_test.dart b/test/timer_test.dart index 82df4cd3..6f1cf8e8 100644 --- a/test/timer_test.dart +++ b/test/timer_test.dart @@ -1,45 +1,97 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; +import 'package:nc_cookbook_api/nc_cookbook_api.dart'; import 'package:nextcloud_cookbook_flutter/src/models/timer.dart'; +import 'helpers/translation_helpers.dart'; + void main() { - const title = 'title'; - const body = 'body'; + const recipeName = 'title'; const duration = Duration(minutes: 5); final done = DateTime.now().add(duration); const recipeId = '12345678'; - const id = 0; + const oldId = 0; + + const title = recipeName; + const body = 'body'; - final timer = Timer.restoreOld(done, id, recipeId, title, duration, recipeId); + final recipe = Recipe( + (b) => b + ..name = recipeName + ..id = recipeId + ..dateCreated = DateTime.now().toUtc() + ..cookTime = duration, + ); + final oldTimer = + Timer.restoreOld(done, oldId, title, body, duration, recipeId); + final newTimer = Timer(recipe); final json = - '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.millisecondsSinceEpoch},"id":$id,"recipeId":"$recipeId"}'; + '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.microsecondsSinceEpoch},"id":$oldId,"recipeId":"$recipeId"}'; final orderedJson = - '{"title":"$title","body":"$body","duration":${duration.inMinutes},"id":$id,"done":${done.millisecondsSinceEpoch},"recipeId":"$recipeId"}'; + '{"title":"$title","body":"$body","duration":${duration.inMinutes},"id":$oldId,"done":${done.microsecondsSinceEpoch},"recipeId":"$recipeId"}'; final oldJson = - '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.millisecondsSinceEpoch},"id":$id,"recipeId":$recipeId}'; + '{"title":"$title","body":"$body","duration":${duration.inMinutes},"done":${done.microsecondsSinceEpoch},"id":$oldId,"recipeId":$recipeId}'; + final newJson = + '{"recipe":${jsonEncode(recipeToJson(recipe))},"done":"${newTimer.done.toIso8601String()}","id":null}'; - final newJson = '{"recipe":null,"done":"${done.toIso8601String()}","id":$id}'; + setUpAll(setupL10n); group(Timer, () { test('toJson', () { - expect(jsonEncode(timer.toJson()), equals(newJson)); + expect(jsonEncode(newTimer.toJson()).trim(), equals(newJson)); }); test('fromJson', () { expect( Timer.fromJson(jsonDecode(json) as Map), - isA(), + equals(oldTimer), ); expect( Timer.fromJson(jsonDecode(orderedJson) as Map), - isA(), + equals(oldTimer), ); + expect( Timer.fromJson(jsonDecode(oldJson) as Map), - isA(), + equals(oldTimer), + ); + + expect( + Timer.fromJson(jsonDecode(newJson) as Map), + equals(newTimer), ); }); + + test('timer progress', () { + const duration = Duration(minutes: 5); + final now = DateTime.now(); + final done = now.add(duration); + + var timer = Timer.restoreOld( + now, + oldId, + title, + body, + Duration.zero, + recipeId, + ); + + expect(timer.remaining, Duration.zero); + expect(timer.progress, 1.0); + + timer = Timer.restoreOld( + done, + oldId, + title, + body, + duration, + recipeId, + ); + + expect(timer.progress, greaterThan(0)); + expect(timer.progress, lessThanOrEqualTo(1.0)); + }); }); }