diff --git a/package-lock.json b/package-lock.json index e9b7d5e9..e962612b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-test-runner-qti", - "version": "2.23.4", + "version": "2.23.4-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5879f988..ca5084e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-test-runner-qti", - "version": "2.23.4", + "version": "2.23.4-1", "description": "TAO Test Runner QTI implementation", "files": [ "dist", diff --git a/src/provider/qti.js b/src/provider/qti.js index ed052e8d..d1ffb19f 100644 --- a/src/provider/qti.js +++ b/src/provider/qti.js @@ -379,7 +379,7 @@ var qtiProvider = { stopwatch.init(); stopwatch.spread(this, 'tick'); - const timerClientMode = config.options.timer && config.options.timer.restoreTimerFromClient; + const isTimerClientMode = () => config.options.timer && config.options.timer.restoreTimerFromClient; /* * Install behavior on events @@ -562,13 +562,13 @@ var qtiProvider = { this.trigger('enableitem enablenav'); }) .on('disableitem', function() { - if (timerClientMode) { + if (isTimerClientMode()) { stopwatch.stop(); } this.trigger('disabletools'); }) .on('enableitem', function() { - if (timerClientMode) { + if (isTimerClientMode()) { stopwatch.start(); } this.trigger('enabletools'); diff --git a/src/proxy/cache/assetPreloader.js b/src/proxy/cache/assetPreloader.js new file mode 100644 index 00000000..c1c11bdd --- /dev/null +++ b/src/proxy/cache/assetPreloader.js @@ -0,0 +1,62 @@ +/* + * 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) 2021 Open Assessment Technologies SA + */ + +import preloaders from 'taoQtiTest/runner/proxy/cache/preloaders/assets/preloaders'; +import preloaderManagerFactory from 'taoQtiTest/runner/proxy/cache/preloaderManager'; + +/** + * @callback assetPreloaderManagerAction + * @param {string} type - The type of asset to preload + * @param {string} url - the url of the asset to load/unload + * @param {string} [sourceUrl] - the unresolved URL (used to index) + * @param {string} [itemIdentifier] - the id of the item the asset belongs to + */ + +/** + * @callback assetPreloaderAction + * @param {string} url - the url of the asset to load/unload + * @param {string} [sourceUrl] - the unresolved URL (used to index) + * @param {string} [itemIdentifier] - the id of the item the asset belongs to + */ + +/** + * @typedef {object} assetPreloaderManager + * @property {string} name - The name of the preloader + * @property {assetPreloaderManagerAction} loaded - Tells whether an asset is loaded or not + * @property {assetPreloaderManagerAction} load - Preload an asset + * @property {assetPreloaderManagerAction} unload - Unload an asset + */ + +/** + * @typedef {object} assetPreloader + * @property {string} name - The name of the preloader + * @property {assetPreloaderAction} loaded - Tells whether an asset is loaded or not + * @property {assetPreloaderAction} load - Preload an asset + * @property {assetPreloaderAction} unload - Unload an asset + */ + +/** + * Manages the preloading of assets + * @function assetPreloaderFactory + * @param assetManager - A reference to the assetManager + * @return {assetPreloaderManager} + */ +const assetPreloaderFactory = preloaderManagerFactory(); +preloaders.forEach(preloader => assetPreloaderFactory.registerProvider(preloader.name, preloader)); + +export default assetPreloaderFactory; diff --git a/src/proxy/cache/interactionPreloader.js b/src/proxy/cache/interactionPreloader.js new file mode 100644 index 00000000..b6b9bda2 --- /dev/null +++ b/src/proxy/cache/interactionPreloader.js @@ -0,0 +1,62 @@ +/* + * 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) 2021 Open Assessment Technologies SA + */ + +import preloaders from 'taoQtiTest/runner/proxy/cache/preloaders/interactions/preloaders'; +import preloaderManagerFactory from 'taoQtiTest/runner/proxy/cache/preloaderManager'; + +/** + * @callback interactionPreloaderManagerAction + * @param {string} type - The type of asset to preload + * @param {object} interaction - The interaction + * @param {object} itemData - The item data + * @param {string} itemIdentifier - the id of the item the interaction belongs to + */ + +/** + * @callback interactionPreloaderAction + * @param {object} interaction - The interaction + * @param {object} itemData - The item data + * @param {string} itemIdentifier - the id of the item the interaction belongs to + */ + +/** + * @typedef {object} interactionPreloaderManager + * @property {string} name - The name of the preloader + * @property {interactionPreloaderManagerAction} loaded - Tells whether an interaction is loaded or not + * @property {interactionPreloaderManagerAction} load - Preload an interaction + * @property {interactionPreloaderManagerAction} unload - Unload an interaction + */ + +/** + * @typedef {object} interactionPreloader + * @property {string} name - The name of the preloader + * @property {interactionPreloaderAction} loaded - Tells whether an interaction is loaded or not + * @property {interactionPreloaderAction} load - Preload an interaction + * @property {interactionPreloaderAction} unload - Unload an interaction + */ + +/** + * Manages the preloading of assets + * @function assetPreloaderFactory + * @param assetManager - A reference to the assetManager + * @return {assetPreloaderManager} + */ +const interactionPreloaderFactory = preloaderManagerFactory(); +preloaders.forEach(preloader => interactionPreloaderFactory.registerProvider(preloader.name, preloader)); + +export default interactionPreloaderFactory; diff --git a/src/proxy/cache/itemPreloader.js b/src/proxy/cache/itemPreloader.js index 590b7776..24345911 100644 --- a/src/proxy/cache/itemPreloader.js +++ b/src/proxy/cache/itemPreloader.js @@ -25,45 +25,26 @@ import _ from 'lodash'; import loggerFactory from 'core/logger'; import qtiItemRunner from 'taoQtiItem/runner/qtiItemRunner'; import getAssetManager from 'taoQtiTest/runner/config/assetManager'; +import assetPreloaderFactory from 'taoQtiTest/runner/proxy/cache/assetPreloader'; +import interactionPreloaderFactory from 'taoQtiTest/runner/proxy/cache/interactionPreloader'; import urlUtil from 'util/url'; -var logger = loggerFactory('taoQtiTest/runner/proxy/cache/itemPreloader'); - -/** - * Test the support of possible `` values. - * @param {String} feature - the value to test - * @returns {Boolean} - */ -var relSupport = function relSupport(feature) { - var fakeLink = document.createElement('link'); - try { - if (fakeLink.relList && _.isFunction(fakeLink.relList.supports)) { - return fakeLink.relList.supports(feature); - } - } catch (err) { - return false; - } -}; - /** - * Does the current env supports `` + * @type {logger} + * @private */ -var supportPreload = relSupport('preload'); - -/** - * Does the current env supports `` - */ -var supportPrefetch = relSupport('prefetch'); +const logger = loggerFactory('taoQtiTest/runner/proxy/cache/itemPreloader'); /** * Check if the given item object matches the expectations - * @param {Object} item - * @param {String} item.itemIdentifier - the item identifier - * @param {String} item.baseUrl - item baseUrl - * @param {Object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] - * @returns {Boolean} + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {string} item.baseUrl - item baseUrl + * @param {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] + * @returns {boolean} + * @private */ -var isItemObjectValid = function isItemObjectValid(item) { +const isItemObjectValid = item => { return ( _.isPlainObject(item) && _.isString(item.baseUrl) && @@ -73,152 +54,64 @@ var isItemObjectValid = function isItemObjectValid(item) { ); }; +/** + * Sets a flag onto an item + * @param {object} item - The item to flag + * @param {string} flag - The flag name to set + */ +const setItemFlag = (item, flag) => { + item.flags = item.flags || {}; + item.flags[flag] = true; +}; + +/** + * Extracts the list of interactions from the item + * @param {object} itemData + * @returns {object[]} + */ +const getItemInteractions = itemData => { + const interactions = []; + if (itemData.data && itemData.data.body && itemData.data.body.elements) { + _.forEach(itemData.data.body.elements, elements => interactions.push(elements)); + } + return interactions; +}; + /** * Create an instance of an item preloader - * @param {Object} options - * @param {String} options.testId - the unique identifier of the test instance, required to get the asset manager + * @param {object} options + * @param {string} options.testId - the unique identifier of the test instance, required to get the asset manager * @returns {itemPreloader} * @throws {TypeError} if the testId is not defined */ -var itemPreloaderFactory = function itemPreloaderFactory(options) { - //this is the test asset manager, referenced under options.testId - var testAssetManager; - +function itemPreloaderFactory(options) { //we also have a specific instance of the asset manager to //resolve assets of a next item (we can't use the test asset manager). - var preloadAssetManager = getAssetManager('item-preloader'); - - //keep references to preloaded images attached - //in order to prevent garbage collection of cached images - var images = {}; - - //keep references to preloaded audio blobs - var audioBlobs = {}; - - /** - * Asset loaders per supported asset types - */ - var loaders = { - /** - * Preload images, using the in memory Image object - * @param {String} url - the url of the image to preload - * @param {String} sourceUrl - the unresolved URL (used to index) - * @param {String} itemIdentifier - the id of the item the asset belongs to - */ - img: function preloadImage(url, sourceUrl, itemIdentifier) { - images[itemIdentifier] = images[itemIdentifier] || {}; - if ('Image' in window && !images[itemIdentifier][sourceUrl]) { - images[itemIdentifier][sourceUrl] = new Image(); - images[itemIdentifier][sourceUrl].src = url; - } - }, - - /** - * Preload stylesheets - * @param {String} url - the url of the css to preload - */ - css: function preloadCss(url) { - var link = document.createElement('link'); - if (supportPreload) { - link.setAttribute('rel', 'preload'); - link.setAttribute('as', 'style'); - } else if (supportPrefetch) { - link.setAttribute('rel', 'prefetch'); - link.setAttribute('as', 'style'); - } else { - link.disabled = true; - link.setAttribute('rel', 'stylesheet'); - link.setAttribute('type', 'text/css'); - } - link.setAttribute('data-preload', true); - link.setAttribute('href', url); - - document.querySelector('head').appendChild(link); - }, - - /** - * Preload audio files : save the blobs for later use in the asset manager - * @param {String} url - the url of the audio file to preload - * @param {String} sourceUrl - the unresolved URL (used to index) - * @param {String} itemIdentifier - the id of the item the asset belongs to - */ - audio: function preloadAudio(url, sourceUrl, itemIdentifier) { - var request; - audioBlobs[itemIdentifier] = audioBlobs[itemIdentifier] || {}; - if (typeof audioBlobs[itemIdentifier][sourceUrl] === 'undefined') { - //direct XHR to benefit from the "blob" response type - request = new XMLHttpRequest(); - request.open('GET', url, true); - request.responseType = 'blob'; - request.onload = function onRequestLoad() { - if (this.status === 200) { - //save the blob, directly - audioBlobs[itemIdentifier][sourceUrl] = this.response; - } - }; - //ignore failed requests, best effort only - request.send(); - } - } - }; - - /** - * Asset unloaders per supported asset types - */ - var unloaders = { - /** - * Remove images ref so they can be garbage collected - * @param {String} url - the url of the image to unload - * @param {String} sourceUrl - the unresolved URL (used to index) - * @param {String} itemIdentifier - the id of the item the asset belongs to - */ - img: function unloadImage(url, sourceUrl, itemIdentifier) { - if (images[itemIdentifier]) { - images[itemIdentifier] = _.omit(images[itemIdentifier], sourceUrl); - } - }, - - /** - * Remove prefteched CSS link tag - * @param {String} url - the url of the css to unload - */ - css: function unloadCss(url) { - var link = document.querySelector(`head link[data-preload][href="${url}"]`); - if (link) { - document.querySelector('head').removeChild(link); - } - }, - - /** - * Remove loaded audio files - * @param {String} url - the url of the css to unload - * @param {String} sourceUrl - the unresolved URL - * @param {String} itemIdentifier - the id of the item the asset belongs to - */ - audio: function unloadAudio(url, sourceUrl, itemIdentifier) { - if (audioBlobs[itemIdentifier]) { - audioBlobs[itemIdentifier] = _.omit(audioBlobs[itemIdentifier], sourceUrl); - } - } - }; + const preloadAssetManager = getAssetManager('item-preloader'); /** * Resolves assets URLS using the assetManager - * @param {String} baseUrl - * @param {Object} assets - as [ type : [urls] ] + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {string} item.baseUrl - item baseUrl + * @param {string} item.itemData.type - type of item + * @param {object} item.itemData.data - item data + * @param {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] * @returns {Promise} assets with URLs resolved + * @private */ - var resolveAssets = function resolveAssets(baseUrl, assets) { - return new Promise(function (resolve) { - preloadAssetManager.setData('baseUrl', baseUrl); + const resolveAssets = item => { + return new Promise(resolve => { + const { assets } = item.itemData; + preloadAssetManager.setData('baseUrl', item.baseUrl); preloadAssetManager.setData('assets', assets); return resolve( _.reduce( assets, - function (acc, assetList, type) { - var resolved = {}; - _.forEach(assetList, function (url) { + (acc, assetList, type) => { + const resolved = {}; + _.forEach(assetList, url => { //filter base64 (also it seems sometimes we just have base64 data, without the protocol...) if (!urlUtil.isBase64(url)) { resolved[url] = preloadAssetManager.resolve(url); @@ -239,129 +132,186 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { throw new TypeError('The test identifier is mandatory to start the item preloader'); } - testAssetManager = getAssetManager(options.testId); + //this is the test asset manager, referenced under options.testId + const testAssetManager = getAssetManager(options.testId); + + //mechanisms to preload assets and runtimes + const assetPreloader = assetPreloaderFactory(testAssetManager); + const interactionPreloader = interactionPreloaderFactory(); + + /** + * Preload the item runner + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {string} item.baseUrl - item baseUrl + * @param {string} item.itemData.type - type of item + * @param {object} item.itemData.data - item data + * @param {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] + * @returns {Promise} + * @private + */ + const itemLoad = item => { + logger.debug(`Start preloading of item ${item.itemIdentifier}`); + return new Promise((resolve, reject) => { + qtiItemRunner(item.itemData.type, item.itemData.data, { + assetManager: preloadAssetManager, + preload: true + }) + .on('init', () => { + logger.debug(`Preloading of item ${item.itemIdentifier} done`); + resolve(true); + }) + .on('error', reject) + .init(); + }); + }; /** - * Prepend a strategy to resolves cached assets + * Preload the interactions + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {object} item.itemData.data - item data + * @returns {Promise} + * @private */ - testAssetManager.prependStrategy({ - name: 'precaching', - handle: function handlePrecache(url, data) { - var sourceUrl = url.toString(); + const interactionLoad = item => { + return Promise.all(getItemInteractions(item.itemData).map(interaction => { + if (interactionPreloader.has(interaction.qtiClass)) { + logger.debug(`Loading interaction ${interaction.serial}(${interaction.qtiClass}) for item ${item.itemIdentifier}`); + return interactionPreloader.load(interaction.qtiClass, interaction, item.itemData, item.itemIdentifier); + } + return Promise.resolve(); + })); + }; - //resolves precached audio files - if ( - data.itemIdentifier && - audioBlobs[data.itemIdentifier] && - typeof audioBlobs[data.itemIdentifier][sourceUrl] !== 'undefined' - ) { - //creates an internal URL to link the audio blob - return URL.createObjectURL(audioBlobs[data.itemIdentifier][sourceUrl]); + /** + * Unload the interactions + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {object} item.itemData.data - item data + * @returns {Promise} + * @private + */ + const interactionUnload = item => { + return Promise.all(getItemInteractions(item.itemData).map(interaction => { + if (interactionPreloader.has(interaction.qtiClass)) { + logger.debug(`Unloading interaction ${interaction.serial}(${interaction.qtiClass}) for item ${item.itemIdentifier}`); + return interactionPreloader.unload(interaction.qtiClass, interaction, item.itemData, item.itemIdentifier); } - } - }); + return Promise.resolve(); + })); + }; /** - * @typedef {Object} itemPreloader + * Preload the item assets + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {string} item.baseUrl - item baseUrl + * @param {string} item.itemData.type - type of item + * @param {object} item.itemData.data - item data + * @param {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] + * @returns {Promise} + * @private + */ + const assetLoad = item => { + return resolveAssets(item).then(resolved => { + _.forEach(resolved, (assets, type) => { + if (assetPreloader.has(type)) { + _.forEach(assets, (url, sourceUrl) => { + logger.debug(`Loading asset ${sourceUrl}(${type}) for item ${item.itemIdentifier}`); + assetPreloader.load(type, url, sourceUrl, item.itemIdentifier); + }); + } else { + setItemFlag(item, 'containsNonPreloadedAssets'); + } + }); + return true; + }); + }; + + /** + * Unload the item assets + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {string} item.baseUrl - item baseUrl + * @param {string} item.itemData.type - type of item + * @param {object} item.itemData.data - item data + * @param {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] + * @returns {Promise} + * @private + */ + const assetUnload = item => { + return resolveAssets(item).then(resolved => { + _.forEach(resolved, (assets, type) => { + if (assetPreloader.has(type)) { + _.forEach(assets, (url, sourceUrl) => { + logger.debug(`Unloading asset ${sourceUrl}(${type}) for item ${item.itemIdentifier}`); + assetPreloader.unload(type, url, sourceUrl, item.itemIdentifier); + }); + } + }); + return true; + }); + }; + + /** + * @typedef {object} itemPreloader */ return { /** * Preload the given item (runtime and assets) * - * @param {Object} item - * @param {String} item.itemIdentifier - the item identifier - * @param {String} item.baseUrl - item baseUrl - * @param {Object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {string} item.baseUrl - item baseUrl + * @param {string} item.itemData.type - type of item + * @param {object} item.itemData.data - item data + * @param {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] * @returns {Promise} resolves with true if the item is loaded */ - preload: function preload(item) { - var loading = []; - - /** - * Preload the item runner - * @returns {Promise} - */ - var itemLoad = function itemLoad() { - logger.debug(`Start preloading of item ${item.itemIdentifier}`); - return new Promise(function (resolve, reject) { - qtiItemRunner(item.itemData.type, item.itemData.data, { - assetManager: preloadAssetManager, - preload: true - }) - .on('init', function () { - logger.debug(`Preloading of item ${item.itemIdentifier} done`); - resolve(true); - }) - .on('error', reject) - .init(); - }); - }; - - /** - * Preload the item assets - * @returns {Promise} - */ - var assetLoad = function assetLoad() { - return resolveAssets(item.baseUrl, item.itemData.assets).then(function (resolved) { - _.forEach(resolved, function (assets, type) { - if (_.isFunction(loaders[type])) { - _.forEach(assets, function (url, sourceUrl) { - logger.debug(`Loading asset ${sourceUrl}(${type}) for item ${item.itemIdentifier}`); - - loaders[type](url, sourceUrl, item.itemIdentifier); - }); - } else { - item.flags = Object.assign({}, item.flags, { containsNonPreloadedAssets: true }); - } - }); - return true; - }); - }; + preload(item) { + const loading = []; if (isItemObjectValid(item)) { - loading.push(itemLoad()); + loading.push(itemLoad(item)); + loading.push(interactionLoad(item)); - if (_.size(item.itemData.data.feedbacks)) { - item.flags = Object.assign({}, item.flags, { hasFeedbacks: true }); + if (_.size(item.itemData.data && item.itemData.data.feedbacks)) { + setItemFlag(item, 'hasFeedbacks'); } if (_.size(item.itemData.assets) > 0) { - loading.push(assetLoad()); + loading.push(assetLoad(item)); } } - return Promise.all(loading).then(function (results) { - return results.length > 0 && _.all(results, _.isTrue); - }); + return Promise.all(loading).then(results => results.length > 0 && _.all(results, _.isTrue)); }, /** * Unload the assets for the given item * - * @param {Object} item - * @param {String} item.itemIdentifier - the item identifier - * @param {String} item.baseUrl - item baseUrl - * @param {Object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] - * @param {String} itemIdentifier - the item identifier + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {string} item.baseUrl - item baseUrl + * @param {string} item.itemData.type - type of item + * @param {object} item.itemData.data - item data + * @param {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] * @returns {Promise} */ - unload: function unload(item) { - if (isItemObjectValid(item) && _.size(item.itemData.assets) > 0) { - return resolveAssets(item.baseUrl, item.itemData.assets).then(function (resolved) { - _.forEach(resolved, function (assets, type) { - if (_.isFunction(unloaders[type])) { - _.forEach(assets, function (url, sourceUrl) { - logger.debug(`Unloading asset ${sourceUrl}(${type}) for item ${item.itemIdentifier}`); + unload(item) { + const loading = []; - unloaders[type](url, sourceUrl, item.itemIdentifier); - }); - } - }); - return true; - }); + if (isItemObjectValid(item)) { + loading.push(interactionUnload(item)); + + if (_.size(item.itemData.assets) > 0) { + loading.push(assetUnload(item)); + } } - return Promise.resolve(false); + + return Promise.all(loading).then(results => results.length > 0 && _.all(results, _.isTrue)); } }; -}; +} export default itemPreloaderFactory; diff --git a/src/proxy/cache/preloaderManager.js b/src/proxy/cache/preloaderManager.js new file mode 100644 index 00000000..a678a628 --- /dev/null +++ b/src/proxy/cache/preloaderManager.js @@ -0,0 +1,110 @@ +/* + * 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) 2021 Open Assessment Technologies SA + */ + +import providerRegistry from 'core/providerRegistry'; + +/** + * @typedef {object} preloaderManager + * @property {Function} has - Tells whether an asset is loaded or not + * @property {preloaderManagerAction} loaded - Tells whether an asset is loaded or not + * @property {preloaderManagerAction} load - Preload an asset + * @property {preloaderManagerAction} unload - Unload an asset + */ + +/** + * @callback preloaderManagerAction + * @param {string} name - The type of asset to preload + * @param {...any} args - The list of args related to the preloader. + * @returns {any} + */ + +/** + * Creates a preloader manager. + * @return {Function} + */ +export default function preloaderManagerFactory() { + /** + * Manages the preloading of assets + * @param assetManager - A reference to the assetManager + * @return {preloaderManager} + */ + function preloaderFactory(assetManager) { + const preloaders = {}; + preloaderFactory.getAvailableProviders().forEach(name => { + preloaders[name] = preloaderFactory.getProvider(name).init(assetManager); + }); + + /** + * @typedef preloaderManager + */ + return { + /** + * Checks whether or not an asset preloader exists for a particular type + * @param {string} name + * @returns {boolean} + */ + has(name) { + return !!preloaders[name]; + }, + + /** + * Tells whether an asset was preloaded or not + * @param {string} name - The type of asset to preload + * @param {...any} args - The list of args related to the preloader. + * @returns {boolean} + */ + loaded(name, ...args) { + const preloader = preloaders[name]; + if (preloader && 'function' === typeof preloader.loaded) { + return !!preloader.loaded(...args); + } + return false; + }, + + /** + * Preloads an asset with respect to it type + * @param {string} name - The type of asset to preload + * @param {...any} args - The list of args related to the preloader. + * @returns {Promise} + */ + load(name, ...args) { + const preloader = preloaders[name]; + if (preloader && 'function' === typeof preloader.load) { + return Promise.resolve(preloader.load(...args)); + } + return Promise.resolve(); + }, + + /** + * Unloads an asset with respect to it type + * @param {string} name - The type of asset to unload + * @param {...any} args - The list of args related to the preloader. + * @returns {Promise} + */ + unload(name, ...args) { + const preloader = preloaders[name]; + if (preloader && 'function' === typeof preloader.unload) { + return Promise.resolve(preloader.unload(...args)); + } + return Promise.resolve(); + } + }; + } + + return providerRegistry(preloaderFactory); +} diff --git a/src/proxy/cache/preloaders/assets/audio.js b/src/proxy/cache/preloaders/assets/audio.js new file mode 100644 index 00000000..bf381119 --- /dev/null +++ b/src/proxy/cache/preloaders/assets/audio.js @@ -0,0 +1,119 @@ +/* + * 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) 2017-2021 Open Assessment Technologies SA + */ + +import _ from 'lodash'; + +/** + * (Pre)load audio content. + * + * @author Bertrand Chevrier + */ +export default { + /** + * The name of the preloader + * @type {string} + */ + name: 'audio', + + /** + * Manages the preloading of audio files + * @param assetManager - A reference to the assetManager + * @returns {assetPreloader} + */ + init(assetManager) { + //keep references to preloaded audio blobs + const audioBlobs = {}; + + //prepend a strategy to resolves cached assets + assetManager.prependStrategy({ + name: 'precaching-audio', + handle(url, data) { + const sourceUrl = url.toString(); + + //resolves precached audio files + if ( + data.itemIdentifier && + audioBlobs[data.itemIdentifier] && + 'undefined' !== typeof audioBlobs[data.itemIdentifier][sourceUrl] + ) { + //creates an internal URL to link the audio blob + return URL.createObjectURL(audioBlobs[data.itemIdentifier][sourceUrl]); + } + } + }); + + return { + /** + * Tells whether an audio file was preloaded or not + * @param {string} url - the url of the audio file to preload + * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {boolean} + */ + loaded(url, sourceUrl, itemIdentifier) { + return !!(audioBlobs[itemIdentifier] && audioBlobs[itemIdentifier][sourceUrl]); + }, + + /** + * Preloads audio files : save the blobs for later use in the asset manager + * @param {string} url - the url of the audio file to preload + * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {Promise} + */ + load(url, sourceUrl, itemIdentifier) { + return new Promise(resolve => { + audioBlobs[itemIdentifier] = audioBlobs[itemIdentifier] || {}; + if ('undefined' === typeof audioBlobs[itemIdentifier][sourceUrl]) { + //direct XHR to benefit from the "blob" response type + const request = new XMLHttpRequest(); + request.open('GET', url, true); + request.responseType = 'blob'; + request.onerror = resolve; + request.onabort = resolve; + request.onload = () => { + if (request.status === 200) { + //save the blob, directly + audioBlobs[itemIdentifier][sourceUrl] = request.response; + } + resolve(); + }; + //ignore failed requests, best effort only + request.send(); + } else { + resolve(); + } + }); + }, + + /** + * Removes loaded audio files + * @param {string} url - the url of the audio file to unload + * @param {string} sourceUrl - the unresolved URL + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {Promise} + */ + unload(url, sourceUrl, itemIdentifier) { + if (audioBlobs[itemIdentifier]) { + audioBlobs[itemIdentifier] = _.omit(audioBlobs[itemIdentifier], sourceUrl); + } + return Promise.resolve(); + } + }; + } +}; diff --git a/src/proxy/cache/preloaders/assets/image.js b/src/proxy/cache/preloaders/assets/image.js new file mode 100644 index 00000000..90c0ccba --- /dev/null +++ b/src/proxy/cache/preloaders/assets/image.js @@ -0,0 +1,85 @@ +/* + * 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) 2017-2021 Open Assessment Technologies SA + */ + +import _ from 'lodash'; + +/** + * (Pre)load images. + * + * @author Bertrand Chevrier + */ +export default { + /** + * The name of the preloader + * @type {string} + */ + name: 'img', + + /** + * Manages the preloading of images + * @returns {assetPreloader} + */ + init() { + //keep references to preloaded images attached + //in order to prevent garbage collection of cached images + const images = {}; + + return { + /** + * Tells whether an image was preloaded or not + * @param {string} url - the url of the image to preload + * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {boolean} + */ + loaded(url, sourceUrl, itemIdentifier) { + return !!(images[itemIdentifier] && images[itemIdentifier][sourceUrl]); + }, + + /** + * Preloads an image, using the in memory Image object + * @param {string} url - the url of the image to preload + * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {Promise} + */ + load(url, sourceUrl, itemIdentifier) { + images[itemIdentifier] = images[itemIdentifier] || {}; + if ('Image' in window && !images[itemIdentifier][sourceUrl]) { + images[itemIdentifier][sourceUrl] = new Image(); + images[itemIdentifier][sourceUrl].src = url; + } + return Promise.resolve(); + }, + + /** + * Removes images ref so they can be garbage collected + * @param {string} url - the url of the image to unload + * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {Promise} + */ + unload(url, sourceUrl, itemIdentifier) { + if (images[itemIdentifier]) { + images[itemIdentifier] = _.omit(images[itemIdentifier], sourceUrl); + } + return Promise.resolve(); + } + }; + } +}; diff --git a/src/proxy/cache/preloaders/assets/preloaders.js b/src/proxy/cache/preloaders/assets/preloaders.js new file mode 100644 index 00000000..58996562 --- /dev/null +++ b/src/proxy/cache/preloaders/assets/preloaders.js @@ -0,0 +1,27 @@ +/* + * 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) 2021 Open Assessment Technologies SA + */ + +import audioPreloader from 'taoQtiTest/runner/proxy/cache/preloaders/assets/audio'; +import imagePreloader from 'taoQtiTest/runner/proxy/cache/preloaders/assets/image'; +import stylesheetPreloader from 'taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'; + +/** + * The list of asset loader factories + * @type {Function[]} + */ +export default [audioPreloader, imagePreloader, stylesheetPreloader]; diff --git a/src/proxy/cache/preloaders/assets/stylesheet.js b/src/proxy/cache/preloaders/assets/stylesheet.js new file mode 100644 index 00000000..95a4f951 --- /dev/null +++ b/src/proxy/cache/preloaders/assets/stylesheet.js @@ -0,0 +1,137 @@ +/* + * 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) 2017-2021 Open Assessment Technologies SA + */ + +import _ from 'lodash'; + +/** + * Test the support of possible `` values. + * @param {string} feature - the value to test + * @returns {boolean} + * @private + */ +const relSupport = feature => { + const fakeLink = document.createElement('link'); + try { + if (fakeLink.relList && 'function' === typeof fakeLink.relList.supports) { + return fakeLink.relList.supports(feature); + } + } catch (err) { + return false; + } +}; + +/** + * Does the current env supports `` + * @type {boolean} + * @private + */ +const supportPreload = relSupport('preload'); + +/** + * Does the current env supports `` + * @type {boolean} + * @private + */ +const supportPrefetch = relSupport('prefetch'); + +/** + * (Pre)load stylesheets. + * + * @author Bertrand Chevrier + */ +export default { + /** + * The name of the preloader + * @type {string} + */ + name: 'css', + + /** + * Manages the preloading of stylesheets + * @returns {assetPreloader} + */ + init() { + //keep references to preloaded CSS files + const stylesheets = {}; + + return { + /** + * Tells whether a stylesheet was preloaded or not + * @param {string} url - the url of the stylesheet to preload + * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {boolean} + */ + loaded(url, sourceUrl, itemIdentifier) { + return !!(stylesheets[itemIdentifier] && stylesheets[itemIdentifier][sourceUrl]); + }, + + /** + * Preloads a stylesheet + * @param {string} url - the url of the stylesheet to preload + * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {Promise} + */ + load(url, sourceUrl, itemIdentifier) { + stylesheets[itemIdentifier] = stylesheets[itemIdentifier] || {}; + + if (!stylesheets[itemIdentifier][sourceUrl]) { + const link = document.createElement('link'); + if (supportPreload) { + link.setAttribute('rel', 'preload'); + link.setAttribute('as', 'style'); + } else if (supportPrefetch) { + link.setAttribute('rel', 'prefetch'); + link.setAttribute('as', 'style'); + } else { + link.disabled = true; + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + } + link.setAttribute('data-preload', true); + link.setAttribute('href', url); + + document.querySelector('head').appendChild(link); + stylesheets[itemIdentifier][sourceUrl] = link; + } + return Promise.resolve(); + }, + + /** + * Removes the prefetched stylesheet + * @param {string} url - the url of the stylesheet to unload + * * @param {string} sourceUrl - the unresolved URL (used to index) + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {Promise} + */ + unload(url, sourceUrl, itemIdentifier) { + if (stylesheets[itemIdentifier]) { + const link = + stylesheets[itemIdentifier][sourceUrl] || + document.querySelector(`head link[data-preload][href="${url}"]`); + if (link) { + document.querySelector('head').removeChild(link); + } + stylesheets[itemIdentifier] = _.omit(stylesheets[itemIdentifier], sourceUrl); + } + return Promise.resolve(); + } + }; + } +}; diff --git a/src/proxy/cache/preloaders/interactions/extendedText.js b/src/proxy/cache/preloaders/interactions/extendedText.js new file mode 100644 index 00000000..a063432c --- /dev/null +++ b/src/proxy/cache/preloaders/interactions/extendedText.js @@ -0,0 +1,136 @@ +/* + * 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) 2021 Open Assessment Technologies SA + */ + +import ckEditor from 'ckeditor'; +import ckConfigurator from 'taoQtiItem/qtiCommonRenderer/helpers/ckConfigurator'; + +/** + * Preloads CKEditor runtimes for a particular language + * @param {string} language + * @returns {Promise} + */ +function preloadCKEditor(language) { + return new Promise(resolve => { + const placeholder = document.createElement('div'); + const ckOptions = { + resize_enabled: true, + secure: location.protocol === 'https:', + forceCustomDomain: true, + language + }; + const editor = ckEditor.inline(placeholder, ckOptions); + editor.on('instanceReady', () => { + resolve(editor); + }); + editor.on('configLoaded', () => { + editor.config = ckConfigurator.getConfig(editor, 'extendedText', ckOptions); + }); + if (editor.status === 'ready' || editor.status === 'loaded') { + resolve(editor); + } + }).then(editor => { + editor.destroy(); + }); +} + +/** + * The default item language + * @type {string} + * @private + */ +const defaultLang = 'en'; + +/** + * Gets the item's language + * @param {object} itemData - The item data + * @returns {string} + * @private + */ +const getItemLanguage = itemData => { + let lang = itemData && itemData.data && itemData.data.attributes && itemData.data.attributes['xml:lang']; + if (!lang) { + lang = window.document.documentElement.getAttribute('lang'); + } + return (lang && lang.split('-')[0]) || defaultLang; +}; + +/** + * Preloads the runtimes for an extendedText interaction + */ +export default { + /** + * The name of the preloader + * @type {string} + */ + name: 'extendedTextInteraction', + + /** + * Manages the preloading of the extendedText interaction runtimes + * @returns {interactionPreloader} + */ + init() { + const preloadedLanguages = {}; + + return { + /** + * Tells whether the runtimes has been preloaded or not + * @param {object} interaction - The interaction + * @param {object} itemData - The item data + * @param {string} itemIdentifier - the id of the item the interaction belongs to + * @returns {boolean} + */ + loaded(interaction, itemData, itemIdentifier) { + if (interaction.attributes && interaction.attributes.format === 'xhtml') { + const lang = getItemLanguage(itemData); + return preloadedLanguages[lang]; + } + return true; + }, + + /** + * Preloads runtimes for an extendedText interaction + * @param {object} interaction - The interaction + * @param {object} itemData - The item data + * @param {string} itemIdentifier - the id of the item the interaction belongs to + * @returns {Promise} + */ + load(interaction, itemData, itemIdentifier) { + if (interaction.attributes && interaction.attributes.format === 'xhtml') { + const lang = getItemLanguage(itemData); + if (!preloadedLanguages[lang]) { + preloadedLanguages[lang] = true; + return preloadCKEditor(lang); + } + } + return Promise.resolve(); + }, + + /** + * Unloads runtimes for an extendedText interaction + * @param {object} interaction - The interaction + * @param {object} itemData - The item data + * @param {string} itemIdentifier - the id of the item the interaction belongs to + * @returns {Promise} + */ + unload(interaction, itemData, itemIdentifier) { + // nothing to do actually + return Promise.resolve(); + } + }; + } +}; diff --git a/src/proxy/cache/preloaders/interactions/preloaders.js b/src/proxy/cache/preloaders/interactions/preloaders.js new file mode 100644 index 00000000..348ea388 --- /dev/null +++ b/src/proxy/cache/preloaders/interactions/preloaders.js @@ -0,0 +1,25 @@ +/* + * 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) 2021 Open Assessment Technologies SA + */ + +import extendedTextPreloader from 'taoQtiTest/runner/proxy/cache/preloaders/interactions/extendedText'; + +/** + * The list of asset loader factories + * @type {Function[]} + */ +export default [extendedTextPreloader]; diff --git a/test/mocks/ckConfiguratorMock.js b/test/mocks/ckConfiguratorMock.js new file mode 100644 index 00000000..7653a746 --- /dev/null +++ b/test/mocks/ckConfiguratorMock.js @@ -0,0 +1,22 @@ +/** + * 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) 2021 (original work) Open Assessment Technologies SA ; + */ +define(function () { + return function (editor, toolbarType, options) { + return Object.assign({}, editor.config, options); + }; +}); diff --git a/test/mocks/ckeditorMock.js b/test/mocks/ckeditorMock.js new file mode 100644 index 00000000..418191e1 --- /dev/null +++ b/test/mocks/ckeditorMock.js @@ -0,0 +1,40 @@ +/** + * 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) 2021 (original work) Open Assessment Technologies SA ; + */ +define(['core/eventifier'], function (eventifier) { + return { + inline(element, config) { + const editor = eventifier({ + config, + status: 'loading', + destroy() { + if (element) { + element.remove(); + } + element = null; + } + }); + editor + .after('configLoaded', () => { + editor.status = 'ready'; + editor.trigger('instanceReady'); + }) + .trigger('configLoaded'); + return editor; + } + }; +}); diff --git a/test/navigator/offlineNavigator/test.html b/test/navigator/offlineNavigator/test.html index da2a155b..5eb665a1 100644 --- a/test/navigator/offlineNavigator/test.html +++ b/test/navigator/offlineNavigator/test.html @@ -7,6 +7,12 @@ + + + +
+
+ + diff --git a/test/proxy/cache/assetPreloader/test.js b/test/proxy/cache/assetPreloader/test.js new file mode 100644 index 00000000..e6c4125b --- /dev/null +++ b/test/proxy/cache/assetPreloader/test.js @@ -0,0 +1,219 @@ +/** + * 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) 2021 Open Assessment Technologies SA ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/assetPreloader + */ +define(['taoQtiTest/runner/proxy/cache/assetPreloader'], function (assetPreloaderFactory) { + 'use strict'; + + const expectedAssetManager = {}; + const expectedUrl = 'sample.css'; + const expectedSourceUrl = 'http://test.com/sample.css'; + const expectedItemIdentifier = 'item-1'; + + QUnit.module('API', { + beforeEach() { + assetPreloaderFactory.clearProviders(); + } + }); + + QUnit.test('module', assert => { + assert.expect(3); + assert.equal(typeof assetPreloaderFactory, 'function', 'The module exposes a function'); + assert.equal(typeof assetPreloaderFactory(), 'object', 'The module is a factory'); + assert.notDeepEqual(assetPreloaderFactory(), assetPreloaderFactory(), 'The factory creates new instances'); + }); + + QUnit.cases + .init([{ title: 'has' }, { title: 'loaded' }, { title: 'load' }, { title: 'unload' }]) + .test('method ', (data, assert) => { + assert.expect(1); + const preloader = assetPreloaderFactory(); + + assert.equal(typeof preloader[data.title], 'function', `The assets preloader has the method ${data.title}`); + }); + + QUnit.module('behavior', { + beforeEach() { + assetPreloaderFactory.clearProviders(); + } + }); + + QUnit.test('has', assert => { + assert.expect(4); + const preloader = { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + loaded() { + return false; + }, + load() { + assert.ok(false, 'The asset should not be loaded'); + }, + unload() { + assert.ok(false, 'The asset should not be unloaded'); + } + }; + } + }; + assetPreloaderFactory.registerProvider(preloader.name, preloader); + const assetPreloader = assetPreloaderFactory(expectedAssetManager); + + assert.ok(assetPreloader.has('asset'), 'The asset preloader has a Asset preloader'); + assert.ok(!assetPreloader.has('dummy'), 'The asset preloader does not have a dummy preloader'); + }); + + QUnit.test('loaded', assert => { + assert.expect(10); + + let loadedStatus = null; + const preloader = { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + loaded(url, sourceUrl, itemIdentifier) { + assert.strictEqual(url, expectedUrl, 'The expected url has been given'); + assert.strictEqual(sourceUrl, expectedSourceUrl, 'The expected source url has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + return loadedStatus; + }, + load() { + assert.ok(false, 'The asset should not be loaded'); + }, + unload() { + assert.ok(false, 'The asset should not be unloaded'); + } + }; + } + }; + assetPreloaderFactory.registerProvider(preloader.name, preloader); + const assetPreloader = assetPreloaderFactory(expectedAssetManager); + + assert.strictEqual( + assetPreloader.loaded('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier), + false + ); + loadedStatus = 1; + assert.strictEqual( + assetPreloader.loaded('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier), + true + ); + }); + + QUnit.test('load', assert => { + assert.expect(8); + const ready = assert.async(); + const preloader = { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'asset', + loaded() { + return true; + }, + load(url, sourceUrl, itemIdentifier) { + assert.strictEqual(url, expectedUrl, 'The expected url has been given'); + assert.strictEqual(sourceUrl, expectedSourceUrl, 'The expected source url has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + assert.ok(true, 'The asset is loaded'); + return Promise.resolve(); + }, + unload() { + assert.ok(false, 'The asset should not be unloaded'); + } + }; + } + }; + assetPreloaderFactory.registerProvider(preloader.name, preloader); + const assetPreloader = assetPreloaderFactory(expectedAssetManager); + + assetPreloader + .load('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The asset preloader loaded asset'); + }) + .then(() => assetPreloader.load('dummy')) + .then(() => { + assert.ok(true, 'The asset preloader accept unknown loader without failing'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); + + QUnit.test('unload', assert => { + assert.expect(8); + const ready = assert.async(); + const preloader = { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'asset', + loaded() { + return true; + }, + load() { + assert.ok(false, 'The asset should not be loaded'); + }, + unload(url, sourceUrl, itemIdentifier) { + assert.strictEqual(url, expectedUrl, 'The expected url has been given'); + assert.strictEqual(sourceUrl, expectedSourceUrl, 'The expected source url has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + assert.ok(true, 'The asset is unloaded'); + return Promise.resolve(); + } + }; + } + }; + assetPreloaderFactory.registerProvider(preloader.name, preloader); + const assetPreloader = assetPreloaderFactory(expectedAssetManager); + + assetPreloader + .unload('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The asset preloader unloaded asset'); + }) + .then(() => assetPreloader.unload('dummy')) + .then(() => { + assert.ok(true, 'The asset preloader accept unknown loader without failing'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); diff --git a/test/proxy/cache/interactionPreloader/test.html b/test/proxy/cache/interactionPreloader/test.html new file mode 100644 index 00000000..96550250 --- /dev/null +++ b/test/proxy/cache/interactionPreloader/test.html @@ -0,0 +1,27 @@ + + + + + Test Runner - interaction preloader + + + + +
+
+ + diff --git a/test/proxy/cache/interactionPreloader/test.js b/test/proxy/cache/interactionPreloader/test.js new file mode 100644 index 00000000..51f7d346 --- /dev/null +++ b/test/proxy/cache/interactionPreloader/test.js @@ -0,0 +1,165 @@ +/** + * 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) 2021 Open Assessment Technologies SA ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/interactionPreloader + */ +define(['taoQtiTest/runner/proxy/cache/interactionPreloader'], function (interactionPreloaderFactory) { + 'use strict'; + + const expectedInteraction = { + attributes: {}, + qtiClass: 'interaction' + }; + const expectedItemData = {}; + const expectedItemIdentifier = 'item-1'; + + QUnit.module('API', { + beforeEach() { + interactionPreloaderFactory.clearProviders(); + } + }); + + QUnit.test('module', assert => { + assert.expect(3); + assert.equal(typeof interactionPreloaderFactory, 'function', 'The module exposes a function'); + assert.equal(typeof interactionPreloaderFactory(), 'object', 'The module is a factory'); + assert.notDeepEqual( + interactionPreloaderFactory(), + interactionPreloaderFactory(), + 'The factory creates new instances' + ); + }); + + QUnit.cases + .init([{ title: 'has' }, { title: 'loaded' }, { title: 'load' }, { title: 'unload' }]) + .test('method ', (data, assert) => { + assert.expect(1); + const preloader = interactionPreloaderFactory(); + + assert.equal( + typeof preloader[data.title], + 'function', + `The interactions preloader has the method ${data.title}` + ); + }); + + QUnit.module('behavior', { + beforeEach() { + interactionPreloaderFactory.clearProviders(); + } + }); + + QUnit.test('has', assert => { + assert.expect(3); + const preloader = { + name: 'interaction', + init() { + assert.ok(true, 'Interaction preloader created'); + return {}; + } + }; + interactionPreloaderFactory.registerProvider(preloader.name, preloader); + const interactionPreloader = interactionPreloaderFactory(); + + assert.ok(interactionPreloader.has('interaction'), 'The interaction preloader has a interaction preloader'); + assert.ok(!interactionPreloader.has('dummy'), 'The interaction preloader does not have a dummy preloader'); + }); + + QUnit.test('load', assert => { + assert.expect(7); + const ready = assert.async(); + const preloader = { + name: 'interaction', + init() { + assert.ok(true, 'Interaction preloader created'); + return { + load(interaction, itemData, itemIdentifier) { + assert.strictEqual(interaction, expectedInteraction, 'The expected interaction has been given'); + assert.strictEqual(itemData, expectedItemData, 'The expected itemData has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + assert.ok(true, 'The interaction is loaded'); + return Promise.resolve(); + }, + unload() { + assert.ok(false, 'The interaction should not be unloaded'); + } + }; + } + }; + interactionPreloaderFactory.registerProvider(preloader.name, preloader); + const interactionPreloader = interactionPreloaderFactory(); + + interactionPreloader + .load('interaction', expectedInteraction, expectedItemData, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The interaction preloader loaded interaction'); + }) + .then(() => interactionPreloader.load('dummy')) + .then(() => { + assert.ok(true, 'The interaction preloader accept unknown loader without failing'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); + + QUnit.test('unload', assert => { + assert.expect(7); + const ready = assert.async(); + const preloader = { + name: 'interaction', + init() { + assert.ok(true, 'Interaction preloader created'); + return { + load() { + assert.ok(false, 'The interaction should not be loaded'); + }, + unload(interaction, itemData, itemIdentifier) { + assert.strictEqual(interaction, expectedInteraction, 'The expected interaction has been given'); + assert.strictEqual(itemData, expectedItemData, 'The expected itemData has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + assert.ok(true, 'The interaction is unloaded'); + return Promise.resolve(); + } + }; + } + }; + + interactionPreloaderFactory.registerProvider(preloader.name, preloader); + const interactionPreloader = interactionPreloaderFactory(); + interactionPreloader + .unload('interaction', expectedInteraction, expectedItemData, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The interaction preloader unloaded interaction'); + }) + .then(() => interactionPreloader.unload('dummy')) + .then(() => { + assert.ok(true, 'The interaction preloader accept unknown loader without failing'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); diff --git a/test/proxy/cache/itemPreloader/test.html b/test/proxy/cache/itemPreloader/test.html index 3ff25642..a3fe9467 100644 --- a/test/proxy/cache/itemPreloader/test.html +++ b/test/proxy/cache/itemPreloader/test.html @@ -7,6 +7,12 @@ + + + +
+
+ + diff --git a/test/proxy/cache/preloaderManager/test.js b/test/proxy/cache/preloaderManager/test.js new file mode 100644 index 00000000..2896bf17 --- /dev/null +++ b/test/proxy/cache/preloaderManager/test.js @@ -0,0 +1,264 @@ +/** + * 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) 2021 Open Assessment Technologies SA ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/assetPreloader + */ +define(['taoQtiTest/runner/proxy/cache/preloaderManager'], function (preloaderManagerFactory) { + 'use strict'; + + const expectedAssetManager = {}; + const expectedUrl = 'sample.css'; + const expectedSourceUrl = 'http://test.com/sample.css'; + const expectedItemIdentifier = 'item-1'; + + QUnit.module('API'); + + QUnit.test('module', assert => { + assert.expect(3); + + assert.equal(typeof preloaderManagerFactory, 'function', 'The module exposes a function'); + assert.equal(typeof preloaderManagerFactory(), 'function', 'The module is a factory'); + assert.notDeepEqual(preloaderManagerFactory(), preloaderManagerFactory(), 'The factory creates new instances'); + }); + + QUnit.cases + .init([ + { title: 'registerProvider' }, + { title: 'getProvider' }, + { title: 'getAvailableProviders' }, + { title: 'clearProviders' } + ]) + .test('registry ', (data, assert) => { + assert.expect(1); + const preloaderManager = preloaderManagerFactory(); + + assert.equal( + typeof preloaderManager[data.title], + 'function', + `The preloader manager has the method ${data.title}` + ); + }); + + QUnit.cases + .init([{ title: 'has' }, { title: 'loaded' }, { title: 'load' }, { title: 'unload' }]) + .test('method ', (data, assert) => { + assert.expect(1); + const preloaderManager = preloaderManagerFactory(); + const preloader = preloaderManager(); + + assert.equal(typeof preloader[data.title], 'function', `The assets preloader has the method ${data.title}`); + }); + + QUnit.module('behavior'); + + QUnit.test('providers', assert => { + assert.expect(8); + const ready = assert.async(); + const preloaderManager = preloaderManagerFactory(); + const provider = { + name: 'asset', + init() { + return {}; + } + }; + + assert.strictEqual(preloaderManager.getAvailableProviders().length, 0, 'No preloader registered yet'); + + preloaderManager.registerProvider(provider.name, provider); + + assert.strictEqual(preloaderManager.getAvailableProviders().length, 1, 'One preloader registered'); + assert.strictEqual(preloaderManager.getProvider('asset'), provider, 'The provider has been registered'); + + const preloader = preloaderManager(); + + assert.ok(preloader.has('asset'), 'The asset preloader is available'); + assert.ok(!preloader.has('dummy'), 'There is no dummy preloader'); + + assert.ok(!preloader.loaded('asset'), 'The preloader manager accept the API is not fully implemented'); + preloader + .load('asset') + .then(() => { + assert.ok(true, 'The asset preloader loaded even if the API is not implemented'); + }) + .then(() => preloader.unload('asset')) + .then(() => { + assert.ok(true, 'The asset preloader unloaded even if the API is not implemented'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); + + QUnit.test('has', assert => { + assert.expect(4); + const preloaderManager = preloaderManagerFactory(); + preloaderManager.registerProvider('asset', { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'asset', + loaded() { + return false; + }, + load() { + assert.ok(false, 'The asset should not be loaded'); + }, + unload() { + assert.ok(false, 'The asset should not be unloaded'); + } + }; + } + }); + + const preloader = preloaderManager(expectedAssetManager); + + assert.ok(preloader.has('asset'), 'The asset preloader has a Asset preloader'); + assert.ok(!preloader.has('dummy'), 'The asset preloader does not have a dummy preloader'); + }); + + QUnit.test('loaded', assert => { + assert.expect(10); + let loadedStatus = null; + const preloaderManager = preloaderManagerFactory(); + preloaderManager.registerProvider('asset', { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'asset', + loaded(url, sourceUrl, itemIdentifier) { + assert.strictEqual(url, expectedUrl, 'The expected url has been given'); + assert.strictEqual(sourceUrl, expectedSourceUrl, 'The expected source url has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + return loadedStatus; + }, + load() { + assert.ok(false, 'The asset should not be loaded'); + }, + unload() { + assert.ok(false, 'The asset should not be unloaded'); + } + }; + } + }); + const preloader = preloaderManager(expectedAssetManager); + + assert.strictEqual(preloader.loaded('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier), false); + loadedStatus = 1; + assert.strictEqual(preloader.loaded('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier), true); + }); + + QUnit.test('load', assert => { + assert.expect(8); + const ready = assert.async(); + const preloaderManager = preloaderManagerFactory(); + preloaderManager.registerProvider('asset', { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'asset', + loaded() { + return true; + }, + load(url, sourceUrl, itemIdentifier) { + assert.strictEqual(url, expectedUrl, 'The expected url has been given'); + assert.strictEqual(sourceUrl, expectedSourceUrl, 'The expected source url has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + assert.ok(true, 'The asset is loaded'); + return Promise.resolve(); + }, + unload() { + assert.ok(false, 'The asset should not be unloaded'); + } + }; + } + }); + const preloader = preloaderManager(expectedAssetManager); + + preloader + .load('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The asset preloader loaded asset'); + }) + .then(() => preloader.load('dummy')) + .then(() => { + assert.ok(true, 'The asset preloader accept unknown loader without failing'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); + + QUnit.test('unload', assert => { + assert.expect(8); + const ready = assert.async(); + const preloaderManager = preloaderManagerFactory(); + preloaderManager.registerProvider('asset', { + name: 'asset', + init(assetManager) { + assert.ok(true, 'Asset preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'asset', + loaded() { + return true; + }, + load() { + assert.ok(false, 'The asset should not be loaded'); + }, + unload(url, sourceUrl, itemIdentifier) { + assert.strictEqual(url, expectedUrl, 'The expected url has been given'); + assert.strictEqual(sourceUrl, expectedSourceUrl, 'The expected source url has been given'); + assert.strictEqual( + itemIdentifier, + expectedItemIdentifier, + 'The expected item identifier has been given' + ); + assert.ok(true, 'The asset is unloaded'); + return Promise.resolve(); + } + }; + } + }); + const preloader = preloaderManager(expectedAssetManager); + + preloader + .unload('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The asset preloader unloaded asset'); + }) + .then(() => preloader.unload('dummy')) + .then(() => { + assert.ok(true, 'The asset preloader accept unknown loader without failing'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); diff --git a/test/proxy/cache/preloaders/assets/audio/sample.mp3 b/test/proxy/cache/preloaders/assets/audio/sample.mp3 new file mode 100644 index 00000000..2713c6b1 Binary files /dev/null and b/test/proxy/cache/preloaders/assets/audio/sample.mp3 differ diff --git a/test/proxy/cache/preloaders/assets/audio/test.html b/test/proxy/cache/preloaders/assets/audio/test.html new file mode 100644 index 00000000..5b3e435a --- /dev/null +++ b/test/proxy/cache/preloaders/assets/audio/test.html @@ -0,0 +1,21 @@ + + + + + Test Runner - audio asset preloader + + + + +
+
+ + diff --git a/test/proxy/cache/preloaders/assets/audio/test.js b/test/proxy/cache/preloaders/assets/audio/test.js new file mode 100644 index 00000000..7c78079a --- /dev/null +++ b/test/proxy/cache/preloaders/assets/audio/test.js @@ -0,0 +1,106 @@ +/** + * 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) 2021 Open Assessment Technologies SA ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/preloaders/assets/audio + */ +define(['taoQtiTest/runner/proxy/cache/preloaders/assets/audio'], function (audioPreloader) { + 'use strict'; + + function assetManagerFactory() { + const strategies = []; + return { + prependStrategy(strategy) { + strategies.push(strategy); + }, + resolve(url, data) { + for (const strategy of strategies) { + const resolved = strategy.handle(url, data); + if (resolved) { + return resolved; + } + } + return false; + } + }; + } + + QUnit.module('API'); + + QUnit.test('module', assert => { + assert.expect(5); + assert.equal(typeof audioPreloader, 'object', 'The module exposes an object'); + assert.equal(audioPreloader.name, 'audio', 'The preloader has a name'); + assert.equal(typeof audioPreloader.init, 'function', 'The preloader has an init method'); + assert.equal(typeof audioPreloader.init(assetManagerFactory()), 'object', 'The preloaded has a factory'); + assert.notDeepEqual( + audioPreloader.init(assetManagerFactory()), + audioPreloader.init(assetManagerFactory()), + 'The factory creates new instances' + ); + }); + + QUnit.cases.init([{ title: 'loaded' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + assert.expect(1); + const preloader = audioPreloader.init(assetManagerFactory()); + + assert.equal(typeof preloader[data.title], 'function', `The preloader has the method ${data.title}`); + }); + + QUnit.module('behavior'); + + QUnit.test('load/unload', assert => { + assert.expect(5); + const ready = assert.async(); + const assetManager = assetManagerFactory(); + const preloader = audioPreloader.init(assetManager); + const data = { + asset: 'sample.mp3', + itemIdentifier: 'item-1' + }; + const assetUrl = `./${data.asset}`; + + preloader + .load(assetUrl, data.asset, data.itemIdentifier) + .then(() => { + assert.ok( + /^blob/.test(assetManager.resolve(data.asset, data)), + 'The mp3 sample was resolved as a blob' + ); + assert.ok(preloader.loaded(assetUrl, data.asset, data.itemIdentifier), 'The asset has been preloaded'); + }) + .then(() => preloader.load(assetUrl, data.asset, data.itemIdentifier)) + .then(() => { + assert.ok( + /^blob/.test(assetManager.resolve(data.asset, data)), + 'The mp3 sample was resolved as a blob' + ); + }) + .then(() => preloader.unload(assetUrl, data.asset, data.itemIdentifier)) + .then(() => { + assert.strictEqual( + assetManager.resolve(data.asset, data), + false, + 'The mp3 sample is not resolved anymore' + ); + assert.ok(!preloader.loaded(assetUrl, data.asset, data.itemIdentifier), 'The asset has been unloaded'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); diff --git a/test/proxy/cache/preloaders/assets/image/sample.jpg b/test/proxy/cache/preloaders/assets/image/sample.jpg new file mode 100644 index 00000000..70739f93 Binary files /dev/null and b/test/proxy/cache/preloaders/assets/image/sample.jpg differ diff --git a/test/proxy/cache/preloaders/assets/image/test.html b/test/proxy/cache/preloaders/assets/image/test.html new file mode 100644 index 00000000..2812bb54 --- /dev/null +++ b/test/proxy/cache/preloaders/assets/image/test.html @@ -0,0 +1,21 @@ + + + + + Test Runner - image asset preloader + + + + +
+
+ + diff --git a/test/proxy/cache/preloaders/assets/image/test.js b/test/proxy/cache/preloaders/assets/image/test.js new file mode 100644 index 00000000..f46a6864 --- /dev/null +++ b/test/proxy/cache/preloaders/assets/image/test.js @@ -0,0 +1,77 @@ +/** + * 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) 2021 Open Assessment Technologies SA ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/preloaders/assets/image + */ +define(['taoQtiTest/runner/proxy/cache/preloaders/assets/image'], function (imagePreloader) { + 'use strict'; + + QUnit.module('API'); + + QUnit.test('module', assert => { + assert.expect(5); + assert.equal(typeof imagePreloader, 'object', 'The module exposes an object'); + assert.equal(imagePreloader.name, 'img', 'The preloader has a name'); + assert.equal(typeof imagePreloader.init, 'function', 'The preloader has an init method'); + assert.equal(typeof imagePreloader.init(), 'object', 'The preloaded has a factory'); + assert.notDeepEqual(imagePreloader.init(), imagePreloader.init(), 'The factory creates new instances'); + }); + + QUnit.cases.init([{ title: 'loaded' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + assert.expect(1); + const preloader = imagePreloader.init(); + + assert.equal(typeof preloader[data.title], 'function', `The preloader has the method ${data.title}`); + }); + + QUnit.module('behavior'); + + QUnit.test('load/unload', assert => { + assert.expect(4); + const ready = assert.async(); + const preloader = imagePreloader.init(); + const data = { + asset: 'sample.jpg', + itemIdentifier: 'item-1' + }; + const assetUrl = `./${data.asset}`; + + //Hack the Image element to assert the load + window.Image = function () { + assert.ok(true, 'A new image is created'); + }; + Object.defineProperty(window.Image.prototype, 'src', { + set: function src(url) { + assert.equal(url, assetUrl, 'The image has been preloaded'); + } + }); + + preloader + .load(assetUrl, data.asset, data.itemIdentifier) + .then(() => { + assert.ok(preloader.loaded(assetUrl, data.asset, data.itemIdentifier), 'The asset has been preloaded'); + }) + .then(() => preloader.unload(assetUrl, data.asset, data.itemIdentifier)) + .then(() => { + assert.ok(!preloader.loaded(assetUrl, data.asset, data.itemIdentifier), 'The asset has been unloaded'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); diff --git a/test/proxy/cache/preloaders/assets/stylesheet/sample.css b/test/proxy/cache/preloaders/assets/stylesheet/sample.css new file mode 100644 index 00000000..dead9869 --- /dev/null +++ b/test/proxy/cache/preloaders/assets/stylesheet/sample.css @@ -0,0 +1,3 @@ +body div.qti-item, body div.qti-item .qti-associateInteraction .result-area > li > .target{background-color:#D52516;} +body div.qti-item{color:#ffffff;} +body div.qti-item * {font-size:16px;} diff --git a/test/proxy/cache/preloaders/assets/stylesheet/test.html b/test/proxy/cache/preloaders/assets/stylesheet/test.html new file mode 100644 index 00000000..a10d6c76 --- /dev/null +++ b/test/proxy/cache/preloaders/assets/stylesheet/test.html @@ -0,0 +1,21 @@ + + + + + Test Runner - stylesheet asset preloader + + + + +
+
+ + diff --git a/test/proxy/cache/preloaders/assets/stylesheet/test.js b/test/proxy/cache/preloaders/assets/stylesheet/test.js new file mode 100644 index 00000000..ca275eea --- /dev/null +++ b/test/proxy/cache/preloaders/assets/stylesheet/test.js @@ -0,0 +1,86 @@ +/** + * 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) 2021 Open Assessment Technologies SA ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet + */ +define(['taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'], function (stylesheetPreloader) { + 'use strict'; + + QUnit.module('API'); + + QUnit.test('module', assert => { + assert.expect(5); + assert.equal(typeof stylesheetPreloader, 'object', 'The module exposes an object'); + assert.equal(stylesheetPreloader.name, 'css', 'The preloader has a name'); + assert.equal(typeof stylesheetPreloader.init, 'function', 'The preloader has an init method'); + assert.equal(typeof stylesheetPreloader.init(), 'object', 'The preloaded has a factory'); + assert.notDeepEqual( + stylesheetPreloader.init(), + stylesheetPreloader.init(), + 'The factory creates new instances' + ); + }); + + QUnit.cases.init([{ title: 'loaded' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + assert.expect(1); + const preloader = stylesheetPreloader.init(); + + assert.equal(typeof preloader[data.title], 'function', `The preloader has the method ${data.title}`); + }); + + QUnit.module('behavior'); + + QUnit.test('load/unload', assert => { + assert.expect(5); + const ready = assert.async(); + const preloader = stylesheetPreloader.init(); + const data = { + asset: 'sample.css', + itemIdentifier: 'item-1' + }; + const assetUrl = `./${data.asset}`; + + assert.strictEqual( + document.querySelector(`head link[href="${assetUrl}"]`), + null, + 'The asset is not yet loaded' + ); + + preloader + .load(assetUrl, data.asset, data.itemIdentifier) + .then(() => { + assert.ok( + document.querySelector(`head link[href="${assetUrl}"]`) instanceof HTMLLinkElement, + 'The asset has been preloaded' + ); + assert.ok(preloader.loaded(assetUrl, data.asset, data.itemIdentifier), 'The asset has been preloaded'); + }) + .then(() => preloader.unload(assetUrl, data.asset, data.itemIdentifier)) + .then(() => { + assert.strictEqual( + document.querySelector(`head link[href="${assetUrl}"]`), + null, + 'The asset has been unloaded' + ); + assert.ok(!preloader.loaded(assetUrl, data.asset, data.itemIdentifier), 'The asset has been unloaded'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); diff --git a/test/proxy/cache/preloaders/interactions/extendedText/test.html b/test/proxy/cache/preloaders/interactions/extendedText/test.html new file mode 100644 index 00000000..bb0fc348 --- /dev/null +++ b/test/proxy/cache/preloaders/interactions/extendedText/test.html @@ -0,0 +1,27 @@ + + + + + Test Runner - extendedText interaction preloader + + + + +
+
+ + diff --git a/test/proxy/cache/preloaders/interactions/extendedText/test.js b/test/proxy/cache/preloaders/interactions/extendedText/test.js new file mode 100644 index 00000000..abdfa04d --- /dev/null +++ b/test/proxy/cache/preloaders/interactions/extendedText/test.js @@ -0,0 +1,85 @@ +/** + * 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) 2021 Open Assessment Technologies SA ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/preloaders/interactions/extendedText + */ +define(['taoQtiTest/runner/proxy/cache/preloaders/interactions/extendedText'], function (extendedTextPreloader) { + 'use strict'; + + QUnit.module('API'); + + QUnit.test('module', assert => { + assert.expect(5); + assert.equal(typeof extendedTextPreloader, 'object', 'The module exposes an object'); + assert.equal(extendedTextPreloader.name, 'extendedTextInteraction', 'The preloader has a name'); + assert.equal(typeof extendedTextPreloader.init, 'function', 'The preloader has an init method'); + assert.equal(typeof extendedTextPreloader.init(), 'object', 'The preloaded has a factory'); + assert.notDeepEqual( + extendedTextPreloader.init(), + extendedTextPreloader.init(), + 'The factory creates new instances' + ); + }); + + QUnit.cases.init([{ title: 'loaded' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + assert.expect(1); + const preloader = extendedTextPreloader.init(); + + assert.equal(typeof preloader[data.title], 'function', `The preloader has the method ${data.title}`); + }); + + QUnit.module('behavior'); + + QUnit.test('load/unload', assert => { + //assert.expect(4); + const ready = assert.async(); + const preloader = extendedTextPreloader.init(); + const interaction = { + attributes: { + format: 'xhtml' + } + }; + const itemData = { + data: { + attributes: { + 'xml:lang': 'fr-FR' + } + } + }; + const itemIdentifier = 'item-1'; + + preloader + .load(interaction, itemData, itemIdentifier) + .then(() => { + assert.ok( + preloader.loaded(interaction, itemData, itemIdentifier), + 'The interaction has been preloaded' + ); + }) + .then(() => preloader.unload(interaction, itemData, itemIdentifier)) + .then(() => { + assert.ok( + preloader.loaded(interaction, itemData, itemIdentifier), + 'The interaction is still preloaded' + ); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); diff --git a/test/proxy/offline/proxy/test.html b/test/proxy/offline/proxy/test.html index 52d951bf..7b04fffa 100644 --- a/test/proxy/offline/proxy/test.html +++ b/test/proxy/offline/proxy/test.html @@ -7,6 +7,12 @@