diff --git a/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_controller.dart b/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_controller.dart index a569d74e..c62dd09b 100644 --- a/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_controller.dart +++ b/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_controller.dart @@ -14,25 +14,33 @@ class LeaveReviewController extends StateNotifier> { final DateTime Function() currentDateBuilder; Future submitReview({ + Review? previousReview, required ProductID productId, required double rating, required String comment, required void Function() onSuccess, }) async { - final review = Review( - rating: rating, - comment: comment, - date: currentDateBuilder(), - ); - state = const AsyncLoading(); - final newState = await AsyncValue.guard(() => - reviewsService.submitReview(productId: productId, review: review)); - if (mounted) { - // * only set the state if the controller hasn't been disposed - state = newState; - if (state.hasError == false) { - onSuccess(); + // * only submit if the rating is new or it has changed + if (previousReview == null || + rating != previousReview.rating || + comment != previousReview.comment) { + final review = Review( + rating: rating, + comment: comment, + date: currentDateBuilder(), + ); + state = const AsyncLoading(); + final newState = await AsyncValue.guard(() => + reviewsService.submitReview(productId: productId, review: review)); + if (mounted) { + // * only set the state if the controller hasn't been disposed + state = newState; + if (state.hasError == false) { + onSuccess(); + } } + } else { + onSuccess(); } } } diff --git a/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_screen.dart b/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_screen.dart index bb34fe19..f839216a 100644 --- a/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_screen.dart +++ b/ecommerce_app/lib/src/features/reviews/presentation/leave_review_screen/leave_review_screen.dart @@ -118,6 +118,7 @@ class _LeaveReviewFormState extends ConsumerState { ? null : () => ref.read(leaveReviewControllerProvider.notifier).submitReview( + previousReview: widget.review, productId: widget.productId, rating: _rating, comment: _controller.text, diff --git a/ecommerce_app/test/src/features/reviews/presentation/leave_review_screen/leave_review_controller_test.dart b/ecommerce_app/test/src/features/reviews/presentation/leave_review_screen/leave_review_controller_test.dart new file mode 100644 index 00000000..f19923c8 --- /dev/null +++ b/ecommerce_app/test/src/features/reviews/presentation/leave_review_screen/leave_review_controller_test.dart @@ -0,0 +1,127 @@ +import 'package:ecommerce_app/src/features/reviews/domain/review.dart'; +import 'package:ecommerce_app/src/features/reviews/presentation/leave_review_screen/leave_review_controller.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../../mocks.dart'; + +void main() { + final testDate = DateTime(2022, 7, 31); + const testRating = 5.0; + const testComment = 'love it!'; + final testReview = Review( + rating: testRating, + comment: testComment, + date: testDate, + ); + const testProductId = '1'; + + late MockReviewsService reviewsService; + setUp(() { + reviewsService = MockReviewsService(); + }); + + group('submitReview', () { + test('success', () async { + // setup + when(() => reviewsService.submitReview( + productId: testProductId, + review: testReview, + )).thenAnswer((_) => Future.value()); + final controller = LeaveReviewController( + reviewsService: reviewsService, + currentDateBuilder: () => testDate, + ); + // run & verify + var didSucceed = false; + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + const AsyncData(null), + ]), + ); + await controller.submitReview( + previousReview: null, + rating: testRating, + comment: testComment, + productId: testProductId, + onSuccess: () => didSucceed = true, + ); + verify(() => reviewsService.submitReview( + productId: testProductId, + review: testReview, + )).called(1); + expect(didSucceed, true); + }); + + test('failure', () async { + // setup + when(() => reviewsService.submitReview( + productId: testProductId, + review: testReview, + )).thenThrow(Exception('Connection failed')); + final controller = LeaveReviewController( + reviewsService: reviewsService, + currentDateBuilder: () => testDate, + ); + // run & verify + var didSucceed = false; + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + predicate>( + (value) { + expect(value.hasError, true); + return true; + }, + ) + ]), + ); + await controller.submitReview( + previousReview: null, + rating: testRating, + comment: testComment, + productId: testProductId, + onSuccess: () => didSucceed = true, + ); + verify(() => reviewsService.submitReview( + productId: testProductId, + review: testReview, + )).called(1); + expect(didSucceed, false); + }); + + test('same data as before', () async { + // setup + when(() => reviewsService.submitReview( + productId: testProductId, + review: testReview, + )).thenThrow(Exception('Connection failed')); + final controller = LeaveReviewController( + reviewsService: reviewsService, + currentDateBuilder: () => testDate, + ); + // run & verify + var didSucceed = false; + expectLater( + controller.stream, + emitsInOrder([]), // no state changes + ); + await controller.submitReview( + previousReview: testReview, + rating: testRating, + comment: testComment, + productId: testProductId, + onSuccess: () => didSucceed = true, + ); + verifyNever(() => reviewsService.submitReview( + productId: testProductId, + review: testReview, + )); + expect(didSucceed, true); + }); + }); +}