switched to use Apollo

This commit is contained in:
Sean Macfarlane
2020-03-23 15:53:18 -04:00
parent 1730988a64
commit 904bf36b00
21 changed files with 21 additions and 1170 deletions

View File

@@ -1,28 +0,0 @@
/*
* AppReducer
*
* The reducer takes care of our data. Using actions, we can
* update our application state. To add a new action,
* add it to the switch statement in the reducer function
*
*/
import produce from 'immer';
// The initial state of the App
export const initialState = {
loading: false,
error: false,
currentUser: false,
};
/* eslint-disable default-case, no-param-reassign */
const appReducer = (state = initialState, action) =>
produce(state, () => {
switch (action.type) {
default:
break;
}
});
export default appReducer;

View File

@@ -1,42 +0,0 @@
/**
* The global state selectors
*/
import { createSelector } from 'reselect';
import { initialState } from './reducer';
const selectGlobal = state => state.global || initialState;
const selectRouter = state => state.router;
const makeSelectCurrentUser = () =>
createSelector(
selectGlobal,
globalState => globalState.currentUser,
);
const makeSelectLoading = () =>
createSelector(
selectGlobal,
globalState => globalState.loading,
);
const makeSelectError = () =>
createSelector(
selectGlobal,
globalState => globalState.error,
);
const makeSelectLocation = () =>
createSelector(
selectRouter,
routerState => routerState.location,
);
export {
selectGlobal,
makeSelectCurrentUser,
makeSelectLoading,
makeSelectError,
makeSelectLocation,
};

View File

@@ -1,31 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import { useLocation } from 'react-router-dom';
import { AppLayout as Layout } from 'cu-ui';
import { makeSelectLocation } from 'containers/App/selectors';
import logo from 'images/logo-light.png';
import logoMobile from 'images/logoxmobile.jpg';
const MasterLayout = ({ children, locationState }) => (
<Layout logo={logo} logoMobile={logoMobile} onLogout={() => {}} locationState={locationState}>
const MasterLayout = ({ children }) => {
const location = useLocation();
return (
<Layout company="ConnectUs" logo={logo} logoMobile={logoMobile} onLogout={() => {}} locationState={location}>
{children}
</Layout>
);
};
MasterLayout.propTypes = {
children: PropTypes.node.isRequired,
locationState: PropTypes.instanceOf(Object).isRequired,
};
export const mapStateToProps = createStructuredSelector({
locationState: makeSelectLocation(),
});
const withConnect = connect(mapStateToProps);
export default compose(withConnect)(MasterLayout);
export default MasterLayout;

View File

@@ -1,27 +1,27 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import { BrowserRouter as Router } from 'react-router-dom';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
import 'styles/antd.less';
import 'styles/index.scss';
import App from 'containers/App';
import configureStore from 'store';
import history from 'utils/history';
// Create redux store with history
const initialState = {};
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById('root');
const client = new ApolloClient({
// uri: "https://48p1r2roz4.sse.codesandbox.io"
});
const render = () => {
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<ApolloProvider client={client}>
<Router>
<App />
</ConnectedRouter>
</Provider>,
</Router>
</ApolloProvider>,
MOUNT_NODE
);
};

View File

@@ -1,23 +0,0 @@
/**
* Combine all reducers in this file and export the combined reducers.
*/
import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
import globalReducer from 'containers/App/reducer';
import history from 'utils/history';
/**
* Merges the main reducer with the router state and dynamically injected reducers
*/
export default function createReducer(injectedReducers = {}) {
const rootReducer = combineReducers({
global: globalReducer,
router: connectRouter(history),
...injectedReducers,
});
return rootReducer;
}

View File

