Skip to content

Commit 3c05b9b

Browse files
author
Madelyn Kasula
authored
Merge pull request #339 from code-dot-org/unit-tests
Setup React unit tests + unit test <Button/>
2 parents 27b53e5 + 3484b59 commit 3c05b9b

File tree

9 files changed

+1028
-55
lines changed

9 files changed

+1028
-55
lines changed

package.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@
55
"main": "dist/main.js",
66
"jest": {
77
"testEnvironment": "node",
8+
"moduleFileExtensions": [
9+
"js",
10+
"jsx"
11+
],
812
"moduleNameMapper": {
9-
".+\\.(bin|jpg|jpeg|png|mp3|ogg|wav)$": "identity-obj-proxy",
10-
"^@ml(.*)$": "<rootDir>/src/$1"
13+
".+\\.(bin|jpg|jpeg|png|mp3|ogg|wav|gif)$": "identity-obj-proxy",
14+
"^@ml(.*)$": "<rootDir>/src/$1",
15+
"^@public(.*)$": "<rootDir>/public/$1"
16+
},
17+
"setupTestFrameworkScriptFile": "<rootDir>/test/setup.js",
18+
"globals": {
19+
"window": {},
20+
"location": {}
1121
}
1222
},
1323
"scripts": {
@@ -55,6 +65,8 @@
5565
"clean-webpack-plugin": "^3.0.0",
5666
"copy-webpack-plugin": "^5.0.5",
5767
"css-loader": "^3.2.0",
68+
"enzyme": "^3.9.0",
69+
"enzyme-adapter-react-15.4": "^1.3.1",
5870
"eslint": ">=4.18.2",
5971
"eslint-plugin-react": "^7.11.0",
6072
"file-loader": "^4.2.0",
@@ -69,8 +81,10 @@
6981
"query-string": "4.1.0",
7082
"radium": "^0.25.2",
7183
"react": "~15.4.0",
84+
"react-addons-test-utils": "~15.4.0",
7285
"react-dom": "~15.4.0",
7386
"react-test-renderer": "~15.4.0",
87+
"sinon": "^7.5.0",
7488
"style-loader": "^1.0.0",
7589
"svm": "uponthesun/svmjs",
7690
"url-loader": "^2.2.0",

src/oceans/init.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import constants, {Modes} from './constants';
66
import {setInitialState, setSetStateCallback} from './state';
77
import {render as renderCanvas} from './renderer';
88
import {toMode} from './toMode';
9-
import {loadSounds, injectSoundAPIs} from './models/soundLibrary';
9+
import * as soundLibrary from './models/soundLibrary';
1010

1111
//
1212
// Required in options:
@@ -16,15 +16,15 @@ import {loadSounds, injectSoundAPIs} from './models/soundLibrary';
1616
// onContinue
1717
//
1818
export const initAll = function(options) {
19-
const { canvas, backgroundCanvas } = options;
19+
const {canvas, backgroundCanvas} = options;
2020

2121
canvas.width = backgroundCanvas.width = constants.canvasWidth;
2222
canvas.height = backgroundCanvas.height = constants.canvasHeight;
2323

2424
// Pass registerSound and playSound from options to soundLibrary.
25-
injectSoundAPIs(options);
25+
soundLibrary.injectSoundAPIs(options);
2626

27-
loadSounds();
27+
soundLibrary.loadSounds();
2828

2929
// Set initial state for UI elements.
3030
setInitialState({

src/oceans/models/guide.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ const guides = [
467467
}
468468
];
469469

470-
export function getCurrentGuide() {
470+
const getCurrentGuide = () => {
471471
if (queryStrFor('guide') === 'off') {
472472
return null;
473473
}
@@ -493,9 +493,9 @@ export function getCurrentGuide() {
493493
}
494494

495495
return null;
496-
}
496+
};
497497

498-
export function dismissCurrentGuide() {
498+
const dismissCurrentGuide = () => {
499499
const currentGuide = getCurrentGuide();
500500

501501
// If we have a current guide, and it's actually showing (rather than still
@@ -511,4 +511,9 @@ export function dismissCurrentGuide() {
511511
}
512512

513513
return false;
514-
}
514+
};
515+
516+
export default {
517+
getCurrentGuide,
518+
dismissCurrentGuide
519+
};

src/oceans/models/soundLibrary.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,24 @@ const soundLibrary = {
3939
]
4040
};
4141

42-
export const injectSoundAPIs = ({registerSound, playSound}) => {
42+
const injectSoundAPIs = ({registerSound, playSound}) => {
4343
registerSoundAPI = registerSound;
4444
playSoundAPI = playSound;
4545
};
4646

47-
export const loadSounds = () => {
47+
const loadSounds = () => {
4848
Object.entries(soundLibrary).forEach(([_, category]) =>
4949
category.forEach(sound => registerSoundAPI({id: sound, mp3: sound}))
5050
);
5151
};
5252

53-
export const playSound = (categoryName, volume = undefined) => {
53+
const playSound = (categoryName, volume = undefined) => {
5454
const index = randomInt(0, soundLibrary[categoryName].length - 1);
5555
playSoundAPI(soundLibrary[categoryName][index], {volume: volume || 1.0});
5656
};
57+
58+
export default {
59+
injectSoundAPIs,
60+
loadSounds,
61+
playSound
62+
};

src/oceans/renderer.js

Lines changed: 3 additions & 3 deletions
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 {playSound} from './models/soundLibrary';
30+
import * as 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';
@@ -485,9 +485,9 @@ const drawPredictBot = state => {
485485

486486
if (scannerImg !== lastScannerImg) {
487487
if (scannerImg === botImages.likeScanner) {
488-
playSound('sortyes');
488+
soundLibrary.playSound('sortyes');
489489
} else if (scannerImg === botImages.dislikeScanner) {
490-
playSound('sortno');
490+
soundLibrary.playSound('sortno');
491491
}
492492
lastScannerImg = scannerImg;
493493
}

src/oceans/ui.jsx

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {getCurrentGuide, dismissCurrentGuide} from './models/guide';
27-
import {playSound} from './models/soundLibrary';
26+
import * as guide from './models/guide';
27+
import * as soundLibrary from './models/soundLibrary';
2828
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
2929
import {
3030
faPlay,
@@ -662,7 +662,7 @@ class Content extends React.Component {
662662
}
663663
}
664664

665-
let Button = class Button extends React.Component {
665+
let UnwrappedButton = class Button extends React.Component {
666666
static propTypes = {
667667
className: PropTypes.string,
668668
style: PropTypes.object,
@@ -671,33 +671,30 @@ let Button = class Button extends React.Component {
671671
sound: PropTypes.string
672672
};
673673

674-
onClick(event) {
675-
dismissCurrentGuide();
674+
onClick = event => {
675+
guide.dismissCurrentGuide();
676676
const clickReturnValue = this.props.onClick(event);
677677

678678
if (clickReturnValue !== false) {
679-
if (this.props.sound && clickReturnValue !== false) {
680-
playSound(this.props.sound);
681-
} else {
682-
playSound('other');
683-
}
679+
const sound = this.props.sound || 'other';
680+
soundLibrary.playSound(sound);
684681
}
685-
}
682+
};
686683

687684
render() {
688685
return (
689686
<button
690687
type="button"
691688
className={this.props.className}
692689
style={[styles.button, this.props.style]}
693-
onClick={event => this.onClick(event)}
690+
onClick={this.onClick}
694691
>
695692
{this.props.children}
696693
</button>
697694
);
698695
}
699696
};
700-
Button = Radium(Button);
697+
export const Button = Radium(UnwrappedButton); // Exported for unit tests.
701698

702699
let ConfirmationDialog = class ConfirmationDialog extends React.Component {
703700
static propTypes = {
@@ -1251,11 +1248,11 @@ let Pond = class Pond extends React.Component {
12511248
if (state.showRecallFish) {
12521249
currentFishSet = state.recallFish;
12531250
nextFishSet = state.pondFish;
1254-
playSound('yes');
1251+
soundLibrary.playSound('yes');
12551252
} else {
12561253
currentFishSet = state.pondFish;
12571254
nextFishSet = state.recallFish;
1258-
playSound('no');
1255+
soundLibrary.playSound('no');
12591256
}
12601257

12611258
// Don't call arrangeFish if fish have already been arranged.
@@ -1275,7 +1272,7 @@ let Pond = class Pond extends React.Component {
12751272

12761273
onPondClick = e => {
12771274
// Don't allow pond clicks if a Guide is currently showing.
1278-
if (getCurrentGuide()) {
1275+
if (guide.getCurrentGuide()) {
12791276
return;
12801277
}
12811278

@@ -1328,7 +1325,7 @@ let Pond = class Pond extends React.Component {
13281325
}
13291326
});
13301327
fishClicked = true;
1331-
playSound('yes');
1328+
soundLibrary.playSound('yes');
13321329

13331330
if (
13341331
state.appMode === AppMode.FishShort ||
@@ -1351,7 +1348,7 @@ let Pond = class Pond extends React.Component {
13511348

13521349
if (!fishClicked) {
13531350
setState({pondClickedFish: null});
1354-
playSound('no');
1351+
soundLibrary.playSound('no');
13551352
}
13561353
}
13571354
};
@@ -1368,9 +1365,9 @@ let Pond = class Pond extends React.Component {
13681365
});
13691366

13701367
if (state.pondPanelShowing) {
1371-
playSound('sortno');
1368+
soundLibrary.playSound('sortno');
13721369
} else {
1373-
playSound('sortyes');
1370+
soundLibrary.playSound('sortyes');
13741371
}
13751372
}
13761373

@@ -1480,15 +1477,15 @@ let Guide = class Guide extends React.Component {
14801477
}
14811478

14821479
dismissGuideClick() {
1483-
const dismissed = dismissCurrentGuide();
1480+
const dismissed = guide.dismissCurrentGuide();
14841481
if (dismissed) {
1485-
playSound('other');
1482+
soundLibrary.playSound('other');
14861483
}
14871484
}
14881485

14891486
render() {
14901487
const state = getState();
1491-
const currentGuide = getCurrentGuide();
1488+
const currentGuide = guide.getCurrentGuide();
14921489

14931490
let guideBgStyle = [styles.guideBackground];
14941491
if (currentGuide) {
@@ -1505,7 +1502,7 @@ let Guide = class Guide extends React.Component {
15051502
// Start playing the typing sounds.
15061503
if (!state.guideShowing && !state.guideTypingTimer && currentGuide) {
15071504
const guideTypingTimer = setInterval(() => {
1508-
playSound('no', 0.5);
1505+
soundLibrary.playSound('no', 0.5);
15091506
}, 1000 / 10);
15101507
setState({guideTypingTimer});
15111508
}

test/setup.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Adapter from 'enzyme-adapter-react-15.4';
2+
import enzyme from 'enzyme';
3+
4+
enzyme.configure({adapter: new Adapter()});

test/unit/oceans/ui.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import {shallow} from 'enzyme';
4+
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';
8+
9+
const DEFAULT_PROPS = {
10+
// radiumConfig.userAgent is required because our unit tests run in the "node" testEnvironment
11+
// (this is necessary for our tensorflow tests), so Radium thinks we are server-side rendering.
12+
// See also: https://github.com/FormidableLabs/radium/tree/master/docs/api#configuseragent
13+
radiumConfig: {userAgent: 'user-agent'}
14+
};
15+
16+
describe('Button', () => {
17+
let onClickMock, playSoundSpy;
18+
19+
beforeEach(() => {
20+
soundLibrary.injectSoundAPIs({playSound: sinon.fake()});
21+
playSoundSpy = sinon.spy(soundLibrary, 'playSound');
22+
onClickMock = sinon.fake.returns(false);
23+
});
24+
25+
afterEach(() => {
26+
soundLibrary.playSound.restore();
27+
});
28+
29+
it('dismisses guide on click', () => {
30+
const dismissCurrentGuideSpy = sinon.spy(guide, 'dismissCurrentGuide');
31+
const wrapper = shallow(
32+
<Button {...DEFAULT_PROPS} onClick={onClickMock} />
33+
);
34+
35+
wrapper.simulate('click');
36+
expect(dismissCurrentGuideSpy.calledOnce);
37+
38+
guide.dismissCurrentGuide.restore();
39+
});
40+
41+
it('calls onClick prop on click', () => {
42+
const wrapper = shallow(
43+
<Button {...DEFAULT_PROPS} onClick={onClickMock} />
44+
);
45+
46+
wrapper.simulate('click');
47+
expect(onClickMock.calledOnce);
48+
});
49+
50+
it('does not play a sound if onClick prop returns false', () => {
51+
const wrapper = shallow(
52+
<Button {...DEFAULT_PROPS} onClick={onClickMock} />
53+
);
54+
55+
wrapper.simulate('click');
56+
expect(!playSoundSpy.called);
57+
});
58+
59+
describe('onClick prop does not return false', () => {
60+
it('plays sound if supplied', () => {
61+
onClickMock = sinon.fake.returns(true);
62+
const wrapper = shallow(
63+
<Button {...DEFAULT_PROPS} onClick={onClickMock} sound="sortyes" />
64+
);
65+
66+
wrapper.simulate('click');
67+
expect(playSoundSpy.withArgs('sortyes').calledOnce);
68+
});
69+
70+
it('plays "other" sound if sound not supplied', () => {
71+
onClickMock = sinon.fake.returns(true);
72+
const wrapper = shallow(
73+
<Button {...DEFAULT_PROPS} onClick={onClickMock} />
74+
);
75+
76+
wrapper.simulate('click');
77+
expect(playSoundSpy.withArgs('other').calledOnce);
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)