@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
305 lines (287 loc) • 13.8 kB
JavaScript
define(['jquery', 'lodash', 'taoTests/runner/plugin', 'taoQtiTest/runner/plugins/controls/timer/strategy/strategyHandler', 'taoQtiTest/runner/plugins/controls/timer/component/timerbox', 'taoQtiTest/runner/plugins/controls/timer/timers', 'taoQtiTest/runner/helpers/isReviewPanelEnabled', 'taoQtiTest/runner/helpers/stats', 'handlebars', 'lib/handlebars/helpers'], function ($$1, _, pluginFactory, getStrategyHandler, timerboxFactory, timersFactory, isReviewPanelEnabled, statsHelper, Handlebars, Helpers0) { 'use strict';
$$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
pluginFactory = pluginFactory && Object.prototype.hasOwnProperty.call(pluginFactory, 'default') ? pluginFactory['default'] : pluginFactory;
getStrategyHandler = getStrategyHandler && Object.prototype.hasOwnProperty.call(getStrategyHandler, 'default') ? getStrategyHandler['default'] : getStrategyHandler;
timerboxFactory = timerboxFactory && Object.prototype.hasOwnProperty.call(timerboxFactory, 'default') ? timerboxFactory['default'] : timerboxFactory;
timersFactory = timersFactory && Object.prototype.hasOwnProperty.call(timersFactory, 'default') ? timersFactory['default'] : timersFactory;
isReviewPanelEnabled = isReviewPanelEnabled && Object.prototype.hasOwnProperty.call(isReviewPanelEnabled, 'default') ? isReviewPanelEnabled['default'] : isReviewPanelEnabled;
statsHelper = statsHelper && Object.prototype.hasOwnProperty.call(statsHelper, 'default') ? statsHelper['default'] : statsHelper;
Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;
if (!Helpers0.__initialized) {
Helpers0(Handlebars);
Helpers0.__initialized = true;
}
var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers);
return "<div aria-live=\"polite\" class=\"visible-hidden\"></div>\n";
});
function screenreaderNotificationTpl(data, options, asString) {
var html = Template(data, options);
return (asString || true) ? html : $(html);
}
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2018-2019 (original work) Open Assessment Technologies SA ;
*/
// timeout after which screenreader notifcation should be cleaned up
const screenreaderNotificationTimeout = 20000;
/**
* Creates the plugin
*/
var plugin = pluginFactory({
name: 'timer',
/**
* Install step, add behavior before the lifecycle
*/
install() {
const testRunner = this.getTestRunner();
/**
* Load the timers, from the given timeConstraints and reading the current value in the store
* @param {store} timeStore - where the values are read
* @param {Object} config - the current config, especially for the warnings
* @returns {Promise<Object[]>} the list of timers for the current context
*/
this.loadTimers = function loadTimers(timeStore, config) {
const testContext = testRunner.getTestContext();
const testPart = testRunner.getCurrentPart();
const isLinear = testPart && testPart.isLinear;
const timeConstraints = testContext.timeConstraints;
const timers = timersFactory(timeConstraints, isLinear, config);
return Promise.all(_.map(timers, function (timer) {
return timeStore.getItem(`consumed_${timer.id}`).then(function (savedConsumedTime) {
if (_.isNumber(savedConsumedTime) && savedConsumedTime >= 0 && config.restoreTimerFromClient) {
timer.remainingTime = timer.originalTime + timer.extraTime.total - savedConsumedTime;
}
});
})).then(function () {
return timers;
});
};
/**
* Save consumed time values into the store
* @param {store} timeStore - where the values are saved
* @param {Object[]} timers - the timers to save
* @returns {Promise} resolves once saved
*/
this.saveTimers = function saveTimers(timeStore, timers) {
return Promise.all(_.map(timers, function (timer) {
return timeStore.setItem(`consumed_${timer.id}`, timer.originalTime + timer.extraTime.total - timer.remainingTime);
}));
};
//define the "timer" store as "volatile" (removed on browser change).
testRunner.getTestStore().setVolatile(this.getName());
},
/**
* Initializes the plugin (called during runner's init)
*
* @returns {Promise}
*/
init: function init() {
const self = this;
const testRunner = this.getTestRunner();
const testRunnerOptions = testRunner.getOptions();
let screenreaderNotifcationTimeoutId;
const stats = {};
['test', 'testPart', 'section', 'item'].forEach(scope => Object.assign(stats, {
[scope]: statsHelper.getInstantStats(scope, testRunner)
}));
/**
* Plugin config,
*/
const config = Object.assign({
/**
* An option to control is the warnings are contextual or global
*/
contextualWarnings: false,
/**
* The list of configured warnings
*/
warnings: testRunnerOptions.timerWarning || {},
/**
* The list of configured warnings for screenreaders
*/
warningsForScreenreader: testRunnerOptions.timerWarningForScreenreader || {},
/**
* The guided navigation option
*/
guidedNavigation: testRunnerOptions.guidedNavigation,
/**
* Restore timer from client.
*/
restoreTimerFromClient: testRunnerOptions.timer && testRunnerOptions.timer.restoreTimerFromClient,
/**
* Questions stats
*/
questionsStats: stats
}, this.getConfig());
/**
* Set up the strategy handler
*/
const strategyHandler = getStrategyHandler(testRunner);
/**
* dispatch errors to the test runner
* @param {Error} err - to dispatch
*/
const handleError = err => {
testRunner.trigger('error', err);
};
function loadSavedTimers(timeStore) {
const testContext = testRunner.getTestContext();
//update the timers before each item
if (self.timerbox && testContext.timeConstraints) {
return self.loadTimers(timeStore, config).then(function (timers) {
return self.timerbox.update(timers);
}).catch(handleError);
}
}
return new Promise(function (resolve) {
//load the plugin store
return testRunner.getPluginStore(self.getName()).then(function (timeStore) {
testRunner.before('renderitem', function () {
return loadSavedTimers(timeStore);
}).before('enableitem', function () {
if (config.restoreTimerFromClient) {
return loadSavedTimers(timeStore);
}
}).on('tick', function (elapsed) {
if (self.timerbox) {
const timers = self.timerbox.getTimers();
const updatedTimers = Object.keys(timers).reduce((acc, timerName) => {
const statsScope = statsHelper.getInstantStats(timers[timerName].scope, testRunner);
const unansweredQuestions = statsScope && statsScope.questions - statsScope.answered;
acc[timerName] = Object.assign({}, timers[timerName], {
remainingTime: timers[timerName].remainingTime - elapsed,
unansweredQuestions
});
return acc;
}, {});
self.timerbox.update(updatedTimers).catch(handleError);
}
}).after('renderitem', function () {
if (self.timerbox) {
$$1(self.timerbox.getElement()).find('.timer-wrapper').attr('aria-hidden', isReviewPanelEnabled(testRunner));
self.timerbox.start();
}
self.$screenreaderWarningContainer.text('');
}).after('enableitem', function () {
if (self.timerbox && config.restoreTimerFromClient) {
//this will "resume" the countdowns if timers have client mode
self.timerbox.start();
}
}).on('move skip', function () {
if (self.timerbox) {
//this will "pause" the countdowns
self.timerbox.stop();
}
}).on('disableitem', function () {
if (self.timerbox && config.restoreTimerFromClient) {
//this will "pause" the countdowns if timers have client mode
self.timerbox.stop();
}
});
timeStore.getItem('zen-mode').then(function (startZen) {
//set up the timerbox
self.timerbox = timerboxFactory({
ariaHidden: isReviewPanelEnabled(testRunner),
zenMode: {
enabled: true,
startHidden: !!startZen
},
displayWarning: config.contextualWarnings
}).on('change', _.throttle(function () {
//update the store with the current timer values
self.saveTimers(timeStore, this.getTimers());
}, 1000)).on('timeradd', function (timer) {
strategyHandler.setUp(timer).catch(handleError);
}).on('timerremove', function (timer) {
strategyHandler.tearDown(timer).catch(handleError);
}).on('timerstart', function (timer) {
strategyHandler.start(timer).catch(handleError);
}).on('timerstop', function (timer) {
strategyHandler.stop(timer).catch(handleError);
}).on('timerend', function (timer) {
strategyHandler.complete(timer).catch(handleError);
}).on('timerchange', function (action, timer) {
//backward compatible events
self.trigger(`${action}timer`, timer.qtiClassName, timer);
}).on('zenchange', function (isZen) {
timeStore.setItem('zen-mode', !!isZen);
}).on('init', resolve).on('error', handleError);
// share this timer values to use in other components
self.timerbox.spread(testRunner, 'timertick');
if (!config.contextualWarnings) {
self.timerbox.on('warn', function (message, level) {
if (level && message) {
testRunner.trigger(level, message);
}
});
// debounce used to prevent multiple invoking at the same time
self.timerbox.on('warnscreenreader', _.debounce((message, remainingTime, scope) => {
const statsScope = statsHelper.getInstantStats(scope, testRunner);
const unansweredQuestions = statsScope && statsScope.questions - statsScope.answered;
if (screenreaderNotifcationTimeoutId) {
clearTimeout(screenreaderNotifcationTimeoutId);
}
self.$screenreaderWarningContainer.text(message(remainingTime, unansweredQuestions));
screenreaderNotifcationTimeoutId = setTimeout(() => self.$screenreaderWarningContainer.text(''), screenreaderNotificationTimeout);
}, 1000, {
'leading': true,
'trailing': false
}));
}
}).catch(handleError);
});
});
},
/**
* Called during the runner's render phase
*/
render: function render() {
const $container = this.getAreaBroker().getControlArea();
this.$screenreaderWarningContainer = $$1(screenreaderNotificationTpl());
this.timerbox.render($container);
$container.append(this.$screenreaderWarningContainer);
},
/**
* Called during the runner's destroy phase
*/
destroy: function destroy() {
if (this.timerbox) {
this.timerbox.stop().destroy();
}
},
/**
* Shows the timers
*/
show: function show() {
if (this.timerbox) {
this.timerbox.show();
}
},
/**
* Hides the timers
*/
hide: function hide() {
if (this.timerbox) {
this.timerbox.hide();
}
}
});
return plugin;
});