@@ -1,55 +0,0 @@
/**
* Create the store with dynamic reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
export default function configureStore(initialState = {}, history) {
let composeEnhancers = compose;
const reduxSagaMonitorOptions = {};
// If Redux Dev Tools and Saga Dev Tools Extensions are installed, enable them
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production' && typeof window === 'object') {
/* eslint-disable no-underscore-dangle */
if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__)
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({});
// NOTE: Uncomment the code below to restore support for Redux Saga
// Dev Tools once it supports redux-saga version 1.x.x
// if (window.__SAGA_MONITOR_EXTENSION__)
// reduxSagaMonitorOptions = {
// sagaMonitor: window.__SAGA_MONITOR_EXTENSION__,
// };
/* eslint-enable */
}
const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [sagaMiddleware, routerMiddleware(history)];
const enhancers = [applyMiddleware(...middlewares)];
const store = createStore(createReducer(), initialState, composeEnhancers(...enhancers));
// Extensions
store.runSaga = sagaMiddleware.run;
store.injectedReducers = {}; // Reducer registry
store.injectedSagas = {}; // Saga registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers));
});
}
return store;
}

View File

@@ -1,43 +0,0 @@
/**
* Test store addons
*/
import { browserHistory } from 'react-router-dom';
import configureStore from '../configureStore';
describe('configureStore', () => {
let store;
beforeAll(() => {
store = configureStore({}, browserHistory);
});
describe('injectedReducers', () => {
it('should contain an object for reducers', () => {
expect(typeof store.injectedReducers).toBe('object');
});
});
describe('injectedSagas', () => {
it('should contain an object for sagas', () => {
expect(typeof store.injectedSagas).toBe('object');
});
});
describe('runSaga', () => {
it('should contain a hook for `sagaMiddleware.run`', () => {
expect(typeof store.runSaga).toBe('function');
});
});
});
describe('configureStore params', () => {
it('should call window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__', () => {
/* eslint-disable no-underscore-dangle */
const compose = jest.fn();
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = () => compose;
configureStore(undefined, browserHistory);
expect(compose).toHaveBeenCalled();
/* eslint-enable */
});
});

View File

@@ -1,21 +0,0 @@
import { conformsTo, isFunction, isObject } from 'lodash';
import invariant from 'invariant';
/**
* Validate the shape of redux store
*/
export default function checkStore(store) {
const shape = {
dispatch: isFunction,
subscribe: isFunction,
getState: isFunction,
replaceReducer: isFunction,
runSaga: isFunction,
injectedReducers: isObject,
injectedSagas: isObject,
};
invariant(
conformsTo(store, shape),
'(app/utils...) injectors: Expected a valid redux store',
);
}

View File

@@ -1,3 +0,0 @@
export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount';
export const DAEMON = '@@saga-injector/daemon';
export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';

View File

@@ -1,4 +0,0 @@
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;

View File

@@ -1,44 +0,0 @@
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ReactReduxContext } from 'react-redux';
import getInjectors from './reducerInjectors';
/**
* Dynamically injects a reducer
*
* @param {string} key A key of the reducer
* @param {function} reducer A reducer that will be injected
*
*/
export default ({ key, reducer }) => WrappedComponent => {
class ReducerInjector extends React.Component {
constructor(props, context) {
super(props, context);
getInjectors(context.store).injectReducer(key, reducer);
}
static WrappedComponent = WrappedComponent;
render() {
return <WrappedComponent {...this.props} />;
}
}
ReducerInjector.displayName = `withReducer(${WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'})`;
ReducerInjector.contextType = ReactReduxContext;
return hoistNonReactStatics(ReducerInjector, WrappedComponent);
};
const useInjectReducer = ({ key, reducer }) => {
const context = React.useContext(ReactReduxContext);
React.useEffect(() => {
getInjectors(context.store).injectReducer(key, reducer);
}, []);
};
export { useInjectReducer };

View File

