diff --git a/.babelrc b/.babelrc
index 108ebfbdf5..6c27f647bc 100644
--- a/.babelrc
+++ b/.babelrc
@@ -28,6 +28,13 @@
"plugins": [
["transform-react-jsx"]
]
+ },
+ "test": {
+ "plugins": [
+ ["babel-plugin-rewire"],
+ ["transform-react-jsx"],
+ ["typecheck"]
+ ]
}
}
}
diff --git a/app/pages/lab/apply-for-beta-form.jsx b/app/pages/lab/apply-for-beta-form.jsx
new file mode 100644
index 0000000000..3babd7b2fd
--- /dev/null
+++ b/app/pages/lab/apply-for-beta-form.jsx
@@ -0,0 +1,243 @@
+import React from 'react';
+import uniq from 'lodash.uniq';
+import apiClient from 'panoptes-client/lib/api-client';
+
+const MINIMUM_SUBJECT_COUNT = 100;
+const REQUIRED_PAGES = ['Research', 'FAQ'];
+
+class ApplyForBetaForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.attemptApplyForBeta = this.attemptApplyForBeta.bind(this);
+ this.canApplyForReview = this.canApplyForReview.bind(this);
+ this.createCheckbox = this.createCheckbox.bind(this);
+ this.projectHasActiveWorkflows = this.projectHasActiveWorkflows.bind(this);
+ this.projectHasMinimumActiveSubjects = this.projectHasMinimumActiveSubjects.bind(this);
+ this.projectHasRequiredContent = this.projectHasRequiredContent.bind(this);
+ this.projectIsLive = this.projectIsLive.bind(this);
+ this.projectIsPublic = this.projectIsPublic.bind(this);
+ this.renderValidationErrors = this.renderValidationErrors.bind(this);
+ this.testAsyncValidations = this.testAsyncValidations.bind(this);
+ this.toggleValidation = this.toggleValidation.bind(this);
+ this.updateValidationsFromProps = this.updateValidationsFromProps.bind(this);
+
+ this.state = {
+ validations: {
+ projectIsPublic: this.projectIsPublic(props.project),
+ projectIsLive: this.projectIsLive(props.project),
+ projectHasActiveWorkflows: this.projectHasActiveWorkflows(props.workflows),
+ labPolicyReviewed: false,
+ bestPracticesReviewed: false,
+ feedbackReviewed: false,
+ },
+ validationErrors: [],
+ doingAsyncValidation: false,
+ };
+ }
+
+ attemptApplyForBeta() {
+ this.testAsyncValidations()
+ .then(() => {
+ this.props.applyFn();
+ })
+ .catch(errors => {
+ this.setState({
+ validationErrors: errors,
+ });
+ });
+ }
+
+ projectHasMinimumActiveSubjects(workflows) {
+ const activeWorkflows = workflows.filter(workflow => workflow.active);
+ const uniqueSetIDs = uniq(activeWorkflows.map(workflow => workflow.links.subject_sets));
+ // Second parameter is an empty object to prevent request caching.
+ return apiClient.type('subject_sets', {})
+ .get(uniqueSetIDs)
+ .then(sets => {
+ const subjectCount = sets.reduce((count, set) => count + set.set_member_subjects_count, 0);
+ return (subjectCount >= MINIMUM_SUBJECT_COUNT) ? true : `The project only has ${subjectCount} of ${MINIMUM_SUBJECT_COUNT} required subjects`;
+ });
+ }
+
+ projectHasRequiredContent(project) {
+ // Second parameter is an empty object to prevent request caching.
+ return apiClient.type('projects')
+ .get(project.id)
+ .get('pages', {})
+ .then(projectPages => {
+ const missingPages = REQUIRED_PAGES.reduce((accumulator, requiredPage) => {
+ const pagePresent = projectPages.find(page => requiredPage === page.title);
+ if (!pagePresent || (pagePresent.content === null || pagePresent.content === ''))
+ accumulator.push(requiredPage);
+ return accumulator;
+ }, []);
+ return (missingPages.length === 0) ? true : 'The following pages are missing content: ' + missingPages.join(', ');
+ });
+ }
+
+ testAsyncValidations() {
+ // Resolves to true if everything passes, else rejects with an array of
+ // error messages.
+ this.setState({ doingAsyncValidation: true });
+ return Promise.all([
+ this.projectHasMinimumActiveSubjects(this.props.workflows),
+ this.projectHasRequiredContent(this.props.project),
+ ])
+ .catch(error => console.error('Error requesting project data', error))
+ .then(results => {
+ this.setState({ doingAsyncValidation: false });
+ // if (results.every(result => typeof result === 'boolean' && result === true)) {
+ return true;
+ // }
+ const errors = results.filter(result => typeof result !== 'boolean');
+ return Promise.reject(errors);
+ })
+ }
+
+ projectIsPublic(project) {
+ return project.private === false;
+ }
+
+ projectIsLive(project) {
+ return project.live === true;
+ }
+
+ projectHasActiveWorkflows(workflows) {
+ return workflows.some(workflow => workflow.active);
+ }
+
+ toggleValidation(validationName, event) {
+ const validations = Object.assign({}, this.state.validations);
+ validations[validationName] = event.target.checked;
+ this.setState({ validations });
+ }
+
+ canApplyForReview() {
+ const { validations } = this.state;
+ const values = Object.keys(validations)
+ .map(key => validations[key]);
+ return values.every(value => value === true);
+ }
+
+ createCheckbox(validationName, content, disabled = false) {
+ // If it's a non-user controlled checkbox, we don't want to trigger anything
+ // on change, so we use Function.prototype as a noop.
+ const changeFn = (disabled) ? Function.prototype : this.toggleValidation.bind(this, validationName);
+ return (
+
+
+ {content}
+
+ );
+ }
+
+ componentWillUpdate(nextProps) {
+ this.updateValidationsFromProps(nextProps);
+ }
+
+ updateValidationsFromProps(props) {
+ // We need to do a props comparison, otherwise we get a loop where props
+ // update state -> updates state repeatedly.
+ //
+ // Unfortunately, we have to do it by comparing the new props against the
+ // current state, instead of using shouldComponentUpdate. This is because
+ // the project prop passed down is mutable, which breaks the props/nextProps
+ // comparison used by shouldComponentUpdate.
+ const validations = Object.assign({}, this.state.validations);
+
+ const newValues = {
+ projectIsPublic: this.projectIsPublic(props.project),
+ projectIsLive: this.projectIsLive(props.project),
+ projectHasActiveWorkflows: this.projectHasActiveWorkflows(props.workflows),
+ };
+
+ for (let key in newValues) {
+ if (validations[key] !== newValues[key])
+ validations[key] = newValues[key];
+ }
+
+ if (!shallowCompare(validations, this.state.validations))
+ this.setState({ validations });
+ }
+
+ renderValidationErrors(errors) {
+ if (errors.length) {
+ return (
+
+
The following errors need to be fixed:
+
+ {errors.map(error => {error} )}
+
+
+ );
+ }
+ return null;
+ }
+
+ render() {
+ const applyButtonDisabled = !this.canApplyForReview() ||
+ this.state.doingAsyncValidation;
+
+ return (
+
+
+ {this.createCheckbox('projectIsPublic',
Project is public , true)}
+
+ {this.createCheckbox('projectIsLive',
Project is live , true)}
+
+ {this.createCheckbox('projectHasActiveWorkflows',
Project has at least one active workflow , true)}
+
+ {this.createCheckbox('labPolicyReviewed',
I have reviewed the policies )}
+
+ {this.createCheckbox('bestPracticesReviewed',
I have reviewed the best practices )}
+
+ {this.createCheckbox('feedbackReviewed',
I have reviewed the sample project review feedback form )}
+
+
To be eligible for beta review, projects also require:
+
+ at least {MINIMUM_SUBJECT_COUNT} subjects in active workflows
+ content on the Research and FAQ pages in the About page
+
+
These will be checked when you click "Apply for review".
+
+
+ Apply for review
+
+
+ {this.renderValidationErrors(this.state.validationErrors)}
+
+
+ );
+
+ }
+}
+
+ApplyForBetaForm.defaultProps = {
+ project: {},
+ workflows: [],
+}
+
+export default ApplyForBetaForm;
+
+// Helper function for comparing objects
+const shallowCompare = (a, b) => {
+ for (let key in a) {
+ if (!(key in b) || a[key] !== b[key])
+ return false;
+ }
+ for (let key in b) {
+ if(!(key in a) || a[key] !== b[key])
+ return false;
+ }
+ return true;
+}
diff --git a/app/pages/lab/apply-for-beta-form.spec.js b/app/pages/lab/apply-for-beta-form.spec.js
new file mode 100644
index 0000000000..3fa3654d54
--- /dev/null
+++ b/app/pages/lab/apply-for-beta-form.spec.js
@@ -0,0 +1,362 @@
+import React from 'react';
+import assert from 'assert';
+import { shallow, mount } from 'enzyme';
+import sinon from 'sinon';
+import ApplyForBetaForm from './apply-for-beta-form';
+
+const MINIMUM_SUBJECT_COUNT = 100;
+const REQUIRED_PAGES = ['Research', 'FAQ'];
+
+const createProjectProp = (properties) => {
+ return Object.assign({}, {
+ id: 1234,
+ private: false,
+ live: true,
+ }, properties);
+};
+
+const assertLabelAndCheckboxExist = function(label, checkbox) {
+ assert.ok(label.length, 'Label exists');
+ assert.ok(checkbox.length, 'Checkbox exists');
+};
+
+const assertCheckboxDisabled = function(checkbox, disabled = true) {
+ const message = `Checkbox is ${disabled ? 'disabled' : 'enabled'}`;
+ assert.ok(checkbox.prop('disabled') === disabled, message);
+};
+
+const assertCheckboxChecked = function(checkbox, checked = true) {
+ const message = `Checkbox is ${checked ? 'checked' : 'unchecked'}`;
+ assert.ok(checkbox.prop('checked') === checked, message);
+};
+
+describe('ApplyForBeta component:', function() {
+
+ let project = {};
+ let workflows = [];
+ let applyFn = Function.prototype;
+ let wrapper = Function.prototype;
+ let label = Function.prototype;
+ let checkbox = Function.prototype;
+ let mockPages = [];
+ let mockSubjectSets = [];
+
+ const testSetup = function() {
+ project = createProjectProp();
+ workflows = [1, 2, 3].map(i => ({
+ id: i.toString(),
+ active: false,
+ links: {
+ subject_sets: [(i + 3).toString()],
+ },
+ }));
+ applyFn = sinon.spy();
+ mockPages = [];
+ mockSubjectSets = [
+ { id: "0", set_member_subjects_count: 98 },
+ ];
+ };
+
+ ApplyForBetaForm.__Rewire__('apiClient', {
+ type: (type) => ({
+ get: () => {
+ let result = null;
+ const resolver = (value) => new Promise(resolve => resolve(value));
+ if (type === 'projects') {
+ result = {
+ get: () => resolver(mockPages),
+ };
+ } else if (type === 'subject_sets') {
+ result = resolver(mockSubjectSets);
+ }
+ return result;
+ },
+ }),
+ });
+
+ it('should render without crashing', function() {
+ testSetup();
+ shallow( );
+ });
+
+ describe('checkbox validation:', function() {
+
+ const setWrappers = function(labelText) {
+ wrapper = shallow( );
+ label = wrapper.findWhere(function(node) {
+ return node.type() === 'label' && node.text() === labelText;
+ }).first();
+ checkbox = label.find('input[type="checkbox"]').first();
+ }
+
+ describe('disabled checkboxes:', function() {
+ describe('public status checkbox:', function() {
+ const setPublicStatusWrappers = function() {
+ setWrappers('Project is public');
+ }
+
+ beforeEach(testSetup);
+
+ it('should exist', function() {
+ setPublicStatusWrappers();
+ assertLabelAndCheckboxExist(label, checkbox);
+ });
+
+ it('should be disabled', function() {
+ setPublicStatusWrappers();
+ assertCheckboxDisabled(checkbox);
+ });
+
+ it('should be unchecked if the project is private', function() {
+ project.private = true;
+ setPublicStatusWrappers();
+ assertCheckboxChecked(checkbox, false);
+ });
+
+ it('should be checked if the project is public', function() {
+ setPublicStatusWrappers();
+ assertCheckboxChecked(checkbox);
+ });
+ });
+
+ describe('live status checkbox:', function() {
+ const setLiveStatusWrappers = function() {
+ setWrappers('Project is live');
+ }
+
+ beforeEach(testSetup);
+
+ it('should exist', function() {
+ setLiveStatusWrappers();
+ assertLabelAndCheckboxExist(label, checkbox);
+ });
+
+ it('should be disabled', function() {
+ setLiveStatusWrappers();
+ assertCheckboxDisabled(checkbox);
+ });
+
+ it('should be unchecked if the project is in development', function() {
+ project.live = false;
+ setLiveStatusWrappers();
+ assertCheckboxChecked(checkbox, false);
+ });
+
+ it('should be checked if the project is live', function() {
+ setLiveStatusWrappers();
+ assertCheckboxChecked(checkbox);
+ });
+ });
+
+ describe('active workflow checkbox:', function() {
+ const setActiveWorkflowStatusWrappers = function() {
+ setWrappers('Project has at least one active workflow');
+ }
+
+ beforeEach(testSetup);
+
+ it('should exist', function() {
+ setActiveWorkflowStatusWrappers();
+ assertLabelAndCheckboxExist(label, checkbox);
+ });
+
+ it('should be disabled', function() {
+ setActiveWorkflowStatusWrappers();
+ assertCheckboxDisabled(checkbox);
+ });
+
+ it('should be unchecked if the project has no active workflows', function() {
+ setActiveWorkflowStatusWrappers();
+ assertCheckboxChecked(checkbox, false);
+ });
+
+ it('should be checked if the project has at least one active workflow', function() {
+ workflows[0].active = true;
+ setActiveWorkflowStatusWrappers();
+ assertCheckboxChecked(checkbox);
+ });
+ });
+ });
+
+ describe('enabled checkboxes:', function() {
+ describe('lab policies checkbox:', function() {
+ const setLabPoliciesWrappers = function() {
+ setWrappers('I have reviewed the policies');
+ };
+
+ beforeEach(testSetup);
+
+ it('should exist', function() {
+ setLabPoliciesWrappers();
+ assertLabelAndCheckboxExist(label, checkbox);
+ });
+
+ it('should be enabled', function() {
+ setLabPoliciesWrappers();
+ assertCheckboxDisabled(checkbox, false);
+ });
+ });
+
+ describe('best practices checkbox:', function() {
+ const setBestPracticesWrappers = function() {
+ setWrappers('I have reviewed the best practices');
+ };
+
+ beforeEach(testSetup);
+
+ it('should exist', function() {
+ setBestPracticesWrappers();
+ assertLabelAndCheckboxExist(label, checkbox);
+ });
+
+ it('should be enabled', function() {
+ setBestPracticesWrappers();
+ assertCheckboxDisabled(checkbox, false);
+ });
+ });
+
+ describe('feedback form checkbox:', function() {
+ const setFeedbackFormWrappers = function() {
+ setWrappers('I have reviewed the sample project review feedback form');
+ };
+
+ beforeEach(testSetup);
+
+ it('should exist', function() {
+ setFeedbackFormWrappers();
+ assertLabelAndCheckboxExist(label, checkbox);
+ });
+
+ it('should be enabled', function() {
+ setFeedbackFormWrappers();
+ assertCheckboxDisabled(checkbox, false);
+ });
+ });
+ });
+ });
+
+ describe('async validation:', function() {
+ const testForErrorMessage = (regex, result, condition, done) => {
+ workflows[0].active = true;
+ wrapper = mount( );
+ wrapper.find('input[type="checkbox"]').forEach(checkbox => {
+ checkbox.simulate('change', { target: { checked: true }});
+ });
+ wrapper.find('button.standard-button').first().simulate('click');
+ setTimeout(() => {
+ const errorMessages = wrapper.find('ul.form-help').at(1).text();
+ const containsError = regex.test(errorMessages);
+ assert.ok(containsError === result, condition);
+ done();
+ }, 100)
+ };
+
+ describe('content page checks:', function () {
+ REQUIRED_PAGES.map(function (pageTitle) {
+ beforeEach(testSetup);
+
+ const pattern = 'The following pages are missing content:(.+?)' + pageTitle;
+ const regex = new RegExp(pattern);
+
+ it(`should show an error if ${pageTitle} doesn't exist`, function(done) {
+ testForErrorMessage(regex, true, `${pageTitle} page doesn't exist`, done);
+ });
+
+ it(`should show an error if ${pageTitle} doesn't have any content`, function(done) {
+ mockPages.push({
+ title: pageTitle,
+ content: '',
+ });
+ testForErrorMessage(regex, true, `${pageTitle} page doesn't contain any content`, done);
+ });
+
+ it(`shouldn't show an error if ${pageTitle} exists and has content`, function(done) {
+ mockPages.push({
+ title: pageTitle,
+ content: 'foobar',
+ });
+ testForErrorMessage(regex, false, `${pageTitle} page doesn't contain any content`, done);
+ });
+ });
+ });
+
+ describe('subject set checks:', function () {
+ beforeEach(testSetup);
+
+ const pattern = 'The project only has (\\d+?) of ' + MINIMUM_SUBJECT_COUNT + ' required subjects';
+ const regex = new RegExp(pattern);
+
+ it(`should show an error if there are less than ${MINIMUM_SUBJECT_COUNT} subjects`, function(done) {
+ testForErrorMessage(regex, true, `Project contains less than ${MINIMUM_SUBJECT_COUNT} subjects`, done);
+ });
+
+ it(`shouldn't show an error if there are ${MINIMUM_SUBJECT_COUNT} subjects`, function(done) {
+ mockSubjectSets.push({
+ id: "1",
+ set_member_subjects_count: 98,
+ });
+ testForErrorMessage(regex, false, `Project contains ${MINIMUM_SUBJECT_COUNT} subjects`, done);
+ });
+
+ it(`shouldn't show an error if there are more than ${MINIMUM_SUBJECT_COUNT} subjects`, function(done) {
+ mockSubjectSets.push({
+ id: "1",
+ set_member_subjects_count: 98,
+ }, {
+ id: "2",
+ set_member_subjects_count: 1,
+ });
+ testForErrorMessage(regex, false, `Project contains ${MINIMUM_SUBJECT_COUNT} subjects`, done);
+ });
+ });
+ });
+
+ describe('apply button behaviour:', function() {
+ beforeEach(testSetup);
+
+ it('should exist', function() {
+ wrapper = shallow( );
+ const button = wrapper.find('button.standard-button').first();
+ assert(button.length > 0, 'Button exists');
+ });
+
+ it('should be disabled unless all checkboxes are checked', function() {
+ workflows[0].active = true;
+ wrapper = mount( );
+ const checkboxes = wrapper.find('input[type="checkbox"]');
+ checkboxes.forEach(function(checkbox, index) {
+ if (index < (checkboxes.length - 1))
+ checkbox.simulate('change', { target: { checked: true }});
+ })
+ const button = wrapper.find('button.standard-button').first();
+ assert(button.prop('disabled') === true, 'Button is disabled');
+ });
+
+ it('should call the applyFn prop on click if all validations pass', function(done) {
+ workflows[0].active = true;
+ mockPages.push({
+ title: 'Research',
+ content: 'foobar',
+ }, {
+ title: 'FAQ',
+ content: 'foobar',
+ });
+ mockSubjectSets.push({
+ id: "1",
+ set_member_subjects_count: 2,
+ });
+
+ wrapper = mount( );
+ wrapper.find('input[type="checkbox"]').forEach(function(checkbox) {
+ checkbox.simulate('change', { target: { checked: true }});
+ })
+ wrapper.find('button.standard-button').first().simulate('click');
+
+ setTimeout(function() {
+ assert.ok(applyFn.calledOnce, 'applyFn has been called');
+ done();
+ }, 100);
+ });
+ });
+
+});
diff --git a/app/pages/lab/visibility.cjsx b/app/pages/lab/visibility.cjsx
index 4c16d1073f..d3cc5209d1 100644
--- a/app/pages/lab/visibility.cjsx
+++ b/app/pages/lab/visibility.cjsx
@@ -1,8 +1,11 @@
React = require 'react'
+apiClient = require 'panoptes-client/lib/api-client'
WorkflowToggle = require '../../components/workflow-toggle'
SetToggle = require '../../lib/set-toggle'
-Dialog = require 'modal-form/dialog'
getWorkflowsInOrder = require '../../lib/get-workflows-in-order'
+uniq = require 'lodash.uniq'
+
+`import ApplyForBetaForm from './apply-for-beta-form';`
module.exports = React.createClass
displayName: 'EditProjectVisibility'
@@ -10,62 +13,35 @@ module.exports = React.createClass
getDefaultProps: ->
project: null
- getInitialState: -> {
- error: null,
- setting: {
- private: false,
- beta_requested: false,
- launch_requested: false,
- },
- workflows: null
- }
+ getInitialState: ->
+ error: null
+ setting:
+ private: false
+ beta_requested: false
+ launch_requested: false
+ workflows: []
+ loadingWorkflows: false
mixins: [SetToggle]
setterProperty: 'project'
componentDidMount: ->
- getWorkflowsInOrder(@props.project, fields: 'display_name,active,configuration')
- .then((workflows) =>
- @setState({ workflows })
- )
-
- isReviewable: ->
- not @props.project.private and
- @props.project.live and not
- @state.setting.beta_requested and not
- @props.project.beta_requested and not
- @props.project.beta_approved
-
- canApplyForReview: ->
- @isReviewable() and
- @state.labPolicyReviewed and
- @state.bestPracticesReviewed and
- @state.feedbackReviewed
+ @setState { loadingWorkflows: true }
+ getWorkflowsInOrder @props.project
+ .then (workflows) =>
+ @setState
+ workflows: workflows
+ loadingWorkflows: false
setRadio: (property, value) ->
@set property, value
- unless @isReviewable()
- @setState
- labPolicyReviewed: false
- bestPracticesReviewed: false
- feedbackReviewed: false
toggleCheckbox: (checkbox) ->
change = { }
change[checkbox] = @refs?[checkbox]?.checked
@setState change
- showFeedbackForm: ->
- Dialog.alert(
-
-
Review Feedback
-
During review, a feedback form will be provided to volunteers that will help you improve your project.
-
A sample of the form is provided for your consideration.
-
,
- closeButton: true
- )
-
handleWorkflowSettingChange: (workflow, e) ->
checked = e.target.checked
@@ -131,51 +107,10 @@ module.exports = React.createClass
All workflows can be edited during development, and subjects will never retire. In a live project, active workflows are locked and can no longer be edited, and classifications count toward subject retirement.
-
+
-
-
- {unless @props.project.beta_approved
-
Pending approval, expose this project to users who have opted in to help test new projects.
}
+
Beta status
{if @props.project.beta_approved
@@ -193,45 +128,18 @@ module.exports = React.createClass
Beta Approval Status:
Pending
-
}
-
-
-
-
-
- Apply for full launch {' '}
-
- {unless @props.project.beta_approved
- Only projects in review can apply for a full launch. }
-
- {if @props.project.launch_approved
-
-
- Launch Approval Status:
- Approved
-
- This project is available to the whole Zooniverse!
-
- else if @props.project.launch_requested
-
-
- Launch Approval Status:
- Pending
-
- Launch is awaiting Zooniverse approval. Cancel application
- }
-
-
- {unless @props.project.launch_approved
-
Pending approval, expose this project to the entire Zooniverse through the main projects listing.
}
+
+ Review status has been applied for. Cancel application
+ else
+
+ }
-
Workflow Settings
- {if @state.workflows is null
+ {if @state.loadingWorkflows is true
Loading workflows...
else if @state.workflows.length is 0
No workflows found
diff --git a/package.json b/package.json
index 4dd2e2efe6..80c0799511 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"babel-loader": "~6.2.4",
"babel-plugin-add-module-exports": "~0.2.1",
"babel-plugin-react-transform": "~2.0.2",
+ "babel-plugin-rewire": "~1.0.0",
"babel-plugin-transform-object-assign": "~6.8.0",
"babel-plugin-transform-react-jsx": "~6.7.5",
"babel-plugin-typecheck": "~3.9.0",
@@ -111,6 +112,6 @@
"stage-with-docker": "./bin/run-through-docker.sh 'npm run stage'",
"_deploy": "export PATH_ROOT=www.zooniverse.org; npm run _build && npm run _publish",
"deploy": "export NODE_ENV=production; npm run _deploy",
- "test": "export NODE_ENV=development; mocha test/setup.js $(find app -name *.spec.js) --reporter nyan --compilers js:babel-core/register,coffee:coffee-script/register,cjsx:coffee-react/register"
+ "test": "NODE_ENV=development BABEL_ENV=test mocha test/setup.js $(find app -name *.spec.js) --reporter nyan --compilers js:babel-core/register,coffee:coffee-script/register,cjsx:coffee-react/register || true"
}
}