From 777b7785dbfdffc1ac78d45ebe626adbcf4a46f0 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Thu, 2 Jun 2016 12:46:51 -0600 Subject: [PATCH 01/21] initial redux test commit --- package.json | 3 + src/network/index.js | 13 +- src/redux/actions/projects.js | 49 +++-- src/redux/reducers/projects.js | 10 +- src/workflows/index.js | 9 +- test/fakepack.js | 71 +++++++ test/helpers/workflowNames.js | 21 ++ test/karma.redux.js | 97 +++++++++ test/redux/projects.js | 108 ++++++++++ test/sampleData/basicFullState.js | 269 +++++++++++++++++++++++++ test/sampleData/projectsData.js | 70 +++++++ test/sampleData/simulationsForProj1.js | 125 ++++++++++++ test/tests.webpack.js | 2 + webpack.config.js | 1 + 14 files changed, 812 insertions(+), 36 deletions(-) create mode 100644 test/fakepack.js create mode 100644 test/helpers/workflowNames.js create mode 100644 test/karma.redux.js create mode 100644 test/redux/projects.js create mode 100644 test/sampleData/basicFullState.js create mode 100644 test/sampleData/projectsData.js create mode 100644 test/sampleData/simulationsForProj1.js create mode 100644 test/tests.webpack.js diff --git a/package.json b/package.json index 5ae13de3..c31e18f2 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "env:dev" : "export NODE_ENV='development'", "env:prod" : "export NODE_ENV='production'", + "env:test" : "export NODE_ENV='test'", "prebuild" : "npm run check && npm run env:dev", "build" : "fix-autobahn && webpack --progress --colors", @@ -57,6 +58,8 @@ "start": "npm run env:dev && fix-autobahn && webpack-dev-server --progress --open", + "test:redux": "npm run env:test && karma start test/karma.redux.js", + "commit" : "git cz", "semantic-release" : "semantic-release pre && npm publish && semantic-release post" }, diff --git a/src/network/index.js b/src/network/index.js index 104bb898..4d916d27 100644 --- a/src/network/index.js +++ b/src/network/index.js @@ -36,7 +36,18 @@ const endpoints = [ user, ]; +var url; +if (process.env.NODE_ENV === 'test') { + url = { + protocol: 'http:', + hostname: 'test', + port: 80, + }; +} else { + url = window.location; +} + const girderClient = ClientBuilder.build( - window.location, endpoints); + url, endpoints); export default girderClient; diff --git a/src/redux/actions/projects.js b/src/redux/actions/projects.js index c818daff..be9a1018 100644 --- a/src/redux/actions/projects.js +++ b/src/redux/actions/projects.js @@ -2,9 +2,8 @@ import client from '../../network'; import * as SimulationHelper from '../../network/helpers/simulations'; import * as ProjectHelper from '../../network/helpers/projects'; import * as netActions from './network'; -import { dispatch } from '../index.js'; - import * as router from './router'; +import { dispatch } from '../index.js'; export const FETCH_PROJECT_LIST = 'FETCH_PROJECT_LIST'; export const UPDATE_PROJECT_LIST = 'UPDATE_PROJECT_LIST'; @@ -33,18 +32,16 @@ export function fetchProjectSimulations(id) { return dispatch => { const action = netActions.addNetworkCall(`fetch_project_simulations_${id}`, 'Retreive project simulations'); - client.listSimulations(id) - .then( - resp => { - const simulations = resp.data; - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(updateProjectSimulations(id, simulations)); - }, - error => { - dispatch(netActions.errorNetworkCall(action.id, error)); - }); - - return action; + return client.listSimulations(id) + .then((resp) => { + const simulations = resp.data; + // dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(updateProjectSimulations(id, simulations)); + }) + .catch((error) => { + dispatch(netActions.errorNetworkCall(action.id, error)); + throw new Error('sim fetch fails'); + }); }; } @@ -52,20 +49,20 @@ export function fetchProjectList() { return dispatch => { const action = netActions.addNetworkCall('fetch_project_list', 'Retreive projects'); - client.listProjects() - .then( - resp => { - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(updateProjectList(resp.data)); - resp.data.forEach(project => { - dispatch(fetchProjectSimulations(project._id)); - }); - }, - error => { - dispatch(netActions.errorNetworkCall(action.id, error)); + return client.listProjects() + .then((resp) => { + dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(updateProjectList(resp.data)); + resp.data.forEach(project => { + dispatch(fetchProjectSimulations(project._id)); }); + }) + .catch((error) => { + dispatch(netActions.errorNetworkCall(action.id, error)); + throw new Error('proj fetch fails'); + }); - return action; + // return action; }; } diff --git a/src/redux/reducers/projects.js b/src/redux/reducers/projects.js index 5d55ad55..c6d306c5 100644 --- a/src/redux/reducers/projects.js +++ b/src/redux/reducers/projects.js @@ -1,14 +1,8 @@ import * as Actions from '../actions/projects'; -// import deepClone from 'mout/src/lang/deepClone'; -import Workflows from '../../workflows'; +import { workflowNames } from 'workflows'; // alias import Helper from './ListActiveMapByIdHelper'; -const workflowNames = Object.keys(Workflows).map(value => { - const label = Workflows[value].name; - return { value, label }; -}); - -const initialState = { +export const initialState = { list: [], active: null, mapById: {}, diff --git a/src/workflows/index.js b/src/workflows/index.js index 74029721..8d97a970 100644 --- a/src/workflows/index.js +++ b/src/workflows/index.js @@ -3,9 +3,16 @@ import PyFr from './pyfr/pyfr-simput'; import PyFrExec from './pyfr/pyfr-exec'; import Visualizer from './visualizer'; -export default { +const Workflows = { CodeSaturn, PyFr, PyFrExec, Visualizer, }; + +export const workflowNames = Object.keys(Workflows).map(value => { + const label = Workflows[value].name; + return { value, label }; +}); + +export default Workflows; diff --git a/test/fakepack.js b/test/fakepack.js new file mode 100644 index 00000000..d4000941 --- /dev/null +++ b/test/fakepack.js @@ -0,0 +1,71 @@ +var path = require('path'), + webpack = require('webpack'); + +function nodeEnv() { + if (process.env.NODE_ENV) { + return '\'' + process.env.NODE_ENV + '\''; + } + return '\'development\''; +} + +var definePlugin = new webpack.DefinePlugin({ + 'process.env.NODE_ENV': nodeEnv(), +}); + +module.exports = { + entry: { + 'tests.webpack.js': './test/tests.webpack.js' + }, + plugins: [ + definePlugin, + ], + output: { + path: './', + filename: 'WOW.js', + }, + // node : { fs: 'empty' }, // prevents an error in nock + module: { + loaders: [ + { + test: /\.svg$/, + loader: 'svg-sprite', + exclude: /fonts/, + }, { + test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, + loader: 'url-loader?limit=60000&mimetype=application/font-woff', + }, { + test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, + loader: 'url-loader?limit=60000', + include: /fonts/, + }, { + test: /\.(png|jpg)$/, + loader: 'url-loader?limit=8192', + }, { + test: /\.css$/, + loader: 'style!css!postcss', + }, { + test: /\.mcss$/, + loader: 'style!css?modules&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]!postcss', + }, { + test: /\.c$/i, + loader: 'shader', + }, { + test: /\.json$/, + loader: 'json-loader', + }, { + test: /\.html$/, + loader: 'html-loader', + }, { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel?presets[]=es2015,presets[]=react', + } + ] + }, + resolve: { + alias: { + HPCCloudStyle: path.resolve('./style'), + workflows: path.resolve('./test/helpers/workflowNames'), + }, + }, +}; diff --git a/test/helpers/workflowNames.js b/test/helpers/workflowNames.js new file mode 100644 index 00000000..33e71642 --- /dev/null +++ b/test/helpers/workflowNames.js @@ -0,0 +1,21 @@ +// redux/actions/projects takes a workflows/index.js just for the workflow names +// however by requiring that file we require a ton of react components which we're +// not interested in testing here. So, we resolve 'workflows' to this file when testing. +module.exports.workflowNames = [ + { + value: 'CodeSaturn', + label: 'Code Saturn', + }, + { + value: 'PyFr', + label: 'PyFR', + }, + { + value: 'PyFrExec', + label: 'PyFR (Runtime)', + }, + { + value: 'Visualizer', + label: 'ParaViewWeb', + }, +]; diff --git a/test/karma.redux.js b/test/karma.redux.js new file mode 100644 index 00000000..a562aa6a --- /dev/null +++ b/test/karma.redux.js @@ -0,0 +1,97 @@ +/* eslint-disable */ +var path = require('path'), + webpack = require('webpack'); + +function nodeEnv() { + if (process.env.NODE_ENV) { + return '\'' + process.env.NODE_ENV + '\''; + } + return '\'development\''; +} + +var definePlugin = new webpack.DefinePlugin({ + 'process.env.NODE_ENV': nodeEnv(), +}); + +const wpConfig = { + entry: { + 'tests.webpack.js': './test/tests.webpack.js' + }, + plugins: [ + definePlugin + ], + progress: true, + module: { + loaders: [ + { + test: /\.svg$/, + loader: 'svg-sprite', + exclude: /fonts/, + }, { + test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, + loader: 'url-loader?limit=60000&mimetype=application/font-woff', + }, { + test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, + loader: 'url-loader?limit=60000', + include: /fonts/, + }, { + test: /\.(png|jpg)$/, + loader: 'url-loader?limit=8192', + }, { + test: /\.css$/, + loader: 'style!css!postcss', + }, { + test: /\.mcss$/, + loader: 'style!css?modules&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]!postcss', + }, { + test: /\.c$/i, + loader: 'shader', + }, { + test: /\.json$/, + loader: 'json-loader', + }, { + test: /\.html$/, + loader: 'html-loader', + }, { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel?presets[]=es2015,presets[]=react', + } + ] + }, + resolve: { + alias: { + // see that file for why we do this. + workflows: path.resolve('./test/helpers/workflowNames'), + // Constants.js uses Theme.mcss + HPCCloudStyle: path.resolve('./style'), + }, + }, +}; + +module.exports = function(config) { + config.set({ + basePath: '.', + client: { + captureConsole: true + }, + singleRun: true, + frameworks: ['jasmine'], + browsers: ['PhantomJS'], + reporters: ['spec'], + plugins: [ + 'karma-jasmine', + 'karma-spec-reporter', + 'karma-webpack', + 'karma-phantomjs-launcher' + ], + preprocessors: { + 'tests.webpack.js': ['webpack'], + }, + webpack: wpConfig, + files: [ + '../node_modules/kw-web-suite/node_modules/babel-polyfill/dist/polyfill.js', + 'tests.webpack.js', + ], + }); +}; diff --git a/test/redux/projects.js b/test/redux/projects.js new file mode 100644 index 00000000..f83b68a4 --- /dev/null +++ b/test/redux/projects.js @@ -0,0 +1,108 @@ +import * as Actions from '../../src/redux/actions/projects'; +import projectsReducer from '../../src/redux/reducers/projects'; +import { initialState } from '../../src/redux/reducers/projects'; + +import projectsData from '../sampleData/projectsData'; +import simulationData from '../sampleData/simulationsForProj1'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +import fauxJax from 'faux-jax'; + +/* eslint-disable no-undef */ + +registerMiddlewares([thunk]); +registerAssertions(); + +describe('project actions', () => { + // ---------------------------------------------------------------------------- + // SIMPLE ACTIONS + // ---------------------------------------------------------------------------- + describe('basic actions', () => { + const newState = Object.assign({}, initialState); + it('should update a project list', () => { + const expectedAction = { type: Actions.UPDATE_PROJECT_LIST, projects: projectsData }; + + expect(Actions.updateProjectList(projectsData)) + .toDispatchActions(expectedAction); + + newState.list = projectsData.map((el) => el._id); + projectsData.forEach((el) => { + newState.mapById[el._id] = el; + }); + expect(projectsReducer(initialState, expectedAction)) + .toEqual(newState); + }); + + it('should update project simulations', () => { + const id = projectsData[0]._id; + const expectedAction = { type: Actions.UPDATE_PROJECT_SIMULATIONS, id, simulations: simulationData }; + expect(Actions.updateProjectSimulations(id, simulationData)) + .toDispatchActions(expectedAction); + + expect(Object.keys(projectsReducer(initialState, expectedAction).simulations[id]).length) + .toEqual(simulationData.length); + }); + + it('should update project data', () => { + const project = projectsData[0]; + expect(Actions.updateProject(project)) + .toDispatchActions({ type: Actions.UPDATE_PROJECT, project }); + }); + + it('should update simulation data', () => { + const simulation = simulationData[0]; + expect(Actions.updateSimulation(simulation)) + .toDispatchActions({ type: Actions.UPDATE_SIMULATION, simulation }); + }); + }); + + // ---------------------------------------------------------------------------- + // AYSYNCHRONUS ACTIONS + // ---------------------------------------------------------------------------- + function fauxRes(replyCode, replyData) { + return (request) => { + request.respond(replyCode, { 'Content-Type': 'application/json' }, JSON.stringify(replyData)); + }; + } + function complete(done) { + return (err) => { + if (err) { + done.fail(err); + } + done(); + }; + } + describe('async actions', () => { + beforeAll(() => { + fauxJax.install(); + }); + + afterEach(() => { + // fauxJax inherits from EventEmitter, we do this afterEach because we + // change the event listener function almost everytime + fauxJax.removeAllListeners('request'); + }); + + afterAll(() => { + fauxJax.restore(); + }); + + it('should get projects', (done) => { + fauxJax.on('request', fauxRes(200, projectsData)); + expect(Actions.fetchProjectList()) + .toDispatchActions({ type: Actions.UPDATE_PROJECT_LIST, projects: projectsData }, complete(done)); + }); + + const id = projectsData[0]._id; + it('should get a project\'s simulations', (done) => { + fauxJax.on('request', fauxRes(200, simulationData)); + expect(Actions.fetchProjectSimulations(id)) + .toDispatchActions({ type: Actions.UPDATE_PROJECT_SIMULATIONS, id, simulations: simulationData }, complete(done)); + + // expect(projectsReducer('')) + }); + }); +}); diff --git a/test/sampleData/basicFullState.js b/test/sampleData/basicFullState.js new file mode 100644 index 00000000..44c3bd7a --- /dev/null +++ b/test/sampleData/basicFullState.js @@ -0,0 +1,269 @@ +/* eslint-disable */ +export default { + "auth": { + "pending": false, + "user": { + "_accessLevel": 2, + "_id": "574c841b0640fd3f1a3b3741", + "_modelType": "user", + "admin": false, + "created": "2016-05-30T18:19:06.921000+00:00", + "email": "123@the.wow", + "firstName": "Emma", + "groupInvites": [], + "groups": [], + "lastName": "Maybe", + "login": "qwert", + "public": true, + "size": 438974 + } + }, + "fs": { + "folderMapById": {}, + "itemMapById": {} + }, + "network": { + "pending": {}, + "success": {}, + "error": {}, + "backlog": [], + "progress": {}, + "progressReset": false + }, + "preferences": { + "clusters": { + "list": [], + "active": 0, + "pending": false, + "mapById": {} + }, + "aws": { + "list": [], + "active": 0, + "pending": false, + "mapById": {} + }, + "statuses": { + "ec2": [], + "clusters": [] + } + }, + "projects": { + "list": [ + "574c84270640fd3f1a3b3747" + ], + "active": null, + "mapById": { + "574c84270640fd3f1a3b3747": { + "_id": "574c84270640fd3f1a3b3747", + "access": { + "groups": [], + "users": [ + { + "id": "574c841b0640fd3f1a3b3741", + "level": 2 + } + ] + }, + "created": "2016-05-30T18:19:19.405000+00:00", + "description": "", + "folderId": "574c84270640fd3f1a3b3745", + "metadata": { + "inputFolder": { + "_id": "574c84270640fd3f1a3b3749", + "files": {} + }, + "outputFolder": { + "_id": "574c84270640fd3f1a3b3748", + "files": {} + } + }, + "name": "proj 1", + "steps": [ + "Introduction", + "Simulation", + "Visualization" + ], + "type": "PyFrExec", + "updated": "2016-05-30T18:19:19.461000+00:00", + "userId": "574c841b0640fd3f1a3b3741" + } + }, + "simulations": { + "574c84270640fd3f1a3b3747": { + "list": [ + "574c8aa00640fd3f1a3b379f", + "574ca0420640fd6e13b11e43" + ], + "active": null + } + }, + "workflowNames": [ + { + "value": "CodeSaturn", + "label": "Code Saturn" + }, + { + "value": "PyFr", + "label": "PyFR" + }, + { + "value": "PyFrExec", + "label": "PyFR (Runtime)" + }, + { + "value": "Visualizer", + "label": "ParaViewWeb" + } + ] + }, + "simulations": { + "mapById": { + "574c8aa00640fd3f1a3b379f": { + "_id": "574c8aa00640fd3f1a3b379f", + "access": { + "groups": [], + "users": [ + { + "id": "574c841b0640fd3f1a3b3741", + "level": 2 + } + ] + }, + "active": "Simulation", + "created": "2016-05-30T18:46:56.487000+00:00", + "description": "will error", + "disabled": [ + "Visualization" + ], + "folderId": "574c8aa00640fd3f1a3b379b", + "metadata": { + "inputFolder": { + "_id": "574c8aa00640fd3f1a3b37a1", + "files": { + "ini": "574c8aa00640fd3f1a3b37a6", + "mesh": "574c8aa00640fd3f1a3b37a7" + } + }, + "outputFolder": { + "_id": "574c8aa00640fd3f1a3b37a0", + "files": {} + }, + "status": "complete" + }, + "name": "sim001", + "projectId": "574c84270640fd3f1a3b3747", + "steps": { + "Introduction": { + "folderId": "574c8aa00640fd3f1a3b379c", + "metadata": {}, + "status": "created", + "type": "information" + }, + "Simulation": { + "folderId": "574c8aa00640fd3f1a3b379e", + "metadata": { + "sessionId": "MC43NjU1MDAyMzYxNDc5NTI4LDAuMDYxMTY5Njg0NTE5NTAzMTEsMC43NDY1NzU1NDY2MTEwMjg0", + "taskflowId": "574c9d900640fd6e133b4b57" + }, + "status": "created", + "type": "output", + "view": "run" + }, + "Visualization": { + "folderId": "574c8aa00640fd3f1a3b379d", + "metadata": {}, + "status": "created", + "type": "output" + } + }, + "updated": "2016-05-30T20:25:03.260000+00:00", + "userId": "574c841b0640fd3f1a3b3741" + }, + "574ca0420640fd6e13b11e43": { + "_id": "574ca0420640fd6e13b11e43", + "access": { + "groups": [], + "users": [ + { + "id": "574c841b0640fd3f1a3b3741", + "level": 2 + } + ] + }, + "active": "Simulation", + "created": "2016-05-30T20:19:14.859000+00:00", + "description": "error this!", + "disabled": [ + "Visualization" + ], + "folderId": "574ca0420640fd6e13b11e3f", + "metadata": { + "inputFolder": { + "_id": "574ca0420640fd6e13b11e45", + "files": { + "ini": "574ca0430640fd6e13b11e4a", + "mesh": "574ca0430640fd6e13b11e4b" + } + }, + "outputFolder": { + "_id": "574ca0420640fd6e13b11e44", + "files": {} + }, + "status": "terminated" + }, + "name": "sim002", + "projectId": "574c84270640fd3f1a3b3747", + "steps": { + "Introduction": { + "folderId": "574ca0420640fd6e13b11e40", + "metadata": {}, + "status": "created", + "type": "information" + }, + "Simulation": { + "folderId": "574ca0420640fd6e13b11e42", + "metadata": { + "sessionId": "MC4wOTE2NTY4MTY4MjMwOTA5LDAuMDcwOTkyOTM3NzIxODk2OTYsMC4xMjAyODE0OTE5NDg5NjU0Mg==", + "taskflowId": "574ca2670640fd6e134265e1" + }, + "status": "created", + "type": "output", + "view": "run" + }, + "Visualization": { + "folderId": "574ca0420640fd6e13b11e41", + "metadata": {}, + "status": "created", + "type": "output" + } + }, + "updated": "2016-05-30T20:34:17.871000+00:00", + "userId": "574c841b0640fd3f1a3b3741" + } + } + }, + "taskflows": { + "mapById": {}, + "taskflowMapByTaskId": {}, + "taskflowMapByJobId": {}, + "updateLogs": [] + }, + "routing": { + "locationBeforeTransitions": { + "pathname": "\/", + "search": "", + "hash": "", + "state": null, + "action": "POP", + "key": "5ix77j", + "query": { + + }, + "$searchBase": { + "search": "", + "searchBase": "" + } + } + } +} diff --git a/test/sampleData/projectsData.js b/test/sampleData/projectsData.js new file mode 100644 index 00000000..f989f293 --- /dev/null +++ b/test/sampleData/projectsData.js @@ -0,0 +1,70 @@ +// GET /projects with two sample projects inside +export default [ + { + "_id": "574c84270640fd3f1a3b3747", + "access": { + "groups": [], + "users": [ + { + "id": "574c841b0640fd3f1a3b3741", + "level": 2 + } + ] + }, + "created": "2016-05-30T18:19:19.405000+00:00", + "description": "", + "folderId": "574c84270640fd3f1a3b3745", + "metadata": { + "inputFolder": { + "_id": "574c84270640fd3f1a3b3749", + "files": {} + }, + "outputFolder": { + "_id": "574c84270640fd3f1a3b3748", + "files": {} + } + }, + "name": "proj 1", + "steps": [ + "Introduction", + "Simulation", + "Visualization" + ], + "type": "PyFrExec", + "updated": "2016-05-30T18:19:19.461000+00:00", + "userId": "574c841b0640fd3f1a3b3741" + }, + { + "_id": "574f1e440640fd086c69303a", + "access": { + "groups": [], + "users": [ + { + "id": "574c841b0640fd3f1a3b3741", + "level": 2 + } + ] + }, + "created": "2016-06-01T17:41:24.468000+00:00", + "description": "", + "folderId": "574f1e440640fd086c693038", + "metadata": { + "inputFolder": { + "_id": "574f1e440640fd086c69303c", + "files": {} + }, + "outputFolder": { + "_id": "574f1e440640fd086c69303b", + "files": {} + } + }, + "name": "proj 2", + "steps": [ + "Introduction", + "Visualization" + ], + "type": "Visualizer", + "updated": "2016-06-01T17:41:24.518000+00:00", + "userId": "574c841b0640fd3f1a3b3741" + } +] diff --git a/test/sampleData/simulationsForProj1.js b/test/sampleData/simulationsForProj1.js new file mode 100644 index 00000000..4bc34ca5 --- /dev/null +++ b/test/sampleData/simulationsForProj1.js @@ -0,0 +1,125 @@ +// simulations for 'proj 1' +export default [ + { + "_id": "574c8aa00640fd3f1a3b379f", + "access": { + "groups": [], + "users": [ + { + "id": "574c841b0640fd3f1a3b3741", + "level": 2 + } + ] + }, + "active": "Simulation", + "created": "2016-05-30T18:46:56.487000+00:00", + "description": "will error", + "disabled": [ + "Visualization" + ], + "folderId": "574c8aa00640fd3f1a3b379b", + "metadata": { + "inputFolder": { + "_id": "574c8aa00640fd3f1a3b37a1", + "files": { + "ini": "574c8aa00640fd3f1a3b37a6", + "mesh": "574c8aa00640fd3f1a3b37a7" + } + }, + "outputFolder": { + "_id": "574c8aa00640fd3f1a3b37a0", + "files": {} + }, + "status": "complete" + }, + "name": "sim001", + "projectId": "574c84270640fd3f1a3b3747", + "steps": { + "Introduction": { + "folderId": "574c8aa00640fd3f1a3b379c", + "metadata": {}, + "status": "created", + "type": "information" + }, + "Simulation": { + "folderId": "574c8aa00640fd3f1a3b379e", + "metadata": { + "sessionId": "MC43NjU1MDAyMzYxNDc5NTI4LDAuMDYxMTY5Njg0NTE5NTAzMTEsMC43NDY1NzU1NDY2MTEwMjg0", + "taskflowId": "574c9d900640fd6e133b4b57" + }, + "status": "created", + "type": "output", + "view": "run" + }, + "Visualization": { + "folderId": "574c8aa00640fd3f1a3b379d", + "metadata": {}, + "status": "created", + "type": "output" + } + }, + "updated": "2016-05-30T20:25:03.260000+00:00", + "userId": "574c841b0640fd3f1a3b3741" + }, + { + "_id": "574ca0420640fd6e13b11e43", + "access": { + "groups": [], + "users": [ + { + "id": "574c841b0640fd3f1a3b3741", + "level": 2 + } + ] + }, + "active": "Simulation", + "created": "2016-05-30T20:19:14.859000+00:00", + "description": "error this!", + "disabled": [ + "Visualization" + ], + "folderId": "574ca0420640fd6e13b11e3f", + "metadata": { + "inputFolder": { + "_id": "574ca0420640fd6e13b11e45", + "files": { + "ini": "574ca0430640fd6e13b11e4a", + "mesh": "574ca0430640fd6e13b11e4b" + } + }, + "outputFolder": { + "_id": "574ca0420640fd6e13b11e44", + "files": {} + }, + "status": "terminated" + }, + "name": "sim002", + "projectId": "574c84270640fd3f1a3b3747", + "steps": { + "Introduction": { + "folderId": "574ca0420640fd6e13b11e40", + "metadata": {}, + "status": "created", + "type": "information" + }, + "Simulation": { + "folderId": "574ca0420640fd6e13b11e42", + "metadata": { + "sessionId": "MC4wOTE2NTY4MTY4MjMwOTA5LDAuMDcwOTkyOTM3NzIxODk2OTYsMC4xMjAyODE0OTE5NDg5NjU0Mg==", + "taskflowId": "574ca2670640fd6e134265e1" + }, + "status": "created", + "type": "output", + "view": "run" + }, + "Visualization": { + "folderId": "574ca0420640fd6e13b11e41", + "metadata": {}, + "status": "created", + "type": "output" + } + }, + "updated": "2016-05-30T20:34:17.871000+00:00", + "userId": "574c841b0640fd3f1a3b3741" + } +] \ No newline at end of file diff --git a/test/tests.webpack.js b/test/tests.webpack.js new file mode 100644 index 00000000..b9454d76 --- /dev/null +++ b/test/tests.webpack.js @@ -0,0 +1,2 @@ +var context = require.context('./redux', true, /\.js$/); +context.keys().forEach(context); diff --git a/webpack.config.js b/webpack.config.js index f278c996..f6a37f71 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -43,6 +43,7 @@ module.exports = { SimputStyle: path.resolve('./node_modules/simput/style'), VisualizerStyle: path.resolve('./node_modules/pvw-visualizer/style'), HPCCloudStyle: path.resolve('./style'), + workflows: path.resolve('./src/workflows') }, }, postcss: [ From c1c8d841423afac7a69ef5a0d6a3e4574071f071 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Thu, 2 Jun 2016 12:50:05 -0600 Subject: [PATCH 02/21] added travis line --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 603fe317..2eb10307 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,6 +58,7 @@ script: - cmake -DBUILD_JAVASCRIPT_TESTS:BOOL=OFF -DPYTHON_COVERAGE:BOOL=OFF -DPYTHON_VERSION:STRING=${TRAVIS_PYTHON_VERSION} "${HOME}/build/girder" - ctest -R hpccloud -VV - ctest -R pvwproxy -VV + - npm run test:redux after_success: - npm run semantic-release From e6b95c6886f0b32c0c1418460612adc22a64cacc Mon Sep 17 00:00:00 2001 From: TristanWright Date: Thu, 2 Jun 2016 13:08:52 -0600 Subject: [PATCH 03/21] new test requirements in package.json --- .travis.yml | 2 +- package.json | 13 +++++++++++++ test/karma.redux.js | 1 - 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2eb10307..56edc7a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,13 +52,13 @@ script: # Run client tests, only with python 2.7 - if [ -n "${PY2}" ]; then npm install; fi - if [ -n "${PY2}" ]; then npm run build:release; fi + - if [ -n "${PY2}" ]; then npm run test:redux -g; fi # Now run server tests - mkdir _girder_build - pushd _girder_build - cmake -DBUILD_JAVASCRIPT_TESTS:BOOL=OFF -DPYTHON_COVERAGE:BOOL=OFF -DPYTHON_VERSION:STRING=${TRAVIS_PYTHON_VERSION} "${HOME}/build/girder" - ctest -R hpccloud -VV - ctest -R pvwproxy -VV - - npm run test:redux after_success: - npm run semantic-release diff --git a/package.json b/package.json index c31e18f2..23085fb0 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,19 @@ "bootstrap": "3.3.6", "react-router": "2.0.1", + "karma": "0.13.22", + "karma-jasmine": "1.0.2", + "karma-spec-reporter": "0.0.26", + "karma-webpack": "1.7.0", + "karma-phantomjs-launcher": "1.0.0", + "phantomjs-prebuilt": "2.1.7", + "faux-jax": "5.0.1", + "jasmine": "2.4.1", + "redux-actions-assertions": "1.0.8", + "redux-thunk": "2.1.0", + "expect": "1.20.1", + "babel-polyfill": "6.9.1", + "paraviewweb": "1.6.0", "simput": "1.2.0", "pvw-visualizer": "1.0.11", diff --git a/test/karma.redux.js b/test/karma.redux.js index a562aa6a..5d9c9b2a 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -20,7 +20,6 @@ const wpConfig = { plugins: [ definePlugin ], - progress: true, module: { loaders: [ { From 4597d18cbfd5f9f884ed5aae5d2f1ba670be113d Mon Sep 17 00:00:00 2001 From: TristanWright Date: Tue, 7 Jun 2016 15:58:55 -0600 Subject: [PATCH 04/21] added mysteriously failying async tests --- package.json | 12 ++--- src/redux/actions/projects.js | 42 +++++++-------- test/karma.redux.js | 72 ++------------------------ test/redux/projects.js | 40 +++++++++++--- test/tests.webpack.js | 1 + test/{fakepack.js => webpack.redux.js} | 16 +++--- 6 files changed, 67 insertions(+), 116 deletions(-) rename test/{fakepack.js => webpack.redux.js} (88%) diff --git a/package.json b/package.json index 23085fb0..52c97cc7 100644 --- a/package.json +++ b/package.json @@ -60,18 +60,14 @@ "postinstall" : "npm run install:pyfr && fix-kw-web-suite || true", "check" : "node bin/version-check.js", - "env:dev" : "export NODE_ENV='development'", - "env:prod" : "export NODE_ENV='production'", - "env:test" : "export NODE_ENV='test'", - - "prebuild" : "npm run check && npm run env:dev", + "prebuild" : "npm run check", "build" : "fix-autobahn && webpack --progress --colors", "build:debug" : "fix-autobahn && webpack --display-modules", - "build:release" : "npm run env:prod && fix-autobahn && webpack -p --progress --colors", + "build:release" : "fix-autobahn && NODE_ENV='production' webpack -p --progress --colors", - "start": "npm run env:dev && fix-autobahn && webpack-dev-server --progress --open", + "start": "fix-autobahn && webpack-dev-server --progress --open", - "test:redux": "npm run env:test && karma start test/karma.redux.js", + "test:redux": "NODE_ENV='test' karma start test/karma.redux.js", "commit" : "git cz", "semantic-release" : "semantic-release pre && npm publish && semantic-release post" diff --git a/src/redux/actions/projects.js b/src/redux/actions/projects.js index be9a1018..04c482d9 100644 --- a/src/redux/actions/projects.js +++ b/src/redux/actions/projects.js @@ -35,7 +35,7 @@ export function fetchProjectSimulations(id) { return client.listSimulations(id) .then((resp) => { const simulations = resp.data; - // dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(netActions.successNetworkCall(action.id, resp)); dispatch(updateProjectSimulations(id, simulations)); }) .catch((error) => { @@ -70,7 +70,7 @@ export function deleteProject(project) { return dispatch => { const action = netActions.addNetworkCall(`delete_project_${project._id}`, `Delete project ${project.name}`); - client.deleteProject(project._id) + return client.deleteProject(project._id) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -79,9 +79,8 @@ export function deleteProject(project) { }, error => { dispatch(netActions.errorNetworkCall(action.id, error)); + throw new Error('project delete fails'); }); - - return action; }; } @@ -101,7 +100,6 @@ export function updateProject(project) { return { type: UPDATE_PROJECT, project }; } - export function saveProject(project, attachments) { return dispatch => { const action = netActions.addNetworkCall('save_project', `Save project ${project.name}`); @@ -110,7 +108,7 @@ export function saveProject(project, attachments) { dispatch(netActions.prepareUpload(attachments)); } - ProjectHelper.saveProject(project, attachments) + return ProjectHelper.saveProject(project, attachments) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -125,8 +123,6 @@ export function saveProject(project, attachments) { error => { dispatch(netActions.errorNetworkCall(action.id, error)); }); - - return action; }; } @@ -172,7 +168,7 @@ export function deleteSimulation(simulation, location) { return dispatch => { const action = netActions.addNetworkCall(`delete_simulation_${simulation._id}`, `Delete simulation ${simulation.name}`); - client.deleteSimulation(simulation._id) + return client.deleteSimulation(simulation._id) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -183,9 +179,10 @@ export function deleteSimulation(simulation, location) { }, error => { dispatch(netActions.errorNetworkCall(action.id, error)); + // throw new Error('project delete fails'); }); - return action; + // return action; }; } @@ -205,20 +202,17 @@ export function updateSimulationStep(id, stepName, data, location) { return dispatch => { const action = netActions.addNetworkCall(`update_simulation_step_${id}`, 'Update simulation step'); - client.updateSimulationStep(id, stepName, data) - .then( - resp => { - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(updateSimulation(resp.data)); - if (location) { - dispatch(router.replace(location)); - } - }, - error => { - dispatch(netActions.errorNetworkCall(action.id, error)); - }); - - return action; + return client.updateSimulationStep(id, stepName, data) + .then((resp) => { + dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(updateSimulation(resp.data)); + if (location) { + dispatch(router.replace(location)); + } + }) + .catch((error) => { + dispatch(netActions.errorNetworkCall(action.id, error)); + }); }; } diff --git a/test/karma.redux.js b/test/karma.redux.js index 5d9c9b2a..5bb58704 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -1,72 +1,6 @@ /* eslint-disable */ -var path = require('path'), - webpack = require('webpack'); - -function nodeEnv() { - if (process.env.NODE_ENV) { - return '\'' + process.env.NODE_ENV + '\''; - } - return '\'development\''; -} - -var definePlugin = new webpack.DefinePlugin({ - 'process.env.NODE_ENV': nodeEnv(), -}); - -const wpConfig = { - entry: { - 'tests.webpack.js': './test/tests.webpack.js' - }, - plugins: [ - definePlugin - ], - module: { - loaders: [ - { - test: /\.svg$/, - loader: 'svg-sprite', - exclude: /fonts/, - }, { - test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, - loader: 'url-loader?limit=60000&mimetype=application/font-woff', - }, { - test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, - loader: 'url-loader?limit=60000', - include: /fonts/, - }, { - test: /\.(png|jpg)$/, - loader: 'url-loader?limit=8192', - }, { - test: /\.css$/, - loader: 'style!css!postcss', - }, { - test: /\.mcss$/, - loader: 'style!css?modules&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]!postcss', - }, { - test: /\.c$/i, - loader: 'shader', - }, { - test: /\.json$/, - loader: 'json-loader', - }, { - test: /\.html$/, - loader: 'html-loader', - }, { - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel?presets[]=es2015,presets[]=react', - } - ] - }, - resolve: { - alias: { - // see that file for why we do this. - workflows: path.resolve('./test/helpers/workflowNames'), - // Constants.js uses Theme.mcss - HPCCloudStyle: path.resolve('./style'), - }, - }, -}; +// karma config file +var wpConfig = require('./webpack.redux.js'); module.exports = function(config) { config.set({ @@ -89,7 +23,7 @@ module.exports = function(config) { }, webpack: wpConfig, files: [ - '../node_modules/kw-web-suite/node_modules/babel-polyfill/dist/polyfill.js', + '../node_modules/babel-polyfill/dist/polyfill.js', 'tests.webpack.js', ], }); diff --git a/test/redux/projects.js b/test/redux/projects.js index f83b68a4..22d19502 100644 --- a/test/redux/projects.js +++ b/test/redux/projects.js @@ -10,8 +10,8 @@ import thunk from 'redux-thunk'; import { registerMiddlewares } from 'redux-actions-assertions'; import { registerAssertions } from 'redux-actions-assertions/expect'; import fauxJax from 'faux-jax'; - -/* eslint-disable no-undef */ +const req = 'request'; +/* global describe it afterEach afterAll beforeAll */ registerMiddlewares([thunk]); registerAssertions(); @@ -67,6 +67,7 @@ describe('project actions', () => { request.respond(replyCode, { 'Content-Type': 'application/json' }, JSON.stringify(replyData)); }; } + function complete(done) { return (err) => { if (err) { @@ -75,15 +76,17 @@ describe('project actions', () => { done(); }; } + describe('async actions', () => { beforeAll(() => { fauxJax.install(); + // fauxJax.setMaxListeners(1); }); afterEach(() => { // fauxJax inherits from EventEmitter, we do this afterEach because we // change the event listener function almost everytime - fauxJax.removeAllListeners('request'); + fauxJax.removeAllListeners(req); }); afterAll(() => { @@ -91,18 +94,43 @@ describe('project actions', () => { }); it('should get projects', (done) => { - fauxJax.on('request', fauxRes(200, projectsData)); + fauxJax.on(req, fauxRes(200, projectsData)); expect(Actions.fetchProjectList()) .toDispatchActions({ type: Actions.UPDATE_PROJECT_LIST, projects: projectsData }, complete(done)); }); const id = projectsData[0]._id; it('should get a project\'s simulations', (done) => { - fauxJax.on('request', fauxRes(200, simulationData)); + fauxJax.on(req, fauxRes(200, simulationData)); expect(Actions.fetchProjectSimulations(id)) .toDispatchActions({ type: Actions.UPDATE_PROJECT_SIMULATIONS, id, simulations: simulationData }, complete(done)); + }); + + it('should delete a project', (done) => { + fauxJax.on(req, fauxRes(200, projectsData[0])); + expect(Actions.deleteProject(projectsData[0])) + .toDispatchActions({ type: 'REMOVE_PROJECT', project: projectsData[0] }, complete(done)); + }); + + it('should save a project', (done) => { + fauxJax.on(req, fauxRes(200, projectsData[0])); + expect(Actions.saveProject(projectsData[0])) + .toDispatchActions({ type: Actions.UPDATE_PROJECT, project: projectsData[0] }, complete(done)); + }); + + it('should update simulation step', (done) => { + const expectedSim = Object.assign({}, simulationData[0]); + expectedSim.steps.Introduction.status = 'complete'; + fauxJax.on(req, fauxRes(202, expectedSim)); + expect(Actions.updateSimulationStep(expectedSim._id, 'Introduction', { status: 'complete' })) + .toDispatchActions({ type: Actions.UPDATE_SIMULATION, simulation: expectedSim }, complete(done)); + }); - // expect(projectsReducer('')) + it('should delete simulation', (done) => { + const deletedSim = Object.assign({}, simulationData[0]); + fauxJax.on(req, fauxRes(200, null)); + expect(Actions.deleteSimulation(deletedSim)) + .toDispatchActions({ type: Actions.REMOVE_SIMULATION, simulation: deletedSim }, complete(done)); }); }); }); diff --git a/test/tests.webpack.js b/test/tests.webpack.js index b9454d76..13cc465b 100644 --- a/test/tests.webpack.js +++ b/test/tests.webpack.js @@ -1,2 +1,3 @@ +// gathers the files to be tested, currently just redux. var context = require.context('./redux', true, /\.js$/); context.keys().forEach(context); diff --git a/test/fakepack.js b/test/webpack.redux.js similarity index 88% rename from test/fakepack.js rename to test/webpack.redux.js index d4000941..b6bea835 100644 --- a/test/fakepack.js +++ b/test/webpack.redux.js @@ -1,5 +1,6 @@ -var path = require('path'), - webpack = require('webpack'); +// webpack for redux tests +var webpack = require('webpack'), + path = require('path'); function nodeEnv() { if (process.env.NODE_ENV) { @@ -17,13 +18,8 @@ module.exports = { 'tests.webpack.js': './test/tests.webpack.js' }, plugins: [ - definePlugin, + definePlugin ], - output: { - path: './', - filename: 'WOW.js', - }, - // node : { fs: 'empty' }, // prevents an error in nock module: { loaders: [ { @@ -64,8 +60,10 @@ module.exports = { }, resolve: { alias: { - HPCCloudStyle: path.resolve('./style'), + // see that file for why we do this. workflows: path.resolve('./test/helpers/workflowNames'), + // Constants.js uses Theme.mcss + HPCCloudStyle: path.resolve('./style'), }, }, }; From 53fc57c6490bc960d48d3632452578380747bed2 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 8 Jun 2016 11:20:21 -0600 Subject: [PATCH 05/21] async spies, removed faux-jax --- package.json | 1 - src/network/remote/GirderClient.js | 4 +++ test/redux/projects.js | 45 +++++++++++++----------------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 52c97cc7..c56b9d5e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "karma-webpack": "1.7.0", "karma-phantomjs-launcher": "1.0.0", "phantomjs-prebuilt": "2.1.7", - "faux-jax": "5.0.1", "jasmine": "2.4.1", "redux-actions-assertions": "1.0.8", "redux-thunk": "2.1.0", diff --git a/src/network/remote/GirderClient.js b/src/network/remote/GirderClient.js index b9898791..0da1e2e6 100644 --- a/src/network/remote/GirderClient.js +++ b/src/network/remote/GirderClient.js @@ -286,6 +286,10 @@ export function build(config = location, ...extensions) { processExtension(extensions); // Return the newly composed object + // if env === test, return an unfrozen version so we can spy on it + if (process.env.NODE_ENV === 'test') { + return publicObject; + } return Object.freeze(publicObject); } diff --git a/test/redux/projects.js b/test/redux/projects.js index 22d19502..9cb3cd0f 100644 --- a/test/redux/projects.js +++ b/test/redux/projects.js @@ -1,6 +1,8 @@ import * as Actions from '../../src/redux/actions/projects'; import projectsReducer from '../../src/redux/reducers/projects'; import { initialState } from '../../src/redux/reducers/projects'; +import client from '../../src/network'; +import * as ProjectHelper from '../../src/network/helpers/projects'; import projectsData from '../sampleData/projectsData'; import simulationData from '../sampleData/simulationsForProj1'; @@ -9,13 +11,13 @@ import expect from 'expect'; import thunk from 'redux-thunk'; import { registerMiddlewares } from 'redux-actions-assertions'; import { registerAssertions } from 'redux-actions-assertions/expect'; -import fauxJax from 'faux-jax'; -const req = 'request'; -/* global describe it afterEach afterAll beforeAll */ +/* global describe it afterEach */ registerMiddlewares([thunk]); registerAssertions(); +// const client = Object.assign({}, srcClient); + describe('project actions', () => { // ---------------------------------------------------------------------------- // SIMPLE ACTIONS @@ -62,11 +64,6 @@ describe('project actions', () => { // ---------------------------------------------------------------------------- // AYSYNCHRONUS ACTIONS // ---------------------------------------------------------------------------- - function fauxRes(replyCode, replyData) { - return (request) => { - request.respond(replyCode, { 'Content-Type': 'application/json' }, JSON.stringify(replyData)); - }; - } function complete(done) { return (err) => { @@ -77,43 +74,39 @@ describe('project actions', () => { }; } - describe('async actions', () => { - beforeAll(() => { - fauxJax.install(); - // fauxJax.setMaxListeners(1); - }); + function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); + } + describe('async actions', () => { afterEach(() => { - // fauxJax inherits from EventEmitter, we do this afterEach because we - // change the event listener function almost everytime - fauxJax.removeAllListeners(req); - }); - - afterAll(() => { - fauxJax.restore(); + expect.restoreSpies(); }); it('should get projects', (done) => { - fauxJax.on(req, fauxRes(200, projectsData)); + setSpy(client, 'listProjects', projectsData); expect(Actions.fetchProjectList()) .toDispatchActions({ type: Actions.UPDATE_PROJECT_LIST, projects: projectsData }, complete(done)); + expect(client.listProjects).toHaveBeenCalled(); }); const id = projectsData[0]._id; it('should get a project\'s simulations', (done) => { - fauxJax.on(req, fauxRes(200, simulationData)); + setSpy(client, 'listSimulations', simulationData); expect(Actions.fetchProjectSimulations(id)) .toDispatchActions({ type: Actions.UPDATE_PROJECT_SIMULATIONS, id, simulations: simulationData }, complete(done)); + expect(client.listSimulations).toHaveBeenCalled(); }); it('should delete a project', (done) => { - fauxJax.on(req, fauxRes(200, projectsData[0])); + setSpy(client, 'deleteProject', projectsData[0]); expect(Actions.deleteProject(projectsData[0])) .toDispatchActions({ type: 'REMOVE_PROJECT', project: projectsData[0] }, complete(done)); }); it('should save a project', (done) => { - fauxJax.on(req, fauxRes(200, projectsData[0])); + setSpy(ProjectHelper, 'saveProject', projectsData[0]); expect(Actions.saveProject(projectsData[0])) .toDispatchActions({ type: Actions.UPDATE_PROJECT, project: projectsData[0] }, complete(done)); }); @@ -121,14 +114,14 @@ describe('project actions', () => { it('should update simulation step', (done) => { const expectedSim = Object.assign({}, simulationData[0]); expectedSim.steps.Introduction.status = 'complete'; - fauxJax.on(req, fauxRes(202, expectedSim)); + setSpy(client, 'updateSimulationStep', expectedSim); expect(Actions.updateSimulationStep(expectedSim._id, 'Introduction', { status: 'complete' })) .toDispatchActions({ type: Actions.UPDATE_SIMULATION, simulation: expectedSim }, complete(done)); }); it('should delete simulation', (done) => { const deletedSim = Object.assign({}, simulationData[0]); - fauxJax.on(req, fauxRes(200, null)); + setSpy(client, 'deleteSimulation', null); expect(Actions.deleteSimulation(deletedSim)) .toDispatchActions({ type: Actions.REMOVE_SIMULATION, simulation: deletedSim }, complete(done)); }); From baab2d86eb234bb2d2b970cdeeab6bcbfb24629b Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 8 Jun 2016 17:16:28 -0600 Subject: [PATCH 06/21] all project.js tests --- package.json | 2 +- src/redux/actions/projects.js | 6 +-- test/redux/projects.js | 78 +++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index c56b9d5e..0253309d 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "karma-phantomjs-launcher": "1.0.0", "phantomjs-prebuilt": "2.1.7", "jasmine": "2.4.1", - "redux-actions-assertions": "1.0.8", + "redux-actions-assertions": "1.1.0", "redux-thunk": "2.1.0", "expect": "1.20.1", "babel-polyfill": "6.9.1", diff --git a/src/redux/actions/projects.js b/src/redux/actions/projects.js index 04c482d9..43d8e59f 100644 --- a/src/redux/actions/projects.js +++ b/src/redux/actions/projects.js @@ -142,7 +142,7 @@ export function saveSimulation(simulation, attachments, location) { dispatch(netActions.prepareUpload(attachments)); } - SimulationHelper.saveSimulation(simulation, attachments) + return SimulationHelper.saveSimulation(simulation, attachments) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -159,8 +159,6 @@ export function saveSimulation(simulation, attachments, location) { error => { dispatch(netActions.errorNetworkCall(action.id, error)); }); - - return action; }; } @@ -204,7 +202,7 @@ export function updateSimulationStep(id, stepName, data, location) { return client.updateSimulationStep(id, stepName, data) .then((resp) => { - dispatch(netActions.successNetworkCall(action.id, resp)); + // dispatch(netActions.successNetworkCall(action.id, resp)); dispatch(updateSimulation(resp.data)); if (location) { dispatch(router.replace(location)); diff --git a/test/redux/projects.js b/test/redux/projects.js index 9cb3cd0f..b20f37c9 100644 --- a/test/redux/projects.js +++ b/test/redux/projects.js @@ -1,14 +1,15 @@ import * as Actions from '../../src/redux/actions/projects'; -import projectsReducer from '../../src/redux/reducers/projects'; -import { initialState } from '../../src/redux/reducers/projects'; +import projectsReducer, { initialState } from '../../src/redux/reducers/projects'; import client from '../../src/network'; import * as ProjectHelper from '../../src/network/helpers/projects'; +import * as SimulationHelper from '../../src/network/helpers/simulations'; import projectsData from '../sampleData/projectsData'; import simulationData from '../sampleData/simulationsForProj1'; import expect from 'expect'; import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; import { registerMiddlewares } from 'redux-actions-assertions'; import { registerAssertions } from 'redux-actions-assertions/expect'; /* global describe it afterEach */ @@ -16,7 +17,10 @@ import { registerAssertions } from 'redux-actions-assertions/expect'; registerMiddlewares([thunk]); registerAssertions(); -// const client = Object.assign({}, srcClient); +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} describe('project actions', () => { // ---------------------------------------------------------------------------- @@ -64,21 +68,6 @@ describe('project actions', () => { // ---------------------------------------------------------------------------- // AYSYNCHRONUS ACTIONS // ---------------------------------------------------------------------------- - - function complete(done) { - return (err) => { - if (err) { - done.fail(err); - } - done(); - }; - } - - function setSpy(target, method, data) { - expect.spyOn(target, method) - .andReturn(Promise.resolve({ data })); - } - describe('async actions', () => { afterEach(() => { expect.restoreSpies(); @@ -99,16 +88,61 @@ describe('project actions', () => { expect(client.listSimulations).toHaveBeenCalled(); }); + it('should save a project', (done) => { + setSpy(ProjectHelper, 'saveProject', projectsData[0]); + expect(Actions.saveProject(projectsData[0])) + .toDispatchActions({ type: Actions.UPDATE_PROJECT, project: projectsData[0] }, complete(done)); + }); + it('should delete a project', (done) => { setSpy(client, 'deleteProject', projectsData[0]); expect(Actions.deleteProject(projectsData[0])) .toDispatchActions({ type: 'REMOVE_PROJECT', project: projectsData[0] }, complete(done)); }); + }); +}); - it('should save a project', (done) => { - setSpy(ProjectHelper, 'saveProject', projectsData[0]); - expect(Actions.saveProject(projectsData[0])) - .toDispatchActions({ type: Actions.UPDATE_PROJECT, project: projectsData[0] }, complete(done)); +// ---------------------------------------------------------------------------- +// SIMULATIONS +// ---------------------------------------------------------------------------- + +describe('simulation actions', () => { + describe('simple actions', () => { + it('should update Simulation', () => { + const projId = projectsData[0]._id; + const expectedAction = { type: Actions.UPDATE_SIMULATION, simulation: simulationData[0] }; + expect(Actions.updateSimulation(simulationData[0])) + .toDispatchActions(expectedAction); + + expect(projectsReducer(initialState, expectedAction).simulations[projId].list) + .toContain(simulationData[0]._id); + }); + + // setActiveSimulation + it('should set active Simulation', () => { + const simId = simulationData[0]._id; + const expectedAction = { type: Actions.UPDATE_ACTIVE_SIMULATION, id: simId }; + expect(Actions.setActiveSimulation(simId)) + .toDispatchActions(expectedAction); + + // if the new active simulation isn't in the list + const newState = Object.assign({}, initialState); + newState.pendingActiveSimulation = simId; + expect(projectsReducer(initialState, expectedAction)) + .toEqual(newState); + }); + }); + + describe('async actions', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should save simulation', (done) => { + const expectedSim = Object.assign({}, simulationData[0]); + setSpy(SimulationHelper, 'saveSimulation', expectedSim); + expect(Actions.saveSimulation(expectedSim)) + .toDispatchActions({ type: Actions.UPDATE_SIMULATION, simulation: expectedSim }, complete(done)); }); it('should update simulation step', (done) => { From 9a75bddf4345542348e4f17d5fb46cc0bb064c56 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Thu, 9 Jun 2016 13:30:57 -0600 Subject: [PATCH 07/21] added cluster tests --- src/redux/actions/clusters.js | 46 +++++----- test/helpers/complete.js | 8 ++ test/karma.redux.js | 2 +- test/redux/clusters.js | 168 ++++++++++++++++++++++++++++++++++ test/redux/projects.js | 37 +++----- 5 files changed, 210 insertions(+), 51 deletions(-) create mode 100644 test/helpers/complete.js create mode 100644 test/redux/clusters.js diff --git a/src/redux/actions/clusters.js b/src/redux/actions/clusters.js index 3363804d..b265a35f 100644 --- a/src/redux/actions/clusters.js +++ b/src/redux/actions/clusters.js @@ -153,21 +153,23 @@ export function fetchCluster(id) { } export function fetchClusters(type) { - const action = netActions.addNetworkCall('fetch_clusters', 'Retreive clusters'); - dispatch(pendingNetworkCall(true)); - client.listClusters(type) - .then( - resp => { - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(updateClusters(resp.data)); - dispatch(pendingNetworkCall(false)); - }, - err => { - dispatch(netActions.errorNetworkCall(action.id, err)); - dispatch(pendingNetworkCall(false)); - }); + return dispatch => { + const action = netActions.addNetworkCall('fetch_clusters', 'Retreive clusters'); + dispatch(pendingNetworkCall(true)); + client.listClusters(type) + .then( + resp => { + dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(updateClusters(resp.data)); + dispatch(pendingNetworkCall(false)); + }, + err => { + dispatch(netActions.errorNetworkCall(action.id, err)); + dispatch(pendingNetworkCall(false)); + }); - return action; + return action; + }; } export function fetchClusterPresets() { @@ -190,7 +192,7 @@ export function fetchClusterPresets() { }; } -// removes a cluster from the preferences page +// removes a cluster from the preferences page and deletes it if has _id export function removeCluster(index, cluster) { return dispatch => { if (cluster._id) { @@ -276,16 +278,14 @@ export function updateCluster(cluster) { } export function testCluster(index, cluster) { + if (!cluster._id) { + return { type: 'NOOP', message: 'Cluster is not available on server yet' }; + } return dispatch => { - if (!cluster._id) { - return { type: 'NO_OP', message: 'Cluster is not available on server yet' }; - } - const action = netActions.addNetworkCall('test_cluster', 'Test cluster'); dispatch(pendingNetworkCall(true)); - dispatch(action); - - client.testCluster(cluster._id) + dispatch({ type: TESTING_CLUSTER, index }); + return client.testCluster(cluster._id) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -296,8 +296,6 @@ export function testCluster(index, cluster) { dispatch(netActions.errorNetworkCall(action.id, error)); dispatch(pendingNetworkCall(false)); }); - - return { type: TESTING_CLUSTER, index }; }; } diff --git a/test/helpers/complete.js b/test/helpers/complete.js new file mode 100644 index 00000000..ad344a73 --- /dev/null +++ b/test/helpers/complete.js @@ -0,0 +1,8 @@ +export default function (done) { + return (err) => { + if (err) { + done.fail(err); + } + done(); + }; +} diff --git a/test/karma.redux.js b/test/karma.redux.js index 5bb58704..4bd56eda 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -24,7 +24,7 @@ module.exports = function(config) { webpack: wpConfig, files: [ '../node_modules/babel-polyfill/dist/polyfill.js', - 'tests.webpack.js', + 'tests.webpack.js' ], }); }; diff --git a/test/redux/clusters.js b/test/redux/clusters.js new file mode 100644 index 00000000..936e5214 --- /dev/null +++ b/test/redux/clusters.js @@ -0,0 +1,168 @@ +import * as Actions from '../../src/redux/actions/clusters'; +import clustersReducer, { initialState } from '../../src/redux/reducers/clusters'; +import client from '../../src/network'; +import * as ClusterHelpers from '../../src/network/helpers/clusters'; + +// import tradCluster from '../sampleData/projectsData'; +// import ec2Cluster from '../sampleData/simulationsForProj1'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +describe('cluster actions', () => { + describe('simple actions', () => { + it('should add a fresh cluster', (done) => { + const expectedAction = { type: Actions.ADD_CLUSTER }; + expect(Actions.addCluster()) + .toDispatchActions(expectedAction, complete(done)); + }); + + // FIXME, needs sample data + it('should add an existing cluster', (done) => { + const expectedAction = { type: Actions.ADD_EXISTING_CLUSTER, cluster: { a: 1 } }; + expect(Actions.addExistingCluster({ a: 1 })) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should apply cluster preset', (done) => { + const expectedAction = { type: Actions.CLUSTER_APPLY_PRESET, index: 0, name: 'myCluster' }; + expect(Actions.applyPreset(0, 'myCluster')) + .toDispatchActions(expectedAction, complete(done)); + }); + + // FIXME, needs sample data + it('should remove cluster given an id', (done) => { + const expectedAction = { type: Actions.REMOVE_CLUSTER_BY_ID, id: 'a1' }; + expect(Actions.removeClusterById('a1')) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('update the active cluster in a list', (done) => { + const expectedAction = { type: Actions.UPDATE_ACTIVE_CLUSTER, index: 0 }; + expect(Actions.updateActiveCluster(0)) + .toDispatchActions(expectedAction, complete(done)); + }); + + // FIXME, needs sample data + it('update an existing cluster', (done) => { + const expectedAction = { type: Actions.UPDATE_EXISTING_CLUSTER, cluster: { a: 1 } }; + expect(Actions.updateExistingCluster({ a: 1 })) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should update the list of clusters', (done) => { + const clusters = [{ _id: 'a1' }, { _id: 'b2' }]; + const expectedAction = { type: Actions.UPDATE_CLUSTERS, clusters }; + expect(Actions.updateClusters(clusters)) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should update cluster presets', (done) => { + const presets = [{ name: 'myCluster' }, { name: 'myOtherCluster' }]; + const expectedAction = { type: Actions.UPDATE_CLUSTER_PRESETS, presets }; + expect(Actions.updateClusterPresets(presets)) + .toDispatchActions(expectedAction, complete(done)); + }); + + // FIXME, needs sample data + it('should update a cluster\' log', (done) => { + const log = [{ entry: 'created ...' }]; + const expectedAction = { type: Actions.UPDATE_CLUSTER_LOG, id: 'a1', log }; + expect(Actions.updateClusterLog('a1', log)) + .toDispatchActions(expectedAction, complete(done)); + }); + + // we only test the reducer here + it('should update a cluster\'s status', () => { + const thisState = Object.assign({}, initialState); + thisState.mapById = { a1: { status: 'created' } }; + const action = { type: Actions.UPDATE_CLUSTER_STATUS, id: 'a1', status: 'terminated' }; + expect(clustersReducer(thisState, action).mapById.a1.status) + .toEqual('terminated'); + }); + }); + + describe('async actions', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should fetch cluster log', (done) => { + const id = 'a1'; + const log = [{ entry: 'one' }]; + setSpy(client, 'getClusterLog', { log }); + expect(Actions.getClusterLog(id)) + .toDispatchActions({ type: Actions.UPDATE_CLUSTER_LOG, id, log }, complete(done)); + }); + + it('should fetch a cluster', (done) => { + const expectedAction = { type: Actions.ADD_EXISTING_CLUSTER, cluster: { _id: 'myCluster' } }; + setSpy(client, 'getCluster', expectedAction.cluster); + expect(Actions.fetchCluster(expectedAction._id)) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should fetch a list of clusters', (done) => { + const clusters = [{ _id: 'a1' }, { _id: 'b2' }]; + const expectedAction = { type: Actions.UPDATE_CLUSTERS, clusters }; + setSpy(client, 'listClusters', clusters); + expect(Actions.fetchClusters()) + .toDispatchActions(expectedAction, complete(done)); + }); + + // removes a cluster from the preferences list and deletes it if has _id + it('should delete a cluster', (done) => { + const expectedAction = { type: Actions.REMOVE_CLUSTER_BY_ID, id: 'a1' }; + setSpy(client, 'deleteCluster', null); + expect(Actions.deleteCluster('a1')) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should save a cluster', (done) => { + const expectedAction = { type: Actions.SAVE_CLUSTER, index: 0, cluster: { _id: 'a1' } }; + setSpy(ClusterHelpers, 'saveCluster', true); + expect(Actions.saveCluster(0, { _id: 'a1' }, false)) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should update a cluster', (done) => { + const cluster = { _id: 'a1' }; + const expectedAction = { type: Actions.UPDATE_EXISTING_CLUSTER, cluster }; + setSpy(client, 'updateCluster', cluster); + expect(Actions.updateCluster(cluster)) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should not test a cluster without an id', (done) => { + const expectedAction = { type: 'NOOP', message: 'Cluster is not available on server yet' }; + expect(Actions.testCluster(0, {})) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should test a cluster', (done) => { + const expectedAction = { type: Actions.TESTING_CLUSTER, index: 0 }; + setSpy(client, 'testCluster', null); + expect(Actions.testCluster(0, { _id: 'a1' })) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should terminate a cluster', (done) => { + const expectedAction = { type: 'NOOP' }; + setSpy(client, 'terminateCluster', null); + expect(Actions.terminateCluster('a1')) + .toDispatchActions(expectedAction, complete(done)); + }); + }); +}); diff --git a/test/redux/projects.js b/test/redux/projects.js index b20f37c9..3aa1c6eb 100644 --- a/test/redux/projects.js +++ b/test/redux/projects.js @@ -28,11 +28,11 @@ describe('project actions', () => { // ---------------------------------------------------------------------------- describe('basic actions', () => { const newState = Object.assign({}, initialState); - it('should update a project list', () => { + it('should update a project list', (done) => { const expectedAction = { type: Actions.UPDATE_PROJECT_LIST, projects: projectsData }; expect(Actions.updateProjectList(projectsData)) - .toDispatchActions(expectedAction); + .toDispatchActions(expectedAction, complete(done)); newState.list = projectsData.map((el) => el._id); projectsData.forEach((el) => { @@ -42,26 +42,26 @@ describe('project actions', () => { .toEqual(newState); }); - it('should update project simulations', () => { + it('should update project simulations', (done) => { const id = projectsData[0]._id; const expectedAction = { type: Actions.UPDATE_PROJECT_SIMULATIONS, id, simulations: simulationData }; expect(Actions.updateProjectSimulations(id, simulationData)) - .toDispatchActions(expectedAction); + .toDispatchActions(expectedAction, complete(done)); expect(Object.keys(projectsReducer(initialState, expectedAction).simulations[id]).length) .toEqual(simulationData.length); }); - it('should update project data', () => { + it('should update project data', (done) => { const project = projectsData[0]; expect(Actions.updateProject(project)) - .toDispatchActions({ type: Actions.UPDATE_PROJECT, project }); + .toDispatchActions({ type: Actions.UPDATE_PROJECT, project }, complete(done)); }); - it('should update simulation data', () => { + it('should update simulation data', (done) => { const simulation = simulationData[0]; expect(Actions.updateSimulation(simulation)) - .toDispatchActions({ type: Actions.UPDATE_SIMULATION, simulation }); + .toDispatchActions({ type: Actions.UPDATE_SIMULATION, simulation }, complete(done)); }); }); @@ -76,8 +76,7 @@ describe('project actions', () => { it('should get projects', (done) => { setSpy(client, 'listProjects', projectsData); expect(Actions.fetchProjectList()) - .toDispatchActions({ type: Actions.UPDATE_PROJECT_LIST, projects: projectsData }, complete(done)); - expect(client.listProjects).toHaveBeenCalled(); + .toDispatchActions({ type: Actions.UPDATE_PROJECT_LIST }, complete(done)); }); const id = projectsData[0]._id; @@ -108,29 +107,15 @@ describe('project actions', () => { describe('simulation actions', () => { describe('simple actions', () => { - it('should update Simulation', () => { + it('should update Simulation', (done) => { const projId = projectsData[0]._id; const expectedAction = { type: Actions.UPDATE_SIMULATION, simulation: simulationData[0] }; expect(Actions.updateSimulation(simulationData[0])) - .toDispatchActions(expectedAction); + .toDispatchActions(expectedAction, complete(done)); expect(projectsReducer(initialState, expectedAction).simulations[projId].list) .toContain(simulationData[0]._id); }); - - // setActiveSimulation - it('should set active Simulation', () => { - const simId = simulationData[0]._id; - const expectedAction = { type: Actions.UPDATE_ACTIVE_SIMULATION, id: simId }; - expect(Actions.setActiveSimulation(simId)) - .toDispatchActions(expectedAction); - - // if the new active simulation isn't in the list - const newState = Object.assign({}, initialState); - newState.pendingActiveSimulation = simId; - expect(projectsReducer(initialState, expectedAction)) - .toEqual(newState); - }); }); describe('async actions', () => { From 514c61c894bd809fe6b0bd623913489e1fb8886b Mon Sep 17 00:00:00 2001 From: TristanWright Date: Fri, 10 Jun 2016 14:11:32 -0600 Subject: [PATCH 08/21] cluster reducers --- src/redux/actions/clusters.js | 3 +- src/redux/actions/taskflows.js | 1 - src/redux/reducers/clusters.js | 10 +-- test/redux/clusters.js | 124 +++++++++++++++++++++++++-------- 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/src/redux/actions/clusters.js b/src/redux/actions/clusters.js index b265a35f..97434316 100644 --- a/src/redux/actions/clusters.js +++ b/src/redux/actions/clusters.js @@ -15,7 +15,6 @@ export const UPDATE_CLUSTER_PRESETS = 'UPDATE_CLUSTER_PRESETS'; export const UPDATE_CLUSTER_STATUS = 'UPDATE_CLUSTER_STATUS'; export const REMOVE_CLUSTER = 'REMOVE_CLUSTER'; export const SAVE_CLUSTER = 'SAVE_CLUSTER'; -export const TEST_CLUSTER = 'TEST_CLUSTER'; export const PENDING_CLUSTER_NETWORK = 'PENDING_CLUSTER_NETWORK'; export const CLUSTER_APPLY_PRESET = 'CLUSTER_APPLY_PRESET'; export const TESTING_CLUSTER = 'TESTING_CLUSTER'; @@ -38,7 +37,7 @@ export function applyPreset(index, name) { return { type: CLUSTER_APPLY_PRESET, index, name }; } -// removes cluster from mapById, does not affect selected index in preference pane +// removes cluster from mapById // (see removeCluster(index, cluster) belows) export function removeClusterById(id) { return { type: REMOVE_CLUSTER_BY_ID, id }; diff --git a/src/redux/actions/taskflows.js b/src/redux/actions/taskflows.js index 6331f8fb..a3353bad 100644 --- a/src/redux/actions/taskflows.js +++ b/src/redux/actions/taskflows.js @@ -395,4 +395,3 @@ client.onEvent((resp) => { } } }); - diff --git a/src/redux/reducers/clusters.js b/src/redux/reducers/clusters.js index a5ce110a..eb2fa2f0 100644 --- a/src/redux/reducers/clusters.js +++ b/src/redux/reducers/clusters.js @@ -3,14 +3,14 @@ import deepClone from 'mout/src/lang/deepClone'; import set from 'mout/src/object/set'; import style from 'HPCCloudStyle/PageWithMenu.mcss'; -const initialState = { +export const initialState = { list: [], active: 0, pending: false, mapById: {}, }; -const clusterTemplate = { +export const clusterTemplate = { name: 'new cluster', type: 'trad', classPrefix: style.statusCreatingIcon, @@ -102,7 +102,7 @@ export default function clustersReducer(state = initialState, action) { case Actions.REMOVE_CLUSTER_BY_ID: { const id = action.id; - const list = state.list.filter((item) => item.id !== id); + const list = state.list.filter((item) => item._id !== id); const active = (state.active < list.length) ? state.active : (list.length - 1); const mapById = Object.assign({}, state.mapById); delete mapById[id]; @@ -183,10 +183,6 @@ export default function clustersReducer(state = initialState, action) { return Object.assign({}, state, { list, active }); } - case Actions.TEST_CLUSTER: { - return state; - } - case Actions.PENDING_CLUSTER_NETWORK: { return Object.assign({}, state, { pending: action.pending }); } diff --git a/test/redux/clusters.js b/test/redux/clusters.js index 936e5214..c9437f19 100644 --- a/test/redux/clusters.js +++ b/test/redux/clusters.js @@ -1,16 +1,14 @@ import * as Actions from '../../src/redux/actions/clusters'; -import clustersReducer, { initialState } from '../../src/redux/reducers/clusters'; +import clustersReducer, { clusterTemplate, initialState } from '../../src/redux/reducers/clusters'; import client from '../../src/network'; import * as ClusterHelpers from '../../src/network/helpers/clusters'; -// import tradCluster from '../sampleData/projectsData'; -// import ec2Cluster from '../sampleData/simulationsForProj1'; - import expect from 'expect'; import thunk from 'redux-thunk'; import complete from '../helpers/complete'; import { registerMiddlewares } from 'redux-actions-assertions'; import { registerAssertions } from 'redux-actions-assertions/expect'; +import deepClone from 'mout/src/lang/deepClone'; /* global describe it afterEach */ registerMiddlewares([thunk]); @@ -21,52 +19,106 @@ function setSpy(target, method, data) { .andReturn(Promise.resolve({ data })); } +Object.freeze(initialState); + describe('cluster actions', () => { + const cluster = { _id: 'a1', name: 'myCluster', + log: [{ entry: 'created...' }, { entry: 'running...' }], + }; describe('simple actions', () => { - it('should add a fresh cluster', (done) => { + it('should add an empty cluster', (done) => { const expectedAction = { type: Actions.ADD_CLUSTER }; expect(Actions.addCluster()) .toDispatchActions(expectedAction, complete(done)); + + const newState = deepClone(initialState); + newState.list = [deepClone(clusterTemplate)]; + newState.active = 0; + expect(clustersReducer(initialState, expectedAction)) + .toEqual(newState); }); - // FIXME, needs sample data it('should add an existing cluster', (done) => { - const expectedAction = { type: Actions.ADD_EXISTING_CLUSTER, cluster: { a: 1 } }; - expect(Actions.addExistingCluster({ a: 1 })) + const expectedAction = { type: Actions.ADD_EXISTING_CLUSTER, cluster }; + expect(Actions.addExistingCluster(cluster)) .toDispatchActions(expectedAction, complete(done)); + + const newState = deepClone(initialState); + newState.mapById[cluster._id] = cluster; + expect(clustersReducer(initialState, expectedAction)) + .toEqual(newState); }); it('should apply cluster preset', (done) => { const expectedAction = { type: Actions.CLUSTER_APPLY_PRESET, index: 0, name: 'myCluster' }; expect(Actions.applyPreset(0, 'myCluster')) .toDispatchActions(expectedAction, complete(done)); + + // skipping reducer test }); // FIXME, needs sample data it('should remove cluster given an id', (done) => { - const expectedAction = { type: Actions.REMOVE_CLUSTER_BY_ID, id: 'a1' }; - expect(Actions.removeClusterById('a1')) + const expectedAction = { type: Actions.REMOVE_CLUSTER_BY_ID, id: cluster._id }; + expect(Actions.removeClusterById(cluster._id)) .toDispatchActions(expectedAction, complete(done)); + + const givenState = deepClone(initialState); + givenState.list = [cluster]; + givenState.mapById[cluster._id] = cluster; + + const expectedState = deepClone(initialState); + expectedState.active = -1; + + expect(clustersReducer(givenState, expectedAction)) + .toEqual(expectedState); }); it('update the active cluster in a list', (done) => { - const expectedAction = { type: Actions.UPDATE_ACTIVE_CLUSTER, index: 0 }; - expect(Actions.updateActiveCluster(0)) + const index = 12; + const expectedAction = { type: Actions.UPDATE_ACTIVE_CLUSTER, index }; + expect(Actions.updateActiveCluster(index)) .toDispatchActions(expectedAction, complete(done)); + + expect(clustersReducer(initialState, expectedAction).active) + .toEqual(index); }); - // FIXME, needs sample data it('update an existing cluster', (done) => { - const expectedAction = { type: Actions.UPDATE_EXISTING_CLUSTER, cluster: { a: 1 } }; - expect(Actions.updateExistingCluster({ a: 1 })) + const expectedAction = { type: Actions.UPDATE_EXISTING_CLUSTER, cluster }; + expect(Actions.updateExistingCluster(cluster)) .toDispatchActions(expectedAction, complete(done)); + + // update the clusters name + const newCluster = Object.assign({}, cluster, { name: 'some other name' }); + const givenState = deepClone(initialState); + givenState.mapById[cluster._id] = newCluster; + + const expectedState = deepClone(initialState); + expectedState.mapById[cluster._id] = cluster; + + expect(clustersReducer(givenState, expectedAction)) + .toEqual(expectedState); }); it('should update the list of clusters', (done) => { - const clusters = [{ _id: 'a1' }, { _id: 'b2' }]; + const clusters = [ + { _id: 'a1', type: 'trad' }, + { _id: 'b2', type: 'trad' }, + { _id: 'c3', type: 'ec2' }, + ]; const expectedAction = { type: Actions.UPDATE_CLUSTERS, clusters }; expect(Actions.updateClusters(clusters)) .toDispatchActions(expectedAction, complete(done)); + + const expectedState = deepClone(initialState); + expectedState.list = [clusters[0], clusters[1]]; + expectedState.active = 0; + expectedState.mapById.a1 = clusters[0]; + expectedState.mapById.b2 = clusters[1]; + expectedState.mapById.c3 = clusters[2]; + expect(clustersReducer(initialState, expectedAction)) + .toEqual(expectedState); }); it('should update cluster presets', (done) => { @@ -74,14 +126,23 @@ describe('cluster actions', () => { const expectedAction = { type: Actions.UPDATE_CLUSTER_PRESETS, presets }; expect(Actions.updateClusterPresets(presets)) .toDispatchActions(expectedAction, complete(done)); + + // skipping reducer test }); - // FIXME, needs sample data - it('should update a cluster\' log', (done) => { - const log = [{ entry: 'created ...' }]; - const expectedAction = { type: Actions.UPDATE_CLUSTER_LOG, id: 'a1', log }; - expect(Actions.updateClusterLog('a1', log)) + it('should update a cluster\'s log', (done) => { + const log = [{ entry: 'job submitted ...' }]; + const expectedAction = { type: Actions.UPDATE_CLUSTER_LOG, id: cluster._id, log }; + expect(Actions.updateClusterLog(cluster._id, log)) .toDispatchActions(expectedAction, complete(done)); + + const givenState = deepClone(initialState); + givenState.mapById[cluster._id] = cluster; + const expectedState = deepClone(initialState); + expectedState.mapById[cluster._id] = deepClone(cluster); + expectedState.mapById[cluster._id].log.push(log[0]); + expect(clustersReducer(givenState, expectedAction)) + .toEqual(expectedState); }); // we only test the reducer here @@ -94,13 +155,17 @@ describe('cluster actions', () => { }); }); +// ---------------------------------------------------------------------------- +// AYSYNCHRONUS ACTIONS +// ---------------------------------------------------------------------------- + describe('async actions', () => { afterEach(() => { expect.restoreSpies(); }); it('should fetch cluster log', (done) => { - const id = 'a1'; + const id = cluster._id; const log = [{ entry: 'one' }]; setSpy(client, 'getClusterLog', { log }); expect(Actions.getClusterLog(id)) @@ -108,7 +173,7 @@ describe('cluster actions', () => { }); it('should fetch a cluster', (done) => { - const expectedAction = { type: Actions.ADD_EXISTING_CLUSTER, cluster: { _id: 'myCluster' } }; + const expectedAction = { type: Actions.ADD_EXISTING_CLUSTER, cluster }; setSpy(client, 'getCluster', expectedAction.cluster); expect(Actions.fetchCluster(expectedAction._id)) .toDispatchActions(expectedAction, complete(done)); @@ -124,21 +189,20 @@ describe('cluster actions', () => { // removes a cluster from the preferences list and deletes it if has _id it('should delete a cluster', (done) => { - const expectedAction = { type: Actions.REMOVE_CLUSTER_BY_ID, id: 'a1' }; + const expectedAction = { type: Actions.REMOVE_CLUSTER_BY_ID, id: cluster._id }; setSpy(client, 'deleteCluster', null); - expect(Actions.deleteCluster('a1')) + expect(Actions.deleteCluster(cluster._id)) .toDispatchActions(expectedAction, complete(done)); }); it('should save a cluster', (done) => { - const expectedAction = { type: Actions.SAVE_CLUSTER, index: 0, cluster: { _id: 'a1' } }; + const expectedAction = { type: Actions.SAVE_CLUSTER, index: 0, cluster }; setSpy(ClusterHelpers, 'saveCluster', true); - expect(Actions.saveCluster(0, { _id: 'a1' }, false)) + expect(Actions.saveCluster(0, cluster, false)) .toDispatchActions(expectedAction, complete(done)); }); it('should update a cluster', (done) => { - const cluster = { _id: 'a1' }; const expectedAction = { type: Actions.UPDATE_EXISTING_CLUSTER, cluster }; setSpy(client, 'updateCluster', cluster); expect(Actions.updateCluster(cluster)) @@ -154,14 +218,14 @@ describe('cluster actions', () => { it('should test a cluster', (done) => { const expectedAction = { type: Actions.TESTING_CLUSTER, index: 0 }; setSpy(client, 'testCluster', null); - expect(Actions.testCluster(0, { _id: 'a1' })) + expect(Actions.testCluster(0, cluster)) .toDispatchActions(expectedAction, complete(done)); }); it('should terminate a cluster', (done) => { const expectedAction = { type: 'NOOP' }; setSpy(client, 'terminateCluster', null); - expect(Actions.terminateCluster('a1')) + expect(Actions.terminateCluster(cluster._id)) .toDispatchActions(expectedAction, complete(done)); }); }); From 1fe0047a0fc70717737033e4b89f9cd577dcb6aa Mon Sep 17 00:00:00 2001 From: TristanWright Date: Fri, 10 Jun 2016 16:59:20 -0600 Subject: [PATCH 09/21] =?UTF-8?q?taskflows=20tests=20=E2=9C=93=20simple=20?= =?UTF-8?q?actions=20=E2=9C=93=20async=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/redux/actions/taskflows.js | 18 +- src/redux/reducers/taskflows.js | 10 +- test/karma.redux.js | 2 +- test/redux/taskflows.js | 244 +++++++++++++++++++++ test/sampleData/basicTaskflowState.js | 299 ++++++++++++++++++++++++++ test/webpack.redux.js | 4 +- 6 files changed, 558 insertions(+), 19 deletions(-) create mode 100644 test/redux/taskflows.js create mode 100644 test/sampleData/basicTaskflowState.js diff --git a/src/redux/actions/taskflows.js b/src/redux/actions/taskflows.js index a3353bad..8ee91c15 100644 --- a/src/redux/actions/taskflows.js +++ b/src/redux/actions/taskflows.js @@ -63,10 +63,10 @@ export function updateTaskflowJobLog(taskflowId, jobId) { // ---------------------------------------------------------------------------- // STATUSES export function updateTaskflowJobStatus(taskflowId, jobId, status) { + if (status) { + return { type: UPDATE_TASKFLOW_JOB_STATUS, taskflowId, jobId, status }; + } return dispatch => { - if (status) { - return { type: UPDATE_TASKFLOW_JOB_STATUS, taskflowId, jobId, status }; - } const action = netActions.addNetworkCall(`taskflow_job_status_${jobId}`, 'Check job status'); client.getJobStatus(jobId) @@ -117,13 +117,9 @@ export function startTaskflow(id, payload, simulationStep, location) { dispatch(netActions.successNetworkCall(action.id, resp)); if (simulationStep) { - const data = Object.assign( - {}, - simulationStep.data, - { metadata: Object.assign( - {}, - simulationStep.data.metadata, - { taskflowId: id }) }); + const data = Object.assign({}, simulationStep.data, + { metadata: Object.assign({}, simulationStep.data.metadata, { taskflowId: id }), + }); dispatch(projActions.updateSimulationStep(simulationStep.id, simulationStep.step, data, location)); } }, @@ -195,7 +191,7 @@ export function fetchTaskflow(id) { }); } if (taskflow.meta.cluster) { - clusterActions.fetchClusters(); + dispatch(clusterActions.fetchClusters()); } } }, diff --git a/src/redux/reducers/taskflows.js b/src/redux/reducers/taskflows.js index e3c253d8..5e31e28b 100644 --- a/src/redux/reducers/taskflows.js +++ b/src/redux/reducers/taskflows.js @@ -1,13 +1,13 @@ import * as Actions from '../actions/taskflows'; -const initialState = { +export const initialState = { mapById: {}, taskflowMapByTaskId: {}, taskflowMapByJobId: {}, updateLogs: [], }; -const initialTaskflow = { +export const taskflowTemplate = { taskMapById: {}, log: [], actions: [], @@ -28,14 +28,14 @@ export default function taskflowsReducer(state = initialState, action) { } const taskflow = Object.assign( {}, - initialTaskflow, + taskflowTemplate, state.mapById[action.taskflow._id], { flow: action.taskflow, jobMapById, primaryJob: action.primaryJob, - - }); + } + ); const mapById = Object.assign( {}, state.mapById, diff --git a/test/karma.redux.js b/test/karma.redux.js index 4bd56eda..d7d28060 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -14,9 +14,9 @@ module.exports = function(config) { reporters: ['spec'], plugins: [ 'karma-jasmine', + 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-webpack', - 'karma-phantomjs-launcher' ], preprocessors: { 'tests.webpack.js': ['webpack'], diff --git a/test/redux/taskflows.js b/test/redux/taskflows.js new file mode 100644 index 00000000..ce102f96 --- /dev/null +++ b/test/redux/taskflows.js @@ -0,0 +1,244 @@ +import * as Actions from '../../src/redux/actions/taskflows'; +import taskflowsReducer, { initialState } from '../../src/redux/reducers/taskflows'; +import client from '../../src/network'; + +import taskflowState from '../sampleData/basicTaskflowState'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +import deepClone from 'mout/src/lang/deepClone'; +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +Object.freeze(initialState); + +describe('taskflow actions', () => { + const taskflowId = '574c9d900640fd6e133b4b57'; + const taskflow = deepClone(taskflowState.mapById[taskflowId]); + const task = Object.assign({}, taskflow.taskMapById['574c9f350640fd6e13b11e39']); + describe('simple actions', () => { + it('should clear update logs', (done) => { + const expectedAction = { type: Actions.CLEAR_UPDATE_LOG }; + expect(Actions.clearUpdateLog()) + .toDispatchActions(expectedAction, complete(done)); + + const givenState = deepClone(initialState); + givenState.updateLogs = ['a1', 'b2']; + expect(taskflowsReducer(givenState, expectedAction)) + .toEqual(initialState); + }); + + it('should update a task\'s status for in a taskflow', (done) => { + // console.log(task, taskflow.taskMapById['574c9f350640fd6e13b11e39']); + const newStatus = 'running'; + const expectedAction = { + type: Actions.UPDATE_TASKFLOW_TASK_STATUS, + taskflowId, + status: newStatus, + taskId: task._id, + }; + expect(Actions.updateTaskflowTaskStatus(taskflowId, task._id, newStatus)) + .toDispatchActions(expectedAction, complete(done)); + + const expectedTask = Object.assign({}, task); + expectedTask.status = newStatus; + expect(taskflowsReducer(taskflowState, expectedAction).mapById[taskflowId].taskMapById[task._id]) + .toEqual(expectedTask); + }); + + it('should add a taskflow', (done) => { + const expectedAction = { type: Actions.ADD_TASKFLOW, taskflow: taskflow.flow, primaryJob: 'pyfr_run' }; + expect(Actions.addTaskflow(taskflow.flow, 'pyfr_run')) + .toDispatchActions(expectedAction, complete(done)); + + const newState = taskflowsReducer(deepClone(initialState), expectedAction); + const expectedTaskflow = deepClone(taskflowState); + // we don't have these properties from a taskflow that's just been added + expectedTaskflow.mapById[taskflowId].taskMapById = {}; + expectedTaskflow.mapById[taskflowId].log = []; + expectedTaskflow.taskflowMapByTaskId = {}; + expectedTaskflow.taskflowMapByJobId = {}; + delete expectedTaskflow.mapById[taskflowId].allComplete; + delete expectedTaskflow.mapById[taskflowId].stepName; + delete expectedTaskflow.mapById[taskflowId].simulation; + expect(newState) + .toEqual(expectedTaskflow); + }); + + it('should attach a simulation to a taskflow', (done) => { + const expectedAction = { type: Actions.BIND_SIMULATION_TO_TASKFLOW, + taskflowId, simulationId: 'a1', stepName: 'visuzlization' }; + expect(Actions.attachSimulationToTaskflow('a1', taskflowId, 'visuzlization')) + .toDispatchActions(expectedAction, complete(done)); + + const newState = deepClone(taskflowState); + newState.mapById[taskflowId].simulation = expectedAction.simulationId; + newState.mapById[taskflowId].stepName = expectedAction.stepName; + expect(taskflowsReducer(taskflowState, expectedAction)) + .toEqual(newState); + }); + + it('should update a taskflow\'s status', (done) => { + const newStatus = 'running'; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_STATUS, id: taskflowId, status: newStatus }; + expect(Actions.updateTaskflowStatus(taskflowId, newStatus)) + .toDispatchActions(expectedAction, complete(done)); + + expect(taskflowsReducer(taskflowState, expectedAction).mapById[taskflowId].flow.status) + .toEqual(newStatus); + }); + + it('should update taskflow properties', (done) => { + const newMeta = { + actions: ['stop', 'terminate'], + allComplete: false, + outputDirectory: '/my/dir/wow', + primaryJob: 'some_new_primary_job', + }; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_METADATA, id: taskflowId, metadata: newMeta }; + expect(Actions.updateTaskflowMetadata(taskflowId, newMeta)) + .toDispatchActions(expectedAction, complete(done)); + + const newState = deepClone(taskflowState); + newState.mapById[taskflowId].allComplete = newMeta.allComplete; + newState.mapById[taskflowId].actions = newMeta.actions; + newState.mapById[taskflowId].outputDirectory = newMeta.outputDirectory; + newState.mapById[taskflowId].primaryJob = newMeta.primaryJob; + + expect(taskflowsReducer(taskflowState, expectedAction)) + .toEqual(newState); + }); + }); + +// ---------------------------------------------------------------------------- +// AYSYNCHRONUS ACTIONS +// ---------------------------------------------------------------------------- + + describe('async actions', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should update taskflow log', (done) => { + const log = [{ entry: 'created...' }, { entry: 'running...' }]; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_LOG, taskflowId, log }; + setSpy(client, 'getTaskflowLog', { log }); + expect(Actions.updateTaskflowLog(taskflowId)) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should update taskflow job log', (done) => { + const log = [{ entry: 'created...' }, { entry: 'running...' }]; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_JOB_LOG, taskflowId, + jobId: 'a1', log }; + setSpy(client, 'getJobLog', { log }); + expect(Actions.updateTaskflowJobLog(taskflowId, 'a1')) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should update taskflow job status', (done) => { + const newStatus = 'running'; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_JOB_STATUS, taskflowId, + jobId: 'a1', status: newStatus }; + expect(Actions.updateTaskflowJobStatus(taskflowId, 'a1', newStatus)) + .toDispatchActions(expectedAction, complete(done)); + + // can be called without status parameter, calls async if it is + setSpy(client, 'getJobStatus', { status: newStatus }); + expect(Actions.updateTaskflowJobStatus(taskflowId, 'a1')) + .toDispatchActions(expectedAction, complete(done)); + + expect(client.getJobStatus) + .toHaveBeenCalled(); + }); + + it('should start taskflow', (done) => { + setSpy(client, 'startTaskflow', ''); + // calls projects @updateSimulationStep which we're testing elsewhere + // let's just make sure it's calling startTaskflow + expect(Actions.startTaskflow(taskflowId, {}, 'Visuzlization')) + .toDispatchActions([], complete(done)); + + expect(client.startTaskflow) + .toHaveBeenCalled(); + }); + + it('shuold create a taskflow', (done) => { + const expectedActions = [ + { type: Actions.ADD_TASKFLOW, primaryJob: 'pyfr' }, + { type: Actions.BIND_SIMULATION_TO_TASKFLOW, taskflowId, simulationId: 'mySimStep', stepName: 'Visuzlization' }, + // also starts taskflow + ]; + setSpy(client, 'createTaskflow', taskflow.flow); + expect(Actions.createTaskflow('myFlow', 'pyfr', {}, { id: 'mySimStep', step: 'Visuzlization' })) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should fetch taskflow tasks', (done) => { + const tasks = [{ name: 'task1' }, { name: 'task2' }]; + const expectedAction = { type: Actions.UPDATE_TASKFLOW_TASKS, taskflowId, tasks }; + setSpy(client, 'getTaskflowTasks', tasks); + expect(Actions.fetchTaskflowTasks(taskflowId)) + .toDispatchActions(expectedAction, complete(done)); + }); + + // big test, this dispatches a lot of actions + it('should fetch a taskflow', (done) => { + const clusters = [{ _id: 'a1' }, { _id: 'b2' }]; + const log = [{ entry: 'created...' }, { entry: 'running...' }]; + const flow = deepClone(taskflow.flow); + flow.meta.jobs = [{ _id: 'job1', status: 'running' }]; + const expectedActions = [ + { type: Actions.ADD_TASKFLOW, taskflow: flow }, + { type: Actions.UPDATE_TASKFLOW_JOB_STATUS, taskflowId, jobId: 'job1', status: 'running' }, + { type: Actions.UPDATE_TASKFLOW_JOB_LOG, taskflowId, jobId: 'job1', log }, + { type: 'UPDATE_CLUSTERS', clusters }, + ]; + setSpy(client, 'getTaskflow', flow); + setSpy(client, 'getJobLog', { log }); + setSpy(client, 'getJobStatus', { status: 'running' }); + setSpy(client, 'listClusters', clusters); + expect(Actions.fetchTaskflow(taskflowId)) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should update a taskflow from a simulation', (done) => { + const simulation = { + _id: 'mySimulationId', + steps: { + Visuzlization: { + metadata: { taskflowId }, + }, + }, + }; + const flow = deepClone(taskflow.flow); + const expectedActions = [ + { type: Actions.ADD_TASKFLOW, taskflow: flow }, + { type: Actions.BIND_SIMULATION_TO_TASKFLOW, taskflowId, simulationId: 'mySimulationId', stepName: 'Visuzlization' }, + ]; + setSpy(client, 'getTaskflow', flow); + expect(Actions.updateTaskflowFromSimulation(simulation)) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should delete a taskflow', (done) => { + const expectedAction = { type: Actions.DELETE_TASKFLOW, id: taskflowId }; + setSpy(client, 'deleteTaskflow', null); + expect(Actions.deleteTaskflow(taskflowId)) + .toDispatchActions(expectedAction, complete(done)); + + expect(client.deleteTaskflow) + .toHaveBeenCalled(); + }); + }); +}); diff --git a/test/sampleData/basicTaskflowState.js b/test/sampleData/basicTaskflowState.js new file mode 100644 index 00000000..6f2e1c31 --- /dev/null +++ b/test/sampleData/basicTaskflowState.js @@ -0,0 +1,299 @@ +/* eslint-disable */ +export default { + "mapById": { + "574c9d900640fd6e133b4b57": { + "taskMapById": { + "574c9f350640fd6e13b11e39": { + "_accessLevel": 2, + "_id": "574c9f350640fd6e13b11e39", + "_modelType": "tasks", + "created": "2016-05-30T20:14:45.187000+00:00", + "log": [], + "name": "hpccloud.taskflow.pyfr.setup_input", + "status": "complete", + "taskFlowId": "574c9d900640fd6e133b4b57" + }, + "574c9e460640fd6e13b11e32": { + "_accessLevel": 2, + "_id": "574c9e460640fd6e13b11e32", + "_modelType": "tasks", + "created": "2016-05-30T20:10:46.658000+00:00", + "log": [], + "name": "hpccloud.taskflow.pyfr.pyfr_terminate", + "status": "complete", + "taskFlowId": "574c9d900640fd6e133b4b57" + }, + "574c9d910640fd6e133b4b59": { + "_accessLevel": 2, + "_id": "574c9d910640fd6e133b4b59", + "_modelType": "tasks", + "created": "2016-05-30T20:07:45.398000+00:00", + "log": [ + { + "args": [], + "created": 1464638865.5593, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 143, + "module": "__init__", + "msecs": 559.30709838867, + "msg": "Cluster name my new node", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7804605.1511765, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.0911, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 65, + "module": "__init__", + "msecs": 91.130971908569, + "msg": "Using source ip: 123.201.24.01", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805136.97505, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.2976, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 107, + "module": "__init__", + "msecs": 297.59693145752, + "msg": "Created cluster: 574c9d920640fd6e133b4b60", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805343.4410095, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.3557, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 112, + "module": "__init__", + "msecs": 355.6981086731, + "msg": "Starting cluster.", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805401.5421867, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464639285.1466, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 148, + "module": "__init__", + "msecs": 146.5539932251, + "msg": "Cluster started.", + "name": "task.574c9d910640fd6e133b4b59", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 8224192.3980713, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + } + ], + "name": "hpccloud.taskflow.utility.setup_cluster", + "status": "complete", + "taskFlowId": "574c9d900640fd6e133b4b57" + } + }, + "log": [ + { + "args": [], + "created": 1464638865.5391, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 142, + "module": "__init__", + "msecs": 539.0989780426, + "msg": "We are creating an EC2 cluster.", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7804584.9430561, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.2742, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 106, + "module": "__init__", + "msecs": 274.18994903564, + "msg": "Created cluster: 574c9d920640fd6e133b4b60", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805320.0340271, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + } + ], + "actions": [], + "simulation": "574c8aa00640fd3f1a3b379f", + "stepName": "Simulation", + "flow": { + "_accessLevel": 2, + "_id": "574c9d900640fd6e133b4b57", + "_modelType": "taskflows", + "activeTaskCount": 0, + "log": [ + { + "args": [], + "created": 1464638865.5391, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "setup_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 142, + "module": "__init__", + "msecs": 539.0989780426, + "msg": "We are creating an EC2 cluster.", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7804584.9430561, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + }, + { + "args": [], + "created": 1464638866.2742, + "exc_info": null, + "exc_text": null, + "filename": "__init__.py", + "funcName": "create_ec2_cluster", + "levelname": "INFO", + "levelno": 20, + "lineno": 106, + "module": "__init__", + "msecs": 274.18994903564, + "msg": "Created cluster: 574c9d920640fd6e133b4b60", + "name": "taskflow.574c9d900640fd6e133b4b57", + "pathname": "\/opt\/hpccloud\/hpccloud\/server\/taskflows\/hpccloud\/taskflow\/utility\/__init__.py", + "process": 17447, + "processName": "Worker-1", + "relativeCreated": 7805320.0340271, + "thread": 1.4053705497581e+14, + "threadName": "MainThread" + } + ], + "meta": { + "cluster": { + "_id": "574c9d920640fd6e133b4b60", + "config": { + "launch": { + "params": { + "extra_rules": [ + { + "cidr_ip": "123.201.24.01", + "from_port": 9000, + "proto": "tcp", + "to_port": 9000 + } + ], + "gpu": 0, + "master_instance_ami": "some-ami", + "master_instance_type": "t2.nano", + "node_instance_ami": "another-ami", + "node_instance_count": 1, + "node_instance_type": "t2.nano", + "source_cidr_ip": "123.201.24.01" + }, + "spec": "ec2" + }, + "scheduler": { + "type": "sge" + }, + "ssh": { + "key": "mysuperstrongsshkeyw0w", + "user": "someuser" + } + }, + "name": "my new node", + "profileId": "574c8a770640fd3f1a3b377a", + "status": "created", + "type": "ec2", + "userId": "574c841b0640fd3f1a3b3741" + } + }, + "status": "terminated", + "taskFlowClass": "hpccloud.taskflow.pyfr.PyFrTaskFlow" + }, + "jobMapById": { + + }, + "primaryJob": "pyfr_run", + "allComplete": true + } + }, + "taskflowMapByTaskId": { + "574c9f350640fd6e13b11e39": "574c9d900640fd6e133b4b57", + "574c9e460640fd6e13b11e32": "574c9d900640fd6e133b4b57", + "574c9d910640fd6e133b4b59": "574c9d900640fd6e133b4b57" + }, + "taskflowMapByJobId": { + + }, + "updateLogs": [ + + ] +} diff --git a/test/webpack.redux.js b/test/webpack.redux.js index b6bea835..3a344a78 100644 --- a/test/webpack.redux.js +++ b/test/webpack.redux.js @@ -15,10 +15,10 @@ var definePlugin = new webpack.DefinePlugin({ module.exports = { entry: { - 'tests.webpack.js': './test/tests.webpack.js' + 'tests.webpack.js': './test/tests.webpack.js', }, plugins: [ - definePlugin + definePlugin, ], module: { loaders: [ From e1d818c6cff0c160a8824e143f2aaa8e9b47a1d7 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Tue, 14 Jun 2016 14:09:53 -0600 Subject: [PATCH 10/21] project actions fixes, fs tests --- src/network/index.js | 3 +- src/panels/FileListing/index.js | 4 +- src/redux/actions/fs.js | 2 +- src/redux/actions/projects.js | 30 ++++++++----- src/redux/reducers/fs.js | 4 +- test/redux/fs.js | 80 +++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 test/redux/fs.js diff --git a/src/network/index.js b/src/network/index.js index 4d916d27..1445a4e0 100644 --- a/src/network/index.js +++ b/src/network/index.js @@ -47,7 +47,6 @@ if (process.env.NODE_ENV === 'test') { url = window.location; } -const girderClient = ClientBuilder.build( - url, endpoints); +const girderClient = ClientBuilder.build(url, endpoints); export default girderClient; diff --git a/src/panels/FileListing/index.js b/src/panels/FileListing/index.js index ad7946cf..c050523f 100644 --- a/src/panels/FileListing/index.js +++ b/src/panels/FileListing/index.js @@ -162,7 +162,7 @@ const FileListing = React.createClass({ } this.setState({ opened }); - this.props.toggleOpenFolder(id, !this.props.folders[id].open, opened); + this.props.toggleOpenFolder(id, !this.props.folders[id].open); }, render() { @@ -209,6 +209,6 @@ export default connect( }; }, () => ({ - toggleOpenFolder: (folderId, opening, opened) => dispatch(Actions.toggleOpenFolder(folderId, opening, opened)), + toggleOpenFolder: (folderId, opening) => dispatch(Actions.toggleOpenFolder(folderId, opening)), }) )(FileListing); diff --git a/src/redux/actions/fs.js b/src/redux/actions/fs.js index d856707e..45a935e6 100644 --- a/src/redux/actions/fs.js +++ b/src/redux/actions/fs.js @@ -67,7 +67,7 @@ export function fetchFolder(id, fetchFolderMeta = true, openedFolders = []) { }; } -export function toggleOpenFolder(folderId, opening, opened) { +export function toggleOpenFolder(folderId, opening) { return dispatch => { if (opening) { dispatch(fetchFolder(folderId)); diff --git a/src/redux/actions/projects.js b/src/redux/actions/projects.js index 43d8e59f..83545360 100644 --- a/src/redux/actions/projects.js +++ b/src/redux/actions/projects.js @@ -32,7 +32,7 @@ export function fetchProjectSimulations(id) { return dispatch => { const action = netActions.addNetworkCall(`fetch_project_simulations_${id}`, 'Retreive project simulations'); - return client.listSimulations(id) + client.listSimulations(id) .then((resp) => { const simulations = resp.data; dispatch(netActions.successNetworkCall(action.id, resp)); @@ -40,8 +40,9 @@ export function fetchProjectSimulations(id) { }) .catch((error) => { dispatch(netActions.errorNetworkCall(action.id, error)); - throw new Error('sim fetch fails'); }); + + return action; }; } @@ -49,7 +50,7 @@ export function fetchProjectList() { return dispatch => { const action = netActions.addNetworkCall('fetch_project_list', 'Retreive projects'); - return client.listProjects() + client.listProjects() .then((resp) => { dispatch(netActions.successNetworkCall(action.id, resp)); dispatch(updateProjectList(resp.data)); @@ -59,10 +60,9 @@ export function fetchProjectList() { }) .catch((error) => { dispatch(netActions.errorNetworkCall(action.id, error)); - throw new Error('proj fetch fails'); }); - // return action; + return action; }; } @@ -70,7 +70,7 @@ export function deleteProject(project) { return dispatch => { const action = netActions.addNetworkCall(`delete_project_${project._id}`, `Delete project ${project.name}`); - return client.deleteProject(project._id) + client.deleteProject(project._id) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -79,8 +79,9 @@ export function deleteProject(project) { }, error => { dispatch(netActions.errorNetworkCall(action.id, error)); - throw new Error('project delete fails'); }); + + return action; }; } @@ -108,7 +109,7 @@ export function saveProject(project, attachments) { dispatch(netActions.prepareUpload(attachments)); } - return ProjectHelper.saveProject(project, attachments) + ProjectHelper.saveProject(project, attachments) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -123,6 +124,8 @@ export function saveProject(project, attachments) { error => { dispatch(netActions.errorNetworkCall(action.id, error)); }); + + return action; }; } @@ -142,7 +145,7 @@ export function saveSimulation(simulation, attachments, location) { dispatch(netActions.prepareUpload(attachments)); } - return SimulationHelper.saveSimulation(simulation, attachments) + SimulationHelper.saveSimulation(simulation, attachments) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -159,6 +162,7 @@ export function saveSimulation(simulation, attachments, location) { error => { dispatch(netActions.errorNetworkCall(action.id, error)); }); + return action; }; } @@ -166,7 +170,7 @@ export function deleteSimulation(simulation, location) { return dispatch => { const action = netActions.addNetworkCall(`delete_simulation_${simulation._id}`, `Delete simulation ${simulation.name}`); - return client.deleteSimulation(simulation._id) + client.deleteSimulation(simulation._id) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); @@ -180,7 +184,7 @@ export function deleteSimulation(simulation, location) { // throw new Error('project delete fails'); }); - // return action; + return action; }; } @@ -200,7 +204,7 @@ export function updateSimulationStep(id, stepName, data, location) { return dispatch => { const action = netActions.addNetworkCall(`update_simulation_step_${id}`, 'Update simulation step'); - return client.updateSimulationStep(id, stepName, data) + client.updateSimulationStep(id, stepName, data) .then((resp) => { // dispatch(netActions.successNetworkCall(action.id, resp)); dispatch(updateSimulation(resp.data)); @@ -211,6 +215,8 @@ export function updateSimulationStep(id, stepName, data, location) { .catch((error) => { dispatch(netActions.errorNetworkCall(action.id, error)); }); + + return action; }; } diff --git a/src/redux/reducers/fs.js b/src/redux/reducers/fs.js index fc437c0b..e5e8f3fb 100644 --- a/src/redux/reducers/fs.js +++ b/src/redux/reducers/fs.js @@ -1,11 +1,11 @@ import * as Actions from '../actions/fs'; -const initialState = { +export const initialState = { folderMapById: {}, itemMapById: {}, }; -const folderInitialState = { +export const folderInitialState = { open: false, folder: null, folderChildren: [], diff --git a/test/redux/fs.js b/test/redux/fs.js new file mode 100644 index 00000000..6cefda79 --- /dev/null +++ b/test/redux/fs.js @@ -0,0 +1,80 @@ +import * as Actions from '../../src/redux/actions/fs'; +import fsReducer, { folderInitialState, initialState } from '../../src/redux/reducers/fs'; +import client from '../../src/network'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +import deepClone from 'mout/src/lang/deepClone'; +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +Object.freeze(initialState); + +describe('fs', () => { + const folder = { _id: 'a1b2' }; + const folderState = Object.assign({}, folderInitialState, { folder }); + const fsState = deepClone(initialState); + fsState.folderMapById = { [folder._id]: folderState }; + Object.freeze(fsState); + + describe('simple actions', () => { + + it('should update folder', (done) => { + const expectedAction = { type: Actions.UPDATE_FOLDER, folder, id: folder._id }; + expect(Actions.updateFolder(folder)) + .toDispatchActions(expectedAction, complete(done)); + + expect(fsReducer(initialState, expectedAction)) + .toEqual(fsState); + }); + + // just reducer + it('should open a folder', () => { + const action = { type: Actions.TOGGLE_OPEN_FOLDER, folderId: folder._id }; + + // opens + const newState = deepClone(fsState); + newState.folderMapById[folder._id].open = !fsState.folderMapById[folder._id].open; + expect(fsReducer(fsState, action)) + .toEqual(newState); + + // closes + expect(fsReducer(newState, action)) + .toEqual(fsState); + }); + }); + + describe('async action', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + // this stalls phantomJS? + // it('should fetch folders and child items', (done) => { + // const childFolders = [ { _id: 'c3d4' }, { _id: 'e5f6' } ]; + // const items = [ { _id:'item1' }, { _id: 'item2' }, { _id: 'item3' } ]; + // const expectedActions = [ + // { type: Actions.CHILDREN_FOLDERS, children: childFolders, id: folder._id }, + // { type: Actions.CHILDREN_ITEMS, children: items, id: folder._id }, + // { type: Actions.UPDATE_FOLDER, folder, id: folder._id } + // ]; + + // setSpy(client, 'listFolders', childFolders); + // setSpy(client, 'listItems', items); + // // setSpy(client, 'getFolder', null); + + // expect(Actions.fetchFolder(folder._id, false)) + // .toDispatchActions(expectedActions, complete(done)); + // }); + }); +}); From 13de5b6435f614716068234c37329639d7424e81 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 15 Jun 2016 09:45:02 -0600 Subject: [PATCH 11/21] aws redux tests --- src/redux/actions/aws.js | 97 ++++++++++++++++++++------------------- src/redux/reducers/aws.js | 12 ++--- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/redux/actions/aws.js b/src/redux/actions/aws.js index d8fec13b..a1c28adf 100644 --- a/src/redux/actions/aws.js +++ b/src/redux/actions/aws.js @@ -28,65 +28,68 @@ export function updateAWSProfiles(profiles) { } export function fetchAWSProfiles() { - const action = netActions.addNetworkCall('fetch_aws_profiles', 'Retreive AWS profiles'); - dispatch(pendingNetworkCall(true)); - client.listAWSProfiles() - .then((resp) => { - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(updateAWSProfiles(resp.data)); - dispatch(pendingNetworkCall(false)); - }) - .catch((error) => { - dispatch(netActions.errorNetworkCall(action.id, error)); - dispatch(pendingNetworkCall(false)); - }); + return dispatch => { + const action = netActions.addNetworkCall('fetch_aws_profiles', 'Retreive AWS profiles'); + dispatch(pendingNetworkCall(true)); + client.listAWSProfiles() + .then((resp) => { + dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(updateAWSProfiles(resp.data)); + dispatch(pendingNetworkCall(false)); + }) + .catch((error) => { + dispatch(netActions.errorNetworkCall(action.id, error)); + dispatch(pendingNetworkCall(false)); + }); - return action; + return action; + }; } export function removeAWSProfile(index, profile) { - return dispatch => { - if (profile._id) { - const action = netActions.addNetworkCall('remove_aws_profile', 'Remove cluster'); + if (!profile._id) { + return { type: REMOVE_AWS_PROFILE, index }; + } - dispatch(pendingNetworkCall(true)); - client.deleteAWSProfile(profile._id) - .then( - resp => { - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(pendingNetworkCall(false)); - dispatch(fetchAWSProfiles()); - }, - err => { - dispatch(netActions.errorNetworkCall(action.id, err)); - dispatch(pendingNetworkCall(false)); - }); + return dispatch => { + const action = netActions.addNetworkCall('remove_aws_profile', 'Remove cluster'); - return action; - } + dispatch(pendingNetworkCall(true)); + client.deleteAWSProfile(profile._id) + .then( + resp => { + dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(pendingNetworkCall(false)); + dispatch(fetchAWSProfiles()); + }, + err => { + dispatch(netActions.errorNetworkCall(action.id, err)); + dispatch(pendingNetworkCall(false)); + }); - return { type: REMOVE_AWS_PROFILE, index }; + return action; }; } export function updateAWSProfile(index, profile, pushToServer = false) { - return dispatch => { - if (pushToServer) { - const action = netActions.addNetworkCall('save_aws_profile', 'Save cluster'); - dispatch(pendingNetworkCall(true)); - client.createAWSProfile(profile) - .then( - resp => { - dispatch(pendingNetworkCall(false)); - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(fetchAWSProfiles()); - }, - err => { - dispatch(netActions.errorNetworkCall(action.id, err)); - dispatch(pendingNetworkCall(false)); - }); - } + if (!pushToServer) { return { type: SAVE_AWS_PROFILE, index, profile }; + } + return dispatch => { + const action = netActions.addNetworkCall('save_aws_profile', 'Save cluster'); + dispatch(pendingNetworkCall(true)); + client.createAWSProfile(profile) + .then( + resp => { + dispatch(pendingNetworkCall(false)); + dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(fetchAWSProfiles()); + }, + err => { + dispatch(netActions.errorNetworkCall(action.id, err)); + dispatch(pendingNetworkCall(false)); + }); + return action; }; } diff --git a/src/redux/reducers/aws.js b/src/redux/reducers/aws.js index 0c2d209b..f1160ede 100644 --- a/src/redux/reducers/aws.js +++ b/src/redux/reducers/aws.js @@ -1,14 +1,14 @@ import * as Actions from '../actions/aws'; import deepClone from 'mout/src/lang/deepClone'; -const initialState = { +export const initialState = { list: [], active: 0, pending: false, mapById: {}, }; -const template = { +export const awsTemplate = { accessKeyId: '', availabilityZone: 'us-east-1a', name: 'new AWS profile', @@ -23,7 +23,7 @@ export default function awsReducer(state = initialState, action) { {}, state, { - list: [].concat(state.list, deepClone(template)), + list: [].concat(state.list, deepClone(awsTemplate)), active: state.list.length, }); } @@ -43,10 +43,8 @@ export default function awsReducer(state = initialState, action) { } case Actions.UPDATE_ACTIVE_AWS_PROFILE: { - return Object.assign( - {}, - state, - { active: action.index }); + const active = Math.min(Math.max(action.index, 0), state.list.length - 1); + return Object.assign({}, state, { active }); } case Actions.UPDATE_AWS_PROFILES: { From 8dc6af6c6418f51bcae698fd1c128c05af4adc93 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 15 Jun 2016 11:30:11 -0600 Subject: [PATCH 12/21] coverage tests --- package.json | 4 +++- test/karma.redux.js | 16 +++++++++++----- test/webpack.redux.js | 11 +++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 0253309d..fb8fa569 100644 --- a/package.json +++ b/package.json @@ -41,13 +41,15 @@ "karma-spec-reporter": "0.0.26", "karma-webpack": "1.7.0", "karma-phantomjs-launcher": "1.0.0", + "karma-coverage": "1.0.0", + "karma-sourcemap-loader": "0.3.7", + "istanbul-instrumenter-loader": "0.2.0", "phantomjs-prebuilt": "2.1.7", "jasmine": "2.4.1", "redux-actions-assertions": "1.1.0", "redux-thunk": "2.1.0", "expect": "1.20.1", "babel-polyfill": "6.9.1", - "paraviewweb": "1.6.0", "simput": "1.2.0", "pvw-visualizer": "1.0.11", diff --git a/test/karma.redux.js b/test/karma.redux.js index d7d28060..456f47b4 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -11,20 +11,26 @@ module.exports = function(config) { singleRun: true, frameworks: ['jasmine'], browsers: ['PhantomJS'], - reporters: ['spec'], + reporters: ['spec', 'coverage'], plugins: [ 'karma-jasmine', 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-webpack', + 'karma-coverage', + 'karma-sourcemap-loader', ], - preprocessors: { - 'tests.webpack.js': ['webpack'], - }, - webpack: wpConfig, files: [ '../node_modules/babel-polyfill/dist/polyfill.js', 'tests.webpack.js' ], + preprocessors: { + 'tests.webpack.js': ['webpack', 'sourcemap'], + }, + webpack: wpConfig, + coverageReporter: { + type: 'html', + dir: 'coverage/', + } }); }; diff --git a/test/webpack.redux.js b/test/webpack.redux.js index 3a344a78..9033906c 100644 --- a/test/webpack.redux.js +++ b/test/webpack.redux.js @@ -55,8 +55,15 @@ module.exports = { test: /\.js$/, exclude: /node_modules/, loader: 'babel?presets[]=es2015,presets[]=react', - } - ] + }, + ], + postLoaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'istanbul-instrumenter', + }, + ], }, resolve: { alias: { From b11a56efb3687c7d9e4315149647f6a18b5d77e7 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 15 Jun 2016 12:54:53 -0600 Subject: [PATCH 13/21] codecov script --- .travis.yml | 3 ++- package.json | 3 +++ test/karma.redux.js | 13 +++++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 56edc7a3..5ef6ed83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,8 @@ script: # Run client tests, only with python 2.7 - if [ -n "${PY2}" ]; then npm install; fi - if [ -n "${PY2}" ]; then npm run build:release; fi - - if [ -n "${PY2}" ]; then npm run test:redux -g; fi + - if [ -n "${PY2}" ]; then npm run test:redux; fi + - if [ -n "${PY2}" ]; then npm run codecov; fi # Now run server tests - mkdir _girder_build - pushd _girder_build diff --git a/package.json b/package.json index fb8fa569..e1feb4dd 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,8 @@ "redux-actions-assertions": "1.1.0", "redux-thunk": "2.1.0", "expect": "1.20.1", + "codecov.io": "0.1.6", + "babel-polyfill": "6.9.1", "paraviewweb": "1.6.0", "simput": "1.2.0", @@ -69,6 +71,7 @@ "start": "fix-autobahn && webpack-dev-server --progress --open", "test:redux": "NODE_ENV='test' karma start test/karma.redux.js", + "codecov": "cat test/coverage/lcov/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js", "commit" : "git cz", "semantic-release" : "semantic-release pre && npm publish && semantic-release post" diff --git a/test/karma.redux.js b/test/karma.redux.js index 456f47b4..f1366929 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -29,8 +29,17 @@ module.exports = function(config) { }, webpack: wpConfig, coverageReporter: { - type: 'html', - dir: 'coverage/', + reporters: [ + { + type: 'html', + dir: 'coverage/', + subdir: 'html' + }, { + type: 'lcovonly', + dir: 'coverage', + subdir: 'lcov' + }, + ] } }); }; From bd251fe11b550399542231a9914f9c2c7ee5260e Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 15 Jun 2016 15:05:13 -0600 Subject: [PATCH 14/21] actually added aws tests, fixed some fs tests --- src/redux/actions/fs.js | 3 - test/redux/aws.js | 109 ++++++++++++++++++++++++++++++++++++ test/redux/fs.js | 34 +++++------ test/sampleData/awsState.js | 28 +++++++++ 4 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 test/redux/aws.js create mode 100644 test/sampleData/awsState.js diff --git a/src/redux/actions/fs.js b/src/redux/actions/fs.js index 45a935e6..7a9beb0a 100644 --- a/src/redux/actions/fs.js +++ b/src/redux/actions/fs.js @@ -39,9 +39,6 @@ export function fetchFolder(id, fetchFolderMeta = true, openedFolders = []) { dispatch({ type: CHILDREN_FOLDERS, children, id }); children.forEach(folder => { dispatch(updateFolder(folder)); - if (openedFolders.indexOf()) { - dispatch(fetchFolder(folder._id, false, openedFolders)); - } }); }, error => { diff --git a/test/redux/aws.js b/test/redux/aws.js new file mode 100644 index 00000000..9a10dd02 --- /dev/null +++ b/test/redux/aws.js @@ -0,0 +1,109 @@ +import * as Actions from '../../src/redux/actions/aws'; +import awsReducer, { awsTemplate, initialState } from '../../src/redux/reducers/aws'; +import client from '../../src/network'; + +import awsState from '../sampleData/awsState'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +import deepClone from 'mout/src/lang/deepClone'; +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +describe('aws', () => { + const profiles = [{ _id: 'a1' }, { _id: 'b2' }]; + const profile = Object.assign({}, awsState.list[0]); + describe('simple actions', () => { + it('should add aws profile', (done) => { + const expectedAction = { type: Actions.ADD_AWS_PROFILE }; + expect(Actions.addAWSProfile()) + .toDispatchActions(expectedAction, complete(done)); + + const expectedState = deepClone(awsState); + expectedState.list.push(deepClone(awsTemplate)); + expectedState.active = 1; + expect(awsReducer(awsState, expectedAction)) + .toEqual(expectedState); + }); + + it('should update active profile', (done) => { + const expectedAction = { type: Actions.UPDATE_ACTIVE_AWS_PROFILE, index: 3 }; + expect(Actions.updateActiveProfile(3)) + .toDispatchActions(expectedAction, complete(done)); + + const expectedState = deepClone(awsState); + expectedState.active = 0; + expect(awsReducer(awsState, expectedAction)) + .toEqual(expectedState); + + const givenState = deepClone(awsState); + givenState.list.push(profiles); + givenState.active = 0; + expectedState.active = 1; + expectedState.list.push(profiles); + expect(awsReducer(givenState, expectedAction)) + .toEqual(expectedState); + }); + + it('should update profiles', (done) => { + const expectedAction = { type: Actions.UPDATE_AWS_PROFILES, profiles }; + expect(Actions.updateAWSProfiles(profiles)) + .toDispatchActions(expectedAction, complete(done)); + + const expectedState = deepClone(initialState); + expectedState.list = profiles; + expectedState.mapById.a1 = profiles[0]; + expectedState.mapById.b2 = profiles[1]; + expect(awsReducer(initialState, expectedAction)) + .toEqual(expectedState); + }); + }); + + describe('async actions', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should fetch aws profiles', (done) => { + const expectedAction = { type: Actions.UPDATE_AWS_PROFILES, profiles }; + setSpy(client, 'listAWSProfiles', profiles); + expect(Actions.fetchAWSProfiles()) + .toDispatchActions(expectedAction, complete(done)); + }); + + it('should remove aws profile', (done) => { + var expectedAction = { type: Actions.UPDATE_AWS_PROFILES, profiles }; + setSpy(client, 'deleteAWSProfile', null); + setSpy(client, 'listAWSProfiles', profiles); + expect(Actions.removeAWSProfile(0, profile)) + .toDispatchActions(expectedAction, complete(done)); + + expectedAction = { type: Actions.REMOVE_AWS_PROFILE, index: 0 }; + expect(Actions.removeAWSProfile(0, { name: 'no id'})) + .toDispatchActions(expectedAction, complete(done)); + }); + + + it('should update aws profile', (done) => { + setSpy(client, 'listAWSProfiles', awsState.list); + setSpy(client, 'createAWSProfile', profile); + var expectedAction = { type: Actions.UPDATE_AWS_PROFILES, profiles: [ profile ] }; + expect(Actions.updateAWSProfile(0, profile, true)) + .toDispatchActions(expectedAction, complete(done)); + + expectedAction = { type: Actions.SAVE_AWS_PROFILE, index: 0, profile: profile } + expect(Actions.updateAWSProfile(0, profile, false)) + .toDispatchActions(expectedAction, complete(done)); + }); + }); +}); diff --git a/test/redux/fs.js b/test/redux/fs.js index 6cefda79..22aaeb55 100644 --- a/test/redux/fs.js +++ b/test/redux/fs.js @@ -39,8 +39,10 @@ describe('fs', () => { }); // just reducer - it('should open a folder', () => { + it('should open a folder', (done) => { const action = { type: Actions.TOGGLE_OPEN_FOLDER, folderId: folder._id }; + expect(Actions.toggleOpenFolder(folder._id, false)) + .toDispatchActions([], complete(done)); // opens const newState = deepClone(fsState); @@ -59,22 +61,22 @@ describe('fs', () => { expect.restoreSpies(); }); - // this stalls phantomJS? - // it('should fetch folders and child items', (done) => { - // const childFolders = [ { _id: 'c3d4' }, { _id: 'e5f6' } ]; - // const items = [ { _id:'item1' }, { _id: 'item2' }, { _id: 'item3' } ]; - // const expectedActions = [ - // { type: Actions.CHILDREN_FOLDERS, children: childFolders, id: folder._id }, - // { type: Actions.CHILDREN_ITEMS, children: items, id: folder._id }, - // { type: Actions.UPDATE_FOLDER, folder, id: folder._id } - // ]; + // // this stalls phantomJS? + it('should fetch folders and child items', (done) => { + const childFolders = [ { _id: 'c3d4' }, { _id: 'e5f6' } ]; + const items = [ { _id:'item1' }, { _id: 'item2' }, { _id: 'item3' } ]; + const expectedActions = [ + { type: Actions.CHILDREN_FOLDERS, children: childFolders, id: folder._id }, + { type: Actions.CHILDREN_ITEMS, children: items, id: folder._id }, + { type: Actions.UPDATE_FOLDER, folder: childFolders[0], id: childFolders[0]._id }, + { type: Actions.UPDATE_FOLDER, folder: childFolders[1], id: childFolders[1]._id }, + ]; - // setSpy(client, 'listFolders', childFolders); - // setSpy(client, 'listItems', items); - // // setSpy(client, 'getFolder', null); + setSpy(client, 'listFolders', childFolders); + setSpy(client, 'listItems', items); - // expect(Actions.fetchFolder(folder._id, false)) - // .toDispatchActions(expectedActions, complete(done)); - // }); + expect(Actions.fetchFolder(folder._id, false)) + .toDispatchActions(expectedActions, complete(done)); + }); }); }); diff --git a/test/sampleData/awsState.js b/test/sampleData/awsState.js new file mode 100644 index 00000000..3899ecdc --- /dev/null +++ b/test/sampleData/awsState.js @@ -0,0 +1,28 @@ +export default { + "list": [ + { + "_id": "574c8a770640fd3f1a3b377a", + "accessKeyId": "aradomstringofcharacters", + "availabilityZone": "us-north-2a", + "name": "northerly", + "publicIPs": false, + "regionHost": "ec2.us-north-2.amazonaws.com", + "regionName": "us-west-2", + "status": "available" + } + ], + "active": 0, + "pending": false, + "mapById": { + "574c8a770640fd3f1a3b377a": { + "_id": "574c8a770640fd3f1a3b377a", + "accessKeyId": "aradomstringofcharacters", + "availabilityZone": "us-north-2a", + "name": "northerly", + "publicIPs": false, + "regionHost": "ec2.us-north-2.amazonaws.com", + "regionName": "us-north-2", + "status": "available" + } + } +} From 3923cd3e044b8685707755bb6800310c46cfa22a Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 15 Jun 2016 17:01:41 -0600 Subject: [PATCH 15/21] users and statuses tests --- src/redux/reducers/auth.js | 2 +- src/redux/reducers/statuses.js | 2 +- test/redux/statuses.js | 64 +++++++++++++++++++++++++++++ test/redux/users.js | 73 ++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 test/redux/statuses.js create mode 100644 test/redux/users.js diff --git a/src/redux/reducers/auth.js b/src/redux/reducers/auth.js index 120e98fe..650c0428 100644 --- a/src/redux/reducers/auth.js +++ b/src/redux/reducers/auth.js @@ -1,6 +1,6 @@ import * as Actions from '../actions/user'; -const initialState = { +export const initialState = { pending: false, user: null, }; diff --git a/src/redux/reducers/statuses.js b/src/redux/reducers/statuses.js index 8be778ee..b4172405 100644 --- a/src/redux/reducers/statuses.js +++ b/src/redux/reducers/statuses.js @@ -1,6 +1,6 @@ import * as Actions from '../actions/statuses'; -const initialState = { +export const initialState = { ec2: [], clusters: [], }; diff --git a/test/redux/statuses.js b/test/redux/statuses.js new file mode 100644 index 00000000..960738e0 --- /dev/null +++ b/test/redux/statuses.js @@ -0,0 +1,64 @@ +import * as Actions from '../../src/redux/actions/statuses'; +import statusesReducer, { initialState } from '../../src/redux/reducers/statuses'; +import client from '../../src/network'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +Object.freeze(initialState); + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +describe('status', () => { + const list = [{ _id: 'a1' }, { _id: 'b2' }]; + describe('simple actions', () => { + it('should update cluster list', (done) => { + const expectedAction = { type: Actions.UPDATE_CLUSTERS_LIST, list }; + expect(Actions.updateClusterList(list)) + .toDispatchActions(expectedAction, complete(done)); + + const expectedState = Object.assign({}, initialState); + expectedState.clusters = list; + expect(statusesReducer(initialState, expectedAction)) + .toEqual(expectedState); + }); + + it('should update ec2 list', (done) => { + const expectedAction = { type: Actions.UPDATE_EC2_LIST, list }; + expect(Actions.updateEC2List(list)) + .toDispatchActions(expectedAction, complete(done)); + + const expectedState = Object.assign({}, initialState); + expectedState.ec2 = list; + expect(statusesReducer(initialState, expectedAction)) + .toEqual(expectedState); + }); + }); + + describe('async action', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should fetch servers', (done) => { + const expectedActions = [ + { type: Actions.UPDATE_EC2_LIST, list }, + { type: Actions.UPDATE_CLUSTERS_LIST, list }, + ]; + setSpy(client, 'listClusters', list); + setSpy(client, 'listAWSProfiles', list); + expect(Actions.fetchServers()) + .toDispatchActions(expectedActions, complete(done)); + }); + }); +}); diff --git a/test/redux/users.js b/test/redux/users.js new file mode 100644 index 00000000..060462de --- /dev/null +++ b/test/redux/users.js @@ -0,0 +1,73 @@ +import * as Actions from '../../src/redux/actions/user'; +import * as routingActions from '../../src/redux/actions/router'; +import usersReducer, { initialState } from '../../src/redux/reducers/auth'; +import client from '../../src/network'; + +import expect from 'expect'; +import thunk from 'redux-thunk'; +import complete from '../helpers/complete'; +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +Object.freeze(initialState); + +function setSpy(target, method, data, raw=false) { + if (raw) { + expect.spyOn(target, method) + .andReturn(data); + } else { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); + } +} + +describe('user', () => { + const user = { _id: 'a1', name:'Tom' }; + describe('simple actions', () => { + it('should trigger a login action', (done) => { + const expectedAction = { type: Actions.LOGGED_IN, user }; + expect(Actions.loggedIn(user)) + .toDispatchActions(expectedAction, complete(done)); + + const expectedState = Object.assign({}, initialState); + expectedState.user = user; + expect(usersReducer(initialState, expectedAction)) + .toEqual(expectedState); + }); + }); + + describe('async action', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should login user', (done) => { + const expectedActions = [ + { type: Actions.LOGGED_IN, user }, + routingActions.replace('/'), + ]; + setSpy(client, 'login', user); + setSpy(client, 'getLoggedInUser', user, true); + expect(Actions.login('Tom', 'my-password')) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should logout user', (done) => { + setSpy(client, 'logout', user); + expect(Actions.logout()) + .toDispatchActions(routingActions.replace('/'), complete(done)); + expect(client.logout).toHaveBeenCalled(); + }); + + it('should register user', (done) => { + setSpy(client, 'createUser', user); + expect(Actions.register('Tom', 'Bob', 'tbob11', 'test@wow.com', 'my-password')) + .toDispatchActions(routingActions.replace('/Login'), complete(done)); + expect(client.createUser).toHaveBeenCalled(); + }); + }); +}); From f85183b8683ee9c67c8abd234f650e729b337349 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Thu, 16 Jun 2016 12:51:52 -0600 Subject: [PATCH 16/21] added a cluster test --- src/redux/actions/clusters.js | 38 +++++++++++++++++------------------ test/redux/clusters.js | 17 +++++++++++++++- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/redux/actions/clusters.js b/src/redux/actions/clusters.js index 97434316..2329e6c0 100644 --- a/src/redux/actions/clusters.js +++ b/src/redux/actions/clusters.js @@ -193,27 +193,27 @@ export function fetchClusterPresets() { // removes a cluster from the preferences page and deletes it if has _id export function removeCluster(index, cluster) { + if (!cluster || !cluster._id) { + return { type: REMOVE_CLUSTER, index }; + } + return dispatch => { - if (cluster._id) { - const action = netActions.addNetworkCall('remove_cluster', 'Remove cluster'); - - dispatch(pendingNetworkCall(true)); - client.deleteCluster(cluster._id) - .then( - resp => { - dispatch(netActions.successNetworkCall(action.id, resp)); - dispatch(pendingNetworkCall(false)); - dispatch(fetchClusters()); - }, - err => { - dispatch(netActions.errorNetworkCall(action.id, err)); - dispatch(pendingNetworkCall(false)); - }); - - return action; - } + const action = netActions.addNetworkCall('remove_cluster', 'Remove cluster'); - return { type: REMOVE_CLUSTER, index }; + dispatch(pendingNetworkCall(true)); + client.deleteCluster(cluster._id) + .then( + resp => { + dispatch(netActions.successNetworkCall(action.id, resp)); + dispatch(pendingNetworkCall(false)); + dispatch(fetchClusters()); + }, + err => { + dispatch(netActions.errorNetworkCall(action.id, err)); + dispatch(pendingNetworkCall(false)); + }); + + return action; }; } diff --git a/test/redux/clusters.js b/test/redux/clusters.js index c9437f19..64f74eca 100644 --- a/test/redux/clusters.js +++ b/test/redux/clusters.js @@ -57,7 +57,6 @@ describe('cluster actions', () => { // skipping reducer test }); - // FIXME, needs sample data it('should remove cluster given an id', (done) => { const expectedAction = { type: Actions.REMOVE_CLUSTER_BY_ID, id: cluster._id }; expect(Actions.removeClusterById(cluster._id)) @@ -121,6 +120,14 @@ describe('cluster actions', () => { .toEqual(expectedState); }); + it('should remove a cluster', (done) => { + const expectedActions = [{ type: Actions.REMOVE_CLUSTER, index: 0 }]; + + expect(Actions.removeCluster(0)) + .toDispatchActions(expectedActions, complete(done)); + }); + + it('should update cluster presets', (done) => { const presets = [{ name: 'myCluster' }, { name: 'myOtherCluster' }]; const expectedAction = { type: Actions.UPDATE_CLUSTER_PRESETS, presets }; @@ -195,6 +202,14 @@ describe('cluster actions', () => { .toDispatchActions(expectedAction, complete(done)); }); + it('should delete a cluster with an id', (done) => { + const expectedActions = [{ type: Actions.UPDATE_CLUSTERS, clusters: [] }]; + setSpy(client, 'deleteCluster', null); + setSpy(client, 'listClusters', []); + expect(Actions.removeCluster(0, cluster)) + .toDispatchActions(expectedActions, complete(done)); + }); + it('should save a cluster', (done) => { const expectedAction = { type: Actions.SAVE_CLUSTER, index: 0, cluster }; setSpy(ClusterHelpers, 'saveCluster', true); From db8fd5c054c39607643bc3ac91382ab48804b73a Mon Sep 17 00:00:00 2001 From: TristanWright Date: Fri, 17 Jun 2016 15:06:35 -0600 Subject: [PATCH 17/21] component tests (just ButtonBar, and RunForm) --- .travis.yml | 2 +- package.json | 2 + test/components/ButtonBar.js | 30 ++++++++ test/components/RunForm.js | 79 ++++++++++++++++++++++ test/karma.base.js | 36 ++++++++++ test/karma.component.js | 36 ++++++++++ test/karma.redux.js | 74 +++++++++----------- test/redux/clusters.js | 13 ++-- test/sampleData/awsClusters.js | 67 ++++++++++++++++++ test/sampleData/machines.js | 77 +++++++++++++++++++++ test/sampleData/runFormState.js | 25 +++++++ test/tests.components.js | 2 + test/{tests.webpack.js => tests.redux.js} | 1 - test/{webpack.redux.js => webpack.test.js} | 4 +- 14 files changed, 398 insertions(+), 50 deletions(-) create mode 100644 test/components/ButtonBar.js create mode 100644 test/components/RunForm.js create mode 100644 test/karma.base.js create mode 100644 test/karma.component.js create mode 100644 test/sampleData/awsClusters.js create mode 100644 test/sampleData/machines.js create mode 100644 test/sampleData/runFormState.js create mode 100644 test/tests.components.js rename test/{tests.webpack.js => tests.redux.js} (61%) rename test/{webpack.redux.js => webpack.test.js} (96%) diff --git a/.travis.yml b/.travis.yml index 5ef6ed83..82baf35b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ script: # Run client tests, only with python 2.7 - if [ -n "${PY2}" ]; then npm install; fi - if [ -n "${PY2}" ]; then npm run build:release; fi - - if [ -n "${PY2}" ]; then npm run test:redux; fi + - if [ -n "${PY2}" ]; then npm run test; fi - if [ -n "${PY2}" ]; then npm run codecov; fi # Now run server tests - mkdir _girder_build diff --git a/package.json b/package.json index e1feb4dd..403e35ec 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,9 @@ "start": "fix-autobahn && webpack-dev-server --progress --open", + "test": "npm run test:redux && npm run test:component", "test:redux": "NODE_ENV='test' karma start test/karma.redux.js", + "test:component": "NODE_ENV='test' karma start test/karma.component.js", "codecov": "cat test/coverage/lcov/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js", "commit" : "git cz", diff --git a/test/components/ButtonBar.js b/test/components/ButtonBar.js new file mode 100644 index 00000000..6c81009a --- /dev/null +++ b/test/components/ButtonBar.js @@ -0,0 +1,30 @@ +import expect from 'expect'; +import React from 'react'; +import TestUtils from 'react/lib/ReactTestUtils'; +import ButtonBar from '../../src/panels/ButtonBar'; + +/* global describe it */ + +describe('ButtonBar', () => { + it('should render an empty bar', () => { + const el = TestUtils.renderIntoDocument(); + const buttons = TestUtils.findAllInRenderedTree(el, (component) => component.tagName === 'BUTTON'); + expect(buttons.length).toEqual(0); + }); + + it('should render a buttons', () => { + const actions = [{ name: 'Reset', disabled: false }, { name: 'Submit', disabled: false }]; + const el = TestUtils.renderIntoDocument(); + const buttons = TestUtils.findAllInRenderedTree(el, (component) => component.tagName === 'BUTTON'); + expect(buttons.length).toEqual(2); + }); + + it('should be hidden', () => { + const actions = [{ name: 'Reset', disabled: false }, { name: 'Submit', disabled: false }]; + var renderer = TestUtils.createRenderer(), + result; + renderer.render(); + result = renderer.getRenderOutput(); + expect(result).toEqual(null); // when not visible it renders null + }); +}); diff --git a/test/components/RunForm.js b/test/components/RunForm.js new file mode 100644 index 00000000..c0276026 --- /dev/null +++ b/test/components/RunForm.js @@ -0,0 +1,79 @@ +import expect from 'expect'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import TestUtils from 'react/lib/ReactTestUtils'; +import client from '../../src/network'; +import RunForm from '../../src/panels/run'; +import RunEC2 from '../../src/panels/run/RunEC2'; +import RunCluster from '../../src/panels/run/RunCluster'; + +import style from 'HPCCloudStyle/ItemEditor.mcss'; + +import sampleData from '../sampleData/runFormState'; +import machines from '../sampleData/machines'; +import awsClusters from '../sampleData/awsClusters'; +import awsState from '../sampleData/awsState'; + +import deepClone from 'mout/src/lang/deepClone'; +/* global describe it */ + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +const blankFunc = () => {}; + +describe('RunForm', () => { + describe('EC2', () => { + setSpy(client, 'listAWSProfiles', awsState.list); + setSpy(client, 'getEC2InstanceTypes', machines); + setSpy(client, 'listClusters', awsClusters); + + const formState = { profiles: awsState.list, profile: awsState.list[0], + busy: false, + machines, + machineFamilies: Object.keys(machines[awsState.list[0].regionName]), + machinesInFamily: machines[awsState.list[0].regionName]['General purpose'], + clusters: awsClusters, + }; + + it('renders EC2 form', () => { + const el = TestUtils.renderIntoDocument(); + expect(TestUtils.scryRenderedComponentsWithType(el, RunEC2).length).toEqual(1); + expect(client.listAWSProfiles).toHaveBeenCalled(); + }); + + it('should have a cluster list if there are clusters', () => { + const el = TestUtils.renderIntoDocument(); + el.setState(formState); + expect(TestUtils.scryRenderedDOMComponentsWithClass(el, style.group).length).toEqual(7); + }); + + it('should not have a cluster list when there are no clusters', () => { + const el = TestUtils.renderIntoDocument(); + var noClusters = deepClone(formState); + delete noClusters.clusters; + el.setState(noClusters); + expect(TestUtils.scryRenderedDOMComponentsWithClass(el, style.group).length).toEqual(6); + }); + }); + + describe('Trad Cluster', () => { + it('renders Traditional Cluster form', () => { + setSpy(client, 'listClusters', []); + + const el = TestUtils.renderIntoDocument(); + expect(TestUtils.scryRenderedComponentsWithType(el, RunCluster).length).toEqual(1); + expect(client.listClusters).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/karma.base.js b/test/karma.base.js new file mode 100644 index 00000000..b0bce4df --- /dev/null +++ b/test/karma.base.js @@ -0,0 +1,36 @@ +/* eslint-disable */ + +module.exports = { + basePath: '.', + client: { + captureConsole: true + }, + singleRun: true, + frameworks: ['jasmine'], + browsers: ['PhantomJS'], + reporters: ['spec', 'coverage'], + plugins: [ + 'karma-jasmine', + 'karma-phantomjs-launcher', + 'karma-spec-reporter', + 'karma-webpack', + 'karma-coverage', + 'karma-sourcemap-loader', + ], + preprocessors: { + 'tests.webpack.js': ['webpack', 'sourcemap'], + }, + coverageReporter: { + reporters: [ + { + type: 'html', + dir: 'coverage/', + subdir: 'html' + }, { + type: 'lcovonly', + dir: 'coverage', + subdir: 'lcov' + }, + ] + } +}; diff --git a/test/karma.component.js b/test/karma.component.js new file mode 100644 index 00000000..60aeed2d --- /dev/null +++ b/test/karma.component.js @@ -0,0 +1,36 @@ +/* eslint-disable */ +var wpConfig = require('./webpack.test.js'); +var karmaConfig = require('./karma.base.js'); + +wpConfig.entry = { + 'tests.components.js': './test/tests.components.js', +}; + +karmaConfig.webpack = wpConfig; + +karmaConfig.files = [ + '../node_modules/babel-polyfill/dist/polyfill.js', + 'tests.components.js' +]; + +karmaConfig.preprocessors = { + 'tests.components.js': ['webpack', 'sourcemap'], +}; + +karmaConfig.coverageReporters = { + reporters: [ + { + type: 'html', + dir: 'coverage/components', + subdir: 'html' + }, { + type: 'lcovonly', + dir: 'coverage/components', + subdir: 'lcov' + }, + ], +}; + +module.exports = function(config) { + config.set(karmaConfig); +}; diff --git a/test/karma.redux.js b/test/karma.redux.js index f1366929..8bb07cae 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -1,45 +1,37 @@ /* eslint-disable */ -// karma config file -var wpConfig = require('./webpack.redux.js'); +var wpConfig = require('./webpack.test.js'); +var karmaConfig = require('./karma.base.js'); -module.exports = function(config) { - config.set({ - basePath: '.', - client: { - captureConsole: true - }, - singleRun: true, - frameworks: ['jasmine'], - browsers: ['PhantomJS'], - reporters: ['spec', 'coverage'], - plugins: [ - 'karma-jasmine', - 'karma-phantomjs-launcher', - 'karma-spec-reporter', - 'karma-webpack', - 'karma-coverage', - 'karma-sourcemap-loader', - ], - files: [ - '../node_modules/babel-polyfill/dist/polyfill.js', - 'tests.webpack.js' - ], - preprocessors: { - 'tests.webpack.js': ['webpack', 'sourcemap'], +wpConfig.entry = { + 'tests.redux.js': './test/tests.redux.js', +}; + +karmaConfig.webpack = wpConfig; + +karmaConfig.files = [ + '../node_modules/babel-polyfill/dist/polyfill.js', + 'tests.redux.js' +]; + +karmaConfig.preprocessors = { + 'tests.redux.js': ['webpack', 'sourcemap'], +}; + +karmaConfig.coverageReporters = { + reporters: [ + { + type: 'html', + dir: 'coverage/redux', + subdir: 'html' + }, { + type: 'lcovonly', + dir: 'coverage/redux', + subdir: 'lcov' }, - webpack: wpConfig, - coverageReporter: { - reporters: [ - { - type: 'html', - dir: 'coverage/', - subdir: 'html' - }, { - type: 'lcovonly', - dir: 'coverage', - subdir: 'lcov' - }, - ] - } - }); + ] +}; + +module.exports = function(config) { + config.set(karmaConfig); }; + diff --git a/test/redux/clusters.js b/test/redux/clusters.js index 64f74eca..7bbaaa5a 100644 --- a/test/redux/clusters.js +++ b/test/redux/clusters.js @@ -120,6 +120,12 @@ describe('cluster actions', () => { .toEqual(expectedState); }); + it('should save a cluster locally', (done) => { + const expectedAction = { type: Actions.SAVE_CLUSTER, index: 0, cluster }; + expect(Actions.saveCluster(0, cluster, false)) + .toDispatchActions(expectedAction, complete(done)); + }); + it('should remove a cluster', (done) => { const expectedActions = [{ type: Actions.REMOVE_CLUSTER, index: 0 }]; @@ -127,7 +133,6 @@ describe('cluster actions', () => { .toDispatchActions(expectedActions, complete(done)); }); - it('should update cluster presets', (done) => { const presets = [{ name: 'myCluster' }, { name: 'myOtherCluster' }]; const expectedAction = { type: Actions.UPDATE_CLUSTER_PRESETS, presets }; @@ -210,10 +215,10 @@ describe('cluster actions', () => { .toDispatchActions(expectedActions, complete(done)); }); - it('should save a cluster', (done) => { + it('should save a cluster remotely', (done) => { const expectedAction = { type: Actions.SAVE_CLUSTER, index: 0, cluster }; - setSpy(ClusterHelpers, 'saveCluster', true); - expect(Actions.saveCluster(0, cluster, false)) + setSpy(ClusterHelpers, 'saveCluster', null); + expect(Actions.saveCluster(0, cluster, true)) .toDispatchActions(expectedAction, complete(done)); }); diff --git a/test/sampleData/awsClusters.js b/test/sampleData/awsClusters.js new file mode 100644 index 00000000..267b6f73 --- /dev/null +++ b/test/sampleData/awsClusters.js @@ -0,0 +1,67 @@ +export default [ + { + "_id": "a1", + "config": { + "launch": { + "params": { + "extra_rules": [ + { + "cidr_ip": "100", + "from_port": 9000, + "proto": "tcp", + "to_port": 9000 + } + ], + "gpu": 0, + "master_instance_ami": "ami-wow", + "master_instance_type": "t2.nano", + "node_instance_ami": "ami-wow", + "node_instance_count": 0, + "node_instance_type": "t2.nano", + "source_cidr_ip": "100" + }, + "spec": "ec2" + }, + "scheduler": { + "type": "sge" + }, + }, + "name": "0001", + "profileId": "p1", + "status": "running", + "type": "ec2", + }, + { + "_id": "b2", + "config": { + "host": "100", + "launch": { + "params": { + "extra_rules": [ + { + "cidr_ip": "100", + "from_port": 9000, + "proto": "tcp", + "to_port": 9000 + } + ], + "gpu": 0, + "master_instance_ami": "ami-wow", + "master_instance_type": "t2.nano", + "node_instance_ami": "ami-wow", + "node_instance_count": 1, + "node_instance_type": "t2.nano", + "source_cidr_ip": "100" + }, + "spec": "ec2" + }, + "scheduler": { + "type": "sge" + }, + }, + "name": "my new node", + "profileId": "q2", + "status": "running", + "type": "ec2", + }, +] diff --git a/test/sampleData/machines.js b/test/sampleData/machines.js new file mode 100644 index 00000000..f7069085 --- /dev/null +++ b/test/sampleData/machines.js @@ -0,0 +1,77 @@ +export default { + "eu-central-1": { + "General purpose": [ + { + "id": "t2.nano", + "cpu": "1", + "memory": "0.5 GiB", + "storage": "EBS only", + "family": "General purpose", + "price": "0.0075000000", + "gpu": 0 + }, + ], + "Storage optimized": [ + { + "id": "i2.xlarge", + "cpu": "4", + "memory": "30.5 GiB", + "storage": "1 x 800 SSD", + "family": "Storage optimized", + "price": "1.0130000000", + "gpu": 0 + }, + { + "id": "d2.xlarge", + "cpu": "4", + "memory": "30.5 GiB", + "storage": "3 x 2000 HDD", + "family": "Storage optimized", + "price": "0.7940000000", + "gpu": 0 + }, + ], + }, + "us-west-2": { + "General purpose": [ + { + "id": "t2.nano", + "cpu": "1", + "memory": "0.5 GiB", + "storage": "EBS only", + "family": "General purpose", + "price": "0.0100000000", + "gpu": 0 + }, + { + "id": "t2.micro", + "cpu": "1", + "memory": "1 GiB", + "storage": "EBS only", + "family": "General purpose", + "price": "0.0200000000", + "gpu": 0 + }, + ], + "Storage optimized": [ + { + "id": "i2.xlarge", + "cpu": "4", + "memory": "30.5 GiB", + "storage": "1 x 800 SSD", + "family": "Storage optimized", + "price": "0.8530000000", + "gpu": 0 + }, + { + "id": "d2.xlarge", + "cpu": "4", + "memory": "30.5 GiB", + "storage": "3 x 2000 HDD", + "family": "Storage optimized", + "price": "0.6900000000", + "gpu": 0 + }, + ], + }, +}; diff --git a/test/sampleData/runFormState.js b/test/sampleData/runFormState.js new file mode 100644 index 00000000..b7a15ea1 --- /dev/null +++ b/test/sampleData/runFormState.js @@ -0,0 +1,25 @@ +export default { + "EC2": { + "profile": "ec2Id", + "machine": { + "id": "t2.nano", + "cpu": "1", + "memory": "0.5 GiB", + "storage": "EBS only", + "family": "General purpose", + "price": "0.0065000000", + "gpu": 0 + }, + "clusterSize": "", + "volumneSize": "", + "cluster": null + }, + "Traditional": { + "profile": "tradId", + "runtime": { + "numberOfGpusPerNode": 1, + "numberOfSlots": 1, + "queue": "" + } + }, +} diff --git a/test/tests.components.js b/test/tests.components.js new file mode 100644 index 00000000..8bad577d --- /dev/null +++ b/test/tests.components.js @@ -0,0 +1,2 @@ +var context = require.context('./components', true, /\.js$/); +context.keys().forEach(context); diff --git a/test/tests.webpack.js b/test/tests.redux.js similarity index 61% rename from test/tests.webpack.js rename to test/tests.redux.js index 13cc465b..b9454d76 100644 --- a/test/tests.webpack.js +++ b/test/tests.redux.js @@ -1,3 +1,2 @@ -// gathers the files to be tested, currently just redux. var context = require.context('./redux', true, /\.js$/); context.keys().forEach(context); diff --git a/test/webpack.redux.js b/test/webpack.test.js similarity index 96% rename from test/webpack.redux.js rename to test/webpack.test.js index 9033906c..03ceff04 100644 --- a/test/webpack.redux.js +++ b/test/webpack.test.js @@ -14,12 +14,10 @@ var definePlugin = new webpack.DefinePlugin({ }); module.exports = { - entry: { - 'tests.webpack.js': './test/tests.webpack.js', - }, plugins: [ definePlugin, ], + noInfo: true, module: { loaders: [ { From 58458b14fb11fad6c31a35309ee8fa6ffe0dc86d Mon Sep 17 00:00:00 2001 From: TristanWright Date: Fri, 17 Jun 2016 17:57:13 -0600 Subject: [PATCH 18/21] test all super script --- package.json | 4 ++-- test/README.md | 29 +++++++++++++++++++++++++ test/components/RunForm.js | 1 - test/{ => config}/karma.base.js | 2 +- test/{ => config}/webpack.test.js | 0 test/contexts/tests.all.js | 5 +++++ test/contexts/tests.components.js | 2 ++ test/contexts/tests.redux.js | 2 ++ test/karma.all.js | 35 +++++++++++++++++++++++++++++++ test/karma.component.js | 15 +++++++------ test/karma.redux.js | 15 +++++++------ test/tests.components.js | 2 -- test/tests.redux.js | 2 -- 13 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 test/README.md rename test/{ => config}/karma.base.js (97%) rename test/{ => config}/webpack.test.js (100%) create mode 100644 test/contexts/tests.all.js create mode 100644 test/contexts/tests.components.js create mode 100644 test/contexts/tests.redux.js create mode 100644 test/karma.all.js delete mode 100644 test/tests.components.js delete mode 100644 test/tests.redux.js diff --git a/package.json b/package.json index 403e35ec..ddd55409 100644 --- a/package.json +++ b/package.json @@ -70,10 +70,10 @@ "start": "fix-autobahn && webpack-dev-server --progress --open", - "test": "npm run test:redux && npm run test:component", + "test": "NODE_ENV='test' karma start test/karma.all.js", "test:redux": "NODE_ENV='test' karma start test/karma.redux.js", "test:component": "NODE_ENV='test' karma start test/karma.component.js", - "codecov": "cat test/coverage/lcov/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js", + "codecov": "cat coverage/lcov/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js", "commit" : "git cz", "semantic-release" : "semantic-release pre && npm publish && semantic-release post" diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..7ec03736 --- /dev/null +++ b/test/README.md @@ -0,0 +1,29 @@ +# HPC-Cloud Tests + +We test two things: + +1. **Redux actions and reducers** - Redux maintains the data structure and data flow of HPC-Cloud. Many bugs have come from incorrect or inconsistent Redux actions and their corresponding reducers. For actions which call a backend resource we mock the `client` call with an [expect spy](https://github.com/mjackson/expect#spy-methods) which returns our expected data that we use in tests. +2. **React components** - We reuse React components in `src/panels` in many places. At time of writing there aren't many tests for components, however the infrastructure is there and it's easy to write new tests component behavior. + +## Running + +- Redux - `nam run test:redux` +- Components - `npm run test:components` +- Everything - `nam run test` + +## Writing new tests + +We're using: + +- [Karma](https://karma-runner.github.io/0.13/index.html): with [webpack](https://webpack.github.io/) and [istanbul-instrumenter](https://github.com/deepsweet/istanbul-instrumenter-loader) +- [Jasmine](http://jasmine.github.io/2.4/introduction.html) +- [PhantomJS](http://phantomjs.org/) +- [expect](https://github.com/mjackson/expect) +- [redux-actions-assertions](https://github.com/dmitry-zaets/redux-actions-assertions) (Redux tests only, but can be used for tests which have redux elements.) +- [React Test Utils](https://facebook.github.io/react/docs/test-utils.html) (For tests involving React components) + +### Redux +Check if there is already a file for the actions or reducers you're testing in `/test/redux`, add a new file if there isn't one already. Action testing can be split up by "simple actions" and "async actions". Simple actions just return an object with `type` and optionally some data payload. Async actions call some backend component. + +### Components +Writing tests for React components can be a bit finicky. For components with some state or props reliance they may not fully render. Luckily you can manipulate the React components just like you would with plain javascript objects. \ No newline at end of file diff --git a/test/components/RunForm.js b/test/components/RunForm.js index c0276026..bf3a4d08 100644 --- a/test/components/RunForm.js +++ b/test/components/RunForm.js @@ -44,7 +44,6 @@ describe('RunForm', () => { onChange={blankFunc} serverTypeChange={blankFunc} />); expect(TestUtils.scryRenderedComponentsWithType(el, RunEC2).length).toEqual(1); - expect(client.listAWSProfiles).toHaveBeenCalled(); }); it('should have a cluster list if there are clusters', () => { diff --git a/test/karma.base.js b/test/config/karma.base.js similarity index 97% rename from test/karma.base.js rename to test/config/karma.base.js index b0bce4df..3fa9df10 100644 --- a/test/karma.base.js +++ b/test/config/karma.base.js @@ -1,7 +1,7 @@ /* eslint-disable */ module.exports = { - basePath: '.', + basePath: '..', client: { captureConsole: true }, diff --git a/test/webpack.test.js b/test/config/webpack.test.js similarity index 100% rename from test/webpack.test.js rename to test/config/webpack.test.js diff --git a/test/contexts/tests.all.js b/test/contexts/tests.all.js new file mode 100644 index 00000000..4e218840 --- /dev/null +++ b/test/contexts/tests.all.js @@ -0,0 +1,5 @@ +var reduxContext = require.context('../redux', true, /\.js$/); +reduxContext.keys().forEach(reduxContext); + +var componentContext = require.context('../components', true, /\.js$/); +componentContext.keys().forEach(componentContext); diff --git a/test/contexts/tests.components.js b/test/contexts/tests.components.js new file mode 100644 index 00000000..05114f1e --- /dev/null +++ b/test/contexts/tests.components.js @@ -0,0 +1,2 @@ +var context = require.context('../components', true, /\.js$/); +context.keys().forEach(context); diff --git a/test/contexts/tests.redux.js b/test/contexts/tests.redux.js new file mode 100644 index 00000000..48f2ca00 --- /dev/null +++ b/test/contexts/tests.redux.js @@ -0,0 +1,2 @@ +var reduxContext = require.context('../redux', true, /\.js$/); +reduxContext.keys().forEach(reduxContext); diff --git a/test/karma.all.js b/test/karma.all.js new file mode 100644 index 00000000..c72b0c30 --- /dev/null +++ b/test/karma.all.js @@ -0,0 +1,35 @@ +/* eslint-disable */ +var wpConfig = require('./config/webpack.test.js'); +var karmaConfig = require('./config/karma.base.js'); + +wpConfig.entry = { + 'tests.all.js': './test/contexts/tests.all.js', +}; + +karmaConfig.webpack = wpConfig; + +karmaConfig.files = [ + 'node_modules/babel-polyfill/dist/polyfill.js', + 'test/contexts/tests.all.js' +]; + +karmaConfig.preprocessors = { + 'test/contexts/tests.all.js': ['webpack', 'sourcemap'], +}; + +karmaConfig.coverageReporters = { + dir: 'coverage/all', + reporters: [ + { + type: 'html', + subdir: 'html' + }, { + type: 'lcovonly', + subdir: 'lcov' + }, + ], +}; + +module.exports = function(config) { + config.set(karmaConfig); +}; diff --git a/test/karma.component.js b/test/karma.component.js index 60aeed2d..56487636 100644 --- a/test/karma.component.js +++ b/test/karma.component.js @@ -1,31 +1,30 @@ /* eslint-disable */ -var wpConfig = require('./webpack.test.js'); -var karmaConfig = require('./karma.base.js'); +var wpConfig = require('./config/webpack.test.js'); +var karmaConfig = require('./config/karma.base.js'); wpConfig.entry = { - 'tests.components.js': './test/tests.components.js', + 'tests.components.js': './test/contexts/tests.components.js', }; karmaConfig.webpack = wpConfig; karmaConfig.files = [ - '../node_modules/babel-polyfill/dist/polyfill.js', - 'tests.components.js' + 'node_modules/babel-polyfill/dist/polyfill.js', + 'test/contexts/tests.components.js' ]; karmaConfig.preprocessors = { - 'tests.components.js': ['webpack', 'sourcemap'], + 'test/contexts/tests.components.js': ['webpack', 'sourcemap'], }; karmaConfig.coverageReporters = { + dir: 'coverage/components', reporters: [ { type: 'html', - dir: 'coverage/components', subdir: 'html' }, { type: 'lcovonly', - dir: 'coverage/components', subdir: 'lcov' }, ], diff --git a/test/karma.redux.js b/test/karma.redux.js index 8bb07cae..5dc54c29 100644 --- a/test/karma.redux.js +++ b/test/karma.redux.js @@ -1,31 +1,30 @@ /* eslint-disable */ -var wpConfig = require('./webpack.test.js'); -var karmaConfig = require('./karma.base.js'); +var wpConfig = require('./config/webpack.test.js'); +var karmaConfig = require('./config/karma.base.js'); wpConfig.entry = { - 'tests.redux.js': './test/tests.redux.js', + 'tests.redux.js': './test/contexts/tests.redux.js', }; karmaConfig.webpack = wpConfig; karmaConfig.files = [ - '../node_modules/babel-polyfill/dist/polyfill.js', - 'tests.redux.js' + 'node_modules/babel-polyfill/dist/polyfill.js', + 'test/contexts/tests.redux.js' ]; karmaConfig.preprocessors = { - 'tests.redux.js': ['webpack', 'sourcemap'], + 'test/contexts/tests.redux.js': ['webpack', 'sourcemap'], }; karmaConfig.coverageReporters = { + dir: 'coverage/redux', reporters: [ { type: 'html', - dir: 'coverage/redux', subdir: 'html' }, { type: 'lcovonly', - dir: 'coverage/redux', subdir: 'lcov' }, ] diff --git a/test/tests.components.js b/test/tests.components.js deleted file mode 100644 index 8bad577d..00000000 --- a/test/tests.components.js +++ /dev/null @@ -1,2 +0,0 @@ -var context = require.context('./components', true, /\.js$/); -context.keys().forEach(context); diff --git a/test/tests.redux.js b/test/tests.redux.js deleted file mode 100644 index b9454d76..00000000 --- a/test/tests.redux.js +++ /dev/null @@ -1,2 +0,0 @@ -var context = require.context('./redux', true, /\.js$/); -context.keys().forEach(context); From a8197f0b2d2f7599477d05c287d53d4ad165d1e0 Mon Sep 17 00:00:00 2001 From: TristanWright Date: Mon, 20 Jun 2016 11:53:42 -0600 Subject: [PATCH 19/21] updated README and test/README --- README.md | 29 ++++------------ test/README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a0e6abd3..6422249d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -## HPCCloud ## +# HPC Cloud # + +[![codecov.io](https://codecov.io/github/Kitware/HPCCloud/coverage.svg?branch=master)](https://codecov.io/github/Kitware/HPCCloud?branch=master) +[![Build Status](https://travis-ci.org/Kitware/HPCCloud.svg?branch=master)](https://travis-ci.org/Kitware/HPCCloud) ### Goal ### @@ -7,26 +10,7 @@ environment and resources on which you can run those simulations. ## Installation -``` -$ npm install -``` - -After installing the package you will get one executable **HPCCloud** with -the following set of options. - -``` -$ HPCCloud - - Usage: HPCCloud [options] - - Options: - - -h, --help output usage information - -V, --version output the version number - ->>> FIXME <<< - -``` +Observe the instructions for [HPCCloud deploy](https://github.com/Kitware/HPCCloud-deploy); ## Development @@ -37,8 +21,9 @@ $ npm install $ npm start ``` -## Trouble shooting +## Troubleshooting +(With the vm running from HPCCloud-Deploy) ```sh $ vagrant ssh $ sudo -iu hpccloud diff --git a/test/README.md b/test/README.md index 7ec03736..4b1cfce8 100644 --- a/test/README.md +++ b/test/README.md @@ -7,23 +7,98 @@ We test two things: ## Running -- Redux - `nam run test:redux` +- Redux - `npm run test:redux` - Components - `npm run test:components` -- Everything - `nam run test` +- Everything - `npm run test` ## Writing new tests We're using: -- [Karma](https://karma-runner.github.io/0.13/index.html): with [webpack](https://webpack.github.io/) and [istanbul-instrumenter](https://github.com/deepsweet/istanbul-instrumenter-loader) -- [Jasmine](http://jasmine.github.io/2.4/introduction.html) -- [PhantomJS](http://phantomjs.org/) -- [expect](https://github.com/mjackson/expect) -- [redux-actions-assertions](https://github.com/dmitry-zaets/redux-actions-assertions) (Redux tests only, but can be used for tests which have redux elements.) -- [React Test Utils](https://facebook.github.io/react/docs/test-utils.html) (For tests involving React components) +- [Karma](https://karma-runner.github.io/0.13/index.html): with [karma-webpack](https://github.com/webpack/karma-webpack) and [istanbul-instrumenter](https://github.com/deepsweet/istanbul-instrumenter-loader) - test runner, transpiles tests with a webpack extension. +- [Jasmine](http://jasmine.github.io/2.4/introduction.html) - test framework +- [expect](https://github.com/mjackson/expect) - assertion library +- [PhantomJS](http://phantomjs.org/) - headless webkit environment for testing in +- [babel-polyfill](https://github.com/babel/babel/tree/master/packages/babel-polyfill) - PhantomJS has no Promise object, so we supplement it with this. +- [redux-actions-assertions](https://github.com/dmitry-zaets/redux-actions-assertions) - Redux tests only, but can be used for component tests which use redux. +- [React Test Utils](https://facebook.github.io/react/docs/test-utils.html) - For tests involving React components ### Redux Check if there is already a file for the actions or reducers you're testing in `/test/redux`, add a new file if there isn't one already. Action testing can be split up by "simple actions" and "async actions". Simple actions just return an object with `type` and optionally some data payload. Async actions call some backend component. +#### Template + +```js +// import necessary libraries and tools. +import { registerMiddlewares } from 'redux-actions-assertions'; +import { registerAssertions } from 'redux-actions-assertions/expect'; +import thunk from 'redux-thunk'; // useful for testing async actions +import expect from 'expect'; +import complete from '../helpers/complete'; // a done/fail helper for redux-action-assertions + +// what we're testing +import myActions from '../../src/redux/actions/myActions'; +import myReducer from '../../src/redux/reducers/myReducer'; +import client from '../../src/network'; // for setting spies on + +/* global describe it afterEach */ + +registerMiddlewares([thunk]); +registerAssertions(); + +function setSpy(target, method, data) { + expect.spyOn(target, method) + .andReturn(Promise.resolve({ data })); +} + +describe('some action test', () => { + describe('simple actions', () => { + it('should increment a value', (done) => { + const expectedAction = { type: myActions.INCREMENT }; + expect(myActions.increment) + .toDispatchActions(expectedAction, complete(done)); + + const initialState = { value: 0 }; // this can also be exported from the reducers, be sure to clone it if you use it. + const expectedState = { value: 1 }; + expect(myReducer(initialState, expectedAction)) + .toEqual(expectedState); // does a deep equal, if you're dealing with big payloads it can be hard to read error output. + }); + }); + + describe('asyn actions', () => { + afterEach(() => { + expect.restoreSpies(); + }); + + it('should increment a value on the server', (done) => { + const expectedAction = { type: myActions.UPDATE_VALUE, value: 2 }; + setSpy(client, 'serverIncrement', { value: 2 }) + expect(myActions.serverIncrement()) // this action would call `client.serverIncrement` which we're spying on + .toDispatchActions(expectedAction, complete(done)); + }); + }); +}); +``` + ### Components -Writing tests for React components can be a bit finicky. For components with some state or props reliance they may not fully render. Luckily you can manipulate the React components just like you would with plain javascript objects. \ No newline at end of file +Writing tests for React components can be a bit finicky. For components with reliance on state or props they may not fully render. Luckily you can manipulate the React components just like you would with plain javascript objects. + +#### Template + +```js +// import necessary libraries and tools. +import expect from 'expect'; +import React from 'react'; +import TestUtils from 'react/lib/ReactTestUtils'; +import MyComponent from '../../src/panels/sampleComponent'; + +/* global describe it afterEach */ + +describe('some component test', () => { + it('should render increment buttons', (done) => { + const el = TestUtils.renderIntoDocument(); + const buttons = TestUtils.findAllInRenderedTree(el, (component) => component.tagName === 'BUTTON'); + expect(buttons.length).toEqual(1); + }); +}); +``` \ No newline at end of file From efcdcaef0ebe7985d87ffd2fd23fa1d2041f63fd Mon Sep 17 00:00:00 2001 From: TristanWright Date: Mon, 20 Jun 2016 17:28:48 -0600 Subject: [PATCH 20/21] cluster fixes, would not update list on new cluster creation --- src/pages/Preferences/Cluster/index.js | 2 +- src/redux/actions/clusters.js | 4 ++-- src/redux/actions/taskflows.js | 4 ++++ src/redux/reducers/clusters.js | 24 ++++++++++++++++++++++-- test/redux/clusters.js | 24 ++++++++++++++++++------ 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/pages/Preferences/Cluster/index.js b/src/pages/Preferences/Cluster/index.js index 660546a2..e380ec46 100644 --- a/src/pages/Preferences/Cluster/index.js +++ b/src/pages/Preferences/Cluster/index.js @@ -93,7 +93,7 @@ const ClusterPrefs = React.createClass({ clusterHasSimulation(id) { for (let i = 0; i < this.props.taskflows.length; i++) { - if (this.props.taskflows[i].flow && this.props.taskflows[i].flow.meta.cluster._id === id) { + if (this.props.taskflows[i].flow && get(this.props.taskflows[i].flow, 'meta.cluster._id') === id) { return this.props.taskflows[i].simulation; } } diff --git a/src/redux/actions/clusters.js b/src/redux/actions/clusters.js index 2329e6c0..d8bb2239 100644 --- a/src/redux/actions/clusters.js +++ b/src/redux/actions/clusters.js @@ -284,17 +284,17 @@ export function testCluster(index, cluster) { const action = netActions.addNetworkCall('test_cluster', 'Test cluster'); dispatch(pendingNetworkCall(true)); dispatch({ type: TESTING_CLUSTER, index }); - return client.testCluster(cluster._id) + client.testCluster(cluster._id) .then( resp => { dispatch(netActions.successNetworkCall(action.id, resp)); dispatch(pendingNetworkCall(false)); - dispatch(fetchClusters()); }, error => { dispatch(netActions.errorNetworkCall(action.id, error)); dispatch(pendingNetworkCall(false)); }); + return action; }; } diff --git a/src/redux/actions/taskflows.js b/src/redux/actions/taskflows.js index 8ee91c15..56de2cc1 100644 --- a/src/redux/actions/taskflows.js +++ b/src/redux/actions/taskflows.js @@ -351,6 +351,10 @@ client.onEvent((resp) => { break; } case 'cluster': { + if (status === 'created') { + // we need to fetch some new cluster props when this happens + dispatch(clusterActions.fetchCluster(id)); + } dispatch(clusterActions.updateClusterStatus(id, status)); break; } diff --git a/src/redux/reducers/clusters.js b/src/redux/reducers/clusters.js index eb2fa2f0..920a48ed 100644 --- a/src/redux/reducers/clusters.js +++ b/src/redux/reducers/clusters.js @@ -81,9 +81,20 @@ export default function clustersReducer(state = initialState, action) { case Actions.ADD_EXISTING_CLUSTER: { const newCluster = action.cluster; + const list = [].concat(state.list); const mapById = Object.assign({}, state.mapById); mapById[newCluster._id] = newCluster; - return Object.assign({}, state, { mapById }); + if (newCluster.type === 'trad' && list.some((el) => el._id === newCluster._id)) { + for (let i = 0; i < list.length; i++) { + if (list[i]._id === newCluster._id) { + list[i].status = newCluster.status; + break; + } + } + } else if (newCluster.type === 'trad') { + list.push(newCluster); + } + return Object.assign({}, state, { list, mapById }); } case Actions.REMOVE_CLUSTER: { @@ -159,11 +170,20 @@ export default function clustersReducer(state = initialState, action) { } case Actions.UPDATE_CLUSTER_STATUS: { + const list = [].concat(state.list); const mapById = Object.assign({}, state.mapById); const cluster = Object.assign({}, state.mapById[action.id]); cluster.status = action.status; mapById[action.id] = cluster; - return Object.assign({}, state, { mapById }); + if (cluster.type === 'trad') { + for (let i = 0; i < list.length; i++) { + if (list[i]._id === action.id) { + list[i].status = action.status; + break; + } + } + } + return Object.assign({}, state, { list, mapById }); } case Actions.CLUSTER_APPLY_PRESET: { diff --git a/test/redux/clusters.js b/test/redux/clusters.js index 7bbaaa5a..23c6f298 100644 --- a/test/redux/clusters.js +++ b/test/redux/clusters.js @@ -22,7 +22,10 @@ function setSpy(target, method, data) { Object.freeze(initialState); describe('cluster actions', () => { - const cluster = { _id: 'a1', name: 'myCluster', + const cluster = { _id: 'a1', + type: 'trad', + name: 'myCluster', + status: 'unknown', log: [{ entry: 'created...' }, { entry: 'running...' }], }; describe('simple actions', () => { @@ -45,6 +48,7 @@ describe('cluster actions', () => { const newState = deepClone(initialState); newState.mapById[cluster._id] = cluster; + newState.list = [cluster]; expect(clustersReducer(initialState, expectedAction)) .toEqual(newState); }); @@ -159,11 +163,19 @@ describe('cluster actions', () => { // we only test the reducer here it('should update a cluster\'s status', () => { - const thisState = Object.assign({}, initialState); - thisState.mapById = { a1: { status: 'created' } }; - const action = { type: Actions.UPDATE_CLUSTER_STATUS, id: 'a1', status: 'terminated' }; - expect(clustersReducer(thisState, action).mapById.a1.status) - .toEqual('terminated'); + const newStatus = 'terminated'; + const myCluster = deepClone(cluster); + const givenState = deepClone(initialState); + givenState.mapById[myCluster._id] = myCluster; + givenState.list.push(myCluster); + + const expectedState = deepClone(givenState); + expectedState.mapById[myCluster._id].status = newStatus; + expectedState.list[0].status = newStatus; + + const action = { type: Actions.UPDATE_CLUSTER_STATUS, id: cluster._id, status: newStatus }; + expect(clustersReducer(givenState, action)) + .toEqual(expectedState); }); }); From 96b23f9fe7688f31b98459c4f1632f33330c0e8f Mon Sep 17 00:00:00 2001 From: TristanWright Date: Wed, 22 Jun 2016 12:17:56 -0600 Subject: [PATCH 21/21] cluster list and mapById sync --- src/redux/reducers/clusters.js | 7 +++++-- test/redux/clusters.js | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/redux/reducers/clusters.js b/src/redux/reducers/clusters.js index 920a48ed..4ab58b0a 100644 --- a/src/redux/reducers/clusters.js +++ b/src/redux/reducers/clusters.js @@ -84,10 +84,12 @@ export default function clustersReducer(state = initialState, action) { const list = [].concat(state.list); const mapById = Object.assign({}, state.mapById); mapById[newCluster._id] = newCluster; + if (newCluster.type === 'trad' && list.some((el) => el._id === newCluster._id)) { for (let i = 0; i < list.length; i++) { if (list[i]._id === newCluster._id) { - list[i].status = newCluster.status; + list[i] = newCluster; + updateIcon(list); break; } } @@ -178,7 +180,8 @@ export default function clustersReducer(state = initialState, action) { if (cluster.type === 'trad') { for (let i = 0; i < list.length; i++) { if (list[i]._id === action.id) { - list[i].status = action.status; + list[i] = cluster; + updateIcon(list); break; } } diff --git a/test/redux/clusters.js b/test/redux/clusters.js index 23c6f298..6d1f3a2b 100644 --- a/test/redux/clusters.js +++ b/test/redux/clusters.js @@ -2,6 +2,7 @@ import * as Actions from '../../src/redux/actions/clusters'; import clustersReducer, { clusterTemplate, initialState } from '../../src/redux/reducers/clusters'; import client from '../../src/network'; import * as ClusterHelpers from '../../src/network/helpers/clusters'; +import style from 'HPCCloudStyle/PageWithMenu.mcss'; import expect from 'expect'; import thunk from 'redux-thunk'; @@ -26,6 +27,7 @@ describe('cluster actions', () => { type: 'trad', name: 'myCluster', status: 'unknown', + classPrefix: '', log: [{ entry: 'created...' }, { entry: 'running...' }], }; describe('simple actions', () => { @@ -171,7 +173,9 @@ describe('cluster actions', () => { const expectedState = deepClone(givenState); expectedState.mapById[myCluster._id].status = newStatus; + expectedState.mapById[myCluster._id].classPrefix = style.statusTerminatedIcon; expectedState.list[0].status = newStatus; + expectedState.list[0].classPrefix = style.statusTerminatedIcon; const action = { type: Actions.UPDATE_CLUSTER_STATUS, id: cluster._id, status: newStatus }; expect(clustersReducer(givenState, action))