@@ -1,61 +0,0 @@
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ReactReduxContext } from 'react-redux';
import getInjectors from './sagaInjectors';
/**
* Dynamically injects a saga, passes component's props as saga arguments
*
* @param {string} key A key of the saga
* @param {function} saga A root saga that will be injected
* @param {string} [mode] By default (constants.DAEMON) the saga will be started
* on component mount and never canceled or started again. Another two options:
* - constants.RESTART_ON_REMOUNT — the saga will be started on component mount and
* cancelled with `task.cancel()` on component unmount for improved performance,
* - constants.ONCE_TILL_UNMOUNT — behaves like 'RESTART_ON_REMOUNT' but never runs it again.
*
*/
export default ({ key, saga, mode }) => WrappedComponent => {
class InjectSaga extends React.Component {
constructor(props, context) {
super(props, context);
this.injectors = getInjectors(context.store);
this.injectors.injectSaga(key, { saga, mode }, this.props);
}
componentWillUnmount() {
this.injectors.ejectSaga(key);
}
static WrappedComponent = WrappedComponent;
render() {
return <WrappedComponent {...this.props} />;
}
}
InjectSaga.contextType = ReactReduxContext;
InjectSaga.displayName = `withSaga(${WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'})`;
return hoistNonReactStatics(InjectSaga, WrappedComponent);
};
const useInjectSaga = ({ key, saga, mode }) => {
const context = React.useContext(ReactReduxContext);
React.useEffect(() => {
const injectors = getInjectors(context.store);
injectors.injectSaga(key, { saga, mode });
return () => {
injectors.ejectSaga(key);
};
}, []);
};
export { useInjectSaga };

View File

@@ -1,34 +0,0 @@
import invariant from 'invariant';
import { isEmpty, isFunction, isString } from 'lodash';
import checkStore from './checkStore';
import createReducer from '../reducers';
export function injectReducerFactory(store, isValid) {
return function injectReducer(key, reducer) {
if (!isValid) checkStore(store);
invariant(
isString(key) && !isEmpty(key) && isFunction(reducer),
'(app/utils...) injectReducer: Expected `reducer` to be a reducer function',
);
// Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
if (
Reflect.has(store.injectedReducers, key) &&
store.injectedReducers[key] === reducer
)
return;
store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.injectedReducers));
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectReducer: injectReducerFactory(store, true),
};
}

View File

@@ -1,91 +0,0 @@
import invariant from 'invariant';
import { isEmpty, isFunction, isString, conformsTo } from 'lodash';
import checkStore from './checkStore';
import { DAEMON, ONCE_TILL_UNMOUNT, RESTART_ON_REMOUNT } from './constants';
const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT];
const checkKey = key =>
invariant(
isString(key) && !isEmpty(key),
'(app/utils...) injectSaga: Expected `key` to be a non empty string',
);
const checkDescriptor = descriptor => {
const shape = {
saga: isFunction,
mode: mode => isString(mode) && allowedModes.includes(mode),
};
invariant(
conformsTo(descriptor, shape),
'(app/utils...) injectSaga: Expected a valid saga descriptor',
);
};
export function injectSagaFactory(store, isValid) {
return function injectSaga(key, descriptor = {}, args) {
if (!isValid) checkStore(store);
const newDescriptor = {
...descriptor,
mode: descriptor.mode || DAEMON,
};
const { saga, mode } = newDescriptor;
checkKey(key);
checkDescriptor(newDescriptor);
let hasSaga = Reflect.has(store.injectedSagas, key);
if (process.env.NODE_ENV !== 'production') {
const oldDescriptor = store.injectedSagas[key];
// enable hot reloading of daemon and once-till-unmount sagas
if (hasSaga && oldDescriptor.saga !== saga) {
oldDescriptor.task.cancel();
hasSaga = false;
}
}
if (
!hasSaga ||
(hasSaga && mode !== DAEMON && mode !== ONCE_TILL_UNMOUNT)
) {
/* eslint-disable no-param-reassign */
store.injectedSagas[key] = {
...newDescriptor,
task: store.runSaga(saga, args),
};
/* eslint-enable no-param-reassign */
}
};
}
export function ejectSagaFactory(store, isValid) {
return function ejectSaga(key) {
if (!isValid) checkStore(store);
checkKey(key);
if (Reflect.has(store.injectedSagas, key)) {
const descriptor = store.injectedSagas[key];
if (descriptor.mode && descriptor.mode !== DAEMON) {
descriptor.task.cancel();
// Clean up in production; in development we need `descriptor.saga` for hot reloading
if (process.env.NODE_ENV === 'production') {
// Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
}
}
}
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectSaga: injectSagaFactory(store, true),
ejectSaga: ejectSagaFactory(store, true),
};
}

View File

