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