diff --git a/mobile/lib/modules/favorite/providers/favorite_provider.dart b/mobile/lib/modules/favorite/providers/favorite_provider.dart index e702eeec62..af63a7de51 100644 --- a/mobile/lib/modules/favorite/providers/favorite_provider.dart +++ b/mobile/lib/modules/favorite/providers/favorite_provider.dart @@ -3,14 +3,15 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; class FavoriteSelectionNotifier extends StateNotifier> { - FavoriteSelectionNotifier(this.ref) : super({}) { - state = ref.watch(assetProvider).allAssets + FavoriteSelectionNotifier(this.assetsState, this.assetNotifier) : super({}) { + state = assetsState.allAssets .where((asset) => asset.isFavorite) .map((asset) => asset.id) .toSet(); } - final Ref ref; + final AssetsState assetsState; + final AssetNotifier assetNotifier; void _setFavoriteForAssetId(String id, bool favorite) { if (!favorite) { @@ -29,7 +30,7 @@ class FavoriteSelectionNotifier extends StateNotifier> { _setFavoriteForAssetId(asset.id, !_isFavorite(asset.id)); - await ref.watch(assetProvider.notifier).toggleFavorite( + await assetNotifier.toggleFavorite( asset, state.contains(asset.id), ); @@ -37,8 +38,8 @@ class FavoriteSelectionNotifier extends StateNotifier> { Future addToFavorites(Iterable assets) { state = state.union(assets.map((a) => a.id).toSet()); - final futures = assets.map((a) => - ref.watch(assetProvider.notifier).toggleFavorite( + final futures = assets.map((a) => + assetNotifier.toggleFavorite( a, true, ), @@ -50,7 +51,10 @@ class FavoriteSelectionNotifier extends StateNotifier> { final favoriteProvider = StateNotifierProvider>((ref) { - return FavoriteSelectionNotifier(ref); + return FavoriteSelectionNotifier( + ref.watch(assetProvider), + ref.watch(assetProvider.notifier), + ); }); final favoriteAssetProvider = StateProvider((ref) { diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index f2ae9ae46c..9f895fadd9 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -740,6 +740,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" + url: "https://pub.dev" + source: hosted + version: "5.3.2" nested: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 624a00aeb1..7d28923d46 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -64,6 +64,7 @@ dev_dependencies: flutter_launcher_icons: "^0.9.2" flutter_native_splash: ^2.2.16 isar_generator: *isar_version + mockito: ^5.3.2 integration_test: sdk: flutter diff --git a/mobile/test/favorite_provider_test.dart b/mobile/test/favorite_provider_test.dart new file mode 100644 index 0000000000..38a24a70d1 --- /dev/null +++ b/mobile/test/favorite_provider_test.dart @@ -0,0 +1,104 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +import 'favorite_provider_test.mocks.dart'; + +Asset _getTestAsset(String id, bool favorite) { + return Asset( + remoteId: id, + deviceAssetId: '', + deviceId: '', + ownerId: '', + fileCreatedAt: DateTime.now(), + fileModifiedAt: DateTime.now(), + durationInSeconds: 0, + fileName: '', + isFavorite: favorite, + ); +} + +void main() { + group("Test favoriteProvider", () { + + late MockAssetsState assetsState; + late MockAssetNotifier assetNotifier; + late ProviderContainer container; + late StateNotifierProvider> testFavoritesProvider; + + setUp(() { + assetsState = MockAssetsState(); + assetNotifier = MockAssetNotifier(); + container = ProviderContainer(); + + testFavoritesProvider = + StateNotifierProvider>((ref) { + return FavoriteSelectionNotifier( + assetsState, + assetNotifier, + ); + }); + },); + + test("Empty favorites provider", () { + when(assetsState.allAssets).thenReturn([]); + expect({}, container.read(testFavoritesProvider)); + }); + + test("Non-empty favorites provider", () { + when(assetsState.allAssets).thenReturn([ + _getTestAsset("001", false), + _getTestAsset("002", true), + _getTestAsset("003", false), + _getTestAsset("004", false), + _getTestAsset("005", true), + ]); + + expect({"002", "005"}, container.read(testFavoritesProvider)); + }); + + test("Toggle favorite", () { + when(assetNotifier.toggleFavorite(null, false)) + .thenAnswer((_) async => false); + + final testAsset1 = _getTestAsset("001", false); + final testAsset2 = _getTestAsset("002", true); + + when(assetsState.allAssets).thenReturn([testAsset1, testAsset2]); + + expect({"002"}, container.read(testFavoritesProvider)); + + container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset2); + expect({}, container.read(testFavoritesProvider)); + + container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset1); + expect({"001"}, container.read(testFavoritesProvider)); + }); + + test("Add favorites", () { + when(assetNotifier.toggleFavorite(null, false)) + .thenAnswer((_) async => false); + + when(assetsState.allAssets).thenReturn([]); + + expect({}, container.read(testFavoritesProvider)); + + container.read(testFavoritesProvider.notifier).addToFavorites( + [ + _getTestAsset("001", false), + _getTestAsset("002", false), + ], + ); + + expect({"001", "002"}, container.read(testFavoritesProvider)); + }); + }); +} diff --git a/mobile/test/favorite_provider_test.mocks.dart b/mobile/test/favorite_provider_test.mocks.dart new file mode 100644 index 0000000000..d79b009d4a --- /dev/null +++ b/mobile/test/favorite_provider_test.mocks.dart @@ -0,0 +1,259 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in immich_mobile/test/favorite_provider_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:hooks_riverpod/hooks_riverpod.dart' as _i7; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart' + as _i6; +import 'package:immich_mobile/shared/models/asset.dart' as _i4; +import 'package:immich_mobile/shared/providers/asset.provider.dart' as _i2; +import 'package:logging/logging.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:state_notifier/state_notifier.dart' as _i8; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeAssetsState_0 extends _i1.SmartFake implements _i2.AssetsState { + _FakeAssetsState_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeLogger_1 extends _i1.SmartFake implements _i3.Logger { + _FakeLogger_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [AssetsState]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAssetsState extends _i1.Mock implements _i2.AssetsState { + @override + List<_i4.Asset> get allAssets => (super.noSuchMethod( + Invocation.getter(#allAssets), + returnValue: <_i4.Asset>[], + returnValueForMissingStub: <_i4.Asset>[], + ) as List<_i4.Asset>); + @override + _i5.Future<_i2.AssetsState> withRenderDataStructure( + _i6.AssetGridLayoutParameters? layout) => + (super.noSuchMethod( + Invocation.method( + #withRenderDataStructure, + [layout], + ), + returnValue: _i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0( + this, + Invocation.method( + #withRenderDataStructure, + [layout], + ), + )), + returnValueForMissingStub: + _i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0( + this, + Invocation.method( + #withRenderDataStructure, + [layout], + ), + )), + ) as _i5.Future<_i2.AssetsState>); + @override + _i2.AssetsState withAdditionalAssets(List<_i4.Asset>? toAdd) => + (super.noSuchMethod( + Invocation.method( + #withAdditionalAssets, + [toAdd], + ), + returnValue: _FakeAssetsState_0( + this, + Invocation.method( + #withAdditionalAssets, + [toAdd], + ), + ), + returnValueForMissingStub: _FakeAssetsState_0( + this, + Invocation.method( + #withAdditionalAssets, + [toAdd], + ), + ), + ) as _i2.AssetsState); +} + +/// A class which mocks [AssetNotifier]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier { + @override + _i3.Logger get log => (super.noSuchMethod( + Invocation.getter(#log), + returnValue: _FakeLogger_1( + this, + Invocation.getter(#log), + ), + returnValueForMissingStub: _FakeLogger_1( + this, + Invocation.getter(#log), + ), + ) as _i3.Logger); + @override + set onError(_i7.ErrorListener? _onError) => super.noSuchMethod( + Invocation.setter( + #onError, + _onError, + ), + returnValueForMissingStub: null, + ); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i5.Stream<_i2.AssetsState> get stream => (super.noSuchMethod( + Invocation.getter(#stream), + returnValue: _i5.Stream<_i2.AssetsState>.empty(), + returnValueForMissingStub: _i5.Stream<_i2.AssetsState>.empty(), + ) as _i5.Stream<_i2.AssetsState>); + @override + _i2.AssetsState get state => (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _FakeAssetsState_0( + this, + Invocation.getter(#state), + ), + returnValueForMissingStub: _FakeAssetsState_0( + this, + Invocation.getter(#state), + ), + ) as _i2.AssetsState); + @override + set state(_i2.AssetsState? value) => super.noSuchMethod( + Invocation.setter( + #state, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i2.AssetsState get debugState => (super.noSuchMethod( + Invocation.getter(#debugState), + returnValue: _FakeAssetsState_0( + this, + Invocation.getter(#debugState), + ), + returnValueForMissingStub: _FakeAssetsState_0( + this, + Invocation.getter(#debugState), + ), + ) as _i2.AssetsState); + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i5.Future rebuildAssetGridDataStructure() => (super.noSuchMethod( + Invocation.method( + #rebuildAssetGridDataStructure, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + void onNewAssetUploaded(_i4.Asset? newAsset) => super.noSuchMethod( + Invocation.method( + #onNewAssetUploaded, + [newAsset], + ), + returnValueForMissingStub: null, + ); + @override + dynamic deleteAssets(Set<_i4.Asset>? deleteAssets) => super.noSuchMethod( + Invocation.method( + #deleteAssets, + [deleteAssets], + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future toggleFavorite( + _i4.Asset? asset, + bool? status, + ) => + (super.noSuchMethod( + Invocation.method( + #toggleFavorite, + [ + asset, + status, + ], + ), + returnValue: _i5.Future.value(false), + returnValueForMissingStub: _i5.Future.value(false), + ) as _i5.Future); + @override + bool updateShouldNotify( + _i2.AssetsState? old, + _i2.AssetsState? current, + ) => + (super.noSuchMethod( + Invocation.method( + #updateShouldNotify, + [ + old, + current, + ], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i7.RemoveListener addListener( + _i8.Listener<_i2.AssetsState>? listener, { + bool? fireImmediately = true, + }) => + (super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + {#fireImmediately: fireImmediately}, + ), + returnValue: () {}, + returnValueForMissingStub: () {}, + ) as _i7.RemoveListener); + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); +}