Refactor(challenges): Move step movement logic into epic
parent
f7cffb2f16
commit
98673fc316
|
@ -7,7 +7,8 @@ import ReactTransitionReplace from 'react-css-transition-replace';
|
|||
import LightBox from 'react-images';
|
||||
|
||||
import {
|
||||
goToStep,
|
||||
stepForward,
|
||||
stepBackward,
|
||||
completeAction,
|
||||
submitChallenge,
|
||||
openLightBoxImage,
|
||||
|
@ -38,12 +39,14 @@ const mapStateToProps = createSelector(
|
|||
step: description[currentIndex],
|
||||
steps: description,
|
||||
numOfSteps: description.length,
|
||||
isLastStep: currentIndex + 1 >= description.length,
|
||||
isGoingForward: currentIndex > previousIndex
|
||||
})
|
||||
);
|
||||
|
||||
const dispatchActions = {
|
||||
goToStep,
|
||||
stepForward,
|
||||
stepBackward,
|
||||
completeAction,
|
||||
submitChallenge,
|
||||
openLightBoxImage,
|
||||
|
@ -53,8 +56,6 @@ const dispatchActions = {
|
|||
export class StepChallenge extends PureComponent {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.handleNextClick = this.handleNextClick.bind(this);
|
||||
this.handleBackClick = this.handleBackClick.bind(this);
|
||||
this.handleLightBoxOpen = this.handleLightBoxOpen.bind(this);
|
||||
}
|
||||
static displayName = 'StepChallenge';
|
||||
|
@ -64,8 +65,10 @@ export class StepChallenge extends PureComponent {
|
|||
steps: PropTypes.array,
|
||||
isActionCompleted: PropTypes.bool,
|
||||
isGoingForward: PropTypes.bool,
|
||||
isLastStep: PropTypes.bool,
|
||||
numOfSteps: PropTypes.number,
|
||||
goToStep: PropTypes.func,
|
||||
stepForward: PropTypes.func,
|
||||
stepBackward: PropTypes.func,
|
||||
completeAction: PropTypes.func,
|
||||
submitChallenge: PropTypes.func,
|
||||
isLightBoxOpen: PropTypes.bool,
|
||||
|
@ -73,20 +76,6 @@ export class StepChallenge extends PureComponent {
|
|||
closeLightBoxImage: PropTypes.func
|
||||
};
|
||||
|
||||
handleNextClick() {
|
||||
const { numOfSteps, currentIndex, submitChallenge, goToStep } = this.props;
|
||||
const isLastStep = currentIndex + 1 >= numOfSteps;
|
||||
if (isLastStep) {
|
||||
return submitChallenge();
|
||||
}
|
||||
return goToStep(currentIndex + 1);
|
||||
}
|
||||
|
||||
handleBackClick() {
|
||||
const { currentIndex, goToStep } = this.props;
|
||||
goToStep(currentIndex - 1);
|
||||
}
|
||||
|
||||
handleLightBoxOpen(e) {
|
||||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
@ -119,7 +108,7 @@ export class StepChallenge extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderBackButton(index) {
|
||||
renderBackButton(index, stepBackward) {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<Col
|
||||
|
@ -135,15 +124,14 @@ export class StepChallenge extends PureComponent {
|
|||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
className='col-sm-4 col-xs-12'
|
||||
onClick={ this.handleBackClick }
|
||||
onClick={ stepBackward }
|
||||
>
|
||||
Go to my previous step
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
renderNextButton(hasAction, index, numOfSteps, isCompleted) {
|
||||
const isLastStep = index + 1 >= numOfSteps;
|
||||
renderNextButton(hasAction, isLastStep, isCompleted, stepForward) {
|
||||
const btnClass = classnames({
|
||||
'col-sm-4 col-xs-12': true,
|
||||
disabled: hasAction && !isCompleted
|
||||
|
@ -154,7 +142,7 @@ export class StepChallenge extends PureComponent {
|
|||
bsStyle='primary'
|
||||
className={ btnClass }
|
||||
disabled={ hasAction && !isCompleted }
|
||||
onClick={ this.handleNextClick }
|
||||
onClick={ stepForward }
|
||||
>
|
||||
{ isLastStep ? 'Finish challenge' : 'Go to my next step'}
|
||||
</Button>
|
||||
|
@ -166,7 +154,10 @@ export class StepChallenge extends PureComponent {
|
|||
currentIndex,
|
||||
numOfSteps,
|
||||
isActionCompleted,
|
||||
completeAction
|
||||
completeAction,
|
||||
isLastStep,
|
||||
stepForward,
|
||||
stepBackward
|
||||
}) {
|
||||
if (!Array.isArray(step)) {
|
||||
return null;
|
||||
|
@ -206,7 +197,7 @@ export class StepChallenge extends PureComponent {
|
|||
<div className='spacer' />
|
||||
<div className='challenge-button-block'>
|
||||
{ this.renderActionButton(action, completeAction) }
|
||||
{ this.renderBackButton(currentIndex) }
|
||||
{ this.renderBackButton(currentIndex, stepBackward) }
|
||||
<Col
|
||||
className='challenge-step-counter large-p text-center'
|
||||
sm={ 4 }
|
||||
|
@ -217,9 +208,9 @@ export class StepChallenge extends PureComponent {
|
|||
{
|
||||
this.renderNextButton(
|
||||
!!action,
|
||||
currentIndex,
|
||||
numOfSteps,
|
||||
isActionCompleted
|
||||
isLastStep,
|
||||
isActionCompleted,
|
||||
stepForward
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,8 @@ import { getMouse, loggerToStr } from '../utils';
|
|||
import types from './types';
|
||||
|
||||
// step
|
||||
export const stepForward = createAction(types.stepForward);
|
||||
export const stepBackward = createAction(types.stepBackward);
|
||||
export const goToStep = createAction(types.goToStep);
|
||||
export const completeAction = createAction(types.completeAction);
|
||||
export const openLightBoxImage = createAction(types.openLightBoxImage);
|
||||
|
|
|
@ -5,6 +5,7 @@ import answerSaga from './answer-saga';
|
|||
import resetChallengeSaga from './reset-challenge-saga';
|
||||
import bugSaga from './bug-saga';
|
||||
import mapUiSaga from './map-ui-saga';
|
||||
import stepChallengeEpic from './step-challenge-epic';
|
||||
|
||||
export * as actions from './actions';
|
||||
export reducer from './reducer';
|
||||
|
@ -19,5 +20,6 @@ export const sagas = [
|
|||
answerSaga,
|
||||
resetChallengeSaga,
|
||||
bugSaga,
|
||||
mapUiSaga
|
||||
mapUiSaga,
|
||||
stepChallengeEpic
|
||||
];
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import types from './types';
|
||||
import { goToStep, submitChallenge } from './actions';
|
||||
import { challengeSelector } from './selectors';
|
||||
import getActionsOfType from '../../../../utils/get-actions-of-type';
|
||||
|
||||
export default function stepChallengeEpic(actions, getState) {
|
||||
return getActionsOfType(
|
||||
actions,
|
||||
types.stepForward,
|
||||
types.stepBackward
|
||||
)
|
||||
.map(({ type }) => {
|
||||
const state = getState();
|
||||
const { challenge: { description = [] } } = challengeSelector(state);
|
||||
const { challengesApp: { currentIndex } } = state;
|
||||
const numOfSteps = description.length;
|
||||
const isLastStep = currentIndex + 1 >= numOfSteps;
|
||||
if (type === types.stepForward) {
|
||||
if (isLastStep) {
|
||||
return submitChallenge();
|
||||
}
|
||||
return goToStep(currentIndex + 1);
|
||||
}
|
||||
return goToStep(currentIndex - 1);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
import { Observable, config } from 'rx';
|
||||
import test from 'tape';
|
||||
import proxy from 'proxyquire';
|
||||
import sinon from 'sinon';
|
||||
import types from './types';
|
||||
|
||||
config.longStackSupport = true;
|
||||
const challengeSelectorStub = {};
|
||||
const stepChallengeEpic = proxy(
|
||||
'./step-challenge-epic',
|
||||
{ './selectors': challengeSelectorStub }
|
||||
);
|
||||
|
||||
const file = 'common/app/routes/challenges/redux/step-challenge-epic';
|
||||
test(file, function(t) {
|
||||
t.test('does not respond to random actions', t => {
|
||||
const actions = Observable.of({ type: 'NotTheMomma' });
|
||||
let called = false;
|
||||
stepChallengeEpic(actions, () => {})
|
||||
.subscribe(
|
||||
() => { called = true; },
|
||||
e => t.fail(e),
|
||||
() => {
|
||||
if (!called) {
|
||||
t.pass();
|
||||
} else {
|
||||
t.fail(new Error('epic should not respond'));
|
||||
}
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
});
|
||||
t.test('steps back', t => {
|
||||
const actions = Observable.of({ type: types.stepBackward });
|
||||
const state = { challengesApp: { currentIndex: 1 } };
|
||||
const onNextSpy = sinon.spy();
|
||||
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
|
||||
t.assert(_state === state, 'challenge selector not called with state');
|
||||
return {
|
||||
challenge: {
|
||||
description: new Array(2)
|
||||
}
|
||||
};
|
||||
});
|
||||
stepChallengeEpic(actions, () => state)
|
||||
.subscribe(
|
||||
onNextSpy,
|
||||
e => {
|
||||
throw e;
|
||||
},
|
||||
() => {
|
||||
t.assert(
|
||||
onNextSpy.calledOnce,
|
||||
'epic not called exactly once'
|
||||
);
|
||||
t.assert(
|
||||
onNextSpy.calledWithMatch({
|
||||
type: types.goToStep,
|
||||
payload: 0
|
||||
}),
|
||||
'Epic did not return the expected action'
|
||||
);
|
||||
delete challengeSelectorStub.challengeSelector;
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
});
|
||||
t.test('steps forward', t => {
|
||||
const actions = Observable.of({ type: types.stepForward });
|
||||
const state = { challengesApp: { currentIndex: 0 } };
|
||||
const onNextSpy = sinon.spy();
|
||||
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
|
||||
t.assert(_state === state, 'challenge selector not called with state');
|
||||
return {
|
||||
challenge: {
|
||||
description: new Array(2)
|
||||
}
|
||||
};
|
||||
});
|
||||
stepChallengeEpic(actions, () => state)
|
||||
.subscribe(
|
||||
onNextSpy,
|
||||
e => {
|
||||
throw e;
|
||||
},
|
||||
() => {
|
||||
t.assert(
|
||||
onNextSpy.calledOnce,
|
||||
'epic not called exactly once'
|
||||
);
|
||||
t.assert(
|
||||
onNextSpy.calledWithMatch({
|
||||
type: types.goToStep,
|
||||
payload: 1
|
||||
}),
|
||||
'Epic did not return the expected action'
|
||||
);
|
||||
delete challengeSelectorStub.challengeSelector;
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
});
|
||||
t.test('submits on last step forward', t => {
|
||||
const actions = Observable.of({ type: types.stepForward });
|
||||
const state = { challengesApp: { currentIndex: 1 } };
|
||||
const onNextSpy = sinon.spy();
|
||||
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
|
||||
t.assert(_state === state, 'challenge selector not called with state');
|
||||
return {
|
||||
challenge: {
|
||||
description: new Array(2)
|
||||
}
|
||||
};
|
||||
});
|
||||
stepChallengeEpic(actions, () => state)
|
||||
.subscribe(
|
||||
onNextSpy,
|
||||
e => {
|
||||
throw e;
|
||||
},
|
||||
() => {
|
||||
t.assert(
|
||||
onNextSpy.calledOnce,
|
||||
'epic not called exactly once'
|
||||
);
|
||||
t.assert(
|
||||
onNextSpy.calledWithMatch({
|
||||
type: types.submitChallenge
|
||||
}),
|
||||
'Epic did not return the expected action'
|
||||
);
|
||||
delete challengeSelectorStub.challengeSelector;
|
||||
t.end();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -2,6 +2,8 @@ import createTypes from '../../../utils/create-types';
|
|||
|
||||
export default createTypes([
|
||||
// step
|
||||
'stepForward',
|
||||
'stepBackward',
|
||||
'goToStep',
|
||||
'completeAction',
|
||||
'openLightBoxImage',
|
||||
|
|
|
@ -157,6 +157,7 @@
|
|||
"less": "^2.5.1",
|
||||
"loopback-component-explorer": "^2.1.1",
|
||||
"merge-stream": "^1.0.0",
|
||||
"proxyquire": "^1.7.10",
|
||||
"rev-del": "^1.0.5",
|
||||
"sinon": "^1.17.3",
|
||||
"sort-keys": "^1.1.1",
|
||||
|
|
Loading…
Reference in New Issue