Skip to content
This repository has been archived by the owner on Feb 19, 2023. It is now read-only.

Commit

Permalink
feat(#115): added dynamic import to redux implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Andreas Gasser committed May 21, 2019
1 parent da9e6f8 commit 8f295e0
Show file tree
Hide file tree
Showing 15 changed files with 300 additions and 77 deletions.
2 changes: 0 additions & 2 deletions src/app/AppRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ const RegisterContainer = React.lazy(() => import('../auth/register/Container'))
const Privacy = React.lazy(() => import('./Privacy'));
const NotFound = React.lazy(() => import('./NotFound'));

// import * as Paths from '../paths';

const AppRoutes = ({ isAuthenticated }, ...props) => (
<Suspense fallback={<AppLoadingView />}>
<Switch {...props}>
Expand Down
12 changes: 10 additions & 2 deletions src/images/detail/Container.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { connect } from 'react-redux';

import { getImage } from '../../redux/images';
import imagesReducer, { getImage } from '../../redux/images';
import labelsReducer from '../../redux/labels';
import facesReducer from '../../redux/faces';
import { imagesByIdSelector, getImageRequestSelector } from '../../redux/images/selectors';

import { labelsByImageIdSelector, labelsByIdSelector } from '../../redux/labels/selectors';
import { facesByImageId, facesByIdSelector } from '../../redux/faces/selectors';

import DetailView from './DetailView';
import getStore from '../../redux/getStore';

const { store } = getStore();
store.injectReducer('images', imagesReducer);
store.injectReducer('labels', labelsReducer);
store.injectReducer('faces', facesReducer);

const mapKeyToValue = (searchString) => {
const params = {};
Expand Down Expand Up @@ -36,7 +44,7 @@ const select = (state, props) => {
} = props;

const byId = imagesByIdSelector(state);
const image = byId[id];
const image = byId[id] || {};

const params = mapKeyToValue(search);

Expand Down
14 changes: 10 additions & 4 deletions src/images/detail/DetailView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ const StyledView = styled(View)`
`;

const convertMetaToAttributes = (meta) => {
if (!meta) {
return [];
}

const {
density, height, width, type, size,
} = meta;
Expand Down Expand Up @@ -153,7 +157,7 @@ const DetailView = ({

useEffect(() => {
// request labels and faces from backend if not yet in store
if (labels.length === 0 && faces.length === 0) {
if (imageId && labels.length === 0 && faces.length === 0) {
getImage(imageId);
}
}, [imageId, faces.length, labels.length, getImage]);
Expand Down Expand Up @@ -184,7 +188,9 @@ const DetailView = ({
<AddImageButton afterOnClick={() => history.push(Paths.HOME)} />
<StyledImageBox fill>
<StyledImageBoxContainer>
<Image image={image} selectedLabel={selectedLabel} selectedFace={selectedFace} />
{ imageId
? <Image image={image} selectedLabel={selectedLabel} selectedFace={selectedFace} />
: null }
</StyledImageBoxContainer>
</StyledImageBox>
<StyledDataBox pad={{ vertical: 'none', horizontal: 'small' }}>
Expand Down Expand Up @@ -228,8 +234,8 @@ Faces (
DetailView.propTypes = {
history: HistoryPropType.isRequired,
image: PropTypes.shape({
id: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
id: PropTypes.string,
path: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
}).isRequired,
Expand Down
2 changes: 1 addition & 1 deletion src/images/detail/__tests__/Container.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { select, mapDispatchToProps, mapKeyToValue } = __testables__;

describe('image details container test suite', () => {
const expectedInitialState = {
image: undefined,
image: {},
labels: [],
faces: [],
selectedFace: null,
Expand Down
14 changes: 14 additions & 0 deletions src/images/detail/__tests__/DetailView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ describe('DetailView test suite', () => {
expect(addImageButton.exists()).toBeTruthy();
});

it('should not render Image component if no image id is provided', () => {
const wrapper = getDetailView({
image: {},
});
expect(wrapper.exists()).toBeTruthy();

const image = wrapper.find(Image);
expect(image.exists()).toBeFalsy();
});

it('should render image with props', () => {
const wrapper = getDetailView();
expect(wrapper.exists()).toBeTruthy();
Expand Down Expand Up @@ -255,6 +265,10 @@ describe('DetailView test suite', () => {
]);
});

it('should return empty array if no meta is provided', () => {
expect(convertMetaToAttributes(undefined)).toEqual([]);
});

it('should filter out invalid image meta properties', () => {
const meta = {
...initialProps.image.meta,
Expand Down
7 changes: 6 additions & 1 deletion src/images/list/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { connect } from 'react-redux';

import { sortedImageListSelector, addImageRequestSelector } from '../../redux/images/selectors';

import { listImages } from '../../redux/images';
import imagesReducer, { listImages } from '../../redux/images';

import ListView from './ListView';

import getStore from '../../redux/getStore';

const { store } = getStore();
store.injectReducer('images', imagesReducer);

const select = state => ({
images: sortedImageListSelector(state),
addImageRequest: addImageRequestSelector(state),
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { PersistGate } from 'redux-persist/integration/react';
import AppContainer from './app/AppContainer';
import * as serviceWorker from './serviceWorker';

import configureStore from './redux/configureStore';
import getStore from './redux/getStore';

// create store object
const { store, persistor } = configureStore();
const { store, persistor } = getStore();

// persistor.purge();

Expand Down
126 changes: 125 additions & 1 deletion src/redux/__tests__/configureStore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@
import { compose } from 'redux';

import configureStore, { __testables__ } from '../configureStore';
import rootReducers from '../rootReducer';

import { appIdle, appReset, __testables__ as applicationTestables } from '../application';
import { __testables__ as authTestables } from '../auth';

import { applicationMessageAdd, applicationMessageShow } from '../application/message';

const { getComposeEnhancers, getEnhancers, getErrorOptions } = __testables__;
const { applicationDidLoad } = applicationTestables;
const { authSetToken } = authTestables;

const {
getComposeEnhancers,
getEnhancers,
getErrorOptions,
createReducer,
} = __testables__;

it('should create redux store', () => {
const { store } = configureStore();
Expand Down Expand Up @@ -74,6 +86,61 @@ describe('redux getComposeEnhancers test suite', () => {
});
});

describe('redux createReducer test suite', () => {
let mockedDateNow;

beforeAll(() => {
const now = Date.now();
mockedDateNow = jest.spyOn(Date, 'now').mockImplementation(() => now);
});

afterAll(() => {
mockedDateNow.restoreMock();
});

it('should create new reducer with static and provided reducers', () => {
const dynamicReducers = {
demo: () => null,
};
const rootReducer = createReducer(dynamicReducers);

const state = rootReducer(undefined, { type: '' });

expect(state).toBeTruthy();

const expectedKeys = Object.keys({
...rootReducers,
...dynamicReducers,
});
expect(Object.keys(state)).toEqual(expectedKeys);
});

it('should create new reducer with static and default empty dynamic reducers', () => {
const rootReducer = createReducer();

const state = rootReducer(undefined, { type: '' });

expect(state).toBeTruthy();

expect(Object.keys(state)).toEqual(Object.keys(rootReducers));
});

it('should reset reducer data on APP_RESET', () => {
const rootReducer = createReducer();

// fire some actions to reducer
const rootState1 = rootReducer(undefined, appIdle());
const rootState2 = rootReducer(rootState1, applicationDidLoad());
const rootState3 = rootReducer(rootState2, authSetToken('invalid token'));

// check for non empty redux state
expect(rootState3).not.toEqual(rootReducer(undefined, appIdle));

// fire reset and check again
expect(rootReducer(rootState3, appReset())).toEqual(rootState1);
});
});

describe('redux getEnhancers test suite', () => {
it('should return default enhancers', () => {
expect(getEnhancers()).toEqual([]);
Expand Down Expand Up @@ -119,3 +186,60 @@ describe('redux getErrorOptions test sutie', () => {
expect(store.getActions()).toEqual(expectedActions);
});
});

describe('dynamic reducer creation test suite', () => {
let store;

beforeEach(() => {
store = configureStore().store; // eslint-disable-line prefer-destructuring
});

it('should be initialized with empty dynamic reducer and injectReducer function', () => {
expect(store.injectReducer).toBeInstanceOf(Function);
expect(store.asyncReducers).toEqual({});
});

it('should add new reducer to redux tree', () => {
const replaceReducerCallback = jest.fn();
const demoReducerCallback = jest.fn();

store.replaceReducer = replaceReducerCallback;

expect(replaceReducerCallback).not.toHaveBeenCalled();

const demoReducer = () => {
demoReducerCallback();
return null;
};
store.injectReducer('demo', demoReducer);

expect(Object.keys(store.asyncReducers)).toEqual(['demo']);
expect(store.asyncReducers.demo).toEqual(demoReducer);

expect(replaceReducerCallback).toHaveBeenCalled();
});

it('should not add new reducer to redux tree if already added', () => {
const replaceReducerCallback = jest.fn();
const demoReducerCallback = jest.fn();

store.replaceReducer = replaceReducerCallback;

expect(replaceReducerCallback).not.toHaveBeenCalled();

const demoReducer = () => {
demoReducerCallback();
return null;
};
store.injectReducer('demo', demoReducer);

expect(Object.keys(store.asyncReducers)).toEqual(['demo']);
expect(store.asyncReducers.demo).toEqual(demoReducer);

expect(replaceReducerCallback).toHaveBeenCalledTimes(1);

store.injectReducer('demo', demoReducer);
expect(Object.keys(store.asyncReducers)).toEqual(['demo']);
expect(replaceReducerCallback).toHaveBeenCalledTimes(1);
});
});
29 changes: 29 additions & 0 deletions src/redux/__tests__/getStore.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import getStore from '../getStore';

describe('getStore test suite', () => {
it('should create and return store & persistor', () => {
const { store, persistor } = getStore();
expect(store).toBeTruthy();
expect(persistor).toBeTruthy();
});

it('should return singleton store & persistor instance', () => {
const { store, persistor } = getStore();
expect(store).toBeTruthy();
expect(persistor).toBeTruthy();

const newStore = getStore();
expect(newStore.store).toEqual(store);
expect(newStore.persistor).toEqual(persistor);
});

it('should recreate store if force is set', () => {
const { store, persistor } = getStore();
expect(store).toBeTruthy();
expect(persistor).toBeTruthy();

const secondGetStore = getStore({ force: true });
expect(store).not.toEqual(secondGetStore.store);
expect(persistor).not.toEqual(secondGetStore.store);
});
});
37 changes: 3 additions & 34 deletions src/redux/__tests__/rootReducer.test.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,9 @@
import rootReducer from '../rootReducer';

import { appIdle, appReset, __testables__ as applicationTestables } from '../application';
import { __testables__ as authTestables } from '../auth';

const { applicationDidLoad } = applicationTestables;
const { authSetToken } = authTestables;

describe('root reducer test suite', () => {
let mockedDateNow;

beforeAll(() => {
const now = Date.now();
mockedDateNow = jest.spyOn(Date, 'now').mockImplementation(() => now);
});

afterAll(() => {
mockedDateNow.restoreMock();
});

const firstLevelKeys = ['appTime', 'application', 'auth', 'images', 'labels', 'faces', 'user'];

it('should return root redux tree', () => {
const rootState = rootReducer(undefined, appIdle());
expect(Object.keys(rootState)).toEqual(firstLevelKeys);
});

it('should reset reducer data on APP_RESET', () => {
// fire some actions to reducer
const rootState1 = rootReducer(undefined, appIdle());
const rootState2 = rootReducer(rootState1, applicationDidLoad());
const rootState3 = rootReducer(rootState2, authSetToken('invalid token'));

// check for non empty redux state
expect(rootState3).not.toEqual(rootReducer(undefined, appIdle));
const firstLevelKeys = ['appTime', 'application', 'auth'];

// fire reset and check again
expect(rootReducer(rootState3, appReset())).toEqual(rootState1);
it('should return root redux object tree', () => {
expect(Object.keys(rootReducer)).toEqual(firstLevelKeys);
});
});
Loading

0 comments on commit 8f295e0

Please sign in to comment.