diff --git a/package.json b/package.json index 4838c4d..ec99e50 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@types/node": "^14.14.31", "@types/react-dom": "^17.0.1", "@types/react-redux": "^7.1.16", + "@types/redux-mock-store": "^1.0.2", "@typescript-eslint/eslint-plugin": "^4.15.2", "@typescript-eslint/parser": "^4.15.2", "autoprefixer": "^10.2.4", @@ -60,6 +61,7 @@ "prettier": "^2.2.1", "prettier-quick": "0.0.5", "pretty-quick": "^3.1.0", + "redux-mock-store": "^1.5.4", "sass": "^1.32.8", "sass-loader": "^11.0.1", "style-loader": "^2.0.0", diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx index 7bacd7f..0d26919 100644 --- a/src/components/App/App.test.tsx +++ b/src/components/App/App.test.tsx @@ -1,24 +1,28 @@ import React from "react"; -import { Provider } from "react-redux"; -import { render } from "@testing-library/react"; - -import store from "../../state/store"; +import renderConnected from "../../utilities/test/renderConnected"; import App from "./App"; -const setup = () => { - const container = render( - - - - ); - return container; -}; +const setup = (initialState = {}) => + renderConnected({ + ui: , + initialState, + }); describe("App", () => { - it("renders app", () => { - const { container } = setup(); - + it("renders Loader when userProfile not loaded", () => { + const { container } = setup({ + userProfile: {}, + }); + const app = container.querySelector(".loader"); + expect(app).toBeInTheDocument(); + }); + it("renders App when userProfile loaded", () => { + const { container } = setup({ + userProfile: { + username: "Steve", + }, + }); const app = container.querySelector(".app"); expect(app).toBeInTheDocument(); }); diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 9b3a37c..a7bca60 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -1,18 +1,33 @@ import React, { FC, useEffect } from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { fetchUserById } from "../../state/userProfile/actions"; +import { getUsername } from "../../state/userProfile/selectors"; + +import Loader from "../Loader/Loader"; +import UserHeader from "../UserHeader/UserHeader"; import "../../styles/app.scss"; const App: FC = () => { const dispatch = useDispatch(); + const username = useSelector(getUsername); + useEffect(() => { - dispatch(fetchUserById()); + if (!username) { + dispatch(fetchUserById()); + } }, [dispatch]); + + const isPageReady = username; + + if (!isPageReady) { + return ; + } + return (
-

App

+
); }; diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx new file mode 100644 index 0000000..478cb07 --- /dev/null +++ b/src/components/Loader/Loader.tsx @@ -0,0 +1,9 @@ +import React, { FC } from "react"; + +const Loader: FC = () => ( +
+

Loading...

+
+); + +export default Loader; diff --git a/src/components/UserHeader/UserHeader.test.tsx b/src/components/UserHeader/UserHeader.test.tsx new file mode 100644 index 0000000..90501f7 --- /dev/null +++ b/src/components/UserHeader/UserHeader.test.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { screen } from "@testing-library/react"; +import renderConnected from "../../utilities/test/renderConnected"; + +import UserHeader from "./UserHeader"; + +const setup = (initialState = {}) => + renderConnected({ + ui: , + initialState, + }); + +describe("UserHeader", () => { + it("should display username", () => { + setup({ + userProfile: { + username: "Steve", + }, + }); + const userHeaderText = screen.getByText("Logged in as Steve"); + expect(userHeaderText).toBeInTheDocument(); + }); +}); diff --git a/src/components/UserHeader/UserHeader.tsx b/src/components/UserHeader/UserHeader.tsx new file mode 100644 index 0000000..8f6862b --- /dev/null +++ b/src/components/UserHeader/UserHeader.tsx @@ -0,0 +1,11 @@ +import React, { FC } from "react"; +import { useSelector } from "react-redux"; + +import { getUsername } from "../../state/userProfile/selectors"; + +const UserHeader: FC = () => { + const userName = useSelector(getUsername); + return
Logged in as {userName}
; +}; + +export default UserHeader; diff --git a/src/state/userProfile/reducer.ts b/src/state/userProfile/reducer.ts index 725a468..ec0751f 100644 --- a/src/state/userProfile/reducer.ts +++ b/src/state/userProfile/reducer.ts @@ -4,10 +4,10 @@ import { fetchUserById } from "./actions"; interface UserState { name?: string; - userName?: string; + username?: string; } -const initialState = {} as UserState; +export const initialState = {} as UserState; const userProfileReducer = createReducer(initialState, (builder) => { builder.addCase(fetchUserById.fulfilled, (state, action) => { diff --git a/src/state/userProfile/selectors.ts b/src/state/userProfile/selectors.ts new file mode 100644 index 0000000..d12acfc --- /dev/null +++ b/src/state/userProfile/selectors.ts @@ -0,0 +1,3 @@ +import { RootState } from "../rootReducer"; + +export const getUsername = (state: RootState): string => state.userProfile.username; diff --git a/src/utilities/test/renderConnected.tsx b/src/utilities/test/renderConnected.tsx new file mode 100644 index 0000000..b0f5d48 --- /dev/null +++ b/src/utilities/test/renderConnected.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { render, RenderResult } from "@testing-library/react"; +import { Provider } from "react-redux"; +import configureStore, { MockStore } from "redux-mock-store"; +import thunk from "redux-thunk"; + +interface RenderConnected { + ui: JSX.Element; + initialState: Record; + store?: MockStore; +} + +interface RenderConnectedWithStore extends RenderResult { + store: MockStore; +} + +const middleware = [thunk]; +const mockStore = configureStore(middleware); + +export const makeStore = (initialState: Record): MockStore => + mockStore(initialState); + +const renderConnected = ({ + ui, + initialState = {}, + store = makeStore(initialState), +}: RenderConnected): RenderConnectedWithStore => { + const rendered = render({ui}); + return { + ...rendered, + store, + }; +}; + +export default renderConnected;