Commit 9ed6666c authored by Viacheslav Pavlov's avatar Viacheslav Pavlov Committed by Viacheslav Pavlov
Browse files

Testing sagas added initial code

-fix cooperator reducer
parent c48f2b1d
...@@ -30,7 +30,11 @@ class ApiCall<T> { ...@@ -30,7 +30,11 @@ class ApiCall<T> {
loading: false, loading: false,
data: null, data: null,
timestamp: Date.now(), timestamp: Date.now(),
error: `${error.message} ${error.response && error.response.data && error.response.data.error || ''}`, error: {
status: error.response.status,
data: error.response && error.response.data,
message: `${error.message} ${error.response && error.response.data && error.response.data.error || ''}`,
},
}; };
} }
} }
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
// src // src
import Authorize from '@gringlobal/client/ui/common/authorized/Authorize'; import Authorize from '@gringlobal/client/ui/common/authorized/Authorize';
// test // test
import { mountWithProvider } from '#/test-util'; import { mountWithProvider } from '@gringlobal/client/test/test-util';
const initState = { const initState = {
login: { login: {
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"@gringlobal/client/*": [ "@gringlobal/client/*": [
"./src/*" "./src/*"
], ],
"#/*": [ "@gringlobal/client/test/*": [
"./test/*" "./test/*"
], ],
"*": [ "*": [
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
"moduleResolution": "node" "moduleResolution": "node"
}, },
"exclude": [ "exclude": [
"test",
"node_modules", "node_modules",
"lib", "lib",
"typings/main", "typings/main",
......
{
"presets": [
[
"env",
{
"modules": false,
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}
],
"react"
],
"compact": false,
"retainLines": true,
"minified": false,
"inputSourceMap": false,
"sourceMaps": false,
"plugins": [
"transform-object-rest-spread",
"babel-plugin-syntax-dynamic-import",
[
"./react-loadable-custom/babel.js",
{
"importName": "utilities/CustomReactLoadable"
}
]
]
}
module.exports = {
preset: 'ts-jest',
snapshotSerializers: ['enzyme-to-json/serializer'],
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '/test/.*\\.test.(ts|tsx)$',
moduleDirectories: ['node_modules', 'src'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'@gringlobal/client/(.*)': '<rootDir>/../client/src/$1',
'@gringlobal/client/test/(.*)': '<rootDir>/../client/test/$1',
'@gringlobal/express/test/(.*)': '<rootDir>/test/$1',
},
setupFiles: ['<rootDir>/test/setupTests.ts'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
};
const config = require('./jest.config');
module.exports = {
...config,
testRegex: '/test/integration/.*\\.test.(ts|tsx)$',
};
const config = require('./jest.config');
module.exports = {
...config,
testRegex: '/test/unit/.*\\.test.(ts|tsx)$',
};
...@@ -10,7 +10,10 @@ ...@@ -10,7 +10,10 @@
"build:server": "webpack --config config/server.config.js", "build:server": "webpack --config config/server.config.js",
"build:client": "cross-env NODE_ENV=development SSR=true webpack --config config/webpack-production.config.js", "build:client": "cross-env NODE_ENV=development SSR=true webpack --config config/webpack-production.config.js",
"start:dev": "yarn run i18nGenerate && cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack-dev-server --config config/webpack-development.config.js", "start:dev": "yarn run i18nGenerate && cross-env NODE_OPTIONS=--max_old_space_size=8192 webpack-dev-server --config config/webpack-development.config.js",
"start:prod": "yarn run build && cd target/app/server && node server.js" "start:prod": "yarn run build && cd target/app/server && node server.js",
"test": "jest",
"test:integration": "jest -c jest.integration.config.js",
"test:unit": "jest -c jest.unit.config.js"
}, },
"dependencies": { "dependencies": {
"@gringlobal/client": "*", "@gringlobal/client": "*",
...@@ -56,6 +59,8 @@ ...@@ -56,6 +59,8 @@
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.8.4", "@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4", "@babel/core": "^7.8.4",
"@types/enzyme": "^3.10.5",
"@types/jest": "^25.1.2",
"@types/node": "13.5.3", "@types/node": "13.5.3",
"@types/react": "^16.9.0", "@types/react": "^16.9.0",
"@types/react-router": "^5.1.0", "@types/react-router": "^5.1.0",
...@@ -80,6 +85,9 @@ ...@@ -80,6 +85,9 @@
"copy-webpack-plugin": "^5.1.1", "copy-webpack-plugin": "^5.1.1",
"cross-env": "^7.0.0", "cross-env": "^7.0.0",
"css-loader": "^3.4.2", "css-loader": "^3.4.2",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.4.4",
"enzyme-adapter-react-16": "^1.15.2",
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1", "eslint-config-airbnb": "^18.0.1",
...@@ -89,12 +97,16 @@ ...@@ -89,12 +97,16 @@
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prefer-arrow": "^1.1.7", "eslint-plugin-prefer-arrow": "^1.1.7",
"eslint-plugin-react": "^7.18.0", "eslint-plugin-react": "^7.18.0",
"fetch-mock": "^9.1.1",
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"git-revision-webpack-plugin": "^3.0.4", "git-revision-webpack-plugin": "^3.0.4",
"html-webpack-exclude-assets-plugin": "0.0.7", "html-webpack-exclude-assets-plugin": "0.0.7",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"jsdom": "^16.1.0",
"lerna": "^3.20.2", "lerna": "^3.20.2",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"moxios": "^0.4.0",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
...@@ -102,6 +114,7 @@ ...@@ -102,6 +114,7 @@
"precss": "^4.0.0", "precss": "^4.0.0",
"react-hot-loader": "^4.12.18", "react-hot-loader": "^4.12.18",
"react-jss": "^10.0.3", "react-jss": "^10.0.3",
"redux-mock-store": "^1.5.4",
"resolve-url-loader": "^3.1.1", "resolve-url-loader": "^3.1.1",
"rimraf": "^3.0.1", "rimraf": "^3.0.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
...@@ -111,6 +124,7 @@ ...@@ -111,6 +124,7 @@
"stylelint": "^13.0.0", "stylelint": "^13.0.0",
"terser-webpack-plugin": "^2.3.4", "terser-webpack-plugin": "^2.3.4",
"ts-node": "^8.6.2", "ts-node": "^8.6.2",
"ts-jest": "^25.2.0",
"tslint": "^6.0.0", "tslint": "^6.0.0",
"tslint-loader": "^3.5.4", "tslint-loader": "^3.5.4",
"tslint-react": "^4.2.0", "tslint-react": "^4.2.0",
...@@ -122,6 +136,7 @@ ...@@ -122,6 +136,7 @@
"webpack-dev-server": "^3.10.1", "webpack-dev-server": "^3.10.1",
"webpack-hot-middleware": "^2.25.0", "webpack-hot-middleware": "^2.25.0",
"webpack-manifest-plugin": "^2.2.0", "webpack-manifest-plugin": "^2.2.0",
"webpack-merge": "^4.2.2" "webpack-merge": "^4.2.2",
"axios-mock-adapter": "latest"
} }
} }
...@@ -26,7 +26,7 @@ export const getCooperatorAction = (id) => ({ ...@@ -26,7 +26,7 @@ export const getCooperatorAction = (id) => ({
}, },
}); });
function* getCooperatorSaga(action) { export function* getCooperatorSaga(action) {
const { id } = action.payload; const { id } = action.payload;
yield put({ yield put({
type: 'API', type: 'API',
......
...@@ -14,19 +14,36 @@ const initialState: { ...@@ -14,19 +14,36 @@ const initialState: {
cooperator: null, cooperator: null,
}; };
const userReducer = (state = initialState, action) => { const cooperatorPublicReducer = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case RECEIVE_COOPERATOR: { case RECEIVE_COOPERATOR: {
const { apiCall } = action.payload; const { apiCall } = action.payload;
if (apiCall.data && state.cooperators) { if (apiCall.data && state.cooperators) {
const { data: cooperators } = state.cooperators; const { data: cooperators } = state.cooperators;
const cooperator = apiCall.data; const cooperator = apiCall.data;
const updatedIndex = cooperators && cooperators.content && cooperators.content.findIndex((cooperator) => cooperator.id === cooperator.id) || -1; const updatedIndex = cooperators && cooperators.content && cooperators.content.findIndex((stateCooperator) => +stateCooperator.id === +cooperator.id);
if (updatedIndex !== -1) {
if (updatedIndex !== undefined && updatedIndex !== -1) {
return update(state, {
cooperator: { $set: apiCall },
cooperators: {
data: {
content: {
[updatedIndex]: { $set: cooperator },
},
},
},
});
} else {
return update(state, { return update(state, {
cooperator: { $set: cooperator }, cooperator: { $set: apiCall },
cooperators: { cooperators: {
[updatedIndex]: { $set: cooperator }, data: {
content: {
$set: [...cooperators.content, cooperator],
},
},
}, },
}); });
} }
...@@ -47,4 +64,4 @@ const userReducer = (state = initialState, action) => { ...@@ -47,4 +64,4 @@ const userReducer = (state = initialState, action) => {
} }
}; };
export default userReducer; export default cooperatorPublicReducer;
...@@ -19,7 +19,7 @@ export default function*() { ...@@ -19,7 +19,7 @@ export default function*() {
} }
function *appendAxiosConfig(action) { function *appendAxiosConfig(action) {
console.log(`Appeding axios config for ${action.type}`); console.log(`Appeding axios config for ${action.target}`);
yield put({ type: action.target, payload: { apiCall: ApiCall.start() } }); // Loading yield put({ type: action.target, payload: { apiCall: ApiCall.start() } }); // Loading
const accessToken = yield select((state) => state.login.access_token); const accessToken = yield select((state) => state.login.access_token);
......
import * as moxios from 'moxios';
// action
import { getCooperatorAction } from 'cooperator/action/public';
// model
import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator';
// utilities
import { axiosBackend } from '@gringlobal/client/utilities/requestUtils';
import { initializeActualStore } from '@gringlobal/express/test/test-util';
describe('Integration Cooperator public', () => {
const mockedCooperator: Partial<Cooperator> = {
id: 1,
title: 'mockedCooperator',
};
const notFoundError = {
message: 'Cooperator not found',
localizedMessage: 'Cooperator with such id not found',
};
const initialState = {};
let store;
const installAxiosMocks = () => {
moxios.install(axiosBackend);
// mock getCooperator
moxios.stubRequest(/^\/api\/v1\/cooperator\/1/, {
status: 200,
response: { ...mockedCooperator },
});
moxios.stubRequest(/^\/api\/v1\/cooperator\/404/, {
status: 404,
response: { ...notFoundError },
});
};
beforeEach(() => {
store = initializeActualStore(initialState);
installAxiosMocks();
});
afterEach(() => {
moxios.uninstall();
});
it('store should contain cooperator Data onSuccess', (done) => {
const id = 1;
store.dispatch(getCooperatorAction(id));
expect(store.getState().cooperator.public.cooperator.loading).toEqual(true);
moxios.wait (() => {
expect(store.getState().cooperator.public.cooperator.loading).toEqual(false);
expect(store.getState().cooperator.public.cooperator.data).toEqual(mockedCooperator);
done()
});
});
it('store should contain error on notFound', (done) => {
const id = 404;
store.dispatch(getCooperatorAction(id));
expect(store.getState().cooperator.public.cooperator.loading).toEqual(true);
moxios.wait (() => {
expect(store.getState().cooperator.public.cooperator.loading).toEqual(false);
expect(store.getState().cooperator.public.cooperator.data).toEqual(null);
expect(store.getState().cooperator.public.cooperator.error.status).toEqual(404);
expect(store.getState().cooperator.public.cooperator.error.data).toEqual(notFoundError);
done()
});
});
});
import { configure } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
import { applyMiddleware, compose, createStore } from 'redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'
import createSagaMiddleware from 'redux-saga';
import { createMemoryHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import saga from 'core/action/saga';
import rootReducer from 'core/reducer';
const defaultMockInitState = {
login: {
access_token: 'token',
},
};
export const initializeMockStore = (initState) => {
const sagaMiddleware = createSagaMiddleware();
const store = configureMockStore([thunk, sagaMiddleware])({ ...defaultMockInitState, ...initState });
sagaMiddleware.run(saga);
return store;
};
export const initializeActualStore = (initialState) => {
const sagaMiddleware = createSagaMiddleware();
const his = createMemoryHistory();
const store = compose(applyMiddleware(thunk, routerMiddleware(his), sagaMiddleware))(createStore)(rootReducer(his), initialState);
sagaMiddleware.run(saga);
return store;
};
import * as moxios from 'moxios';
import { SAGA_RECEIVE_COOPERATOR, RECEIVE_COOPERATOR } from 'cooperator/constants'
import { getCooperatorAction, getCooperatorSaga } from 'cooperator/action/public';
import { initializeMockStore } from '@gringlobal/express/test/test-util';
import { axiosBackend } from '@gringlobal/client/utilities/requestUtils';
describe('Unit Cooperator actions public', () => {
const initialState = {};
let store;
beforeEach(() => {
moxios.install(axiosBackend);
store = initializeMockStore(initialState);
moxios.stubRequest(/^\/api\/v1\/cooperator\/\d+/, {
status: 200,
response: { id: 1 },
});
});
it('store should contain 4 actions in specific order', (done) => {
const id = 1;
store.dispatch(getCooperatorAction(id));
const expectedActionsCount = 4;
moxios.wait(() => {
expect(store.getActions()).toHaveLength(expectedActionsCount);
// actionCreator
expect(store.getActions()[0].type).toEqual(SAGA_RECEIVE_COOPERATOR);
expect(store.getActions()[0].payload).toEqual({ id });
// Api Saga
expect(store.getActions()[1].type).toEqual('API');
// ApiCall loading
expect(store.getActions()[2].type).toEqual(RECEIVE_COOPERATOR);
expect(store.getActions()[2].payload.apiCall).toBeDefined();
expect(store.getActions()[2].payload.apiCall.loading).toBeTruthy();
// ApiCall success
expect(store.getActions()[3].type).toEqual(RECEIVE_COOPERATOR);
expect(store.getActions()[3].payload.apiCall).toBeDefined();
expect(store.getActions()[3].payload.apiCall.loading).toBeFalsy();
expect(store.getActions()[3].payload.apiCall.data).toBeDefined();
done();
});
});
it('saga should return correct call of ServiceMethod', () => {
const id = 1;
const getCooperatorGenerator = getCooperatorSaga({ payload: { id } });
const sagaResultPayload: any = getCooperatorGenerator.next().value;
expect(sagaResultPayload.payload.action.type).toEqual('API');
expect(sagaResultPayload.payload.action.params).toHaveLength(1);
expect(sagaResultPayload.payload.action.params[0]).toEqual(id);
expect(getCooperatorGenerator.next().done).toBeTruthy();
});
});
import { RECEIVE_COOPERATOR } from 'cooperator/constants'
import reducer from 'cooperator/reducer/public';
import Cooperator from '@gringlobal/client/model/gringlobal/Cooperator';
import ApiCall from '@gringlobal/client/model/common/ApiCall';
describe('Unit Cooperator reducer public', () => {
const cooperator: Partial<Cooperator> = {
id: 1,
title: 'Cooperator',
};
const anotherCooperator: Partial<Cooperator> = {
id: 2,
title: 'another Cooperator',
};
it('reducer should return correct initial state', () => {
const expectedState = {
cooperator: null,
cooperators: null,
};
expect(reducer(undefined, {})).toEqual(expectedState);
});
it('reducer should return correct state on RECEIVE_COOPERATOR', () => {
const expectedState = {
cooperators: null,
cooperator: {
loading: false,
data: cooperator,
},
};
const action = {
type: RECEIVE_COOPERATOR,
payload: { apiCall: ApiCall.success(cooperator) },
};
expect(reducer(undefined, action)).toMatchObject(expectedState);
});
it('reducer should update cooperator on RECEIVE_COOPERATOR with existing cooperators', () => {
const stateBefore = {
cooperators: {
loading: false,
data: {
content: [
{ id: 1, title: 'NOT updated cooperator' },
],
},
error: null,
},
cooperator: null,
};
const expectedState = {
cooperators: {
loading: false,
data: {
content: [
cooperator,
],
},
error: null,
},
cooperator: {
loading: false,
data: cooperator,
},
};
const action = {