@coorpacademy/progression-engine
Version:
525 lines (519 loc) • 18.5 kB
JavaScript
"use strict";
var _get2 = _interopRequireDefault(require("lodash/fp/get"));
var _pick2 = _interopRequireDefault(require("lodash/fp/pick"));
var _omit2 = _interopRequireDefault(require("lodash/fp/omit"));
var _ava = _interopRequireDefault(require("ava"));
var _updateState = _interopRequireDefault(require("../update-state"));
var _config = require("../config");
var _states = require("../compute-next-step/test/fixtures/states");
var _states2 = require("./fixtures/states");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const engine = {
ref: 'microlearning',
version: '1'
};
const config = (0, _config.getConfig)(engine);
function contentResourceViewedAction(contentType, contentRef, lessonRef) {
return Object.freeze({
type: 'resource',
payload: {
resource: {
ref: lessonRef,
type: 'video',
version: '1'
},
content: {
ref: contentRef,
type: contentType,
version: '1'
}
}
});
}
const wrongAnswerAction = Object.freeze({
type: 'answer',
payload: {
answer: ['foo'],
content: {
ref: '1.A1.2',
type: 'slide'
},
nextContent: {
ref: '1.A1.1',
type: 'slide'
},
isCorrect: false,
godMode: false,
instructions: null
}
});
const extraLifeAcceptedAction = Object.freeze({
type: 'extraLifeAccepted',
payload: {
content: {
type: 'node',
ref: 'extraLife'
},
nextContent: {
ref: '1.A1.1',
type: 'slide'
},
instructions: null
}
});
// test('should return a valid state when there are no actions', t => {
// const content: Content = Object.freeze({
// type: 'chapter',
// ref: '1.A1',
// version: '1.0.0'
// });
// const initialContent: Content = Object.freeze({
// type: 'slide',
// ref: '1.A1.1'
// });
// const progression = createProgression(engine, content, {livesDisabled: false}, initialContent);
// const state = updateState(engine, progression.state, []);
// t.is(state.lives, 1);
// t.is(state.stars, 0);
// t.true(state.isCorrect);
// t.is(state.content, undefined);
// t.deepEqual(state.nextContent, initialContent);
// t.deepEqual(state.slides, []);
// t.deepEqual(state.requestedClues, []);
// t.deepEqual(state.viewedResources, []);
// t.deepEqual(state.step, {current: 1});
// });
(0, _ava.default)('should return a valid state when there are no actions and state is empty', t => {
// $FlowFixMe
const state = (0, _updateState.default)((0, _config.getConfig)(engine), {}, []);
t.is(state.lives, 1);
t.is(state.stars, 0);
t.true(state.isCorrect);
t.is(state.content, undefined);
t.is(state.nextContent, undefined);
t.deepEqual(state.slides, []);
t.deepEqual(state.requestedClues, []);
t.deepEqual(state.viewedResources, []);
t.deepEqual(state.step, {
current: 1
});
});
(0, _ava.default)('should update state when answering the first question correctly', t => {
const state = Object.freeze(_states2.stateForFirstSlide);
const action = Object.freeze({
type: 'answer',
payload: {
answer: ['foo'],
content: {
ref: '1.A1.1',
type: 'slide'
},
nextContent: {
ref: '1.A1.2',
type: 'slide'
},
isCorrect: true,
godMode: false,
instructions: null
}
});
const pickUnchangedFields = (0, _pick2.default)(['lives', 'requestedClues', 'viewedResources']);
const newState = (0, _updateState.default)(config, state, [action]);
t.true(newState.isCorrect, '`isCorrect` should reflect the `isCorrect` field from the action payload');
t.deepEqual(newState.slides, ['1.A1.1'], 'answered slide should have been stored in `slides`');
t.deepEqual(newState.step, {
current: 2
}, 'step progression is wrong');
t.is(newState.stars, 4, 'step progression is wrong');
t.deepEqual(newState.content, state.nextContent, '`content` should be updated to be the previous `nextContent`');
t.deepEqual(newState.nextContent, action.payload.nextContent, "`nextContent` should be updated to be the action's `nextContent`");
t.deepEqual(pickUnchangedFields(newState), pickUnchangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should update state when answering the another question correctly', t => {
const state = Object.freeze(_states2.stateForSecondSlide);
const action = Object.freeze({
type: 'answer',
payload: {
answer: ['foo'],
content: {
ref: '1.A1.2',
type: 'slide'
},
nextContent: {
ref: '1.A1.1',
type: 'slide'
},
isCorrect: true,
godMode: false,
instructions: null
}
});
const pickUnchangedFields = (0, _pick2.default)(['lives', 'requestedClues', 'viewedResources']);
const newState = (0, _updateState.default)(config, state, [action]);
t.true(newState.isCorrect, '`isCorrect` should reflect the `isCorrect` field from the action payload');
t.deepEqual(newState.slides, ['1.A1.4', '1.A1.2'], 'answered slide should have been stored in `slides`');
t.deepEqual(newState.step, {
current: 3
}, 'step progression is wrong');
t.is(newState.stars, 8, 'step progression is wrong');
t.deepEqual(newState.allAnswers, [{
slideRef: '1.A1.4',
isCorrect: true,
answer: ['bar']
}, {
slideRef: '1.A1.2',
isCorrect: true,
answer: ['foo']
}], 'answer should have been stored in `allAnswers`');
t.deepEqual(newState.content, state.nextContent, '`content` should be updated to be the previous `nextContent`');
t.deepEqual(newState.nextContent, action.payload.nextContent, "`nextContent` should be updated to be the action's `nextContent`");
t.deepEqual(pickUnchangedFields(newState), pickUnchangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should update state when answering a question incorrectly', t => {
const state = Object.freeze(_states2.stateForSecondSlide);
const pickUnchangedFields = (0, _pick2.default)(['stars', 'requestedClues', 'viewedResources']);
const newState = (0, _updateState.default)(config, state, [wrongAnswerAction]);
t.is(newState.hasViewedAResourceAtThisStep, false);
t.true(newState.lives === 0, '`lives` should have been decremented');
t.false(newState.isCorrect, '`isCorrect` should reflect the `isCorrect` field from the action payload');
t.deepEqual(newState.step, {
current: 3
}, 'step progression is wrong');
t.deepEqual(newState.slides, ['1.A1.4', '1.A1.2'], 'answered slide should have been stored in `slides`');
t.deepEqual(newState.content, state.nextContent, '`content` should be updated to be the previous `nextContent`');
t.deepEqual(newState.nextContent, wrongAnswerAction.payload.nextContent, "`nextContent` should be updated to be the action's `nextContent`");
t.deepEqual(pickUnchangedFields(newState), pickUnchangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should set livesDisabled to false when it is undefined', t => {
const actions = [wrongAnswerAction];
const stateUndefined = (0, _updateState.default)(config, Object.freeze({
..._states2.stateForSecondSlide,
livesDisabled: undefined
}), actions);
const stateFalse = (0, _updateState.default)(config, Object.freeze({
..._states2.stateForSecondSlide,
livesDisabled: false
}), actions);
t.false(stateUndefined.livesDisabled);
t.deepEqual(stateUndefined, stateFalse);
});
(0, _ava.default)('should not decrement lives when answering a question incorrectly when lives are disabled', t => {
const state = Object.freeze(_states2.stateForSecondSlide);
const newState = (0, _updateState.default)({
...config,
livesDisabled: true
}, state, [wrongAnswerAction]);
t.is(newState.lives, 1, '`lives` should not have been decremented');
t.true(newState.livesDisabled);
});
(0, _ava.default)('should update state when asking for a clue', t => {
const state = Object.freeze(_states2.stateForSecondSlide);
const action = Object.freeze({
type: 'clue',
payload: {
content: {
ref: '1.A1.2',
type: 'slide'
}
}
});
const pickUnchangedFields = (0, _pick2.default)(['content', 'nextContent', 'lives', 'slides', 'isCorrect', 'step']);
const newState = (0, _updateState.default)(config, state, [action]);
t.is(newState.stars, 3);
t.deepEqual(newState.requestedClues, ['1.A1.2']);
t.deepEqual(pickUnchangedFields(newState), pickUnchangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should update stars once when actions has several AskClueAction for the same content', t => {
const state = Object.freeze(_states2.stateForSecondSlide);
const action = Object.freeze({
type: 'clue',
payload: {
content: {
ref: '1.A1.2',
type: 'slide'
}
}
});
const omitChangedFields = (0, _omit2.default)(['requestedClues', 'stars']);
const newState = (0, _updateState.default)(config, state, [action, action]);
t.is(newState.stars, 3);
t.deepEqual(newState.requestedClues, ['1.A1.2']);
t.deepEqual(omitChangedFields(newState), omitChangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should update stars after viewing a resource', t => {
const state = Object.freeze(_states2.stateForFirstSlide);
const omitChangedFields = (0, _omit2.default)(['viewedResources', 'stars', 'hasViewedAResourceAtThisStep']);
const newState = (0, _updateState.default)(config, state, [contentResourceViewedAction('chapter', '1.A1', 'lesson_1')]);
t.is(newState.stars, 4);
t.deepEqual(newState.viewedResources, [{
type: 'chapter',
ref: '1.A1',
resources: ['lesson_1']
}]);
t.deepEqual(omitChangedFields(newState), omitChangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should update stars after viewing a resource (with different number of stars)', t => {
const state = Object.freeze(_states2.stateForFirstSlide);
const configWithDifferentStars = {
...config,
starsPerResourceViewed: 5
};
const omitChangedFields = (0, _omit2.default)(['viewedResources', 'stars', 'hasViewedAResourceAtThisStep']);
const newState = (0, _updateState.default)(configWithDifferentStars, state, [contentResourceViewedAction('chapter', '1.A1', 'lesson_1')]);
t.is(newState.stars, 5);
t.deepEqual(newState.viewedResources, [{
type: 'chapter',
ref: '1.A1',
resources: ['lesson_1']
}]);
t.deepEqual(omitChangedFields(newState), omitChangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should only count stars for viewing a resource once for every chapter even if there are multiple resource viewing actions', t => {
const state = Object.freeze(_states2.stateForFirstSlide);
const omitChangedFields = (0, _omit2.default)(['viewedResources', 'stars', 'hasViewedAResourceAtThisStep']);
const newState = (0, _updateState.default)(config, state, [contentResourceViewedAction('chapter', '1.A1', 'lesson_1'), contentResourceViewedAction('chapter', '1.A1', 'lesson_2'), contentResourceViewedAction('chapter', '1.A1', 'lesson_1')]);
t.is(newState.stars, 4);
t.deepEqual(newState.viewedResources, [{
type: 'chapter',
ref: '1.A1',
resources: ['lesson_1', 'lesson_2']
}]);
t.deepEqual(omitChangedFields(newState), omitChangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)('should count stars for viewing resources multiple times as long as they are for different chapters', t => {
const state = Object.freeze(_states2.stateForFirstSlide);
const omitChangedFields = (0, _omit2.default)(['viewedResources', 'stars', 'hasViewedAResourceAtThisStep']);
const newState = (0, _updateState.default)(config, state, [contentResourceViewedAction('chapter', '1.A1', 'lesson_1'), contentResourceViewedAction('chapter', '1.A1', 'lesson_1'), contentResourceViewedAction('chapter', '1.A2', 'lesson_1')]);
t.is(newState.stars, 8);
t.deepEqual(newState.viewedResources, [{
type: 'chapter',
ref: '1.A1',
resources: ['lesson_1']
}, {
type: 'chapter',
ref: '1.A2',
resources: ['lesson_1']
}]);
t.deepEqual(omitChangedFields(newState), omitChangedFields(state), 'Some fields that should not have been touched have been modified');
});
(0, _ava.default)("should throw if the state's nextContent is not the same as the action's content", t => {
const state = Object.freeze(_states2.stateForSecondSlide);
const action = Object.freeze({
type: 'answer',
payload: {
answer: ['foo'],
content: {
ref: '1.A1.200',
type: 'slide'
},
nextContent: {
ref: '1.A1.1',
type: 'slide'
},
isCorrect: true,
godMode: false,
instructions: null
}
});
t.throws(() => (0, _updateState.default)(config, state, [action]), {
message: 'The content of the progression state does not match the answer action: state.nextContent: {"ref":"1.A1.2","type":"slide"} | action.payload.content: {"ref":"1.A1.200","type":"slide"}'
});
});
(0, _ava.default)('should add one life when using extra life', t => {
const state = Object.freeze(_states2.extraLifeProgressionState);
const newState = (0, _updateState.default)(config, state, [extraLifeAcceptedAction]);
t.is(newState.lives, 1);
t.is(newState.remainingLifeRequests, 0);
t.is(newState.nextContent.type, 'slide');
t.is((0, _get2.default)('content.ref', newState), 'extraLife');
t.is(newState.hasViewedAResourceAtThisStep, false);
});
(0, _ava.default)('should not add a life when accepting an extraLife when lives are disabled', t => {
const state = Object.freeze(_states2.extraLifeProgressionState);
const newState = (0, _updateState.default)({
...config,
livesDisabled: true
}, state, [extraLifeAcceptedAction]);
t.is(newState.lives, 0, '`lives` should not have been incremented');
t.true(newState.livesDisabled);
t.is(newState.hasViewedAResourceAtThisStep, false);
});
(0, _ava.default)('should go to failure when refusing extra life', t => {
const state = Object.freeze(_states2.extraLifeProgressionState);
const action = Object.freeze({
type: 'extraLifeRefused',
payload: {
content: {
type: 'node',
ref: 'extraLife'
},
nextContent: {
ref: 'failExitNode',
type: 'failure'
}
}
});
const newState = (0, _updateState.default)(config, state, [action]);
t.is(newState.lives, 0);
t.is(newState.remainingLifeRequests, 1);
t.is(newState.nextContent.type, 'failure');
t.is((0, _get2.default)('content.ref', newState), 'extraLife');
});
(0, _ava.default)('should update step when answering a question', t => {
const state = Object.freeze({
..._states2.stateForSecondSlide,
chapters: ['1.A1', '1.A2'],
step: {
current: 2
}
});
const action = Object.freeze({
type: 'answer',
payload: {
answer: ['foo'],
content: {
ref: '1.A1.2',
type: 'slide'
},
nextContent: {
ref: '1.A1.1',
type: 'slide'
},
isCorrect: true,
godMode: false,
instructions: null
}
});
const newState = (0, _updateState.default)(config, state, [action]);
t.deepEqual(newState.step, {
current: 3
}, 'step progression is wrong');
t.is(newState.hasViewedAResourceAtThisStep, false);
});
(0, _ava.default)('should update pendingSlides when answer is wrong', t => {
const configReview = (0, _config.getConfig)({
ref: 'review',
version: '1'
});
const action = Object.freeze({
type: 'answer',
payload: {
answer: ['foo'],
content: {
ref: '1.A1.1',
type: 'slide'
},
nextContent: {
ref: '1.A1.2',
type: 'slide'
},
isCorrect: false,
godMode: false,
instructions: null
}
});
const newState = (0, _updateState.default)(configReview, _states.firstStateReview, [action]);
t.deepEqual(newState, {
content: {
ref: '1.A1.1',
type: 'slide'
},
nextContent: {
ref: '1.A1.2',
type: 'slide'
},
lives: 0,
livesDisabled: true,
stars: 0,
slides: ['1.A1.1'],
requestedClues: [],
viewedResources: [],
step: {
current: 2
},
isCorrect: false,
remainingLifeRequests: 0,
hasViewedAResourceAtThisStep: false,
allAnswers: [{
slideRef: '1.A1.1',
isCorrect: false,
answer: ['foo']
}],
variables: {},
pendingSlides: ['1.A1.1']
});
});
(0, _ava.default)('should update pendingSlides when a pending slide if finally correctly answered', t => {
const configReview = (0, _config.getConfig)({
ref: 'review',
version: '1'
});
const action = Object.freeze({
type: 'answer',
payload: {
answer: ['foo'],
content: {
ref: '1.A1.2',
type: 'slide'
},
nextContent: {
ref: '1.A1.4',
type: 'slide'
},
isCorrect: true,
godMode: false,
instructions: null
}
});
const newState = (0, _updateState.default)(configReview, _states.wrongAnswersAfterLastStepStateReview, [action]);
t.deepEqual(newState, {
content: {
ref: '1.A1.2',
type: 'slide'
},
nextContent: {
ref: '1.A1.4',
type: 'slide'
},
lives: 0,
livesDisabled: true,
stars: 24,
slides: ['1.A1.1', '1.A1.2', '1.A1.3', '1.A1.4', '1.A1.5', '1.A1.2'],
requestedClues: [],
viewedResources: [],
step: {
current: 7
},
isCorrect: true,
remainingLifeRequests: 0,
hasViewedAResourceAtThisStep: false,
allAnswers: [{
slideRef: '1.A1.1',
isCorrect: true,
answer: ['foo', 'bar']
}, {
slideRef: '1.A1.2',
isCorrect: false,
answer: ['foo']
}, {
slideRef: '1.A1.3',
isCorrect: true,
answer: ['foo']
}, {
slideRef: '1.A1.4',
isCorrect: false,
answer: ['foo']
}, {
slideRef: '1.A1.5',
isCorrect: false,
answer: ['foo']
}, {
slideRef: '1.A1.2',
isCorrect: true,
answer: ['foo']
}],
variables: {},
pendingSlides: ['1.A1.4', '1.A1.5']
});
});
//# sourceMappingURL=update-state.js.map