From 76af4d60e155801736c75c30131fa8a9bdc13070 Mon Sep 17 00:00:00 2001 From: jsconan Date: Fri, 22 Oct 2021 11:39:58 +0200 Subject: [PATCH 01/17] fix: check the client timer mode just in time instead of at the beginning (the options may be defined later) --- src/provider/qti.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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'); From d1dfbc40a2e00d6d3ba192879f1dd1ad0c7ff18c Mon Sep 17 00:00:00 2001 From: jsconan Date: Tue, 5 Oct 2021 13:17:44 +0200 Subject: [PATCH 02/17] refactor: convert itemPreloader to ES6 --- src/proxy/cache/itemPreloader.js | 186 +++++++++++++++++-------------- 1 file changed, 100 insertions(+), 86 deletions(-) diff --git a/src/proxy/cache/itemPreloader.js b/src/proxy/cache/itemPreloader.js index 590b7776..12634298 100644 --- a/src/proxy/cache/itemPreloader.js +++ b/src/proxy/cache/itemPreloader.js @@ -27,17 +27,22 @@ import qtiItemRunner from 'taoQtiItem/runner/qtiItemRunner'; import getAssetManager from 'taoQtiTest/runner/config/assetManager'; import urlUtil from 'util/url'; -var logger = loggerFactory('taoQtiTest/runner/proxy/cache/itemPreloader'); +/** + * @type {logger} + * @private + */ +const logger = loggerFactory('taoQtiTest/runner/proxy/cache/itemPreloader'); /** * Test the support of possible `` values. - * @param {String} feature - the value to test - * @returns {Boolean} + * @param {string} feature - the value to test + * @returns {boolean} + * @private */ -var relSupport = function relSupport(feature) { - var fakeLink = document.createElement('link'); +const relSupport = feature => { + const fakeLink = document.createElement('link'); try { - if (fakeLink.relList && _.isFunction(fakeLink.relList.supports)) { + if (fakeLink.relList && 'function' === typeof fakeLink.relList.supports) { return fakeLink.relList.supports(feature); } } catch (err) { @@ -47,23 +52,28 @@ var relSupport = function relSupport(feature) { /** * Does the current env supports `` + * @type {boolean} + * @private */ -var supportPreload = relSupport('preload'); +const supportPreload = relSupport('preload'); /** * Does the current env supports `` + * @type {boolean} + * @private */ -var supportPrefetch = relSupport('prefetch'); +const supportPrefetch = relSupport('prefetch'); /** * 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) && @@ -75,37 +85,35 @@ var isItemObjectValid = function isItemObjectValid(item) { /** * 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'); + const preloadAssetManager = getAssetManager('item-preloader'); //keep references to preloaded images attached //in order to prevent garbage collection of cached images - var images = {}; + const images = {}; //keep references to preloaded audio blobs - var audioBlobs = {}; + const audioBlobs = {}; /** * Asset loaders per supported asset types */ - var loaders = { + const 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 + * @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 + * @private */ - img: function preloadImage(url, sourceUrl, itemIdentifier) { + img(url, sourceUrl, itemIdentifier) { images[itemIdentifier] = images[itemIdentifier] || {}; if ('Image' in window && !images[itemIdentifier][sourceUrl]) { images[itemIdentifier][sourceUrl] = new Image(); @@ -115,10 +123,11 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { /** * Preload stylesheets - * @param {String} url - the url of the css to preload + * @param {string} url - the url of the css to preload + * @private */ - css: function preloadCss(url) { - var link = document.createElement('link'); + css(url) { + const link = document.createElement('link'); if (supportPreload) { link.setAttribute('rel', 'preload'); link.setAttribute('as', 'style'); @@ -138,16 +147,16 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { /** * 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 + * @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 + * @private */ - audio: function preloadAudio(url, sourceUrl, itemIdentifier) { - var request; + audio(url, sourceUrl, itemIdentifier) { audioBlobs[itemIdentifier] = audioBlobs[itemIdentifier] || {}; if (typeof audioBlobs[itemIdentifier][sourceUrl] === 'undefined') { //direct XHR to benefit from the "blob" response type - request = new XMLHttpRequest(); + const request = new XMLHttpRequest(); request.open('GET', url, true); request.responseType = 'blob'; request.onload = function onRequestLoad() { @@ -165,14 +174,15 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { /** * Asset unloaders per supported asset types */ - var unloaders = { + const 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 + * @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 + * @private */ - img: function unloadImage(url, sourceUrl, itemIdentifier) { + img(url, sourceUrl, itemIdentifier) { if (images[itemIdentifier]) { images[itemIdentifier] = _.omit(images[itemIdentifier], sourceUrl); } @@ -180,10 +190,11 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { /** * Remove prefteched CSS link tag - * @param {String} url - the url of the css to unload + * @param {string} url - the url of the css to unload + * @private */ - css: function unloadCss(url) { - var link = document.querySelector(`head link[data-preload][href="${url}"]`); + css(url) { + const link = document.querySelector(`head link[data-preload][href="${url}"]`); if (link) { document.querySelector('head').removeChild(link); } @@ -191,11 +202,12 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { /** * 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 + * @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 + * @private */ - audio: function unloadAudio(url, sourceUrl, itemIdentifier) { + audio(url, sourceUrl, itemIdentifier) { if (audioBlobs[itemIdentifier]) { audioBlobs[itemIdentifier] = _.omit(audioBlobs[itemIdentifier], sourceUrl); } @@ -204,21 +216,22 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { /** * Resolves assets URLS using the assetManager - * @param {String} baseUrl - * @param {Object} assets - as [ type : [urls] ] + * @param {string} baseUrl + * @param {object} assets - as [ type : [urls] ] * @returns {Promise} assets with URLs resolved + * @private */ - var resolveAssets = function resolveAssets(baseUrl, assets) { - return new Promise(function (resolve) { + const resolveAssets = (baseUrl, assets) => { + return new Promise(resolve => { preloadAssetManager.setData('baseUrl', 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,15 +252,16 @@ 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); /** * Prepend a strategy to resolves cached assets */ testAssetManager.prependStrategy({ name: 'precaching', - handle: function handlePrecache(url, data) { - var sourceUrl = url.toString(); + handle(url, data) { + const sourceUrl = url.toString(); //resolves precached audio files if ( @@ -262,33 +276,33 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { }); /** - * @typedef {Object} itemPreloader + * @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 {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(item) { + const loading = []; /** * Preload the item runner * @returns {Promise} */ - var itemLoad = function itemLoad() { + const itemLoad = () => { logger.debug(`Start preloading of item ${item.itemIdentifier}`); - return new Promise(function (resolve, reject) { + return new Promise((resolve, reject) => { qtiItemRunner(item.itemData.type, item.itemData.data, { assetManager: preloadAssetManager, preload: true }) - .on('init', function () { + .on('init', () => { logger.debug(`Preloading of item ${item.itemIdentifier} done`); resolve(true); }) @@ -301,11 +315,11 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { * 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) { + const assetLoad = () => { + return resolveAssets(item.baseUrl, item.itemData.assets).then(resolved => { + _.forEach(resolved, (assets, type) => { + if ('function' === typeof loaders[type]) { + _.forEach(assets, (url, sourceUrl) => { logger.debug(`Loading asset ${sourceUrl}(${type}) for item ${item.itemIdentifier}`); loaders[type](url, sourceUrl, item.itemIdentifier); @@ -329,7 +343,7 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { loading.push(assetLoad()); } } - return Promise.all(loading).then(function (results) { + return Promise.all(loading).then(results => { return results.length > 0 && _.all(results, _.isTrue); }); }, @@ -337,19 +351,19 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { /** * 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 {object} item.itemData.assets - assets per types : img : ['url1', 'url2' ] + * @param {string} itemIdentifier - the item identifier * @returns {Promise} */ - unload: function unload(item) { + 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) { + return resolveAssets(item.baseUrl, item.itemData.assets).then(resolved => { + _.forEach(resolved, (assets, type) => { + if ('function' === typeof unloaders[type]) { + _.forEach(assets, (url, sourceUrl) => { logger.debug(`Unloading asset ${sourceUrl}(${type}) for item ${item.itemIdentifier}`); unloaders[type](url, sourceUrl, item.itemIdentifier); @@ -362,6 +376,6 @@ var itemPreloaderFactory = function itemPreloaderFactory(options) { return Promise.resolve(false); } }; -}; +} export default itemPreloaderFactory; From 68dd197a3ffbc2e3ada5ae06b4bfd0853c573d4a Mon Sep 17 00:00:00 2001 From: jsconan Date: Wed, 6 Oct 2021 13:20:15 +0200 Subject: [PATCH 03/17] feat: extract the audio asset preloader --- src/proxy/cache/preloaders/assets/audio.js | 112 ++++++++++++++++++ .../cache/preloaders/assets/audio/sample.mp3 | Bin 0 -> 4761 bytes .../cache/preloaders/assets/audio/test.html | 21 ++++ .../cache/preloaders/assets/audio/test.js | 110 +++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 src/proxy/cache/preloaders/assets/audio.js create mode 100644 test/proxy/cache/preloaders/assets/audio/sample.mp3 create mode 100644 test/proxy/cache/preloaders/assets/audio/test.html create mode 100644 test/proxy/cache/preloaders/assets/audio/test.js diff --git a/src/proxy/cache/preloaders/assets/audio.js b/src/proxy/cache/preloaders/assets/audio.js new file mode 100644 index 00000000..d6635007 --- /dev/null +++ b/src/proxy/cache/preloaders/assets/audio.js @@ -0,0 +1,112 @@ +/* + * 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 + */ + +/** + * (Pre)load audio content. + * + * @author Bertrand Chevrier + */ + +/** + * (Pre)load an item and it's assets. + * + * @author Bertrand Chevrier + */ +import _ from 'lodash'; + +/** + * Manages the preloading of audio files + * @param assetManager - A reference to the assetManager + * @returns {assetPreloader} + */ +export default function audioPreloaderFactory(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 { + /** + * The name of the preloader + * @type {string} + */ + name: 'audio', + + /** + * 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/test/proxy/cache/preloaders/assets/audio/sample.mp3 b/test/proxy/cache/preloaders/assets/audio/sample.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..2713c6b102459cd04e36b9cdb871ec084072d50e GIT binary patch literal 4761 zcmds*cQ9OC+{YIoQnV=1%OXl7j}i$Yf?e#cZV9_;lxWdQwAdg@lthgdN%XdeRiZ=} z1krnm-dkAR+WYd%Jb%1@z5lihk>o5^~F8SMdJX0 zZY6{0=;*k)xrHwf6BCoU00My^EMv+(YwNvWbaZs)f|Zq(oePLWA^-pv?~+whP*;+;FD>nQ#qqx=l9YU8w|PJ$ z-IqD`7u7NUb^IS!0ROcWL{&xlyg~lrNzc|;)2bl_ko=syJgv|b5)fpGhE*$upN$0udl^UO~S=Ej@W(RTyKmCTviUr747 zK1&y$97d2F=i17Q84bH|m9}}E+<4n|qV*vmjj~nA`1bX{##)*7G}PE63#a5yYnL>G zfjwT?o882)^L73s7`;j;N9>cSVb6h9YYNSx&R#D4avR~A#;%~_lWAUYojXqSPhysZ zj65l0=E`vPqc6i$0DN@o0VIgH!Ik2tP+SJv-%j3MGEVl~QzPCzZ*Ns^c~4Zic6)Tv zJZ6T>%Q<>{2_~Pekzpl}At@$A4ZqP)sLQ7ISy-|s4lc+>8A!^bJU^vKkHf;Vq>A1P zJPbjVM#ZU+X~V0zZFbQ1K>c=x68vH6Sy+AX6a9@7lRJsV)}bhlgUa_ldi(1A>btzS z%)Iw#e)EZ0`w#B<+Tr(H?UUW9NFNmZwA4*vHQVP~6t!j8|NeS7Z}sEs54|wYO66W5 zA4&D{7R#-2gm;Wl-dXnPRgKyAP!79hdFb*@yu-qFp{rrhv*Z%Lzfqy3^u~Pj<``}3@x9YFkxVVU__EmQ3H7Puk;z$&QMs7% z@Pwt{96E2cy5bp}&*rv^P+gXvfpnn${CNbVOjGs;&dEqu6p{Icv?bz=S+F(p?Nlv> zW@Rjun%Yfq3GB{@-6*)cEuH=?69r$0r( zqmLZ3^;DoLPq>h*m)Q<^77j16b>*o)AkM zKLpdf!<2-E4yI>c=6Rj+iO1Znz;SjGa2#5XTsX^p`{$v`$%o{fM3zUIc=#iFQ!%_d zSqsFeLA)*U+2zQcbGgmqIn6r9+5L?_ijG!srzC84*HwEsW&2%UJGPnrgd1$_8g&-Z zb(SHc5or!%3ooSQBWWzF!KB&WVC|exMrWV@+~%q{`vZ=l737a!=3&OE%{MvesdPRF z-dtG@4@YbtIMBZmqfP66@-aJE>&-y6x_a`C)9d@)h02$9_Q7oH+eEME{2GaLUa@lg zaCclUu>A#q>j<1y)qSCbcj7sOs|$)oP9}eUxPOSJgwEy(@^Bky43S%1EiucabM#y_ z%z+BN{tEMDmQEM6g_JJLC_JJYI@LA0f+57ya z0ZS!}80WqxAyog-2;cH!%#RT9gu1*|#e|!{*zmVB@1eYJ~k{8aB|V6)$jXW<^5Jk zg^rK@3Mj?jB0P!I@bpboBr1?1x5%L&&Ok*o2b5_F3mx~-?z+zevUMr8P!U9lSqy1VZo%6lV1Na_e z_a~AHY}V}^I<;oQ_N{J-U)6l^0$fV0WviQD3Ek`oqFK2*+ge^s|6`m5j3O7lLuqLs}GmSt`KZ>XwpMkXWCV zG08MyL|h9<9a;a$Q-HkM@7Jk^=KOt`ZI3(RaNyJjVYIni8sGCwxCXUZvRId6-Y zjy-sg<)hL>g_C3$xU(>|pb}-Q_Kjhpjov6lABX4XzlzWp^ARXw%W-}S8iQ)D@i!#N zHo%j+iyS`pDL7jzX4wY*4S7Z6N+v@|nzRKcHiIn88b44lT&(weJeO$7k4EyEqZve2 z1_7Y*%WqU*vA^NJbfMi4PjvHQ(72)UL^DtF2X@-S z*2$qyfL(p$sLw*BYmCki5$}7w9~D=Ny%mB%?CSdwTAgD~G!;mZv>#V;h>7%>vOEC~nprwN>x1cmp_5`%lqEZ7 zRrPn+Ej5Y#E*NYX5>Ycm^P*O>-_ADOf>@uZnnG+iUq5h61eWhYUT%QT3+ zb|uhF0~$x;Vil1|A@GeVMtp*w8xd)wXjSu9T~O3iHPeI|#ZKjF%hnCh3VoztH2dq# zp~6A#gdzAt7<7L@Ul6lgldY%K^br~dG+zxF_t*ZN_52ctZ3K&cSpQh(=6a6XQ}OQF z=FJ`vMX<=q3T#{5A|^J_CALLEw*Qu1&`eiCV!?!QhyS4#1S`tEx zv9$sfNU>|q+1mCJoX?fqzfy(-|13Lexu~|wY@0lWu5ARkibuyMVv!P+=1KfNC&0}; zto>b=9}RcKjTmQg%6BFfoD3!;2!)C<9){~DeQ~A`@Y0J=4=idam2XbzSqj)yXRLaT zj-NLW=7T=d(=WUS9fnEs&3M@1-f{dE`f86^`daqLe{Nkn@?1+4Ka()PAhIKlS-QI_ zt954rmk8w7BWdVZSLlC|D=8}e0yI~xFS8x+`_|VRHLKiPrZdkRAHTX^K3HwEW$`$J zQ(H$mwj>TX8FlD$s64{k*`>aU+A6N3oqU~fH>d)-PU0!k_fyA}Y+6p!o@28lv#o7@ zWDuslOMY}zJ9b|#r9!w`*D|(udd6^jd+{)%_K&B~HQk^=phGM=pi$@h>dXO>x1e@8 zeHR>W=8XY>%|JI^vDk()>AmO#p~%Z@+gy{)%@=FS3SRaQX{t*9y0x+-Bbm0Km&)Oh zohe(Wg9+MHi%>td&4S7%4aqb<4QL@GE_Jf1i&Hs-nX~#WVCSmxs&);o{@UgEeqTBt z)jba?VB$#7Y3Y_jtya2l;Vq+$%ulj}r5kkvZvv9+-#-ZKA9~G=IqO;9)7u39)P*|a zpKDs4{;{Os==iXn=B`v>$V(freK{M?xfUubi9V_#O0Ht--aV1-ha0tZWf9)H3E_D* zzea7tLrkO(Bd+gJ^Uf85x@TXCFd}pN+}(aZ=Vu(Kb4!uu{)h6VnyLX6-_%FWo(wyj zkCY;D_?=R8o4yrwI_fOvCrM~Zh%gdxl`Y5Ui_Vui-=u z(FaOEW6{#}A)YuE4hL3mo^)FLcR!q3US1@lNP=~Op82HTY3m}isW0TV8+R9|JVY&A zaXWK`p8M^OK3IPc_XAm)U`X{tyYjZT?bLdq2K6B`pXU#VnfDepQLKGzTt^%D z7U+y9(kRKMcQIiD*gjaE%}uq{M^6ft>0K82u~VJ>XzfGp9VR2tr5!w1rW}so)w$Ur z%<`e!xUhHT!MQ(x!PX1@-DRJk8^JbSy$-g4L3u#jw6KCI&jmqmaDY%=@JC1X!l+xi z=JNE=2X{YEYs9}Wr`LGS$tYY=8EXS3RWKkkA(44s%8boha%WUvGEI;mj^_)(U9b}PlcU#hE&FHFhgvjTnD)Nu8?jfc(&a^rSI7v~;B z{Y6#@CU)8D-lyM0NbbfG955P#=kW)!+pX3(aP3EQyyJ?j0pQ3P;oA4ibnMb6bhYD9|FMQf;@H$nN{)RmX6rp%;dDaMtd% kaMpfyTK>1-zjKoNk_G=y0PwV}10WC?fus=x`d{DlKmHArl>h($ literal 0 HcmV?d00001 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..6a6d23e7 --- /dev/null +++ b/test/proxy/cache/preloaders/assets/audio/test.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 ; + */ + +/** + * Test of taoQtiTest/runner/proxy/cache/preloaders/assets/audio + */ +define(['taoQtiTest/runner/proxy/cache/preloaders/assets/audio'], function (audioPreloaderFactory) { + '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(3); + + assert.equal(typeof audioPreloaderFactory, 'function', 'The module exposes a function'); + assert.equal(typeof audioPreloaderFactory(assetManagerFactory()), 'object', 'The module is a factory'); + assert.notDeepEqual( + audioPreloaderFactory(assetManagerFactory()), + audioPreloaderFactory(assetManagerFactory()), + 'The factory creates new instances' + ); + }); + + QUnit.test('property [name]', assert => { + assert.expect(1); + const preloader = audioPreloaderFactory(assetManagerFactory()); + + assert.strictEqual(preloader.name, 'audio', 'The preloader has the name "audio"'); + }); + + QUnit.cases.init([{ title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + assert.expect(1); + const preloader = audioPreloaderFactory(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(3); + const ready = assert.async(); + const assetManager = assetManagerFactory(); + const preloader = audioPreloaderFactory(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' + ); + }) + .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' + ); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); From 1070f67c5a6dcaaa2e0c9ff9c861fe2df156933f Mon Sep 17 00:00:00 2001 From: jsconan Date: Wed, 6 Oct 2021 13:21:55 +0200 Subject: [PATCH 04/17] feat: extract the image asset preloader --- src/proxy/cache/preloaders/assets/image.js | 73 +++++++++++++++ .../cache/preloaders/assets/image/sample.jpg | Bin 0 -> 539 bytes .../cache/preloaders/assets/image/test.html | 21 +++++ .../cache/preloaders/assets/image/test.js | 83 ++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 src/proxy/cache/preloaders/assets/image.js create mode 100644 test/proxy/cache/preloaders/assets/image/sample.jpg create mode 100644 test/proxy/cache/preloaders/assets/image/test.html create mode 100644 test/proxy/cache/preloaders/assets/image/test.js diff --git a/src/proxy/cache/preloaders/assets/image.js b/src/proxy/cache/preloaders/assets/image.js new file mode 100644 index 00000000..f63d37a5 --- /dev/null +++ b/src/proxy/cache/preloaders/assets/image.js @@ -0,0 +1,73 @@ +/* + * 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 + */ + +/** + * (Pre)load images. + * + * @author Bertrand Chevrier + */ + +import _ from 'lodash'; + +/** + * Manages the preloading of images + * @returns {assetPreloader} + */ +export default function imagePreloaderFactory() { + //keep references to preloaded images attached + //in order to prevent garbage collection of cached images + const images = {}; + + return { + /** + * The name of the preloader + * @type {string} + */ + name: 'img', + + /** + * 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/test/proxy/cache/preloaders/assets/image/sample.jpg b/test/proxy/cache/preloaders/assets/image/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70739f9346b9e5a5c9fd85d7432ee0d3ac957fc8 GIT binary patch literal 539 zcmb7r;Ft(yFdV$vj#hL-SxX%>tqpa+bQQHiJ{eUKFsS8lwz(I>%yR$e5 aPl%9nj$@!1D)Ts!F8* literal 0 HcmV?d00001 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..80284b43 --- /dev/null +++ b/test/proxy/cache/preloaders/assets/image/test.js @@ -0,0 +1,83 @@ +/** + * 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 (imagePreloaderFactory) { + 'use strict'; + + QUnit.module('API'); + + QUnit.test('module', assert => { + assert.expect(3); + + assert.equal(typeof imagePreloaderFactory, 'function', 'The module exposes a function'); + assert.equal(typeof imagePreloaderFactory(), 'object', 'The module is a factory'); + assert.notDeepEqual(imagePreloaderFactory(), imagePreloaderFactory(), 'The factory creates new instances'); + }); + + QUnit.test('property [name]', assert => { + assert.expect(1); + const preloader = imagePreloaderFactory(); + + assert.strictEqual(preloader.name, 'img', 'The preloader has the name "img"'); + }); + + QUnit.cases.init([{ title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + assert.expect(1); + const preloader = imagePreloaderFactory(); + + 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 = imagePreloaderFactory(); + 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(true, 'The asset has been preloaded'); + }) + .then(() => preloader.unload(assetUrl, data.asset, data.itemIdentifier)) + .then(() => { + assert.ok(true, 'The asset has been unloaded'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); From 9111c999ca063ae84a0876a8d0e97d7d2e5a75a3 Mon Sep 17 00:00:00 2001 From: jsconan Date: Wed, 6 Oct 2021 13:23:42 +0200 Subject: [PATCH 05/17] feat: extract the stylesheet asset preloader --- .../cache/preloaders/assets/stylesheet.js | 125 ++++++++++++++++++ .../preloaders/assets/stylesheet/sample.css | 3 + .../preloaders/assets/stylesheet/test.html | 21 +++ .../preloaders/assets/stylesheet/test.js | 90 +++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 src/proxy/cache/preloaders/assets/stylesheet.js create mode 100644 test/proxy/cache/preloaders/assets/stylesheet/sample.css create mode 100644 test/proxy/cache/preloaders/assets/stylesheet/test.html create mode 100644 test/proxy/cache/preloaders/assets/stylesheet/test.js diff --git a/src/proxy/cache/preloaders/assets/stylesheet.js b/src/proxy/cache/preloaders/assets/stylesheet.js new file mode 100644 index 00000000..e24fe23e --- /dev/null +++ b/src/proxy/cache/preloaders/assets/stylesheet.js @@ -0,0 +1,125 @@ +/* + * 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 + */ + +/** + * (Pre)load stylesheets. + * + * @author Bertrand Chevrier + */ + +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'); + +/** + * Manages the preloading of stylesheets + * @returns {assetPreloader} + */ +export default function stylesheetPreloaderFactory() { + //keep references to preloaded CSS files + const stylesheets = {}; + + return { + /** + * The name of the preloader + * @type {string} + */ + name: 'css', + + /** + * 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/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..2bee9dbc --- /dev/null +++ b/test/proxy/cache/preloaders/assets/stylesheet/test.js @@ -0,0 +1,90 @@ +/** + * 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 (stylesheetPreloaderFactory) { + 'use strict'; + + QUnit.module('API'); + + QUnit.test('module', assert => { + assert.expect(3); + + assert.equal(typeof stylesheetPreloaderFactory, 'function', 'The module exposes a function'); + assert.equal(typeof stylesheetPreloaderFactory(), 'object', 'The module is a factory'); + assert.notDeepEqual( + stylesheetPreloaderFactory(), + stylesheetPreloaderFactory(), + 'The factory creates new instances' + ); + }); + + QUnit.test('property [name]', assert => { + assert.expect(1); + const preloader = stylesheetPreloaderFactory(); + + assert.strictEqual(preloader.name, 'css', 'The preloader has the name "css"'); + }); + + QUnit.cases.init([{ title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + assert.expect(1); + const preloader = stylesheetPreloaderFactory(); + + assert.equal(typeof preloader[data.title], 'function', `The preloader has the method ${data.title}`); + }); + + QUnit.module('behavior'); + + QUnit.test('load/unload', assert => { + assert.expect(3); + const ready = assert.async(); + const preloader = stylesheetPreloaderFactory(); + 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' + ); + }) + .then(() => preloader.unload(assetUrl, data.asset, data.itemIdentifier)) + .then(() => { + assert.strictEqual( + document.querySelector(`head link[href="${assetUrl}"]`), + null, + 'The asset has been unloaded' + ); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); From a5415170e4f0b1fe3773ed946cd76ea1c660b05d Mon Sep 17 00:00:00 2001 From: jsconan Date: Wed, 6 Oct 2021 13:24:32 +0200 Subject: [PATCH 06/17] feat: add an asset preloader manager --- src/proxy/cache/assetPreloader.js | 92 ++++++++++ .../cache/preloaders/assets/preloaders.js | 27 +++ test/proxy/cache/assetPreloader/test.html | 24 +++ test/proxy/cache/assetPreloader/test.js | 159 ++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 src/proxy/cache/assetPreloader.js create mode 100644 src/proxy/cache/preloaders/assets/preloaders.js create mode 100644 test/proxy/cache/assetPreloader/test.html create mode 100644 test/proxy/cache/assetPreloader/test.js diff --git a/src/proxy/cache/assetPreloader.js b/src/proxy/cache/assetPreloader.js new file mode 100644 index 00000000..7a5ce3d6 --- /dev/null +++ b/src/proxy/cache/assetPreloader.js @@ -0,0 +1,92 @@ +/* + * 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 preloaders from 'taoQtiTest/runner/proxy/cache/preloaders/assets/preloaders'; + +/** + * @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} assetPreloader + * @property {string} name - The name of the preloader + * @property {assetPreloaderAction} load - Preload an asset + * @property {assetPreloaderAction} unload - Unload an asset + */ + +/** + * Manages the preloading of assets + * @param assetManager - A reference to the assetManager + * @return {assetPreloaderManager} + */ +export default function assetPreloaderFactory(assetManager) { + const assetPreloaders = preloaders.reduce((map, factory) => { + const preloader = factory(assetManager); + map[preloader.name] = preloader; + return map; + }, {}); + + /** + * @typedef assetPreloaderManager + */ + return { + /** + * Checks whether or not an asset preloader exists for a particular type + * @param {string} type + * @returns {boolean} + */ + has(type) { + return !!assetPreloaders[type]; + }, + + /** + * Preloads an asset with respect to it type + * @param {string} type - The type of asset to preload + * @param {string} url - the url of the asset 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(type, url, sourceUrl, itemIdentifier) { + const preloader = assetPreloaders[type]; + if (preloader) { + return preloader.load(url, sourceUrl, itemIdentifier); + } + return Promise.resolve(); + }, + + /** + * Unloads an asset with respect to it type + * @param {string} type - The type of asset to preload + * @param {string} url - the url of the asset to unload + * @param {string} sourceUrl - the unresolved URL + * @param {string} itemIdentifier - the id of the item the asset belongs to + * @returns {Promise} + */ + unload(type, url, sourceUrl, itemIdentifier) { + const preloader = assetPreloaders[type]; + if (preloader) { + return preloader.unload(url, sourceUrl, itemIdentifier); + } + 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..a7a9a25b --- /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 audioPreloaderFactory from 'taoQtiTest/runner/proxy/cache/preloaders/assets/audio'; +import imagePreloaderFactory from 'taoQtiTest/runner/proxy/cache/preloaders/assets/image'; +import stylesheetPreloaderFactory from 'taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'; + +/** + * The list of asset loader factories + * @type {Function[]} + */ +export default [audioPreloaderFactory, imagePreloaderFactory, stylesheetPreloaderFactory]; diff --git a/test/proxy/cache/assetPreloader/test.html b/test/proxy/cache/assetPreloader/test.html new file mode 100644 index 00000000..1b71a19b --- /dev/null +++ b/test/proxy/cache/assetPreloader/test.html @@ -0,0 +1,24 @@ + + + + + Test Runner - asset preloader + + + + +
+
+ + diff --git a/test/proxy/cache/assetPreloader/test.js b/test/proxy/cache/assetPreloader/test.js new file mode 100644 index 00000000..bcae605a --- /dev/null +++ b/test/proxy/cache/assetPreloader/test.js @@ -0,0 +1,159 @@ +/** + * 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', + 'taoQtiTest/runner/proxy/cache/preloaders/assets/preloaders' +], function (assetPreloaderFactory, preloaders) { + 'use strict'; + + QUnit.module('API'); + + 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: '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() { + preloaders.splice(0, preloaders.length); + } + }); + + QUnit.test('has', assert => { + assert.expect(4); + const expectedAssetManager = {}; + const cssPreloaderFactory = assetManager => { + assert.ok(true, 'CSS preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'css', + load() { + assert.ok(false, 'The asset should not be loaded'); + }, + unload() { + assert.ok(false, 'The asset should not be unloaded'); + } + }; + }; + preloaders.push(cssPreloaderFactory); + const preloader = assetPreloaderFactory(expectedAssetManager); + + assert.ok(preloader.has('css'), 'The asset preloader has a CSS preloader'); + assert.ok(!preloader.has('dummy'), 'The asset preloader does not have a dummy preloader'); + }); + + QUnit.test('load', assert => { + assert.expect(8); + const ready = assert.async(); + const expectedAssetManager = {}; + const expectedUrl = 'sample.css'; + const expectedSourceUrl = 'http://test.com/sample.css'; + const expectedItemIdentifier = 'item-1'; + const cssPreloaderFactory = assetManager => { + assert.ok(true, 'CSS preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'css', + 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'); + } + }; + }; + preloaders.push(cssPreloaderFactory); + const preloader = assetPreloaderFactory(expectedAssetManager); + preloader + .load('css', 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 expectedAssetManager = {}; + const expectedUrl = 'sample.css'; + const expectedSourceUrl = 'http://test.com/sample.css'; + const expectedItemIdentifier = 'item-1'; + const cssPreloaderFactory = assetManager => { + assert.ok(true, 'CSS preloader created'); + assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); + return { + name: 'css', + 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(); + } + }; + }; + preloaders.push(cssPreloaderFactory); + const preloader = assetPreloaderFactory(expectedAssetManager); + preloader + .unload('css', 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); + }); +}); From d5cd24a3b26ac2f59e52601f7abf68c312fdc4e3 Mon Sep 17 00:00:00 2001 From: jsconan Date: Wed, 6 Oct 2021 13:26:34 +0200 Subject: [PATCH 07/17] refactor: use the extracted asset preloading mechanism in the itemPreloader --- src/proxy/cache/itemPreloader.js | 337 ++++++++++--------------------- 1 file changed, 106 insertions(+), 231 deletions(-) diff --git a/src/proxy/cache/itemPreloader.js b/src/proxy/cache/itemPreloader.js index 12634298..de3d47d4 100644 --- a/src/proxy/cache/itemPreloader.js +++ b/src/proxy/cache/itemPreloader.js @@ -25,6 +25,7 @@ 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 urlUtil from 'util/url'; /** @@ -33,37 +34,6 @@ import urlUtil from 'util/url'; */ const logger = loggerFactory('taoQtiTest/runner/proxy/cache/itemPreloader'); -/** - * 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'); - /** * Check if the given item object matches the expectations * @param {object} item @@ -83,6 +53,16 @@ const 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; +}; + /** * Create an instance of an item preloader * @param {object} options @@ -95,135 +75,21 @@ function itemPreloaderFactory(options) { //resolve assets of a next item (we can't use the test asset manager). const preloadAssetManager = getAssetManager('item-preloader'); - //keep references to preloaded images attached - //in order to prevent garbage collection of cached images - const images = {}; - - //keep references to preloaded audio blobs - const audioBlobs = {}; - - /** - * Asset loaders per supported asset types - */ - const 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 - * @private - */ - img(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 - * @private - */ - css(url) { - 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); - }, - - /** - * 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 - * @private - */ - audio(url, sourceUrl, itemIdentifier) { - audioBlobs[itemIdentifier] = audioBlobs[itemIdentifier] || {}; - if (typeof audioBlobs[itemIdentifier][sourceUrl] === 'undefined') { - //direct XHR to benefit from the "blob" response type - const 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 - */ - const 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 - * @private - */ - img(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 - * @private - */ - css(url) { - const 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 - * @private - */ - audio(url, sourceUrl, itemIdentifier) { - if (audioBlobs[itemIdentifier]) { - audioBlobs[itemIdentifier] = _.omit(audioBlobs[itemIdentifier], sourceUrl); - } - } - }; - /** * 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 */ - const resolveAssets = (baseUrl, assets) => { + const resolveAssets = item => { return new Promise(resolve => { - preloadAssetManager.setData('baseUrl', baseUrl); + const { assets } = item.itemData; + preloadAssetManager.setData('baseUrl', item.baseUrl); preloadAssetManager.setData('assets', assets); return resolve( @@ -254,26 +120,86 @@ function itemPreloaderFactory(options) { //this is the test asset manager, referenced under options.testId const testAssetManager = getAssetManager(options.testId); + const assetPreloader = assetPreloaderFactory(testAssetManager); /** - * Prepend a strategy to resolves cached assets + * 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 */ - testAssetManager.prependStrategy({ - name: 'precaching', - handle(url, data) { - const sourceUrl = url.toString(); + 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(); + }); + }; - //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]); - } - } - }); + /** + * 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 @@ -285,67 +211,26 @@ function itemPreloaderFactory(options) { * @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(item) { const loading = []; - /** - * Preload the item runner - * @returns {Promise} - */ - const itemLoad = () => { - 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(); - }); - }; - - /** - * Preload the item assets - * @returns {Promise} - */ - const assetLoad = () => { - return resolveAssets(item.baseUrl, item.itemData.assets).then(resolved => { - _.forEach(resolved, (assets, type) => { - if ('function' === typeof loaders[type]) { - _.forEach(assets, (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; - }); - }; - if (isItemObjectValid(item)) { - loading.push(itemLoad()); + loading.push(itemLoad(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(results => { - return results.length > 0 && _.all(results, _.isTrue); - }); + return Promise.all(loading).then(results => results.length > 0 && _.all(results, _.isTrue)); }, /** @@ -354,24 +239,14 @@ function itemPreloaderFactory(options) { * @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' ] - * @param {string} itemIdentifier - the item identifier * @returns {Promise} */ unload(item) { if (isItemObjectValid(item) && _.size(item.itemData.assets) > 0) { - return resolveAssets(item.baseUrl, item.itemData.assets).then(resolved => { - _.forEach(resolved, (assets, type) => { - if ('function' === typeof unloaders[type]) { - _.forEach(assets, (url, sourceUrl) => { - logger.debug(`Unloading asset ${sourceUrl}(${type}) for item ${item.itemIdentifier}`); - - unloaders[type](url, sourceUrl, item.itemIdentifier); - }); - } - }); - return true; - }); + return assetUnload(item); } return Promise.resolve(false); } From af9924bc597a954b56bcd7b8e9aacb0c01e26ebd Mon Sep 17 00:00:00 2001 From: jsconan Date: Wed, 6 Oct 2021 13:48:52 +0200 Subject: [PATCH 08/17] doc: typo in jsdoc for the assetPreloader --- src/proxy/cache/assetPreloader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy/cache/assetPreloader.js b/src/proxy/cache/assetPreloader.js index 7a5ce3d6..870a0368 100644 --- a/src/proxy/cache/assetPreloader.js +++ b/src/proxy/cache/assetPreloader.js @@ -75,7 +75,7 @@ export default function assetPreloaderFactory(assetManager) { /** * Unloads an asset with respect to it type - * @param {string} type - The type of asset to preload + * @param {string} type - The type of asset to unload * @param {string} url - the url of the asset to unload * @param {string} sourceUrl - the unresolved URL * @param {string} itemIdentifier - the id of the item the asset belongs to From 47d382a83a6b11a35121cfd8d2bd908dd434b554 Mon Sep 17 00:00:00 2001 From: jsconan Date: Wed, 6 Oct 2021 16:26:15 +0200 Subject: [PATCH 09/17] feat: add an interaction runtimes preloader manager --- src/proxy/cache/interactionPreloader.js | 92 ++++++++++ src/proxy/cache/itemPreloader.js | 67 +++++++- .../preloaders/interactions/preloaders.js | 23 +++ .../cache/interactionPreloader/test.html | 24 +++ test/proxy/cache/interactionPreloader/test.js | 159 ++++++++++++++++++ 5 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 src/proxy/cache/interactionPreloader.js create mode 100644 src/proxy/cache/preloaders/interactions/preloaders.js create mode 100644 test/proxy/cache/interactionPreloader/test.html create mode 100644 test/proxy/cache/interactionPreloader/test.js diff --git a/src/proxy/cache/interactionPreloader.js b/src/proxy/cache/interactionPreloader.js new file mode 100644 index 00000000..e30fdd1d --- /dev/null +++ b/src/proxy/cache/interactionPreloader.js @@ -0,0 +1,92 @@ +/* + * 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'; + +/** + * @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} interactionPreloader + * @property {string} name - The name of the preloader + * @property {interactionPreloaderAction} load - Preload an interaction + * @property {interactionPreloaderAction} unload - Unload an interaction + */ + + +/** + * Manages the preloading of interaction runtimes + * @return {interactionPreloaderManager} + */ +export default function interactionPreloaderFactory() { + const interactionPreloaders = preloaders.reduce((map, factory) => { + const preloader = factory(); + map[preloader.name] = preloader; + return map; + }, {}); + + /** + * @typedef interactionPreloaderManager + */ + return { + /** + * Checks whether or not an interaction preloader exists for a particular type + * @param {string} type + * @returns {boolean} + */ + has(type) { + return !!interactionPreloaders[type]; + }, + + /** + * Preloads an interaction with respect to it type + * @param {string} type - The type of interaction 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 + * @returns {Promise} + */ + load(type, interaction, itemData, itemIdentifier) { + const preloader = interactionPreloaders[type]; + if (preloader) { + return preloader.load(interaction, itemData, itemIdentifier); + } + return Promise.resolve(); + }, + + /** + * Unloads an interaction with respect to it type + * @param {string} type - The type of interaction to unload + * @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(type, interaction, itemData, itemIdentifier) { + const preloader = interactionPreloaders[type]; + if (preloader) { + return preloader.unload(interaction, itemData, itemIdentifier); + } + return Promise.resolve(); + } + }; +} diff --git a/src/proxy/cache/itemPreloader.js b/src/proxy/cache/itemPreloader.js index de3d47d4..24345911 100644 --- a/src/proxy/cache/itemPreloader.js +++ b/src/proxy/cache/itemPreloader.js @@ -26,6 +26,7 @@ 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'; /** @@ -63,6 +64,19 @@ const setItemFlag = (item, flag) => { 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 @@ -120,7 +134,10 @@ function itemPreloaderFactory(options) { //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 @@ -149,6 +166,42 @@ function itemPreloaderFactory(options) { }); }; + /** + * Preload the interactions + * @param {object} item + * @param {string} item.itemIdentifier - the item identifier + * @param {object} item.itemData.data - item data + * @returns {Promise} + * @private + */ + 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(); + })); + }; + + /** + * 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(); + })); + }; + /** * Preload the item assets * @param {object} item @@ -221,6 +274,7 @@ function itemPreloaderFactory(options) { if (isItemObjectValid(item)) { loading.push(itemLoad(item)); + loading.push(interactionLoad(item)); if (_.size(item.itemData.data && item.itemData.data.feedbacks)) { setItemFlag(item, 'hasFeedbacks'); @@ -245,10 +299,17 @@ function itemPreloaderFactory(options) { * @returns {Promise} */ unload(item) { - if (isItemObjectValid(item) && _.size(item.itemData.assets) > 0) { - return assetUnload(item); + const loading = []; + + 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)); } }; } diff --git a/src/proxy/cache/preloaders/interactions/preloaders.js b/src/proxy/cache/preloaders/interactions/preloaders.js new file mode 100644 index 00000000..8a7d22c2 --- /dev/null +++ b/src/proxy/cache/preloaders/interactions/preloaders.js @@ -0,0 +1,23 @@ +/* + * 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 + */ + +/** + * The list of asset loader factories + * @type {Function[]} + */ +export default []; diff --git a/test/proxy/cache/interactionPreloader/test.html b/test/proxy/cache/interactionPreloader/test.html new file mode 100644 index 00000000..e3a33bcc --- /dev/null +++ b/test/proxy/cache/interactionPreloader/test.html @@ -0,0 +1,24 @@ + + + + + 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..c67ceca6 --- /dev/null +++ b/test/proxy/cache/interactionPreloader/test.js @@ -0,0 +1,159 @@ +/** + * 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', + 'taoQtiTest/runner/proxy/cache/preloaders/interactions/preloaders' +], function (interactionPreloaderFactory, preloaders) { + 'use strict'; + + QUnit.module('API'); + + 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: '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() { + preloaders.splice(0, preloaders.length); + } + }); + + QUnit.test('has', assert => { + assert.expect(3); + const preloaderFactory = () => { + assert.ok(true, 'Interaction preloader created'); + return { + name: 'interaction', + load() { + assert.ok(false, 'The interaction should not be loaded'); + }, + unload() { + assert.ok(false, 'The interaction should not be unloaded'); + } + }; + }; + preloaders.push(preloaderFactory); + const preloader = interactionPreloaderFactory(); + + assert.ok(preloader.has('interaction'), 'The interaction preloader has a interaction preloader'); + assert.ok(!preloader.has('dummy'), 'The interaction preloader does not have a dummy preloader'); + }); + + QUnit.test('load', assert => { + assert.expect(7); + const ready = assert.async(); + const expectedInteraction = { + attributes: {}, + qtiClass: 'interaction' + }; + const expectedItemData = {}; + const expectedItemIdentifier = 'item-1'; + const preloaderFactory = () => { + assert.ok(true, 'Interaction preloader created'); + return { + name: 'interaction', + 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'); + } + }; + }; + preloaders.push(preloaderFactory); + const preloader = interactionPreloaderFactory(); + preloader + .load('interaction', expectedInteraction, expectedItemData, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The interaction preloader loaded interaction'); + }) + .then(() => preloader.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 expectedInteraction = { + attributes: {}, + qtiClass: 'interaction' + }; + const expectedItemData = {}; + const expectedItemIdentifier = 'item-1'; + const preloaderFactory = () => { + assert.ok(true, 'Interaction preloader created'); + return { + name: 'interaction', + 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(); + } + }; + }; + preloaders.push(preloaderFactory); + const preloader = interactionPreloaderFactory(); + preloader + .unload('interaction', expectedInteraction, expectedItemData, expectedItemIdentifier) + .then(() => { + assert.ok(true, 'The interaction preloader unloaded interaction'); + }) + .then(() => preloader.unload('dummy')) + .then(() => { + assert.ok(true, 'The interaction preloader accept unknown loader without failing'); + }) + .catch(err => assert.ok(false, err)) + .then(ready); + }); +}); From 1fb11f704663e011dafadfccf0f8a5881a516e57 Mon Sep 17 00:00:00 2001 From: jsconan Date: Thu, 7 Oct 2021 11:11:03 +0200 Subject: [PATCH 10/17] feat: add method to check if asset has been preloaded an asset preloader manager --- src/proxy/cache/assetPreloader.js | 17 ++++ src/proxy/cache/preloaders/assets/audio.js | 11 +++ src/proxy/cache/preloaders/assets/image.js | 11 +++ .../cache/preloaders/assets/stylesheet.js | 11 +++ test/proxy/cache/assetPreloader/test.js | 88 ++++++++++++++----- .../cache/preloaders/assets/audio/test.js | 6 +- .../cache/preloaders/assets/image/test.js | 6 +- .../preloaders/assets/stylesheet/test.js | 6 +- 8 files changed, 129 insertions(+), 27 deletions(-) diff --git a/src/proxy/cache/assetPreloader.js b/src/proxy/cache/assetPreloader.js index 870a0368..0b32bd7c 100644 --- a/src/proxy/cache/assetPreloader.js +++ b/src/proxy/cache/assetPreloader.js @@ -28,6 +28,7 @@ import preloaders from 'taoQtiTest/runner/proxy/cache/preloaders/assets/preloade /** * @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 */ @@ -57,6 +58,22 @@ export default function assetPreloaderFactory(assetManager) { return !!assetPreloaders[type]; }, + /** + * Tells whether an asset was preloaded or not + * @param {string} type - The type of asset to preload + * @param {string} url - the url of the asset 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(type, url, sourceUrl, itemIdentifier) { + const preloader = assetPreloaders[type]; + if (preloader) { + return !!preloader.loaded(url, sourceUrl, itemIdentifier); + } + return false; + }, + /** * Preloads an asset with respect to it type * @param {string} type - The type of asset to preload diff --git a/src/proxy/cache/preloaders/assets/audio.js b/src/proxy/cache/preloaders/assets/audio.js index d6635007..fb3cac7d 100644 --- a/src/proxy/cache/preloaders/assets/audio.js +++ b/src/proxy/cache/preloaders/assets/audio.js @@ -63,6 +63,17 @@ export default function audioPreloaderFactory(assetManager) { */ name: 'audio', + /** + * 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 diff --git a/src/proxy/cache/preloaders/assets/image.js b/src/proxy/cache/preloaders/assets/image.js index f63d37a5..d08e4141 100644 --- a/src/proxy/cache/preloaders/assets/image.js +++ b/src/proxy/cache/preloaders/assets/image.js @@ -40,6 +40,17 @@ export default function imagePreloaderFactory() { */ name: 'img', + /** + * 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 diff --git a/src/proxy/cache/preloaders/assets/stylesheet.js b/src/proxy/cache/preloaders/assets/stylesheet.js index e24fe23e..fa5ea9dc 100644 --- a/src/proxy/cache/preloaders/assets/stylesheet.js +++ b/src/proxy/cache/preloaders/assets/stylesheet.js @@ -70,6 +70,17 @@ export default function stylesheetPreloaderFactory() { */ name: 'css', + /** + * 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 diff --git a/test/proxy/cache/assetPreloader/test.js b/test/proxy/cache/assetPreloader/test.js index bcae605a..25cd30db 100644 --- a/test/proxy/cache/assetPreloader/test.js +++ b/test/proxy/cache/assetPreloader/test.js @@ -35,12 +35,14 @@ define([ assert.notDeepEqual(assetPreloaderFactory(), assetPreloaderFactory(), 'The factory creates new instances'); }); - QUnit.cases.init([{ title: 'has' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { - assert.expect(1); - const preloader = assetPreloaderFactory(); + 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}`); - }); + assert.equal(typeof preloader[data.title], 'function', `The assets preloader has the method ${data.title}`); + }); QUnit.module('behavior', { beforeEach() { @@ -51,11 +53,14 @@ define([ QUnit.test('has', assert => { assert.expect(4); const expectedAssetManager = {}; - const cssPreloaderFactory = assetManager => { - assert.ok(true, 'CSS preloader created'); + const preloaderFactory = assetManager => { + assert.ok(true, 'Asset preloader created'); assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); return { - name: 'css', + name: 'asset', + loaded() { + return false; + }, load() { assert.ok(false, 'The asset should not be loaded'); }, @@ -64,13 +69,50 @@ define([ } }; }; - preloaders.push(cssPreloaderFactory); + preloaders.push(preloaderFactory); const preloader = assetPreloaderFactory(expectedAssetManager); - assert.ok(preloader.has('css'), 'The asset preloader has a CSS preloader'); + 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); + const expectedAssetManager = {}; + const expectedUrl = 'sample.css'; + const expectedSourceUrl = 'http://test.com/sample.css'; + const expectedItemIdentifier = 'item-1'; + let loadedStatus = null; + const preloaderFactory = 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'); + } + }; + }; + preloaders.push(preloaderFactory); + const preloader = assetPreloaderFactory(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(); @@ -78,11 +120,14 @@ define([ const expectedUrl = 'sample.css'; const expectedSourceUrl = 'http://test.com/sample.css'; const expectedItemIdentifier = 'item-1'; - const cssPreloaderFactory = assetManager => { - assert.ok(true, 'CSS preloader created'); + const preloaderFactory = assetManager => { + assert.ok(true, 'Asset preloader created'); assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); return { - name: 'css', + 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'); @@ -99,10 +144,10 @@ define([ } }; }; - preloaders.push(cssPreloaderFactory); + preloaders.push(preloaderFactory); const preloader = assetPreloaderFactory(expectedAssetManager); preloader - .load('css', expectedUrl, expectedSourceUrl, expectedItemIdentifier) + .load('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier) .then(() => { assert.ok(true, 'The asset preloader loaded asset'); }) @@ -121,11 +166,14 @@ define([ const expectedUrl = 'sample.css'; const expectedSourceUrl = 'http://test.com/sample.css'; const expectedItemIdentifier = 'item-1'; - const cssPreloaderFactory = assetManager => { - assert.ok(true, 'CSS preloader created'); + const preloaderFactory = assetManager => { + assert.ok(true, 'Asset preloader created'); assert.strictEqual(assetManager, expectedAssetManager, 'The expected assetManager has been given'); return { - name: 'css', + name: 'asset', + loaded() { + return true; + }, load() { assert.ok(false, 'The asset should not be loaded'); }, @@ -142,10 +190,10 @@ define([ } }; }; - preloaders.push(cssPreloaderFactory); + preloaders.push(preloaderFactory); const preloader = assetPreloaderFactory(expectedAssetManager); preloader - .unload('css', expectedUrl, expectedSourceUrl, expectedItemIdentifier) + .unload('asset', expectedUrl, expectedSourceUrl, expectedItemIdentifier) .then(() => { assert.ok(true, 'The asset preloader unloaded asset'); }) diff --git a/test/proxy/cache/preloaders/assets/audio/test.js b/test/proxy/cache/preloaders/assets/audio/test.js index 6a6d23e7..477eaf03 100644 --- a/test/proxy/cache/preloaders/assets/audio/test.js +++ b/test/proxy/cache/preloaders/assets/audio/test.js @@ -61,7 +61,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/audio'], function (audi assert.strictEqual(preloader.name, 'audio', 'The preloader has the name "audio"'); }); - QUnit.cases.init([{ title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + QUnit.cases.init([{ title: 'loaded' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { assert.expect(1); const preloader = audioPreloaderFactory(assetManagerFactory()); @@ -71,7 +71,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/audio'], function (audi QUnit.module('behavior'); QUnit.test('load/unload', assert => { - assert.expect(3); + assert.expect(5); const ready = assert.async(); const assetManager = assetManagerFactory(); const preloader = audioPreloaderFactory(assetManager); @@ -88,6 +88,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/audio'], function (audi /^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(() => { @@ -103,6 +104,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/audio'], function (audi 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/test.js b/test/proxy/cache/preloaders/assets/image/test.js index 80284b43..1a19bfdf 100644 --- a/test/proxy/cache/preloaders/assets/image/test.js +++ b/test/proxy/cache/preloaders/assets/image/test.js @@ -39,7 +39,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/image'], function (imag assert.strictEqual(preloader.name, 'img', 'The preloader has the name "img"'); }); - QUnit.cases.init([{ title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + QUnit.cases.init([{ title: 'loaded' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { assert.expect(1); const preloader = imagePreloaderFactory(); @@ -71,11 +71,11 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/image'], function (imag preloader .load(assetUrl, data.asset, data.itemIdentifier) .then(() => { - assert.ok(true, '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.ok(true, '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/assets/stylesheet/test.js b/test/proxy/cache/preloaders/assets/stylesheet/test.js index 2bee9dbc..38fa8d01 100644 --- a/test/proxy/cache/preloaders/assets/stylesheet/test.js +++ b/test/proxy/cache/preloaders/assets/stylesheet/test.js @@ -43,7 +43,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'], function assert.strictEqual(preloader.name, 'css', 'The preloader has the name "css"'); }); - QUnit.cases.init([{ title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { + QUnit.cases.init([{ title: 'loaded' }, { title: 'load' }, { title: 'unload' }]).test('method ', (data, assert) => { assert.expect(1); const preloader = stylesheetPreloaderFactory(); @@ -53,7 +53,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'], function QUnit.module('behavior'); QUnit.test('load/unload', assert => { - assert.expect(3); + assert.expect(5); const ready = assert.async(); const preloader = stylesheetPreloaderFactory(); const data = { @@ -75,6 +75,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'], function 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(() => { @@ -83,6 +84,7 @@ define(['taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'], function 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); From 3a5e56db5c8f2182c6c7d5c1b80351abc3fff3a3 Mon Sep 17 00:00:00 2001 From: jsconan Date: Thu, 7 Oct 2021 12:35:46 +0200 Subject: [PATCH 11/17] feat: add a preloader manager registry --- src/proxy/cache/preloaderManager.js | 110 ++++++++ test/proxy/cache/preloaderManager/test.html | 21 ++ test/proxy/cache/preloaderManager/test.js | 264 ++++++++++++++++++++ 3 files changed, 395 insertions(+) create mode 100644 src/proxy/cache/preloaderManager.js create mode 100644 test/proxy/cache/preloaderManager/test.html create mode 100644 test/proxy/cache/preloaderManager/test.js 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/test/proxy/cache/preloaderManager/test.html b/test/proxy/cache/preloaderManager/test.html new file mode 100644 index 00000000..d160aa85 --- /dev/null +++ b/test/proxy/cache/preloaderManager/test.html @@ -0,0 +1,21 @@ + + + + + Test Runner - preloader manager + + + + +
+
+ + 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); + }); +}); From 8e049bd5ebcb91634f4e1c9a042bef1de51d8d13 Mon Sep 17 00:00:00 2001 From: jsconan Date: Thu, 7 Oct 2021 14:11:52 +0200 Subject: [PATCH 12/17] refactor: revamp the asset loader to use the preloaderManager instead --- src/proxy/cache/assetPreloader.js | 91 ++----- src/proxy/cache/preloaders/assets/audio.js | 178 ++++++------- src/proxy/cache/preloaders/assets/image.js | 113 ++++---- .../cache/preloaders/assets/preloaders.js | 8 +- .../cache/preloaders/assets/stylesheet.js | 151 +++++------ test/proxy/cache/assetPreloader/test.html | 3 - test/proxy/cache/assetPreloader/test.js | 250 +++++++++--------- .../cache/preloaders/assets/audio/test.js | 26 +- .../cache/preloaders/assets/image/test.js | 24 +- .../preloaders/assets/stylesheet/test.js | 26 +- 10 files changed, 406 insertions(+), 464 deletions(-) diff --git a/src/proxy/cache/assetPreloader.js b/src/proxy/cache/assetPreloader.js index 0b32bd7c..c1c11bdd 100644 --- a/src/proxy/cache/assetPreloader.js +++ b/src/proxy/cache/assetPreloader.js @@ -13,10 +13,19 @@ * 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 + * 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 @@ -25,6 +34,14 @@ import preloaders from 'taoQtiTest/runner/proxy/cache/preloaders/assets/preloade * @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 @@ -35,75 +52,11 @@ import preloaders from 'taoQtiTest/runner/proxy/cache/preloaders/assets/preloade /** * Manages the preloading of assets + * @function assetPreloaderFactory * @param assetManager - A reference to the assetManager * @return {assetPreloaderManager} */ -export default function assetPreloaderFactory(assetManager) { - const assetPreloaders = preloaders.reduce((map, factory) => { - const preloader = factory(assetManager); - map[preloader.name] = preloader; - return map; - }, {}); - - /** - * @typedef assetPreloaderManager - */ - return { - /** - * Checks whether or not an asset preloader exists for a particular type - * @param {string} type - * @returns {boolean} - */ - has(type) { - return !!assetPreloaders[type]; - }, - - /** - * Tells whether an asset was preloaded or not - * @param {string} type - The type of asset to preload - * @param {string} url - the url of the asset 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(type, url, sourceUrl, itemIdentifier) { - const preloader = assetPreloaders[type]; - if (preloader) { - return !!preloader.loaded(url, sourceUrl, itemIdentifier); - } - return false; - }, - - /** - * Preloads an asset with respect to it type - * @param {string} type - The type of asset to preload - * @param {string} url - the url of the asset 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(type, url, sourceUrl, itemIdentifier) { - const preloader = assetPreloaders[type]; - if (preloader) { - return preloader.load(url, sourceUrl, itemIdentifier); - } - return Promise.resolve(); - }, +const assetPreloaderFactory = preloaderManagerFactory(); +preloaders.forEach(preloader => assetPreloaderFactory.registerProvider(preloader.name, preloader)); - /** - * Unloads an asset with respect to it type - * @param {string} type - The type of asset to unload - * @param {string} url - the url of the asset to unload - * @param {string} sourceUrl - the unresolved URL - * @param {string} itemIdentifier - the id of the item the asset belongs to - * @returns {Promise} - */ - unload(type, url, sourceUrl, itemIdentifier) { - const preloader = assetPreloaders[type]; - if (preloader) { - return preloader.unload(url, sourceUrl, itemIdentifier); - } - return Promise.resolve(); - } - }; -} +export default assetPreloaderFactory; diff --git a/src/proxy/cache/preloaders/assets/audio.js b/src/proxy/cache/preloaders/assets/audio.js index fb3cac7d..bf381119 100644 --- a/src/proxy/cache/preloaders/assets/audio.js +++ b/src/proxy/cache/preloaders/assets/audio.js @@ -16,108 +16,104 @@ * Copyright (c) 2017-2021 Open Assessment Technologies SA */ -/** - * (Pre)load audio content. - * - * @author Bertrand Chevrier - */ +import _ from 'lodash'; /** - * (Pre)load an item and it's assets. + * (Pre)load audio content. * * @author Bertrand Chevrier */ -import _ from 'lodash'; +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} - */ -export default function audioPreloaderFactory(assetManager) { - //keep references to preloaded audio blobs - const audioBlobs = {}; + /** + * 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(); + //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]); + //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 { - /** - * The name of the preloader - * @type {string} - */ - name: 'audio', + 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]); + }, - /** - * 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; - } + /** + * 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(); - }; - //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); + /** + * 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(); } - return Promise.resolve(); - } - }; -} + }; + } +}; diff --git a/src/proxy/cache/preloaders/assets/image.js b/src/proxy/cache/preloaders/assets/image.js index d08e4141..90c0ccba 100644 --- a/src/proxy/cache/preloaders/assets/image.js +++ b/src/proxy/cache/preloaders/assets/image.js @@ -16,69 +16,70 @@ * 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', -import _ from 'lodash'; - -/** - * Manages the preloading of images - * @returns {assetPreloader} - */ -export default function imagePreloaderFactory() { - //keep references to preloaded images attached - //in order to prevent garbage collection of cached images - const images = {}; - - return { - /** - * 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 = {}; - /** - * 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]); - }, + 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(); - }, + /** + * 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); + /** + * 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(); } - return Promise.resolve(); - } - }; -} + }; + } +}; diff --git a/src/proxy/cache/preloaders/assets/preloaders.js b/src/proxy/cache/preloaders/assets/preloaders.js index a7a9a25b..58996562 100644 --- a/src/proxy/cache/preloaders/assets/preloaders.js +++ b/src/proxy/cache/preloaders/assets/preloaders.js @@ -16,12 +16,12 @@ * Copyright (c) 2021 Open Assessment Technologies SA */ -import audioPreloaderFactory from 'taoQtiTest/runner/proxy/cache/preloaders/assets/audio'; -import imagePreloaderFactory from 'taoQtiTest/runner/proxy/cache/preloaders/assets/image'; -import stylesheetPreloaderFactory from 'taoQtiTest/runner/proxy/cache/preloaders/assets/stylesheet'; +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 [audioPreloaderFactory, imagePreloaderFactory, stylesheetPreloaderFactory]; +export default [audioPreloader, imagePreloader, stylesheetPreloader]; diff --git a/src/proxy/cache/preloaders/assets/stylesheet.js b/src/proxy/cache/preloaders/assets/stylesheet.js index fa5ea9dc..95a4f951 100644 --- a/src/proxy/cache/preloaders/assets/stylesheet.js +++ b/src/proxy/cache/preloaders/assets/stylesheet.js @@ -16,12 +16,6 @@ * Copyright (c) 2017-2021 Open Assessment Technologies SA */ -/** - * (Pre)load stylesheets. - * - * @author Bertrand Chevrier - */ - import _ from 'lodash'; /** @@ -56,81 +50,88 @@ const supportPreload = relSupport('preload'); const supportPrefetch = relSupport('prefetch'); /** - * Manages the preloading of stylesheets - * @returns {assetPreloader} + * (Pre)load stylesheets. + * + * @author Bertrand Chevrier */ -export default function stylesheetPreloaderFactory() { - //keep references to preloaded CSS files - const stylesheets = {}; +export default { + /** + * The name of the preloader + * @type {string} + */ + name: 'css', - return { - /** - * 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 = {}; - /** - * 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]); - }, + 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] || {}; + /** + * 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); + 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(); - }, + 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); + /** + * 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); } - stylesheets[itemIdentifier] = _.omit(stylesheets[itemIdentifier], sourceUrl); + return Promise.resolve(); } - return Promise.resolve(); - } - }; -} + }; + } +}; diff --git a/test/proxy/cache/assetPreloader/test.html b/test/proxy/cache/assetPreloader/test.html index 1b71a19b..a6998f00 100644 --- a/test/proxy/cache/assetPreloader/test.html +++ b/test/proxy/cache/assetPreloader/test.html @@ -7,9 +7,6 @@ + + + +
+
+ + 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); + }); +}); From 3f74ba942a0f17c190dba7e9163d6742da5e0086 Mon Sep 17 00:00:00 2001 From: jsconan Date: Thu, 7 Oct 2021 15:16:03 +0200 Subject: [PATCH 15/17] test: fix some unit tests by setting up missing mocks --- test/proxy/cache/interactionPreloader/test.html | 7 +++++-- test/proxy/cache/itemPreloader/test.html | 6 ++++++ test/proxy/cache/itemStore/test.html | 6 ++++++ test/proxy/offline/proxy/test.html | 6 ++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/test/proxy/cache/interactionPreloader/test.html b/test/proxy/cache/interactionPreloader/test.html index e3a33bcc..96550250 100644 --- a/test/proxy/cache/interactionPreloader/test.html +++ b/test/proxy/cache/interactionPreloader/test.html @@ -7,8 +7,11 @@