Repository: zooniverse/Panoptes-Front-End
Pull request #5069 (2018-11-13 10:14:09)

Bump react from 16.6.1 to 16.6.3
Bumps [react](https://github.com/facebook/react) from 16.6.1 to 16.6.3.
<details>
<summary>Changelog</summary>

*Sourced from [react's changelog](https://github.com/facebook/react/blob/master/CHANGELOG.md).*

> ## [Unreleased]
> <details>
>   <summary>
>     Changes that have landed in master but are not yet released.
>     Click to see more.
>   </summary>
> </details>
</details>
<details>
<summary>Commits</summary>

- See full diff in [compare view](https://github.com/facebook/react/commits)
</details>
<br />

[![Dependabot compatibility score](https://api.dependabot.com/badges/compatibility_score?dependency-name=react&package-manager=npm_and_yarn&previous-version=16.6.1&new-version=16.6.3)](https://dependabot.com/compatibility-score.html?dependency-name=react&package-manager=npm_and_yarn&previous-version=16.6.1&new-version=16.6.3)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot ignore this [patch|minor|major] version` will close this PR and stop Dependabot creating any more for this minor/major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language
- `@dependabot badge me` will comment on this PR with code to add a "Dependabot enabled" badge to your readme

Additionally, you can set the following in your Dependabot [dashboard](https://app.dependabot.com):
- Update frequency (including time of day and day of week)
- Automerge options (never/patch/minor, and dev/runtime dependencies)
- Pull request limits (per update run and/or open at any time)
- Out-of-range updates (receive only lockfile updates, if desired)
- Security updates (receive only security updates, if desired)

Finally, you can contact us by mentioning @dependabot.

</details>

coveralls: 
[![Coverage Status](https://coveralls.io/builds/20072712/badge)](https://coveralls.io/builds/20072712)

Coverage remained the same at 47.3% when pulling **dc48e7f9d5be7ea1e73b8f86387ccb87c59e1035 on dependabot/npm_and_yarn/react-16.6.3** into **c8978988d817848c57d9a150d1a54945ab74daf2 on master**.

eatyourgreens: Staged at https://pr-5069.pfe-preview.zooniverse.org

Patch:
diff --git a/app/classifier/tasks/survey/choice.jsx b/app/classifier/tasks/survey/choice.jsx
index 582d9406..63fee2ce 100644
--- a/app/classifier/tasks/survey/choice.jsx
+++ b/app/classifier/tasks/survey/choice.jsx
@@ -4,7 +4,7 @@ import TriggeredModalForm from 'modal-form/triggered';
 import { Markdown } from 'markdownz';
 import Translate from 'react-translate-component';
 import ImageFlipper from './image-flipper';
-import Utility from './utility';
+import { getQuestionIDs } from './utility';
 
 const BACKSPACE = 8;
 const SPACE = 32;
@@ -22,13 +22,13 @@ class Choice extends React.Component {
 
   checkFilledIn() {
     // if there are no questions, don't make them fill one in
-    if (Utility.getQuestionIDs(this.props.task, this.props.choiceID).length === 0) {
+    if (getQuestionIDs(this.props.task, this.props.choiceID).length === 0) {
       return true;
     }
 
     // if there are questions, it's fine as long as they've filled required ones in
     const answerProvided = [];
-    Utility.getQuestionIDs(this.props.task, this.props.choiceID).map((questionId) => {
+    getQuestionIDs(this.props.task, this.props.choiceID).map((questionId) => {
       const question = this.props.task.questions[questionId];
       if (question.required) {
         const answer = this.state.answers[questionId];
@@ -160,7 +160,7 @@ class Choice extends React.Component {
           <hr />
 
           {!choice.noQuestions &&
-            Utility.getQuestionIDs(task, choiceID).map((questionId) => {
+            getQuestionIDs(task, choiceID).map((questionId) => {
               const question = task.questions[questionId] || { answers: {}, answersOrder: [] };
               const inputType = question.multiple ? 'checkbox' : 'radio';
               return (
@@ -203,7 +203,7 @@ class Choice extends React.Component {
               );
             })
           }
-          {Utility.getQuestionIDs(task, choiceID).length > 0 && <hr />}
+          {getQuestionIDs(task, choiceID).length > 0 && <hr />}
         </div>
         <div style={{ textAlign: 'center' }}>
           <button
@@ -264,4 +264,4 @@ Choice.defaultProps = {
   onCancel: Function.prototype
 };
 
-export default Choice;
\ No newline at end of file
+export default Choice;
diff --git a/app/classifier/tasks/survey/editor-help.cjsx b/app/classifier/tasks/survey/editor-help.jsx
similarity index 80%
rename from app/classifier/tasks/survey/editor-help.cjsx
rename to app/classifier/tasks/survey/editor-help.jsx
index 5282c70c..2232234a 100644
--- a/app/classifier/tasks/survey/editor-help.cjsx
+++ b/app/classifier/tasks/survey/editor-help.jsx
@@ -1,8 +1,11 @@
-React = require 'react'
+import React from 'react';
 
-module.exports =
-  choices: <div className="content-container">
-    <p>User-identifiable <strong>choices</strong>, with these headers:</p>
+export default {
+  choices:
+  <div className="content-container">
+    <p>
+      User-identifiable <strong>choices</strong>, with these headers:
+    </p>
 
     <table className="standard-table">
       <tbody>
@@ -32,14 +35,16 @@ module.exports =
         </tr>
       </tbody>
     </table>
-  </div>
-
-  images: <div className="content-container">
+  </div>,
+  images:
+  <div className="content-container">
     Pick some <strong>images</strong>. Make sure they have the exact same names as the ones in your other files.
-  </div>
-
-  characteristics: <div className="content-container">
-    <p><strong>Characteristics</strong> of those choices that the user can filter through. Note that the values in Name have to match the values in choices.csv exactly. Format like this:</p>
+  </div>,
+  characteristics:
+  <div className="content-container">
+    <p>
+      <strong>Characteristics</strong> of those choices that the user can filter through. Note that the values in Name have to match the values in choices.csv exactly. Format like this:
+    </p>
 
     <table className="standard-table">
       <tbody>
@@ -70,10 +75,12 @@ module.exports =
         </tr>
       </tbody>
     </table>
-  </div>
-
-  confusions: <div className="content-container">
-    <p>Commonly <strong>confused pairs</strong> of choices:</p>
+  </div>,
+  confusions:
+  <div className="content-container">
+    <p>
+      Commonly <strong>confused pairs</strong> of choices:
+    </p>
 
     <table className="standard-table">
       <tbody>
@@ -94,10 +101,12 @@ module.exports =
         </tr>
       </tbody>
     </table>
-  </div>
-
-  questions: <div className="content-container">
-    <p><strong>Questions</strong> to ask about each identification:</p>
+  </div>,
+  questions:
+  <div className="content-container">
+    <p>
+      <strong>Questions</strong> to ask about each identification:
+    </p>
 
     <table className="standard-table">
       <tbody>
@@ -122,9 +131,10 @@ module.exports =
           <td>Y</td>
           <td>N</td>
           <td>Yes; No; Maybe</td>
-          <td> </td>
+          <td />
           <td>Dolphin</td>
         </tr>
       </tbody>
     </table>
   </div>
+};
diff --git a/app/classifier/tasks/survey/editor.cjsx b/app/classifier/tasks/survey/editor.cjsx
index ff0a4867..13387147 100644
--- a/app/classifier/tasks/survey/editor.cjsx
+++ b/app/classifier/tasks/survey/editor.cjsx
@@ -4,7 +4,7 @@ FileButton = require '../../../components/file-button'
 alert = require('../../../lib/alert').default
 Details = require '../../../components/details'
 TriggeredModalForm = require 'modal-form/triggered'
-surveyEditorHelp = require './editor-help'
+surveyEditorHelp = require('./editor-help').default
 AutoSave = require '../../../components/auto-save'
 handleInputChange = require '../../../lib/handle-input-change'
 NextTaskSelector = require '../next-task-selector'
diff --git a/app/classifier/tasks/survey/index.cjsx b/app/classifier/tasks/survey/index.cjsx
deleted file mode 100644
index 6df42c97..00000000
--- a/app/classifier/tasks/survey/index.cjsx
+++ /dev/null
@@ -1,97 +0,0 @@
-React = require 'react'
-createReactClass = require 'create-react-class'
-Editor = require './editor'
-Summary = require('./summary').default
-Chooser = require('./chooser').default
-Choice = require('./choice').default
-AnnotationView = require('./annotation-view').default
-
-module.exports = createReactClass
-  displayName: 'SurveyTask'
-
-  statics:
-    Editor: Editor
-    Summary: Summary
-    AfterSubject: AnnotationView
-
-    getDefaultTask: ->
-      type: 'survey'
-      characteristicsOrder: []
-      characteristics: {}
-      choicesOrder: []
-      choices: {}
-      questionsOrder: []
-      questions: {}
-      images: {}
-
-    getTaskText: (task) ->
-      "Survey of #{task.choicesOrder.length} choices"
-
-    getDefaultAnnotation: ->
-      value: []
-
-    isAnnotationComplete: (task, annotation) ->
-      minRequiredAnswers = if task.required then 1 else 0
-      annotation.value.length >= minRequiredAnswers and not annotation._choiceInProgress
-
-  getDefaultProps: ->
-    task: null
-    annotation: null
-    onChange: Function.prototype
-
-  getInitialState: ->
-    filters: {}
-    selectedChoiceID: ''
-    focusedChoice: ''
-
-  render: ->
-    <div className="survey-task">
-      {if @state.selectedChoiceID is ''
-        <Chooser task={@props.task} filters={@state.filters} onFilter={@handleFilter} onChoose={@handleChoice} onRemove={@handleRemove} annotation={@props.annotation} focusedChoice={@state.focusedChoice} translation={@props.translation} />
-      else
-        # @ is undefined within the scope of find
-        currentSelection = @state.selectedChoiceID
-        existingAnnotationValue = @props.annotation.value.find (value) -> value.choice is currentSelection
-        <Choice annotation={@props.annotation} annotationValue={existingAnnotationValue} task={@props.task} choiceID={@state.selectedChoiceID} onSwitch={@handleChoice} onCancel={@clearSelection} onConfirm={@handleAnnotation} translation={@props.translation} />
-      }
-    </div>
-
-  handleFilter: (characteristicID, valueID) ->
-    setTimeout =>
-      if valueID?
-        @state.filters[characteristicID] = valueID
-      else
-        delete @state.filters[characteristicID]
-      @setState filters: @state.filters
-
-  handleChoice: (choiceID) ->
-    @setState selectedChoiceID: choiceID
-    newAnnotation = Object.assign {}, @props.annotation, _choiceInProgress: true
-    @props.onChange newAnnotation
-
-  handleRemove: (choiceID) ->
-    selectedChoices = (item.choice for item in @props.annotation.value)
-    index = selectedChoices.indexOf choiceID
-    @props.annotation.value.splice index, 1
-    newAnnotation = Object.assign {}, @props.annotation
-    @props.onChange newAnnotation
-
-  clearFilters: ->
-    @setState filters: {}
-
-  clearSelection: ->
-    @setState
-      selectedChoiceID: ''
-      focusedChoice: @state.selectedChoiceID
-    newAnnotation = Object.assign {}, @props.annotation, _choiceInProgress: false
-    @props.onChange newAnnotation
-
-  handleAnnotation: (choice, answers, e) ->
-    filters = JSON.parse JSON.stringify @state.filters
-    value = @props.annotation.value?.filter (value) -> value.choice isnt choice
-    value.push {choice, answers, filters}
-    @clearFilters()
-    @clearSelection()
-    setTimeout => # Wait for filters and selection to clear.
-      newAnnotation = Object.assign {}, @props.annotation, {value}
-      @props.onChange newAnnotation
diff --git a/app/classifier/tasks/survey/index.jsx b/app/classifier/tasks/survey/index.jsx
new file mode 100644
index 00000000..22848bf1
--- /dev/null
+++ b/app/classifier/tasks/survey/index.jsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Editor from './editor';
+import Summary from './summary';
+import Chooser from './chooser';
+import Choice from './choice';
+import AnnotationView from './annotation-view';
+
+export default class SurveyTask extends React.Component {
+  static Editor = Editor
+
+  static Summary = Summary
+
+  static AfterSubject = AnnotationView
+
+  static getDefaultTask() {
+    return {
+      type: 'survey',
+      characteristicsOrder: [],
+      characteristics: {},
+      choicesOrder: [],
+      choices: {},
+      questionsOrder: [],
+      questions: {},
+      images: {}
+    };
+  }
+
+  static getTaskText(task) {
+    return `Survey of ${task.choicesOrder.length} choices`;
+  }
+
+  static getDefaultAnnotation() {
+    return { value: [] };
+  }
+
+  static isAnnotationComplete(task, annotation) {
+    const minRequiredAnswers = task.required ? 1 : 0;
+    return annotation.value.length >= minRequiredAnswers && !annotation._choiceInProgress;
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.clearSelection = this.clearSelection.bind(this);
+    this.handleAnnotation = this.handleAnnotation.bind(this);
+    this.handleChoice = this.handleChoice.bind(this);
+    this.handleFilter = this.handleFilter.bind(this);
+    this.handleRemove = this.handleRemove.bind(this);
+
+    this.state = {
+      filters: {},
+      focusedChoice: '',
+      selectedChoiceID: ''
+    };
+  }
+
+  handleFilter(characteristicID, valueID) {
+    const { filters } = this.state;
+
+    setTimeout(() => {
+      if (valueID) {
+        filters[characteristicID] = valueID;
+      } else {
+        delete filters[characteristicID];
+      }
+      this.setState({ filters });
+    });
+  }
+
+  handleChoice(selectedChoiceID) {
+    const { annotation, onChange } = this.props;
+    const newAnnotation = Object.assign({}, annotation, { _choiceInProgress: true });
+
+    this.setState({ selectedChoiceID });
+    onChange(newAnnotation);
+  }
+
+  handleRemove(choiceID) {
+    const { annotation, onChange } = this.props;
+    const selectedChoices = annotation.value.map(item => item.choice);
+    const index = selectedChoices.indexOf(choiceID);
+    annotation.value.splice(index, 1);
+    const newAnnotation = Object.assign({}, annotation);
+    onChange(newAnnotation);
+  }
+
+  clearFilters() {
+    this.setState({ filters: {}});
+  }
+
+  clearSelection() {
+    const { annotation, onChange } = this.props;
+    const { selectedChoiceID } = this.state;
+
+    this.setState({
+      selectedChoiceID: '',
+      focusedChoice: selectedChoiceID
+    });
+    const newAnnotation = Object.assign({}, annotation, { _choiceInProgress: false });
+    onChange(newAnnotation);
+  }
+
+  handleAnnotation(choice, answers, e) {
+    const { filters } = this.state;
+    const { annotation, onChange } = this.props;
+
+    const parsedFilters = JSON.parse(JSON.stringify(filters));
+    const value = annotation.value.filter(item => item.choice !== choice);
+    value.push({ choice, answers, filters: parsedFilters });
+    this.clearFilters();
+    this.clearSelection();
+    const newAnnotation = Object.assign({}, annotation, { value, _choiceInProgress: false });
+    onChange(newAnnotation);
+  }
+
+  render() {
+    const {
+      annotation,
+      task,
+      translation
+    } = this.props;
+
+    const {
+      filters,
+      focusedChoice,
+      selectedChoiceID
+    } = this.state;
+
+    const existingAnnotationValue = annotation.value
+      && annotation.value.find(value => value.choice === selectedChoiceID);
+
+    return (
+      <div className="survey-task">
+        {(selectedChoiceID === '') ? (
+          <Chooser
+            task={task}
+            filters={filters}
+            onFilter={this.handleFilter}
+            onChoose={this.handleChoice}
+            onRemove={this.handleRemove}
+            annotation={annotation}
+            focusedChoice={focusedChoice}
+            translation={translation}
+          />
+        ) : (
+          <Choice
+            annotation={annotation}
+            annotationValue={existingAnnotationValue}
+            task={task}
+            choiceID={selectedChoiceID}
+            onSwitch={this.handleChoice}
+            onCancel={this.clearSelection}
+            onConfirm={this.handleAnnotation}
+            translation={translation}
+          />
+        )}
+      </div>
+    );
+  }
+}
+
+SurveyTask.propTypes = {
+  annotation: PropTypes.shape({
+    value: PropTypes.array
+  }),
+  onChange: PropTypes.func,
+  task: PropTypes.shape({
+    choicesOrder: PropTypes.array,
+    required: PropTypes.bool
+  }),
+  translation: PropTypes.shape({})
+};
+
+SurveyTask.defaultProps = {
+  annotation: {},
+  onChange: () => {},
+  task: null,
+  translation: null
+};
diff --git a/app/classifier/tasks/survey/index.spec.js b/app/classifier/tasks/survey/index.spec.js
index b54c7a80..21cdc1c6 100644
--- a/app/classifier/tasks/survey/index.spec.js
+++ b/app/classifier/tasks/survey/index.spec.js
@@ -1,72 +1,202 @@
-import { mount } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 import React from 'react';
-import assert from 'assert';
+import { expect } from 'chai';
+import sinon from 'sinon';
 import SurveyTask from './';
 import Choice from './choice';
 import { workflow } from '../../../pages/dev-classifier/mock-data';
 
 describe('Survey Task', function () {
-  const annotation = { value: [] };
+  const selection = {
+    choice: 'ar',
+    answers: {
+      ho: 'two',
+      be: [
+        'ea'
+      ]
+    },
+    filters: {}
+  };
+  const annotation = {
+    value: [selection]
+  };
+  const incompleteAnnotation = {
+    _choiceInProgress: true,
+    value: []
+  };
+
   const task = workflow.tasks.survey;
+  const onChangeSpy = sinon.stub().callsFake(newAnnotation => newAnnotation);
   let wrapper;
 
   beforeEach(function () {
     wrapper = mount(<SurveyTask translation={task} task={task} annotation={annotation} />);
   });
 
+  afterEach(function () {
+    onChangeSpy.resetHistory();
+  });
+
   it('should render a survey task', function () {
-    assert.equal(wrapper.prop('task'), task);
+    expect(wrapper.prop('task'), task).to.be.equal;
   });
 
   it('should render the Choice component when a choice is selected', function () {
     wrapper.setState({ selectedChoiceID: 'ar' });
     const choice = wrapper.find(Choice);
-    assert.equal(choice.props().choiceID, 'ar');
+    expect(choice.props().choiceID, 'ar').to.be.equal;
   });
 
   describe('with an existing annotation', function () {
-    const annotation = {
-      value: [{
-        choice: 'ar',
-        answers: {
-          ho: 'two',
-          be: [
-            'ea'
-          ]
-        },
-        filters: {}
-      }]
-    };
     const task = workflow.tasks.survey;
     let wrapper;
 
     beforeEach(function () {
-      wrapper = mount(<SurveyTask translation={task} task={task} annotation={annotation} />);
+      wrapper = mount(
+        <SurveyTask
+          annotation={annotation}
+          onChange={onChangeSpy}
+          translation={task}
+          task={task}
+        />
+      );
+    });
+
+    afterEach(function () {
+      onChangeSpy.resetHistory();
     });
+    
 
     it('should render a valid annotation as a selected choice', function () {
       const selectedChoice = wrapper.find('[data-choiceid="ar"]');
-      assert.equal(selectedChoice.prop('className'), 'survey-task-chooser-choice-button survey-task-chooser-choice-button-chosen');
+      expect(selectedChoice.prop('className'), 'survey-task-chooser-choice-button survey-task-chooser-choice-button-chosen').to.be.equal;
     });
 
     it('should pass existing answers to the Choice component', function () {
       wrapper.setState({ selectedChoiceID: 'ar' });
       const choice = wrapper.find(Choice);
-      assert.equal(choice.props().annotationValue, annotation.value[0]);
+      expect(choice.props().annotationValue, annotation.value[0]).to.be.equal;
     });
 
     it('should reset saved choices when the annotation resets', function () {
       wrapper.setState({ selectedChoiceID: 'ar' });
       let choice = wrapper.find(Choice);
-      assert.equal(choice.props().annotationValue.answers.ho, 'two');
-      assert.equal(choice.instance().state.answers.ho, 'two');
+      expect(choice.props().annotationValue.answers.ho, 'two').to.be.equal;
+      expect(choice.instance().state.answers.ho, 'two').to.be.equal;
       wrapper.setState({ selectedChoiceID: '' });
       wrapper.setProps({ annotation: { value: [] }});
       wrapper.update();
       wrapper.setState({ selectedChoiceID: 'ar' });
       choice = wrapper.find(Choice);
-      assert.equal(choice.props().annotationValue.answers.ho, undefined);
-      assert.equal(choice.instance().state.answers.ho, undefined);
+      expect(choice.props().annotationValue.answers.ho, undefined).to.be.equal;
+      expect(choice.instance().state.answers.ho, undefined).to.be.equal;
+    });
+  });
+
+  describe('functions', function() {
+    let clock;
+    let wrapper = shallow(
+      <SurveyTask
+        annotation={annotation}
+        onChange={onChangeSpy}
+        translation={task}
+        task={task}
+      />
+    );
+
+    before(function () {
+      const date = new Date();
+      clock = sinon.useFakeTimers(date.getTime());
+    });
+
+    describe('handleAnnotation', function () {
+      afterEach(function () {
+        onChangeSpy.resetHistory();
+      });
+
+      it('should call onChange with an annotation', function() {
+        wrapper.instance().handleAnnotation(selection.choice, selection.answers);
+        expect(onChangeSpy.callCount).to.equal(2);
+        const returnValues = onChangeSpy.returnValues[0];
+        expect(returnValues, annotation).to.be.equal;
+      });
+
+      it('should complete an incomplete annotation', function () {
+        wrapper.setProps({ annotation: incompleteAnnotation });
+        wrapper.instance().handleAnnotation(selection.choice, selection.answers);
+        const newAnnotation = onChangeSpy.returnValues[0];
+        expect(newAnnotation._choiceInProgress).to.be.false;
+      })
+    })
+
+    it('should correctly remove an annotation on handleRemove', function() {
+      const currentLength = wrapper.instance().props.annotation.value.length;
+      wrapper.instance().handleRemove('ar');
+      const newLength = wrapper.instance().props.annotation.value.length;
+      expect(currentLength, newLength).to.not.equal;
+      expect(newLength).to.equal(0);
+    });
+
+    it('handleChoice should set the selected choice', function() {
+      wrapper.instance().handleChoice('aa');
+      expect(wrapper.instance().state.selectedChoiceID, 'aa').to.be.equal;
+    });
+
+    describe('handleFilter', function () {
+      it('should add the correct filter', function() {
+        const expectedFilter = { aa: 'bb' };
+        wrapper.instance().handleFilter('aa', 'bb');
+        clock.tick();
+        expect(wrapper.state().filters, expectedFilter).to.be.equal;
+      });
+
+      it('should remove the correct filter', function () {
+        wrapper.instance().handleFilter('aa', undefined);
+        clock.tick();
+        expect(wrapper.state().filters, {}).to.be.equal;
+      });
+    });
+
+    after(function () {
+      clock.restore();
+    });
+  });
+
+  describe('static methods', function () {
+    it('isAnnotationComplete should return true if annotation is completed', function() {
+      const isComplete = SurveyTask.isAnnotationComplete(task, annotation);
+      expect(isComplete).to.be.a('boolean');
+      expect(isComplete).to.be.true;
+    });
+
+    it('isAnnotationComplete should return false if annotation is incomplete', function() {
+      const isComplete = SurveyTask.isAnnotationComplete(task, incompleteAnnotation);
+      expect(isComplete).to.be.a('boolean');
+      expect(isComplete).to.be.false;
+    });
+
+    it('default annotation should be an object', function() {
+      const defaultAnnotation = SurveyTask.getDefaultAnnotation();
+      expect(defaultAnnotation).to.be.an('object');
+      expect(defaultAnnotation).to.have.property('value').to.be.an('array');
+    });
+
+    it('getTaskText should return a string', function() {
+      const taskText = SurveyTask.getTaskText(task);
+      expect(taskText).to.be.a('string');
+    });
+
+    it('default task should be an object', function() {
+      const defaultTask = SurveyTask.getDefaultTask(task);
+      expect(defaultTask).to.be.an('object');
+      expect(defaultTask).to.have.property('type').to.be.a('string');
+      expect(defaultTask).to.have.property('characteristicsOrder').to.be.an('array');
+      expect(defaultTask).to.have.property('characteristics').to.be.an('object');
+      expect(defaultTask).to.have.property('choicesOrder').to.be.an('array');
+      expect(defaultTask).to.have.property('choices').to.be.an('object');
+      expect(defaultTask).to.have.property('questionsOrder').to.be.an('array');
+      expect(defaultTask).to.have.property('questions').to.be.an('object');
+      expect(defaultTask).to.have.property('images').to.be.an('object');
     });
   });
 });
diff --git a/app/classifier/tasks/survey/utility.coffee b/app/classifier/tasks/survey/utility.coffee
deleted file mode 100644
index 7863d774..00000000
--- a/app/classifier/tasks/survey/utility.coffee
+++ /dev/null
@@ -1,8 +0,0 @@
-module.exports =
-  getQuestionIDs: (task, choiceID) ->
-    return [] if task.choices[choiceID].noQuestions
-    return task.questionsOrder unless task.questionsMap? and choiceID of task.questionsMap
-    task.questionsMap[choiceID]
-  getQuestions: (task, choiceID) ->
-    @getQuestionIDs(task, choiceID).map (idx) ->
-      task.questions[idx]
diff --git a/app/classifier/tasks/survey/utility.js b/app/classifier/tasks/survey/utility.js
new file mode 100644
index 00000000..01aba125
--- /dev/null
+++ b/app/classifier/tasks/survey/utility.js
@@ -0,0 +1,11 @@
+function getQuestionIDs(task, choiceID) {
+  if (task.choices[choiceID].noQuestions) {
+    return [];
+  }
+  if (!(task.questionsMap && Object.keys(task.questionsMap).indexOf(choiceID) >= 0)) {
+    return task.questionsOrder;
+  }
+  return task.questionsMap[choiceID];
+}
+
+export { getQuestionIDs };
diff --git a/app/lib/nav-helpers/getProjectLinks.js b/app/lib/nav-helpers/getProjectLinks.js
index 9c4a7330..1b445a47 100644
--- a/app/lib/nav-helpers/getProjectLinks.js
+++ b/app/lib/nav-helpers/getProjectLinks.js
@@ -3,7 +3,7 @@ import _ from 'lodash';
 import isAdmin from '../is-admin';
 import userHasLabAccess from './userHasLabAccess';
 
-function getProjectLinks({ project, projectRoles, workflow, user }) {
+function getProjectLinks({ project, projectRoles, user }) {
   const { id, redirect, slug } = project;
 
   const links = {
@@ -44,6 +44,8 @@ function getProjectLinks({ project, projectRoles, workflow, user }) {
     }
   };
 
+  const canClassify = project.links.active_workflows && project.links.active_workflows.length > 0;
+
   // For projects with external front ends
   if (redirect) {
     const redirectUrl = `${redirect.replace(/\/+$/, '')}/classify`;
@@ -52,7 +54,7 @@ function getProjectLinks({ project, projectRoles, workflow, user }) {
     _.unset(links, 'about');
   }
 
-  if (!workflow) {
+  if (!canClassify) {
     links.classify.disabled = true;
   }
 
diff --git a/app/lib/nav-helpers/getProjectLinks.spec.js b/app/lib/nav-helpers/getProjectLinks.spec.js
index 6642f9b0..d4022f96 100644
--- a/app/lib/nav-helpers/getProjectLinks.spec.js
+++ b/app/lib/nav-helpers/getProjectLinks.spec.js
@@ -8,21 +8,20 @@ import {
   projectRoles,
   projectWithRedirect,
   projectWithoutRedirect,
-  randomUser,
-  workflow
+  randomUser
 } from '../../../test';
 
 
 describe('getProjectLinks', function() {
   it('should use the project slug in building the navbar links', function() {
-    const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: projectOwnerUser });
+    const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: projectOwnerUser });
     Object.keys(navLinks).forEach((link) => {
       expect(navLinks[link].url.includes(projectWithoutRedirect.slug));
     });
   });
 
   it('should return about, classify, talk, and collections set of navbar links', function() {
-    const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: null });
+    const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: null });
     expect(Object.keys(navLinks).indexOf('about') > -1).to.be.true;
     expect(Object.keys(navLinks).indexOf('classify') > -1).to.be.true;
     expect(Object.keys(navLinks).indexOf('talk') > -1).to.be.true;
@@ -31,45 +30,45 @@ describe('getProjectLinks', function() {
 
   describe('without a user', function() {
     it('should not return a recents link', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: null });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: null });
       expect(Object.keys(navLinks).indexOf('recents') > -1).to.be.false;
     });
 
     it('should not return a lab link', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: null });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: null });
       expect(Object.keys(navLinks).indexOf('lab') > -1).to.be.false;
     });
 
     it('should not return an admin link', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: null });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: null });
       expect(Object.keys(navLinks).indexOf('admin') > -1).to.be.false;
     });
   });
 
   describe('with a user', function() {
     it('should return a recents link', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: randomUser });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: randomUser });
       expect(Object.keys(navLinks).indexOf('recents') > -1).to.be.true;
     });
 
     it('should not return a lab link if the user is not the project owner or a collaborator', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: randomUser });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: randomUser });
       expect(Object.keys(navLinks).indexOf('lab') > -1).to.be.false;
     });
 
     it('should not return an admin page link if the user is not an admin', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: randomUser });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: randomUser });
       expect(Object.keys(navLinks).indexOf('admin') > -1).to.be.false;
     });
 
     describe('who have a project role', function() {
       it('should return the lab link if the user is the project owner', function() {
-        const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: projectOwnerUser });
+        const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: projectOwnerUser });
         expect(Object.keys(navLinks).indexOf('lab') > -1).to.be.true;
       });
 
       it('should return the lab link if the user is a project collaborator', function() {
-        const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: projectCollabUser });
+        const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: projectCollabUser });
         expect(Object.keys(navLinks).indexOf('lab') > -1).to.be.true;
       });
     });
