From 88dcd09622c852bc89d46f9d0d56ac45bf2e5462 Mon Sep 17 00:00:00 2001 From: avshir Date: Sat, 15 Apr 2023 16:47:26 +0200 Subject: [PATCH 01/13] feat: init redux, add redux, react-redux, @reduxjs/toolkit --- package-lock.json | 210 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + 2 files changed, 213 insertions(+) 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" From fd7fb3f6fe1ee967093d2fac61524988c11c6e5a Mon Sep 17 00:00:00 2001 From: avshir Date: Sat, 15 Apr 2023 16:47:26 +0200 Subject: [PATCH 02/13] init: react-redux, @reduxjs/toolkit --- package-lock.json | 209 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + 2 files changed, 211 insertions(+) diff --git a/package-lock.json b/package-lock.json index 030f70b..4cea336 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,6 +19,7 @@ "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", "sass": "^1.59.3", @@ -3166,6 +3168,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 +3873,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 +4080,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 +8896,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 +15462,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 +15670,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 +15812,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 +17553,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 +20715,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 +21218,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 +21425,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 +24910,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 +29451,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 +29591,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 +29706,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 +30973,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..e90e40d 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,6 +15,7 @@ "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", "sass": "^1.59.3", From 249f7951d8ee9d9221fed00b158e46ea1c408b69 Mon Sep 17 00:00:00 2001 From: avshir Date: Sun, 16 Apr 2023 01:27:22 +0200 Subject: [PATCH 03/13] feat: add store + TS, update SearchPanel component with store --- .../search-panel/search-panel.test.tsx | 5 ++-- src/components/search-panel/search-panel.tsx | 26 ++++++------------- src/hook.ts | 5 ++++ src/index.tsx | 10 ++++--- src/store/index.ts | 15 +++++++++++ src/store/searchSlice.ts | 23 ++++++++++++++++ 6 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 src/hook.ts create mode 100644 src/store/index.ts create mode 100644 src/store/searchSlice.ts diff --git a/src/components/search-panel/search-panel.test.tsx b/src/components/search-panel/search-panel.test.tsx index a9ce170..e424e2d 100644 --- a/src/components/search-panel/search-panel.test.tsx +++ b/src/components/search-panel/search-panel.test.tsx @@ -2,14 +2,13 @@ import { fireEvent, render, screen } from '@testing-library/react'; import SearchPanel from './search-panel'; 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'); 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/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..4bc8abc --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,15 @@ +import { configureStore } from '@reduxjs/toolkit'; +import searchReducer from './searchSlice'; +import moviesReducer from './moviesSlice'; + +const store = configureStore({ + reducer: { + searchReducer: searchReducer, + moviesReducer: moviesReducer, + }, +}); + +export default store; + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; 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; From dd7efc65dfb262b3dcf9c067a6e4192f0b645dca Mon Sep 17 00:00:00 2001 From: avshir Date: Sun, 16 Apr 2023 14:18:00 +0200 Subject: [PATCH 04/13] feat: create store for movies, moviesSlice, update HomePage, DetailInfo components --- .../{ => modal}/detailInfo/detailInfo.scss | 2 +- .../{ => modal}/detailInfo/detailInfo.tsx | 11 ++--- .../{ => modal}/detailInfo/index.tsx | 0 src/pages/homePage/homePage.tsx | 40 +++++++++---------- src/store/moviesSlice.ts | 39 ++++++++++++++++++ 5 files changed, 63 insertions(+), 29 deletions(-) rename src/components/{ => modal}/detailInfo/detailInfo.scss (86%) rename src/components/{ => modal}/detailInfo/detailInfo.tsx (68%) rename src/components/{ => modal}/detailInfo/index.tsx (100%) create mode 100644 src/store/moviesSlice.ts 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 68% rename from src/components/detailInfo/detailInfo.tsx rename to src/components/modal/detailInfo/detailInfo.tsx index 4704c37..051c253 100644 --- a/src/components/detailInfo/detailInfo.tsx +++ b/src/components/modal/detailInfo/detailInfo.tsx @@ -1,14 +1,11 @@ import React from 'react'; import './detailInfo.scss'; -import { IMovie } from '../types'; +import { useAppSelector } from '../../../hook'; -type DetailInfoProps = { - info: IMovie | null; -}; - -const DetailInfo = ({ info }: DetailInfoProps) => { - const { overview, title, homepage } = info!; +const DetailInfo: React.FC = () => { + const movieById = useAppSelector((state) => state.moviesReducer.movieById); + const { overview, title, homepage } = movieById!; 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/pages/homePage/homePage.tsx b/src/pages/homePage/homePage.tsx index 0f79631..16ebab1 100644 --- a/src/pages/homePage/homePage.tsx +++ b/src/pages/homePage/homePage.tsx @@ -1,25 +1,27 @@ import React, { FC, useEffect, useState } 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'; + +import { useAppDispatch, useAppSelector } from '../../hook'; +import { updateMovies, updateTrendingMovies, updateMovieId, updateMovieById } from './../../store/moviesSlice'; 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); + + const dispatch = useAppDispatch(); + const searchValue = useAppSelector((state) => state.searchReducer.searchValue); + const movies = useAppSelector((state) => state.moviesReducer.movies); + const trendingMovies = useAppSelector((state) => state.moviesReducer.trendingMovies); + const movieId = useAppSelector((state) => state.moviesReducer.movieId); const onError = () => { setError(true); @@ -29,42 +31,38 @@ const HomePage: FC = () => { useEffect(() => { getTrendingMovies('week') .then((trendingMovies) => { - setTrendingMovies(trendingMovies); + dispatch(updateTrendingMovies(trendingMovies)); setLoading(false); }) .catch(onError); - }, []); + }, [dispatch]); useEffect(() => { if (searchValue !== '') { getMoviesBySearch(searchValue) .then((movies) => { - setMovies(movies); + dispatch(updateMovies(movies)); setLoading(false); }) .catch(onError); } - }, [searchValue]); - - const updateSearchValue = (newValue: string) => { - setSearchValue(newValue); - }; + }, [dispatch, searchValue]); function showDetailInfo(id: number) { setLoading(true); - setMovieId(id); + dispatch(updateMovieId(id)); } useEffect(() => { if (isModalOpen && movieId) { getMovieById(movieId) .then((movie) => { - setDetailInfo(movie); + dispatch(updateMovieById(movie)); setLoading(false); }) .catch(onError); } - }, [movieId, isModalOpen]); + }, [movieId, isModalOpen, dispatch]); const hasData = !(loading || error); const errorMessage = error ? : null; @@ -78,13 +76,13 @@ const HomePage: FC = () => { return (

