diff --git a/ecommerce_app/lib/src/features/cart/data/local/fake_local_cart_repository.dart b/ecommerce_app/lib/src/features/cart/data/local/fake_local_cart_repository.dart new file mode 100644 index 00000000..ae24cedb --- /dev/null +++ b/ecommerce_app/lib/src/features/cart/data/local/fake_local_cart_repository.dart @@ -0,0 +1,23 @@ +import 'package:ecommerce_app/src/features/cart/data/local/local_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/domain/cart.dart'; +import 'package:ecommerce_app/src/utils/delay.dart'; +import 'package:ecommerce_app/src/utils/in_memory_store.dart'; + +class FakeLocalCartRepository implements LocalCartRepository { + FakeLocalCartRepository({this.addDelay = true}); + final bool addDelay; + + final _cart = InMemoryStore(const Cart()); + + @override + Future fetchCart() => Future.value(_cart.value); + + @override + Stream watchCart() => _cart.stream; + + @override + Future setCart(Cart cart) async { + await delay(addDelay); + _cart.value = cart; + } +} diff --git a/ecommerce_app/lib/src/features/cart/data/local/local_cart_repository.dart b/ecommerce_app/lib/src/features/cart/data/local/local_cart_repository.dart new file mode 100644 index 00000000..bcc4e867 --- /dev/null +++ b/ecommerce_app/lib/src/features/cart/data/local/local_cart_repository.dart @@ -0,0 +1,16 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:ecommerce_app/src/features/cart/domain/cart.dart'; + +/// API for reading, watching and writing local cart data (guest user) +abstract class LocalCartRepository { + Future fetchCart(); + + Stream watchCart(); + + Future setCart(Cart cart); +} + +final localCartRepositoryProvider = Provider((ref) { + // * Override this in the main method + throw UnimplementedError(); +}); diff --git a/ecommerce_app/lib/src/features/cart/data/remote/fake_remote_cart_repository.dart b/ecommerce_app/lib/src/features/cart/data/remote/fake_remote_cart_repository.dart new file mode 100644 index 00000000..f54adac7 --- /dev/null +++ b/ecommerce_app/lib/src/features/cart/data/remote/fake_remote_cart_repository.dart @@ -0,0 +1,35 @@ +import 'package:ecommerce_app/src/features/cart/data/remote/remote_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/domain/cart.dart'; +import 'package:ecommerce_app/src/utils/delay.dart'; +import 'package:ecommerce_app/src/utils/in_memory_store.dart'; + +class FakeRemoteCartRepository implements RemoteCartRepository { + FakeRemoteCartRepository({this.addDelay = true}); + final bool addDelay; + + /// An InMemoryStore containing the shopping cart data for all users, where: + /// key: uid of the user + /// value: Cart of that user + final _carts = InMemoryStore>({}); + + @override + Future fetchCart(String uid) { + return Future.value(_carts.value[uid] ?? const Cart()); + } + + @override + Stream watchCart(String uid) { + return _carts.stream.map((cartData) => cartData[uid] ?? const Cart()); + } + + @override + Future setCart(String uid, Cart cart) async { + await delay(addDelay); + // First, get the current carts data for all users + final carts = _carts.value; + // Then, set the cart for the given uid + carts[uid] = cart; + // Finally, update the carts data (will emit a new value) + _carts.value = carts; + } +} diff --git a/ecommerce_app/lib/src/features/cart/data/remote/remote_cart_repository.dart b/ecommerce_app/lib/src/features/cart/data/remote/remote_cart_repository.dart new file mode 100644 index 00000000..119057c6 --- /dev/null +++ b/ecommerce_app/lib/src/features/cart/data/remote/remote_cart_repository.dart @@ -0,0 +1,17 @@ +import 'package:ecommerce_app/src/features/cart/data/remote/fake_remote_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/domain/cart.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// API for reading, watching and writing cart data for a specific user ID +abstract class RemoteCartRepository { + Future fetchCart(String uid); + + Stream watchCart(String uid); + + Future setCart(String uid, Cart cart); +} + +final remoteCartRepositoryProvider = Provider((ref) { + // TODO: replace with "real" remote cart repository + return FakeRemoteCartRepository(); +}); diff --git a/ecommerce_app/lib/src/features/cart/domain/mutable_cart.dart b/ecommerce_app/lib/src/features/cart/domain/mutable_cart.dart index c9a7c978..402c263c 100644 --- a/ecommerce_app/lib/src/features/cart/domain/mutable_cart.dart +++ b/ecommerce_app/lib/src/features/cart/domain/mutable_cart.dart @@ -4,45 +4,48 @@ import 'package:ecommerce_app/src/features/products/domain/product.dart'; /// Helper extension used to mutate the items in the shopping cart. extension MutableCart on Cart { + /// add an item to the cart by *overriding* the quantity if it already exists + Cart setItem(Item item) { + final copy = Map.from(items); + copy[item.productId] = item.quantity; + return Cart(copy); + } + + /// add an item to the cart by *updating* the quantity if it already exists Cart addItem(Item item) { final copy = Map.from(items); - if (copy.containsKey(item.productId)) { - copy[item.productId] = item.quantity + copy[item.productId]!; - } else { - copy[item.productId] = item.quantity; - } + // * update item quantity. Read this for more details: + // * https://codewithandrea.com/tips/dart-map-update-method/ + copy.update( + item.productId, + // if there is already a value, update it by adding the item quantity + (value) => item.quantity + value, + // otherwise, add the item with the given quantity + ifAbsent: () => item.quantity, + ); return Cart(copy); } + /// add a list of items to the cart by *updating* the quantities of items that + /// already exist Cart addItems(List itemsToAdd) { final copy = Map.from(items); for (var item in itemsToAdd) { - if (copy.containsKey(item.productId)) { - copy[item.productId] = item.quantity + copy[item.productId]!; - } else { - copy[item.productId] = item.quantity; - } + copy.update( + item.productId, + // if there is already a value, update it by adding the item quantity + (value) => item.quantity + value, + // otherwise, add the item with the given quantity + ifAbsent: () => item.quantity, + ); } return Cart(copy); } + /// if an item with the given productId is found, remove it Cart removeItemById(ProductID productId) { final copy = Map.from(items); copy.remove(productId); return Cart(copy); } - - Cart updateItemIfExists(Item item) { - if (items.containsKey(item.productId)) { - final copy = Map.from(items); - copy[item.productId] = item.quantity; - return Cart(copy); - } else { - return this; - } - } - - Cart clear() { - return const Cart(); - } } diff --git a/ecommerce_app/lib/src/features/cart/presentation/cart_total/cart_total_text.dart b/ecommerce_app/lib/src/features/cart/presentation/cart_total/cart_total_text.dart index f7e4a450..7700e334 100644 --- a/ecommerce_app/lib/src/features/cart/presentation/cart_total/cart_total_text.dart +++ b/ecommerce_app/lib/src/features/cart/presentation/cart_total/cart_total_text.dart @@ -1,16 +1,17 @@ import 'package:ecommerce_app/src/utils/currency_formatter.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Text widget for showing the total price of the cart -class CartTotalText extends StatelessWidget { +class CartTotalText extends ConsumerWidget { const CartTotalText({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // TODO: Read from data source const cartTotal = 104.0; - // TODO: Inject formatter - final totalFormatted = kCurrencyFormatter.format(cartTotal); + final totalFormatted = + ref.watch(currencyFormatterProvider).format(cartTotal); return Text( 'Total: $totalFormatted', style: Theme.of(context).textTheme.headlineSmall, diff --git a/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_item.dart b/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_item.dart index 0ee8582e..bf369ef4 100644 --- a/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_item.dart +++ b/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_item.dart @@ -4,6 +4,7 @@ import 'package:ecommerce_app/src/common_widgets/alert_dialogs.dart'; import 'package:ecommerce_app/src/common_widgets/async_value_widget.dart'; import 'package:ecommerce_app/src/features/products/data/fake_products_repository.dart'; import 'package:ecommerce_app/src/localization/string_hardcoded.dart'; +import 'package:ecommerce_app/src/utils/currency_formatter.dart'; import 'package:flutter/material.dart'; import 'package:ecommerce_app/src/common_widgets/custom_image.dart'; import 'package:ecommerce_app/src/common_widgets/item_quantity_selector.dart'; @@ -12,7 +13,6 @@ import 'package:ecommerce_app/src/constants/app_sizes.dart'; import 'package:ecommerce_app/src/features/cart/domain/item.dart'; import 'package:ecommerce_app/src/features/products/domain/product.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; /// Shows a shopping cart item (or loading/error UI if needed) class ShoppingCartItem extends ConsumerWidget { @@ -54,7 +54,7 @@ class ShoppingCartItem extends ConsumerWidget { } /// Shows a shopping cart item for a given product -class ShoppingCartItemContents extends StatelessWidget { +class ShoppingCartItemContents extends ConsumerWidget { const ShoppingCartItemContents({ super.key, required this.product, @@ -71,10 +71,9 @@ class ShoppingCartItemContents extends StatelessWidget { static Key deleteKey(int index) => Key('delete-$index'); @override - Widget build(BuildContext context) { - // TODO: error handling - // TODO: Inject formatter - final priceFormatted = NumberFormat.simpleCurrency().format(product.price); + Widget build(BuildContext context, WidgetRef ref) { + final priceFormatted = + ref.watch(currencyFormatterProvider).format(product.price); return ResponsiveTwoColumnLayout( startFlex: 1, endFlex: 2, @@ -90,28 +89,10 @@ class ShoppingCartItemContents extends StatelessWidget { gapH24, isEditable // show the quantity selector and a delete button - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ItemQuantitySelector( - quantity: item.quantity, - maxQuantity: min(product.availableQuantity, 10), - itemIndex: itemIndex, - // TODO: Implement onChanged - onChanged: (value) { - showNotImplementedAlertDialog(context: context); - }, - ), - IconButton( - key: deleteKey(itemIndex), - icon: Icon(Icons.delete, color: Colors.red[700]), - // TODO: Implement onPressed - onPressed: () { - showNotImplementedAlertDialog(context: context); - }, - ), - const Spacer(), - ], + ? EditOrRemoveItemWidget( + product: product, + item: item, + itemIndex: itemIndex, ) // else, show the quantity as a read-only label : Padding( @@ -125,3 +106,46 @@ class ShoppingCartItemContents extends StatelessWidget { ); } } + +// custom widget to show the quantity selector and a delete button +class EditOrRemoveItemWidget extends ConsumerWidget { + const EditOrRemoveItemWidget({ + super.key, + required this.product, + required this.item, + required this.itemIndex, + }); + final Product product; + final Item item; + final int itemIndex; + + // * Keys for testing using find.byKey() + static Key deleteKey(int index) => Key('delete-$index'); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ItemQuantitySelector( + quantity: item.quantity, + maxQuantity: min(product.availableQuantity, 10), + itemIndex: itemIndex, + // TODO: Implement onChanged + onChanged: (value) { + showNotImplementedAlertDialog(context: context); + }, + ), + IconButton( + key: deleteKey(itemIndex), + icon: Icon(Icons.delete, color: Colors.red[700]), + // TODO: Implement onPressed + onPressed: () { + showNotImplementedAlertDialog(context: context); + }, + ), + const Spacer(), + ], + ); + } +} diff --git a/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_screen.dart b/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_screen.dart index b6e1aa1b..168d7a60 100644 --- a/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_screen.dart +++ b/ecommerce_app/lib/src/features/cart/presentation/shopping_cart/shopping_cart_screen.dart @@ -14,6 +14,7 @@ class ShoppingCartScreen extends StatelessWidget { @override Widget build(BuildContext context) { + // TODO: error handling // TODO: Read from data source const cartItemsList = [ Item( diff --git a/ecommerce_app/lib/src/features/orders/presentation/orders_list/order_card.dart b/ecommerce_app/lib/src/features/orders/presentation/orders_list/order_card.dart index ba7d2c0d..3d624d77 100644 --- a/ecommerce_app/lib/src/features/orders/presentation/orders_list/order_card.dart +++ b/ecommerce_app/lib/src/features/orders/presentation/orders_list/order_card.dart @@ -7,6 +7,7 @@ import 'package:ecommerce_app/src/features/cart/domain/item.dart'; import 'package:ecommerce_app/src/features/orders/domain/order.dart'; import 'package:ecommerce_app/src/utils/currency_formatter.dart'; import 'package:ecommerce_app/src/utils/date_formatter.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Shows all the details for a given order class OrderCard extends StatelessWidget { @@ -35,16 +36,16 @@ class OrderCard extends StatelessWidget { /// Order header showing the following: /// - Total order amount /// - Order date -class OrderHeader extends StatelessWidget { +class OrderHeader extends ConsumerWidget { const OrderHeader({super.key, required this.order}); final Order order; @override - Widget build(BuildContext context) { - // TODO: Inject currency formatter - final totalFormatted = kCurrencyFormatter.format(order.total); - // TODO: Inject date formatter - final dateFormatted = kDateFormatter.format(order.orderDate); + Widget build(BuildContext context, WidgetRef ref) { + final totalFormatted = + ref.watch(currencyFormatterProvider).format(order.total); + final dateFormatted = + ref.watch(dateFormatterProvider).format(order.orderDate); return Container( color: Colors.grey[200], padding: const EdgeInsets.all(Sizes.p16), diff --git a/ecommerce_app/lib/src/features/products/data/fake_products_repository.dart b/ecommerce_app/lib/src/features/products/data/fake_products_repository.dart index 63a1a731..1e10eed3 100644 --- a/ecommerce_app/lib/src/features/products/data/fake_products_repository.dart +++ b/ecommerce_app/lib/src/features/products/data/fake_products_repository.dart @@ -40,7 +40,8 @@ class FakeProductsRepository { } final productsRepositoryProvider = Provider((ref) { - return FakeProductsRepository(); + // * Set addDelay to false for faster loading + return FakeProductsRepository(addDelay: false); }); final productsListStreamProvider = diff --git a/ecommerce_app/lib/src/features/products/presentation/home_app_bar/shopping_cart_icon.dart b/ecommerce_app/lib/src/features/products/presentation/home_app_bar/shopping_cart_icon.dart index d421bf54..c83f37d5 100644 --- a/ecommerce_app/lib/src/features/products/presentation/home_app_bar/shopping_cart_icon.dart +++ b/ecommerce_app/lib/src/features/products/presentation/home_app_bar/shopping_cart_icon.dart @@ -1,17 +1,18 @@ import 'package:ecommerce_app/src/routing/app_router.dart'; import 'package:flutter/material.dart'; import 'package:ecommerce_app/src/constants/app_sizes.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; /// Shopping cart icon with items count badge -class ShoppingCartIcon extends StatelessWidget { +class ShoppingCartIcon extends ConsumerWidget { const ShoppingCartIcon({super.key}); // * Keys for testing using find.byKey() static const shoppingCartIconKey = Key('shopping-cart'); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // TODO: Read from data source const cartItemsCount = 3; return Stack( diff --git a/ecommerce_app/lib/src/features/products/presentation/product_screen/leave_review_action.dart b/ecommerce_app/lib/src/features/products/presentation/product_screen/leave_review_action.dart index 656da6e5..1c4d0973 100644 --- a/ecommerce_app/lib/src/features/products/presentation/product_screen/leave_review_action.dart +++ b/ecommerce_app/lib/src/features/products/presentation/product_screen/leave_review_action.dart @@ -6,21 +6,22 @@ import 'package:flutter/material.dart'; import 'package:ecommerce_app/src/common_widgets/custom_text_button.dart'; import 'package:ecommerce_app/src/common_widgets/responsive_two_column_layout.dart'; import 'package:ecommerce_app/src/constants/app_sizes.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; /// Simple widget to show the product purchase date along with a button to /// leave a review. -class LeaveReviewAction extends StatelessWidget { +class LeaveReviewAction extends ConsumerWidget { const LeaveReviewAction({super.key, required this.productId}); final String productId; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // TODO: Read from data source final purchase = Purchase(orderId: 'abc', orderDate: DateTime.now()); if (purchase != null) { - // TODO: Inject date formatter - final dateFormatted = kDateFormatter.format(purchase.orderDate); + final dateFormatted = + ref.watch(dateFormatterProvider).format(purchase.orderDate); return Column( children: [ const Divider(), diff --git a/ecommerce_app/lib/src/features/products/presentation/product_screen/product_screen.dart b/ecommerce_app/lib/src/features/products/presentation/product_screen/product_screen.dart index ca4f8acd..5bea4236 100644 --- a/ecommerce_app/lib/src/features/products/presentation/product_screen/product_screen.dart +++ b/ecommerce_app/lib/src/features/products/presentation/product_screen/product_screen.dart @@ -53,13 +53,14 @@ class ProductScreen extends StatelessWidget { /// Shows all the product details along with actions to: /// - leave a review /// - add to cart -class ProductDetails extends StatelessWidget { +class ProductDetails extends ConsumerWidget { const ProductDetails({super.key, required this.product}); final Product product; @override - Widget build(BuildContext context) { - final priceFormatted = kCurrencyFormatter.format(product.price); + Widget build(BuildContext context, WidgetRef ref) { + final priceFormatted = + ref.watch(currencyFormatterProvider).format(product.price); return ResponsiveTwoColumnLayout( startContent: Card( child: Padding( diff --git a/ecommerce_app/lib/src/features/products/presentation/products_list/product_card.dart b/ecommerce_app/lib/src/features/products/presentation/products_list/product_card.dart index b50a468f..1a7ec6d9 100644 --- a/ecommerce_app/lib/src/features/products/presentation/products_list/product_card.dart +++ b/ecommerce_app/lib/src/features/products/presentation/products_list/product_card.dart @@ -5,9 +5,10 @@ import 'package:ecommerce_app/src/common_widgets/custom_image.dart'; import 'package:ecommerce_app/src/constants/app_sizes.dart'; import 'package:ecommerce_app/src/features/products/domain/product.dart'; import 'package:ecommerce_app/src/utils/currency_formatter.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Used to show a single product inside a card. -class ProductCard extends StatelessWidget { +class ProductCard extends ConsumerWidget { const ProductCard({super.key, required this.product, this.onPressed}); final Product product; final VoidCallback? onPressed; @@ -16,9 +17,9 @@ class ProductCard extends StatelessWidget { static const productCardKey = Key('product-card'); @override - Widget build(BuildContext context) { - // TODO: Inject formatter - final priceFormatted = kCurrencyFormatter.format(product.price); + Widget build(BuildContext context, WidgetRef ref) { + final priceFormatted = + ref.watch(currencyFormatterProvider).format(product.price); return Card( child: InkWell( key: productCardKey, diff --git a/ecommerce_app/lib/src/features/reviews/presentation/product_reviews/product_review_card.dart b/ecommerce_app/lib/src/features/reviews/presentation/product_reviews/product_review_card.dart index 5040db78..8c6e2507 100644 --- a/ecommerce_app/lib/src/features/reviews/presentation/product_reviews/product_review_card.dart +++ b/ecommerce_app/lib/src/features/reviews/presentation/product_reviews/product_review_card.dart @@ -4,15 +4,15 @@ import 'package:flutter/material.dart'; import 'package:ecommerce_app/src/constants/app_sizes.dart'; import 'package:ecommerce_app/src/features/reviews/domain/review.dart'; import 'package:ecommerce_app/src/utils/date_formatter.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Simple card widget to show a product review info (score, comment, date) -class ProductReviewCard extends StatelessWidget { +class ProductReviewCard extends ConsumerWidget { const ProductReviewCard(this.review, {super.key}); final Review review; @override - Widget build(BuildContext context) { - // TODO: Inject date formatter - final dateFormatted = kDateFormatter.format(review.date); + Widget build(BuildContext context, WidgetRef ref) { + final dateFormatted = ref.watch(dateFormatterProvider).format(review.date); return Card( child: Padding( padding: const EdgeInsets.all(Sizes.p16), diff --git a/ecommerce_app/lib/src/utils/currency_formatter.dart b/ecommerce_app/lib/src/utils/currency_formatter.dart index 7de90c68..6a5027f5 100644 --- a/ecommerce_app/lib/src/utils/currency_formatter.dart +++ b/ecommerce_app/lib/src/utils/currency_formatter.dart @@ -1,4 +1,9 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -/// Currency formatter to be used in the app. -final kCurrencyFormatter = NumberFormat.simpleCurrency(); +final currencyFormatterProvider = Provider((ref) { + /// Currency formatter to be used in the app. + /// * en_US is hardcoded to ensure all prices show with a dollar sign ($) + /// * This may or may not be what you want in your own apps. + return NumberFormat.simpleCurrency(locale: "en_US"); +}); diff --git a/ecommerce_app/lib/src/utils/date_formatter.dart b/ecommerce_app/lib/src/utils/date_formatter.dart index 0145f913..48d3e059 100644 --- a/ecommerce_app/lib/src/utils/date_formatter.dart +++ b/ecommerce_app/lib/src/utils/date_formatter.dart @@ -1,4 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -/// Date formatter to be used in the app. -final kDateFormatter = DateFormat.MMMEd(); +final dateFormatterProvider = Provider((ref) { + /// Date formatter to be used in the app. + return DateFormat.MMMEd(); +}); diff --git a/ecommerce_app/test/src/features/cart/domain/mutable_cart_test.dart b/ecommerce_app/test/src/features/cart/domain/mutable_cart_test.dart new file mode 100644 index 00000000..8014ea33 --- /dev/null +++ b/ecommerce_app/test/src/features/cart/domain/mutable_cart_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ecommerce_app/src/features/cart/domain/cart.dart'; +import 'package:ecommerce_app/src/features/cart/domain/item.dart'; +import 'package:ecommerce_app/src/features/cart/domain/mutable_cart.dart'; + +void main() { + group('set item', () { + test('empty cart - set item with quantity', () { + final cart = + const Cart().setItem(const Item(productId: '1', quantity: 2)); + expect(cart.items, {'1': 2}); + }); + test('cart with same item - override quantity', () { + final cart = const Cart() + .addItem(const Item(productId: '1', quantity: 1)) + .setItem(const Item(productId: '1', quantity: 2)); + expect(cart.items, {'1': 2}); + }); + test('cart with different item - set item with quantity', () { + final cart = const Cart() + .addItem(const Item(productId: '2', quantity: 1)) + .setItem(const Item(productId: '1', quantity: 2)); + expect(cart.items, { + '2': 1, + '1': 2, + }); + }); + }); + + group('add item', () { + test('empty cart - add item', () { + final cart = + const Cart().addItem(const Item(productId: '1', quantity: 1)); + expect(cart.items, {'1': 1}); + }); + test('empty cart - add two items', () { + final cart = const Cart() + .addItem(const Item(productId: '1', quantity: 1)) + .addItem(const Item(productId: '2', quantity: 1)); + expect(cart.items, { + '1': 1, + '2': 1, + }); + }); + test('empty cart - add same item twice', () { + final cart = const Cart() + .addItem(const Item(productId: '1', quantity: 1)) + .addItem(const Item(productId: '1', quantity: 1)); + expect(cart.items, {'1': 2}); + }); + }); + + group('add items', () { + test('empty cart - add two items', () { + final cart = const Cart().addItems([ + const Item(productId: '1', quantity: 1), + const Item(productId: '2', quantity: 1), + ]); + expect(cart.items, { + '1': 1, + '2': 1, + }); + }); + test('cart with one item - add two items of which one matching', () { + final cart = const Cart() + .addItem(const Item(productId: '1', quantity: 1)) + .addItems([ + const Item(productId: '1', quantity: 1), + const Item(productId: '2', quantity: 1), + ]); + expect(cart.items, { + '1': 2, + '2': 1, + }); + }); + test('cart with one item - add two new items', () { + final cart = const Cart() + .addItem(const Item(productId: '1', quantity: 1)) + .addItems([ + const Item(productId: '2', quantity: 1), + const Item(productId: '3', quantity: 1), + ]); + expect(cart.items, { + '1': 1, + '2': 1, + '3': 1, + }); + }); + }); + group('remove item', () { + test('empty cart - remove item', () { + final cart = const Cart().removeItemById('1'); + expect(cart.items, {}); + }); + test('empty cart - remove matching item', () { + final cart = const Cart() + .addItem(const Item(productId: '1', quantity: 1)) + .removeItemById('1'); + expect(cart.items, {}); + }); + test('empty cart - remove non-matching item', () { + final cart = const Cart() + .addItem(const Item(productId: '2', quantity: 1)) + .removeItemById('1'); + expect(cart.items, {'2': 1}); + }); + }); +} diff --git a/ecommerce_app/test/src/mocks.dart b/ecommerce_app/test/src/mocks.dart index 33a1c527..23595384 100644 --- a/ecommerce_app/test/src/mocks.dart +++ b/ecommerce_app/test/src/mocks.dart @@ -1,4 +1,10 @@ import 'package:ecommerce_app/src/features/authentication/data/fake_auth_repository.dart'; +import 'package:ecommerce_app/src/features/cart/data/local/local_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/data/remote/remote_cart_repository.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthRepository extends Mock implements FakeAuthRepository {} + +class MockRemoteCartRepository extends Mock implements RemoteCartRepository {} + +class MockLocalCartRepository extends Mock implements LocalCartRepository {}