├── .gitignore ├── .travis.yml ├── libs ├── jquery-loader.js └── qunit │ ├── qunit.css │ └── qunit.js ├── bower.json ├── package.json ├── LICENSE-MIT ├── test ├── waitforimages.html └── jquery.waitforimages_test.js ├── dist ├── jquery.waitforimages.min.js └── jquery.waitforimages.js ├── Gruntfile.js ├── CONTRIBUTING.md ├── README.md └── src └── jquery.waitforimages.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | bower_components/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - node 5 | - lts/* 6 | - 8 7 | - 6 8 | - 4 9 | install: 10 | - npm i -g npm@latest 11 | - npm install 12 | deploy: 13 | provider: npm 14 | email: alex@alexanderdickson.com 15 | api_key: 16 | secure: Tjp9eJJqvvN0DHtdk9NKCKH6mq+0rang1tAhtUWoxUQ4TZySVFR9NT3PzuqHIgxTY7wR8/zquyc4i9yXBv2Wilp1U1fPZrZpkEg62y+NlFMdikb6sjuL/QoTS6LFYwirE/P0MJiXPk1lTxUkE6emHxgomWlmYClnVRst3hq4Xy4= 17 | -------------------------------------------------------------------------------- /libs/jquery-loader.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Get any jquery=___ param from the query string. 3 | var jqversion = location.search.match(/[?&]jquery=(.*?)(?=&|$)/); 4 | var path; 5 | if (jqversion) { 6 | // A version was specified, load that version from code.jquery.com. 7 | path = 'http://code.jquery.com/jquery-' + jqversion[1] + '.js'; 8 | } else { 9 | // No version was specified, load the local version. 10 | path = '../libs/jquery/jquery.js'; 11 | } 12 | // This is the only time I'll ever use document.write, I promise! 13 | document.write(''); 14 | }()); 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waitForImages", 3 | "version": "2.4.0", 4 | "homepage": "https://github.com/alexanderdickson/waitForImages", 5 | "authors": [ 6 | "Alex Dickson " 7 | ], 8 | "description": "Provides callbacks for image loading events", 9 | "main": "src/jquery.waitforimages.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "jquery", 15 | "images" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ], 25 | "dependencies": { 26 | "jquery": ">=1.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "waitForImages jQuery Plugin", 3 | "main": "./dist/jquery.waitforimages.js", 4 | "name": "jquery.waitforimages", 5 | "author": { 6 | "name": "Alex Dickson", 7 | "email": "alex@alexanderdickson.com", 8 | "url": "http://alexanderdickson.com" 9 | }, 10 | "version": "2.4.0", 11 | "homepage": "https://github.com/alexanderdickson/waitForImages", 12 | "bugs": { 13 | "url": "https://github.com/alexanderdickson/waitForImages/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/alexanderdickson/waitForImages" 18 | }, 19 | "license": "MIT", 20 | "scripts": { 21 | "test": "grunt travis --verbose" 22 | }, 23 | "devDependencies": { 24 | "grunt": "^1.0.1", 25 | "grunt-cli": "~1.2.0", 26 | "grunt-contrib-concat": "~1.0.1", 27 | "grunt-contrib-jshint": "~1.1.0", 28 | "grunt-contrib-qunit": "~1.3.0", 29 | "grunt-contrib-uglify": "~2.1.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Alex Dickson 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/waitforimages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | waitForImages Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 |

waitForImages Test Suite

21 |

22 |
23 |

24 |
    25 |
    26 | 27 | 28 | -------------------------------------------------------------------------------- /dist/jquery.waitforimages.min.js: -------------------------------------------------------------------------------- 1 | /*! waitForImages jQuery Plugin 2018-02-13 */ 2 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){var b="waitForImages",c=function(a){return a.srcset&&a.sizes}(new Image);a.waitForImages={hasImageProperties:["backgroundImage","listStyleImage","borderImage","borderCornerImage","cursor"],hasImageAttributes:["srcset"]},a.expr.pseudos["has-src"]=function(b){return a(b).is('img[src][src!=""]')},a.expr.pseudos.uncached=function(b){return!!a(b).is(":has-src")&&!b.complete},a.fn.waitForImages=function(){var d,e,f,g=0,h=0,i=a.Deferred(),j=this,k=[],l=a.waitForImages.hasImageProperties||[],m=a.waitForImages.hasImageAttributes||[],n=/url\(\s*(['"]?)(.*?)\1\s*\)/g;if(a.isPlainObject(arguments[0])?(f=arguments[0].waitForAll,e=arguments[0].each,d=arguments[0].finished):1===arguments.length&&"boolean"===a.type(arguments[0])?f=arguments[0]:(d=arguments[0],e=arguments[1],f=arguments[2]),d=d||a.noop,e=e||a.noop,f=!!f,!a.isFunction(d)||!a.isFunction(e))throw new TypeError("An invalid callback was supplied.");return this.each(function(){var b=a(this);f?b.find("*").addBack().each(function(){var b=a(this);b.is("img:has-src")&&!b.is("[srcset]")&&k.push({src:b.attr("src"),element:b[0]}),a.each(l,function(a,c){var d,e=b.css(c);if(!e)return!0;for(;d=n.exec(e);)k.push({src:d[2],element:b[0]})}),a.each(m,function(a,c){var d=b.attr(c);return!d||void k.push({src:b.attr("src"),srcset:b.attr("srcset"),element:b[0]})})}):b.find("img:has-src").each(function(){k.push({src:this.src,element:this})})}),g=k.length,h=0,0===g&&(d.call(j),i.resolveWith(j)),a.each(k,function(f,k){var l=new Image,m="load."+b+" error."+b;a(l).one(m,function b(c){var f=[h,g,"load"==c.type];if(h++,e.apply(k.element,f),i.notifyWith(k.element,f),a(this).off(m,b),h==g)return d.call(j[0]),i.resolveWith(j[0]),!1}),c&&k.srcset&&(l.srcset=k.srcset,l.sizes=k.sizes),l.src=k.src}),i.promise()}}); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | concat: { 8 | options: { 9 | stripBanners: true, 10 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 11 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 12 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + 13 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 14 | ' Licensed <%= pkg.license %> */\n' 15 | }, 16 | dist: { 17 | src: 'src/<%= pkg.name %>.js', 18 | dest: 'dist/<%= pkg.name %>.js' 19 | } 20 | }, 21 | qunit: { 22 | files: ['test/**/*.html'] 23 | }, 24 | jshint: { 25 | files: ['grunt.js', 'src/**/*.js', 'test/**/*.js'], 26 | options: { 27 | curly: true, 28 | eqeqeq: false, 29 | immed: true, 30 | latedef: true, 31 | newcap: true, 32 | noarg: true, 33 | sub: true, 34 | undef: true, 35 | boss: true, 36 | eqnull: true, 37 | browser: true, 38 | globals: { 39 | jQuery: true, 40 | define: true, 41 | module: true, 42 | require: true 43 | } 44 | }, 45 | }, 46 | uglify: { 47 | options: { 48 | banner: '/*! <%= pkg.title %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' 49 | }, 50 | build: { 51 | src: 'src/<%= pkg.name %>.js', 52 | dest: 'dist/<%= pkg.name %>.min.js' 53 | } 54 | } 55 | }); 56 | 57 | grunt.loadNpmTasks('grunt-contrib-uglify'); 58 | grunt.loadNpmTasks('grunt-contrib-jshint'); 59 | grunt.loadNpmTasks('grunt-contrib-qunit'); 60 | grunt.loadNpmTasks('grunt-contrib-concat'); 61 | 62 | // Default task. 63 | grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); 64 | 65 | // Travis CI task. 66 | grunt.registerTask('travis', ['jshint', 'qunit']); 67 | 68 | }; 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Important notes 4 | Please don't edit files in the `dist` subdirectory as they are generated via grunt. You'll find source code in the `src` subdirectory! 5 | 6 | ### Code style 7 | Regarding code style like indentation and whitespace, **follow the conventions you see used in the source already.** 8 | 9 | ### PhantomJS 10 | While grunt can run the included unit tests via [PhantomJS](http://phantomjs.org/), this shouldn't be considered a substitute for the real thing. Please be sure to test the `test/*.html` unit test file(s) in _actual_ browsers. 11 | 12 | See the [Why does grunt complain that PhantomJS isn't installed?](https://github.com/gruntjs/grunt/blob/master/docs/faq.md#why-does-grunt-complain-that-phantomjs-isnt-installed) guide in the [Grunt FAQ](https://github.com/gruntjs/grunt/blob/master/docs/faq.md) for help with installing or troubleshooting PhantomJS. 13 | 14 | ## Modifying the code 15 | First, ensure that you have the latest [Node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed. 16 | 17 | Test that grunt is installed globally by running `grunt --version` at the command-line. If grunt isn't installed globally, run `npm install -g grunt` to install the latest version. _You may need to run `sudo npm install -g grunt`._ 18 | 19 | _Note that in Windows, you may have to run `grunt.cmd` instead of `grunt`._ 20 | 21 | 1. Fork and clone the repo. 22 | 1. Run `npm install` to install all dependencies (including grunt). 23 | 1. Run `grunt` to grunt this project. 24 | 25 | Assuming that you don't see any red, you're ready to go. Just be sure to run `grunt` after making any changes, to ensure that nothing is broken. 26 | 27 | ## Submitting pull requests 28 | 29 | 1. Create a new branch, please don't work in your `master` branch directly. 30 | 1. Add failing tests for the change you want to make. Run `grunt` to see the tests fail. 31 | 1. Fix stuff. 32 | 1. Run `grunt` to see if the tests pass. Repeat steps 2-4 until done. 33 | 1. Open `test/*.html` unit test file(s) in actual browser to ensure tests pass everywhere. 34 | 1. Update the documentation to reflect any changes. 35 | 1. Push to your fork and submit a pull request. 36 | -------------------------------------------------------------------------------- /libs/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-header label { 58 | display: inline-block; 59 | } 60 | 61 | #qunit-banner { 62 | height: 5px; 63 | } 64 | 65 | #qunit-testrunner-toolbar { 66 | padding: 0.5em 0 0.5em 2em; 67 | color: #5E740B; 68 | background-color: #eee; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2b81af; 74 | color: #fff; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | 79 | /** Tests: Pass/Fail */ 80 | 81 | #qunit-tests { 82 | list-style-position: inside; 83 | } 84 | 85 | #qunit-tests li { 86 | padding: 0.4em 0.5em 0.4em 2.5em; 87 | border-bottom: 1px solid #fff; 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 92 | display: none; 93 | } 94 | 95 | #qunit-tests li strong { 96 | cursor: pointer; 97 | } 98 | 99 | #qunit-tests li a { 100 | padding: 0.5em; 101 | color: #c2ccd1; 102 | text-decoration: none; 103 | } 104 | #qunit-tests li a:hover, 105 | #qunit-tests li a:focus { 106 | color: #000; 107 | } 108 | 109 | #qunit-tests ol { 110 | margin-top: 0.5em; 111 | padding: 0.5em; 112 | 113 | background-color: #fff; 114 | 115 | border-radius: 15px; 116 | -moz-border-radius: 15px; 117 | -webkit-border-radius: 15px; 118 | 119 | box-shadow: inset 0px 2px 13px #999; 120 | -moz-box-shadow: inset 0px 2px 13px #999; 121 | -webkit-box-shadow: inset 0px 2px 13px #999; 122 | } 123 | 124 | #qunit-tests table { 125 | border-collapse: collapse; 126 | margin-top: .2em; 127 | } 128 | 129 | #qunit-tests th { 130 | text-align: right; 131 | vertical-align: top; 132 | padding: 0 .5em 0 0; 133 | } 134 | 135 | #qunit-tests td { 136 | vertical-align: top; 137 | } 138 | 139 | #qunit-tests pre { 140 | margin: 0; 141 | white-space: pre-wrap; 142 | word-wrap: break-word; 143 | } 144 | 145 | #qunit-tests del { 146 | background-color: #e0f2be; 147 | color: #374e0c; 148 | text-decoration: none; 149 | } 150 | 151 | #qunit-tests ins { 152 | background-color: #ffcaca; 153 | color: #500; 154 | text-decoration: none; 155 | } 156 | 157 | /*** Test Counts */ 158 | 159 | #qunit-tests b.counts { color: black; } 160 | #qunit-tests b.passed { color: #5E740B; } 161 | #qunit-tests b.failed { color: #710909; } 162 | 163 | #qunit-tests li li { 164 | margin: 0.5em; 165 | padding: 0.4em 0.5em 0.4em 0.5em; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #5E740B; 175 | background-color: #fff; 176 | border-left: 26px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 26px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 15px 15px; 198 | -moz-border-radius: 0 0 15px 15px; 199 | -webkit-border-bottom-right-radius: 15px; 200 | -webkit-border-bottom-left-radius: 15px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | 224 | /** Fixture */ 225 | 226 | #qunit-fixture { 227 | position: absolute; 228 | top: -10000px; 229 | left: -10000px; 230 | width: 1000px; 231 | height: 1000px; 232 | } 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # waitForImages 2 | 3 | Copyright (c) 2011-2018 Alexander Dickson [@alexdickson](http://twitter.com/alexdickson) 4 | 5 | Licensed under the [MIT licenses](https://raw.github.com/alexanderdickson/waitForImages/master/LICENSE-MIT). 6 | 7 | [http://alexanderdickson.com](http://alexanderdickson.com) 8 | 9 | [Donate!](http://paypal.me/alexwilliamdickson) 10 | 11 | 12 | [![Build Status](https://secure.travis-ci.org/alexanderdickson/waitForImages.svg)](http://travis-ci.org/alexanderdickson/waitForImages) 13 | 14 | ## Overview 15 | 16 | Provides useful callbacks once descendant images have loaded. 17 | 18 | waitForImages also supports both images referenced in CSS, such as the `background-image` property, and images referenced in element attributes such as `srcset`. Images referenced in attributes can also be a comma-separated list of images. 19 | 20 | It can be useful when WebKit incorrectly reports element dimensions/offsets on document ready, because it has not calculated their descendant `img` dimensions yet. 21 | 22 | Supports all browsers you probably care about. 23 | 24 | ## Get it 25 | 26 | You can either grab the source yourself... 27 | 28 | - [Production (minified)](https://raw.github.com/alexanderdickson/waitForImages/master/dist/jquery.waitforimages.min.js) 29 | - [Development (unminified)](https://raw.github.com/alexanderdickson/waitForImages/master/dist/jquery.waitforimages.js) 30 | 31 | ...or you can use a hosted version... 32 | 33 | - [Hosted on CDNJS (minified)](http://cdnjs.cloudflare.com/ajax/libs/jquery.waitforimages/1.5.0/jquery.waitforimages.min.js) 34 | 35 | Alternatively, you can install with [`bower`](http://bower.io/)... 36 | 37 | ```bash 38 | bower install waitForImages 39 | ``` 40 | 41 | ...or [`npm`](https://www.npmjs.com/)... 42 | 43 | ```bash 44 | npm install jquery.waitforimages 45 | ``` 46 | 47 | Of course, these need to be loaded after `jQuery` is made available. The current version should be supported by at least jQuery 1.8, or perhaps earlier. If you find incompatibility issues, please check out a previous tagged version. 48 | 49 | ## Usage 50 | 51 | There are two ways to use waitForImages: with a standard callback system (previously the only API) or receiving a promise. 52 | 53 | ### Standard 54 | 55 | Just provide a callback function and it will be called once all descendant images have loaded. 56 | 57 | ```javascript 58 | $('selector').waitForImages(function() { 59 | // All descendant images have loaded, now slide up. 60 | $(this).slideUp(); 61 | }); 62 | ``` 63 | 64 | You can also use the jQuery promise API. 65 | 66 | ```javascript 67 | $('selector').waitForImages().done(function() { 68 | // All descendant images have loaded, now slide up. 69 | $(this).slideUp(); 70 | }); 71 | ``` 72 | 73 | In the callbacks, `this` is a reference to the collection that `waitForImages()` was called on. 74 | 75 | ### Advanced 76 | 77 | You can pass a second function as a callback that will be called for each image that is loaded, with some information passed as arguments. 78 | 79 | ```javascript 80 | $('selector').waitForImages(function() { 81 | alert('All images have loaded.'); 82 | }, function(loaded, count, success) { 83 | alert(loaded + ' of ' + count + ' images has ' + (success ? 'loaded' : 'failed to load') + '.'); 84 | $(this).addClass('loaded'); 85 | }); 86 | ``` 87 | 88 | Using the jQuery promises API, you can then use the `progress()` method to know when an individual image has been loaded. 89 | 90 | ```javascript 91 | $('selector').waitForImages().progress(function(loaded, count, success) { 92 | alert(loaded + ' of ' + count + ' images has ' + (success ? 'loaded' : 'failed to load') + '.'); 93 | $(this).addClass('loaded'); 94 | }); 95 | ``` 96 | 97 | You can also set the third argument to `true` if you'd like the plugin to iterate over the collection and all descendent elements, checking for images referenced in the CSS (by default, it looks at the `background-image`, `list-style-image`, `border-image`, `border-corner-image` and `cursor` properties). If it finds any, they will be treated as a descendant image. 98 | 99 | The callback will be called on the successful **and** unsuccessful loading of the image. Check the third argument to determine the success of the image load. It will be `true` if the image loaded successfully. 100 | 101 | If you want to skip the first argument, pass `$.noop` or alternatively, pass an object literal to the plugin, instead of the arguments individually. 102 | 103 | ```javascript 104 | $('selector').waitForImages({ 105 | finished: function() { 106 | // ... 107 | }, 108 | each: function() { 109 | // ... 110 | }, 111 | waitForAll: true 112 | }); 113 | ``` 114 | 115 | To use this with the promise API, simply pass one argument, which is `waitForAll`. 116 | 117 | ```javascript 118 | $('selector').waitForImages(true).done(function() { 119 | // ... 120 | }); 121 | ``` 122 | 123 | You may also set the CSS properties that possibly contain image references yourself. Just assign an array of properties to the plugin. 124 | 125 | ```javascript 126 | $.waitForImages.hasImgProperties = ['backgroundImage']; 127 | ``` 128 | 129 | waitForImages also exposes two custom selectors, `img:has-src` and `img:uncached`, (both used in conjunction with the `img` selector), which allow you to select `img` elements with a valid `src` attribute or that are not already cached already by the browser, respectively. 130 | 131 | ```javascript 132 | $('img').not(':has-src').remove(); 133 | $('img:uncached').attr('title', 'Loading Image'); 134 | ``` 135 | 136 | ## Feedback 137 | 138 | Please use the [Issues](https://github.com/alexanderdickson/waitForImages/issues) for any bugs, feature requests, etc. 139 | 140 | If you're having problems using the plugin, [ask a question on Stack Overflow](http://stackoverflow.com/questions/tagged/waitforimages). 141 | -------------------------------------------------------------------------------- /src/jquery.waitforimages.js: -------------------------------------------------------------------------------- 1 | ;(function (factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(['jquery'], factory); 5 | } else if (typeof exports === 'object') { 6 | // CommonJS / nodejs module 7 | module.exports = factory(require('jquery')); 8 | } else { 9 | // Browser globals 10 | factory(jQuery); 11 | } 12 | }(function ($) { 13 | // Namespace all events. 14 | var eventNamespace = 'waitForImages'; 15 | 16 | // Is srcset supported by this browser? 17 | var hasSrcset = (function(img) { 18 | return img.srcset && img.sizes; 19 | })(new Image()); 20 | 21 | // CSS properties which contain references to images. 22 | $.waitForImages = { 23 | hasImageProperties: [ 24 | 'backgroundImage', 25 | 'listStyleImage', 26 | 'borderImage', 27 | 'borderCornerImage', 28 | 'cursor' 29 | ], 30 | hasImageAttributes: ['srcset'] 31 | }; 32 | 33 | // Custom selector to find all `img` elements with a valid `src` attribute. 34 | $.expr.pseudos['has-src'] = function (obj) { 35 | // Ensure we are dealing with an `img` element with a valid 36 | // `src` attribute. 37 | return $(obj).is('img[src][src!=""]'); 38 | }; 39 | 40 | // Custom selector to find images which are not already cached by the 41 | // browser. 42 | $.expr.pseudos.uncached = function (obj) { 43 | // Ensure we are dealing with an `img` element with a valid 44 | // `src` attribute. 45 | if (!$(obj).is(':has-src')) { 46 | return false; 47 | } 48 | 49 | return !obj.complete; 50 | }; 51 | 52 | $.fn.waitForImages = function () { 53 | 54 | var allImgsLength = 0; 55 | var allImgsLoaded = 0; 56 | var deferred = $.Deferred(); 57 | var originalCollection = this; 58 | var allImgs = []; 59 | 60 | // CSS properties which may contain an image. 61 | var hasImgProperties = $.waitForImages.hasImageProperties || []; 62 | // Element attributes which may contain an image. 63 | var hasImageAttributes = $.waitForImages.hasImageAttributes || []; 64 | // To match `url()` references. 65 | // Spec: http://www.w3.org/TR/CSS2/syndata.html#value-def-uri 66 | var matchUrl = /url\(\s*(['"]?)(.*?)\1\s*\)/g; 67 | 68 | var finishedCallback; 69 | var eachCallback; 70 | var waitForAll; 71 | 72 | // Handle options object (if passed). 73 | if ($.isPlainObject(arguments[0])) { 74 | 75 | waitForAll = arguments[0].waitForAll; 76 | eachCallback = arguments[0].each; 77 | finishedCallback = arguments[0].finished; 78 | 79 | } else { 80 | 81 | // Handle if using deferred object and only one param was passed in. 82 | if (arguments.length === 1 && $.type(arguments[0]) === 'boolean') { 83 | waitForAll = arguments[0]; 84 | } else { 85 | finishedCallback = arguments[0]; 86 | eachCallback = arguments[1]; 87 | waitForAll = arguments[2]; 88 | } 89 | 90 | } 91 | 92 | // Handle missing callbacks. 93 | finishedCallback = finishedCallback || $.noop; 94 | eachCallback = eachCallback || $.noop; 95 | 96 | // Convert waitForAll to Boolean. 97 | waitForAll = !! waitForAll; 98 | 99 | // Ensure callbacks are functions. 100 | if (!$.isFunction(finishedCallback) || !$.isFunction(eachCallback)) { 101 | throw new TypeError('An invalid callback was supplied.'); 102 | } 103 | 104 | this.each(function () { 105 | // Build a list of all imgs, dependent on what images will 106 | // be considered. 107 | var obj = $(this); 108 | 109 | if (waitForAll) { 110 | 111 | // Get all elements (including the original), as any one of 112 | // them could have a background image. 113 | obj.find('*').addBack().each(function () { 114 | var element = $(this); 115 | 116 | // If an `img` element, add it. But keep iterating in 117 | // case it has a background image too. 118 | if (element.is('img:has-src') && 119 | !element.is('[srcset]')) { 120 | allImgs.push({ 121 | src: element.attr('src'), 122 | element: element[0] 123 | }); 124 | } 125 | 126 | $.each(hasImgProperties, function (i, property) { 127 | var propertyValue = element.css(property); 128 | var match; 129 | 130 | // If it doesn't contain this property, skip. 131 | if (!propertyValue) { 132 | return true; 133 | } 134 | 135 | // Get all url() of this element. 136 | while (match = matchUrl.exec(propertyValue)) { 137 | allImgs.push({ 138 | src: match[2], 139 | element: element[0] 140 | }); 141 | } 142 | }); 143 | 144 | $.each(hasImageAttributes, function (i, attribute) { 145 | var attributeValue = element.attr(attribute); 146 | var attributeValues; 147 | 148 | // If it doesn't contain this property, skip. 149 | if (!attributeValue) { 150 | return true; 151 | } 152 | 153 | allImgs.push({ 154 | src: element.attr('src'), 155 | srcset: element.attr('srcset'), 156 | element: element[0] 157 | }); 158 | }); 159 | }); 160 | } else { 161 | // For images only, the task is simpler. 162 | obj.find('img:has-src') 163 | .each(function () { 164 | allImgs.push({ 165 | src: this.src, 166 | element: this 167 | }); 168 | }); 169 | } 170 | }); 171 | 172 | allImgsLength = allImgs.length; 173 | allImgsLoaded = 0; 174 | 175 | // If no images found, don't bother. 176 | if (allImgsLength === 0) { 177 | finishedCallback.call(originalCollection); 178 | deferred.resolveWith(originalCollection); 179 | } 180 | 181 | // Now that we've found all imgs in all elements in this, 182 | // load them and attach callbacks. 183 | $.each(allImgs, function (i, img) { 184 | 185 | var image = new Image(); 186 | var events = 187 | 'load.' + eventNamespace + ' error.' + eventNamespace; 188 | 189 | // Handle the image loading and error with the same callback. 190 | $(image).one(events, function me (event) { 191 | // If an error occurred with loading the image, set the 192 | // third argument accordingly. 193 | var eachArguments = [ 194 | allImgsLoaded, 195 | allImgsLength, 196 | event.type == 'load' 197 | ]; 198 | allImgsLoaded++; 199 | 200 | eachCallback.apply(img.element, eachArguments); 201 | deferred.notifyWith(img.element, eachArguments); 202 | 203 | // Unbind the event listeners. I use this in addition to 204 | // `one` as one of those events won't be called (either 205 | // 'load' or 'error' will be called). 206 | $(this).off(events, me); 207 | 208 | if (allImgsLoaded == allImgsLength) { 209 | finishedCallback.call(originalCollection[0]); 210 | deferred.resolveWith(originalCollection[0]); 211 | return false; 212 | } 213 | 214 | }); 215 | 216 | if (hasSrcset && img.srcset) { 217 | image.srcset = img.srcset; 218 | image.sizes = img.sizes; 219 | } 220 | image.src = img.src; 221 | }); 222 | 223 | return deferred.promise(); 224 | 225 | }; 226 | })); 227 | -------------------------------------------------------------------------------- /dist/jquery.waitforimages.js: -------------------------------------------------------------------------------- 1 | /*! waitForImages jQuery Plugin - v2.4.0 - 2018-02-13 2 | * https://github.com/alexanderdickson/waitForImages 3 | * Copyright (c) 2018 Alex Dickson; Licensed MIT */ 4 | ;(function (factory) { 5 | if (typeof define === 'function' && define.amd) { 6 | // AMD. Register as an anonymous module. 7 | define(['jquery'], factory); 8 | } else if (typeof exports === 'object') { 9 | // CommonJS / nodejs module 10 | module.exports = factory(require('jquery')); 11 | } else { 12 | // Browser globals 13 | factory(jQuery); 14 | } 15 | }(function ($) { 16 | // Namespace all events. 17 | var eventNamespace = 'waitForImages'; 18 | 19 | // Is srcset supported by this browser? 20 | var hasSrcset = (function(img) { 21 | return img.srcset && img.sizes; 22 | })(new Image()); 23 | 24 | // CSS properties which contain references to images. 25 | $.waitForImages = { 26 | hasImageProperties: [ 27 | 'backgroundImage', 28 | 'listStyleImage', 29 | 'borderImage', 30 | 'borderCornerImage', 31 | 'cursor' 32 | ], 33 | hasImageAttributes: ['srcset'] 34 | }; 35 | 36 | // Custom selector to find all `img` elements with a valid `src` attribute. 37 | $.expr.pseudos['has-src'] = function (obj) { 38 | // Ensure we are dealing with an `img` element with a valid 39 | // `src` attribute. 40 | return $(obj).is('img[src][src!=""]'); 41 | }; 42 | 43 | // Custom selector to find images which are not already cached by the 44 | // browser. 45 | $.expr.pseudos.uncached = function (obj) { 46 | // Ensure we are dealing with an `img` element with a valid 47 | // `src` attribute. 48 | if (!$(obj).is(':has-src')) { 49 | return false; 50 | } 51 | 52 | return !obj.complete; 53 | }; 54 | 55 | $.fn.waitForImages = function () { 56 | 57 | var allImgsLength = 0; 58 | var allImgsLoaded = 0; 59 | var deferred = $.Deferred(); 60 | var originalCollection = this; 61 | var allImgs = []; 62 | 63 | // CSS properties which may contain an image. 64 | var hasImgProperties = $.waitForImages.hasImageProperties || []; 65 | // Element attributes which may contain an image. 66 | var hasImageAttributes = $.waitForImages.hasImageAttributes || []; 67 | // To match `url()` references. 68 | // Spec: http://www.w3.org/TR/CSS2/syndata.html#value-def-uri 69 | var matchUrl = /url\(\s*(['"]?)(.*?)\1\s*\)/g; 70 | 71 | var finishedCallback; 72 | var eachCallback; 73 | var waitForAll; 74 | 75 | // Handle options object (if passed). 76 | if ($.isPlainObject(arguments[0])) { 77 | 78 | waitForAll = arguments[0].waitForAll; 79 | eachCallback = arguments[0].each; 80 | finishedCallback = arguments[0].finished; 81 | 82 | } else { 83 | 84 | // Handle if using deferred object and only one param was passed in. 85 | if (arguments.length === 1 && $.type(arguments[0]) === 'boolean') { 86 | waitForAll = arguments[0]; 87 | } else { 88 | finishedCallback = arguments[0]; 89 | eachCallback = arguments[1]; 90 | waitForAll = arguments[2]; 91 | } 92 | 93 | } 94 | 95 | // Handle missing callbacks. 96 | finishedCallback = finishedCallback || $.noop; 97 | eachCallback = eachCallback || $.noop; 98 | 99 | // Convert waitForAll to Boolean. 100 | waitForAll = !! waitForAll; 101 | 102 | // Ensure callbacks are functions. 103 | if (!$.isFunction(finishedCallback) || !$.isFunction(eachCallback)) { 104 | throw new TypeError('An invalid callback was supplied.'); 105 | } 106 | 107 | this.each(function () { 108 | // Build a list of all imgs, dependent on what images will 109 | // be considered. 110 | var obj = $(this); 111 | 112 | if (waitForAll) { 113 | 114 | // Get all elements (including the original), as any one of 115 | // them could have a background image. 116 | obj.find('*').addBack().each(function () { 117 | var element = $(this); 118 | 119 | // If an `img` element, add it. But keep iterating in 120 | // case it has a background image too. 121 | if (element.is('img:has-src') && 122 | !element.is('[srcset]')) { 123 | allImgs.push({ 124 | src: element.attr('src'), 125 | element: element[0] 126 | }); 127 | } 128 | 129 | $.each(hasImgProperties, function (i, property) { 130 | var propertyValue = element.css(property); 131 | var match; 132 | 133 | // If it doesn't contain this property, skip. 134 | if (!propertyValue) { 135 | return true; 136 | } 137 | 138 | // Get all url() of this element. 139 | while (match = matchUrl.exec(propertyValue)) { 140 | allImgs.push({ 141 | src: match[2], 142 | element: element[0] 143 | }); 144 | } 145 | }); 146 | 147 | $.each(hasImageAttributes, function (i, attribute) { 148 | var attributeValue = element.attr(attribute); 149 | var attributeValues; 150 | 151 | // If it doesn't contain this property, skip. 152 | if (!attributeValue) { 153 | return true; 154 | } 155 | 156 | allImgs.push({ 157 | src: element.attr('src'), 158 | srcset: element.attr('srcset'), 159 | element: element[0] 160 | }); 161 | }); 162 | }); 163 | } else { 164 | // For images only, the task is simpler. 165 | obj.find('img:has-src') 166 | .each(function () { 167 | allImgs.push({ 168 | src: this.src, 169 | element: this 170 | }); 171 | }); 172 | } 173 | }); 174 | 175 | allImgsLength = allImgs.length; 176 | allImgsLoaded = 0; 177 | 178 | // If no images found, don't bother. 179 | if (allImgsLength === 0) { 180 | finishedCallback.call(originalCollection); 181 | deferred.resolveWith(originalCollection); 182 | } 183 | 184 | // Now that we've found all imgs in all elements in this, 185 | // load them and attach callbacks. 186 | $.each(allImgs, function (i, img) { 187 | 188 | var image = new Image(); 189 | var events = 190 | 'load.' + eventNamespace + ' error.' + eventNamespace; 191 | 192 | // Handle the image loading and error with the same callback. 193 | $(image).one(events, function me (event) { 194 | // If an error occurred with loading the image, set the 195 | // third argument accordingly. 196 | var eachArguments = [ 197 | allImgsLoaded, 198 | allImgsLength, 199 | event.type == 'load' 200 | ]; 201 | allImgsLoaded++; 202 | 203 | eachCallback.apply(img.element, eachArguments); 204 | deferred.notifyWith(img.element, eachArguments); 205 | 206 | // Unbind the event listeners. I use this in addition to 207 | // `one` as one of those events won't be called (either 208 | // 'load' or 'error' will be called). 209 | $(this).off(events, me); 210 | 211 | if (allImgsLoaded == allImgsLength) { 212 | finishedCallback.call(originalCollection[0]); 213 | deferred.resolveWith(originalCollection[0]); 214 | return false; 215 | } 216 | 217 | }); 218 | 219 | if (hasSrcset && img.srcset) { 220 | image.srcset = img.srcset; 221 | image.sizes = img.sizes; 222 | } 223 | image.src = img.src; 224 | }); 225 | 226 | return deferred.promise(); 227 | 228 | }; 229 | })); 230 | -------------------------------------------------------------------------------- /test/jquery.waitforimages_test.js: -------------------------------------------------------------------------------- 1 | /*global QUnit:false, module:false, test:false, asyncTest:false, expect:false*/ 2 | /*global start:false, stop:false, ok:false, equal:false, notEqual:false, deepEqual:false*/ 3 | /*global notDeepEqual:false, strictEqual:false, notStrictEqual:false, raises:false*/ 4 | (function($) { 5 | 6 | var IMG_ELEMENTS = 1; 7 | var DIV_ELEMENTS = 1; 8 | var ATTR_ELEMENTS = 1; 9 | var IMG_SRCSET_ELEMENTS = 1; 10 | 11 | var getImageUrl = function() { 12 | return "about:"; 13 | }; 14 | 15 | var setup = { 16 | setup: function() { 17 | 18 | var i; 19 | 20 | this.container = $("
    ").appendTo("#qunit-fixture"); 21 | 22 | for (i = 0; i < IMG_ELEMENTS; i++) { 23 | $("", { 24 | src: getImageUrl(), 25 | alt: "" 26 | }).appendTo(this.container); 27 | } 28 | 29 | for (i = 0; i < DIV_ELEMENTS; i++) { 30 | $("
    ", { 31 | css: { 32 | background: "url(" + getImageUrl() + ")" 33 | } 34 | }).appendTo(this.container); 35 | } 36 | 37 | for (i = 0; i < ATTR_ELEMENTS; i++) { 38 | $("
    ", { 39 | srcset: getImageUrl() + " 2x" 40 | }).appendTo(this.container); 41 | } 42 | 43 | for (i = 0; i < IMG_SRCSET_ELEMENTS; i++) { 44 | $("", { 45 | srcset: getImageUrl() + " 2x" 46 | }).appendTo(this.container); 47 | } 48 | 49 | this.container.css("background", "url(" + getImageUrl() + ")"); 50 | 51 | }, 52 | 53 | teardown: function() { 54 | this.container.empty(); 55 | } 56 | }; 57 | 58 | var setup2 = { 59 | setup: function() { 60 | setup.setup.call(this); 61 | this.container2 = $("
    ").appendTo("#qunit-fixture"); 62 | this.container2.css("background", "url(" + getImageUrl() + ")"); 63 | $("", { 64 | src: getImageUrl(), 65 | alt: "" 66 | }).appendTo(this.container2); 67 | this.combinedContainers = this.container.add(this.container2); 68 | }, 69 | 70 | teardown: function() { 71 | setup.teardown.call(this); 72 | this.container2.empty(); 73 | } 74 | }; 75 | 76 | module("Argument checking", setup); 77 | 78 | test("Check Callbacks", function() { 79 | 80 | expect(4); 81 | 82 | var self = this; 83 | 84 | raises(function() { 85 | self.container.waitForImages("string"); 86 | }, TypeError, "Finished Callback is function as argument"); 87 | 88 | raises(function() { 89 | self.container.waitForImages($.noop, "string"); 90 | }, TypeError, "Each callback is function as argument"); 91 | 92 | raises(function() { 93 | self.container.waitForImages({ 94 | finished: "string" 95 | }); 96 | }, TypeError, "Finished callback is function as passed in object"); 97 | 98 | raises(function() { 99 | self.container.waitForImages({ 100 | each: "string" 101 | }); 102 | }, TypeError, "Each callback is function as passed in object"); 103 | 104 | }); 105 | 106 | 107 | module("Img Elements", setup); 108 | 109 | asyncTest("Finished Callback", function() { 110 | 111 | expect(2); 112 | 113 | var self = this; 114 | 115 | this.container.waitForImages(function() { 116 | equal(this, self.container[0], "Assert `this` is set correctly."); 117 | ok(true, "Assert callback called."); 118 | start(); 119 | }); 120 | 121 | }); 122 | 123 | asyncTest("Finished Promise", function() { 124 | 125 | expect(2); 126 | 127 | var self = this; 128 | this.container.waitForImages().done(function() { 129 | equal(this, self.container[0], "Assert `this` is set correctly."); 130 | ok(true, "Assert callback called."); 131 | start(); 132 | }); 133 | 134 | }); 135 | 136 | asyncTest("Each Callback", function() { 137 | 138 | expect(4 * IMG_ELEMENTS); 139 | 140 | this.container.waitForImages($.noop, function(loaded, count, success) { 141 | ok($(this).is("img"), "Assert `this` is an `img` element."); 142 | ok(loaded <= count, "Assert loaded count is never larger than the count."); 143 | ok(typeof success == "boolean", "Assert `success` argument is a Boolean."); 144 | ok(true, "Assert callback called."); 145 | start(); 146 | }); 147 | 148 | }); 149 | 150 | asyncTest("Each Promise", function() { 151 | 152 | expect(4 * IMG_ELEMENTS); 153 | 154 | this.container.waitForImages().progress(function(loaded, count, success) { 155 | ok($(this).is("img"), "Assert `this` is an `img` element."); 156 | ok(loaded <= count, "Assert loaded count is never larger than the count."); 157 | ok(typeof success == "boolean", "Assert `success` argument is a Boolean."); 158 | ok(true, "Assert callback called."); 159 | start(); 160 | }); 161 | 162 | }); 163 | 164 | module("Img Elements, Elements with CSS Backgrounds & Elements with Attributes", setup); 165 | 166 | asyncTest("Finished Callback", function() { 167 | 168 | expect(2); 169 | 170 | var self = this; 171 | 172 | this.container.waitForImages(function() { 173 | equal(this, self.container[0], "Assert `this` is set correctly."); 174 | ok(true, "Assert callback called."); 175 | start(); 176 | }, $.noop, true); 177 | 178 | }); 179 | 180 | asyncTest("Finished Promise", function() { 181 | 182 | expect(2); 183 | 184 | var self = this; 185 | 186 | this.container.waitForImages(true).done(function() { 187 | equal(this, self.container[0], "Assert `this` is set correctly."); 188 | ok(true, "Assert callback called."); 189 | start(); 190 | }); 191 | 192 | }); 193 | 194 | asyncTest("Each Callback", function() { 195 | 196 | expect(4 * (IMG_ELEMENTS + DIV_ELEMENTS + ATTR_ELEMENTS + IMG_SRCSET_ELEMENTS + 1) + 1); 197 | 198 | var self = this; 199 | 200 | this.container.waitForImages($.noop, function(loaded, count, success) { 201 | if (this === self.container[0]) { 202 | ok(true, "Assert container element is checked."); 203 | } 204 | 205 | ok($(this).filter("*").length, "Assert `this` is an element."); 206 | ok(loaded <= count, "Assert loaded count is never larger than the count."); 207 | ok(typeof success == "boolean", "Assert `success` argument is a Boolean."); 208 | ok(true, "Assert callback called."); 209 | start(); 210 | }, true); 211 | 212 | }); 213 | 214 | asyncTest("Each Promise", function() { 215 | 216 | expect(4 * (IMG_ELEMENTS + DIV_ELEMENTS + ATTR_ELEMENTS + IMG_SRCSET_ELEMENTS + 1) + 1); 217 | 218 | var self = this; 219 | 220 | this.container.waitForImages(true).progress(function(loaded, count, success) { 221 | if (this === self.container[0]) { 222 | ok(true, "Assert container element is checked."); 223 | } 224 | 225 | ok($(this).filter("*").length, "Assert `this` is an element."); 226 | ok(loaded <= count, "Assert loaded count is never larger than the count."); 227 | ok(typeof success == "boolean", "Assert `success` argument is a Boolean."); 228 | ok(true, "Assert callback called."); 229 | start(); 230 | }, true); 231 | 232 | }); 233 | 234 | module("Two parent containers", setup2); 235 | 236 | asyncTest("Finished Callback gets called after all Each Callbacks", function() { 237 | 238 | expect(15); 239 | 240 | var self = this; 241 | var finishCalled = false; 242 | 243 | this.combinedContainers.waitForImages(function() { 244 | finishCalled = true; 245 | ok(true, "Assert finished callback called."); 246 | start(); 247 | }, function() { 248 | equal(finishCalled, false); 249 | ok(true, "Assert each callback called."); 250 | }, true); 251 | 252 | }); 253 | 254 | asyncTest("Finished Promise gets resolved after all Each Promises", function() { 255 | expect(22); 256 | 257 | var self = this; 258 | var finishCalled = false; 259 | 260 | this.combinedContainers.waitForImages(true).done(function() { 261 | finishCalled = true; 262 | ok(true, "Assert done promise called."); 263 | start(); 264 | }).progress(function(loaded, count, success) { 265 | equal(finishCalled, false); 266 | ok(loaded <= count, "Assert loaded count is never larger than the count."); 267 | ok(true, "Assert each callback called."); 268 | }); 269 | }); 270 | 271 | }(jQuery)); 272 | -------------------------------------------------------------------------------- /libs/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.4.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | var x = "qunit-test-string"; 17 | try { 18 | sessionStorage.setItem(x, x); 19 | sessionStorage.removeItem(x); 20 | return true; 21 | } catch(e) { 22 | return false; 23 | } 24 | }()) 25 | }; 26 | 27 | var testId = 0, 28 | toString = Object.prototype.toString, 29 | hasOwn = Object.prototype.hasOwnProperty; 30 | 31 | var Test = function(name, testName, expected, async, callback) { 32 | this.name = name; 33 | this.testName = testName; 34 | this.expected = expected; 35 | this.async = async; 36 | this.callback = callback; 37 | this.assertions = []; 38 | }; 39 | Test.prototype = { 40 | init: function() { 41 | var tests = id("qunit-tests"); 42 | if (tests) { 43 | var b = document.createElement("strong"); 44 | b.innerHTML = "Running " + this.name; 45 | var li = document.createElement("li"); 46 | li.appendChild( b ); 47 | li.className = "running"; 48 | li.id = this.id = "test-output" + testId++; 49 | tests.appendChild( li ); 50 | } 51 | }, 52 | setup: function() { 53 | if (this.module != config.previousModule) { 54 | if ( config.previousModule ) { 55 | runLoggingCallbacks('moduleDone', QUnit, { 56 | name: config.previousModule, 57 | failed: config.moduleStats.bad, 58 | passed: config.moduleStats.all - config.moduleStats.bad, 59 | total: config.moduleStats.all 60 | } ); 61 | } 62 | config.previousModule = this.module; 63 | config.moduleStats = { all: 0, bad: 0 }; 64 | runLoggingCallbacks( 'moduleStart', QUnit, { 65 | name: this.module 66 | } ); 67 | } else if (config.autorun) { 68 | runLoggingCallbacks( 'moduleStart', QUnit, { 69 | name: this.module 70 | } ); 71 | } 72 | 73 | config.current = this; 74 | this.testEnvironment = extend({ 75 | setup: function() {}, 76 | teardown: function() {} 77 | }, this.moduleTestEnvironment); 78 | 79 | runLoggingCallbacks( 'testStart', QUnit, { 80 | name: this.testName, 81 | module: this.module 82 | }); 83 | 84 | // allow utility functions to access the current test environment 85 | // TODO why?? 86 | QUnit.current_testEnvironment = this.testEnvironment; 87 | 88 | if ( !config.pollution ) { 89 | saveGlobal(); 90 | } 91 | if ( config.notrycatch ) { 92 | this.testEnvironment.setup.call(this.testEnvironment); 93 | return; 94 | } 95 | try { 96 | this.testEnvironment.setup.call(this.testEnvironment); 97 | } catch(e) { 98 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 99 | } 100 | }, 101 | run: function() { 102 | config.current = this; 103 | if ( this.async ) { 104 | QUnit.stop(); 105 | } 106 | 107 | if ( config.notrycatch ) { 108 | this.callback.call(this.testEnvironment); 109 | return; 110 | } 111 | try { 112 | this.callback.call(this.testEnvironment); 113 | } catch(e) { 114 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + ": " + e.message, extractStacktrace( e, 1 ) ); 115 | // else next test will carry the responsibility 116 | saveGlobal(); 117 | 118 | // Restart the tests if they're blocking 119 | if ( config.blocking ) { 120 | QUnit.start(); 121 | } 122 | } 123 | }, 124 | teardown: function() { 125 | config.current = this; 126 | if ( config.notrycatch ) { 127 | this.testEnvironment.teardown.call(this.testEnvironment); 128 | return; 129 | } else { 130 | try { 131 | this.testEnvironment.teardown.call(this.testEnvironment); 132 | } catch(e) { 133 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 134 | } 135 | } 136 | checkPollution(); 137 | }, 138 | finish: function() { 139 | config.current = this; 140 | if ( this.expected != null && this.expected != this.assertions.length ) { 141 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 142 | } else if ( this.expected == null && !this.assertions.length ) { 143 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions." ); 144 | } 145 | 146 | var good = 0, bad = 0, 147 | li, i, 148 | tests = id("qunit-tests"); 149 | 150 | config.stats.all += this.assertions.length; 151 | config.moduleStats.all += this.assertions.length; 152 | 153 | if ( tests ) { 154 | var ol = document.createElement("ol"); 155 | 156 | for ( i = 0; i < this.assertions.length; i++ ) { 157 | var assertion = this.assertions[i]; 158 | 159 | li = document.createElement("li"); 160 | li.className = assertion.result ? "pass" : "fail"; 161 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 162 | ol.appendChild( li ); 163 | 164 | if ( assertion.result ) { 165 | good++; 166 | } else { 167 | bad++; 168 | config.stats.bad++; 169 | config.moduleStats.bad++; 170 | } 171 | } 172 | 173 | // store result when possible 174 | if ( QUnit.config.reorder && defined.sessionStorage ) { 175 | if (bad) { 176 | sessionStorage.setItem("qunit-test-" + this.module + "-" + this.testName, bad); 177 | } else { 178 | sessionStorage.removeItem("qunit-test-" + this.module + "-" + this.testName); 179 | } 180 | } 181 | 182 | if (bad === 0) { 183 | ol.style.display = "none"; 184 | } 185 | 186 | var b = document.createElement("strong"); 187 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 188 | 189 | var a = document.createElement("a"); 190 | a.innerHTML = "Rerun"; 191 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 192 | 193 | addEvent(b, "click", function() { 194 | var next = b.nextSibling.nextSibling, 195 | display = next.style.display; 196 | next.style.display = display === "none" ? "block" : "none"; 197 | }); 198 | 199 | addEvent(b, "dblclick", function(e) { 200 | var target = e && e.target ? e.target : window.event.srcElement; 201 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 202 | target = target.parentNode; 203 | } 204 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 205 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 206 | } 207 | }); 208 | 209 | li = id(this.id); 210 | li.className = bad ? "fail" : "pass"; 211 | li.removeChild( li.firstChild ); 212 | li.appendChild( b ); 213 | li.appendChild( a ); 214 | li.appendChild( ol ); 215 | 216 | } else { 217 | for ( i = 0; i < this.assertions.length; i++ ) { 218 | if ( !this.assertions[i].result ) { 219 | bad++; 220 | config.stats.bad++; 221 | config.moduleStats.bad++; 222 | } 223 | } 224 | } 225 | 226 | QUnit.reset(); 227 | 228 | runLoggingCallbacks( 'testDone', QUnit, { 229 | name: this.testName, 230 | module: this.module, 231 | failed: bad, 232 | passed: this.assertions.length - bad, 233 | total: this.assertions.length 234 | } ); 235 | }, 236 | 237 | queue: function() { 238 | var test = this; 239 | synchronize(function() { 240 | test.init(); 241 | }); 242 | function run() { 243 | // each of these can by async 244 | synchronize(function() { 245 | test.setup(); 246 | }); 247 | synchronize(function() { 248 | test.run(); 249 | }); 250 | synchronize(function() { 251 | test.teardown(); 252 | }); 253 | synchronize(function() { 254 | test.finish(); 255 | }); 256 | } 257 | // defer when previous test run passed, if storage is available 258 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-test-" + this.module + "-" + this.testName); 259 | if (bad) { 260 | run(); 261 | } else { 262 | synchronize(run, true); 263 | } 264 | } 265 | 266 | }; 267 | 268 | var QUnit = { 269 | 270 | // call on start of module test to prepend name to all tests 271 | module: function(name, testEnvironment) { 272 | config.currentModule = name; 273 | config.currentModuleTestEnviroment = testEnvironment; 274 | }, 275 | 276 | asyncTest: function(testName, expected, callback) { 277 | if ( arguments.length === 2 ) { 278 | callback = expected; 279 | expected = null; 280 | } 281 | 282 | QUnit.test(testName, expected, callback, true); 283 | }, 284 | 285 | test: function(testName, expected, callback, async) { 286 | var name = '' + escapeInnerText(testName) + ''; 287 | 288 | if ( arguments.length === 2 ) { 289 | callback = expected; 290 | expected = null; 291 | } 292 | 293 | if ( config.currentModule ) { 294 | name = '' + config.currentModule + ": " + name; 295 | } 296 | 297 | if ( !validTest(config.currentModule + ": " + testName) ) { 298 | return; 299 | } 300 | 301 | var test = new Test(name, testName, expected, async, callback); 302 | test.module = config.currentModule; 303 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 304 | test.queue(); 305 | }, 306 | 307 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 308 | expect: function(asserts) { 309 | config.current.expected = asserts; 310 | }, 311 | 312 | // Asserts true. 313 | // @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 314 | ok: function(result, msg) { 315 | if (!config.current) { 316 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 317 | } 318 | result = !!result; 319 | var details = { 320 | result: result, 321 | message: msg 322 | }; 323 | msg = escapeInnerText(msg || (result ? "okay" : "failed")); 324 | if ( !result ) { 325 | var source = sourceFromStacktrace(2); 326 | if (source) { 327 | details.source = source; 328 | msg += '
    Source:
    ' + escapeInnerText(source) + '
    '; 329 | } 330 | } 331 | runLoggingCallbacks( 'log', QUnit, details ); 332 | config.current.assertions.push({ 333 | result: result, 334 | message: msg 335 | }); 336 | }, 337 | 338 | // Checks that the first two arguments are equal, with an optional message. Prints out both actual and expected values. 339 | // @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 340 | equal: function(actual, expected, message) { 341 | QUnit.push(expected == actual, actual, expected, message); 342 | }, 343 | 344 | notEqual: function(actual, expected, message) { 345 | QUnit.push(expected != actual, actual, expected, message); 346 | }, 347 | 348 | deepEqual: function(actual, expected, message) { 349 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 350 | }, 351 | 352 | notDeepEqual: function(actual, expected, message) { 353 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 354 | }, 355 | 356 | strictEqual: function(actual, expected, message) { 357 | QUnit.push(expected === actual, actual, expected, message); 358 | }, 359 | 360 | notStrictEqual: function(actual, expected, message) { 361 | QUnit.push(expected !== actual, actual, expected, message); 362 | }, 363 | 364 | raises: function(block, expected, message) { 365 | var actual, ok = false; 366 | 367 | if (typeof expected === 'string') { 368 | message = expected; 369 | expected = null; 370 | } 371 | 372 | try { 373 | block(); 374 | } catch (e) { 375 | actual = e; 376 | } 377 | 378 | if (actual) { 379 | // we don't want to validate thrown error 380 | if (!expected) { 381 | ok = true; 382 | // expected is a regexp 383 | } else if (QUnit.objectType(expected) === "regexp") { 384 | ok = expected.test(actual); 385 | // expected is a constructor 386 | } else if (actual instanceof expected) { 387 | ok = true; 388 | // expected is a validation function which returns true is validation passed 389 | } else if (expected.call({}, actual) === true) { 390 | ok = true; 391 | } 392 | } 393 | 394 | QUnit.ok(ok, message); 395 | }, 396 | 397 | start: function(count) { 398 | config.semaphore -= count || 1; 399 | if (config.semaphore > 0) { 400 | // don't start until equal number of stop-calls 401 | return; 402 | } 403 | if (config.semaphore < 0) { 404 | // ignore if start is called more often then stop 405 | config.semaphore = 0; 406 | } 407 | // A slight delay, to avoid any current callbacks 408 | if ( defined.setTimeout ) { 409 | window.setTimeout(function() { 410 | if (config.semaphore > 0) { 411 | return; 412 | } 413 | if ( config.timeout ) { 414 | clearTimeout(config.timeout); 415 | } 416 | 417 | config.blocking = false; 418 | process(true); 419 | }, 13); 420 | } else { 421 | config.blocking = false; 422 | process(true); 423 | } 424 | }, 425 | 426 | stop: function(count) { 427 | config.semaphore += count || 1; 428 | config.blocking = true; 429 | 430 | if ( config.testTimeout && defined.setTimeout ) { 431 | clearTimeout(config.timeout); 432 | config.timeout = window.setTimeout(function() { 433 | QUnit.ok( false, "Test timed out" ); 434 | config.semaphore = 1; 435 | QUnit.start(); 436 | }, config.testTimeout); 437 | } 438 | } 439 | }; 440 | 441 | //We want access to the constructor's prototype 442 | (function() { 443 | function F(){} 444 | F.prototype = QUnit; 445 | QUnit = new F(); 446 | //Make F QUnit's constructor so that we can add to the prototype later 447 | QUnit.constructor = F; 448 | }()); 449 | 450 | // deprecated; still export them to window to provide clear error messages 451 | // next step: remove entirely 452 | QUnit.equals = function() { 453 | QUnit.push(false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 454 | }; 455 | QUnit.same = function() { 456 | QUnit.push(false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 457 | }; 458 | 459 | // Maintain internal state 460 | var config = { 461 | // The queue of tests to run 462 | queue: [], 463 | 464 | // block until document ready 465 | blocking: true, 466 | 467 | // when enabled, show only failing tests 468 | // gets persisted through sessionStorage and can be changed in UI via checkbox 469 | hidepassed: false, 470 | 471 | // by default, run previously failed tests first 472 | // very useful in combination with "Hide passed tests" checked 473 | reorder: true, 474 | 475 | // by default, modify document.title when suite is done 476 | altertitle: true, 477 | 478 | urlConfig: ['noglobals', 'notrycatch'], 479 | 480 | //logging callback queues 481 | begin: [], 482 | done: [], 483 | log: [], 484 | testStart: [], 485 | testDone: [], 486 | moduleStart: [], 487 | moduleDone: [] 488 | }; 489 | 490 | // Load parameters 491 | (function() { 492 | var location = window.location || { search: "", protocol: "file:" }, 493 | params = location.search.slice( 1 ).split( "&" ), 494 | length = params.length, 495 | urlParams = {}, 496 | current; 497 | 498 | if ( params[ 0 ] ) { 499 | for ( var i = 0; i < length; i++ ) { 500 | current = params[ i ].split( "=" ); 501 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 502 | // allow just a key to turn on a flag, e.g., test.html?noglobals 503 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 504 | urlParams[ current[ 0 ] ] = current[ 1 ]; 505 | } 506 | } 507 | 508 | QUnit.urlParams = urlParams; 509 | config.filter = urlParams.filter; 510 | 511 | // Figure out if we're running the tests from a server or not 512 | QUnit.isLocal = location.protocol === 'file:'; 513 | }()); 514 | 515 | // Expose the API as global variables, unless an 'exports' 516 | // object exists, in that case we assume we're in CommonJS - export everything at the end 517 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 518 | extend(window, QUnit); 519 | window.QUnit = QUnit; 520 | } 521 | 522 | // define these after exposing globals to keep them in these QUnit namespace only 523 | extend(QUnit, { 524 | config: config, 525 | 526 | // Initialize the configuration options 527 | init: function() { 528 | extend(config, { 529 | stats: { all: 0, bad: 0 }, 530 | moduleStats: { all: 0, bad: 0 }, 531 | started: +new Date(), 532 | updateRate: 1000, 533 | blocking: false, 534 | autostart: true, 535 | autorun: false, 536 | filter: "", 537 | queue: [], 538 | semaphore: 0 539 | }); 540 | 541 | var qunit = id( "qunit" ); 542 | if ( qunit ) { 543 | qunit.innerHTML = 544 | '

    ' + escapeInnerText( document.title ) + '

    ' + 545 | '

    ' + 546 | '
    ' + 547 | '

    ' + 548 | '
      '; 549 | } 550 | 551 | var tests = id( "qunit-tests" ), 552 | banner = id( "qunit-banner" ), 553 | result = id( "qunit-testresult" ); 554 | 555 | if ( tests ) { 556 | tests.innerHTML = ""; 557 | } 558 | 559 | if ( banner ) { 560 | banner.className = ""; 561 | } 562 | 563 | if ( result ) { 564 | result.parentNode.removeChild( result ); 565 | } 566 | 567 | if ( tests ) { 568 | result = document.createElement( "p" ); 569 | result.id = "qunit-testresult"; 570 | result.className = "result"; 571 | tests.parentNode.insertBefore( result, tests ); 572 | result.innerHTML = 'Running...
       '; 573 | } 574 | }, 575 | 576 | // Resets the test setup. Useful for tests that modify the DOM. 577 | // If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 578 | reset: function() { 579 | if ( window.jQuery ) { 580 | jQuery( "#qunit-fixture" ).html( config.fixture ); 581 | } else { 582 | var main = id( 'qunit-fixture' ); 583 | if ( main ) { 584 | main.innerHTML = config.fixture; 585 | } 586 | } 587 | }, 588 | 589 | // Trigger an event on an element. 590 | // @example triggerEvent( document.body, "click" ); 591 | triggerEvent: function( elem, type, event ) { 592 | if ( document.createEvent ) { 593 | event = document.createEvent("MouseEvents"); 594 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 595 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 596 | elem.dispatchEvent( event ); 597 | 598 | } else if ( elem.fireEvent ) { 599 | elem.fireEvent("on"+type); 600 | } 601 | }, 602 | 603 | // Safe object type checking 604 | is: function( type, obj ) { 605 | return QUnit.objectType( obj ) == type; 606 | }, 607 | 608 | objectType: function( obj ) { 609 | if (typeof obj === "undefined") { 610 | return "undefined"; 611 | 612 | // consider: typeof null === object 613 | } 614 | if (obj === null) { 615 | return "null"; 616 | } 617 | 618 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 619 | 620 | switch (type) { 621 | case 'Number': 622 | if (isNaN(obj)) { 623 | return "nan"; 624 | } 625 | return "number"; 626 | case 'String': 627 | case 'Boolean': 628 | case 'Array': 629 | case 'Date': 630 | case 'RegExp': 631 | case 'Function': 632 | return type.toLowerCase(); 633 | } 634 | if (typeof obj === "object") { 635 | return "object"; 636 | } 637 | return undefined; 638 | }, 639 | 640 | push: function(result, actual, expected, message) { 641 | if (!config.current) { 642 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 643 | } 644 | var details = { 645 | result: result, 646 | message: message, 647 | actual: actual, 648 | expected: expected 649 | }; 650 | 651 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 652 | message = '' + message + ""; 653 | var output = message; 654 | if (!result) { 655 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 656 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 657 | output += ''; 658 | if (actual != expected) { 659 | output += ''; 660 | output += ''; 661 | } 662 | var source = sourceFromStacktrace(); 663 | if (source) { 664 | details.source = source; 665 | output += ''; 666 | } 667 | output += "
      Expected:
      ' + expected + '
      Result:
      ' + actual + '
      Diff:
      ' + QUnit.diff(expected, actual) +'
      Source:
      ' + escapeInnerText(source) + '
      "; 668 | } 669 | 670 | runLoggingCallbacks( 'log', QUnit, details ); 671 | 672 | config.current.assertions.push({ 673 | result: !!result, 674 | message: output 675 | }); 676 | }, 677 | 678 | pushFailure: function(message, source) { 679 | var details = { 680 | result: false, 681 | message: message 682 | }; 683 | var output = escapeInnerText(message); 684 | if (source) { 685 | details.source = source; 686 | output += '
      Source:
      ' + escapeInnerText(source) + '
      '; 687 | } 688 | runLoggingCallbacks( 'log', QUnit, details ); 689 | config.current.assertions.push({ 690 | result: false, 691 | message: output 692 | }); 693 | }, 694 | 695 | url: function( params ) { 696 | params = extend( extend( {}, QUnit.urlParams ), params ); 697 | var querystring = "?", 698 | key; 699 | for ( key in params ) { 700 | if ( !hasOwn.call( params, key ) ) { 701 | continue; 702 | } 703 | querystring += encodeURIComponent( key ) + "=" + 704 | encodeURIComponent( params[ key ] ) + "&"; 705 | } 706 | return window.location.pathname + querystring.slice( 0, -1 ); 707 | }, 708 | 709 | extend: extend, 710 | id: id, 711 | addEvent: addEvent 712 | }); 713 | 714 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 715 | //Doing this allows us to tell if the following methods have been overwritten on the actual 716 | //QUnit object, which is a deprecated way of using the callbacks. 717 | extend(QUnit.constructor.prototype, { 718 | // Logging callbacks; all receive a single argument with the listed properties 719 | // run test/logs.html for any related changes 720 | begin: registerLoggingCallback('begin'), 721 | // done: { failed, passed, total, runtime } 722 | done: registerLoggingCallback('done'), 723 | // log: { result, actual, expected, message } 724 | log: registerLoggingCallback('log'), 725 | // testStart: { name } 726 | testStart: registerLoggingCallback('testStart'), 727 | // testDone: { name, failed, passed, total } 728 | testDone: registerLoggingCallback('testDone'), 729 | // moduleStart: { name } 730 | moduleStart: registerLoggingCallback('moduleStart'), 731 | // moduleDone: { name, failed, passed, total } 732 | moduleDone: registerLoggingCallback('moduleDone') 733 | }); 734 | 735 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 736 | config.autorun = true; 737 | } 738 | 739 | QUnit.load = function() { 740 | runLoggingCallbacks( 'begin', QUnit, {} ); 741 | 742 | // Initialize the config, saving the execution queue 743 | var oldconfig = extend({}, config); 744 | QUnit.init(); 745 | extend(config, oldconfig); 746 | 747 | config.blocking = false; 748 | 749 | var urlConfigHtml = '', len = config.urlConfig.length; 750 | for ( var i = 0, val; i < len; i++ ) { 751 | val = config.urlConfig[i]; 752 | config[val] = QUnit.urlParams[val]; 753 | urlConfigHtml += ''; 754 | } 755 | 756 | var userAgent = id("qunit-userAgent"); 757 | if ( userAgent ) { 758 | userAgent.innerHTML = navigator.userAgent; 759 | } 760 | var banner = id("qunit-header"); 761 | if ( banner ) { 762 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 763 | addEvent( banner, "change", function( event ) { 764 | var params = {}; 765 | params[ event.target.name ] = event.target.checked ? true : undefined; 766 | window.location = QUnit.url( params ); 767 | }); 768 | } 769 | 770 | var toolbar = id("qunit-testrunner-toolbar"); 771 | if ( toolbar ) { 772 | var filter = document.createElement("input"); 773 | filter.type = "checkbox"; 774 | filter.id = "qunit-filter-pass"; 775 | addEvent( filter, "click", function() { 776 | var ol = document.getElementById("qunit-tests"); 777 | if ( filter.checked ) { 778 | ol.className = ol.className + " hidepass"; 779 | } else { 780 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 781 | ol.className = tmp.replace(/ hidepass /, " "); 782 | } 783 | if ( defined.sessionStorage ) { 784 | if (filter.checked) { 785 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 786 | } else { 787 | sessionStorage.removeItem("qunit-filter-passed-tests"); 788 | } 789 | } 790 | }); 791 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 792 | filter.checked = true; 793 | var ol = document.getElementById("qunit-tests"); 794 | ol.className = ol.className + " hidepass"; 795 | } 796 | toolbar.appendChild( filter ); 797 | 798 | var label = document.createElement("label"); 799 | label.setAttribute("for", "qunit-filter-pass"); 800 | label.innerHTML = "Hide passed tests"; 801 | toolbar.appendChild( label ); 802 | } 803 | 804 | var main = id('qunit-fixture'); 805 | if ( main ) { 806 | config.fixture = main.innerHTML; 807 | } 808 | 809 | if (config.autostart) { 810 | QUnit.start(); 811 | } 812 | }; 813 | 814 | addEvent(window, "load", QUnit.load); 815 | 816 | // addEvent(window, "error") gives us a useless event object 817 | window.onerror = function( message, file, line ) { 818 | if ( QUnit.config.current ) { 819 | QUnit.pushFailure( message, file + ":" + line ); 820 | } else { 821 | QUnit.test( "global failure", function() { 822 | QUnit.pushFailure( message, file + ":" + line ); 823 | }); 824 | } 825 | }; 826 | 827 | function done() { 828 | config.autorun = true; 829 | 830 | // Log the last module results 831 | if ( config.currentModule ) { 832 | runLoggingCallbacks( 'moduleDone', QUnit, { 833 | name: config.currentModule, 834 | failed: config.moduleStats.bad, 835 | passed: config.moduleStats.all - config.moduleStats.bad, 836 | total: config.moduleStats.all 837 | } ); 838 | } 839 | 840 | var banner = id("qunit-banner"), 841 | tests = id("qunit-tests"), 842 | runtime = +new Date() - config.started, 843 | passed = config.stats.all - config.stats.bad, 844 | html = [ 845 | 'Tests completed in ', 846 | runtime, 847 | ' milliseconds.
      ', 848 | '', 849 | passed, 850 | ' tests of ', 851 | config.stats.all, 852 | ' passed, ', 853 | config.stats.bad, 854 | ' failed.' 855 | ].join(''); 856 | 857 | if ( banner ) { 858 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 859 | } 860 | 861 | if ( tests ) { 862 | id( "qunit-testresult" ).innerHTML = html; 863 | } 864 | 865 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 866 | // show ✖ for good, ✔ for bad suite result in title 867 | // use escape sequences in case file gets loaded with non-utf-8-charset 868 | document.title = [ 869 | (config.stats.bad ? "\u2716" : "\u2714"), 870 | document.title.replace(/^[\u2714\u2716] /i, "") 871 | ].join(" "); 872 | } 873 | 874 | // clear own sessionStorage items if all tests passed 875 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 876 | for (var key in sessionStorage) { 877 | if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-test-") === 0 ) { 878 | sessionStorage.removeItem(key); 879 | } 880 | } 881 | } 882 | 883 | runLoggingCallbacks( 'done', QUnit, { 884 | failed: config.stats.bad, 885 | passed: passed, 886 | total: config.stats.all, 887 | runtime: runtime 888 | } ); 889 | } 890 | 891 | function validTest( name ) { 892 | var filter = config.filter, 893 | run = false; 894 | 895 | if ( !filter ) { 896 | return true; 897 | } 898 | 899 | var not = filter.charAt( 0 ) === "!"; 900 | if ( not ) { 901 | filter = filter.slice( 1 ); 902 | } 903 | 904 | if ( name.indexOf( filter ) !== -1 ) { 905 | return !not; 906 | } 907 | 908 | if ( not ) { 909 | run = true; 910 | } 911 | 912 | return run; 913 | } 914 | 915 | // so far supports only Firefox, Chrome and Opera (buggy) 916 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 917 | function extractStacktrace( e, offset ) { 918 | offset = offset || 3; 919 | if (e.stacktrace) { 920 | // Opera 921 | return e.stacktrace.split("\n")[offset + 3]; 922 | } else if (e.stack) { 923 | // Firefox, Chrome 924 | var stack = e.stack.split("\n"); 925 | if (/^error$/i.test(stack[0])) { 926 | stack.shift(); 927 | } 928 | return stack[offset]; 929 | } else if (e.sourceURL) { 930 | // Safari, PhantomJS 931 | // hopefully one day Safari provides actual stacktraces 932 | // exclude useless self-reference for generated Error objects 933 | if ( /qunit.js$/.test( e.sourceURL ) ) { 934 | return; 935 | } 936 | // for actual exceptions, this is useful 937 | return e.sourceURL + ":" + e.line; 938 | } 939 | } 940 | function sourceFromStacktrace(offset) { 941 | try { 942 | throw new Error(); 943 | } catch ( e ) { 944 | return extractStacktrace( e, offset ); 945 | } 946 | } 947 | 948 | function escapeInnerText(s) { 949 | if (!s) { 950 | return ""; 951 | } 952 | s = s + ""; 953 | return s.replace(/[\&<>]/g, function(s) { 954 | switch(s) { 955 | case "&": return "&"; 956 | case "<": return "<"; 957 | case ">": return ">"; 958 | default: return s; 959 | } 960 | }); 961 | } 962 | 963 | function synchronize( callback, last ) { 964 | config.queue.push( callback ); 965 | 966 | if ( config.autorun && !config.blocking ) { 967 | process(last); 968 | } 969 | } 970 | 971 | function process( last ) { 972 | function next() { 973 | process( last ); 974 | } 975 | var start = new Date().getTime(); 976 | config.depth = config.depth ? config.depth + 1 : 1; 977 | 978 | while ( config.queue.length && !config.blocking ) { 979 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 980 | config.queue.shift()(); 981 | } else { 982 | window.setTimeout( next, 13 ); 983 | break; 984 | } 985 | } 986 | config.depth--; 987 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 988 | done(); 989 | } 990 | } 991 | 992 | function saveGlobal() { 993 | config.pollution = []; 994 | 995 | if ( config.noglobals ) { 996 | for ( var key in window ) { 997 | if ( !hasOwn.call( window, key ) ) { 998 | continue; 999 | } 1000 | config.pollution.push( key ); 1001 | } 1002 | } 1003 | } 1004 | 1005 | function checkPollution( name ) { 1006 | var old = config.pollution; 1007 | saveGlobal(); 1008 | 1009 | var newGlobals = diff( config.pollution, old ); 1010 | if ( newGlobals.length > 0 ) { 1011 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1012 | } 1013 | 1014 | var deletedGlobals = diff( old, config.pollution ); 1015 | if ( deletedGlobals.length > 0 ) { 1016 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1017 | } 1018 | } 1019 | 1020 | // returns a new Array with the elements that are in a but not in b 1021 | function diff( a, b ) { 1022 | var result = a.slice(); 1023 | for ( var i = 0; i < result.length; i++ ) { 1024 | for ( var j = 0; j < b.length; j++ ) { 1025 | if ( result[i] === b[j] ) { 1026 | result.splice(i, 1); 1027 | i--; 1028 | break; 1029 | } 1030 | } 1031 | } 1032 | return result; 1033 | } 1034 | 1035 | function extend(a, b) { 1036 | for ( var prop in b ) { 1037 | if ( b[prop] === undefined ) { 1038 | delete a[prop]; 1039 | 1040 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1041 | } else if ( prop !== "constructor" || a !== window ) { 1042 | a[prop] = b[prop]; 1043 | } 1044 | } 1045 | 1046 | return a; 1047 | } 1048 | 1049 | function addEvent(elem, type, fn) { 1050 | if ( elem.addEventListener ) { 1051 | elem.addEventListener( type, fn, false ); 1052 | } else if ( elem.attachEvent ) { 1053 | elem.attachEvent( "on" + type, fn ); 1054 | } else { 1055 | fn(); 1056 | } 1057 | } 1058 | 1059 | function id(name) { 1060 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1061 | document.getElementById( name ); 1062 | } 1063 | 1064 | function registerLoggingCallback(key){ 1065 | return function(callback){ 1066 | config[key].push( callback ); 1067 | }; 1068 | } 1069 | 1070 | // Supports deprecated method of completely overwriting logging callbacks 1071 | function runLoggingCallbacks(key, scope, args) { 1072 | //debugger; 1073 | var callbacks; 1074 | if ( QUnit.hasOwnProperty(key) ) { 1075 | QUnit[key].call(scope, args); 1076 | } else { 1077 | callbacks = config[key]; 1078 | for( var i = 0; i < callbacks.length; i++ ) { 1079 | callbacks[i].call( scope, args ); 1080 | } 1081 | } 1082 | } 1083 | 1084 | // Test for equality any JavaScript type. 1085 | // Author: Philippe Rathé 1086 | QUnit.equiv = (function() { 1087 | 1088 | var innerEquiv; // the real equiv function 1089 | var callers = []; // stack to decide between skip/abort functions 1090 | var parents = []; // stack to avoiding loops from circular referencing 1091 | 1092 | // Call the o related callback with the given arguments. 1093 | function bindCallbacks(o, callbacks, args) { 1094 | var prop = QUnit.objectType(o); 1095 | if (prop) { 1096 | if (QUnit.objectType(callbacks[prop]) === "function") { 1097 | return callbacks[prop].apply(callbacks, args); 1098 | } else { 1099 | return callbacks[prop]; // or undefined 1100 | } 1101 | } 1102 | } 1103 | 1104 | var getProto = Object.getPrototypeOf || function (obj) { 1105 | return obj.__proto__; 1106 | }; 1107 | 1108 | var callbacks = (function () { 1109 | 1110 | // for string, boolean, number and null 1111 | function useStrictEquality(b, a) { 1112 | if (b instanceof a.constructor || a instanceof b.constructor) { 1113 | // to catch short annotation VS 'new' annotation of a 1114 | // declaration 1115 | // e.g. var i = 1; 1116 | // var j = new Number(1); 1117 | return a == b; 1118 | } else { 1119 | return a === b; 1120 | } 1121 | } 1122 | 1123 | return { 1124 | "string" : useStrictEquality, 1125 | "boolean" : useStrictEquality, 1126 | "number" : useStrictEquality, 1127 | "null" : useStrictEquality, 1128 | "undefined" : useStrictEquality, 1129 | 1130 | "nan" : function(b) { 1131 | return isNaN(b); 1132 | }, 1133 | 1134 | "date" : function(b, a) { 1135 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1136 | }, 1137 | 1138 | "regexp" : function(b, a) { 1139 | return QUnit.objectType(b) === "regexp" && 1140 | // the regex itself 1141 | a.source === b.source && 1142 | // and its modifiers 1143 | a.global === b.global && 1144 | // (gmi) ... 1145 | a.ignoreCase === b.ignoreCase && 1146 | a.multiline === b.multiline; 1147 | }, 1148 | 1149 | // - skip when the property is a method of an instance (OOP) 1150 | // - abort otherwise, 1151 | // initial === would have catch identical references anyway 1152 | "function" : function() { 1153 | var caller = callers[callers.length - 1]; 1154 | return caller !== Object && typeof caller !== "undefined"; 1155 | }, 1156 | 1157 | "array" : function(b, a) { 1158 | var i, j, loop; 1159 | var len; 1160 | 1161 | // b could be an object literal here 1162 | if (QUnit.objectType(b) !== "array") { 1163 | return false; 1164 | } 1165 | 1166 | len = a.length; 1167 | if (len !== b.length) { // safe and faster 1168 | return false; 1169 | } 1170 | 1171 | // track reference to avoid circular references 1172 | parents.push(a); 1173 | for (i = 0; i < len; i++) { 1174 | loop = false; 1175 | for (j = 0; j < parents.length; j++) { 1176 | if (parents[j] === a[i]) { 1177 | loop = true;// dont rewalk array 1178 | } 1179 | } 1180 | if (!loop && !innerEquiv(a[i], b[i])) { 1181 | parents.pop(); 1182 | return false; 1183 | } 1184 | } 1185 | parents.pop(); 1186 | return true; 1187 | }, 1188 | 1189 | "object" : function(b, a) { 1190 | var i, j, loop; 1191 | var eq = true; // unless we can prove it 1192 | var aProperties = [], bProperties = []; // collection of 1193 | // strings 1194 | 1195 | // comparing constructors is more strict than using 1196 | // instanceof 1197 | if (a.constructor !== b.constructor) { 1198 | // Allow objects with no prototype to be equivalent to 1199 | // objects with Object as their constructor. 1200 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1201 | (getProto(b) === null && getProto(a) === Object.prototype))) 1202 | { 1203 | return false; 1204 | } 1205 | } 1206 | 1207 | // stack constructor before traversing properties 1208 | callers.push(a.constructor); 1209 | // track reference to avoid circular references 1210 | parents.push(a); 1211 | 1212 | for (i in a) { // be strict: don't ensures hasOwnProperty 1213 | // and go deep 1214 | loop = false; 1215 | for (j = 0; j < parents.length; j++) { 1216 | if (parents[j] === a[i]) { 1217 | // don't go down the same path twice 1218 | loop = true; 1219 | } 1220 | } 1221 | aProperties.push(i); // collect a's properties 1222 | 1223 | if (!loop && !innerEquiv(a[i], b[i])) { 1224 | eq = false; 1225 | break; 1226 | } 1227 | } 1228 | 1229 | callers.pop(); // unstack, we are done 1230 | parents.pop(); 1231 | 1232 | for (i in b) { 1233 | bProperties.push(i); // collect b's properties 1234 | } 1235 | 1236 | // Ensures identical properties name 1237 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1238 | } 1239 | }; 1240 | }()); 1241 | 1242 | innerEquiv = function() { // can take multiple arguments 1243 | var args = Array.prototype.slice.apply(arguments); 1244 | if (args.length < 2) { 1245 | return true; // end transition 1246 | } 1247 | 1248 | return (function(a, b) { 1249 | if (a === b) { 1250 | return true; // catch the most you can 1251 | } else if (a === null || b === null || typeof a === "undefined" || 1252 | typeof b === "undefined" || 1253 | QUnit.objectType(a) !== QUnit.objectType(b)) { 1254 | return false; // don't lose time with error prone cases 1255 | } else { 1256 | return bindCallbacks(a, callbacks, [ b, a ]); 1257 | } 1258 | 1259 | // apply transition with (1..n) arguments 1260 | }(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1))); 1261 | }; 1262 | 1263 | return innerEquiv; 1264 | 1265 | }()); 1266 | 1267 | /** 1268 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1269 | * http://flesler.blogspot.com Licensed under BSD 1270 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1271 | * 1272 | * @projectDescription Advanced and extensible data dumping for Javascript. 1273 | * @version 1.0.0 1274 | * @author Ariel Flesler 1275 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1276 | */ 1277 | QUnit.jsDump = (function() { 1278 | function quote( str ) { 1279 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1280 | } 1281 | function literal( o ) { 1282 | return o + ''; 1283 | } 1284 | function join( pre, arr, post ) { 1285 | var s = jsDump.separator(), 1286 | base = jsDump.indent(), 1287 | inner = jsDump.indent(1); 1288 | if ( arr.join ) { 1289 | arr = arr.join( ',' + s + inner ); 1290 | } 1291 | if ( !arr ) { 1292 | return pre + post; 1293 | } 1294 | return [ pre, inner + arr, base + post ].join(s); 1295 | } 1296 | function array( arr, stack ) { 1297 | var i = arr.length, ret = new Array(i); 1298 | this.up(); 1299 | while ( i-- ) { 1300 | ret[i] = this.parse( arr[i] , undefined , stack); 1301 | } 1302 | this.down(); 1303 | return join( '[', ret, ']' ); 1304 | } 1305 | 1306 | var reName = /^function (\w+)/; 1307 | 1308 | var jsDump = { 1309 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1310 | stack = stack || [ ]; 1311 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1312 | type = typeof parser; 1313 | var inStack = inArray(obj, stack); 1314 | if (inStack != -1) { 1315 | return 'recursion('+(inStack - stack.length)+')'; 1316 | } 1317 | //else 1318 | if (type == 'function') { 1319 | stack.push(obj); 1320 | var res = parser.call( this, obj, stack ); 1321 | stack.pop(); 1322 | return res; 1323 | } 1324 | // else 1325 | return (type == 'string') ? parser : this.parsers.error; 1326 | }, 1327 | typeOf: function( obj ) { 1328 | var type; 1329 | if ( obj === null ) { 1330 | type = "null"; 1331 | } else if (typeof obj === "undefined") { 1332 | type = "undefined"; 1333 | } else if (QUnit.is("RegExp", obj)) { 1334 | type = "regexp"; 1335 | } else if (QUnit.is("Date", obj)) { 1336 | type = "date"; 1337 | } else if (QUnit.is("Function", obj)) { 1338 | type = "function"; 1339 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1340 | type = "window"; 1341 | } else if (obj.nodeType === 9) { 1342 | type = "document"; 1343 | } else if (obj.nodeType) { 1344 | type = "node"; 1345 | } else if ( 1346 | // native arrays 1347 | toString.call( obj ) === "[object Array]" || 1348 | // NodeList objects 1349 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1350 | ) { 1351 | type = "array"; 1352 | } else { 1353 | type = typeof obj; 1354 | } 1355 | return type; 1356 | }, 1357 | separator: function() { 1358 | return this.multiline ? this.HTML ? '
      ' : '\n' : this.HTML ? ' ' : ' '; 1359 | }, 1360 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1361 | if ( !this.multiline ) { 1362 | return ''; 1363 | } 1364 | var chr = this.indentChar; 1365 | if ( this.HTML ) { 1366 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1367 | } 1368 | return new Array( this._depth_ + (extra||0) ).join(chr); 1369 | }, 1370 | up: function( a ) { 1371 | this._depth_ += a || 1; 1372 | }, 1373 | down: function( a ) { 1374 | this._depth_ -= a || 1; 1375 | }, 1376 | setParser: function( name, parser ) { 1377 | this.parsers[name] = parser; 1378 | }, 1379 | // The next 3 are exposed so you can use them 1380 | quote: quote, 1381 | literal: literal, 1382 | join: join, 1383 | // 1384 | _depth_: 1, 1385 | // This is the list of parsers, to modify them, use jsDump.setParser 1386 | parsers: { 1387 | window: '[Window]', 1388 | document: '[Document]', 1389 | error: '[ERROR]', //when no parser is found, shouldn't happen 1390 | unknown: '[Unknown]', 1391 | 'null': 'null', 1392 | 'undefined': 'undefined', 1393 | 'function': function( fn ) { 1394 | var ret = 'function', 1395 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1396 | if ( name ) { 1397 | ret += ' ' + name; 1398 | } 1399 | ret += '('; 1400 | 1401 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1402 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1403 | }, 1404 | array: array, 1405 | nodelist: array, 1406 | 'arguments': array, 1407 | object: function( map, stack ) { 1408 | var ret = [ ], keys, key, val, i; 1409 | QUnit.jsDump.up(); 1410 | if (Object.keys) { 1411 | keys = Object.keys( map ); 1412 | } else { 1413 | keys = []; 1414 | for (key in map) { keys.push( key ); } 1415 | } 1416 | keys.sort(); 1417 | for (i = 0; i < keys.length; i++) { 1418 | key = keys[ i ]; 1419 | val = map[ key ]; 1420 | ret.push( QUnit.jsDump.parse( key, 'key' ) + ': ' + QUnit.jsDump.parse( val, undefined, stack ) ); 1421 | } 1422 | QUnit.jsDump.down(); 1423 | return join( '{', ret, '}' ); 1424 | }, 1425 | node: function( node ) { 1426 | var open = QUnit.jsDump.HTML ? '<' : '<', 1427 | close = QUnit.jsDump.HTML ? '>' : '>'; 1428 | 1429 | var tag = node.nodeName.toLowerCase(), 1430 | ret = open + tag; 1431 | 1432 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1433 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1434 | if ( val ) { 1435 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1436 | } 1437 | } 1438 | return ret + close + open + '/' + tag + close; 1439 | }, 1440 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1441 | var l = fn.length; 1442 | if ( !l ) { 1443 | return ''; 1444 | } 1445 | 1446 | var args = new Array(l); 1447 | while ( l-- ) { 1448 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1449 | } 1450 | return ' ' + args.join(', ') + ' '; 1451 | }, 1452 | key: quote, //object calls it internally, the key part of an item in a map 1453 | functionCode: '[code]', //function calls it internally, it's the content of the function 1454 | attribute: quote, //node calls it internally, it's an html attribute value 1455 | string: quote, 1456 | date: quote, 1457 | regexp: literal, //regex 1458 | number: literal, 1459 | 'boolean': literal 1460 | }, 1461 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1462 | id:'id', 1463 | name:'name', 1464 | 'class':'className' 1465 | }, 1466 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1467 | indentChar:' ',//indentation unit 1468 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1469 | }; 1470 | 1471 | return jsDump; 1472 | }()); 1473 | 1474 | // from Sizzle.js 1475 | function getText( elems ) { 1476 | var ret = "", elem; 1477 | 1478 | for ( var i = 0; elems[i]; i++ ) { 1479 | elem = elems[i]; 1480 | 1481 | // Get the text from text nodes and CDATA nodes 1482 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1483 | ret += elem.nodeValue; 1484 | 1485 | // Traverse everything else, except comment nodes 1486 | } else if ( elem.nodeType !== 8 ) { 1487 | ret += getText( elem.childNodes ); 1488 | } 1489 | } 1490 | 1491 | return ret; 1492 | } 1493 | 1494 | //from jquery.js 1495 | function inArray( elem, array ) { 1496 | if ( array.indexOf ) { 1497 | return array.indexOf( elem ); 1498 | } 1499 | 1500 | for ( var i = 0, length = array.length; i < length; i++ ) { 1501 | if ( array[ i ] === elem ) { 1502 | return i; 1503 | } 1504 | } 1505 | 1506 | return -1; 1507 | } 1508 | 1509 | /* 1510 | * Javascript Diff Algorithm 1511 | * By John Resig (http://ejohn.org/) 1512 | * Modified by Chu Alan "sprite" 1513 | * 1514 | * Released under the MIT license. 1515 | * 1516 | * More Info: 1517 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1518 | * 1519 | * Usage: QUnit.diff(expected, actual) 1520 | * 1521 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1522 | */ 1523 | QUnit.diff = (function() { 1524 | function diff(o, n) { 1525 | var ns = {}; 1526 | var os = {}; 1527 | var i; 1528 | 1529 | for (i = 0; i < n.length; i++) { 1530 | if (ns[n[i]] == null) { 1531 | ns[n[i]] = { 1532 | rows: [], 1533 | o: null 1534 | }; 1535 | } 1536 | ns[n[i]].rows.push(i); 1537 | } 1538 | 1539 | for (i = 0; i < o.length; i++) { 1540 | if (os[o[i]] == null) { 1541 | os[o[i]] = { 1542 | rows: [], 1543 | n: null 1544 | }; 1545 | } 1546 | os[o[i]].rows.push(i); 1547 | } 1548 | 1549 | for (i in ns) { 1550 | if ( !hasOwn.call( ns, i ) ) { 1551 | continue; 1552 | } 1553 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1554 | n[ns[i].rows[0]] = { 1555 | text: n[ns[i].rows[0]], 1556 | row: os[i].rows[0] 1557 | }; 1558 | o[os[i].rows[0]] = { 1559 | text: o[os[i].rows[0]], 1560 | row: ns[i].rows[0] 1561 | }; 1562 | } 1563 | } 1564 | 1565 | for (i = 0; i < n.length - 1; i++) { 1566 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1567 | n[i + 1] == o[n[i].row + 1]) { 1568 | n[i + 1] = { 1569 | text: n[i + 1], 1570 | row: n[i].row + 1 1571 | }; 1572 | o[n[i].row + 1] = { 1573 | text: o[n[i].row + 1], 1574 | row: i + 1 1575 | }; 1576 | } 1577 | } 1578 | 1579 | for (i = n.length - 1; i > 0; i--) { 1580 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1581 | n[i - 1] == o[n[i].row - 1]) { 1582 | n[i - 1] = { 1583 | text: n[i - 1], 1584 | row: n[i].row - 1 1585 | }; 1586 | o[n[i].row - 1] = { 1587 | text: o[n[i].row - 1], 1588 | row: i - 1 1589 | }; 1590 | } 1591 | } 1592 | 1593 | return { 1594 | o: o, 1595 | n: n 1596 | }; 1597 | } 1598 | 1599 | return function(o, n) { 1600 | o = o.replace(/\s+$/, ''); 1601 | n = n.replace(/\s+$/, ''); 1602 | var out = diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/)); 1603 | 1604 | var str = ""; 1605 | var i; 1606 | 1607 | var oSpace = o.match(/\s+/g); 1608 | if (oSpace == null) { 1609 | oSpace = [" "]; 1610 | } 1611 | else { 1612 | oSpace.push(" "); 1613 | } 1614 | var nSpace = n.match(/\s+/g); 1615 | if (nSpace == null) { 1616 | nSpace = [" "]; 1617 | } 1618 | else { 1619 | nSpace.push(" "); 1620 | } 1621 | 1622 | if (out.n.length === 0) { 1623 | for (i = 0; i < out.o.length; i++) { 1624 | str += '' + out.o[i] + oSpace[i] + ""; 1625 | } 1626 | } 1627 | else { 1628 | if (out.n[0].text == null) { 1629 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1630 | str += '' + out.o[n] + oSpace[n] + ""; 1631 | } 1632 | } 1633 | 1634 | for (i = 0; i < out.n.length; i++) { 1635 | if (out.n[i].text == null) { 1636 | str += '' + out.n[i] + nSpace[i] + ""; 1637 | } 1638 | else { 1639 | var pre = ""; 1640 | 1641 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1642 | pre += '' + out.o[n] + oSpace[n] + ""; 1643 | } 1644 | str += " " + out.n[i].text + nSpace[i] + pre; 1645 | } 1646 | } 1647 | } 1648 | 1649 | return str; 1650 | }; 1651 | }()); 1652 | 1653 | // for CommonJS environments, export everything 1654 | if ( typeof exports !== "undefined" || typeof require !== "undefined" ) { 1655 | extend(exports, QUnit); 1656 | } 1657 | 1658 | // get at whatever the global object is, like window in browsers 1659 | }( (function() {return this;}.call()) )); 1660 | --------------------------------------------------------------------------------