HomePage

- + {spinner} {errorMessage} {content} {isModalOpen && hasData && ( - + )}
diff --git a/src/store/moviesSlice.ts b/src/store/moviesSlice.ts new file mode 100644 index 0000000..935d4be --- /dev/null +++ b/src/store/moviesSlice.ts @@ -0,0 +1,39 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IMovie } from '../components/types'; + +type moviesState = { + movies: IMovie[]; + trendingMovies: IMovie[]; + movieId: number | null; + movieById: IMovie | null; +}; + +const initialState: moviesState = { + movies: [], + trendingMovies: [], + movieId: null, + movieById: null, +}; + +const moviesSlice = createSlice({ + name: 'movies', + initialState, + reducers: { + updateMovies(state, action: PayloadAction) { + state.movies = action.payload; + }, + updateTrendingMovies(state, action: PayloadAction) { + state.trendingMovies = action.payload; + }, + updateMovieId(state, action: PayloadAction) { + state.movieId = action.payload; + }, + updateMovieById(state, action: PayloadAction) { + state.movieById = action.payload; + }, + }, +}); + +export const { updateMovies, updateTrendingMovies, updateMovieId, updateMovieById } = moviesSlice.actions; + +export default moviesSlice.reducer; From 4d7d7f00eb0cf96a581149838e23762494dba7bf Mon Sep 17 00:00:00 2001 From: avshir Date: Sun, 16 Apr 2023 15:30:41 +0200 Subject: [PATCH 05/13] feat: create store for form, formSlice, update FormPage, CardForm, CardForm-list components --- .../cardForm-list/cardForm-list.tsx | 10 +++--- src/components/cardForm/cardForm.tsx | 2 +- src/components/form/form.tsx | 16 ++++----- src/pages/formPage/formPage.tsx | 36 ++----------------- src/store/formSlice.ts | 24 +++++++++++++ src/store/index.ts | 2 ++ 6 files changed, 42 insertions(+), 48 deletions(-) create mode 100644 src/store/formSlice.ts diff --git a/src/components/cardForm-list/cardForm-list.tsx b/src/components/cardForm-list/cardForm-list.tsx index 99d21ee..a43cc28 100644 --- a/src/components/cardForm-list/cardForm-list.tsx +++ b/src/components/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/cardForm.tsx b/src/components/cardForm/cardForm.tsx index 4fca094..90ede5d 100644 --- a/src/components/cardForm/cardForm.tsx +++ b/src/components/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/form/form.tsx b/src/components/form/form.tsx index 3a5757c..74be35d 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -4,10 +4,8 @@ import { useForm, SubmitHandler } from 'react-hook-form'; import './form.scss'; 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/pages/formPage/formPage.tsx b/src/pages/formPage/formPage.tsx index 6139756..5a068a8 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'; 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/store/formSlice.ts b/src/store/formSlice.ts new file mode 100644 index 0000000..9bd65ba --- /dev/null +++ b/src/store/formSlice.ts @@ -0,0 +1,24 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { ICardForm } from '../components/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 index 4bc8abc..9eacfab 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,11 +1,13 @@ 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, }, }); From 49bca536cb06ffe90fd97c3082268bf43817c9b4 Mon Sep 17 00:00:00 2001 From: avshir Date: Sun, 16 Apr 2023 19:47:05 +0200 Subject: [PATCH 06/13] refactor: change structure files of the project - testData and components with form --- src/components/card/card.test.tsx | 2 +- src/components/cards-list/cards-list.test.tsx | 2 +- src/components/{ => form}/cardForm-list/cardForm-list.scss | 0 .../{ => form}/cardForm-list/cardForm-list.test.tsx | 2 +- src/components/{ => form}/cardForm-list/cardForm-list.tsx | 2 +- src/components/{ => form}/cardForm-list/index.tsx | 0 src/components/{ => form}/cardForm/cardForm.scss | 0 src/components/{ => form}/cardForm/cardForm.test.tsx | 4 ++-- src/components/{ => form}/cardForm/cardForm.tsx | 0 src/components/{ => form}/cardForm/index.tsx | 0 src/components/form/form.tsx | 2 +- src/mocks/handlers.tsx | 2 +- src/pages/formPage/formPage.tsx | 2 +- src/store/formSlice.ts | 2 +- src/{data => testData}/dataMovie.ts | 0 src/{data => testData}/dataProduct.ts | 0 src/{data => testData}/dataProducts.ts | 0 17 files changed, 10 insertions(+), 10 deletions(-) rename src/components/{ => form}/cardForm-list/cardForm-list.scss (100%) rename src/components/{ => form}/cardForm-list/cardForm-list.test.tsx (89%) rename src/components/{ => form}/cardForm-list/cardForm-list.tsx (91%) rename src/components/{ => form}/cardForm-list/index.tsx (100%) rename src/components/{ => form}/cardForm/cardForm.scss (100%) rename src/components/{ => form}/cardForm/cardForm.test.tsx (93%) rename src/components/{ => form}/cardForm/cardForm.tsx (100%) rename src/components/{ => form}/cardForm/index.tsx (100%) rename src/{data => testData}/dataMovie.ts (100%) rename src/{data => testData}/dataProduct.ts (100%) rename src/{data => testData}/dataProducts.ts (100%) diff --git a/src/components/card/card.test.tsx b/src/components/card/card.test.tsx index 719ac41..2d9b7f8 100644 --- a/src/components/card/card.test.tsx +++ b/src/components/card/card.test.tsx @@ -1,7 +1,7 @@ import { render, screen } 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(); diff --git a/src/components/cards-list/cards-list.test.tsx b/src/components/cards-list/cards-list.test.tsx index 9e85e30..e693ebc 100644 --- a/src/components/cards-list/cards-list.test.tsx +++ b/src/components/cards-list/cards-list.test.tsx @@ -2,7 +2,7 @@ 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'; describe('test CardsList component', () => { 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/cardForm-list/cardForm-list.test.tsx b/src/components/form/cardForm-list/cardForm-list.test.tsx similarity index 89% rename from src/components/cardForm-list/cardForm-list.test.tsx rename to src/components/form/cardForm-list/cardForm-list.test.tsx index 9108865..04312d6 100644 --- a/src/components/cardForm-list/cardForm-list.test.tsx +++ b/src/components/form/cardForm-list/cardForm-list.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; -import { testCardsForm } from '../../testData/testCardsForm'; +import { testCardsForm } from '../../../testData/testCardsForm'; import CardFormList from './cardForm-list'; describe('test CardFormList component', () => { diff --git a/src/components/cardForm-list/cardForm-list.tsx b/src/components/form/cardForm-list/cardForm-list.tsx similarity index 91% rename from src/components/cardForm-list/cardForm-list.tsx rename to src/components/form/cardForm-list/cardForm-list.tsx index a43cc28..f81ea27 100644 --- a/src/components/cardForm-list/cardForm-list.tsx +++ b/src/components/form/cardForm-list/cardForm-list.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import CardForm from '../cardForm'; -import { useAppSelector } from '../../hook'; +import { useAppSelector } from '../../../hook'; import './cardForm-list.scss'; 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 100% rename from src/components/cardForm/cardForm.scss rename to src/components/form/cardForm/cardForm.scss 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 100% rename from src/components/cardForm/cardForm.tsx rename to src/components/form/cardForm/cardForm.tsx 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.tsx b/src/components/form/form.tsx index 74be35d..2277887 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -2,7 +2,7 @@ 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'; import { useAppDispatch } from '../../hook'; import { addCardForm } from '../../store/formSlice'; 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.tsx b/src/pages/formPage/formPage.tsx index 5a068a8..e3023be 100644 --- a/src/pages/formPage/formPage.tsx +++ b/src/pages/formPage/formPage.tsx @@ -3,7 +3,7 @@ import React, { FC } from 'react'; import './formPage.scss'; import Form from '../../components/form'; -import CardFormList from '../../components/cardForm-list'; +import CardFormList from '../../components/form/cardForm-list'; const FormPage: FC = () => { return ( diff --git a/src/store/formSlice.ts b/src/store/formSlice.ts index 9bd65ba..d1f9a15 100644 --- a/src/store/formSlice.ts +++ b/src/store/formSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { ICardForm } from '../components/cardForm/cardForm'; +import { ICardForm } from '../components/form/cardForm/cardForm'; type formState = { cardsForm: ICardForm[]; 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 From 8b9fb588b0837a969ac4e54871caf901ced0790d Mon Sep 17 00:00:00 2001 From: avshir Date: Sun, 16 Apr 2023 20:07:58 +0200 Subject: [PATCH 07/13] feat: add tests for redux store --- src/store/moviesSlice.ts | 2 +- src/store/tests/formSlice.test.tsx | 21 +++++++++++ src/store/tests/moviesSlice.test.tsx | 53 ++++++++++++++++++++++++++++ src/store/tests/searchSlice.test.tsx | 19 ++++++++++ src/testData/testCardForm.ts | 4 +-- src/testData/testCardsForm.ts | 8 ++--- 6 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 src/store/tests/formSlice.test.tsx create mode 100644 src/store/tests/moviesSlice.test.tsx create mode 100644 src/store/tests/searchSlice.test.tsx diff --git a/src/store/moviesSlice.ts b/src/store/moviesSlice.ts index 935d4be..70b4ac3 100644 --- a/src/store/moviesSlice.ts +++ b/src/store/moviesSlice.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { IMovie } from '../components/types'; -type moviesState = { +export type moviesState = { movies: IMovie[]; trendingMovies: IMovie[]; movieId: number | null; 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..c74bc29 --- /dev/null +++ b/src/store/tests/moviesSlice.test.tsx @@ -0,0 +1,53 @@ +import { dataMovie } from '../../testData/dataMovie'; +import moviesReducer, { + updateMovies, + updateTrendingMovies, + updateMovieId, + updateMovieById, + moviesState, +} from '../moviesSlice'; + +const initialState: moviesState = { + movies: [], + trendingMovies: [], + movieId: null, + movieById: null, +}; + +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 movies with "updateMovies" action', () => { + const action = { type: updateMovies.type, payload: [dataMovie] }; + + const result = moviesReducer(initialState, action); + expect(result.movies.length).toBe(1); + expect(result.movies[0].title).toBe('Fight Club'); + }); + + it('should add trendingMovies with "updateTrendingMovies" action', () => { + const action = { type: updateTrendingMovies.type, payload: [dataMovie] }; + + const result = moviesReducer(initialState, action); + expect(result.trendingMovies.length).toBe(1); + expect(result.trendingMovies[0].id).toBe(550); + }); + + 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 add movie by id with "updateMovieById" action', () => { + const action = { type: updateMovieById.type, payload: dataMovie }; + + const result = moviesReducer(initialState, action); + expect(result.movieById).toBe(dataMovie); + expect(result.movieById?.title).toBe('Fight Club'); + }); +}); 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/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', From 6349fb87ce3a349721f18ca759b4fd50fcde5d5c Mon Sep 17 00:00:00 2001 From: avshir Date: Mon, 17 Apr 2023 01:21:22 +0200 Subject: [PATCH 08/13] feat: add test for Modal Component --- src/components/modal/modal.test.tsx | 37 +++++++++++++++++++++++++++++ src/components/modal/modal.tsx | 4 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/components/modal/modal.test.tsx diff --git a/src/components/modal/modal.test.tsx b/src/components/modal/modal.test.tsx new file mode 100644 index 0000000..c493cf4 --- /dev/null +++ b/src/components/modal/modal.test.tsx @@ -0,0 +1,37 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import Modal from './modal'; + +const foo = jest.fn(); + +describe('test Modal', () => { + it('render modal', () => { + render( + +

test

+
+ ); + const modal = screen.getByTestId('modal'); + expect(modal).toBeInTheDocument(); + }); + + it('render close-modal button in Modal', () => { + render( + +

test

+
+ ); + const closeButton = screen.getByTestId('close-modal'); + expect(closeButton).toBeInTheDocument(); + }); + + it('onClick by "close-modal" call function ', () => { + render( + +

test

+
+ ); + const closeButton = screen.getByTestId('close-modal'); + fireEvent.click(closeButton); + expect(foo).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx index ede58b2..5d912b4 100644 --- a/src/components/modal/modal.tsx +++ b/src/components/modal/modal.tsx @@ -19,8 +19,8 @@ const Modal = ({ setIsModalOpen, children }: ModalProps) => { return (
-
-
+
+
X
{children} From dc1ffb8b2c34c98f921ee339869e3e576d2076be Mon Sep 17 00:00:00 2001 From: avshir Date: Mon, 17 Apr 2023 01:42:41 +0200 Subject: [PATCH 09/13] feat: update tests use redux --- src/components/app/App.test.tsx | 2 + .../form/cardForm-list/cardForm-list.test.tsx | 14 ++- src/components/form/cardForm/cardForm.scss | 2 +- src/components/form/form.test.tsx | 90 +++---------------- .../search-panel/search-panel.test.tsx | 12 +++ src/pages/formPage/formPage.test.tsx | 19 +++- src/pages/homePage/home-page.test.tsx | 29 +++--- 7 files changed, 73 insertions(+), 95 deletions(-) diff --git a/src/components/app/App.test.tsx b/src/components/app/App.test.tsx index 8ce39e0..f6383b0 100644 --- a/src/components/app/App.test.tsx +++ b/src/components/app/App.test.tsx @@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react'; import App from './App'; import { MemoryRouter } from 'react-router-dom'; +jest.mock('react-redux'); + describe('test App component', () => { test('it renders', () => { render( diff --git a/src/components/form/cardForm-list/cardForm-list.test.tsx b/src/components/form/cardForm-list/cardForm-list.test.tsx index 04312d6..1efbf9e 100644 --- a/src/components/form/cardForm-list/cardForm-list.test.tsx +++ b/src/components/form/cardForm-list/cardForm-list.test.tsx @@ -1,16 +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('render CardFormList', () => { - render(); + test('should create CardFormList with empty list', () => { + mockedUseSelector.mockReturnValue([]); + render(); expect(screen.getByTestId('card-form-list')).toBeInTheDocument(); }); - test('should be render all CardsForm in CardFormList ', () => { - render(); + 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/form/cardForm/cardForm.scss b/src/components/form/cardForm/cardForm.scss index 6838cc7..be242be 100644 --- a/src/components/form/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/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( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getByRole('form')).toBeInTheDocument(); }); test('should contain input type="text" field "userName"', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getByRole('user-name')).toBeInTheDocument(); }); test('value of input type="text" field "userName" changes by user', () => { - render( - { - throw new Error(); - }} - /> - ); + 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( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getByRole('date-birthday')).toBeInTheDocument(); }); test('should contain 1 select name="country"', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getByRole('select-country')).toBeInTheDocument(); }); test('should contain select name="country" with 6 option field', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getAllByRole('option').length).toBe(6); }); test('should contain 2 input type="radio" field name="gender"', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getAllByRole('radio').length).toBe(2); }); test('should contain 1 input type="file" field name="profile"', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getByRole('profile')).toBeInTheDocument(); }); test('should contain 1 textarea name="feedback"', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getByRole('textarea')).toBeInTheDocument(); }); test('should contain 1 input type="checkbox" name="agree-consent-data"', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getByRole('checkbox')).toBeInTheDocument(); }); test('should contain 2 buttons: Submit and Reset', () => { - render( - { - throw new Error(); - }} - /> - ); + render(); expect(screen.getAllByRole('button').length).toBe(2); }); }); diff --git a/src/components/search-panel/search-panel.test.tsx b/src/components/search-panel/search-panel.test.tsx index e424e2d..c65ada2 100644 --- a/src/components/search-panel/search-panel.test.tsx +++ b/src/components/search-panel/search-panel.test.tsx @@ -1,6 +1,11 @@ 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', () => { test('should contains input', () => { render(); @@ -13,4 +18,11 @@ describe('test SearchPanel component', () => { 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/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/homePage/home-page.test.tsx b/src/pages/homePage/home-page.test.tsx index dc91e79..2f70a1d 100644 --- a/src/pages/homePage/home-page.test.tsx +++ b/src/pages/homePage/home-page.test.tsx @@ -5,11 +5,24 @@ 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'); +import { moviesState } from '../../store/moviesSlice'; + +const initialState: moviesState = { + movies: [], + trendingMovies: [], + movieId: null, + movieById: null, +}; describe('test HomePage component', () => { - test('it renders', () => { + test('it renders with empty list movies', () => { + mockedUseSelector.mockReturnValue(initialState); render(); expect(screen.getByRole('home-page')).toBeInTheDocument(); }); @@ -19,13 +32,6 @@ describe('test HomePage component', () => { 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) => { @@ -38,7 +44,8 @@ describe('test HomePage component', () => { }); test('render detail-info on Home Page ', async () => { - render(); + mockedUseSelector.mockReturnValue(dataMovie); + render(); const item = screen.getByTestId('detail-info'); expect(item).toBeInTheDocument(); }); From 90da3f7df286154febcd8b26fe1d14fec3d6b055 Mon Sep 17 00:00:00 2001 From: avshir Date: Mon, 17 Apr 2023 01:44:02 +0200 Subject: [PATCH 10/13] feat: add test for movies-services.tsx --- src/services/api.test.tsx | 8 +++++++- src/services/movies-services.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) 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 From e7e3d7b8e3b89b222dea5346efb5e72d5199e516 Mon Sep 17 00:00:00 2001 From: avshir Date: Tue, 18 Apr 2023 18:17:58 +0200 Subject: [PATCH 11/13] feat: apply thunks for all requests to API --- src/components/card/card.tsx | 13 ++- src/components/cards-list/cards-list.tsx | 6 +- .../modal/detailInfo/detailInfo.tsx | 13 ++- src/components/modal/modal.tsx | 10 +- src/pages/homePage/homePage.tsx | 58 +++------- src/store/moviesSlice.ts | 104 ++++++++++++++++-- 6 files changed, 131 insertions(+), 73 deletions(-) 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/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/modal/detailInfo/detailInfo.tsx b/src/components/modal/detailInfo/detailInfo.tsx index 051c253..fcd13b2 100644 --- a/src/components/modal/detailInfo/detailInfo.tsx +++ b/src/components/modal/detailInfo/detailInfo.tsx @@ -1,11 +1,16 @@ import React from 'react'; import './detailInfo.scss'; -import { useAppSelector } from '../../../hook'; +import { IMovie } from '../../types'; -const DetailInfo: React.FC = () => { - const movieById = useAppSelector((state) => state.moviesReducer.movieById); - const { overview, title, homepage } = movieById!; +type DetailInfoProps = { + info: IMovie | null; +}; + +const DetailInfo: React.FC = ({ info }) => { + if (!info) return <>; + + const { overview, title, homepage } = info; return (
    diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx index 5d912b4..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 => { diff --git a/src/pages/homePage/homePage.tsx b/src/pages/homePage/homePage.tsx index 16ebab1..a481182 100644 --- a/src/pages/homePage/homePage.tsx +++ b/src/pages/homePage/homePage.tsx @@ -1,77 +1,45 @@ -import React, { FC, useEffect, useState } from 'react'; +import React, { FC, useEffect } from 'react'; import './homePage.scss'; 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/modal/detailInfo'; import { useAppDispatch, useAppSelector } from '../../hook'; -import { updateMovies, updateTrendingMovies, updateMovieId, updateMovieById } from './../../store/moviesSlice'; +import { fetchTrendingMovies, fetchMoviesBySearch, fetchMovieById } from './../../store/moviesSlice'; const HomePage: FC = () => { - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - const dispatch = useAppDispatch(); const searchValue = useAppSelector((state) => state.searchReducer.searchValue); - const movies = useAppSelector((state) => state.moviesReducer.movies); - const trendingMovies = useAppSelector((state) => state.moviesReducer.trendingMovies); - const movieId = useAppSelector((state) => state.moviesReducer.movieId); - - const onError = () => { - setError(true); - setLoading(false); - }; + const { movies, trendingMovies, movieId, movieById, isModalOpen, loading, error } = useAppSelector( + (state) => state.moviesReducer + ); useEffect(() => { - getTrendingMovies('week') - .then((trendingMovies) => { - dispatch(updateTrendingMovies(trendingMovies)); - setLoading(false); - }) - .catch(onError); + dispatch(fetchTrendingMovies('week')); }, [dispatch]); useEffect(() => { if (searchValue !== '') { - getMoviesBySearch(searchValue) - .then((movies) => { - dispatch(updateMovies(movies)); - setLoading(false); - }) - .catch(onError); + dispatch(fetchMoviesBySearch(searchValue)); } }, [dispatch, searchValue]); - function showDetailInfo(id: number) { - setLoading(true); - dispatch(updateMovieId(id)); - } - useEffect(() => { - if (isModalOpen && movieId) { - getMovieById(movieId) - .then((movie) => { - dispatch(updateMovieById(movie)); - setLoading(false); - }) - .catch(onError); + if (movieId) { + dispatch(fetchMovieById(movieId)); } - }, [movieId, isModalOpen, dispatch]); + }, [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 (
    @@ -81,8 +49,8 @@ const HomePage: FC = () => { {errorMessage} {content} {isModalOpen && hasData && ( - - + + )}
    diff --git a/src/store/moviesSlice.ts b/src/store/moviesSlice.ts index 70b4ac3..31c3d71 100644 --- a/src/store/moviesSlice.ts +++ b/src/store/moviesSlice.ts @@ -1,39 +1,121 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +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; }; -const initialState: moviesState = { +export const initialState: moviesState = { movies: [], trendingMovies: [], movieId: null, movieById: null, + isModalOpen: false, + loading: true, + error: null, }; const moviesSlice = createSlice({ name: 'movies', initialState, reducers: { - updateMovies(state, action: PayloadAction) { - state.movies = action.payload; - }, - updateTrendingMovies(state, action: PayloadAction) { - state.trendingMovies = action.payload; - }, updateMovieId(state, action: PayloadAction) { state.movieId = action.payload; }, - updateMovieById(state, action: PayloadAction) { - state.movieById = 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); + }); + }, }); -export const { updateMovies, updateTrendingMovies, updateMovieId, updateMovieById } = moviesSlice.actions; +function isError(action: AnyAction) { + return action.type.endsWith('rejected'); +} + +export const { updateMovieId, toggleModal } = moviesSlice.actions; export default moviesSlice.reducer; From 37da3592d58eeaec699779d20d5233a65afe8921 Mon Sep 17 00:00:00 2001 From: avshir Date: Tue, 18 Apr 2023 18:19:42 +0200 Subject: [PATCH 12/13] feat: update test after apply thunks requests --- src/components/app/App.test.tsx | 9 +++ src/components/app/App.tsx | 2 +- src/components/card/card.test.tsx | 31 ++++++++-- src/components/cards-list/cards-list.test.tsx | 9 +-- .../errorIndicator/errorIndicator.test.tsx | 10 ++++ src/components/modal/modal.test.tsx | 29 +++++++-- src/pages/homePage/home-page.test.tsx | 42 +++++++------ src/store/tests/extraReducers.test.tsx | 60 +++++++++++++++++++ src/store/tests/moviesSlice.test.tsx | 39 ++---------- 9 files changed, 158 insertions(+), 73 deletions(-) create mode 100644 src/components/errorIndicator/errorIndicator.test.tsx create mode 100644 src/store/tests/extraReducers.test.tsx diff --git a/src/components/app/App.test.tsx b/src/components/app/App.test.tsx index f6383b0..1c0961a 100644 --- a/src/components/app/App.test.tsx +++ b/src/components/app/App.test.tsx @@ -3,15 +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 2d9b7f8..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 '../../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/cards-list/cards-list.test.tsx b/src/components/cards-list/cards-list.test.tsx index e693ebc..850939e 100644 --- a/src/components/cards-list/cards-list.test.tsx +++ b/src/components/cards-list/cards-list.test.tsx @@ -5,19 +5,20 @@ import CardsList from './cards-list'; 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/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/modal/modal.test.tsx b/src/components/modal/modal.test.tsx index c493cf4..e6feb97 100644 --- a/src/components/modal/modal.test.tsx +++ b/src/components/modal/modal.test.tsx @@ -1,12 +1,15 @@ import { fireEvent, render, screen } from '@testing-library/react'; import Modal from './modal'; -const foo = 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 Modal', () => { it('render modal', () => { render( - +

    test

    ); @@ -14,9 +17,16 @@ describe('test 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

    ); @@ -24,14 +34,21 @@ describe('test Modal', () => { expect(closeButton).toBeInTheDocument(); }); - it('onClick by "close-modal" call function ', () => { + 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(foo).toHaveBeenCalledTimes(1); + + expect(dispatch).toHaveBeenCalledTimes(1); + expect(mockedToggleModal).toHaveBeenCalledTimes(1); }); }); diff --git a/src/pages/homePage/home-page.test.tsx b/src/pages/homePage/home-page.test.tsx index 2f70a1d..19ced6c 100644 --- a/src/pages/homePage/home-page.test.tsx +++ b/src/pages/homePage/home-page.test.tsx @@ -1,9 +1,6 @@ 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/modal/detailInfo'; import { dataMovie } from '../../testData/dataMovie'; @@ -11,42 +8,43 @@ import { dataMovie } from '../../testData/dataMovie'; import * as reduxHooks from 'react-redux'; jest.mock('react-redux'); const mockedUseSelector = jest.spyOn(reduxHooks, 'useSelector'); -import { moviesState } from '../../store/moviesSlice'; +const mockedUseDispatch = jest.spyOn(reduxHooks, 'useDispatch'); -const initialState: moviesState = { - movies: [], - trendingMovies: [], - movieId: null, - movieById: null, -}; +import { initialState } from '../../store/moviesSlice'; describe('test HomePage component', () => { test('it renders with empty list movies', () => { + const dispatch = jest.fn(); + mockedUseDispatch.mockReturnValue(dispatch); mockedUseSelector.mockReturnValue(initialState); + 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 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(); + + 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/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/moviesSlice.test.tsx b/src/store/tests/moviesSlice.test.tsx index c74bc29..c707238 100644 --- a/src/store/tests/moviesSlice.test.tsx +++ b/src/store/tests/moviesSlice.test.tsx @@ -1,18 +1,4 @@ -import { dataMovie } from '../../testData/dataMovie'; -import moviesReducer, { - updateMovies, - updateTrendingMovies, - updateMovieId, - updateMovieById, - moviesState, -} from '../moviesSlice'; - -const initialState: moviesState = { - movies: [], - trendingMovies: [], - movieId: null, - movieById: null, -}; +import moviesReducer, { updateMovieId, initialState, toggleModal } from '../moviesSlice'; describe('test moviesSlice', () => { it('should return default state when passed an empty action', () => { @@ -20,22 +6,6 @@ describe('test moviesSlice', () => { expect(result).toEqual(initialState); }); - it('should add movies with "updateMovies" action', () => { - const action = { type: updateMovies.type, payload: [dataMovie] }; - - const result = moviesReducer(initialState, action); - expect(result.movies.length).toBe(1); - expect(result.movies[0].title).toBe('Fight Club'); - }); - - it('should add trendingMovies with "updateTrendingMovies" action', () => { - const action = { type: updateTrendingMovies.type, payload: [dataMovie] }; - - const result = moviesReducer(initialState, action); - expect(result.trendingMovies.length).toBe(1); - expect(result.trendingMovies[0].id).toBe(550); - }); - it('should add movieId with "updateMovieId" action', () => { const action = { type: updateMovieId.type, payload: 355 }; @@ -43,11 +13,10 @@ describe('test moviesSlice', () => { expect(result.movieId).toBe(355); }); - it('should add movie by id with "updateMovieById" action', () => { - const action = { type: updateMovieById.type, payload: dataMovie }; + it('should toggle status isModalOpen with "toggleModal" action', () => { + const action = { type: toggleModal.type }; const result = moviesReducer(initialState, action); - expect(result.movieById).toBe(dataMovie); - expect(result.movieById?.title).toBe('Fight Club'); + expect(result.isModalOpen).toBe(true); }); }); From 3ce1d913823e2a8bbd9e0e46643114443a125728 Mon Sep 17 00:00:00 2001 From: avshir Date: Sat, 29 Apr 2023 18:23:10 +0200 Subject: [PATCH 13/13] fix: test --- src/pages/homePage/home-page.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/homePage/home-page.test.tsx b/src/pages/homePage/home-page.test.tsx index 19ced6c..bdf4fa6 100644 --- a/src/pages/homePage/home-page.test.tsx +++ b/src/pages/homePage/home-page.test.tsx @@ -14,9 +14,9 @@ import { initialState } from '../../store/moviesSlice'; describe('test HomePage component', () => { test('it renders with empty list movies', () => { + mockedUseSelector.mockReturnValue(initialState); const dispatch = jest.fn(); mockedUseDispatch.mockReturnValue(dispatch); - mockedUseSelector.mockReturnValue(initialState); render(); expect(screen.getByRole('home-page')).toBeInTheDocument();