Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvements on header component + adding tests to getRouteLabel func… #90

Open
wants to merge 2 commits into
base: auth-overlay
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ imports/ui/service-worker-2.js
imports/ui/components/smart/pwa/subscribe-btns/
imports/ui/layouts/default/
TODO.txt
coverage/
6 changes: 4 additions & 2 deletions client/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,18 @@
<div class="flex justify-between items-center">
<div class="flex justify-center items-center header__burger">
<svg class="menu__icon no--select" width="24px" height="24px" viewBox="0 0 48 48" fill="#fff">
<path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"></path>
<path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z" />
</svg>
</div>
<div id="header" class="center header__title no--select">
<div id="header-title" class="center header__title no--select">
<span>loading...</span>
</div>
<div class="header__avatar"></div>
</div>
</header>

<div id="burger-btn-controller"></div>

<div class="fixed top-0 bottom-0 z2 menu">
<div class="menu__header"></div>
<ul id="menu" class="menu__list"></ul>
Expand Down
9 changes: 2 additions & 7 deletions imports/api/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,8 @@ const Constants = {
AUTH_SERVICES: ['password', 'facebook'],
ALL_ROLES: ['admin', 'normal'],
ROUTES: [
{ path: '/', label: 'Home', auth: true },
{ path: '/login', label: 'Login' },
{ path: '/signup', label: 'Signup' },
{ path: '/verify-email', label: 'Verify Email' },
{ path: '/link-expired', label: 'Link Expired' },
{ path: '/forgot-password', label: 'Forgot Password' },
{ path: '/reset-password', label: 'Reset Password' },
{ path: '/', label: 'Home' },
{ path: '/admin', label: 'Admin', admin: true },
],
};

Expand Down
9 changes: 6 additions & 3 deletions imports/entry-points/client/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ async function renderAsync() {
React,
{ render },
{ default: App },
{ default: Header },
{ default: BurgerBtnController },
{ default: HeaderTitle },
{ default: Routes },
{ default: Menu },
] = await Promise.all([
import('react'),
import('react-dom'),
import('../../ui/app'),
import('../../ui/components/smart/header'),
import('../../ui/components/smart/header/burger-btn-controller'),
import('../../ui/components/smart/header/header-title'),
import('../../ui/routes'),
import('../../ui/components/smart/menu'),
]);

// Inject react app components into App's Shell
render(<App component={Header} />, document.getElementById('header'));
render(<App component={BurgerBtnController} />, document.getElementById('burger-btn-controller'));
render(<App component={HeaderTitle} />, document.getElementById('header-title'));
render(<App component={Menu} />, document.getElementById('menu'));
render(<App component={Routes} />, document.getElementById('main'));
}
Expand Down
27 changes: 13 additions & 14 deletions imports/entry-points/server/fixtures.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import { Meteor } from 'meteor/meteor';
import { Roles } from 'meteor/alanning:roles';
import { Accounts } from 'meteor/accounts-base';
import Users from '../../api/users/';

// OBSERVATION: use the following mutation to set email to verified:
// db.users.update(
// {_id: "yourUserId", "emails.address": "yourEmailGoesHere"},
// {$set: {"emails.$.verified": true }}
// )

// Insert admin user
const users = [
{ email: '[email protected]', password: '123456', roles: ['admin'] },
];
const { admins } = Meteor.settings;