@@ -1,33 +0,0 @@
/**
* Test injectors
*/
import checkStore from '../checkStore';
describe('checkStore', () => {
let store;
beforeEach(() => {
store = {
dispatch: () => {},
subscribe: () => {},
getState: () => {},
replaceReducer: () => {},
runSaga: () => {},
injectedReducers: {},
injectedSagas: {},
};
});
it('should not throw if passed valid store shape', () => {
expect(() => checkStore(store)).not.toThrow();
});
it('should throw if passed invalid store shape', () => {
expect(() => checkStore({})).toThrow();
expect(() => checkStore({ ...store, injectedSagas: null })).toThrow();
expect(() => checkStore({ ...store, injectedReducers: null })).toThrow();
expect(() => checkStore({ ...store, runSaga: null })).toThrow();
expect(() => checkStore({ ...store, replaceReducer: null })).toThrow();
});
});

View File

@@ -1,98 +0,0 @@
/**
* Test injectors
*/
import { memoryHistory } from 'react-router-dom';
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { render } from 'react-testing-library';
import configureStore from '../../configureStore';
import injectReducer, { useInjectReducer } from '../injectReducer';
import * as reducerInjectors from '../reducerInjectors';
// Fixtures
const Component = () => null;
const reducer = s => s;
describe('injectReducer decorator', () => {
let store;
let injectors;
let ComponentWithReducer;
beforeAll(() => {
reducerInjectors.default = jest.fn().mockImplementation(() => injectors);
});
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectors = {
injectReducer: jest.fn(),
};
ComponentWithReducer = injectReducer({ key: 'test', reducer })(Component);
reducerInjectors.default.mockClear();
});
it('should inject a given reducer', () => {
renderer.create(
<Provider store={store}>
<ComponentWithReducer />
</Provider>,
);
expect(injectors.injectReducer).toHaveBeenCalledTimes(1);
expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer);
});
it('should set a correct display name', () => {
expect(ComponentWithReducer.displayName).toBe('withReducer(Component)');
expect(
injectReducer({ key: 'test', reducer })(() => null).displayName,
).toBe('withReducer(Component)');
});
it('should propagate props', () => {
const props = { testProp: 'test' };
const renderedComponent = renderer.create(
<Provider store={store}>
<ComponentWithReducer {...props} />
</Provider>,
);
const {
props: { children },
} = renderedComponent.getInstance();
expect(children.props).toEqual(props);
});
});
describe('useInjectReducer hook', () => {
let store;
let injectors;
let ComponentWithReducer;
beforeAll(() => {
injectors = {
injectReducer: jest.fn(),
};
reducerInjectors.default = jest.fn().mockImplementation(() => injectors);
store = configureStore({}, memoryHistory);
ComponentWithReducer = () => {
useInjectReducer({ key: 'test', reducer });
return null;
};
});
it('should inject a given reducer', () => {
render(
<Provider store={store}>
<ComponentWithReducer />
</Provider>,
);
expect(injectors.injectReducer).toHaveBeenCalledTimes(1);
expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer);
});
});

View File

