1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-02-21 07:29:37 +01:00

feat: replaced element-react by Material-UI (#950) (#985)

* feat: added material-ui

refactor: replaced element-react by material-ui

refactor: updated snapshots

refactor: updated tests

* fix: modified validation.WIP

* refactor: modified tests.WIP

* test(fix): unit test for login and validat ecredentials

* chore(fix): e2e update css selectors

* test(fix): replace Object.values by supported syntax on node6
This commit is contained in:
Juan Picado @jotadeveloper 2018-09-06 21:26:54 +02:00 committed by GitHub
parent ec186794c6
commit 3639557118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 416 additions and 208 deletions

@ -15,6 +15,8 @@
"verdaccio": "./bin/verdaccio" "verdaccio": "./bin/verdaccio"
}, },
"dependencies": { "dependencies": {
"@material-ui/core": "3.0.2",
"@material-ui/icons": "2.0.3",
"@verdaccio/file-locking": "0.0.7", "@verdaccio/file-locking": "0.0.7",
"@verdaccio/local-storage": "1.2.0", "@verdaccio/local-storage": "1.2.0",
"@verdaccio/streams": "1.0.0", "@verdaccio/streams": "1.0.0",
@ -82,7 +84,6 @@
"codecov": "3.0.4", "codecov": "3.0.4",
"cross-env": "5.1.4", "cross-env": "5.1.4",
"css-loader": "0.28.10", "css-loader": "0.28.10",
"element-react": "1.4.8",
"element-theme-default": "1.4.13", "element-theme-default": "1.4.13",
"enzyme": "3.3.0", "enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1", "enzyme-adapter-react-16": "1.1.1",

@ -1,19 +1,15 @@
import React, {Component} from 'react'; import React, { Component } from 'react';
import isNil from 'lodash/isNil'; import isNil from 'lodash/isNil';
import 'element-theme-default'; import 'element-theme-default';
import {i18n} from 'element-react';
import locale from 'element-react/src/locale/lang/en';
import storage from './utils/storage'; import storage from './utils/storage';
import logo from './utils/logo'; import logo from './utils/logo';
import {makeLogin, isTokenExpire} from './utils/login'; import { makeLogin, isTokenExpire } from './utils/login';
import Header from './components/Header'; import Header from './components/Header';
import Footer from './components/Footer'; import Footer from './components/Footer';
import LoginModal from './components/Login'; import LoginModal from './components/Login';
i18n.use(locale);
import Route from './router'; import Route from './router';
import './styles/main.scss'; import './styles/main.scss';
@ -52,7 +48,7 @@ export default class App extends Component {
this.handleLogout(); this.handleLogout();
} else { } else {
this.setState({ this.setState({
user: {username, token}, user: { username, token },
isUserLoggedIn: true isUserLoggedIn: true
}); });
} }
@ -60,7 +56,7 @@ export default class App extends Component {
async loadLogo() { async loadLogo() {
const logoUrl = await logo(); const logoUrl = await logo();
this.setState({logoUrl}); this.setState({ logoUrl });
} }
/** /**
@ -79,7 +75,7 @@ export default class App extends Component {
* Required by: <Header /> * Required by: <Header />
*/ */
async doLogin(usernameValue, passwordValue) { async doLogin(usernameValue, passwordValue) {
const {username, token, error} = await makeLogin( const { username, token, error } = await makeLogin(
usernameValue, usernameValue,
passwordValue passwordValue
); );
@ -152,12 +148,12 @@ export default class App extends Component {
} }
render() { render() {
const {isUserLoggedIn} = this.state; const { isUserLoggedIn } = this.state;
return ( return (
<div className="page-full-height"> <div className="page-full-height">
{this.renderHeader()} {this.renderHeader()}
{this.renderLoginModal()} {this.renderLoginModal()}
<Route isUserLoggedIn={isUserLoggedIn} /> <Route isUserLoggedIn={isUserLoggedIn} />
<Footer /> <Footer />
</div> </div>
); );

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Button} from 'element-react'; import Button from '@material-ui/core/Button';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import {getRegistryURL} from '../../utils/url'; import { getRegistryURL } from '../../utils/url';
import classes from './header.scss'; import classes from './header.scss';
import './logo.png'; import './logo.png';
@ -10,8 +10,8 @@ const Header = ({
logo = '', logo = '',
scope = '', scope = '',
username = '', username = '',
handleLogout = () => {}, handleLogout = () => { },
toggleLoginModal = () => {} toggleLoginModal = () => { }
}) => { }) => {
const registryUrl = getRegistryURL(); const registryUrl = getRegistryURL();
return ( return (
@ -37,20 +37,21 @@ const Header = ({
</span> </span>
<Button <Button
className={`${classes.headerButton} header-button-logout`} className={`${classes.headerButton} header-button-logout`}
type="danger" color="primary"
onClick={handleLogout} onClick={handleLogout}
> >
Logout Logout
</Button> </Button>
</div> </div>
) : ( ) : (
<Button <Button
className={`${classes.headerButton} header-button-login`} className={`${classes.headerButton} header-button-login`}
onClick={toggleLoginModal} color="primary"
> onClick={toggleLoginModal}
Login >
Login
</Button> </Button>
)} )}
</div> </div>
</div> </div>
</header> </header>

@ -1,6 +1,18 @@
import React, {Component, createRef} from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Form, Button, Dialog, Input, Alert} from 'element-react'; import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import SnackbarContent from '@material-ui/core/SnackbarContent';
import ErrorIcon from '@material-ui/icons/Error';
import InputLabel from '@material-ui/core/InputLabel';
import Input from '@material-ui/core/Input';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import classes from "./login.scss";
export default class LoginModal extends Component { export default class LoginModal extends Component {
static propTypes = { static propTypes = {
@ -15,141 +27,174 @@ export default class LoginModal extends Component {
error: {}, error: {},
onCancel: () => {}, onCancel: () => {},
onSubmit: () => {} onSubmit: () => {}
}; }
state = {
form: {
username: '',
password: ''
},
rules: {
username: [
{
required: true,
message: 'Please input the username',
trigger: 'change'
}
],
password: [
{
required: true,
message: 'Please input the password',
trigger: 'change'
}
]
}
};
constructor(props) { constructor(props) {
super(props); super(props);
this.formRef = createRef();
this.submitCredentials = this.submitCredentials.bind(this); this.submitCredentials = this.submitCredentials.bind(this);
this.setCredentials = this.setCredentials.bind(this); this.setCredentials = this.setCredentials.bind(this);
this.validateCredentials = this.validateCredentials.bind(this);
this.state = {
form: {
username: {
required: true,
pristine: true,
helperText: 'Field required',
value: ''
},
password: {
required: true,
pristine: true,
helperText: 'Field required',
value: ''
},
},
error: props.error
};
} }
/** /**
* set login modal's username and password to current state * set login modal's username and password to current state
* Required by: <LoginModal /> * Required to login
*/ */
setCredentials(key, value) { setCredentials(name, e) {
this.setState( this.setState({
(prevState) => ({ form: {
form: {...prevState.form, [key]: value} ...this.state.form,
}) [name]: {
); ...this.state.form[name],
} value: e.target.value,
pristine: false
/** }
* Clears the username and password field.
*/
handleReset() {
this.formRef.current.resetFields();
}
submitCredentials(event) {
// prevents default submit behavior
event.preventDefault();
this.formRef.current.validate((valid) => {
if (valid) {
const {username, password} = this.state.form;
this.props.onSubmit(username, password);
this.setState({
form: {username}
});
} }
return false;
}); });
} }
renderLoginError({type, title, description} = {}) { validateCredentials(event) {
return type ? ( // prevents default submit behavior
<Alert event.preventDefault();
title={title}
type={type} this.setState({
description={description} form: Object.keys(this.state.form).reduce((acc, key) => ({
showIcon={true} ...acc,
closable={false} ...{ [key]: {...this.state.form[key], pristine: false } }
style={{lineHeight: '10px'}} }), {})
}, () => {
if (!Object.keys(this.state.form).some(id => !this.state.form[id])) {
this.submitCredentials();
}
});
}
async submitCredentials() {
const { form: { username, password } } = this.state;
await this.props.onSubmit(username.value, password.value);
// let's wait for API response and then set
// username and password filed to empty state
this.setState({
form: Object.keys(this.state.form).reduce((acc, key) => ({
...acc,
...{ [key]: {...this.state.form[key], value: "", pristine: true } }
}), {})
});
}
renderLoginError({ type, title, description } = {}) {
return type === 'error' && (
<SnackbarContent
className={classes.loginError}
aria-describedby="client-snackbar"
message={
<div
id="client-snackbar"
className={classes.loginErrorMsg}
>
<ErrorIcon className={classes.loginIcon} />
<span>
<div><strong>{title}</strong></div>
<div>{description}</div>
</span>
</div>
}
/> />
) : (
''
); );
} }
render() { render() {
const {visibility, onCancel, error} = this.props; const { visibility, onCancel, error } = this.props;
const {username, password} = this.state.form; const { form: { username, password } } = this.state;
return ( return (
<div className="login-dialog"> <div className="login">
<Dialog <Dialog
title="Login" onClose={onCancel}
size="tiny" open={visibility}
visible={visibility} className="login-dialog"
onCancel={onCancel} maxWidth="xs"
aria-labelledby="login-dialog"
fullWidth
> >
<Dialog.Body> <DialogTitle id="login-dialog">Login</DialogTitle>
<Form <DialogContent>
className="login-form" {this.renderLoginError(error)}
ref={this.formRef} <FormControl
model={this.state.form} error={!username.value && !username.pristine}
rules={this.state.rules} aria-describedby='username'
required={username.required}
fullWidth
> >
<Form.Item> <InputLabel htmlFor="username">Username</InputLabel>
{this.renderLoginError(error)} <Input
</Form.Item> id="username"
<Form.Item prop="username" labelPosition="top"> value={username.value}
<Input onChange={this.setCredentials.bind(this, 'username')}
name="username" placeholder="Your username"
placeholder="Type your username" />
value={username} {!username.value && !username.pristine && (
onChange={this.setCredentials.bind(this, 'username')} <FormHelperText id='username-error'>
/> {username.helperText}
</Form.Item> </FormHelperText>
<Form.Item prop="password"> )}
<Input </FormControl>
name="password" <FormControl
type="password" error={!password.value && !password.pristine}
placeholder="Type your password" aria-describedby='password'
value={password} required={password.required}
onChange={this.setCredentials.bind(this, 'password')} style={{ marginTop: '8px' }}
/> fullWidth
</Form.Item> >
<Form.Item style={{float: 'right'}}> <InputLabel htmlFor="password">Password</InputLabel>
<Button onClick={onCancel} className="cancel-login-button"> <Input
Cancel id="password"
</Button> type="password"
<Button value={password.value}
nativeType="submit" onChange={this.setCredentials.bind(this, 'password')}
className="login-button" placeholder="Your strong password"
onClick={this.submitCredentials} />
> {!password.value && !password.pristine && (
Login <FormHelperText id='password-error'>
</Button> {password.helperText}
</Form.Item> </FormHelperText>
</Form> )}
</Dialog.Body> </FormControl>
</DialogContent>
<DialogActions className="dialog-footer">
<Button
onClick={onCancel}
className="cancel-login-button"
color="inherit"
>
Cancel
</Button>
<Button
className="login-button"
onClick={this.validateCredentials.bind(this)}
color="inherit"
>
Login
</Button>
</DialogActions>
</Dialog> </Dialog>
</div> </div >
); );
} }
} }

@ -0,0 +1,22 @@
@import '../../styles/variables';
.loginDialog {
min-width: 300px;
}
.loginError {
background-color: $red !important;
min-width: inherit !important;
margin-bottom: 10px !important;
}
.loginErrorMsg {
display: flex;
align-items: center;
}
.loginIcon {
opacity: 0.9;
margin-right: 8px;
}

@ -1,23 +1,23 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Tag} from 'element-react'; import Chip from '@material-ui/core/Chip';
import {Link} from 'react-router-dom'; import { Link } from 'react-router-dom';
import {formatDateDistance} from '../../utils/package'; import { formatDateDistance } from '../../utils/package';
import classes from './package.scss'; import classes from './package.scss';
const Package = ({name, version, author, description, license, time, keywords}) => { const Package = ({ name, version, author, description, license, time, keywords }) => {
return (<section className={classes.package}> return (<section className={classes.package}>
<Link to={`detail/${name}`}> <Link to={`detail/${name}`}>
<div className={classes.header}> <div className={classes.header}>
<div className={classes.title}> <div className={classes.title}>
<h1> <h1>
{name} <Tag type="gray">v{version}</Tag> {name} <Chip label={`v${version}`} />
</h1> </h1>
</div> </div>
<div role="author" className={classes.author}> <div role="author" className={classes.author}>
{ author ? `By: ${author}`: ''} {author ? `By: ${author}` : ''}
</div> </div>
</div> </div>
<div className={classes.footer}> <div className={classes.footer}>
@ -27,9 +27,11 @@ const Package = ({name, version, author, description, license, time, keywords})
</div> </div>
<div className={classes.tags}> <div className={classes.tags}>
{keywords && keywords.map((keyword, index) => ( {keywords && keywords.map((keyword, index) => (
<Tag key={index} type="gray"> <Chip
{keyword} key={index}
</Tag> label={keyword}
className={classes.tag}
/>
))} ))}
</div> </div>
<div className={classes.details}> <div className={classes.details}>

@ -27,6 +27,9 @@ .package {
margin-right: 5px; margin-right: 5px;
} }
} }
.tag {
margin: 4px;
}
} }
.details { .details {

@ -1,6 +1,6 @@
import React, {Component} from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Loading} from 'element-react'; import CircularProgress from '@material-ui/core/CircularProgress';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import PackageDetail from '../../components/PackageDetail'; import PackageDetail from '../../components/PackageDetail';
@ -10,8 +10,6 @@ import API from '../../utils/api';
import classes from './detail.scss'; import classes from './detail.scss';
import PackageSidebar from '../../components/PackageSidebar/index'; import PackageSidebar from '../../components/PackageSidebar/index';
const loadingMessage = 'Loading...';
export default class Detail extends Component { export default class Detail extends Component {
static propTypes = { static propTypes = {
match: PropTypes.object, match: PropTypes.object,
@ -66,12 +64,12 @@ export default class Detail extends Component {
} }
render() { render() {
const {notFound, readMe} = this.state; const { notFound, readMe } = this.state;
if (notFound) { if (notFound) {
return <NotFound pkg={this.packageName} />; return <NotFound pkg={this.packageName} />;
} else if (isEmpty(readMe)) { } else if (isEmpty(readMe)) {
return <Loading text={loadingMessage} />; return <CircularProgress size={50} />;
} }
return ( return (
<div className={classes.twoColumn}> <div className={classes.twoColumn}>

@ -0,0 +1,16 @@
@import '../../styles/variables';
.alertError {
background-color: $red !important;
min-width: inherit !important;
}
.alertErrorMsg {
display: flex;
align-items: center;
}
.alertIcon {
opacity: 0.9;
margin-right: 8px;
}

@ -1,6 +1,13 @@
import React, {Component, Fragment} from 'react'; import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Loading, MessageBox} from 'element-react'; import CircularProgress from '@material-ui/core/CircularProgress';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import SnackbarContent from '@material-ui/core/SnackbarContent';
import ErrorIcon from '@material-ui/icons/Error';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
@ -9,13 +16,20 @@ import API from '../../utils/api';
import PackageList from '../../components/PackageList'; import PackageList from '../../components/PackageList';
import Search from '../../components/Search'; import Search from '../../components/Search';
export default class Home extends Component { import classes from "./home.scss";
class Home extends Component {
static propTypes = { static propTypes = {
children: PropTypes.element, children: PropTypes.element,
isUserLoggedIn: PropTypes.bool isUserLoggedIn: PropTypes.bool
}; };
state = { state = {
showAlertDialog: false,
alertDialogContent: {
title: '',
message: ''
},
loading: true, loading: true,
fistTime: true, fistTime: true,
query: '' query: ''
@ -24,6 +38,8 @@ export default class Home extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleSearchInput = this.handleSearchInput.bind(this); this.handleSearchInput = this.handleSearchInput.bind(this);
this.handleShowAlertDialog = this.handleShowAlertDialog.bind(this);
this.handleCloseAlertDialog = this.handleCloseAlertDialog.bind(this);
this.searchPackage = debounce(this.searchPackage, 800); this.searchPackage = debounce(this.searchPackage, 800);
} }
@ -61,8 +77,7 @@ export default class Home extends Component {
}); });
} }
} catch (error) { } catch (error) {
MessageBox.msgbox({ this.handleShowAlertDialog({
type: 'error',
title: 'Warning', title: 'Warning',
message: `Unable to load package list: ${error.message}` message: `Unable to load package list: ${error.message}`
}); });
@ -82,14 +97,66 @@ export default class Home extends Component {
}); });
} }
} catch (err) { } catch (err) {
MessageBox.msgbox({ this.handleShowAlertDialog({
type: 'error',
title: 'Warning', title: 'Warning',
message: 'Unable to get search result, please try again later.' message: 'Unable to get search result, please try again later.'
}); });
} }
} }
renderAlertDialog() {
return (
<Dialog
open={this.state.showAlertDialog}
onClose={this.handleCloseAlertDialog}
aria-labelledby="alert-dialog-title"
>
<DialogTitle id="alert-dialog-title">
{this.state.alertDialogContent.title}
</DialogTitle>
<DialogContent>
<SnackbarContent
className={classes.alertError}
aria-describedby="client-snackbar"
message={
<div
id="client-snackbar"
className={classes.alertErrorMsg}
>
<ErrorIcon className={classes.alertIcon} />
<span>
{this.state.alertDialogContent.message}
</span>
</div>
}
/>
</DialogContent>
<DialogActions>
<Button
onClick={this.handleCloseAlertDialog}
color="primary"
autoFocus
>
Ok
</Button>
</DialogActions>
</Dialog>
);
}
handleShowAlertDialog(content) {
this.setState({
showAlertDialog: true,
alertDialogContent: content
});
};
handleCloseAlertDialog() {
this.setState({
showAlertDialog: false
});
};
handleSearchInput(e) { handleSearchInput(e) {
this.setState({ this.setState({
query: e.target.value.trim() query: e.target.value.trim()
@ -101,15 +168,16 @@ export default class Home extends Component {
} }
render() { render() {
const {packages, loading} = this.state; const { packages, loading } = this.state;
return ( return (
<Fragment> <Fragment>
{this.renderSearchBar()} {this.renderSearchBar()}
{loading ? ( {loading ? (
<Loading text="Loading..." /> <CircularProgress size={50} />
) : ( ) : (
<PackageList help={isEmpty(packages) === true} packages={packages} /> <PackageList help={isEmpty(packages) === true} packages={packages} />
)} )}
{this.renderAlertDialog()}
</Fragment> </Fragment>
); );
} }
@ -121,3 +189,5 @@ export default class Home extends Component {
return <Search handleSearchInput={this.handleSearchInput} />; return <Search handleSearchInput={this.handleSearchInput} />;
} }
} }
export default Home;

@ -3,6 +3,7 @@
$black: #000; $black: #000;
$white: #fff; $white: #fff;
$red: #d32f2f;
$grey: #808080; $grey: #808080;
$grey-light: #d3d3d3; $grey-light: #d3d3d3;
$grey-dark: #a9a9a9; $grey-dark: #a9a9a9;

@ -1,16 +1,25 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en-us"> <html lang="en-us">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title> <title>
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.verdaccioURL %>/-/static/favicon.ico"/> <%= htmlWebpackPlugin.options.title %>
<meta name="viewport" content="width=device-width, initial-scale=1"> </title>
<link rel="icon" type="image/png" href="<%= htmlWebpackPlugin.options.verdaccioURL %>/-/static/favicon.ico" />
<!-- Robot Font -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
<!-- Material Icons Font -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script> <script>
window.VERDACCIO_API_URL = '<%= htmlWebpackPlugin.options.verdaccioURL %>/-/verdaccio/'; window.VERDACCIO_API_URL = '<%= htmlWebpackPlugin.options.verdaccioURL %>/-/verdaccio/';
window.VERDACCIO_SCOPE = '<%= htmlWebpackPlugin.options.scope %>'; window.VERDACCIO_SCOPE = '<%= htmlWebpackPlugin.options.scope %>';
</script> </script>
</head> </head>
<body class="body"> <body class="body">
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

@ -25,10 +25,10 @@ describe('/ (Verdaccio Page)', () => {
await clickElement('header button'); await clickElement('header button');
await page.waitFor(500); await page.waitFor(500);
// we fill the sign in form // we fill the sign in form
const signInDialog = await page.$('.el-dialog'); const signInDialog = await page.$('.login-dialog');
const userInput = await signInDialog.$('input[type=text]'); const userInput = await signInDialog.$('#username');
expect(userInput).not.toBeNull(); expect(userInput).not.toBeNull();
const passInput = await signInDialog.$('input[type=password]'); const passInput = await signInDialog.$('#password');
expect(passInput).not.toBeNull(); expect(passInput).not.toBeNull();
await userInput.type('test', {delay: 100}); await userInput.type('test', {delay: 100});
await passInput.type('test', {delay: 100}); await passInput.type('test', {delay: 100});
@ -86,7 +86,7 @@ describe('/ (Verdaccio Page)', () => {
it('should click on sign in button', async () => { it('should click on sign in button', async () => {
const signInButton = await page.$('header button'); const signInButton = await page.$('header button');
await signInButton.click(); await signInButton.click();
const signInDialog = await page.$('.login-dialog .el-dialog__wrapper'); const signInDialog = await page.$('#login-dialog');
expect(signInDialog).not.toBeNull(); expect(signInDialog).not.toBeNull();
}) })

@ -35,7 +35,7 @@ describe('basic system test', () => {
url: 'http://localhost:' + port + '/', url: 'http://localhost:' + port + '/',
}, function(err, res, body) { }, function(err, res, body) {
expect(err).toBeNull(); expect(err).toBeNull();
expect(body).toMatch(/<title>Verdaccio<\/title>/); expect(body).toMatch(/Verdaccio/);
done(); done();
}); });
}); });

