From 61b3e583b62b323d7864f4a6b6d34edd314c1ce9 Mon Sep 17 00:00:00 2001
From: abayram <38274063+abdullahbayram@users.noreply.github.com>
Date: Thu, 26 Dec 2024 03:01:50 +0100
Subject: [PATCH 1/7] feat: refactor ProductListScreen and improve test
utilities
- Implement `useBasket` hook to simplify basket state management.
- Add `useNavigationHandlers` hook for cleaner navigation logic.
- Introduce `ErrorState` and `LoadingState` components for better state handling.
- Update tests to match refactored structure.
- Add `renderWithProvidersAndNavigation` utility to support navigation and Redux in tests.
---
.eslintrc.json | 5 +-
.../utils/renderInProvidersAndNavigation.js | 27 +++++++
babel.config.js | 3 +
jest.config.js | 3 +
src/Navigator/Navigator.test.js | 4 +-
.../CheckoutCard/CheckoutCard.test.js | 2 +-
.../molecules/ProductCard/ProductCard.test.js | 2 +-
.../molecules/Toggle/Toggle.test.js | 2 +-
.../CheckoutList/CheckoutList.test.js | 2 +-
.../organisms/ProductList/ProductList.test.js | 2 +-
.../templates/BaseScreen/BaseScreen.test.js | 2 +-
.../RedirectWithAnimation.test.js | 6 +-
.../templates/RedirectWithAnimation/index.js | 2 +-
src/hooks/index.js | 3 +
.../{ => useBackHandler}/useBackHandler.js | 0
src/hooks/useBasket/useBasket.js | 12 +++
.../useNavigationHandlers.js | 24 ++++++
.../CheckoutScreen/CheckoutScreen.test.js | 24 ++++--
src/screens/CheckoutScreen/index.js | 15 ++--
.../PaymentForm/PaymentForm.style.js | 0
.../PaymentForm/PaymentForm.test.js | 4 +-
.../{components => }/PaymentForm/index.js | 0
.../PaymentScreen/PaymentScreen.test.js | 32 +++++---
src/screens/PaymentScreen/index.js | 24 ++----
.../ErrorState/ErrorState.style.js | 13 ++++
.../ProductListScreen/ErrorState/index.js | 25 ++++++
.../ProductListScreen/LoadingState/index.js | 17 ++++
.../ProductListScreen.style.js | 3 +-
.../ProductListScreen.test.js | 56 +++++++-------
src/screens/ProductListScreen/index.js | 77 +++++++------------
.../SuccessScreen/SuccessScreen.test.js | 2 +-
31 files changed, 256 insertions(+), 137 deletions(-)
create mode 100644 __tests__/utils/renderInProvidersAndNavigation.js
create mode 100644 src/hooks/index.js
rename src/hooks/{ => useBackHandler}/useBackHandler.js (100%)
create mode 100644 src/hooks/useBasket/useBasket.js
create mode 100644 src/hooks/useNavigationHandlers/useNavigationHandlers.js
rename src/screens/PaymentScreen/{components => }/PaymentForm/PaymentForm.style.js (100%)
rename src/screens/PaymentScreen/{components => }/PaymentForm/PaymentForm.test.js (90%)
rename src/screens/PaymentScreen/{components => }/PaymentForm/index.js (100%)
create mode 100644 src/screens/ProductListScreen/ErrorState/ErrorState.style.js
create mode 100644 src/screens/ProductListScreen/ErrorState/index.js
create mode 100644 src/screens/ProductListScreen/LoadingState/index.js
diff --git a/.eslintrc.json b/.eslintrc.json
index ec7959f..94ebea6 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -14,10 +14,13 @@
["@components", "./src/components"],
["@constants", "./src/constants"],
["@context", "./src/context"],
+ ["@hooks", "./src/hooks"],
["@screens", "./src/screens"],
["@redux", "./src/redux"],
["@utils", "./src/utils"],
- ["@validate", "./src/validate"]
+ ["@validate", "./src/validate"],
+ ["@testUtils", "./__tests__/utils"],
+ ["@mocks", "./__tests__/mocks"]
],
"extensions": [".js", ".jsx", ".json"]
}
diff --git a/__tests__/utils/renderInProvidersAndNavigation.js b/__tests__/utils/renderInProvidersAndNavigation.js
new file mode 100644
index 0000000..598b0f6
--- /dev/null
+++ b/__tests__/utils/renderInProvidersAndNavigation.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import { NavigationContainer } from '@react-navigation/native';
+import { Provider } from 'react-redux';
+import { setupApiStore } from './testUtil';
+import { apiSlice } from '../../src/redux/api/apiSlice';
+import basketSlice from '../../src/redux/slices/basketSlice';
+
+const renderWithProvidersAndNavigation = (
+ ui,
+ {
+ initialState = { basket: { items: [] } }, // Default Redux initial state
+ navigationOptions = {}, // NavigationContainer options
+ } = {},
+) => {
+ const { store } = setupApiStore(apiSlice, { basket: basketSlice }, initialState);
+
+ const Wrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ return render(ui, { wrapper: Wrapper });
+};
+
+export default renderWithProvidersAndNavigation;
diff --git a/babel.config.js b/babel.config.js
index 3d0c8f1..8a80bf3 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -18,10 +18,13 @@ module.exports = function (api) {
'@components': './src/components',
'@constants': './src/constants',
'@context': './src/context',
+ '@hooks': './src/hooks',
'@redux': './src/redux',
'@screens': './src/screens',
'@utils': './src/utils',
'@validate': './src/validate',
+ '@testUtils': './__tests__/utils',
+ '@mocks': './__tests__/mocks',
},
},
],
diff --git a/jest.config.js b/jest.config.js
index bfc6c36..00b1f30 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -31,10 +31,13 @@ module.exports = {
'^@components/(.*)$': '/src/components/$1',
'^@constants/(.*)$': '/src/constants/$1',
'^@context/(.*)$': '/src/context/$1',
+ '^@hooks/(.*)$': '/src/hooks/$1',
'^@redux/(.*)$': '/src/redux/$1',
'^@screens/(.*)$': '/src/screens/$1',
'^@utils/(.*)$': '/src/utils/$1',
'^@validate/(.*)$': '/src/validate/$1',
+ '^@testUtils/(.*)$': '/__tests__/utils/$1',
+ '^@mocks/(.*)$': '/__tests__/mocks/$1',
},
coverageReporters: ['json', 'json-summary', 'text', 'lcov'],
};
diff --git a/src/Navigator/Navigator.test.js b/src/Navigator/Navigator.test.js
index e98b69e..ab1aeeb 100644
--- a/src/Navigator/Navigator.test.js
+++ b/src/Navigator/Navigator.test.js
@@ -1,8 +1,8 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react-native';
+import renderInProvider from '@testUtils/renderInProvider';
+import { sampleBasket } from '@mocks/handlers';
import Navigator from './Navigator';
-import renderInProvider from '../../__tests__/utils/renderInProvider';
-import { sampleBasket } from '../../__tests__/mocks/handlers';
import { ThemeProvider } from '../context/ThemeContext'; // Import ThemeProvider
const initialState = { basket: { items: sampleBasket } };
diff --git a/src/components/molecules/CheckoutCard/CheckoutCard.test.js b/src/components/molecules/CheckoutCard/CheckoutCard.test.js
index 84122f8..628305f 100644
--- a/src/components/molecules/CheckoutCard/CheckoutCard.test.js
+++ b/src/components/molecules/CheckoutCard/CheckoutCard.test.js
@@ -1,7 +1,7 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
import { showToast } from '@utils';
-import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider';
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider';
import CheckoutCard from '.';
jest.mock('@utils', () => ({
diff --git a/src/components/molecules/ProductCard/ProductCard.test.js b/src/components/molecules/ProductCard/ProductCard.test.js
index 65f2dbd..5b37de8 100644
--- a/src/components/molecules/ProductCard/ProductCard.test.js
+++ b/src/components/molecules/ProductCard/ProductCard.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
-import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider';
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider';
import ProductCard from '.';
describe('', () => {
diff --git a/src/components/molecules/Toggle/Toggle.test.js b/src/components/molecules/Toggle/Toggle.test.js
index e66c8bc..f8c78df 100644
--- a/src/components/molecules/Toggle/Toggle.test.js
+++ b/src/components/molecules/Toggle/Toggle.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
-import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider'; // Import the utility function
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider'; // Import the utility function
import Toggle from '.';
import { darkTheme, lightTheme } from '../../../constants/theme';
diff --git a/src/components/organisms/CheckoutList/CheckoutList.test.js b/src/components/organisms/CheckoutList/CheckoutList.test.js
index 5006bc9..832d3ea 100644
--- a/src/components/organisms/CheckoutList/CheckoutList.test.js
+++ b/src/components/organisms/CheckoutList/CheckoutList.test.js
@@ -1,7 +1,7 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
import strings from '@constants/strings';
-import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider';
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider';
import CheckoutList from '.';
describe('', () => {
diff --git a/src/components/organisms/ProductList/ProductList.test.js b/src/components/organisms/ProductList/ProductList.test.js
index 1206c76..779fdf3 100644
--- a/src/components/organisms/ProductList/ProductList.test.js
+++ b/src/components/organisms/ProductList/ProductList.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
-import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider'; // Utility for ThemeProvider wrapping
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider'; // Utility for ThemeProvider wrapping
import ProductList from '.';
describe('', () => {
diff --git a/src/components/templates/BaseScreen/BaseScreen.test.js b/src/components/templates/BaseScreen/BaseScreen.test.js
index 55b9014..74037c7 100644
--- a/src/components/templates/BaseScreen/BaseScreen.test.js
+++ b/src/components/templates/BaseScreen/BaseScreen.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { screen } from '@testing-library/react-native';
-import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider';
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider';
import Screen from '.';
import Text from '../../atoms/Text';
diff --git a/src/components/templates/RedirectWithAnimation/RedirectWithAnimation.test.js b/src/components/templates/RedirectWithAnimation/RedirectWithAnimation.test.js
index 743b642..ecae1ec 100644
--- a/src/components/templates/RedirectWithAnimation/RedirectWithAnimation.test.js
+++ b/src/components/templates/RedirectWithAnimation/RedirectWithAnimation.test.js
@@ -1,11 +1,11 @@
import React from 'react';
import { screen, act, fireEvent } from '@testing-library/react-native';
import { strings } from '@constants';
-import { renderInThemeProvider } from '../../../../__tests__/utils/renderInThemeProvider';
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider';
import RedirectWithAnimation from '.';
-import useBackHandler from '../../../hooks/useBackHandler';
+import { useBackHandler } from '../../../hooks';
-jest.mock('../../../hooks/useBackHandler', () => jest.fn());
+jest.mock('../../../hooks/useBackHandler/useBackHandler', () => jest.fn());
jest.useFakeTimers();
diff --git a/src/components/templates/RedirectWithAnimation/index.js b/src/components/templates/RedirectWithAnimation/index.js
index 1230cf1..a0fb28e 100644
--- a/src/components/templates/RedirectWithAnimation/index.js
+++ b/src/components/templates/RedirectWithAnimation/index.js
@@ -7,7 +7,7 @@ import { spacing } from '@constants/theme';
import { strings } from '@constants';
import { Button, Text, LinearGradient } from '../../atoms';
import createStyles from './RedirectWithAnimation.style';
-import useBackHandler from '../../../hooks/useBackHandler';
+import useBackHandler from '../../../hooks/useBackHandler/useBackHandler';
const ANIMATION_DURATION = 1500;
const REDIRECT_DURATION = 5000;
diff --git a/src/hooks/index.js b/src/hooks/index.js
new file mode 100644
index 0000000..67ac358
--- /dev/null
+++ b/src/hooks/index.js
@@ -0,0 +1,3 @@
+export { default as useBasket } from './useBasket/useBasket';
+export { default as useBackHandler } from './useBackHandler/useBackHandler';
+export { default as useNavigationHandlers } from './useNavigationHandlers/useNavigationHandlers';
diff --git a/src/hooks/useBackHandler.js b/src/hooks/useBackHandler/useBackHandler.js
similarity index 100%
rename from src/hooks/useBackHandler.js
rename to src/hooks/useBackHandler/useBackHandler.js
diff --git a/src/hooks/useBasket/useBasket.js b/src/hooks/useBasket/useBasket.js
new file mode 100644
index 0000000..cca95ea
--- /dev/null
+++ b/src/hooks/useBasket/useBasket.js
@@ -0,0 +1,12 @@
+import { useSelector } from 'react-redux';
+import { selectBasketItems, selectTotalItemCount, selectTotalPrice } from '@redux/selectors/basketSelector';
+
+const useBasket = () => {
+ const basketItems = useSelector(selectBasketItems);
+ const totalItemCount = useSelector(selectTotalItemCount);
+ const totalPrice = useSelector(selectTotalPrice);
+
+ return { basketItems, totalItemCount, totalPrice };
+};
+
+export default useBasket;
diff --git a/src/hooks/useNavigationHandlers/useNavigationHandlers.js b/src/hooks/useNavigationHandlers/useNavigationHandlers.js
new file mode 100644
index 0000000..90034b9
--- /dev/null
+++ b/src/hooks/useNavigationHandlers/useNavigationHandlers.js
@@ -0,0 +1,24 @@
+import { useNavigation } from '@react-navigation/native';
+import { strings } from '@constants';
+
+export default () => {
+ const navigation = useNavigation();
+
+ const navigateToCheckout = () => {
+ navigation.navigate(strings.screens.checkout);
+ };
+
+ const navigateToPayment = () => {
+ navigation.navigate(strings.screens.payment);
+ };
+
+ const navigateToError = (errorMessage) => {
+ navigation.navigate(strings.screens.error, { errorMessage });
+ };
+
+ const navigateToSuccess = () => {
+ navigation.navigate(strings.screens.success);
+ };
+
+ return { navigateToCheckout, navigateToPayment, navigateToError, navigateToSuccess };
+};
diff --git a/src/screens/CheckoutScreen/CheckoutScreen.test.js b/src/screens/CheckoutScreen/CheckoutScreen.test.js
index f1dd9a0..ac365f7 100644
--- a/src/screens/CheckoutScreen/CheckoutScreen.test.js
+++ b/src/screens/CheckoutScreen/CheckoutScreen.test.js
@@ -1,25 +1,37 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react-native';
+import { useNavigation } from '@react-navigation/native';
+import renderWithProvidersAndNavigation from '@testUtils/renderInProvidersAndNavigation';
+import { sampleBasket } from '@mocks/handlers';
+import mockNavigation from '@mocks/navigation';
import CheckoutScreen from '.';
-import renderInProvider from '../../../__tests__/utils/renderInProvider';
-import { sampleBasket } from '../../../__tests__/mocks/handlers';
-import mockNavigation from '../../../__tests__/mocks/navigation';
const initialState = { basket: { items: sampleBasket } };
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: jest.fn(),
+}));
+
+const navigateMock = jest.fn();
+
+useNavigation.mockReturnValue({
+ navigate: navigateMock,
+});
+
describe('CheckoutScreen', () => {
it('should render CheckoutScreen correctly', () => {
- renderInProvider(, { initialState });
+ renderWithProvidersAndNavigation(, { initialState });
expect(screen.getByText('Mens Casual Premium Slim Fit T-Shirts')).toBeTruthy();
// expect(screen.getByText('Remove Item')).toBeTruthy();
// expect(screen.getByText('ORDER')).toBeTruthy(); + (14 items)
});
it('should match the snapshot', () => {
- renderInProvider();
+ renderWithProvidersAndNavigation();
expect(screen.toJSON()).toMatchSnapshot();
});
it('should update credit card input value', () => {
- renderInProvider(, { initialState });
+ renderWithProvidersAndNavigation(, { initialState });
// const creditCardInput = screen.getByLabelText('Credit Card');
// const creditCardInput = screen.getByPlaceholderText('Enter your credit card number');
diff --git a/src/screens/CheckoutScreen/index.js b/src/screens/CheckoutScreen/index.js
index ebf7782..db35b7b 100644
--- a/src/screens/CheckoutScreen/index.js
+++ b/src/screens/CheckoutScreen/index.js
@@ -1,7 +1,7 @@
import React from 'react';
import { View } from 'react-native';
import { useTheme } from 'react-native-paper';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
import { useForm, Controller } from 'react-hook-form';
import { Button, Text, TextInput } from '@components/atoms';
import { Input, ActivityOverlay } from '@components/molecules';
@@ -9,15 +9,16 @@ import { CheckoutList } from '@components/organisms';
import { BaseScreen } from '@components/templates';
import { removeItemFromBasket, updateItemQuantity, setDiscount } from '@redux/slices/basketSlice';
import { useValidatePromoCodeMutation } from '@redux/api/apiSlice';
-import { selectBasketItems, selectTotalItemCount, selectTotalPrice } from '@redux/selectors/basketSelector';
import { validateBasket } from '@validate';
import { showToast } from '@utils';
import { toastMessages, strings } from '@constants';
import styles from './CheckoutScreen.style';
+import { useBasket, useNavigationHandlers } from '../../hooks';
-const CheckoutScreen = ({ navigation }) => {
+const CheckoutScreen = () => {
const { colors } = useTheme();
const dispatch = useDispatch();
+ const { navigateToPayment } = useNavigationHandlers();
const {
control,
@@ -30,11 +31,7 @@ const CheckoutScreen = ({ navigation }) => {
},
});
- const { basketItems, totalItemCount, totalPrice } = useSelector((state) => ({
- basketItems: selectBasketItems(state),
- totalItemCount: selectTotalItemCount(state),
- totalPrice: selectTotalPrice(state),
- }));
+ const { basketItems, totalItemCount, totalPrice } = useBasket();
const isBasketEmpty = basketItems.length === 0;
@@ -49,7 +46,7 @@ const CheckoutScreen = ({ navigation }) => {
showToast(toastMessages.basket.empty);
return;
}
- navigation.navigate(strings.screens.payment);
+ navigateToPayment();
};
const onApplyPromoCode = async (data) => {
diff --git a/src/screens/PaymentScreen/components/PaymentForm/PaymentForm.style.js b/src/screens/PaymentScreen/PaymentForm/PaymentForm.style.js
similarity index 100%
rename from src/screens/PaymentScreen/components/PaymentForm/PaymentForm.style.js
rename to src/screens/PaymentScreen/PaymentForm/PaymentForm.style.js
diff --git a/src/screens/PaymentScreen/components/PaymentForm/PaymentForm.test.js b/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js
similarity index 90%
rename from src/screens/PaymentScreen/components/PaymentForm/PaymentForm.test.js
rename to src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js
index 87b9fde..d981cac 100644
--- a/src/screens/PaymentScreen/components/PaymentForm/PaymentForm.test.js
+++ b/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js
@@ -2,8 +2,8 @@ import React from 'react';
import { screen } from '@testing-library/react-native';
import { useForm, FormProvider } from 'react-hook-form';
import { strings } from '@constants';
-import PaymentForm from '.';
-import { renderInThemeProvider } from '../../../../../__tests__/utils/renderInThemeProvider';
+import { renderInThemeProvider } from '@testUtils/renderInThemeProvider';
+import PaymentForm from './index';
jest.mock('@utils', () => ({
paymentUtils: {
diff --git a/src/screens/PaymentScreen/components/PaymentForm/index.js b/src/screens/PaymentScreen/PaymentForm/index.js
similarity index 100%
rename from src/screens/PaymentScreen/components/PaymentForm/index.js
rename to src/screens/PaymentScreen/PaymentForm/index.js
diff --git a/src/screens/PaymentScreen/PaymentScreen.test.js b/src/screens/PaymentScreen/PaymentScreen.test.js
index 8ab3c36..741f03c 100644
--- a/src/screens/PaymentScreen/PaymentScreen.test.js
+++ b/src/screens/PaymentScreen/PaymentScreen.test.js
@@ -1,9 +1,21 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react-native';
import { strings } from '@constants';
+import { useNavigation } from '@react-navigation/native';
+import renderWithProvidersAndNavigation from '@testUtils/renderInProvidersAndNavigation';
+import { sampleBasket } from '@mocks/handlers';
import PaymentScreen from '.';
-import renderInProvider from '../../../__tests__/utils/renderInProvider';
-import { sampleBasket } from '../../../__tests__/mocks/handlers';
+
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: jest.fn(),
+}));
+
+const navigateMock = jest.fn();
+
+useNavigation.mockReturnValue({
+ navigate: navigateMock,
+});
const initialState = {
basket: {
@@ -19,7 +31,7 @@ describe('', () => {
});
it('displays the correct basket summary', () => {
- renderInProvider(, {
+ renderWithProvidersAndNavigation(, {
initialState,
});
const itemCountText = screen.getByText('Items in the basket: 14');
@@ -32,7 +44,7 @@ describe('', () => {
});
it('renders PaymentForm', () => {
- renderInProvider(, {
+ renderWithProvidersAndNavigation(, {
initialState,
});
@@ -44,7 +56,7 @@ describe('', () => {
it('disables the order button when basket is empty', () => {
const mockOnPress = jest.fn();
- renderInProvider(, {
+ renderWithProvidersAndNavigation(, {
initialState,
});
@@ -54,7 +66,7 @@ describe('', () => {
expect(mockOnPress).not.toHaveBeenCalled();
});
it('validates credit card input', async () => {
- renderInProvider(, {
+ renderWithProvidersAndNavigation(, {
initialState,
});
@@ -69,7 +81,7 @@ describe('', () => {
});
});
it('show feedback messages about required fields when pressed pay and order button without filling inputs', async () => {
- renderInProvider(, {
+ renderWithProvidersAndNavigation(, {
initialState,
});
@@ -89,7 +101,7 @@ describe('', () => {
expect(screen.getAllByText(strings.payment.invalidExpirationDate)[0]).toBeTruthy();
});
it('show feedback messages about required fields when pressed pay and order button without filling inputs', async () => {
- renderInProvider(, {
+ renderWithProvidersAndNavigation(, {
initialState,
});
@@ -101,6 +113,8 @@ describe('', () => {
});
expect(screen.getAllByText(strings.payment.cvvRequired)[0]).toBeTruthy();
expect(screen.getAllByText(strings.payment.expirationDateRequired)[0]).toBeTruthy();
- expect(screen.getAllByText(strings.payment.creditCardRequired)[0]).toBeTruthy();
+ await waitFor(() => {
+ expect(screen.getAllByText(strings.payment.creditCardRequired)[0]).toBeTruthy();
+ });
});
});
diff --git a/src/screens/PaymentScreen/index.js b/src/screens/PaymentScreen/index.js
index 62cc21e..62b30bd 100644
--- a/src/screens/PaymentScreen/index.js
+++ b/src/screens/PaymentScreen/index.js
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { View } from 'react-native';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
import { useForm } from 'react-hook-form';
import { useTheme } from 'react-native-paper';
import { Button } from '@components/atoms';
@@ -8,12 +8,12 @@ import { ActivityOverlay, BasketSummary } from '@components/molecules';
import { BaseScreen } from '@components/templates';
import { clearBasket, clearDiscount } from '@redux/slices/basketSlice';
import { usePlaceOrderMutation } from '@redux/api/apiSlice';
-import { selectBasketItems, selectTotalItemCount, selectTotalPrice } from '@redux/selectors/basketSelector';
import { showToast, paymentUtils } from '@utils';
import { toastMessages, strings } from '@constants';
import { checkCreditCardWithCardValidator, validateBasket } from '@validate';
+import { useBasket, useNavigationHandlers } from '@hooks';
import styles from './PaymentScreen.style';
-import PaymentForm from './components/PaymentForm';
+import PaymentForm from './PaymentForm';
const DEFAULT_FORM_VALUES = {
creditCardNumber: '',
@@ -22,9 +22,9 @@ const DEFAULT_FORM_VALUES = {
cvv: '',
};
-const PaymentScreen = ({ navigation }) => {
+const PaymentScreen = () => {
const dispatch = useDispatch();
- const { navigate } = navigation;
+ const { navigateToSuccess, navigateToError } = useNavigationHandlers();
const { colors } = useTheme();
const {
control,
@@ -36,23 +36,11 @@ const PaymentScreen = ({ navigation }) => {
defaultValues: DEFAULT_FORM_VALUES,
});
- const { basketItems, totalItemCount, totalPrice } = useSelector((state) => ({
- basketItems: selectBasketItems(state),
- totalItemCount: selectTotalItemCount(state),
- totalPrice: selectTotalPrice(state),
- }));
+ const { basketItems, totalItemCount, totalPrice } = useBasket();
const [placeOrder, { isLoading }] = usePlaceOrderMutation();
const isOrderDisabled = basketItems.length === 0 || isLoading;
- const navigateToError = (errorMessage) => {
- navigate(strings.screens.error, { errorMessage });
- };
-
- const navigateToSuccess = () => {
- navigate(strings.screens.success);
- };
-
const isCreditCardValid = useMemo(() => checkCreditCardWithCardValidator(watch('creditCardNumber')), [watch]);
const handleOrderError = (err) => {
diff --git a/src/screens/ProductListScreen/ErrorState/ErrorState.style.js b/src/screens/ProductListScreen/ErrorState/ErrorState.style.js
new file mode 100644
index 0000000..d5294ed
--- /dev/null
+++ b/src/screens/ProductListScreen/ErrorState/ErrorState.style.js
@@ -0,0 +1,13 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 16,
+ },
+ errorText: {
+ marginBottom: 8,
+ textAlign: 'center',
+ },
+});
diff --git a/src/screens/ProductListScreen/ErrorState/index.js b/src/screens/ProductListScreen/ErrorState/index.js
new file mode 100644
index 0000000..6099271
--- /dev/null
+++ b/src/screens/ProductListScreen/ErrorState/index.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { View } from 'react-native';
+import { Button, HelperText } from '@components/atoms';
+import PropTypes from 'prop-types';
+import styles from './ErrorState.style';
+
+const ErrorState = ({ errorMessage, onRetry }) => {
+ return (
+
+
+ {errorMessage}
+
+
+
+ );
+};
+
+ErrorState.propTypes = {
+ errorMessage: PropTypes.string.isRequired,
+ onRetry: PropTypes.func.isRequired,
+};
+
+export default ErrorState;
diff --git a/src/screens/ProductListScreen/LoadingState/index.js b/src/screens/ProductListScreen/LoadingState/index.js
new file mode 100644
index 0000000..1c6f21e
--- /dev/null
+++ b/src/screens/ProductListScreen/LoadingState/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { View } from 'react-native';
+import { ActivityIndicator } from '@components/atoms';
+import PropTypes from 'prop-types';
+import globalStyles from '../../../global.style';
+
+const LoadingState = ({ color }) => (
+
+
+
+);
+
+export default LoadingState;
+
+LoadingState.propTypes = {
+ color: PropTypes.string,
+};
diff --git a/src/screens/ProductListScreen/ProductListScreen.style.js b/src/screens/ProductListScreen/ProductListScreen.style.js
index 83178f8..2770a77 100644
--- a/src/screens/ProductListScreen/ProductListScreen.style.js
+++ b/src/screens/ProductListScreen/ProductListScreen.style.js
@@ -9,6 +9,5 @@ export default StyleSheet.create({
flexGrow: 1,
justifyContent: 'center',
},
- errorText: { alignSelf: 'center' },
- avtivityIndicator: { marginBottom: 72 },
+ activityIndicator: { marginBottom: 72 },
});
diff --git a/src/screens/ProductListScreen/ProductListScreen.test.js b/src/screens/ProductListScreen/ProductListScreen.test.js
index 59e5a79..f254212 100644
--- a/src/screens/ProductListScreen/ProductListScreen.test.js
+++ b/src/screens/ProductListScreen/ProductListScreen.test.js
@@ -1,46 +1,46 @@
import React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react-native';
+import { useNavigation } from '@react-navigation/native';
+import renderWithProvidersAndNavigation from '@testUtils/renderInProvidersAndNavigation';
+import { sampleBasket, sampleResponse } from '@mocks/handlers';
import ProductListScreen from '.';
-import renderInProvider from '../../../__tests__/utils/renderInProvider';
-import { sampleBasket, sampleResponse } from '../../../__tests__/mocks/handlers';
-
-const mockOnPress = jest.fn();
-const mockNavigation = {
- navigate: jest.fn(),
- goBack: jest.fn(),
- setParams: jest.fn(),
-};
-const initialState = { items: { items: sampleResponse }, basket: { items: sampleBasket } };
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: jest.fn(),
+}));
+
+const navigateMock = jest.fn();
+
+useNavigation.mockReturnValue({
+ navigate: navigateMock,
+});
+
+const initialState = {
+ items: { items: sampleResponse },
+ basket: { items: sampleBasket },
+};
describe('ProductListScreen', () => {
it('should render the first two items', async () => {
- renderInProvider();
+ renderWithProvidersAndNavigation(, { initialState });
- // expect(screen.getByText('Items in the basket: 0')).toBeTruthy();
-
- // Check that product titles are rendered
await waitFor(() => {
expect(screen.getByText('Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops')).toBeTruthy();
});
+
expect(screen.getByText('Mens Casual Premium Slim Fit T-Shirts ')).toBeTruthy();
});
- it('should call onPress when the checkout button is pressed', async () => {
- renderInProvider(, { initialState });
- let button;
- await waitFor(() => {
- button = screen.getByText('CHECKOUT');
- });
- fireEvent.press(button);
+ it('should call navigate when the checkout button is pressed', async () => {
+ renderWithProvidersAndNavigation(, { initialState });
- expect(mockNavigation.navigate).toHaveBeenCalledTimes(1);
+ const checkoutButton = await screen.findByText('CHECKOUT');
+ fireEvent.press(checkoutButton);
- expect(mockNavigation.navigate).toHaveBeenCalledWith('Checkout');
+ await waitFor(() => {
+ expect(navigateMock).toHaveBeenCalledTimes(1);
+ });
+ expect(navigateMock).toHaveBeenCalledWith('Checkout'); // Replace 'Checkout' with the actual route name
});
- /* // mock the state to simulate a change to CheckoutScreen
- it('should render the CheckoutScreen when the state changes', () => {
- // mock the `useState` to return the `CHECKOUT_SCREEN` state
- // simulate the flow to change screens
- }); */
});
diff --git a/src/screens/ProductListScreen/index.js b/src/screens/ProductListScreen/index.js
index 1d74a80..67a707a 100644
--- a/src/screens/ProductListScreen/index.js
+++ b/src/screens/ProductListScreen/index.js
@@ -1,38 +1,38 @@
import React, { useMemo } from 'react';
import { View } from 'react-native';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
import { useTheme } from 'react-native-paper';
-import { ActivityIndicator, Button, HelperText } from '@components/atoms';
+import { Button } from '@components/atoms';
import { BasketSummary } from '@components/molecules';
import { ProductList } from '@components/organisms';
import { BaseScreen } from '@components/templates';
import { addItemToBasket } from '@redux/slices/basketSlice';
import { useGetProductsQuery } from '@redux/api/apiSlice';
-import { selectTotalItemCount, selectBasketItems, selectTotalPrice } from '@redux/selectors/basketSelector';
import { validateBasket } from '@validate';
import showToast from '@utils/showToast';
import { toastMessages, strings } from '@constants';
+import { useBasket, useNavigationHandlers } from '@hooks';
import styles from './ProductListScreen.style';
-import globalStyles from '../../global.style';
+import LoadingState from './LoadingState';
+import ErrorState from './ErrorState';
-const ProductListScreen = ({ navigation }) => {
+const ProductListScreen = () => {
const { colors } = useTheme();
const dispatch = useDispatch();
- const { basketItems, totalItemCount, totalPrice } = useSelector((state) => ({
- basketItems: selectBasketItems(state),
- totalItemCount: selectTotalItemCount(state),
- totalPrice: selectTotalPrice(state),
- }));
+ const { basketItems, totalItemCount, totalPrice } = useBasket();
+ const { navigateToCheckout } = useNavigationHandlers();
const { data: products, error, isLoading, refetch } = useGetProductsQuery();
const basketItemsMap = useMemo(() => new Map(basketItems.map((item) => [item.id, item])), [basketItems]);
+ const isCheckoutButtonVisible = !error && !isLoading;
+ const isBasketSummaryVisible = !error && !isLoading;
const onCheckoutPress = () => {
if (!validateBasket(basketItems)) {
showToast(toastMessages.promo.error);
return;
}
- navigation.navigate(strings.screens.checkout);
+ navigateToCheckout();
};
const onAddToBasket = (item) => {
@@ -44,47 +44,26 @@ const ProductListScreen = ({ navigation }) => {
dispatch(addItemToBasket(item));
};
+ if (isLoading) return ;
+ if (error) return ;
+
return (
- {isLoading ? (
-
-
+ {isBasketSummaryVisible && }
+ {!error && (
+
+ )}
+ {isCheckoutButtonVisible && (
+
+
- ) : (
- <>
- {error ? (
-
-
- {strings.productList.errorLoading}
-
-
-
- ) : (
-
- )}
- {!error && (
-
- )}
- {!error && (
-
-
-
- )}
- >
)}
);
diff --git a/src/screens/SuccessScreen/SuccessScreen.test.js b/src/screens/SuccessScreen/SuccessScreen.test.js
index 31afb2d..f7a4e49 100644
--- a/src/screens/SuccessScreen/SuccessScreen.test.js
+++ b/src/screens/SuccessScreen/SuccessScreen.test.js
@@ -1,8 +1,8 @@
import React from 'react';
import { screen } from '@testing-library/react-native';
import strings from '@constants/strings';
+import renderInNavigation from '@testUtils/renderInNavigation';
import SuccessScreen from '.';
-import renderInNavigation from '../../../__tests__/utils/renderInNavigation';
describe('SuccessScreen Component', () => {
it('should render the success message', () => {
From 354bf63289d3e2596b6da88e83599c27bf71ed51 Mon Sep 17 00:00:00 2001
From: abayram <38274063+abdullahbayram@users.noreply.github.com>
Date: Thu, 26 Dec 2024 14:54:00 +0100
Subject: [PATCH 2/7] test: enhance ProductListScreen and related unit tests
- Add unit tests for `ErrorState` and `LoadingState` components
- Implement tests for basket functionality, including:
- Enforcing a limit of 5 items per product in the basket.
- Validating "Add to basket" button removal and limit message display.
- Add navigation tests to ensure proper redirection to the Checkout screen.
---
.../molecules}/ErrorState/ErrorState.style.js | 0
.../molecules/ErrorState/ErrorState.test.js | 26 +++
.../__snapshots__/ErrorState.test.js.snap | 190 ++++++++++++++++++
.../molecules}/ErrorState/index.js | 0
.../LoadingState/LoadingState.test.js | 10 +
.../__snapshots__/LoadingState.test.js.snap | 23 +++
.../molecules}/LoadingState/index.js | 2 +-
src/components/molecules/ProductCard/index.js | 3 +-
src/components/molecules/index.js | 2 +
src/constants/strings.js | 3 +-
src/constants/toastMessages.js | 2 +-
.../PaymentScreen/PaymentScreen.test.js | 8 +-
src/screens/PaymentScreen/index.js | 2 +-
.../ProductListScreen.test.js | 161 +++++++++++++--
src/screens/ProductListScreen/index.js | 19 +-
15 files changed, 414 insertions(+), 37 deletions(-)
rename src/{screens/ProductListScreen => components/molecules}/ErrorState/ErrorState.style.js (100%)
create mode 100644 src/components/molecules/ErrorState/ErrorState.test.js
create mode 100644 src/components/molecules/ErrorState/__snapshots__/ErrorState.test.js.snap
rename src/{screens/ProductListScreen => components/molecules}/ErrorState/index.js (100%)
create mode 100644 src/components/molecules/LoadingState/LoadingState.test.js
create mode 100644 src/components/molecules/LoadingState/__snapshots__/LoadingState.test.js.snap
rename src/{screens/ProductListScreen => components/molecules}/LoadingState/index.js (81%)
diff --git a/src/screens/ProductListScreen/ErrorState/ErrorState.style.js b/src/components/molecules/ErrorState/ErrorState.style.js
similarity index 100%
rename from src/screens/ProductListScreen/ErrorState/ErrorState.style.js
rename to src/components/molecules/ErrorState/ErrorState.style.js
diff --git a/src/components/molecules/ErrorState/ErrorState.test.js b/src/components/molecules/ErrorState/ErrorState.test.js
new file mode 100644
index 0000000..92e4f97
--- /dev/null
+++ b/src/components/molecules/ErrorState/ErrorState.test.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import ErrorState from '.';
+
+describe('ErrorState Component', () => {
+ const mockRetry = jest.fn();
+
+ it('renders the error message', () => {
+ render();
+ expect(screen.getByText('Test error message')).toBeTruthy();
+ });
+
+ it('calls the retry function when the Retry button is pressed', () => {
+ render();
+
+ const retryButton = screen.getByText('Retry');
+ fireEvent.press(retryButton);
+
+ expect(mockRetry).toHaveBeenCalledTimes(1);
+ });
+
+ it('matches the snapshot', () => {
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/molecules/ErrorState/__snapshots__/ErrorState.test.js.snap b/src/components/molecules/ErrorState/__snapshots__/ErrorState.test.js.snap
new file mode 100644
index 0000000..7142802
--- /dev/null
+++ b/src/components/molecules/ErrorState/__snapshots__/ErrorState.test.js.snap
@@ -0,0 +1,190 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ErrorState Component matches the snapshot 1`] = `
+
+
+ Snapshot error
+
+
+
+
+
+
+ Retry
+
+
+
+
+
+
+`;
diff --git a/src/screens/ProductListScreen/ErrorState/index.js b/src/components/molecules/ErrorState/index.js
similarity index 100%
rename from src/screens/ProductListScreen/ErrorState/index.js
rename to src/components/molecules/ErrorState/index.js
diff --git a/src/components/molecules/LoadingState/LoadingState.test.js b/src/components/molecules/LoadingState/LoadingState.test.js
new file mode 100644
index 0000000..20c4bbc
--- /dev/null
+++ b/src/components/molecules/LoadingState/LoadingState.test.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import LoadingState from '.';
+
+describe('LoadingState Component', () => {
+ it('matches the snapshot', () => {
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/src/components/molecules/LoadingState/__snapshots__/LoadingState.test.js.snap b/src/components/molecules/LoadingState/__snapshots__/LoadingState.test.js.snap
new file mode 100644
index 0000000..3bb12e4
--- /dev/null
+++ b/src/components/molecules/LoadingState/__snapshots__/LoadingState.test.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`LoadingState Component matches the snapshot 1`] = `
+
+
+
+`;
diff --git a/src/screens/ProductListScreen/LoadingState/index.js b/src/components/molecules/LoadingState/index.js
similarity index 81%
rename from src/screens/ProductListScreen/LoadingState/index.js
rename to src/components/molecules/LoadingState/index.js
index 1c6f21e..77904ff 100644
--- a/src/screens/ProductListScreen/LoadingState/index.js
+++ b/src/components/molecules/LoadingState/index.js
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import globalStyles from '../../../global.style';
const LoadingState = ({ color }) => (
-
+
);
diff --git a/src/components/molecules/ProductCard/index.js b/src/components/molecules/ProductCard/index.js
index 45caf54..4eb7aa0 100644
--- a/src/components/molecules/ProductCard/index.js
+++ b/src/components/molecules/ProductCard/index.js
@@ -5,6 +5,7 @@ import { useTheme } from 'react-native-paper';
import { spacing } from '@constants/theme';
import { Card, Button, Text, HelperText } from '../../atoms';
import createStyles from './ProductCard.style';
+import { strings } from '../../../constants';
const leftCardMargin = { marginRight: 5 };
const rightCardMargin = { marginLeft: 5 };
@@ -35,7 +36,7 @@ const ProductCard = ({ product, onButtonPress, isMaxQuantityPerProductReached, i
) : (
- You reached the max quantity per product!
+ {strings.productList.limitReached}
)}
diff --git a/src/components/molecules/index.js b/src/components/molecules/index.js
index f93054e..42ede1c 100644
--- a/src/components/molecules/index.js
+++ b/src/components/molecules/index.js
@@ -2,7 +2,9 @@ export { default as ActivityOverlay } from './ActivityOverlay';
export { default as BasketSummary } from './BasketSummary';
export { default as ControlledInput } from './ControlledInput';
export { default as CheckoutCard } from './CheckoutCard';
+export { default as ErrorState } from './ErrorState';
export { default as Header } from './Header';
export { default as Input } from './Input';
+export { default as LoadingState } from './LoadingState';
export { default as ProductCard } from './ProductCard';
export { default as Toggle } from './Toggle';
diff --git a/src/constants/strings.js b/src/constants/strings.js
index 09b72f9..0f4bd03 100644
--- a/src/constants/strings.js
+++ b/src/constants/strings.js
@@ -22,13 +22,14 @@ export default {
retry: 'Retry',
checkout: 'CHECKOUT',
order: 'ORDER',
- payAndorder: 'PAY AND ORDER',
+ payAndOrder: 'PAY AND ORDER',
gotoProducts: 'Go to Products',
},
productList: {
basketItemCount: 'Items in the basket: ',
loading: 'Loading products...',
errorLoading: 'Unable to load products. Please try again.',
+ limitReached: 'You reached the max quantity per product!',
},
checkout: {
total: 'Total:',
diff --git a/src/constants/toastMessages.js b/src/constants/toastMessages.js
index 69fc7ca..03c2f00 100644
--- a/src/constants/toastMessages.js
+++ b/src/constants/toastMessages.js
@@ -1,6 +1,6 @@
const messages = {
basket: {
- limitReached: { title: 'Limit Reached', msg: 'You can only add a maximum of 15 units per item.' },
+ limitReached: { title: 'Limit Reached', msg: 'You can only add a maximum of 5 units per item.' },
empty: { title: 'Empty Basket', msg: 'Please add items to your basket before placing an order.' },
invalidQuantity: { title: 'Invalid Quantity', msg: 'Quantity must be between 1 and 5.' },
},
diff --git a/src/screens/PaymentScreen/PaymentScreen.test.js b/src/screens/PaymentScreen/PaymentScreen.test.js
index 741f03c..a7b5fde 100644
--- a/src/screens/PaymentScreen/PaymentScreen.test.js
+++ b/src/screens/PaymentScreen/PaymentScreen.test.js
@@ -60,7 +60,7 @@ describe('', () => {
initialState,
});
- const orderButton = screen.getByText(strings.buttons.payAndorder);
+ const orderButton = screen.getByText(strings.buttons.payAndOrder);
fireEvent.press(orderButton);
expect(mockOnPress).not.toHaveBeenCalled();
@@ -73,7 +73,7 @@ describe('', () => {
const cardNumberInput = screen.getAllByText(strings.payment.creditCardNumber)[0];
fireEvent.changeText(cardNumberInput, '1234');
- const orderButton = screen.getByText(strings.buttons.payAndorder);
+ const orderButton = screen.getByText(strings.buttons.payAndOrder);
fireEvent.press(orderButton);
await waitFor(() => {
@@ -92,7 +92,7 @@ describe('', () => {
const expirationInput = screen.getAllByText(strings.payment.expirationDate)[0];
fireEvent.changeText(expirationInput, '07');
- const orderButton = screen.getByText(strings.buttons.payAndorder);
+ const orderButton = screen.getByText(strings.buttons.payAndOrder);
fireEvent.press(orderButton);
await waitFor(() => {
expect(screen.getAllByText(strings.payment.cardHolderMinLength)[0]).toBeTruthy();
@@ -105,7 +105,7 @@ describe('', () => {
initialState,
});
- const orderButton = screen.getByText(strings.buttons.payAndorder);
+ const orderButton = screen.getByText(strings.buttons.payAndOrder);
fireEvent.press(orderButton);
await waitFor(() => {
diff --git a/src/screens/PaymentScreen/index.js b/src/screens/PaymentScreen/index.js
index 62b30bd..91d8f93 100644
--- a/src/screens/PaymentScreen/index.js
+++ b/src/screens/PaymentScreen/index.js
@@ -79,7 +79,7 @@ const PaymentScreen = () => {
diff --git a/src/screens/ProductListScreen/ProductListScreen.test.js b/src/screens/ProductListScreen/ProductListScreen.test.js
index f254212..3eaf7af 100644
--- a/src/screens/ProductListScreen/ProductListScreen.test.js
+++ b/src/screens/ProductListScreen/ProductListScreen.test.js
@@ -1,8 +1,10 @@
import React from 'react';
-import { fireEvent, screen, waitFor } from '@testing-library/react-native';
+import { fireEvent, screen, waitFor, within } from '@testing-library/react-native';
import { useNavigation } from '@react-navigation/native';
import renderWithProvidersAndNavigation from '@testUtils/renderInProvidersAndNavigation';
import { sampleBasket, sampleResponse } from '@mocks/handlers';
+import { strings } from '@constants';
+import { useGetProductsQuery } from '@redux/api/apiSlice';
import ProductListScreen from '.';
jest.mock('@react-navigation/native', () => ({
@@ -10,30 +12,92 @@ jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));
+jest.mock('@redux/api/apiSlice', () => ({
+ ...jest.requireActual('@redux/api/apiSlice'),
+ useGetProductsQuery: jest.fn(), // Mock the hook
+}));
const navigateMock = jest.fn();
useNavigation.mockReturnValue({
navigate: navigateMock,
});
-const initialState = {
- items: { items: sampleResponse },
- basket: { items: sampleBasket },
-};
+const anItem = sampleResponse[0];
describe('ProductListScreen', () => {
- it('should render the first two items', async () => {
- renderWithProvidersAndNavigation(, { initialState });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+ it('should render the loading state when loading', () => {
+ useGetProductsQuery.mockReturnValue({
+ data: [],
+ error: false,
+ isLoading: true,
+ });
- await waitFor(() => {
- expect(screen.getByText('Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops')).toBeTruthy();
+ renderWithProvidersAndNavigation();
+
+ // Ensure the loading indicator is visible and error message is not
+ expect(screen.getByTestId('loading-state')).toBeTruthy();
+ expect(screen.queryByText(strings.productList.errorLoading)).toBeFalsy();
+ });
+
+ it('should render the error state when there is an error', async () => {
+ const refetchMock = jest.fn();
+
+ useGetProductsQuery.mockReturnValue({
+ data: [],
+ error: true,
+ isLoading: false,
+ refetch: refetchMock,
});
+ renderWithProvidersAndNavigation();
+
+ // Ensure the error message is visible and Loading state is not
+ expect(screen.getByText(strings.productList.errorLoading)).toBeTruthy();
+ expect(screen.queryByText('loading-state')).toBeFalsy();
+ });
+
+ it('should call refetch when Retry pressed in error state', async () => {
+ const refetchMock = jest.fn();
+
+ useGetProductsQuery.mockReturnValue({
+ data: [],
+ error: true,
+ isLoading: false,
+ refetch: refetchMock,
+ });
+
+ renderWithProvidersAndNavigation();
+
+ const retryButton = screen.getByText('Retry');
+ fireEvent.press(retryButton);
+
+ expect(refetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should render the first two items from the product list', async () => {
+ useGetProductsQuery.mockReturnValue({
+ data: sampleResponse,
+ error: false,
+ isLoading: false,
+ });
+ renderWithProvidersAndNavigation();
+
+ await waitFor(() => {
+ expect(screen.getByText(anItem.title)).toBeTruthy();
+ });
expect(screen.getByText('Mens Casual Premium Slim Fit T-Shirts ')).toBeTruthy();
});
- it('should call navigate when the checkout button is pressed', async () => {
- renderWithProvidersAndNavigation(, { initialState });
+ it('navigates to Checkout screen when checkout button is pressed', async () => {
+ useGetProductsQuery.mockReturnValue({
+ data: sampleResponse,
+ error: false,
+ isLoading: false,
+ });
+ renderWithProvidersAndNavigation(, { initialState: { basket: { items: sampleBasket } } });
const checkoutButton = await screen.findByText('CHECKOUT');
fireEvent.press(checkoutButton);
@@ -41,6 +105,79 @@ describe('ProductListScreen', () => {
await waitFor(() => {
expect(navigateMock).toHaveBeenCalledTimes(1);
});
- expect(navigateMock).toHaveBeenCalledWith('Checkout'); // Replace 'Checkout' with the actual route name
+ expect(navigateMock).toHaveBeenCalledWith('Checkout'); // Replace with the actual route name if necessary
+ });
+
+ it('should disable the checkout button when the basket is invalid', () => {
+ useGetProductsQuery.mockReturnValue({
+ data: sampleResponse,
+ error: false,
+ isLoading: false,
+ });
+ renderWithProvidersAndNavigation();
+
+ const checkoutButton = screen.getByText('CHECKOUT');
+ fireEvent.press(checkoutButton);
+ expect(navigateMock).toHaveBeenCalledTimes(0);
+ });
+
+ it('should add a product to the basket and update totalItemCount and total price', async () => {
+ useGetProductsQuery.mockReturnValue({
+ data: sampleResponse,
+ error: false,
+ isLoading: false,
+ });
+
+ renderWithProvidersAndNavigation();
+
+ // Initial totalItemCount
+ expect(screen.getByText('Items in the basket: 0')).toBeTruthy();
+
+ // Simulate adding the first product to the basket
+ const addToBasketButton = screen.getAllByText('Add to basket')[0];
+ fireEvent.press(addToBasketButton);
+
+ // Updated totalItemCount
+ await waitFor(() => {
+ expect(screen.getByText('Items in the basket: 1')).toBeTruthy();
+ });
+ expect(screen.getByText('Total: $109.95')).toBeTruthy();
+ });
+ it('should not allow adding the same product to the basket more than five times', async () => {
+ useGetProductsQuery.mockReturnValue({
+ data: sampleResponse,
+ error: false,
+ isLoading: false,
+ });
+
+ renderWithProvidersAndNavigation();
+
+ // Get the first product's Card and its "Add to basket" button
+ const firstProductCard = screen.getAllByTestId('product-card')[0];
+ const addToBasketButton = within(firstProductCard).getByText('Add to basket');
+
+ // Initial assertions
+ expect(screen.getByText('Items in the basket: 0')).toBeTruthy(); // Basket starts empty
+ expect(screen.queryByText(strings.productList.limitReached)).toBeFalsy();
+
+ // Add the same product to the basket five times
+ for (let i = 0; i < 5; i += 1) {
+ fireEvent.press(addToBasketButton);
+ }
+
+ // Check total item count and total price updated
+ await waitFor(() => {
+ expect(screen.getByText('Items in the basket: 5')).toBeTruthy();
+ });
+ expect(screen.getByText('Total: $549.75')).toBeTruthy(); // Ensure total price is updated correctly
+
+ // Ensure the "Add to basket" button is removed after reaching the limit
+ expect(within(firstProductCard).queryByText('Add to basket')).toBeFalsy();
+
+ // Ensure the limit message is displayed
+ expect(screen.getByText(strings.productList.limitReached)).toBeTruthy();
+
+ // TotalItemCount should still be 5, not updated further
+ expect(screen.getByText('Items in the basket: 5')).toBeTruthy();
});
});
diff --git a/src/screens/ProductListScreen/index.js b/src/screens/ProductListScreen/index.js
index 67a707a..31c02d0 100644
--- a/src/screens/ProductListScreen/index.js
+++ b/src/screens/ProductListScreen/index.js
@@ -1,20 +1,17 @@
-import React, { useMemo } from 'react';
+import React from 'react';
import { View } from 'react-native';
import { useDispatch } from 'react-redux';
import { useTheme } from 'react-native-paper';
import { Button } from '@components/atoms';
-import { BasketSummary } from '@components/molecules';
+import { BasketSummary, ErrorState, LoadingState } from '@components/molecules';
import { ProductList } from '@components/organisms';
import { BaseScreen } from '@components/templates';
import { addItemToBasket } from '@redux/slices/basketSlice';
import { useGetProductsQuery } from '@redux/api/apiSlice';
import { validateBasket } from '@validate';
-import showToast from '@utils/showToast';
-import { toastMessages, strings } from '@constants';
+import { strings } from '@constants';
import { useBasket, useNavigationHandlers } from '@hooks';
import styles from './ProductListScreen.style';
-import LoadingState from './LoadingState';
-import ErrorState from './ErrorState';
const ProductListScreen = () => {
const { colors } = useTheme();
@@ -23,24 +20,14 @@ const ProductListScreen = () => {
const { navigateToCheckout } = useNavigationHandlers();
const { data: products, error, isLoading, refetch } = useGetProductsQuery();
- const basketItemsMap = useMemo(() => new Map(basketItems.map((item) => [item.id, item])), [basketItems]);
const isCheckoutButtonVisible = !error && !isLoading;
const isBasketSummaryVisible = !error && !isLoading;
const onCheckoutPress = () => {
- if (!validateBasket(basketItems)) {
- showToast(toastMessages.promo.error);
- return;
- }
navigateToCheckout();
};
const onAddToBasket = (item) => {
- const existingItem = basketItemsMap.get(item.id);
- if (existingItem && existingItem.quantity >= 5) {
- showToast(toastMessages.basket.limitReached);
- return;
- }
dispatch(addItemToBasket(item));
};
From 7b6310e47e7dc68feaa1b44276030c47f9722295 Mon Sep 17 00:00:00 2001
From: abayram <38274063+abdullahbayram@users.noreply.github.com>
Date: Thu, 26 Dec 2024 15:54:42 +0100
Subject: [PATCH 3/7] test: move snapshots to parent folder and remove
unnecessary __snapshots__ folders
- Relocated snapshot files to the same level as their corresponding test files.
- Removed unnecessary `__snapshots__` folders
where only a single snapshot file existed.
- Added `snapshotResolver` to Jest configuration to ensure
new snapshots are generated at the desired location.
---
__tests__/{__snapshots__ => }/App.test.js.snap | 0
jest.config.js | 1 +
scripts/snapshotResolver.js | 8 ++++++++
.../{__snapshots__ => }/ActivityIndicator.test.js.snap | 0
.../atoms/Appbar/{__snapshots__ => }/Appbar.test.js.snap | 0
.../atoms/Button/{__snapshots__ => }/Button.test.js.snap | 0
.../atoms/Card/{__snapshots__ => }/Card.test.js.snap | 0
.../FlatList/{__snapshots__ => }/FlatList.test.js.snap | 0
.../{__snapshots__ => }/HelperText.test.js.snap | 0
.../atoms/Icon/{__snapshots__ => }/Icon.test.js.snap | 0
.../{__snapshots__ => }/LinearGradient.test.js.snap | 0
.../atoms/Switch/{__snapshots__ => }/Switch.test.js.snap | 0
.../atoms/Text/{__snapshots__ => }/Text.test.js.snap | 0
.../TextInput/{__snapshots__ => }/TextInput.test.js.snap | 0
.../{__snapshots__ => }/ActivityOverlay.test.js.snap | 0
.../{__snapshots__ => }/BasketSummary.test.js.snap | 0
.../{__snapshots__ => }/CheckoutCard.test.js.snap | 0
.../{__snapshots__ => }/ControlledInput.test.js.snap | 0
.../{__snapshots__ => }/ErrorState.test.js.snap | 0
.../Header/{__snapshots__ => }/Header.test.js.snap | 0
.../Input/{__snapshots__ => }/Input.test.js.snap | 0
.../{__snapshots__ => }/LoadingState.test.js.snap | 0
.../{__snapshots__ => }/ProductCard.test.js.snap | 0
.../Toggle/{__snapshots__ => }/Toggle.test.js.snap | 0
.../{__snapshots__ => }/BaseScreen.test.js.snap | 0
.../RedirectWithAnimation.test.js.snap | 0
.../{__snapshots__ => }/CheckoutScreen.test.js.snap | 0
.../{__snapshots__ => }/ErrorScreen.test.js.snap | 0
.../{__snapshots__ => }/SuccessScreen.test.js.snap | 0
29 files changed, 9 insertions(+)
rename __tests__/{__snapshots__ => }/App.test.js.snap (100%)
create mode 100644 scripts/snapshotResolver.js
rename src/components/atoms/ActivityIndicator/{__snapshots__ => }/ActivityIndicator.test.js.snap (100%)
rename src/components/atoms/Appbar/{__snapshots__ => }/Appbar.test.js.snap (100%)
rename src/components/atoms/Button/{__snapshots__ => }/Button.test.js.snap (100%)
rename src/components/atoms/Card/{__snapshots__ => }/Card.test.js.snap (100%)
rename src/components/atoms/FlatList/{__snapshots__ => }/FlatList.test.js.snap (100%)
rename src/components/atoms/HelperText/{__snapshots__ => }/HelperText.test.js.snap (100%)
rename src/components/atoms/Icon/{__snapshots__ => }/Icon.test.js.snap (100%)
rename src/components/atoms/LinearGradient/{__snapshots__ => }/LinearGradient.test.js.snap (100%)
rename src/components/atoms/Switch/{__snapshots__ => }/Switch.test.js.snap (100%)
rename src/components/atoms/Text/{__snapshots__ => }/Text.test.js.snap (100%)
rename src/components/atoms/TextInput/{__snapshots__ => }/TextInput.test.js.snap (100%)
rename src/components/molecules/ActivityOverlay/{__snapshots__ => }/ActivityOverlay.test.js.snap (100%)
rename src/components/molecules/BasketSummary/{__snapshots__ => }/BasketSummary.test.js.snap (100%)
rename src/components/molecules/CheckoutCard/{__snapshots__ => }/CheckoutCard.test.js.snap (100%)
rename src/components/molecules/ControlledInput/{__snapshots__ => }/ControlledInput.test.js.snap (100%)
rename src/components/molecules/ErrorState/{__snapshots__ => }/ErrorState.test.js.snap (100%)
rename src/components/molecules/Header/{__snapshots__ => }/Header.test.js.snap (100%)
rename src/components/molecules/Input/{__snapshots__ => }/Input.test.js.snap (100%)
rename src/components/molecules/LoadingState/{__snapshots__ => }/LoadingState.test.js.snap (100%)
rename src/components/molecules/ProductCard/{__snapshots__ => }/ProductCard.test.js.snap (100%)
rename src/components/molecules/Toggle/{__snapshots__ => }/Toggle.test.js.snap (100%)
rename src/components/templates/BaseScreen/{__snapshots__ => }/BaseScreen.test.js.snap (100%)
rename src/components/templates/RedirectWithAnimation/{__snapshots__ => }/RedirectWithAnimation.test.js.snap (100%)
rename src/screens/CheckoutScreen/{__snapshots__ => }/CheckoutScreen.test.js.snap (100%)
rename src/screens/ErrorScreen/{__snapshots__ => }/ErrorScreen.test.js.snap (100%)
rename src/screens/SuccessScreen/{__snapshots__ => }/SuccessScreen.test.js.snap (100%)
diff --git a/__tests__/__snapshots__/App.test.js.snap b/__tests__/App.test.js.snap
similarity index 100%
rename from __tests__/__snapshots__/App.test.js.snap
rename to __tests__/App.test.js.snap
diff --git a/jest.config.js b/jest.config.js
index 00b1f30..9316690 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -40,4 +40,5 @@ module.exports = {
'^@mocks/(.*)$': '/__tests__/mocks/$1',
},
coverageReporters: ['json', 'json-summary', 'text', 'lcov'],
+ snapshotResolver: './scripts/snapshotResolver.js',
};
diff --git a/scripts/snapshotResolver.js b/scripts/snapshotResolver.js
new file mode 100644
index 0000000..978ee51
--- /dev/null
+++ b/scripts/snapshotResolver.js
@@ -0,0 +1,8 @@
+// eslint-disable-next-line no-unused-vars
+const path = require('path');
+
+module.exports = {
+ resolveSnapshotPath: (testPath, snapshotExtension) => testPath + snapshotExtension, // Place snapshots next to the test file
+ resolveTestPath: (snapshotPath, snapshotExtension) => snapshotPath.slice(0, -snapshotExtension.length), // Match snapshots with test files
+ testPathForConsistencyCheck: 'some/example.test.js',
+};
diff --git a/src/components/atoms/ActivityIndicator/__snapshots__/ActivityIndicator.test.js.snap b/src/components/atoms/ActivityIndicator/ActivityIndicator.test.js.snap
similarity index 100%
rename from src/components/atoms/ActivityIndicator/__snapshots__/ActivityIndicator.test.js.snap
rename to src/components/atoms/ActivityIndicator/ActivityIndicator.test.js.snap
diff --git a/src/components/atoms/Appbar/__snapshots__/Appbar.test.js.snap b/src/components/atoms/Appbar/Appbar.test.js.snap
similarity index 100%
rename from src/components/atoms/Appbar/__snapshots__/Appbar.test.js.snap
rename to src/components/atoms/Appbar/Appbar.test.js.snap
diff --git a/src/components/atoms/Button/__snapshots__/Button.test.js.snap b/src/components/atoms/Button/Button.test.js.snap
similarity index 100%
rename from src/components/atoms/Button/__snapshots__/Button.test.js.snap
rename to src/components/atoms/Button/Button.test.js.snap
diff --git a/src/components/atoms/Card/__snapshots__/Card.test.js.snap b/src/components/atoms/Card/Card.test.js.snap
similarity index 100%
rename from src/components/atoms/Card/__snapshots__/Card.test.js.snap
rename to src/components/atoms/Card/Card.test.js.snap
diff --git a/src/components/atoms/FlatList/__snapshots__/FlatList.test.js.snap b/src/components/atoms/FlatList/FlatList.test.js.snap
similarity index 100%
rename from src/components/atoms/FlatList/__snapshots__/FlatList.test.js.snap
rename to src/components/atoms/FlatList/FlatList.test.js.snap
diff --git a/src/components/atoms/HelperText/__snapshots__/HelperText.test.js.snap b/src/components/atoms/HelperText/HelperText.test.js.snap
similarity index 100%
rename from src/components/atoms/HelperText/__snapshots__/HelperText.test.js.snap
rename to src/components/atoms/HelperText/HelperText.test.js.snap
diff --git a/src/components/atoms/Icon/__snapshots__/Icon.test.js.snap b/src/components/atoms/Icon/Icon.test.js.snap
similarity index 100%
rename from src/components/atoms/Icon/__snapshots__/Icon.test.js.snap
rename to src/components/atoms/Icon/Icon.test.js.snap
diff --git a/src/components/atoms/LinearGradient/__snapshots__/LinearGradient.test.js.snap b/src/components/atoms/LinearGradient/LinearGradient.test.js.snap
similarity index 100%
rename from src/components/atoms/LinearGradient/__snapshots__/LinearGradient.test.js.snap
rename to src/components/atoms/LinearGradient/LinearGradient.test.js.snap
diff --git a/src/components/atoms/Switch/__snapshots__/Switch.test.js.snap b/src/components/atoms/Switch/Switch.test.js.snap
similarity index 100%
rename from src/components/atoms/Switch/__snapshots__/Switch.test.js.snap
rename to src/components/atoms/Switch/Switch.test.js.snap
diff --git a/src/components/atoms/Text/__snapshots__/Text.test.js.snap b/src/components/atoms/Text/Text.test.js.snap
similarity index 100%
rename from src/components/atoms/Text/__snapshots__/Text.test.js.snap
rename to src/components/atoms/Text/Text.test.js.snap
diff --git a/src/components/atoms/TextInput/__snapshots__/TextInput.test.js.snap b/src/components/atoms/TextInput/TextInput.test.js.snap
similarity index 100%
rename from src/components/atoms/TextInput/__snapshots__/TextInput.test.js.snap
rename to src/components/atoms/TextInput/TextInput.test.js.snap
diff --git a/src/components/molecules/ActivityOverlay/__snapshots__/ActivityOverlay.test.js.snap b/src/components/molecules/ActivityOverlay/ActivityOverlay.test.js.snap
similarity index 100%
rename from src/components/molecules/ActivityOverlay/__snapshots__/ActivityOverlay.test.js.snap
rename to src/components/molecules/ActivityOverlay/ActivityOverlay.test.js.snap
diff --git a/src/components/molecules/BasketSummary/__snapshots__/BasketSummary.test.js.snap b/src/components/molecules/BasketSummary/BasketSummary.test.js.snap
similarity index 100%
rename from src/components/molecules/BasketSummary/__snapshots__/BasketSummary.test.js.snap
rename to src/components/molecules/BasketSummary/BasketSummary.test.js.snap
diff --git a/src/components/molecules/CheckoutCard/__snapshots__/CheckoutCard.test.js.snap b/src/components/molecules/CheckoutCard/CheckoutCard.test.js.snap
similarity index 100%
rename from src/components/molecules/CheckoutCard/__snapshots__/CheckoutCard.test.js.snap
rename to src/components/molecules/CheckoutCard/CheckoutCard.test.js.snap
diff --git a/src/components/molecules/ControlledInput/__snapshots__/ControlledInput.test.js.snap b/src/components/molecules/ControlledInput/ControlledInput.test.js.snap
similarity index 100%
rename from src/components/molecules/ControlledInput/__snapshots__/ControlledInput.test.js.snap
rename to src/components/molecules/ControlledInput/ControlledInput.test.js.snap
diff --git a/src/components/molecules/ErrorState/__snapshots__/ErrorState.test.js.snap b/src/components/molecules/ErrorState/ErrorState.test.js.snap
similarity index 100%
rename from src/components/molecules/ErrorState/__snapshots__/ErrorState.test.js.snap
rename to src/components/molecules/ErrorState/ErrorState.test.js.snap
diff --git a/src/components/molecules/Header/__snapshots__/Header.test.js.snap b/src/components/molecules/Header/Header.test.js.snap
similarity index 100%
rename from src/components/molecules/Header/__snapshots__/Header.test.js.snap
rename to src/components/molecules/Header/Header.test.js.snap
diff --git a/src/components/molecules/Input/__snapshots__/Input.test.js.snap b/src/components/molecules/Input/Input.test.js.snap
similarity index 100%
rename from src/components/molecules/Input/__snapshots__/Input.test.js.snap
rename to src/components/molecules/Input/Input.test.js.snap
diff --git a/src/components/molecules/LoadingState/__snapshots__/LoadingState.test.js.snap b/src/components/molecules/LoadingState/LoadingState.test.js.snap
similarity index 100%
rename from src/components/molecules/LoadingState/__snapshots__/LoadingState.test.js.snap
rename to src/components/molecules/LoadingState/LoadingState.test.js.snap
diff --git a/src/components/molecules/ProductCard/__snapshots__/ProductCard.test.js.snap b/src/components/molecules/ProductCard/ProductCard.test.js.snap
similarity index 100%
rename from src/components/molecules/ProductCard/__snapshots__/ProductCard.test.js.snap
rename to src/components/molecules/ProductCard/ProductCard.test.js.snap
diff --git a/src/components/molecules/Toggle/__snapshots__/Toggle.test.js.snap b/src/components/molecules/Toggle/Toggle.test.js.snap
similarity index 100%
rename from src/components/molecules/Toggle/__snapshots__/Toggle.test.js.snap
rename to src/components/molecules/Toggle/Toggle.test.js.snap
diff --git a/src/components/templates/BaseScreen/__snapshots__/BaseScreen.test.js.snap b/src/components/templates/BaseScreen/BaseScreen.test.js.snap
similarity index 100%
rename from src/components/templates/BaseScreen/__snapshots__/BaseScreen.test.js.snap
rename to src/components/templates/BaseScreen/BaseScreen.test.js.snap
diff --git a/src/components/templates/RedirectWithAnimation/__snapshots__/RedirectWithAnimation.test.js.snap b/src/components/templates/RedirectWithAnimation/RedirectWithAnimation.test.js.snap
similarity index 100%
rename from src/components/templates/RedirectWithAnimation/__snapshots__/RedirectWithAnimation.test.js.snap
rename to src/components/templates/RedirectWithAnimation/RedirectWithAnimation.test.js.snap
diff --git a/src/screens/CheckoutScreen/__snapshots__/CheckoutScreen.test.js.snap b/src/screens/CheckoutScreen/CheckoutScreen.test.js.snap
similarity index 100%
rename from src/screens/CheckoutScreen/__snapshots__/CheckoutScreen.test.js.snap
rename to src/screens/CheckoutScreen/CheckoutScreen.test.js.snap
diff --git a/src/screens/ErrorScreen/__snapshots__/ErrorScreen.test.js.snap b/src/screens/ErrorScreen/ErrorScreen.test.js.snap
similarity index 100%
rename from src/screens/ErrorScreen/__snapshots__/ErrorScreen.test.js.snap
rename to src/screens/ErrorScreen/ErrorScreen.test.js.snap
diff --git a/src/screens/SuccessScreen/__snapshots__/SuccessScreen.test.js.snap b/src/screens/SuccessScreen/SuccessScreen.test.js.snap
similarity index 100%
rename from src/screens/SuccessScreen/__snapshots__/SuccessScreen.test.js.snap
rename to src/screens/SuccessScreen/SuccessScreen.test.js.snap
From f59ba7d1a65fb633ad0dfb53ea9c4bc63ef252ea Mon Sep 17 00:00:00 2001
From: abayram <38274063+abdullahbayram@users.noreply.github.com>
Date: Thu, 26 Dec 2024 16:33:55 +0100
Subject: [PATCH 4/7] feat: enhance CheckoutScreen with modular components and
hooks
- Split `CheckoutScreen` into `TotalSummary` and `PromoCodeInput`
for better modularity.
- Introduced `useCheckout` custom hook to
encapsulate business logic, improving testability and readability.
---
.eslintrc.json | 1 +
scripts/snapshotResolver.js | 3 -
.../molecules/ControlledInput/index.js | 6 +-
src/hooks/index.js | 1 +
src/hooks/useCheckout/useCheckout.js | 46 +
.../CheckoutScreen/CheckoutScreen.style.js | 4 -
.../CheckoutScreen.test.js.snap | 1180 ++++++++---------
.../components/PromoCodeInput/index.js | 55 +
.../TotalSummary/TotalSummary.style.js | 12 +
.../components/TotalSummary/index.js | 25 +
.../CheckoutScreen/components/index.js | 2 +
src/screens/CheckoutScreen/index.js | 120 +-
.../PaymentScreen/PaymentForm/index.js | 4 +-
13 files changed, 754 insertions(+), 705 deletions(-)
create mode 100644 src/hooks/useCheckout/useCheckout.js
create mode 100644 src/screens/CheckoutScreen/components/PromoCodeInput/index.js
create mode 100644 src/screens/CheckoutScreen/components/TotalSummary/TotalSummary.style.js
create mode 100644 src/screens/CheckoutScreen/components/TotalSummary/index.js
create mode 100644 src/screens/CheckoutScreen/components/index.js
diff --git a/.eslintrc.json b/.eslintrc.json
index 94ebea6..39f90b3 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -73,6 +73,7 @@
],
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off",
+ "react/forbid-prop-types": "off",
// prevent eslint to complain about the "styles" variable being used before it was defined
"no-use-before-define": [
"error",
diff --git a/scripts/snapshotResolver.js b/scripts/snapshotResolver.js
index 978ee51..8b34f62 100644
--- a/scripts/snapshotResolver.js
+++ b/scripts/snapshotResolver.js
@@ -1,6 +1,3 @@
-// eslint-disable-next-line no-unused-vars
-const path = require('path');
-
module.exports = {
resolveSnapshotPath: (testPath, snapshotExtension) => testPath + snapshotExtension, // Place snapshots next to the test file
resolveTestPath: (snapshotPath, snapshotExtension) => snapshotPath.slice(0, -snapshotExtension.length), // Match snapshots with test files
diff --git a/src/components/molecules/ControlledInput/index.js b/src/components/molecules/ControlledInput/index.js
index 883416d..6021a45 100644
--- a/src/components/molecules/ControlledInput/index.js
+++ b/src/components/molecules/ControlledInput/index.js
@@ -36,16 +36,16 @@ const ControlledInput = ({
);
ControlledInput.propTypes = {
- control: PropTypes.oneOfType([PropTypes.object]).isRequired,
+ control: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
placeholder: PropTypes.string,
maxLength: PropTypes.number,
keyboardType: PropTypes.string,
- errorObject: PropTypes.oneOfType([PropTypes.object]),
+ errorObject: PropTypes.object,
right: PropTypes.node,
formatValue: PropTypes.func,
- rules: PropTypes.oneOfType([PropTypes.object]),
+ rules: PropTypes.object,
};
export default ControlledInput;
diff --git a/src/hooks/index.js b/src/hooks/index.js
index 67ac358..3ded3b6 100644
--- a/src/hooks/index.js
+++ b/src/hooks/index.js
@@ -1,3 +1,4 @@
export { default as useBasket } from './useBasket/useBasket';
export { default as useBackHandler } from './useBackHandler/useBackHandler';
export { default as useNavigationHandlers } from './useNavigationHandlers/useNavigationHandlers';
+export { default as useCheckout } from './useCheckout/useCheckout';
diff --git a/src/hooks/useCheckout/useCheckout.js b/src/hooks/useCheckout/useCheckout.js
new file mode 100644
index 0000000..8e45544
--- /dev/null
+++ b/src/hooks/useCheckout/useCheckout.js
@@ -0,0 +1,46 @@
+import { useDispatch } from 'react-redux';
+import { removeItemFromBasket, updateItemQuantity, setDiscount } from '@redux/slices/basketSlice';
+import { useValidatePromoCodeMutation } from '@redux/api/apiSlice';
+import useBasket from '@hooks/useBasket/useBasket';
+import { showToast } from '@utils';
+import { toastMessages } from '@constants';
+
+const useCheckout = () => {
+ const dispatch = useDispatch();
+ const { basketItems, totalItemCount, totalPrice } = useBasket();
+ const [validatePromoCode, { isLoading }] = useValidatePromoCodeMutation();
+
+ const isBasketEmpty = basketItems.length === 0;
+
+ const onRemoveItem = (item) => dispatch(removeItemFromBasket(item.id));
+
+ const onApplyPromoCode = async (promoCode) => {
+ try {
+ const { amount } = await validatePromoCode(promoCode).unwrap();
+ if (amount) {
+ dispatch(setDiscount(amount));
+ showToast(toastMessages.promo.success);
+ } else {
+ showToast(toastMessages.promo.invalid);
+ }
+ } catch (err) {
+ const errorMsg = err?.msg || toastMessages.promo.error;
+ showToast({ ...toastMessages.promo.error, msg: errorMsg });
+ }
+ };
+
+ const onQuantityChange = (item, newQuantity) => dispatch(updateItemQuantity({ id: item.id, quantity: newQuantity }));
+
+ return {
+ basketItems,
+ totalItemCount,
+ totalPrice,
+ isBasketEmpty,
+ isLoading,
+ onRemoveItem,
+ onApplyPromoCode,
+ onQuantityChange,
+ };
+};
+
+export default useCheckout;
diff --git a/src/screens/CheckoutScreen/CheckoutScreen.style.js b/src/screens/CheckoutScreen/CheckoutScreen.style.js
index cf32aa2..7b6b90c 100644
--- a/src/screens/CheckoutScreen/CheckoutScreen.style.js
+++ b/src/screens/CheckoutScreen/CheckoutScreen.style.js
@@ -15,8 +15,4 @@ export default StyleSheet.create({
orderButtonContainer: {
flex: 4,
},
- promoContainer: {
- flex: 6,
- marginBottom: spacing.sm,
- },
});
diff --git a/src/screens/CheckoutScreen/CheckoutScreen.test.js.snap b/src/screens/CheckoutScreen/CheckoutScreen.test.js.snap
index c0182da..927c985 100644
--- a/src/screens/CheckoutScreen/CheckoutScreen.test.js.snap
+++ b/src/screens/CheckoutScreen/CheckoutScreen.test.js.snap
@@ -16,7 +16,7 @@ exports[`CheckoutScreen should match the snapshot 1`] = `
- Total:
- $
+ Total: $
0.00
-
-
-
-
-
-
-
-
+
+
+
+
- ORDER
- (
- 0
- items)
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Order (
+ 0
+ items)
+
+
+
+
-
-
-
- Promo Code
-
-
+
- Promo Code
-
-
+ }
+ testID="text-input-flat-label-inactive"
+ >
+ Promo Code
+
-
-
+
+
+
-
+
-
-
-
-
+
+
-
+
+
+
-
-
-
-
-
-
+
+
+
+
- APPLY PROMO CODE
-
-
+ ],
+ ]
+ }
+ testID="button-text"
+ >
+ Apply
+
diff --git a/src/screens/CheckoutScreen/components/PromoCodeInput/index.js b/src/screens/CheckoutScreen/components/PromoCodeInput/index.js
new file mode 100644
index 0000000..1c0a299
--- /dev/null
+++ b/src/screens/CheckoutScreen/components/PromoCodeInput/index.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import { Controller } from 'react-hook-form';
+import { Button, TextInput } from '@components/atoms';
+import { Input } from '@components/molecules';
+import { useTheme } from 'react-native-paper';
+import PropTypes from 'prop-types';
+import { View } from 'react-native';
+
+const PromoCodeInput = ({ control, errors, handleSubmit, onApplyPromoCode, register, isApplyButtonDisabled }) => {
+ const { colors } = useTheme();
+
+ return (
+
+ (
+ }
+ errorObject={errors.promoCode}
+ />
+ )}
+ name="promoCode"
+ />
+
+
+ );
+};
+
+PromoCodeInput.propTypes = {
+ control: PropTypes.object.isRequired,
+ errors: PropTypes.object,
+ handleSubmit: PropTypes.func.isRequired,
+ onApplyPromoCode: PropTypes.func.isRequired,
+ register: PropTypes.func.isRequired,
+ isApplyButtonDisabled: PropTypes.bool.isRequired,
+};
+
+export default PromoCodeInput;
diff --git a/src/screens/CheckoutScreen/components/TotalSummary/TotalSummary.style.js b/src/screens/CheckoutScreen/components/TotalSummary/TotalSummary.style.js
new file mode 100644
index 0000000..3544930
--- /dev/null
+++ b/src/screens/CheckoutScreen/components/TotalSummary/TotalSummary.style.js
@@ -0,0 +1,12 @@
+import { StyleSheet } from 'react-native';
+import { spacing } from '@constants/theme';
+
+export default StyleSheet.create({
+ totalContainer: {
+ marginBottom: spacing.md,
+ paddingLeft: spacing.xxs,
+ },
+ totalPrice: {
+ marginBottom: spacing.special,
+ },
+});
diff --git a/src/screens/CheckoutScreen/components/TotalSummary/index.js b/src/screens/CheckoutScreen/components/TotalSummary/index.js
new file mode 100644
index 0000000..835f50c
--- /dev/null
+++ b/src/screens/CheckoutScreen/components/TotalSummary/index.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { View } from 'react-native';
+import { Button, Text } from '@components/atoms';
+import PropTypes from 'prop-types';
+import styles from './TotalSummary.style';
+
+const TotalSummary = ({ totalPrice, totalItemCount, isOrderButtonDisabled, onOrderPress }) => (
+
+
+ Total: ${Number.isNaN(totalPrice) ? '0.00' : totalPrice.toFixed(2)}
+
+
+
+);
+
+TotalSummary.propTypes = {
+ totalPrice: PropTypes.number.isRequired,
+ totalItemCount: PropTypes.number.isRequired,
+ isOrderButtonDisabled: PropTypes.bool.isRequired,
+ onOrderPress: PropTypes.func.isRequired,
+};
+
+export default TotalSummary;
diff --git a/src/screens/CheckoutScreen/components/index.js b/src/screens/CheckoutScreen/components/index.js
new file mode 100644
index 0000000..2d02e13
--- /dev/null
+++ b/src/screens/CheckoutScreen/components/index.js
@@ -0,0 +1,2 @@
+export { default as TotalSummary } from './TotalSummary';
+export { default as PromoCodeInput } from './PromoCodeInput';
diff --git a/src/screens/CheckoutScreen/index.js b/src/screens/CheckoutScreen/index.js
index db35b7b..e91d624 100644
--- a/src/screens/CheckoutScreen/index.js
+++ b/src/screens/CheckoutScreen/index.js
@@ -1,25 +1,24 @@
import React from 'react';
-import { View } from 'react-native';
-import { useTheme } from 'react-native-paper';
-import { useDispatch } from 'react-redux';
-import { useForm, Controller } from 'react-hook-form';
-import { Button, Text, TextInput } from '@components/atoms';
-import { Input, ActivityOverlay } from '@components/molecules';
+import { useForm } from 'react-hook-form';
+import { ActivityOverlay } from '@components/molecules';
import { CheckoutList } from '@components/organisms';
import { BaseScreen } from '@components/templates';
-import { removeItemFromBasket, updateItemQuantity, setDiscount } from '@redux/slices/basketSlice';
-import { useValidatePromoCodeMutation } from '@redux/api/apiSlice';
-import { validateBasket } from '@validate';
-import { showToast } from '@utils';
-import { toastMessages, strings } from '@constants';
-import styles from './CheckoutScreen.style';
-import { useBasket, useNavigationHandlers } from '../../hooks';
+import { useCheckout, useNavigationHandlers } from '@hooks';
+import { PromoCodeInput, TotalSummary } from './components';
const CheckoutScreen = () => {
- const { colors } = useTheme();
- const dispatch = useDispatch();
const { navigateToPayment } = useNavigationHandlers();
+ const {
+ basketItems,
+ totalItemCount,
+ totalPrice,
+ isBasketEmpty,
+ isLoading,
+ onRemoveItem,
+ onApplyPromoCode,
+ onQuantityChange,
+ } = useCheckout();
const {
control,
handleSubmit,
@@ -31,87 +30,28 @@ const CheckoutScreen = () => {
},
});
- const { basketItems, totalItemCount, totalPrice } = useBasket();
-
- const isBasketEmpty = basketItems.length === 0;
-
- const [validatePromoCode, { isLoading }] = useValidatePromoCodeMutation();
-
- const onRemoveItem = (item) => {
- dispatch(removeItemFromBasket(item.id));
- };
-
const onOrderPress = () => {
- if (!validateBasket(basketItems)) {
- showToast(toastMessages.basket.empty);
- return;
- }
navigateToPayment();
};
- const onApplyPromoCode = async (data) => {
- try {
- const { amount } = await validatePromoCode(data.promoCode).unwrap();
- if (amount) {
- dispatch(setDiscount(amount));
- showToast(toastMessages.promo.success);
- } else {
- showToast(toastMessages.promo.invalid);
- }
- } catch (err) {
- const errorMsg = err?.msg || toastMessages.promo.error;
- showToast({ ...toastMessages.promo.error, msg: errorMsg });
- }
- };
-
- const onQuantityChange = (item, newQuantity) => {
- dispatch(updateItemQuantity({ id: item.id, quantity: newQuantity }));
- };
-
return (
-
-
-
- {strings.checkout.total} ${Number.isNaN(totalPrice) ? '0.00' : totalPrice.toFixed(2)}
-
-
-
-
-
-
-
-
-
- (
- }
- errorObject={errors.promoCode}
- />
- )}
- name="promoCode"
- />
-
-
-
-
-
+
+
+ onApplyPromoCode(data.promoCode)}
+ register={register}
+ />
+
);
};
diff --git a/src/screens/PaymentScreen/PaymentForm/index.js b/src/screens/PaymentScreen/PaymentForm/index.js
index a1e68ed..74f7c9e 100644
--- a/src/screens/PaymentScreen/PaymentForm/index.js
+++ b/src/screens/PaymentScreen/PaymentForm/index.js
@@ -86,8 +86,8 @@ const PaymentForm = ({ control, errors, isCreditCardValid }) => {
};
PaymentForm.propTypes = {
- control: PropTypes.oneOfType([PropTypes.object]).isRequired,
- errors: PropTypes.oneOfType([PropTypes.object]).isRequired,
+ control: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
isCreditCardValid: PropTypes.bool.isRequired,
};
From c841a89bc61c510cd7ae8720709a4a1bedee9e3a Mon Sep 17 00:00:00 2001
From: abayram <38274063+abdullahbayram@users.noreply.github.com>
Date: Thu, 26 Dec 2024 20:29:25 +0100
Subject: [PATCH 5/7] test(checkout): enhance CheckoutScreen tests and add
integration tests
- Add unit tests for cart item quantity controls:
- Increment/decrement item quantity
- Verify total item count and price updates
- Remove item when quantity reaches 1 to 0
- Add integration tests with promocode functionality
- Add mock handler for promocode API
- Implement renderInFormProvider test utility
- Update test snapshots
---
__tests__/App.test.js.snap | 8 +
__tests__/mocks/handlers.js | 9 +
__tests__/utils/renderInFormProvider.js | 21 +
jest.config.js | 3 +-
src/components/atoms/Button/Button.test.js | 1 +
src/components/atoms/Text/Text.test.js.snap | 1 +
src/components/atoms/Text/index.js | 4 +-
.../CheckoutCard/CheckoutCard.test.js.snap | 8 +-
.../molecules/CheckoutCard/index.js | 12 +-
.../ControlledInput/ControlledInput.test.js | 44 +-
.../ProductCard/ProductCard.test.js.snap | 1 +
.../BaseScreen/BaseScreen.test.js.snap | 1 +
.../RedirectWithAnimation.test.js.snap | 2 +
src/constants/strings.js | 2 +-
.../CheckoutScreen.integration.test.js | 35 +
.../CheckoutScreen/CheckoutScreen.test.js | 144 ++-
.../CheckoutScreen.test.js.snap | 895 ------------------
.../components/PromoCodeInput/index.js | 5 +-
.../TotalSummary/TotalSummary.test.js | 36 +
.../components/TotalSummary/index.js | 8 +-
.../ErrorScreen/ErrorScreen.test.js.snap | 2 +
.../PaymentForm/PaymentForm.test.js | 15 +-
.../SuccessScreen/SuccessScreen.test.js.snap | 2 +
23 files changed, 294 insertions(+), 965 deletions(-)
create mode 100644 __tests__/utils/renderInFormProvider.js
create mode 100644 src/screens/CheckoutScreen/CheckoutScreen.integration.test.js
delete mode 100644 src/screens/CheckoutScreen/CheckoutScreen.test.js.snap
create mode 100644 src/screens/CheckoutScreen/components/TotalSummary/TotalSummary.test.js
diff --git a/__tests__/App.test.js.snap b/__tests__/App.test.js.snap
index b4bdd6e..fa619ff 100644
--- a/__tests__/App.test.js.snap
+++ b/__tests__/App.test.js.snap
@@ -596,6 +596,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
109.95
@@ -1041,6 +1042,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
22.30
@@ -1505,6 +1507,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
55.99
@@ -1950,6 +1953,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
15.99
@@ -2414,6 +2418,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
695.00
@@ -2859,6 +2864,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
168.00
@@ -3323,6 +3329,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
9.99
@@ -3768,6 +3775,7 @@ exports[` Text renders correctly on App 1`] = `
],
]
}
+ testID="text"
>
€
10.99
diff --git a/__tests__/mocks/handlers.js b/__tests__/mocks/handlers.js
index ce53401..e39352d 100644
--- a/__tests__/mocks/handlers.js
+++ b/__tests__/mocks/handlers.js
@@ -188,4 +188,13 @@ export const handlers = [
http.post('/checkout', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ success: true }));
}),
+
+ http.post('http://localhost:9001/promocode', async (req, res, ctx) => {
+ try {
+ return HttpResponse.json({ discountType: 'percent', amount: 10 });
+ } catch (error) {
+ console.error('Error:', error);
+ return res(ctx.status(400), ctx.json({ message: 'Invalid promo code' }));
+ }
+ }),
];
diff --git a/__tests__/utils/renderInFormProvider.js b/__tests__/utils/renderInFormProvider.js
new file mode 100644
index 0000000..d83d47f
--- /dev/null
+++ b/__tests__/utils/renderInFormProvider.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import { useForm, FormProvider } from 'react-hook-form';
+
+/**
+ * Utility to render a component wrapped in a FormProvider
+ * @param {React.ReactNode} ui - The React component to render.
+ * @param {object} formOptions - Options to initialize useForm (default: {}).
+ * @param {object} renderOptions - Additional options for the render method (default: {}).
+ * @returns {object} - The result of the render function.
+ */
+const renderInFormProvider = (ui, formOptions = {}, renderOptions = {}) => {
+ const Wrapper = ({ children }) => {
+ const methods = useForm(formOptions);
+ return {children};
+ };
+
+ return render(ui, { wrapper: Wrapper, ...renderOptions });
+};
+
+export default renderInFormProvider;
diff --git a/jest.config.js b/jest.config.js
index 9316690..048b7cd 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -23,7 +23,8 @@ module.exports = {
'src/components/organisms/index.js',
'src/components/templates/index.js',
'src/screens/index.js',
- 'src/screens/PaymentScreen/components/index.js',
+ 'src/hooks/index.js',
+ 'src/screens/CheckoutScreen/components/index.js',
'wdyr.js',
],
testPathIgnorePatterns: ['/__tests__/mocks/', '/__tests__/utils/'],
diff --git a/src/components/atoms/Button/Button.test.js b/src/components/atoms/Button/Button.test.js
index ecb90db..49ee5ca 100644
--- a/src/components/atoms/Button/Button.test.js
+++ b/src/components/atoms/Button/Button.test.js
@@ -58,6 +58,7 @@ describe('', () => {
});
const button = screen.getByText(BUTTON_TEXT_PRESS_ME);
fireEvent.press(button);
+ expect(button).toBeDisabled();
expect(mockOnPress).not.toHaveBeenCalled();
});
diff --git a/src/components/atoms/Text/Text.test.js.snap b/src/components/atoms/Text/Text.test.js.snap
index 6b65e54..a420793 100644
--- a/src/components/atoms/Text/Text.test.js.snap
+++ b/src/components/atoms/Text/Text.test.js.snap
@@ -28,6 +28,7 @@ exports[` matches the snapshot 1`] = `
],
]
}
+ testID="text"
>
Hello, World!
diff --git a/src/components/atoms/Text/index.js b/src/components/atoms/Text/index.js
index d504d28..f3a1395 100644
--- a/src/components/atoms/Text/index.js
+++ b/src/components/atoms/Text/index.js
@@ -2,10 +2,11 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { Text as PaperText, useTheme } from 'react-native-paper';
-const Text = ({ children, variant = 'bodyMedium', style, numberOfLines, ellipsizeMode }) => {
+const Text = ({ children, variant = 'bodyMedium', style, numberOfLines, ellipsizeMode, testID = 'text' }) => {
const theme = useTheme();
return (
+
Test Product
@@ -72,6 +75,7 @@ exports[`CheckoutCard renders correctly and matches the snapshot 1`] = `
],
]
}
+ testID="text"
>
This is a test product.
@@ -102,6 +106,7 @@ exports[`CheckoutCard renders correctly and matches the snapshot 1`] = `
],
]
}
+ testID="text"
>
$
20.00
@@ -286,6 +291,7 @@ exports[`CheckoutCard renders correctly and matches the snapshot 1`] = `
],
]
}
+ testID="product-quantity"
>
2
diff --git a/src/components/molecules/CheckoutCard/index.js b/src/components/molecules/CheckoutCard/index.js
index aa59654..d513e0c 100644
--- a/src/components/molecules/CheckoutCard/index.js
+++ b/src/components/molecules/CheckoutCard/index.js
@@ -33,7 +33,7 @@ const CheckoutCard = ({ product, maxQuantity = 5, onQuantityChange, onRemoveButt
};
return (
-
+
@@ -49,11 +49,17 @@ const CheckoutCard = ({ product, maxQuantity = 5, onQuantityChange, onRemoveButt
-
diff --git a/src/screens/ErrorScreen/ErrorScreen.test.js.snap b/src/screens/ErrorScreen/ErrorScreen.test.js.snap
index 763a611..71b8700 100644
--- a/src/screens/ErrorScreen/ErrorScreen.test.js.snap
+++ b/src/screens/ErrorScreen/ErrorScreen.test.js.snap
@@ -64,6 +64,7 @@ exports[`ErrorScreen Component should match the snapshot 1`] = `
],
]
}
+ testID="text"
>
Snapshot Error Test
@@ -97,6 +98,7 @@ exports[`ErrorScreen Component should match the snapshot 1`] = `
],
]
}
+ testID="text"
>
Redirecting to product list in
10
diff --git a/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js b/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js
index d981cac..e5d8abd 100644
--- a/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js
+++ b/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js
@@ -1,8 +1,7 @@
import React from 'react';
import { screen } from '@testing-library/react-native';
-import { useForm, FormProvider } from 'react-hook-form';
import { strings } from '@constants';
-import { renderInThemeProvider } from '@testUtils/renderInThemeProvider';
+import renderInFormProvider from '@testUtils/renderInFormProvider';
import PaymentForm from './index';
jest.mock('@utils', () => ({
@@ -14,12 +13,6 @@ jest.mock('@utils', () => ({
}));
describe('', () => {
- // eslint-disable-next-line react/prop-types
- const Wrapper = ({ children }) => {
- const methods = useForm();
- return {children};
- };
-
const mockErrors = {
cardholderName: false,
creditCardNumber: false,
@@ -28,11 +21,7 @@ describe('', () => {
};
it('renders all inputs with correct labels', () => {
- renderInThemeProvider(
-
-
- ,
- );
+ renderInFormProvider();
expect(screen.getAllByText(strings.payment.cardholderName).length).toBe(2);
expect(screen.getAllByText(strings.payment.creditCardNumber).length).toBe(2);
diff --git a/src/screens/SuccessScreen/SuccessScreen.test.js.snap b/src/screens/SuccessScreen/SuccessScreen.test.js.snap
index 4e5a7c1..c101f54 100644
--- a/src/screens/SuccessScreen/SuccessScreen.test.js.snap
+++ b/src/screens/SuccessScreen/SuccessScreen.test.js.snap
@@ -64,6 +64,7 @@ exports[`SuccessScreen Component should match the snapshot 1`] = `
],
]
}
+ testID="text"
>
Thank you!
@@ -100,6 +101,7 @@ Your order has been placed successfully.
],
]
}
+ testID="text"
>
Redirecting to product list in
5
From 5a08ed24346a2d63b719de1fd54cc76e6ad37f58 Mon Sep 17 00:00:00 2001
From: abayram <38274063+abdullahbayram@users.noreply.github.com>
Date: Fri, 27 Dec 2024 00:40:47 +0100
Subject: [PATCH 6/7] test: add primitive integration test for PaymentScreen
---
__tests__/App.test.js | 3 +-
__tests__/App.test.js.snap | 4291 -----------------
__tests__/mocks/handlers.js | 17 +-
.../CheckoutScreen.integration.test.js | 1 -
.../PaymentScreen.integration.test.js | 49 +
.../PaymentScreen/PaymentScreen.test.js | 2 +-
6 files changed, 66 insertions(+), 4297 deletions(-)
delete mode 100644 __tests__/App.test.js.snap
create mode 100644 src/screens/PaymentScreen/PaymentScreen.integration.test.js
diff --git a/__tests__/App.test.js b/__tests__/App.test.js
index 9bdf67c..a507c24 100644
--- a/__tests__/App.test.js
+++ b/__tests__/App.test.js
@@ -3,11 +3,10 @@ import App from '../App';
import renderInProvider from './utils/renderInProvider';
describe('', () => {
- test('Text renders correctly on App', async () => {
+ test('App renders correctly and displays the initial text', async () => {
const { toJSON } = renderInProvider();
await waitFor(() => {
screen.getByText('Items in the basket: 0');
});
- expect(toJSON()).toMatchSnapshot();
});
});
diff --git a/__tests__/App.test.js.snap b/__tests__/App.test.js.snap
deleted file mode 100644
index fa619ff..0000000
--- a/__tests__/App.test.js.snap
+++ /dev/null
@@ -1,4291 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` Text renders correctly on App 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
- Items in the basket:
-
- 0
-
-
-
- Total:
- $
- 0.00
-
-
-
-
- }
- removeClippedSubviews={true}
- renderItem={[Function]}
- scrollEventThrottle={0.0001}
- stickyHeaderIndices={[]}
- style={
- {
- "marginTop": 7,
- }
- }
- viewabilityConfigCallbackPairs={[]}
- windowSize={10}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops
-
-
- ★★★★☆
-
-
-
-
-
-
- €
- 109.95
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mens Casual Premium Slim Fit T-Shirts
-
-
- ★★★★☆
-
-
-
-
-
-
- €
- 22.30
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mens Cotton Jacket
-
-
- ★★★★★
-
-
-
-
-
-
- €
- 55.99
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Mens Casual Slim Fit
-
-
- ★★☆☆☆
-
-
-
-
-
-
- €
- 15.99
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet
-
-
- ★★★★★
-
-
-
-
-
-
- €
- 695.00
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Solid Gold Petite Micropave
-
-
- ★★★★☆
-
-
-
-
-
-
- €
- 168.00
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- White Gold Plated Princess
-
-
- ★★★☆☆
-
-
-
-
-
-
- €
- 9.99
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Pierced Owl Rose Gold Plated Stainless Steel Double
-
-
- ★★☆☆☆
-
-
-
-
-
-
- €
- 10.99
-
-
-
-
-
-
-
-
- Add to basket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- CHECKOUT
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/__tests__/mocks/handlers.js b/__tests__/mocks/handlers.js
index e39352d..cdbc001 100644
--- a/__tests__/mocks/handlers.js
+++ b/__tests__/mocks/handlers.js
@@ -185,8 +185,12 @@ export const handlers = [
}),
// Mock the /checkout API endpoint
- http.post('/checkout', (req, res, ctx) => {
- return res(ctx.status(200), ctx.json({ success: true }));
+ http.post('http://localhost:9001/checkout', async (req, res, ctx) => {
+ try {
+ return HttpResponse.json({ msg: 'The transaction was completed successfully.' });
+ } catch (error) {
+ console.error('Error processing /checkout:', error);
+ }
}),
http.post('http://localhost:9001/promocode', async (req, res, ctx) => {
@@ -197,4 +201,13 @@ export const handlers = [
return res(ctx.status(400), ctx.json({ message: 'Invalid promo code' }));
}
}),
+
+ http.post('http://localhost:9001/checkout', async (req, res, ctx) => {
+ try {
+ return HttpResponse.json({ msg: 'The transaction was completed successfully.' });
+ } catch (error) {
+ console.error('Error:', error);
+ return res(ctx.status(400), ctx.json({ message: 'Invalid promo code' }));
+ }
+ }),
];
diff --git a/src/screens/CheckoutScreen/CheckoutScreen.integration.test.js b/src/screens/CheckoutScreen/CheckoutScreen.integration.test.js
index 60c2176..38dd083 100644
--- a/src/screens/CheckoutScreen/CheckoutScreen.integration.test.js
+++ b/src/screens/CheckoutScreen/CheckoutScreen.integration.test.js
@@ -9,7 +9,6 @@ const initialState = { basket: { items: sampleBasket } };
// integration test with msw
describe('CheckoutScreen', () => {
- // Integration test with msw, //TODO split into separate file
it('should apply a promo code and update the total price', async () => {
// console.log('Rendering CheckoutScreen...');
renderWithProvidersAndNavigation(, { initialState });
diff --git a/src/screens/PaymentScreen/PaymentScreen.integration.test.js b/src/screens/PaymentScreen/PaymentScreen.integration.test.js
new file mode 100644
index 0000000..f3853dd
--- /dev/null
+++ b/src/screens/PaymentScreen/PaymentScreen.integration.test.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import { fireEvent, screen, waitFor } from '@testing-library/react-native';
+import renderWithProvidersAndNavigation from '@testUtils/renderInProvidersAndNavigation';
+import { sampleBasket } from '@mocks/handlers';
+import { useNavigation } from '@react-navigation/native';
+import PaymentScreen from '.';
+import { strings } from '../../constants';
+
+const initialState = { basket: { items: sampleBasket } };
+
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: jest.fn(),
+}));
+
+const navigateMock = jest.fn();
+
+useNavigation.mockReturnValue({
+ navigate: navigateMock,
+});
+
+// integration test with msw
+describe('PaymentScreen', () => {
+ it('should navigate SuccessScreen when filled valid inputs and PAY and ORDER button press', async () => {
+ // console.log('Rendering CheckoutScreen...');
+ renderWithProvidersAndNavigation(, { initialState });
+
+ const cardHolderNameInput = screen.getAllByText(strings.payment.cardholderName)[0];
+ fireEvent.changeText(cardHolderNameInput, 'James Bond');
+
+ const creditCardInput = screen.getAllByText(strings.payment.creditCardNumber)[0];
+ fireEvent.changeText(creditCardInput, '5566561551349323');
+
+ const expirationInput = screen.getAllByText(strings.payment.expirationDate)[0];
+ fireEvent.changeText(expirationInput, '12/28');
+
+ const cvvInput = screen.getAllByText(strings.payment.cvv)[0];
+ fireEvent.changeText(cvvInput, '156');
+
+ const payAndOrder = screen.getByText(strings.buttons.payAndOrder);
+ fireEvent.press(payAndOrder);
+
+ await waitFor(() => {
+ expect(navigateMock).toHaveBeenCalledWith('Success');
+ });
+ }, 10000);
+
+ // TODO Error case
+});
diff --git a/src/screens/PaymentScreen/PaymentScreen.test.js b/src/screens/PaymentScreen/PaymentScreen.test.js
index a7b5fde..1ed7b58 100644
--- a/src/screens/PaymentScreen/PaymentScreen.test.js
+++ b/src/screens/PaymentScreen/PaymentScreen.test.js
@@ -80,7 +80,7 @@ describe('', () => {
expect(screen.getByText(strings.payment.invalidCard)).toBeTruthy();
});
});
- it('show feedback messages about required fields when pressed pay and order button without filling inputs', async () => {
+ it('show feedback messages about required fields when pressed pay and order button with filling invalid inputs', async () => {
renderWithProvidersAndNavigation(, {
initialState,
});
From 1a4deab220f49f09723f6b8c20e3d4852f9f8883 Mon Sep 17 00:00:00 2001
From: abayram <38274063+abdullahbayram@users.noreply.github.com>
Date: Fri, 27 Dec 2024 00:55:22 +0100
Subject: [PATCH 7/7] test: add unit test for basketSelectors
---
README.md | 2 +-
src/redux/selectors/basketSelectors.test.js | 67 +++++++++++++++++++++
2 files changed, 68 insertions(+), 1 deletion(-)
create mode 100644 src/redux/selectors/basketSelectors.test.js
diff --git a/README.md b/README.md
index 580ae97..8ba2ef1 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,7 @@ Click the image above to view the demo video.
- ✅ Run only necessary tests with `--findRelatedTests` flag with husky pre-commit hook.
- ✅ Use aliases for paths in imports.
- ✅ Generate automated test coverage reports and badges.
-- ✅ Ensure test coverage remains at or above 80%.
+- ✅ Ensure test coverage remains at or above 90%.
---
diff --git a/src/redux/selectors/basketSelectors.test.js b/src/redux/selectors/basketSelectors.test.js
new file mode 100644
index 0000000..06422d3
--- /dev/null
+++ b/src/redux/selectors/basketSelectors.test.js
@@ -0,0 +1,67 @@
+import { selectBasketItems, selectTotalItemCount, selectDiscount, selectTotalPrice } from './basketSelector';
+
+describe('Basket Selectors', () => {
+ const mockState = {
+ basket: {
+ items: [
+ { id: 1, name: 'Item 1', price: 50, quantity: 2 },
+ { id: 2, name: 'Item 2', price: 100, quantity: 1 },
+ ],
+ discount: 10, // 10%
+ },
+ };
+
+ describe('selectBasketItems', () => {
+ it('should return the basket items', () => {
+ const result = selectBasketItems(mockState);
+ expect(result).toEqual(mockState.basket.items);
+ });
+
+ it('should return an empty array if basket items are undefined', () => {
+ const result = selectBasketItems({ basket: {} });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('selectTotalItemCount', () => {
+ it('should calculate the total number of items in the basket', () => {
+ const result = selectTotalItemCount(mockState);
+ expect(result).toBe(3); // 2 + 1
+ });
+
+ it('should return 0 if basket items are empty', () => {
+ const result = selectTotalItemCount({ basket: { items: [] } });
+ expect(result).toBe(0);
+ });
+ });
+
+ describe('selectDiscount', () => {
+ it('should return the discount value', () => {
+ const result = selectDiscount(mockState);
+ expect(result).toBe(10); // 10%
+ });
+
+ it('should return 0 if discount is undefined', () => {
+ const result = selectDiscount({ basket: {} });
+ expect(result).toBe(0);
+ });
+ });
+
+ describe('selectTotalPrice', () => {
+ it('should calculate the total price with the discount applied', () => {
+ const result = selectTotalPrice(mockState);
+ expect(result).toBe(180); // (50 * 2 + 100) * 0.9
+ });
+
+ it('should return 0 if basket items are empty', () => {
+ const result = selectTotalPrice({ basket: { items: [], discount: 10 } });
+ expect(result).toBe(0);
+ });
+
+ it('should calculate total price without discount if discount is 0', () => {
+ const stateWithoutDiscount = { basket: { items: mockState.basket.items, discount: 0 } };
+ const result = selectTotalPrice(stateWithoutDiscount);
+ expect(result).toBe(200); // 50 * 2 + 100
+ });
+ });
+});