UNPKG

@oat-sa/tao-test-runner-qti

Version:
760 lines (726 loc) 34.3 kB
define(['jquery', 'lodash', 'i18n', 'core/cachedStore', 'util/browser', 'taoTests/runner/areaBroker', 'taoTests/runner/proxy', 'taoTests/runner/probeOverseer', 'taoTests/runner/testStore', 'taoQtiTest/runner/provider/dataUpdater', 'taoQtiTest/runner/provider/toolStateBridge', 'taoQtiTest/runner/helpers/currentItem', 'taoQtiTest/runner/helpers/map', 'taoQtiTest/runner/ui/toolbox/toolbox', 'taoQtiItem/runner/qtiItemRunner', 'taoQtiTest/runner/config/assetManager', 'handlebars', 'lib/handlebars/helpers', 'taoQtiTest/runner/config/states', 'taoQtiTest/runner/provider/stopwatch'], function ($$1, _, __, cachedStore, browser, areaBrokerFactory, proxyFactory, probeOverseerFactory, testStoreFactory, dataUpdater, toolStateBridgeFactory, currentItemHelper, mapHelper, toolboxFactory, qtiItemRunner, getAssetManager, Handlebars, Helpers0, states, stopwatchFactory) { 'use strict'; $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; __ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __; cachedStore = cachedStore && Object.prototype.hasOwnProperty.call(cachedStore, 'default') ? cachedStore['default'] : cachedStore; browser = browser && Object.prototype.hasOwnProperty.call(browser, 'default') ? browser['default'] : browser; areaBrokerFactory = areaBrokerFactory && Object.prototype.hasOwnProperty.call(areaBrokerFactory, 'default') ? areaBrokerFactory['default'] : areaBrokerFactory; proxyFactory = proxyFactory && Object.prototype.hasOwnProperty.call(proxyFactory, 'default') ? proxyFactory['default'] : proxyFactory; probeOverseerFactory = probeOverseerFactory && Object.prototype.hasOwnProperty.call(probeOverseerFactory, 'default') ? probeOverseerFactory['default'] : probeOverseerFactory; testStoreFactory = testStoreFactory && Object.prototype.hasOwnProperty.call(testStoreFactory, 'default') ? testStoreFactory['default'] : testStoreFactory; dataUpdater = dataUpdater && Object.prototype.hasOwnProperty.call(dataUpdater, 'default') ? dataUpdater['default'] : dataUpdater; toolStateBridgeFactory = toolStateBridgeFactory && Object.prototype.hasOwnProperty.call(toolStateBridgeFactory, 'default') ? toolStateBridgeFactory['default'] : toolStateBridgeFactory; currentItemHelper = currentItemHelper && Object.prototype.hasOwnProperty.call(currentItemHelper, 'default') ? currentItemHelper['default'] : currentItemHelper; mapHelper = mapHelper && Object.prototype.hasOwnProperty.call(mapHelper, 'default') ? mapHelper['default'] : mapHelper; toolboxFactory = toolboxFactory && Object.prototype.hasOwnProperty.call(toolboxFactory, 'default') ? toolboxFactory['default'] : toolboxFactory; qtiItemRunner = qtiItemRunner && Object.prototype.hasOwnProperty.call(qtiItemRunner, 'default') ? qtiItemRunner['default'] : qtiItemRunner; getAssetManager = getAssetManager && Object.prototype.hasOwnProperty.call(getAssetManager, 'default') ? getAssetManager['default'] : getAssetManager; Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars; Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0; states = states && Object.prototype.hasOwnProperty.call(states, 'default') ? states['default'] : states; stopwatchFactory = stopwatchFactory && Object.prototype.hasOwnProperty.call(stopwatchFactory, 'default') ? stopwatchFactory['default'] : stopwatchFactory; 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); data = data || {}; var buffer = "", helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression; buffer += "<main aria-labelledby=\"test-title-header\" class=\"test-runner-scope\">\n <div class=\"action-bar content-action-bar horizontal-action-bar top-action-bar\">\n <div class=\"control-box size-wrapper\"></div>\n </div>\n\n <div class=\"test-runner-sections\">\n\n <aside class=\"test-sidebar test-sidebar-left\" aria-labelledby=\"test-sidebar-left-header\">\n <h2 id=\"test-sidebar-left-header\" class=\"landmark-title-hidden\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Test status and review structure", options) : helperMissing.call(depth0, "__", "Test status and review structure", options))) + "</h2>\n </aside>\n\n <section class=\"content-wrapper\" aria-labelledby=\"test-title-header\">\n <h2 id=\"test-title-header\" class=\"landmark-title-hidden\"></h2>\n <div id=\"qti-content\"></div>\n </section>\n </div>\n\n <div class=\"action-bar content-action-bar horizontal-action-bar bottom-action-bar\">\n <div class=\"control-box size-wrapper\">\n <aside class=\"lft tools-box\" aria-labelledby=\"toolboxheader\">\n <h2 id=\"toolboxheader\" class=\"landmark-title-hidden\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Tool box and flagging for review", options) : helperMissing.call(depth0, "__", "Tool box and flagging for review", options))) + "</h2>\n </aside>\n <nav class=\"rgt navi-box\" aria-labelledby=\"navheader\">\n <h2 id=\"navheader\" class=\"landmark-title-hidden\">" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Main navigation", options) : helperMissing.call(depth0, "__", "Main navigation", options))) + "</h2>\n <ul class=\"plain navi-box-list\"></ul>\n </nav>\n </div>\n </div>\n\n</main>\n"; return buffer; }); function layoutTpl(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) 2016-2021 (original work) Open Assessment Technologies SA ; */ /** * A Test runner provider to be registered against the runner */ var qtiProvider = { //provider name name: 'qti', /** * Initialize and load the area broker with a correct mapping * @returns {areaBroker} */ loadAreaBroker: function loadAreaBroker() { var $layout = $$1(layoutTpl()); return areaBrokerFactory($layout, { content: $$1('#qti-content', $layout), toolbox: $$1('.tools-box', $layout), navigation: $$1('.navi-box-list', $layout), mainLandmark: $$1('#test-title-header', $layout), control: $$1('.top-action-bar .control-box', $layout), actionsBar: $$1('.bottom-action-bar .control-box', $layout), panel: $$1('.test-sidebar-left', $layout), header: $$1('.title-box', $layout) }); }, /** * Initialize and load the test runner proxy * @returns {proxy} */ loadProxy: function loadProxy() { var config = this.getConfig(); var proxyProvider = config.provider.proxy || 'qtiServiceProxy'; var proxyConfig = _.pick(config, ['testDefinition', 'testCompilation', 'serviceCallId', 'bootstrap', 'options']); return proxyFactory(proxyProvider, proxyConfig); }, /** * Initialize and load the probe overseer * @returns {probeOverseer} */ loadProbeOverseer: function loadProbeOverseer() { //the test run needs to be identified uniquely return probeOverseerFactory(this); }, /** * Initialize and load the test store * @returns {testStore} */ loadTestStore: function loadTestStore() { var config = this.getConfig(); //the test run needs to be identified uniquely var identifier = config.serviceCallId || `test-${Date.now()}`; return testStoreFactory(identifier); }, /** * Loads the persistent states storage * * @returns {Promise} */ loadPersistentStates: function loadPersistentStates() { var self = this; var config = this.getConfig(); var persistencePromise = cachedStore(`test-states-${config.serviceCallId}`, 'states'); persistencePromise.catch(function (err) { self.trigger('error', err); }); return persistencePromise.then(function (storage) { self.stateStorage = storage; }); }, /** * Checks a runner persistent state * * @param {String} name - the state name * @returns {Boolean} if active, false if not set */ getPersistentState: function getPersistentState(name) { if (this.stateStorage) { return this.stateStorage.getItem(name); } }, /** * Defines a runner persistent state * * @param {String} name - the state name * @param {Boolean} active - is the state active * @returns {Promise} Returns a promise that: * - will be resolved once the state is fully stored * - will be rejected if any error occurs or if the state name is not a valid string */ setPersistentState: function setPersistentState(name, active) { var self = this; var setPromise; if (this.stateStorage) { setPromise = this.stateStorage.setItem(name, active); setPromise.catch(function (err) { self.trigger('error', err); }); return setPromise; } }, /** * Install step : install new methods/behavior * * @this {runner} the runner context, not the provider */ install() { /** * Delegates the update of testMap, testContext and testData * to a 3rd part component, the dataUpdater. */ this.dataUpdater = dataUpdater(this.getDataHolder()); /** * The tool state bridge manages the state of the tools (plugins) * it updated directly the store of the plugins when configured to resume their values */ this.toolStateBridge = toolStateBridgeFactory(this.getTestStore(), _.keys(this.getPlugins())); /** * Convenience function to load the current item from the testMap * @returns {Object?} the current item if any or falsy */ this.getCurrentItem = function getCurrentItem() { const testContext = this.getTestContext(); const testMap = this.getTestMap(); if (testContext && testMap && testContext.itemIdentifier) { return mapHelper.getItem(testMap, testContext.itemIdentifier); } }; /** * Convenience function to load the current section from the testMap * @returns {Object?} the current section if any or falsy */ this.getCurrentSection = function getCurrentSection() { const testContext = this.getTestContext(); const testMap = this.getTestMap(); if (testContext && testMap && testContext.sectionId) { return mapHelper.getSection(testMap, testContext.sectionId); } }; /** * Convenience function to load the current part from the testMap * @returns {Object?} the current part if any or falsy */ this.getCurrentPart = function getCurrentPart() { const testContext = this.getTestContext(); const testMap = this.getTestMap(); if (testContext && testMap && testContext.testPartId) { return mapHelper.getPart(testMap, testContext.testPartId); } }; }, /** * Initialization of the provider, called during test runner init phase. * * We install behaviors during this phase (ie. even handlers) * and we call proxy.init. * * @this {runner} the runner context, not the provider * @returns {Promise} to chain proxy.init */ init: function init() { const self = this; const config = this.getConfig(); const areaBroker = this.getAreaBroker(); /** * Retrieve the item results * @returns {Object} the results */ function getItemResults() { var results = {}; var context = self.getTestContext(); if (context && self.itemRunner && context.itemSessionState <= states.itemSession.interacting) { results = { itemResponse: self.itemRunner.getResponses(), itemState: self.itemRunner.getState() }; } return results; } /** * Compute the next item for the given action * @param {String} action - item action like move/next, skip, etc. * @param {Object} [params] - the item action additional params * @param {Promise} [loadPromise] - wait this Promise to resolve before loading the item. */ function computeNext(action, params, loadPromise) { const context = self.getTestContext(); const currentItem = self.getCurrentItem(); const options = self.getOptions(); const skipPausedAssessmentDialog = !!options.skipPausedAssessmentDialog; const { partiallyAnsweredIsAnswered } = options.review; //catch server errors var submitError = function submitError(err) { if (err && err.unrecoverable) { if (!skipPausedAssessmentDialog) { self.trigger('alert.error', __('An unrecoverable error occurred. Your test session will be paused.')); } self.trigger('pause', { message: err.message }); } else if (err.code === 200) { //some server errors are valid, so we don't fail (prevent empty responses) self.trigger('alert.submitError', err.message || __('An error occurred during results submission. Please retry.'), load); } else { self.trigger('error', err); } }; //if we have to display modal feedbacks, we submit the responses before the move const feedbackPromise = new Promise(resolve => { //@deprecated feedbacks from testContext if ((currentItem.hasFeedbacks || context.hasFeedbacks) && context.itemSessionState <= states.itemSession.interacting) { params = _.omit(params, ['itemState', 'itemResponse']); self.getProxy().submitItem(context.itemIdentifier, self.itemRunner.getState(), self.itemRunner.getResponses(), params).then(results => { if (results.itemSession) { currentItem.answered = results.itemSession.itemAnswered; if (results.displayFeedbacks === true && results.feedbacks) { self.itemRunner.renderFeedbacks(results.feedbacks, results.itemSession, function (queue) { self.trigger('modalFeedbacks', queue, resolve); }); return; } } return resolve(); }).catch(submitError); } else { if (action === 'skip') { currentItem.answered = false; } else { // when the test part is linear, the item is always answered as we cannot come back to it const testPart = self.getCurrentPart(); const isLinear = testPart && testPart.isLinear; currentItem.answered = isLinear || currentItemHelper.isAnswered(self, partiallyAnsweredIsAnswered); } resolve(); } }); feedbackPromise.then(function () { return self.toolStateBridge.getStates(); }).then(function (toolStates) { if (toolStates && _.size(toolStates) > 0) { params.toolStates = toolStates; } // ensure the answered state of the current item is correctly set and the stats are aligned self.setTestMap(self.dataUpdater.updateStats()); //to be sure load start after unload... //we add an intermediate ns event on unload self.on(`unloaditem.${action}`, function () { self.off(`.${action}`); self.getProxy().callItemAction(context.itemIdentifier, action, params).then(function (results) { loadPromise = loadPromise || Promise.resolve(); return loadPromise.then(function () { return results; }); }).then(function (results) { //update testData, testContext and build testMap self.dataUpdater.update(results); load(); }).catch(submitError); }); self.unloadItem(context.itemIdentifier); }).catch(submitError); } /** * Load the next action: load the current item or call finish based the test state */ function load() { var context = self.getTestContext(); if (context.state <= states.testSession.interacting) { self.loadItem(context.itemIdentifier); } else if (context.state === states.testSession.closed) { self.finish(); } } areaBroker.setComponent('toolbox', toolboxFactory()); areaBroker.getToolbox().init(); const stopwatch = stopwatchFactory({}); stopwatch.init(); stopwatch.spread(this, 'tick'); const isTimerClientMode = () => config.options.timer && config.options.timer.restoreTimerFromClient; /* * Install behavior on events */ this.on('ready', function () { //load the 1st item load(); }).on('move', function (direction, scope, position) { // get the item results/state before disabling the tools // otherwise the state could be partially lost for tools that clean up when disabling var itemResults = getItemResults(); this.trigger('disablenav disabletools'); computeNext('move', _.merge(itemResults, { direction: direction, scope: scope || 'item', ref: position })); }).on('skip', function (scope) { this.trigger('disablenav disabletools'); computeNext('skip', { scope: scope || 'item' }); }).on('exit', function (reason) { var context = self.getTestContext(); this.disableItem(context.itemIdentifier); this.getProxy().callTestAction('exitTest', _.merge(getItemResults(), { itemDefinition: context.itemIdentifier, reason: reason })).then(function () { return self.finish(); }).catch(function (err) { self.trigger('error', err); }); }).on('timeout', function (scope, ref, timer) { const context = self.getTestContext(); const noAlertTimeout = mapHelper.hasItemCategory(self.getTestMap(), context.itemIdentifier, 'noAlertTimeout', true); context.isTimeout = true; this.setTestContext(context); if (timer && timer.allowLateSubmission) { self.trigger('alert.timeout', __('Time limit reached, this part of the test has ended. However you are allowed to finish the current item.')); self.before('move.latetimeout', function () { self.off('move.latetimeout'); computeNext('timeout', _.merge(getItemResults(), { scope: scope, ref: ref, late: true })); return Promise.reject({ cancel: true }); }); } else { this.disableItem(context.itemIdentifier); computeNext('timeout', _.merge(getItemResults(), { scope: scope, ref: ref }), new Promise(function (resolve) { if (noAlertTimeout) { resolve(); } else { self.trigger('alert.timeout', __('The time limit has been reached for this part of the test.'), () => { self.trigger('timeoutAccepted'); resolve(); }); } })); } }).on('pause', function (data) { const testContext = self.getTestContext(); const options = self.getOptions(); const skipPausedAssessmentDialog = !!options.skipPausedAssessmentDialog; this.setState('closedOrSuspended', true); const params = { itemDefinition: testContext.itemIdentifier, reason: { reasons: data && data.reasons, comment: data && (data.originalMessage || data.message) } }; const itemState = self.itemRunner.getState(); if (Object.keys(itemState).length) { params.itemState = itemState; } this.getProxy().callTestAction('pause', params).then(function () { self.trigger('leave', { code: states.testSession.suspended, message: data && data.message, skipExitMessage: skipPausedAssessmentDialog }); }).catch(function (err) { self.trigger('error', err); }); }).on('move skip exit timeout pause', function () { stopwatch.stop(); }).on('loaditem', function () { var context = this.getTestContext(); var warning = false; /** * Get the label of the current item * @returns {String} the label (fallback to the item identifier); */ var getItemLabel = function getItemLabel() { const item = self.getCurrentItem(); return item && item.label ? item.label : context.itemIdentifier; }; //The item is rendered but in a state that prevents us from interacting if (context.isTimeout) { warning = __('Time limit reached for item "%s".', getItemLabel()); } else if (context.itemSessionState > states.itemSession.interacting) { if (context.remainingAttempts === 0) { warning = __('No more attempts allowed for item "%s".', getItemLabel()); } else { warning = __('Item "%s" is completed.', getItemLabel()); } } //we disable the item and warn the user if (warning) { self.disableItem(context.itemIdentifier); self.trigger('warning', warning); } }).on('renderitem', function () { var context = this.getTestContext(); if (!this.getItemState(context.itemIdentifier, 'disabled')) { this.trigger('enabletools'); } this.trigger('enablenav'); }).after('renderitem', function () { stopwatch.start(); }).on('resumeitem', function () { this.trigger('enableitem enablenav'); }).on('disableitem', function () { if (isTimerClientMode()) { stopwatch.stop(); } this.trigger('disabletools'); }).on('enableitem', function () { if (isTimerClientMode()) { stopwatch.start(); } this.trigger('enabletools'); }).on('error', function () { stopwatch.stop(); this.trigger('disabletools enablenav'); }).on('finish', function () { this.flush(); }).on('leave', function () { this.trigger('endsession'); this.flush(); }).on('flush', function () { this.destroy(); stopwatch.destroy(); }); //starts the event collection if (this.getProbeOverseer()) { this.getProbeOverseer().start(); } //get the current store identifier to send it along with the init call return this.getTestStore().getStorageIdentifier().then(function (storeId) { //load data and current context in parallel at initialization return self.getProxy().init({ storeId: storeId }).then(function (response) { //handle backward compatibility with testData if (response.testData) { Object.assign(config.options, response.testData.config); } //fill the dataHolder, build the jump table, etc. self.dataUpdater.update(response); //set the plugin config self.dataUpdater.updatePluginsConfig(self.getPlugins(), self.getPluginsConfig()); //this checks the received storeId and clear the volatiles stores return self.getTestStore().clearVolatileIfStoreChange(response.lastStoreId).then(function () { return response; }); }).then(function (response) { var isNewStore = !response.lastStoreId || response.lastStoreId !== storeId; if (response.toolStates && isNewStore) { return self.toolStateBridge.setTools(_.keys(response.toolStates)).restoreStates(response.toolStates); } }); }); }, /** * Rendering phase of the test runner * * Attach the test runner to the DOM * * @this {runner} the runner context, not the provider */ render: function render() { var config = this.getConfig(); var areaBroker = this.getAreaBroker(); config.renderTo.append(areaBroker.getContainer()); areaBroker.getToolbox().render(areaBroker.getToolboxArea()); }, /** * LoadItem phase of the test runner * * We call the proxy in order to get the item data * * @this {runner} the runner context, not the provider * @param {String} itemIdentifier - The identifier of the item to update * @returns {Promise} that calls in parallel the state and the item data */ loadItem: function loadItem(itemIdentifier) { return this.getProxy().getItem(itemIdentifier).then(_ref => { let { itemData, baseUrl, itemState, portableElements, flags } = _ref; return { content: itemData, baseUrl, state: itemState, portableElements, flags }; }); }, /** * RenderItem phase of the test runner * * Here we initialize the item runner and wrap it's call to the test runner * * @this {runner} the runner context, not the provider * @param {String} itemIdentifier - The identifier of the item to update * @param {Object} itemData - The definition data of the item * @returns {Promise} resolves when the item is ready */ renderItem: function renderItem(itemIdentifier, itemData) { var self = this; var config = this.getConfig(); var assetManager = getAssetManager(config.serviceCallId); var changeState = function changeState() { self.setItemState(itemIdentifier, 'changed', true); }; return new Promise(function (resolve, reject) { assetManager.setData('baseUrl', itemData.baseUrl); assetManager.setData('itemIdentifier', itemIdentifier); assetManager.setData('assets', itemData.content.assets); itemData.content = itemData.content || {}; self.itemRunner = qtiItemRunner(itemData.content.type, itemData.content.data, { assetManager: assetManager }).on('error', function (err) { if (err && err.unrecoverable) { self.trigger('pause', { message: err.message }); } else { self.trigger('enablenav'); reject(err); } }).on('init', function () { var itemContainer = self.getAreaBroker().getContentArea(); var itemRenderingOptions = _.pick(itemData, ['state', 'portableElements']); this.render(itemContainer, itemRenderingOptions); }).on('render', function () { this.on('responsechange', changeState); this.on('statechange', changeState); resolve(); }).after('render', function () { //iOS only fix: sometimes the wrapper is not scrollable because of some bugs in Safari iOS //we have to force it to reconsider if the scroll needs to apply if (browser.isIOs()) { const wrapperElt = self.getAreaBroker().getContainer().find('.content-wrapper').get(0); if (wrapperElt) { wrapperElt.style.overflow = 'hidden'; setTimeout(() => wrapperElt.style.overflow = 'auto', 0); } } }).on('warning', function (err) { self.trigger('warning', err); }).init(); }); }, /** * UnloadItem phase of the test runner * * Item clean up * * @this {runner} the runner context, not the provider * @returns {Promise} resolves when the item is cleared */ unloadItem: function unloadItem() { var self = this; self.trigger('beforeunloaditem disablenav disabletools'); return new Promise(function (resolve) { if (self.itemRunner) { self.itemRunner.on('clear', resolve).clear(); return; } resolve(); }); }, /** * Finish phase of the test runner * * Calls proxy.finish to close the test * * @this {runner} the runner context, not the provider * @returns {Promise} proxy.finish */ finish: function finish() { if (!this.getState('finish')) { this.trigger('disablenav disabletools'); if (this.stateStorage) { return this.stateStorage.removeStore(); } } }, /** * Flushes the test variables before leaving the runner * * Clean up * * @this {runner} the runner context, not the provider * @returns {Promise} */ flush: function flush() { var self = this; var probeOverseer = this.getProbeOverseer(); var proxy = this.getProxy(); var flushPromise; //if there is trace data collected by the probes if (probeOverseer && !this.getState('disconnected')) { flushPromise = probeOverseer.flush().then(function (data) { var traceData = {}; //we reformat the time set into a trace variables if (data && data.length) { _.forEach(data, function (entry) { var id = `${entry.type}-${entry.id}`; if (entry.marker) { id = `${entry.marker}-${id}`; } traceData[id] = entry; }); //and send them return self.getProxy().sendVariables(traceData); } }).then(function () { probeOverseer.stop(); }).catch(function () { probeOverseer.stop(); }); } else { flushPromise = Promise.resolve(); } return flushPromise.then(function () { // safely stop the communicator to prevent inconsistent communication while leaving if (proxy.hasCommunicator()) { proxy.getCommunicator().then(function (communicator) { return communicator.close(); }) // Silently catch the potential errors to avoid polluting the console. // The code above is present to close an already open communicator in order to avoid later // communication while the test is destroying. So if any error occurs here it is not very important, // the most time it will be a missing communicator error, due to disabled config. .catch(_.noop); } }); }, /** * Destroy phase of the test runner * * Clean up * * @this {runner} the runner context, not the provider * @returns void */ destroy: function destroy() { var areaBroker = this.getAreaBroker(); // prevent the item to be displayed while test runner is destroying if (this.itemRunner) { this.itemRunner.clear(); } this.itemRunner = null; if (areaBroker) { areaBroker.getToolbox().destroy(); } //we remove the store(s) only if the finish step was reached if (this.getState('finish')) { return this.getTestStore().remove(); } } }; return qtiProvider; });