users.forEach(({ email, password, roles }) => {
const userExists = Users.collection.findOne({ 'emails.address': email });
// Insert admin users
admins.forEach(({ email, roles }) => {
const user = Users.collection.findOne({ 'emails.address': email });

// In case user already exists, do nothing
if (userExists) {
if (user) {
return;
}

// Otherwise, insert user and set his role to 'admin'
const userId = Accounts.createUser({ email, password });
const userId = Accounts.createUser({ email });
Roles.addUsersToRoles(userId, roles);
});

// OBSERVATION: use the following operation to set email to verified:
// db.users.update(
// {_id: "yourUserId", "emails.address": "yourEmailGoesHere"},
// {$set: {"emails.$.verified": true }}
// )
6 changes: 3 additions & 3 deletions imports/ui/app.js → imports/ui/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import createMuiTheme from 'material-ui/styles/createMuiTheme';
import store from './redux/store.js';
import theme from './theme';
import GlobalDataProvider from './global-data-provider';
import store from '../redux/store.js';
import theme from '../theme';
import GlobalDataProvider from '../global-data-provider';

// To get started, create an ApolloClient instance and point it at your GraphQL
// server (handled in our case by meteor-apollo). By default, this client will
Expand Down
2 changes: 1 addition & 1 deletion imports/ui/app.test.js → imports/ui/app/index.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import App from './app';
import App from './index';

const Component = () => (<div>Hey</div>);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { propType } from 'graphql-anywhere';
import userFragment from '../../apollo-client/user/fragment/user';
import Constants from '../../../api/constants';
import userFragment from '../../../apollo-client/user/fragment/user';

//------------------------------------------------------------------------------
// AUX FUNCTIONS:
Expand All @@ -15,25 +12,9 @@ const showHideBurgerBtn = (curUser) => {
menuIconElement.classList[curUser ? 'add' : 'remove']('header__burger--show');
};
//------------------------------------------------------------------------------
const getRouteLabel = (pathname) => {
const route = Constants.ROUTES
// Sort routes by longest route paths first. '/' will be the last route in
// the list
.sort((r1, r2) => (
r2.path.length - r1.path.length
))
// Use regular expression to find current route based on path. '/' must be
// the last path we test, otherwise the test will always return true
.find(({ path }) => {
const reg = new RegExp(path);
return reg.test(pathname);
});
return route ? route.label : undefined;
};
//------------------------------------------------------------------------------
// COMPONENT:
//------------------------------------------------------------------------------
class Header extends React.Component {
class BurgerBtnController extends React.Component {
componentWillMount() {
const { curUser } = this.props;
// Get current user and display burger button if necessary
Expand All @@ -50,22 +31,16 @@ class Header extends React.Component {
}

render() {
const { location } = this.props;
const label = getRouteLabel(location.pathname);
return <span>{label || 'Not Found'}</span>;
return null;
}
}

Header.propTypes = {
BurgerBtnController.propTypes = {
curUser: propType(userFragment),
location: PropTypes.shape({
pathname: PropTypes.String,
}).isRequired,
};

Header.defaultProps = {
BurgerBtnController.defaultProps = {
curUser: null,
};

// withRouter provides access to location.pathname
export default withRouter(Header);
export default BurgerBtnController;
15 changes: 15 additions & 0 deletions imports/ui/components/smart/header/get-route-label.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { matchPath } from 'react-router-dom';
import Constants from '../../../../api/constants';

/**
* @summary mapping function between route pathname ('/admin') and route
* name/label ('Admin').
*/
const getRouteLabel = (pathname) => {
const route = Constants.ROUTES.find(({ path }) => (
matchPath(pathname, { path, exact: true })
));
return route ? route.label : undefined;
};

export default getRouteLabel;
16 changes: 16 additions & 0 deletions imports/ui/components/smart/header/get-route-label.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Constants from '../../../../api/constants';
import getRouteLabel from './get-route-label';

describe('getRouteLabel', () => {
it('should return the right label when existing route is provided', () => {
Constants.ROUTES.forEach((route) => {
expect(getRouteLabel(route.path)).toBe(route.label);
});
});

it('should return undefined when route is not found', () => {
expect(getRouteLabel('/some-wired-path')).toBe(undefined);
expect(getRouteLabel('/admin ')).toBe(undefined);
expect(getRouteLabel('/ ')).toBe(undefined);
});
});
41 changes: 41 additions & 0 deletions imports/ui/components/smart/header/header-title.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { propType } from 'graphql-anywhere';
import userFragment from '../../../apollo-client/user/fragment/user';
import getRouteLabel from './get-route-label';

//------------------------------------------------------------------------------
// AUX FUNCTIONS:
//------------------------------------------------------------------------------
const getHeaderTitle = ({ curUser, routeLabel }) => {
if (!routeLabel) {
return 'Not Found';
} else if (!curUser) {
return 'Login';
}
return routeLabel;
};
//------------------------------------------------------------------------------
// COMPONENT:
//------------------------------------------------------------------------------
const HeaderTitle = ({ curUser, location }) => {
// Get label for current route ('/' --> 'Home', '/b$^$%^$' --> undefined)
const routeLabel = getRouteLabel(location.pathname);
// Display label based on route and user logged in state
return <span>{getHeaderTitle({ curUser, routeLabel })}</span>;
};

HeaderTitle.propTypes = {
curUser: propType(userFragment),
location: PropTypes.shape({
pathname: PropTypes.String,
}).isRequired,
};

HeaderTitle.defaultProps = {
curUser: null,
};

// withRouter provides access to location.pathname
export default withRouter(HeaderTitle);
62 changes: 62 additions & 0 deletions imports/ui/components/smart/header/header-title.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { shallow, mount } from 'enzyme';
import Constants from '../../../../api/constants';
import HeaderTitle from './header-title';

const mockUser = {
_id: '123',
createdAt: new Date(),
services: ['email'],
emails: {
address: '[email protected]',
verified: true,
},
profile: {
name: 'John Doe',
gender: 'male',
avatar: 'bla.jpg',
},
subscriptions: [],
};

const tester = ({ path, label, curUser }) => {
const wrapper = mount(
<MemoryRouter
initialEntries={[path]}
initialIndex={0}
>
<HeaderTitle curUser={curUser} />
</MemoryRouter>,
);
expect(wrapper.find(HeaderTitle).text()).toBe(label);
};

describe('<HeaderTitle />', () => {
it('should render', () => {
const wrapper = shallow(<HeaderTitle />);
expect(wrapper.exists()).toBe(true);
});

it('should display Not Found if route does not exist', () => {
tester({
path: '/404-this-route-does-not-exist',
label: 'Not Found',
curUser: null,
});
});

it('should display Login text if user is not logged in and the route exists', () => {
tester({
path: '/',
label: 'Login',
curUser: null,
});
});

it('should display route name if user is logged in and route exists', () => {
Constants.ROUTES.forEach(({ path, label }) => {
tester({ path, label, curUser: mockUser });
});
});
});
14 changes: 10 additions & 4 deletions imports/ui/components/smart/menu.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Roles } from 'meteor/alanning:roles';
import { propType } from 'graphql-anywhere';
import userFragment from '../../apollo-client/user/fragment/user';
import Constants from '../../../api/constants';
Expand All @@ -14,11 +15,16 @@ const Menu = ({ curUser }) => {
return null;
}

// Display authenticated routes plus logout button
// Get list of routes to be displayed on the side-menu. Include admin route
// if and only if current user is admin
const routes = Constants.ROUTES
.filter(({ admin }) => (
(admin && Roles.userIsInRole(curUser._id, ['admin'])) || !admin
));

// Display menu routes plus logout button
return [
Constants.ROUTES
.filter(({ auth }) => auth)
.map(({ path, label }) => (
routes.map(({ path, label }) => (
<li key={path}>
<Link
to={path}
Expand Down
8 changes: 3 additions & 5 deletions imports/ui/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import userFragment from './apollo-client/user/fragment/user';
import {
ScrollToTop,
LoggedInRoute,
// LoggedOutRoute,
// RouteWithProps,
AdminRoute,
} from './components/smart/route-wrappers';
import LoadableWrapper from './components/dumb/loadable-wrapper';
Expand All @@ -19,24 +17,24 @@ const LoginPage = LoadableWrapper({ loader: () => import('./pages/auth/login-pag
const Routes = props => (
<ScrollToTop>
<Switch>
{/* HOME */}
<LoggedInRoute
exact
name="home"
path="/"
component={LoadableWrapper({ loader: () => import('./pages/home-page') })}
overlay={LoginPage}
{...props}
/>
{/* ADMIN */}
<AdminRoute
exact
name="admin"
path="/admin"
component={LoadableWrapper({ loader: () => import('./pages/admin/admin-page') })}
overlay={LoginPage}
{...props}
/>
{/* NOT FOUND */}
<Route
name="notFound"
component={LoadableWrapper({ loader: () => import('./pages/not-found-page') })}
/>
</Switch>
Expand Down
Loading