1
0
mirror of https://github.com/SomboChea/ui synced 2026-01-18 09:06:14 +07:00

Refactor(#209): Converted App component from class to func

This commit is contained in:
Priscila Oliveira
2019-11-24 19:21:08 +01:00
committed by GitHub
parent 0a48906fc8
commit 0d9232a92c
16 changed files with 520 additions and 304 deletions

View File

@@ -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('<App />', () => {
test('should display the Loading component at the beginning ', () => {
const { container, queryByTestId } = render(<App />);
beforeEach(() => {
wrapper = mount(<App />);
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(<App />);
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(<App />);
// 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(<App />);
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();
}
}
});
});

View File

@@ -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: <Header />
*/
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<HTMLDivElement> {
const { isLoading, isUserLoggedIn, packages, logoUrl, user, scope } = this.state;
const context = { isUserLoggedIn, packages, logoUrl, user, scope };
return (
<Container isLoading={isLoading}>
{isLoading ? <Loading /> : <AppContextProvider value={context}>{this.renderContent()}</AppContextProvider>}
{this.renderLoginModal()}
</Container>
);
}
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<any[]>('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: <LoginModal /> <Header />
*/
public handleToggleLoginModal = () => {
this.setState(prevState => ({
showLoginModal: !prevState.showLoginModal,
}));
};
/**
* handles login
* Required by: <Header />
*/
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: <Header />
*/
public handleLogout = () => {
storage.removeItem('username');
storage.removeItem('token');
this.setState({
user: {},
isUserLoggedIn: false,
});
};
return (
<>
<StyleBaseline />
<Box display="flex" flexDirection="column" height="100%">
{isLoading ? (
<Loading />
) : (
<>
<Router history={history}>
<AppContextProvider packages={packages} user={user}>
<Header />
<StyledBoxContent flexGrow={1}>
<AppRoute />
</StyledBoxContent>
</AppContextProvider>
</Router>
<Footer />
</>
)}
</Box>
</>
);
};
public renderLoginModal = (): ReactElement<HTMLElement> => {
const { error, showLoginModal } = this.state;
return (
<LoginModal
error={error}
onCancel={this.handleToggleLoginModal}
onSubmit={this.handleDoLogin}
visibility={showLoginModal}
/>
);
};
public renderContent = (): ReactElement<HTMLElement> => {
return (
<>
<Content>
<AppRoute>{this.renderHeader()}</AppRoute>
</Content>
<Footer />
</>
);
};
public renderHeader = (): ReactElement<HTMLElement> => {
const {
logoUrl,
user: { username },
scope,
} = this.state;
return (
<Header
logo={logoUrl}
onLogout={this.handleLogout}
onToggleLoginModal={this.handleToggleLoginModal}
scope={scope}
username={username}
/>
);
};
}
export default App;

23
src/App/AppContext.ts Normal file
View File

@@ -0,0 +1,23 @@
import { createContext } from 'react';
import { FormError } from '../components/Login/Login';
export interface AppProps {
error?: FormError;
user?: User;
scope: string;
packages: any[];
}
export interface User {
username: string;
}
export interface AppContextProps extends AppProps {
setUser: (user?: User) => void;
setError: (error?: FormError) => void;
}
const AppContext = createContext<undefined | AppContextProps>(undefined);
export default AppContext;

View File

@@ -1,20 +0,0 @@
import { createContext } from 'react';
import { FormError } from '../components/Login/Login';
export interface AppProps {
error?: FormError;
logoUrl: string;
user: {
username?: string;
};
scope: string;
showLoginModal: boolean;
isUserLoggedIn: boolean;
packages: [];
isLoading: boolean;
}
export const AppContext = createContext<Partial<AppProps>>({});
export const AppContextProvider = AppContext.Provider;
export const AppContextConsumer = AppContext.Consumer;

View File

@@ -0,0 +1,61 @@
import React, { useState, useEffect } from 'react';
import { FormError } from '../components/Login/Login';
import AppContext, { AppProps, User } from './AppContext';
interface Props {
packages: any[];
user?: User;
}
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable react-hooks/exhaustive-deps */
const AppContextProvider: React.FC<Props> = ({ children, packages, user }) => {
const [state, setState] = useState<AppProps>({
scope: window.VERDACCIO_SCOPE || '',
packages,
user,
});
useEffect(() => {
setState({
...state,
user,
});
}, [user]);
useEffect(() => {
setState({
...state,
packages,
});
}, [packages]);
const setUser = (user?: User) => {
setState({
...state,
user,
});
};
const setError = (error?: FormError) => {
setState({
...state,
error,
});
};
return (
<AppContext.Provider
value={{
...state,
setUser,
setError,
}}>
{children}
</AppContext.Provider>
);
};
export default AppContextProvider;

View File

@@ -4,7 +4,7 @@ import { createBrowserHistory } from 'history';
import Loading from '../components/Loading';
import { AppContext } from './AppContext';
import AppContext from './AppContext';
const NotFound = lazy(() => import('../components/NotFound'));
const VersionContextProvider = lazy(() => import('../pages/Version/VersionContextProvider'));
@@ -19,19 +19,25 @@ enum Route {
PACKAGE_VERSION = '/-/web/detail/:package/v/:version',
}
const history = createBrowserHistory({
export const history = createBrowserHistory({
basename: window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix,
});
/* eslint react/jsx-max-depth: 0 */
const AppRoute: React.FC = ({ children }) => {
const AppRoute: React.FC = () => {
const appContext = useContext(AppContext);
const { isUserLoggedIn, packages } = appContext;
if (!appContext) {
throw Error('The app Context was not correct used');
}
const { user, packages } = appContext;
const isUserLoggedIn = user && user.username;
return (
<Router history={history}>
<Suspense fallback={<Loading />}>
{children}
<Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage isUserLoggedIn={!!isUserLoggedIn} packages={packages || []} />

View File

@@ -0,0 +1,179 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<App /> should display the Header component 1`] = `
.emotion-8 {
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
top: 50%;
left: 50%;
position: absolute;
}
.emotion-2 {
margin: 0 0 30px 0;
border-radius: 25px;
box-shadow: 0 10px 20px 0 rgba(69,58,100,0.2);
background: #f7f8f6;
}
.emotion-0 {
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
background-position: center;
background-size: contain;
background-image: url([object Object]);
background-repeat: no-repeat;
width: 90px;
height: 90px;
}
.emotion-6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.emotion-4 {
color: #4b5e40;
}
<div
class="MuiBox-root MuiBox-root-215"
>
<div
class="emotion-8 emotion-9"
data-testid="loading"
>
<div
class="emotion-2 emotion-3"
>
<div
class="emotion-0 emotion-1"
/>
</div>
<div
class="emotion-6 emotion-7"
>
<div
class="MuiCircularProgress-root emotion-4 emotion-5 MuiCircularProgress-colorPrimary MuiCircularProgress-indeterminate"
role="progressbar"
style="width: 50px; height: 50px;"
>
<svg
class="MuiCircularProgress-svg"
viewBox="22 22 44 44"
>
<circle
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate"
cx="44"
cy="44"
fill="none"
r="20.2"
stroke-width="3.6"
/>
</svg>
</div>
</div>
</div>
</div>
`;
exports[`<App /> should display the Loading component at the beginning 1`] = `
.emotion-8 {
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
top: 50%;
left: 50%;
position: absolute;
}
.emotion-2 {
margin: 0 0 30px 0;
border-radius: 25px;
box-shadow: 0 10px 20px 0 rgba(69,58,100,0.2);
background: #f7f8f6;
}
.emotion-0 {
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
background-position: center;
background-size: contain;
background-image: url([object Object]);
background-repeat: no-repeat;
width: 90px;
height: 90px;
}
.emotion-6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.emotion-4 {
color: #4b5e40;
}
<div
class="MuiBox-root MuiBox-root-2"
>
<div
class="emotion-8 emotion-9"
data-testid="loading"
>
<div
class="emotion-2 emotion-3"
>
<div
class="emotion-0 emotion-1"
/>
</div>
<div
class="emotion-6 emotion-7"
>
<div
class="MuiCircularProgress-root emotion-4 emotion-5 MuiCircularProgress-colorPrimary MuiCircularProgress-indeterminate"
role="progressbar"
style="width: 50px; height: 50px;"
>
<svg
class="MuiCircularProgress-svg"
viewBox="22 22 44 44"
>
<circle
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate"
cx="44"
cy="44"
fill="none"
r="20.2"
stroke-width="3.6"
/>
</svg>
</div>
</div>
</div>
</div>
`;

View File

@@ -1 +1,2 @@
export { default } from './App';
export { default as AppContextProvider } from './AppContextProvider';