From a9f799fb2fa94f5e40bb28ca333c9faee2662fc9 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 22:15:51 +0300 Subject: [PATCH 01/16] Install react-router-dom --- package-lock.json | 83 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b13fb598..470d4bba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11550,6 +11550,19 @@ "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==", "dev": true }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -15493,6 +15506,15 @@ "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=", "dev": true }, + "mini-create-react-context": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz", + "integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==", + "requires": { + "@babel/runtime": "^7.5.5", + "tiny-warning": "^1.0.3" + } + }, "mini-css-extract-plugin": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.7.0.tgz", @@ -18063,6 +18085,52 @@ } } }, + "react-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, "react-select": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.0.tgz", @@ -18821,6 +18889,11 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -20648,6 +20721,11 @@ "dev": true, "optional": true }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -21559,6 +21637,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index c77f4f3f..5141a49b 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ "@emotion/styled": "^10.0.27", "formik": "^2.1.4", "react": "^16.13.1", - "react-dom": "^16.13.1" + "react-dom": "^16.13.1", + "react-router-dom": "^5.2.0" }, "loki": { "configurations": { From 7405399bd6e35bdc69525b5c70734441850f4bac Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 22:23:36 +0300 Subject: [PATCH 02/16] Setup root alias --- tsconfig.json | 3 ++- webpack.config.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index e640fc02..045de2c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "baseUrl": "./", "paths": { "types/*": ["src/types/*"], - "components/*": ["src/components/*"] + "components/*": ["src/components/*"], + "@/*": ["src/*"] }, "target": "es5", "module": "commonjs", diff --git a/webpack.config.js b/webpack.config.js index 76195f35..494af939 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,6 +11,7 @@ module.exports = { alias: { types: path.resolve(__dirname, "src/types"), components: path.resolve(__dirname, "src/components"), + "@": path.resolve(__dirname, "src"), }, }, output: { From b13dc279256aeb1fc12f493fac45281511d7b7e4 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 22:24:03 +0300 Subject: [PATCH 03/16] Create App file --- src/App.tsx | 3 +++ src/index.tsx | 13 ++----------- 2 files changed, 5 insertions(+), 11 deletions(-) create mode 100644 src/App.tsx diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..ab945c52 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,3 @@ +import React from "react"; + +export const App: React.FC<{}> = () =>

Put App here

; diff --git a/src/index.tsx b/src/index.tsx index 24c68e23..cdc96b1f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,5 @@ import React from "react"; import { render } from "react-dom"; +import { App } from "@/App"; -import { InteractiveField, Field } from "./components"; - -render( - , - document.getElementById("root") -); +render(, document.getElementById("root")); From b989532490a1ade23083136cad441a522329fc1f Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 22:28:43 +0300 Subject: [PATCH 04/16] Setup basic routing --- package-lock.json | 21 +++++++++++++++++++++ package.json | 1 + src/App.tsx | 32 +++++++++++++++++++++++++++++++- src/screens/FieldScreen.tsx | 11 +++++++++++ src/screens/LoginScreen.tsx | 3 +++ src/screens/NoMatchScreen.tsx | 3 +++ 6 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/screens/FieldScreen.tsx create mode 100644 src/screens/LoginScreen.tsx create mode 100644 src/screens/NoMatchScreen.tsx diff --git a/package-lock.json b/package-lock.json index 470d4bba..f2099c6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4426,6 +4426,27 @@ "@types/react": "*" } }, + "@types/react-router": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.7.tgz", + "integrity": "sha512-2ouP76VQafKjtuc0ShpwUebhHwJo0G6rhahW9Pb8au3tQTjYXd2jta4wv6U2tGLR/I42yuG00+UXjNYY0dTzbg==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.5.tgz", + "integrity": "sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/react-syntax-highlighter": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz", diff --git a/package.json b/package.json index 5141a49b..533c87f9 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/jest": "^25.1.4", "@types/react": "^16.9.33", "@types/react-dom": "^16.9.6", + "@types/react-router-dom": "^5.1.5", "@types/react-test-renderer": "^16.9.2", "@typescript-eslint/eslint-plugin": "^2.25.0", "@typescript-eslint/parser": "^2.25.0", diff --git a/src/App.tsx b/src/App.tsx index ab945c52..9d7c7153 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,33 @@ import React from "react"; +import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; +import { LoginScreen } from "@/screens/LoginScreen"; +import { FieldScreen } from "@/screens/FieldScreen"; +import { NoMatchScreen } from "@/screens/NoMatchScreen"; -export const App: React.FC<{}> = () =>

Put App here

; +export const App: React.FC<{}> = () => ( + +
+ + + + + + + + + + + + +
+
+); diff --git a/src/screens/FieldScreen.tsx b/src/screens/FieldScreen.tsx new file mode 100644 index 00000000..d0a6409c --- /dev/null +++ b/src/screens/FieldScreen.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { InteractiveField, Field } from "@/components"; + +export const FieldScreen = () => ( + +); diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx new file mode 100644 index 00000000..241b9329 --- /dev/null +++ b/src/screens/LoginScreen.tsx @@ -0,0 +1,3 @@ +import React from "react"; + +export const LoginScreen: React.FC<{}> = () =>

Login screen

; diff --git a/src/screens/NoMatchScreen.tsx b/src/screens/NoMatchScreen.tsx new file mode 100644 index 00000000..ccf54a58 --- /dev/null +++ b/src/screens/NoMatchScreen.tsx @@ -0,0 +1,3 @@ +import React from "react"; + +export const NoMatchScreen: React.FC<{}> = () =>

404

; From f49a15fc667d03bf874e7a13645c0feae7830680 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 22:50:49 +0300 Subject: [PATCH 05/16] Add basic login flow --- src/App.tsx | 2 ++ src/api/auth.ts | 12 ++++++++++++ src/screens/LoginScreen.tsx | 30 ++++++++++++++++++++++++++++-- src/screens/UserScreen.tsx | 19 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/api/auth.ts create mode 100644 src/screens/UserScreen.tsx diff --git a/src/App.tsx b/src/App.tsx index 9d7c7153..74ff8d87 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; import { LoginScreen } from "@/screens/LoginScreen"; import { FieldScreen } from "@/screens/FieldScreen"; import { NoMatchScreen } from "@/screens/NoMatchScreen"; +import { UserScreen } from "@/screens/UserScreen"; export const App: React.FC<{}> = () => ( @@ -24,6 +25,7 @@ export const App: React.FC<{}> = () => ( + diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 00000000..f93098d0 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,12 @@ +export const login = async (name: string) => { + await localStorage.setItem("login", name); +}; + +export const logout = async () => { + await localStorage.removeItem("login"); +}; + +export const isLoggedIn = async () => { + const login = await localStorage.getItem("login"); + return Boolean(login); +}; diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index 241b9329..ea329c57 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -1,3 +1,29 @@ -import React from "react"; +import React, { useCallback, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { login } from "@/api/auth"; -export const LoginScreen: React.FC<{}> = () =>

Login screen

; +export const LoginScreen: React.FC<{}> = () => { + const [name, setName] = useState(""); + const history = useHistory(); + const onSubmit = useCallback( + async (ev) => { + ev.preventDefault(); + await login(name); + history.push(`/user/${name}`); + }, + [name] + ); + return ( +
+ + +
+ ); +}; diff --git a/src/screens/UserScreen.tsx b/src/screens/UserScreen.tsx new file mode 100644 index 00000000..5df9214f --- /dev/null +++ b/src/screens/UserScreen.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { RouteComponentProps } from "react-router-dom"; + +interface RouteParams { + name: string; +} + +export class UserScreen extends React.PureComponent< + RouteComponentProps, + {} +> { + render() { + return ( +
+

Hello, {this.props.match.params.name}!

+
+ ); + } +} From 7f07055a2516ae2dc91c76bffac691f7811890d5 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 23:07:48 +0300 Subject: [PATCH 06/16] Tweak devserver setup --- src/screens/UserScreen.tsx | 6 ++++++ webpack.config.js | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/screens/UserScreen.tsx b/src/screens/UserScreen.tsx index 5df9214f..46517def 100644 --- a/src/screens/UserScreen.tsx +++ b/src/screens/UserScreen.tsx @@ -1,5 +1,6 @@ import React from "react"; import { RouteComponentProps } from "react-router-dom"; +import { logout } from "@/api/auth"; interface RouteParams { name: string; @@ -9,10 +10,15 @@ export class UserScreen extends React.PureComponent< RouteComponentProps, {} > { + logout = async () => { + await logout(); + this.props.history.push("/"); + }; render() { return (

Hello, {this.props.match.params.name}!

+
); } diff --git a/webpack.config.js b/webpack.config.js index 494af939..d324b47b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,7 +16,8 @@ module.exports = { }, output: { path: path.join(__dirname, "/dist"), - filename: "index.js", + filename: "./index.js", + publicPath: "/", }, module: { rules: [ From 16353bef5bc405b95618090f6b2624a1f40c9638 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 23:26:37 +0300 Subject: [PATCH 07/16] Add authorizedOnlyHoc --- src/api/auth.ts | 3 +++ src/screens/UserScreen.tsx | 5 ++++- src/utils/authorizedOnlyHOC.tsx | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/utils/authorizedOnlyHOC.tsx diff --git a/src/api/auth.ts b/src/api/auth.ts index f93098d0..659294fd 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,3 +1,5 @@ +const sleep = (x: number) => new Promise((r) => setTimeout(r, x)); + export const login = async (name: string) => { await localStorage.setItem("login", name); }; @@ -7,6 +9,7 @@ export const logout = async () => { }; export const isLoggedIn = async () => { + await sleep(2000); const login = await localStorage.getItem("login"); return Boolean(login); }; diff --git a/src/screens/UserScreen.tsx b/src/screens/UserScreen.tsx index 46517def..cee72d4d 100644 --- a/src/screens/UserScreen.tsx +++ b/src/screens/UserScreen.tsx @@ -1,12 +1,13 @@ import React from "react"; import { RouteComponentProps } from "react-router-dom"; import { logout } from "@/api/auth"; +import { authorizedOnlyHoc } from "@/utils/authorizedOnlyHOC"; interface RouteParams { name: string; } -export class UserScreen extends React.PureComponent< +class RawUserScreen extends React.PureComponent< RouteComponentProps, {} > { @@ -23,3 +24,5 @@ export class UserScreen extends React.PureComponent< ); } } + +export const UserScreen = authorizedOnlyHoc(RawUserScreen, "/login"); diff --git a/src/utils/authorizedOnlyHOC.tsx b/src/utils/authorizedOnlyHOC.tsx new file mode 100644 index 00000000..3846ddd2 --- /dev/null +++ b/src/utils/authorizedOnlyHOC.tsx @@ -0,0 +1,34 @@ +import React, { useState, useEffect } from "react"; +import { isLoggedIn } from "@/api/auth"; +import { Redirect } from "react-router-dom"; + +enum CheckState { + initiated, + succeed, + failed, +} + +export const authorizedOnlyHoc = ( + Component: React.ComponentType, + redirectPath: "/" +) => (props: Props) => { + const [isAuthorized, setIsAuthorized] = useState(CheckState.initiated); + + useEffect(() => { + //https://medium.com/javascript-in-plain-english/how-to-use-async-function-in-react-hook-useeffect-typescript-js-6204a788a435 + (async () => { + const isAuthorized = await isLoggedIn(); + setIsAuthorized(isAuthorized ? CheckState.succeed : CheckState.failed); + })(); + }, []); + + if (isAuthorized === CheckState.initiated) { + return
Checking if user is authorized
; + } + + if (isAuthorized === CheckState.failed) { + return ; + } + + return ; +}; From 143364c0150899ce63b44ccc33bf70fa9861cda6 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 23:45:08 +0300 Subject: [PATCH 08/16] Add LoginScreen test --- jest.config.js | 1 + src/screens/LoginScreen.test.tsx | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/screens/LoginScreen.test.tsx diff --git a/jest.config.js b/jest.config.js index efed1dbb..13c5445b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,7 @@ module.exports = { moduleNameMapper: { // https://jestjs.io/docs/en/webpack#handling-static-assets "\\.(css|less)$": "/internals/__mocks__/styleMock.js", + "^@/(.*)$": "/src/$1", }, moduleDirectories: ["node_modules", "src"], }; diff --git a/src/screens/LoginScreen.test.tsx b/src/screens/LoginScreen.test.tsx new file mode 100644 index 00000000..5a3b5c5a --- /dev/null +++ b/src/screens/LoginScreen.test.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { LoginScreen } from "./LoginScreen"; +import { shallow } from "enzyme"; +import { login } from "@/api/auth"; + +const sleep = (x: number) => new Promise((r) => setTimeout(r, x)); + +const mockHistory = { push: jest.fn() }; +jest.mock("react-router-dom", () => ({ + useHistory: () => mockHistory, +})); + +jest.mock("@/api/auth", () => ({ + login: jest.fn(), +})); + +describe("LoginScreen", () => { + it("navigates to user page on submit", async () => { + const name = "BobMarley"; + const screen = shallow(); + + screen.find("input").simulate("change", { target: { value: name } }); + await screen + .find("form") + .simulate("submit", { preventDefault: () => null }); + + expect(login).toHaveBeenCalledWith(name); + expect(mockHistory.push).toHaveBeenCalledWith(`/user/${name}`); + }); +}); From bf185c45e0bec8061b5ba5d319df10bab18a22fc Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Wed, 13 May 2020 23:46:10 +0300 Subject: [PATCH 09/16] Correct hoc typing --- src/utils/authorizedOnlyHOC.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/authorizedOnlyHOC.tsx b/src/utils/authorizedOnlyHOC.tsx index 3846ddd2..5a04f525 100644 --- a/src/utils/authorizedOnlyHOC.tsx +++ b/src/utils/authorizedOnlyHOC.tsx @@ -10,7 +10,7 @@ enum CheckState { export const authorizedOnlyHoc = ( Component: React.ComponentType, - redirectPath: "/" + redirectPath = "/" ) => (props: Props) => { const [isAuthorized, setIsAuthorized] = useState(CheckState.initiated); From b07fba5dae35db630d521d9ec376f3aceb64df84 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Thu, 14 May 2020 00:05:22 +0300 Subject: [PATCH 10/16] Add tests for authorizedOnlyHOC --- src/utils/authorizedOnlyHOC.test.tsx | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/utils/authorizedOnlyHOC.test.tsx diff --git a/src/utils/authorizedOnlyHOC.test.tsx b/src/utils/authorizedOnlyHOC.test.tsx new file mode 100644 index 00000000..643c9ae8 --- /dev/null +++ b/src/utils/authorizedOnlyHOC.test.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { authorizedOnlyHoc } from "./authorizedOnlyHoc"; +import { isLoggedIn } from "@/api/auth"; +import { mount } from "enzyme"; + +jest.mock("@/api/auth", () => ({ + isLoggedIn: jest.fn(), +})); + +jest.mock("react-router-dom", () => ({ + Redirect: function Redirect(props: any) { + return
Redirect: {JSON.stringify(props)}
; + }, +})); + +const sleep = (x: number) => new Promise((r) => setTimeout(r, x)); + +describe("authorizedOnlyHoc", () => { + interface ComponentProps { + name: string; + } + const Component: React.FC = ({ name }) =>

{name}

; + let WrappedComponent: React.ComponentType; + beforeEach(() => { + WrappedComponent = authorizedOnlyHoc(Component); + }); + + it("renders placeholder during request and component on success", async () => { + const wrapper = mount(); + (isLoggedIn as jest.Mock).mockResolvedValueOnce(true); + expect(wrapper.html()).toMatchInlineSnapshot( + `"
Checking if user is authorized
"` + ); + await sleep(10); + + wrapper.update(); + expect(wrapper.html()).toMatchInlineSnapshot( + `"
Redirect: {\\"to\\":\\"/\\"}
"` + ); + }); + + it("renders placeholder during request and redirect on failure", async () => { + const wrapper = mount(); + (isLoggedIn as jest.Mock).mockResolvedValueOnce(false); + expect(wrapper.html()).toMatchInlineSnapshot( + `"
Checking if user is authorized
"` + ); + await sleep(10); + wrapper.update(); + expect(wrapper.html()).toMatchInlineSnapshot(`"

Bob

"`); + }); +}); From 15f7737e13be7103dea02dfb88ac7928df5f6451 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Thu, 14 May 2020 00:10:28 +0300 Subject: [PATCH 11/16] Extract sleep helper --- src/api/auth.ts | 2 +- src/screens/LoginScreen.test.tsx | 2 -- src/utils/authorizedOnlyHOC.test.tsx | 3 +-- src/utils/sleep.ts | 1 + 4 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 src/utils/sleep.ts diff --git a/src/api/auth.ts b/src/api/auth.ts index 659294fd..f9e0bd6e 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,4 +1,4 @@ -const sleep = (x: number) => new Promise((r) => setTimeout(r, x)); +import { sleep } from '@/utils/sleep'; export const login = async (name: string) => { await localStorage.setItem("login", name); diff --git a/src/screens/LoginScreen.test.tsx b/src/screens/LoginScreen.test.tsx index 5a3b5c5a..2fc1f45c 100644 --- a/src/screens/LoginScreen.test.tsx +++ b/src/screens/LoginScreen.test.tsx @@ -4,8 +4,6 @@ import { LoginScreen } from "./LoginScreen"; import { shallow } from "enzyme"; import { login } from "@/api/auth"; -const sleep = (x: number) => new Promise((r) => setTimeout(r, x)); - const mockHistory = { push: jest.fn() }; jest.mock("react-router-dom", () => ({ useHistory: () => mockHistory, diff --git a/src/utils/authorizedOnlyHOC.test.tsx b/src/utils/authorizedOnlyHOC.test.tsx index 643c9ae8..c87a19fc 100644 --- a/src/utils/authorizedOnlyHOC.test.tsx +++ b/src/utils/authorizedOnlyHOC.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { authorizedOnlyHoc } from "./authorizedOnlyHoc"; import { isLoggedIn } from "@/api/auth"; import { mount } from "enzyme"; +import { sleep } from "@/utils/sleep"; jest.mock("@/api/auth", () => ({ isLoggedIn: jest.fn(), @@ -13,8 +14,6 @@ jest.mock("react-router-dom", () => ({ }, })); -const sleep = (x: number) => new Promise((r) => setTimeout(r, x)); - describe("authorizedOnlyHoc", () => { interface ComponentProps { name: string; diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts new file mode 100644 index 00000000..1c6942d9 --- /dev/null +++ b/src/utils/sleep.ts @@ -0,0 +1 @@ +export const sleep = (x: number) => new Promise((r) => setTimeout(r, x)); From 536cc1f7019a4d85d39d92a0f1953e0b16a67eff Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Thu, 14 May 2020 10:26:55 +0300 Subject: [PATCH 12/16] Remove redundant wrapper --- src/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 74ff8d87..2bb7d143 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import { UserScreen } from "@/screens/UserScreen"; export const App: React.FC<{}> = () => ( -
); From 8dd32f393ec942523e85c9c9d34dc865b9d1952e Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Thu, 14 May 2020 10:27:12 +0300 Subject: [PATCH 13/16] Add login/logout delays --- src/api/auth.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/api/auth.ts b/src/api/auth.ts index f9e0bd6e..32120fad 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,10 +1,14 @@ import { sleep } from '@/utils/sleep'; export const login = async (name: string) => { + await sleep(1000); + await localStorage.setItem("login", name); }; export const logout = async () => { + await sleep(1000); + await localStorage.removeItem("login"); }; From 70d4d2bb29dd83e3e1089e60c70eadb4a23c03f5 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Thu, 14 May 2020 10:33:44 +0300 Subject: [PATCH 14/16] Remove redundant beforeEach --- src/utils/authorizedOnlyHOC.test.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/authorizedOnlyHOC.test.tsx b/src/utils/authorizedOnlyHOC.test.tsx index c87a19fc..e9e57ae8 100644 --- a/src/utils/authorizedOnlyHOC.test.tsx +++ b/src/utils/authorizedOnlyHOC.test.tsx @@ -19,10 +19,7 @@ describe("authorizedOnlyHoc", () => { name: string; } const Component: React.FC = ({ name }) =>

{name}

; - let WrappedComponent: React.ComponentType; - beforeEach(() => { - WrappedComponent = authorizedOnlyHoc(Component); - }); + const WrappedComponent = authorizedOnlyHoc(Component); it("renders placeholder during request and component on success", async () => { const wrapper = mount(); From ebf064104dcd8b30355169e4b5b493c080d421d4 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Thu, 14 May 2020 17:56:36 +0300 Subject: [PATCH 15/16] Update README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index bf099ac1..d1741a53 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,9 @@ React Patterns [Pull request](https://github.com/nickovchinnikov/react-js-tutorial/pull/20)
[Presentation](https://docs.google.com/presentation/d/1ASdPCTPObKZHHfFvd2RARfmHeq4EvHM8x-YBMCzGcb4/edit?usp=sharing)
[Code samples](https://codesandbox.io/s/react-hooks-c3jf4?file=/src/index.tsx) + +## Lesson 12: +* React Router + +[Pull request](https://github.com/nickovchinnikov/react-js-tutorial/pull/21)
+[Presentation](https://drive.google.com/file/d/19xQYJASeA3HQf0tR-tQ3J_I6MRyoHRvw/view?usp=sharing)
\ No newline at end of file From 5825b8281413449fa9f78ca674636d1624ade4c4 Mon Sep 17 00:00:00 2001 From: Vasil Vanchuk Date: Thu, 14 May 2020 22:22:53 +0300 Subject: [PATCH 16/16] Fix errors in tests --- src/App.tsx | 7 ++++--- src/screens/FieldScreen.tsx | 5 +++-- src/utils/authorizedOnlyHOC.test.tsx | 12 ++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2bb7d143..ca158958 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,15 +15,16 @@ export const App: React.FC<{}> = () => (
  • Field
  • +
  • + Nick +
  • - - - + } /> diff --git a/src/screens/FieldScreen.tsx b/src/screens/FieldScreen.tsx index d0a6409c..2eedaedd 100644 --- a/src/screens/FieldScreen.tsx +++ b/src/screens/FieldScreen.tsx @@ -1,11 +1,12 @@ import React from "react"; import { InteractiveField, Field } from "@/components"; +import { authorizedOnlyHoc } from "@/utils/authorizedOnlyHOC"; -export const FieldScreen = () => ( +export const FieldScreen = authorizedOnlyHoc(() => ( -); +)); diff --git a/src/utils/authorizedOnlyHOC.test.tsx b/src/utils/authorizedOnlyHOC.test.tsx index e9e57ae8..2db3556e 100644 --- a/src/utils/authorizedOnlyHOC.test.tsx +++ b/src/utils/authorizedOnlyHOC.test.tsx @@ -22,27 +22,27 @@ describe("authorizedOnlyHoc", () => { const WrappedComponent = authorizedOnlyHoc(Component); it("renders placeholder during request and component on success", async () => { - const wrapper = mount(); (isLoggedIn as jest.Mock).mockResolvedValueOnce(true); + const wrapper = mount(); expect(wrapper.html()).toMatchInlineSnapshot( `"
    Checking if user is authorized
    "` ); await sleep(10); wrapper.update(); - expect(wrapper.html()).toMatchInlineSnapshot( - `"
    Redirect: {\\"to\\":\\"/\\"}
    "` - ); + expect(wrapper.html()).toMatchInlineSnapshot(`"

    Bob

    "`); }); it("renders placeholder during request and redirect on failure", async () => { - const wrapper = mount(); (isLoggedIn as jest.Mock).mockResolvedValueOnce(false); + const wrapper = mount(); expect(wrapper.html()).toMatchInlineSnapshot( `"
    Checking if user is authorized
    "` ); await sleep(10); wrapper.update(); - expect(wrapper.html()).toMatchInlineSnapshot(`"

    Bob

    "`); + expect(wrapper.html()).toMatchInlineSnapshot( + `"
    Redirect: {\\"to\\":\\"/\\"}
    "` + ); }); });