diff --git a/lib/controllers/feed_controller.dart b/lib/controllers/feed_controller.dart index ec221c3..e13bc66 100755 --- a/lib/controllers/feed_controller.dart +++ b/lib/controllers/feed_controller.dart @@ -35,6 +35,8 @@ class FeedState { // Notifier // --------------------------------------------------------------------------- class FeedNotifier extends Notifier { + FeedRepository get _repository => ref.read(feedRepositoryProvider); + @override FeedState build() { _fetchPosts(); @@ -43,7 +45,7 @@ class FeedNotifier extends Notifier { Future _fetchPosts() async { try { - final posts = await feedRepository.fetchPosts(); + final posts = await _repository.fetchPosts(); state = state.copyWith(posts: posts, isLoading: false, clearError: true); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); @@ -72,7 +74,7 @@ class FeedNotifier extends Notifier { try { final updatedLikes = - await feedRepository.toggleLike(postId, currentPetId); + await _repository.toggleLike(postId, currentPetId); state = state.copyWith( posts: state.posts.map((post) { if (post.id != postId) return post; @@ -81,7 +83,18 @@ class FeedNotifier extends Notifier { ); } catch (_) { // Revert optimistic update on failure - await _fetchPosts(); + state = state.copyWith( + posts: state.posts.map((post) { + if (post.id != postId) return post; + final revertedLikes = List.from(post.likedByPetIds); + if (revertedLikes.contains(currentPetId)) { + revertedLikes.remove(currentPetId); + } else { + revertedLikes.add(currentPetId); + } + return post.copyWith(likedByPetIds: revertedLikes); + }).toList(), + ); } } @@ -90,7 +103,7 @@ class FeedNotifier extends Notifier { // ------------------------------------------------------------------------- Future addPost(PetModel pet, String mediaUrl, String caption) async { try { - final newPost = await feedRepository.createPost( + final newPost = await _repository.createPost( petId: pet.id, mediaUrl: mediaUrl, caption: caption, @@ -107,7 +120,7 @@ class FeedNotifier extends Notifier { Future addComment( String postId, String petId, String petName, String text) async { try { - final newComment = await feedRepository.addComment( + final newComment = await _repository.addComment( postId: postId, petId: petId, text: text, @@ -128,6 +141,4 @@ class FeedNotifier extends Notifier { // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- -final feedProvider = NotifierProvider(() { - return FeedNotifier(); -}); +final feedProvider = NotifierProvider(FeedNotifier.new); diff --git a/lib/repositories/feed_repository.dart b/lib/repositories/feed_repository.dart index 701e144..21d2867 100644 --- a/lib/repositories/feed_repository.dart +++ b/lib/repositories/feed_repository.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/post_model.dart'; import '../utils/supabase_config.dart'; @@ -111,3 +112,5 @@ class FeedRepository { } final feedRepository = FeedRepository(); + +final feedRepositoryProvider = Provider((ref) => feedRepository); diff --git a/test/controllers/feed_controller_test.dart b/test/controllers/feed_controller_test.dart new file mode 100644 index 0000000..388242f --- /dev/null +++ b/test/controllers/feed_controller_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pet_dating_app/controllers/feed_controller.dart'; +import 'package:pet_dating_app/repositories/feed_repository.dart'; +import 'package:pet_dating_app/models/post_model.dart'; +import 'package:pet_dating_app/models/pet_model.dart'; +import 'dart:io'; + +class MockFeedRepository implements FeedRepository { + int fetchPostsCallCount = 0; + int toggleLikeCallCount = 0; + bool shouldFailToggleLike = false; + + @override + Future> fetchPosts() async { + fetchPostsCallCount++; + return [ + PostModel( + id: 'post1', + pet: PetModel(id: 'pet1', name: 'Pet 1', ownerId: 'owner1', breed: 'Breed 1', type: 'Dog', imageUrl: ''), + mediaUrl: '', + caption: 'Caption 1', + likedByPetIds: [], + createdAt: DateTime.now(), + ), + ]; + } + + @override + Future> toggleLike(String postId, String petId) async { + toggleLikeCallCount++; + if (shouldFailToggleLike) { + throw Exception('Failed to toggle like'); + } + return [petId]; + } + + @override + Future createPost({required String petId, required String mediaUrl, required String caption}) async { + throw UnimplementedError(); + } + + @override + Future uploadPostMedia(File file) async { + throw UnimplementedError(); + } + + @override + Future addComment({required String postId, required String petId, required String text}) async { + throw UnimplementedError(); + } +} + +void main() { + late MockFeedRepository mockRepository; + late ProviderContainer container; + + setUp(() { + mockRepository = MockFeedRepository(); + container = ProviderContainer( + overrides: [ + feedRepositoryProvider.overrideWithValue(mockRepository), + ], + ); + }); + + tearDown(() { + container.dispose(); + }); + + test('toggleLike does NOT call fetchPosts on failure and reverts state', () async { + // Wait for initial fetch + await container.read(feedProvider.notifier).refresh(); + expect(mockRepository.fetchPostsCallCount, 2); // 1 from build, 1 from refresh + + final initialPost = container.read(feedProvider).posts.first; + expect(initialPost.likedByPetIds, isEmpty); + + mockRepository.shouldFailToggleLike = true; + + await container.read(feedProvider.notifier).toggleLike('post1', 'pet_me'); + + // Optimized behavior: toggleLike should NOT call fetchPosts() on failure + expect(mockRepository.fetchPostsCallCount, 2); + + // State should be reverted + final finalPost = container.read(feedProvider).posts.first; + expect(finalPost.likedByPetIds, isEmpty); + }); +}