diff --git a/package-lock.json b/package-lock.json
index 030f70b..d2531b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "rss-react",
"version": "0.1.0",
"dependencies": {
+ "@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
@@ -18,8 +19,10 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9",
+ "react-redux": "^8.0.5",
"react-router-dom": "^6.9.0",
"react-scripts": "5.0.1",
+ "redux": "^4.2.1",
"sass": "^1.59.3",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
@@ -3166,6 +3169,29 @@
}
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
+ "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==",
+ "dependencies": {
+ "immer": "^9.0.16",
+ "redux": "^4.2.0",
+ "redux-thunk": "^2.4.2",
+ "reselect": "^4.1.7"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18",
+ "react-redux": "^7.2.1 || ^8.0.2"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.4.0.tgz",
@@ -3848,6 +3874,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -4046,6 +4081,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"node_modules/@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
@@ -8857,6 +8897,19 @@
"integrity": "sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==",
"dev": true
},
+ "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",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -15410,6 +15463,49 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/react-redux": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
+ "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.1",
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "@types/use-sync-external-store": "^0.0.3",
+ "hoist-non-react-statics": "^3.3.2",
+ "react-is": "^18.0.0",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8 || ^17.0 || ^18.0",
+ "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react-native": ">=0.59",
+ "redux": "^4"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-redux/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -15575,6 +15671,22 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/redux-thunk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
+ "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
+ "peerDependencies": {
+ "redux": "^4"
+ }
+ },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -15701,6 +15813,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "node_modules/reselect": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
+ "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
+ },
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -17437,6 +17554,14 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -20591,6 +20716,17 @@
"source-map": "^0.7.3"
}
},
+ "@reduxjs/toolkit": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
+ "integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==",
+ "requires": {
+ "immer": "^9.0.16",
+ "redux": "^4.2.0",
+ "redux-thunk": "^2.4.2",
+ "reselect": "^4.1.7"
+ }
+ },
"@remix-run/router": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.4.0.tgz",
@@ -21083,6 +21219,15 @@
"@types/node": "*"
}
},
+ "@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "requires": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -21281,6 +21426,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
+ "@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
@@ -24761,6 +24911,21 @@
"integrity": "sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==",
"dev": true
},
+ "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",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "requires": {
+ "react-is": "^16.7.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ }
+ }
+ },
"hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -29287,6 +29452,26 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "react-redux": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
+ "integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
+ "requires": {
+ "@babel/runtime": "^7.12.1",
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "@types/use-sync-external-store": "^0.0.3",
+ "hoist-non-react-statics": "^3.3.2",
+ "react-is": "^18.0.0",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ }
+ }
+ },
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -29407,6 +29592,20 @@
"strip-indent": "^3.0.0"
}
},
+ "redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "requires": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "redux-thunk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
+ "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
+ "requires": {}
+ },
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -29508,6 +29707,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "reselect": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
+ "integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
+ },
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -30770,6 +30974,12 @@
"requires-port": "^1.0.0"
}
},
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "requires": {}
+ },
"util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
diff --git a/package.json b/package.json
index 93e9782..ca82528 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
@@ -14,8 +15,10 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9",
+ "react-redux": "^8.0.5",
"react-router-dom": "^6.9.0",
"react-scripts": "5.0.1",
+ "redux": "^4.2.1",
"sass": "^1.59.3",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
diff --git a/src/components/app/App.test.tsx b/src/components/app/App.test.tsx
index 8ce39e0..1c0961a 100644
--- a/src/components/app/App.test.tsx
+++ b/src/components/app/App.test.tsx
@@ -3,13 +3,24 @@ import { render, screen } from '@testing-library/react';
import App from './App';
import { MemoryRouter } from 'react-router-dom';
+import * as reduxHooks from 'react-redux';
+jest.mock('react-redux');
+const mockedUseSelector = jest.spyOn(reduxHooks, 'useSelector');
+const mockedUseDispatch = jest.spyOn(reduxHooks, 'useDispatch');
+import { initialState } from '../../store/moviesSlice';
+
describe('test App component', () => {
test('it renders', () => {
+ mockedUseSelector.mockReturnValue(initialState);
+ const dispatch = jest.fn();
+ mockedUseDispatch.mockReturnValue(dispatch);
+
render(
);
+ expect(screen.getByRole('app')).toBeInTheDocument();
screen.debug();
});
diff --git a/src/components/app/App.tsx b/src/components/app/App.tsx
index 15f6782..fad80e4 100644
--- a/src/components/app/App.tsx
+++ b/src/components/app/App.tsx
@@ -12,7 +12,7 @@ import Layout from '../layout';
export default class App extends Component<{}, {}> {
render() {
return (
-
+
}>
}>
diff --git a/src/components/card/card.test.tsx b/src/components/card/card.test.tsx
index 719ac41..98da2b5 100644
--- a/src/components/card/card.test.tsx
+++ b/src/components/card/card.test.tsx
@@ -1,19 +1,40 @@
-import { render, screen } from '@testing-library/react';
+import { render, screen, fireEvent } from '@testing-library/react';
import Card from './card';
-import { dataMovie } from '../../data/dataMovie';
+import { dataMovie } from '../../testData/dataMovie';
-describe('test Card component', () => {
- const mockFunction = jest.fn();
+import * as reduxHooks from 'react-redux';
+import * as actions from './../../store/moviesSlice';
+jest.mock('react-redux');
+const mockedUseDispatch = jest.spyOn(reduxHooks, 'useDispatch');
+describe('test Card component', () => {
test('it renders', () => {
- render();
+ render();
expect(screen.getByRole('card-item')).toBeInTheDocument();
});
test('it renders title of film "Fight Club", release-year "1999"', () => {
- render();
+ render();
expect(screen.getByText(/Fight Club/i)).toBeInTheDocument();
expect(screen.getByText(/1999/i)).toBeInTheDocument();
});
+
+ test('dispatch actions called ', () => {
+ const dispatch = jest.fn();
+ mockedUseDispatch.mockReturnValue(dispatch);
+ const mockedToggleModal = jest.spyOn(actions, 'toggleModal');
+ const mockedUpdateMovieId = jest.spyOn(actions, 'updateMovieId');
+
+ render();
+
+ const button = screen.getByRole('showMoreInfo-button');
+ fireEvent.click(button);
+
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ expect(mockedToggleModal).toHaveBeenCalledTimes(1);
+
+ expect(mockedUpdateMovieId).toHaveBeenCalledTimes(1);
+ expect(mockedUpdateMovieId).toHaveBeenCalledWith(550); //get id=550 from dataMovie
+ });
});
diff --git a/src/components/card/card.tsx b/src/components/card/card.tsx
index 73a517f..fba44ab 100644
--- a/src/components/card/card.tsx
+++ b/src/components/card/card.tsx
@@ -4,20 +4,23 @@ import './card.scss';
import { IMovie } from '../types';
import { _baseImagePath } from '../../services/movies-services';
+import { useAppDispatch } from '../../hook';
+import { toggleModal, updateMovieId } from './../../store/moviesSlice';
+
type CardProps = {
item: IMovie;
- setIsModalOpen: (newValue: boolean) => void;
- showDetailInfo: (id: number) => void;
};
-const Card = ({ item, setIsModalOpen, showDetailInfo }: CardProps) => {
+const Card = ({ item }: CardProps) => {
const { id, title, original_title, poster_path, vote_average, release_date } = item;
const baseImagePath = _baseImagePath;
const yearRelease: string = release_date.slice(0, 4);
+ const dispatch = useAppDispatch();
+
const showMoreInfo = () => {
- setIsModalOpen(true);
- showDetailInfo(id);
+ dispatch(toggleModal());
+ dispatch(updateMovieId(id));
};
return (
diff --git a/src/components/cardForm-list/cardForm-list.test.tsx b/src/components/cardForm-list/cardForm-list.test.tsx
deleted file mode 100644
index 9108865..0000000
--- a/src/components/cardForm-list/cardForm-list.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { render, screen } from '@testing-library/react';
-
-import { testCardsForm } from '../../testData/testCardsForm';
-import CardFormList from './cardForm-list';
-
-describe('test CardFormList component', () => {
- test('render CardFormList', () => {
- render();
- expect(screen.getByTestId('card-form-list')).toBeInTheDocument();
- });
-
- test('should be render all CardsForm in CardFormList ', () => {
- render();
- expect(screen.getAllByRole('card-form').length).toBe(testCardsForm.length);
- });
-});
diff --git a/src/components/cards-list/cards-list.test.tsx b/src/components/cards-list/cards-list.test.tsx
index 9e85e30..850939e 100644
--- a/src/components/cards-list/cards-list.test.tsx
+++ b/src/components/cards-list/cards-list.test.tsx
@@ -2,22 +2,23 @@ import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import CardsList from './cards-list';
-import { dataMovie } from '../../data/dataMovie';
+import { dataMovie } from '../../testData/dataMovie';
import { IMovie } from '../types';
+jest.mock('react-redux');
+
describe('test CardsList component', () => {
- const itemsMoke: IMovie[] = [dataMovie, dataMovie];
- const mockFunction = jest.fn();
+ const itemsMoke: IMovie[] = [dataMovie];
test('it renders', async () => {
- render();
+ render();
const cardsList = await waitFor(() => screen.getByTestId('cards-list'));
expect(cardsList).toBeInTheDocument();
});
test('should be render all Card in CardsList ', () => {
- render();
+ render();
expect(screen.getAllByRole('card-item').length).toBe(itemsMoke.length);
});
});
diff --git a/src/components/cards-list/cards-list.tsx b/src/components/cards-list/cards-list.tsx
index fd989e9..7d30010 100644
--- a/src/components/cards-list/cards-list.tsx
+++ b/src/components/cards-list/cards-list.tsx
@@ -7,17 +7,15 @@ import { IMovie } from '../types';
type CardListProps = {
items: IMovie[];
- setIsModalOpen: (newValue: boolean) => void;
- showDetailInfo: (id: number) => void;
};
-const CardList = ({ items, setIsModalOpen, showDetailInfo }: CardListProps) => {
+const CardList = ({ items }: CardListProps) => {
const cards = items.map((item) => {
const { id } = item;
return (
-
+
);
});
diff --git a/src/components/errorIndicator/errorIndicator.test.tsx b/src/components/errorIndicator/errorIndicator.test.tsx
new file mode 100644
index 0000000..06ea82b
--- /dev/null
+++ b/src/components/errorIndicator/errorIndicator.test.tsx
@@ -0,0 +1,10 @@
+import { render, screen } from '@testing-library/react';
+
+import ErrorIndicator from './errorIndicator';
+
+describe('test ErrorIndicator', () => {
+ test('should render', () => {
+ render();
+ expect(screen.getByTestId('error-indicator')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/cardForm-list/cardForm-list.scss b/src/components/form/cardForm-list/cardForm-list.scss
similarity index 100%
rename from src/components/cardForm-list/cardForm-list.scss
rename to src/components/form/cardForm-list/cardForm-list.scss
diff --git a/src/components/form/cardForm-list/cardForm-list.test.tsx b/src/components/form/cardForm-list/cardForm-list.test.tsx
new file mode 100644
index 0000000..1efbf9e
--- /dev/null
+++ b/src/components/form/cardForm-list/cardForm-list.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from '@testing-library/react';
+import * as reduxHooks from 'react-redux';
+
+jest.mock('react-redux');
+const mockedUseSelector = jest.spyOn(reduxHooks, 'useSelector');
+
+import { testCardsForm } from '../../../testData/testCardsForm';
+import CardFormList from './cardForm-list';
+
+describe('test CardFormList component', () => {
+ test('should create CardFormList with empty list', () => {
+ mockedUseSelector.mockReturnValue([]);
+ render();
+ expect(screen.getByTestId('card-form-list')).toBeInTheDocument();
+ });
+
+ test('should create CardFormList with data testCardsForm', () => {
+ mockedUseSelector.mockReturnValue(testCardsForm);
+ render();
+ expect(screen.getAllByRole('card-form').length).toBe(testCardsForm.length);
+ });
+});
diff --git a/src/components/cardForm-list/cardForm-list.tsx b/src/components/form/cardForm-list/cardForm-list.tsx
similarity index 62%
rename from src/components/cardForm-list/cardForm-list.tsx
rename to src/components/form/cardForm-list/cardForm-list.tsx
index 99d21ee..f81ea27 100644
--- a/src/components/cardForm-list/cardForm-list.tsx
+++ b/src/components/form/cardForm-list/cardForm-list.tsx
@@ -1,16 +1,14 @@
import React, { FC } from 'react';
-import { ICardForm } from '../cardForm/cardForm';
import CardForm from '../cardForm';
+import { useAppSelector } from '../../../hook';
import './cardForm-list.scss';
-type CardFormListProps = {
- cardsForm: ICardForm[];
-};
+const CardFormList: FC = () => {
+ const cardsForm = useAppSelector((state) => state.formReducer.cardsForm);
-const CardFormList: FC = (props: CardFormListProps) => {
- const cards = props.cardsForm.map((card) => {
+ const cards = cardsForm.map((card) => {
const { id } = card;
return (
diff --git a/src/components/cardForm-list/index.tsx b/src/components/form/cardForm-list/index.tsx
similarity index 100%
rename from src/components/cardForm-list/index.tsx
rename to src/components/form/cardForm-list/index.tsx
diff --git a/src/components/cardForm/cardForm.scss b/src/components/form/cardForm/cardForm.scss
similarity index 96%
rename from src/components/cardForm/cardForm.scss
rename to src/components/form/cardForm/cardForm.scss
index 6838cc7..be242be 100644
--- a/src/components/cardForm/cardForm.scss
+++ b/src/components/form/cardForm/cardForm.scss
@@ -1,4 +1,4 @@
-@import '../../scss/constants.scss';
+@import '../../../scss/constants.scss';
.card {
display: flex;
diff --git a/src/components/cardForm/cardForm.test.tsx b/src/components/form/cardForm/cardForm.test.tsx
similarity index 93%
rename from src/components/cardForm/cardForm.test.tsx
rename to src/components/form/cardForm/cardForm.test.tsx
index e1cda13..f404268 100644
--- a/src/components/cardForm/cardForm.test.tsx
+++ b/src/components/form/cardForm/cardForm.test.tsx
@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
-import CardForm from '../../components/cardForm/cardForm';
-import { testCardForm } from '../../testData/testCardForm';
+import CardForm from './cardForm';
+import { testCardForm } from '../../../testData/testCardForm';
describe('test CardForm component', () => {
test('CardForm renders', () => {
diff --git a/src/components/cardForm/cardForm.tsx b/src/components/form/cardForm/cardForm.tsx
similarity index 98%
rename from src/components/cardForm/cardForm.tsx
rename to src/components/form/cardForm/cardForm.tsx
index 4fca094..90ede5d 100644
--- a/src/components/cardForm/cardForm.tsx
+++ b/src/components/form/cardForm/cardForm.tsx
@@ -3,7 +3,7 @@ import React, { FC } from 'react';
import './cardForm.scss';
export interface ICardForm {
- id?: number;
+ id?: string;
userName: string;
gender: string;
birthday: string;
diff --git a/src/components/cardForm/index.tsx b/src/components/form/cardForm/index.tsx
similarity index 100%
rename from src/components/cardForm/index.tsx
rename to src/components/form/cardForm/index.tsx
diff --git a/src/components/form/form.test.tsx b/src/components/form/form.test.tsx
index cd4e2fd..b575db0 100644
--- a/src/components/form/form.test.tsx
+++ b/src/components/form/form.test.tsx
@@ -2,127 +2,63 @@ import { render, screen, fireEvent } from '@testing-library/react';
import Form from './form';
+jest.mock('react-redux');
+
describe('test Form component', () => {
test('render Form', () => {
- render(
- );
expect(screen.getByRole('form')).toBeInTheDocument();
});
test('should contain input type="text" field "userName"', () => {
- render(
- );
expect(screen.getByRole('user-name')).toBeInTheDocument();
});
test('value of input type="text" field "userName" changes by user', () => {
- render(
- );
const input = screen.getByRole('user-name') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'Anna' } });
expect(input.value).toBe('Anna');
});
test('should contain input type="date" field "date-birthday"', () => {
- render(
- );
expect(screen.getByRole('date-birthday')).toBeInTheDocument();
});
test('should contain 1 select name="country"', () => {
- render(
- );
expect(screen.getByRole('select-country')).toBeInTheDocument();
});
test('should contain select name="country" with 6 option field', () => {
- render(
- );
expect(screen.getAllByRole('option').length).toBe(6);
});
test('should contain 2 input type="radio" field name="gender"', () => {
- render(
- );
expect(screen.getAllByRole('radio').length).toBe(2);
});
test('should contain 1 input type="file" field name="profile"', () => {
- render(
- );
expect(screen.getByRole('profile')).toBeInTheDocument();
});
test('should contain 1 textarea name="feedback"', () => {
- render(
- );
expect(screen.getByRole('textarea')).toBeInTheDocument();
});
test('should contain 1 input type="checkbox" name="agree-consent-data"', () => {
- render(
- );
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
test('should contain 2 buttons: Submit and Reset', () => {
- render(
- );
expect(screen.getAllByRole('button').length).toBe(2);
});
});
diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx
index 3a5757c..2277887 100644
--- a/src/components/form/form.tsx
+++ b/src/components/form/form.tsx
@@ -2,12 +2,10 @@ import React, { FC, useState } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import './form.scss';
-import { ICardForm } from '../cardForm/cardForm';
+import { ICardForm } from './cardForm/cardForm';
import { errorTextMessage } from './form-utils';
-
-type FormProps = {
- addCardForm: (card: ICardForm) => void;
-};
+import { useAppDispatch } from '../../hook';
+import { addCardForm } from '../../store/formSlice';
type FormValues = {
userName: string;
@@ -19,7 +17,7 @@ type FormValues = {
gender: string;
};
-const Form: FC = (props: FormProps) => {
+const Form: FC = () => {
const {
register,
handleSubmit,
@@ -27,6 +25,8 @@ const Form: FC = (props: FormProps) => {
reset,
} = useForm({ mode: 'onSubmit' });
+ const dispatch = useAppDispatch();
+
const [success, setSuccess] = useState('');
const showSuccessMessage = () => {
@@ -40,9 +40,9 @@ const Form: FC = (props: FormProps) => {
const onSubmit: SubmitHandler = (data) => {
const { image, ...dataLast } = data;
const imageSrc: string = URL.createObjectURL(image[0]);
- const newCardForm: ICardForm = { imageSrc, ...dataLast };
-
- props.addCardForm(newCardForm);
+ const id = new Date().toISOString();
+ const newCardForm: ICardForm = { imageSrc, ...dataLast, id };
+ dispatch(addCardForm(newCardForm));
showSuccessMessage();
reset();
diff --git a/src/components/detailInfo/detailInfo.scss b/src/components/modal/detailInfo/detailInfo.scss
similarity index 86%
rename from src/components/detailInfo/detailInfo.scss
rename to src/components/modal/detailInfo/detailInfo.scss
index 583894d..800baa4 100644
--- a/src/components/detailInfo/detailInfo.scss
+++ b/src/components/modal/detailInfo/detailInfo.scss
@@ -1,4 +1,4 @@
-@import '../../scss/constants.scss';
+@import '../../../scss/constants.scss';
.detail-info {
&__title {
diff --git a/src/components/detailInfo/detailInfo.tsx b/src/components/modal/detailInfo/detailInfo.tsx
similarity index 74%
rename from src/components/detailInfo/detailInfo.tsx
rename to src/components/modal/detailInfo/detailInfo.tsx
index 4704c37..fcd13b2 100644
--- a/src/components/detailInfo/detailInfo.tsx
+++ b/src/components/modal/detailInfo/detailInfo.tsx
@@ -1,14 +1,16 @@
import React from 'react';
import './detailInfo.scss';
-import { IMovie } from '../types';
+import { IMovie } from '../../types';
type DetailInfoProps = {
info: IMovie | null;
};
-const DetailInfo = ({ info }: DetailInfoProps) => {
- const { overview, title, homepage } = info!;
+const DetailInfo: React.FC = ({ info }) => {
+ if (!info) return <>>;
+
+ const { overview, title, homepage } = info;
return (
diff --git a/src/components/detailInfo/index.tsx b/src/components/modal/detailInfo/index.tsx
similarity index 100%
rename from src/components/detailInfo/index.tsx
rename to src/components/modal/detailInfo/index.tsx
diff --git a/src/components/modal/modal.test.tsx b/src/components/modal/modal.test.tsx
new file mode 100644
index 0000000..e6feb97
--- /dev/null
+++ b/src/components/modal/modal.test.tsx
@@ -0,0 +1,54 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import Modal from './modal';
+
+import * as reduxHooks from 'react-redux';
+import * as actions from './../../store/moviesSlice';
+jest.mock('react-redux');
+const mockedUseDispatch = jest.spyOn(reduxHooks, 'useDispatch');
+
+describe('test Modal', () => {
+ it('render modal', () => {
+ render(
+
+ test
+
+ );
+ const modal = screen.getByTestId('modal');
+ expect(modal).toBeInTheDocument();
+ });
+
+ it('render Modal with "children" content', () => {
+ const children =
test
;
+ render(
{children});
+ const content = screen.getByText('test');
+ expect(content).toBeInTheDocument();
+ });
+
+ it('render close-modal button in Modal', () => {
+ render(
+
+ test
+
+ );
+ const closeButton = screen.getByTestId('close-modal');
+ expect(closeButton).toBeInTheDocument();
+ });
+
+ it('onClick by "close-modal" dispatch "toggleModal"', () => {
+ const dispatch = jest.fn();
+ mockedUseDispatch.mockReturnValue(dispatch);
+ const mockedToggleModal = jest.spyOn(actions, 'toggleModal');
+
+ render(
+
+ test
+
+ );
+
+ const closeButton = screen.getByTestId('close-modal');
+ fireEvent.click(closeButton);
+
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ expect(mockedToggleModal).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx
index ede58b2..0f76578 100644
--- a/src/components/modal/modal.tsx
+++ b/src/components/modal/modal.tsx
@@ -1,16 +1,18 @@
import React, { MouseEvent, ReactElement } from 'react';
+import { useAppDispatch } from '../../hook';
+import { toggleModal } from './../../store/moviesSlice';
import './modal.scss';
type ModalProps = {
- isModalOpen: boolean;
- setIsModalOpen: (newValue: boolean) => void;
children: ReactElement;
};
-const Modal = ({ setIsModalOpen, children }: ModalProps) => {
+const Modal = ({ children }: ModalProps) => {
+ const dispatch = useAppDispatch();
+
const closeModal = () => {
- setIsModalOpen(false);
+ dispatch(toggleModal());
};
const unCloseModal = (e: MouseEvent
): void => {
@@ -19,8 +21,8 @@ const Modal = ({ setIsModalOpen, children }: ModalProps) => {
return (
-
-
+
+
X
{children}
diff --git a/src/components/search-panel/search-panel.test.tsx b/src/components/search-panel/search-panel.test.tsx
index a9ce170..c65ada2 100644
--- a/src/components/search-panel/search-panel.test.tsx
+++ b/src/components/search-panel/search-panel.test.tsx
@@ -1,17 +1,28 @@
import { fireEvent, render, screen } from '@testing-library/react';
import SearchPanel from './search-panel';
+import * as reduxHooks from 'react-redux';
+
+jest.mock('react-redux');
+const mockedUseSelector = jest.spyOn(reduxHooks, 'useSelector');
+
describe('test SearchPanel component', () => {
- const onChange = jest.fn();
test('should contains input', () => {
- render(
);
+ render(
);
expect(screen.getByRole('search-input')).toBeInTheDocument();
});
test('value of input changes by user', () => {
- render(
);
+ render(
);
const input = screen.getByRole('search-input') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'some text for test' } });
expect(input.value).toBe('some text for test');
});
+
+ test('should contains input value from store', () => {
+ mockedUseSelector.mockReturnValue('hello');
+ render(
);
+ const input = screen.getByRole('search-input') as HTMLInputElement;
+ expect(input.value).toBe('hello');
+ });
});
diff --git a/src/components/search-panel/search-panel.tsx b/src/components/search-panel/search-panel.tsx
index b23ac70..2946c35 100644
--- a/src/components/search-panel/search-panel.tsx
+++ b/src/components/search-panel/search-panel.tsx
@@ -1,32 +1,22 @@
-import React, { ChangeEvent, KeyboardEvent, useState, useEffect, useRef } from 'react';
+import React, { ChangeEvent, KeyboardEvent, useRef } from 'react';
+import { useAppSelector, useAppDispatch } from './../../hook';
+import { updateSearchValue } from '../../store/searchSlice';
import './search-panel.scss';
-type SearchPanelProps = {
- updateSearchValue: (searchValue: string) => void;
-};
-
-const SearchPanel = ({ updateSearchValue }: SearchPanelProps) => {
- const initSearchValue: string = localStorage.getItem('searchValue') || '';
- const [searchValue, setSearchValue] = useState(initSearchValue);
+const SearchPanel: React.FC = () => {
+ const searchValue = useAppSelector((state) => state.searchReducer.searchValue);
+ const dispatch = useAppDispatch();
const searchRef = useRef
(searchValue);
- useEffect(() => {
- //like componentWillUnmount
- return function saveToLS() {
- localStorage.setItem('searchValue', searchRef.current);
- };
- }, []);
-
const onSearchChange = (e: ChangeEvent): void => {
const newSearchValue = e.target.value.trimStart();
- setSearchValue(newSearchValue);
searchRef.current = newSearchValue;
};
const handleKeyDown = (e: KeyboardEvent): void => {
if (e.key === 'Enter') {
- updateSearchValue(searchValue);
+ dispatch(updateSearchValue(searchRef.current));
}
};
@@ -37,7 +27,7 @@ const SearchPanel = ({ updateSearchValue }: SearchPanelProps) => {
type='text'
className='search-panel search-input btn--border'
placeholder={searchText}
- value={searchValue}
+ defaultValue={searchValue}
onChange={(e) => onSearchChange(e)}
onKeyDown={handleKeyDown}
role='search-input'
diff --git a/src/hook.ts b/src/hook.ts
new file mode 100644
index 0000000..8c4ab48
--- /dev/null
+++ b/src/hook.ts
@@ -0,0 +1,5 @@
+import { useSelector, useDispatch, TypedUseSelectorHook } from 'react-redux';
+import type { RootState, AppDispatch } from './store';
+
+export const useAppDispatch = () => useDispatch();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
diff --git a/src/index.tsx b/src/index.tsx
index 6832360..267eed8 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
+import { Provider } from 'react-redux';
+import store from './store';
import './index.scss';
import App from './components/app';
@@ -8,8 +10,10 @@ import App from './components/app';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
-
-
-
+
+
+
+
+
);
diff --git a/src/mocks/handlers.tsx b/src/mocks/handlers.tsx
index 5d589a7..f710af3 100644
--- a/src/mocks/handlers.tsx
+++ b/src/mocks/handlers.tsx
@@ -1,5 +1,5 @@
import { rest } from 'msw';
-import { dataMovie } from '../data/dataMovie';
+import { dataMovie } from '../testData/dataMovie';
export const handlers = [
rest.post('/login', (req, res, ctx) => {
diff --git a/src/pages/formPage/formPage.test.tsx b/src/pages/formPage/formPage.test.tsx
index be79cac..e88558a 100644
--- a/src/pages/formPage/formPage.test.tsx
+++ b/src/pages/formPage/formPage.test.tsx
@@ -2,20 +2,35 @@ import { render, screen } from '@testing-library/react';
import FormPage from './formPage';
+import * as reduxHooks from 'react-redux';
+import { testCardsForm } from '../../testData/testCardsForm';
+jest.mock('react-redux');
+const mockedUseSelector = jest.spyOn(reduxHooks, 'useSelector');
+
describe('test FormPage component', () => {
- test('renders FormPage', () => {
+ test('renders FormPage with empty state for CardFormList', () => {
+ mockedUseSelector.mockReturnValue([]);
render();
expect(screen.getByRole('form-page')).toBeInTheDocument();
});
test('renders Form on FormPage', () => {
+ mockedUseSelector.mockReturnValue([]);
render();
expect(screen.getByText(/Add feedback/i)).toBeInTheDocument();
expect(screen.getByRole('form')).toBeInTheDocument();
});
- test('render CardFormList on FormPage', () => {
+ test('render CardFormList on FormPage with empty state for CardFormList', () => {
+ mockedUseSelector.mockReturnValue([]);
+ render();
+ expect(screen.getByTestId('card-form-list')).toBeInTheDocument();
+ });
+
+ test('render CardFormList on FormPage with data testCardsForm', () => {
+ mockedUseSelector.mockReturnValue(testCardsForm);
render();
expect(screen.getByTestId('card-form-list')).toBeInTheDocument();
+ expect(screen.getAllByRole('card-form').length).toBe(testCardsForm.length);
});
});
diff --git a/src/pages/formPage/formPage.tsx b/src/pages/formPage/formPage.tsx
index 6139756..e3023be 100644
--- a/src/pages/formPage/formPage.tsx
+++ b/src/pages/formPage/formPage.tsx
@@ -1,46 +1,16 @@
-import React, { FC, useState } from 'react';
+import React, { FC } from 'react';
import './formPage.scss';
import Form from '../../components/form';
-import { ICardForm } from '../../components/cardForm/cardForm';
-import CardFormList from '../../components/cardForm-list';
+import CardFormList from '../../components/form/cardForm-list';
const FormPage: FC = () => {
- let cardId = 1;
- const [cardsForm, setCardsForm] = useState([]);
-
- const addCardForm = (card: ICardForm): void => {
- const newCard = createCard(card);
-
- setCardsForm((prevCardsForm) => {
- const newArr = [...prevCardsForm];
- newArr.push(newCard);
-
- return newArr;
- });
- };
-
- const createCard = (cardData: ICardForm) => {
- const { userName, gender, birthday, country, isConsentPersonalData, feedbackText, imageSrc } = cardData;
-
- return {
- userName: userName,
- gender: gender,
- birthday: birthday,
- country: country,
- isConsentPersonalData,
- feedbackText,
- imageSrc,
- id: cardId++,
- };
- };
-
return (
Form page
-
-
+
+
);
};
diff --git a/src/pages/homePage/home-page.test.tsx b/src/pages/homePage/home-page.test.tsx
index dc91e79..bdf4fa6 100644
--- a/src/pages/homePage/home-page.test.tsx
+++ b/src/pages/homePage/home-page.test.tsx
@@ -1,45 +1,50 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
-import { server } from './../../mocks/server';
-import { rest } from 'msw';
-
import HomePage from './homePage';
-import DetailInfo from '../../components/detailInfo';
-import { dataMovie } from '../../data/dataMovie';
+import DetailInfo from '../../components/modal/detailInfo';
+import { dataMovie } from '../../testData/dataMovie';
+
+import * as reduxHooks from 'react-redux';
+jest.mock('react-redux');
+const mockedUseSelector = jest.spyOn(reduxHooks, 'useSelector');
+const mockedUseDispatch = jest.spyOn(reduxHooks, 'useDispatch');
+
+import { initialState } from '../../store/moviesSlice';
describe('test HomePage component', () => {
- test('it renders', () => {
+ test('it renders with empty list movies', () => {
+ mockedUseSelector.mockReturnValue(initialState);
+ const dispatch = jest.fn();
+ mockedUseDispatch.mockReturnValue(dispatch);
+
render();
expect(screen.getByRole('home-page')).toBeInTheDocument();
});
test('show spinner component', async () => {
+ const dispatch = jest.fn();
+ mockedUseDispatch.mockReturnValue(dispatch);
+ mockedUseSelector.mockReturnValue(initialState);
+
const { findByTestId } = render();
expect(await findByTestId('spinner')).toBeInTheDocument();
});
- test('render cards from mocks API ', async () => {
- render();
- const expectedLength = 3; //length arrMovies in mock API in mocks/server.tsx
- const movies = await screen.findAllByRole('card-item');
- expect(movies).toHaveLength(expectedLength);
- });
-
- test('render error ', async () => {
- server.use(
- rest.get('https://api.themoviedb.org/3/trending/movie/week', (req, res, ctx) => {
- return res(ctx.status(500));
- })
- );
- render();
- const error = await screen.findByTestId('error-indicator');
- expect(error).toBeInTheDocument();
- });
-
test('render detail-info on Home Page ', async () => {
+ mockedUseSelector.mockReturnValue(dataMovie);
+
render();
const item = screen.getByTestId('detail-info');
expect(item).toBeInTheDocument();
});
+
+ test('dispatch actions called ', async () => {
+ mockedUseSelector.mockReturnValue(initialState);
+ const dispatch = jest.fn();
+ mockedUseDispatch.mockReturnValue(dispatch);
+
+ render();
+ expect(dispatch).toHaveBeenCalled();
+ });
});
diff --git a/src/pages/homePage/homePage.tsx b/src/pages/homePage/homePage.tsx
index 0f79631..a481182 100644
--- a/src/pages/homePage/homePage.tsx
+++ b/src/pages/homePage/homePage.tsx
@@ -1,90 +1,56 @@
-import React, { FC, useEffect, useState } from 'react';
+import React, { FC, useEffect } from 'react';
import './homePage.scss';
-import { IMovie } from '../../components/types';
import CardList from '../../components/cards-list';
import SearchPanel from '../../components/search-panel';
-import { getMovieById, getMoviesBySearch, getTrendingMovies } from '../../services/movies-services';
import Spinner from '../../components/spinner';
import ErrorIndicator from '../../components/errorIndicator';
import Modal from '../../components/modal';
-import DetailInfo from '../../components/detailInfo';
+import DetailInfo from '../../components/modal/detailInfo';
-const HomePage: FC = () => {
- const initSearchValue: string = localStorage.getItem('searchValue') || '';
- const [searchValue, setSearchValue] = useState(initSearchValue);
- const [trendingMovies, setTrendingMovies] = useState([]);
- const [movies, setMovies] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(false);
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [detailInfo, setDetailInfo] = useState(null);
- const [movieId, setMovieId] = useState(null);
+import { useAppDispatch, useAppSelector } from '../../hook';
+import { fetchTrendingMovies, fetchMoviesBySearch, fetchMovieById } from './../../store/moviesSlice';
- const onError = () => {
- setError(true);
- setLoading(false);
- };
+const HomePage: FC = () => {
+ const dispatch = useAppDispatch();
+ const searchValue = useAppSelector((state) => state.searchReducer.searchValue);
+ const { movies, trendingMovies, movieId, movieById, isModalOpen, loading, error } = useAppSelector(
+ (state) => state.moviesReducer
+ );
useEffect(() => {
- getTrendingMovies('week')
- .then((trendingMovies) => {
- setTrendingMovies(trendingMovies);
- setLoading(false);
- })
- .catch(onError);
- }, []);
+ dispatch(fetchTrendingMovies('week'));
+ }, [dispatch]);
useEffect(() => {
if (searchValue !== '') {
- getMoviesBySearch(searchValue)
- .then((movies) => {
- setMovies(movies);
- setLoading(false);
- })
- .catch(onError);
+ dispatch(fetchMoviesBySearch(searchValue));
}
- }, [searchValue]);
-
- const updateSearchValue = (newValue: string) => {
- setSearchValue(newValue);
- };
-
- function showDetailInfo(id: number) {
- setLoading(true);
- setMovieId(id);
- }
+ }, [dispatch, searchValue]);
useEffect(() => {
- if (isModalOpen && movieId) {
- getMovieById(movieId)
- .then((movie) => {
- setDetailInfo(movie);
- setLoading(false);
- })
- .catch(onError);
+ if (movieId) {
+ dispatch(fetchMovieById(movieId));
}
- }, [movieId, isModalOpen]);
+ }, [dispatch, movieId]);
const hasData = !(loading || error);
const errorMessage = error ? : null;
const spinner = loading ? : null;
const searchedMovies = searchValue !== '' ? movies : trendingMovies;
- const content = hasData ? (
-
- ) : null;
+ const content = hasData ? : null;
return (
HomePage
-
+
{spinner}
{errorMessage}
{content}
{isModalOpen && hasData && (
-
-
+
+
)}
diff --git a/src/services/api.test.tsx b/src/services/api.test.tsx
index 968910f..7bdaaf8 100644
--- a/src/services/api.test.tsx
+++ b/src/services/api.test.tsx
@@ -1,4 +1,4 @@
-import { getMovieById, getMoviesBySearch, getTrendingMovies } from './movies-services';
+import { getMovieById, getMoviesBySearch, getTrendingMovies, getResource } from './movies-services';
describe('test API component with mws', () => {
it('receives all requested data from Api "https://api.themoviedb.org/3/trending/movie/week" ', async () => {
@@ -23,4 +23,10 @@ describe('test API component with mws', () => {
const data = await getMoviesBySearch('avatar');
expect(data).toHaveLength(expectedLength);
});
+
+ it('should', async () => {
+ const testURL = '/movie/550';
+ const data = await getResource(testURL);
+ expect(data).toBeTruthy();
+ });
});
diff --git a/src/services/movies-services.tsx b/src/services/movies-services.tsx
index 4681415..155d458 100644
--- a/src/services/movies-services.tsx
+++ b/src/services/movies-services.tsx
@@ -5,7 +5,7 @@ const _apiKey = '75b017a3a227731c05610048a94948e5';
export const _baseImagePath = 'image.tmdb.org/t/p/w300';
const _lang = 'en-US';
-const getResource = async (url: string) => {
+export const getResource = async (url: string) => {
const res = await fetch(`${_apiBase}${url}?api_key=${_apiKey}`);
//handle all answers, except 200 ok
diff --git a/src/store/formSlice.ts b/src/store/formSlice.ts
new file mode 100644
index 0000000..d1f9a15
--- /dev/null
+++ b/src/store/formSlice.ts
@@ -0,0 +1,24 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { ICardForm } from '../components/form/cardForm/cardForm';
+
+type formState = {
+ cardsForm: ICardForm[];
+};
+
+const initialState: formState = {
+ cardsForm: [],
+};
+
+const formSlice = createSlice({
+ name: 'form',
+ initialState,
+ reducers: {
+ addCardForm(state, action: PayloadAction) {
+ state.cardsForm = [...state.cardsForm, action.payload];
+ },
+ },
+});
+
+export const { addCardForm } = formSlice.actions;
+
+export default formSlice.reducer;
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..9eacfab
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,17 @@
+import { configureStore } from '@reduxjs/toolkit';
+import searchReducer from './searchSlice';
+import moviesReducer from './moviesSlice';
+import formReducer from './formSlice';
+
+const store = configureStore({
+ reducer: {
+ searchReducer: searchReducer,
+ moviesReducer: moviesReducer,
+ formReducer: formReducer,
+ },
+});
+
+export default store;
+
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/store/moviesSlice.ts b/src/store/moviesSlice.ts
new file mode 100644
index 0000000..31c3d71
--- /dev/null
+++ b/src/store/moviesSlice.ts
@@ -0,0 +1,121 @@
+import { createSlice, PayloadAction, createAsyncThunk, AnyAction } from '@reduxjs/toolkit';
+import { IMovie } from '../components/types';
+import { getMovieById, getMoviesBySearch, getTrendingMovies } from '../services/movies-services';
+
+type timeByTrendingMovies = 'week' | 'day';
+
+export const fetchTrendingMovies = createAsyncThunk(
+ 'movies/fetchTrendingMovies',
+ async function (time, { rejectWithValue }) {
+ try {
+ const trendingMovies = await getTrendingMovies(time);
+ return trendingMovies;
+ } catch (err) {
+ console.log('error in fetchTrendingMovies: ', (err as Error).message);
+ return rejectWithValue('fetchTrendingMovies do not work!');
+ }
+ }
+);
+
+export const fetchMoviesBySearch = createAsyncThunk(
+ 'movies/fetchMoviesBySearch',
+ async function (searchValue, { rejectWithValue }) {
+ try {
+ const movies = await getMoviesBySearch(searchValue);
+ return movies;
+ } catch (err) {
+ console.log('error in fetchMoviesBySearch: ', (err as Error).message);
+ return rejectWithValue('fetchMoviesBySearch do not work!');
+ }
+ }
+);
+
+export const fetchMovieById = createAsyncThunk(
+ 'movies/fetchMovieById',
+ async function (movie_id, { rejectWithValue }) {
+ try {
+ const movieById = await getMovieById(movie_id);
+ return movieById;
+ } catch (err) {
+ console.log('error in fetchMovieById: ', (err as Error).message);
+ return rejectWithValue('fetchMovieById do not work!');
+ }
+ }
+);
+
+export type moviesState = {
+ movies: IMovie[];
+ trendingMovies: IMovie[];
+ movieId: number | null;
+ movieById: IMovie | null;
+ isModalOpen: boolean;
+ loading: boolean;
+ error: string | null;
+};
+
+export const initialState: moviesState = {
+ movies: [],
+ trendingMovies: [],
+ movieId: null,
+ movieById: null,
+ isModalOpen: false,
+ loading: true,
+ error: null,
+};
+
+const moviesSlice = createSlice({
+ name: 'movies',
+ initialState,
+ reducers: {
+ updateMovieId(state, action: PayloadAction) {
+ state.movieId = action.payload;
+ },
+ toggleModal(state) {
+ state.isModalOpen = !state.isModalOpen;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchTrendingMovies.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchTrendingMovies.fulfilled, (state, action) => {
+ state.trendingMovies = action.payload;
+ state.loading = false;
+ state.error = null;
+ })
+ .addCase(fetchMoviesBySearch.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchMoviesBySearch.fulfilled, (state, action) => {
+ state.movies = action.payload;
+ state.loading = false;
+ state.error = null;
+ })
+ .addCase(fetchMovieById.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchMovieById.fulfilled, (state, action) => {
+ state.movieById = action.payload;
+ state.loading = false;
+ state.error = null;
+ })
+ .addMatcher(isError, (state, action: PayloadAction) => {
+ state.error = action.payload;
+ state.loading = false;
+ state.isModalOpen = false;
+ console.log(state.error);
+ });
+ },
+});
+
+function isError(action: AnyAction) {
+ return action.type.endsWith('rejected');
+}
+
+export const { updateMovieId, toggleModal } = moviesSlice.actions;
+
+export default moviesSlice.reducer;
diff --git a/src/store/searchSlice.ts b/src/store/searchSlice.ts
new file mode 100644
index 0000000..d478b8f
--- /dev/null
+++ b/src/store/searchSlice.ts
@@ -0,0 +1,23 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+type searchState = {
+ searchValue: string;
+};
+
+const initialState: searchState = {
+ searchValue: '',
+};
+
+const searchSlice = createSlice({
+ name: 'search',
+ initialState,
+ reducers: {
+ updateSearchValue(state, action: PayloadAction) {
+ state.searchValue = action.payload;
+ },
+ },
+});
+
+export const { updateSearchValue } = searchSlice.actions;
+
+export default searchSlice.reducer;
diff --git a/src/store/tests/extraReducers.test.tsx b/src/store/tests/extraReducers.test.tsx
new file mode 100644
index 0000000..8ef555e
--- /dev/null
+++ b/src/store/tests/extraReducers.test.tsx
@@ -0,0 +1,60 @@
+import moviesReducer, { initialState, fetchTrendingMovies, fetchMoviesBySearch, fetchMovieById } from '../moviesSlice';
+import { dataMovie } from '../../testData/dataMovie';
+
+describe('test extraReducers', () => {
+ const mockData = [dataMovie];
+
+ it('should change status with "fetchTrendingMovies.pending" action', () => {
+ const state = moviesReducer(initialState, fetchTrendingMovies.pending('', 'week'));
+ expect(state.loading).toBe(true);
+ expect(state.error).toBeNull();
+ });
+
+ it('should fetch trendingMovies with "fetchTrendingMovies.fulfilled" action', () => {
+ const action = { type: fetchTrendingMovies.fulfilled.type, payload: mockData };
+ const state = moviesReducer(initialState, action);
+ expect(state.loading).toBe(false);
+ expect(state.error).toBeNull();
+ expect(state.trendingMovies).toBe(mockData);
+ });
+
+ it('should change status with "fetchMoviesBySearch.pending" action', () => {
+ const action = { type: fetchMoviesBySearch.pending.type };
+ const state = moviesReducer(initialState, action);
+ expect(state.loading).toBe(true);
+ expect(state.error).toBeNull();
+ });
+
+ it('should fetch movies with "fetchMoviesBySearch.fulfilled" action', () => {
+ const action = { type: fetchMoviesBySearch.fulfilled.type, payload: mockData };
+ const state = moviesReducer(initialState, action);
+ expect(state.loading).toBe(false);
+ expect(state.error).toBeNull();
+ expect(state.movies).toBe(mockData);
+ });
+
+ it('should return status with "fetchMovieById.pending" action', () => {
+ const action = { type: fetchMovieById.pending.type };
+ const state = moviesReducer(initialState, action);
+ expect(state.loading).toBe(true);
+ expect(state.error).toBeNull();
+ expect(state.movieById).toBeNull();
+ });
+
+ it('should fetch movieById with "fetchMovieById.fulfilled" action', () => {
+ const action = { type: fetchMovieById.fulfilled.type, payload: dataMovie };
+ const state = moviesReducer(initialState, action);
+ expect(state.loading).toBe(false);
+ expect(state.error).toBeNull();
+ expect(state.movieById).toBe(dataMovie);
+ expect(state.movieById?.title).toBe('Fight Club');
+ });
+
+ it('should update state.error with "fetchMovieById.rejected" action', () => {
+ const action = { type: fetchMovieById.rejected.type, payload: 'Server Error' };
+ const state = moviesReducer(initialState, action);
+ expect(state.loading).toBe(false);
+ expect(state.movieById).toBeNull();
+ expect(state.error).toBe('Server Error');
+ });
+});
diff --git a/src/store/tests/formSlice.test.tsx b/src/store/tests/formSlice.test.tsx
new file mode 100644
index 0000000..ffe06dd
--- /dev/null
+++ b/src/store/tests/formSlice.test.tsx
@@ -0,0 +1,21 @@
+import { testCardForm } from '../../testData/testCardForm';
+import formReducer, { addCardForm } from '../formSlice';
+
+const initialState = {
+ cardsForm: [],
+};
+
+describe('test formSlice', () => {
+ it('should return default state when passed an empty action', () => {
+ const result = formReducer(undefined, { type: '' });
+ expect(result).toEqual(initialState);
+ });
+
+ it('should add new cardForm with "addCardForm" action', () => {
+ const action = { type: addCardForm.type, payload: testCardForm };
+
+ const result = formReducer(initialState, action);
+ expect(result.cardsForm.length).toBe(1);
+ expect(result.cardsForm[0].userName).toBe('Anna Sh');
+ });
+});
diff --git a/src/store/tests/moviesSlice.test.tsx b/src/store/tests/moviesSlice.test.tsx
new file mode 100644
index 0000000..c707238
--- /dev/null
+++ b/src/store/tests/moviesSlice.test.tsx
@@ -0,0 +1,22 @@
+import moviesReducer, { updateMovieId, initialState, toggleModal } from '../moviesSlice';
+
+describe('test moviesSlice', () => {
+ it('should return default state when passed an empty action', () => {
+ const result = moviesReducer(undefined, { type: '' });
+ expect(result).toEqual(initialState);
+ });
+
+ it('should add movieId with "updateMovieId" action', () => {
+ const action = { type: updateMovieId.type, payload: 355 };
+
+ const result = moviesReducer(initialState, action);
+ expect(result.movieId).toBe(355);
+ });
+
+ it('should toggle status isModalOpen with "toggleModal" action', () => {
+ const action = { type: toggleModal.type };
+
+ const result = moviesReducer(initialState, action);
+ expect(result.isModalOpen).toBe(true);
+ });
+});
diff --git a/src/store/tests/searchSlice.test.tsx b/src/store/tests/searchSlice.test.tsx
new file mode 100644
index 0000000..caf257b
--- /dev/null
+++ b/src/store/tests/searchSlice.test.tsx
@@ -0,0 +1,19 @@
+import searchReducer, { updateSearchValue } from '../searchSlice';
+
+const initialState = {
+ searchValue: '',
+};
+
+describe('test searchSlice', () => {
+ it('should return default state when passed an empty action', () => {
+ const result = searchReducer(undefined, { type: '' });
+ expect(result).toEqual(initialState);
+ });
+
+ it('should add new search value with "updateSearchValue" action', () => {
+ const action = { type: updateSearchValue.type, payload: 'test value' };
+
+ const result = searchReducer(initialState, action);
+ expect(result.searchValue).toBe('test value');
+ });
+});
diff --git a/src/data/dataMovie.ts b/src/testData/dataMovie.ts
similarity index 100%
rename from src/data/dataMovie.ts
rename to src/testData/dataMovie.ts
diff --git a/src/data/dataProduct.ts b/src/testData/dataProduct.ts
similarity index 100%
rename from src/data/dataProduct.ts
rename to src/testData/dataProduct.ts
diff --git a/src/data/dataProducts.ts b/src/testData/dataProducts.ts
similarity index 100%
rename from src/data/dataProducts.ts
rename to src/testData/dataProducts.ts
diff --git a/src/testData/testCardForm.ts b/src/testData/testCardForm.ts
index 36f1695..abe4081 100644
--- a/src/testData/testCardForm.ts
+++ b/src/testData/testCardForm.ts
@@ -1,7 +1,7 @@
-import { ICardForm } from '../components/cardForm/cardForm';
+import { ICardForm } from '../components/form/cardForm/cardForm';
export const testCardForm: ICardForm = {
- id: 100,
+ id: '100',
userName: 'Anna Sh',
gender: 'female',
birthday: '27.10.1983',
diff --git a/src/testData/testCardsForm.ts b/src/testData/testCardsForm.ts
index 9be3cf5..711723c 100644
--- a/src/testData/testCardsForm.ts
+++ b/src/testData/testCardsForm.ts
@@ -1,8 +1,8 @@
-import { ICardForm } from '../components/cardForm/cardForm';
+import { ICardForm } from '../components/form/cardForm/cardForm';
export const testCardsForm: ICardForm[] = [
{
- id: 101,
+ id: '101',
userName: 'Ivan Bobrov',
gender: 'male',
birthday: '21.02.1980',
@@ -12,7 +12,7 @@ export const testCardsForm: ICardForm[] = [
imageSrc: 'https://i.dummyjson.com/data/products/25/thumbnail.jpg',
},
{
- id: 102,
+ id: '102',
userName: 'ALen Koch',
gender: 'female',
birthday: '21.02.1990',
@@ -22,7 +22,7 @@ export const testCardsForm: ICardForm[] = [
imageSrc: 'https://i.dummyjson.com/data/products/25/thumbnail.jpg',
},
{
- id: 103,
+ id: '103',
userName: 'Anna Sh',
gender: 'female',
birthday: '21.02.2222',