@@ -84,7 +83,7 @@ describe('getProjectLinks', function() {
       });
 
       it('should return the admin link if the user is an admin', function() {
-        const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: adminUser });
+        const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: adminUser });
         expect(Object.keys(navLinks).indexOf('admin') > -1).to.be.true;
       });
     });
@@ -92,20 +91,31 @@ describe('getProjectLinks', function() {
 
   describe('a project with a redirect', function() {
     it('should reset the classify link to the redirected project\'s classify page', function() {
-      const navLinks = getProjectLinks({ project: projectWithRedirect, projectRoles, workflow, user: randomUser });
+      const navLinks = getProjectLinks({ project: projectWithRedirect, projectRoles, user: randomUser });
       expect(navLinks.classify.url.includes(projectWithRedirect.redirect)).to.be.true;
       expect(navLinks.classify.isExternalLink).to.be.true;
     });
 
     it('should not return the about link', function() {
-      const navLinks = getProjectLinks({ project: projectWithRedirect, projectRoles, workflow, user: randomUser });
+      const navLinks = getProjectLinks({ project: projectWithRedirect, projectRoles, user: randomUser });
       expect(Object.keys(navLinks).indexOf('about') > -1).to.be.false;
     });
   });
 
+  describe('a project with active workflows', function() {
+    it('should not disable the classify link', function() {
+      const project = Object.assign({}, projectWithoutRedirect);
+      project.links.active_workflows = ['1', '2'];
+      const navLinks = getProjectLinks({ project, projectRoles, user: null });
+      expect(navLinks.classify).not.to.have.property('disabled');
+    });
+  });
+
   describe('a project without a workflow', function() {
     it('should return the classify link with disabled: true', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow: null, user: null });
+      const project = Object.assign({}, projectWithoutRedirect);
+      project.links.active_workflows = [];
+      const navLinks = getProjectLinks({ project, projectRoles, user: null });
       expect(navLinks.classify.disabled).to.be.true;
     });
   });
