├── .gitignore ├── LICENSE.md ├── README.md ├── bower.json ├── dist └── CordovaFileCache.js ├── index.js ├── murmerhash.js ├── package.json └── test ├── index.html ├── promiscuous.js ├── qunit-1.15.0.css ├── qunit-1.15.0.js └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Mark Marijnissen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cordova-file-cache 2 | ========== 3 | > Super Awesome File Cache for Cordova Apps 4 | 5 | Based on [cordova-promise-fs](https://github.com/markmarijnissen/cordova-promise-fs). 6 | 7 | ## Getting started 8 | 9 | ```bash 10 | # fetch code using bower 11 | bower install cordova-file-cache cordova-promise-fs 12 | # ...or npm... 13 | npm install cordova-file-cache cordova-promise-fs 14 | 15 | # install Cordova and plugins 16 | cordova platform add ios 17 | cordova plugin add cordova-plugin-file 18 | cordova plugin add cordova-plugin-file-transfer # optional 19 | ``` 20 | 21 | **IMPORTANT:** For iOS, use Cordova 3.7.0 or higher (due to a [bug](https://github.com/AppGyver/steroids/issues/534) that affects requestFileSystem). 22 | 23 | Or just download and include [CordovaPromiseFS.js](https://raw.githubusercontent.com/markmarijnissen/cordova-promise-fs/master/dist/CordovaPromiseFS.js) and [CordovaFileCache.js](https://raw.githubusercontent.com/markmarijnissen/cordova-file-cache/master/dist/CordovaFileCache.js) 24 | 25 | ## Usage 26 | 27 | ### Initialize & configuration 28 | ```javascript 29 | // Initialize a Cache 30 | var cache = new CordovaFileCache({ 31 | fs: new CordovaPromiseFS({ // An instance of CordovaPromiseFS is REQUIRED 32 | Promise: Promise // <-- your favorite Promise lib (REQUIRED) 33 | }), 34 | mode: 'hash', // or 'mirror', optional 35 | localRoot: 'data', //optional 36 | serverRoot: 'http://yourserver.com/files/', // optional, required on 'mirror' mode 37 | cacheBuster: false // optional 38 | }); 39 | 40 | cache.ready.then(function(list){ 41 | // Promise when cache is ready. 42 | // Returns a list of paths on the FileSystem that are cached. 43 | }) 44 | ``` 45 | 46 | * **CordovaPromiseFS** is **REQUIRED**! 47 | * You need to include a **Promise** library when creating a CordovaPromiseFS. Any library that follows the A+ spec will work. For example: bluebird or promiscuous. 48 | * **mode: "mirror"**: Mirrors the file structure from `serverRoot` at `localRoot`. 49 | * **mode: "hash"**: Filename is hash of server url (plus extension). 50 | * **CordovaPromiseFS()** is an instance of [cordova-promise-fs](https://github.com/markmarijnissen/cordova-promise-fs). 51 | * `cacheBuster` appends a timestamp to the url `?xxxxxx` to avoid the network cache. 52 | 53 | 54 | ### Add files to the cache 55 | ```javascript 56 | 57 | // First, add files 58 | cache.add('http://yourserver.com/folder/photo1.jpg') 59 | cache.add('folder/photo2.jpg') // automatically prepends the `severRoot` 60 | cache.add(['photo3.jpg','photo4.jpg']) 61 | 62 | // Now the cache is dirty: It needs to download. 63 | cache.isDirty() === true 64 | // cache.add also returns if the cache is dirty. 65 | var dirty = cache.add(['photo3.jpg']) 66 | 67 | // Downloading files. 68 | // The optional 'onprogress' event handler is enhanced with information 69 | // about the total download queue. 70 | // It is recommended to avoid heavy UI and animation while downloading. 71 | // 72 | // The optional 'includeFileProgress' defaults to "false". 73 | // When set to "true", you will also receive progress events from individual file tranfers. 74 | // 75 | // "false" is recommended as sending many FileTransfer progress events from native to JS can 76 | // slow down performance. 77 | var onprogress = function(e) { 78 | var progress ="Progress: " 79 | + e.queueIndex // current download index 80 | + " " 81 | + e.queueSize; // total files to download 82 | 83 | // Download files. 84 | cache.download(onprogress,includeFileProgress).then(function(cache){ ... },function(failedDownloads) { ... }) 85 | 86 | } 87 | ``` 88 | 89 | ### Use the cache 90 | ```javascript 91 | // Get the cached internalURL of the file: "cdvfile://localhost/persisent/cache/photo3.jpg" 92 | cache.get('photo3.jpg'); 93 | cache.toInternalURL('photo3.jpg'); 94 | cache.toInternalURL('http://yourserver.com/photo3.jpg'); 95 | 96 | // Get the file URL of the file: "file://.../photo3.jpg"; 97 | cache.toURL('photo3.jpg'); 98 | 99 | // When file is not cached, the server URL is returned as a fallback. 100 | cache.get('http://yoursever.com/never-cached-this.jpg') === 'http://yoursever.com/never-cached-this.jpg' 101 | cache.get('never-cached-this.jpg') === 'http://yoursever.com/never-cached-this.jpg' 102 | 103 | // Get Base64 encoded data URL. 104 | cache.toDataURL('photo3.jpg').then(function(base64){},function(err){}); 105 | ``` 106 | 107 | ### Other functions 108 | ```javascript 109 | // Abort all downloads 110 | cache.abort() 111 | 112 | // Clear cache (removes localRoot directory) 113 | cache.clear().then( ... ) 114 | 115 | // Or remove a single file 116 | cache.remove('photo3.jpg').then( ... ) 117 | 118 | // Returns path on Cordova Filesystem, i.e. "/cache/photo3.jpg" 119 | cache.toPath('photo3.jpg'); 120 | 121 | // Returns server URL to download, i.e. "http://yourserver.com/photo3.jpg"; 122 | cache.toServerURL('photo3.jpg'); 123 | 124 | // Needs a download? 125 | cache.isDirty(); 126 | 127 | // Returns a list of server URLs that need to be downloaded. 128 | cache.getDownloadQueue(); 129 | 130 | // Return a list of paths that are cached (i.e. ["/cache/photo3.jpg"]) 131 | cache.list().then(function(list){...},function(err){...}) 132 | 133 | ``` 134 | 135 | ## Changelog 136 | 137 | ### 1.2.2 - bugfixes 138 | 139 | * When download is ready, fail if there are no errors instead of fail if cache is dirty (again) 140 | 141 | ### 1.2.0 142 | 143 | * Return errors when not all files are loaded instead of empty array 144 | 145 | ### 1.1.0 146 | 147 | * Update cordova-promise-fs to 1.1.0 148 | * Default download progress events only updates on download finished (performance boost!) 149 | 150 | ### 1.0.0 151 | 152 | * Update to cordova-promise-fs 1.0.0 153 | 154 | ### 0.14.0 (07/02/2016) 155 | 156 | * Handle REST api calls better (thanks @xontab) 157 | * Fix download onprogress index 158 | 159 | ### 0.13.0 (23/01/2016) 160 | 161 | * Fix download onprogress index 162 | 163 | ### 0.12.0 (18/03/2015) 164 | 165 | * Export hash function as CordovaFileCache.hash (needed by App Loader) 166 | 167 | ### 0.11.0 (17/03/2015) 168 | 169 | * Update CordovaPromiseFS dependency. 170 | * Fix some errors in README 171 | 172 | ### 0.10.0 (21/12/2014) 173 | 174 | * Update CordovaPromiseFS dependency 175 | 176 | ### 0.9.0 (21/12/2014) 177 | 178 | * Bugfix with cacheBuster 179 | 180 | ### 0.8.0 (28/11/2014) 181 | 182 | * Normalized path everywhere. 183 | 184 | ### 0.7.0 (27/11/2014) 185 | 186 | * Added tests and fixed few minor bugs 187 | 188 | ### 0.6.0 (19/11/2014) 189 | 190 | * Bugfix: changes to "get" and "toInternalURL" methods. 191 | * Bugfix: LocalRoot should NOT start with a slash (Android) 192 | 193 | ### 0.5.0 (15/11/2014) 194 | 195 | * Bugfix: Make sure cache returns a valid server URL if file is not cached. 196 | 197 | ### 0.4.0 (13/11/2014) 198 | 199 | * Added Chrome Support! 200 | 201 | ### 0.3.0 (09/11/2014) 202 | 203 | * Added `cacheBuster` option. 204 | 205 | ### 0.2.0 (07/11/2014) 206 | 207 | * Many small bugfixes 208 | * Upgraded the build process with `webpack` 209 | 210 | ### 0.1.0 (06/11/2014) 211 | 212 | ## Contribute 213 | 214 | Convert CommonJS to a browser-version: 215 | ```bash 216 | npm install webpack -g 217 | npm run-script prepublish 218 | ``` 219 | 220 | Feel free to contribute to this project in any way. The easiest way to support this project is by giving it a star. 221 | 222 | ## Contact 223 | - @markmarijnissen 224 | - http://www.madebymark.nl 225 | - info@madebymark.nl 226 | 227 | © 2014 - Mark Marijnissen 228 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-file-cache", 3 | "main": "dist/CordovaFileCache.js", 4 | "version": "1.2.0", 5 | "homepage": "https://github.com/markmarijnissen/cordova-file-cache", 6 | "authors": [ 7 | "Mark Marijnissen " 8 | ], 9 | "description": "Awesome File Cache for Cordova Apps", 10 | "keywords": [ 11 | "cordova", 12 | "file", 13 | "cache" 14 | ], 15 | "license": "MIT", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test", 21 | "tests" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /dist/CordovaFileCache.js: -------------------------------------------------------------------------------- 1 | var CordovaFileCache = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) 11 | /******/ return installedModules[moduleId].exports; 12 | 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ exports: {}, 16 | /******/ id: moduleId, 17 | /******/ loaded: false 18 | /******/ }; 19 | 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | 23 | /******/ // Flag the module as loaded 24 | /******/ module.loaded = true; 25 | 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | 30 | 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | 37 | /******/ // __webpack_public_path__ 38 | /******/ __webpack_require__.p = ""; 39 | 40 | /******/ // Load entry module and return exports 41 | /******/ return __webpack_require__(0); 42 | /******/ }) 43 | /************************************************************************/ 44 | /******/ ([ 45 | /* 0 */ 46 | /***/ function(module, exports, __webpack_require__) { 47 | 48 | var hash = __webpack_require__(1); 49 | var Promise = null; 50 | 51 | /* Cordova File Cache x */ 52 | function FileCache(options){ 53 | var self = this; 54 | // cordova-promise-fs 55 | this._fs = options.fs; 56 | if(!this._fs) { 57 | throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.'); 58 | } 59 | // Use Promises from fs. 60 | Promise = this._fs.Promise; 61 | 62 | // 'mirror' mirrors files structure from "serverRoot" to "localRoot" 63 | // 'hash' creates a 1-deep filestructure, where the filenames are hashed server urls (with extension) 64 | this._mirrorMode = options.mode !== 'hash'; 65 | this._retry = options.retry || [500,1500,8000]; 66 | this._cacheBuster = !!options.cacheBuster; 67 | 68 | // normalize path 69 | this.localRoot = this._fs.normalize(options.localRoot || 'data'); 70 | this.serverRoot = this._fs.normalize(options.serverRoot || ''); 71 | 72 | // set internal variables 73 | this._downloading = []; // download promises 74 | this._added = []; // added files 75 | this._cached = {}; // cached files 76 | 77 | // list existing cache contents 78 | this.ready = this._fs.ensure(this.localRoot) 79 | .then(function(entry){ 80 | self.localInternalURL = typeof entry.toInternalURL === 'function'? entry.toInternalURL(): entry.toURL(); 81 | self.localUrl = entry.toURL(); 82 | return self.list(); 83 | }); 84 | } 85 | 86 | FileCache.hash = hash; 87 | 88 | /** 89 | * Helper to cache all 'internalURL' and 'URL' for quick synchronous access 90 | * to the cached files. 91 | */ 92 | FileCache.prototype.list = function list(){ 93 | var self = this; 94 | return new Promise(function(resolve,reject){ 95 | self._fs.list(self.localRoot,'rfe').then(function(entries){ 96 | self._cached = {}; 97 | entries = entries.map(function(entry){ 98 | var fullPath = self._fs.normalize(entry.fullPath); 99 | self._cached[fullPath] = { 100 | toInternalURL: typeof entry.toInternalURL === 'function'? entry.toInternalURL(): entry.toURL(), 101 | toURL: entry.toURL(), 102 | }; 103 | return fullPath; 104 | }); 105 | resolve(entries); 106 | },function(){ 107 | resolve([]); 108 | }); 109 | }); 110 | }; 111 | 112 | FileCache.prototype.add = function add(urls){ 113 | if(!urls) urls = []; 114 | if(typeof urls === 'string') urls = [urls]; 115 | var self = this; 116 | urls.forEach(function(url){ 117 | url = self.toServerURL(url); 118 | if(self._added.indexOf(url) === -1) { 119 | self._added.push(url); 120 | } 121 | }); 122 | return self.isDirty(); 123 | }; 124 | 125 | FileCache.prototype.remove = function remove(urls,returnPromises){ 126 | if(!urls) urls = []; 127 | var promises = []; 128 | if(typeof urls === 'string') urls = [urls]; 129 | var self = this; 130 | urls.forEach(function(url){ 131 | var index = self._added.indexOf(self.toServerURL(url)); 132 | if(index >= 0) self._added.splice(index,1); 133 | var path = self.toPath(url); 134 | promises.push(self._fs.remove(path)); 135 | delete self._cached[path]; 136 | }); 137 | return returnPromises? Promise.all(promises): self.isDirty(); 138 | }; 139 | 140 | FileCache.prototype.getDownloadQueue = function(){ 141 | var self = this; 142 | var queue = self._added.filter(function(url){ 143 | return !self.isCached(url); 144 | }); 145 | return queue; 146 | }; 147 | 148 | FileCache.prototype.getAdded = function() { 149 | return this._added; 150 | }; 151 | 152 | FileCache.prototype.isDirty = function isDirty(){ 153 | return this.getDownloadQueue().length > 0; 154 | }; 155 | 156 | FileCache.prototype.download = function download(onprogress,includeFileProgressEvents){ 157 | var fs = this._fs; 158 | var self = this; 159 | includeFileProgressEvents = includeFileProgressEvents || false; 160 | self.abort(); 161 | 162 | return new Promise(function(resolve,reject){ 163 | // make sure cache directory exists and that 164 | // we have retrieved the latest cache contents 165 | // to avoid downloading files we already have! 166 | fs.ensure(self.localRoot).then(function(){ 167 | return self.list(); 168 | }).then(function(){ 169 | // no dowloads needed, resolve 170 | if(!self.isDirty()) { 171 | resolve(self); 172 | return; 173 | } 174 | 175 | // keep track of number of downloads! 176 | var queue = self.getDownloadQueue(); 177 | var done = self._downloading.length; 178 | var total = self._downloading.length + queue.length; 179 | var percentage = 0; 180 | var errors = []; 181 | 182 | // download every file in the queue (which is the diff from _added with _cached) 183 | queue.forEach(function(url){ 184 | var path = self.toPath(url); 185 | // augment progress event with done/total stats 186 | var onSingleDownloadProgress; 187 | if(typeof onprogress === 'function') { 188 | onSingleDownloadProgress = function(ev){ 189 | ev.queueIndex = done; 190 | ev.queueSize = total; 191 | ev.url = url; 192 | ev.path = path; 193 | ev.percentage = done / total; 194 | if(ev.loaded > 0 && ev.total > 0 && done !== total){ 195 | ev.percentage += (ev.loaded / ev.total) / total; 196 | } 197 | ev.percentage = Math.max(percentage,ev.percentage); 198 | percentage = ev.percentage; 199 | onprogress(ev); 200 | }; 201 | } 202 | 203 | // callback 204 | var onDone = function(){ 205 | done++; 206 | if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); 207 | 208 | // when we're done 209 | if(done === total) { 210 | // reset downloads 211 | self._downloading = []; 212 | // check if we got everything 213 | self.list().then(function(){ 214 | // final progress event! 215 | if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); 216 | // Yes, we're not dirty anymore! 217 | if(!self.isDirty()) { 218 | resolve(self); 219 | // Aye, some files got left behind! 220 | } else { 221 | reject(errors); 222 | } 223 | },reject); 224 | } 225 | }; 226 | var onErr = function(err){ 227 | if(err && err.target && err.target.error) err = err.target.error; 228 | errors.push(err); 229 | onDone(); 230 | }; 231 | 232 | var downloadUrl = url; 233 | if(self._cacheBuster) downloadUrl += "?"+Date.now(); 234 | var download = fs.download(downloadUrl,path,{retry:self._retry},includeFileProgressEvents && onSingleDownloadProgress? onSingleDownloadProgress: undefined); 235 | download.then(onDone,onErr); 236 | self._downloading.push(download); 237 | }); 238 | },reject); 239 | }); 240 | }; 241 | 242 | FileCache.prototype.abort = function abort(){ 243 | this._downloading.forEach(function(download){ 244 | download.abort(); 245 | }); 246 | this._downloading = []; 247 | }; 248 | 249 | FileCache.prototype.isCached = function isCached(url){ 250 | url = this.toPath(url); 251 | return !!this._cached[url]; 252 | }; 253 | 254 | FileCache.prototype.clear = function clear(){ 255 | var self = this; 256 | this._cached = {}; 257 | return this._fs.removeDir(this.localRoot).then(function(){ 258 | return self._fs.ensure(self.localRoot); 259 | }); 260 | }; 261 | 262 | /** 263 | * Helpers to output to various formats 264 | */ 265 | FileCache.prototype.toInternalURL = function toInternalURL(url){ 266 | var path = this.toPath(url); 267 | if(this._cached[path]) return this._cached[path].toInternalURL; 268 | return url; 269 | }; 270 | 271 | FileCache.prototype.get = function get(url){ 272 | var path = this.toPath(url); 273 | if(this._cached[path]) return this._cached[path].toURL; 274 | return this.toServerURL(url); 275 | }; 276 | 277 | FileCache.prototype.toDataURL = function toDataURL(url){ 278 | return this._fs.toDataURL(this.toPath(url)); 279 | }; 280 | 281 | FileCache.prototype.toURL = function toURL(url){ 282 | var path = this.toPath(url); 283 | return this._cached[path]? this._cached[path].toURL: url; 284 | }; 285 | 286 | FileCache.prototype.toServerURL = function toServerURL(path){ 287 | var path = this._fs.normalize(path); 288 | return path.indexOf('://') < 0? this.serverRoot + path: path; 289 | }; 290 | 291 | /** 292 | * Helper to transform remote URL to a local path (for cordova-promise-fs) 293 | */ 294 | FileCache.prototype.toPath = function toPath(url){ 295 | if(this._mirrorMode) { 296 | var query = url.indexOf('?'); 297 | if(query > -1){ 298 | url = url.substr(0,query); 299 | } 300 | url = this._fs.normalize(url || ''); 301 | var len = this.serverRoot.length; 302 | if(url.substr(0,len) !== this.serverRoot) { 303 | return this.localRoot + url; 304 | } else { 305 | return this.localRoot + url.substr(len); 306 | } 307 | } else { 308 | var ext = url.match(/\.[a-z]{1,}/g); 309 | if (ext) { 310 | ext = ext[ext.length-1]; 311 | } else { 312 | ext = '.txt'; 313 | } 314 | return this.localRoot + hash(url) + ext; 315 | } 316 | }; 317 | 318 | module.exports = FileCache; 319 | 320 | 321 | /***/ }, 322 | /* 1 */ 323 | /***/ function(module, exports) { 324 | 325 | /** 326 | * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) 327 | * 328 | * @author Gary Court 329 | * @see http://github.com/garycourt/murmurhash-js 330 | * @author Austin Appleby 331 | * @see http://sites.google.com/site/murmurhash/ 332 | * 333 | * @param {string} key ASCII only 334 | * @param {number} seed Positive integer only 335 | * @return {number} 32-bit positive integer hash 336 | */ 337 | 338 | function murmurhash3_32_gc(key, seed) { 339 | var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; 340 | 341 | remainder = key.length & 3; // key.length % 4 342 | bytes = key.length - remainder; 343 | h1 = seed; 344 | c1 = 0xcc9e2d51; 345 | c2 = 0x1b873593; 346 | i = 0; 347 | 348 | while (i < bytes) { 349 | k1 = 350 | ((key.charCodeAt(i) & 0xff)) | 351 | ((key.charCodeAt(++i) & 0xff) << 8) | 352 | ((key.charCodeAt(++i) & 0xff) << 16) | 353 | ((key.charCodeAt(++i) & 0xff) << 24); 354 | ++i; 355 | 356 | k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; 357 | k1 = (k1 << 15) | (k1 >>> 17); 358 | k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; 359 | 360 | h1 ^= k1; 361 | h1 = (h1 << 13) | (h1 >>> 19); 362 | h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; 363 | h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); 364 | } 365 | 366 | k1 = 0; 367 | 368 | switch (remainder) { 369 | case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; 370 | case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; 371 | case 1: k1 ^= (key.charCodeAt(i) & 0xff); 372 | 373 | k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; 374 | k1 = (k1 << 15) | (k1 >>> 17); 375 | k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; 376 | h1 ^= k1; 377 | } 378 | 379 | h1 ^= key.length; 380 | 381 | h1 ^= h1 >>> 16; 382 | h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; 383 | h1 ^= h1 >>> 13; 384 | h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; 385 | h1 ^= h1 >>> 16; 386 | 387 | return h1 >>> 0; 388 | } 389 | 390 | module.exports = murmurhash3_32_gc; 391 | 392 | /***/ } 393 | /******/ ]); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var hash = require('./murmerhash'); 2 | var Promise = null; 3 | 4 | /* Cordova File Cache x */ 5 | function FileCache(options){ 6 | var self = this; 7 | // cordova-promise-fs 8 | this._fs = options.fs; 9 | if(!this._fs) { 10 | throw new Error('Missing required option "fs". Add an instance of cordova-promise-fs.'); 11 | } 12 | // Use Promises from fs. 13 | Promise = this._fs.Promise; 14 | 15 | // 'mirror' mirrors files structure from "serverRoot" to "localRoot" 16 | // 'hash' creates a 1-deep filestructure, where the filenames are hashed server urls (with extension) 17 | this._mirrorMode = options.mode !== 'hash'; 18 | this._retry = options.retry || [500,1500,8000]; 19 | this._cacheBuster = !!options.cacheBuster; 20 | 21 | // normalize path 22 | this.localRoot = this._fs.normalize(options.localRoot || 'data'); 23 | this.serverRoot = this._fs.normalize(options.serverRoot || ''); 24 | 25 | // set internal variables 26 | this._downloading = []; // download promises 27 | this._added = []; // added files 28 | this._cached = {}; // cached files 29 | 30 | // list existing cache contents 31 | this.ready = this._fs.ensure(this.localRoot) 32 | .then(function(entry){ 33 | self.localInternalURL = typeof entry.toInternalURL === 'function'? entry.toInternalURL(): entry.toURL(); 34 | self.localUrl = entry.toURL(); 35 | return self.list(); 36 | }); 37 | } 38 | 39 | FileCache.hash = hash; 40 | 41 | /** 42 | * Helper to cache all 'internalURL' and 'URL' for quick synchronous access 43 | * to the cached files. 44 | */ 45 | FileCache.prototype.list = function list(){ 46 | var self = this; 47 | return new Promise(function(resolve,reject){ 48 | self._fs.list(self.localRoot,'rfe').then(function(entries){ 49 | self._cached = {}; 50 | entries = entries.map(function(entry){ 51 | var fullPath = self._fs.normalize(entry.fullPath); 52 | self._cached[fullPath] = { 53 | toInternalURL: typeof entry.toInternalURL === 'function'? entry.toInternalURL(): entry.toURL(), 54 | toURL: entry.toURL(), 55 | }; 56 | return fullPath; 57 | }); 58 | resolve(entries); 59 | },function(){ 60 | resolve([]); 61 | }); 62 | }); 63 | }; 64 | 65 | FileCache.prototype.add = function add(urls){ 66 | if(!urls) urls = []; 67 | if(typeof urls === 'string') urls = [urls]; 68 | var self = this; 69 | urls.forEach(function(url){ 70 | url = self.toServerURL(url); 71 | if(self._added.indexOf(url) === -1) { 72 | self._added.push(url); 73 | } 74 | }); 75 | return self.isDirty(); 76 | }; 77 | 78 | FileCache.prototype.remove = function remove(urls,returnPromises){ 79 | if(!urls) urls = []; 80 | var promises = []; 81 | if(typeof urls === 'string') urls = [urls]; 82 | var self = this; 83 | urls.forEach(function(url){ 84 | var index = self._added.indexOf(self.toServerURL(url)); 85 | if(index >= 0) self._added.splice(index,1); 86 | var path = self.toPath(url); 87 | promises.push(self._fs.remove(path)); 88 | delete self._cached[path]; 89 | }); 90 | return returnPromises? Promise.all(promises): self.isDirty(); 91 | }; 92 | 93 | FileCache.prototype.getDownloadQueue = function(){ 94 | var self = this; 95 | var queue = self._added.filter(function(url){ 96 | return !self.isCached(url); 97 | }); 98 | return queue; 99 | }; 100 | 101 | FileCache.prototype.getAdded = function() { 102 | return this._added; 103 | }; 104 | 105 | FileCache.prototype.isDirty = function isDirty(){ 106 | return this.getDownloadQueue().length > 0; 107 | }; 108 | 109 | FileCache.prototype.download = function download(onprogress,includeFileProgressEvents){ 110 | var fs = this._fs; 111 | var self = this; 112 | includeFileProgressEvents = includeFileProgressEvents || false; 113 | self.abort(); 114 | 115 | return new Promise(function(resolve,reject){ 116 | // make sure cache directory exists and that 117 | // we have retrieved the latest cache contents 118 | // to avoid downloading files we already have! 119 | fs.ensure(self.localRoot).then(function(){ 120 | return self.list(); 121 | }).then(function(){ 122 | // no dowloads needed, resolve 123 | if(!self.isDirty()) { 124 | resolve(self); 125 | return; 126 | } 127 | 128 | // keep track of number of downloads! 129 | var queue = self.getDownloadQueue(); 130 | var done = self._downloading.length; 131 | var total = self._downloading.length + queue.length; 132 | var percentage = 0; 133 | var errors = []; 134 | 135 | // download every file in the queue (which is the diff from _added with _cached) 136 | queue.forEach(function(url){ 137 | var path = self.toPath(url); 138 | // augment progress event with done/total stats 139 | var onSingleDownloadProgress; 140 | if(typeof onprogress === 'function') { 141 | onSingleDownloadProgress = function(ev){ 142 | ev.queueIndex = done; 143 | ev.queueSize = total; 144 | ev.url = url; 145 | ev.path = path; 146 | ev.percentage = done / total; 147 | if(ev.loaded > 0 && ev.total > 0 && done !== total){ 148 | ev.percentage += (ev.loaded / ev.total) / total; 149 | } 150 | ev.percentage = Math.max(percentage,ev.percentage); 151 | percentage = ev.percentage; 152 | onprogress(ev); 153 | }; 154 | } 155 | 156 | // callback 157 | var onDone = function(){ 158 | done++; 159 | if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); 160 | 161 | // when we're done 162 | if(done === total) { 163 | // reset downloads 164 | self._downloading = []; 165 | // check if we got everything 166 | self.list().then(function(){ 167 | // final progress event! 168 | if(onSingleDownloadProgress) onSingleDownloadProgress(new ProgressEvent()); 169 | // Yes, we're not dirty anymore! 170 | if(errors.length === 0) { 171 | resolve(self); 172 | // Aye, some files got left behind! 173 | } else { 174 | reject(errors); 175 | } 176 | },reject); 177 | } 178 | }; 179 | var onErr = function(err){ 180 | if(err && err.target && err.target.error) err = err.target.error; 181 | errors.push(err); 182 | onDone(); 183 | }; 184 | 185 | var downloadUrl = url; 186 | if(self._cacheBuster) downloadUrl += "?"+Date.now(); 187 | var download = fs.download(downloadUrl,path,{retry:self._retry},includeFileProgressEvents && onSingleDownloadProgress? onSingleDownloadProgress: undefined); 188 | download.then(onDone,onErr); 189 | self._downloading.push(download); 190 | }); 191 | },reject); 192 | }); 193 | }; 194 | 195 | FileCache.prototype.abort = function abort(){ 196 | this._downloading.forEach(function(download){ 197 | download.abort(); 198 | }); 199 | this._downloading = []; 200 | }; 201 | 202 | FileCache.prototype.isCached = function isCached(url){ 203 | url = this.toPath(url); 204 | return !!this._cached[url]; 205 | }; 206 | 207 | FileCache.prototype.clear = function clear(){ 208 | var self = this; 209 | this._cached = {}; 210 | return this._fs.removeDir(this.localRoot).then(function(){ 211 | return self._fs.ensure(self.localRoot); 212 | }); 213 | }; 214 | 215 | /** 216 | * Helpers to output to various formats 217 | */ 218 | FileCache.prototype.toInternalURL = function toInternalURL(url){ 219 | var path = this.toPath(url); 220 | if(this._cached[path]) return this._cached[path].toInternalURL; 221 | return url; 222 | }; 223 | 224 | FileCache.prototype.get = function get(url){ 225 | var path = this.toPath(url); 226 | if(this._cached[path]) return this._cached[path].toURL; 227 | return this.toServerURL(url); 228 | }; 229 | 230 | FileCache.prototype.toDataURL = function toDataURL(url){ 231 | return this._fs.toDataURL(this.toPath(url)); 232 | }; 233 | 234 | FileCache.prototype.toURL = function toURL(url){ 235 | var path = this.toPath(url); 236 | return this._cached[path]? this._cached[path].toURL: url; 237 | }; 238 | 239 | FileCache.prototype.toServerURL = function toServerURL(path){ 240 | var path = this._fs.normalize(path); 241 | return path.indexOf('://') < 0? this.serverRoot + path: path; 242 | }; 243 | 244 | /** 245 | * Helper to transform remote URL to a local path (for cordova-promise-fs) 246 | */ 247 | FileCache.prototype.toPath = function toPath(url){ 248 | if(this._mirrorMode) { 249 | var query = url.indexOf('?'); 250 | if(query > -1){ 251 | url = url.substr(0,query); 252 | } 253 | url = this._fs.normalize(url || ''); 254 | var len = this.serverRoot.length; 255 | if(url.substr(0,len) !== this.serverRoot) { 256 | return this.localRoot + url; 257 | } else { 258 | return this.localRoot + url.substr(len); 259 | } 260 | } else { 261 | var ext = url.match(/\.[a-z]{1,}/g); 262 | if (ext) { 263 | ext = ext[ext.length-1]; 264 | } else { 265 | ext = '.txt'; 266 | } 267 | return this.localRoot + hash(url) + ext; 268 | } 269 | }; 270 | 271 | module.exports = FileCache; 272 | -------------------------------------------------------------------------------- /murmerhash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) 3 | * 4 | * @author Gary Court 5 | * @see http://github.com/garycourt/murmurhash-js 6 | * @author Austin Appleby 7 | * @see http://sites.google.com/site/murmurhash/ 8 | * 9 | * @param {string} key ASCII only 10 | * @param {number} seed Positive integer only 11 | * @return {number} 32-bit positive integer hash 12 | */ 13 | 14 | function murmurhash3_32_gc(key, seed) { 15 | var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; 16 | 17 | remainder = key.length & 3; // key.length % 4 18 | bytes = key.length - remainder; 19 | h1 = seed; 20 | c1 = 0xcc9e2d51; 21 | c2 = 0x1b873593; 22 | i = 0; 23 | 24 | while (i < bytes) { 25 | k1 = 26 | ((key.charCodeAt(i) & 0xff)) | 27 | ((key.charCodeAt(++i) & 0xff) << 8) | 28 | ((key.charCodeAt(++i) & 0xff) << 16) | 29 | ((key.charCodeAt(++i) & 0xff) << 24); 30 | ++i; 31 | 32 | k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; 33 | k1 = (k1 << 15) | (k1 >>> 17); 34 | k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; 35 | 36 | h1 ^= k1; 37 | h1 = (h1 << 13) | (h1 >>> 19); 38 | h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; 39 | h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); 40 | } 41 | 42 | k1 = 0; 43 | 44 | switch (remainder) { 45 | case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; 46 | case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; 47 | case 1: k1 ^= (key.charCodeAt(i) & 0xff); 48 | 49 | k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; 50 | k1 = (k1 << 15) | (k1 >>> 17); 51 | k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; 52 | h1 ^= k1; 53 | } 54 | 55 | h1 ^= key.length; 56 | 57 | h1 ^= h1 >>> 16; 58 | h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; 59 | h1 ^= h1 >>> 13; 60 | h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; 61 | h1 ^= h1 >>> 16; 62 | 63 | return h1 >>> 0; 64 | } 65 | 66 | module.exports = murmurhash3_32_gc; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-file-cache", 3 | "version": "1.2.2", 4 | "description": "Cordova File Cache", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepublish": "webpack index.js dist/CordovaFileCache.js --output-library CordovaFileCache --output-library-target var", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:markmarijnissen/cordova-file-cache.git" 13 | }, 14 | "keywords": [ 15 | "cordova", 16 | "file", 17 | "cache" 18 | ], 19 | "author": "Mark Marijnissen", 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cordova-File-Cache tests 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/promiscuous.js: -------------------------------------------------------------------------------- 1 | /**@license MIT-promiscuous-©Ruben Verborgh*/ 2 | (function (func, obj) { 3 | // Type checking utility function 4 | function is(type, item) { return (typeof item)[0] == type; } 5 | 6 | // Creates a promise, calling callback(resolve, reject), ignoring other parameters. 7 | function Promise(callback, handler) { 8 | // The `handler` variable points to the function that will 9 | // 1) handle a .then(resolved, rejected) call 10 | // 2) handle a resolve or reject call (if the first argument === `is`) 11 | // Before 2), `handler` holds a queue of callbacks. 12 | // After 2), `handler` is a finalized .then handler. 13 | handler = function pendingHandler(resolved, rejected, value, queue, then, i) { 14 | queue = pendingHandler.q; 15 | 16 | // Case 1) handle a .then(resolved, rejected) call 17 | if (resolved != is) { 18 | return Promise(function (resolve, reject) { 19 | queue.push({ p: this, r: resolve, j: reject, 1: resolved, 0: rejected }); 20 | }); 21 | } 22 | 23 | // Case 2) handle a resolve or reject call 24 | // (`resolved` === `is` acts as a sentinel) 25 | // The actual function signature is 26 | // .re[ject|solve](, success, value) 27 | 28 | // Check if the value is a promise and try to obtain its `then` method 29 | if (value && (is(func, value) | is(obj, value))) { 30 | try { then = value.then; } 31 | catch (reason) { rejected = 0; value = reason; } 32 | } 33 | // If the value is a promise, take over its state 34 | if (is(func, then)) { 35 | function valueHandler(resolved) { 36 | return function (value) { then && (then = 0, pendingHandler(is, resolved, value)); }; 37 | } 38 | try { then.call(value, valueHandler(1), rejected = valueHandler(0)); } 39 | catch (reason) { rejected(reason); } 40 | } 41 | // The value is not a promise; handle resolve/reject 42 | else { 43 | // Replace this handler with a finalized resolved/rejected handler 44 | handler = function (Resolved, Rejected) { 45 | // If the Resolved or Rejected parameter is not a function, 46 | // return the original promise (now stored in the `callback` variable) 47 | if (!is(func, (Resolved = rejected ? Resolved : Rejected))) 48 | return callback; 49 | // Otherwise, return a finalized promise, transforming the value with the function 50 | return Promise(function (resolve, reject) { finalize(this, resolve, reject, value, Resolved); }); 51 | }; 52 | // Resolve/reject pending callbacks 53 | i = 0; 54 | while (i < queue.length) { 55 | then = queue[i++]; 56 | // If no callback, just resolve/reject the promise 57 | if (!is(func, resolved = then[rejected])) 58 | (rejected ? then.r : then.j)(value); 59 | // Otherwise, resolve/reject the promise with the result of the callback 60 | else 61 | finalize(then.p, then.r, then.j, value, resolved); 62 | } 63 | } 64 | }; 65 | // The queue of pending callbacks; garbage-collected when handler is resolved/rejected 66 | handler.q = []; 67 | 68 | // Create and return the promise (reusing the callback variable) 69 | callback.call(callback = { then: function (resolved, rejected) { return handler(resolved, rejected); }, 70 | catch: function (rejected) { return handler(0, rejected); } }, 71 | function (value) { handler(is, 1, value); }, 72 | function (reason) { handler(is, 0, reason); }); 73 | return callback; 74 | } 75 | 76 | // Finalizes the promise by resolving/rejecting it with the transformed value 77 | function finalize(promise, resolve, reject, value, transform) { 78 | setImmediate(function () { 79 | try { 80 | // Transform the value through and check whether it's a promise 81 | value = transform(value); 82 | transform = value && (is(obj, value) | is(func, value)) && value.then; 83 | // Return the result if it's not a promise 84 | if (!is(func, transform)) 85 | resolve(value); 86 | // If it's a promise, make sure it's not circular 87 | else if (value == promise) 88 | reject(TypeError()); 89 | // Take over the promise's state 90 | else 91 | transform.call(value, resolve, reject); 92 | } 93 | catch (error) { reject(error); } 94 | }); 95 | } 96 | 97 | // Export the main module 98 | module.exports = Promise; 99 | 100 | // Creates a resolved promise 101 | Promise.resolve = ResolvedPromise; 102 | function ResolvedPromise(value) { return Promise(function (resolve) { resolve(value); }); } 103 | 104 | // Creates a rejected promise 105 | Promise.reject = function (reason) { return Promise(function (resolve, reject) { reject(reason); }); }; 106 | 107 | // Transforms an array of promises into a promise for an array 108 | Promise.all = function (promises) { 109 | return Promise(function (resolve, reject, count, values) { 110 | // Array of collected values 111 | values = []; 112 | // Resolve immediately if there are no promises 113 | count = promises.length || resolve(values); 114 | // Transform all elements (`map` is shorter than `forEach`) 115 | promises.map(function (promise, index) { 116 | ResolvedPromise(promise).then( 117 | // Store the value and resolve if it was the last 118 | function (value) { 119 | values[index] = value; 120 | --count || resolve(values); 121 | }, 122 | // Reject if one element fails 123 | reject); 124 | }); 125 | }); 126 | }; 127 | })('f', 'o'); 128 | -------------------------------------------------------------------------------- /test/qunit-1.15.0.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.15.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-08-08T16:00Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 1em 0.5em 1em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 1em 0.5em 1em; 73 | background-color: #2B81AF; 74 | color: #FFF; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | #qunit-modulefilter-container { 79 | float: right; 80 | } 81 | 82 | /** Tests: Pass/Fail */ 83 | 84 | #qunit-tests { 85 | list-style-position: inside; 86 | } 87 | 88 | #qunit-tests li { 89 | padding: 0.4em 1em 0.4em 1em; 90 | border-bottom: 1px solid #FFF; 91 | list-style-position: inside; 92 | } 93 | 94 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 95 | display: none; 96 | } 97 | 98 | #qunit-tests li strong { 99 | cursor: pointer; 100 | } 101 | 102 | #qunit-tests li a { 103 | padding: 0.5em; 104 | color: #C2CCD1; 105 | text-decoration: none; 106 | } 107 | #qunit-tests li a:hover, 108 | #qunit-tests li a:focus { 109 | color: #000; 110 | } 111 | 112 | #qunit-tests li .runtime { 113 | float: right; 114 | font-size: smaller; 115 | } 116 | 117 | .qunit-assert-list { 118 | margin-top: 0.5em; 119 | padding: 0.5em; 120 | 121 | background-color: #FFF; 122 | 123 | border-radius: 5px; 124 | } 125 | 126 | .qunit-collapsed { 127 | display: none; 128 | } 129 | 130 | #qunit-tests table { 131 | border-collapse: collapse; 132 | margin-top: 0.2em; 133 | } 134 | 135 | #qunit-tests th { 136 | text-align: right; 137 | vertical-align: top; 138 | padding: 0 0.5em 0 0; 139 | } 140 | 141 | #qunit-tests td { 142 | vertical-align: top; 143 | } 144 | 145 | #qunit-tests pre { 146 | margin: 0; 147 | white-space: pre-wrap; 148 | word-wrap: break-word; 149 | } 150 | 151 | #qunit-tests del { 152 | background-color: #E0F2BE; 153 | color: #374E0C; 154 | text-decoration: none; 155 | } 156 | 157 | #qunit-tests ins { 158 | background-color: #FFCACA; 159 | color: #500; 160 | text-decoration: none; 161 | } 162 | 163 | /*** Test Counts */ 164 | 165 | #qunit-tests b.counts { color: #000; } 166 | #qunit-tests b.passed { color: #5E740B; } 167 | #qunit-tests b.failed { color: #710909; } 168 | 169 | #qunit-tests li li { 170 | padding: 5px; 171 | background-color: #FFF; 172 | border-bottom: none; 173 | list-style-position: inside; 174 | } 175 | 176 | /*** Passing Styles */ 177 | 178 | #qunit-tests li li.pass { 179 | color: #3C510C; 180 | background-color: #FFF; 181 | border-left: 10px solid #C6E746; 182 | } 183 | 184 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 185 | #qunit-tests .pass .test-name { color: #366097; } 186 | 187 | #qunit-tests .pass .test-actual, 188 | #qunit-tests .pass .test-expected { color: #999; } 189 | 190 | #qunit-banner.qunit-pass { background-color: #C6E746; } 191 | 192 | /*** Failing Styles */ 193 | 194 | #qunit-tests li li.fail { 195 | color: #710909; 196 | background-color: #FFF; 197 | border-left: 10px solid #EE5757; 198 | white-space: pre; 199 | } 200 | 201 | #qunit-tests > li:last-child { 202 | border-radius: 0 0 5px 5px; 203 | } 204 | 205 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 206 | #qunit-tests .fail .test-name, 207 | #qunit-tests .fail .module-name { color: #000; } 208 | 209 | #qunit-tests .fail .test-actual { color: #EE5757; } 210 | #qunit-tests .fail .test-expected { color: #008000; } 211 | 212 | #qunit-banner.qunit-fail { background-color: #EE5757; } 213 | 214 | 215 | /** Result */ 216 | 217 | #qunit-testresult { 218 | padding: 0.5em 1em 0.5em 1em; 219 | 220 | color: #2B81AF; 221 | background-color: #D2E0E6; 222 | 223 | border-bottom: 1px solid #FFF; 224 | } 225 | #qunit-testresult .module-name { 226 | font-weight: 700; 227 | } 228 | 229 | /** Fixture */ 230 | 231 | #qunit-fixture { 232 | position: absolute; 233 | top: -10000px; 234 | left: -10000px; 235 | width: 1000px; 236 | height: 1000px; 237 | } 238 | -------------------------------------------------------------------------------- /test/qunit-1.15.0.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.15.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-08-08T16:00Z 10 | */ 11 | 12 | (function( window ) { 13 | 14 | var QUnit, 15 | config, 16 | onErrorFnPrev, 17 | fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ), 18 | toString = Object.prototype.toString, 19 | hasOwn = Object.prototype.hasOwnProperty, 20 | // Keep a local reference to Date (GH-283) 21 | Date = window.Date, 22 | now = Date.now || function() { 23 | return new Date().getTime(); 24 | }, 25 | setTimeout = window.setTimeout, 26 | clearTimeout = window.clearTimeout, 27 | defined = { 28 | document: typeof window.document !== "undefined", 29 | setTimeout: typeof window.setTimeout !== "undefined", 30 | sessionStorage: (function() { 31 | var x = "qunit-test-string"; 32 | try { 33 | sessionStorage.setItem( x, x ); 34 | sessionStorage.removeItem( x ); 35 | return true; 36 | } catch ( e ) { 37 | return false; 38 | } 39 | }()) 40 | }, 41 | /** 42 | * Provides a normalized error string, correcting an issue 43 | * with IE 7 (and prior) where Error.prototype.toString is 44 | * not properly implemented 45 | * 46 | * Based on http://es5.github.com/#x15.11.4.4 47 | * 48 | * @param {String|Error} error 49 | * @return {String} error message 50 | */ 51 | errorString = function( error ) { 52 | var name, message, 53 | errorString = error.toString(); 54 | if ( errorString.substring( 0, 7 ) === "[object" ) { 55 | name = error.name ? error.name.toString() : "Error"; 56 | message = error.message ? error.message.toString() : ""; 57 | if ( name && message ) { 58 | return name + ": " + message; 59 | } else if ( name ) { 60 | return name; 61 | } else if ( message ) { 62 | return message; 63 | } else { 64 | return "Error"; 65 | } 66 | } else { 67 | return errorString; 68 | } 69 | }, 70 | /** 71 | * Makes a clone of an object using only Array or Object as base, 72 | * and copies over the own enumerable properties. 73 | * 74 | * @param {Object} obj 75 | * @return {Object} New object with only the own properties (recursively). 76 | */ 77 | objectValues = function( obj ) { 78 | var key, val, 79 | vals = QUnit.is( "array", obj ) ? [] : {}; 80 | for ( key in obj ) { 81 | if ( hasOwn.call( obj, key ) ) { 82 | val = obj[ key ]; 83 | vals[ key ] = val === Object( val ) ? objectValues( val ) : val; 84 | } 85 | } 86 | return vals; 87 | }; 88 | 89 | // Root QUnit object. 90 | // `QUnit` initialized at top of scope 91 | QUnit = { 92 | 93 | // call on start of module test to prepend name to all tests 94 | module: function( name, testEnvironment ) { 95 | config.currentModule = name; 96 | config.currentModuleTestEnvironment = testEnvironment; 97 | config.modules[ name ] = true; 98 | }, 99 | 100 | asyncTest: function( testName, expected, callback ) { 101 | if ( arguments.length === 2 ) { 102 | callback = expected; 103 | expected = null; 104 | } 105 | 106 | QUnit.test( testName, expected, callback, true ); 107 | }, 108 | 109 | test: function( testName, expected, callback, async ) { 110 | var test; 111 | 112 | if ( arguments.length === 2 ) { 113 | callback = expected; 114 | expected = null; 115 | } 116 | 117 | test = new Test({ 118 | testName: testName, 119 | expected: expected, 120 | async: async, 121 | callback: callback, 122 | module: config.currentModule, 123 | moduleTestEnvironment: config.currentModuleTestEnvironment, 124 | stack: sourceFromStacktrace( 2 ) 125 | }); 126 | 127 | if ( !validTest( test ) ) { 128 | return; 129 | } 130 | 131 | test.queue(); 132 | }, 133 | 134 | start: function( count ) { 135 | var message; 136 | 137 | // QUnit hasn't been initialized yet. 138 | // Note: RequireJS (et al) may delay onLoad 139 | if ( config.semaphore === undefined ) { 140 | QUnit.begin(function() { 141 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 142 | setTimeout(function() { 143 | QUnit.start( count ); 144 | }); 145 | }); 146 | return; 147 | } 148 | 149 | config.semaphore -= count || 1; 150 | // don't start until equal number of stop-calls 151 | if ( config.semaphore > 0 ) { 152 | return; 153 | } 154 | 155 | // Set the starting time when the first test is run 156 | QUnit.config.started = QUnit.config.started || now(); 157 | // ignore if start is called more often then stop 158 | if ( config.semaphore < 0 ) { 159 | config.semaphore = 0; 160 | 161 | message = "Called start() while already started (QUnit.config.semaphore was 0 already)"; 162 | 163 | if ( config.current ) { 164 | QUnit.pushFailure( message, sourceFromStacktrace( 2 ) ); 165 | } else { 166 | throw new Error( message ); 167 | } 168 | 169 | return; 170 | } 171 | // A slight delay, to avoid any current callbacks 172 | if ( defined.setTimeout ) { 173 | setTimeout(function() { 174 | if ( config.semaphore > 0 ) { 175 | return; 176 | } 177 | if ( config.timeout ) { 178 | clearTimeout( config.timeout ); 179 | } 180 | 181 | config.blocking = false; 182 | process( true ); 183 | }, 13 ); 184 | } else { 185 | config.blocking = false; 186 | process( true ); 187 | } 188 | }, 189 | 190 | stop: function( count ) { 191 | config.semaphore += count || 1; 192 | config.blocking = true; 193 | 194 | if ( config.testTimeout && defined.setTimeout ) { 195 | clearTimeout( config.timeout ); 196 | config.timeout = setTimeout(function() { 197 | QUnit.ok( false, "Test timed out" ); 198 | config.semaphore = 1; 199 | QUnit.start(); 200 | }, config.testTimeout ); 201 | } 202 | } 203 | }; 204 | 205 | // We use the prototype to distinguish between properties that should 206 | // be exposed as globals (and in exports) and those that shouldn't 207 | (function() { 208 | function F() {} 209 | F.prototype = QUnit; 210 | QUnit = new F(); 211 | 212 | // Make F QUnit's constructor so that we can add to the prototype later 213 | QUnit.constructor = F; 214 | }()); 215 | 216 | /** 217 | * Config object: Maintain internal state 218 | * Later exposed as QUnit.config 219 | * `config` initialized at top of scope 220 | */ 221 | config = { 222 | // The queue of tests to run 223 | queue: [], 224 | 225 | // block until document ready 226 | blocking: true, 227 | 228 | // when enabled, show only failing tests 229 | // gets persisted through sessionStorage and can be changed in UI via checkbox 230 | hidepassed: false, 231 | 232 | // by default, run previously failed tests first 233 | // very useful in combination with "Hide passed tests" checked 234 | reorder: true, 235 | 236 | // by default, modify document.title when suite is done 237 | altertitle: true, 238 | 239 | // by default, scroll to top of the page when suite is done 240 | scrolltop: true, 241 | 242 | // when enabled, all tests must call expect() 243 | requireExpects: false, 244 | 245 | // add checkboxes that are persisted in the query-string 246 | // when enabled, the id is set to `true` as a `QUnit.config` property 247 | urlConfig: [ 248 | { 249 | id: "noglobals", 250 | label: "Check for Globals", 251 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 252 | }, 253 | { 254 | id: "notrycatch", 255 | label: "No try-catch", 256 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 257 | } 258 | ], 259 | 260 | // Set of all modules. 261 | modules: {}, 262 | 263 | callbacks: {} 264 | }; 265 | 266 | // Initialize more QUnit.config and QUnit.urlParams 267 | (function() { 268 | var i, current, 269 | location = window.location || { search: "", protocol: "file:" }, 270 | params = location.search.slice( 1 ).split( "&" ), 271 | length = params.length, 272 | urlParams = {}; 273 | 274 | if ( params[ 0 ] ) { 275 | for ( i = 0; i < length; i++ ) { 276 | current = params[ i ].split( "=" ); 277 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 278 | 279 | // allow just a key to turn on a flag, e.g., test.html?noglobals 280 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 281 | if ( urlParams[ current[ 0 ] ] ) { 282 | urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); 283 | } else { 284 | urlParams[ current[ 0 ] ] = current[ 1 ]; 285 | } 286 | } 287 | } 288 | 289 | QUnit.urlParams = urlParams; 290 | 291 | // String search anywhere in moduleName+testName 292 | config.filter = urlParams.filter; 293 | 294 | // Exact match of the module name 295 | config.module = urlParams.module; 296 | 297 | config.testNumber = []; 298 | if ( urlParams.testNumber ) { 299 | 300 | // Ensure that urlParams.testNumber is an array 301 | urlParams.testNumber = [].concat( urlParams.testNumber ); 302 | for ( i = 0; i < urlParams.testNumber.length; i++ ) { 303 | current = urlParams.testNumber[ i ]; 304 | config.testNumber.push( parseInt( current, 10 ) ); 305 | } 306 | } 307 | 308 | // Figure out if we're running the tests from a server or not 309 | QUnit.isLocal = location.protocol === "file:"; 310 | }()); 311 | 312 | extend( QUnit, { 313 | 314 | config: config, 315 | 316 | // Safe object type checking 317 | is: function( type, obj ) { 318 | return QUnit.objectType( obj ) === type; 319 | }, 320 | 321 | objectType: function( obj ) { 322 | if ( typeof obj === "undefined" ) { 323 | return "undefined"; 324 | } 325 | 326 | // Consider: typeof null === object 327 | if ( obj === null ) { 328 | return "null"; 329 | } 330 | 331 | var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), 332 | type = match && match[ 1 ] || ""; 333 | 334 | switch ( type ) { 335 | case "Number": 336 | if ( isNaN( obj ) ) { 337 | return "nan"; 338 | } 339 | return "number"; 340 | case "String": 341 | case "Boolean": 342 | case "Array": 343 | case "Date": 344 | case "RegExp": 345 | case "Function": 346 | return type.toLowerCase(); 347 | } 348 | if ( typeof obj === "object" ) { 349 | return "object"; 350 | } 351 | return undefined; 352 | }, 353 | 354 | url: function( params ) { 355 | params = extend( extend( {}, QUnit.urlParams ), params ); 356 | var key, 357 | querystring = "?"; 358 | 359 | for ( key in params ) { 360 | if ( hasOwn.call( params, key ) ) { 361 | querystring += encodeURIComponent( key ) + "=" + 362 | encodeURIComponent( params[ key ] ) + "&"; 363 | } 364 | } 365 | return window.location.protocol + "//" + window.location.host + 366 | window.location.pathname + querystring.slice( 0, -1 ); 367 | }, 368 | 369 | extend: extend 370 | }); 371 | 372 | /** 373 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 374 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 375 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 376 | * Doing this allows us to tell if the following methods have been overwritten on the actual 377 | * QUnit object. 378 | */ 379 | extend( QUnit.constructor.prototype, { 380 | 381 | // Logging callbacks; all receive a single argument with the listed properties 382 | // run test/logs.html for any related changes 383 | begin: registerLoggingCallback( "begin" ), 384 | 385 | // done: { failed, passed, total, runtime } 386 | done: registerLoggingCallback( "done" ), 387 | 388 | // log: { result, actual, expected, message } 389 | log: registerLoggingCallback( "log" ), 390 | 391 | // testStart: { name } 392 | testStart: registerLoggingCallback( "testStart" ), 393 | 394 | // testDone: { name, failed, passed, total, runtime } 395 | testDone: registerLoggingCallback( "testDone" ), 396 | 397 | // moduleStart: { name } 398 | moduleStart: registerLoggingCallback( "moduleStart" ), 399 | 400 | // moduleDone: { name, failed, passed, total } 401 | moduleDone: registerLoggingCallback( "moduleDone" ) 402 | }); 403 | 404 | QUnit.load = function() { 405 | runLoggingCallbacks( "begin", { 406 | totalTests: Test.count 407 | }); 408 | 409 | // Initialize the configuration options 410 | extend( config, { 411 | stats: { all: 0, bad: 0 }, 412 | moduleStats: { all: 0, bad: 0 }, 413 | started: 0, 414 | updateRate: 1000, 415 | autostart: true, 416 | filter: "", 417 | semaphore: 1 418 | }, true ); 419 | 420 | config.blocking = false; 421 | 422 | if ( config.autostart ) { 423 | QUnit.start(); 424 | } 425 | }; 426 | 427 | // `onErrorFnPrev` initialized at top of scope 428 | // Preserve other handlers 429 | onErrorFnPrev = window.onerror; 430 | 431 | // Cover uncaught exceptions 432 | // Returning true will suppress the default browser handler, 433 | // returning false will let it run. 434 | window.onerror = function( error, filePath, linerNr ) { 435 | var ret = false; 436 | if ( onErrorFnPrev ) { 437 | ret = onErrorFnPrev( error, filePath, linerNr ); 438 | } 439 | 440 | // Treat return value as window.onerror itself does, 441 | // Only do our handling if not suppressed. 442 | if ( ret !== true ) { 443 | if ( QUnit.config.current ) { 444 | if ( QUnit.config.current.ignoreGlobalErrors ) { 445 | return true; 446 | } 447 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 448 | } else { 449 | QUnit.test( "global failure", extend(function() { 450 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 451 | }, { validTest: validTest } ) ); 452 | } 453 | return false; 454 | } 455 | 456 | return ret; 457 | }; 458 | 459 | function done() { 460 | config.autorun = true; 461 | 462 | // Log the last module results 463 | if ( config.previousModule ) { 464 | runLoggingCallbacks( "moduleDone", { 465 | name: config.previousModule, 466 | failed: config.moduleStats.bad, 467 | passed: config.moduleStats.all - config.moduleStats.bad, 468 | total: config.moduleStats.all 469 | }); 470 | } 471 | delete config.previousModule; 472 | 473 | var runtime = now() - config.started, 474 | passed = config.stats.all - config.stats.bad; 475 | 476 | runLoggingCallbacks( "done", { 477 | failed: config.stats.bad, 478 | passed: passed, 479 | total: config.stats.all, 480 | runtime: runtime 481 | }); 482 | } 483 | 484 | /** @return Boolean: true if this test should be ran */ 485 | function validTest( test ) { 486 | var include, 487 | filter = config.filter && config.filter.toLowerCase(), 488 | module = config.module && config.module.toLowerCase(), 489 | fullName = ( test.module + ": " + test.testName ).toLowerCase(); 490 | 491 | // Internally-generated tests are always valid 492 | if ( test.callback && test.callback.validTest === validTest ) { 493 | delete test.callback.validTest; 494 | return true; 495 | } 496 | 497 | if ( config.testNumber.length > 0 ) { 498 | if ( inArray( test.testNumber, config.testNumber ) < 0 ) { 499 | return false; 500 | } 501 | } 502 | 503 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 504 | return false; 505 | } 506 | 507 | if ( !filter ) { 508 | return true; 509 | } 510 | 511 | include = filter.charAt( 0 ) !== "!"; 512 | if ( !include ) { 513 | filter = filter.slice( 1 ); 514 | } 515 | 516 | // If the filter matches, we need to honour include 517 | if ( fullName.indexOf( filter ) !== -1 ) { 518 | return include; 519 | } 520 | 521 | // Otherwise, do the opposite 522 | return !include; 523 | } 524 | 525 | // Doesn't support IE6 to IE9 526 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 527 | function extractStacktrace( e, offset ) { 528 | offset = offset === undefined ? 4 : offset; 529 | 530 | var stack, include, i; 531 | 532 | if ( e.stacktrace ) { 533 | 534 | // Opera 12.x 535 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 536 | } else if ( e.stack ) { 537 | 538 | // Firefox, Chrome, Safari 6+, IE10+, PhantomJS and Node 539 | stack = e.stack.split( "\n" ); 540 | if ( /^error$/i.test( stack[ 0 ] ) ) { 541 | stack.shift(); 542 | } 543 | if ( fileName ) { 544 | include = []; 545 | for ( i = offset; i < stack.length; i++ ) { 546 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 547 | break; 548 | } 549 | include.push( stack[ i ] ); 550 | } 551 | if ( include.length ) { 552 | return include.join( "\n" ); 553 | } 554 | } 555 | return stack[ offset ]; 556 | } else if ( e.sourceURL ) { 557 | 558 | // Safari < 6 559 | // exclude useless self-reference for generated Error objects 560 | if ( /qunit.js$/.test( e.sourceURL ) ) { 561 | return; 562 | } 563 | 564 | // for actual exceptions, this is useful 565 | return e.sourceURL + ":" + e.line; 566 | } 567 | } 568 | function sourceFromStacktrace( offset ) { 569 | try { 570 | throw new Error(); 571 | } catch ( e ) { 572 | return extractStacktrace( e, offset ); 573 | } 574 | } 575 | 576 | function synchronize( callback, last ) { 577 | config.queue.push( callback ); 578 | 579 | if ( config.autorun && !config.blocking ) { 580 | process( last ); 581 | } 582 | } 583 | 584 | function process( last ) { 585 | function next() { 586 | process( last ); 587 | } 588 | var start = now(); 589 | config.depth = config.depth ? config.depth + 1 : 1; 590 | 591 | while ( config.queue.length && !config.blocking ) { 592 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( now() - start ) < config.updateRate ) ) { 593 | config.queue.shift()(); 594 | } else { 595 | setTimeout( next, 13 ); 596 | break; 597 | } 598 | } 599 | config.depth--; 600 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 601 | done(); 602 | } 603 | } 604 | 605 | function saveGlobal() { 606 | config.pollution = []; 607 | 608 | if ( config.noglobals ) { 609 | for ( var key in window ) { 610 | if ( hasOwn.call( window, key ) ) { 611 | // in Opera sometimes DOM element ids show up here, ignore them 612 | if ( /^qunit-test-output/.test( key ) ) { 613 | continue; 614 | } 615 | config.pollution.push( key ); 616 | } 617 | } 618 | } 619 | } 620 | 621 | function checkPollution() { 622 | var newGlobals, 623 | deletedGlobals, 624 | old = config.pollution; 625 | 626 | saveGlobal(); 627 | 628 | newGlobals = diff( config.pollution, old ); 629 | if ( newGlobals.length > 0 ) { 630 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); 631 | } 632 | 633 | deletedGlobals = diff( old, config.pollution ); 634 | if ( deletedGlobals.length > 0 ) { 635 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); 636 | } 637 | } 638 | 639 | // returns a new Array with the elements that are in a but not in b 640 | function diff( a, b ) { 641 | var i, j, 642 | result = a.slice(); 643 | 644 | for ( i = 0; i < result.length; i++ ) { 645 | for ( j = 0; j < b.length; j++ ) { 646 | if ( result[ i ] === b[ j ] ) { 647 | result.splice( i, 1 ); 648 | i--; 649 | break; 650 | } 651 | } 652 | } 653 | return result; 654 | } 655 | 656 | function extend( a, b, undefOnly ) { 657 | for ( var prop in b ) { 658 | if ( hasOwn.call( b, prop ) ) { 659 | 660 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 661 | if ( !( prop === "constructor" && a === window ) ) { 662 | if ( b[ prop ] === undefined ) { 663 | delete a[ prop ]; 664 | } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { 665 | a[ prop ] = b[ prop ]; 666 | } 667 | } 668 | } 669 | } 670 | 671 | return a; 672 | } 673 | 674 | function registerLoggingCallback( key ) { 675 | 676 | // Initialize key collection of logging callback 677 | if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { 678 | config.callbacks[ key ] = []; 679 | } 680 | 681 | return function( callback ) { 682 | config.callbacks[ key ].push( callback ); 683 | }; 684 | } 685 | 686 | function runLoggingCallbacks( key, args ) { 687 | var i, l, callbacks; 688 | 689 | callbacks = config.callbacks[ key ]; 690 | for ( i = 0, l = callbacks.length; i < l; i++ ) { 691 | callbacks[ i ]( args ); 692 | } 693 | } 694 | 695 | // from jquery.js 696 | function inArray( elem, array ) { 697 | if ( array.indexOf ) { 698 | return array.indexOf( elem ); 699 | } 700 | 701 | for ( var i = 0, length = array.length; i < length; i++ ) { 702 | if ( array[ i ] === elem ) { 703 | return i; 704 | } 705 | } 706 | 707 | return -1; 708 | } 709 | 710 | function Test( settings ) { 711 | extend( this, settings ); 712 | this.assert = new Assert( this ); 713 | this.assertions = []; 714 | this.testNumber = ++Test.count; 715 | } 716 | 717 | Test.count = 0; 718 | 719 | Test.prototype = { 720 | setup: function() { 721 | if ( 722 | 723 | // Emit moduleStart when we're switching from one module to another 724 | this.module !== config.previousModule || 725 | 726 | // They could be equal (both undefined) but if the previousModule property doesn't 727 | // yet exist it means this is the first test in a suite that isn't wrapped in a 728 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 729 | // Without this, reporters can get testStart before moduleStart which is a problem. 730 | !hasOwn.call( config, "previousModule" ) 731 | ) { 732 | if ( hasOwn.call( config, "previousModule" ) ) { 733 | runLoggingCallbacks( "moduleDone", { 734 | name: config.previousModule, 735 | failed: config.moduleStats.bad, 736 | passed: config.moduleStats.all - config.moduleStats.bad, 737 | total: config.moduleStats.all 738 | }); 739 | } 740 | config.previousModule = this.module; 741 | config.moduleStats = { all: 0, bad: 0 }; 742 | runLoggingCallbacks( "moduleStart", { 743 | name: this.module 744 | }); 745 | } 746 | 747 | config.current = this; 748 | 749 | this.testEnvironment = extend({ 750 | setup: function() {}, 751 | teardown: function() {} 752 | }, this.moduleTestEnvironment ); 753 | 754 | this.started = now(); 755 | runLoggingCallbacks( "testStart", { 756 | name: this.testName, 757 | module: this.module, 758 | testNumber: this.testNumber 759 | }); 760 | 761 | if ( !config.pollution ) { 762 | saveGlobal(); 763 | } 764 | if ( config.notrycatch ) { 765 | this.testEnvironment.setup.call( this.testEnvironment, this.assert ); 766 | return; 767 | } 768 | try { 769 | this.testEnvironment.setup.call( this.testEnvironment, this.assert ); 770 | } catch ( e ) { 771 | this.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 772 | } 773 | }, 774 | run: function() { 775 | config.current = this; 776 | 777 | if ( this.async ) { 778 | QUnit.stop(); 779 | } 780 | 781 | this.callbackStarted = now(); 782 | 783 | if ( config.notrycatch ) { 784 | this.callback.call( this.testEnvironment, this.assert ); 785 | this.callbackRuntime = now() - this.callbackStarted; 786 | return; 787 | } 788 | 789 | try { 790 | this.callback.call( this.testEnvironment, this.assert ); 791 | this.callbackRuntime = now() - this.callbackStarted; 792 | } catch ( e ) { 793 | this.callbackRuntime = now() - this.callbackStarted; 794 | 795 | this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 796 | 797 | // else next test will carry the responsibility 798 | saveGlobal(); 799 | 800 | // Restart the tests if they're blocking 801 | if ( config.blocking ) { 802 | QUnit.start(); 803 | } 804 | } 805 | }, 806 | teardown: function() { 807 | config.current = this; 808 | if ( config.notrycatch ) { 809 | if ( typeof this.callbackRuntime === "undefined" ) { 810 | this.callbackRuntime = now() - this.callbackStarted; 811 | } 812 | this.testEnvironment.teardown.call( this.testEnvironment, this.assert ); 813 | return; 814 | } else { 815 | try { 816 | this.testEnvironment.teardown.call( this.testEnvironment, this.assert ); 817 | } catch ( e ) { 818 | this.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 819 | } 820 | } 821 | checkPollution(); 822 | }, 823 | finish: function() { 824 | config.current = this; 825 | if ( config.requireExpects && this.expected === null ) { 826 | this.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 827 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 828 | this.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 829 | } else if ( this.expected === null && !this.assertions.length ) { 830 | this.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 831 | } 832 | 833 | var i, 834 | bad = 0; 835 | 836 | this.runtime = now() - this.started; 837 | config.stats.all += this.assertions.length; 838 | config.moduleStats.all += this.assertions.length; 839 | 840 | for ( i = 0; i < this.assertions.length; i++ ) { 841 | if ( !this.assertions[ i ].result ) { 842 | bad++; 843 | config.stats.bad++; 844 | config.moduleStats.bad++; 845 | } 846 | } 847 | 848 | runLoggingCallbacks( "testDone", { 849 | name: this.testName, 850 | module: this.module, 851 | failed: bad, 852 | passed: this.assertions.length - bad, 853 | total: this.assertions.length, 854 | runtime: this.runtime, 855 | 856 | // HTML Reporter use 857 | assertions: this.assertions, 858 | testNumber: this.testNumber, 859 | 860 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead 861 | duration: this.runtime 862 | }); 863 | 864 | config.current = undefined; 865 | }, 866 | 867 | queue: function() { 868 | var bad, 869 | test = this; 870 | 871 | function run() { 872 | // each of these can by async 873 | synchronize(function() { 874 | test.setup(); 875 | }); 876 | synchronize(function() { 877 | test.run(); 878 | }); 879 | synchronize(function() { 880 | test.teardown(); 881 | }); 882 | synchronize(function() { 883 | test.finish(); 884 | }); 885 | } 886 | 887 | // `bad` initialized at top of scope 888 | // defer when previous test run passed, if storage is available 889 | bad = QUnit.config.reorder && defined.sessionStorage && 890 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 891 | 892 | if ( bad ) { 893 | run(); 894 | } else { 895 | synchronize( run, true ); 896 | } 897 | }, 898 | 899 | push: function( result, actual, expected, message ) { 900 | var source, 901 | details = { 902 | module: this.module, 903 | name: this.testName, 904 | result: result, 905 | message: message, 906 | actual: actual, 907 | expected: expected, 908 | testNumber: this.testNumber 909 | }; 910 | 911 | if ( !result ) { 912 | source = sourceFromStacktrace(); 913 | 914 | if ( source ) { 915 | details.source = source; 916 | } 917 | } 918 | 919 | runLoggingCallbacks( "log", details ); 920 | 921 | this.assertions.push({ 922 | result: !!result, 923 | message: message 924 | }); 925 | }, 926 | 927 | pushFailure: function( message, source, actual ) { 928 | if ( !this instanceof Test ) { 929 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace( 2 ) ); 930 | } 931 | 932 | var details = { 933 | module: this.module, 934 | name: this.testName, 935 | result: false, 936 | message: message || "error", 937 | actual: actual || null, 938 | testNumber: this.testNumber 939 | }; 940 | 941 | if ( source ) { 942 | details.source = source; 943 | } 944 | 945 | runLoggingCallbacks( "log", details ); 946 | 947 | this.assertions.push({ 948 | result: false, 949 | message: message 950 | }); 951 | } 952 | }; 953 | 954 | QUnit.pushFailure = function() { 955 | if ( !QUnit.config.current ) { 956 | throw new Error( "pushFailure() assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 957 | } 958 | 959 | // Gets current test obj 960 | var currentTest = QUnit.config.current.assert.test; 961 | 962 | return currentTest.pushFailure.apply( currentTest, arguments ); 963 | }; 964 | 965 | function Assert( testContext ) { 966 | this.test = testContext; 967 | } 968 | 969 | // Assert helpers 970 | QUnit.assert = Assert.prototype = { 971 | 972 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 973 | expect: function( asserts ) { 974 | if ( arguments.length === 1 ) { 975 | this.test.expected = asserts; 976 | } else { 977 | return this.test.expected; 978 | } 979 | }, 980 | 981 | // Exports test.push() to the user API 982 | push: function() { 983 | var assert = this; 984 | 985 | // Backwards compatibility fix. 986 | // Allows the direct use of global exported assertions and QUnit.assert.* 987 | // Although, it's use is not recommended as it can leak assertions 988 | // to other tests from async tests, because we only get a reference to the current test, 989 | // not exactly the test where assertion were intended to be called. 990 | if ( !QUnit.config.current ) { 991 | throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 992 | } 993 | if ( !( assert instanceof Assert ) ) { 994 | assert = QUnit.config.current.assert; 995 | } 996 | return assert.test.push.apply( assert.test, arguments ); 997 | }, 998 | 999 | /** 1000 | * Asserts rough true-ish result. 1001 | * @name ok 1002 | * @function 1003 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 1004 | */ 1005 | ok: function( result, message ) { 1006 | message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + 1007 | QUnit.dump.parse( result ) ); 1008 | if ( !!result ) { 1009 | this.push( true, result, true, message ); 1010 | } else { 1011 | this.test.pushFailure( message, null, result ); 1012 | } 1013 | }, 1014 | 1015 | /** 1016 | * Assert that the first two arguments are equal, with an optional message. 1017 | * Prints out both actual and expected values. 1018 | * @name equal 1019 | * @function 1020 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 1021 | */ 1022 | equal: function( actual, expected, message ) { 1023 | /*jshint eqeqeq:false */ 1024 | this.push( expected == actual, actual, expected, message ); 1025 | }, 1026 | 1027 | /** 1028 | * @name notEqual 1029 | * @function 1030 | */ 1031 | notEqual: function( actual, expected, message ) { 1032 | /*jshint eqeqeq:false */ 1033 | this.push( expected != actual, actual, expected, message ); 1034 | }, 1035 | 1036 | /** 1037 | * @name propEqual 1038 | * @function 1039 | */ 1040 | propEqual: function( actual, expected, message ) { 1041 | actual = objectValues( actual ); 1042 | expected = objectValues( expected ); 1043 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1044 | }, 1045 | 1046 | /** 1047 | * @name notPropEqual 1048 | * @function 1049 | */ 1050 | notPropEqual: function( actual, expected, message ) { 1051 | actual = objectValues( actual ); 1052 | expected = objectValues( expected ); 1053 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); 1054 | }, 1055 | 1056 | /** 1057 | * @name deepEqual 1058 | * @function 1059 | */ 1060 | deepEqual: function( actual, expected, message ) { 1061 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1062 | }, 1063 | 1064 | /** 1065 | * @name notDeepEqual 1066 | * @function 1067 | */ 1068 | notDeepEqual: function( actual, expected, message ) { 1069 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); 1070 | }, 1071 | 1072 | /** 1073 | * @name strictEqual 1074 | * @function 1075 | */ 1076 | strictEqual: function( actual, expected, message ) { 1077 | this.push( expected === actual, actual, expected, message ); 1078 | }, 1079 | 1080 | /** 1081 | * @name notStrictEqual 1082 | * @function 1083 | */ 1084 | notStrictEqual: function( actual, expected, message ) { 1085 | this.push( expected !== actual, actual, expected, message ); 1086 | }, 1087 | 1088 | "throws": function( block, expected, message ) { 1089 | var actual, expectedType, 1090 | expectedOutput = expected, 1091 | ok = false; 1092 | 1093 | // 'expected' is optional unless doing string comparison 1094 | if ( message == null && typeof expected === "string" ) { 1095 | message = expected; 1096 | expected = null; 1097 | } 1098 | 1099 | this.test.ignoreGlobalErrors = true; 1100 | try { 1101 | block.call( this.test.testEnvironment ); 1102 | } catch (e) { 1103 | actual = e; 1104 | } 1105 | this.test.ignoreGlobalErrors = false; 1106 | 1107 | if ( actual ) { 1108 | expectedType = QUnit.objectType( expected ); 1109 | 1110 | // we don't want to validate thrown error 1111 | if ( !expected ) { 1112 | ok = true; 1113 | expectedOutput = null; 1114 | 1115 | // expected is a regexp 1116 | } else if ( expectedType === "regexp" ) { 1117 | ok = expected.test( errorString( actual ) ); 1118 | 1119 | // expected is a string 1120 | } else if ( expectedType === "string" ) { 1121 | ok = expected === errorString( actual ); 1122 | 1123 | // expected is a constructor, maybe an Error constructor 1124 | } else if ( expectedType === "function" && actual instanceof expected ) { 1125 | ok = true; 1126 | 1127 | // expected is an Error object 1128 | } else if ( expectedType === "object" ) { 1129 | ok = actual instanceof expected.constructor && 1130 | actual.name === expected.name && 1131 | actual.message === expected.message; 1132 | 1133 | // expected is a validation function which returns true if validation passed 1134 | } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { 1135 | expectedOutput = null; 1136 | ok = true; 1137 | } 1138 | 1139 | this.push( ok, actual, expectedOutput, message ); 1140 | } else { 1141 | this.test.pushFailure( message, null, "No exception was thrown." ); 1142 | } 1143 | } 1144 | }; 1145 | 1146 | // Test for equality any JavaScript type. 1147 | // Author: Philippe Rathé 1148 | QUnit.equiv = (function() { 1149 | 1150 | // Call the o related callback with the given arguments. 1151 | function bindCallbacks( o, callbacks, args ) { 1152 | var prop = QUnit.objectType( o ); 1153 | if ( prop ) { 1154 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1155 | return callbacks[ prop ].apply( callbacks, args ); 1156 | } else { 1157 | return callbacks[ prop ]; // or undefined 1158 | } 1159 | } 1160 | } 1161 | 1162 | // the real equiv function 1163 | var innerEquiv, 1164 | 1165 | // stack to decide between skip/abort functions 1166 | callers = [], 1167 | 1168 | // stack to avoiding loops from circular referencing 1169 | parents = [], 1170 | parentsB = [], 1171 | 1172 | getProto = Object.getPrototypeOf || function( obj ) { 1173 | /* jshint camelcase: false, proto: true */ 1174 | return obj.__proto__; 1175 | }, 1176 | callbacks = (function() { 1177 | 1178 | // for string, boolean, number and null 1179 | function useStrictEquality( b, a ) { 1180 | 1181 | /*jshint eqeqeq:false */ 1182 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1183 | 1184 | // to catch short annotation VS 'new' annotation of a 1185 | // declaration 1186 | // e.g. var i = 1; 1187 | // var j = new Number(1); 1188 | return a == b; 1189 | } else { 1190 | return a === b; 1191 | } 1192 | } 1193 | 1194 | return { 1195 | "string": useStrictEquality, 1196 | "boolean": useStrictEquality, 1197 | "number": useStrictEquality, 1198 | "null": useStrictEquality, 1199 | "undefined": useStrictEquality, 1200 | 1201 | "nan": function( b ) { 1202 | return isNaN( b ); 1203 | }, 1204 | 1205 | "date": function( b, a ) { 1206 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1207 | }, 1208 | 1209 | "regexp": function( b, a ) { 1210 | return QUnit.objectType( b ) === "regexp" && 1211 | 1212 | // the regex itself 1213 | a.source === b.source && 1214 | 1215 | // and its modifiers 1216 | a.global === b.global && 1217 | 1218 | // (gmi) ... 1219 | a.ignoreCase === b.ignoreCase && 1220 | a.multiline === b.multiline && 1221 | a.sticky === b.sticky; 1222 | }, 1223 | 1224 | // - skip when the property is a method of an instance (OOP) 1225 | // - abort otherwise, 1226 | // initial === would have catch identical references anyway 1227 | "function": function() { 1228 | var caller = callers[ callers.length - 1 ]; 1229 | return caller !== Object && typeof caller !== "undefined"; 1230 | }, 1231 | 1232 | "array": function( b, a ) { 1233 | var i, j, len, loop, aCircular, bCircular; 1234 | 1235 | // b could be an object literal here 1236 | if ( QUnit.objectType( b ) !== "array" ) { 1237 | return false; 1238 | } 1239 | 1240 | len = a.length; 1241 | if ( len !== b.length ) { 1242 | // safe and faster 1243 | return false; 1244 | } 1245 | 1246 | // track reference to avoid circular references 1247 | parents.push( a ); 1248 | parentsB.push( b ); 1249 | for ( i = 0; i < len; i++ ) { 1250 | loop = false; 1251 | for ( j = 0; j < parents.length; j++ ) { 1252 | aCircular = parents[ j ] === a[ i ]; 1253 | bCircular = parentsB[ j ] === b[ i ]; 1254 | if ( aCircular || bCircular ) { 1255 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1256 | loop = true; 1257 | } else { 1258 | parents.pop(); 1259 | parentsB.pop(); 1260 | return false; 1261 | } 1262 | } 1263 | } 1264 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1265 | parents.pop(); 1266 | parentsB.pop(); 1267 | return false; 1268 | } 1269 | } 1270 | parents.pop(); 1271 | parentsB.pop(); 1272 | return true; 1273 | }, 1274 | 1275 | "object": function( b, a ) { 1276 | 1277 | /*jshint forin:false */ 1278 | var i, j, loop, aCircular, bCircular, 1279 | // Default to true 1280 | eq = true, 1281 | aProperties = [], 1282 | bProperties = []; 1283 | 1284 | // comparing constructors is more strict than using 1285 | // instanceof 1286 | if ( a.constructor !== b.constructor ) { 1287 | 1288 | // Allow objects with no prototype to be equivalent to 1289 | // objects with Object as their constructor. 1290 | if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || 1291 | ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { 1292 | return false; 1293 | } 1294 | } 1295 | 1296 | // stack constructor before traversing properties 1297 | callers.push( a.constructor ); 1298 | 1299 | // track reference to avoid circular references 1300 | parents.push( a ); 1301 | parentsB.push( b ); 1302 | 1303 | // be strict: don't ensure hasOwnProperty and go deep 1304 | for ( i in a ) { 1305 | loop = false; 1306 | for ( j = 0; j < parents.length; j++ ) { 1307 | aCircular = parents[ j ] === a[ i ]; 1308 | bCircular = parentsB[ j ] === b[ i ]; 1309 | if ( aCircular || bCircular ) { 1310 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1311 | loop = true; 1312 | } else { 1313 | eq = false; 1314 | break; 1315 | } 1316 | } 1317 | } 1318 | aProperties.push( i ); 1319 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1320 | eq = false; 1321 | break; 1322 | } 1323 | } 1324 | 1325 | parents.pop(); 1326 | parentsB.pop(); 1327 | callers.pop(); // unstack, we are done 1328 | 1329 | for ( i in b ) { 1330 | bProperties.push( i ); // collect b's properties 1331 | } 1332 | 1333 | // Ensures identical properties name 1334 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1335 | } 1336 | }; 1337 | }()); 1338 | 1339 | innerEquiv = function() { // can take multiple arguments 1340 | var args = [].slice.apply( arguments ); 1341 | if ( args.length < 2 ) { 1342 | return true; // end transition 1343 | } 1344 | 1345 | return ( (function( a, b ) { 1346 | if ( a === b ) { 1347 | return true; // catch the most you can 1348 | } else if ( a === null || b === null || typeof a === "undefined" || 1349 | typeof b === "undefined" || 1350 | QUnit.objectType( a ) !== QUnit.objectType( b ) ) { 1351 | 1352 | // don't lose time with error prone cases 1353 | return false; 1354 | } else { 1355 | return bindCallbacks( a, callbacks, [ b, a ] ); 1356 | } 1357 | 1358 | // apply transition with (1..n) arguments 1359 | }( args[ 0 ], args[ 1 ] ) ) && innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); 1360 | }; 1361 | 1362 | return innerEquiv; 1363 | }()); 1364 | 1365 | // Based on jsDump by Ariel Flesler 1366 | // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html 1367 | QUnit.dump = (function() { 1368 | function quote( str ) { 1369 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; 1370 | } 1371 | function literal( o ) { 1372 | return o + ""; 1373 | } 1374 | function join( pre, arr, post ) { 1375 | var s = dump.separator(), 1376 | base = dump.indent(), 1377 | inner = dump.indent( 1 ); 1378 | if ( arr.join ) { 1379 | arr = arr.join( "," + s + inner ); 1380 | } 1381 | if ( !arr ) { 1382 | return pre + post; 1383 | } 1384 | return [ pre, inner + arr, base + post ].join( s ); 1385 | } 1386 | function array( arr, stack ) { 1387 | var i = arr.length, 1388 | ret = new Array( i ); 1389 | this.up(); 1390 | while ( i-- ) { 1391 | ret[ i ] = this.parse( arr[ i ], undefined, stack ); 1392 | } 1393 | this.down(); 1394 | return join( "[", ret, "]" ); 1395 | } 1396 | 1397 | var reName = /^function (\w+)/, 1398 | dump = { 1399 | // type is used mostly internally, you can fix a (custom)type in advance 1400 | parse: function( obj, type, stack ) { 1401 | stack = stack || []; 1402 | var inStack, res, 1403 | parser = this.parsers[ type || this.typeOf( obj ) ]; 1404 | 1405 | type = typeof parser; 1406 | inStack = inArray( obj, stack ); 1407 | 1408 | if ( inStack !== -1 ) { 1409 | return "recursion(" + ( inStack - stack.length ) + ")"; 1410 | } 1411 | if ( type === "function" ) { 1412 | stack.push( obj ); 1413 | res = parser.call( this, obj, stack ); 1414 | stack.pop(); 1415 | return res; 1416 | } 1417 | return ( type === "string" ) ? parser : this.parsers.error; 1418 | }, 1419 | typeOf: function( obj ) { 1420 | var type; 1421 | if ( obj === null ) { 1422 | type = "null"; 1423 | } else if ( typeof obj === "undefined" ) { 1424 | type = "undefined"; 1425 | } else if ( QUnit.is( "regexp", obj ) ) { 1426 | type = "regexp"; 1427 | } else if ( QUnit.is( "date", obj ) ) { 1428 | type = "date"; 1429 | } else if ( QUnit.is( "function", obj ) ) { 1430 | type = "function"; 1431 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1432 | type = "window"; 1433 | } else if ( obj.nodeType === 9 ) { 1434 | type = "document"; 1435 | } else if ( obj.nodeType ) { 1436 | type = "node"; 1437 | } else if ( 1438 | 1439 | // native arrays 1440 | toString.call( obj ) === "[object Array]" || 1441 | 1442 | // NodeList objects 1443 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && typeof obj[ 0 ] === "undefined" ) ) ) 1444 | ) { 1445 | type = "array"; 1446 | } else if ( obj.constructor === Error.prototype.constructor ) { 1447 | type = "error"; 1448 | } else { 1449 | type = typeof obj; 1450 | } 1451 | return type; 1452 | }, 1453 | separator: function() { 1454 | return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; 1455 | }, 1456 | // extra can be a number, shortcut for increasing-calling-decreasing 1457 | indent: function( extra ) { 1458 | if ( !this.multiline ) { 1459 | return ""; 1460 | } 1461 | var chr = this.indentChar; 1462 | if ( this.HTML ) { 1463 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1464 | } 1465 | return new Array( this.depth + ( extra || 0 ) ).join( chr ); 1466 | }, 1467 | up: function( a ) { 1468 | this.depth += a || 1; 1469 | }, 1470 | down: function( a ) { 1471 | this.depth -= a || 1; 1472 | }, 1473 | setParser: function( name, parser ) { 1474 | this.parsers[ name ] = parser; 1475 | }, 1476 | // The next 3 are exposed so you can use them 1477 | quote: quote, 1478 | literal: literal, 1479 | join: join, 1480 | // 1481 | depth: 1, 1482 | // This is the list of parsers, to modify them, use dump.setParser 1483 | parsers: { 1484 | window: "[Window]", 1485 | document: "[Document]", 1486 | error: function( error ) { 1487 | return "Error(\"" + error.message + "\")"; 1488 | }, 1489 | unknown: "[Unknown]", 1490 | "null": "null", 1491 | "undefined": "undefined", 1492 | "function": function( fn ) { 1493 | var ret = "function", 1494 | // functions never have name in IE 1495 | name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; 1496 | 1497 | if ( name ) { 1498 | ret += " " + name; 1499 | } 1500 | ret += "( "; 1501 | 1502 | ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1503 | return join( ret, dump.parse( fn, "functionCode" ), "}" ); 1504 | }, 1505 | array: array, 1506 | nodelist: array, 1507 | "arguments": array, 1508 | object: function( map, stack ) { 1509 | /*jshint forin:false */ 1510 | var ret = [], keys, key, val, i, nonEnumerableProperties; 1511 | dump.up(); 1512 | keys = []; 1513 | for ( key in map ) { 1514 | keys.push( key ); 1515 | } 1516 | 1517 | // Some properties are not always enumerable on Error objects. 1518 | nonEnumerableProperties = [ "message", "name" ]; 1519 | for ( i in nonEnumerableProperties ) { 1520 | key = nonEnumerableProperties[ i ]; 1521 | if ( key in map && !( key in keys ) ) { 1522 | keys.push( key ); 1523 | } 1524 | } 1525 | keys.sort(); 1526 | for ( i = 0; i < keys.length; i++ ) { 1527 | key = keys[ i ]; 1528 | val = map[ key ]; 1529 | ret.push( dump.parse( key, "key" ) + ": " + dump.parse( val, undefined, stack ) ); 1530 | } 1531 | dump.down(); 1532 | return join( "{", ret, "}" ); 1533 | }, 1534 | node: function( node ) { 1535 | var len, i, val, 1536 | open = dump.HTML ? "<" : "<", 1537 | close = dump.HTML ? ">" : ">", 1538 | tag = node.nodeName.toLowerCase(), 1539 | ret = open + tag, 1540 | attrs = node.attributes; 1541 | 1542 | if ( attrs ) { 1543 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1544 | val = attrs[ i ].nodeValue; 1545 | 1546 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1547 | // Those have values like undefined, null, 0, false, "" or "inherit". 1548 | if ( val && val !== "inherit" ) { 1549 | ret += " " + attrs[ i ].nodeName + "=" + dump.parse( val, "attribute" ); 1550 | } 1551 | } 1552 | } 1553 | ret += close; 1554 | 1555 | // Show content of TextNode or CDATASection 1556 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1557 | ret += node.nodeValue; 1558 | } 1559 | 1560 | return ret + open + "/" + tag + close; 1561 | }, 1562 | 1563 | // function calls it internally, it's the arguments part of the function 1564 | functionArgs: function( fn ) { 1565 | var args, 1566 | l = fn.length; 1567 | 1568 | if ( !l ) { 1569 | return ""; 1570 | } 1571 | 1572 | args = new Array( l ); 1573 | while ( l-- ) { 1574 | 1575 | // 97 is 'a' 1576 | args[ l ] = String.fromCharCode( 97 + l ); 1577 | } 1578 | return " " + args.join( ", " ) + " "; 1579 | }, 1580 | // object calls it internally, the key part of an item in a map 1581 | key: quote, 1582 | // function calls it internally, it's the content of the function 1583 | functionCode: "[code]", 1584 | // node calls it internally, it's an html attribute value 1585 | attribute: quote, 1586 | string: quote, 1587 | date: quote, 1588 | regexp: literal, 1589 | number: literal, 1590 | "boolean": literal 1591 | }, 1592 | // if true, entities are escaped ( <, >, \t, space and \n ) 1593 | HTML: false, 1594 | // indentation unit 1595 | indentChar: " ", 1596 | // if true, items in a collection, are separated by a \n, else just a space. 1597 | multiline: true 1598 | }; 1599 | 1600 | return dump; 1601 | }()); 1602 | 1603 | // back compat 1604 | QUnit.jsDump = QUnit.dump; 1605 | 1606 | // For browser, export only select globals 1607 | if ( typeof window !== "undefined" ) { 1608 | 1609 | // Deprecated 1610 | // Extend assert methods to QUnit and Global scope through Backwards compatibility 1611 | (function() { 1612 | var i, 1613 | assertions = Assert.prototype; 1614 | 1615 | function applyCurrent( current ) { 1616 | return function() { 1617 | var assert = new Assert( QUnit.config.current ); 1618 | current.apply( assert, arguments ); 1619 | }; 1620 | } 1621 | 1622 | for ( i in assertions ) { 1623 | QUnit[ i ] = applyCurrent( assertions[ i ] ); 1624 | } 1625 | })(); 1626 | 1627 | (function() { 1628 | var i, l, 1629 | keys = [ 1630 | "test", 1631 | "module", 1632 | "expect", 1633 | "asyncTest", 1634 | "start", 1635 | "stop", 1636 | "ok", 1637 | "equal", 1638 | "notEqual", 1639 | "propEqual", 1640 | "notPropEqual", 1641 | "deepEqual", 1642 | "notDeepEqual", 1643 | "strictEqual", 1644 | "notStrictEqual", 1645 | "throws" 1646 | ]; 1647 | 1648 | for ( i = 0, l = keys.length; i < l; i++ ) { 1649 | window[ keys[ i ] ] = QUnit[ keys[ i ] ]; 1650 | } 1651 | })(); 1652 | 1653 | window.QUnit = QUnit; 1654 | } 1655 | 1656 | // For CommonJS environments, export everything 1657 | if ( typeof module !== "undefined" && module.exports ) { 1658 | module.exports = QUnit; 1659 | } 1660 | 1661 | // Get a reference to the global object, like window in browsers 1662 | }( (function() { 1663 | return this; 1664 | })() )); 1665 | 1666 | /*istanbul ignore next */ 1667 | /* 1668 | * Javascript Diff Algorithm 1669 | * By John Resig (http://ejohn.org/) 1670 | * Modified by Chu Alan "sprite" 1671 | * 1672 | * Released under the MIT license. 1673 | * 1674 | * More Info: 1675 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1676 | * 1677 | * Usage: QUnit.diff(expected, actual) 1678 | * 1679 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 1680 | */ 1681 | QUnit.diff = (function() { 1682 | var hasOwn = Object.prototype.hasOwnProperty; 1683 | 1684 | /*jshint eqeqeq:false, eqnull:true */ 1685 | function diff( o, n ) { 1686 | var i, 1687 | ns = {}, 1688 | os = {}; 1689 | 1690 | for ( i = 0; i < n.length; i++ ) { 1691 | if ( !hasOwn.call( ns, n[ i ] ) ) { 1692 | ns[ n[ i ] ] = { 1693 | rows: [], 1694 | o: null 1695 | }; 1696 | } 1697 | ns[ n[ i ] ].rows.push( i ); 1698 | } 1699 | 1700 | for ( i = 0; i < o.length; i++ ) { 1701 | if ( !hasOwn.call( os, o[ i ] ) ) { 1702 | os[ o[ i ] ] = { 1703 | rows: [], 1704 | n: null 1705 | }; 1706 | } 1707 | os[ o[ i ] ].rows.push( i ); 1708 | } 1709 | 1710 | for ( i in ns ) { 1711 | if ( hasOwn.call( ns, i ) ) { 1712 | if ( ns[ i ].rows.length === 1 && hasOwn.call( os, i ) && os[ i ].rows.length === 1 ) { 1713 | n[ ns[ i ].rows[ 0 ] ] = { 1714 | text: n[ ns[ i ].rows[ 0 ] ], 1715 | row: os[ i ].rows[ 0 ] 1716 | }; 1717 | o[ os[ i ].rows[ 0 ] ] = { 1718 | text: o[ os[ i ].rows[ 0 ] ], 1719 | row: ns[ i ].rows[ 0 ] 1720 | }; 1721 | } 1722 | } 1723 | } 1724 | 1725 | for ( i = 0; i < n.length - 1; i++ ) { 1726 | if ( n[ i ].text != null && n[ i + 1 ].text == null && n[ i ].row + 1 < o.length && o[ n[ i ].row + 1 ].text == null && 1727 | n[ i + 1 ] == o[ n[ i ].row + 1 ] ) { 1728 | 1729 | n[ i + 1 ] = { 1730 | text: n[ i + 1 ], 1731 | row: n[ i ].row + 1 1732 | }; 1733 | o[ n[ i ].row + 1 ] = { 1734 | text: o[ n[ i ].row + 1 ], 1735 | row: i + 1 1736 | }; 1737 | } 1738 | } 1739 | 1740 | for ( i = n.length - 1; i > 0; i-- ) { 1741 | if ( n[ i ].text != null && n[ i - 1 ].text == null && n[ i ].row > 0 && o[ n[ i ].row - 1 ].text == null && 1742 | n[ i - 1 ] == o[ n[ i ].row - 1 ] ) { 1743 | 1744 | n[ i - 1 ] = { 1745 | text: n[ i - 1 ], 1746 | row: n[ i ].row - 1 1747 | }; 1748 | o[ n[ i ].row - 1 ] = { 1749 | text: o[ n[ i ].row - 1 ], 1750 | row: i - 1 1751 | }; 1752 | } 1753 | } 1754 | 1755 | return { 1756 | o: o, 1757 | n: n 1758 | }; 1759 | } 1760 | 1761 | return function( o, n ) { 1762 | o = o.replace( /\s+$/, "" ); 1763 | n = n.replace( /\s+$/, "" ); 1764 | 1765 | var i, pre, 1766 | str = "", 1767 | out = diff( o === "" ? [] : o.split( /\s+/ ), n === "" ? [] : n.split( /\s+/ ) ), 1768 | oSpace = o.match( /\s+/g ), 1769 | nSpace = n.match( /\s+/g ); 1770 | 1771 | if ( oSpace == null ) { 1772 | oSpace = [ " " ]; 1773 | } else { 1774 | oSpace.push( " " ); 1775 | } 1776 | 1777 | if ( nSpace == null ) { 1778 | nSpace = [ " " ]; 1779 | } else { 1780 | nSpace.push( " " ); 1781 | } 1782 | 1783 | if ( out.n.length === 0 ) { 1784 | for ( i = 0; i < out.o.length; i++ ) { 1785 | str += "" + out.o[ i ] + oSpace[ i ] + ""; 1786 | } 1787 | } else { 1788 | if ( out.n[ 0 ].text == null ) { 1789 | for ( n = 0; n < out.o.length && out.o[ n ].text == null; n++ ) { 1790 | str += "" + out.o[ n ] + oSpace[ n ] + ""; 1791 | } 1792 | } 1793 | 1794 | for ( i = 0; i < out.n.length; i++ ) { 1795 | if ( out.n[ i ].text == null ) { 1796 | str += "" + out.n[ i ] + nSpace[ i ] + ""; 1797 | } else { 1798 | 1799 | // `pre` initialized at top of scope 1800 | pre = ""; 1801 | 1802 | for ( n = out.n[ i ].row + 1; n < out.o.length && out.o[ n ].text == null; n++ ) { 1803 | pre += "" + out.o[ n ] + oSpace[ n ] + ""; 1804 | } 1805 | str += " " + out.n[ i ].text + nSpace[ i ] + pre; 1806 | } 1807 | } 1808 | } 1809 | 1810 | return str; 1811 | }; 1812 | }()); 1813 | 1814 | (function() { 1815 | 1816 | // Deprecated QUnit.init - Ref #530 1817 | // Re-initialize the configuration options 1818 | QUnit.init = function() { 1819 | var tests, banner, result, qunit, 1820 | config = QUnit.config; 1821 | 1822 | config.stats = { all: 0, bad: 0 }; 1823 | config.moduleStats = { all: 0, bad: 0 }; 1824 | config.started = 0; 1825 | config.updateRate = 1000; 1826 | config.blocking = false; 1827 | config.autostart = true; 1828 | config.autorun = false; 1829 | config.filter = ""; 1830 | config.queue = []; 1831 | config.semaphore = 1; 1832 | 1833 | // Return on non-browser environments 1834 | // This is necessary to not break on node tests 1835 | if ( typeof window === "undefined" ) { 1836 | return; 1837 | } 1838 | 1839 | qunit = id( "qunit" ); 1840 | if ( qunit ) { 1841 | qunit.innerHTML = 1842 | "

" + escapeText( document.title ) + "

" + 1843 | "

" + 1844 | "
" + 1845 | "

" + 1846 | "
    "; 1847 | } 1848 | 1849 | tests = id( "qunit-tests" ); 1850 | banner = id( "qunit-banner" ); 1851 | result = id( "qunit-testresult" ); 1852 | 1853 | if ( tests ) { 1854 | tests.innerHTML = ""; 1855 | } 1856 | 1857 | if ( banner ) { 1858 | banner.className = ""; 1859 | } 1860 | 1861 | if ( result ) { 1862 | result.parentNode.removeChild( result ); 1863 | } 1864 | 1865 | if ( tests ) { 1866 | result = document.createElement( "p" ); 1867 | result.id = "qunit-testresult"; 1868 | result.className = "result"; 1869 | tests.parentNode.insertBefore( result, tests ); 1870 | result.innerHTML = "Running...
     "; 1871 | } 1872 | }; 1873 | 1874 | // Resets the test setup. Useful for tests that modify the DOM. 1875 | /* 1876 | DEPRECATED: Use multiple tests instead of resetting inside a test. 1877 | Use testStart or testDone for custom cleanup. 1878 | This method will throw an error in 2.0, and will be removed in 2.1 1879 | */ 1880 | QUnit.reset = function() { 1881 | 1882 | // Return on non-browser environments 1883 | // This is necessary to not break on node tests 1884 | if ( typeof window === "undefined" ) { 1885 | return; 1886 | } 1887 | 1888 | var fixture = id( "qunit-fixture" ); 1889 | if ( fixture ) { 1890 | fixture.innerHTML = config.fixture; 1891 | } 1892 | }; 1893 | 1894 | // Don't load the HTML Reporter on non-Browser environments 1895 | if ( typeof window === "undefined" ) { 1896 | return; 1897 | } 1898 | 1899 | var config = QUnit.config, 1900 | hasOwn = Object.prototype.hasOwnProperty, 1901 | defined = { 1902 | document: typeof window.document !== "undefined", 1903 | sessionStorage: (function() { 1904 | var x = "qunit-test-string"; 1905 | try { 1906 | sessionStorage.setItem( x, x ); 1907 | sessionStorage.removeItem( x ); 1908 | return true; 1909 | } catch ( e ) { 1910 | return false; 1911 | } 1912 | }()) 1913 | }; 1914 | 1915 | /** 1916 | * Escape text for attribute or text content. 1917 | */ 1918 | function escapeText( s ) { 1919 | if ( !s ) { 1920 | return ""; 1921 | } 1922 | s = s + ""; 1923 | 1924 | // Both single quotes and double quotes (for attributes) 1925 | return s.replace( /['"<>&]/g, function( s ) { 1926 | switch ( s ) { 1927 | case "'": 1928 | return "'"; 1929 | case "\"": 1930 | return """; 1931 | case "<": 1932 | return "<"; 1933 | case ">": 1934 | return ">"; 1935 | case "&": 1936 | return "&"; 1937 | } 1938 | }); 1939 | } 1940 | 1941 | /** 1942 | * @param {HTMLElement} elem 1943 | * @param {string} type 1944 | * @param {Function} fn 1945 | */ 1946 | function addEvent( elem, type, fn ) { 1947 | if ( elem.addEventListener ) { 1948 | 1949 | // Standards-based browsers 1950 | elem.addEventListener( type, fn, false ); 1951 | } else if ( elem.attachEvent ) { 1952 | 1953 | // support: IE <9 1954 | elem.attachEvent( "on" + type, fn ); 1955 | } 1956 | } 1957 | 1958 | /** 1959 | * @param {Array|NodeList} elems 1960 | * @param {string} type 1961 | * @param {Function} fn 1962 | */ 1963 | function addEvents( elems, type, fn ) { 1964 | var i = elems.length; 1965 | while ( i-- ) { 1966 | addEvent( elems[ i ], type, fn ); 1967 | } 1968 | } 1969 | 1970 | function hasClass( elem, name ) { 1971 | return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; 1972 | } 1973 | 1974 | function addClass( elem, name ) { 1975 | if ( !hasClass( elem, name ) ) { 1976 | elem.className += ( elem.className ? " " : "" ) + name; 1977 | } 1978 | } 1979 | 1980 | function toggleClass( elem, name ) { 1981 | if ( hasClass( elem, name ) ) { 1982 | removeClass( elem, name ); 1983 | } else { 1984 | addClass( elem, name ); 1985 | } 1986 | } 1987 | 1988 | function removeClass( elem, name ) { 1989 | var set = " " + elem.className + " "; 1990 | 1991 | // Class name may appear multiple times 1992 | while ( set.indexOf( " " + name + " " ) >= 0 ) { 1993 | set = set.replace( " " + name + " ", " " ); 1994 | } 1995 | 1996 | // trim for prettiness 1997 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); 1998 | } 1999 | 2000 | function id( name ) { 2001 | return defined.document && document.getElementById && document.getElementById( name ); 2002 | } 2003 | 2004 | function getUrlConfigHtml() { 2005 | var i, j, val, 2006 | escaped, escapedTooltip, 2007 | selection = false, 2008 | len = config.urlConfig.length, 2009 | urlConfigHtml = ""; 2010 | 2011 | for ( i = 0; i < len; i++ ) { 2012 | val = config.urlConfig[ i ]; 2013 | if ( typeof val === "string" ) { 2014 | val = { 2015 | id: val, 2016 | label: val 2017 | }; 2018 | } 2019 | 2020 | escaped = escapeText( val.id ); 2021 | escapedTooltip = escapeText( val.tooltip ); 2022 | 2023 | config[ val.id ] = QUnit.urlParams[ val.id ]; 2024 | if ( !val.value || typeof val.value === "string" ) { 2025 | urlConfigHtml += ""; 2031 | } else { 2032 | urlConfigHtml += ""; 2061 | } 2062 | } 2063 | 2064 | return urlConfigHtml; 2065 | } 2066 | 2067 | function toolbarUrlConfigContainer() { 2068 | var urlConfigContainer = document.createElement( "span" ); 2069 | 2070 | urlConfigContainer.innerHTML = getUrlConfigHtml(); 2071 | 2072 | // For oldIE support: 2073 | // * Add handlers to the individual elements instead of the container 2074 | // * Use "click" instead of "change" for checkboxes 2075 | // * Fallback from event.target to event.srcElement 2076 | addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", function( event ) { 2077 | var params = {}, 2078 | target = event.target || event.srcElement; 2079 | params[ target.name ] = target.checked ? 2080 | target.defaultValue || true : 2081 | undefined; 2082 | window.location = QUnit.url( params ); 2083 | }); 2084 | addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", function( event ) { 2085 | var params = {}, 2086 | target = event.target || event.srcElement; 2087 | params[ target.name ] = target.options[ target.selectedIndex ].value || undefined; 2088 | window.location = QUnit.url( params ); 2089 | }); 2090 | 2091 | return urlConfigContainer; 2092 | } 2093 | 2094 | function getModuleNames() { 2095 | var i, 2096 | moduleNames = []; 2097 | 2098 | for ( i in config.modules ) { 2099 | if ( config.modules.hasOwnProperty( i ) ) { 2100 | moduleNames.push( i ); 2101 | } 2102 | } 2103 | 2104 | moduleNames.sort(function( a, b ) { 2105 | return a.localeCompare( b ); 2106 | }); 2107 | 2108 | return moduleNames; 2109 | } 2110 | 2111 | function toolbarModuleFilterHtml() { 2112 | var i, 2113 | moduleFilterHtml = "", 2114 | moduleNames = getModuleNames(); 2115 | 2116 | if ( moduleNames.length <= 1 ) { 2117 | return false; 2118 | } 2119 | 2120 | moduleFilterHtml += "" + 2121 | ""; 2132 | 2133 | return moduleFilterHtml; 2134 | } 2135 | 2136 | function toolbarModuleFilter() { 2137 | var moduleFilter = document.createElement( "span" ), 2138 | moduleFilterHtml = toolbarModuleFilterHtml(); 2139 | 2140 | if ( !moduleFilterHtml ) { 2141 | return false; 2142 | } 2143 | 2144 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 2145 | moduleFilter.innerHTML = moduleFilterHtml; 2146 | 2147 | addEvent( moduleFilter.lastChild, "change", function() { 2148 | var selectBox = moduleFilter.getElementsByTagName( "select" )[ 0 ], 2149 | selectedModule = decodeURIComponent( selectBox.options[ selectBox.selectedIndex ].value ); 2150 | 2151 | window.location = QUnit.url({ 2152 | module: ( selectedModule === "" ) ? undefined : selectedModule, 2153 | 2154 | // Remove any existing filters 2155 | filter: undefined, 2156 | testNumber: undefined 2157 | }); 2158 | }); 2159 | 2160 | return moduleFilter; 2161 | } 2162 | 2163 | function toolbarFilter() { 2164 | var testList = id( "qunit-tests" ), 2165 | filter = document.createElement( "input" ); 2166 | 2167 | filter.type = "checkbox"; 2168 | filter.id = "qunit-filter-pass"; 2169 | 2170 | addEvent( filter, "click", function() { 2171 | if ( filter.checked ) { 2172 | addClass( testList, "hidepass" ); 2173 | if ( defined.sessionStorage ) { 2174 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 2175 | } 2176 | } else { 2177 | removeClass( testList, "hidepass" ); 2178 | if ( defined.sessionStorage ) { 2179 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 2180 | } 2181 | } 2182 | }); 2183 | 2184 | if ( config.hidepassed || defined.sessionStorage && 2185 | sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 2186 | filter.checked = true; 2187 | 2188 | addClass( testList, "hidepass" ); 2189 | } 2190 | 2191 | return filter; 2192 | } 2193 | 2194 | function toolbarLabel() { 2195 | var label = document.createElement( "label" ); 2196 | label.setAttribute( "for", "qunit-filter-pass" ); 2197 | label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); 2198 | label.innerHTML = "Hide passed tests"; 2199 | 2200 | return label; 2201 | } 2202 | 2203 | function appendToolbar() { 2204 | var moduleFilter, 2205 | toolbar = id( "qunit-testrunner-toolbar" ); 2206 | 2207 | if ( toolbar ) { 2208 | toolbar.appendChild( toolbarFilter() ); 2209 | toolbar.appendChild( toolbarLabel() ); 2210 | toolbar.appendChild( toolbarUrlConfigContainer() ); 2211 | 2212 | moduleFilter = toolbarModuleFilter(); 2213 | if ( moduleFilter ) { 2214 | toolbar.appendChild( moduleFilter ); 2215 | } 2216 | } 2217 | } 2218 | 2219 | function appendBanner() { 2220 | var banner = id( "qunit-banner" ); 2221 | 2222 | if ( banner ) { 2223 | banner.className = ""; 2224 | banner.innerHTML = "" + banner.innerHTML + " "; 2227 | } 2228 | } 2229 | 2230 | function appendTestResults() { 2231 | var tests = id( "qunit-tests" ), 2232 | result = id( "qunit-testresult" ); 2233 | 2234 | if ( result ) { 2235 | result.parentNode.removeChild( result ); 2236 | } 2237 | 2238 | if ( tests ) { 2239 | tests.innerHTML = ""; 2240 | result = document.createElement( "p" ); 2241 | result.id = "qunit-testresult"; 2242 | result.className = "result"; 2243 | tests.parentNode.insertBefore( result, tests ); 2244 | result.innerHTML = "Running...
     "; 2245 | } 2246 | } 2247 | 2248 | function storeFixture() { 2249 | var fixture = id( "qunit-fixture" ); 2250 | if ( fixture ) { 2251 | config.fixture = fixture.innerHTML; 2252 | } 2253 | } 2254 | 2255 | function appendUserAgent() { 2256 | var userAgent = id( "qunit-userAgent" ); 2257 | if ( userAgent ) { 2258 | userAgent.innerHTML = navigator.userAgent; 2259 | } 2260 | } 2261 | 2262 | // HTML Reporter initialization and load 2263 | QUnit.begin(function() { 2264 | var qunit = id( "qunit" ); 2265 | 2266 | if ( qunit ) { 2267 | qunit.innerHTML = 2268 | "

    " + escapeText( document.title ) + "

    " + 2269 | "

    " + 2270 | "
    " + 2271 | "

    " + 2272 | "
      "; 2273 | } 2274 | 2275 | appendBanner(); 2276 | appendTestResults(); 2277 | appendUserAgent(); 2278 | appendToolbar(); 2279 | storeFixture(); 2280 | }); 2281 | 2282 | QUnit.done(function( details ) { 2283 | var i, key, 2284 | banner = id( "qunit-banner" ), 2285 | tests = id( "qunit-tests" ), 2286 | html = [ 2287 | "Tests completed in ", 2288 | details.runtime, 2289 | " milliseconds.
      ", 2290 | "", 2291 | details.passed, 2292 | " assertions of ", 2293 | details.total, 2294 | " passed, ", 2295 | details.failed, 2296 | " failed." 2297 | ].join( "" ); 2298 | 2299 | if ( banner ) { 2300 | banner.className = details.failed ? "qunit-fail" : "qunit-pass"; 2301 | } 2302 | 2303 | if ( tests ) { 2304 | id( "qunit-testresult" ).innerHTML = html; 2305 | } 2306 | 2307 | if ( config.altertitle && defined.document && document.title ) { 2308 | 2309 | // show ✖ for good, ✔ for bad suite result in title 2310 | // use escape sequences in case file gets loaded with non-utf-8-charset 2311 | document.title = [ 2312 | ( details.failed ? "\u2716" : "\u2714" ), 2313 | document.title.replace( /^[\u2714\u2716] /i, "" ) 2314 | ].join( " " ); 2315 | } 2316 | 2317 | // clear own sessionStorage items if all tests passed 2318 | if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { 2319 | for ( i = 0; i < sessionStorage.length; i++ ) { 2320 | key = sessionStorage.key( i++ ); 2321 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 2322 | sessionStorage.removeItem( key ); 2323 | } 2324 | } 2325 | } 2326 | 2327 | // scroll back to top to show results 2328 | if ( config.scrolltop && window.scrollTo ) { 2329 | window.scrollTo( 0, 0 ); 2330 | } 2331 | }); 2332 | 2333 | function getNameHtml( name, module ) { 2334 | var nameHtml = ""; 2335 | 2336 | if ( module ) { 2337 | nameHtml = "" + escapeText( module ) + ": "; 2338 | } 2339 | 2340 | nameHtml += "" + escapeText( name ) + ""; 2341 | 2342 | return nameHtml; 2343 | } 2344 | 2345 | QUnit.testStart(function( details ) { 2346 | var a, b, li, running, assertList, 2347 | name = getNameHtml( details.name, details.module ), 2348 | tests = id( "qunit-tests" ); 2349 | 2350 | if ( tests ) { 2351 | b = document.createElement( "strong" ); 2352 | b.innerHTML = name; 2353 | 2354 | a = document.createElement( "a" ); 2355 | a.innerHTML = "Rerun"; 2356 | a.href = QUnit.url({ testNumber: details.testNumber }); 2357 | 2358 | li = document.createElement( "li" ); 2359 | li.appendChild( b ); 2360 | li.appendChild( a ); 2361 | li.className = "running"; 2362 | li.id = "qunit-test-output" + details.testNumber; 2363 | 2364 | assertList = document.createElement( "ol" ); 2365 | assertList.className = "qunit-assert-list"; 2366 | 2367 | li.appendChild( assertList ); 2368 | 2369 | tests.appendChild( li ); 2370 | } 2371 | 2372 | running = id( "qunit-testresult" ); 2373 | if ( running ) { 2374 | running.innerHTML = "Running:
      " + name; 2375 | } 2376 | 2377 | }); 2378 | 2379 | QUnit.log(function( details ) { 2380 | var assertList, assertLi, 2381 | message, expected, actual, 2382 | testItem = id( "qunit-test-output" + details.testNumber ); 2383 | 2384 | if ( !testItem ) { 2385 | return; 2386 | } 2387 | 2388 | message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); 2389 | message = "" + message + ""; 2390 | 2391 | // pushFailure doesn't provide details.expected 2392 | // when it calls, it's implicit to also not show expected and diff stuff 2393 | // Also, we need to check details.expected existence, as it can exist and be undefined 2394 | if ( !details.result && hasOwn.call( details, "expected" ) ) { 2395 | expected = escapeText( QUnit.dump.parse( details.expected ) ); 2396 | actual = escapeText( QUnit.dump.parse( details.actual ) ); 2397 | message += ""; 2400 | 2401 | if ( actual !== expected ) { 2402 | message += "" + 2404 | ""; 2406 | } 2407 | 2408 | if ( details.source ) { 2409 | message += ""; 2411 | } 2412 | 2413 | message += "
      Expected:
      " +
      2398 | 			expected +
      2399 | 			"
      Result:
      " +
      2403 | 				actual + "
      Diff:
      " +
      2405 | 				QUnit.diff( expected, actual ) + "
      Source:
      " +
      2410 | 				escapeText( details.source ) + "
      "; 2414 | 2415 | // this occours when pushFailure is set and we have an extracted stack trace 2416 | } else if ( !details.result && details.source ) { 2417 | message += "" + 2418 | "" + 2420 | "
      Source:
      " +
      2419 | 			escapeText( details.source ) + "
      "; 2421 | } 2422 | 2423 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 2424 | 2425 | assertLi = document.createElement( "li" ); 2426 | assertLi.className = details.result ? "pass" : "fail"; 2427 | assertLi.innerHTML = message; 2428 | assertList.appendChild( assertLi ); 2429 | }); 2430 | 2431 | QUnit.testDone(function( details ) { 2432 | var testTitle, time, testItem, assertList, 2433 | good, bad, testCounts, 2434 | tests = id( "qunit-tests" ); 2435 | 2436 | // QUnit.reset() is deprecated and will be replaced for a new 2437 | // fixture reset function on QUnit 2.0/2.1. 2438 | // It's still called here for backwards compatibility handling 2439 | QUnit.reset(); 2440 | 2441 | if ( !tests ) { 2442 | return; 2443 | } 2444 | 2445 | testItem = id( "qunit-test-output" + details.testNumber ); 2446 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 2447 | 2448 | good = details.passed; 2449 | bad = details.failed; 2450 | 2451 | // store result when possible 2452 | if ( config.reorder && defined.sessionStorage ) { 2453 | if ( bad ) { 2454 | sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); 2455 | } else { 2456 | sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); 2457 | } 2458 | } 2459 | 2460 | if ( bad === 0 ) { 2461 | addClass( assertList, "qunit-collapsed" ); 2462 | } 2463 | 2464 | // testItem.firstChild is the test name 2465 | testTitle = testItem.firstChild; 2466 | 2467 | testCounts = bad ? 2468 | "" + bad + ", " + "" + good + ", " : 2469 | ""; 2470 | 2471 | testTitle.innerHTML += " (" + testCounts + 2472 | details.assertions.length + ")"; 2473 | 2474 | addEvent( testTitle, "click", function() { 2475 | toggleClass( assertList, "qunit-collapsed" ); 2476 | }); 2477 | 2478 | time = document.createElement( "span" ); 2479 | time.className = "runtime"; 2480 | time.innerHTML = details.runtime + " ms"; 2481 | 2482 | testItem.className = bad ? "fail" : "pass"; 2483 | 2484 | testItem.insertBefore( time, assertList ); 2485 | }); 2486 | 2487 | if ( !defined.document || document.readyState === "complete" ) { 2488 | config.autorun = true; 2489 | } 2490 | 2491 | if ( defined.document ) { 2492 | addEvent( window, "load", QUnit.load ); 2493 | } 2494 | 2495 | })(); 2496 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | QUnit.config.reorder = false; 3 | var SERVER = 'http://data.madebymark.nl/cordova-file-cache/'; 4 | 5 | /*************************************/ 6 | QUnit.module('CordovaFileCache'); 7 | /*************************************/ 8 | var cache = window.cache = null; 9 | var fs; 10 | 11 | QUnit.asyncTest('cache = new CordovaFileCache(...)',function(assert){ 12 | fs = new CordovaPromiseFS({ 13 | persistent: 0, 14 | Promise:Promise 15 | }); 16 | 17 | cache = new CordovaFileCache({ 18 | fs:fs, 19 | localRoot: '/cache-test', 20 | serverRoot: SERVER, 21 | retry: [0,0] 22 | }); 23 | 24 | cache.ready.then(ok(assert,truthy),err(assert)); 25 | }); 26 | 27 | QUnit.test('normalize "localRoot" to "cache-test/"',function(assert){ 28 | assert.equal(cache.localRoot,'cache-test/'); 29 | }); 30 | 31 | QUnit.test('normalize "serverRoot" to end with "/"',function(assert){ 32 | assert.equal(cache.serverRoot[cache.serverRoot.length-1],'/'); 33 | }); 34 | 35 | QUnit.asyncTest('"localRoot" exists',function(assert){ 36 | fs.list(cache.localRoot,'rfd').then(ok(assert,truthy),err(assert)); 37 | }); 38 | 39 | /*************************************/ 40 | // Methods that actually do stuff 41 | /*************************************/ 42 | 43 | //clear 44 | QUnit.asyncTest('clear: leaves an empty but existing directory',function(assert){ 45 | cache.ready.then(function(){ 46 | return cache.clear(); 47 | }) 48 | .then(function(){ 49 | return fs.list(cache.localRoot,'rfd'); 50 | }) 51 | .then(function(list){ 52 | assert.equal(list.length,0); 53 | QUnit.start(); 54 | },err(assert)); 55 | }); 56 | 57 | //list 58 | QUnit.asyncTest('list: should be empty after clear',function(assert){ 59 | cache.list() 60 | .then(function(list){ 61 | assert.equal(list.length,0); 62 | QUnit.start(); 63 | },err(assert)); 64 | }); 65 | 66 | //add 67 | var file1 = 'file1.txt'; 68 | var file2 = 'file2.txt'; 69 | var file3 = 'file3.txt'; 70 | var server_file1 = SERVER + file1; 71 | var server_file2 = SERVER + file2; 72 | var server_file3 = SERVER + file3; 73 | 74 | 75 | QUnit.test('add single file (with server url)',function(assert){ 76 | var dirty = cache.add(server_file1); 77 | assert.ok(dirty,'should be dirty'); 78 | assert.deepEqual(cache.getAdded(),[server_file1]); 79 | }); 80 | 81 | QUnit.test('add single file (without server url)',function(assert){ 82 | var dirty = cache.add(file1); 83 | assert.ok(dirty,'should be dirty'); 84 | assert.deepEqual(cache.getAdded(),[server_file1]); 85 | }); 86 | 87 | QUnit.test("don't add duplicates",function(assert){ 88 | var dirty = cache.add([server_file1,server_file1]); 89 | assert.ok(dirty,'should be dirty'); 90 | assert.deepEqual(cache.getAdded(),[server_file1]); 91 | }); 92 | 93 | QUnit.test('add array of files (with server url)',function(assert){ 94 | var dirty = cache.add([server_file2,server_file3]); 95 | assert.ok(dirty,'should be dirty'); 96 | assert.deepEqual(cache.getAdded(),[server_file1,server_file2,server_file3]); 97 | }); 98 | 99 | //remove 100 | QUnit.test('remove single file',function(assert){ 101 | cache.remove(file1); 102 | assert.deepEqual(cache.getAdded(),[server_file2,server_file3]); 103 | }); 104 | 105 | QUnit.test('remove array of files',function(assert){ 106 | cache.remove([file2]); 107 | assert.deepEqual(cache.getAdded(),[server_file3]); 108 | }); 109 | 110 | //download 111 | QUnit.asyncTest('download queue is correct',function(assert){ 112 | cache.add([file1,file2,file3]); 113 | fs.write('cache-test/file1.txt','stuff') 114 | .then(function(){ 115 | return cache.list(); 116 | }) 117 | .then(function(){ 118 | assert.equal(cache.isCached(file1),true,'file1 is cached (without server url)'); 119 | assert.equal(cache.isCached(server_file1),true,'file1 is cached (with server url)'); 120 | assert.deepEqual(cache.getDownloadQueue(),[server_file3,server_file2],'downloadQueue has now 2 and 3'); 121 | QUnit.start(); 122 | },err(assert)); 123 | }); 124 | 125 | QUnit.asyncTest('download',function(assert){ 126 | cache.download() 127 | .then(function(x){ 128 | assert.deepEqual(x,cache,'download returns cache'); 129 | assert.equal(cache.isCached(file2),true,'file2 is cached'); 130 | assert.equal(cache.isCached(file3),true,'file3 is cached'); 131 | return fs.list(cache.localRoot,'rf'); 132 | }) 133 | .then(function(list){ 134 | assert.deepEqual(list,['/cache-test/file3.txt','/cache-test/file2.txt','/cache-test/file1.txt'],'files are really there!'); 135 | QUnit.start(); 136 | },err(assert)); 137 | // assert.equal(cache._downloading.length,2); 138 | }); 139 | 140 | QUnit.asyncTest('download with invalid files (rejects with failed files)',function(assert){ 141 | var url = SERVER+'does-not-exist.txt'; 142 | cache.add(url); 143 | cache.clear() 144 | .then(function(){ 145 | return cache.download(); 146 | }) 147 | .then(err(assert),function(errors){ 148 | assert.deepEqual(cache.getDownloadQueue(),[url],'downloadQueue has failed url'); 149 | assert.deepEqual(errors,[url],'returns failed urls'); 150 | QUnit.start(); 151 | }); 152 | }); 153 | 154 | QUnit.test('isDirty() returns if downloadQueue has downloads',function(assert){ 155 | assert.equal(cache.isDirty(),true); 156 | cache.remove(SERVER+'does-not-exist.txt'); 157 | assert.equal(cache.isDirty(),false); 158 | }); 159 | 160 | //abort 161 | 162 | 163 | /*************************************/ 164 | // Getters 165 | /*************************************/ 166 | //toInternalURL 167 | QUnit.test('toInternalURL',function(assert){ 168 | var expected = fs.toInternalURLSync('cache-test/'+file1); 169 | assert.equal(cache.toInternalURL(file1),expected,'handles urls without server'); 170 | assert.equal(cache.toInternalURL(server_file1),expected,'handles path without server'); 171 | assert.equal(cache.toInternalURL('nonsense'),'nonsense','returns input if not cached'); 172 | }); 173 | 174 | //get 175 | QUnit.test('get (cached file) returns internal url',function(assert){ 176 | var expected = fs.toInternalURLSync('cache-test/'+file1); 177 | assert.equal(cache.get(file1),expected); 178 | assert.equal(cache.get(server_file1),expected); 179 | }); 180 | 181 | QUnit.test('get (uncached file) return server url',function(assert){ 182 | var expected = SERVER + 'does-not-exist.txt'; 183 | assert.equal(cache.get('does-not-exist.txt'),expected); 184 | assert.equal(cache.get(expected),expected); 185 | }); 186 | 187 | //toDataURL 188 | QUnit.asyncTest('toDataURL',function(assert){ 189 | cache.toDataURL(file1) 190 | .then(ok(assert,'data:text/plain;base64,aGVsbG8gd29ybGQ='),err(assert)); 191 | }); 192 | 193 | //toURL 194 | QUnit.test('toURL',function(assert){ 195 | var expected = 'file'; 196 | assert.equal(cache.toURL(file1).substr(0,4),expected); 197 | assert.equal(cache.toURL(server_file1).substr(0,4),expected); 198 | assert.equal(cache.toURL('nonsense'),'nonsense','returns input if not cached'); 199 | }); 200 | 201 | //toServerURL 202 | QUnit.test('toServerURL',function(assert){ 203 | var expected = SERVER + 'test.txt'; 204 | assert.equal(cache.toServerURL('test.txt'),expected); 205 | assert.equal(cache.toServerURL(expected),expected); 206 | }); 207 | 208 | //toPath 209 | QUnit.test('toPath',function(assert){ 210 | var expected = 'cache-test/ok.txt'; 211 | assert.equal(cache.toPath('ok.txt'),expected); 212 | assert.equal(cache.toPath('/ok.txt'),expected); 213 | assert.equal(cache.toPath(SERVER+'ok.txt'),expected); 214 | }); 215 | 216 | 217 | /*************************************/ 218 | // Helpers 219 | /*************************************/ 220 | //getDownloadQueue 221 | //getAdded 222 | //isDirty 223 | //isCached 224 | 225 | //clear 226 | QUnit.asyncTest('clear: leaves an empty but existing directory',function(assert){ 227 | cache.ready.then(function(){ 228 | return cache.clear(); 229 | }) 230 | .then(function(){ 231 | return fs.list(cache.localRoot,'rfd'); 232 | }) 233 | .then(function(list){ 234 | assert.equal(list.length,0); 235 | QUnit.start(); 236 | },err(assert)); 237 | }); 238 | 239 | /*************************************/ 240 | // Helper methods for promises 241 | var truthy = 'TRUTHY'; 242 | function print(){ 243 | console.log(arguments); 244 | } 245 | function ok(assert,expected){ 246 | return function(actual){ 247 | if(expected === truthy){ 248 | assert.ok(actual); 249 | } else { 250 | assert.equal(actual,expected); 251 | } 252 | QUnit.start(); 253 | }; 254 | } 255 | 256 | function err(assert){ 257 | return function(err){ 258 | assert.equal(err,'unexpeced promise callback(resolve or reject)'); 259 | QUnit.start(); 260 | }; 261 | } 262 | })(); --------------------------------------------------------------------------------