diff --git a/__tests__/App.integration.test.js b/__tests__/App.integration.test.js index e34c801..2bb52dc 100644 --- a/__tests__/App.integration.test.js +++ b/__tests__/App.integration.test.js @@ -3,101 +3,214 @@ import App from '../App'; import renderInProvider from './utils/renderInProvider'; import { sampleResponse } from './mocks/handlers'; import { strings } from '../src/constants'; +import { + changeText, + fillPaymentInputs, + getXthOfItemsByText, + pressButton, + verifyCheckoutScreenContents, + verifyExistenceByText, + verifyItemCount, + verifyPaymentInputsFilled, +} from './utils/testUtil'; +import { darkTheme, lightTheme } from '../src/constants/theme'; const anItem = sampleResponse[1]; // 2nd item in the sampleResponse +// Helper functions +const addItemToBasket = async () => { + const addToBasketButton = getXthOfItemsByText('Add to basket', 1); + fireEvent.press(addToBasketButton); + + await waitFor(() => { + verifyExistenceByText('CHECKOUT (1)'); + }); + + // Verify updated basket summary + verifyExistenceByText('Total: $22.30'); +}; + +const navigateToCheckoutScreen = async () => { + pressButton('CHECKOUT (1)'); + + let orderButton; + await waitFor(() => { + orderButton = screen.getByText(/Order\s*\(\s*1\s*items\s*\)/); + }); + + // Verify checkout screen static and dynamic contents + await verifyCheckoutScreenContents({ + totalPrice: '22.30', + totalItemCount: 1, + titleOfAnItem: anItem.title, + }); + + // Verify the item in the basket and in the checkout card + const checkoutCard = screen.getByTestId('checkout-card'); + expect(within(checkoutCard).getByText(anItem.title)).toBeTruthy(); + + return orderButton; +}; + +const applyPromoCodeAndVerifyApplied = async (promoCode) => { + changeText('Promo Code', promoCode); + pressButton('Apply'); + + await waitFor(() => { + verifyExistenceByText('Total: $22.30'); + }); + + // Verify the discount is applied + verifyExistenceByText('Total: $2.23'); +}; + +const submitPayment = async (cardDetails) => { + fillPaymentInputs(cardDetails); + verifyPaymentInputsFilled(cardDetails); + pressButton(strings.buttons.payAndOrder); +}; + +const verifyPaymentScreenContents = (payAndOrderButton) => { + expect(payAndOrderButton).toBeTruthy(); + verifyExistenceByText('Items in the basket: 1'); + verifyExistenceByText('Total: $2.23'); +}; + describe('', () => { - renderInProvider(); - test('user expected journey', async () => { - // Initial state in ProductListScreen + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('user expected journey (success)', async () => { + renderInProvider(); + + // Verify initial state in ProductListScreen await waitFor(() => { - screen.getByText('CHECKOUT (0)'); + verifyExistenceByText('CHECKOUT (0)'); }); - expect(screen.getAllByText('Add to basket').length).toBe(20); // 20 items in the list - // add 2nd item in the sampleResponse - const addToBasketButton = screen.getAllByText('Add to basket')[1]; - fireEvent.press(addToBasketButton); + verifyItemCount('Add to basket', 8); // 8 items in the list - // Updated totalItemCount + // Add item to basket and navigate + await addItemToBasket(); + const orderButton = await navigateToCheckoutScreen(); + + // Apply promo code + await applyPromoCodeAndVerifyApplied('A90'); + + // Navigate to PaymentScreen + fireEvent.press(orderButton); + + let payAndOrderButton; await waitFor(() => { - expect(screen.getByText('CHECKOUT (1)')).toBeTruthy(); + payAndOrderButton = screen.getByText(strings.buttons.payAndOrder); }); - expect(screen.getByText('Total: $22.30')).toBeTruthy(); - const checkoutButton = await screen.findByText('CHECKOUT (1)'); - fireEvent.press(checkoutButton); - // check navigates to CheckoutScreen - let orderButton; + verifyPaymentScreenContents(payAndOrderButton); + + // Submit payment + await submitPayment({ + cardholderName: 'James Bond', + cardNumber: '5566561551349323', // Valid card for success + expirationDate: '12/28', + cvv: '156', + }); + + // Verify navigation to SuccessScreen await waitFor(() => { - orderButton = screen.getByText(/Order\s*\(\s*1\s*items\s*\)/); + verifyExistenceByText(strings.payment.success); + }); + verifyExistenceByText('Redirecting to product list in 5 seconds...'); + + act(() => { + jest.advanceTimersByTime(5000); }); - expect(orderButton).toBeTruthy(); - const promoInput = screen.getAllByText('Promo Code')[0]; - expect(promoInput).toBeTruthy(); - const applyButton = screen.getByText('Apply'); - expect(applyButton).toBeTruthy(); - - // check item in the basket - const checkoutCard = screen.getByTestId('checkout-card'); - expect(within(checkoutCard).getByText(anItem.title)).toBeTruthy(); - - //apply promo code - fireEvent.changeText(promoInput, 'A90'); - fireEvent.press(applyButton); + + // Verify navigation back to ProductListScreen await waitFor(() => { - expect(screen.queryByText('Total: $22.30')).toBeFalsy(); + verifyExistenceByText('CHECKOUT (0)'); }); - expect(screen.queryByText('Total: $2.23')).toBeTruthy(); + verifyItemCount('Add to basket', 8); // 8 items in the list + }, 10000); - // check navigates to PaymentScreen + test('user expected journey (error)', async () => { + renderInProvider(); + + // Verify initial state in ProductListScreen + await waitFor(() => { + verifyExistenceByText('CHECKOUT (0)'); + }); + verifyItemCount('Add to basket', 8); // 8 items in the list + + // Add item to basket and navigate + await addItemToBasket(); + const orderButton = await navigateToCheckoutScreen(); + + // Apply promo code + await applyPromoCodeAndVerifyApplied('A90'); + + // Navigate to PaymentScreen fireEvent.press(orderButton); let payAndOrderButton; await waitFor(() => { payAndOrderButton = screen.getByText(strings.buttons.payAndOrder); }); - expect(payAndOrderButton).toBeTruthy(); - - const itemCountText = screen.getByText('Items in the basket: 1'); - const totalText = screen.getByText('Total: $2.23'); - const cardholderNameInput = screen.getAllByText(strings.payment.cardholderName)[0]; - const cardNumberInput = screen.getAllByText(strings.payment.creditCardNumber)[0]; - const cvvInput = screen.getAllByText(strings.payment.cvv)[0]; - fireEvent.changeText(cvvInput, '12'); - const expirationInput = screen.getAllByText(strings.payment.expirationDate)[0]; - fireEvent.changeText(expirationInput, '07'); - - expect(itemCountText).toBeTruthy(); - expect(totalText).toBeTruthy(); - expect(cardholderNameInput).toBeTruthy(); - expect(cvvInput).toBeTruthy(); - expect(expirationInput).toBeTruthy(); - expect(cardNumberInput).toBeTruthy(); - - // Fill in valid inputs - fireEvent.changeText(cardholderNameInput, 'James Bond'); - fireEvent.changeText(cardNumberInput, '5566561551349323'); // Successful card - fireEvent.changeText(expirationInput, '12/28'); - fireEvent.changeText(cvvInput, '156'); - // Submit payment - fireEvent.press(screen.getByText(strings.buttons.payAndOrder)); + verifyPaymentScreenContents(payAndOrderButton); - // Assert navigation to Success screen - await waitFor(() => { - expect(screen.getByText(strings.payment.success)).toBeTruthy(); + // Submit payment with failing card + await submitPayment({ + cardholderName: 'James Bond', + cardNumber: '5249045959484101', // Failing card + expirationDate: '12/28', + cvv: '156', }); - expect(screen.getByText('Redirecting to product list in 5 seconds...')).toBeTruthy(); + // Verify navigation to ErrorScreen + await waitFor(() => { + verifyExistenceByText('Card can not be processed'); + }); + verifyExistenceByText('Redirecting to product list in 10 seconds...'); act(() => { - jest.advanceTimersByTime(5000); + jest.advanceTimersByTime(10000); }); - // check navigates to ProductListScreen after 5 seconds + // Verify navigation back to ProductListScreen await waitFor(() => { - screen.getByText('CHECKOUT (0)'); + verifyExistenceByText('CHECKOUT (1)'); // Basket still contains the item after failure }); - expect(screen.getAllByText('Add to basket').length).toBe(20); // 20 items in the list + verifyItemCount('Add to basket', 8); // 8 items in the list }, 10000); + test('toggle dark mode', async () => { + renderInProvider(); + + // Verify light mode is enabled by default + await waitFor(() => { + expect(screen.getByTestId('base-screen')).toHaveStyle({ + backgroundColor: lightTheme.colors.background, + }); + }); + + // Locate and toggle dark mode + const toggleButton = await waitFor(() => screen.getByTestId('toggle-dark-mode')); + fireEvent(toggleButton, 'valueChange', true); + + // Verify dark mode is enabled + await waitFor(() => { + expect(screen.getByTestId('base-screen')).toHaveStyle({ + backgroundColor: darkTheme.colors.background, + }); + }); + + // Toggle back to light mode + fireEvent(toggleButton, 'valueChange', false); + + // Verify light mode is enabled + await waitFor(() => { + expect(screen.getByTestId('base-screen')).toHaveStyle({ + backgroundColor: lightTheme.colors.background, + }); + }); + }); }); diff --git a/__tests__/utils/testUtil.js b/__tests__/utils/testUtil.js index 92910bf..7658a7e 100644 --- a/__tests__/utils/testUtil.js +++ b/__tests__/utils/testUtil.js @@ -1,23 +1,182 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { fireEvent, screen, waitFor } from '@testing-library/react-native'; +import { sampleResponse, sampleBasket } from '@mocks/handlers'; +import { strings } from '../../src/constants'; -/* const initialState = { - count: 0, +// Utility for logging during tests +const logHelper = (message, details = {}) => { + console.log(`Test Helper: ${message}`, details); }; -const mockStore = configureStore({ - getState: () => {}, - reducer: { - [apiSlice.reducerPath]: apiSlice.reducer, - }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware), // Add RTK Query middleware - preloadedState: { - counter: { count: 0 }, // Initial state - }, -}); */ -// const store = mockStore(initialState); +const errorHelper = (message, details = {}) => { + console.error(`Test Helper Error: ${message}`, details); +}; + +// Getters + +export const getFirstOfItemsByText = (text) => { + const items = screen.getAllByText(text); + if (!items.length) { + errorHelper('No items found by text', { text }); + throw new Error(`No items found for text: ${text}`); + } + return items[0]; +}; + +export const getXthOfItemsByText = (text, index) => { + const items = screen.getAllByText(text); + if (!items.length) { + errorHelper('No items found by text', { text }); + throw new Error(`No items found for text: ${text}`); + } + return items[index]; +}; + +export const getFirstOfItemsByTestId = (testID) => { + const items = screen.getAllByTestId(testID); + if (!items.length) { + errorHelper('No items found by Test ID', { testID }); + throw new Error(`No items found for Test ID: ${testID}`); + } + return items[0]; +}; + +// Enhanced verification utils +export const verifyItemCount = (text, count) => { + const items = screen.getAllByText(text); + if (items.length !== count) { + errorHelper('Item count mismatch', { text, expected: count, actual: items.length }); + } + expect(items.length).toBe(count); +}; + +export const verifyInExistenceByText = (text) => { + const result = screen.queryByText(text); + if (result) { + errorHelper('Unexpected text found', { text }); + } + expect(result).toBeFalsy(); +}; + +export const verifyExistenceByText = (text) => { + const result = screen.getByText(text); + if (!result) { + errorHelper('Expected text not found', { text }); + } + expect(result).toBeTruthy(); +}; + +export const verifyExistenceByTestId = (testID) => { + const result = screen.getByTestId(testID); + if (!result) { + errorHelper('Expected element not found by Test ID', { testID }); + } + expect(result).toBeTruthy(); +}; + +export const verifyInExistenceByTestId = (testID) => { + const result = screen.queryByTestId(testID); + if (result) { + errorHelper('Unexpected item found by Test ID', { testID }); + } + expect(result).toBeFalsy(); +}; + +export const verifyInputExistenceByText = (text) => { + const inputs = screen.getAllByText(text); + if (inputs.length < 2) { + errorHelper('Expected multiple inputs but found less', { text, count: inputs.length }); + } + expect(inputs[0]).toBeTruthy(); + expect(inputs[1]).toBeTruthy(); +}; -// Custom global render function that wraps the component with Provider +export const verifyCheckoutScreenContents = async ({ totalPrice, totalItemCount, titleOfAnItem }) => { + try { + verifyExistenceByText('Apply'); + verifyInputExistenceByText('Promo Code'); + verifyExistenceByText('Total: $' + totalPrice); + verifyExistenceByText(new RegExp(`Order\\s*\\(\\s*${totalItemCount}\\s*items\\s*\\)`)); + verifyExistenceByText(titleOfAnItem); + } catch (error) { + errorHelper('Error verifying checkout screen contents', { error }); + throw error; + } +}; + +export const verifyExistenceOfPaymentInputs = () => { + expect(screen.getAllByText(strings.payment.cardholderName).length).toBe(2); + expect(screen.getAllByText(strings.payment.creditCardNumber).length).toBe(2); + expect(screen.getAllByText(strings.payment.expirationDate).length).toBe(2); + expect(screen.getAllByText(strings.payment.cvv).length).toBe(2); +}; + +export const verifyPaymentInputsFilled = (cardDetails) => { + const { cardholderName, cardNumber, expirationDate, cvv } = cardDetails; + + // Verify inputs are filled correctly + expect(screen.getByDisplayValue(cardholderName)).toBeTruthy(); + expect(screen.getByDisplayValue(cardNumber)).toBeTruthy(); + expect(screen.getByDisplayValue(expirationDate)).toBeTruthy(); + expect(screen.getByDisplayValue(cvv)).toBeTruthy(); +}; + +// Enhanced action utils +export const pressButton = (buttonText) => { + const button = screen.getByText(buttonText); + if (!button) { + errorHelper('Button not found', { buttonText }); + } + fireEvent.press(button); +}; + +export const pressButtonAsync = async (buttonText) => { + const button = await screen.findByText(buttonText); + if (!button) { + errorHelper('Button not found', { buttonText }); + } + fireEvent.press(button); +}; + +export const changeText = (inputLabelText, text) => { + const input = screen.getAllByText(inputLabelText)[0]; + if (!input) { + errorHelper('Input not found', { inputLabelText }); + } + fireEvent.changeText(input, text); +}; + +export const applyPromoCodeAndVerifyApplied = async (code) => { + changeText('Promo Code', code); + pressButton('Apply'); + await waitFor(() => { + try { + verifyInExistenceByText('Total: $2648.01'); + } catch (error) { + errorHelper('Error applying promo code', { code, error }); + throw error; + } + }); +}; + +export const fillPaymentInputs = (cardDetails) => { + const { cardholderName, cardNumber, expirationDate, cvv } = cardDetails; + changeText(strings.payment.cardholderName, cardholderName); + changeText(strings.payment.creditCardNumber, cardNumber); + changeText(strings.payment.expirationDate, expirationDate); + changeText(strings.payment.cvv, cvv); +}; + +// Mock states +export const mockBasketState = { + basket: { items: sampleBasket }, +}; + +export const mockProductState = { + products: sampleResponse, +}; +// Setup utils export function setupApiStore(api, extraReducers = {}, initialState = {}) { const getStore = (preloadedState) => configureStore({ diff --git a/src/Navigator/Navigator.js b/src/Navigator/Navigator.js index b36c695..4ff316a 100644 --- a/src/Navigator/Navigator.js +++ b/src/Navigator/Navigator.js @@ -10,7 +10,9 @@ import { ThemeContext } from '@context/ThemeContext'; const Stack = createNativeStackNavigator(); const HeaderBackgroundFunc = () => ; -const ToggleDark = (isDarkMode, toggleTheme) => ; +const ToggleDark = (isDarkMode, toggleTheme) => ( + +); const HeaderBackground = () => { const { colors } = useTheme(); diff --git a/src/components/atoms/Switch/index.js b/src/components/atoms/Switch/index.js index f749560..77789ab 100644 --- a/src/components/atoms/Switch/index.js +++ b/src/components/atoms/Switch/index.js @@ -1,3 +1,3 @@ -import { Switch as PSwitch } from 'react-native'; +import { Switch as RNSwitch } from 'react-native'; -export default PSwitch; +export default RNSwitch; diff --git a/src/components/molecules/Toggle/index.js b/src/components/molecules/Toggle/index.js index 35e001f..ad7385f 100644 --- a/src/components/molecules/Toggle/index.js +++ b/src/components/molecules/Toggle/index.js @@ -6,7 +6,7 @@ import { spacing } from '@constants/theme'; import styles from './Toggle.style'; import { Switch, Icon } from '../../atoms'; -const Toggle = ({ isDarkMode, toggleTheme }) => { +const Toggle = ({ isDarkMode, toggleTheme, testID = 'toggle-switch' }) => { const { colors } = useTheme(); return ( @@ -19,7 +19,7 @@ const Toggle = ({ isDarkMode, toggleTheme }) => { ios_backgroundColor={isDarkMode ? colors.textPrimary : colors.textSecondary} value={isDarkMode} onValueChange={toggleTheme} - testID="toggle-switch" + testID={testID} /> ); @@ -28,6 +28,7 @@ const Toggle = ({ isDarkMode, toggleTheme }) => { Toggle.propTypes = { isDarkMode: PropTypes.bool.isRequired, toggleTheme: PropTypes.func.isRequired, + testID: PropTypes.string, }; export default Toggle; diff --git a/src/screens/CheckoutScreen/CheckoutScreen.test.js b/src/screens/CheckoutScreen/CheckoutScreen.test.js index af32ecc..763ae35 100644 --- a/src/screens/CheckoutScreen/CheckoutScreen.test.js +++ b/src/screens/CheckoutScreen/CheckoutScreen.test.js @@ -1,13 +1,19 @@ import React from 'react'; -import { fireEvent, screen, waitFor, within } from '@testing-library/react-native'; import { useNavigation } from '@react-navigation/native'; +import { screen, waitFor, fireEvent, within } from '@testing-library/react-native'; import renderWithProvidersAndNavigation from '@testUtils/renderInProvidersAndNavigation'; +import { + mockBasketState, + verifyCheckoutScreenContents, + verifyInExistenceByText, + verifyExistenceByText, + pressButton, + changeText, + getFirstOfItemsByTestId, +} from '@testUtils/testUtil'; import { sampleBasket } from '@mocks/handlers'; -import mockNavigation from '@mocks/navigation'; +import { strings } from '@constants'; import CheckoutScreen from '.'; -import { strings } from '../../constants'; - -const initialState = { basket: { items: sampleBasket } }; jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), @@ -20,70 +26,79 @@ useNavigation.mockReturnValue({ navigate: navigateMock, }); +// Apply promo code is in the integration test describe('CheckoutScreen', () => { - it('should render CheckoutScreen correctly', () => { - renderWithProvidersAndNavigation(, { initialState }); + beforeEach(() => { + jest.clearAllMocks(); + }); - expect(screen.getByText('Mens Casual Premium Slim Fit T-Shirts')).toBeTruthy(); + const initialState = mockBasketState; - expect(screen.getByText('Total: $2648.01')).toBeTruthy(); - expect(screen.getByText(/Order\s*\(\s*14\s*items\s*\)/)).toBeTruthy(); + it('renders CheckoutScreen correctly', () => { + renderWithProvidersAndNavigation(, { initialState }); - expect(screen.getAllByText('Promo Code')[0]).toBeTruthy(); - expect(screen.getByText('Apply')).toBeTruthy(); + verifyCheckoutScreenContents({ + totalPrice: '2648.01', + totalItemCount: 14, + titleOfAnItem: sampleBasket[0].title, + }); }); + it('should remove an item from the basket when "Remove Item" is pressed', async () => { - renderWithProvidersAndNavigation(, { initialState }); + renderWithProvidersAndNavigation(, { initialState }); // Get the checkoutcard and its 'Remove Item' button - const firstCheckoutCard = screen.getAllByTestId('checkout-card')[0]; + const firstCheckoutCard = getFirstOfItemsByTestId('checkout-card'); const removeItemButton = within(firstCheckoutCard).getByText('Remove Item'); // Initial assertions - expect(screen.getByText(sampleBasket[0].title)).toBeTruthy(); - expect(screen.getByText(/Order\s*\(\s*14\s*items\s*\)/)).toBeTruthy(); // Basket starts with 14 item - expect(screen.getByText('Total: $2648.01')).toBeTruthy(); + verifyCheckoutScreenContents({ + totalPrice: '2648.01', + totalItemCount: 14, // Basket starts with 14 item + titleOfAnItem: sampleBasket[0].title, + }); fireEvent.press(removeItemButton); // there were 3 items of removed product // Check total item count and total price updated - expect(screen.queryByText(sampleBasket[0].title)).toBeFalsy(); - expect(screen.getByText(/Order\s*\(\s*11\s*items\s*\)/)).toBeTruthy(); - expect(screen.getByText('Total: $2318.16')).toBeTruthy(); + verifyInExistenceByText(sampleBasket[0].title); + verifyExistenceByText(/Order\s*\(\s*11\s*items\s*\)/); + verifyExistenceByText('Total: $2318.16'); }); - it('should navigate to Payment screen when the ORDER button is pressed', () => { - renderWithProvidersAndNavigation(, { initialState }); + + it('should navigate to Payment screen when ORDER button is pressed', async () => { + renderWithProvidersAndNavigation(, { initialState }); // Press the "ORDER" button - const orderButton = screen.getByText(/Order\s*\(\s*14\s*items\s*\)/); - fireEvent.press(orderButton); + pressButton(/Order\s*\(\s*14\s*items\s*\)/); - // Assert that navigation is called - expect(navigateMock).toHaveBeenCalledTimes(1); - expect(navigateMock).toHaveBeenCalledWith('Payment'); // Replace with the actual route name + await waitFor(() => { + expect(navigateMock).toHaveBeenCalledWith('Payment'); + }); }); - it('should show an error message when an invalid promo code is applied', async () => { - renderWithProvidersAndNavigation(, { initialState }); - const applyButton = screen.getByText('Apply'); - fireEvent.press(applyButton); + + it('should show an error message when an invalid promo code or empty code is applied ', async () => { + renderWithProvidersAndNavigation(, { initialState }); + + pressButton('Apply'); + await waitFor(() => { - expect(screen.getByText(strings.checkout.promoCodeRequired)).toBeTruthy(); + verifyExistenceByText(strings.checkout.promoCodeRequired); }); - const promoInput = screen.getAllByText('Promo Code')[0]; - fireEvent.changeText(promoInput, 'INVALIDCODE'); + changeText('Promo Code', 'INVALIDCODE'); - fireEvent.press(applyButton); + pressButton('Apply'); await waitFor(() => { - expect(screen.getByText(strings.checkout.promoCodeNotValid)).toBeTruthy(); + verifyExistenceByText(strings.checkout.promoCodeNotValid); }); }); it('should disable order and promo buttons if the basket is empty', () => { - renderWithProvidersAndNavigation(, { + renderWithProvidersAndNavigation(, { initialState: { basket: { items: [] } }, }); - expect(screen.getByText(strings.checkout.emptyBasket)).toBeTruthy(); + verifyExistenceByText(strings.checkout.emptyBasket); const orderButton = screen.getByTestId('order-button'); const applyPromoButton = screen.getByText('Apply'); @@ -91,60 +106,69 @@ describe('CheckoutScreen', () => { expect(applyPromoButton).toBeDisabled(); }); it('should increase unit quantity, total count, and total price on "+" button press', async () => { - renderWithProvidersAndNavigation(, { initialState }); + renderWithProvidersAndNavigation(, { initialState }); - const firstCheckoutCard = screen.getAllByTestId('checkout-card')[0]; + const firstCheckoutCard = getFirstOfItemsByTestId('checkout-card'); const increaseQuantityButton = within(firstCheckoutCard).getByTestId('increase-button'); // Check initial state - expect(screen.getByText(/Order\s*\(\s*14\s*items\s*\)/)).toBeTruthy(); - expect(screen.getByText('Total: $2648.01')).toBeTruthy(); + verifyCheckoutScreenContents({ + totalPrice: '2648.01', + totalItemCount: 14, + titleOfAnItem: sampleBasket[0].title, + }); // Press "+" button fireEvent.press(increaseQuantityButton); // Assertions after increment await waitFor(() => { - expect(screen.getByText(/Order\s*\(\s*15\s*items\s*\)/)).toBeTruthy(); + verifyExistenceByText(/Order\s*\(\s*15\s*items\s*\)/); }); - expect(screen.getByText('Total: $2757.96')).toBeTruthy(); // Adjusted for increased item + verifyExistenceByText('Total: $2757.96'); // Updated for increased item }); it('should decrease unit quantity, total count, and total price on "-" button press', async () => { - renderWithProvidersAndNavigation(, { initialState }); + renderWithProvidersAndNavigation(, { initialState }); - const firstCheckoutCard = screen.getAllByTestId('checkout-card')[0]; + const firstCheckoutCard = getFirstOfItemsByTestId('checkout-card'); const decreaseQuantityButton = within(firstCheckoutCard).getByTestId('decrease-button'); // Check initial state - expect(screen.getByText(/Order\s*\(\s*14\s*items\s*\)/)).toBeTruthy(); - expect(screen.getByText('Total: $2648.01')).toBeTruthy(); + verifyCheckoutScreenContents({ + totalPrice: '2648.01', + totalItemCount: 14, + titleOfAnItem: sampleBasket[0].title, + }); // Press "-" button fireEvent.press(decreaseQuantityButton); // Assertions after decrement await waitFor(() => { - expect(screen.getByText(/Order\s*\(\s*13\s*items\s*\)/)).toBeTruthy(); + verifyExistenceByText(/Order\s*\(\s*13\s*items\s*\)/); }); - expect(screen.getByText('Total: $2538.06')).toBeTruthy(); // Adjusted for decreased item + verifyExistenceByText('Total: $2538.06'); // Adjusted for decreased item }); it('should remove an item from the basket if quantity is 1 on delete icon press', async () => { - renderWithProvidersAndNavigation(, { initialState }); + renderWithProvidersAndNavigation(, { initialState }); - const firstCheckoutCard = screen.getAllByTestId('checkout-card')[0]; // first card in the basket + const firstCheckoutCard = getFirstOfItemsByTestId('checkout-card'); const decreaseQuantityButton = within(firstCheckoutCard).getByTestId('decrease-button'); const quantityText = within(firstCheckoutCard).getByTestId('product-quantity'); // Check initial state - expect(screen.getByText(/Order\s*\(\s*14\s*items\s*\)/)).toBeTruthy(); - expect(screen.getByText('Total: $2648.01')).toBeTruthy(); - expect(screen.getByText(sampleBasket[0].title)).toBeTruthy(); // title of first item in the basket + verifyCheckoutScreenContents({ + totalPrice: '2648.01', + totalItemCount: 14, + titleOfAnItem: sampleBasket[0].title, + }); + expect(within(quantityText).getByText('3')).toBeTruthy(); // count of first item in the basket is 3 expect(within(firstCheckoutCard).queryByTestId('delete-button')).toBeFalsy(); // delete button is not present - fireEvent.press(decreaseQuantityButton); // product count descrased to 2 + fireEvent.press(decreaseQuantityButton); // product count decreased to 2 // await waitFor(() => {}); - fireEvent.press(decreaseQuantityButton); // product count descrased to 1 + fireEvent.press(decreaseQuantityButton); // product count decreased to 1 expect(within(quantityText).getByText('1')).toBeTruthy(); // check product count is 1 expect(within(firstCheckoutCard).queryByTestId('decrease-button')).toBeFalsy(); // decrease button is removed namely changed to delete button @@ -154,9 +178,9 @@ describe('CheckoutScreen', () => { // Check item removed await waitFor(() => { - expect(screen.queryByText(sampleBasket[0].title)).toBeFalsy(); // first item in the basket removed + verifyInExistenceByText(sampleBasket[0].title); // first item in the basket removed }); - expect(screen.getByText(/Order\s*\(\s*11\s*items\s*\)/)).toBeTruthy(); - expect(screen.getByText('Total: $2318.16')).toBeTruthy(); + verifyExistenceByText(/Order\s*\(\s*11\s*items\s*\)/); + verifyExistenceByText('Total: $2318.16'); }); }); diff --git a/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js b/src/screens/PaymentScreen/PaymentForm/PaymentForm.test.js index e5d8abd..198198f 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 { strings } from '@constants'; import renderInFormProvider from '@testUtils/renderInFormProvider'; import PaymentForm from './index'; +import { verifyExistenceOfPaymentInputs } from '../../../../__tests__/utils/testUtil'; jest.mock('@utils', () => ({ paymentUtils: { @@ -23,9 +22,6 @@ describe('', () => { it('renders all inputs with correct labels', () => { renderInFormProvider(); - expect(screen.getAllByText(strings.payment.cardholderName).length).toBe(2); - expect(screen.getAllByText(strings.payment.creditCardNumber).length).toBe(2); - expect(screen.getAllByText(strings.payment.expirationDate).length).toBe(2); - expect(screen.getAllByText(strings.payment.cvv).length).toBe(2); + verifyExistenceOfPaymentInputs(); }); }); diff --git a/src/screens/PaymentScreen/PaymentScreen.test.js b/src/screens/PaymentScreen/PaymentScreen.test.js index 1ed7b58..0f769fd 100644 --- a/src/screens/PaymentScreen/PaymentScreen.test.js +++ b/src/screens/PaymentScreen/PaymentScreen.test.js @@ -1,9 +1,17 @@ import React from 'react'; -import { fireEvent, screen, waitFor } from '@testing-library/react-native'; +import { 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 { + fillPaymentInputs, + verifyPaymentInputsFilled, + verifyExistenceOfPaymentInputs, + changeText, + pressButton, + verifyExistenceByText, +} from '@testUtils/testUtil'; import PaymentScreen from '.'; jest.mock('@react-navigation/native', () => ({ @@ -34,13 +42,8 @@ describe('', () => { renderWithProvidersAndNavigation(, { initialState, }); - const itemCountText = screen.getByText('Items in the basket: 14'); - const totalText = screen.getByText('Total: $2648.01'); - // const itemCountText = screen.getByText(/Items in the basket:/i); - // const totalText = screen.getByText(/Total:/i); - - expect(itemCountText).toBeTruthy(); - expect(totalText).toBeTruthy(); + verifyExistenceByText('Items in the basket: 14'); + verifyExistenceByText('Total: $2648.01'); }); it('renders PaymentForm', () => { @@ -48,10 +51,7 @@ describe('', () => { initialState, }); - expect(screen.getAllByText(strings.payment.cardholderName)[0]).toBeTruthy(); - expect(screen.getAllByText(strings.payment.creditCardNumber)[0]).toBeTruthy(); - expect(screen.getAllByText(strings.payment.expirationDate)[0]).toBeTruthy(); - expect(screen.getAllByText(strings.payment.cvv)[0]).toBeTruthy(); + verifyExistenceOfPaymentInputs(); }); it('disables the order button when basket is empty', () => { @@ -60,61 +60,61 @@ describe('', () => { initialState, }); - const orderButton = screen.getByText(strings.buttons.payAndOrder); - fireEvent.press(orderButton); + pressButton(strings.buttons.payAndOrder); expect(mockOnPress).not.toHaveBeenCalled(); }); + it('validates credit card input', async () => { renderWithProvidersAndNavigation(, { initialState, }); - const cardNumberInput = screen.getAllByText(strings.payment.creditCardNumber)[0]; - fireEvent.changeText(cardNumberInput, '1234'); - - const orderButton = screen.getByText(strings.buttons.payAndOrder); - fireEvent.press(orderButton); + changeText('Credit Card Number', '1234'); + pressButton(strings.buttons.payAndOrder); await waitFor(() => { - expect(screen.getByText(strings.payment.invalidCard)).toBeTruthy(); + verifyExistenceByText(strings.payment.invalidCard); }); }); + it('show feedback messages about required fields when pressed pay and order button with filling invalid inputs', async () => { renderWithProvidersAndNavigation(, { initialState, }); - const cardholderNameInput = screen.getAllByText(strings.payment.cardholderName)[0]; - fireEvent.changeText(cardholderNameInput, 'AB'); - const cvvInput = screen.getAllByText(strings.payment.cvv)[0]; - fireEvent.changeText(cvvInput, '12'); - const expirationInput = screen.getAllByText(strings.payment.expirationDate)[0]; - fireEvent.changeText(expirationInput, '07'); + const cardDetails = { + cardholderName: 'AB', + cardNumber: '5566561551349323', // Valid card + expirationDate: '07', + cvv: '12', + }; + + fillPaymentInputs(cardDetails); + verifyPaymentInputsFilled(cardDetails); - const orderButton = screen.getByText(strings.buttons.payAndOrder); - fireEvent.press(orderButton); + pressButton(strings.buttons.payAndOrder); await waitFor(() => { - expect(screen.getAllByText(strings.payment.cardHolderMinLength)[0]).toBeTruthy(); + verifyExistenceByText(strings.payment.cardHolderMinLength); }); - expect(screen.getAllByText(strings.payment.cvvLength)[0]).toBeTruthy(); - expect(screen.getAllByText(strings.payment.invalidExpirationDate)[0]).toBeTruthy(); + + verifyExistenceByText(strings.payment.invalidExpirationDate); + verifyExistenceByText(strings.payment.invalidExpirationDate); + verifyExistenceByText(strings.payment.cvvLength); }); + it('show feedback messages about required fields when pressed pay and order button without filling inputs', async () => { renderWithProvidersAndNavigation(, { initialState, }); - const orderButton = screen.getByText(strings.buttons.payAndOrder); - fireEvent.press(orderButton); + pressButton(strings.buttons.payAndOrder); await waitFor(() => { - expect(screen.getAllByText(strings.payment.cardHolderRequired)[0]).toBeTruthy(); - }); - expect(screen.getAllByText(strings.payment.cvvRequired)[0]).toBeTruthy(); - expect(screen.getAllByText(strings.payment.expirationDateRequired)[0]).toBeTruthy(); - await waitFor(() => { - expect(screen.getAllByText(strings.payment.creditCardRequired)[0]).toBeTruthy(); + verifyExistenceByText(strings.payment.cardHolderRequired); }); + verifyExistenceByText(strings.payment.cvvRequired); + verifyExistenceByText(strings.payment.expirationDateRequired); + verifyExistenceByText(strings.payment.creditCardRequired); }); }); diff --git a/src/screens/ProductListScreen/ProductListScreen.test.js b/src/screens/ProductListScreen/ProductListScreen.test.js index 791ae6e..582e4f5 100644 --- a/src/screens/ProductListScreen/ProductListScreen.test.js +++ b/src/screens/ProductListScreen/ProductListScreen.test.js @@ -1,11 +1,21 @@ import React from 'react'; -import { fireEvent, screen, waitFor, within } from '@testing-library/react-native'; +import { fireEvent, 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 '.'; +import { + verifyExistenceByTestId, + verifyInExistenceByText, + verifyInExistenceByTestId, + pressButton, + verifyExistenceByText, + getFirstOfItemsByText, + getFirstOfItemsByTestId, + pressButtonAsync, +} from '../../../__tests__/utils/testUtil'; jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), @@ -38,8 +48,8 @@ describe('ProductListScreen', () => { 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(); + verifyExistenceByTestId('loading-state'); + verifyInExistenceByText(strings.productList.errorLoading); }); it('should render the error state when there is an error', async () => { @@ -55,8 +65,8 @@ describe('ProductListScreen', () => { 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(); + verifyExistenceByText(strings.productList.errorLoading); + verifyInExistenceByTestId('loading-state'); }); it('should call refetch when Retry pressed in error state', async () => { @@ -71,9 +81,7 @@ describe('ProductListScreen', () => { renderWithProvidersAndNavigation(); - const retryButton = screen.getByText('Retry'); - fireEvent.press(retryButton); - + pressButton('Retry'); expect(refetchMock).toHaveBeenCalledTimes(1); }); @@ -86,9 +94,9 @@ describe('ProductListScreen', () => { renderWithProvidersAndNavigation(); await waitFor(() => { - expect(screen.getByText(anItem.title)).toBeTruthy(); + verifyExistenceByText(anItem.title); }); - expect(screen.getByText('Mens Casual Premium Slim Fit T-Shirts ')).toBeTruthy(); + verifyExistenceByText('Mens Casual Premium Slim Fit T-Shirts '); }); it('navigates to Checkout screen when checkout button is pressed', async () => { @@ -99,13 +107,11 @@ describe('ProductListScreen', () => { }); renderWithProvidersAndNavigation(, { initialState: { basket: { items: sampleBasket } } }); - const checkoutButton = await screen.findByText('CHECKOUT (14)'); - fireEvent.press(checkoutButton); + await pressButtonAsync('CHECKOUT (14)'); await waitFor(() => { - expect(navigateMock).toHaveBeenCalledTimes(1); + expect(navigateMock).toHaveBeenCalledWith('Checkout'); }); - expect(navigateMock).toHaveBeenCalledWith('Checkout'); // Replace with the actual route name if necessary }); it('should disable the checkout button when the basket is invalid', () => { @@ -116,8 +122,7 @@ describe('ProductListScreen', () => { }); renderWithProvidersAndNavigation(); - const checkoutButton = screen.getByText('CHECKOUT (0)'); - fireEvent.press(checkoutButton); + pressButton('CHECKOUT (0)'); expect(navigateMock).toHaveBeenCalledTimes(0); }); @@ -131,17 +136,17 @@ describe('ProductListScreen', () => { renderWithProvidersAndNavigation(); // Initial totalItemCount - expect(screen.getByText('CHECKOUT (0)')).toBeTruthy(); + verifyExistenceByText('CHECKOUT (0)'); // Simulate adding the first product to the basket - const addToBasketButton = screen.getAllByText('Add to basket')[0]; + const addToBasketButton = getFirstOfItemsByText('Add to basket'); fireEvent.press(addToBasketButton); // Updated totalItemCount await waitFor(() => { - expect(screen.getByText('CHECKOUT (1)')).toBeTruthy(); + verifyExistenceByText('CHECKOUT (1)'); }); - expect(screen.getByText('Total: $109.95')).toBeTruthy(); + verifyExistenceByText('Total: $109.95'); }); it('should not allow adding the same product to the basket more than five times', async () => { useGetProductsQuery.mockReturnValue({ @@ -153,12 +158,13 @@ describe('ProductListScreen', () => { renderWithProvidersAndNavigation(); // Get the first product's Card and its "Add to basket" button - const firstProductCard = screen.getAllByTestId('product-card')[0]; + const firstProductCard = getFirstOfItemsByTestId('product-card'); const addToBasketButton = within(firstProductCard).getByText('Add to basket'); // Initial assertions - expect(screen.getByText('CHECKOUT (0)')).toBeTruthy(); // Basket starts empty - expect(screen.queryByText(strings.productList.limitReached)).toBeFalsy(); + verifyExistenceByText('CHECKOUT (0)'); // Basket starts empty + verifyExistenceByText('Total: $0.00'); // Total price starts at 0 + verifyInExistenceByText(strings.productList.limitReached); // Add the same product to the basket five times for (let i = 0; i < 5; i += 1) { @@ -167,17 +173,18 @@ describe('ProductListScreen', () => { // Check total item count and total price updated await waitFor(() => { - expect(screen.getByText('CHECKOUT (5)')).toBeTruthy(); + verifyExistenceByText('CHECKOUT (5)'); }); - expect(screen.getByText('Total: $549.75')).toBeTruthy(); // Ensure total price is updated correctly + // Ensure total price is updated correctly + verifyExistenceByText('Total: $549.75'); // 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(); + verifyExistenceByText(strings.productList.limitReached); // TotalItemCount should still be 5, not updated further - expect(screen.getByText('CHECKOUT (5)')).toBeTruthy(); + verifyExistenceByText('CHECKOUT (5)'); }); });