diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index d414f3ae25d6350e86f4e5e230e1a76f5ccaa7e4..fc216063a89091523735081e9c3ab407739e316a 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -10,23 +10,25 @@ const AudioTrimmer = props => ( className={styles.absolute} ref={props.containerRef} > - <Box - className={classNames(styles.absolute, styles.trimBackground, styles.startTrimBackground)} - style={{ - width: `${100 * props.trimStart}%` - }} - onMouseDown={props.onTrimStartMouseDown} - > - <Box className={classNames(styles.absolute, styles.trimBackgroundMask)} /> - <Box className={classNames(styles.trimLine, styles.startTrimLine)}> - <Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.startTrimHandle)}> - <img src={handleIcon} /> - </Box> - <Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.startTrimHandle)}> - <img src={handleIcon} /> + {props.trimStart !== null ? ( + <Box + className={classNames(styles.absolute, styles.trimBackground, styles.startTrimBackground)} + style={{ + width: `${100 * props.trimStart}%` + }} + onMouseDown={props.onTrimStartMouseDown} + > + <Box className={classNames(styles.absolute, styles.trimBackgroundMask)} /> + <Box className={classNames(styles.trimLine, styles.startTrimLine)}> + <Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.startTrimHandle)}> + <img src={handleIcon} /> + </Box> + <Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.startTrimHandle)}> + <img src={handleIcon} /> + </Box> </Box> </Box> - </Box> + ) : null} {props.playhead ? ( <Box @@ -37,24 +39,26 @@ const AudioTrimmer = props => ( /> ) : null} - <Box - className={classNames(styles.absolute, styles.trimBackground, styles.endTrimBackground)} - style={{ - left: `${100 * props.trimEnd}%`, - width: `${100 - 100 * props.trimEnd}%` - }} - onMouseDown={props.onTrimEndMouseDown} - > - <Box className={classNames(styles.absolute, styles.trimBackgroundMask)} /> - <Box className={classNames(styles.trimLine, styles.endTrimLine)}> - <Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.endTrimHandle)}> - <img src={handleIcon} /> - </Box> - <Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.endTrimHandle)}> - <img src={handleIcon} /> + {props.trimEnd !== null ? ( + <Box + className={classNames(styles.absolute, styles.trimBackground, styles.endTrimBackground)} + style={{ + left: `${100 * props.trimEnd}%`, + width: `${100 - 100 * props.trimEnd}%` + }} + onMouseDown={props.onTrimEndMouseDown} + > + <Box className={classNames(styles.absolute, styles.trimBackgroundMask)} /> + <Box className={classNames(styles.trimLine, styles.endTrimLine)}> + <Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.endTrimHandle)}> + <img src={handleIcon} /> + </Box> + <Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.endTrimHandle)}> + <img src={handleIcon} /> + </Box> </Box> </Box> - </Box> + ) : null } </div> ); @@ -63,8 +67,8 @@ AudioTrimmer.propTypes = { onTrimEndMouseDown: PropTypes.func.isRequired, onTrimStartMouseDown: PropTypes.func.isRequired, playhead: PropTypes.number, - trimEnd: PropTypes.number.isRequired, - trimStart: PropTypes.number.isRequired + trimEnd: PropTypes.number, + trimStart: PropTypes.number }; export default AudioTrimmer; diff --git a/src/components/sound-editor/icon--trim.svg b/src/components/sound-editor/icon--trim.svg new file mode 100644 index 0000000000000000000000000000000000000000..cf64941cab62829199fa7d895442c4c17f506c57 Binary files /dev/null and b/src/components/sound-editor/icon--trim.svg differ diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 0bb44f115c65dfabf9e3e899e40aca4b0322fb68..1d67886da3674e57240139e4fdaede6d581ab443 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -36,17 +36,36 @@ } .button { - width: 2rem; height: 2rem; padding: 0.25rem; outline: none; background: white; border-radius: 0.25rem; border: 1px solid #ddd; + cursor: pointer; + font-size: 0.85rem; + transition: 0.2s; } .button img { flex-grow: 1; max-width: 100%; max-height: 100%; + min-width: 1.5rem; +} + +.trim-button { + display: flex; + align-items: center; + padding-right: 10px; /* To equalize with empty whitespace from image on left */ +} + +.trim-button-active { + filter: hue-rotate(155deg); /* @todo replace blue -> red with real submit icon */ +} + +.input-group-right { + flex-grow: 1; + display: flex; + flex-direction: row-reverse; } diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index 3e379b3f5372e57c977438246a8690768eac66d9..7ac6811abfb4c6219608f84ab541bbc533123278 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -1,26 +1,58 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; -import Box from '../box/box.jsx'; +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'; import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import AudioTrimmer from '../../containers/audio-trimmer.jsx'; import styles from './sound-editor.css'; import playIcon from '../record-modal/icon--play.svg'; import stopIcon from '../record-modal/icon--stop-playback.svg'; +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 => ( - <Box className={styles.editorContainer}> - <Box className={styles.row}> - <Box className={styles.inputGroup}> + <div className={styles.editorContainer}> + <div className={styles.row}> + <div className={styles.inputGroup}> {props.playhead ? ( <button className={classNames(styles.button, styles.stopButtonn)} + title={props.intl.formatMessage(messages.stop)} onClick={props.onStop} > <img src={stopIcon} /> @@ -28,14 +60,15 @@ const SoundEditor = props => ( ) : ( <button className={classNames(styles.button, styles.playButton)} + title={props.intl.formatMessage(messages.play)} onClick={props.onPlay} > <img src={playIcon} /> </button> )} - </Box> - <Box className={styles.inputGroup}> - <Label text="Sound"> + </div> + <div className={styles.inputGroup}> + <Label text={props.intl.formatMessage(messages.sound)}> <BufferedInput tabIndex="1" type="text" @@ -43,27 +76,60 @@ const SoundEditor = props => ( onSubmit={props.onChangeName} /> </Label> - </Box> - </Box> - <Box className={styles.row}> - <Box className={styles.waveformContainer}> + </div> + <div className={styles.inputGroupRight}> + <button + className={classNames(styles.button, styles.trimButton, { + [styles.trimButtonActive]: props.trimStart !== null + })} + title={props.trimStart === null ? ( + props.intl.formatMessage(messages.trim) + ) : ( + props.intl.formatMessage(messages.save) + )} + onClick={props.onActivateTrim} + > + <img src={trimIcon} /> + {props.trimStart === null ? ( + <FormattedMessage {...messages.trim} /> + ) : ( + <FormattedMessage {...messages.save} /> + )} + </button> + </div> + </div> + <div className={styles.row}> + <div className={styles.waveformContainer}> <Waveform data={props.chunkLevels} height={180} width={600} /> - </Box> - </Box> - </Box> + <AudioTrimmer + playhead={props.playhead} + trimEnd={props.trimEnd} + trimStart={props.trimStart} + onSetTrimEnd={props.onSetTrimEnd} + onSetTrimStart={props.onSetTrimStart} + /> + </div> + </div> + </div> ); SoundEditor.propTypes = { chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired, + intl: intlShape, name: PropTypes.string.isRequired, + onActivateTrim: PropTypes.func, onChangeName: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired, + onSetTrimEnd: PropTypes.func, + onSetTrimStart: PropTypes.func, onStop: PropTypes.func.isRequired, - playhead: PropTypes.number + playhead: PropTypes.number, + trimEnd: PropTypes.number, + trimStart: PropTypes.number }; -export default SoundEditor; +export default injectIntl(SoundEditor); diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index 07ba2154dbecd3dd8905198226c2f91f18cfb8ac..03e91443eb32f538640ee9a1fd17ac2a66aa2375 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -20,12 +20,14 @@ class AudioTrimmer extends React.Component { const dx = (e.clientX - this.initialX) / containerSize; const newTrim = Math.max(0, Math.min(this.props.trimEnd, this.initialTrim + dx)); this.props.onSetTrimStart(newTrim); + e.preventDefault(); } handleTrimEndMouseMove (e) { const containerSize = this.containerElement.getBoundingClientRect().width; const dx = (e.clientX - this.initialX) / containerSize; const newTrim = Math.min(1, Math.max(this.props.trimStart, this.initialTrim + dx)); this.props.onSetTrimEnd(newTrim); + e.preventDefault(); } handleTrimStartMouseUp () { window.removeEventListener('mousemove', this.handleTrimStartMouseMove); diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 7706b1e98c984fcfebfe4c206acb313ec6b878d1..d18678273bf183ab1eea7298a3a82b9c41938e63 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -17,30 +17,50 @@ class SoundEditor extends React.Component { 'handleChangeName', 'handlePlay', 'handleStopPlaying', - 'handleUpdatePlayhead' + 'handleUpdatePlayhead', + 'handleActivateTrim', + 'handleUpdateTrimEnd', + 'handleUpdateTrimStart' ]); this.state = { + chunkLevels: computeChunkedRMS(this.props.samples), playhead: null, // null is not playing, [0 -> 1] is playing percent - chunkLevels: computeChunkedRMS(this.props.samples) + trimStart: null, + trimEnd: null }; } componentDidMount () { this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate); } componentWillReceiveProps (newProps) { - if (newProps.soundIndex !== this.props.soundIndex) { - this.audioBufferPlayer.stop(); - this.audioBufferPlayer = new AudioBufferPlayer(newProps.samples, newProps.sampleRate); - this.setState({chunkLevels: computeChunkedRMS(newProps.samples)}); + if (newProps.soundId !== this.props.soundId) { // A different sound has been selected + this.resetState(newProps.samples, newProps.sampleRate); } } componentWillUnmount () { this.audioBufferPlayer.stop(); } + resetState (samples, sampleRate) { + this.audioBufferPlayer.stop(); + this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate); + this.setState({ + chunkLevels: computeChunkedRMS(samples), + playhead: null, + trimStart: null, + trimEnd: null + }); + } + submitNewSamples (samples, sampleRate) { + this.resetState(samples, sampleRate); + this.props.onUpdateSoundBuffer( + this.props.soundIndex, + this.audioBufferPlayer.buffer + ); + } handlePlay () { this.audioBufferPlayer.play( - 0, - 1, + this.state.trimStart || 0, + this.state.trimEnd || 1, this.handleUpdatePlayhead, this.handleStoppedPlaying); } @@ -57,6 +77,23 @@ class SoundEditor extends React.Component { handleChangeName (name) { this.props.onRenameSound(this.props.soundIndex, name); } + handleActivateTrim () { + if (this.state.trimStart === null && this.state.trimEnd === null) { + this.setState({trimEnd: 0.95, trimStart: 0.05}); + } else { + const sampleCount = this.props.samples.length; + const startIndex = Math.floor(this.state.trimStart * sampleCount); + const endIndex = Math.floor(this.state.trimEnd * sampleCount); + const clippedSamples = this.props.samples.slice(startIndex, endIndex); + this.submitNewSamples(clippedSamples, this.props.sampleRate); + } + } + handleUpdateTrimEnd (trimEnd) { + this.setState({trimEnd}); + } + handleUpdateTrimStart (trimStart) { + this.setState({trimStart}); + } render () { return ( <SoundEditorComponent @@ -65,12 +102,12 @@ class SoundEditor extends React.Component { playhead={this.state.playhead} trimEnd={this.state.trimEnd} trimStart={this.state.trimStart} + onActivateTrim={this.handleActivateTrim} onChangeName={this.handleChangeName} onPlay={this.handlePlay} onSetTrimEnd={this.handleUpdateTrimEnd} onSetTrimStart={this.handleUpdateTrimStart} onStop={this.handleStopPlaying} - onTrim={this.handleActivateTrim} /> ); } @@ -79,8 +116,10 @@ class SoundEditor extends React.Component { SoundEditor.propTypes = { name: PropTypes.string.isRequired, onRenameSound: PropTypes.func.isRequired, + onUpdateSoundBuffer: PropTypes.func.isRequired, sampleRate: PropTypes.number, samples: PropTypes.instanceOf(Float32Array), + soundId: PropTypes.string, soundIndex: PropTypes.number }; @@ -88,10 +127,12 @@ const mapStateToProps = (state, {soundIndex}) => { const sound = state.vm.editingTarget.sprite.sounds[soundIndex]; const audioBuffer = state.vm.getSoundBuffer(soundIndex); return { + soundId: sound.soundId, sampleRate: audioBuffer.sampleRate, samples: audioBuffer.getChannelData(0), name: sound.name, - onRenameSound: state.vm.renameSound.bind(state.vm) + onRenameSound: state.vm.renameSound.bind(state.vm), + onUpdateSoundBuffer: state.vm.updateSoundBuffer.bind(state.vm) }; }; diff --git a/test/__mocks__/audio-buffer-player.js b/test/__mocks__/audio-buffer-player.js new file mode 100644 index 0000000000000000000000000000000000000000..c36092be339f166b2376a5340c1e82abed6c9ca9 --- /dev/null +++ b/test/__mocks__/audio-buffer-player.js @@ -0,0 +1,12 @@ +/* eslint-env jest */ +export default class MockAudioBufferPlayer { + constructor (samples, sampleRate) { + this.samples = samples; + this.sampleRate = sampleRate; + this.play = jest.fn((trimStart, trimEnd, onUpdate) => { + this.onUpdate = onUpdate; + }); + this.stop = jest.fn(); + MockAudioBufferPlayer.instance = this; + } +} diff --git a/test/helpers/intl-helpers.js b/test/helpers/intl-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..d658aeae0981adc70d8de4f57d3ec196a06f2df1 --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..c757bc5d5679537c6125e4e7ed3b5a97be4d88ae --- /dev/null +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -0,0 +1,313 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sound Editor Component matches snapshot 1`] = ` +<div + className={undefined} +> + <div + className={undefined} + > + <div + className={undefined} + > + <button + className="" + onClick={[Function]} + title="Stop" + > + <img + src="test-file-stub" + /> + </button> + </div> + <div + className={undefined} + > + <label + className={undefined} + > + <span + className={undefined} + > + Sound + </span> + <input + className="" + onBlur={[Function]} + onChange={[Function]} + onKeyPress={[Function]} + onSubmit={[Function]} + tabIndex="1" + type="text" + value="sound name" + /> + </label> + </div> + <div + className={undefined} + > + <button + className="undefined" + onClick={[Function]} + title="Save" + > + <img + src="test-file-stub" + /> + <span> + Save + </span> + </button> + </div> + </div> + <div + className={undefined} + > + <div + className={undefined} + > + <svg + className={undefined} + viewBox="0 0 600 180" + > + <g + transform="scale(1, -1) translate(0, -90)" + > + <path + className={undefined} + d="M0 0Q0 0 60 45,Q120 90 180 135,Q240 180 300 225,Q360 270 420 135,Q480 0 480 0,Q480 0 420 -135,Q360 -270 300 -225,Q240 -180 180 -135,Q120 -90 60 -45,Q0 0 0 0Z" + strokeLinejoin="round" + strokeWidth={2} + /> + </g> + </svg> + <div + className={undefined} + > + <div + className="" + onMouseDown={[Function]} + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": "20%", + } + } + > + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + /> + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + > + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + > + <img + src="test-file-stub" + /> + </div> + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + > + <img + src="test-file-stub" + /> + </div> + </div> + </div> + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "left": "50%", + "width": undefined, + } + } + /> + <div + className="" + onMouseDown={[Function]} + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "left": "80%", + "width": "20%", + } + } + > + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + /> + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + > + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + > + <img + src="test-file-stub" + /> + </div> + <div + className="" + style={ + Object { + "alignContent": undefined, + "alignItems": undefined, + "alignSelf": undefined, + "flexBasis": undefined, + "flexDirection": undefined, + "flexGrow": undefined, + "flexShrink": undefined, + "flexWrap": undefined, + "height": undefined, + "justifyContent": undefined, + "width": undefined, + } + } + > + <img + src="test-file-stub" + /> + </div> + </div> + </div> + </div> + </div> + </div> +</div> +`; diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9c98edda9ba2632ad95e1760bc4fdafd12ed1289 --- /dev/null +++ b/test/unit/components/sound-editor.test.jsx @@ -0,0 +1,60 @@ +/* eslint-env jest */ +import React from 'react'; // eslint-disable-line no-unused-vars +import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers'; +import SoundEditor from '../../../src/components/sound-editor/sound-editor'; // eslint-disable-line no-unused-vars + +describe('Sound Editor Component', () => { + let props; + beforeEach(() => { + props = { + chunkLevels: [1, 2, 3], + name: 'sound name', + playhead: 0.5, + trimStart: 0.2, + trimEnd: 0.8, + onActivateTrim: jest.fn(), + onChangeName: jest.fn(), + onPlay: jest.fn(), + onSetTrimEnd: jest.fn(), + onSetTrimStart: jest.fn(), + onStop: jest.fn() + }; + }); + + test('matches snapshot', () => { + const component = componentWithIntl(<SoundEditor {...props} />); + expect(component.toJSON()).toMatchSnapshot(); + }); + + test('trim button appears when trims are 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 = 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 = 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 = 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 = mountWithIntl(<SoundEditor {...props} />); + wrapper.find('input') + .simulate('change', {target: {value: 'hello'}}) + .simulate('blur'); + expect(props.onChangeName).toHaveBeenCalled(); + }); +}); diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..39f1e19b07b2f3a9a24aee00136fdb7a2eefa0c3 --- /dev/null +++ b/test/unit/containers/sound-editor.test.jsx @@ -0,0 +1,93 @@ +/* eslint-env jest */ +import React from 'react'; // eslint-disable-line no-unused-vars +import {mountWithIntl} from '../../helpers/intl-helpers'; +import configureStore from 'redux-mock-store'; +import mockAudioBufferPlayer from '../../__mocks__/audio-buffer-player.js'; + +import SoundEditor from '../../../src/containers/sound-editor'; // eslint-disable-line no-unused-vars +// eslint-disable-next-line no-unused-vars +import SoundEditorComponent from '../../../src/components/sound-editor/sound-editor'; + +jest.mock('../../../src/lib/audio/audio-buffer-player', () => mockAudioBufferPlayer); + +describe('Sound Editor Container', () => { + const mockStore = configureStore(); + let store; + let soundIndex; + let soundBuffer; + let samples = new Float32Array([0, 0, 0]); // eslint-disable-line no-undef + let vm; + + beforeEach(() => { + soundIndex = 0; + soundBuffer = { + sampleRate: 0, + getChannelData: jest.fn(() => samples) + }; + vm = { + getSoundBuffer: jest.fn(() => soundBuffer), + renameSound: jest.fn(), + updateSoundBuffer: jest.fn(), + editingTarget: { + sprite: { + sounds: [{name: 'first name', id: 'first id'}] + } + } + }; + store = mockStore({vm}); + }); + + test('should pass the correct data to the component from the store', () => { + 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'); + expect(componentProps.chunkLevels).toEqual([0]); + expect(mockAudioBufferPlayer.instance.samples).toEqual(samples); + // Initial data + expect(componentProps.playhead).toEqual(null); + expect(componentProps.trimStart).toEqual(null); + expect(componentProps.trimEnd).toEqual(null); + + }); + + test('it plays when clicked and stops when clicked again', () => { + 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([]); + expect(mockAudioBufferPlayer.instance.stop.mock.calls).toEqual([]); + + component.props().onPlay(); + expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled(); + + // Mock the audio buffer player calling onUpdate + mockAudioBufferPlayer.instance.onUpdate(0.5); + expect(component.props().playhead).toEqual(0.5); + + component.props().onStop(); + expect(mockAudioBufferPlayer.instance.stop).toHaveBeenCalled(); + expect(component.props().playhead).toEqual(null); + }); + + test('it sets the component props for trimming and submits to the vm', () => { + const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const component = wrapper.find(SoundEditorComponent); + + component.props().onActivateTrim(); + expect(component.props().trimStart).not.toEqual(null); + expect(component.props().trimEnd).not.toEqual(null); + + component.props().onActivateTrim(); + expect(vm.updateSoundBuffer).toHaveBeenCalled(); + expect(component.props().trimStart).toEqual(null); + expect(component.props().trimEnd).toEqual(null); + }); + + test('it submits name changes to the vm', () => { + const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />); + const component = wrapper.find(SoundEditorComponent); + component.props().onChangeName('hello'); + expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello'); + }); +});