Merge pull request #42 from sharetribe/update-react-router2

Update React Router
This commit is contained in:
Kimmo Puputti 2017-02-15 10:41:20 +02:00 committed by GitHub
commit d41eb61be8
37 changed files with 243 additions and 245 deletions

View file

@ -16,7 +16,7 @@
"react-helmet": "^4.0.0",
"react-intl": "^2.2.3",
"react-redux": "^5.0.2",
"react-router": "4.0.0-alpha.6",
"react-router-dom": "4.0.0-beta.6",
"redux": "^3.6.0",
"redux-form": "^6.5.0",
"redux-saga": "^0.14.3",

View file

@ -25,7 +25,6 @@ const qs = require('qs');
const url = require('url');
const _ = require('lodash');
const React = require('react');
const { createServerRenderContext } = require('react-router');
const sagaEffects = require('redux-saga/effects');
const auth = require('./auth');
const sdk = require('./fakeSDK');
@ -143,22 +142,16 @@ app.use(compression());
app.use('/static', express.static(path.join(buildPath, 'static')));
app.get('*', (req, res) => {
const context = createServerRenderContext();
const context = {};
const filters = qs.parse(req.query);
// TODO fetch this asynchronously
fetchInitialState(req.url)
.then(preloadedState => {
const html = render(req.url, context, preloadedState);
const result = context.getResult();
if (result.redirect) {
res.redirect(result.redirect.pathname);
} else if (result.missed) {
// Do a second render pass with the context to clue <Miss>
// components into rendering this time.
// See: https://react-router.now.sh/ServerRouter
res.status(404).send(render(req.url, context, preloadedState));
if (context.url) {
res.redirect(context.url);
} else {
res.send(html);
}

View file

@ -1,27 +1,54 @@
import React, { PropTypes } from 'react';
import { Miss } from 'react-router';
import { RouterProvider, RoutesProvider } from './components';
import { NotFoundPage, MatchWithSubRoutes } from './containers';
import { flattenRoutes } from './routesConfiguration';
import { connect } from 'react-redux';
import { Switch, Route } from 'react-router-dom';
import { NotFoundPage } from './containers';
import { NamedRedirect } from './components';
const Routes = props => {
const flattenedRoutes = flattenRoutes(props.routes);
const matches = flattenedRoutes.map(route => <MatchWithSubRoutes key={route.name} {...route} />);
const { isAuthenticated, routes } = props;
const renderComponent = (route, matchProps) => {
const { auth, component: Component } = route;
const { match, location } = matchProps;
const canShowComponent = !auth || isAuthenticated;
return canShowComponent
? <Component params={match.params} location={location} />
: <NamedRedirect name="LogInPage" state={{ from: match.url }} />;
};
const toRouteComponent = route => (
<Route
key={route.name}
path={route.pattern}
exact={route.exactly}
render={matchProps => renderComponent(route, matchProps)}
/>
);
return (
<RouterProvider router={props.router}>
<RoutesProvider routes={props.routes}>
<div>
{matches}
<Miss component={NotFoundPage} />
</div>
</RoutesProvider>
</RouterProvider>
<Switch>
{routes.map(toRouteComponent)}
<Route component={NotFoundPage} />
</Switch>
);
};
const { any, array } = PropTypes;
const { bool, arrayOf, shape, string, func } = PropTypes;
Routes.propTypes = { router: any.isRequired, routes: array.isRequired };
Routes.propTypes = {
isAuthenticated: bool.isRequired,
routes: arrayOf(
shape({
name: string.isRequired,
pattern: string.isRequired,
exactly: bool,
strict: bool,
component: func.isRequired,
loadData: func,
}),
).isRequired,
};
export default Routes;
const mapStateToProps = state => ({ isAuthenticated: state.Auth.isAuthenticated });
export default connect(mapStateToProps)(Routes);

View file

@ -1,27 +1,29 @@
import React, { PropTypes } from 'react';
import ReactDOMServer from 'react-dom/server';
import Helmet from 'react-helmet';
import { BrowserRouter, ServerRouter } from 'react-router';
import { BrowserRouter, StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import configureStore from './store';
import Routes from './Routes';
import routesConfiguration from './routesConfiguration';
import { RoutesProvider } from './components';
import routesConfiguration, { flattenRoutes } from './routesConfiguration';
import localeData from './translations/en.json';
export const ClientApp = props => {
const { store } = props;
addLocaleData([...en]);
const flattenedRoutes = flattenRoutes(routesConfiguration);
return (
<IntlProvider locale="en" messages={localeData}>
<BrowserRouter>
{({ router }) => (
<Provider store={store}>
<Routes router={router} routes={routesConfiguration} />
</Provider>
)}
</BrowserRouter>
<Provider store={store}>
<RoutesProvider routes={routesConfiguration}>
<BrowserRouter>
<Routes routes={flattenedRoutes} />
</BrowserRouter>
</RoutesProvider>
</Provider>
</IntlProvider>
);
};
@ -33,15 +35,16 @@ ClientApp.propTypes = { store: any.isRequired };
export const ServerApp = props => {
const { url, context, store } = props;
addLocaleData([...en]);
const flattenedRoutes = flattenRoutes(routesConfiguration);
return (
<IntlProvider locale="en" messages={localeData}>
<ServerRouter location={url} context={context}>
{({ router }) => (
<Provider store={store}>
<Routes router={router} routes={routesConfiguration} />
</Provider>
)}
</ServerRouter>
<Provider store={store}>
<RoutesProvider routes={routesConfiguration}>
<StaticRouter location={url} context={context}>
<Routes routes={flattenedRoutes} />
</StaticRouter>
</RoutesProvider>
</Provider>
</IntlProvider>
);
};

View file

@ -2,26 +2,26 @@ import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import { forEach } from 'lodash';
import { createServerRenderContext } from 'react-router';
import { ClientApp, ServerApp } from './app';
import configureStore from './store';
const store = configureStore({});
const render = (url, context) =>
ReactDOMServer.renderToString(<ServerApp url={url} context={context} store={store} />);
const render = (url, context) => {
const store = configureStore({});
return ReactDOMServer.renderToString(<ServerApp url={url} context={context} store={store} />);
};
describe('Application', () => {
it('renders in the client without crashing', () => {
const store = configureStore({});
const div = document.createElement('div');
ReactDOM.render(<ClientApp store={store} />, div);
});
it('renders in the server without crashing', () => {
render('/', createServerRenderContext());
render('/', {});
});
it('renders correct routes in the server', () => {
it('server renders pages that do not require authentication', () => {
const urlTitles = {
'/': 'Landing page',
'/s': 'Search page',
@ -35,11 +35,13 @@ describe('Application', () => {
'/this-url-should-not-be-found': 'Page not found',
};
forEach(urlTitles, (title, url) => {
const context = createServerRenderContext();
const context = {};
const body = render(url, context);
expect(body).toMatch(`>${title}</h1>`);
});
});
it('server renders redirects for pages that require authentication', () => {
const urlRedirects = {
'/orders': '/login',
'/sales': '/login',
@ -56,10 +58,9 @@ describe('Application', () => {
'/account/security': '/login',
};
forEach(urlRedirects, (redirectPath, url) => {
const context = createServerRenderContext();
const context = {};
const body = render(url, context);
const result = context.getResult();
expect(result.redirect.pathname).toEqual(redirectPath);
expect(context.url).toEqual(redirectPath);
});
});
});

View file

@ -7,7 +7,7 @@ exports[`FilterPanel matches snapshot 1`] = `
hash=""
name="SearchListingsPage"
params={Object {}}
query={Object {}}
search=""
state={Object {}}>
See studios
</NamedLink>
@ -15,7 +15,7 @@ exports[`FilterPanel matches snapshot 1`] = `
hash=""
name="SearchListingsPage"
params={Object {}}
query={Object {}}
search=""
state={Object {}}>
X
</NamedLink>

View file

@ -18,7 +18,7 @@ exports[`ListingCard matches snapshot 1`] = `
"slug": "Banyan-Studios",
}
}
query={Object {}}
search=""
state={Object {}}>
Banyan Studios
</NamedLink>

View file

@ -14,10 +14,9 @@ exports[`ListingCardSmall matches snapshot 1`] = `
<div
className={undefined}>
<a
className=""
className={undefined}
href="/l/Banyan-Studios/123"
onClick={[Function]}
style={Object {}}>
onClick={[Function]}>
Banyan Studios
</a>
<div

View file

@ -8,7 +8,7 @@ exports[`MapPanel matches snapshot 1`] = `
hash=""
name="SearchFiltersPage"
params={Object {}}
query={Object {}}
search=""
state={Object {}}>
Filters
</NamedLink>
@ -16,7 +16,7 @@ exports[`MapPanel matches snapshot 1`] = `
hash=""
name="SearchListingsPage"
params={Object {}}
query={Object {}}
search=""
state={Object {}}>
X
</NamedLink>

View file

@ -4,28 +4,28 @@
* (Helps to narrow down the scope of possible format changes to routes.)
*/
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { pathByRouteName } from '../../routesConfiguration';
const NamedLink = (props, context) => {
const { name, params, query, hash, state, ...rest } = props;
const { name, search, hash, state, params, ...rest } = props;
const pathname = pathByRouteName(name, context.routes, params);
const locationDescriptor = { pathname, query, hash, state };
const locationDescriptor = { pathname, search, hash, state };
return <Link to={locationDescriptor} {...rest} />;
};
const { array, object, string } = PropTypes;
NamedLink.contextTypes = { routes: array };
NamedLink.contextTypes = { routes: array.isRequired };
NamedLink.defaultProps = { hash: '', params: {}, query: {}, state: {} };
NamedLink.defaultProps = { search: '', hash: '', state: {}, params: {} };
NamedLink.propTypes = {
hash: string,
name: string.isRequired,
params: object,
query: object,
search: string,
hash: string,
state: object,
params: object,
};
export default NamedLink;

View file

@ -4,28 +4,27 @@
* (Helps to narrow down the scope of possible format changes to routes.)
*/
import React, { PropTypes } from 'react';
import { Redirect } from 'react-router';
import { Redirect } from 'react-router-dom';
import { pathByRouteName } from '../../routesConfiguration';
const NamedRedirect = (props, context) => {
const { name, params, query, hash, state, ...rest } = props;
const { name, search, state, params, ...rest } = props;
const pathname = pathByRouteName(name, context.routes, params);
const locationDescriptor = { pathname, query, hash, state };
const locationDescriptor = { pathname, search, state };
return <Redirect to={locationDescriptor} {...rest} />;
};
const { array, object, string } = PropTypes;
NamedRedirect.contextTypes = { routes: array };
NamedRedirect.contextTypes = { routes: array.isRequired };
NamedRedirect.defaultProps = { hash: '', params: {}, query: {}, state: {} };
NamedRedirect.defaultProps = { search: '', state: {}, params: {} };
NamedRedirect.propTypes = {
hash: string,
name: string.isRequired,
params: object,
query: object,
search: string,
state: object,
params: object,
};
export default NamedRedirect;

View file

@ -36,7 +36,7 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
"id": "some-test-order-id",
}
}
query={Object {}}
search=""
state={Object {}}>
You have a new message!
</NamedLink>

View file

@ -10,11 +10,15 @@ const scrollToTop = () => {
class PageLayout extends Component {
componentDidMount() {
this.historyUnlisten = this.context.history.listen(() => scrollToTop());
if (this.context && this.context.history && this.context.history.listen) {
this.historyUnlisten = this.context.history.listen(() => scrollToTop());
}
}
componentWillUnmount() {
this.historyUnlisten();
if (this.historyUnlisten) {
this.historyUnlisten();
}
}
render() {

View file

@ -6,7 +6,7 @@ exports[`SearchResultsPanel matches snapshot 1`] = `
hash=""
name="SearchFiltersPage"
params={Object {}}
query={Object {}}
search=""
state={Object {}}>
Filters
</NamedLink>
@ -14,7 +14,7 @@ exports[`SearchResultsPanel matches snapshot 1`] = `
hash=""
name="SearchMapPage"
params={Object {}}
query={Object {}}
search=""
state={Object {}}>
Map
</NamedLink>

View file

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link, Redirect } from 'react-router';
import { Link, Redirect } from 'react-router-dom';
import { PageLayout } from '../../components';
import { LoginForm, SignUpForm } from '../../containers';
import { login } from '../../ducks/Auth.ducks';
@ -9,15 +9,14 @@ export const AuthenticationPageComponent = props => {
const { location, tab, isAuthenticated, onLoginSubmit, onSignUpSubmit } = props;
const isLogin = tab === 'login';
const from = location.state && location.state.from ? location.state.from : null;
const pathname = from ? from.pathname : null;
return (
<PageLayout title={`Authentication page: ${tab} tab`}>
{isAuthenticated ? <Redirect to={from || '/'} /> : null}
{pathname
{from
? <p>
You must log in to view the page at
<code>{pathname}</code>
<code>{from}</code>
</p>
: null}
{isLogin ? <LoginForm onSubmit={onLoginSubmit} /> : <SignUpForm onSubmit={onSignUpSubmit} />}
@ -28,12 +27,12 @@ export const AuthenticationPageComponent = props => {
);
};
AuthenticationPageComponent.defaultProps = { location: {}, tab: 'signup' };
AuthenticationPageComponent.defaultProps = { tab: 'signup' };
const { any, oneOf, shape, bool, func } = PropTypes;
const { object, oneOf, shape, bool, func } = PropTypes;
AuthenticationPageComponent.propTypes = {
location: shape({ state: shape({ from: any }) }),
location: shape({ state: object }).isRequired,
tab: oneOf(['login', 'signup']),
isAuthenticated: bool.isRequired,
onLoginSubmit: func.isRequired,

View file

@ -1,16 +1,16 @@
exports[`AuthenticationPageComponent matches snapshot 1`] = `
<Connect(PageLayout)
title="Authentication page: signup tab">
<p>
You must log in to view the page at
<code>
/protected
</code>
</p>
<ReduxForm
onSubmit={[Function]} />
<Link
activeClassName=""
activeOnlyWhenExact={false}
activeStyle={Object {}}
className=""
isActive={[Function]}
replace={false}
style={Object {}}
to={
Object {
"pathname": "/login",

View file

@ -27,7 +27,7 @@ exports[`CheckoutPage matches snapshot 1`] = `
"id": 12345,
}
}
query={Object {}}
search=""
state={Object {}}>
Confirm & Pay
</NamedLink>

View file

@ -1,5 +1,5 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { PageLayout } from '../../components';
const toPath = (filter, id) => {

View file

@ -4,13 +4,7 @@ exports[`InboxPage matches snapshot 1`] = `
<ul>
<li>
<Link
activeClassName=""
activeOnlyWhenExact={false}
activeStyle={Object {}}
className=""
isActive={[Function]}
replace={false}
style={Object {}}
to="/conversation/1234">
Single thread
</Link>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { NamedLink, PageLayout } from '../../components';
import css from './ListingPage.css';

View file

@ -74,13 +74,7 @@ exports[`ListingPage matches snapshot 1`] = `
</p>
</div>
<Link
activeClassName=""
activeOnlyWhenExact={false}
activeStyle={Object {}}
className=""
isActive={[Function]}
replace={false}
style={Object {}}
to="mailto:studio.dude@mystudio.com">
<h2>
Contact studio
@ -137,7 +131,7 @@ exports[`ListingPage matches snapshot 1`] = `
"id": 12345,
}
}
query={Object {}}
search=""
state={Object {}}>
Book Banyan Studios
</NamedLink>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { PageLayout } from '../../components';
export default () => (

View file

@ -4,13 +4,7 @@ exports[`ManageListingsPage matches snapshot 1`] = `
<ul>
<li>
<Link
activeClassName=""
activeOnlyWhenExact={false}
activeStyle={Object {}}
className=""
isActive={[Function]}
replace={false}
style={Object {}}
to="/l/1234">
Listing 1234
</Link>

View file

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Match, Redirect } from 'react-router';
import { Match, Redirect } from 'react-router-dom';
import routesConfiguration, { pathByRouteName } from '../../routesConfiguration';
// wrap `Match` and use this everywhere instead, then when

View file

@ -1,5 +1,5 @@
import React from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { PageLayout } from '../../components';
export default () => (

View file

@ -2,13 +2,7 @@ exports[`NotFoundPage matches snapshot 1`] = `
<Connect(PageLayout)
title="Page not found">
<Link
activeClassName=""
activeOnlyWhenExact={false}
activeStyle={Object {}}
className=""
isActive={[Function]}
replace={false}
style={Object {}}
to="/">
Index page
</Link>

View file

@ -28,7 +28,6 @@ const OrderPage = props => {
confirmationCode: 'X2587X',
};
const activeStyle = { fontWeight: 'bold' };
const detailsClassName = classNames(css.tabContent, {
[css.tabContentVisible]: props.tab === 'details',
});
@ -38,15 +37,10 @@ const OrderPage = props => {
return (
<PageLayout title={`Your ${title} booking is confirmed!`}>
<NamedLink
name="OrderDetailsPage"
params={{ id: orderId }}
style={{ marginRight: '2rem' }}
activeStyle={activeStyle}
>
<NamedLink name="OrderDetailsPage" params={{ id: orderId }} style={{ marginRight: '2rem' }}>
Booking details
</NamedLink>
<NamedLink name="OrderDiscussionPage" params={{ id: orderId }} activeStyle={activeStyle}>
<NamedLink name="OrderDiscussionPage" params={{ id: orderId }}>
Discussion
</NamedLink>
<OrderDetailsPanel className={detailsClassName} {...detailsProps} />

View file

@ -2,11 +2,6 @@ exports[`OrderPage matches snapshot 1`] = `
<Connect(PageLayout)
title="Your Banyan Studios booking is confirmed!">
<NamedLink
activeStyle={
Object {
"fontWeight": "bold",
}
}
hash=""
name="OrderDetailsPage"
params={
@ -14,7 +9,7 @@ exports[`OrderPage matches snapshot 1`] = `
"id": 1234,
}
}
query={Object {}}
search=""
state={Object {}}
style={
Object {
@ -24,11 +19,6 @@ exports[`OrderPage matches snapshot 1`] = `
Booking details
</NamedLink>
<NamedLink
activeStyle={
Object {
"fontWeight": "bold",
}
}
hash=""
name="OrderDiscussionPage"
params={
@ -36,7 +26,7 @@ exports[`OrderPage matches snapshot 1`] = `
"id": 1234,
}
}
query={Object {}}
search=""
state={Object {}}>
Discussion
</NamedLink>

View file

@ -1,6 +1,6 @@
/* eslint-disable react/no-unescaped-entities */
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { PageLayout } from '../../components';
const SalesConversationPage = props => {

View file

@ -6,25 +6,13 @@ exports[`SalesConversationPage matches snapshot 1`] = `
12345
</p>
<Link
activeClassName=""
activeOnlyWhenExact={false}
activeStyle={Object {}}
className=""
isActive={[Function]}
replace={false}
style={Object {}}
to="/sale/12345/discussion">
Discussion tab
</Link>
<br />
<Link
activeClassName=""
activeOnlyWhenExact={false}
activeStyle={Object {}}
className=""
isActive={[Function]}
replace={false}
style={Object {}}
to="/sale/12345/details">
Details tab
</Link>

View file

@ -1,5 +1,5 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import { map, size } from 'lodash';
import * as allExamples from '../../examples';

View file

@ -1,25 +1,24 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import { Link, withRouter } from 'react-router-dom';
import { logout } from '../../ducks/Auth.ducks';
import css from './Topbar.css';
const Topbar = (props, context) => {
const { isAuthenticated, onLogout, user } = props;
const { router } = context;
const Topbar = props => {
const { isAuthenticated, onLogout, user, push: historyPush } = props;
const house = { dangerouslySetInnerHTML: { __html: '&#127968;' } };
const hamburger = { dangerouslySetInnerHTML: { __html: '&#127828;' } };
const handleChange = e => {
const value = e.target.value;
router.transitionTo(value);
historyPush(value);
};
const handleLogout = () => {
// Router is passed to the action to enable redirect to home when
// logout succeeds.
onLogout(router);
// History push function is passed to the action to enable
// redirect to home when logout succeeds.
onLogout(historyPush);
};
return (
@ -60,17 +59,21 @@ const Topbar = (props, context) => {
Topbar.defaultProps = { user: null };
const { bool, object, func, any } = PropTypes;
const { bool, object, func } = PropTypes;
Topbar.propTypes = { isAuthenticated: bool.isRequired, user: object, onLogout: func.isRequired };
Topbar.contextTypes = { router: any };
Topbar.propTypes = {
isAuthenticated: bool.isRequired,
user: object,
onLogout: func.isRequired,
// history.push prop from withRouter
push: func.isRequired,
};
const mapStateToProps = state => ({
isAuthenticated: state.Auth.isAuthenticated,
user: state.Auth.user,
});
const mapDispatchToProps = dispatch => ({ onLogout: router => dispatch(logout(router)) });
const mapDispatchToProps = dispatch => ({ onLogout: historyPush => dispatch(logout(historyPush)) });
export default connect(mapStateToProps, mapDispatchToProps)(Topbar);
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Topbar));

View file

@ -58,7 +58,7 @@ export const login = (email, password) => ({ type: LOGIN_REQUEST, payload: { ema
export const loginSuccess = user => ({ type: LOGIN_SUCCESS, payload: user });
export const loginError = error => ({ type: LOGIN_ERROR, payload: error, error: true });
export const logout = router => ({ type: LOGOUT_REQUEST, payload: { router } });
export const logout = historyPush => ({ type: LOGOUT_REQUEST, payload: { historyPush } });
export const logoutSuccess = () => ({ type: LOGOUT_SUCCESS });
export const logoutError = error => ({ type: LOGOUT_ERROR, payload: error, error: true });
@ -77,11 +77,11 @@ export function* callLogin(action, sdk) {
export function* callLogout(action, sdk) {
const { payload } = action;
const { router } = payload;
const { historyPush } = payload;
try {
yield call(sdk.logout);
yield put(logoutSuccess());
yield call(router.transitionTo, '/');
yield call(historyPush, '/');
} catch (e) {
yield put(logoutError(e));
}

View file

@ -93,28 +93,28 @@ describe('Auth duck', () => {
describe('logout worker', () => {
it('should redirect to root after logout', () => {
const sdk = { logout: jest.fn() };
const router = { transitionTo: jest.fn() };
const action = logout(router);
const historyPush = jest.fn();
const action = logout(historyPush);
const worker = callLogout(action, sdk);
expect(worker.next()).toEqual({ done: false, value: call(sdk.logout) });
expect(worker.next()).toEqual({ done: false, value: put(logoutSuccess()) });
expect(worker.next()).toEqual({ done: false, value: call(router.transitionTo, '/') });
expect(worker.next()).toEqual({ done: false, value: call(historyPush, '/') });
expect(worker.next().done).toEqual(true);
expect(sdk.logout).not.toHaveBeenCalled();
expect(router.transitionTo).not.toHaveBeenCalled();
expect(historyPush).not.toHaveBeenCalled();
});
it('should not redirect if logout fails', () => {
const sdk = { logout: jest.fn() };
const router = { transitionTo: jest.fn() };
const action = logout(router);
const historyPush = jest.fn();
const action = logout(historyPush);
const worker = callLogout(action, sdk);
expect(worker.next()).toEqual({ done: false, value: call(sdk.logout) });
const error = new Error('Test logout error');
expect(worker.throw(error)).toEqual({ done: false, value: put(logoutError(error)) });
expect(worker.next().done).toEqual(true);
expect(sdk.logout).not.toHaveBeenCalled();
expect(router.transitionTo).not.toHaveBeenCalled();
expect(historyPush).not.toHaveBeenCalled();
});
});
@ -140,9 +140,9 @@ describe('Auth duck', () => {
it('calls logout', () => {
const sdk = { logout: jest.fn() };
const router = { transitionTo: jest.fn() };
const historyPush = jest.fn();
const watcher = watchAuth(sdk);
const logoutAction = logout(router);
const logoutAction = logout(historyPush);
const takeLoginOrLogout = take([LOGIN_REQUEST, LOGOUT_REQUEST]);
const forkLogout = fork(callLogout, logoutAction, sdk);
@ -156,7 +156,7 @@ describe('Auth duck', () => {
expect(watcher.next().value).toEqual(takeLoginOrLogout);
expect(sdk.logout).not.toHaveBeenCalled();
expect(router.transitionTo).not.toHaveBeenCalled();
expect(historyPush).not.toHaveBeenCalled();
});
it('should cancel login if another login comes', () => {
@ -195,10 +195,10 @@ describe('Auth duck', () => {
it('should cancel login if a logout comes', () => {
const sdk = { login: jest.fn(), logout: jest.fn() };
const router = { transitionTo: jest.fn() };
const historyPush = jest.fn();
const watcher = watchAuth(sdk);
const loginAction = login('email', 'password');
const logoutAction = logout(router);
const logoutAction = logout(historyPush);
const task = createMockTask();
const takeLoginOrLogout = take([LOGIN_REQUEST, LOGOUT_REQUEST]);
const forkLogin = fork(callLogin, loginAction, sdk);
@ -226,7 +226,7 @@ describe('Auth duck', () => {
expect(sdk.login).not.toHaveBeenCalled();
expect(sdk.logout).not.toHaveBeenCalled();
expect(router.transitionTo).not.toHaveBeenCalled();
expect(historyPush).not.toHaveBeenCalled();
});
});
});

View file

@ -1,10 +1,6 @@
import React from 'react';
import { find } from 'lodash';
import { Redirect } from 'react-router';
// This will change to `matchPath` soonish
import matchPattern from 'react-router/matchPattern';
import { Redirect, matchPath } from 'react-router-dom';
import pathToRegexp from 'path-to-regexp';
import {
AuthenticationPage,
@ -31,33 +27,38 @@ import {
const RedirectLandingPage = () => <Redirect to="/" />;
const routesConfiguration = [
{ pattern: '/', exactly: true, name: 'LandingPage', component: LandingPage },
{
pattern: '/',
exactly: true,
name: 'LandingPage',
component: match => <LandingPage {...match} />,
},
{
pattern: '/s',
exactly: true,
name: 'SearchPage',
component: SearchPage,
component: match => <SearchPage {...match} />,
loadData: SearchPage ? SearchPage.loadData : null,
routes: [
{
pattern: '/s/filters',
exactly: true,
name: 'SearchFiltersPage',
component: props => <SearchPage {...props} tab="filters" />,
component: match => <SearchPage {...match} tab="filters" />,
loadData: SearchPage ? SearchPage.loadData : null,
},
{
pattern: '/s/listings',
exactly: true,
name: 'SearchListingsPage',
component: props => <SearchPage {...props} tab="listings" />,
component: match => <SearchPage {...match} tab="listings" />,
loadData: SearchPage ? SearchPage.loadData : null,
},
{
pattern: '/s/map',
exactly: true,
name: 'SearchMapPage',
component: props => <SearchPage {...props} tab="map" />,
component: match => <SearchPage {...match} tab="map" />,
loadData: SearchPage ? SearchPage.loadData : null,
},
],
@ -68,7 +69,12 @@ const routesConfiguration = [
name: 'ListingBasePage',
component: RedirectLandingPage,
routes: [
{ pattern: '/l/:slug/:id', exactly: true, name: 'ListingPage', component: ListingPage },
{
pattern: '/l/:slug/:id',
exactly: true,
name: 'ListingPage',
component: match => <ListingPage {...match} />,
},
],
},
{
@ -81,14 +87,14 @@ const routesConfiguration = [
pattern: '/u/:displayName',
exactly: true,
name: 'ProfilePage',
component: ProfilePage,
component: match => <ProfilePage {...match} />,
routes: [
{
pattern: '/u/:displayName/edit',
auth: true,
exactly: true,
name: 'EditProfilePage',
component: EditProfilePage,
component: match => <EditProfilePage {...match} />,
},
],
},
@ -104,7 +110,7 @@ const routesConfiguration = [
pattern: '/checkout/:listingId',
exactly: true,
name: 'CheckoutPage',
component: CheckoutPage,
component: match => <CheckoutPage {...match} />,
},
],
},
@ -112,61 +118,66 @@ const routesConfiguration = [
pattern: '/login',
exactly: true,
name: 'LogInPage',
component: props => <AuthenticationPage {...props} tab="login" />,
component: match => <AuthenticationPage {...match} tab="login" />,
},
{
pattern: '/signup',
exactly: true,
name: 'SignUpPage',
component: props => <AuthenticationPage {...props} tab="signup" />,
component: match => <AuthenticationPage {...match} tab="signup" />,
},
{
pattern: '/password',
exactly: true,
name: 'PasswordPage',
component: match => <PasswordForgottenPage {...match} />,
},
{ pattern: '/password', exactly: true, name: 'PasswordPage', component: PasswordForgottenPage },
{
pattern: '/password/forgotten',
exactly: true,
name: 'PasswordForgottenPage',
component: PasswordForgottenPage,
component: match => <PasswordForgottenPage {...match} />,
},
{
pattern: '/password/change',
exactly: true,
name: 'PasswordChangePage',
component: PasswordChangePage,
component: match => <PasswordChangePage {...match} />,
},
{
pattern: '/orders',
auth: true,
exactly: true,
name: 'OrdersPage',
component: props => <InboxPage {...props} filter="orders" />,
component: match => <InboxPage {...match} filter="orders" />,
},
{
pattern: '/sales',
auth: true,
exactly: true,
name: 'SalesPage',
component: props => <InboxPage {...props} filter="sales" />,
component: match => <InboxPage {...match} filter="sales" />,
},
{
pattern: '/order/:id',
auth: true,
exactly: true,
name: 'OrderPage',
component: () => <Redirect to="/" />,
component: RedirectLandingPage,
routes: [
{
pattern: '/order/:id/details',
auth: true,
exactly: true,
name: 'OrderDetailsPage',
component: props => <OrderPage {...props} tab="details" />,
component: match => <OrderPage {...match} tab="details" />,
},
{
pattern: '/order/:id/discussion',
auth: true,
exactly: true,
name: 'OrderDiscussionPage',
component: props => <OrderPage {...props} tab="discussion" />,
component: match => <OrderPage {...match} tab="discussion" />,
},
],
},
@ -175,21 +186,21 @@ const routesConfiguration = [
auth: true,
exactly: true,
name: 'SalePage',
component: props => <SalesConversationPage {...props} tab="discussion" />,
component: match => <SalesConversationPage {...match} tab="discussion" />,
routes: [
{
pattern: '/sale/:id/details',
auth: true,
exactly: true,
name: 'SaleDetailsPage',
component: props => <SalesConversationPage {...props} tab="discussion" />,
component: match => <SalesConversationPage {...match} tab="discussion" />,
},
{
pattern: '/sale/:id/discussion',
auth: true,
exactly: true,
name: 'SaleDiscussionPage',
component: props => <SalesConversationPage {...props} tab="discussion" />,
component: match => <SalesConversationPage {...match} tab="discussion" />,
},
],
},
@ -198,7 +209,7 @@ const routesConfiguration = [
auth: true,
exactly: true,
name: 'ManageListingsPage',
component: ManageListingsPage,
component: match => <ManageListingsPage {...match} />,
},
{
pattern: '/account',
@ -212,42 +223,47 @@ const routesConfiguration = [
auth: true,
exactly: true,
name: 'ContactDetailsPage',
component: ContactDetailsPage,
component: match => <ContactDetailsPage {...match} />,
},
{
pattern: '/account/payout-preferences',
auth: true,
exactly: true,
name: 'PayoutPreferencesPage',
component: PayoutPreferencesPage,
component: match => <PayoutPreferencesPage {...match} />,
},
{
pattern: '/account/security',
auth: true,
exactly: true,
name: 'SecurityPage',
component: SecurityPage,
component: match => <SecurityPage {...match} />,
},
],
},
{ pattern: '/styleguide', exactly: true, name: 'Styleguide', component: StyleguidePage },
{
pattern: '/styleguide',
exactly: true,
name: 'Styleguide',
component: match => <StyleguidePage {...match} />,
},
{
pattern: '/styleguide/:component',
exactly: true,
name: 'StyleguideComponent',
component: StyleguidePage,
component: match => <StyleguidePage {...match} />,
},
{
pattern: '/styleguide/:component/:example',
exactly: true,
name: 'StyleguideComponentExample',
component: StyleguidePage,
component: match => <StyleguidePage {...match} />,
},
{
pattern: '/styleguide/:component/:example/:type',
exactly: true,
name: 'StyleguideComponentExampleRaw',
component: StyleguidePage,
component: match => <StyleguidePage {...match} />,
},
];
@ -286,7 +302,7 @@ const matchRoutesToLocation = (routes, location, matchedRoutes = [], params = {}
routes.forEach(route => {
const { exactly = false } = route;
const match = !route.pattern ? true : matchPattern(route.pattern, location, exactly);
const match = !route.pattern || matchPath(location.pathname, route.pattern, { exact: exactly });
if (match) {
matched.push(route);
@ -297,7 +313,12 @@ const matchRoutesToLocation = (routes, location, matchedRoutes = [], params = {}
}
if (route.routes) {
const { matchedRoutes: subRouteMatches, params: subRouteParams } = matchRoutesToLocation(route.routes, location, matched, parameters);
const { matchedRoutes: subRouteMatches, params: subRouteParams } = matchRoutesToLocation(
route.routes,
location,
matched,
parameters,
);
matched = matched.concat(subRouteMatches);
parameters = { ...parameters, ...subRouteParams };
}

View file

@ -3,7 +3,7 @@ import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import { IntlProvider } from 'react-intl';
import { BrowserRouter } from 'react-router';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import configureStore from '../store';

View file

@ -2707,7 +2707,7 @@ hide-powered-by@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
history@^4.3.0:
history@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.5.1.tgz#44935a51021e3b8e67ebac267a35675732aba569"
dependencies:
@ -3833,7 +3833,7 @@ longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
@ -5064,7 +5064,7 @@ qs@^6.3.0, qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
query-string@4.2.3, query-string@^4.1.0:
query-string@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.2.3.tgz#9f27273d207a25a8ee4c7b8c74dcd45d556db822"
dependencies:
@ -5110,12 +5110,6 @@ react-addons-test-utils@^15.4.2:
fbjs "^0.8.4"
object-assign "^4.1.0"
react-broadcast@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/react-broadcast/-/react-broadcast-0.1.2.tgz#950de63578a2af399a396067a617af7402182330"
dependencies:
invariant "^2.2.1"
react-dev-utils@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-0.4.2.tgz#ba6fae581fe945a2fc402e9b27c71fda4f62f6a1"
@ -5163,14 +5157,21 @@ react-redux@^5.0.2:
lodash-es "^4.2.0"
loose-envify "^1.1.0"
react-router@4.0.0-alpha.6:
version "4.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.0.0-alpha.6.tgz#239fcf9a6ba7997021022c9b51d72d370f7b6bf4"
react-router-dom@4.0.0-beta.6:
version "4.0.0-beta.6"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.0.0-beta.6.tgz#bdbd8f2fea3def52970735778db03b24cc082a02"
dependencies:
history "^4.3.0"
history "^4.5.1"
react-router "^4.0.0-beta.6"
react-router@^4.0.0-beta.6:
version "4.0.0-beta.6"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.0.0-beta.6.tgz#561ac0bf1929960813bf201319ff85d821d5547b"
dependencies:
history "^4.5.1"
invariant "^2.2.2"
loose-envify "^1.3.1"
path-to-regexp "^1.5.3"
query-string "4.2.3"
react-broadcast "^0.1.2"
react-side-effect@^1.1.0:
version "1.1.0"