Bläddra i källkod

chore(mobile): Favorite provider unit test (#1874)

* Favorite provider tests

* Remove unused mock

* Add setUp function to avoid duplicate code
Matthias Rupp 2 år sedan
förälder
incheckning
c9a6820de7

+ 11 - 7
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';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 
 
 class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
 class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
-  FavoriteSelectionNotifier(this.ref) : super({}) {
-    state = ref.watch(assetProvider).allAssets
+  FavoriteSelectionNotifier(this.assetsState, this.assetNotifier) : super({}) {
+    state = assetsState.allAssets
         .where((asset) => asset.isFavorite)
         .where((asset) => asset.isFavorite)
         .map((asset) => asset.id)
         .map((asset) => asset.id)
         .toSet();
         .toSet();
   }
   }
 
 
-  final Ref ref;
+  final AssetsState assetsState;
+  final AssetNotifier assetNotifier;
 
 
   void _setFavoriteForAssetId(String id, bool favorite) {
   void _setFavoriteForAssetId(String id, bool favorite) {
     if (!favorite) {
     if (!favorite) {
@@ -29,7 +30,7 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
 
 
     _setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
     _setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
 
 
-    await ref.watch(assetProvider.notifier).toggleFavorite(
+    await assetNotifier.toggleFavorite(
       asset,
       asset,
       state.contains(asset.id),
       state.contains(asset.id),
     );
     );
@@ -37,8 +38,8 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
 
 
   Future<void> addToFavorites(Iterable<Asset> assets) {
   Future<void> addToFavorites(Iterable<Asset> assets) {
     state = state.union(assets.map((a) => a.id).toSet());
     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,
           a,
           true,
           true,
         ),
         ),
@@ -50,7 +51,10 @@ class FavoriteSelectionNotifier extends StateNotifier<Set<String>> {
 
 
 final favoriteProvider =
 final favoriteProvider =
     StateNotifierProvider<FavoriteSelectionNotifier, Set<String>>((ref) {
     StateNotifierProvider<FavoriteSelectionNotifier, Set<String>>((ref) {
-  return FavoriteSelectionNotifier(ref);
+  return FavoriteSelectionNotifier(
+      ref.watch(assetProvider),
+      ref.watch(assetProvider.notifier),
+  );
 });
 });
 
 
 final favoriteAssetProvider = StateProvider((ref) {
 final favoriteAssetProvider = StateProvider((ref) {

+ 8 - 0
mobile/pubspec.lock

@@ -740,6 +740,14 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "1.0.4"
     version: "1.0.4"
+  mockito:
+    dependency: "direct dev"
+    description:
+      name: mockito
+      sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.3.2"
   nested:
   nested:
     dependency: transitive
     dependency: transitive
     description:
     description:

+ 1 - 0
mobile/pubspec.yaml

@@ -64,6 +64,7 @@ dev_dependencies:
   flutter_launcher_icons: "^0.9.2"
   flutter_launcher_icons: "^0.9.2"
   flutter_native_splash: ^2.2.16
   flutter_native_splash: ^2.2.16
   isar_generator: *isar_version
   isar_generator: *isar_version
+  mockito: ^5.3.2
   integration_test:
   integration_test:
     sdk: flutter
     sdk: flutter
 
 

+ 104 - 0
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<AssetsState>(),
+  MockSpec<AssetNotifier>(),
+])
+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<FavoriteSelectionNotifier, Set<String>> testFavoritesProvider;
+
+    setUp(() {
+      assetsState = MockAssetsState();
+      assetNotifier = MockAssetNotifier();
+      container = ProviderContainer();
+
+      testFavoritesProvider =
+          StateNotifierProvider<FavoriteSelectionNotifier, Set<String>>((ref) {
+            return FavoriteSelectionNotifier(
+              assetsState,
+              assetNotifier,
+            );
+          });
+    },);
+
+    test("Empty favorites provider", () {
+      when(assetsState.allAssets).thenReturn([]);
+      expect(<String>{}, 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(<String>{"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(<String>{"002"}, container.read(testFavoritesProvider));
+
+      container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset2);
+      expect(<String>{}, container.read(testFavoritesProvider));
+
+      container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset1);
+      expect(<String>{"001"}, container.read(testFavoritesProvider));
+    });
+
+    test("Add favorites", () {
+      when(assetNotifier.toggleFavorite(null, false))
+          .thenAnswer((_) async => false);
+
+      when(assetsState.allAssets).thenReturn([]);
+
+      expect(<String>{}, container.read(testFavoritesProvider));
+
+      container.read(testFavoritesProvider.notifier).addToFavorites(
+        [
+          _getTestAsset("001", false),
+          _getTestAsset("002", false),
+        ],
+      );
+
+      expect(<String>{"001", "002"}, container.read(testFavoritesProvider));
+    });
+  });
+}

+ 259 - 0
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<void> rebuildAssetGridDataStructure() => (super.noSuchMethod(
+        Invocation.method(
+          #rebuildAssetGridDataStructure,
+          [],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @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<bool> toggleFavorite(
+    _i4.Asset? asset,
+    bool? status,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #toggleFavorite,
+          [
+            asset,
+            status,
+          ],
+        ),
+        returnValue: _i5.Future<bool>.value(false),
+        returnValueForMissingStub: _i5.Future<bool>.value(false),
+      ) as _i5.Future<bool>);
+  @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,
+      );
+}