Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions lib/controllers/feed_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class FeedState {
// Notifier
// ---------------------------------------------------------------------------
class FeedNotifier extends Notifier<FeedState> {
FeedRepository get _repository => ref.read(feedRepositoryProvider);

@override
FeedState build() {
_fetchPosts();
Expand All @@ -43,7 +45,7 @@ class FeedNotifier extends Notifier<FeedState> {

Future<void> _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());
Expand Down Expand Up @@ -72,7 +74,7 @@ class FeedNotifier extends Notifier<FeedState> {

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;
Expand All @@ -81,7 +83,18 @@ class FeedNotifier extends Notifier<FeedState> {
);
} 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<String>.from(post.likedByPetIds);
if (revertedLikes.contains(currentPetId)) {
revertedLikes.remove(currentPetId);
} else {
revertedLikes.add(currentPetId);
}
return post.copyWith(likedByPetIds: revertedLikes);
}).toList(),
);
}
}

Expand All @@ -90,7 +103,7 @@ class FeedNotifier extends Notifier<FeedState> {
// -------------------------------------------------------------------------
Future<void> 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,
Expand All @@ -107,7 +120,7 @@ class FeedNotifier extends Notifier<FeedState> {
Future<void> 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,
Expand All @@ -128,6 +141,4 @@ class FeedNotifier extends Notifier<FeedState> {
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
final feedProvider = NotifierProvider<FeedNotifier, FeedState>(() {
return FeedNotifier();
});
final feedProvider = NotifierProvider<FeedNotifier, FeedState>(FeedNotifier.new);
3 changes: 3 additions & 0 deletions lib/repositories/feed_repository.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/post_model.dart';
import '../utils/supabase_config.dart';

Expand Down Expand Up @@ -111,3 +112,5 @@ class FeedRepository {
}

final feedRepository = FeedRepository();

final feedRepositoryProvider = Provider((ref) => feedRepository);
90 changes: 90 additions & 0 deletions test/controllers/feed_controller_test.dart
Original file line number Diff line number Diff line change
@@ -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<List<PostModel>> 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<List<String>> toggleLike(String postId, String petId) async {
toggleLikeCallCount++;
if (shouldFailToggleLike) {
throw Exception('Failed to toggle like');
}
return [petId];
}

@override
Future<PostModel> createPost({required String petId, required String mediaUrl, required String caption}) async {
throw UnimplementedError();
}

@override
Future<String> uploadPostMedia(File file) async {
throw UnimplementedError();
}

@override
Future<CommentModel> 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);
});
}