diff --git a/openbis_ng_ui/.eslintrc.js b/openbis_ng_ui/.eslintrc.js index c7098787dadc161c6a5d87172499eba27b514eb9..43d7806f1e68dcd1ceaaabe842c434f0b926b15f 100644 --- a/openbis_ng_ui/.eslintrc.js +++ b/openbis_ng_ui/.eslintrc.js @@ -10,6 +10,7 @@ module.exports = { }, env: { browser: true, + jest: true, }, extends: [ 'eslint:recommended', diff --git a/openbis_ng_ui/build.gradle b/openbis_ng_ui/build.gradle index c438aa54af21a58d3fd7f4867640473811e61a55..cd0bc9066919d66dfe4fbf3c987867ad2a3915aa 100644 --- a/openbis_ng_ui/build.gradle +++ b/openbis_ng_ui/build.gradle @@ -30,5 +30,5 @@ node { nodeModulesDir = file("${projectDir}") } -// npm_run_build.dependsOn npm_run_test -build.dependsOn npm_run_build \ No newline at end of file +npm_run_build.dependsOn npm_run_test +build.dependsOn npm_run_build diff --git a/openbis_ng_ui/package.json b/openbis_ng_ui/package.json index 48f1e7b01fa4edc9729e07478b25fd1f67ad5af2..8a88d49a78fe7e7cda92d9928905b47ed5bcb307 100644 --- a/openbis_ng_ui/package.json +++ b/openbis_ng_ui/package.json @@ -20,6 +20,7 @@ "typeface-roboto": "0.0.54" }, "devDependencies": { + "auto-bind": "^2.0.0", "babel-core": "6.26.0", "babel-eslint": "8.2.5", "babel-jest": "^23.6.0", @@ -39,6 +40,7 @@ "file-loader": "1.1.11", "html-webpack-plugin": "3.2.0", "jest": "^23.1.0", + "jest-junit": "^6.0.1", "raw-loader": "0.5.1", "react-hot-loader": "4.3.6", "react-loader": "2.4.5", @@ -55,6 +57,13 @@ "unit": "jest", "lint": "eslint --ext .js,.jsx src test", "lint:fix": "eslint --ext .js,.jsx src test --fix", - "test": "npm run lint && npm run unit" + "test": "jest", + "test:watch": "npm test -- --watch" + }, + "jest": { + "reporters": [ + "default", + "jest-junit" + ] } } diff --git a/openbis_ng_ui/src/index.js b/openbis_ng_ui/src/index.js index 2f67930c4671fbd6952ad1650d743ab23d5fefbb..070d04fadb02747dd133dc808d095f0d28e174cc 100644 --- a/openbis_ng_ui/src/index.js +++ b/openbis_ng_ui/src/index.js @@ -1,24 +1,15 @@ -/* eslint-disable */ -import "regenerator-runtime/runtime" +import 'regenerator-runtime/runtime' import React from 'react' import ReactDOM from 'react-dom' -import { createStore, applyMiddleware, compose } from 'redux' import { Provider } from 'react-redux' -import createSagaMiddleware from 'redux-saga' -import reducer from './reducer/reducer.js' -import { watchActions } from './reducer/sagas' -const sagaMiddleware = createSagaMiddleware() -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; -export const store = createStore(reducer, composeEnhancers(applyMiddleware(sagaMiddleware))) - -sagaMiddleware.run(watchActions) const render = () => { const App = require('./components/App.jsx').default const WithLogin = require('./components/WithLogin.jsx').default const WithLoader = require('./components/WithLoader.jsx').default const WithError = require('./components/WithError.jsx').default + const store = require('./reducer/middleware.js').default.store ReactDOM.render( <Provider store = { store }> <WithLoader> @@ -29,7 +20,7 @@ const render = () => { </WithError> </WithLoader> </Provider>, - document.getElementById("app") + document.getElementById('app') ) } @@ -38,6 +29,6 @@ if (module.hot) { module.hot.accept('./reducer/reducer.js', () => { const nextRootReducer = require('./reducer/reducer.js').default store.replaceReducer(nextRootReducer) - }); + }) } render() \ No newline at end of file diff --git a/openbis_ng_ui/src/reducer/actions.test.js b/openbis_ng_ui/src/reducer/actions.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d97bc3e02d01671d2ba0e6dc1e842a220f2c5403 --- /dev/null +++ b/openbis_ng_ui/src/reducer/actions.test.js @@ -0,0 +1,15 @@ +import actions from './actions' + + +// note: This is just an example. It would not make much sense to +// test simple action creators such as this. +describe('actions', () => { + it('should create a login action', () => { + const expectedAction = { + type: 'LOGIN', + username: 'mark.watney', + password: 'secret', + } + expect(actions.login('mark.watney', 'secret')).toEqual(expectedAction) + }) +}) diff --git a/openbis_ng_ui/src/reducer/middleware.js b/openbis_ng_ui/src/reducer/middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..5b90bde21317a525f456c2794e1079d99a73c951 --- /dev/null +++ b/openbis_ng_ui/src/reducer/middleware.js @@ -0,0 +1,15 @@ +import { createStore, applyMiddleware, compose } from 'redux' +import createSagaMiddleware from 'redux-saga' +import reducer from './reducer.js' +import { watchActions } from './sagas' + +const sagaMiddleware = createSagaMiddleware() +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose +export const store = createStore(reducer, composeEnhancers(applyMiddleware(sagaMiddleware))) + +sagaMiddleware.run(watchActions) +store.dispatch({type: 'INIT'}) + +export default { + store: store +} diff --git a/openbis_ng_ui/src/reducer/reducer.test.js b/openbis_ng_ui/src/reducer/reducer.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d454e9debc6f2dbd2e816ec951942c89b49c1292 --- /dev/null +++ b/openbis_ng_ui/src/reducer/reducer.test.js @@ -0,0 +1,43 @@ +import actions from './actions' +import initialState from './initialstate.js' +import reducer from './reducer.js' + + +describe('reducer', () => { + it('should add exception', () => { + const exception = { message: 'test error' } + const action = actions.error(exception) + const expectedExceptions = [exception] + const state = reducer(initialState, action) + expect(state.exceptions).toEqual(expectedExceptions) + }) +}) + +describe('reducer', () => { + it('should add and select entity', () => { + const entityPermId = 0 + const action = actions.selectEntity(entityPermId) + const state = reducer(initialState, action) + expect(state.openEntities).toEqual({ + entities: [entityPermId], + selectedEntity: entityPermId, + }) + }) +}) + +describe('reducer', () => { + it('should close an entity and select the next open one', () => { + const beforeState = Object.assign({}, initialState, { + openEntities: { + entities: [0, 1, 2], + selectedEntity: 1 + } + }) + const action = actions.closeEntity(1) + const state = reducer(beforeState, action) + expect(state.openEntities).toEqual({ + entities: [0, 2], + selectedEntity: 2, + }) + }) +}) diff --git a/openbis_ng_ui/src/reducer/sagas.js b/openbis_ng_ui/src/reducer/sagas.js index fa1bd26523cd665f110b01b8ac17ae738e018bb9..e29c5056996b0d282a362a5456782fbb6ff47965 100644 --- a/openbis_ng_ui/src/reducer/sagas.js +++ b/openbis_ng_ui/src/reducer/sagas.js @@ -1,29 +1,59 @@ import { put, takeEvery, call } from 'redux-saga/effects' -import openbis from '../services/openbis' +import Openbis from '../services/openbis' import actions from './actions' -// TODO split sagas +// TODO split sagas when it gets too big + +let openbis = new Openbis() + +// used only for testing - need to have a new mock for each test +export function newOpenbis() { + openbis = new Openbis() + return openbis +} + +export function* watchActions() { + yield takeEvery('INIT', init) + yield takeEvery('LOGIN', login) + yield takeEvery('LOGIN-DONE', loginDone) + yield takeEvery('LOGOUT', logout) + yield takeEvery('SAVE-ENTITY', saveEntity) + yield takeEvery('SET-SPACES', selectSpace) + yield takeEvery('EXPAND-NODE', expandNode) +} + +function* handleException(f) { + try { + yield f() + } catch(exception) { + yield put(actions.error(exception)) + } +} function* init() { - // TODO we want to check if there is an active session here to avoid the login + // TODO Check for session token and yield loginDone if valid. + // This can properly be done when we have the session token in a cookie. +} + +function* login(action) { + yield handleException(function*() { + yield openbis.login(action.username, action.password) + yield put(actions.loginDone()) + }) } function* loginDone() { - try { + yield handleException(function*() { const result = yield call(openbis.getSpaces) yield put(actions.setSpaces(result.getObjects())) - } catch(exception) { - yield put(actions.error(exception)) - } + }) } function* logout() { - try { - const result = yield call(openbis.logout) + yield handleException(function*() { + yield call(openbis.logout) yield put(actions.logoutDone()) - } catch(exception) { - yield put(actions.error(exception)) - } + }) } function* selectSpace(action) { @@ -31,19 +61,17 @@ function* selectSpace(action) { } function* saveEntity(action) { - try { + yield handleException(function*() { yield openbis.updateSpace(action.entity.permId, action.entity.description) const result = yield call(openbis.getSpaces) const spaces = result.getObjects() const space = spaces.filter(space => space.permId.permId === action.entity.permId.permId)[0] - yield put(actions.saveEntityDone(space)) - } catch(exception) { - yield put(actions.error(exception)) - } + yield put(actions.saveEntityDone(space)) + }) } function* expandNode(action) { - try { + yield handleException(function*() { const node = action.node if (node.loaded === false) { if (node.type === 'as.dto.space.Space') { @@ -52,26 +80,5 @@ function* expandNode(action) { yield put(actions.setProjects(projects, node.id)) } } - } catch(exception) { - yield put(actions.error(exception)) - } -} - -export function* login(action) { - try { - const result = yield openbis.login(action.username, action.password) - yield put(actions.loginDone()) - } catch(exception) { - yield put(actions.error(exception)) - } -} - -export function* watchActions() { - yield takeEvery('INIT', init) - yield takeEvery('LOGIN', login) - yield takeEvery('LOGIN-DONE', loginDone) - yield takeEvery('LOGOUT', logout) - yield takeEvery('SAVE-ENTITY', saveEntity) - yield takeEvery('SET-SPACES', selectSpace) - yield takeEvery('EXPAND-NODE', expandNode) + }) } diff --git a/openbis_ng_ui/src/reducer/sagas.test.js b/openbis_ng_ui/src/reducer/sagas.test.js new file mode 100644 index 0000000000000000000000000000000000000000..0ffd9c891b118950073aa53384df4e44a78595b9 --- /dev/null +++ b/openbis_ng_ui/src/reducer/sagas.test.js @@ -0,0 +1,81 @@ +import Openbis from '../services/openbis' +import actions from './actions' +import { newOpenbis } from './sagas.js' +import { store } from './middleware' + +// These tests test sagas, reducer and the interaction with openbis. + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ setup ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +// replace the exported Openbis class by a mock generator +jest.mock('../services/openbis') + +// set in beforeEach +let openbis = null + +// before each test, remove the openbis mock and create a new one +// to reset all captured calls +beforeEach(() => { + Openbis.mockClear() + openbis = newOpenbis() + openbis.getSpaces.mockReturnValue({ getObjects: getSpaces }) +}) + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +describe('sagas', () => { + it('should login and load spaces', () => { + // given + const username = 'mark.watney' + const password = 'secret' + // when + store.dispatch(actions.login(username, password)) + // then + const state = store.getState() + const spaces = getSpaces() + expect(openbis.login).toHaveBeenCalledWith(username, password) + expect(state.sessionActive).toEqual(true) + expect(openbis.getSpaces).toHaveBeenCalled() + expect(state.exceptions).toEqual([]) + expect(state.database.spaces).toEqual({ + '0': spaces[0], + '1': spaces[1], + '2': spaces[2], + }) + }) +}) + +describe('sagas', () => { + it('should logout', () => { + // given + store.dispatch(actions.login('kiva.lagos', 'secret')) + // when + store.dispatch(actions.logout()) + // then + const state = store.getState() + expect(openbis.logout).toHaveBeenCalled() + expect(state.sessionActive).toEqual(false) + }) +}) + +describe('sagas', () => { + it('should save entity', () => { + // given + store.dispatch(actions.login('paul.atreides', 'secret')) + // when + store.dispatch(actions.saveEntity(getSpaces()[0])) + // then + const space = getSpaces()[0] + expect(openbis.updateSpace).toHaveBeenCalledWith(space.permId, space.description) + }) +}) + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ mocked data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +function getSpaces() { + return [ + { permId: { permId: '0' }, description: 'desc0' }, + { permId: { permId: '1' }, description: 'desc1' }, + { permId: { permId: '2' }, description: 'desc2' }, + ] +} diff --git a/openbis_ng_ui/src/services/openbis.js b/openbis_ng_ui/src/services/openbis.js index 4362afe4850c8cc706273905720b911a3c18eaa2..51ec7bac4887c72365c01a2f45257b895067a362 100644 --- a/openbis_ng_ui/src/services/openbis.js +++ b/openbis_ng_ui/src/services/openbis.js @@ -1,72 +1,75 @@ -import { store } from '../index.js' +const autoBind = require('auto-bind') -let v3 = null -/* eslint-disable-next-line no-undef */ -requirejs([ 'openbis' ], openbis => { - v3 = new openbis() - store.dispatch({type: 'INIT'}) -}) +export default class Openbis { -function login(user, password) { - return new Promise( (resolve, reject) => { - v3.login(user, password).done(resolve).fail(() => { - reject({ message: 'Login failed' }) + constructor() { + autoBind(this) + let _this = this + /* eslint-disable-next-line no-undef */ + requirejs([ 'openbis' ], openbis => { + _this.v3 = new openbis() }) - }) -} + } -function logout() { - return new Promise( (resolve, reject) => { - v3.logout().done(resolve).fail(reject) - }) -} + login(user, password) { + let v3 = this.v3 + return new Promise( (resolve, reject) => { + v3.login(user, password).done(resolve).fail(() => { + reject({ message: 'Login failed' }) + }) + }) + } -function getSpaces() { - return new Promise( (resolve, reject) => { - /* eslint-disable-next-line no-undef */ - requirejs( - ['as/dto/space/search/SpaceSearchCriteria', 'as/dto/space/fetchoptions/SpaceFetchOptions' ], - (SpaceSearchCriteria, SpaceFetchOptions) => - v3.searchSpaces(new SpaceSearchCriteria(), new SpaceFetchOptions()).done(resolve).fail(reject) - ) - }) -} + logout() { + let v3 = this.v3 + return new Promise( (resolve, reject) => { + v3.logout().done(resolve).fail(reject) + }) + } -function updateSpace(permId, description) { - return new Promise( (resolve, reject) => { - /* eslint-disable-next-line no-undef */ - requirejs( - ['as/dto/space/update/SpaceUpdate'], - (SpaceUpdate) => { - let spaceUpdate = new SpaceUpdate() - spaceUpdate.setSpaceId(permId) - spaceUpdate.setDescription(description) - v3.updateSpaces([spaceUpdate]).done(resolve).fail(reject) - } - ) - }) -} + getSpaces() { + let v3 = this.v3 + return new Promise( (resolve, reject) => { + /* eslint-disable-next-line no-undef */ + requirejs( + ['as/dto/space/search/SpaceSearchCriteria', 'as/dto/space/fetchoptions/SpaceFetchOptions' ], + (SpaceSearchCriteria, SpaceFetchOptions) => + v3.searchSpaces(new SpaceSearchCriteria(), new SpaceFetchOptions()).done(resolve).fail(reject) + ) + }) + } -function searchProjects(spacePermId) { - return new Promise( (resolve, reject) => { - /* eslint-disable-next-line no-undef */ - requirejs( - ['as/dto/project/search/ProjectSearchCriteria', 'as/dto/project/fetchoptions/ProjectFetchOptions'], - (ProjectSearchCriteria, ProjectFetchOptions) => { - let searchCriteria = new ProjectSearchCriteria() - searchCriteria.withSpace().withPermId().thatEquals(spacePermId) - let fetchOptions = new ProjectFetchOptions() - v3.searchProjects(searchCriteria, fetchOptions).done(resolve).fail(reject) - } - ) - }) -} + updateSpace(permId, description) { + let v3 = this.v3 + return new Promise( (resolve, reject) => { + /* eslint-disable-next-line no-undef */ + requirejs( + ['as/dto/space/update/SpaceUpdate'], + (SpaceUpdate) => { + let spaceUpdate = new SpaceUpdate() + spaceUpdate.setSpaceId(permId) + spaceUpdate.setDescription(description) + v3.updateSpaces([spaceUpdate]).done(resolve).fail(reject) + } + ) + }) + } + + searchProjects(spacePermId) { + let v3 = this.v3 + return new Promise( (resolve, reject) => { + /* eslint-disable-next-line no-undef */ + requirejs( + ['as/dto/project/search/ProjectSearchCriteria', 'as/dto/project/fetchoptions/ProjectFetchOptions'], + (ProjectSearchCriteria, ProjectFetchOptions) => { + let searchCriteria = new ProjectSearchCriteria() + searchCriteria.withSpace().withPermId().thatEquals(spacePermId) + let fetchOptions = new ProjectFetchOptions() + v3.searchProjects(searchCriteria, fetchOptions).done(resolve).fail(reject) + } + ) + }) + } -export default { - login: login, - logout: logout, - getSpaces: getSpaces, - updateSpace: updateSpace, - searchProjects: searchProjects, }