From b6f5a6e33c09bad79271bedfb0c0304641d7b5ee Mon Sep 17 00:00:00 2001
From: Paul Kaplan <pkaplan@media.mit.edu>
Date: Wed, 2 Aug 2017 15:40:02 -0400
Subject: [PATCH] Internationalize and update testing to handle react-intl

---
 src/components/sound-editor/sound-editor.jsx  | 51 ++++++++++++++++---
 test/helpers/intl-helpers.js                  | 45 ++++++++++++++++
 .../__snapshots__/sound-editor.test.jsx.snap  |  4 +-
 test/unit/components/sound-editor.test.jsx    | 17 +++----
 test/unit/containers/sound-editor.test.jsx    | 10 ++--
 5 files changed, 105 insertions(+), 22 deletions(-)
 create mode 100644 test/helpers/intl-helpers.js

diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx
index 2168ee651..7ac6811ab 100644
--- a/src/components/sound-editor/sound-editor.jsx
+++ b/src/components/sound-editor/sound-editor.jsx
@@ -1,6 +1,8 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import classNames from 'classnames';
+import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
+
 import Waveform from '../waveform/waveform.jsx';
 import Label from '../forms/label.jsx';
 import Input from '../forms/input.jsx';
@@ -15,6 +17,34 @@ import trimIcon from './icon--trim.svg';
 
 const BufferedInput = BufferedInputHOC(Input);
 
+const messages = defineMessages({
+    sound: {
+        id: 'soundEditor.sound',
+        description: 'Lable for the name of the sound',
+        defaultMessage: 'Sound'
+    },
+    play: {
+        id: 'soundEditor.play',
+        description: 'Title of the button to start playing the sound',
+        defaultMessage: 'Play'
+    },
+    stop: {
+        id: 'soundEditor.stop',
+        description: 'Title of the button to stop the sound',
+        defaultMessage: 'Stop'
+    },
+    trim: {
+        id: 'soundEditor.trim',
+        description: 'Title of the button to start trimminging the sound',
+        defaultMessage: 'Trim'
+    },
+    save: {
+        id: 'soundEditor.save',
+        description: 'Title of the button to save trimmed sound',
+        defaultMessage: 'Save'
+    }
+});
+
 const SoundEditor = props => (
     <div className={styles.editorContainer}>
         <div className={styles.row}>
@@ -22,7 +52,7 @@ const SoundEditor = props => (
                 {props.playhead ? (
                     <button
                         className={classNames(styles.button, styles.stopButtonn)}
-                        title={'Stop'}
+                        title={props.intl.formatMessage(messages.stop)}
                         onClick={props.onStop}
                     >
                         <img src={stopIcon} />
@@ -30,7 +60,7 @@ const SoundEditor = props => (
                 ) : (
                     <button
                         className={classNames(styles.button, styles.playButton)}
-                        title={'Play'}
+                        title={props.intl.formatMessage(messages.play)}
                         onClick={props.onPlay}
                     >
                         <img src={playIcon} />
@@ -38,7 +68,7 @@ const SoundEditor = props => (
                 )}
             </div>
             <div className={styles.inputGroup}>
-                <Label text="Sound">
+                <Label text={props.intl.formatMessage(messages.sound)}>
                     <BufferedInput
                         tabIndex="1"
                         type="text"
@@ -52,11 +82,19 @@ const SoundEditor = props => (
                     className={classNames(styles.button, styles.trimButton, {
                         [styles.trimButtonActive]: props.trimStart !== null
                     })}
-                    title={props.trimStart === null ? 'Trim' : 'Save'}
+                    title={props.trimStart === null ? (
+                        props.intl.formatMessage(messages.trim)
+                    ) : (
+                        props.intl.formatMessage(messages.save)
+                    )}
                     onClick={props.onActivateTrim}
                 >
                     <img src={trimIcon} />
-                    {props.trimStart === null ? 'Trim' : 'Save'}
+                    {props.trimStart === null ? (
+                        <FormattedMessage {...messages.trim} />
+                    ) : (
+                        <FormattedMessage {...messages.save} />
+                    )}
                 </button>
             </div>
         </div>
@@ -81,6 +119,7 @@ const SoundEditor = props => (
 
 SoundEditor.propTypes = {
     chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired,
+    intl: intlShape,
     name: PropTypes.string.isRequired,
     onActivateTrim: PropTypes.func,
     onChangeName: PropTypes.func.isRequired,
@@ -93,4 +132,4 @@ SoundEditor.propTypes = {
     trimStart: PropTypes.number
 };
 
-export default SoundEditor;
+export default injectIntl(SoundEditor);
diff --git a/test/helpers/intl-helpers.js b/test/helpers/intl-helpers.js
new file mode 100644
index 000000000..d658aeae0
--- /dev/null
+++ b/test/helpers/intl-helpers.js
@@ -0,0 +1,45 @@
+/*
+ * Helpers for using enzyme and react-test-renderer with react-intl
+ * Directly from https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl
+ */
+import React from 'react';
+import renderer from 'react-test-renderer';
+import {IntlProvider, intlShape} from 'react-intl';
+import {mount, shallow} from 'enzyme';
+
+const intlProvider = new IntlProvider({locale: 'en'}, {});
+const {intl} = intlProvider.getChildContext();
+
+const nodeWithIntlProp = node => {
+    return React.cloneElement(node, {intl});
+};
+
+const shallowWithIntl = (node, {context} = {}) => {
+    return shallow(
+        nodeWithIntlProp(node),
+        {
+            context: Object.assign({}, context, {intl})
+        }
+    );
+};
+
+const mountWithIntl = (node, {context, childContextTypes} = {}) => {
+    return mount(
+        nodeWithIntlProp(node),
+        {
+            context: Object.assign({}, context, {intl}),
+            childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes)
+        }
+    );
+};
+
+// react-test-renderer component for use with snapshot testing
+const componentWithIntl = (children, props = {locale: 'en'}) => {
+    return renderer.create(<IntlProvider {...props}>{children}</IntlProvider>);
+};
+
+export {
+    componentWithIntl,
+    shallowWithIntl,
+    mountWithIntl
+};
diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
index b32ad42e3..c757bc5d5 100644
--- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
+++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
@@ -54,7 +54,9 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         <img
           src="test-file-stub"
         />
-        Save
+        <span>
+          Save
+        </span>
       </button>
     </div>
   </div>
diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx
index f22609cdb..9c98edda9 100644
--- a/test/unit/components/sound-editor.test.jsx
+++ b/test/unit/components/sound-editor.test.jsx
@@ -1,8 +1,7 @@
 /* eslint-env jest */
 import React from 'react'; // eslint-disable-line no-unused-vars
-import {mount} from 'enzyme';
+import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers';
 import SoundEditor from '../../../src/components/sound-editor/sound-editor'; // eslint-disable-line no-unused-vars
-import renderer from 'react-test-renderer';
 
 describe('Sound Editor Component', () => {
     let props;
@@ -23,38 +22,36 @@ describe('Sound Editor Component', () => {
     });
 
     test('matches snapshot', () => {
-        const component = renderer.create(
-            <SoundEditor {...props} />
-        );
+        const component = componentWithIntl(<SoundEditor {...props} />);
         expect(component.toJSON()).toMatchSnapshot();
     });
 
     test('trim button appears when trims are null', () => {
-        const wrapper = mount(<SoundEditor {...props} trimStart={null} trimEnd={null} />);
+        const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={null} trimEnd={null} />);
         wrapper.find('button[title="Trim"]').simulate('click');
         expect(props.onActivateTrim).toHaveBeenCalled();
     });
 
     test('save button appears when trims are not null', () => {
-        const wrapper = mount(<SoundEditor {...props} trimStart={0.25} trimEnd={0.75} />);
+        const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={0.25} trimEnd={0.75} />);
         wrapper.find('button[title="Save"]').simulate('click');
         expect(props.onActivateTrim).toHaveBeenCalled();
     });
 
     test('play button appears when playhead is null', () => {
-        const wrapper = mount(<SoundEditor {...props} playhead={null} />);
+        const wrapper = mountWithIntl(<SoundEditor {...props} playhead={null} />);
         wrapper.find('button[title="Play"]').simulate('click');
         expect(props.onPlay).toHaveBeenCalled();
     });
 
     test('stop button appears when playhead is not null', () => {
-        const wrapper = mount(<SoundEditor {...props} playhead={0.5} />);
+        const wrapper = mountWithIntl(<SoundEditor {...props} playhead={0.5} />);
         wrapper.find('button[title="Stop"]').simulate('click');
         expect(props.onStop).toHaveBeenCalled();
     });
 
     test('submitting name calls the callback', () => {
-        const wrapper = mount(<SoundEditor {...props} />);
+        const wrapper = mountWithIntl(<SoundEditor {...props} />);
         wrapper.find('input')
             .simulate('change', {target: {value: 'hello'}})
             .simulate('blur');
diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx
index a0838689f..39f1e19b0 100644
--- a/test/unit/containers/sound-editor.test.jsx
+++ b/test/unit/containers/sound-editor.test.jsx
@@ -1,6 +1,6 @@
 /* eslint-env jest */
 import React from 'react'; // eslint-disable-line no-unused-vars
-import {mount} from 'enzyme';
+import {mountWithIntl} from '../../helpers/intl-helpers';
 import configureStore from 'redux-mock-store';
 import mockAudioBufferPlayer from '../../__mocks__/audio-buffer-player.js';
 
@@ -38,7 +38,7 @@ describe('Sound Editor Container', () => {
     });
 
     test('should pass the correct data to the component from the store', () => {
-        const wrapper = mount(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
         const componentProps = wrapper.find(SoundEditorComponent).props();
         // Data retreived and processed by the `connect` with the store
         expect(componentProps.name).toEqual('first name');
@@ -52,7 +52,7 @@ describe('Sound Editor Container', () => {
     });
 
     test('it plays when clicked and stops when clicked again', () => {
-        const wrapper = mount(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
         const component = wrapper.find(SoundEditorComponent);
         // Ensure rendering doesn't start playing any sounds
         expect(mockAudioBufferPlayer.instance.play.mock.calls).toEqual([]);
@@ -71,7 +71,7 @@ describe('Sound Editor Container', () => {
     });
 
     test('it sets the component props for trimming and submits to the vm', () => {
-        const wrapper = mount(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
         const component = wrapper.find(SoundEditorComponent);
 
         component.props().onActivateTrim();
@@ -85,7 +85,7 @@ describe('Sound Editor Container', () => {
     });
 
     test('it submits name changes to the vm', () => {
-        const wrapper = mount(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
         const component = wrapper.find(SoundEditorComponent);
         component.props().onChangeName('hello');
         expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello');
-- 
GitLab