@ -4,7 +4,7 @@ import storage from '../../../src/webui/utils/storage';
import App from '../../../src/webui/app'; import App from '../../../src/webui/app';
import { API_ERROR } from '../../../src/lib/constants'; import { API_ERROR } from '../../../src/lib/constants';
import {generateTokenWithTimeRange} from './components/__mocks__/token'; import { generateTokenWithTimeRange } from './components/__mocks__/token';
jest.mock('../../../src/webui/utils/storage', () => { jest.mock('../../../src/webui/utils/storage', () => {
class LocalStorageMock { class LocalStorageMock {
@ -29,8 +29,6 @@ jest.mock('../../../src/webui/utils/storage', () => {
jest.mock('element-theme-default', () => ({})); jest.mock('element-theme-default', () => ({}));
jest.mock('element-react/src/locale/lang/en', () => ({}));
jest.mock('../../../src/webui/utils/api', () => ({ jest.mock('../../../src/webui/utils/api', () => ({
request: require('./components/__mocks__/api').default.request request: require('./components/__mocks__/api').default.request
})); }));
@ -58,7 +56,7 @@ describe('App', () => {
}); });
it('isUserAlreadyLoggedIn: token already available in storage', async () => { it('isUserAlreadyLoggedIn: token already available in storage', async () => {
storage.setItem('username', 'verdaccio'); storage.setItem('username', 'verdaccio');
storage.setItem('token', generateTokenWithTimeRange(24)); storage.setItem('token', generateTokenWithTimeRange(24));
const { isUserAlreadyLoggedIn } = wrapper.instance(); const { isUserAlreadyLoggedIn } = wrapper.instance();

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Header /> component shallow should load header component in login state 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/#/\\"><img src=\\"logo.png\\" class=\\"logo\\"/></a><figure>npm set scope:registry http://localhost<br/>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><div class=\\"user-logged\\"><span class=\\"user-logged-greetings usernameField\\">Hi, Verdaccio</span><button class=\\"el-button el-button--danger headerButton header-button-logout\\" type=\\"button\\"><span>Logout</span></button></div></div></div></header>"`; exports[`<Header /> component shallow should load header component in login state 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/#/\\"><img src=\\"logo.png\\" class=\\"logo\\"/></a><figure>npm set scope:registry http://localhost<br/>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><div class=\\"user-logged\\"><span class=\\"user-logged-greetings usernameField\\">Hi, Verdaccio</span><button tabindex=\\"0\\" class=\\"MuiButtonBase-root-27 MuiButton-root-1 MuiButton-text-3 MuiButton-textPrimary-4 MuiButton-flat-6 MuiButton-flatPrimary-7 headerButton header-button-logout\\" type=\\"button\\"><span class=\\"MuiButton-label-2\\">Logout</span><span class=\\"MuiTouchRipple-root-30\\"></span></button></div></div></div></header>"`;
exports[`<Header /> component shallow should load header component in logout state 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/#/\\"><img src=\\"logo.png\\" class=\\"logo\\"/></a><figure>npm set scope: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></header>"`; exports[`<Header /> component shallow should load header component in logout state 1`] = `"<header class=\\"header\\"><div class=\\"headerWrap\\"><a href=\\"/#/\\"><img src=\\"logo.png\\" class=\\"logo\\"/></a><figure>npm set scope:registry http://localhost<br/>npm adduser --registry http://localhost</figure><div class=\\"headerRight\\"><button tabindex=\\"0\\" class=\\"MuiButtonBase-root-27 MuiButton-root-1 MuiButton-text-3 MuiButton-textPrimary-4 MuiButton-flat-6 MuiButton-flatPrimary-7 headerButton header-button-login\\" type=\\"button\\"><span class=\\"MuiButton-label-2\\">Login</span><span class=\\"MuiTouchRipple-root-30\\"></span></button></div></div></header>"`;

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LoginModal /> should load the component in default state 1`] = `"<div class=\\"login-dialog\\"><div><div style=\\"z-index: 1013;\\" 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><div class=\\"el-dialog__body\\"><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-form-item\\"><div class=\\"el-form-item__content\\"></div></div><div class=\\"el-form-item is-required\\"><div class=\\"el-form-item__content\\"><div class=\\"el-input\\"><input name=\\"username\\" placeholder=\\"Type your username\\" type=\\"text\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div></div></div><div class=\\"el-form-item is-required\\"><div class=\\"el-form-item__content\\"><div class=\\"el-input\\"><input name=\\"password\\" placeholder=\\"Type your password\\" type=\\"password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div></div></div><div style=\\"float: right;\\" class=\\"el-form-item\\"><div class=\\"el-form-item__content\\"><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></div></form></div></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012;\\"></div></div></div>"`; exports[`<LoginModal /> should load the component in default state 1`] = `"<div class=\\"login\\"></div>"`;
exports[`<LoginModal /> should load the component with props 1`] = `"<div class=\\"login-dialog\\"><div><div style=\\"z-index: 1013;\\" 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><div class=\\"el-dialog__body\\"><form class=\\"el-form el-form--label-right login-form\\"><div class=\\"el-form-item\\"><div class=\\"el-form-item__content\\"><div style=\\"line-height: 10px;\\" class=\\"el-alert el-alert--error\\"><i class=\\"el-alert__icon el-icon-circle-cross is-big\\"></i><div class=\\"el-alert__content\\"><span class=\\"el-alert__title is-bold\\">Error Title</span><p class=\\"el-alert__description\\">Error Description</p><i class=\\"el-alert__closebtn el-icon-close\\" style=\\"display: none;\\"></i></div></div></div></div><div class=\\"el-form-item is-required\\"><div class=\\"el-form-item__content\\"><div class=\\"el-input\\"><input name=\\"username\\" placeholder=\\"Type your username\\" type=\\"text\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div></div></div><div class=\\"el-form-item is-required\\"><div class=\\"el-form-item__content\\"><div class=\\"el-input\\"><input name=\\"password\\" placeholder=\\"Type your password\\" type=\\"password\\" class=\\"el-input__inner\\" autocomplete=\\"off\\" value=\\"\\"></div></div></div><div style=\\"float: right;\\" class=\\"el-form-item\\"><div class=\\"el-form-item__content\\"><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></div></form></div></div></div><div class=\\"v-modal\\" style=\\"z-index: 1012;\\"></div></div></div>"`; exports[`<LoginModal /> should load the component with props 1`] = `"<div class=\\"login\\"></div>"`;

@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Package /> component should load the component 1`] = `"<section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published about 1 month ago</div><div class=\\"license\\">MIT</div></div></a></section>"`; exports[`<Package /> component should load the component 1`] = `"<section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <div role=\\"button\\" class=\\"MuiChip-root-1\\" tabindex=\\"-1\\"><span class=\\"MuiChip-label-17\\">v1.0.0</span></div></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published about 1 month ago</div><div class=\\"license\\">MIT</div></div></a></section>"`;
exports[`<Package /> component should load the component without author 1`] = `"<section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1></div><div role=\\"author\\" class=\\"author\\"></div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published about 1 month ago</div><div class=\\"license\\">MIT</div></div></a></section>"`; exports[`<Package /> component should load the component without author 1`] = `"<section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <div role=\\"button\\" class=\\"MuiChip-root-1\\" tabindex=\\"-1\\"><span class=\\"MuiChip-label-17\\">v1.0.0</span></div></h1></div><div role=\\"author\\" class=\\"author\\"></div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published about 1 month ago</div><div class=\\"license\\">MIT</div></div></a></section>"`;

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PackageList /> component should load the component with packages 1`] = `"<div class=\\"package-list-items\\"><div class=\\"pkgContainer\\"><h1 class=\\"listTitle\\">Available Packages</h1><li><section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <span class=\\"el-tag el-tag--gray\\">v1.0.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published less than a minute ago</div><div class=\\"license\\"></div></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/abc\\"><div class=\\"header\\"><div class=\\"title\\"><h1>abc <span class=\\"el-tag el-tag--gray\\">v1.0.1</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Rose</div></div><div class=\\"footer\\"><p class=\\"description\\">abc description</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published less than a minute ago</div><div class=\\"license\\"></div></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/xyz\\"><div class=\\"header\\"><div class=\\"title\\"><h1>xyz <span class=\\"el-tag el-tag--gray\\">v1.1.0</span></h1></div><div role=\\"author\\" class=\\"author\\">By: Martin</div></div><div class=\\"footer\\"><p class=\\"description\\">xyz description</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\"></div><div class=\\"license\\"></div></div></a></section></li></div></div>"`; exports[`<PackageList /> component should load the component with packages 1`] = `"<div class=\\"package-list-items\\"><div class=\\"pkgContainer\\"><h1 class=\\"listTitle\\">Available Packages</h1><li><section class=\\"package\\"><a href=\\"detail/verdaccio\\"><div class=\\"header\\"><div class=\\"title\\"><h1>verdaccio <div role=\\"button\\" class=\\"MuiChip-root-1\\" tabindex=\\"-1\\"><span class=\\"MuiChip-label-17\\">v1.0.0</span></div></h1></div><div role=\\"author\\" class=\\"author\\">By: Sam</div></div><div class=\\"footer\\"><p class=\\"description\\">Private NPM repository</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published less than a minute ago</div><div class=\\"license\\"></div></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/abc\\"><div class=\\"header\\"><div class=\\"title\\"><h1>abc <div role=\\"button\\" class=\\"MuiChip-root-1\\" tabindex=\\"-1\\"><span class=\\"MuiChip-label-17\\">v1.0.1</span></div></h1></div><div role=\\"author\\" class=\\"author\\">By: Rose</div></div><div class=\\"footer\\"><p class=\\"description\\">abc description</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\">Published less than a minute ago</div><div class=\\"license\\"></div></div></a></section></li><li><section class=\\"package\\"><a href=\\"detail/xyz\\"><div class=\\"header\\"><div class=\\"title\\"><h1>xyz <div role=\\"button\\" class=\\"MuiChip-root-1\\" tabindex=\\"-1\\"><span class=\\"MuiChip-label-17\\">v1.1.0</span></div></h1></div><div role=\\"author\\" class=\\"author\\">By: Martin</div></div><div class=\\"footer\\"><p class=\\"description\\">xyz description</p></div><div class=\\"tags\\"></div><div class=\\"details\\"><div class=\\"homepage\\"></div><div class=\\"license\\"></div></div></a></section></li></div></div>"`;

@ -2,6 +2,22 @@ import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import LoginModal from '../../../../src/webui/components/Login'; import LoginModal from '../../../../src/webui/components/Login';
const eventUsername = {
target: {
value: 'xyz'
}
}
const eventPassword = {
target: {
value: '1234'
}
}
const event = {
preventDefault: jest.fn()
};
describe('<LoginModal />', () => { describe('<LoginModal />', () => {
it('should load the component in default state', () => { it('should load the component in default state', () => {
const wrapper = mount(<LoginModal />); const wrapper = mount(<LoginModal />);
@ -16,8 +32,8 @@ describe('<LoginModal />', () => {
title: 'Error Title', title: 'Error Title',
description: 'Error Description' description: 'Error Description'
}, },
onCancel: () => {}, onCancel: () => { },
onSubmit: () => {} onSubmit: () => { }
}; };
const wrapper = mount(<LoginModal {...props} />); const wrapper = mount(<LoginModal {...props} />);
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
@ -32,12 +48,12 @@ describe('<LoginModal />', () => {
description: 'Error Description' description: 'Error Description'
}, },
onCancel: jest.fn(), onCancel: jest.fn(),
onSubmit: () => {} onSubmit: () => { }
}; };
const wrapper = mount(<LoginModal {...props} />); const wrapper = mount(<LoginModal {...props} />);
wrapper.find('button.cancel-login-button').simulate('click'); wrapper.find('.dialog-footer > .cancel-login-button').simulate('click');
expect(props.onCancel).toHaveBeenCalled(); expect(props.onCancel).toHaveBeenCalled();
wrapper.find('.el-dialog__headerbtn > .el-dialog__close').simulate('click'); wrapper.find('.dialog-footer > .login-button').simulate('click');
expect(props.onCancel).toHaveBeenCalled(); expect(props.onCancel).toHaveBeenCalled();
}); });
@ -51,14 +67,14 @@ describe('<LoginModal />', () => {
const wrapper = mount(<LoginModal {...props} />); const wrapper = mount(<LoginModal {...props} />);
const { setCredentials } = wrapper.instance(); const { setCredentials } = wrapper.instance();
expect(setCredentials('username', 'xyz')).toBeUndefined(); expect(setCredentials('username', eventUsername)).toBeUndefined();
expect(wrapper.state('form').username).toEqual('xyz'); expect(wrapper.state('form').username.value).toEqual('xyz');
expect(setCredentials('password', '1234')).toBeUndefined(); expect(setCredentials('password', eventPassword)).toBeUndefined();
expect(wrapper.state('form').password).toEqual('1234'); expect(wrapper.state('form').password.value).toEqual('1234');
}); });
it('submitCredential: should call the onSubmit', () => { it('validateCredentials: should validate credentials', async () => {
const props = { const props = {
visibility: true, visibility: true,
error: {}, error: {},
@ -66,19 +82,46 @@ describe('<LoginModal />', () => {
onSubmit: jest.fn() onSubmit: jest.fn()
}; };
const event = {
preventDefault: jest.fn()
};
const wrapper = mount(<LoginModal {...props} />); const wrapper = mount(<LoginModal {...props} />);
const { submitCredentials } = wrapper.instance(); const instance = wrapper.instance();
wrapper
.find('input[type="text"]') instance.submitCredentials = jest.fn();
.simulate('change', { target: { value: 'sam' } }); const { validateCredentials, setCredentials, submitCredentials } = instance;
wrapper
.find('input[type="password"]') expect(setCredentials('username', eventUsername)).toBeUndefined();
.simulate('change', { target: { value: '1234' } }); expect(wrapper.state('form').username.value).toEqual('xyz');
submitCredentials(event);
expect(setCredentials('password', eventPassword)).toBeUndefined();
expect(wrapper.state('form').password.value).toEqual('1234');
validateCredentials(event);
expect(event.preventDefault).toHaveBeenCalled(); expect(event.preventDefault).toHaveBeenCalled();
expect(props.onSubmit).toHaveBeenCalledWith('sam', '1234'); expect(wrapper.state('form').username.pristine).toEqual(false);
expect(wrapper.state('form').password.pristine).toEqual(false);
expect(submitCredentials).toHaveBeenCalledTimes(1);
}); });
it('submitCredentials: should submit credentials', async () => {
const props = {
onSubmit: jest.fn()
};
const wrapper = mount(<LoginModal {...props} />);
const { setCredentials, submitCredentials } = wrapper.instance();
expect(setCredentials('username', eventUsername)).toBeUndefined();
expect(wrapper.state('form').username.value).toEqual('xyz');
expect(setCredentials('password', eventPassword)).toBeUndefined();
expect(wrapper.state('form').password.value).toEqual('1234');
await submitCredentials();
expect(props.onSubmit).toHaveBeenCalledWith('xyz', '1234');
expect(wrapper.state('form').username.value).toEqual('');
expect(wrapper.state('form').username.pristine).toEqual(true);
expect(wrapper.state('form').password.value).toEqual('');
expect(wrapper.state('form').password.pristine).toEqual(true);
});
}); });

@ -6,6 +6,7 @@ import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import Package from '../../../../src/webui/components/Package/index'; import Package from '../../../../src/webui/components/Package/index';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import Chip from '@material-ui/core/Chip';
/** /**
* Generates one month back date from current time * Generates one month back date from current time
@ -29,14 +30,17 @@ describe('<Package /> component', () => {
}; };
const wrapper = mount( const wrapper = mount(
<BrowserRouter> <BrowserRouter>
<Package {...props}/> <Package {...props} />
</BrowserRouter> </BrowserRouter>
); );
const version =
wrapper.findWhere(node => node.is(Chip) && node.prop('label') === 'v1.0.0');
// integration expectations // integration expectations
expect(wrapper.find('a').prop('href')).toEqual('detail/verdaccio'); expect(wrapper.find('a').prop('href')).toEqual('detail/verdaccio');
expect(wrapper.find('h1').text()).toEqual('verdaccio v1.0.0'); expect(wrapper.find('h1').text()).toEqual('verdaccio v1.0.0');
expect(wrapper.find('.el-tag--gray').text()).toEqual('v1.0.0'); expect(version.exists()).toBe(true);
expect( expect(
wrapper.find('div').filterWhere(n => n.prop('role') === 'author') wrapper.find('div').filterWhere(n => n.prop('role') === 'author')
.text() .text()
@ -68,4 +72,3 @@ describe('<Package /> component', () => {
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
}); });
}); });

BIN
yarn.lock

Binary file not shown.