@@ -1,149 +0,0 @@
/**
* Test injectors
*/
import { memoryHistory } from 'react-router-dom';
import { put } from 'redux-saga/effects';
import renderer from 'react-test-renderer';
import { render } from 'react-testing-library';
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../configureStore';
import injectSaga, { useInjectSaga } from '../injectSaga';
import * as sagaInjectors from '../sagaInjectors';
// Fixtures
const Component = () => null;
function* testSaga() {
yield put({ type: 'TEST', payload: 'yup' });
}
describe('injectSaga decorator', () => {
let store;
let injectors;
let ComponentWithSaga;
beforeAll(() => {
sagaInjectors.default = jest.fn().mockImplementation(() => injectors);
});
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectors = {
injectSaga: jest.fn(),
ejectSaga: jest.fn(),
};
ComponentWithSaga = injectSaga({
key: 'test',
saga: testSaga,
mode: 'testMode',
})(Component);
sagaInjectors.default.mockClear();
});
it('should inject given saga, mode, and props', () => {
const props = { test: 'test' };
renderer.create(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
expect(injectors.injectSaga).toHaveBeenCalledTimes(1);
expect(injectors.injectSaga).toHaveBeenCalledWith(
'test',
{ saga: testSaga, mode: 'testMode' },
props,
);
});
it('should eject on unmount with a correct saga key', () => {
const props = { test: 'test' };
const renderedComponent = renderer.create(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
renderedComponent.unmount();
expect(injectors.ejectSaga).toHaveBeenCalledTimes(1);
expect(injectors.ejectSaga).toHaveBeenCalledWith('test');
});
it('should set a correct display name', () => {
expect(ComponentWithSaga.displayName).toBe('withSaga(Component)');
expect(
injectSaga({ key: 'test', saga: testSaga })(() => null).displayName,
).toBe('withSaga(Component)');
});
it('should propagate props', () => {
const props = { testProp: 'test' };
const renderedComponent = renderer.create(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
const {
props: { children },
} = renderedComponent.getInstance();
expect(children.props).toEqual(props);
});
});
describe('useInjectSaga hook', () => {
let store;
let injectors;
let ComponentWithSaga;
beforeAll(() => {
sagaInjectors.default = jest.fn().mockImplementation(() => injectors);
});
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectors = {
injectSaga: jest.fn(),
ejectSaga: jest.fn(),
};
ComponentWithSaga = () => {
useInjectSaga({
key: 'test',
saga: testSaga,
mode: 'testMode',
});
return null;
};
sagaInjectors.default.mockClear();
});
it('should inject given saga and mode', () => {
const props = { test: 'test' };
render(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
expect(injectors.injectSaga).toHaveBeenCalledTimes(1);
expect(injectors.injectSaga).toHaveBeenCalledWith('test', {
saga: testSaga,
mode: 'testMode',
});
});
it('should eject on unmount with a correct saga key', () => {
const props = { test: 'test' };
const { unmount } = render(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
unmount();
expect(injectors.ejectSaga).toHaveBeenCalledTimes(1);
expect(injectors.ejectSaga).toHaveBeenCalledWith('test');
});
});

View File

@@ -1,100 +0,0 @@
/**
* Test injectors
*/
import produce from 'immer';
import { memoryHistory } from 'react-router-dom';
import identity from 'lodash/identity';
import configureStore from '../../configureStore';
import getInjectors, { injectReducerFactory } from '../reducerInjectors';
// Fixtures
const initialState = { reduced: 'soon' };
/* eslint-disable default-case, no-param-reassign */
const reducer = (state = initialState, action) =>
produce(state, draft => {
switch (action.type) {
case 'TEST':
draft.reduced = action.payload;
break;
}
});
describe('reducer injectors', () => {
let store;
let injectReducer;
describe('getInjectors', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
});
it('should return injectors', () => {
expect(getInjectors(store)).toEqual(
expect.objectContaining({
injectReducer: expect.any(Function),
}),
);
});
it('should throw if passed invalid store shape', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => getInjectors(store)).toThrow();
});
});
describe('injectReducer helper', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectReducer = injectReducerFactory(store, true);
});
it('should check a store if the second argument is falsy', () => {
const inject = injectReducerFactory({});
expect(() => inject('test', reducer)).toThrow();
});
it('it should not check a store if the second argument is true', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => injectReducer('test', reducer)).not.toThrow();
});
it("should validate a reducer and reducer's key", () => {
expect(() => injectReducer('', reducer)).toThrow();
expect(() => injectReducer(1, reducer)).toThrow();
expect(() => injectReducer(1, 1)).toThrow();
});
it('given a store, it should provide a function to inject a reducer', () => {
injectReducer('test', reducer);
const actual = store.getState().test;
const expected = initialState;
expect(actual).toEqual(expected);
});
it('should not assign reducer if already existing', () => {
store.replaceReducer = jest.fn();
injectReducer('test', reducer);
injectReducer('test', reducer);
expect(store.replaceReducer).toHaveBeenCalledTimes(1);
});
it('should assign reducer if different implementation for hot reloading', () => {
store.replaceReducer = jest.fn();
injectReducer('test', reducer);
injectReducer('test', identity);
expect(store.replaceReducer).toHaveBeenCalledTimes(2);
});
});
});

View File

