Merge pull request #578 from paulkaplan/sound-editor--trim

Add trimming to the sound editor
This commit is contained in:
Paul Kaplan authored and GitHub committed 2017-08-03 13:05:56 -04:00
commit a06a839e45
11 files changed
+716 -60

No files matched your search

+37 -33
View File
@@ -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;
@@ -0,0 +1 @@
<svg viewBox="-4 -1 16 16" xmlns="http://www.w3.org/2000/svg"><title>sissors</title><g fill="#4C97FF" fill-rule="evenodd"><path d="M5.874 10.102c-.437 0-.792.36-.792.806 0 .445.355.807.792.807.437 0 .793-.362.793-.807 0-.445-.356-.806-.793-.806m0 2.715C4.84 12.817 4 11.96 4 10.907 4 9.858 4.84 9 5.874 9c1.034 0 1.875.856 1.875 1.908 0 1.053-.842 1.91-1.876 1.91M1.874 10.102c-.437 0-.792.36-.792.806 0 .445.355.807.792.807.437 0 .793-.362.793-.807 0-.445-.356-.806-.793-.806m0 2.715C.84 12.817 0 11.96 0 10.907 0 9.858.84 9 1.874 9c1.034 0 1.875.856 1.875 1.908 0 1.053-.842 1.91-1.876 1.91M3.907 8.452c-.274 0-.495-.225-.495-.504 0-.275.22-.5.495-.5.27 0 .492.225.492.5 0 .28-.222.504-.493.504zm-.042-3.63l-.004.008L2.278.61C2.212.43 2.017.34 1.847.41l-.71.27C.578.905.293 1.545.51 2.12l2.27 6.06c.17.464.62.74 1.08.713.462.028.908-.248 1.083-.713l.088-.237-1.165-3.118zM5.564 6.396L4.4 3.276 5.44.61c.065-.18.26-.268.43-.198l.71.27c.563.222.845.862.628 1.436L5.564 6.396z"/></g></svg>

After

Width:  |  Height:  |  Size: 989 B

+20 -1
View File
@@ -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;
}
+82 -16
View File
@@ -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);
+2
View File
@@ -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);
+51 -10
View File
@@ -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)
};
};
+12
View File
@@ -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;
}
}
+45
View File
@@ -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
};
@@ -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>
`;
@@ -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();
});
});
@@ -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');
});
});