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;

});