@@ -1,80 +0,0 @@
/**
* Test the request function
*/
import 'whatwg-fetch';
import request from '../request';
describe('request', () => {
// Before each test, stub the fetch function
beforeEach(() => {
window.fetch = jest.fn();
});
describe('stubbing successful response', () => {
// Before each test, pretend we got a successful response
beforeEach(() => {
const res = new Response('{"hello":"world"}', {
status: 200,
headers: {
'Content-type': 'application/json',
},
});
window.fetch.mockReturnValue(Promise.resolve(res));
});
it('should format the response correctly', done => {
request('/thisurliscorrect')
.catch(done)
.then(json => {
expect(json.hello).toBe('world');
done();
});
});
});
describe('stubbing 204 response', () => {
// Before each test, pretend we got a successful response
beforeEach(() => {
const res = new Response('', {
status: 204,
statusText: 'No Content',
});
window.fetch.mockReturnValue(Promise.resolve(res));
});
it('should return null on 204 response', done => {
request('/thisurliscorrect')
.catch(done)
.then(json => {
expect(json).toBeNull();
done();
});
});
});
describe('stubbing error response', () => {
// Before each test, pretend we got an unsuccessful response
beforeEach(() => {
const res = new Response('', {
status: 404,
statusText: 'Not Found',
headers: {
'Content-type': 'application/json',
},
});
window.fetch.mockReturnValue(Promise.resolve(res));
});
it('should catch errors', done => {
request('/thisdoesntexist').catch(err => {
expect(err.response.status).toBe(404);
expect(err.response.statusText).toBe('Not Found');
done();
});
});
});
});

View File

