From 8c95cd287be5e72b7a999bdc36fb7b4ed3c8d4b4 Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Thu, 30 Jun 2022 06:53:35 +0100 Subject: [PATCH] Add remaining tests for shopping cart feature # Conflicts: # ecommerce_app/test/src/goldens/products_list_1000x1000.png # ecommerce_app/test/src/goldens/products_list_300x600.png # ecommerce_app/test/src/goldens/products_list_600x800.png --- .../integration_test/auth_flow_test.dart | 22 ---- .../integration_test/purchase_flow_test.dart | 35 +++++ .../authentication/auth_flow_test.dart | 6 +- .../test/src/features/cart/cart_robot.dart | 93 ++++++++++++++ .../shopping_cart_screen_controller_test.dart | 98 ++++++++++++++ .../shopping_cart_screen_test.dart | 120 ++++++++++++++++++ .../src/features/products/products_robot.dart | 36 ++++++ .../test/src/features/purchase_flow_test.dart | 33 +++++ ecommerce_app/test/src/robot.dart | 54 ++++++-- 9 files changed, 460 insertions(+), 37 deletions(-) delete mode 100644 ecommerce_app/integration_test/auth_flow_test.dart create mode 100644 ecommerce_app/integration_test/purchase_flow_test.dart create mode 100644 ecommerce_app/test/src/features/cart/cart_robot.dart create mode 100644 ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart create mode 100644 ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_test.dart create mode 100644 ecommerce_app/test/src/features/products/products_robot.dart create mode 100644 ecommerce_app/test/src/features/purchase_flow_test.dart diff --git a/ecommerce_app/integration_test/auth_flow_test.dart b/ecommerce_app/integration_test/auth_flow_test.dart deleted file mode 100644 index 29e7eb7b..00000000 --- a/ecommerce_app/integration_test/auth_flow_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../test/src/robot.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Sign in and sign out flow', (tester) async { - final r = Robot(tester); - await r.pumpMyApp(); - r.expectFindAllProductCards(); - await r.openPopupMenu(); - await r.auth.openEmailPasswordSignInScreen(); - await r.auth.signInWithEmailAndPassword(); - r.expectFindAllProductCards(); - await r.openPopupMenu(); - await r.auth.openAccountScreen(); - await r.auth.tapLogoutButton(); - await r.auth.tapDialogLogoutButton(); - r.expectFindAllProductCards(); - }); -} diff --git a/ecommerce_app/integration_test/purchase_flow_test.dart b/ecommerce_app/integration_test/purchase_flow_test.dart new file mode 100644 index 00000000..852796b5 --- /dev/null +++ b/ecommerce_app/integration_test/purchase_flow_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../test/src/robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets('Full purchase flow', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + r.products.expectFindAllProductCards(); + // add to cart flows + await r.products.selectProduct(); + await r.products.setProductQuantity(3); + await r.cart.addToCart(); + await r.cart.openCart(); + r.cart.expectFindNCartItems(1); + await r.closePage(); + // sign in + await r.openPopupMenu(); + await r.auth.openEmailPasswordSignInScreen(); + await r.auth.signInWithEmailAndPassword(); + r.products.expectFindAllProductCards(); + // check cart again (to verify cart synchronization) + await r.cart.openCart(); + r.cart.expectFindNCartItems(1); + await r.closePage(); + // sign out + await r.openPopupMenu(); + await r.auth.openAccountScreen(); + await r.auth.tapLogoutButton(); + await r.auth.tapDialogLogoutButton(); + r.products.expectFindAllProductCards(); + }); +} diff --git a/ecommerce_app/test/src/features/authentication/auth_flow_test.dart b/ecommerce_app/test/src/features/authentication/auth_flow_test.dart index 2f59fcd0..e32afdf7 100644 --- a/ecommerce_app/test/src/features/authentication/auth_flow_test.dart +++ b/ecommerce_app/test/src/features/authentication/auth_flow_test.dart @@ -6,15 +6,15 @@ void main() { testWidgets('Sign in and sign out flow', (tester) async { final r = Robot(tester); await r.pumpMyApp(); - r.expectFindAllProductCards(); + r.products.expectFindAllProductCards(); await r.openPopupMenu(); await r.auth.openEmailPasswordSignInScreen(); await r.auth.signInWithEmailAndPassword(); - r.expectFindAllProductCards(); + r.products.expectFindAllProductCards(); await r.openPopupMenu(); await r.auth.openAccountScreen(); await r.auth.tapLogoutButton(); await r.auth.tapDialogLogoutButton(); - r.expectFindAllProductCards(); + r.products.expectFindAllProductCards(); }); } diff --git a/ecommerce_app/test/src/features/cart/cart_robot.dart b/ecommerce_app/test/src/features/cart/cart_robot.dart new file mode 100644 index 00000000..7bb9e8c5 --- /dev/null +++ b/ecommerce_app/test/src/features/cart/cart_robot.dart @@ -0,0 +1,93 @@ +import 'package:ecommerce_app/src/common_widgets/item_quantity_selector.dart'; +import 'package:ecommerce_app/src/features/cart/presentation/shopping_cart/shopping_cart_item.dart'; +import 'package:ecommerce_app/src/features/products/presentation/home_app_bar/shopping_cart_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class CartRobot { + CartRobot(this.tester); + final WidgetTester tester; + + Future addToCart() async { + final finder = find.text('Add to Cart'); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + } + + // shopping cart + Future openCart() async { + final finder = find.byKey(ShoppingCartIcon.shoppingCartIconKey); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + } + + void expectProductIsOutOfStock() async { + final finder = find.text('Out of Stock'); + expect(finder, findsOneWidget); + } + + Future incrementCartItemQuantity( + {required int quantity, required int atIndex}) async { + final finder = find.byKey(ItemQuantitySelector.incrementKey(atIndex)); + expect(finder, findsOneWidget); + for (var i = 0; i < quantity; i++) { + await tester.tap(finder); + await tester.pumpAndSettle(); + } + } + + Future decrementCartItemQuantity( + {required int quantity, required int atIndex}) async { + final finder = find.byKey(ItemQuantitySelector.decrementKey(atIndex)); + expect(finder, findsOneWidget); + for (var i = 0; i < quantity; i++) { + await tester.tap(finder); + await tester.pumpAndSettle(); + } + } + + Future deleteCartItem({required int atIndex}) async { + final finder = find.byKey(ShoppingCartItemContents.deleteKey(atIndex)); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + } + + void expectShoppingCartIsLoading() { + final finder = find.byType(CircularProgressIndicator); + expect(finder, findsOneWidget); + } + + void expectShoppingCartIsEmpty() { + final finder = find.text('Your shopping cart is empty'); + expect(finder, findsOneWidget); + } + + void expectFindZeroCartItems() { + final finder = find.byType(ShoppingCartItem); + expect(finder, findsNothing); + } + + void expectFindNCartItems(int count) { + final finder = find.byType(ShoppingCartItem); + expect(finder, findsNWidgets(count)); + } + + Text getItemQuantityWidget({int? atIndex}) { + final finder = find.byKey(ItemQuantitySelector.quantityKey(atIndex)); + expect(finder, findsOneWidget); + return finder.evaluate().single.widget as Text; + } + + void expectItemQuantity({required int quantity, int? atIndex}) { + final text = getItemQuantityWidget(atIndex: atIndex); + expect(text.data, '$quantity'); + } + + void expectShoppingCartTotalIs(String text) { + final finder = find.text(text); + expect(finder, findsOneWidget); + } +} diff --git a/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart b/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart new file mode 100644 index 00000000..484bb9cd --- /dev/null +++ b/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_controller_test.dart @@ -0,0 +1,98 @@ +import 'package:ecommerce_app/src/features/cart/domain/item.dart'; +import 'package:ecommerce_app/src/features/cart/presentation/shopping_cart/shopping_cart_screen_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() { + const productId = '1'; + group('updateItemQuantity', () { + test('update quantity, success', () async { + // setup + const item = Item(productId: productId, quantity: 3); + final cartService = MockCartService(); + when(() => cartService.setItem(item)).thenAnswer( + (_) => Future.value(null), + ); + final controller = ShoppingCartScreenController(cartService: cartService); + // run & verify + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + const AsyncData(null), + ]), + ); + await controller.updateItemQuantity(productId, 3); + }); + + test('update quantity, failure', () async { + // setup + const item = Item(productId: productId, quantity: 3); + final cartService = MockCartService(); + when(() => cartService.setItem(item)).thenThrow( + (_) => Exception('Connection failed'), + ); + final controller = ShoppingCartScreenController(cartService: cartService); + // run & verify + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + predicate>( + (value) { + expect(value.hasError, true); + return true; + }, + ), + ]), + ); + await controller.updateItemQuantity(productId, 3); + // verify + verify(() => cartService.setItem(item)).called(1); + }); + }); + group('removeItemById', () { + test('remove item, success', () async { + // setup + final cartService = MockCartService(); + when(() => cartService.removeItemById(productId)).thenAnswer( + (_) => Future.value(null), + ); + final controller = ShoppingCartScreenController(cartService: cartService); + // run & verify + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + const AsyncData(null), + ]), + ); + await controller.removeItemById(productId); + }); + test('remove item, failure', () async { + // setup + final cartService = MockCartService(); + when(() => cartService.removeItemById(productId)).thenThrow( + (_) => Exception('Connection failed'), + ); + final controller = ShoppingCartScreenController(cartService: cartService); + // run & verify + expectLater( + controller.stream, + emitsInOrder([ + const AsyncLoading(), + predicate>( + (value) { + expect(value.hasError, true); + return true; + }, + ), + ]), + ); + await controller.removeItemById(productId); + }); + }); +} diff --git a/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_test.dart b/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_test.dart new file mode 100644 index 00000000..fa6f28b9 --- /dev/null +++ b/ecommerce_app/test/src/features/cart/presentation/shopping_cart/shopping_cart_screen_test.dart @@ -0,0 +1,120 @@ +import 'package:flutter_test/flutter_test.dart'; +import '../../../../robot.dart'; + +void main() { + group('shopping cart', () { + testWidgets('Empty shopping cart', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + r.products.expectFindNProductCards(14); // check all products are found + await r.cart.openCart(); + r.cart.expectShoppingCartIsEmpty(); + }); + + testWidgets('Add product with quantity = 1', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.cart.addToCart(); + await r.cart.openCart(); + r.cart.expectItemQuantity(quantity: 1, atIndex: 0); + r.cart.expectShoppingCartTotalIs('Total: \$15.00'); + }); + + testWidgets('Add product with quantity = 5', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.products.setProductQuantity(5); + await r.cart.addToCart(); + await r.cart.openCart(); + r.cart.expectItemQuantity(quantity: 5, atIndex: 0); + r.cart.expectShoppingCartTotalIs('Total: \$75.00'); + }); + + testWidgets('Add product with quantity = 6', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.products.setProductQuantity(6); + await r.cart.addToCart(); + await r.cart.openCart(); + r.cart.expectItemQuantity(quantity: 5, atIndex: 0); + r.cart.expectShoppingCartTotalIs('Total: \$75.00'); + }); + + testWidgets('Add product with quantity = 2, then increment by 2', + (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.products.setProductQuantity(2); + await r.cart.addToCart(); + await r.cart.openCart(); + await r.cart.incrementCartItemQuantity(quantity: 2, atIndex: 0); + r.cart.expectItemQuantity(quantity: 4, atIndex: 0); + r.cart.expectShoppingCartTotalIs('Total: \$60.00'); + }); + + testWidgets('Add product with quantity = 5, then decrement by 2', + (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.products.setProductQuantity(5); + await r.cart.addToCart(); + await r.cart.openCart(); + await r.cart.decrementCartItemQuantity(quantity: 2, atIndex: 0); + r.cart.expectItemQuantity(quantity: 3, atIndex: 0); + r.cart.expectShoppingCartTotalIs('Total: \$45.00'); + }); + + testWidgets('Add two products', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + // add first product + await r.products.selectProduct(atIndex: 0); + await r.cart.addToCart(); + await r.goBack(); + // add second product + await r.products.selectProduct(atIndex: 1); + await r.cart.addToCart(); + await r.cart.openCart(); + r.cart.expectFindNCartItems(2); + r.cart.expectShoppingCartTotalIs('Total: \$28.00'); + }); + + testWidgets('Add product, then delete it', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.cart.addToCart(); + await r.cart.openCart(); + await r.cart.deleteCartItem(atIndex: 0); + r.cart.expectShoppingCartIsEmpty(); + }); + + testWidgets('Add product with quantity = 5, goes out of stock', + (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.products.setProductQuantity(5); + await r.cart.addToCart(); + r.cart.expectProductIsOutOfStock(); + }); + + testWidgets( + 'Add product with quantity = 5, remains out of stock when opened again', + (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + await r.products.selectProduct(); + await r.products.setProductQuantity(5); + await r.cart.addToCart(); + await r.goBack(); + await r.products.selectProduct(); + r.cart.expectProductIsOutOfStock(); + }); + }); +} diff --git a/ecommerce_app/test/src/features/products/products_robot.dart b/ecommerce_app/test/src/features/products/products_robot.dart new file mode 100644 index 00000000..d71b818d --- /dev/null +++ b/ecommerce_app/test/src/features/products/products_robot.dart @@ -0,0 +1,36 @@ +import 'package:ecommerce_app/src/constants/test_products.dart'; +import 'package:ecommerce_app/src/features/products/presentation/products_list/product_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class ProductsRobot { + ProductsRobot(this.tester); + final WidgetTester tester; + + // products list + Future selectProduct({int atIndex = 0}) async { + final finder = find.byKey(ProductCard.productCardKey); + await tester.tap(finder.at(atIndex)); + await tester.pumpAndSettle(); + } + + void expectFindAllProductCards() { + final finder = find.byType(ProductCard); + expect(finder, findsNWidgets(kTestProducts.length)); + } + + void expectFindNProductCards(int count) { + final finder = find.byType(ProductCard); + expect(finder, findsNWidgets(count)); + } + + // product page + Future setProductQuantity(int quantity) async { + final finder = find.byIcon(Icons.add); + expect(finder, findsOneWidget); + for (var i = 1; i < quantity; i++) { + await tester.tap(finder); + await tester.pumpAndSettle(); + } + } +} diff --git a/ecommerce_app/test/src/features/purchase_flow_test.dart b/ecommerce_app/test/src/features/purchase_flow_test.dart new file mode 100644 index 00000000..efa40386 --- /dev/null +++ b/ecommerce_app/test/src/features/purchase_flow_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../robot.dart'; + +void main() { + testWidgets('Full purchase flow', (tester) async { + final r = Robot(tester); + await r.pumpMyApp(); + r.products.expectFindAllProductCards(); + // add to cart flows + await r.products.selectProduct(); + await r.products.setProductQuantity(3); + await r.cart.addToCart(); + await r.cart.openCart(); + r.cart.expectFindNCartItems(1); + await r.closePage(); + // sign in + await r.openPopupMenu(); + await r.auth.openEmailPasswordSignInScreen(); + await r.auth.signInWithEmailAndPassword(); + r.products.expectFindAllProductCards(); + // check cart again (to verify cart synchronization) + await r.cart.openCart(); + r.cart.expectFindNCartItems(1); + await r.closePage(); + // sign out + await r.openPopupMenu(); + await r.auth.openAccountScreen(); + await r.auth.tapLogoutButton(); + await r.auth.tapDialogLogoutButton(); + r.products.expectFindAllProductCards(); + }); +} diff --git a/ecommerce_app/test/src/robot.dart b/ecommerce_app/test/src/robot.dart index 3c844ef6..64be4d88 100644 --- a/ecommerce_app/test/src/robot.dart +++ b/ecommerce_app/test/src/robot.dart @@ -1,44 +1,59 @@ import 'package:ecommerce_app/src/app.dart'; -import 'package:ecommerce_app/src/constants/test_products.dart'; import 'package:ecommerce_app/src/features/authentication/data/fake_auth_repository.dart'; +import 'package:ecommerce_app/src/features/cart/application/cart_sync_service.dart'; +import 'package:ecommerce_app/src/features/cart/data/local/fake_local_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/data/local/local_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/data/remote/fake_remote_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/data/remote/remote_cart_repository.dart'; import 'package:ecommerce_app/src/features/products/data/fake_products_repository.dart'; import 'package:ecommerce_app/src/features/products/presentation/home_app_bar/more_menu_button.dart'; -import 'package:ecommerce_app/src/features/products/presentation/products_list/product_card.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'features/authentication/auth_robot.dart'; +import 'features/cart/cart_robot.dart'; +import 'features/products/products_robot.dart'; import 'goldens/golden_robot.dart'; class Robot { Robot(this.tester) : auth = AuthRobot(tester), + products = ProductsRobot(tester), + cart = CartRobot(tester), golden = GoldenRobot(tester); final WidgetTester tester; final AuthRobot auth; + final ProductsRobot products; + final CartRobot cart; final GoldenRobot golden; Future pumpMyApp() async { // Override repositories final productsRepository = FakeProductsRepository(addDelay: false); final authRepository = FakeAuthRepository(addDelay: false); + final localCartRepository = FakeLocalCartRepository(addDelay: false); + final remoteCartRepository = FakeRemoteCartRepository(addDelay: false); + // * Create ProviderContainer with any required overrides + final container = ProviderContainer( + overrides: [ + productsRepositoryProvider.overrideWithValue(productsRepository), + authRepositoryProvider.overrideWithValue(authRepository), + localCartRepositoryProvider.overrideWithValue(localCartRepository), + remoteCartRepositoryProvider.overrideWithValue(remoteCartRepository), + ], + ); + // * Initialize CartSyncService to start the listener + container.read(cartSyncServiceProvider); + // * Entry point of the app await tester.pumpWidget( - ProviderScope( - overrides: [ - productsRepositoryProvider.overrideWithValue(productsRepository), - authRepositoryProvider.overrideWithValue(authRepository), - ], + UncontrolledProviderScope( + container: container, child: const MyApp(), ), ); await tester.pumpAndSettle(); } - void expectFindAllProductCards() { - final finder = find.byType(ProductCard); - expect(finder, findsNWidgets(kTestProducts.length)); - } - Future openPopupMenu() async { final finder = find.byType(MoreMenuButton); final matches = finder.evaluate(); @@ -50,4 +65,19 @@ class Robot { } // else no-op, as the items are already visible } + + // navigation + Future closePage() async { + final finder = find.byTooltip('Close'); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + } + + Future goBack() async { + final finder = find.byTooltip('Back'); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + } }