mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-02-17 23:45:29 -05:00
refactor: header component test (#878)
* refactor: improves <Header /> component test cases * refactor: adds destructuring for state in render block
This commit is contained in:
parent
9c4931dc77
commit
dc9460ff07
5 changed files with 679 additions and 590 deletions
|
@ -14,7 +14,6 @@ import {HEADERS} from '../../../lib/constants';
|
|||
import classes from './header.scss';
|
||||
import './logo.png';
|
||||
|
||||
|
||||
export default class Header extends React.Component {
|
||||
state = {
|
||||
showLogin: false,
|
||||
|
@ -31,6 +30,7 @@ export default class Header extends React.Component {
|
|||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.loadLogo = this.loadLogo.bind(this);
|
||||
this.renderUserActionButton = this.renderUserActionButton.bind(this);
|
||||
}
|
||||
|
||||
toggleLoginModal() {
|
||||
|
@ -63,11 +63,13 @@ export default class Header extends React.Component {
|
|||
event.preventDefault();
|
||||
|
||||
if (this.state.username === '' || this.state.password === '') {
|
||||
return this.setState({loginError: {
|
||||
title: 'Unable to login',
|
||||
type: 'error',
|
||||
description: 'Username or password can\'t be empty!'
|
||||
}});
|
||||
return this.setState({
|
||||
loginError: {
|
||||
title: 'Unable to login',
|
||||
type: 'error',
|
||||
description: 'Username or password can\'t be empty!'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -119,8 +121,8 @@ export default class Header extends React.Component {
|
|||
if (!payload.exp || !isNumber(payload.exp)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const jsTimestamp = (payload.exp * 1000) - 30000; // Report as expire before (real expire time - 30s)
|
||||
// Report as expire before (real expire time - 30s)
|
||||
const jsTimestamp = payload.exp * 1000 - 30000;
|
||||
const expired = Date.now() >= jsTimestamp;
|
||||
|
||||
if (expired) {
|
||||
|
@ -136,35 +138,54 @@ export default class Header extends React.Component {
|
|||
}
|
||||
|
||||
renderUserActionButton() {
|
||||
if (!this.isTokenExpire) { // TODO: Check jwt token expire
|
||||
if (!this.isTokenExpire) {
|
||||
// TODO: Check jwt token expire
|
||||
const username = capitalize(storage.getItem('username'));
|
||||
return (
|
||||
<div className="user-logged">
|
||||
<span className="user-logged-greetings" style={{marginRight: '10px'}}>Hi, {username}</span>
|
||||
<Button className={`${classes.headerButton} header-button-logout`} type="danger" onClick={this.handleLogout}>Logout</Button>
|
||||
<span
|
||||
className="user-logged-greetings"
|
||||
style={{marginRight: '10px'}}
|
||||
>
|
||||
Hi, {username}
|
||||
</span>
|
||||
<Button
|
||||
className={`${classes.headerButton} header-button-logout`}
|
||||
type="danger"
|
||||
onClick={this.handleLogout}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <Button className={`${classes.headerButton} header-button-login`} onClick={ this.toggleLoginModal }>Login</Button>;
|
||||
return (
|
||||
<Button
|
||||
className={`${classes.headerButton} header-button-login`}
|
||||
onClick={this.toggleLoginModal}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const registryURL = getRegistryURL();
|
||||
|
||||
const {logo, scope, loginError, showLogin} = this.state;
|
||||
return (
|
||||
<header className={ classes.header }>
|
||||
<div className={ classes.headerWrap }>
|
||||
<header className={classes.header}>
|
||||
<div className={classes.headerWrap}>
|
||||
<Link to="/">
|
||||
<img src={ this.state.logo } className={ classes.logo } />
|
||||
<img src={logo} className={classes.logo} />
|
||||
</Link>
|
||||
<figure>
|
||||
npm set { this.state.scope }registry { registryURL }
|
||||
npm set { scope }registry { registryURL }
|
||||
<br/>
|
||||
npm adduser --registry { registryURL }
|
||||
</figure>
|
||||
|
||||
<div className={ classes.headerRight }>
|
||||
<div className={classes.headerRight}>
|
||||
{this.renderUserActionButton()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -172,27 +193,47 @@ export default class Header extends React.Component {
|
|||
<Dialog
|
||||
title="Login"
|
||||
size="tiny"
|
||||
visible={ this.state.showLogin }
|
||||
onCancel={ () => this.toggleLoginModal() }
|
||||
visible={showLogin}
|
||||
onCancel={this.toggleLoginModal}
|
||||
>
|
||||
<Form className="login-form">
|
||||
<Dialog.Body>
|
||||
{ this.state.loginError &&
|
||||
<Alert
|
||||
title={this.state.loginError.title} type={this.state.loginError.type}
|
||||
description={this.state.loginError.description} showIcon={true} closable={false}>
|
||||
</Alert>
|
||||
}
|
||||
<br/>
|
||||
<Input name="username" placeholder="Username" onChange={this.handleInput.bind(this, 'username')} />
|
||||
<br/><br/>
|
||||
<Input name="password" type="password" placeholder="Type your password" onChange={this.handleInput.bind(this, 'password')} />
|
||||
{loginError && (
|
||||
<Alert
|
||||
title={loginError.title}
|
||||
type={loginError.type}
|
||||
description={loginError.description}
|
||||
showIcon={true}
|
||||
closable={false}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
<Input
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
onChange={this.handleInput.bind(this, 'username')}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Type your password"
|
||||
onChange={this.handleInput.bind(this, 'password')}
|
||||
/>
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer className="dialog-footer">
|
||||
<Button onClick={ () => this.toggleLoginModal() } className="cancel-login-button">
|
||||
<Button
|
||||
onClick={this.toggleLoginModal}
|
||||
className="cancel-login-button"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button nativeType="submit" className="login-button" onClick={ this.handleSubmit }>
|
||||
<Button
|
||||
nativeType="submit"
|
||||
className="login-button"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
|
|
|
@ -8,4 +8,28 @@ import Adapter from 'enzyme-adapter-react-16';
|
|||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
global.__APP_VERSION__ = '1.0.0';
|
||||
global.__APP_VERSION__ = '1.0.0';
|
||||
|
||||
class LocalStorageMock {
|
||||
constructor() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.store[key] || null;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.store[key] = value.toString();
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
|
||||
global.localStorage = new LocalStorageMock();
|
|
@ -1,3 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Header /> component shallow renderUserActionButton - should show login button 1`] = `
|
||||
<Button
|
||||
className="headerButton header-button-login"
|
||||
disabled={false}
|
||||
loading={false}
|
||||
nativeType="button"
|
||||
onClick={[Function]}
|
||||
plain={false}
|
||||
type="default"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`<Header /> component shallow renderUserActionButton - should show users as loggedin 1`] = `
|
||||
<div
|
||||
className="user-logged"
|
||||
>
|
||||
<span
|
||||
className="user-logged-greetings"
|
||||
style={
|
||||
Object {
|
||||
"marginRight": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Hi,
|
||||
Sam
|
||||
</span>
|
||||
<Button
|
||||
className="headerButton header-button-logout"
|
||||
disabled={false}
|
||||
loading={false}
|
||||
nativeType="button"
|
||||
onClick={[Function]}
|
||||
plain={false}
|
||||
type="danger"
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<Header /> snapshot for loggedin user should match snapshot 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/\\"><img src=\\"\\" class=\\"logo\\"></a><figure>npm set registry http://localhost<br>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><div class=\\"user-logged\\"><span class=\\"user-logged-greetings\\" style=\\"margin-right: 10px;\\">Hi, Verdaccio</span><button class=\\"el-button el-button--danger headerButton header-button-logout\\" type=\\"button\\"><span>Logout</span></button></div></div></div><div><div style=\\"z-index: 1013; display: none;\\" class=\\"el-dialog__wrapper\\"><div style=\\"top: 15%;\\" class=\\"el-dialog el-dialog--tiny\\"><div class=\\"el-dialog__header\\"><span class=\\"el-dialog__title\\">Login</span><button type=\\"button\\" class=\\"el-dialog__headerbtn\\"><i class=\\"el-dialog__close el-icon el-icon-close\\"></i></button></div><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-dialog__body\\"><br><div class=\\"el-input\\"><input type=\\"text\\" name=\\"username\\" placeholder=\\"Username\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div><br><br><div class=\\"el-input\\"><input type=\\"password\\" name=\\"password\\" placeholder=\\"Type your password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div></div><div class=\\"el-dialog__footer dialog-footer\\"><button class=\\"el-button el-button--default cancel-login-button\\" type=\\"button\\"><span>Cancel</span></button><button class=\\"el-button el-button--default login-button\\" type=\\"submit\\"><span>Login</span></button></div></form></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012; display: none;\\"></div></div></header>"`;
|
||||
|
||||
exports[`<Header /> snapshot test shoud match snapshot 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/\\"><img src=\\"\\" class=\\"logo\\"></a><figure>npm set registry http://localhost<br>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><button class=\\"el-button el-button--default headerButton header-button-login\\" type=\\"button\\"><span>Login</span></button></div></div><div><div style=\\"z-index: 1013; display: none;\\" class=\\"el-dialog__wrapper\\"><div style=\\"top: 15%;\\" class=\\"el-dialog el-dialog--tiny\\"><div class=\\"el-dialog__header\\"><span class=\\"el-dialog__title\\">Login</span><button type=\\"button\\" class=\\"el-dialog__headerbtn\\"><i class=\\"el-dialog__close el-icon el-icon-close\\"></i></button></div><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-dialog__body\\"><br><div class=\\"el-input\\"><input type=\\"text\\" name=\\"username\\" placeholder=\\"Username\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div><br><br><div class=\\"el-input\\"><input type=\\"password\\" name=\\"password\\" placeholder=\\"Type your password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\"></div></div><div class=\\"el-dialog__footer dialog-footer\\"><button class=\\"el-button el-button--default cancel-login-button\\" type=\\"button\\"><span>Cancel</span></button><button class=\\"el-button el-button--default login-button\\" type=\\"submit\\"><span>Login</span></button></div></form></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012; display: none;\\"></div></div></header>"`;
|
||||
|
|
|
@ -3,14 +3,51 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { Base64 } from 'js-base64';
|
||||
import addHours from 'date-fns/add_hours'
|
||||
import Header from '../../../../src/webui/components/Header';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import storage from '../../../../src/webui/utils/storage';
|
||||
|
||||
jest.mock('../../../../src/webui/utils/storage', () => {
|
||||
class LocalStorageMock {
|
||||
constructor() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.store[key] || null;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.store[key] = value.toString();
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
return new LocalStorageMock();
|
||||
});
|
||||
|
||||
jest.mock('../../../../src/webui/utils/api', () => ({
|
||||
request: require('./__mocks__/api').default.request,
|
||||
}));
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
const generateTokenWithTimeRange = (limit = 0) => {
|
||||
const payload = {
|
||||
username: 'verdaccio',
|
||||
exp: Number.parseInt((addHours(new Date(), limit).getTime() / 1000), 10)
|
||||
}
|
||||
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
||||
}
|
||||
|
||||
describe('<Header /> component shallow', () => {
|
||||
let wrapper;
|
||||
|
||||
|
@ -35,7 +72,7 @@ describe('<Header /> component shallow', () => {
|
|||
expect(HeaderWrapper.state()).toEqual(state);
|
||||
});
|
||||
|
||||
it('should load verdaccio logo', () => {
|
||||
it('loadLogo - should load verdaccio logo', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const { loadLogo } = HeaderWrapper.instance();
|
||||
|
||||
|
@ -45,15 +82,24 @@ describe('<Header /> component shallow', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should toggleLogin modal', () => {
|
||||
it('toggleLoginModal - should toggle login modal', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const { toggleLoginModal } = HeaderWrapper.instance();
|
||||
|
||||
|
||||
expect(toggleLoginModal()).toBeUndefined();
|
||||
expect(HeaderWrapper.state('showLogin')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handleInput set state', () => {
|
||||
it('toggleLoginModal - click on login button and cancel button in login dialog', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const spy = jest.spyOn(HeaderWrapper.instance(), 'toggleLoginModal');
|
||||
HeaderWrapper.find('.header-button-login').simulate('click');
|
||||
HeaderWrapper.find('.cancel-login-button').simulate('click');
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
})
|
||||
|
||||
it('handleInput - should set username and password in state', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const handleInput = HeaderWrapper.instance().handleInput;
|
||||
|
||||
|
@ -127,8 +173,110 @@ describe('<Header /> component shallow', () => {
|
|||
expect(HeaderWrapper.state('loginError')).toEqual(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('renderUserActionButton - should show login button', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const { renderUserActionButton } = HeaderWrapper.instance();
|
||||
expect(renderUserActionButton()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renderUserActionButton - should show users as loggedin', () => {
|
||||
class MockedHeader extends Header {
|
||||
get isTokenExpire() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
wrapper = shallow(
|
||||
<BrowserRouter>
|
||||
<MockedHeader />
|
||||
</BrowserRouter>);
|
||||
const HeaderWrapper = wrapper.find(MockedHeader).dive();
|
||||
const { renderUserActionButton } = HeaderWrapper.instance();
|
||||
expect(renderUserActionButton()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('isTokenExpire - token is not availabe in storage', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const { isTokenExpire } = HeaderWrapper.instance();
|
||||
expect(isTokenExpire).toBeTruthy();
|
||||
});
|
||||
|
||||
it('isTokenExpire - token is not a valid payload', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const { isTokenExpire } = HeaderWrapper.instance();
|
||||
storage.setItem('token', 'not_a_valid_token');
|
||||
expect(isTokenExpire).toBeTruthy();
|
||||
});
|
||||
|
||||
it('isTokenExpire - token should not expire in 24 hrs range', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
storage.setItem('token', generateTokenWithTimeRange(24));
|
||||
expect(HeaderWrapper.instance().isTokenExpire).toBeFalsy();
|
||||
storage.removeItem('token');
|
||||
});
|
||||
|
||||
it('isTokenExpire - token should expire for present', () => {
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
storage.setItem('token', generateTokenWithTimeRange());
|
||||
expect(HeaderWrapper.instance().isTokenExpire).toBeTruthy();
|
||||
storage.removeItem('token');
|
||||
});
|
||||
|
||||
it('isTokenExpire - token expiration is not available', () => {
|
||||
const generateTokenWithOutExpiration = () => {
|
||||
const payload = {
|
||||
username: 'verdaccio'
|
||||
}
|
||||
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
||||
}
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
storage.setItem('token', generateTokenWithOutExpiration());
|
||||
expect(HeaderWrapper.instance().isTokenExpire).toBeTruthy();
|
||||
storage.removeItem('token');
|
||||
})
|
||||
|
||||
it('isTokenExpire - token expiration is not a number', () => {
|
||||
const generateTokenWithExpirationAsString = () => {
|
||||
const payload = {
|
||||
username: 'verdaccio',
|
||||
exp: 'I am not a number'
|
||||
};
|
||||
return `xxxxxx.${Base64.encode(JSON.stringify(payload))}.xxxxxx`;
|
||||
};
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
storage.setItem('token', generateTokenWithExpirationAsString());
|
||||
expect(HeaderWrapper.instance().isTokenExpire).toBeTruthy();
|
||||
storage.removeItem('token');
|
||||
});
|
||||
|
||||
it('isTokenExpire - token is not a valid json token', () => {
|
||||
const generateTokenWithExpirationAsString = () => {
|
||||
const payload = { username: 'verdaccio', exp: 'I am not a number' };
|
||||
return `xxxxxx.${Base64.encode(payload)}.xxxxxx`;
|
||||
};
|
||||
const result = [
|
||||
'Invalid token:',
|
||||
SyntaxError('Unexpected token o in JSON at position 1'),
|
||||
'xxxxxx.W29iamVjdCBPYmplY3Rd.xxxxxx'
|
||||
]
|
||||
storage.setItem('token', generateTokenWithExpirationAsString());
|
||||
wrapper.find(Header).dive().isTokenExpire;
|
||||
expect(console.error).toBeCalledWith(...result);
|
||||
storage.removeItem('token');
|
||||
});
|
||||
|
||||
it('handleLogout - should clear the local stoage', () => {
|
||||
const storageSpy = jest.spyOn(storage, 'clear');
|
||||
const locationSpy = jest.spyOn(window.location, 'reload');
|
||||
const HeaderWrapper = wrapper.find(Header).dive();
|
||||
const { handleLogout } = HeaderWrapper.instance();
|
||||
handleLogout();
|
||||
expect(storageSpy).toHaveBeenCalled();
|
||||
expect(locationSpy).toHaveBeenCalled()
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('<Header /> snapshot test', () => {
|
||||
it('shoud match snapshot', () => {
|
||||
const wrapper = mount(
|
||||
|
@ -139,3 +287,20 @@ describe('<Header /> snapshot test', () => {
|
|||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<Header /> snapshot for loggedin user', () => {
|
||||
beforeAll(() => {
|
||||
storage.setItem('token', generateTokenWithTimeRange(24));
|
||||
storage.setItem('username', 'verdaccio');
|
||||
})
|
||||
afterAll(() => {
|
||||
storage.removeItem('token');
|
||||
})
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = mount(
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
</BrowserRouter>);
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue