From 0d9232a92cbe3d507f43c2ae86278a63ac23996d Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Sun, 24 Nov 2019 19:21:08 +0100 Subject: [PATCH] Refactor(#209): Converted App component from class to func --- src/App/App.test.tsx | 102 ++++---- src/App/App.tsx | 234 ++++++------------ src/App/AppContext.ts | 23 ++ src/App/AppContext.tsx | 20 -- src/App/AppContextProvider.tsx | 61 +++++ src/App/AppRoute.tsx | 16 +- src/App/__snapshots__/App.test.tsx.snap | 179 ++++++++++++++ src/App/index.ts | 1 + src/components/Header/Header.test.tsx | 93 +++---- src/components/Header/Header.tsx | 69 +++++- src/components/Header/HeaderGreetings.tsx | 2 +- src/components/Header/HeaderMenu.tsx | 4 +- src/components/Header/HeaderRight.tsx | 2 +- .../Header/__snapshots__/Header.test.tsx.snap | 6 +- src/components/Login/Login.tsx | 8 +- .../Login/__snapshots__/Login.test.tsx.snap | 4 +- 16 files changed, 520 insertions(+), 304 deletions(-) create mode 100644 src/App/AppContext.ts delete mode 100644 src/App/AppContext.tsx create mode 100644 src/App/AppContextProvider.tsx create mode 100644 src/App/__snapshots__/App.test.tsx.snap diff --git a/src/App/App.test.tsx b/src/App/App.test.tsx index 3e273bc..6b0a71d 100644 --- a/src/App/App.test.tsx +++ b/src/App/App.test.tsx @@ -1,13 +1,11 @@ import React from 'react'; -import { ReactWrapper } from 'enzyme'; -import { mount } from '../utils/test-enzyme'; +import { render, waitForElement, fireEvent } from '../utils/test-react-testing-library'; import storage from '../utils/storage'; // eslint-disable-next-line jest/no-mocks-import import { generateTokenWithTimeRange } from '../../jest/unit/components/__mocks__/token'; import App from './App'; -import { AppProps } from './AppContext'; jest.mock('../utils/storage', () => { class LocalStorageMock { @@ -36,63 +34,71 @@ jest.mock('../utils/api', () => ({ request: require('../../jest/unit/components/__mocks__/api').default.request, })); -describe('App', () => { - let wrapper: ReactWrapper<{}, AppProps, App>; +/* eslint-disable react/jsx-no-bind*/ +describe('', () => { + test('should display the Loading component at the beginning ', () => { + const { container, queryByTestId } = render(); - beforeEach(() => { - wrapper = mount(); + expect(container.firstChild).toMatchSnapshot(); + expect(queryByTestId('loading')).toBeTruthy(); }); - test('toggleLoginModal: should toggle the value in state', () => { - const { handleToggleLoginModal } = wrapper.instance(); - expect(wrapper.state().showLoginModal).toBeFalsy(); - handleToggleLoginModal(); - expect(wrapper.state('showLoginModal')).toBeTruthy(); - expect(wrapper.state('error')).toEqual(undefined); + test('should display the Header component ', async () => { + const { container, queryByTestId } = render(); + + expect(container.firstChild).toMatchSnapshot(); + expect(queryByTestId('loading')).toBeTruthy(); + + // wait for the Header component appearance and return the element + const headerElement = await waitForElement(() => queryByTestId('header')); + expect(headerElement).toBeTruthy(); + }); + + test('handleLogout - logouts the user and clear localstorage', async () => { + storage.setItem('username', 'verdaccio'); + storage.setItem('token', generateTokenWithTimeRange(24)); + + const { queryByTestId } = render(); + + // wait for the Account's circle element component appearance and return the element + const accountCircleElement = await waitForElement(() => queryByTestId('header--menu-accountcircle')); + expect(accountCircleElement).toBeTruthy(); + + if (accountCircleElement) { + fireEvent.click(accountCircleElement); + + // wait for the Button's logout element component appearance and return the element + const buttonLogoutElement = await waitForElement(() => queryByTestId('header--button-logout')); + expect(buttonLogoutElement).toBeTruthy(); + + if (buttonLogoutElement) { + fireEvent.click(buttonLogoutElement); + + expect(queryByTestId('greetings-label')).toBeFalsy(); + } + } }); test('isUserAlreadyLoggedIn: token already available in storage', async () => { storage.setItem('username', 'verdaccio'); storage.setItem('token', generateTokenWithTimeRange(24)); - const { isUserAlreadyLoggedIn } = wrapper.instance(); - isUserAlreadyLoggedIn(); + const { queryByTestId, queryAllByText } = render(); - expect(wrapper.state('user').username).toEqual('verdaccio'); - }); + // wait for the Account's circle element component appearance and return the element + const accountCircleElement = await waitForElement(() => queryByTestId('header--menu-accountcircle')); + expect(accountCircleElement).toBeTruthy(); - test('handleLogout - logouts the user and clear localstorage', async () => { - const { handleLogout } = wrapper.instance(); - storage.setItem('username', 'verdaccio'); - storage.setItem('token', 'xxxx.TOKEN.xxxx'); + if (accountCircleElement) { + fireEvent.click(accountCircleElement); - await handleLogout(); - expect(wrapper.state('user')).toEqual({}); - expect(wrapper.state('isUserLoggedIn')).toBeFalsy(); - }); + // wait for the Greeting's label element component appearance and return the element + const greetingsLabelElement = await waitForElement(() => queryByTestId('greetings-label')); + expect(greetingsLabelElement).toBeTruthy(); - test('handleDoLogin - login the user successfully', async () => { - const { handleDoLogin } = wrapper.instance(); - await handleDoLogin('sam', '1234'); - const result = { - username: 'sam', - }; - expect(wrapper.state('isUserLoggedIn')).toBeTruthy(); - expect(wrapper.state('showLoginModal')).toBeFalsy(); - expect(storage.getItem('username')).toEqual('sam'); - expect(storage.getItem('token')).toEqual('TEST_TOKEN'); - expect(wrapper.state('user')).toEqual(result); - }); - - test('handleDoLogin - authentication failure', async () => { - const { handleDoLogin } = wrapper.instance(); - await handleDoLogin('sam', '12345'); - const result = { - description: 'bad username/password, access denied', - title: 'Unable to login', - type: 'error', - }; - expect(wrapper.state('user')).toEqual({}); - expect(wrapper.state('error')).toEqual(result); + if (greetingsLabelElement) { + expect(queryAllByText('verdaccio')).toBeTruthy(); + } + } }); }); diff --git a/src/App/App.tsx b/src/App/App.tsx index 702d5e2..e32b812 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,184 +1,106 @@ -import React, { Component, ReactElement } from 'react'; +import React, { useState, useEffect } from 'react'; +import styled from '@emotion/styled'; import isNil from 'lodash/isNil'; +import { Router } from 'react-router-dom'; import storage from '../utils/storage'; -import { makeLogin, isTokenExpire } from '../utils/login'; -import Loading from '../components/Loading'; -import LoginModal from '../components/Login'; -import Header from '../components/Header'; -import { Container, Content } from '../components/Layout'; +import { isTokenExpire } from '../utils/login'; import API from '../utils/api'; +import Header from '../components/Header'; import Footer from '../components/Footer'; +import Box from '../muiComponents/Box'; +import Loading from '../components/Loading'; +import StyleBaseline from '../design-tokens/StyleBaseline'; +import { breakpoints } from '../utils/styles/media'; -import AppRoute from './AppRoute'; -import { AppProps, AppContextProvider } from './AppContext'; +import AppContextProvider from './AppContextProvider'; +import AppRoute, { history } from './AppRoute'; -export default class App extends Component<{}, AppProps> { - public state: AppProps = { - logoUrl: window.VERDACCIO_LOGO, - user: {}, - scope: window.VERDACCIO_SCOPE || '', - showLoginModal: false, - isUserLoggedIn: false, - packages: [], - isLoading: true, +const StyledBoxContent = styled(Box)({ + padding: 15, + [`@media screen and (min-width: ${breakpoints.container}px)`]: { + maxWidth: breakpoints.container, + width: '100%', + marginLeft: 'auto', + marginRight: 'auto', + }, +}); + +/* eslint-disable react/jsx-max-depth */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable react/jsx-no-bind */ +/* eslint-disable react-hooks/exhaustive-deps */ +const App: React.FC = () => { + const [user, setUser] = useState(); + const [packages, setPackages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + /** + * Logouts user + * Required by:
+ */ + const logout = () => { + storage.removeItem('username'); + storage.removeItem('token'); + setUser(undefined); }; - public componentDidMount(): void { - this.isUserAlreadyLoggedIn(); - this.loadOnHandler(); - } - - // eslint-disable-next-line no-unused-vars - public componentDidUpdate(_: AppProps, prevState: AppProps): void { - const { isUserLoggedIn } = this.state; - if (prevState.isUserLoggedIn !== isUserLoggedIn) { - this.loadOnHandler(); - } - } - - public render(): React.ReactElement { - const { isLoading, isUserLoggedIn, packages, logoUrl, user, scope } = this.state; - - const context = { isUserLoggedIn, packages, logoUrl, user, scope }; - - return ( - - {isLoading ? : {this.renderContent()}} - {this.renderLoginModal()} - - ); - } - - public isUserAlreadyLoggedIn = () => { + const checkUserAlreadyLoggedIn = () => { // checks for token validity const token = storage.getItem('token'); - const username: string = storage.getItem('username') as string; + const username = storage.getItem('username'); + if (isTokenExpire(token) || isNil(username)) { - this.handleLogout(); - } else { - this.setState({ - user: { username }, - isUserLoggedIn: true, - }); + logout(); + return; } + + setUser({ username }); }; - public loadOnHandler = async () => { + const loadOnHandler = async () => { try { - const packages = await API.request('packages', 'GET'); - // @ts-ignore: FIX THIS TYPE: Type 'any[]' is not assignable to type '[]' - this.setState({ - packages, - isLoading: false, - }); + const packages = await API.request('packages', 'GET'); + // FIXME add correct type for package + setPackages(packages as never[]); } catch (error) { // FIXME: add dialog console.error({ title: 'Warning', message: `Unable to load package list: ${error.message}`, }); - this.setLoading(false); - } - }; - - public setLoading = (isLoading: boolean) => - this.setState({ - isLoading, - }); - - /** - * Toggles the login modal - * Required by:
- */ - public handleToggleLoginModal = () => { - this.setState(prevState => ({ - showLoginModal: !prevState.showLoginModal, - })); - }; - - /** - * handles login - * Required by:
- */ - public handleDoLogin = async (usernameValue: string, passwordValue: string) => { - const { username, token, error } = await makeLogin(usernameValue, passwordValue); - - if (username && token) { - storage.setItem('username', username); - storage.setItem('token', token); - this.setLoggedUser(username); } - if (error) { - this.setState({ - user: {}, - error, - }); - } + setIsLoading(false); }; - public setLoggedUser = (username: string) => { - this.setState({ - user: { - username, - }, - isUserLoggedIn: true, // close login modal after successful login - showLoginModal: false, // set isUserLoggedIn to true - }); - }; + useEffect(() => { + checkUserAlreadyLoggedIn(); + loadOnHandler(); + }, []); - /** - * Logouts user - * Required by:
- */ - public handleLogout = () => { - storage.removeItem('username'); - storage.removeItem('token'); - this.setState({ - user: {}, - isUserLoggedIn: false, - }); - }; + return ( + <> + + + {isLoading ? ( + + ) : ( + <> + + +
+ + + + + +