UNPKG

@coorpacademy/progression-engine

Version:

525 lines (519 loc) 18.5 kB
"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