@@ -1,231 +0,0 @@
/**
* Test injectors
*/
import { memoryHistory } from 'react-router-dom';
import { put } from 'redux-saga/effects';
import configureStore from '../../configureStore';
import getInjectors, {
injectSagaFactory,
ejectSagaFactory,
} from '../sagaInjectors';
import { DAEMON, ONCE_TILL_UNMOUNT, RESTART_ON_REMOUNT } from '../constants';
function* testSaga() {
yield put({ type: 'TEST', payload: 'yup' });
}
describe('injectors', () => {
const originalNodeEnv = process.env.NODE_ENV;
let store;
let injectSaga;
let ejectSaga;
describe('getInjectors', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
});
it('should return injectors', () => {
expect(getInjectors(store)).toEqual(
expect.objectContaining({
injectSaga: expect.any(Function),
ejectSaga: expect.any(Function),
}),
);
});
it('should throw if passed invalid store shape', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => getInjectors(store)).toThrow();
});
});
describe('ejectSaga helper', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectSaga = injectSagaFactory(store, true);
ejectSaga = ejectSagaFactory(store, true);
});
it('should check a store if the second argument is falsy', () => {
const eject = ejectSagaFactory({});
expect(() => eject('test')).toThrow();
});
it('should not check a store if the second argument is true', () => {
Reflect.deleteProperty(store, 'dispatch');
injectSaga('test', { saga: testSaga });
expect(() => ejectSaga('test')).not.toThrow();
});
it("should validate saga's key", () => {
expect(() => ejectSaga('')).toThrow();
expect(() => ejectSaga(1)).toThrow();
});
it('should cancel a saga in RESTART_ON_REMOUNT mode', () => {
const cancel = jest.fn();
store.injectedSagas.test = { task: { cancel }, mode: RESTART_ON_REMOUNT };
ejectSaga('test');
expect(cancel).toHaveBeenCalled();
});
it('should not cancel a daemon saga', () => {
const cancel = jest.fn();
store.injectedSagas.test = { task: { cancel }, mode: DAEMON };
ejectSaga('test');
expect(cancel).not.toHaveBeenCalled();
});
it('should ignore saga that was not previously injected', () => {
expect(() => ejectSaga('test')).not.toThrow();
});
it("should remove non daemon saga's descriptor in production", () => {
process.env.NODE_ENV = 'production';
injectSaga('test', { saga: testSaga, mode: RESTART_ON_REMOUNT });
injectSaga('test1', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
ejectSaga('test');
ejectSaga('test1');
expect(store.injectedSagas.test).toBe('done');
expect(store.injectedSagas.test1).toBe('done');
process.env.NODE_ENV = originalNodeEnv;
});
it("should not remove daemon saga's descriptor in production", () => {
process.env.NODE_ENV = 'production';
injectSaga('test', { saga: testSaga, mode: DAEMON });
ejectSaga('test');
expect(store.injectedSagas.test.saga).toBe(testSaga);
process.env.NODE_ENV = originalNodeEnv;
});
it("should not remove daemon saga's descriptor in development", () => {
injectSaga('test', { saga: testSaga, mode: DAEMON });
ejectSaga('test');
expect(store.injectedSagas.test.saga).toBe(testSaga);
});
});
describe('injectSaga helper', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectSaga = injectSagaFactory(store, true);
ejectSaga = ejectSagaFactory(store, true);
});
it('should check a store if the second argument is falsy', () => {
const inject = injectSagaFactory({});
expect(() => inject('test', testSaga)).toThrow();
});
it('it should not check a store if the second argument is true', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => injectSaga('test', { saga: testSaga })).not.toThrow();
});
it("should validate saga's key", () => {
expect(() => injectSaga('', { saga: testSaga })).toThrow();
expect(() => injectSaga(1, { saga: testSaga })).toThrow();
});
it("should validate saga's descriptor", () => {
expect(() => injectSaga('test')).toThrow();
expect(() => injectSaga('test', { saga: 1 })).toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: 'testMode' }),
).toThrow();
expect(() => injectSaga('test', { saga: testSaga, mode: 1 })).toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: RESTART_ON_REMOUNT }),
).not.toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: DAEMON }),
).not.toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: ONCE_TILL_UNMOUNT }),
).not.toThrow();
});
it('should pass args to saga.run', () => {
const args = {};
store.runSaga = jest.fn();
injectSaga('test', { saga: testSaga }, args);
expect(store.runSaga).toHaveBeenCalledWith(testSaga, args);
});
it('should not start daemon and once-till-unmount sagas if were started before', () => {
store.runSaga = jest.fn();
injectSaga('test1', { saga: testSaga, mode: DAEMON });
injectSaga('test1', { saga: testSaga, mode: DAEMON });
injectSaga('test2', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
injectSaga('test2', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
expect(store.runSaga).toHaveBeenCalledTimes(2);
});
it('should start any saga that was not started before', () => {
store.runSaga = jest.fn();
injectSaga('test1', { saga: testSaga });
injectSaga('test2', { saga: testSaga, mode: DAEMON });
injectSaga('test3', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
expect(store.runSaga).toHaveBeenCalledTimes(3);
});
it('should restart a saga if different implementation for hot reloading', () => {
const cancel = jest.fn();
store.injectedSagas.test = { saga: testSaga, task: { cancel } };
store.runSaga = jest.fn();
function* testSaga1() {
yield put({ type: 'TEST', payload: 'yup' });
}
injectSaga('test', { saga: testSaga1 });
expect(cancel).toHaveBeenCalledTimes(1);
expect(store.runSaga).toHaveBeenCalledWith(testSaga1, undefined);
});
it('should not cancel saga if different implementation in production', () => {
process.env.NODE_ENV = 'production';
const cancel = jest.fn();
store.injectedSagas.test = {
saga: testSaga,
task: { cancel },
mode: RESTART_ON_REMOUNT,
};
function* testSaga1() {
yield put({ type: 'TEST', payload: 'yup' });
}
injectSaga('test', { saga: testSaga1, mode: DAEMON });
expect(cancel).toHaveBeenCalledTimes(0);
process.env.NODE_ENV = originalNodeEnv;
});
it('should save an entire descriptor in the saga registry', () => {
injectSaga('test', { saga: testSaga, foo: 'bar' });
expect(store.injectedSagas.test.foo).toBe('bar');
});
});
});

View File

@@ -23,7 +23,6 @@
"clean-webpack-plugin": "^3.0.0",
"cu-ui": "git+https://sean_macfarlane@bitbucket.org/connectustechnologies/connectus-wlan-ui-workspace.git",
"graphql": "^14.6.0",
"history": "^4.10.1",
"prop-types": "^15.7.2",
"react": "^16.13.0",
"react-dom": "^16.13.0",