diff --git a/Makefile b/Makefile index 05a7a56e2..44c300e17 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,12 @@ client/dist/scripts/bundle.js: client/app/scripts/* mkdir -p client/dist docker run -ti -v $(shell pwd)/client/app:/home/weave/app \ -v $(shell pwd)/client/dist:/home/weave/dist \ - $(SCOPE_UI_BUILD_IMAGE) + $(SCOPE_UI_BUILD_IMAGE) gulp build + +client-test: client/test/* + docker run -ti -v $(shell pwd)/client/app:/home/weave/app \ + -v $(shell pwd)/client/test:/home/weave/test \ + $(SCOPE_UI_BUILD_IMAGE) npm test $(SCOPE_UI_BUILD_EXPORT): client/Dockerfile client/gulpfile.js client/package.json docker build -t $(SCOPE_UI_BUILD_IMAGE) client diff --git a/circle.yml b/circle.yml index d7995d5b0..b70d62941 100644 --- a/circle.yml +++ b/circle.yml @@ -37,6 +37,7 @@ test: - cd $SRCDIR; make static - cd $SRCDIR; make - cd $SRCDIR; ./bin/test + - cd $SRCDIR; make client-test post: - goveralls -repotoken $COVERALLS_REPO_TOKEN -coverprofile=$SRCDIR/profile.cov -service=circleci || true - cd $SRCDIR; cp coverage.html $CIRCLE_ARTIFACTS diff --git a/client/Dockerfile b/client/Dockerfile index 9ec183c6f..7eeb476ce 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,7 +1,12 @@ -FROM mhart/alpine-node - +FROM debian:latest WORKDIR /home/weave +RUN apt-get update && apt-get install -y curl bzip2 libfreetype6 libfontconfig1 + +# Install nodejs +RUN curl -sL https://deb.nodesource.com/setup | bash - +RUN apt-get install -y nodejs + # build tool RUN npm install -g gulp @@ -11,9 +16,4 @@ RUN npm install ADD gulpfile.js /home/weave/ -# run container via -# -# `docker run -v $GOPATH/src/github.com/weaveworks/scope/client:/app weaveworks/scope-build` -# -# after the container is run, bundled app should be in ./dist/ dir -CMD gulp build +# For instructions on running this container, consult the toplevel Makefile diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index ac82b7984..8f493e189 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -15,7 +15,7 @@ module.exports = { grouping: grouping }); RouterUtils.updateRoute(); - WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology())); + WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl()); }, clickNode: function(nodeId) { @@ -24,7 +24,7 @@ module.exports = { nodeId: nodeId }); RouterUtils.updateRoute(); - WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId()); + WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId()); }, clickTopology: function(topologyId) { @@ -33,7 +33,14 @@ module.exports = { topologyId: topologyId }); RouterUtils.updateRoute(); - WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology())); + WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl()); + }, + + enterNode: function(nodeId) { + AppDispatcher.dispatch({ + type: ActionTypes.ENTER_NODE, + nodeId: nodeId + }); }, hitEsc: function() { @@ -43,6 +50,13 @@ module.exports = { RouterUtils.updateRoute(); }, + leaveNode: function(nodeId) { + AppDispatcher.dispatch({ + type: ActionTypes.LEAVE_NODE, + nodeId: nodeId + }); + }, + receiveNodeDetails: function(details) { AppDispatcher.dispatch({ type: ActionTypes.RECEIVE_NODE_DETAILS, @@ -50,13 +64,20 @@ module.exports = { }); }, + receiveNodesDelta: function(delta) { + AppDispatcher.dispatch({ + type: ActionTypes.RECEIVE_NODES_DELTA, + delta: delta + }); + }, + receiveTopologies: function(topologies) { AppDispatcher.dispatch({ type: ActionTypes.RECEIVE_TOPOLOGIES, topologies: topologies }); - WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology())); - WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId()); + WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl()); + WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId()); }, route: function(state) { @@ -64,8 +85,8 @@ module.exports = { state: state, type: ActionTypes.ROUTE_TOPOLOGY }); - WebapiUtils.getNodesDelta(AppStore.getUrlForTopology(AppStore.getCurrentTopology())); - WebapiUtils.getNodeDetails(AppStore.getUrlForTopology(AppStore.getCurrentTopology()), AppStore.getSelectedNodeId()); + WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl()); + WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId()); } }; diff --git a/client/app/scripts/actions/topology-actions.js b/client/app/scripts/actions/topology-actions.js deleted file mode 100644 index 471c8cfc8..000000000 --- a/client/app/scripts/actions/topology-actions.js +++ /dev/null @@ -1,25 +0,0 @@ -var AppDispatcher = require('../dispatcher/app-dispatcher'); -var ActionTypes = require('../constants/action-types'); - -module.exports = { - enterNode: function(nodeId) { - AppDispatcher.dispatch({ - type: ActionTypes.ENTER_NODE, - nodeId: nodeId - }); - }, - - leaveNode: function(nodeId) { - AppDispatcher.dispatch({ - type: ActionTypes.LEAVE_NODE, - nodeId: nodeId - }); - }, - - receiveNodesDelta: function(delta) { - AppDispatcher.dispatch({ - type: ActionTypes.RECEIVE_NODES_DELTA, - delta: delta - }); - } -}; \ No newline at end of file diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 7f815ecd5..326fd891d 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -8,7 +8,6 @@ var AppStore = require('../stores/app-store'); var Groupings = require('./groupings.js'); var Status = require('./status.js'); var Topologies = require('./topologies.js'); -var TopologyStore = require('../stores/topology-store'); var WebapiUtils = require('../utils/web-api-utils'); var AppActions = require('../actions/app-actions'); var Details = require('./details'); @@ -20,12 +19,12 @@ var ESC_KEY_CODE = 27; function getStateFromStores() { return { - activeTopology: AppStore.getCurrentTopology(), + currentTopology: AppStore.getCurrentTopology(), connectionState: AppStore.getConnectionState(), currentGrouping: AppStore.getCurrentGrouping(), selectedNodeId: AppStore.getSelectedNodeId(), nodeDetails: AppStore.getNodeDetails(), - nodes: TopologyStore.getNodes(), + nodes: AppStore.getNodes(), topologies: AppStore.getTopologies() } } @@ -38,7 +37,6 @@ var App = React.createClass({ }, componentDidMount: function() { - TopologyStore.on(TopologyStore.CHANGE_EVENT, this.onChange); AppStore.on(AppStore.CHANGE_EVENT, this.onChange); window.addEventListener('keyup', this.onKeyPress); @@ -63,13 +61,12 @@ var App = React.createClass({
{showingDetails &&
} + details={this.state.nodeDetails} /> }
- - + +
diff --git a/client/app/scripts/components/details.js b/client/app/scripts/components/details.js index 574e8c3a0..c9092f5a3 100644 --- a/client/app/scripts/components/details.js +++ b/client/app/scripts/components/details.js @@ -8,7 +8,6 @@ var IconButton = mui.IconButton; var AppActions = require('../actions/app-actions'); var NodeDetails = require('./node-details'); -var WebapiUtils = require('../utils/web-api-utils'); var Details = React.createClass({ diff --git a/client/app/scripts/components/groupings.js b/client/app/scripts/components/groupings.js index 97d976e44..d99457143 100644 --- a/client/app/scripts/components/groupings.js +++ b/client/app/scripts/components/groupings.js @@ -8,10 +8,12 @@ var AppStore = require('../stores/app-store'); var GROUPINGS = [{ id: 'none', - iconClass: 'fa fa-th' + iconClass: 'fa fa-th', + needsTopology: false }, { id: 'grouped', - iconClass: 'fa fa-th-large' + iconClass: 'fa fa-th-large', + needsTopology: 'grouped_url' }]; var Groupings = React.createClass({ @@ -21,22 +23,40 @@ var Groupings = React.createClass({ AppActions.clickGrouping(ev.currentTarget.getAttribute('rel')); }, - renderGrouping: function(grouping, active) { - var className = grouping.id === active ? "groupings-item groupings-item-active" : "groupings-item"; + isGroupingSupportedByTopology: function(topology, grouping) { + return !grouping.needsTopology || topology && topology[grouping.needsTopology]; + }, + + getGroupingsSupportedByTopology: function(topology) { + return _.filter(GROUPINGS, _.partial(this.isGroupingSupportedByTopology, topology)); + }, + + renderGrouping: function(grouping, activeGroupingId) { + var className = "groupings-item", + isSupportedByTopology = this.isGroupingSupportedByTopology(this.props.currentTopology, grouping); + + if (grouping.id === activeGroupingId) { + className += " groupings-item-active"; + } else if (!isSupportedByTopology) { + className += " groupings-item-disabled"; + } else { + className += " groupings-item-default"; + } return ( -
+
); }, render: function() { - var activeGrouping = this.props.active; + var activeGrouping = this.props.active, + isGroupingSupported = _.size(this.getGroupingsSupportedByTopology(this.props.currentTopology)) > 1; return (
- {GROUPINGS.map(function(grouping) { + {isGroupingSupported && GROUPINGS.map(function(grouping) { return this.renderGrouping(grouping, activeGrouping); }, this)}
diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js index 8c93e8c44..b5347e406 100644 --- a/client/app/scripts/components/topologies.js +++ b/client/app/scripts/components/topologies.js @@ -14,8 +14,9 @@ var Topologies = React.createClass({ }, renderTopology: function(topology, active) { - var className = AppStore.isUrlForTopology(topology.url, active) ? "topologies-item topologies-item-active" : "topologies-item", - topologyId = AppStore.getTopologyForUrl(topology.url), + var isActive = topology.name === this.props.currentTopology.name, + className = isActive ? "topologies-item topologies-item-active" : "topologies-item", + topologyId = AppStore.getTopologyIdForUrl(topology.url), title = ['Topology: ' + topology.name, 'Nodes: ' + topology.stats.node_count, 'Connections: ' + topology.stats.node_count].join('\n'); @@ -32,15 +33,14 @@ var Topologies = React.createClass({ }, render: function() { - var activeTopologyId = this.props.active, - topologies = _.sortBy(this.props.topologies, function(topology) { + var topologies = _.sortBy(this.props.topologies, function(topology) { return topology.name; }); return (
- {topologies.map(function(topology) { - return this.renderTopology(topology, activeTopologyId); + {this.props.currentTopology && topologies.map(function(topology) { + return this.renderTopology(topology); }, this)}
); diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js new file mode 100644 index 000000000..4c7159d0a --- /dev/null +++ b/client/app/scripts/stores/__tests__/app-store-test.js @@ -0,0 +1,61 @@ + +describe('AppStore', function() { + + var ActionTypes = require('../../constants/action-types'); + var AppStore, registeredCallback; + + // actions + + var ClickTopologyAction = { + type: ActionTypes.CLICK_TOPOLOGY, + topologyId: 'topo1' + }; + + var ClickGroupingAction = { + type: ActionTypes.CLICK_GROUPING, + grouping: 'grouped' + }; + + var ReceiveTopologiesAction = { + type: ActionTypes.RECEIVE_TOPOLOGIES, + topologies: [{ + url: '/topo1', + grouped_url: '/topo1grouped', + name: 'Topo1' + }] + }; + + beforeEach(function() { + AppStore = require('../app-store'); + registeredCallback = AppStore.registeredCallback; + }); + + // topology tests + + it('init with no topologies', function() { + var topos = AppStore.getTopologies(); + expect(topos.length).toBe(0); + expect(AppStore.getCurrentTopology()).toBeUndefined(); + }); + + it('get current topology', function() { + registeredCallback(ClickTopologyAction); + registeredCallback(ReceiveTopologiesAction); + + expect(AppStore.getTopologies().length).toBe(1); + expect(AppStore.getCurrentTopology().name).toBe('Topo1'); + expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1'); + }); + + it('get grouped topology', function() { + registeredCallback(ClickTopologyAction); + registeredCallback(ReceiveTopologiesAction); + registeredCallback(ClickGroupingAction); + + expect(AppStore.getTopologies().length).toBe(1); + expect(AppStore.getCurrentTopology().name).toBe('Topo1'); + expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1grouped'); + }); + + +}); \ No newline at end of file diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index f4cc8d18d..e670b6170 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -5,15 +5,15 @@ var assign = require('object-assign'); var AppDispatcher = require('../dispatcher/app-dispatcher'); var ActionTypes = require('../constants/action-types'); -var TopologyStore = require('./topology-store'); -// var topologies = require('../constants/topologies'); // Initial values var connectionState = 'disconnected'; var currentGrouping = 'none'; -var currentTopology = 'applications'; +var currentTopologyId = 'applications'; +var mouseOverNode = null; +var nodes = {}; var nodeDetails = null; var selectedNodeId = null; var topologies = []; @@ -26,8 +26,8 @@ var AppStore = assign({}, EventEmitter.prototype, { getAppState: function() { return { - currentTopology: this.getCurrentTopology(), - currentGrouping: this.getCurrentGrouping(), + topologyId: currentTopologyId, + grouping: this.getCurrentGrouping(), selectedNodeId: this.getSelectedNodeId() }; }, @@ -37,7 +37,17 @@ var AppStore = assign({}, EventEmitter.prototype, { }, getCurrentTopology: function() { - return currentTopology; + return _.find(topologies, function(topology) { + return isUrlForTopologyId(topology.url, currentTopologyId); + }); + }, + + getCurrentTopologyUrl: function() { + var topology = this.getCurrentTopology(); + + if (topology) { + return topology.grouped_url && currentGrouping == 'grouped' ? topology.grouped_url : topology.url; + } }, getCurrentGrouping: function() { @@ -48,6 +58,10 @@ var AppStore = assign({}, EventEmitter.prototype, { return nodeDetails; }, + getNodes: function() { + return nodes; + }, + getSelectedNodeId: function() { return selectedNodeId; }, @@ -56,40 +70,35 @@ var AppStore = assign({}, EventEmitter.prototype, { return topologies; }, - getTopologyForUrl: function(url) { + getTopologyIdForUrl: function(url) { return url.split('/').pop(); - }, - - getUrlForTopology: function(topologyId) { - var topology = _.find(topologies, function(topology) { - return this.isUrlForTopology(topology.url, topologyId); - }, this); - - if (topology) { - return topology.grouped_url && currentGrouping == 'grouped' ? topology.grouped_url : topology.url; - } - }, - - isUrlForTopology: function(url, topologyId) { - return _.endsWith(url, topologyId); } - }); +// Helpers + +function isUrlForTopologyId(url, topologyId) { + return _.endsWith(url, topologyId); +} + + // Store Dispatch Hooks -AppStore.dispatchToken = AppDispatcher.register(function(payload) { +AppStore.registeredCallback = function(payload) { switch (payload.type) { + case ActionTypes.CLICK_CLOSE_DETAILS: selectedNodeId = null; AppStore.emit(AppStore.CHANGE_EVENT); break; case ActionTypes.CLICK_GROUPING: - currentGrouping = payload.grouping; - AppDispatcher.waitFor([TopologyStore.dispatchToken]); - AppStore.emit(AppStore.CHANGE_EVENT); + if (payload.grouping !== currentGrouping) { + currentGrouping = payload.grouping; + nodes = {}; + AppStore.emit(AppStore.CHANGE_EVENT); + } break; case ActionTypes.CLICK_NODE: @@ -98,8 +107,15 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) { break; case ActionTypes.CLICK_TOPOLOGY: - currentTopology = payload.topologyId; - AppDispatcher.waitFor([TopologyStore.dispatchToken]); + if (payload.topologyId !== currentTopologyId) { + currentTopologyId = payload.topologyId; + nodes = {}; + } + AppStore.emit(AppStore.CHANGE_EVENT); + break; + + case ActionTypes.ENTER_NODE: + mouseOverNode = payload.nodeId; AppStore.emit(AppStore.CHANGE_EVENT); break; @@ -109,14 +125,43 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) { AppStore.emit(AppStore.CHANGE_EVENT); break; + case ActionTypes.LEAVE_NODE: + mouseOverNode = null; + AppStore.emit(AppStore.CHANGE_EVENT); + break; + case ActionTypes.RECEIVE_NODE_DETAILS: nodeDetails = payload.details; AppStore.emit(AppStore.CHANGE_EVENT); break; case ActionTypes.RECEIVE_NODES_DELTA: + console.log('RECEIVE_NODES_DELTA', + 'remove', _.size(payload.delta.remove), + 'update', _.size(payload.delta.update), + 'add', _.size(payload.delta.add)); + connectionState = "connected"; - AppDispatcher.waitFor([TopologyStore.dispatchToken]); + + // nodes that no longer exist + _.each(payload.delta.remove, function(nodeId) { + // in case node disappears before mouseleave event + if (mouseOverNode === nodeId) { + mouseOverNode = null; + } + delete nodes[nodeId]; + }); + + // update existing nodes + _.each(payload.delta.update, function(node) { + nodes[node.id] = node; + }); + + // add new nodes + _.each(payload.delta.add, function(node) { + nodes[node.id] = node; + }); + AppStore.emit(AppStore.CHANGE_EVENT); break; @@ -126,10 +171,10 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) { break; case ActionTypes.ROUTE_TOPOLOGY: - currentTopology = payload.state.currentTopology; - currentGrouping = payload.state.currentGrouping; + nodes = {}; + currentTopologyId = payload.state.topologyId; + currentGrouping = payload.state.grouping; selectedNodeId = payload.state.selectedNodeId; - AppDispatcher.waitFor([TopologyStore.dispatchToken]); AppStore.emit(AppStore.CHANGE_EVENT); break; @@ -137,6 +182,8 @@ AppStore.dispatchToken = AppDispatcher.register(function(payload) { break; } -}); +}; + +AppStore.dispatchToken = AppDispatcher.register(AppStore.registeredCallback); module.exports = AppStore; diff --git a/client/app/scripts/stores/topology-store.js b/client/app/scripts/stores/topology-store.js deleted file mode 100644 index e3422d986..000000000 --- a/client/app/scripts/stores/topology-store.js +++ /dev/null @@ -1,86 +0,0 @@ -var EventEmitter = require('events').EventEmitter; -var _ = require('lodash'); -var assign = require('object-assign'); - -var AppDispatcher = require('../dispatcher/app-dispatcher'); -var ActionTypes = require('../constants/action-types'); - - - -// Initial values - -var nodes = {}; -var mouseOverNode = null; - -// Store API - -var TopologyStore = assign({}, EventEmitter.prototype, { - - CHANGE_EVENT: 'change', - - getNodes: function() { - return nodes; - } - -}); - - -// Store Dispatch Hooks - -TopologyStore.dispatchToken = AppDispatcher.register(function(payload) { - switch (payload.type) { - case ActionTypes.CLICK_GROUPING: - nodes = {}; - TopologyStore.emit(TopologyStore.CHANGE_EVENT); - break; - - case ActionTypes.CLICK_TOPOLOGY: - nodes = {}; - TopologyStore.emit(TopologyStore.CHANGE_EVENT); - break; - - case ActionTypes.ENTER_NODE: - mouseOverNode = payload.nodeId; - TopologyStore.emit(TopologyStore.CHANGE_EVENT); - break; - - case ActionTypes.LEAVE_NODE: - mouseOverNode = null; - TopologyStore.emit(TopologyStore.CHANGE_EVENT); - break; - - case ActionTypes.RECEIVE_NODES_DELTA: - // nodes that no longer exist - _.each(payload.delta.remove, function(nodeId) { - // in case node disappears before mouseleave event - if (mouseOverNode === nodeId) { - mouseOverNode = null; - } - delete nodes[nodeId]; - }); - - // update existing nodes - _.each(payload.delta.update, function(node) { - nodes[node.id] = node; - }); - - // add new nodes - _.each(payload.delta.add, function(node) { - nodes[node.id] = node; - }); - - TopologyStore.emit(TopologyStore.CHANGE_EVENT); - break; - - case ActionTypes.ROUTE_TOPOLOGY: - nodes = {}; - TopologyStore.emit(TopologyStore.CHANGE_EVENT); - break; - - default: - break; - - } -}); - -module.exports = TopologyStore; \ No newline at end of file diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 64ef97cff..cd391ffdb 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -1,8 +1,6 @@ var reqwest = require('reqwest'); -var TopologyActions = require('../actions/topology-actions'); var AppActions = require('../actions/app-actions'); -var AppStore = require('../stores/app-store'); var WS_URL = window.WS_URL || 'ws://' + location.host; @@ -33,7 +31,7 @@ function createWebsocket(topologyUrl) { socket.onmessage = function(event) { var msg = JSON.parse(event.data); if (msg.add || msg.remove || msg.update) { - TopologyActions.receiveNodesDelta(msg); + AppActions.receiveNodesDelta(msg); } }; @@ -48,9 +46,9 @@ function getTopologies() { }); } -function getNodeDetails(topology, nodeId) { - if (nodeId) { - var url = [AppStore.getUrlForTopology(topology), nodeId].join('/'); +function getNodeDetails(topologyUrl, nodeId) { + if (topologyUrl && nodeId) { + var url = [topologyUrl, nodeId].join('/'); reqwest(url, function(res) { AppActions.receiveNodeDetails(res.node); }); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index f508e89ec..9f96db31f 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -101,7 +101,13 @@ body { display: inline-block; color: @text-tertiary-color; - &-active, &:hover { + &-disabled { + color: @text-tertiary-color; + cursor: default; + } + + &-default:hover, + &-active { color: @text-color; } } diff --git a/client/package.json b/client/package.json index cb3b71d28..7b14a29f5 100644 --- a/client/package.json +++ b/client/package.json @@ -38,15 +38,25 @@ "gulp-size": "^1.2.1", "gulp-sourcemaps": "^1.5.2", "gulp-uglify": "^1.2.0", + "gulp-useref": "^1.1.1", "gulp-util": "^3.0.4", + "jasmine-core": "^2.3.4", "jshint-stylish": "^1.0.2", + "karma": "^0.12.32", + "karma-browserify": "^4.2.1", + "karma-cli": "0.0.4", + "karma-jasmine": "^0.3.5", + "karma-phantomjs-launcher": "^0.1.4", + "opn": "^1.0.1", "proxy-middleware": "^0.11.1", + "react-tools": "^0.13.3", "reactify": "^1.1.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, "scripts": { - "start": "gulp" + "start": "gulp", + "test": "karma start test/karma.conf.js --single-run" }, "engines": { "node": ">=0.10.0" diff --git a/client/test/README.md b/client/test/README.md new file mode 100644 index 000000000..f8419bcf9 --- /dev/null +++ b/client/test/README.md @@ -0,0 +1,9 @@ +# Testing + +Scope unit testing is done unsing Karma/Jasmine. (Jest was too big and slow.) + +To run tests, do `npm test` in the toplevel directory. + +The tests are placed in `__tests__` directories, relative to what they are testing. + +For more info see [Testing Flux Apps with Karma](http://kentor.me/posts/testing-react-and-flux-applications-with-karma-and-webpack/) diff --git a/client/test/bower.json b/client/test/bower.json deleted file mode 100644 index 26de14fc4..000000000 --- a/client/test/bower.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "gulp-webapp", - "private": true, - "dependencies": { - "chai": "~1.8.0", - "mocha": "~1.14.0" - }, - "devDependencies": {} -} diff --git a/client/test/index.html b/client/test/index.html deleted file mode 100644 index d10cee026..000000000 --- a/client/test/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Mocha Spec Runner - - - -
- - - - - - - - - - - - - diff --git a/client/test/karma.conf.js b/client/test/karma.conf.js new file mode 100644 index 000000000..1d50e1f7b --- /dev/null +++ b/client/test/karma.conf.js @@ -0,0 +1,23 @@ +module.exports = function(config) { + config.set({ + browsers: [ + 'PhantomJS' + ], + files: [ + '../app/**/__tests__/*.js' + ], + frameworks: [ + 'jasmine', 'browserify' + ], + preprocessors: { + '../app/**/__tests__/*.js': ['browserify'] + }, + browserify: { + debug: true, + transform: ['reactify'] + }, + reporters: [ + 'dots' + ] + }); +}; diff --git a/client/test/preprocessor.js b/client/test/preprocessor.js new file mode 100644 index 000000000..4c91ab16b --- /dev/null +++ b/client/test/preprocessor.js @@ -0,0 +1,7 @@ +var ReactTools = require('react-tools'); + +module.exports = { + process: function(src) { + return ReactTools.transform(src); + } +}; diff --git a/client/test/spec/test.js b/client/test/spec/test.js deleted file mode 100644 index acbc11a4c..000000000 --- a/client/test/spec/test.js +++ /dev/null @@ -1,13 +0,0 @@ -/* global describe, it */ - -(function () { - 'use strict'; - - describe('Give it some context', function () { - describe('maybe a bit more context here', function () { - it('should run here few assertions', function () { - - }); - }); - }); -})();