diff --git a/e-commerce-app/package-lock.json b/e-commerce-app/package-lock.json index 74e9da4..375576e 100644 --- a/e-commerce-app/package-lock.json +++ b/e-commerce-app/package-lock.json @@ -15,8 +15,10 @@ "@mui/material": "^5.14.4", "@reduxjs/toolkit": "^1.9.5", "classnames": "^2.3.2", + "history": "^5.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", "react-hook-form": "^7.45.4", "react-redux": "^8.1.2", "react-router-dom": "^6.14.2", @@ -34,11 +36,13 @@ "@types/react": "^18.2.18", "@types/react-dom": "^18.2.7", "@types/react-router-dom": "^5.3.3", + "@types/redux-mock-store": "^1.0.3", "eslint": "^8.46.0", "eslint-config-prettier": "^8.9.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", "prettier": "^3.0.0", + "redux-mock-store": "^1.5.4", "sass": "^1.66.1" } }, @@ -4731,6 +4735,15 @@ "@types/react": "*" } }, + "node_modules/@types/redux-mock-store": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz", + "integrity": "sha512-Wqe3tJa6x9MxMN4DJnMfZoBRBRak1XTPklqj4qkVm5VBpZnC8PSADf4kLuFQ9NAdHaowfWoEeUMz7NWc2GMtnA==", + "dev": true, + "dependencies": { + "redux": "^4.0.5" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -9516,6 +9529,14 @@ "he": "bin/he" } }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -12707,6 +12728,12 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -15292,6 +15319,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -15569,6 +15607,15 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, + "dependencies": { + "lodash.isplainobject": "^4.0.6" + } + }, "node_modules/redux-thunk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", diff --git a/e-commerce-app/package.json b/e-commerce-app/package.json index ed6597f..f8f4c23 100644 --- a/e-commerce-app/package.json +++ b/e-commerce-app/package.json @@ -11,8 +11,10 @@ "@mui/material": "^5.14.4", "@reduxjs/toolkit": "^1.9.5", "classnames": "^2.3.2", + "history": "^5.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", "react-hook-form": "^7.45.4", "react-redux": "^8.1.2", "react-router-dom": "^6.14.2", @@ -24,7 +26,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test --coverage", + "test": "react-scripts test --coverage --watchAll", "eject": "react-scripts eject", "lint": "eslint src/**/*.{js,jsx,ts,tsx}", "lint:fix": "eslint --fix src/**/*.{js,jsx,ts,tsx}", @@ -38,6 +40,12 @@ "react-app/jest" ] }, + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/**/*.test.{js,jsx,ts,tsx}" + ] + }, "browserslist": { "production": [ ">0.2%", @@ -59,11 +67,13 @@ "@types/react": "^18.2.18", "@types/react-dom": "^18.2.7", "@types/react-router-dom": "^5.3.3", + "@types/redux-mock-store": "^1.0.3", "eslint": "^8.46.0", "eslint-config-prettier": "^8.9.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", "prettier": "^3.0.0", + "redux-mock-store": "^1.5.4", "sass": "^1.66.1" } } diff --git a/e-commerce-app/src/components/BasketEmpty/BasketEmpty.tsx b/e-commerce-app/src/components/BasketEmpty/BasketEmpty.tsx index 9393286..9918038 100644 --- a/e-commerce-app/src/components/BasketEmpty/BasketEmpty.tsx +++ b/e-commerce-app/src/components/BasketEmpty/BasketEmpty.tsx @@ -11,7 +11,7 @@ const BasketEmpty = (): JSX.Element => { const navigate = useNavigate(); return ( <> - + Your cart is empty diff --git a/e-commerce-app/src/components/Footer/Footer.tsx b/e-commerce-app/src/components/Footer/Footer.tsx index 3ac6d8e..1b57d64 100644 --- a/e-commerce-app/src/components/Footer/Footer.tsx +++ b/e-commerce-app/src/components/Footer/Footer.tsx @@ -9,7 +9,7 @@ import { Box } from '@mui/material'; export const Footer: React.FC = () => { return ( - + diff --git a/e-commerce-app/src/components/Header/Header.tsx b/e-commerce-app/src/components/Header/Header.tsx index 614e2d4..b3cfd0c 100644 --- a/e-commerce-app/src/components/Header/Header.tsx +++ b/e-commerce-app/src/components/Header/Header.tsx @@ -28,7 +28,7 @@ export const Header: React.FC = () => { }; return ( - + diff --git a/e-commerce-app/src/components/UserPersonalAddAddress/UserPersonalAddAddress.tsx b/e-commerce-app/src/components/UserPersonalAddAddress/UserPersonalAddAddress.tsx index 9947179..4171f2d 100644 --- a/e-commerce-app/src/components/UserPersonalAddAddress/UserPersonalAddAddress.tsx +++ b/e-commerce-app/src/components/UserPersonalAddAddress/UserPersonalAddAddress.tsx @@ -108,8 +108,6 @@ const UserPersonalAddAddress = (): JSX.Element => { }; const submitForm: SubmitHandler = (data) => { - console.log('Submit, errors: ', errors); - console.log(data); const addressObject: IUpdateMyCustomerActionAddAddress = { action: 'addAddress', address: { diff --git a/e-commerce-app/src/pages/AboutPage/AboutPage.tsx b/e-commerce-app/src/pages/AboutPage/AboutPage.tsx index c9a3d1c..69cf730 100644 --- a/e-commerce-app/src/pages/AboutPage/AboutPage.tsx +++ b/e-commerce-app/src/pages/AboutPage/AboutPage.tsx @@ -26,6 +26,7 @@ export const AboutPage: React.FC = () => { backgroundSize: 'cover', backgroundPosition: 'top', }} + > { We're on a mission to complete
eCommerce Application successfully. - + RSlogo diff --git a/e-commerce-app/src/pages/RootPage/RootPage.tsx b/e-commerce-app/src/pages/RootPage/RootPage.tsx index 4e3a9d1..6ae744a 100644 --- a/e-commerce-app/src/pages/RootPage/RootPage.tsx +++ b/e-commerce-app/src/pages/RootPage/RootPage.tsx @@ -7,7 +7,7 @@ import { Box, Stack } from '@mui/material'; const RootPage = (): JSX.Element => { return ( - +
diff --git a/e-commerce-app/src/tests/AboutPage.test.tsx b/e-commerce-app/src/tests/AboutPage.test.tsx new file mode 100644 index 0000000..45be3e7 --- /dev/null +++ b/e-commerce-app/src/tests/AboutPage.test.tsx @@ -0,0 +1,26 @@ +import { Provider } from 'react-redux'; +import { store } from '../store/store'; +import { AboutPage } from '../pages/AboutPage/AboutPage'; +import { render, screen } from '@testing-library/react'; + +describe('AboutPage component', () => { + test('renders component without errors', () => { + render( + + + , + ); + + expect(screen.getByText('About Us')).toBeInTheDocument(); + expect( + screen.getByText("We're on a mission to complete eCommerce Application successfully."), + ).toBeInTheDocument(); + + const githubLinks = screen.getAllByRole('link'); + githubLinks.forEach((link) => { + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href'); + expect(link).toHaveAttribute('target', '_blank'); + }); + }); +}); diff --git a/e-commerce-app/src/tests/BasketEmpty.test.tsx b/e-commerce-app/src/tests/BasketEmpty.test.tsx new file mode 100644 index 0000000..27a3223 --- /dev/null +++ b/e-commerce-app/src/tests/BasketEmpty.test.tsx @@ -0,0 +1,42 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import BasketEmpty from '../components/BasketEmpty/BasketEmpty'; +import { MemoryRouter } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +describe('BasketEmpty component', () => { + test('renders the empty cart message', () => { + render( + + + , + ); + + const emptyCartMessage = screen.getByText(/Your cart is empty/i); + expect(emptyCartMessage).toBeInTheDocument(); + }); + + test('renders the start shopping button', () => { + render( + + + , + ); + + const startShoppingButton = screen.getByRole('button', { name: /Start shopping/i }); + expect(startShoppingButton).toBeInTheDocument(); + }); + + test('clicking the start shopping button navigates to /products', () => { + const history = createMemoryHistory(); + render( + + + , + ); + + const startShoppingButton = screen.getByRole('button', { name: /Start shopping/i }); + fireEvent.click(startShoppingButton); + + expect(history.location.pathname).toBe('/'); + }); +}); diff --git a/e-commerce-app/src/tests/BasketPage.test.tsx b/e-commerce-app/src/tests/BasketPage.test.tsx new file mode 100644 index 0000000..5be2e8b --- /dev/null +++ b/e-commerce-app/src/tests/BasketPage.test.tsx @@ -0,0 +1,46 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { store } from '../store/store'; +import { BasketPage } from '../pages/BasketPage/BasketPage'; +import { MemoryRouter } from 'react-router-dom'; + +describe('BasketPage component', () => { + test('renders BasketFull when there are items in the cart', async () => { + jest.mock('../api/cartApi', () => ({ + getTotalQuantityLineItemsInCart: jest.fn().mockReturnValue(2), + })); + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.queryByText(/Shopping cart/)).toBeInTheDocument(); + expect(screen.queryByText(/Loading.../)).toBeNull(); + }); + }); + + test('renders BasketEmpty when there are no items in the cart', async () => { + jest.mock('../api/cartApi', () => ({ + getTotalQuantityLineItemsInCart: jest.fn().mockReturnValue(0), + })); + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText('Your cart is empty')).toBeInTheDocument(); + expect(screen.queryByText(/Continue shopping/)).toBeNull(); + expect(screen.getByText(/Shopping cart/)).toBeInTheDocument(); + }); + }); +}); diff --git a/e-commerce-app/src/tests/EmptyProducts.test.tsx b/e-commerce-app/src/tests/EmptyProducts.test.tsx new file mode 100644 index 0000000..2e148d7 --- /dev/null +++ b/e-commerce-app/src/tests/EmptyProducts.test.tsx @@ -0,0 +1,16 @@ +import { render } from '@testing-library/react'; +import { EmptyProducts } from '../components/ProductsList/EmptyProducts'; + +describe('EmptyProducts Component', () => { + it('should render empty product message and image', () => { + const { getByText, getByAltText } = render(); + + const emptyMessage = getByText( + 'This is an empty product list. There are no products available!', + ); + expect(emptyMessage).toBeInTheDocument(); + + const emptyImage = getByAltText('home-icon'); + expect(emptyImage).toBeInTheDocument(); + }); +}); diff --git a/e-commerce-app/src/tests/Footer.test.tsx b/e-commerce-app/src/tests/Footer.test.tsx new file mode 100644 index 0000000..a1b94a0 --- /dev/null +++ b/e-commerce-app/src/tests/Footer.test.tsx @@ -0,0 +1,8 @@ +import { render } from '@testing-library/react'; +import { Footer } from '../components/Footer/Footer'; + +describe('Footer component', () => { + test('renders component without errors', () => { + render(