├── .gitignore ├── CONTRIBUTORS.md ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── ajax-retry.jquery.json ├── dist ├── jquery.ajax-retry.js └── jquery.ajax-retry.min.js ├── libs ├── jquery │ └── jquery.js ├── qunit │ ├── qunit.css │ └── qunit.js └── sinon │ └── sinon.js ├── package.json ├── src └── jquery.ajax-retry.js └── test ├── jquery.ajax-retry.html └── jquery.ajax-retry_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # gcc coverage testing tool files 2 | 3 | *.gcno 4 | *.gcda 5 | *.gcov 6 | 7 | # npm modules 8 | node_modules 9 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | [John K. Paul](http://twitter.com/johnkpaul) 2 | [Corey Frang](http://twitter.com/gnarf37) 3 | [Oleg Slobodskoi](http://twitter.com/oleg008) 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | require('load-grunt-tasks')(grunt); 5 | 6 | // Project configuration. 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | concat: { 10 | dist: { 11 | src: ['src/<%= pkg.name %>.js'], 12 | dest: 'dist/<%= pkg.name %>.js' 13 | } 14 | }, 15 | uglify: { 16 | dist: { 17 | src: ['<%= concat.dist.dest %>'], 18 | dest: 'dist/<%= pkg.name %>.min.js' 19 | } 20 | }, 21 | qunit: { 22 | files: ['test/**/*.html'] 23 | }, 24 | jshint: { 25 | files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'], 26 | options: { 27 | curly: true, 28 | eqeqeq: true, 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 | ok: true, 42 | require: true 43 | } 44 | } 45 | } 46 | }); 47 | 48 | // Default task. 49 | grunt.registerTask('default', ['jshint','qunit', 'concat', 'uglify', 'manifest']); 50 | 51 | grunt.registerTask( "manifest", function() { 52 | var pkg = grunt.config( "pkg" ); 53 | grunt.file.write( "ajax-retry.jquery.json", JSON.stringify({ 54 | name: "ajax-retry", 55 | title: pkg.title, 56 | description: pkg.description, 57 | keywords: pkg.keywords, 58 | version: pkg.version, 59 | author: pkg.author, 60 | maintainers: pkg.maintainers, 61 | licenses: pkg.licenses.map(function( license ) { 62 | license.url = license.url.replace( "master", pkg.version ); 63 | return license; 64 | }), 65 | bugs: pkg.bugs, 66 | homepage: pkg.homepage, 67 | docs: pkg.homepage, 68 | dependencies: { 69 | jquery: ">=1.5" 70 | } 71 | }, null, " " ) ); 72 | }); 73 | 74 | }; 75 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 John Paul 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery Ajax Retry 2 | 3 | Retry ajax calls using the deferred API 4 | 5 | ## Getting Started 6 | Download the [production version][min] or the [development version][max]. 7 | 8 | [min]: https://raw.github.com/johnkpaul/jquery-ajax-retry/master/dist/jquery.ajax-retry.min.js 9 | [max]: https://raw.github.com/johnkpaul/jquery-ajax-retry/master/dist/jquery.ajax-retry.js 10 | 11 | In your web page: 12 | 13 | ```html 14 | 15 | 16 | 37 | ``` 38 | 39 | ## Contributing 40 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [grunt](https://github.com/cowboy/grunt). 41 | 42 | _Also, please don't edit files in the "dist" subdirectory as they are generated via grunt. You'll find source code in the "src" subdirectory!_ 43 | 44 | ## Release History 45 | _(Nothing yet)_ 46 | 47 | ## License 48 | Copyright (c) 2012 John Paul 49 | Licensed under the MIT license. 50 | -------------------------------------------------------------------------------- /ajax-retry.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajax-retry", 3 | "title": "jQuery Ajax Retry", 4 | "description": "Retry ajax calls using the deferred API", 5 | "keywords": [ 6 | "ajax", 7 | "retry" 8 | ], 9 | "version": "1.0.0", 10 | "author": { 11 | "name": "John Paul", 12 | "email": "john@johnkpaul.com", 13 | "url": "http://www.johnkpaul.com" 14 | }, 15 | "maintainers": [ 16 | { 17 | "name": "John Paul", 18 | "email": "john@johnkpaul.com", 19 | "url": "http://www.johnkpaul.com" 20 | } 21 | ], 22 | "licenses": [ 23 | { 24 | "type": "MIT", 25 | "url": "https://github.com/johnkpaul/jquery-ajax-retry/blob/1.0.0/LICENSE-MIT" 26 | } 27 | ], 28 | "bugs": { 29 | "url": "https://github.com/johnkpaul/jquery-ajax-retry/issues" 30 | }, 31 | "homepage": "https://github.com/johnkpaul/jquery-ajax-retry", 32 | "docs": "https://github.com/johnkpaul/jquery-ajax-retry", 33 | "dependencies": { 34 | "jquery": ">=1.5" 35 | } 36 | } -------------------------------------------------------------------------------- /dist/jquery.ajax-retry.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.ajax-retry 3 | * https://github.com/johnkpaul/jquery-ajax-retry 4 | * 5 | * Copyright (c) 2012 John Paul 6 | * Licensed under the MIT license. 7 | */ 8 | (function(factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as an anonymous module. 11 | define(['jquery'], factory); 12 | } else if (typeof exports === 'object') { 13 | // Node/CommonJS 14 | factory(require('jquery')); 15 | } else { 16 | // Browser globals 17 | factory(jQuery); 18 | } 19 | })(function($) { 20 | 21 | // enhance all ajax requests with our retry API 22 | $.ajaxPrefilter(function(options, originalOptions, jqXHR) { 23 | jqXHR.retry = function(opts) { 24 | if(opts.timeout) { 25 | this.timeout = opts.timeout; 26 | } 27 | if (opts.statusCodes) { 28 | this.statusCodes = opts.statusCodes; 29 | } 30 | return this.pipe(null, pipeFailRetry(this, opts)); 31 | }; 32 | }); 33 | 34 | // generates a fail pipe function that will retry `jqXHR` `times` more times 35 | function pipeFailRetry(jqXHR, opts) { 36 | var times = opts.times; 37 | var timeout = jqXHR.timeout; 38 | 39 | // takes failure data as input, returns a new deferred 40 | return function(input, status, msg) { 41 | var ajaxOptions = this; 42 | var output = new $.Deferred(); 43 | var retryAfter = jqXHR.getResponseHeader('Retry-After'); 44 | 45 | // whenever we do make this request, pipe its output to our deferred 46 | function nextRequest() { 47 | $.ajax(ajaxOptions) 48 | .retry({times: times - 1, timeout: opts.timeout, statusCodes: opts.statusCodes}) 49 | .pipe(output.resolve, output.reject); 50 | } 51 | 52 | if (times > 1 && (!jqXHR.statusCodes || $.inArray(input.status, jqXHR.statusCodes) > -1)) { 53 | // implement Retry-After rfc 54 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 55 | if (retryAfter) { 56 | // it must be a date 57 | if (isNaN(retryAfter)) { 58 | timeout = new Date(retryAfter).getTime() - $.now(); 59 | // its a number in seconds 60 | } else { 61 | timeout = parseInt(retryAfter, 10) * 1000; 62 | } 63 | // ensure timeout is a positive number 64 | if (isNaN(timeout) || timeout < 0) { 65 | timeout = jqXHR.timeout; 66 | } 67 | } 68 | 69 | if (timeout !== undefined){ 70 | setTimeout(nextRequest, timeout); 71 | } else { 72 | nextRequest(); 73 | } 74 | } else { 75 | // no times left, reject our deferred with the current arguments 76 | output.rejectWith(this, arguments); 77 | } 78 | 79 | return output; 80 | }; 81 | } 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /dist/jquery.ajax-retry.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){function b(b,c){var d=c.times,e=b.timeout;return function(f){function g(){a.ajax(h).retry({times:d-1,timeout:c.timeout,statusCodes:c.statusCodes}).pipe(i.resolve,i.reject)}var h=this,i=new a.Deferred,j=b.getResponseHeader("Retry-After");return d>1&&(!b.statusCodes||a.inArray(f.status,b.statusCodes)>-1)?(j&&(e=isNaN(j)?new Date(j).getTime()-a.now():1e3*parseInt(j,10),(isNaN(e)||0>e)&&(e=b.timeout)),void 0!==e?setTimeout(g,e):g()):i.rejectWith(this,arguments),i}}a.ajaxPrefilter(function(a,c,d){d.retry=function(a){return a.timeout&&(this.timeout=a.timeout),a.statusCodes&&(this.statusCodes=a.statusCodes),this.pipe(null,b(this,a))}})}); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 gurantee 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 paramaters 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 annotaion 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 modifers 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 proove 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 enviroments, 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 | -------------------------------------------------------------------------------- /libs/sinon/sinon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sinon.JS 1.3.4, 2012/04/16 3 | * 4 | * @author Christian Johansen (christian@cjohansen.no) 5 | * 6 | * (The BSD License) 7 | * 8 | * Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no 9 | * All rights reserved. 10 | * 11 | * Redistribution and use in source and binary forms, with or without modification, 12 | * are permitted provided that the following conditions are met: 13 | * 14 | * * Redistributions of source code must retain the above copyright notice, 15 | * this list of conditions and the following disclaimer. 16 | * * Redistributions in binary form must reproduce the above copyright notice, 17 | * this list of conditions and the following disclaimer in the documentation 18 | * and/or other materials provided with the distribution. 19 | * * Neither the name of Christian Johansen nor the names of his contributors 20 | * may be used to endorse or promote products derived from this software 21 | * without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 32 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | "use strict"; 36 | var sinon = (function () { 37 | var buster = (function (buster, setTimeout) { 38 | function extend(target) { 39 | if (!target) { 40 | return; 41 | } 42 | 43 | for (var i = 1, l = arguments.length, prop; i < l; ++i) { 44 | for (prop in arguments[i]) { 45 | target[prop] = arguments[i][prop]; 46 | } 47 | } 48 | 49 | return target; 50 | } 51 | 52 | var div = typeof document != "undefined" && document.createElement("div"); 53 | 54 | return extend(buster, { 55 | bind: function (obj, methOrProp) { 56 | var method = typeof methOrProp == "string" ? obj[methOrProp] : methOrProp; 57 | var args = Array.prototype.slice.call(arguments, 2); 58 | 59 | return function () { 60 | var allArgs = args.concat(Array.prototype.slice.call(arguments)); 61 | return method.apply(obj, allArgs); 62 | }; 63 | }, 64 | 65 | create: (function () { 66 | function F() {} 67 | 68 | return function create(object) { 69 | F.prototype = object; 70 | return new F(); 71 | } 72 | }()), 73 | 74 | extend: extend, 75 | 76 | nextTick: function (callback) { 77 | if (typeof process != "undefined" && process.nextTick) { 78 | return process.nextTick(callback); 79 | } 80 | 81 | setTimeout(callback, 0); 82 | }, 83 | 84 | functionName: function (func) { 85 | if (!func) return ""; 86 | if (func.displayName) return func.displayName; 87 | if (func.name) return func.name; 88 | 89 | var matches = func.toString().match(/function\s+([^\(]+)/m); 90 | return matches && matches[1] || ""; 91 | }, 92 | 93 | isNode: function (obj) { 94 | if (!div) return false; 95 | 96 | try { 97 | obj.appendChild(div); 98 | obj.removeChild(div); 99 | } catch (e) { 100 | return false; 101 | } 102 | 103 | return true; 104 | }, 105 | 106 | isElement: function (obj) { 107 | return obj && buster.isNode(obj) && obj.nodeType === 1; 108 | } 109 | }); 110 | }(buster || {}, setTimeout)); 111 | 112 | if (typeof module == "object" && typeof require == "function") { 113 | module.exports = buster; 114 | buster.eventEmitter = require("./buster-event-emitter"); 115 | 116 | Object.defineProperty(buster, "defineVersionGetter", { 117 | get: function () { 118 | return require("./define-version-getter"); 119 | } 120 | }); 121 | } 122 | if (typeof buster === "undefined") { 123 | var buster = {}; 124 | } 125 | 126 | if (typeof module === "object" && typeof require === "function") { 127 | buster = require("buster-core"); 128 | } 129 | 130 | buster.format = buster.format || {}; 131 | buster.format.excludeConstructors = ["Object", /^.$/]; 132 | buster.format.quoteStrings = true; 133 | 134 | buster.format.ascii = (function () { 135 | 136 | function keys(object) { 137 | var k = Object.keys && Object.keys(object) || []; 138 | 139 | if (k.length == 0) { 140 | for (var prop in object) { 141 | if (object.hasOwnProperty(prop)) { 142 | k.push(prop); 143 | } 144 | } 145 | } 146 | 147 | return k.sort(); 148 | } 149 | 150 | function isCircular(object, objects) { 151 | if (typeof object != "object") { 152 | return false; 153 | } 154 | 155 | for (var i = 0, l = objects.length; i < l; ++i) { 156 | if (objects[i] === object) { 157 | return true; 158 | } 159 | } 160 | 161 | return false; 162 | } 163 | 164 | function ascii(object, processed, indent) { 165 | if (typeof object == "string") { 166 | var quote = typeof this.quoteStrings != "boolean" || this.quoteStrings; 167 | return processed || quote ? '"' + object + '"' : object; 168 | } 169 | 170 | if (typeof object == "function" && !(object instanceof RegExp)) { 171 | return ascii.func(object); 172 | } 173 | 174 | processed = processed || []; 175 | 176 | if (isCircular(object, processed)) { 177 | return "[Circular]"; 178 | } 179 | 180 | if (Object.prototype.toString.call(object) == "[object Array]") { 181 | return ascii.array.call(this, object); 182 | } 183 | 184 | if (!object) { 185 | return "" + object; 186 | } 187 | 188 | if (buster.isElement(object)) { 189 | return ascii.element(object); 190 | } 191 | 192 | if (typeof object.toString == "function" && 193 | object.toString !== Object.prototype.toString) { 194 | return object.toString(); 195 | } 196 | 197 | return ascii.object.call(this, object, processed, indent); 198 | } 199 | 200 | ascii.func = function (func) { 201 | return "function " + buster.functionName(func) + "() {}"; 202 | }; 203 | 204 | ascii.array = function (array, processed) { 205 | processed = processed || []; 206 | processed.push(array); 207 | var pieces = []; 208 | 209 | for (var i = 0, l = array.length; i < l; ++i) { 210 | pieces.push(ascii.call(this, array[i], processed)); 211 | } 212 | 213 | return "[" + pieces.join(", ") + "]"; 214 | }; 215 | 216 | ascii.object = function (object, processed, indent) { 217 | processed = processed || []; 218 | processed.push(object); 219 | indent = indent || 0; 220 | var pieces = [], properties = keys(object), prop, str, obj; 221 | var is = ""; 222 | var length = 3; 223 | 224 | for (var i = 0, l = indent; i < l; ++i) { 225 | is += " "; 226 | } 227 | 228 | for (i = 0, l = properties.length; i < l; ++i) { 229 | prop = properties[i]; 230 | obj = object[prop]; 231 | 232 | if (isCircular(obj, processed)) { 233 | str = "[Circular]"; 234 | } else { 235 | str = ascii.call(this, obj, processed, indent + 2); 236 | } 237 | 238 | str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str; 239 | length += str.length; 240 | pieces.push(str); 241 | } 242 | 243 | var cons = ascii.constructorName.call(this, object); 244 | var prefix = cons ? "[" + cons + "] " : "" 245 | 246 | return (length + indent) > 80 ? 247 | prefix + "{\n " + is + pieces.join(",\n " + is) + "\n" + is + "}" : 248 | prefix + "{ " + pieces.join(", ") + " }"; 249 | }; 250 | 251 | ascii.element = function (element) { 252 | var tagName = element.tagName.toLowerCase(); 253 | var attrs = element.attributes, attribute, pairs = [], attrName; 254 | 255 | for (var i = 0, l = attrs.length; i < l; ++i) { 256 | attribute = attrs.item(i); 257 | attrName = attribute.nodeName.toLowerCase().replace("html:", ""); 258 | 259 | if (attrName == "contenteditable" && attribute.nodeValue == "inherit") { 260 | continue; 261 | } 262 | 263 | if (!!attribute.nodeValue) { 264 | pairs.push(attrName + "=\"" + attribute.nodeValue + "\""); 265 | } 266 | } 267 | 268 | var formatted = "<" + tagName + (pairs.length > 0 ? " " : ""); 269 | var content = element.innerHTML; 270 | 271 | if (content.length > 20) { 272 | content = content.substr(0, 20) + "[...]"; 273 | } 274 | 275 | var res = formatted + pairs.join(" ") + ">" + content + ""; 276 | 277 | return res.replace(/ contentEditable="inherit"/, ""); 278 | }; 279 | 280 | ascii.constructorName = function (object) { 281 | var name = buster.functionName(object && object.constructor); 282 | var excludes = this.excludeConstructors || buster.format.excludeConstructors || []; 283 | 284 | for (var i = 0, l = excludes.length; i < l; ++i) { 285 | if (typeof excludes[i] == "string" && excludes[i] == name) { 286 | return ""; 287 | } else if (excludes[i].test && excludes[i].test(name)) { 288 | return ""; 289 | } 290 | } 291 | 292 | return name; 293 | }; 294 | 295 | return ascii; 296 | }()); 297 | 298 | if (typeof module != "undefined") { 299 | module.exports = buster.format; 300 | } 301 | /*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ 302 | /*global module, require, __dirname, document*/ 303 | /** 304 | * Sinon core utilities. For internal use only. 305 | * 306 | * @author Christian Johansen (christian@cjohansen.no) 307 | * @license BSD 308 | * 309 | * Copyright (c) 2010-2011 Christian Johansen 310 | */ 311 | 312 | var sinon = (function (buster) { 313 | var div = typeof document != "undefined" && document.createElement("div"); 314 | var hasOwn = Object.prototype.hasOwnProperty; 315 | 316 | function isDOMNode(obj) { 317 | var success = false; 318 | 319 | try { 320 | obj.appendChild(div); 321 | success = div.parentNode == obj; 322 | } catch (e) { 323 | return false; 324 | } finally { 325 | try { 326 | obj.removeChild(div); 327 | } catch (e) { 328 | // Remove failed, not much we can do about that 329 | } 330 | } 331 | 332 | return success; 333 | } 334 | 335 | function isElement(obj) { 336 | return div && obj && obj.nodeType === 1 && isDOMNode(obj); 337 | } 338 | 339 | function isFunction(obj) { 340 | return !!(obj && obj.constructor && obj.call && obj.apply); 341 | } 342 | 343 | function mirrorProperties(target, source) { 344 | for (var prop in source) { 345 | if (!hasOwn.call(target, prop)) { 346 | target[prop] = source[prop]; 347 | } 348 | } 349 | } 350 | 351 | var sinon = { 352 | wrapMethod: function wrapMethod(object, property, method) { 353 | if (!object) { 354 | throw new TypeError("Should wrap property of object"); 355 | } 356 | 357 | if (typeof method != "function") { 358 | throw new TypeError("Method wrapper should be function"); 359 | } 360 | 361 | var wrappedMethod = object[property]; 362 | 363 | if (!isFunction(wrappedMethod)) { 364 | throw new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " + 365 | property + " as function"); 366 | } 367 | 368 | if (wrappedMethod.restore && wrappedMethod.restore.sinon) { 369 | throw new TypeError("Attempted to wrap " + property + " which is already wrapped"); 370 | } 371 | 372 | if (wrappedMethod.calledBefore) { 373 | var verb = !!wrappedMethod.returns ? "stubbed" : "spied on"; 374 | throw new TypeError("Attempted to wrap " + property + " which is already " + verb); 375 | } 376 | 377 | // IE 8 does not support hasOwnProperty on the window object. 378 | var owned = hasOwn.call(object, property); 379 | object[property] = method; 380 | method.displayName = property; 381 | 382 | method.restore = function () { 383 | if(owned) { 384 | object[property] = wrappedMethod; 385 | } else { 386 | delete object[property]; 387 | } 388 | }; 389 | 390 | method.restore.sinon = true; 391 | mirrorProperties(method, wrappedMethod); 392 | 393 | return method; 394 | }, 395 | 396 | extend: function extend(target) { 397 | for (var i = 1, l = arguments.length; i < l; i += 1) { 398 | for (var prop in arguments[i]) { 399 | if (arguments[i].hasOwnProperty(prop)) { 400 | target[prop] = arguments[i][prop]; 401 | } 402 | 403 | // DONT ENUM bug, only care about toString 404 | if (arguments[i].hasOwnProperty("toString") && 405 | arguments[i].toString != target.toString) { 406 | target.toString = arguments[i].toString; 407 | } 408 | } 409 | } 410 | 411 | return target; 412 | }, 413 | 414 | create: function create(proto) { 415 | var F = function () {}; 416 | F.prototype = proto; 417 | return new F(); 418 | }, 419 | 420 | deepEqual: function deepEqual(a, b) { 421 | if (typeof a != "object" || typeof b != "object") { 422 | return a === b; 423 | } 424 | 425 | if (isElement(a) || isElement(b)) { 426 | return a === b; 427 | } 428 | 429 | if (a === b) { 430 | return true; 431 | } 432 | 433 | var aString = Object.prototype.toString.call(a); 434 | if (aString != Object.prototype.toString.call(b)) { 435 | return false; 436 | } 437 | 438 | if (aString == "[object Array]") { 439 | if (a.length !== b.length) { 440 | return false; 441 | } 442 | 443 | for (var i = 0, l = a.length; i < l; i += 1) { 444 | if (!deepEqual(a[i], b[i])) { 445 | return false; 446 | } 447 | } 448 | 449 | return true; 450 | } 451 | 452 | var prop, aLength = 0, bLength = 0; 453 | 454 | for (prop in a) { 455 | aLength += 1; 456 | 457 | if (!deepEqual(a[prop], b[prop])) { 458 | return false; 459 | } 460 | } 461 | 462 | for (prop in b) { 463 | bLength += 1; 464 | } 465 | 466 | if (aLength != bLength) { 467 | return false; 468 | } 469 | 470 | return true; 471 | }, 472 | 473 | functionName: function functionName(func) { 474 | var name = func.displayName || func.name; 475 | 476 | // Use function decomposition as a last resort to get function 477 | // name. Does not rely on function decomposition to work - if it 478 | // doesn't debugging will be slightly less informative 479 | // (i.e. toString will say 'spy' rather than 'myFunc'). 480 | if (!name) { 481 | var matches = func.toString().match(/function ([^\s\(]+)/); 482 | name = matches && matches[1]; 483 | } 484 | 485 | return name; 486 | }, 487 | 488 | functionToString: function toString() { 489 | if (this.getCall && this.callCount) { 490 | var thisValue, prop, i = this.callCount; 491 | 492 | while (i--) { 493 | thisValue = this.getCall(i).thisValue; 494 | 495 | for (prop in thisValue) { 496 | if (thisValue[prop] === this) { 497 | return prop; 498 | } 499 | } 500 | } 501 | } 502 | 503 | return this.displayName || "sinon fake"; 504 | }, 505 | 506 | getConfig: function (custom) { 507 | var config = {}; 508 | custom = custom || {}; 509 | var defaults = sinon.defaultConfig; 510 | 511 | for (var prop in defaults) { 512 | if (defaults.hasOwnProperty(prop)) { 513 | config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop]; 514 | } 515 | } 516 | 517 | return config; 518 | }, 519 | 520 | format: function (val) { 521 | return "" + val; 522 | }, 523 | 524 | defaultConfig: { 525 | injectIntoThis: true, 526 | injectInto: null, 527 | properties: ["spy", "stub", "mock", "clock", "server", "requests"], 528 | useFakeTimers: true, 529 | useFakeServer: true 530 | }, 531 | 532 | timesInWords: function timesInWords(count) { 533 | return count == 1 && "once" || 534 | count == 2 && "twice" || 535 | count == 3 && "thrice" || 536 | (count || 0) + " times"; 537 | }, 538 | 539 | calledInOrder: function (spies) { 540 | for (var i = 1, l = spies.length; i < l; i++) { 541 | if (!spies[i - 1].calledBefore(spies[i])) { 542 | return false; 543 | } 544 | } 545 | 546 | return true; 547 | }, 548 | 549 | orderByFirstCall: function (spies) { 550 | return spies.sort(function (a, b) { 551 | // uuid, won't ever be equal 552 | var aCall = a.getCall(0); 553 | var bCall = b.getCall(0); 554 | var aId = aCall && aCall.callId || -1; 555 | var bId = bCall && bCall.callId || -1; 556 | 557 | return aId < bId ? -1 : 1; 558 | }); 559 | }, 560 | 561 | log: function () {}, 562 | 563 | logError: function (label, err) { 564 | var msg = label + " threw exception: " 565 | sinon.log(msg + "[" + err.name + "] " + err.message); 566 | if (err.stack) { sinon.log(err.stack); } 567 | 568 | setTimeout(function () { 569 | err.message = msg + err.message; 570 | throw err; 571 | }, 0); 572 | } 573 | }; 574 | 575 | var isNode = typeof module == "object" && typeof require == "function"; 576 | 577 | if (isNode) { 578 | try { 579 | buster = { format: require("buster-format") }; 580 | } catch (e) {} 581 | module.exports = sinon; 582 | module.exports.spy = require("./sinon/spy"); 583 | module.exports.stub = require("./sinon/stub"); 584 | module.exports.mock = require("./sinon/mock"); 585 | module.exports.collection = require("./sinon/collection"); 586 | module.exports.assert = require("./sinon/assert"); 587 | module.exports.sandbox = require("./sinon/sandbox"); 588 | module.exports.test = require("./sinon/test"); 589 | module.exports.testCase = require("./sinon/test_case"); 590 | module.exports.assert = require("./sinon/assert"); 591 | } 592 | 593 | if (buster) { 594 | var formatter = sinon.create(buster.format); 595 | formatter.quoteStrings = false; 596 | sinon.format = function () { 597 | return formatter.ascii.apply(formatter, arguments); 598 | }; 599 | } else if (isNode) { 600 | try { 601 | var util = require("util"); 602 | sinon.format = function (value) { 603 | return typeof value == "object" ? util.inspect(value) : value; 604 | }; 605 | } catch (e) { 606 | /* Node, but no util module - would be very old, but better safe than 607 | sorry */ 608 | } 609 | } 610 | 611 | return sinon; 612 | }(typeof buster == "object" && buster)); 613 | 614 | /* @depend ../sinon.js */ 615 | /*jslint eqeqeq: false, onevar: false, plusplus: false*/ 616 | /*global module, require, sinon*/ 617 | /** 618 | * Spy functions 619 | * 620 | * @author Christian Johansen (christian@cjohansen.no) 621 | * @license BSD 622 | * 623 | * Copyright (c) 2010-2011 Christian Johansen 624 | */ 625 | 626 | (function (sinon) { 627 | var commonJSModule = typeof module == "object" && typeof require == "function"; 628 | var spyCall; 629 | var callId = 0; 630 | var push = [].push; 631 | var slice = Array.prototype.slice; 632 | 633 | if (!sinon && commonJSModule) { 634 | sinon = require("../sinon"); 635 | } 636 | 637 | if (!sinon) { 638 | return; 639 | } 640 | 641 | function spy(object, property) { 642 | if (!property && typeof object == "function") { 643 | return spy.create(object); 644 | } 645 | 646 | if (!object || !property) { 647 | return spy.create(function () {}); 648 | } 649 | 650 | var method = object[property]; 651 | return sinon.wrapMethod(object, property, spy.create(method)); 652 | } 653 | 654 | sinon.extend(spy, (function () { 655 | 656 | function delegateToCalls(api, method, matchAny, actual, notCalled) { 657 | api[method] = function () { 658 | if (!this.called) { 659 | if (notCalled) { 660 | return notCalled.apply(this, arguments); 661 | } 662 | return false; 663 | } 664 | 665 | var currentCall; 666 | var matches = 0; 667 | 668 | for (var i = 0, l = this.callCount; i < l; i += 1) { 669 | currentCall = this.getCall(i); 670 | 671 | if (currentCall[actual || method].apply(currentCall, arguments)) { 672 | matches += 1; 673 | 674 | if (matchAny) { 675 | return true; 676 | } 677 | } 678 | } 679 | 680 | return matches === this.callCount; 681 | }; 682 | } 683 | 684 | function matchingFake(fakes, args, strict) { 685 | if (!fakes) { 686 | return; 687 | } 688 | 689 | var alen = args.length; 690 | 691 | for (var i = 0, l = fakes.length; i < l; i++) { 692 | if (fakes[i].matches(args, strict)) { 693 | return fakes[i]; 694 | } 695 | } 696 | } 697 | 698 | function incrementCallCount() { 699 | this.called = true; 700 | this.callCount += 1; 701 | this.calledOnce = this.callCount == 1; 702 | this.calledTwice = this.callCount == 2; 703 | this.calledThrice = this.callCount == 3; 704 | } 705 | 706 | function createCallProperties() { 707 | this.firstCall = this.getCall(0); 708 | this.secondCall = this.getCall(1); 709 | this.thirdCall = this.getCall(2); 710 | this.lastCall = this.getCall(this.callCount - 1); 711 | } 712 | 713 | var uuid = 0; 714 | 715 | // Public API 716 | var spyApi = { 717 | reset: function () { 718 | this.called = false; 719 | this.calledOnce = false; 720 | this.calledTwice = false; 721 | this.calledThrice = false; 722 | this.callCount = 0; 723 | this.firstCall = null; 724 | this.secondCall = null; 725 | this.thirdCall = null; 726 | this.lastCall = null; 727 | this.args = []; 728 | this.returnValues = []; 729 | this.thisValues = []; 730 | this.exceptions = []; 731 | this.callIds = []; 732 | }, 733 | 734 | create: function create(func) { 735 | var name; 736 | 737 | if (typeof func != "function") { 738 | func = function () {}; 739 | } else { 740 | name = sinon.functionName(func); 741 | } 742 | 743 | function proxy() { 744 | return proxy.invoke(func, this, slice.call(arguments)); 745 | } 746 | 747 | sinon.extend(proxy, spy); 748 | delete proxy.create; 749 | sinon.extend(proxy, func); 750 | 751 | proxy.reset(); 752 | proxy.prototype = func.prototype; 753 | proxy.displayName = name || "spy"; 754 | proxy.toString = sinon.functionToString; 755 | proxy._create = sinon.spy.create; 756 | proxy.id = "spy#" + uuid++; 757 | 758 | return proxy; 759 | }, 760 | 761 | invoke: function invoke(func, thisValue, args) { 762 | var matching = matchingFake(this.fakes, args); 763 | var exception, returnValue; 764 | 765 | incrementCallCount.call(this); 766 | push.call(this.thisValues, thisValue); 767 | push.call(this.args, args); 768 | push.call(this.callIds, callId++); 769 | 770 | try { 771 | if (matching) { 772 | returnValue = matching.invoke(func, thisValue, args); 773 | } else { 774 | returnValue = (this.func || func).apply(thisValue, args); 775 | } 776 | } catch (e) { 777 | push.call(this.returnValues, undefined); 778 | exception = e; 779 | throw e; 780 | } finally { 781 | push.call(this.exceptions, exception); 782 | } 783 | 784 | push.call(this.returnValues, returnValue); 785 | 786 | createCallProperties.call(this); 787 | 788 | return returnValue; 789 | }, 790 | 791 | getCall: function getCall(i) { 792 | if (i < 0 || i >= this.callCount) { 793 | return null; 794 | } 795 | 796 | return spyCall.create(this, this.thisValues[i], this.args[i], 797 | this.returnValues[i], this.exceptions[i], 798 | this.callIds[i]); 799 | }, 800 | 801 | calledBefore: function calledBefore(spyFn) { 802 | if (!this.called) { 803 | return false; 804 | } 805 | 806 | if (!spyFn.called) { 807 | return true; 808 | } 809 | 810 | return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1]; 811 | }, 812 | 813 | calledAfter: function calledAfter(spyFn) { 814 | if (!this.called || !spyFn.called) { 815 | return false; 816 | } 817 | 818 | return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1]; 819 | }, 820 | 821 | withArgs: function () { 822 | var args = slice.call(arguments); 823 | 824 | if (this.fakes) { 825 | var match = matchingFake(this.fakes, args, true); 826 | 827 | if (match) { 828 | return match; 829 | } 830 | } else { 831 | this.fakes = []; 832 | } 833 | 834 | var original = this; 835 | var fake = this._create(); 836 | fake.matchingAguments = args; 837 | push.call(this.fakes, fake); 838 | 839 | fake.withArgs = function () { 840 | return original.withArgs.apply(original, arguments); 841 | }; 842 | 843 | for (var i = 0; i < this.args.length; i++) { 844 | if (fake.matches(this.args[i])) { 845 | incrementCallCount.call(fake); 846 | push.call(fake.thisValues, this.thisValues[i]); 847 | push.call(fake.args, this.args[i]); 848 | push.call(fake.returnValues, this.returnValues[i]); 849 | push.call(fake.exceptions, this.exceptions[i]); 850 | push.call(fake.callIds, this.callIds[i]); 851 | } 852 | } 853 | createCallProperties.call(fake); 854 | 855 | return fake; 856 | }, 857 | 858 | matches: function (args, strict) { 859 | var margs = this.matchingAguments; 860 | 861 | if (margs.length <= args.length && 862 | sinon.deepEqual(margs, args.slice(0, margs.length))) { 863 | return !strict || margs.length == args.length; 864 | } 865 | }, 866 | 867 | printf: function (format) { 868 | var spy = this; 869 | var args = slice.call(arguments, 1); 870 | var formatter; 871 | 872 | return (format || "").replace(/%(.)/g, function (match, specifyer) { 873 | formatter = spyApi.formatters[specifyer]; 874 | 875 | if (typeof formatter == "function") { 876 | return formatter.call(null, spy, args); 877 | } else if (!isNaN(parseInt(specifyer), 10)) { 878 | return sinon.format(args[specifyer - 1]); 879 | } 880 | 881 | return "%" + specifyer; 882 | }); 883 | } 884 | }; 885 | 886 | delegateToCalls(spyApi, "calledOn", true); 887 | delegateToCalls(spyApi, "alwaysCalledOn", false, "calledOn"); 888 | delegateToCalls(spyApi, "calledWith", true); 889 | delegateToCalls(spyApi, "alwaysCalledWith", false, "calledWith"); 890 | delegateToCalls(spyApi, "calledWithExactly", true); 891 | delegateToCalls(spyApi, "alwaysCalledWithExactly", false, "calledWithExactly"); 892 | delegateToCalls(spyApi, "neverCalledWith", false, "notCalledWith", 893 | function () { return true; }); 894 | delegateToCalls(spyApi, "threw", true); 895 | delegateToCalls(spyApi, "alwaysThrew", false, "threw"); 896 | delegateToCalls(spyApi, "returned", true); 897 | delegateToCalls(spyApi, "alwaysReturned", false, "returned"); 898 | delegateToCalls(spyApi, "calledWithNew", true); 899 | delegateToCalls(spyApi, "alwaysCalledWithNew", false, "calledWithNew"); 900 | delegateToCalls(spyApi, "callArg", false, "callArgWith", function () { 901 | throw new Error(this.toString() + " cannot call arg since it was not yet invoked."); 902 | }); 903 | spyApi.callArgWith = spyApi.callArg; 904 | delegateToCalls(spyApi, "yield", false, "yield", function () { 905 | throw new Error(this.toString() + " cannot yield since it was not yet invoked."); 906 | }); 907 | // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode. 908 | spyApi.invokeCallback = spyApi.yield; 909 | delegateToCalls(spyApi, "yieldTo", false, "yieldTo", function (property) { 910 | throw new Error(this.toString() + " cannot yield to '" + property + 911 | "' since it was not yet invoked."); 912 | }); 913 | 914 | spyApi.formatters = { 915 | "c": function (spy) { 916 | return sinon.timesInWords(spy.callCount); 917 | }, 918 | 919 | "n": function (spy) { 920 | return spy.toString(); 921 | }, 922 | 923 | "C": function (spy) { 924 | var calls = []; 925 | 926 | for (var i = 0, l = spy.callCount; i < l; ++i) { 927 | push.call(calls, " " + spy.getCall(i).toString()); 928 | } 929 | 930 | return calls.length > 0 ? "\n" + calls.join("\n") : ""; 931 | }, 932 | 933 | "t": function (spy) { 934 | var objects = []; 935 | 936 | for (var i = 0, l = spy.callCount; i < l; ++i) { 937 | push.call(objects, sinon.format(spy.thisValues[i])); 938 | } 939 | 940 | return objects.join(", "); 941 | }, 942 | 943 | "*": function (spy, args) { 944 | return args.join(", "); 945 | } 946 | }; 947 | 948 | return spyApi; 949 | }())); 950 | 951 | spyCall = (function () { 952 | 953 | function throwYieldError(proxy, text, args) { 954 | var msg = sinon.functionName(proxy) + text; 955 | if (args.length) { 956 | msg += " Received [" + slice.call(args).join(", ") + "]"; 957 | } 958 | throw new Error(msg); 959 | } 960 | 961 | return { 962 | create: function create(spy, thisValue, args, returnValue, exception, id) { 963 | var proxyCall = sinon.create(spyCall); 964 | delete proxyCall.create; 965 | proxyCall.proxy = spy; 966 | proxyCall.thisValue = thisValue; 967 | proxyCall.args = args; 968 | proxyCall.returnValue = returnValue; 969 | proxyCall.exception = exception; 970 | proxyCall.callId = typeof id == "number" && id || callId++; 971 | 972 | return proxyCall; 973 | }, 974 | 975 | calledOn: function calledOn(thisValue) { 976 | return this.thisValue === thisValue; 977 | }, 978 | 979 | calledWith: function calledWith() { 980 | for (var i = 0, l = arguments.length; i < l; i += 1) { 981 | if (!sinon.deepEqual(arguments[i], this.args[i])) { 982 | return false; 983 | } 984 | } 985 | 986 | return true; 987 | }, 988 | 989 | calledWithExactly: function calledWithExactly() { 990 | return arguments.length == this.args.length && 991 | this.calledWith.apply(this, arguments); 992 | }, 993 | 994 | notCalledWith: function notCalledWith() { 995 | for (var i = 0, l = arguments.length; i < l; i += 1) { 996 | if (!sinon.deepEqual(arguments[i], this.args[i])) { 997 | return true; 998 | } 999 | } 1000 | return false; 1001 | }, 1002 | 1003 | returned: function returned(value) { 1004 | return this.returnValue === value; 1005 | }, 1006 | 1007 | threw: function threw(error) { 1008 | if (typeof error == "undefined" || !this.exception) { 1009 | return !!this.exception; 1010 | } 1011 | 1012 | if (typeof error == "string") { 1013 | return this.exception.name == error; 1014 | } 1015 | 1016 | return this.exception === error; 1017 | }, 1018 | 1019 | calledWithNew: function calledWithNew(thisValue) { 1020 | return this.thisValue instanceof this.proxy; 1021 | }, 1022 | 1023 | calledBefore: function (other) { 1024 | return this.callId < other.callId; 1025 | }, 1026 | 1027 | calledAfter: function (other) { 1028 | return this.callId > other.callId; 1029 | }, 1030 | 1031 | callArg: function (pos) { 1032 | this.args[pos](); 1033 | }, 1034 | 1035 | callArgWith: function (pos) { 1036 | var args = slice.call(arguments, 1); 1037 | this.args[pos].apply(null, args); 1038 | }, 1039 | 1040 | "yield": function () { 1041 | var args = this.args; 1042 | for (var i = 0, l = args.length; i < l; ++i) { 1043 | if (typeof args[i] === "function") { 1044 | args[i].apply(null, slice.call(arguments)); 1045 | return; 1046 | } 1047 | } 1048 | throwYieldError(this.proxy, " cannot yield since no callback was passed.", args); 1049 | }, 1050 | 1051 | yieldTo: function (prop) { 1052 | var args = this.args; 1053 | for (var i = 0, l = args.length; i < l; ++i) { 1054 | if (args[i] && typeof args[i][prop] === "function") { 1055 | args[i][prop].apply(null, slice.call(arguments, 1)); 1056 | return; 1057 | } 1058 | } 1059 | throwYieldError(this.proxy, " cannot yield to '" + prop + 1060 | "' since no callback was passed.", args); 1061 | }, 1062 | 1063 | toString: function () { 1064 | var callStr = this.proxy.toString() + "("; 1065 | var args = []; 1066 | 1067 | for (var i = 0, l = this.args.length; i < l; ++i) { 1068 | push.call(args, sinon.format(this.args[i])); 1069 | } 1070 | 1071 | callStr = callStr + args.join(", ") + ")"; 1072 | 1073 | if (typeof this.returnValue != "undefined") { 1074 | callStr += " => " + sinon.format(this.returnValue); 1075 | } 1076 | 1077 | if (this.exception) { 1078 | callStr += " !" + this.exception.name; 1079 | 1080 | if (this.exception.message) { 1081 | callStr += "(" + this.exception.message + ")"; 1082 | } 1083 | } 1084 | 1085 | return callStr; 1086 | } 1087 | }; 1088 | }()); 1089 | 1090 | spy.spyCall = spyCall; 1091 | 1092 | // This steps outside the module sandbox and will be removed 1093 | sinon.spyCall = spyCall; 1094 | 1095 | if (commonJSModule) { 1096 | module.exports = spy; 1097 | } else { 1098 | sinon.spy = spy; 1099 | } 1100 | }(typeof sinon == "object" && sinon || null)); 1101 | 1102 | /** 1103 | * @depend ../sinon.js 1104 | * @depend spy.js 1105 | */ 1106 | /*jslint eqeqeq: false, onevar: false*/ 1107 | /*global module, require, sinon*/ 1108 | /** 1109 | * Stub functions 1110 | * 1111 | * @author Christian Johansen (christian@cjohansen.no) 1112 | * @license BSD 1113 | * 1114 | * Copyright (c) 2010-2011 Christian Johansen 1115 | */ 1116 | 1117 | (function (sinon) { 1118 | var commonJSModule = typeof module == "object" && typeof require == "function"; 1119 | 1120 | if (!sinon && commonJSModule) { 1121 | sinon = require("../sinon"); 1122 | } 1123 | 1124 | if (!sinon) { 1125 | return; 1126 | } 1127 | 1128 | function stub(object, property, func) { 1129 | if (!!func && typeof func != "function") { 1130 | throw new TypeError("Custom stub should be function"); 1131 | } 1132 | 1133 | var wrapper; 1134 | 1135 | if (func) { 1136 | wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func; 1137 | } else { 1138 | wrapper = stub.create(); 1139 | } 1140 | 1141 | if (!object && !property) { 1142 | return sinon.stub.create(); 1143 | } 1144 | 1145 | if (!property && !!object && typeof object == "object") { 1146 | for (var prop in object) { 1147 | if (typeof object[prop] === "function") { 1148 | stub(object, prop); 1149 | } 1150 | } 1151 | 1152 | return object; 1153 | } 1154 | 1155 | return sinon.wrapMethod(object, property, wrapper); 1156 | } 1157 | 1158 | function getCallback(stub, args) { 1159 | if (stub.callArgAt < 0) { 1160 | for (var i = 0, l = args.length; i < l; ++i) { 1161 | if (!stub.callArgProp && typeof args[i] == "function") { 1162 | return args[i]; 1163 | } 1164 | 1165 | if (stub.callArgProp && args[i] && 1166 | typeof args[i][stub.callArgProp] == "function") { 1167 | return args[i][stub.callArgProp]; 1168 | } 1169 | } 1170 | 1171 | return null; 1172 | } 1173 | 1174 | return args[stub.callArgAt]; 1175 | } 1176 | 1177 | var join = Array.prototype.join; 1178 | 1179 | function getCallbackError(stub, func, args) { 1180 | if (stub.callArgAt < 0) { 1181 | var msg; 1182 | 1183 | if (stub.callArgProp) { 1184 | msg = sinon.functionName(stub) + 1185 | " expected to yield to '" + stub.callArgProp + 1186 | "', but no object with such a property was passed." 1187 | } else { 1188 | msg = sinon.functionName(stub) + 1189 | " expected to yield, but no callback was passed." 1190 | } 1191 | 1192 | if (args.length > 0) { 1193 | msg += " Received [" + join.call(args, ", ") + "]"; 1194 | } 1195 | 1196 | return msg; 1197 | } 1198 | 1199 | return "argument at index " + stub.callArgAt + " is not a function: " + func; 1200 | } 1201 | 1202 | function callCallback(stub, args) { 1203 | if (typeof stub.callArgAt == "number") { 1204 | var func = getCallback(stub, args); 1205 | 1206 | if (typeof func != "function") { 1207 | throw new TypeError(getCallbackError(stub, func, args)); 1208 | } 1209 | 1210 | func.apply(stub.callbackContext, stub.callbackArguments); 1211 | } 1212 | } 1213 | 1214 | var uuid = 0; 1215 | 1216 | sinon.extend(stub, (function () { 1217 | var slice = Array.prototype.slice; 1218 | 1219 | function throwsException(error, message) { 1220 | if (typeof error == "string") { 1221 | this.exception = new Error(message || ""); 1222 | this.exception.name = error; 1223 | } else if (!error) { 1224 | this.exception = new Error("Error"); 1225 | } else { 1226 | this.exception = error; 1227 | } 1228 | 1229 | return this; 1230 | } 1231 | 1232 | return { 1233 | create: function create() { 1234 | var functionStub = function () { 1235 | if (functionStub.exception) { 1236 | throw functionStub.exception; 1237 | } else if (typeof functionStub.returnArgAt == 'number') { 1238 | return arguments[functionStub.returnArgAt]; 1239 | } 1240 | 1241 | callCallback(functionStub, arguments); 1242 | 1243 | return functionStub.returnValue; 1244 | }; 1245 | 1246 | functionStub.id = "stub#" + uuid++; 1247 | var orig = functionStub; 1248 | functionStub = sinon.spy.create(functionStub); 1249 | functionStub.func = orig; 1250 | 1251 | sinon.extend(functionStub, stub); 1252 | functionStub._create = sinon.stub.create; 1253 | functionStub.displayName = "stub"; 1254 | functionStub.toString = sinon.functionToString; 1255 | 1256 | return functionStub; 1257 | }, 1258 | 1259 | returns: function returns(value) { 1260 | this.returnValue = value; 1261 | 1262 | return this; 1263 | }, 1264 | 1265 | returnsArg: function returnsArg(pos) { 1266 | if (typeof pos != "number") { 1267 | throw new TypeError("argument index is not number"); 1268 | } 1269 | 1270 | this.returnArgAt = pos; 1271 | 1272 | return this; 1273 | }, 1274 | 1275 | "throws": throwsException, 1276 | throwsException: throwsException, 1277 | 1278 | callsArg: function callsArg(pos) { 1279 | if (typeof pos != "number") { 1280 | throw new TypeError("argument index is not number"); 1281 | } 1282 | 1283 | this.callArgAt = pos; 1284 | this.callbackArguments = []; 1285 | 1286 | return this; 1287 | }, 1288 | 1289 | callsArgOn: function callsArgOn(pos, context) { 1290 | if (typeof pos != "number") { 1291 | throw new TypeError("argument index is not number"); 1292 | } 1293 | if (typeof context != "object") { 1294 | throw new TypeError("argument context is not an object"); 1295 | } 1296 | 1297 | this.callArgAt = pos; 1298 | this.callbackArguments = []; 1299 | this.callbackContext = context; 1300 | 1301 | return this; 1302 | }, 1303 | 1304 | callsArgWith: function callsArgWith(pos) { 1305 | if (typeof pos != "number") { 1306 | throw new TypeError("argument index is not number"); 1307 | } 1308 | 1309 | this.callArgAt = pos; 1310 | this.callbackArguments = slice.call(arguments, 1); 1311 | 1312 | return this; 1313 | }, 1314 | 1315 | callsArgOnWith: function callsArgWith(pos, context) { 1316 | if (typeof pos != "number") { 1317 | throw new TypeError("argument index is not number"); 1318 | } 1319 | if (typeof context != "object") { 1320 | throw new TypeError("argument context is not an object"); 1321 | } 1322 | 1323 | this.callArgAt = pos; 1324 | this.callbackArguments = slice.call(arguments, 2); 1325 | this.callbackContext = context; 1326 | 1327 | return this; 1328 | }, 1329 | 1330 | yields: function () { 1331 | this.callArgAt = -1; 1332 | this.callbackArguments = slice.call(arguments, 0); 1333 | 1334 | return this; 1335 | }, 1336 | 1337 | yieldsOn: function (context) { 1338 | if (typeof context != "object") { 1339 | throw new TypeError("argument context is not an object"); 1340 | } 1341 | 1342 | this.callArgAt = -1; 1343 | this.callbackArguments = slice.call(arguments, 1); 1344 | this.callbackContext = context; 1345 | 1346 | return this; 1347 | }, 1348 | 1349 | yieldsTo: function (prop) { 1350 | this.callArgAt = -1; 1351 | this.callArgProp = prop; 1352 | this.callbackArguments = slice.call(arguments, 1); 1353 | 1354 | return this; 1355 | }, 1356 | 1357 | yieldsToOn: function (prop, context) { 1358 | if (typeof context != "object") { 1359 | throw new TypeError("argument context is not an object"); 1360 | } 1361 | 1362 | this.callArgAt = -1; 1363 | this.callArgProp = prop; 1364 | this.callbackArguments = slice.call(arguments, 2); 1365 | this.callbackContext = context; 1366 | 1367 | return this; 1368 | } 1369 | }; 1370 | }())); 1371 | 1372 | if (commonJSModule) { 1373 | module.exports = stub; 1374 | } else { 1375 | sinon.stub = stub; 1376 | } 1377 | }(typeof sinon == "object" && sinon || null)); 1378 | 1379 | /** 1380 | * @depend ../sinon.js 1381 | * @depend stub.js 1382 | */ 1383 | /*jslint eqeqeq: false, onevar: false, nomen: false*/ 1384 | /*global module, require, sinon*/ 1385 | /** 1386 | * Mock functions. 1387 | * 1388 | * @author Christian Johansen (christian@cjohansen.no) 1389 | * @license BSD 1390 | * 1391 | * Copyright (c) 2010-2011 Christian Johansen 1392 | */ 1393 | 1394 | (function (sinon) { 1395 | var commonJSModule = typeof module == "object" && typeof require == "function"; 1396 | var push = [].push; 1397 | 1398 | if (!sinon && commonJSModule) { 1399 | sinon = require("../sinon"); 1400 | } 1401 | 1402 | if (!sinon) { 1403 | return; 1404 | } 1405 | 1406 | function mock(object) { 1407 | if (!object) { 1408 | return sinon.expectation.create("Anonymous mock"); 1409 | } 1410 | 1411 | return mock.create(object); 1412 | } 1413 | 1414 | sinon.mock = mock; 1415 | 1416 | sinon.extend(mock, (function () { 1417 | function each(collection, callback) { 1418 | if (!collection) { 1419 | return; 1420 | } 1421 | 1422 | for (var i = 0, l = collection.length; i < l; i += 1) { 1423 | callback(collection[i]); 1424 | } 1425 | } 1426 | 1427 | return { 1428 | create: function create(object) { 1429 | if (!object) { 1430 | throw new TypeError("object is null"); 1431 | } 1432 | 1433 | var mockObject = sinon.extend({}, mock); 1434 | mockObject.object = object; 1435 | delete mockObject.create; 1436 | 1437 | return mockObject; 1438 | }, 1439 | 1440 | expects: function expects(method) { 1441 | if (!method) { 1442 | throw new TypeError("method is falsy"); 1443 | } 1444 | 1445 | if (!this.expectations) { 1446 | this.expectations = {}; 1447 | this.proxies = []; 1448 | } 1449 | 1450 | if (!this.expectations[method]) { 1451 | this.expectations[method] = []; 1452 | var mockObject = this; 1453 | 1454 | sinon.wrapMethod(this.object, method, function () { 1455 | return mockObject.invokeMethod(method, this, arguments); 1456 | }); 1457 | 1458 | push.call(this.proxies, method); 1459 | } 1460 | 1461 | var expectation = sinon.expectation.create(method); 1462 | push.call(this.expectations[method], expectation); 1463 | 1464 | return expectation; 1465 | }, 1466 | 1467 | restore: function restore() { 1468 | var object = this.object; 1469 | 1470 | each(this.proxies, function (proxy) { 1471 | if (typeof object[proxy].restore == "function") { 1472 | object[proxy].restore(); 1473 | } 1474 | }); 1475 | }, 1476 | 1477 | verify: function verify() { 1478 | var expectations = this.expectations || {}; 1479 | var messages = [], met = []; 1480 | 1481 | each(this.proxies, function (proxy) { 1482 | each(expectations[proxy], function (expectation) { 1483 | if (!expectation.met()) { 1484 | push.call(messages, expectation.toString()); 1485 | } else { 1486 | push.call(met, expectation.toString()); 1487 | } 1488 | }); 1489 | }); 1490 | 1491 | this.restore(); 1492 | 1493 | if (messages.length > 0) { 1494 | sinon.expectation.fail(messages.concat(met).join("\n")); 1495 | } 1496 | 1497 | return true; 1498 | }, 1499 | 1500 | invokeMethod: function invokeMethod(method, thisValue, args) { 1501 | var expectations = this.expectations && this.expectations[method]; 1502 | var length = expectations && expectations.length || 0; 1503 | 1504 | for (var i = 0; i < length; i += 1) { 1505 | if (!expectations[i].met() && 1506 | expectations[i].allowsCall(thisValue, args)) { 1507 | return expectations[i].apply(thisValue, args); 1508 | } 1509 | } 1510 | 1511 | var messages = []; 1512 | 1513 | for (i = 0; i < length; i += 1) { 1514 | push.call(messages, " " + expectations[i].toString()); 1515 | } 1516 | 1517 | messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({ 1518 | proxy: method, 1519 | args: args 1520 | })); 1521 | 1522 | sinon.expectation.fail(messages.join("\n")); 1523 | } 1524 | }; 1525 | }())); 1526 | 1527 | var times = sinon.timesInWords; 1528 | 1529 | sinon.expectation = (function () { 1530 | var slice = Array.prototype.slice; 1531 | var _invoke = sinon.spy.invoke; 1532 | 1533 | function callCountInWords(callCount) { 1534 | if (callCount == 0) { 1535 | return "never called"; 1536 | } else { 1537 | return "called " + times(callCount); 1538 | } 1539 | } 1540 | 1541 | function expectedCallCountInWords(expectation) { 1542 | var min = expectation.minCalls; 1543 | var max = expectation.maxCalls; 1544 | 1545 | if (typeof min == "number" && typeof max == "number") { 1546 | var str = times(min); 1547 | 1548 | if (min != max) { 1549 | str = "at least " + str + " and at most " + times(max); 1550 | } 1551 | 1552 | return str; 1553 | } 1554 | 1555 | if (typeof min == "number") { 1556 | return "at least " + times(min); 1557 | } 1558 | 1559 | return "at most " + times(max); 1560 | } 1561 | 1562 | function receivedMinCalls(expectation) { 1563 | var hasMinLimit = typeof expectation.minCalls == "number"; 1564 | return !hasMinLimit || expectation.callCount >= expectation.minCalls; 1565 | } 1566 | 1567 | function receivedMaxCalls(expectation) { 1568 | if (typeof expectation.maxCalls != "number") { 1569 | return false; 1570 | } 1571 | 1572 | return expectation.callCount == expectation.maxCalls; 1573 | } 1574 | 1575 | return { 1576 | minCalls: 1, 1577 | maxCalls: 1, 1578 | 1579 | create: function create(methodName) { 1580 | var expectation = sinon.extend(sinon.stub.create(), sinon.expectation); 1581 | delete expectation.create; 1582 | expectation.method = methodName; 1583 | 1584 | return expectation; 1585 | }, 1586 | 1587 | invoke: function invoke(func, thisValue, args) { 1588 | this.verifyCallAllowed(thisValue, args); 1589 | 1590 | return _invoke.apply(this, arguments); 1591 | }, 1592 | 1593 | atLeast: function atLeast(num) { 1594 | if (typeof num != "number") { 1595 | throw new TypeError("'" + num + "' is not number"); 1596 | } 1597 | 1598 | if (!this.limitsSet) { 1599 | this.maxCalls = null; 1600 | this.limitsSet = true; 1601 | } 1602 | 1603 | this.minCalls = num; 1604 | 1605 | return this; 1606 | }, 1607 | 1608 | atMost: function atMost(num) { 1609 | if (typeof num != "number") { 1610 | throw new TypeError("'" + num + "' is not number"); 1611 | } 1612 | 1613 | if (!this.limitsSet) { 1614 | this.minCalls = null; 1615 | this.limitsSet = true; 1616 | } 1617 | 1618 | this.maxCalls = num; 1619 | 1620 | return this; 1621 | }, 1622 | 1623 | never: function never() { 1624 | return this.exactly(0); 1625 | }, 1626 | 1627 | once: function once() { 1628 | return this.exactly(1); 1629 | }, 1630 | 1631 | twice: function twice() { 1632 | return this.exactly(2); 1633 | }, 1634 | 1635 | thrice: function thrice() { 1636 | return this.exactly(3); 1637 | }, 1638 | 1639 | exactly: function exactly(num) { 1640 | if (typeof num != "number") { 1641 | throw new TypeError("'" + num + "' is not a number"); 1642 | } 1643 | 1644 | this.atLeast(num); 1645 | return this.atMost(num); 1646 | }, 1647 | 1648 | met: function met() { 1649 | return !this.failed && receivedMinCalls(this); 1650 | }, 1651 | 1652 | verifyCallAllowed: function verifyCallAllowed(thisValue, args) { 1653 | if (receivedMaxCalls(this)) { 1654 | this.failed = true; 1655 | sinon.expectation.fail(this.method + " already called " + times(this.maxCalls)); 1656 | } 1657 | 1658 | if ("expectedThis" in this && this.expectedThis !== thisValue) { 1659 | sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " + 1660 | this.expectedThis); 1661 | } 1662 | 1663 | if (!("expectedArguments" in this)) { 1664 | return; 1665 | } 1666 | 1667 | if (!args || args.length === 0) { 1668 | sinon.expectation.fail(this.method + " received no arguments, expected " + 1669 | this.expectedArguments.join()); 1670 | } 1671 | 1672 | if (args.length < this.expectedArguments.length) { 1673 | sinon.expectation.fail(this.method + " received too few arguments (" + args.join() + 1674 | "), expected " + this.expectedArguments.join()); 1675 | } 1676 | 1677 | if (this.expectsExactArgCount && 1678 | args.length != this.expectedArguments.length) { 1679 | sinon.expectation.fail(this.method + " received too many arguments (" + args.join() + 1680 | "), expected " + this.expectedArguments.join()); 1681 | } 1682 | 1683 | for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { 1684 | if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { 1685 | sinon.expectation.fail(this.method + " received wrong arguments (" + args.join() + 1686 | "), expected " + this.expectedArguments.join()); 1687 | } 1688 | } 1689 | }, 1690 | 1691 | allowsCall: function allowsCall(thisValue, args) { 1692 | if (this.met()) { 1693 | return false; 1694 | } 1695 | 1696 | if ("expectedThis" in this && this.expectedThis !== thisValue) { 1697 | return false; 1698 | } 1699 | 1700 | if (!("expectedArguments" in this)) { 1701 | return true; 1702 | } 1703 | 1704 | args = args || []; 1705 | 1706 | if (args.length < this.expectedArguments.length) { 1707 | return false; 1708 | } 1709 | 1710 | if (this.expectsExactArgCount && 1711 | args.length != this.expectedArguments.length) { 1712 | return false; 1713 | } 1714 | 1715 | for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { 1716 | if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { 1717 | return false; 1718 | } 1719 | } 1720 | 1721 | return true; 1722 | }, 1723 | 1724 | withArgs: function withArgs() { 1725 | this.expectedArguments = slice.call(arguments); 1726 | return this; 1727 | }, 1728 | 1729 | withExactArgs: function withExactArgs() { 1730 | this.withArgs.apply(this, arguments); 1731 | this.expectsExactArgCount = true; 1732 | return this; 1733 | }, 1734 | 1735 | on: function on(thisValue) { 1736 | this.expectedThis = thisValue; 1737 | return this; 1738 | }, 1739 | 1740 | toString: function () { 1741 | var args = (this.expectedArguments || []).slice(); 1742 | 1743 | if (!this.expectsExactArgCount) { 1744 | push.call(args, "[...]"); 1745 | } 1746 | 1747 | var callStr = sinon.spyCall.toString.call({ 1748 | proxy: this.method, args: args 1749 | }); 1750 | 1751 | var message = callStr.replace(", [...", "[, ...") + " " + 1752 | expectedCallCountInWords(this); 1753 | 1754 | if (this.met()) { 1755 | return "Expectation met: " + message; 1756 | } 1757 | 1758 | return "Expected " + message + " (" + 1759 | callCountInWords(this.callCount) + ")"; 1760 | }, 1761 | 1762 | verify: function verify() { 1763 | if (!this.met()) { 1764 | sinon.expectation.fail(this.toString()); 1765 | } 1766 | 1767 | return true; 1768 | }, 1769 | 1770 | fail: function (message) { 1771 | var exception = new Error(message); 1772 | exception.name = "ExpectationError"; 1773 | 1774 | throw exception; 1775 | } 1776 | }; 1777 | }()); 1778 | 1779 | if (commonJSModule) { 1780 | module.exports = mock; 1781 | } else { 1782 | sinon.mock = mock; 1783 | } 1784 | }(typeof sinon == "object" && sinon || null)); 1785 | 1786 | /** 1787 | * @depend ../sinon.js 1788 | * @depend stub.js 1789 | * @depend mock.js 1790 | */ 1791 | /*jslint eqeqeq: false, onevar: false, forin: true*/ 1792 | /*global module, require, sinon*/ 1793 | /** 1794 | * Collections of stubs, spies and mocks. 1795 | * 1796 | * @author Christian Johansen (christian@cjohansen.no) 1797 | * @license BSD 1798 | * 1799 | * Copyright (c) 2010-2011 Christian Johansen 1800 | */ 1801 | 1802 | (function (sinon) { 1803 | var commonJSModule = typeof module == "object" && typeof require == "function"; 1804 | var push = [].push; 1805 | 1806 | if (!sinon && commonJSModule) { 1807 | sinon = require("../sinon"); 1808 | } 1809 | 1810 | if (!sinon) { 1811 | return; 1812 | } 1813 | 1814 | function getFakes(fakeCollection) { 1815 | if (!fakeCollection.fakes) { 1816 | fakeCollection.fakes = []; 1817 | } 1818 | 1819 | return fakeCollection.fakes; 1820 | } 1821 | 1822 | function each(fakeCollection, method) { 1823 | var fakes = getFakes(fakeCollection); 1824 | 1825 | for (var i = 0, l = fakes.length; i < l; i += 1) { 1826 | if (typeof fakes[i][method] == "function") { 1827 | fakes[i][method](); 1828 | } 1829 | } 1830 | } 1831 | 1832 | function compact(fakeCollection) { 1833 | var fakes = getFakes(fakeCollection); 1834 | var i = 0; 1835 | while (i < fakes.length) { 1836 | fakes.splice(i, 1); 1837 | } 1838 | } 1839 | 1840 | var collection = { 1841 | verify: function resolve() { 1842 | each(this, "verify"); 1843 | }, 1844 | 1845 | restore: function restore() { 1846 | each(this, "restore"); 1847 | compact(this); 1848 | }, 1849 | 1850 | verifyAndRestore: function verifyAndRestore() { 1851 | var exception; 1852 | 1853 | try { 1854 | this.verify(); 1855 | } catch (e) { 1856 | exception = e; 1857 | } 1858 | 1859 | this.restore(); 1860 | 1861 | if (exception) { 1862 | throw exception; 1863 | } 1864 | }, 1865 | 1866 | add: function add(fake) { 1867 | push.call(getFakes(this), fake); 1868 | return fake; 1869 | }, 1870 | 1871 | spy: function spy() { 1872 | return this.add(sinon.spy.apply(sinon, arguments)); 1873 | }, 1874 | 1875 | stub: function stub(object, property, value) { 1876 | if (property) { 1877 | var original = object[property]; 1878 | 1879 | if (typeof original != "function") { 1880 | if (!object.hasOwnProperty(property)) { 1881 | throw new TypeError("Cannot stub non-existent own property " + property); 1882 | } 1883 | 1884 | object[property] = value; 1885 | 1886 | return this.add({ 1887 | restore: function () { 1888 | object[property] = original; 1889 | } 1890 | }); 1891 | } 1892 | } 1893 | 1894 | return this.add(sinon.stub.apply(sinon, arguments)); 1895 | }, 1896 | 1897 | mock: function mock() { 1898 | return this.add(sinon.mock.apply(sinon, arguments)); 1899 | }, 1900 | 1901 | inject: function inject(obj) { 1902 | var col = this; 1903 | 1904 | obj.spy = function () { 1905 | return col.spy.apply(col, arguments); 1906 | }; 1907 | 1908 | obj.stub = function () { 1909 | return col.stub.apply(col, arguments); 1910 | }; 1911 | 1912 | obj.mock = function () { 1913 | return col.mock.apply(col, arguments); 1914 | }; 1915 | 1916 | return obj; 1917 | } 1918 | }; 1919 | 1920 | if (commonJSModule) { 1921 | module.exports = collection; 1922 | } else { 1923 | sinon.collection = collection; 1924 | } 1925 | }(typeof sinon == "object" && sinon || null)); 1926 | 1927 | /*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ 1928 | /*global module, require, window*/ 1929 | /** 1930 | * Fake timer API 1931 | * setTimeout 1932 | * setInterval 1933 | * clearTimeout 1934 | * clearInterval 1935 | * tick 1936 | * reset 1937 | * Date 1938 | * 1939 | * Inspired by jsUnitMockTimeOut from JsUnit 1940 | * 1941 | * @author Christian Johansen (christian@cjohansen.no) 1942 | * @license BSD 1943 | * 1944 | * Copyright (c) 2010-2011 Christian Johansen 1945 | */ 1946 | 1947 | if (typeof sinon == "undefined") { 1948 | var sinon = {}; 1949 | } 1950 | 1951 | (function (global) { 1952 | var id = 1; 1953 | 1954 | function addTimer(args, recurring) { 1955 | if (args.length === 0) { 1956 | throw new Error("Function requires at least 1 parameter"); 1957 | } 1958 | 1959 | var toId = id++; 1960 | var delay = args[1] || 0; 1961 | 1962 | if (!this.timeouts) { 1963 | this.timeouts = {}; 1964 | } 1965 | 1966 | this.timeouts[toId] = { 1967 | id: toId, 1968 | func: args[0], 1969 | callAt: this.now + delay 1970 | }; 1971 | 1972 | if (recurring === true) { 1973 | this.timeouts[toId].interval = delay; 1974 | } 1975 | 1976 | return toId; 1977 | } 1978 | 1979 | function parseTime(str) { 1980 | if (!str) { 1981 | return 0; 1982 | } 1983 | 1984 | var strings = str.split(":"); 1985 | var l = strings.length, i = l; 1986 | var ms = 0, parsed; 1987 | 1988 | if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { 1989 | throw new Error("tick only understands numbers and 'h:m:s'"); 1990 | } 1991 | 1992 | while (i--) { 1993 | parsed = parseInt(strings[i], 10); 1994 | 1995 | if (parsed >= 60) { 1996 | throw new Error("Invalid time " + str); 1997 | } 1998 | 1999 | ms += parsed * Math.pow(60, (l - i - 1)); 2000 | } 2001 | 2002 | return ms * 1000; 2003 | } 2004 | 2005 | function createObject(object) { 2006 | var newObject; 2007 | 2008 | if (Object.create) { 2009 | newObject = Object.create(object); 2010 | } else { 2011 | var F = function () {}; 2012 | F.prototype = object; 2013 | newObject = new F(); 2014 | } 2015 | 2016 | newObject.Date.clock = newObject; 2017 | return newObject; 2018 | } 2019 | 2020 | sinon.clock = { 2021 | now: 0, 2022 | 2023 | create: function create(now) { 2024 | var clock = createObject(this); 2025 | 2026 | if (typeof now == "number") { 2027 | clock.now = now; 2028 | } 2029 | 2030 | if (!!now && typeof now == "object") { 2031 | throw new TypeError("now should be milliseconds since UNIX epoch"); 2032 | } 2033 | 2034 | return clock; 2035 | }, 2036 | 2037 | setTimeout: function setTimeout(callback, timeout) { 2038 | return addTimer.call(this, arguments, false); 2039 | }, 2040 | 2041 | clearTimeout: function clearTimeout(timerId) { 2042 | if (!this.timeouts) { 2043 | this.timeouts = []; 2044 | } 2045 | 2046 | if (timerId in this.timeouts) { 2047 | delete this.timeouts[timerId]; 2048 | } 2049 | }, 2050 | 2051 | setInterval: function setInterval(callback, timeout) { 2052 | return addTimer.call(this, arguments, true); 2053 | }, 2054 | 2055 | clearInterval: function clearInterval(timerId) { 2056 | this.clearTimeout(timerId); 2057 | }, 2058 | 2059 | tick: function tick(ms) { 2060 | ms = typeof ms == "number" ? ms : parseTime(ms); 2061 | var tickFrom = this.now, tickTo = this.now + ms, previous = this.now; 2062 | var timer = this.firstTimerInRange(tickFrom, tickTo); 2063 | 2064 | var firstException; 2065 | while (timer && tickFrom <= tickTo) { 2066 | if (this.timeouts[timer.id]) { 2067 | tickFrom = this.now = timer.callAt; 2068 | try { 2069 | this.callTimer(timer); 2070 | } catch (e) { 2071 | firstException = firstException || e; 2072 | } 2073 | } 2074 | 2075 | timer = this.firstTimerInRange(previous, tickTo); 2076 | previous = tickFrom; 2077 | } 2078 | 2079 | this.now = tickTo; 2080 | 2081 | if (firstException) { 2082 | throw firstException; 2083 | } 2084 | }, 2085 | 2086 | firstTimerInRange: function (from, to) { 2087 | var timer, smallest, originalTimer; 2088 | 2089 | for (var id in this.timeouts) { 2090 | if (this.timeouts.hasOwnProperty(id)) { 2091 | if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) { 2092 | continue; 2093 | } 2094 | 2095 | if (!smallest || this.timeouts[id].callAt < smallest) { 2096 | originalTimer = this.timeouts[id]; 2097 | smallest = this.timeouts[id].callAt; 2098 | 2099 | timer = { 2100 | func: this.timeouts[id].func, 2101 | callAt: this.timeouts[id].callAt, 2102 | interval: this.timeouts[id].interval, 2103 | id: this.timeouts[id].id 2104 | }; 2105 | } 2106 | } 2107 | } 2108 | 2109 | return timer || null; 2110 | }, 2111 | 2112 | callTimer: function (timer) { 2113 | try { 2114 | if (typeof timer.func == "function") { 2115 | timer.func.call(null); 2116 | } else { 2117 | eval(timer.func); 2118 | } 2119 | } catch (e) { 2120 | var exception = e; 2121 | } 2122 | 2123 | if (!this.timeouts[timer.id]) { 2124 | if (exception) { 2125 | throw exception; 2126 | } 2127 | return; 2128 | } 2129 | 2130 | if (typeof timer.interval == "number") { 2131 | this.timeouts[timer.id].callAt += timer.interval; 2132 | } else { 2133 | delete this.timeouts[timer.id]; 2134 | } 2135 | 2136 | if (exception) { 2137 | throw exception; 2138 | } 2139 | }, 2140 | 2141 | reset: function reset() { 2142 | this.timeouts = {}; 2143 | }, 2144 | 2145 | Date: (function () { 2146 | var NativeDate = Date; 2147 | 2148 | function ClockDate(year, month, date, hour, minute, second, ms) { 2149 | // Defensive and verbose to avoid potential harm in passing 2150 | // explicit undefined when user does not pass argument 2151 | switch (arguments.length) { 2152 | case 0: 2153 | return new NativeDate(ClockDate.clock.now); 2154 | case 1: 2155 | return new NativeDate(year); 2156 | case 2: 2157 | return new NativeDate(year, month); 2158 | case 3: 2159 | return new NativeDate(year, month, date); 2160 | case 4: 2161 | return new NativeDate(year, month, date, hour); 2162 | case 5: 2163 | return new NativeDate(year, month, date, hour, minute); 2164 | case 6: 2165 | return new NativeDate(year, month, date, hour, minute, second); 2166 | default: 2167 | return new NativeDate(year, month, date, hour, minute, second, ms); 2168 | } 2169 | } 2170 | 2171 | return mirrorDateProperties(ClockDate, NativeDate); 2172 | }()) 2173 | }; 2174 | 2175 | function mirrorDateProperties(target, source) { 2176 | if (source.now) { 2177 | target.now = function now() { 2178 | return target.clock.now; 2179 | }; 2180 | } else { 2181 | delete target.now; 2182 | } 2183 | 2184 | if (source.toSource) { 2185 | target.toSource = function toSource() { 2186 | return source.toSource(); 2187 | }; 2188 | } else { 2189 | delete target.toSource; 2190 | } 2191 | 2192 | target.toString = function toString() { 2193 | return source.toString(); 2194 | }; 2195 | 2196 | target.prototype = source.prototype; 2197 | target.parse = source.parse; 2198 | target.UTC = source.UTC; 2199 | target.prototype.toUTCString = source.prototype.toUTCString; 2200 | return target; 2201 | } 2202 | 2203 | var methods = ["Date", "setTimeout", "setInterval", 2204 | "clearTimeout", "clearInterval"]; 2205 | 2206 | function restore() { 2207 | var method; 2208 | 2209 | for (var i = 0, l = this.methods.length; i < l; i++) { 2210 | method = this.methods[i]; 2211 | global[method] = this["_" + method]; 2212 | } 2213 | } 2214 | 2215 | function stubGlobal(method, clock) { 2216 | clock["_" + method] = global[method]; 2217 | 2218 | if (method == "Date") { 2219 | var date = mirrorDateProperties(clock[method], global[method]); 2220 | global[method] = date; 2221 | } else { 2222 | global[method] = function () { 2223 | return clock[method].apply(clock, arguments); 2224 | }; 2225 | 2226 | for (var prop in clock[method]) { 2227 | if (clock[method].hasOwnProperty(prop)) { 2228 | global[method][prop] = clock[method][prop]; 2229 | } 2230 | } 2231 | } 2232 | 2233 | global[method].clock = clock; 2234 | } 2235 | 2236 | sinon.useFakeTimers = function useFakeTimers(now) { 2237 | var clock = sinon.clock.create(now); 2238 | clock.restore = restore; 2239 | clock.methods = Array.prototype.slice.call(arguments, 2240 | typeof now == "number" ? 1 : 0); 2241 | 2242 | if (clock.methods.length === 0) { 2243 | clock.methods = methods; 2244 | } 2245 | 2246 | for (var i = 0, l = clock.methods.length; i < l; i++) { 2247 | stubGlobal(clock.methods[i], clock); 2248 | } 2249 | 2250 | return clock; 2251 | }; 2252 | }(typeof global != "undefined" && typeof global !== "function" ? global : this)); 2253 | 2254 | sinon.timers = { 2255 | setTimeout: setTimeout, 2256 | clearTimeout: clearTimeout, 2257 | setInterval: setInterval, 2258 | clearInterval: clearInterval, 2259 | Date: Date 2260 | }; 2261 | 2262 | if (typeof module == "object" && typeof require == "function") { 2263 | module.exports = sinon; 2264 | } 2265 | 2266 | /*jslint eqeqeq: false, onevar: false*/ 2267 | /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ 2268 | /** 2269 | * Minimal Event interface implementation 2270 | * 2271 | * Original implementation by Sven Fuchs: https://gist.github.com/995028 2272 | * Modifications and tests by Christian Johansen. 2273 | * 2274 | * @author Sven Fuchs (svenfuchs@artweb-design.de) 2275 | * @author Christian Johansen (christian@cjohansen.no) 2276 | * @license BSD 2277 | * 2278 | * Copyright (c) 2011 Sven Fuchs, Christian Johansen 2279 | */ 2280 | 2281 | if (typeof sinon == "undefined") { 2282 | this.sinon = {}; 2283 | } 2284 | 2285 | (function () { 2286 | var push = [].push; 2287 | 2288 | sinon.Event = function Event(type, bubbles, cancelable) { 2289 | this.initEvent(type, bubbles, cancelable); 2290 | }; 2291 | 2292 | sinon.Event.prototype = { 2293 | initEvent: function(type, bubbles, cancelable) { 2294 | this.type = type; 2295 | this.bubbles = bubbles; 2296 | this.cancelable = cancelable; 2297 | }, 2298 | 2299 | stopPropagation: function () {}, 2300 | 2301 | preventDefault: function () { 2302 | this.defaultPrevented = true; 2303 | } 2304 | }; 2305 | 2306 | sinon.EventTarget = { 2307 | addEventListener: function addEventListener(event, listener, useCapture) { 2308 | this.eventListeners = this.eventListeners || {}; 2309 | this.eventListeners[event] = this.eventListeners[event] || []; 2310 | push.call(this.eventListeners[event], listener); 2311 | }, 2312 | 2313 | removeEventListener: function removeEventListener(event, listener, useCapture) { 2314 | var listeners = this.eventListeners && this.eventListeners[event] || []; 2315 | 2316 | for (var i = 0, l = listeners.length; i < l; ++i) { 2317 | if (listeners[i] == listener) { 2318 | return listeners.splice(i, 1); 2319 | } 2320 | } 2321 | }, 2322 | 2323 | dispatchEvent: function dispatchEvent(event) { 2324 | var type = event.type; 2325 | var listeners = this.eventListeners && this.eventListeners[type] || []; 2326 | 2327 | for (var i = 0; i < listeners.length; i++) { 2328 | if (typeof listeners[i] == "function") { 2329 | listeners[i].call(this, event); 2330 | } else { 2331 | listeners[i].handleEvent(event); 2332 | } 2333 | } 2334 | 2335 | return !!event.defaultPrevented; 2336 | } 2337 | }; 2338 | }()); 2339 | 2340 | /** 2341 | * @depend event.js 2342 | */ 2343 | /*jslint eqeqeq: false, onevar: false*/ 2344 | /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ 2345 | /** 2346 | * Fake XMLHttpRequest object 2347 | * 2348 | * @author Christian Johansen (christian@cjohansen.no) 2349 | * @license BSD 2350 | * 2351 | * Copyright (c) 2010-2011 Christian Johansen 2352 | */ 2353 | 2354 | if (typeof sinon == "undefined") { 2355 | this.sinon = {}; 2356 | } 2357 | sinon.xhr = { XMLHttpRequest: this.XMLHttpRequest }; 2358 | 2359 | // wrapper for global 2360 | (function(global) { 2361 | var xhr = sinon.xhr; 2362 | xhr.GlobalXMLHttpRequest = global.XMLHttpRequest; 2363 | xhr.GlobalActiveXObject = global.ActiveXObject; 2364 | xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined"; 2365 | xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined"; 2366 | xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX 2367 | ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false; 2368 | 2369 | /*jsl:ignore*/ 2370 | var unsafeHeaders = { 2371 | "Accept-Charset": true, 2372 | "Accept-Encoding": true, 2373 | "Connection": true, 2374 | "Content-Length": true, 2375 | "Cookie": true, 2376 | "Cookie2": true, 2377 | "Content-Transfer-Encoding": true, 2378 | "Date": true, 2379 | "Expect": true, 2380 | "Host": true, 2381 | "Keep-Alive": true, 2382 | "Referer": true, 2383 | "TE": true, 2384 | "Trailer": true, 2385 | "Transfer-Encoding": true, 2386 | "Upgrade": true, 2387 | "User-Agent": true, 2388 | "Via": true 2389 | }; 2390 | /*jsl:end*/ 2391 | 2392 | function FakeXMLHttpRequest() { 2393 | this.readyState = FakeXMLHttpRequest.UNSENT; 2394 | this.requestHeaders = {}; 2395 | this.requestBody = null; 2396 | this.status = 0; 2397 | this.statusText = ""; 2398 | 2399 | if (typeof FakeXMLHttpRequest.onCreate == "function") { 2400 | FakeXMLHttpRequest.onCreate(this); 2401 | } 2402 | } 2403 | 2404 | function verifyState(xhr) { 2405 | if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { 2406 | throw new Error("INVALID_STATE_ERR"); 2407 | } 2408 | 2409 | if (xhr.sendFlag) { 2410 | throw new Error("INVALID_STATE_ERR"); 2411 | } 2412 | } 2413 | 2414 | // filtering to enable a white-list version of Sinon FakeXhr, 2415 | // where whitelisted requests are passed through to real XHR 2416 | function each(collection, callback) { 2417 | if (!collection) return; 2418 | for (var i = 0, l = collection.length; i < l; i += 1) { 2419 | callback(collection[i]); 2420 | } 2421 | } 2422 | function some(collection, callback) { 2423 | for (var index = 0; index < collection.length; index++) { 2424 | if(callback(collection[index]) === true) return true; 2425 | }; 2426 | return false; 2427 | } 2428 | // largest arity in XHR is 5 - XHR#open 2429 | var apply = function(obj,method,args) { 2430 | switch(args.length) { 2431 | case 0: return obj[method](); 2432 | case 1: return obj[method](args[0]); 2433 | case 2: return obj[method](args[0],args[1]); 2434 | case 3: return obj[method](args[0],args[1],args[2]); 2435 | case 4: return obj[method](args[0],args[1],args[2],args[3]); 2436 | case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]); 2437 | }; 2438 | }; 2439 | 2440 | FakeXMLHttpRequest.filters = []; 2441 | FakeXMLHttpRequest.addFilter = function(fn) { 2442 | this.filters.push(fn) 2443 | }; 2444 | var IE6Re = /MSIE 6/; 2445 | FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) { 2446 | var xhr = new sinon.xhr.workingXHR(); 2447 | each(["open","setRequestHeader","send","abort","getResponseHeader", 2448 | "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"], 2449 | function(method) { 2450 | fakeXhr[method] = function() { 2451 | return apply(xhr,method,arguments); 2452 | }; 2453 | }); 2454 | 2455 | var copyAttrs = function(args) { 2456 | each(args, function(attr) { 2457 | try { 2458 | fakeXhr[attr] = xhr[attr] 2459 | } catch(e) { 2460 | if(!IE6Re.test(navigator.userAgent)) throw e; 2461 | } 2462 | }); 2463 | }; 2464 | 2465 | var stateChange = function() { 2466 | fakeXhr.readyState = xhr.readyState; 2467 | if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) { 2468 | copyAttrs(["status","statusText"]); 2469 | } 2470 | if(xhr.readyState >= FakeXMLHttpRequest.LOADING) { 2471 | copyAttrs(["responseText"]); 2472 | } 2473 | if(xhr.readyState === FakeXMLHttpRequest.DONE) { 2474 | copyAttrs(["responseXML"]); 2475 | } 2476 | if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr); 2477 | }; 2478 | if(xhr.addEventListener) { 2479 | for(var event in fakeXhr.eventListeners) { 2480 | if(fakeXhr.eventListeners.hasOwnProperty(event)) { 2481 | each(fakeXhr.eventListeners[event],function(handler) { 2482 | xhr.addEventListener(event, handler); 2483 | }); 2484 | } 2485 | } 2486 | xhr.addEventListener("readystatechange",stateChange); 2487 | } else { 2488 | xhr.onreadystatechange = stateChange; 2489 | } 2490 | apply(xhr,"open",xhrArgs); 2491 | }; 2492 | FakeXMLHttpRequest.useFilters = false; 2493 | 2494 | function verifyRequestSent(xhr) { 2495 | if (xhr.readyState == FakeXMLHttpRequest.DONE) { 2496 | throw new Error("Request done"); 2497 | } 2498 | } 2499 | 2500 | function verifyHeadersReceived(xhr) { 2501 | if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { 2502 | throw new Error("No headers received"); 2503 | } 2504 | } 2505 | 2506 | function verifyResponseBodyType(body) { 2507 | if (typeof body != "string") { 2508 | var error = new Error("Attempted to respond to fake XMLHttpRequest with " + 2509 | body + ", which is not a string."); 2510 | error.name = "InvalidBodyException"; 2511 | throw error; 2512 | } 2513 | } 2514 | 2515 | sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, { 2516 | async: true, 2517 | 2518 | open: function open(method, url, async, username, password) { 2519 | this.method = method; 2520 | this.url = url; 2521 | this.async = typeof async == "boolean" ? async : true; 2522 | this.username = username; 2523 | this.password = password; 2524 | this.responseText = null; 2525 | this.responseXML = null; 2526 | this.requestHeaders = {}; 2527 | this.sendFlag = false; 2528 | if(sinon.FakeXMLHttpRequest.useFilters === true) { 2529 | var xhrArgs = arguments; 2530 | var defake = some(FakeXMLHttpRequest.filters,function(filter) { 2531 | return filter.apply(this,xhrArgs) 2532 | }); 2533 | if (defake) { 2534 | return sinon.FakeXMLHttpRequest.defake(this,arguments); 2535 | } 2536 | } 2537 | this.readyStateChange(FakeXMLHttpRequest.OPENED); 2538 | }, 2539 | 2540 | readyStateChange: function readyStateChange(state) { 2541 | this.readyState = state; 2542 | 2543 | if (typeof this.onreadystatechange == "function") { 2544 | try { 2545 | this.onreadystatechange(); 2546 | } catch (e) { 2547 | sinon.logError("Fake XHR onreadystatechange handler", e); 2548 | } 2549 | } 2550 | 2551 | this.dispatchEvent(new sinon.Event("readystatechange")); 2552 | }, 2553 | 2554 | setRequestHeader: function setRequestHeader(header, value) { 2555 | verifyState(this); 2556 | 2557 | if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { 2558 | throw new Error("Refused to set unsafe header \"" + header + "\""); 2559 | } 2560 | 2561 | if (this.requestHeaders[header]) { 2562 | this.requestHeaders[header] += "," + value; 2563 | } else { 2564 | this.requestHeaders[header] = value; 2565 | } 2566 | }, 2567 | 2568 | // Helps testing 2569 | setResponseHeaders: function setResponseHeaders(headers) { 2570 | this.responseHeaders = {}; 2571 | 2572 | for (var header in headers) { 2573 | if (headers.hasOwnProperty(header)) { 2574 | this.responseHeaders[header] = headers[header]; 2575 | } 2576 | } 2577 | 2578 | if (this.async) { 2579 | this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); 2580 | } 2581 | }, 2582 | 2583 | // Currently treats ALL data as a DOMString (i.e. no Document) 2584 | send: function send(data) { 2585 | verifyState(this); 2586 | 2587 | if (!/^(get|head)$/i.test(this.method)) { 2588 | if (this.requestHeaders["Content-Type"]) { 2589 | var value = this.requestHeaders["Content-Type"].split(";"); 2590 | this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; 2591 | } else { 2592 | this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; 2593 | } 2594 | 2595 | this.requestBody = data; 2596 | } 2597 | 2598 | this.errorFlag = false; 2599 | this.sendFlag = this.async; 2600 | this.readyStateChange(FakeXMLHttpRequest.OPENED); 2601 | 2602 | if (typeof this.onSend == "function") { 2603 | this.onSend(this); 2604 | } 2605 | }, 2606 | 2607 | abort: function abort() { 2608 | this.aborted = true; 2609 | this.responseText = null; 2610 | this.errorFlag = true; 2611 | this.requestHeaders = {}; 2612 | 2613 | if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) { 2614 | this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); 2615 | this.sendFlag = false; 2616 | } 2617 | 2618 | this.readyState = sinon.FakeXMLHttpRequest.UNSENT; 2619 | }, 2620 | 2621 | getResponseHeader: function getResponseHeader(header) { 2622 | if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { 2623 | return null; 2624 | } 2625 | 2626 | if (/^Set-Cookie2?$/i.test(header)) { 2627 | return null; 2628 | } 2629 | 2630 | header = header.toLowerCase(); 2631 | 2632 | for (var h in this.responseHeaders) { 2633 | if (h.toLowerCase() == header) { 2634 | return this.responseHeaders[h]; 2635 | } 2636 | } 2637 | 2638 | return null; 2639 | }, 2640 | 2641 | getAllResponseHeaders: function getAllResponseHeaders() { 2642 | if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { 2643 | return ""; 2644 | } 2645 | 2646 | var headers = ""; 2647 | 2648 | for (var header in this.responseHeaders) { 2649 | if (this.responseHeaders.hasOwnProperty(header) && 2650 | !/^Set-Cookie2?$/i.test(header)) { 2651 | headers += header + ": " + this.responseHeaders[header] + "\r\n"; 2652 | } 2653 | } 2654 | 2655 | return headers; 2656 | }, 2657 | 2658 | setResponseBody: function setResponseBody(body) { 2659 | verifyRequestSent(this); 2660 | verifyHeadersReceived(this); 2661 | verifyResponseBodyType(body); 2662 | 2663 | var chunkSize = this.chunkSize || 10; 2664 | var index = 0; 2665 | this.responseText = ""; 2666 | 2667 | do { 2668 | if (this.async) { 2669 | this.readyStateChange(FakeXMLHttpRequest.LOADING); 2670 | } 2671 | 2672 | this.responseText += body.substring(index, index + chunkSize); 2673 | index += chunkSize; 2674 | } while (index < body.length); 2675 | 2676 | var type = this.getResponseHeader("Content-Type"); 2677 | 2678 | if (this.responseText && 2679 | (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { 2680 | try { 2681 | this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); 2682 | } catch (e) { 2683 | // Unable to parse XML - no biggie 2684 | } 2685 | } 2686 | 2687 | if (this.async) { 2688 | this.readyStateChange(FakeXMLHttpRequest.DONE); 2689 | } else { 2690 | this.readyState = FakeXMLHttpRequest.DONE; 2691 | } 2692 | }, 2693 | 2694 | respond: function respond(status, headers, body) { 2695 | this.setResponseHeaders(headers || {}); 2696 | this.status = typeof status == "number" ? status : 200; 2697 | this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; 2698 | this.setResponseBody(body || ""); 2699 | } 2700 | }); 2701 | 2702 | sinon.extend(FakeXMLHttpRequest, { 2703 | UNSENT: 0, 2704 | OPENED: 1, 2705 | HEADERS_RECEIVED: 2, 2706 | LOADING: 3, 2707 | DONE: 4 2708 | }); 2709 | 2710 | // Borrowed from JSpec 2711 | FakeXMLHttpRequest.parseXML = function parseXML(text) { 2712 | var xmlDoc; 2713 | 2714 | if (typeof DOMParser != "undefined") { 2715 | var parser = new DOMParser(); 2716 | xmlDoc = parser.parseFromString(text, "text/xml"); 2717 | } else { 2718 | xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); 2719 | xmlDoc.async = "false"; 2720 | xmlDoc.loadXML(text); 2721 | } 2722 | 2723 | return xmlDoc; 2724 | }; 2725 | 2726 | FakeXMLHttpRequest.statusCodes = { 2727 | 100: "Continue", 2728 | 101: "Switching Protocols", 2729 | 200: "OK", 2730 | 201: "Created", 2731 | 202: "Accepted", 2732 | 203: "Non-Authoritative Information", 2733 | 204: "No Content", 2734 | 205: "Reset Content", 2735 | 206: "Partial Content", 2736 | 300: "Multiple Choice", 2737 | 301: "Moved Permanently", 2738 | 302: "Found", 2739 | 303: "See Other", 2740 | 304: "Not Modified", 2741 | 305: "Use Proxy", 2742 | 307: "Temporary Redirect", 2743 | 400: "Bad Request", 2744 | 401: "Unauthorized", 2745 | 402: "Payment Required", 2746 | 403: "Forbidden", 2747 | 404: "Not Found", 2748 | 405: "Method Not Allowed", 2749 | 406: "Not Acceptable", 2750 | 407: "Proxy Authentication Required", 2751 | 408: "Request Timeout", 2752 | 409: "Conflict", 2753 | 410: "Gone", 2754 | 411: "Length Required", 2755 | 412: "Precondition Failed", 2756 | 413: "Request Entity Too Large", 2757 | 414: "Request-URI Too Long", 2758 | 415: "Unsupported Media Type", 2759 | 416: "Requested Range Not Satisfiable", 2760 | 417: "Expectation Failed", 2761 | 422: "Unprocessable Entity", 2762 | 500: "Internal Server Error", 2763 | 501: "Not Implemented", 2764 | 502: "Bad Gateway", 2765 | 503: "Service Unavailable", 2766 | 504: "Gateway Timeout", 2767 | 505: "HTTP Version Not Supported" 2768 | }; 2769 | 2770 | sinon.useFakeXMLHttpRequest = function () { 2771 | sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { 2772 | if (xhr.supportsXHR) { 2773 | global.XMLHttpRequest = xhr.GlobalXMLHttpRequest; 2774 | } 2775 | 2776 | if (xhr.supportsActiveX) { 2777 | global.ActiveXObject = xhr.GlobalActiveXObject; 2778 | } 2779 | 2780 | delete sinon.FakeXMLHttpRequest.restore; 2781 | 2782 | if (keepOnCreate !== true) { 2783 | delete sinon.FakeXMLHttpRequest.onCreate; 2784 | } 2785 | }; 2786 | if (xhr.supportsXHR) { 2787 | global.XMLHttpRequest = sinon.FakeXMLHttpRequest; 2788 | } 2789 | 2790 | if (xhr.supportsActiveX) { 2791 | global.ActiveXObject = function ActiveXObject(objId) { 2792 | if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) { 2793 | 2794 | return new sinon.FakeXMLHttpRequest(); 2795 | } 2796 | 2797 | return new xhr.GlobalActiveXObject(objId); 2798 | }; 2799 | } 2800 | 2801 | return sinon.FakeXMLHttpRequest; 2802 | }; 2803 | 2804 | sinon.FakeXMLHttpRequest = FakeXMLHttpRequest; 2805 | })(this); 2806 | 2807 | if (typeof module == "object" && typeof require == "function") { 2808 | module.exports = sinon; 2809 | } 2810 | 2811 | /** 2812 | * @depend fake_xml_http_request.js 2813 | */ 2814 | /*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ 2815 | /*global module, require, window*/ 2816 | /** 2817 | * The Sinon "server" mimics a web server that receives requests from 2818 | * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, 2819 | * both synchronously and asynchronously. To respond synchronuously, canned 2820 | * answers have to be provided upfront. 2821 | * 2822 | * @author Christian Johansen (christian@cjohansen.no) 2823 | * @license BSD 2824 | * 2825 | * Copyright (c) 2010-2011 Christian Johansen 2826 | */ 2827 | 2828 | if (typeof sinon == "undefined") { 2829 | var sinon = {}; 2830 | } 2831 | 2832 | sinon.fakeServer = (function () { 2833 | var push = [].push; 2834 | function F() {} 2835 | 2836 | function create(proto) { 2837 | F.prototype = proto; 2838 | return new F(); 2839 | } 2840 | 2841 | function responseArray(handler) { 2842 | var response = handler; 2843 | 2844 | if (Object.prototype.toString.call(handler) != "[object Array]") { 2845 | response = [200, {}, handler]; 2846 | } 2847 | 2848 | if (typeof response[2] != "string") { 2849 | throw new TypeError("Fake server response body should be string, but was " + 2850 | typeof response[2]); 2851 | } 2852 | 2853 | return response; 2854 | } 2855 | 2856 | var wloc = window.location; 2857 | var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host); 2858 | 2859 | function matchOne(response, reqMethod, reqUrl) { 2860 | var rmeth = response.method; 2861 | var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); 2862 | var url = response.url; 2863 | var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl)); 2864 | 2865 | return matchMethod && matchUrl; 2866 | } 2867 | 2868 | function match(response, request) { 2869 | var requestMethod = this.getHTTPMethod(request); 2870 | var requestUrl = request.url; 2871 | 2872 | if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { 2873 | requestUrl = requestUrl.replace(rCurrLoc, ""); 2874 | } 2875 | 2876 | if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { 2877 | if (typeof response.response == "function") { 2878 | var ru = response.url; 2879 | var args = [request].concat(!ru ? [] : requestUrl.match(ru).slice(1)); 2880 | return response.response.apply(response, args); 2881 | } 2882 | 2883 | return true; 2884 | } 2885 | 2886 | return false; 2887 | } 2888 | 2889 | return { 2890 | create: function () { 2891 | var server = create(this); 2892 | this.xhr = sinon.useFakeXMLHttpRequest(); 2893 | server.requests = []; 2894 | 2895 | this.xhr.onCreate = function (xhrObj) { 2896 | server.addRequest(xhrObj); 2897 | }; 2898 | 2899 | return server; 2900 | }, 2901 | 2902 | addRequest: function addRequest(xhrObj) { 2903 | var server = this; 2904 | push.call(this.requests, xhrObj); 2905 | 2906 | xhrObj.onSend = function () { 2907 | server.handleRequest(this); 2908 | }; 2909 | 2910 | if (this.autoRespond && !this.responding) { 2911 | setTimeout(function () { 2912 | server.responding = false; 2913 | server.respond(); 2914 | }, this.autoRespondAfter || 10); 2915 | 2916 | this.responding = true; 2917 | } 2918 | }, 2919 | 2920 | getHTTPMethod: function getHTTPMethod(request) { 2921 | if (this.fakeHTTPMethods && /post/i.test(request.method)) { 2922 | var matches = (request.requestBody || "").match(/_method=([^\b;]+)/); 2923 | return !!matches ? matches[1] : request.method; 2924 | } 2925 | 2926 | return request.method; 2927 | }, 2928 | 2929 | handleRequest: function handleRequest(xhr) { 2930 | if (xhr.async) { 2931 | if (!this.queue) { 2932 | this.queue = []; 2933 | } 2934 | 2935 | push.call(this.queue, xhr); 2936 | } else { 2937 | this.processRequest(xhr); 2938 | } 2939 | }, 2940 | 2941 | respondWith: function respondWith(method, url, body) { 2942 | if (arguments.length == 1 && typeof method != "function") { 2943 | this.response = responseArray(method); 2944 | return; 2945 | } 2946 | 2947 | if (!this.responses) { this.responses = []; } 2948 | 2949 | if (arguments.length == 1) { 2950 | body = method; 2951 | url = method = null; 2952 | } 2953 | 2954 | if (arguments.length == 2) { 2955 | body = url; 2956 | url = method; 2957 | method = null; 2958 | } 2959 | 2960 | push.call(this.responses, { 2961 | method: method, 2962 | url: url, 2963 | response: typeof body == "function" ? body : responseArray(body) 2964 | }); 2965 | }, 2966 | 2967 | respond: function respond() { 2968 | if (arguments.length > 0) this.respondWith.apply(this, arguments); 2969 | var queue = this.queue || []; 2970 | var request; 2971 | 2972 | while(request = queue.shift()) { 2973 | this.processRequest(request); 2974 | } 2975 | }, 2976 | 2977 | processRequest: function processRequest(request) { 2978 | try { 2979 | if (request.aborted) { 2980 | return; 2981 | } 2982 | 2983 | var response = this.response || [404, {}, ""]; 2984 | 2985 | if (this.responses) { 2986 | for (var i = 0, l = this.responses.length; i < l; i++) { 2987 | if (match.call(this, this.responses[i], request)) { 2988 | response = this.responses[i].response; 2989 | break; 2990 | } 2991 | } 2992 | } 2993 | 2994 | if (request.readyState != 4) { 2995 | request.respond(response[0], response[1], response[2]); 2996 | } 2997 | } catch (e) { 2998 | sinon.logError("Fake server request processing", e); 2999 | } 3000 | }, 3001 | 3002 | restore: function restore() { 3003 | return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); 3004 | } 3005 | }; 3006 | }()); 3007 | 3008 | if (typeof module == "object" && typeof require == "function") { 3009 | module.exports = sinon; 3010 | } 3011 | 3012 | /** 3013 | * @depend fake_server.js 3014 | * @depend fake_timers.js 3015 | */ 3016 | /*jslint browser: true, eqeqeq: false, onevar: false*/ 3017 | /*global sinon*/ 3018 | /** 3019 | * Add-on for sinon.fakeServer that automatically handles a fake timer along with 3020 | * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery 3021 | * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, 3022 | * it polls the object for completion with setInterval. Dispite the direct 3023 | * motivation, there is nothing jQuery-specific in this file, so it can be used 3024 | * in any environment where the ajax implementation depends on setInterval or 3025 | * setTimeout. 3026 | * 3027 | * @author Christian Johansen (christian@cjohansen.no) 3028 | * @license BSD 3029 | * 3030 | * Copyright (c) 2010-2011 Christian Johansen 3031 | */ 3032 | 3033 | (function () { 3034 | function Server() {} 3035 | Server.prototype = sinon.fakeServer; 3036 | 3037 | sinon.fakeServerWithClock = new Server(); 3038 | 3039 | sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { 3040 | if (xhr.async) { 3041 | if (typeof setTimeout.clock == "object") { 3042 | this.clock = setTimeout.clock; 3043 | } else { 3044 | this.clock = sinon.useFakeTimers(); 3045 | this.resetClock = true; 3046 | } 3047 | 3048 | if (!this.longestTimeout) { 3049 | var clockSetTimeout = this.clock.setTimeout; 3050 | var clockSetInterval = this.clock.setInterval; 3051 | var server = this; 3052 | 3053 | this.clock.setTimeout = function (fn, timeout) { 3054 | server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); 3055 | 3056 | return clockSetTimeout.apply(this, arguments); 3057 | }; 3058 | 3059 | this.clock.setInterval = function (fn, timeout) { 3060 | server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); 3061 | 3062 | return clockSetInterval.apply(this, arguments); 3063 | }; 3064 | } 3065 | } 3066 | 3067 | return sinon.fakeServer.addRequest.call(this, xhr); 3068 | }; 3069 | 3070 | sinon.fakeServerWithClock.respond = function respond() { 3071 | var returnVal = sinon.fakeServer.respond.apply(this, arguments); 3072 | 3073 | if (this.clock) { 3074 | this.clock.tick(this.longestTimeout || 0); 3075 | this.longestTimeout = 0; 3076 | 3077 | if (this.resetClock) { 3078 | this.clock.restore(); 3079 | this.resetClock = false; 3080 | } 3081 | } 3082 | 3083 | return returnVal; 3084 | }; 3085 | 3086 | sinon.fakeServerWithClock.restore = function restore() { 3087 | if (this.clock) { 3088 | this.clock.restore(); 3089 | } 3090 | 3091 | return sinon.fakeServer.restore.apply(this, arguments); 3092 | }; 3093 | }()); 3094 | 3095 | /** 3096 | * @depend ../sinon.js 3097 | * @depend collection.js 3098 | * @depend util/fake_timers.js 3099 | * @depend util/fake_server_with_clock.js 3100 | */ 3101 | /*jslint eqeqeq: false, onevar: false, plusplus: false*/ 3102 | /*global require, module*/ 3103 | /** 3104 | * Manages fake collections as well as fake utilities such as Sinon's 3105 | * timers and fake XHR implementation in one convenient object. 3106 | * 3107 | * @author Christian Johansen (christian@cjohansen.no) 3108 | * @license BSD 3109 | * 3110 | * Copyright (c) 2010-2011 Christian Johansen 3111 | */ 3112 | 3113 | if (typeof module == "object" && typeof require == "function") { 3114 | var sinon = require("../sinon"); 3115 | sinon.extend(sinon, require("./util/fake_timers")); 3116 | } 3117 | 3118 | (function () { 3119 | var push = [].push; 3120 | 3121 | function exposeValue(sandbox, config, key, value) { 3122 | if (!value) { 3123 | return; 3124 | } 3125 | 3126 | if (config.injectInto) { 3127 | config.injectInto[key] = value; 3128 | } else { 3129 | push.call(sandbox.args, value); 3130 | } 3131 | } 3132 | 3133 | function prepareSandboxFromConfig(config) { 3134 | var sandbox = sinon.create(sinon.sandbox); 3135 | 3136 | if (config.useFakeServer) { 3137 | if (typeof config.useFakeServer == "object") { 3138 | sandbox.serverPrototype = config.useFakeServer; 3139 | } 3140 | 3141 | sandbox.useFakeServer(); 3142 | } 3143 | 3144 | if (config.useFakeTimers) { 3145 | if (typeof config.useFakeTimers == "object") { 3146 | sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers); 3147 | } else { 3148 | sandbox.useFakeTimers(); 3149 | } 3150 | } 3151 | 3152 | return sandbox; 3153 | } 3154 | 3155 | sinon.sandbox = sinon.extend(sinon.create(sinon.collection), { 3156 | useFakeTimers: function useFakeTimers() { 3157 | this.clock = sinon.useFakeTimers.apply(sinon, arguments); 3158 | 3159 | return this.add(this.clock); 3160 | }, 3161 | 3162 | serverPrototype: sinon.fakeServer, 3163 | 3164 | useFakeServer: function useFakeServer() { 3165 | var proto = this.serverPrototype || sinon.fakeServer; 3166 | 3167 | if (!proto || !proto.create) { 3168 | return null; 3169 | } 3170 | 3171 | this.server = proto.create(); 3172 | return this.add(this.server); 3173 | }, 3174 | 3175 | inject: function (obj) { 3176 | sinon.collection.inject.call(this, obj); 3177 | 3178 | if (this.clock) { 3179 | obj.clock = this.clock; 3180 | } 3181 | 3182 | if (this.server) { 3183 | obj.server = this.server; 3184 | obj.requests = this.server.requests; 3185 | } 3186 | 3187 | return obj; 3188 | }, 3189 | 3190 | create: function (config) { 3191 | if (!config) { 3192 | return sinon.create(sinon.sandbox); 3193 | } 3194 | 3195 | var sandbox = prepareSandboxFromConfig(config); 3196 | sandbox.args = sandbox.args || []; 3197 | var prop, value, exposed = sandbox.inject({}); 3198 | 3199 | if (config.properties) { 3200 | for (var i = 0, l = config.properties.length; i < l; i++) { 3201 | prop = config.properties[i]; 3202 | value = exposed[prop] || prop == "sandbox" && sandbox; 3203 | exposeValue(sandbox, config, prop, value); 3204 | } 3205 | } else { 3206 | exposeValue(sandbox, config, "sandbox", value); 3207 | } 3208 | 3209 | return sandbox; 3210 | } 3211 | }); 3212 | 3213 | sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer; 3214 | 3215 | if (typeof module == "object" && typeof require == "function") { 3216 | module.exports = sinon.sandbox; 3217 | } 3218 | }()); 3219 | 3220 | /** 3221 | * @depend ../sinon.js 3222 | * @depend stub.js 3223 | * @depend mock.js 3224 | * @depend sandbox.js 3225 | */ 3226 | /*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/ 3227 | /*global module, require, sinon*/ 3228 | /** 3229 | * Test function, sandboxes fakes 3230 | * 3231 | * @author Christian Johansen (christian@cjohansen.no) 3232 | * @license BSD 3233 | * 3234 | * Copyright (c) 2010-2011 Christian Johansen 3235 | */ 3236 | 3237 | (function (sinon) { 3238 | var commonJSModule = typeof module == "object" && typeof require == "function"; 3239 | 3240 | if (!sinon && commonJSModule) { 3241 | sinon = require("../sinon"); 3242 | } 3243 | 3244 | if (!sinon) { 3245 | return; 3246 | } 3247 | 3248 | function test(callback) { 3249 | var type = typeof callback; 3250 | 3251 | if (type != "function") { 3252 | throw new TypeError("sinon.test needs to wrap a test function, got " + type); 3253 | } 3254 | 3255 | return function () { 3256 | var config = sinon.getConfig(sinon.config); 3257 | config.injectInto = config.injectIntoThis && this || config.injectInto; 3258 | var sandbox = sinon.sandbox.create(config); 3259 | var exception, result; 3260 | var args = Array.prototype.slice.call(arguments).concat(sandbox.args); 3261 | 3262 | try { 3263 | result = callback.apply(this, args); 3264 | } finally { 3265 | sandbox.verifyAndRestore(); 3266 | } 3267 | 3268 | return result; 3269 | }; 3270 | } 3271 | 3272 | test.config = { 3273 | injectIntoThis: true, 3274 | injectInto: null, 3275 | properties: ["spy", "stub", "mock", "clock", "server", "requests"], 3276 | useFakeTimers: true, 3277 | useFakeServer: true 3278 | }; 3279 | 3280 | if (commonJSModule) { 3281 | module.exports = test; 3282 | } else { 3283 | sinon.test = test; 3284 | } 3285 | }(typeof sinon == "object" && sinon || null)); 3286 | 3287 | /** 3288 | * @depend ../sinon.js 3289 | * @depend test.js 3290 | */ 3291 | /*jslint eqeqeq: false, onevar: false, eqeqeq: false*/ 3292 | /*global module, require, sinon*/ 3293 | /** 3294 | * Test case, sandboxes all test functions 3295 | * 3296 | * @author Christian Johansen (christian@cjohansen.no) 3297 | * @license BSD 3298 | * 3299 | * Copyright (c) 2010-2011 Christian Johansen 3300 | */ 3301 | 3302 | (function (sinon) { 3303 | var commonJSModule = typeof module == "object" && typeof require == "function"; 3304 | 3305 | if (!sinon && commonJSModule) { 3306 | sinon = require("../sinon"); 3307 | } 3308 | 3309 | if (!sinon || !Object.prototype.hasOwnProperty) { 3310 | return; 3311 | } 3312 | 3313 | function createTest(property, setUp, tearDown) { 3314 | return function () { 3315 | if (setUp) { 3316 | setUp.apply(this, arguments); 3317 | } 3318 | 3319 | var exception, result; 3320 | 3321 | try { 3322 | result = property.apply(this, arguments); 3323 | } catch (e) { 3324 | exception = e; 3325 | } 3326 | 3327 | if (tearDown) { 3328 | tearDown.apply(this, arguments); 3329 | } 3330 | 3331 | if (exception) { 3332 | throw exception; 3333 | } 3334 | 3335 | return result; 3336 | }; 3337 | } 3338 | 3339 | function testCase(tests, prefix) { 3340 | /*jsl:ignore*/ 3341 | if (!tests || typeof tests != "object") { 3342 | throw new TypeError("sinon.testCase needs an object with test functions"); 3343 | } 3344 | /*jsl:end*/ 3345 | 3346 | prefix = prefix || "test"; 3347 | var rPrefix = new RegExp("^" + prefix); 3348 | var methods = {}, testName, property, method; 3349 | var setUp = tests.setUp; 3350 | var tearDown = tests.tearDown; 3351 | 3352 | for (testName in tests) { 3353 | if (tests.hasOwnProperty(testName)) { 3354 | property = tests[testName]; 3355 | 3356 | if (/^(setUp|tearDown)$/.test(testName)) { 3357 | continue; 3358 | } 3359 | 3360 | if (typeof property == "function" && rPrefix.test(testName)) { 3361 | method = property; 3362 | 3363 | if (setUp || tearDown) { 3364 | method = createTest(property, setUp, tearDown); 3365 | } 3366 | 3367 | methods[testName] = sinon.test(method); 3368 | } else { 3369 | methods[testName] = tests[testName]; 3370 | } 3371 | } 3372 | } 3373 | 3374 | return methods; 3375 | } 3376 | 3377 | if (commonJSModule) { 3378 | module.exports = testCase; 3379 | } else { 3380 | sinon.testCase = testCase; 3381 | } 3382 | }(typeof sinon == "object" && sinon || null)); 3383 | 3384 | /** 3385 | * @depend ../sinon.js 3386 | * @depend stub.js 3387 | */ 3388 | /*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/ 3389 | /*global module, require, sinon*/ 3390 | /** 3391 | * Assertions matching the test spy retrieval interface. 3392 | * 3393 | * @author Christian Johansen (christian@cjohansen.no) 3394 | * @license BSD 3395 | * 3396 | * Copyright (c) 2010-2011 Christian Johansen 3397 | */ 3398 | 3399 | (function (sinon, global) { 3400 | var commonJSModule = typeof module == "object" && typeof require == "function"; 3401 | var slice = Array.prototype.slice; 3402 | var assert; 3403 | 3404 | if (!sinon && commonJSModule) { 3405 | sinon = require("../sinon"); 3406 | } 3407 | 3408 | if (!sinon) { 3409 | return; 3410 | } 3411 | 3412 | function verifyIsStub() { 3413 | var method; 3414 | 3415 | for (var i = 0, l = arguments.length; i < l; ++i) { 3416 | method = arguments[i]; 3417 | 3418 | if (!method) { 3419 | assert.fail("fake is not a spy"); 3420 | } 3421 | 3422 | if (typeof method != "function") { 3423 | assert.fail(method + " is not a function"); 3424 | } 3425 | 3426 | if (typeof method.getCall != "function") { 3427 | assert.fail(method + " is not stubbed"); 3428 | } 3429 | } 3430 | } 3431 | 3432 | function failAssertion(object, msg) { 3433 | object = object || global; 3434 | var failMethod = object.fail || assert.fail; 3435 | failMethod.call(object, msg); 3436 | } 3437 | 3438 | function mirrorPropAsAssertion(name, method, message) { 3439 | if (arguments.length == 2) { 3440 | message = method; 3441 | method = name; 3442 | } 3443 | 3444 | assert[name] = function (fake) { 3445 | verifyIsStub(fake); 3446 | 3447 | var args = slice.call(arguments, 1); 3448 | var failed = false; 3449 | 3450 | if (typeof method == "function") { 3451 | failed = !method(fake); 3452 | } else { 3453 | failed = typeof fake[method] == "function" ? 3454 | !fake[method].apply(fake, args) : !fake[method]; 3455 | } 3456 | 3457 | if (failed) { 3458 | failAssertion(this, fake.printf.apply(fake, [message].concat(args))); 3459 | } else { 3460 | assert.pass(name); 3461 | } 3462 | }; 3463 | } 3464 | 3465 | function exposedName(prefix, prop) { 3466 | return !prefix || /^fail/.test(prop) ? prop : 3467 | prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1); 3468 | }; 3469 | 3470 | assert = { 3471 | failException: "AssertError", 3472 | 3473 | fail: function fail(message) { 3474 | var error = new Error(message); 3475 | error.name = this.failException || assert.failException; 3476 | 3477 | throw error; 3478 | }, 3479 | 3480 | pass: function pass(assertion) {}, 3481 | 3482 | callOrder: function assertCallOrder() { 3483 | verifyIsStub.apply(null, arguments); 3484 | var expected = "", actual = ""; 3485 | 3486 | if (!sinon.calledInOrder(arguments)) { 3487 | try { 3488 | expected = [].join.call(arguments, ", "); 3489 | actual = sinon.orderByFirstCall(slice.call(arguments)).join(", "); 3490 | } catch (e) { 3491 | // If this fails, we'll just fall back to the blank string 3492 | } 3493 | 3494 | failAssertion(this, "expected " + expected + " to be " + 3495 | "called in order but were called as " + actual); 3496 | } else { 3497 | assert.pass("callOrder"); 3498 | } 3499 | }, 3500 | 3501 | callCount: function assertCallCount(method, count) { 3502 | verifyIsStub(method); 3503 | 3504 | if (method.callCount != count) { 3505 | var msg = "expected %n to be called " + sinon.timesInWords(count) + 3506 | " but was called %c%C"; 3507 | failAssertion(this, method.printf(msg)); 3508 | } else { 3509 | assert.pass("callCount"); 3510 | } 3511 | }, 3512 | 3513 | expose: function expose(target, options) { 3514 | if (!target) { 3515 | throw new TypeError("target is null or undefined"); 3516 | } 3517 | 3518 | var o = options || {}; 3519 | var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix; 3520 | var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail; 3521 | 3522 | for (var method in this) { 3523 | if (method != "export" && (includeFail || !/^(fail)/.test(method))) { 3524 | target[exposedName(prefix, method)] = this[method]; 3525 | } 3526 | } 3527 | 3528 | return target; 3529 | } 3530 | }; 3531 | 3532 | mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called"); 3533 | mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; }, 3534 | "expected %n to not have been called but was called %c%C"); 3535 | mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C"); 3536 | mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C"); 3537 | mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C"); 3538 | mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t"); 3539 | mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t"); 3540 | mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C"); 3541 | mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C"); 3542 | mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C"); 3543 | mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C"); 3544 | mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C"); 3545 | mirrorPropAsAssertion("threw", "%n did not throw exception%C"); 3546 | mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C"); 3547 | 3548 | if (commonJSModule) { 3549 | module.exports = assert; 3550 | } else { 3551 | sinon.assert = assert; 3552 | } 3553 | }(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : global)); 3554 | 3555 | return sinon;}.call(typeof window != 'undefined' && window || {})); 3556 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.ajax-retry", 3 | "title": "jQuery Ajax Retry", 4 | "description": "Retry ajax calls using the deferred API", 5 | "version": "1.0.0", 6 | "homepage": "https://github.com/johnkpaul/jquery-ajax-retry", 7 | "author": { 8 | "name": "John Paul", 9 | "email": "john@johnkpaul.com", 10 | "url": "http://www.johnkpaul.com" 11 | }, 12 | "maintainers": [ 13 | { 14 | "name": "John Paul", 15 | "email": "john@johnkpaul.com", 16 | "url": "http://www.johnkpaul.com" 17 | } 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/johnkpaul/jquery-ajax-retry.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/johnkpaul/jquery-ajax-retry/issues" 25 | }, 26 | "licenses": [ 27 | { 28 | "type": "MIT", 29 | "url": "https://github.com/johnkpaul/jquery-ajax-retry/blob/master/LICENSE-MIT" 30 | } 31 | ], 32 | "peerDependencies": { 33 | "jquery": ">=1.5" 34 | }, 35 | "dependencies": { 36 | "grunt-contrib-qunit": "~0.5.1", 37 | "grunt-contrib-concat": "~0.4.0" 38 | }, 39 | "devDependencies": { 40 | "grunt": "~0.4.5", 41 | "load-grunt-tasks": "~0.5.0", 42 | "grunt-contrib-uglify": "~0.5.0", 43 | "grunt-contrib-jshint": "~0.10.0" 44 | }, 45 | "keywords": [ 46 | "ajax", 47 | "retry" 48 | ], 49 | "main": "src/jquery.ajax-retry.js" 50 | } 51 | -------------------------------------------------------------------------------- /src/jquery.ajax-retry.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.ajax-retry 3 | * https://github.com/johnkpaul/jquery-ajax-retry 4 | * 5 | * Copyright (c) 2012 John Paul 6 | * Licensed under the MIT license. 7 | */ 8 | (function(factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as an anonymous module. 11 | define(['jquery'], factory); 12 | } else if (typeof exports === 'object') { 13 | // Node/CommonJS 14 | factory(require('jquery')); 15 | } else { 16 | // Browser globals 17 | factory(jQuery); 18 | } 19 | })(function($) { 20 | 21 | // enhance all ajax requests with our retry API 22 | $.ajaxPrefilter(function(options, originalOptions, jqXHR) { 23 | jqXHR.retry = function(opts) { 24 | if(opts.timeout) { 25 | this.timeout = opts.timeout; 26 | } 27 | if (opts.statusCodes) { 28 | this.statusCodes = opts.statusCodes; 29 | } 30 | return this.pipe(null, pipeFailRetry(this, opts)); 31 | }; 32 | }); 33 | 34 | // generates a fail pipe function that will retry `jqXHR` `times` more times 35 | function pipeFailRetry(jqXHR, opts) { 36 | var times = opts.times; 37 | var timeout = jqXHR.timeout; 38 | 39 | // takes failure data as input, returns a new deferred 40 | return function(input, status, msg) { 41 | var ajaxOptions = this; 42 | var output = new $.Deferred(); 43 | var retryAfter = jqXHR.getResponseHeader('Retry-After'); 44 | 45 | // whenever we do make this request, pipe its output to our deferred 46 | function nextRequest() { 47 | $.ajax(ajaxOptions) 48 | .retry({times: times - 1, timeout: opts.timeout, statusCodes: opts.statusCodes}) 49 | .pipe(output.resolve, output.reject); 50 | } 51 | 52 | if (times > 1 && (!jqXHR.statusCodes || $.inArray(input.status, jqXHR.statusCodes) > -1)) { 53 | // implement Retry-After rfc 54 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 55 | if (retryAfter) { 56 | // it must be a date 57 | if (isNaN(retryAfter)) { 58 | timeout = new Date(retryAfter).getTime() - $.now(); 59 | // its a number in seconds 60 | } else { 61 | timeout = parseInt(retryAfter, 10) * 1000; 62 | } 63 | // ensure timeout is a positive number 64 | if (isNaN(timeout) || timeout < 0) { 65 | timeout = jqXHR.timeout; 66 | } 67 | } 68 | 69 | if (timeout !== undefined){ 70 | setTimeout(nextRequest, timeout); 71 | } else { 72 | nextRequest(); 73 | } 74 | } else { 75 | // no times left, reject our deferred with the current arguments 76 | output.rejectWith(this, arguments); 77 | } 78 | 79 | return output; 80 | }; 81 | } 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /test/jquery.ajax-retry.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Ajax Retry Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

    jQuery Ajax Retry Test Suite

    19 |

    20 |
    21 |

    22 |
      23 |
      24 | lame test markup 25 | normal test markup 26 | awesome test markup 27 |
      28 | 29 | 30 | -------------------------------------------------------------------------------- /test/jquery.ajax-retry_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 | /*global sinon:true */ 5 | (function($) { 6 | 7 | /* 8 | ======== A Handy Little QUnit Reference ======== 9 | http://docs.jquery.com/QUnit 10 | 11 | Test methods: 12 | expect(numAssertions) 13 | stop(increment) 14 | start(decrement) 15 | Test assertions: 16 | ok(value, [message]) 17 | equal(actual, expected, [message]) 18 | notEqual(actual, expected, [message]) 19 | deepEqual(actual, expected, [message]) 20 | notDeepEqual(actual, expected, [message]) 21 | strictEqual(actual, expected, [message]) 22 | notStrictEqual(actual, expected, [message]) 23 | raises(block, [expected], [message]) 24 | */ 25 | 26 | module('jQuery retry tries again', { 27 | setup: function() { 28 | this.xhr = sinon.useFakeXMLHttpRequest(); 29 | var requests = this.requests = []; 30 | this.xhr.onCreate = function (xhr) { 31 | requests.push(xhr); 32 | }; 33 | }, 34 | teardown: function(){ 35 | this.xhr.restore(); 36 | } 37 | }); 38 | 39 | test('ajax Deferreds have new retry method', 1, function() { 40 | var def = $.ajax({url:"/test",data:{},type:"POST"}); 41 | var hasRetry = "retry" in def; 42 | ok(hasRetry); 43 | }); 44 | 45 | test('ajax Deferreds have existing done and then', 1, function() { 46 | var def = $.ajax({url:"/test",data:{},type:"POST"}); 47 | var hasOld = "done" in def && "then" in def; 48 | ok(hasOld); 49 | }); 50 | 51 | test('ajax Deferred works as original in the case of a 200', 1, function() { 52 | var def = $.post("/test",{}); 53 | this.requests[0].respond(200, { "Content-Type": "application/json" }, 54 | '{ "id": 12, "comment": "Hey there" }'); 55 | def.then(function(data){ 56 | ok(data.id === 12); 57 | }); 58 | }); 59 | 60 | asyncTest('ajax Deferred tries again if needed', 1, function() { 61 | var def = $.post("/test",{}); 62 | def.retry({times:2}).then(function(data){ 63 | ok(data.id === 12); 64 | start(); 65 | }); 66 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 67 | '{ "id": 13, "comment": "error!" }'); 68 | this.requests[1].respond(200, { "Content-Type": "application/json" }, 69 | '{ "id": 12, "comment": "Hey there" }'); 70 | }); 71 | 72 | asyncTest('ajax Deferred tries again if needed', 1, function() { 73 | var def = $.post("/test",{}); 74 | def.retry({times:3}).then(function(data){ 75 | ok(data.id === 12); 76 | start(); 77 | }); 78 | 79 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 80 | '{ "id": 13, "comment": "error!" }'); 81 | this.requests[1].respond(400, { "Content-Type": "application/json" }, 82 | '{ "id": 13, "comment": "error!" }'); 83 | this.requests[2].respond(200, { "Content-Type": "application/json" }, 84 | '{ "id": 12, "comment": "Hey there" }'); 85 | }); 86 | 87 | asyncTest('ajax Deferred tries only once if failing', 0, function() { 88 | var def = $.post("/test",{}); 89 | def.retry({times:2}).fail(function(){ 90 | start(); 91 | }); 92 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 93 | '{ "id": 12, "comment": "error!" }'); 94 | this.requests[1].respond(400, { "Content-Type": "application/json" }, 95 | '{ "id": 12, "comment": "error!" }'); 96 | }); 97 | 98 | asyncTest('ajax Deferred gets correct parameters to fail callback', 1, function() { 99 | var def = $.post("/test",{}); 100 | def.retry({times:2}).fail(function(deferred, status, msg){ 101 | ok(status === "error"); 102 | start(); 103 | }); 104 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 105 | '{ "id": 12, "comment": "error!" }'); 106 | this.requests[1].respond(400, { "Content-Type": "application/json" }, 107 | '{ "id": 12, "comment": "error!" }'); 108 | }); 109 | 110 | test('data is taken from successful response ', 1, function() { 111 | var def = $.post("/test",{}); 112 | 113 | def.retry({times:2}).done(function(data) { 114 | ok(data.id === 12); 115 | }); 116 | 117 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 118 | '{ "id": 11, "comment": "error!" }'); 119 | 120 | 121 | this.requests[1].respond(200, { "Content-Type": "application/json" }, 122 | '{ "id": 12, "comment": "Hey there" }'); 123 | }); 124 | 125 | 126 | module('jQuery retry uses timeout value', { 127 | setup: function() { 128 | this.xhr = sinon.useFakeXMLHttpRequest(); 129 | var requests = this.requests = []; 130 | this.xhr.onCreate = function (xhr) { 131 | requests.push(xhr); 132 | }; 133 | this.clock = sinon.useFakeTimers(); 134 | }, 135 | teardown: function(){ 136 | this.xhr.restore(); 137 | this.clock.restore(); 138 | } 139 | }); 140 | 141 | test('timeout is waited before next retry', 3, function() { 142 | var def = $.post("/test",{}); 143 | 144 | def.retry({times:2, timeout:2000}); 145 | 146 | ok(this.requests.length === 1); 147 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 148 | '{ "id": 11, "comment": "error!" }'); 149 | ok(this.requests.length === 1); 150 | 151 | this.clock.tick(2000); 152 | 153 | ok(this.requests.length === 2); 154 | this.requests[1].respond(200, { "Content-Type": "application/json" }, 155 | '{ "id": 12, "comment": "Hey there" }'); 156 | }); 157 | 158 | test('timeout is waited between multiple retries', 4, function() { 159 | var def = $.post("/test",{}); 160 | 161 | def.retry({times:3, timeout:2000}); 162 | 163 | ok(this.requests.length === 1); 164 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 165 | '{ "id": 11, "comment": "error!" }'); 166 | ok(this.requests.length === 1); 167 | 168 | this.clock.tick(2000); 169 | 170 | ok(this.requests.length === 2); 171 | 172 | this.requests[1].respond(400, { "Content-Type": "application/json" }, 173 | '{ "id": 11, "comment": "error!" }'); 174 | 175 | this.clock.tick(2000); 176 | 177 | ok(this.requests.length === 3); 178 | 179 | this.requests[2].respond(200, { "Content-Type": "application/json" }, 180 | '{ "id": 12, "comment": "Hey there" }'); 181 | }); 182 | 183 | test('retry does not happen, if timeout has not been met', 3, function() { 184 | var def = $.post("/test",{}); 185 | def.retry({times:2, timeout:2000}); 186 | ok(this.requests.length === 1); 187 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 188 | '{ "id": 12, "comment": "error!" }'); 189 | ok(this.requests.length === 1); 190 | this.clock.tick(1999); 191 | ok(this.requests.length === 1); 192 | }); 193 | 194 | test('data is taken from successful response when using timeout option', 1, function() { 195 | var def = $.post("/test",{}); 196 | def.retry({times:2, timeout:2000}).done(function(data) { 197 | ok(data.id === 12); 198 | }); 199 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 200 | '{ "id": 13, "comment": "error!" }'); 201 | this.clock.tick(2000); 202 | this.requests[1].respond(200, { "Content-Type": "application/json" }, 203 | '{ "id": 12, "comment": "error!" }'); 204 | }); 205 | 206 | test('retry-after http header (seconds) is used as timeout', 3, function() { 207 | var def = $.post("/test",{}); 208 | def.retry({times:2, timeout:100}); 209 | ok(this.requests.length === 1); 210 | this.requests[0].respond(400, { 211 | "Content-Type": "application/json", 212 | "Retry-After": "1" 213 | }, '{ "id": 12, "comment": "error!" }'); 214 | this.clock.tick(200); 215 | ok(this.requests.length === 1); 216 | this.clock.tick(1001); 217 | ok(this.requests.length === 2); 218 | }); 219 | 220 | test('retry-after http header (HTTP-date) is used as timeout', 3, function() { 221 | var def = $.post("/test",{}); 222 | def.retry({times:2, timeout:100}); 223 | ok(this.requests.length === 1); 224 | this.requests[0].respond(400, { 225 | "Content-Type": "application/json", 226 | "Retry-After": new Date($.now() + 3000) 227 | }, '{ "id": 12, "comment": "error!" }'); 228 | this.clock.tick(200); 229 | ok(this.requests.length === 1); 230 | this.clock.tick(3001); 231 | ok(this.requests.length === 2); 232 | }); 233 | 234 | module('jQuery retry uses retry codes', { 235 | setup: function() { 236 | this.xhr = sinon.useFakeXMLHttpRequest(); 237 | var requests = this.requests = []; 238 | this.xhr.onCreate = function (xhr) { 239 | requests.push(xhr); 240 | }; 241 | }, 242 | teardown: function(){ 243 | this.xhr.restore(); 244 | } 245 | }); 246 | 247 | asyncTest('retry happens on provided status code', 1, function() { 248 | var def = $.post("/test",{}); 249 | def.retry({times:2, statusCodes: [503]}).then(function(data){ 250 | ok(data.id === 12); 251 | start(); 252 | }); 253 | this.requests[0].respond(503, { "Content-Type": "application/json" }, 254 | '{ "id": 13, "comment": "error!" }'); 255 | this.requests[1].respond(200, { "Content-Type": "application/json" }, 256 | '{ "id": 12, "comment": "Hey there" }'); 257 | }); 258 | 259 | asyncTest('retry does not happen if status code is different from provided', 1, function() { 260 | var def = $.post("/test",{ 261 | error: function(data) { 262 | ok(true); 263 | start(); 264 | } 265 | }); 266 | 267 | def.retry({times:2, statusCodes:[503]}); 268 | 269 | this.requests[0].respond(400, { "Content-Type": "application/json" }, 270 | '{ "id": 12, "comment": "error!" }'); 271 | 272 | }); 273 | 274 | }(jQuery)); 275 | --------------------------------------------------------------------------------