Skip to content

Commit e205ba0

Browse files
author
Madelyn Kasula
authored
Merge pull request #341 from code-dot-org/testing
ui.jsx unit testing, part 2
2 parents e5f21c9 + 50b39ea commit e205ba0

File tree

6 files changed

+163
-38
lines changed

6 files changed

+163
-38
lines changed

src/oceans/init.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import UI from './ui';
55
import constants, {Modes} from './constants';
66
import {setInitialState, setSetStateCallback} from './state';
77
import {render as renderCanvas} from './renderer';
8-
import {toMode} from './toMode';
9-
import * as soundLibrary from './models/soundLibrary';
8+
import modeHelpers from './modeHelpers';
9+
import soundLibrary from './models/soundLibrary';
1010

1111
//
1212
// Required in options:
@@ -33,7 +33,7 @@ export const initAll = function(options) {
3333
});
3434

3535
// Initialize our first model.
36-
toMode(Modes.Loading);
36+
modeHelpers.toMode(Modes.Loading);
3737

3838
// Start the canvas renderer. It will self-perpetute by calling
3939
// requestAnimationFrame on itself.
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {init as initModel} from './models';
22
import {setState} from './state';
33

4-
export const toMode = mode => {
4+
const toMode = mode => {
55
const state = setState({currentMode: mode});
66
initModel(state);
77
};
88

9-
9+
export default {
10+
toMode
11+
};

src/oceans/models/loading.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {getState, setState} from '../state';
44
import {AppMode, Modes} from '../constants';
55
import {initFishData} from '../../utils/fishData';
66
import {getAppMode, $time, finishLoading} from '../helpers';
7-
import {toMode} from '../toMode';
7+
import modeHelpers from '../modeHelpers';
88
import SimpleTrainer from '../../utils/SimpleTrainer';
99

1010
export const init = async () => {
@@ -45,6 +45,5 @@ export const init = async () => {
4545
mode = Modes.Words;
4646
}
4747

48-
// Ensure that we show the loading UI for at least 2 seconds, to avoid a flash.
49-
finishLoading(startTime, () => toMode(mode));
48+
finishLoading(startTime, () => modeHelpers.toMode(mode));
5049
};

src/oceans/renderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import aiBotNo from '@public/images/ai-bot/ai-bot-no.png';
2727
import redScanner from '@public/images/ai-bot/red-scanner.png';
2828
import greenScanner from '@public/images/ai-bot/green-scanner.png';
2929
import blueScanner from '@public/images/ai-bot/blue-scanner.png';
30-
import * as soundLibrary from './models/soundLibrary';
30+
import soundLibrary from './models/soundLibrary';
3131
import bluePredictionFrame from '@public/images/blue-prediction-frame.png';
3232
import questionIcon from '@public/images/question-icon.png';
3333
import greenPredictionFrame from '@public/images/green-prediction-frame.png';

src/oceans/ui.jsx

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Radium from 'radium';
44
import _ from 'lodash';
55
import {getState, setState} from './state';
66
import constants, {AppMode, Modes} from './constants';
7-
import {toMode} from './toMode';
7+
import modeHelpers from './modeHelpers';
88
import {
99
$time,
1010
currentRunTime,
@@ -23,8 +23,8 @@ import arrowDownImage from '@public/images/arrow-down.png';
2323
import snail from '@public/images/snail-large.png';
2424
import loadingGif from '@public/images/loading.gif';
2525
import Typist from 'react-typist';
26-
import * as guide from './models/guide';
27-
import * as soundLibrary from './models/soundLibrary';
26+
import guide from './models/guide';
27+
import soundLibrary from './models/soundLibrary';
2828
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
2929
import {
3030
faPlay,
@@ -696,10 +696,10 @@ let UnwrappedButton = class Button extends React.Component {
696696
};
697697
export const Button = Radium(UnwrappedButton); // Exported for unit tests.
698698

699-
let ConfirmationDialog = class ConfirmationDialog extends React.Component {
699+
let UnwrappedConfirmationDialog = class ConfirmationDialog extends React.Component {
700700
static propTypes = {
701-
onYesClick: PropTypes.func,
702-
onNoClick: PropTypes.func
701+
onYesClick: PropTypes.func.isRequired,
702+
onNoClick: PropTypes.func.isRequired
703703
};
704704

705705
render() {
@@ -742,19 +742,19 @@ let ConfirmationDialog = class ConfirmationDialog extends React.Component {
742742
);
743743
}
744744
};
745-
ConfirmationDialog = Radium(ConfirmationDialog);
745+
export const ConfirmationDialog = Radium(UnwrappedConfirmationDialog); // Exported for unit tests.
746746

747-
let Loading = class Loading extends React.Component {
747+
class Loading extends React.Component {
748748
render() {
749749
return (
750750
<Body>
751751
<img src={loadingGif} style={styles.loading} />
752752
</Body>
753753
);
754754
}
755-
};
755+
}
756756

757-
const wordSet = {
757+
export const wordSet = {
758758
short: {
759759
text: ['What type of fish do you want to train A.I. to detect?'],
760760
choices: [
@@ -788,12 +788,17 @@ const wordSet = {
788788
}
789789
};
790790

791-
let Words = class Words extends React.Component {
791+
let UnwrappedWords = class Words extends React.Component {
792792
constructor(props) {
793793
super(props);
794794

795795
// Randomize word choices in each set, merge the sets, and set as state.
796796
const appMode = getState().appMode;
797+
798+
if (!wordSet[appMode]) {
799+
throw `Could not find a set of choices in wordSet for appMode '${appMode}'`;
800+
}
801+
797802
const appModeWordSet = wordSet[appMode].choices;
798803
let choices = [];
799804
let maxSize = 0;
@@ -823,7 +828,7 @@ let Words = class Words extends React.Component {
823828
word,
824829
trainingQuestion: `Is this fish “${word.toLowerCase()}”?`
825830
});
826-
toMode(Modes.Training);
831+
modeHelpers.toMode(Modes.Training);
827832

828833
// Report an analytics event for the word chosen.
829834
if (window.trackEvent) {
@@ -868,7 +873,7 @@ let Words = class Words extends React.Component {
868873
);
869874
}
870875
};
871-
Words = Radium(Words);
876+
export const Words = Radium(UnwrappedWords); // Exported for unit tests.
872877

873878
let Train = class Train extends React.Component {
874879
state = {
@@ -945,7 +950,7 @@ let Train = class Train extends React.Component {
945950
</div>
946951
<Button
947952
style={styles.continueButton}
948-
onClick={() => toMode(Modes.Predicting)}
953+
onClick={() => modeHelpers.toMode(Modes.Predicting)}
949954
>
950955
Continue
951956
</Button>
@@ -982,7 +987,7 @@ let Predict = class Predict extends React.Component {
982987
state.onContinue();
983988
} else {
984989
setState({showRecallFish: false});
985-
toMode(Modes.Pond);
990+
modeHelpers.toMode(Modes.Pond);
986991
}
987992
};
988993

@@ -1430,7 +1435,7 @@ let Pond = class Pond extends React.Component {
14301435
onClick={() => {
14311436
setState({pondClickedFish: null, pondPanelShowing: false});
14321437
resetTraining(state);
1433-
toMode(Modes.Words);
1438+
modeHelpers.toMode(Modes.Words);
14341439
}}
14351440
>
14361441
New Word
@@ -1454,7 +1459,7 @@ let Pond = class Pond extends React.Component {
14541459
<Button
14551460
style={styles.backButton}
14561461
onClick={() => {
1457-
toMode(Modes.Training);
1462+
modeHelpers.toMode(Modes.Training);
14581463
setState({pondClickedFish: null, pondPanelShowing: false});
14591464
}}
14601465
>

test/unit/oceans/ui.test.js

Lines changed: 131 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import React from 'react';
22
import ReactDOM from 'react-dom';
33
import {shallow} from 'enzyme';
44
import sinon from 'sinon';
5-
import {Button} from '@ml/oceans/ui';
6-
import * as guide from '@ml/oceans/models/guide';
7-
import * as soundLibrary from '@ml/oceans/models/soundLibrary';
5+
import {Button, ConfirmationDialog, Words, wordSet} from '@ml/oceans/ui';
6+
import guide from '@ml/oceans/models/guide';
7+
import soundLibrary from '@ml/oceans/models/soundLibrary';
8+
import modeHelpers from '@ml/oceans/modeHelpers';
9+
import {setState, getState, resetState} from '@ml/oceans/state';
10+
import {AppMode, Modes} from '@ml/oceans/constants';
811

912
const DEFAULT_PROPS = {
1013
// radiumConfig.userAgent is required because our unit tests run in the "node" testEnvironment
@@ -14,11 +17,10 @@ const DEFAULT_PROPS = {
1417
};
1518

1619
describe('Button', () => {
17-
let onClickMock, playSoundSpy;
20+
let onClickMock, playSoundStub;
1821

1922
beforeEach(() => {
20-
soundLibrary.injectSoundAPIs({playSound: sinon.fake()});
21-
playSoundSpy = sinon.spy(soundLibrary, 'playSound');
23+
playSoundStub = sinon.stub(soundLibrary, 'playSound');
2224
onClickMock = sinon.fake.returns(false);
2325
});
2426

@@ -27,13 +29,13 @@ describe('Button', () => {
2729
});
2830

2931
it('dismisses guide on click', () => {
30-
const dismissCurrentGuideSpy = sinon.spy(guide, 'dismissCurrentGuide');
32+
const dismissCurrentGuideSpy = sinon.stub(guide, 'dismissCurrentGuide');
3133
const wrapper = shallow(
3234
<Button {...DEFAULT_PROPS} onClick={onClickMock} />
3335
);
3436

3537
wrapper.simulate('click');
36-
expect(dismissCurrentGuideSpy.calledOnce);
38+
expect(dismissCurrentGuideSpy.callCount).toEqual(1);
3739

3840
guide.dismissCurrentGuide.restore();
3941
});
@@ -44,7 +46,7 @@ describe('Button', () => {
4446
);
4547

4648
wrapper.simulate('click');
47-
expect(onClickMock.calledOnce);
49+
expect(onClickMock.callCount).toEqual(1);
4850
});
4951

5052
it('does not play a sound if onClick prop returns false', () => {
@@ -53,7 +55,7 @@ describe('Button', () => {
5355
);
5456

5557
wrapper.simulate('click');
56-
expect(!playSoundSpy.called);
58+
expect(playSoundStub.callCount).toEqual(0);
5759
});
5860

5961
describe('onClick prop does not return false', () => {
@@ -64,7 +66,7 @@ describe('Button', () => {
6466
);
6567

6668
wrapper.simulate('click');
67-
expect(playSoundSpy.withArgs('sortyes').calledOnce);
69+
expect(playSoundStub.withArgs('sortyes').calledOnce).toBeTruthy();
6870
});
6971

7072
it('plays "other" sound if sound not supplied', () => {
@@ -74,7 +76,124 @@ describe('Button', () => {
7476
);
7577

7678
wrapper.simulate('click');
77-
expect(playSoundSpy.withArgs('other').calledOnce);
79+
expect(playSoundStub.withArgs('other').calledOnce).toBeTruthy();
80+
});
81+
});
82+
});
83+
84+
describe('ConfirmationDialog', () => {
85+
let onYesClickSpy, onNoClickSpy;
86+
87+
beforeEach(() => {
88+
onYesClickSpy = sinon.spy();
89+
onNoClickSpy = sinon.spy();
90+
});
91+
92+
it('calls onYesClick prop when erase button is clicked', () => {
93+
const wrapper = shallow(
94+
<ConfirmationDialog
95+
{...DEFAULT_PROPS}
96+
onYesClick={onYesClickSpy}
97+
onNoClick={onNoClickSpy}
98+
/>
99+
);
100+
101+
const eraseButton = wrapper.find('Button').at(0);
102+
eraseButton.simulate('click');
103+
expect(onYesClickSpy.callCount).toEqual(1);
104+
expect(onNoClickSpy.callCount).toEqual(0);
105+
});
106+
107+
it('calls onNoClick prop when cancel button is clicked', () => {
108+
const wrapper = shallow(
109+
<ConfirmationDialog
110+
{...DEFAULT_PROPS}
111+
onYesClick={onYesClickSpy}
112+
onNoClick={onNoClickSpy}
113+
/>
114+
);
115+
116+
const cancelButton = wrapper.find('Button').at(1);
117+
cancelButton.simulate('click');
118+
expect(onNoClickSpy.callCount).toEqual(1);
119+
expect(onYesClickSpy.callCount).toEqual(0);
120+
});
121+
});
122+
123+
describe('Words', () => {
124+
afterEach(() => {
125+
resetState();
126+
});
127+
128+
it('selects the set of words based on the current appMode', () => {
129+
const appMode = AppMode.FishShort;
130+
setState({appMode});
131+
const wrapper = shallow(<Words {...DEFAULT_PROPS} />);
132+
const wordChoices = wrapper.state().choices;
133+
// Flatten the array of choices as we know it is 2D.
134+
const expectedChoices = [].concat.apply([], wordSet[appMode].choices);
135+
136+
// We expect the actual word choices to be randomly sorted
137+
expect(wordChoices).not.toEqual(expectedChoices);
138+
expect(wordChoices.sort()).toEqual(expectedChoices.sort());
139+
});
140+
141+
it('throws an error if no set of words are found for the current appMode', () => {
142+
setState({appMode: 'a-fake-one!'});
143+
144+
expect(() => {
145+
shallow(<Words {...DEFAULT_PROPS} />);
146+
}).toThrowError(
147+
new Error(
148+
"Could not find a set of choices in wordSet for appMode 'a-fake-one!'"
149+
)
150+
);
151+
});
152+
153+
describe('onChangeWord', () => {
154+
let toModeStub;
155+
156+
beforeEach(() => {
157+
toModeStub = sinon.stub(modeHelpers, 'toMode');
158+
setState({appMode: AppMode.FishLong});
159+
});
160+
161+
afterEach(() => {
162+
modeHelpers.toMode.restore();
163+
resetState();
164+
});
165+
166+
it('sets the selected word in state', () => {
167+
const wrapper = shallow(<Words {...DEFAULT_PROPS} />);
168+
169+
const i = 1;
170+
wrapper
171+
.find('Button')
172+
.at(i)
173+
.simulate('click');
174+
const expectedWord = wrapper.state().choices[i];
175+
expect(getState().word).toEqual(expectedWord);
176+
});
177+
178+
it('transitions to Modes.Training', () => {
179+
const wrapper = shallow(<Words {...DEFAULT_PROPS} />);
180+
181+
wrapper
182+
.find('Button')
183+
.at(0)
184+
.simulate('click');
185+
expect(toModeStub.withArgs(Modes.Training).calledOnce).toBeTruthy();
186+
});
187+
188+
it('reports an analytics event if window.trackEvent is provided', () => {
189+
window.trackEvent = sinon.spy();
190+
const wrapper = shallow(<Words {...DEFAULT_PROPS} />);
191+
192+
wrapper
193+
.find('Button')
194+
.at(0)
195+
.simulate('click');
196+
expect(window.trackEvent.calledOnce).toBeTruthy();
78197
});
79198
});
80199
});

0 commit comments

Comments
 (0)