Commit 4406bef6 authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '10-testing-sagas' into 'master'

Testing sagas added initial code

Closes #10

See merge request grin-global/grin-global-ui!8
parents c48f2b1d 9ed6666c
......@@ -30,7 +30,11 @@ class ApiCall<T> {
loading: false,
data: null,
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';
// src
import Authorize from '@gringlobal/client/ui/common/authorized/Authorize';
// test
import { mountWithProvider } from '#/test-util';
import { mountWithProvider } from '@gringlobal/client/test/test-util';
const initState = {
login: {
......
......@@ -6,7 +6,7 @@
"@gringlobal/client/*": [
"./src/*"
],
"#/*": [
"@gringlobal/client/test/*": [
"./test/*"
],
"*": [
......@@ -21,7 +21,6 @@
"moduleResolution": "node"
},
"exclude": [
"test",
"node_modules",
"lib",
"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 @@
"build:server": "webpack --config config/server.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: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": {
"@gringlobal/client": "*",
......@@ -56,6 +59,8 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@types/enzyme": "^3.10.5",
"@types/jest": "^25.1.2",
"@types/node": "13.5.3",
"@types/react": "^16.9.0",
"@types/react-router": "^5.1.0",
......@@ -80,6 +85,9 @@
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^7.0.0",
"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",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
......@@ -89,12 +97,16 @@
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prefer-arrow": "^1.1.7",
"eslint-plugin-react": "^7.18.0",
"fetch-mock": "^9.1.1",
"file-loader": "^5.0.2",
"git-revision-webpack-plugin": "^3.0.4",
"html-webpack-exclude-assets-plugin": "0.0.7",
"html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"jsdom": "^16.1.0",
"lerna": "^3.20.2",
"mini-css-extract-plugin": "^0.9.0",
"moxios": "^0.4.0",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^3.0.0",
......@@ -102,6 +114,7 @@
"precss": "^4.0.0",
"react-hot-loader": "^4.12.18",
"react-jss": "^10.0.3",
"redux-mock-store": "^1.5.4",
"resolve-url-loader": "^3.1.1",
"rimraf": "^3.0.1",
"roboto-fontface": "^0.10.0",
......@@ -111,6 +124,7 @@
"stylelint": "^13.0.0",
"terser-webpack-plugin": "^2.3.4",
"ts-node": "^8.6.2",
"ts-jest": "^25.2.0",
"tslint": "^6.0.0",
"tslint-loader": "^3.5.4",
"tslint-react": "^4.2.0",
......@@ -122,6 +136,7 @@
"webpack-dev-server": "^3.10.1",
"webpack-hot-middleware": "^2.25.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) => ({
},
});
function* getCooperatorSaga(action) {
export function* getCooperatorSaga(action) {
const { id } = action.payload;
yield put({
type: 'API',
......
......@@ -14,19 +14,36 @@ const initialState: {
cooperator: null,
};
const userReducer = (state = initialState, action) => {
const cooperatorPublicReducer = (state = initialState, action) => {
switch (action.type) {
case RECEIVE_COOPERATOR: {
const { apiCall } = action.payload;
if (apiCall.data && state.cooperators) {
const { data: cooperators } = state.cooperators;
const cooperator = apiCall.data;
const updatedIndex = cooperators && cooperators.content && cooperators.content.findIndex((cooperator) => cooperator.id === cooperator.id) || -1;
if (updatedIndex !== -1) {
const updatedIndex = cooperators && cooperators.content && cooperators.content.findIndex((stateCooperator) => +stateCooperator.id === +cooperator.id);
if (updatedIndex !== undefined && updatedIndex !== -1) {
return update(state, {
cooperator: { $set: apiCall },
cooperators: {
data: {
content: {
[updatedIndex]: { $set: cooperator },
},
},
},
});
} else {
return update(state, {
cooperator: { $set: cooperator },
cooperator: { $set: apiCall },
cooperators: {
[updatedIndex]: { $set: cooperator },
data: {
content: {
$set: [...cooperators.content, cooperator],
},
},
},
});
}
......@@ -47,4 +64,4 @@ const userReducer = (state = initialState, action) => {
}
};
export default userReducer;
export default cooperatorPublicReducer;
......@@ -19,7 +19,7 @@ export default function*() {
}
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
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 = {
type: RECEIVE_COOPERATOR,
payload: { apiCall: ApiCall.success(cooperator) },
};
// @ts-ignore
expect(reducer(stateBefore, action)).toMatchObject(expectedState);
});
it('reducer should append cooperator to cooperators on RECEIVE_COOPERATOR with existing cooperators', () => {
const stateBefore = {
cooperators: {
loading: false,
data: {
content: [
anotherCooperator,
],
},
error: null,
},
cooperator: null,
};
const expectedState = {
cooperators: {
loading: false,
data: {
content: [
anotherCooperator,
cooperator,
],
},
error: null,
},
cooperator: {
loading: false,
data: cooperator,
},
};
const action = {
type: RECEIVE_COOPERATOR,
payload: { apiCall: ApiCall.success(cooperator) },
};
// @ts-ignore
expect(reducer(stateBefore, action)).toMatchObject(expectedState);
});
});
......@@ -14,6 +14,12 @@
],
"@gringlobal/i18n/*": [
"../i18n/src/*"
],
"@gringlobal/client/test/*": [
"./client/test/*"
],
"@gringlobal/express/test/*": [
"./test/*"
]
},
"outDir": "../../target",
......@@ -26,11 +32,7 @@
"noUnusedLocals": true,
"jsx": "react",
"moduleResolution": "node",
"importHelpers": true,
"types": [
"node",
"webpack-env"
]
"importHelpers": true
},
"exclude": [
"node_modules",
......