@coorpacademy/progression-engine
Version:
720 lines (719 loc) • 17.1 kB
JavaScript
"use strict";
var _filter2 = _interopRequireDefault(require("lodash/fp/filter"));
var _omit2 = _interopRequireDefault(require("lodash/fp/omit"));
var _ava = _interopRequireDefault(require("ava"));
var _config = require("../../config");
var _ = require("..");
var _slides = _interopRequireDefault(require("./fixtures/slides"));
var _getSlide = _interopRequireDefault(require("./helpers/get-slide"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const config = (0, _config.getConfig)({
ref: 'learner',
version: '1'
});
const partialAction = state => ({
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true
}
});
const rulesFor1A1 = [{
source: {
type: 'slide',
ref: 'i do not exist'
},
destination: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'foo',
type: 'set',
value: 'highest_priority'
}],
conditions: [],
priority: 0
}, {
source: {
type: 'slide',
ref: '1.A1.2'
},
destination: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'foo',
type: 'set',
value: 'higher_priority'
}],
conditions: [],
priority: 5
}, {
source: {
type: 'slide',
ref: '1.A1.2'
},
destination: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'foo',
type: 'set',
value: 'lower_priority'
}],
conditions: [],
priority: 10
}, {
source: {
type: 'slide',
ref: '1.A1.3'
},
destination: {
type: 'slide',
ref: '1.A1.4'
},
instructions: [],
conditions: [{
target: {
scope: 'variable',
field: 'lives'
},
operator: 'EQUALS',
values: [2]
}],
priority: 10
}, {
source: {
type: 'slide',
ref: '1.A1.3'
},
destination: {
type: 'slide',
ref: '1.A1.5'
},
instructions: [],
conditions: [{
target: {
scope: 'variable',
field: 'lives'
},
operator: 'EQUALS',
values: [1]
}],
priority: 20
}, {
source: {
type: 'slide',
ref: '1.A1.5'
},
destination: {
type: 'chapter',
ref: '2.A1'
},
instructions: [{
field: 'bar',
type: 'set',
value: 'chapter_change'
}],
conditions: [],
priority: 5
}, {
source: {
type: 'slide',
ref: '1.A1.7'
},
destination: {
type: 'chapter',
ref: '2.A1'
},
instructions: [{
field: 'bar',
type: 'set',
value: 'I was in 1.A1.7'
}],
conditions: [],
priority: 10
}, {
source: {
type: 'slide',
ref: '*'
},
destination: {
type: 'slide',
ref: 'slide-matching-star-source'
},
instructions: [],
conditions: [],
priority: 100
}];
const rulesFor2A1 = [{
source: {
type: 'slide',
ref: ''
},
destination: {
type: 'slide',
ref: '2.A1.2'
},
instructions: [],
conditions: [{
target: {
scope: 'variable',
field: 'bar'
},
operator: 'EQUALS',
values: ['I was in 1.A1.7']
}],
priority: 5
}, {
source: {
type: 'slide',
ref: ''
},
destination: {
type: 'slide',
ref: '2.A1.1'
},
instructions: [{
field: 'baz',
type: 'set',
value: 'some value'
}],
conditions: [],
priority: 10
}, {
source: {
type: 'slide',
ref: '2.A1.1'
},
destination: {
type: 'slide',
ref: '2.A1.2'
},
instructions: [],
conditions: [],
priority: 10
}];
const rulesFor1A2 = [{
source: {
type: 'slide',
ref: '1.A1.2'
},
destination: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
value: 4,
type: 'add',
field: 'stars'
}],
conditions: [{
target: {
scope: 'slide',
ref: '1.A1.2',
field: 'isCorrect'
},
operator: 'EQUALS',
values: [true]
}],
priority: 5
}, {
source: {
type: 'slide',
ref: '1.A1.2'
},
destination: {
type: 'slide',
ref: '1.A1.4'
},
instructions: [{
value: 0,
type: 'add',
field: 'stars'
}],
conditions: [{
target: {
scope: 'slide',
ref: '1.A1.2',
field: 'isCorrect'
},
operator: 'EQUALS',
values: [false]
}],
priority: 5
}];
const availableContent = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: rulesFor1A1
}, {
ref: '2.A1',
slides: (0, _filter2.default)({
chapter_id: '2.A1'
}, _slides.default),
rules: rulesFor2A1
}];
const adaptiveState = {
content: {
ref: '1.A1.1',
type: 'slide'
},
nextContent: {
ref: '1.A1.2',
type: 'slide'
},
lives: 1,
livesDisabled: false,
stars: 0,
slides: ['1.A1.1'],
requestedClues: [],
viewedResources: [],
isCorrect: true,
step: {
current: 1
},
remainingLifeRequests: 1,
hasViewedAResourceAtThisStep: false,
allAnswers: [],
variables: {},
pendingSlides: []
};
(0, _ava.default)('should return a slide from the list of slides if the current chapter has no rules', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '2.A1.1'
}
});
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const availableContentWithSlides = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: rulesFor1A1
}, {
ref: '2.A1',
slides: (0, _filter2.default)({
chapter_id: '2.A1'
}, _slides.default),
rules: null
}];
const resultAction = (0, _.computeNextStepAfterAnswer)(config, state, availableContentWithSlides, currentSlide, partialAction(state));
if (!resultAction) {
throw new Error('action should not be falsy');
}
t.deepEqual((0, _omit2.default)(['payload.nextContent.ref'], resultAction), {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide'
},
instructions: null,
isCorrect: true
}
});
t.regex(resultAction.payload.nextContent.ref, /^2\.A1\.[2-9]+$/);
});
(0, _ava.default)('should return the slide from the chapter rules matching the source with the lowest priority value first', t => {
const state = Object.freeze(adaptiveState);
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const resultAction = (0, _.computeNextStepAfterAnswer)(config, state, availableContent, currentSlide, partialAction(state));
t.deepEqual(resultAction, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'foo',
type: 'set',
value: 'higher_priority'
}],
isCorrect: null
}
});
});
(0, _ava.default)('should match the rule with "*" source whatever state.nextContent may be', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.6'
}
});
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const resultAction = (0, _.computeNextStepAfterAnswer)(config, state, availableContent, currentSlide, partialAction(state));
t.deepEqual(resultAction, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: 'slide-matching-star-source'
},
instructions: [],
isCorrect: null
}
});
});
(0, _ava.default)("should only select a rule if it matches the rules's conditions", t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.3'
}
});
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const resultAction = (0, _.computeNextStepAfterAnswer)(config, state, availableContent, currentSlide, partialAction(state));
t.deepEqual(resultAction, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: '1.A1.5'
},
instructions: [],
isCorrect: null
}
});
});
(0, _ava.default)('should return the slide of a new chapter when a rule requests to change to a different chapter that does not have rules', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.5'
}
});
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const availableContentWithSlides = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: rulesFor1A1
}, {
ref: '2.A1',
slides: (0, _filter2.default)({
chapter_id: '2.A1'
}, _slides.default),
rules: null
}];
const resultAction = (0, _.computeNextStepAfterAnswer)(config, state, availableContentWithSlides, currentSlide, partialAction(state));
if (!resultAction) {
throw new Error('action should not be falsy');
}
t.deepEqual((0, _omit2.default)(['payload.nextContent.ref'], resultAction), {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide'
},
instructions: [{
field: 'bar',
type: 'set',
value: 'chapter_change'
}],
isCorrect: null
}
});
t.regex(resultAction.payload.nextContent.ref, /^2\.A1\.[1-9]+$/);
});
(0, _ava.default)('should concatenate the instructions from all intermediary rules when switching chapters', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.5'
}
});
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const resultAction = (0, _.computeNextStepAfterAnswer)(config, state, availableContent, currentSlide, partialAction(state));
t.deepEqual(resultAction, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: '2.A1.1'
},
instructions: [{
field: 'bar',
type: 'set',
value: 'chapter_change'
}, {
field: 'baz',
type: 'set',
value: 'some value'
}],
isCorrect: null
}
});
});
(0, _ava.default)("should apply the instructions from last chapter's rule before selecting a new rule from the next chapter when switching chapters", t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.7'
}
});
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const resultAction = (0, _.computeNextStepAfterAnswer)(config, state, availableContent, currentSlide, partialAction(state));
t.deepEqual(resultAction, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: '2.A1.2'
},
instructions: [{
field: 'bar',
type: 'set',
value: 'I was in 1.A1.7'
}],
isCorrect: null
}
});
});
(0, _ava.default)('should be able to switch from a non-adaptive chapter to an adaptive chapter', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.4'
},
slides: ['1.A1.1', '1.A1.2', '1.A1.3']
});
const availableContentWithNoRulesInFirstChapter = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: null
}, {
ref: '2.A1',
slides: (0, _filter2.default)({
chapter_id: '2.A1'
}, _slides.default),
rules: rulesFor2A1
}];
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const result = (0, _.computeNextStepAfterAnswer)(config, state, availableContentWithNoRulesInFirstChapter, currentSlide, partialAction(state));
t.deepEqual(result, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: '2.A1.1'
},
instructions: [{
field: 'baz',
type: 'set',
value: 'some value'
}],
isCorrect: true
}
});
});
(0, _ava.default)('should use slide scoped instructions to select right rule', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.2'
}
});
const availableContentWithSlideScopedRules = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: rulesFor1A2
}, {
ref: '2.A1',
slides: (0, _filter2.default)({
chapter_id: '2.A1'
}, _slides.default),
rules: rulesFor2A1
}];
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const result = (0, _.computeNextStepAfterAnswer)(config, state, availableContentWithSlideScopedRules, currentSlide, partialAction(state));
t.deepEqual(result, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'stars',
type: 'add',
value: 4
}],
isCorrect: true
}
});
});
(0, _ava.default)('should always use rules to select nextContent, even if the number of answered slides is > config.slidesToComplete', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.2'
},
slides: ['1.A1.1', '1.A1.3', '1.A1.4', '1.A1.5', '1.A1.6']
});
const availableContentWithSlideScopedRules = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: rulesFor1A2
}];
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const result = (0, _.computeNextStepAfterAnswer)(config, state, availableContentWithSlideScopedRules, currentSlide, partialAction(state));
t.deepEqual(result, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'stars',
type: 'add',
value: 4
}],
isCorrect: true
}
});
});
(0, _ava.default)('should return null when switching to new chapter but no content could be found for the chapter', t => {
const state = Object.freeze({
...adaptiveState,
nextContent: {
type: 'slide',
ref: '1.A1.4'
},
slides: ['1.A1.1', '1.A1.2', '1.A1.3']
});
const availableContentWithNoContentInSecondChapter = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: null
}, {
ref: '2.A1',
slides: [],
rules: null
}];
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
const result = (0, _.computeNextStepAfterAnswer)(config, state, availableContentWithNoContentInSecondChapter, currentSlide, partialAction(state));
t.deepEqual(result, {
type: 'answer',
payload: {
answer: [],
content: state.nextContent,
godMode: true,
nextContent: {
type: 'success',
ref: 'successExitNode'
},
instructions: null,
isCorrect: true
}
});
});
(0, _ava.default)("should return null when there are no rules matching the progression's conditions", t => {
const state = Object.freeze(adaptiveState);
const availableContentWithNoContentInSecondChapter = [{
ref: '1.A1',
slides: (0, _filter2.default)({
chapter_id: '1.A1'
}, _slides.default),
rules: [{
source: {
type: 'slide',
ref: 'i do not exist'
},
destination: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'foo',
type: 'set',
value: 'highest_priority'
}],
conditions: [],
priority: 0
}, {
source: {
type: 'slide',
ref: '1.A1.1'
},
destination: {
type: 'slide',
ref: '1.A1.3'
},
instructions: [{
field: 'foo',
type: 'set',
value: 'higher_priority'
}],
conditions: [{
target: {
scope: 'variable',
field: 'unknown variable'
},
operator: 'EQUALS',
values: [2]
}],
priority: 5
}]
}];
const currentSlide = (0, _getSlide.default)(_slides.default, state.nextContent);
t.is((0, _.computeNextStepAfterAnswer)(config, state, availableContentWithNoContentInSecondChapter, currentSlide, partialAction(state)), null);
});
//# sourceMappingURL=compute-next-step-after-answer.adaptive.js.map