diff --git a/app/pages/notifications/notification-section.jsx b/app/pages/notifications/notification-section.jsx
index b1f5af82..ee96537d 100644
--- a/app/pages/notifications/notification-section.jsx
+++ b/app/pages/notifications/notification-section.jsx
@@ -31,11 +31,11 @@ export default class NotificationSection extends Component {
         name: 'Zooniverse'
       });
     } else {
-      apiClient.type('projects').get({ id: this.props.projectID, cards: true })
+      apiClient.type('projects').get(this.props.projectID)
       .catch((error) => {
         this.setState({ error });
       })
-      .then(([project]) => {
+      .then((project) => {
         this.setState({
           name: project.display_name,
           avatar: project.avatar_src
diff --git a/app/pages/project/classify.jsx b/app/pages/project/classify.jsx
index 3f751dbb..8cb596d5 100644
--- a/app/pages/project/classify.jsx
+++ b/app/pages/project/classify.jsx
@@ -19,6 +19,7 @@ import Classifier from '../../classifier';
 import FinishedBanner from './finished-banner';
 import WorkflowAssignmentDialog from '../../components/workflow-assignment-dialog';
 import ProjectThemeButton from './components/ProjectThemeButton';
+import WorkflowSelection from './workflow-selection';
 import { zooTheme } from '../../theme';
 
 function onClassificationSaved(actualClassification) {
@@ -81,7 +82,7 @@ export class ProjectClassifyPage extends React.Component {
   }
 
   componentDidUpdate(prevProps) {
-    const { actions, classification, project, upcomingSubjects, workflow } = this.props;
+    const { classification, upcomingSubjects, workflow } = this.props;
 
     if (workflow !== prevProps.workflow) {
       this.loadAppropriateClassification();
@@ -308,7 +309,6 @@ ProjectClassifyPage.propTypes = {
     })
   }),
   classification: PropTypes.shape({}),
-  loadingSelectedWorkflow: PropTypes.bool,
   location: PropTypes.shape({
     query: PropTypes.shape({
       group: PropTypes.string
@@ -354,4 +354,20 @@ const mapDispatchToProps = dispatch => ({
   }
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(ProjectClassifyPage);
+const ConnectedClassifyPage = connect(mapStateToProps, mapDispatchToProps)(ProjectClassifyPage);
+
+function ConnectedClassifyPageWithWorkflow(props) {
+  return (
+    <WorkflowSelection
+      actions={props.actions}
+      location={props.location}
+      preferences={props.preferences}
+      project={props.project}
+      projectRoles={props.projectRoles}
+      user={props.user}
+    >
+      <ConnectedClassifyPage {...props} />
+    </WorkflowSelection>
+  );
+}
+export default ConnectedClassifyPageWithWorkflow;
diff --git a/app/pages/project/classify.spec.js b/app/pages/project/classify.spec.js
index a5455bbe..cacb2039 100644
--- a/app/pages/project/classify.spec.js
+++ b/app/pages/project/classify.spec.js
@@ -30,7 +30,6 @@ describe('ProjectClassifyPage', function () {
       <ProjectClassifyPage
         actions={actions}
         classification={null}
-        loadingSelectedWorkflow={false}
         project={project}
         upcomingSubjects={[]}
         workflow={workflow}
@@ -134,7 +133,6 @@ describe('ProjectClassifyPage', function () {
         <ProjectClassifyPage
           actions={actions}
           classification={classification}
-          loadingSelectedWorkflow={false}
           project={project}
           upcomingSubjects={[]}
           workflow={newWorkflow}
@@ -169,7 +167,6 @@ describe('ProjectClassifyPage', function () {
         <ProjectClassifyPage
           actions={actions}
           classification={classification}
-          loadingSelectedWorkflow={false}
           project={project}
           upcomingSubjects={[]}
           workflow={workflow}
diff --git a/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx b/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx
index b811cfa1..3f099e78 100644
--- a/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx
+++ b/app/pages/project/components/ProjectNavbar/ProjectNavbarContainer.jsx
@@ -46,8 +46,8 @@ class ProjectNavbarContainer extends Component {
   }
 
   getProjectLinks() {
-    const { project, projectRoles, user, workflow } = this.props;
-    const links = getProjectLinks({ project, projectRoles, workflow, user });
+    const { project, projectRoles, user } = this.props;
+    const links = getProjectLinks({ project, projectRoles, user });
 
     return _.map(links, link => ({
       disabled: link.disabled || false,
@@ -115,8 +115,7 @@ ProjectNavbarContainer.propTypes = {
   translation: PropTypes.shape({
     display_name: PropTypes.string
   }),
-  user: PropTypes.object,
-  workflow: PropTypes.object
+  user: PropTypes.object
 };
 
 export default ProjectNavbarContainer;
diff --git a/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js b/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js
index 63bd8358..82b22620 100644
--- a/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js
+++ b/app/pages/project/components/ProjectNavbar/components/NavLink/NavLink.spec.js
@@ -27,10 +27,10 @@ describe('NavLink', function() {
 
   describe('when the link is disabled', function() {
     it('should be the StyledLinkPlaceholder component ', function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow: null, user: null });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: null });
       const navLinksWithLabels = buildLinksWithLabels(navLinks);
       const classifyLink = navLinksWithLabels[1];
-      const wrapper = shallow(<NavLink url={classifyLink.url} label={classifyLink.label} disabled={classifyLink.disabled} />);
+      const wrapper = shallow(<NavLink url={classifyLink.url} label={classifyLink.label} disabled={true} />);
       expect(wrapper.name()).to.equal('styled.span');
     });
   });
@@ -39,7 +39,7 @@ describe('NavLink', function() {
     let wrapper;
     let firstLink;
     before(function() {
-      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, workflow, user: null });
+      const navLinks = getProjectLinks({ project: projectWithoutRedirect, projectRoles, user: null });
       const navLinksWithLabels = buildLinksWithLabels(navLinks);
       firstLink = navLinksWithLabels[0];
       wrapper = shallow(<NavLink url={firstLink.url} label={firstLink.label} />);
diff --git a/app/pages/project/home/project-home.jsx b/app/pages/project/home/project-home.jsx
index 1ab14020..eb94a95f 100644
--- a/app/pages/project/home/project-home.jsx
+++ b/app/pages/project/home/project-home.jsx
@@ -16,6 +16,7 @@ import ExternalLinksBlock from '../../../components/ExternalLinksBlock';
 
 const ProjectHomePage = (props) => {
   const projectIsNotRedirected = props.project && !props.project.redirect;
+  const canClassify = props.project.links.active_workflows && props.project.links.active_workflows.length > 0;
   const avatarSrc = props.researcherAvatar || '/assets/simple-avatar.png';
 
   const descriptionClass = classnames(
@@ -60,7 +61,7 @@ const ProjectHomePage = (props) => {
             <Link to={`/projects/${props.project.slug}/about`} className="project-home-page__button call-to-action__button call-to-action__button--learn-more">
               <Translate content="project.home.learnMore" />
             </Link>}
-          {showGetStartedLink &&
+          {canClassify && showGetStartedLink &&
             <VisibilitySplit splits={props.splits} splitKey='workflow.assignment' elementKey='link'>
               <Link
                 to={`/projects/${props.project.slug}/classify`}
diff --git a/app/pages/project/home/project-home.spec.js b/app/pages/project/home/project-home.spec.js
index d56ff722..1ce44c2e 100644
--- a/app/pages/project/home/project-home.spec.js
+++ b/app/pages/project/home/project-home.spec.js
@@ -26,7 +26,10 @@ const project = {
       site: 'facebook.com/',
       url: 'https://www.facebook.com/find-the-thing'
     }
-  ]
+  ],
+  links: {
+    active_workflows: ['1']
+  }
 };
 
 const talkSubjects = [
diff --git a/app/pages/project/index.jsx b/app/pages/project/index.jsx
index 0cebe86b..cdc4f422 100644
--- a/app/pages/project/index.jsx
+++ b/app/pages/project/index.jsx
@@ -15,7 +15,6 @@ import * as interventionActions from '../../redux/ducks/interventions';
 import * as translationActions from '../../redux/ducks/translations';
 import ProjectPage from './project-page';
 import ProjectTranslations from './project-translations';
-import WorkflowSelection from './workflow-selection';
 import getAllLinked from '../../lib/get-all-linked';
 
 counterpart.registerTranslations('en', require('../../locales/en').default);
@@ -367,34 +366,24 @@ class ProjectPageController extends React.Component {
           <ProjectTranslations
             project={this.state.project}
           >
-            <WorkflowSelection
-              actions={this.props.actions}
-              location={this.props.location}
+            <ProjectPage
+              {...this.props}
+              background={this.state.background}
+              guide={this.state.guide}
+              guideIcons={this.state.guideIcons}
+              loading={this.state.loading}
+              organization={this.state.organization}
+              owner={this.state.owner}
+              pages={this.state.pages}
               preferences={this.state.projectPreferences}
               project={this.state.project}
+              projectAvatar={this.state.projectAvatar}
+              projectIsComplete={this.state.projectIsComplete}
               projectRoles={this.state.projectRoles}
-              translations={this.props.translations}
-              user={this.props.user}
-            >
-              <ProjectPage
-                {...this.props}
-                background={this.state.background}
-                guide={this.state.guide}
-                guideIcons={this.state.guideIcons}
-                loading={this.state.loading}
-                organization={this.state.organization}
-                owner={this.state.owner}
-                pages={this.state.pages}
-                preferences={this.state.projectPreferences}
-                project={this.state.project}
-                projectAvatar={this.state.projectAvatar}
-                projectIsComplete={this.state.projectIsComplete}
-                projectRoles={this.state.projectRoles}
-                requestUserProjectPreferences={this.requestUserProjectPreferences.bind(this)}
-                splits={this.state.splits}
-                workflow={this.props.workflow}
-              />
-            </WorkflowSelection>
+              requestUserProjectPreferences={this.requestUserProjectPreferences.bind(this)}
+              splits={this.state.splits}
+              workflow={this.props.workflow}
+            />
           </ProjectTranslations>
         }
         {!!this.state.loading &&
diff --git a/app/pages/project/project-page.jsx b/app/pages/project/project-page.jsx
index a984d47a..e30a7ad9 100644
--- a/app/pages/project/project-page.jsx
+++ b/app/pages/project/project-page.jsx
@@ -42,7 +42,6 @@ export default class ProjectPage extends React.Component {
           routes={this.props.routes}
           user={this.props.user}
           translation={this.props.translation}
-          workflow={this.props.workflow}
         />
         {(this.props.project.configuration && this.props.project.configuration.announcement) &&
           <div className="informational project-announcement-banner">
@@ -52,7 +51,6 @@ export default class ProjectPage extends React.Component {
           </div>}
         {React.cloneElement(this.props.children, {
           background: this.props.background,
-          loadingSelectedWorkflow: this.props.loadingSelectedWorkflow,
           onChangePreferences: this.props.onChangePreferences,
           organization: this.props.organization,
           owner: this.props.owner,
diff --git a/app/pages/project/project-page.spec.js b/app/pages/project/project-page.spec.js
index 57b49bf3..810c07b9 100644
--- a/app/pages/project/project-page.spec.js
+++ b/app/pages/project/project-page.spec.js
@@ -4,7 +4,7 @@ import { shallow, mount } from 'enzyme';
 import { expect } from 'chai';
 import sinon from 'sinon';
 import GeordiLogger from '../../lib/zooniverse-logging';
-import { project } from '../dev-classifier/mock-data';
+import { projectWithoutRedirect } from '../../../test/mockObjects';
 import ProjectPage from './project-page';
 import ProjectNavbar from './components/ProjectNavbar';
 import ProjectHomeContainer from './home/';
@@ -22,6 +22,8 @@ function Page() {
 }
 
 describe('ProjectPage', function () {
+  const project = projectWithoutRedirect;
+
   it('should render without crashing', function () {
     shallow(<ProjectPage><Page /></ProjectPage>);
   });
diff --git a/app/pages/project/workflow-selection.jsx b/app/pages/project/workflow-selection.jsx
index 8937bfbc..efb01126 100644
--- a/app/pages/project/workflow-selection.jsx
+++ b/app/pages/project/workflow-selection.jsx
@@ -20,26 +20,19 @@ class WorkflowSelection extends React.Component {
     this.getSelectedWorkflow(this.props);
   }
 
-  componentWillUpdate(nextProps, nextState) {
-    const { preferences } = nextProps;
+  componentDidUpdate(prevProps, prevState) {
+    const { locale, preferences, workflow } = this.props;
     const userSelectedWorkflow = (preferences && preferences.preferences) ? this.sanitiseID(preferences.preferences.selected_workflow) : undefined;
-    if (userSelectedWorkflow &&
-      this.props.workflow
-    ) {
-      if (!nextState.loadingSelectedWorkflow &&
-        userSelectedWorkflow !== this.props.workflow.id
-      ) {
-        this.getSelectedWorkflow(nextProps);
+    if (userSelectedWorkflow && workflow) {
+      if (!this.state.loadingSelectedWorkflow && userSelectedWorkflow !== workflow.id) {
+        this.getSelectedWorkflow(this.props);
       }
     }
-  }
-
-  componentDidUpdate(prevProps, prevState) {
     if (prevProps.project.id !== this.props.project.id) {
       this.getSelectedWorkflow(this.props);
     }
-    if (prevProps.translations.locale !== this.props.translations.locale) {
-      this.props.actions.translations.load('workflow', this.props.workflow.id, this.props.translations.locale);
+    if (workflow && prevProps.locale !== locale) {
+      this.props.actions.translations.load('workflow', workflow.id, locale);
     }
   }
 
@@ -79,11 +72,11 @@ class WorkflowSelection extends React.Component {
     }
 
     if (selectedWorkflowID) return this.getWorkflow(selectedWorkflowID, activeFilter);
-    if (process.env.BABEL_ENV !== 'test') console.warn('Cannot select a workflow.')
+    if (process.env.BABEL_ENV !== 'test') console.warn('Cannot select a workflow.');
   }
 
   getWorkflow(selectedWorkflowID, activeFilter = true) {
-    const { actions, project, translations } = this.props;
+    const { actions, locale, project } = this.props;
     const sanitisedWorkflowID = this.sanitiseID(selectedWorkflowID);
     let isValidWorkflow = false;
 
@@ -113,7 +106,7 @@ class WorkflowSelection extends React.Component {
     return awaitWorkflow
     .then((workflow) => {
       if (workflow) {
-        actions.translations.load('workflow', workflow.id, translations.locale);
+        actions.translations.load('workflow', workflow.id, locale);
         actions.classifier.setWorkflow(workflow);
         this.setState({ loadingSelectedWorkflow: false });
       } else {
@@ -193,9 +186,8 @@ class WorkflowSelection extends React.Component {
   }
 
   render() {
-    const { translation, workflow } = this.props;
     const { loadingSelectedWorkflow } = this.state;
-    return React.cloneElement(this.props.children, { translation, loadingSelectedWorkflow });
+    return loadingSelectedWorkflow ? <p>Loading workflow</p> : this.props.children;
   }
 }
 
@@ -206,18 +198,12 @@ WorkflowSelection.defaultProps = {
     }
   },
   children: null,
+  locale: 'en',
   location: {
     query: {}
   },
   preferences: {},
   projectRoles: [],
-  translation: {
-    id: '',
-    display_name: ''
-  },
-  translations: {
-    locale: 'en'
-  },
   user: null,
   workflow: null
 };
@@ -234,6 +220,7 @@ WorkflowSelection.propTypes = {
       workflow: PropTypes.string
     })
   }),
+  locale: PropTypes.string,
   preferences: PropTypes.shape({
     save: PropTypes.func,
     update: PropTypes.func
@@ -247,12 +234,6 @@ WorkflowSelection.propTypes = {
     uncacheLink: PropTypes.func
   }).isRequired,
   projectRoles: PropTypes.arrayOf(PropTypes.object),
-  translation: PropTypes.shape({
-    display_name: PropTypes.string
-  }),
-  translations: PropTypes.shape({
-    locale: PropTypes.string
-  }),
   workflow: PropTypes.shape({
     id: PropTypes.string
   })
@@ -263,6 +244,7 @@ WorkflowSelection.contextTypes = {
 };
 
 const mapStateToProps = state => ({
+  locale: state.translations.locale,
   workflow: state.classify.workflow
 });
 
@@ -274,4 +256,4 @@ const mapDispatchToProps = dispatch => ({
 });
 
 export default connect(mapStateToProps, mapDispatchToProps)(WorkflowSelection);
-export { WorkflowSelection };
\ No newline at end of file
+export { WorkflowSelection };
diff --git a/app/pages/project/workflow-selection.spec.js b/app/pages/project/workflow-selection.spec.js
index 454dd8d4..195e65fc 100644
--- a/app/pages/project/workflow-selection.spec.js
+++ b/app/pages/project/workflow-selection.spec.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import assert from 'assert';
+import { expect } from 'chai';
 import { mount } from 'enzyme';
 import sinon from 'sinon';
 import apiClient from 'panoptes-client/lib/api-client';
@@ -78,20 +78,17 @@ const preferences = mockPanoptesResource(
 describe('WorkflowSelection', function () {
   const actions = {
     translations: {
-      load: () => null
+      load: sinon.stub()
     }
   };
-  const translations = {
-    locale: 'en'
-  };
   const wrapper = mount(
     <WorkflowSelection
       actions={actions}
       project={project}
       preferences={preferences}
       projectRoles={projectRoles}
+      locale='en'
       location={location}
-      translations={translations}
     >
       <StubPage />
     </WorkflowSelection>,
@@ -119,6 +116,7 @@ describe('WorkflowSelection', function () {
 
   beforeEach(function () {
     workflowStub.resetHistory();
+    actions.translations.load.resetHistory();
     wrapper.update();
   });
 
@@ -134,24 +132,24 @@ describe('WorkflowSelection', function () {
   it('should fetch a random active workflow by default', function () {
     controller.getSelectedWorkflow({ project });
     const selectedWorkflowID = workflowStub.getCall(0).args[0];
-    sinon.assert.calledOnce(workflowStub);
-    assert.notEqual(project.links.active_workflows.indexOf(selectedWorkflowID.toString()), -1);
+    expect(workflowStub.calledOnce).to.be.true;
+    expect(project.links.active_workflows).to.include(selectedWorkflowID.toString());
   });
 
   it('should respect the workflow query param if "allow workflow query" is set', function () {
     location.query.workflow = '6';
     project.experimental_tools = ['allow workflow query'];
     controller.getSelectedWorkflow({ project });
-    sinon.assert.calledOnce(workflowStub);
-    sinon.assert.calledWith(workflowStub, '6', true);
+    expect(workflowStub.calledOnce).to.be.true;
+    expect(workflowStub.calledWith('6', true)).to.be.true;
   });
 
   it('should sanitise the workflow query param if "allow workflow query" is set', function () {
     location.query.workflow = '6random78';
     project.experimental_tools = ['allow workflow query'];
     controller.getSelectedWorkflow({ project });
-    sinon.assert.calledOnce(workflowStub);
-    sinon.assert.calledWith(workflowStub, '6', true);
+    expect(workflowStub.calledOnce).to.be.true;
+    expect(workflowStub.calledWith('6', true)).to.be.true;
   });
 
   describe('with a logged-in user', function () {
@@ -163,32 +161,32 @@ describe('WorkflowSelection', function () {
     it('should load the specified workflow for the project owner', function () {
       const user = owner;
       controller.getSelectedWorkflow({ project, preferences, user });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '6', false);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('6', false)).to.be.true;
     });
 
     it('should load the specified workflow for a collaborator', function () {
       const user = apiClient.type('users').create({ id: '2' });
       controller.getSelectedWorkflow({ project, preferences, user });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '6', false);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('6', false)).to.be.true;
     });
 
     it('should load the specified workflow for a tester', function () {
       const user = apiClient.type('users').create({ id: '3' });
       controller.getSelectedWorkflow({ project, preferences, user });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '6', false);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('6', false)).to.be.true;
     });
 
     it('should load an active workflow for a general user', function () {
       const user = apiClient.type('users').create({ id: '4' });
       controller.getSelectedWorkflow({ project, preferences, user });
-      sinon.assert.calledOnce(workflowStub);
+      expect(workflowStub.calledOnce).to.be.true;
       const selectedWorkflowID = workflowStub.getCall(0).args[0];
       const activeFilter = workflowStub.getCall(0).args[1];
-      assert.notEqual(project.links.active_workflows.indexOf(selectedWorkflowID), -1);
-      assert.equal(activeFilter, true);
+      expect(project.links.active_workflows).to.include(selectedWorkflowID);
+      expect(activeFilter).to.be.true;
     });
   });
 
@@ -203,53 +201,53 @@ describe('WorkflowSelection', function () {
     it('should try to load the stored workflow', function () {
       preferences.update({ 'preferences.selected_workflow': '4' });
       controller.getSelectedWorkflow({ project, preferences });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '4', true);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('4', true)).to.be.true;
     });
 
     it('should parse the stored user workflow as an int', function () {
       preferences.update({ 'preferences.selected_workflow': '4random' });
       controller.getSelectedWorkflow({ project, preferences });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '4', true);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('4', true)).to.be.true;
     });
 
     it('should clear an invalid workflow string from user preferences', function () {
       preferences.update({ 'preferences.selected_workflow': '4' });
       controller.clearInactiveWorkflow('4');
-      assert.equal(preferences.preferences.selected_workflow, undefined);
+      expect(preferences.preferences.selected_workflow).to.be.undefined;
     });
 
     it('should clear an invalid workflow int from user preferences', function () {
       preferences.update({ 'preferences.selected_workflow': 4 });
       controller.clearInactiveWorkflow('4');
-      assert.equal(preferences.preferences.selected_workflow, undefined);
+      expect(preferences.preferences.selected_workflow).to.be.undefined;
     });
 
     it('should try to load a stored project workflow', function () {
       preferences.update({ 'settings.workflow_id': '2' });
       controller.getSelectedWorkflow({ project, preferences });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '2', true);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('2', true)).to.be.true;
     });
 
     it('parse the stored project workflow as an int', function () {
       preferences.update({ 'settings.workflow_id': '2random' });
       controller.getSelectedWorkflow({ project, preferences });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '2', true);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('2', true)).to.be.true;
     });
 
     it('should clear an invalid workflow string from project settings', function () {
       preferences.update({ 'settings.workflow_id': '2' });
       controller.clearInactiveWorkflow('2');
-      assert.equal(preferences.settings.workflow_id, undefined);
+      expect(preferences.settings.workflow_id).to.be.undefined;
     });
 
     it('should clear an invalid workflow int from project settings', function () {
       preferences.update({ 'settings.workflow_id': 2 });
       controller.clearInactiveWorkflow('2');
-      assert.equal(preferences.settings.workflow_id, undefined);
+      expect(preferences.settings.workflow_id).to.be.undefined;
     });
   });
 
@@ -263,8 +261,8 @@ describe('WorkflowSelection', function () {
     it('should load the project default workflow', function () {
       const user = apiClient.type('users').create({ id: '4' });
       controller.getSelectedWorkflow({ project, preferences, user });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '1', true);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('1', true)).to.be.true;
     });
   });
 
@@ -286,8 +284,8 @@ describe('WorkflowSelection', function () {
         }
       );
       wrapper.setProps({ project: newProject });
-      sinon.assert.calledOnce(workflowStub);
-      sinon.assert.calledWith(workflowStub, '10', true);
+      expect(workflowStub.calledOnce).to.be.true;
+      expect(workflowStub.calledWith('10', true)).to.be.true;
     });
   });
 
@@ -324,7 +322,15 @@ describe('WorkflowSelection', function () {
 
       wrapper.setProps({ project: projectWithoutWorkflows });
 
-      sinon.assert.notCalled(workflowStub)
+      expect(workflowStub.notCalled).to.be.true;
+    });
+  });
+
+  describe('on language change', function () {
+    it('should load new translation strings', function () {
+      const workflow = mockPanoptesResource('workflows', { id: '1', tasks: {} });
+      wrapper.setProps({ locale: 'it', workflow });
+      expect(actions.translations.load.calledWith('workflow', '1', 'it')).to.be.true;
     });
   });
 });
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index fe694912..3aaaa9cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1744,7 +1744,6 @@
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
       "dev": true,
-      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -6509,8 +6508,7 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -6531,14 +6529,12 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -6553,20 +6549,17 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -6683,8 +6676,7 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -6696,7 +6688,6 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -6711,7 +6702,6 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -6719,14 +6709,12 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -6745,7 +6733,6 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -6826,8 +6813,7 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -6839,7 +6825,6 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -6925,8 +6910,7 @@
         "safe-buffer": {
           "version": "5.1.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -6962,7 +6946,6 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -6982,7 +6965,6 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -7026,14 +7008,12 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "yallist": {
           "version": "3.0.2",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         }
       }
     },
@@ -8795,8 +8775,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
       "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "loose-envify": {
       "version": "1.3.1",
@@ -9729,7 +9708,6 @@
           "version": "0.1.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "kind-of": "^3.0.2",
             "longest": "^1.0.1",
@@ -10054,8 +10032,7 @@
         "is-buffer": {
           "version": "1.1.6",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "is-builtin-module": {
           "version": "1.0.0",
@@ -10139,7 +10116,6 @@
           "version": "3.2.2",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "is-buffer": "^1.1.5"
           }
@@ -10186,8 +10162,7 @@
         "longest": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "lru-cache": {
           "version": "4.1.3",
@@ -10453,8 +10428,7 @@
         "repeat-string": {
           "version": "1.6.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "require-directory": {
           "version": "2.1.1",
@@ -11944,20 +11918,20 @@
       }
     },
     "react": {
-      "version": "16.6.1",
-      "resolved": "https://registry.npmjs.org/react/-/react-16.6.1.tgz",
-      "integrity": "sha512-OtawJThYlvRgm9BXK+xTL7BIlDx8vv21j+fbQDjRRUyok6y7NyjlweGorielTahLZHYIdKUoK2Dp9ByVWuMqxw==",
+      "version": "16.6.3",
+      "resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
+      "integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "scheduler": "^0.11.0"
+        "scheduler": "^0.11.2"
       },
       "dependencies": {
         "scheduler": {
-          "version": "0.11.0",
-          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.0.tgz",
-          "integrity": "sha512-MAYbBfmiEHxF0W+c4CxMpEqMYK+rYF584VP/qMKSiHM6lTkBKKYOJaDiSILpJHla6hBOsVd6GucPL46o2Uq3sg==",
+          "version": "0.11.2",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz",
+          "integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==",
           "requires": {
             "loose-envify": "^1.1.0",
             "object-assign": "^4.1.1"
@@ -11974,20 +11948,20 @@
       }
     },
     "react-dom": {
-      "version": "16.6.1",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.1.tgz",
-      "integrity": "sha512-zm+wBuEMGm009Wt1uE4Zw5KcXOW7qC4E/xW/fpJsCsbOco4U/R84u+DzzO/S4SYSdNBcqcaulcp4w3FXl8pImw==",
+      "version": "16.6.3",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.3.tgz",
+      "integrity": "sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "scheduler": "^0.11.0"
+        "scheduler": "^0.11.2"
       },
       "dependencies": {
         "scheduler": {
-          "version": "0.11.0",
-          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.0.tgz",
-          "integrity": "sha512-MAYbBfmiEHxF0W+c4CxMpEqMYK+rYF584VP/qMKSiHM6lTkBKKYOJaDiSILpJHla6hBOsVd6GucPL46o2Uq3sg==",
+          "version": "0.11.2",
+          "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz",
+          "integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==",
           "requires": {
             "loose-envify": "^1.1.0",
             "object-assign": "^4.1.1"
diff --git a/package.json b/package.json
index 3f289849..e86cf41b 100644
--- a/package.json
+++ b/package.json
@@ -25,9 +25,9 @@
     "prop-types": "~15.6.2",
     "qs": "~6.5.2",
     "rc-slider": "~8.6.3",
-    "react": "~16.6.1",
+    "react": "~16.6.3",
     "react-chartist": "~0.13.1",
-    "react-dom": "~16.6.1",
+    "react-dom": "~16.6.3",
     "react-helmet": "~5.2.0",
     "react-interpolate-component": "~0.12.0",
     "react-pullout": "~1.0.2",


Test patch:
diff --git a/test/mockObjects.js b/test/mockObjects.js
index 2da4e9dd..6ba2e6be 100644
--- a/test/mockObjects.js
+++ b/test/mockObjects.js
@@ -41,14 +41,20 @@ export const projectWithoutRedirect = {
   id: '2',
   slug: 'zooniverse/find-the-thing',
   title: 'Find the Thing',
-  urls: [socialMediaUrl, externalUrl]
+  urls: [socialMediaUrl, externalUrl],
+  links: {
+    active_workflows: ['35']
+  }
 };
 
 export const projectWithRedirect = {
   id: '3',
   redirect: 'https://www.redirected-project.org',
   title: 'Find the Thing',
-  urls: [socialMediaUrl, externalUrl]
+  urls: [socialMediaUrl, externalUrl],
+  links: {
+    active_workflows: ['35']
+  }
 };
 
 export const workflow = { id: '35' };