├── .gitignore ├── README.md ├── app.js ├── jshintConfig.json ├── karma.app.conf.js ├── package.json ├── run-tests.sh ├── test ├── compiler.jar ├── deps.js ├── jshintConfigTests.json ├── openmix-externs.js ├── resources │ ├── qunit-1.15.0.css │ ├── qunit-1.15.0.js │ ├── sinon-1.10.3.js │ └── sinon-qunit-1.0.0.js ├── test.html └── tests.js └── validate-js.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | karma.app.results.xml 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsDelivr Openmix DNS application 2 | 3 | #### DEPRICATED - https://www.jsdelivr.com/blog/introducing-next-gen-load-balancing-at-jsdelivr/ 4 | 5 | ## Description 6 | 7 | This is the live code that jsDelivr uses to do load balancing. 8 | 9 | By default only MaxCDN and CloudFlare are considered for any requests. Set in array `defaultProviders` 10 | 11 | But by using the `countryMapping` array we set overrides and change the default providers that the system will consider for different countries. 12 | 13 | `asnMapping` sets ASN overides for monitoring tools because we collect Real User Metrics so the system does not work very well with synthetic tests originated from ISPs used by hosting providers 14 | 15 | ## Validating the Code 16 | 17 | The validate-js.sh script looks for possible errors in the application and test 18 | code. You must have Java installed for it to run. It executes both the Google 19 | Closure compiler and JSHint. 20 | 21 | Here's an example where Google Closure compiler detects a misspelled property 22 | name, and JSLint detects that we forgot to comment-out a console print 23 | statement. We'd want to fix these issues before uploading the Openmix app. 24 | 25 | $ ./validate-js.sh 26 | 27 | Validating with Google Closure Compiler 28 | 29 | app.js:171: WARNING - Property geo_default_on_marketasdf never defined on all_reasons 30 | decision_reasons.push(all_reasons.geo_default_on_marketasdf); 31 | ^ 32 | 33 | 0 error(s), 1 warning(s), 65.1% typed 34 | 35 | Validating with JSHint 36 | 37 | app.js: line 50, col 14, 'filter_object' is defined but never used. 38 | app.js: line 69, col 14, 'filter_empty' is defined but never used. 39 | app.js: line 80, col 14, 'get_lowest' is defined but never used. 40 | app.js: line 42, col 45, 'response' is defined but never used. 41 | app.js: line 42, col 36, 'request' is defined but never used. 42 | app.js: line 70, col 18, 'key' is defined but never used. 43 | 44 | 6 errors 45 | 46 | ## Running Unit Tests 47 | 48 | Unit tests are a great way to make sure your application runs properly. Given 49 | an adequate understanding of the Openmix API, you can write tests to simulate 50 | most runtime conditions. 51 | 52 | Unit tests are found in the test/tests.js file. 53 | 54 | There are two different ways to execute the unit tests. The simplest is to open 55 | the test/test.html file in your browser (e.g. file:///path/to/test/test.html). 56 | Google Chrome works well for this. You can use the Chrome Developer Tools to 57 | debug any failed tests. 58 | 59 | Another way to run the unit tests is on the command line using Karma Runner and 60 | PhantomJS. This requires that you have Node.js installed. It's a little more 61 | work to set up, but probably worthwhile in the long run if you anticipate writing 62 | a lot of Openmix code. 63 | 64 | You can run the run-tests.sh script provided to execute the tests in Karma 65 | Runner. Here's an example: 66 | 67 | $ ./run-tests.sh 68 | 69 | Running Openmix application unit tests 70 | 71 | INFO [karma]: Karma v0.10.10 server started at http://localhost:9876/ 72 | INFO [launcher]: Starting browser PhantomJS 73 | INFO [PhantomJS 1.9.7 (Mac OS X)]: Connected on socket KHoK6W4HH8YDwj9EHCSk 74 | LOG: Object{requireProvider: requireProvider} 75 | LOG: Object{request: Object{getProbe: getProbe}, getProbe: getProbe, respond: respond, setTTL: setTTL, setReasonCode: setReasonCode} 76 | LOG: Object{request: Object{getProbe: getProbe}, getProbe: getProbe, respond: respond, setTTL: setTTL, setReasonCode: setReasonCode} 77 | ... 78 | PhantomJS 1.9.7 (Mac OS X): Executed 12 of 12 SUCCESS (0.075 secs / 0.017 secs) 79 | 80 | All unit tests passed 81 | 82 | ### Installing Node.js 83 | 84 | Node.js is used by Karma Runner to run and execute your test code. 85 | 86 | #### On Mac OS X 87 | 88 | There are two good options for installing Node on Mac OS X. You can download 89 | an installer from [nodejs.org](http://nodejs.org/download/). Or via 90 | [Homebrew](https://github.com/Homebrew/homebrew/blob/master/README.md). 91 | 92 | With Homebrew, 93 | 94 | $ brew install node 95 | 96 | #### On Linux (Ubuntu) 97 | 98 | Installing Node.js on Ubuntu is simple, but requires an extra package that users 99 | sometimes miss: 100 | 101 | $ sudo apt-get install nodejs nodejs-legacy 102 | 103 | ### Installing JSHint and Karma 104 | 105 | With Node.js installed you can use *npm*, the package manager for Node.js, to 106 | install JSHint, Karma Runner and other dependencies. The project directory 107 | contains a package.json file, which npm uses to download and install software 108 | locally. 109 | 110 | From the directory containing package.json: 111 | 112 | $ npm install 113 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var handler = new OpenmixApplication({ 2 | providers: { 3 | 'cloudflare': 'cdn.jsdelivr.net.cdn.cloudflare.net', 4 | 'maxcdn': 'jsdelivr3.dak.netdna-cdn.com', 5 | 'quantil': 'cdn.jsdelivr.net.mwcloudcdn.com', 6 | 'fastly': 'dualstack.f3.shared.global.fastly.net' 7 | }, 8 | // Use countryMapping to give consideration to additional providers for 9 | // specific countries: 10 | countryMapping: { 11 | 'CN': [ 'quantil'], 12 | 'MO': [ 'quantil'], 13 | 'TH': [ 'maxcdn', 'cloudflare', 'quantil'], 14 | 'BR': [ 'cloudflare'] 15 | }, 16 | defaultProviders: [ 'maxcdn', 'cloudflare', 'fastly' ], 17 | lastResortProvider: 'cloudflare', 18 | defaultTtl: 20, 19 | availabilityThresholds: { 20 | normal: 95 21 | }, 22 | //Set Fusion Sonar threshold for availability for the platform to be included. 23 | // sonar values are between 0 - 5 24 | fusionSonarThreshold: 2 25 | }); 26 | 27 | function init(config) { 28 | 'use strict'; 29 | handler.doInit(config); 30 | } 31 | 32 | function onRequest(request, response) { 33 | 'use strict'; 34 | handler.handleRequest(request, response); 35 | } 36 | 37 | /** 38 | * @constructor 39 | * @param {{ 40 | * providers:!Object. 41 | * }} settings 42 | */ 43 | function OpenmixApplication(settings) { 44 | 'use strict'; 45 | 46 | var aliases = Object.keys(settings.providers); 47 | 48 | /** @param {OpenmixConfiguration} config */ 49 | this.doInit = function(config) { 50 | var i = aliases.length; 51 | while (i--) { 52 | config.requireProvider(aliases[i]); 53 | } 54 | }; 55 | 56 | /** 57 | * @param {OpenmixRequest} request 58 | * @param {OpenmixResponse} response 59 | */ 60 | this.handleRequest = function(request, response) { 61 | 62 | var reasons, 63 | candidates, 64 | candidateAliases, 65 | dataFusionAliases, 66 | dataAvail = request.getProbe('avail'), 67 | dataRtt = request.getProbe('http_rtt'), 68 | /** @type { !Object. } */ 69 | dataFusion = parseFusionData(request.getData('fusion')), 70 | subpopulation = settings.defaultProviders, 71 | availabilityThreshold = settings.availabilityThresholds.normal, 72 | country = request.country, 73 | decisionAlias, 74 | decisionReasons = [], 75 | decisionTtl; 76 | 77 | /** 78 | * @param {{avail:number}} candidate 79 | * @param {string} alias 80 | */ 81 | function filterAvailCandidates(candidate, alias) { 82 | return (-1 < subpopulation.indexOf(alias)) 83 | && (candidate.avail !== undefined) 84 | && (candidate.avail >= availabilityThreshold); 85 | } 86 | 87 | /** 88 | * @param {{avail:number}} candidate 89 | * @param {string} alias 90 | */ 91 | function filterAvailAndFusionSonarCandidates(candidate, alias) { 92 | return (-1 < subpopulation.indexOf(alias)) 93 | && (candidate.avail !== undefined) 94 | && (candidate.avail >= availabilityThreshold) 95 | && (dataFusion[alias] !== undefined) 96 | && (dataFusion[alias].health_score.value > settings.fusionSonarThreshold); 97 | } 98 | 99 | // Application logic here 100 | reasons = { 101 | rtt: 'A', 102 | singleAvailableCandidate: 'D', 103 | noneAvailableOrNoRtt: 'E' 104 | }; 105 | 106 | if (settings.countryMapping) { 107 | if (settings.countryMapping[country] !== undefined) { 108 | subpopulation = settings.countryMapping[country]; 109 | } 110 | } 111 | 112 | if (Object.keys(dataAvail).length > 0 && Object.keys(dataRtt).length > 0) { 113 | if (Object.keys(dataFusion).length > 0) { 114 | dataFusionAliases = Object.keys(dataFusion); 115 | //check if "Big Red Button" isn't activated 116 | if (dataFusion[dataFusionAliases[0]].availability_override === undefined) { 117 | // remove any that don't meet the Fusion Sonar threshold 118 | candidates = filterObject(dataAvail, filterAvailAndFusionSonarCandidates); 119 | } 120 | } 121 | if (candidates === undefined) { 122 | candidates = filterObject(dataAvail, filterAvailCandidates); 123 | } 124 | 125 | //console.log('candidates: ' + JSON.stringify(candidates)); 126 | candidates = joinObjects(candidates, dataRtt, 'http_rtt'); 127 | //console.log('candidates (with rtt): ' + JSON.stringify(candidates)); 128 | candidateAliases = Object.keys(candidates); 129 | 130 | if (1 === candidateAliases.length) { 131 | decisionAlias = candidateAliases[0]; 132 | decisionReasons.push(reasons.singleAvailableCandidate); 133 | decisionTtl = decisionTtl || settings.defaultTtl; 134 | } else if (0 === candidateAliases.length) { 135 | decisionAlias = country === 'CN' ? 'quantil' : settings.lastResortProvider; 136 | decisionReasons.push(reasons.noneAvailableOrNoRtt); 137 | decisionTtl = decisionTtl || settings.defaultTtl; 138 | } else { 139 | decisionAlias = getLowest(candidates, 'http_rtt'); 140 | decisionReasons.push(reasons.rtt); 141 | decisionTtl = decisionTtl || settings.defaultTtl; 142 | } 143 | } 144 | else { 145 | decisionAlias = country === 'CN' ? 'quantil' : settings.lastResortProvider; 146 | decisionReasons.push(reasons.noneAvailableOrNoRtt); 147 | decisionTtl = decisionTtl || settings.defaultTtl; 148 | } 149 | 150 | response.respond(decisionAlias, settings.providers[decisionAlias]); 151 | response.setReasonCode(decisionReasons.join('')); 152 | response.setTTL(decisionTtl); 153 | }; 154 | 155 | /** 156 | * @param {!Object} object 157 | * @param {Function} filter 158 | */ 159 | function filterObject(object, filter) { 160 | var keys = Object.keys(object), 161 | i = keys.length, 162 | key; 163 | 164 | while (i --) { 165 | key = keys[i]; 166 | 167 | if (!filter(object[key], key)) { 168 | delete object[key]; 169 | } 170 | } 171 | 172 | return object; 173 | } 174 | 175 | /** 176 | * @param {!Object} target 177 | * @param {Object} source 178 | * @param {string} property 179 | */ 180 | function joinObjects(target, source, property) { 181 | var keys = Object.keys(target), 182 | i = keys.length, 183 | key; 184 | 185 | while (i --) { 186 | key = keys[i]; 187 | 188 | if (typeof source[key] !== 'undefined' && typeof source[key][property] !== 'undefined') { 189 | target[key][property] = source[key][property]; 190 | } 191 | else { 192 | delete target[key]; 193 | } 194 | } 195 | 196 | return target; 197 | } 198 | 199 | /** 200 | * @param {!Object} source 201 | * @param {string} property 202 | */ 203 | function getLowest(source, property) { 204 | var keys = Object.keys(source), 205 | i = keys.length, 206 | key, 207 | candidate, 208 | min = Infinity, 209 | value; 210 | 211 | while (i --) { 212 | key = keys[i]; 213 | value = source[key][property]; 214 | 215 | if (value < min) { 216 | candidate = key; 217 | min = value; 218 | } 219 | } 220 | 221 | return candidate; 222 | } 223 | 224 | /** 225 | * @param {!Object} data 226 | */ 227 | function parseFusionData(data) { 228 | var keys = Object.keys(data), 229 | i = keys.length, 230 | key; 231 | while (i --) { 232 | key = keys[i]; 233 | try { 234 | data[key] = JSON.parse(data[key]); 235 | } 236 | catch (e) { 237 | delete data[key]; 238 | } 239 | } 240 | return data; 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /jshintConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "laxbreak": true, 5 | "camelcase": true, 6 | "exported": [ "init", "onRequest" ] 7 | } 8 | -------------------------------------------------------------------------------- /karma.app.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 4 | // base path, that will be used to resolve files and exclude 5 | basePath: '', 6 | 7 | // frameworks to use 8 | frameworks: [ 'qunit' ], 9 | 10 | plugins: [ 11 | 'karma-qunit', 12 | 'karma-phantomjs-launcher', 13 | 'karma-junit-reporter' 14 | ], 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test/resources/sinon-1.10.3.js', 19 | 'test/resources/sinon-qunit-1.0.0.js', 20 | 'app.js', 21 | 'test/tests.js' 22 | ], 23 | 24 | // list of files to exclude 25 | exclude: [ 26 | 27 | ], 28 | 29 | // test results reporter to use 30 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 31 | reporters: [ 32 | 'progress', 33 | 'junit' 34 | ], 35 | 36 | junitReporter: { 37 | outputFile: 'karma.app.results.xml' 38 | }, 39 | 40 | // web server port 41 | port: 9876, 42 | 43 | // enable / disable colors in the output (reporters and logs) 44 | colors: true, 45 | 46 | // level of logging 47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | // Useful for troubleshooting issues with Karma configuration 50 | //logLevel: config.LOG_DEBUG, 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: false, 54 | 55 | // Start these browsers, currently available: 56 | // - Chrome 57 | // - ChromeCanary 58 | // - Firefox 59 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 60 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 61 | // - PhantomJS 62 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 63 | browsers: [ 'PhantomJS' ], 64 | 65 | // If browser does not capture in given timeout [ms], kill it 66 | captureTimeout: 6000, 67 | 68 | // Continuous Integration mode 69 | // if true, it capture browsers, run tests and exit 70 | singleRun: true 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "karma": "0.10.x", 4 | "karma-qunit": "0.1.x", 5 | "karma-phantomjs-launcher": "0.1.x", 6 | "karma-junit-reporter": "0.2.x", 7 | "jshint": "2.5.x" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors 4 | red='\033[0;31m' 5 | green='\033[0;32m' 6 | yellow='\033[1;33m' 7 | nocolor='\033[0m' 8 | 9 | run_test_suite() { 10 | echo 11 | echo -e "${yellow}Running $1 unit tests${nocolor}" 12 | echo 13 | ./node_modules/karma/bin/karma start $2 14 | LAST=$? 15 | if [ "$LAST" -ne "0" ] 16 | then 17 | echo 18 | echo -e "${red}$1 unit tests failed${nocolor}" 19 | echo 20 | exit 1 21 | fi 22 | } 23 | 24 | run_test_suite "Openmix application" "karma.app.conf.js" 25 | 26 | echo 27 | echo -e "${green}All unit tests passed${nocolor}" 28 | echo 29 | -------------------------------------------------------------------------------- /test/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsdelivr/dns-openmix/8dd6383b1ac5a69703382f17e35107245e0ccfb6/test/compiler.jar -------------------------------------------------------------------------------- /test/deps.js: -------------------------------------------------------------------------------- 1 | 2 | /** @constructor */ 3 | function OpenmixConfiguration() {} 4 | 5 | OpenmixConfiguration.prototype.requireProvider = function(alias) {}; 6 | 7 | /** @constructor */ 8 | function OpenmixRequest() { 9 | /** @type {string} */ 10 | this.market = 'some market'; 11 | /** @type {string} */ 12 | this.country = 'some country'; 13 | } 14 | 15 | OpenmixRequest.prototype.getProbe = function(probe_type) {}; 16 | OpenmixRequest.prototype.getData = function(feed_name) {}; 17 | 18 | /** @constructor */ 19 | function OpenmixResponse() {} 20 | 21 | OpenmixResponse.prototype.respond = function(alias, cname) {}; 22 | OpenmixResponse.prototype.setTTL = function(value) {}; 23 | OpenmixResponse.prototype.setReasonCode = function(value) {}; 24 | -------------------------------------------------------------------------------- /test/jshintConfigTests.json: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "camelcase": true, 5 | "predef": [ 6 | "module", 7 | "OpenmixApplication", 8 | "test", 9 | "console", 10 | "equal", 11 | "deepEqual", 12 | "strictEqual", 13 | "ok" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/openmix-externs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @typedef {{requireProvider:function(string)}} 4 | */ 5 | var OpenmixConfiguration; 6 | 7 | /** 8 | * @typedef {{ 9 | * market:string, 10 | * country:string, 11 | * asn:number, 12 | * hostname_prefix:string, 13 | * getProbe:function(string):!Object.>, 14 | * getData:function(string):!Object. 15 | * }} 16 | */ 17 | var OpenmixRequest; 18 | 19 | /** 20 | * @typedef {{ 21 | * addCName:function(string), 22 | * respond:function(string,string), 23 | * setTTL:function(number), 24 | * setReasonCode:function(string) 25 | * }} 26 | */ 27 | var OpenmixResponse; 28 | -------------------------------------------------------------------------------- /test/resources/qunit-1.15.0.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.15.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-08-08T16:00Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 1em 0.5em 1em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 1em 0.5em 1em; 73 | background-color: #2B81AF; 74 | color: #FFF; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | #qunit-modulefilter-container { 79 | float: right; 80 | } 81 | 82 | /** Tests: Pass/Fail */ 83 | 84 | #qunit-tests { 85 | list-style-position: inside; 86 | } 87 | 88 | #qunit-tests li { 89 | padding: 0.4em 1em 0.4em 1em; 90 | border-bottom: 1px solid #FFF; 91 | list-style-position: inside; 92 | } 93 | 94 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 95 | display: none; 96 | } 97 | 98 | #qunit-tests li strong { 99 | cursor: pointer; 100 | } 101 | 102 | #qunit-tests li a { 103 | padding: 0.5em; 104 | color: #C2CCD1; 105 | text-decoration: none; 106 | } 107 | #qunit-tests li a:hover, 108 | #qunit-tests li a:focus { 109 | color: #000; 110 | } 111 | 112 | #qunit-tests li .runtime { 113 | float: right; 114 | font-size: smaller; 115 | } 116 | 117 | .qunit-assert-list { 118 | margin-top: 0.5em; 119 | padding: 0.5em; 120 | 121 | background-color: #FFF; 122 | 123 | border-radius: 5px; 124 | } 125 | 126 | .qunit-collapsed { 127 | display: none; 128 | } 129 | 130 | #qunit-tests table { 131 | border-collapse: collapse; 132 | margin-top: 0.2em; 133 | } 134 | 135 | #qunit-tests th { 136 | text-align: right; 137 | vertical-align: top; 138 | padding: 0 0.5em 0 0; 139 | } 140 | 141 | #qunit-tests td { 142 | vertical-align: top; 143 | } 144 | 145 | #qunit-tests pre { 146 | margin: 0; 147 | white-space: pre-wrap; 148 | word-wrap: break-word; 149 | } 150 | 151 | #qunit-tests del { 152 | background-color: #E0F2BE; 153 | color: #374E0C; 154 | text-decoration: none; 155 | } 156 | 157 | #qunit-tests ins { 158 | background-color: #FFCACA; 159 | color: #500; 160 | text-decoration: none; 161 | } 162 | 163 | /*** Test Counts */ 164 | 165 | #qunit-tests b.counts { color: #000; } 166 | #qunit-tests b.passed { color: #5E740B; } 167 | #qunit-tests b.failed { color: #710909; } 168 | 169 | #qunit-tests li li { 170 | padding: 5px; 171 | background-color: #FFF; 172 | border-bottom: none; 173 | list-style-position: inside; 174 | } 175 | 176 | /*** Passing Styles */ 177 | 178 | #qunit-tests li li.pass { 179 | color: #3C510C; 180 | background-color: #FFF; 181 | border-left: 10px solid #C6E746; 182 | } 183 | 184 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 185 | #qunit-tests .pass .test-name { color: #366097; } 186 | 187 | #qunit-tests .pass .test-actual, 188 | #qunit-tests .pass .test-expected { color: #999; } 189 | 190 | #qunit-banner.qunit-pass { background-color: #C6E746; } 191 | 192 | /*** Failing Styles */ 193 | 194 | #qunit-tests li li.fail { 195 | color: #710909; 196 | background-color: #FFF; 197 | border-left: 10px solid #EE5757; 198 | white-space: pre; 199 | } 200 | 201 | #qunit-tests > li:last-child { 202 | border-radius: 0 0 5px 5px; 203 | } 204 | 205 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 206 | #qunit-tests .fail .test-name, 207 | #qunit-tests .fail .module-name { color: #000; } 208 | 209 | #qunit-tests .fail .test-actual { color: #EE5757; } 210 | #qunit-tests .fail .test-expected { color: #008000; } 211 | 212 | #qunit-banner.qunit-fail { background-color: #EE5757; } 213 | 214 | 215 | /** Result */ 216 | 217 | #qunit-testresult { 218 | padding: 0.5em 1em 0.5em 1em; 219 | 220 | color: #2B81AF; 221 | background-color: #D2E0E6; 222 | 223 | border-bottom: 1px solid #FFF; 224 | } 225 | #qunit-testresult .module-name { 226 | font-weight: 700; 227 | } 228 | 229 | /** Fixture */ 230 | 231 | #qunit-fixture { 232 | position: absolute; 233 | top: -10000px; 234 | left: -10000px; 235 | width: 1000px; 236 | height: 1000px; 237 | } 238 | -------------------------------------------------------------------------------- /test/resources/qunit-1.15.0.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.15.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-08-08T16:00Z 10 | */ 11 | 12 | (function( window ) { 13 | 14 | var QUnit, 15 | config, 16 | onErrorFnPrev, 17 | fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ), 18 | toString = Object.prototype.toString, 19 | hasOwn = Object.prototype.hasOwnProperty, 20 | // Keep a local reference to Date (GH-283) 21 | Date = window.Date, 22 | now = Date.now || function() { 23 | return new Date().getTime(); 24 | }, 25 | setTimeout = window.setTimeout, 26 | clearTimeout = window.clearTimeout, 27 | defined = { 28 | document: typeof window.document !== "undefined", 29 | setTimeout: typeof window.setTimeout !== "undefined", 30 | sessionStorage: (function() { 31 | var x = "qunit-test-string"; 32 | try { 33 | sessionStorage.setItem( x, x ); 34 | sessionStorage.removeItem( x ); 35 | return true; 36 | } catch ( e ) { 37 | return false; 38 | } 39 | }()) 40 | }, 41 | /** 42 | * Provides a normalized error string, correcting an issue 43 | * with IE 7 (and prior) where Error.prototype.toString is 44 | * not properly implemented 45 | * 46 | * Based on http://es5.github.com/#x15.11.4.4 47 | * 48 | * @param {String|Error} error 49 | * @return {String} error message 50 | */ 51 | errorString = function( error ) { 52 | var name, message, 53 | errorString = error.toString(); 54 | if ( errorString.substring( 0, 7 ) === "[object" ) { 55 | name = error.name ? error.name.toString() : "Error"; 56 | message = error.message ? error.message.toString() : ""; 57 | if ( name && message ) { 58 | return name + ": " + message; 59 | } else if ( name ) { 60 | return name; 61 | } else if ( message ) { 62 | return message; 63 | } else { 64 | return "Error"; 65 | } 66 | } else { 67 | return errorString; 68 | } 69 | }, 70 | /** 71 | * Makes a clone of an object using only Array or Object as base, 72 | * and copies over the own enumerable properties. 73 | * 74 | * @param {Object} obj 75 | * @return {Object} New object with only the own properties (recursively). 76 | */ 77 | objectValues = function( obj ) { 78 | var key, val, 79 | vals = QUnit.is( "array", obj ) ? [] : {}; 80 | for ( key in obj ) { 81 | if ( hasOwn.call( obj, key ) ) { 82 | val = obj[ key ]; 83 | vals[ key ] = val === Object( val ) ? objectValues( val ) : val; 84 | } 85 | } 86 | return vals; 87 | }; 88 | 89 | // Root QUnit object. 90 | // `QUnit` initialized at top of scope 91 | QUnit = { 92 | 93 | // call on start of module test to prepend name to all tests 94 | module: function( name, testEnvironment ) { 95 | config.currentModule = name; 96 | config.currentModuleTestEnvironment = testEnvironment; 97 | config.modules[ name ] = true; 98 | }, 99 | 100 | asyncTest: function( testName, expected, callback ) { 101 | if ( arguments.length === 2 ) { 102 | callback = expected; 103 | expected = null; 104 | } 105 | 106 | QUnit.test( testName, expected, callback, true ); 107 | }, 108 | 109 | test: function( testName, expected, callback, async ) { 110 | var test; 111 | 112 | if ( arguments.length === 2 ) { 113 | callback = expected; 114 | expected = null; 115 | } 116 | 117 | test = new Test({ 118 | testName: testName, 119 | expected: expected, 120 | async: async, 121 | callback: callback, 122 | module: config.currentModule, 123 | moduleTestEnvironment: config.currentModuleTestEnvironment, 124 | stack: sourceFromStacktrace( 2 ) 125 | }); 126 | 127 | if ( !validTest( test ) ) { 128 | return; 129 | } 130 | 131 | test.queue(); 132 | }, 133 | 134 | start: function( count ) { 135 | var message; 136 | 137 | // QUnit hasn't been initialized yet. 138 | // Note: RequireJS (et al) may delay onLoad 139 | if ( config.semaphore === undefined ) { 140 | QUnit.begin(function() { 141 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 142 | setTimeout(function() { 143 | QUnit.start( count ); 144 | }); 145 | }); 146 | return; 147 | } 148 | 149 | config.semaphore -= count || 1; 150 | // don't start until equal number of stop-calls 151 | if ( config.semaphore > 0 ) { 152 | return; 153 | } 154 | 155 | // Set the starting time when the first test is run 156 | QUnit.config.started = QUnit.config.started || now(); 157 | // ignore if start is called more often then stop 158 | if ( config.semaphore < 0 ) { 159 | config.semaphore = 0; 160 | 161 | message = "Called start() while already started (QUnit.config.semaphore was 0 already)"; 162 | 163 | if ( config.current ) { 164 | QUnit.pushFailure( message, sourceFromStacktrace( 2 ) ); 165 | } else { 166 | throw new Error( message ); 167 | } 168 | 169 | return; 170 | } 171 | // A slight delay, to avoid any current callbacks 172 | if ( defined.setTimeout ) { 173 | setTimeout(function() { 174 | if ( config.semaphore > 0 ) { 175 | return; 176 | } 177 | if ( config.timeout ) { 178 | clearTimeout( config.timeout ); 179 | } 180 | 181 | config.blocking = false; 182 | process( true ); 183 | }, 13 ); 184 | } else { 185 | config.blocking = false; 186 | process( true ); 187 | } 188 | }, 189 | 190 | stop: function( count ) { 191 | config.semaphore += count || 1; 192 | config.blocking = true; 193 | 194 | if ( config.testTimeout && defined.setTimeout ) { 195 | clearTimeout( config.timeout ); 196 | config.timeout = setTimeout(function() { 197 | QUnit.ok( false, "Test timed out" ); 198 | config.semaphore = 1; 199 | QUnit.start(); 200 | }, config.testTimeout ); 201 | } 202 | } 203 | }; 204 | 205 | // We use the prototype to distinguish between properties that should 206 | // be exposed as globals (and in exports) and those that shouldn't 207 | (function() { 208 | function F() {} 209 | F.prototype = QUnit; 210 | QUnit = new F(); 211 | 212 | // Make F QUnit's constructor so that we can add to the prototype later 213 | QUnit.constructor = F; 214 | }()); 215 | 216 | /** 217 | * Config object: Maintain internal state 218 | * Later exposed as QUnit.config 219 | * `config` initialized at top of scope 220 | */ 221 | config = { 222 | // The queue of tests to run 223 | queue: [], 224 | 225 | // block until document ready 226 | blocking: true, 227 | 228 | // when enabled, show only failing tests 229 | // gets persisted through sessionStorage and can be changed in UI via checkbox 230 | hidepassed: false, 231 | 232 | // by default, run previously failed tests first 233 | // very useful in combination with "Hide passed tests" checked 234 | reorder: true, 235 | 236 | // by default, modify document.title when suite is done 237 | altertitle: true, 238 | 239 | // by default, scroll to top of the page when suite is done 240 | scrolltop: true, 241 | 242 | // when enabled, all tests must call expect() 243 | requireExpects: false, 244 | 245 | // add checkboxes that are persisted in the query-string 246 | // when enabled, the id is set to `true` as a `QUnit.config` property 247 | urlConfig: [ 248 | { 249 | id: "noglobals", 250 | label: "Check for Globals", 251 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 252 | }, 253 | { 254 | id: "notrycatch", 255 | label: "No try-catch", 256 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 257 | } 258 | ], 259 | 260 | // Set of all modules. 261 | modules: {}, 262 | 263 | callbacks: {} 264 | }; 265 | 266 | // Initialize more QUnit.config and QUnit.urlParams 267 | (function() { 268 | var i, current, 269 | location = window.location || { search: "", protocol: "file:" }, 270 | params = location.search.slice( 1 ).split( "&" ), 271 | length = params.length, 272 | urlParams = {}; 273 | 274 | if ( params[ 0 ] ) { 275 | for ( i = 0; i < length; i++ ) { 276 | current = params[ i ].split( "=" ); 277 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 278 | 279 | // allow just a key to turn on a flag, e.g., test.html?noglobals 280 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 281 | if ( urlParams[ current[ 0 ] ] ) { 282 | urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); 283 | } else { 284 | urlParams[ current[ 0 ] ] = current[ 1 ]; 285 | } 286 | } 287 | } 288 | 289 | QUnit.urlParams = urlParams; 290 | 291 | // String search anywhere in moduleName+testName 292 | config.filter = urlParams.filter; 293 | 294 | // Exact match of the module name 295 | config.module = urlParams.module; 296 | 297 | config.testNumber = []; 298 | if ( urlParams.testNumber ) { 299 | 300 | // Ensure that urlParams.testNumber is an array 301 | urlParams.testNumber = [].concat( urlParams.testNumber ); 302 | for ( i = 0; i < urlParams.testNumber.length; i++ ) { 303 | current = urlParams.testNumber[ i ]; 304 | config.testNumber.push( parseInt( current, 10 ) ); 305 | } 306 | } 307 | 308 | // Figure out if we're running the tests from a server or not 309 | QUnit.isLocal = location.protocol === "file:"; 310 | }()); 311 | 312 | extend( QUnit, { 313 | 314 | config: config, 315 | 316 | // Safe object type checking 317 | is: function( type, obj ) { 318 | return QUnit.objectType( obj ) === type; 319 | }, 320 | 321 | objectType: function( obj ) { 322 | if ( typeof obj === "undefined" ) { 323 | return "undefined"; 324 | } 325 | 326 | // Consider: typeof null === object 327 | if ( obj === null ) { 328 | return "null"; 329 | } 330 | 331 | var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), 332 | type = match && match[ 1 ] || ""; 333 | 334 | switch ( type ) { 335 | case "Number": 336 | if ( isNaN( obj ) ) { 337 | return "nan"; 338 | } 339 | return "number"; 340 | case "String": 341 | case "Boolean": 342 | case "Array": 343 | case "Date": 344 | case "RegExp": 345 | case "Function": 346 | return type.toLowerCase(); 347 | } 348 | if ( typeof obj === "object" ) { 349 | return "object"; 350 | } 351 | return undefined; 352 | }, 353 | 354 | url: function( params ) { 355 | params = extend( extend( {}, QUnit.urlParams ), params ); 356 | var key, 357 | querystring = "?"; 358 | 359 | for ( key in params ) { 360 | if ( hasOwn.call( params, key ) ) { 361 | querystring += encodeURIComponent( key ) + "=" + 362 | encodeURIComponent( params[ key ] ) + "&"; 363 | } 364 | } 365 | return window.location.protocol + "//" + window.location.host + 366 | window.location.pathname + querystring.slice( 0, -1 ); 367 | }, 368 | 369 | extend: extend 370 | }); 371 | 372 | /** 373 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 374 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 375 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 376 | * Doing this allows us to tell if the following methods have been overwritten on the actual 377 | * QUnit object. 378 | */ 379 | extend( QUnit.constructor.prototype, { 380 | 381 | // Logging callbacks; all receive a single argument with the listed properties 382 | // run test/logs.html for any related changes 383 | begin: registerLoggingCallback( "begin" ), 384 | 385 | // done: { failed, passed, total, runtime } 386 | done: registerLoggingCallback( "done" ), 387 | 388 | // log: { result, actual, expected, message } 389 | log: registerLoggingCallback( "log" ), 390 | 391 | // testStart: { name } 392 | testStart: registerLoggingCallback( "testStart" ), 393 | 394 | // testDone: { name, failed, passed, total, runtime } 395 | testDone: registerLoggingCallback( "testDone" ), 396 | 397 | // moduleStart: { name } 398 | moduleStart: registerLoggingCallback( "moduleStart" ), 399 | 400 | // moduleDone: { name, failed, passed, total } 401 | moduleDone: registerLoggingCallback( "moduleDone" ) 402 | }); 403 | 404 | QUnit.load = function() { 405 | runLoggingCallbacks( "begin", { 406 | totalTests: Test.count 407 | }); 408 | 409 | // Initialize the configuration options 410 | extend( config, { 411 | stats: { all: 0, bad: 0 }, 412 | moduleStats: { all: 0, bad: 0 }, 413 | started: 0, 414 | updateRate: 1000, 415 | autostart: true, 416 | filter: "", 417 | semaphore: 1 418 | }, true ); 419 | 420 | config.blocking = false; 421 | 422 | if ( config.autostart ) { 423 | QUnit.start(); 424 | } 425 | }; 426 | 427 | // `onErrorFnPrev` initialized at top of scope 428 | // Preserve other handlers 429 | onErrorFnPrev = window.onerror; 430 | 431 | // Cover uncaught exceptions 432 | // Returning true will suppress the default browser handler, 433 | // returning false will let it run. 434 | window.onerror = function( error, filePath, linerNr ) { 435 | var ret = false; 436 | if ( onErrorFnPrev ) { 437 | ret = onErrorFnPrev( error, filePath, linerNr ); 438 | } 439 | 440 | // Treat return value as window.onerror itself does, 441 | // Only do our handling if not suppressed. 442 | if ( ret !== true ) { 443 | if ( QUnit.config.current ) { 444 | if ( QUnit.config.current.ignoreGlobalErrors ) { 445 | return true; 446 | } 447 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 448 | } else { 449 | QUnit.test( "global failure", extend(function() { 450 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 451 | }, { validTest: validTest } ) ); 452 | } 453 | return false; 454 | } 455 | 456 | return ret; 457 | }; 458 | 459 | function done() { 460 | config.autorun = true; 461 | 462 | // Log the last module results 463 | if ( config.previousModule ) { 464 | runLoggingCallbacks( "moduleDone", { 465 | name: config.previousModule, 466 | failed: config.moduleStats.bad, 467 | passed: config.moduleStats.all - config.moduleStats.bad, 468 | total: config.moduleStats.all 469 | }); 470 | } 471 | delete config.previousModule; 472 | 473 | var runtime = now() - config.started, 474 | passed = config.stats.all - config.stats.bad; 475 | 476 | runLoggingCallbacks( "done", { 477 | failed: config.stats.bad, 478 | passed: passed, 479 | total: config.stats.all, 480 | runtime: runtime 481 | }); 482 | } 483 | 484 | /** @return Boolean: true if this test should be ran */ 485 | function validTest( test ) { 486 | var include, 487 | filter = config.filter && config.filter.toLowerCase(), 488 | module = config.module && config.module.toLowerCase(), 489 | fullName = ( test.module + ": " + test.testName ).toLowerCase(); 490 | 491 | // Internally-generated tests are always valid 492 | if ( test.callback && test.callback.validTest === validTest ) { 493 | delete test.callback.validTest; 494 | return true; 495 | } 496 | 497 | if ( config.testNumber.length > 0 ) { 498 | if ( inArray( test.testNumber, config.testNumber ) < 0 ) { 499 | return false; 500 | } 501 | } 502 | 503 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 504 | return false; 505 | } 506 | 507 | if ( !filter ) { 508 | return true; 509 | } 510 | 511 | include = filter.charAt( 0 ) !== "!"; 512 | if ( !include ) { 513 | filter = filter.slice( 1 ); 514 | } 515 | 516 | // If the filter matches, we need to honour include 517 | if ( fullName.indexOf( filter ) !== -1 ) { 518 | return include; 519 | } 520 | 521 | // Otherwise, do the opposite 522 | return !include; 523 | } 524 | 525 | // Doesn't support IE6 to IE9 526 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 527 | function extractStacktrace( e, offset ) { 528 | offset = offset === undefined ? 4 : offset; 529 | 530 | var stack, include, i; 531 | 532 | if ( e.stacktrace ) { 533 | 534 | // Opera 12.x 535 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 536 | } else if ( e.stack ) { 537 | 538 | // Firefox, Chrome, Safari 6+, IE10+, PhantomJS and Node 539 | stack = e.stack.split( "\n" ); 540 | if ( /^error$/i.test( stack[ 0 ] ) ) { 541 | stack.shift(); 542 | } 543 | if ( fileName ) { 544 | include = []; 545 | for ( i = offset; i < stack.length; i++ ) { 546 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 547 | break; 548 | } 549 | include.push( stack[ i ] ); 550 | } 551 | if ( include.length ) { 552 | return include.join( "\n" ); 553 | } 554 | } 555 | return stack[ offset ]; 556 | } else if ( e.sourceURL ) { 557 | 558 | // Safari < 6 559 | // exclude useless self-reference for generated Error objects 560 | if ( /qunit.js$/.test( e.sourceURL ) ) { 561 | return; 562 | } 563 | 564 | // for actual exceptions, this is useful 565 | return e.sourceURL + ":" + e.line; 566 | } 567 | } 568 | function sourceFromStacktrace( offset ) { 569 | try { 570 | throw new Error(); 571 | } catch ( e ) { 572 | return extractStacktrace( e, offset ); 573 | } 574 | } 575 | 576 | function synchronize( callback, last ) { 577 | config.queue.push( callback ); 578 | 579 | if ( config.autorun && !config.blocking ) { 580 | process( last ); 581 | } 582 | } 583 | 584 | function process( last ) { 585 | function next() { 586 | process( last ); 587 | } 588 | var start = now(); 589 | config.depth = config.depth ? config.depth + 1 : 1; 590 | 591 | while ( config.queue.length && !config.blocking ) { 592 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( now() - start ) < config.updateRate ) ) { 593 | config.queue.shift()(); 594 | } else { 595 | setTimeout( next, 13 ); 596 | break; 597 | } 598 | } 599 | config.depth--; 600 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 601 | done(); 602 | } 603 | } 604 | 605 | function saveGlobal() { 606 | config.pollution = []; 607 | 608 | if ( config.noglobals ) { 609 | for ( var key in window ) { 610 | if ( hasOwn.call( window, key ) ) { 611 | // in Opera sometimes DOM element ids show up here, ignore them 612 | if ( /^qunit-test-output/.test( key ) ) { 613 | continue; 614 | } 615 | config.pollution.push( key ); 616 | } 617 | } 618 | } 619 | } 620 | 621 | function checkPollution() { 622 | var newGlobals, 623 | deletedGlobals, 624 | old = config.pollution; 625 | 626 | saveGlobal(); 627 | 628 | newGlobals = diff( config.pollution, old ); 629 | if ( newGlobals.length > 0 ) { 630 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); 631 | } 632 | 633 | deletedGlobals = diff( old, config.pollution ); 634 | if ( deletedGlobals.length > 0 ) { 635 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); 636 | } 637 | } 638 | 639 | // returns a new Array with the elements that are in a but not in b 640 | function diff( a, b ) { 641 | var i, j, 642 | result = a.slice(); 643 | 644 | for ( i = 0; i < result.length; i++ ) { 645 | for ( j = 0; j < b.length; j++ ) { 646 | if ( result[ i ] === b[ j ] ) { 647 | result.splice( i, 1 ); 648 | i--; 649 | break; 650 | } 651 | } 652 | } 653 | return result; 654 | } 655 | 656 | function extend( a, b, undefOnly ) { 657 | for ( var prop in b ) { 658 | if ( hasOwn.call( b, prop ) ) { 659 | 660 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 661 | if ( !( prop === "constructor" && a === window ) ) { 662 | if ( b[ prop ] === undefined ) { 663 | delete a[ prop ]; 664 | } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { 665 | a[ prop ] = b[ prop ]; 666 | } 667 | } 668 | } 669 | } 670 | 671 | return a; 672 | } 673 | 674 | function registerLoggingCallback( key ) { 675 | 676 | // Initialize key collection of logging callback 677 | if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { 678 | config.callbacks[ key ] = []; 679 | } 680 | 681 | return function( callback ) { 682 | config.callbacks[ key ].push( callback ); 683 | }; 684 | } 685 | 686 | function runLoggingCallbacks( key, args ) { 687 | var i, l, callbacks; 688 | 689 | callbacks = config.callbacks[ key ]; 690 | for ( i = 0, l = callbacks.length; i < l; i++ ) { 691 | callbacks[ i ]( args ); 692 | } 693 | } 694 | 695 | // from jquery.js 696 | function inArray( elem, array ) { 697 | if ( array.indexOf ) { 698 | return array.indexOf( elem ); 699 | } 700 | 701 | for ( var i = 0, length = array.length; i < length; i++ ) { 702 | if ( array[ i ] === elem ) { 703 | return i; 704 | } 705 | } 706 | 707 | return -1; 708 | } 709 | 710 | function Test( settings ) { 711 | extend( this, settings ); 712 | this.assert = new Assert( this ); 713 | this.assertions = []; 714 | this.testNumber = ++Test.count; 715 | } 716 | 717 | Test.count = 0; 718 | 719 | Test.prototype = { 720 | setup: function() { 721 | if ( 722 | 723 | // Emit moduleStart when we're switching from one module to another 724 | this.module !== config.previousModule || 725 | 726 | // They could be equal (both undefined) but if the previousModule property doesn't 727 | // yet exist it means this is the first test in a suite that isn't wrapped in a 728 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 729 | // Without this, reporters can get testStart before moduleStart which is a problem. 730 | !hasOwn.call( config, "previousModule" ) 731 | ) { 732 | if ( hasOwn.call( config, "previousModule" ) ) { 733 | runLoggingCallbacks( "moduleDone", { 734 | name: config.previousModule, 735 | failed: config.moduleStats.bad, 736 | passed: config.moduleStats.all - config.moduleStats.bad, 737 | total: config.moduleStats.all 738 | }); 739 | } 740 | config.previousModule = this.module; 741 | config.moduleStats = { all: 0, bad: 0 }; 742 | runLoggingCallbacks( "moduleStart", { 743 | name: this.module 744 | }); 745 | } 746 | 747 | config.current = this; 748 | 749 | this.testEnvironment = extend({ 750 | setup: function() {}, 751 | teardown: function() {} 752 | }, this.moduleTestEnvironment ); 753 | 754 | this.started = now(); 755 | runLoggingCallbacks( "testStart", { 756 | name: this.testName, 757 | module: this.module, 758 | testNumber: this.testNumber 759 | }); 760 | 761 | if ( !config.pollution ) { 762 | saveGlobal(); 763 | } 764 | if ( config.notrycatch ) { 765 | this.testEnvironment.setup.call( this.testEnvironment, this.assert ); 766 | return; 767 | } 768 | try { 769 | this.testEnvironment.setup.call( this.testEnvironment, this.assert ); 770 | } catch ( e ) { 771 | this.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 772 | } 773 | }, 774 | run: function() { 775 | config.current = this; 776 | 777 | if ( this.async ) { 778 | QUnit.stop(); 779 | } 780 | 781 | this.callbackStarted = now(); 782 | 783 | if ( config.notrycatch ) { 784 | this.callback.call( this.testEnvironment, this.assert ); 785 | this.callbackRuntime = now() - this.callbackStarted; 786 | return; 787 | } 788 | 789 | try { 790 | this.callback.call( this.testEnvironment, this.assert ); 791 | this.callbackRuntime = now() - this.callbackStarted; 792 | } catch ( e ) { 793 | this.callbackRuntime = now() - this.callbackStarted; 794 | 795 | this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 796 | 797 | // else next test will carry the responsibility 798 | saveGlobal(); 799 | 800 | // Restart the tests if they're blocking 801 | if ( config.blocking ) { 802 | QUnit.start(); 803 | } 804 | } 805 | }, 806 | teardown: function() { 807 | config.current = this; 808 | if ( config.notrycatch ) { 809 | if ( typeof this.callbackRuntime === "undefined" ) { 810 | this.callbackRuntime = now() - this.callbackStarted; 811 | } 812 | this.testEnvironment.teardown.call( this.testEnvironment, this.assert ); 813 | return; 814 | } else { 815 | try { 816 | this.testEnvironment.teardown.call( this.testEnvironment, this.assert ); 817 | } catch ( e ) { 818 | this.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 819 | } 820 | } 821 | checkPollution(); 822 | }, 823 | finish: function() { 824 | config.current = this; 825 | if ( config.requireExpects && this.expected === null ) { 826 | this.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 827 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 828 | this.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 829 | } else if ( this.expected === null && !this.assertions.length ) { 830 | this.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 831 | } 832 | 833 | var i, 834 | bad = 0; 835 | 836 | this.runtime = now() - this.started; 837 | config.stats.all += this.assertions.length; 838 | config.moduleStats.all += this.assertions.length; 839 | 840 | for ( i = 0; i < this.assertions.length; i++ ) { 841 | if ( !this.assertions[ i ].result ) { 842 | bad++; 843 | config.stats.bad++; 844 | config.moduleStats.bad++; 845 | } 846 | } 847 | 848 | runLoggingCallbacks( "testDone", { 849 | name: this.testName, 850 | module: this.module, 851 | failed: bad, 852 | passed: this.assertions.length - bad, 853 | total: this.assertions.length, 854 | runtime: this.runtime, 855 | 856 | // HTML Reporter use 857 | assertions: this.assertions, 858 | testNumber: this.testNumber, 859 | 860 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead 861 | duration: this.runtime 862 | }); 863 | 864 | config.current = undefined; 865 | }, 866 | 867 | queue: function() { 868 | var bad, 869 | test = this; 870 | 871 | function run() { 872 | // each of these can by async 873 | synchronize(function() { 874 | test.setup(); 875 | }); 876 | synchronize(function() { 877 | test.run(); 878 | }); 879 | synchronize(function() { 880 | test.teardown(); 881 | }); 882 | synchronize(function() { 883 | test.finish(); 884 | }); 885 | } 886 | 887 | // `bad` initialized at top of scope 888 | // defer when previous test run passed, if storage is available 889 | bad = QUnit.config.reorder && defined.sessionStorage && 890 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 891 | 892 | if ( bad ) { 893 | run(); 894 | } else { 895 | synchronize( run, true ); 896 | } 897 | }, 898 | 899 | push: function( result, actual, expected, message ) { 900 | var source, 901 | details = { 902 | module: this.module, 903 | name: this.testName, 904 | result: result, 905 | message: message, 906 | actual: actual, 907 | expected: expected, 908 | testNumber: this.testNumber 909 | }; 910 | 911 | if ( !result ) { 912 | source = sourceFromStacktrace(); 913 | 914 | if ( source ) { 915 | details.source = source; 916 | } 917 | } 918 | 919 | runLoggingCallbacks( "log", details ); 920 | 921 | this.assertions.push({ 922 | result: !!result, 923 | message: message 924 | }); 925 | }, 926 | 927 | pushFailure: function( message, source, actual ) { 928 | if ( !this instanceof Test ) { 929 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace( 2 ) ); 930 | } 931 | 932 | var details = { 933 | module: this.module, 934 | name: this.testName, 935 | result: false, 936 | message: message || "error", 937 | actual: actual || null, 938 | testNumber: this.testNumber 939 | }; 940 | 941 | if ( source ) { 942 | details.source = source; 943 | } 944 | 945 | runLoggingCallbacks( "log", details ); 946 | 947 | this.assertions.push({ 948 | result: false, 949 | message: message 950 | }); 951 | } 952 | }; 953 | 954 | QUnit.pushFailure = function() { 955 | if ( !QUnit.config.current ) { 956 | throw new Error( "pushFailure() assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 957 | } 958 | 959 | // Gets current test obj 960 | var currentTest = QUnit.config.current.assert.test; 961 | 962 | return currentTest.pushFailure.apply( currentTest, arguments ); 963 | }; 964 | 965 | function Assert( testContext ) { 966 | this.test = testContext; 967 | } 968 | 969 | // Assert helpers 970 | QUnit.assert = Assert.prototype = { 971 | 972 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 973 | expect: function( asserts ) { 974 | if ( arguments.length === 1 ) { 975 | this.test.expected = asserts; 976 | } else { 977 | return this.test.expected; 978 | } 979 | }, 980 | 981 | // Exports test.push() to the user API 982 | push: function() { 983 | var assert = this; 984 | 985 | // Backwards compatibility fix. 986 | // Allows the direct use of global exported assertions and QUnit.assert.* 987 | // Although, it's use is not recommended as it can leak assertions 988 | // to other tests from async tests, because we only get a reference to the current test, 989 | // not exactly the test where assertion were intended to be called. 990 | if ( !QUnit.config.current ) { 991 | throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 992 | } 993 | if ( !( assert instanceof Assert ) ) { 994 | assert = QUnit.config.current.assert; 995 | } 996 | return assert.test.push.apply( assert.test, arguments ); 997 | }, 998 | 999 | /** 1000 | * Asserts rough true-ish result. 1001 | * @name ok 1002 | * @function 1003 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 1004 | */ 1005 | ok: function( result, message ) { 1006 | message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + 1007 | QUnit.dump.parse( result ) ); 1008 | if ( !!result ) { 1009 | this.push( true, result, true, message ); 1010 | } else { 1011 | this.test.pushFailure( message, null, result ); 1012 | } 1013 | }, 1014 | 1015 | /** 1016 | * Assert that the first two arguments are equal, with an optional message. 1017 | * Prints out both actual and expected values. 1018 | * @name equal 1019 | * @function 1020 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 1021 | */ 1022 | equal: function( actual, expected, message ) { 1023 | /*jshint eqeqeq:false */ 1024 | this.push( expected == actual, actual, expected, message ); 1025 | }, 1026 | 1027 | /** 1028 | * @name notEqual 1029 | * @function 1030 | */ 1031 | notEqual: function( actual, expected, message ) { 1032 | /*jshint eqeqeq:false */ 1033 | this.push( expected != actual, actual, expected, message ); 1034 | }, 1035 | 1036 | /** 1037 | * @name propEqual 1038 | * @function 1039 | */ 1040 | propEqual: function( actual, expected, message ) { 1041 | actual = objectValues( actual ); 1042 | expected = objectValues( expected ); 1043 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1044 | }, 1045 | 1046 | /** 1047 | * @name notPropEqual 1048 | * @function 1049 | */ 1050 | notPropEqual: function( actual, expected, message ) { 1051 | actual = objectValues( actual ); 1052 | expected = objectValues( expected ); 1053 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); 1054 | }, 1055 | 1056 | /** 1057 | * @name deepEqual 1058 | * @function 1059 | */ 1060 | deepEqual: function( actual, expected, message ) { 1061 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1062 | }, 1063 | 1064 | /** 1065 | * @name notDeepEqual 1066 | * @function 1067 | */ 1068 | notDeepEqual: function( actual, expected, message ) { 1069 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); 1070 | }, 1071 | 1072 | /** 1073 | * @name strictEqual 1074 | * @function 1075 | */ 1076 | strictEqual: function( actual, expected, message ) { 1077 | this.push( expected === actual, actual, expected, message ); 1078 | }, 1079 | 1080 | /** 1081 | * @name notStrictEqual 1082 | * @function 1083 | */ 1084 | notStrictEqual: function( actual, expected, message ) { 1085 | this.push( expected !== actual, actual, expected, message ); 1086 | }, 1087 | 1088 | "throws": function( block, expected, message ) { 1089 | var actual, expectedType, 1090 | expectedOutput = expected, 1091 | ok = false; 1092 | 1093 | // 'expected' is optional unless doing string comparison 1094 | if ( message == null && typeof expected === "string" ) { 1095 | message = expected; 1096 | expected = null; 1097 | } 1098 | 1099 | this.test.ignoreGlobalErrors = true; 1100 | try { 1101 | block.call( this.test.testEnvironment ); 1102 | } catch (e) { 1103 | actual = e; 1104 | } 1105 | this.test.ignoreGlobalErrors = false; 1106 | 1107 | if ( actual ) { 1108 | expectedType = QUnit.objectType( expected ); 1109 | 1110 | // we don't want to validate thrown error 1111 | if ( !expected ) { 1112 | ok = true; 1113 | expectedOutput = null; 1114 | 1115 | // expected is a regexp 1116 | } else if ( expectedType === "regexp" ) { 1117 | ok = expected.test( errorString( actual ) ); 1118 | 1119 | // expected is a string 1120 | } else if ( expectedType === "string" ) { 1121 | ok = expected === errorString( actual ); 1122 | 1123 | // expected is a constructor, maybe an Error constructor 1124 | } else if ( expectedType === "function" && actual instanceof expected ) { 1125 | ok = true; 1126 | 1127 | // expected is an Error object 1128 | } else if ( expectedType === "object" ) { 1129 | ok = actual instanceof expected.constructor && 1130 | actual.name === expected.name && 1131 | actual.message === expected.message; 1132 | 1133 | // expected is a validation function which returns true if validation passed 1134 | } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { 1135 | expectedOutput = null; 1136 | ok = true; 1137 | } 1138 | 1139 | this.push( ok, actual, expectedOutput, message ); 1140 | } else { 1141 | this.test.pushFailure( message, null, "No exception was thrown." ); 1142 | } 1143 | } 1144 | }; 1145 | 1146 | // Test for equality any JavaScript type. 1147 | // Author: Philippe Rathé 1148 | QUnit.equiv = (function() { 1149 | 1150 | // Call the o related callback with the given arguments. 1151 | function bindCallbacks( o, callbacks, args ) { 1152 | var prop = QUnit.objectType( o ); 1153 | if ( prop ) { 1154 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1155 | return callbacks[ prop ].apply( callbacks, args ); 1156 | } else { 1157 | return callbacks[ prop ]; // or undefined 1158 | } 1159 | } 1160 | } 1161 | 1162 | // the real equiv function 1163 | var innerEquiv, 1164 | 1165 | // stack to decide between skip/abort functions 1166 | callers = [], 1167 | 1168 | // stack to avoiding loops from circular referencing 1169 | parents = [], 1170 | parentsB = [], 1171 | 1172 | getProto = Object.getPrototypeOf || function( obj ) { 1173 | /* jshint camelcase: false, proto: true */ 1174 | return obj.__proto__; 1175 | }, 1176 | callbacks = (function() { 1177 | 1178 | // for string, boolean, number and null 1179 | function useStrictEquality( b, a ) { 1180 | 1181 | /*jshint eqeqeq:false */ 1182 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1183 | 1184 | // to catch short annotation VS 'new' annotation of a 1185 | // declaration 1186 | // e.g. var i = 1; 1187 | // var j = new Number(1); 1188 | return a == b; 1189 | } else { 1190 | return a === b; 1191 | } 1192 | } 1193 | 1194 | return { 1195 | "string": useStrictEquality, 1196 | "boolean": useStrictEquality, 1197 | "number": useStrictEquality, 1198 | "null": useStrictEquality, 1199 | "undefined": useStrictEquality, 1200 | 1201 | "nan": function( b ) { 1202 | return isNaN( b ); 1203 | }, 1204 | 1205 | "date": function( b, a ) { 1206 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1207 | }, 1208 | 1209 | "regexp": function( b, a ) { 1210 | return QUnit.objectType( b ) === "regexp" && 1211 | 1212 | // the regex itself 1213 | a.source === b.source && 1214 | 1215 | // and its modifiers 1216 | a.global === b.global && 1217 | 1218 | // (gmi) ... 1219 | a.ignoreCase === b.ignoreCase && 1220 | a.multiline === b.multiline && 1221 | a.sticky === b.sticky; 1222 | }, 1223 | 1224 | // - skip when the property is a method of an instance (OOP) 1225 | // - abort otherwise, 1226 | // initial === would have catch identical references anyway 1227 | "function": function() { 1228 | var caller = callers[ callers.length - 1 ]; 1229 | return caller !== Object && typeof caller !== "undefined"; 1230 | }, 1231 | 1232 | "array": function( b, a ) { 1233 | var i, j, len, loop, aCircular, bCircular; 1234 | 1235 | // b could be an object literal here 1236 | if ( QUnit.objectType( b ) !== "array" ) { 1237 | return false; 1238 | } 1239 | 1240 | len = a.length; 1241 | if ( len !== b.length ) { 1242 | // safe and faster 1243 | return false; 1244 | } 1245 | 1246 | // track reference to avoid circular references 1247 | parents.push( a ); 1248 | parentsB.push( b ); 1249 | for ( i = 0; i < len; i++ ) { 1250 | loop = false; 1251 | for ( j = 0; j < parents.length; j++ ) { 1252 | aCircular = parents[ j ] === a[ i ]; 1253 | bCircular = parentsB[ j ] === b[ i ]; 1254 | if ( aCircular || bCircular ) { 1255 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1256 | loop = true; 1257 | } else { 1258 | parents.pop(); 1259 | parentsB.pop(); 1260 | return false; 1261 | } 1262 | } 1263 | } 1264 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1265 | parents.pop(); 1266 | parentsB.pop(); 1267 | return false; 1268 | } 1269 | } 1270 | parents.pop(); 1271 | parentsB.pop(); 1272 | return true; 1273 | }, 1274 | 1275 | "object": function( b, a ) { 1276 | 1277 | /*jshint forin:false */ 1278 | var i, j, loop, aCircular, bCircular, 1279 | // Default to true 1280 | eq = true, 1281 | aProperties = [], 1282 | bProperties = []; 1283 | 1284 | // comparing constructors is more strict than using 1285 | // instanceof 1286 | if ( a.constructor !== b.constructor ) { 1287 | 1288 | // Allow objects with no prototype to be equivalent to 1289 | // objects with Object as their constructor. 1290 | if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || 1291 | ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { 1292 | return false; 1293 | } 1294 | } 1295 | 1296 | // stack constructor before traversing properties 1297 | callers.push( a.constructor ); 1298 | 1299 | // track reference to avoid circular references 1300 | parents.push( a ); 1301 | parentsB.push( b ); 1302 | 1303 | // be strict: don't ensure hasOwnProperty and go deep 1304 | for ( i in a ) { 1305 | loop = false; 1306 | for ( j = 0; j < parents.length; j++ ) { 1307 | aCircular = parents[ j ] === a[ i ]; 1308 | bCircular = parentsB[ j ] === b[ i ]; 1309 | if ( aCircular || bCircular ) { 1310 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1311 | loop = true; 1312 | } else { 1313 | eq = false; 1314 | break; 1315 | } 1316 | } 1317 | } 1318 | aProperties.push( i ); 1319 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1320 | eq = false; 1321 | break; 1322 | } 1323 | } 1324 | 1325 | parents.pop(); 1326 | parentsB.pop(); 1327 | callers.pop(); // unstack, we are done 1328 | 1329 | for ( i in b ) { 1330 | bProperties.push( i ); // collect b's properties 1331 | } 1332 | 1333 | // Ensures identical properties name 1334 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1335 | } 1336 | }; 1337 | }()); 1338 | 1339 | innerEquiv = function() { // can take multiple arguments 1340 | var args = [].slice.apply( arguments ); 1341 | if ( args.length < 2 ) { 1342 | return true; // end transition 1343 | } 1344 | 1345 | return ( (function( a, b ) { 1346 | if ( a === b ) { 1347 | return true; // catch the most you can 1348 | } else if ( a === null || b === null || typeof a === "undefined" || 1349 | typeof b === "undefined" || 1350 | QUnit.objectType( a ) !== QUnit.objectType( b ) ) { 1351 | 1352 | // don't lose time with error prone cases 1353 | return false; 1354 | } else { 1355 | return bindCallbacks( a, callbacks, [ b, a ] ); 1356 | } 1357 | 1358 | // apply transition with (1..n) arguments 1359 | }( args[ 0 ], args[ 1 ] ) ) && innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); 1360 | }; 1361 | 1362 | return innerEquiv; 1363 | }()); 1364 | 1365 | // Based on jsDump by Ariel Flesler 1366 | // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html 1367 | QUnit.dump = (function() { 1368 | function quote( str ) { 1369 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; 1370 | } 1371 | function literal( o ) { 1372 | return o + ""; 1373 | } 1374 | function join( pre, arr, post ) { 1375 | var s = dump.separator(), 1376 | base = dump.indent(), 1377 | inner = dump.indent( 1 ); 1378 | if ( arr.join ) { 1379 | arr = arr.join( "," + s + inner ); 1380 | } 1381 | if ( !arr ) { 1382 | return pre + post; 1383 | } 1384 | return [ pre, inner + arr, base + post ].join( s ); 1385 | } 1386 | function array( arr, stack ) { 1387 | var i = arr.length, 1388 | ret = new Array( i ); 1389 | this.up(); 1390 | while ( i-- ) { 1391 | ret[ i ] = this.parse( arr[ i ], undefined, stack ); 1392 | } 1393 | this.down(); 1394 | return join( "[", ret, "]" ); 1395 | } 1396 | 1397 | var reName = /^function (\w+)/, 1398 | dump = { 1399 | // type is used mostly internally, you can fix a (custom)type in advance 1400 | parse: function( obj, type, stack ) { 1401 | stack = stack || []; 1402 | var inStack, res, 1403 | parser = this.parsers[ type || this.typeOf( obj ) ]; 1404 | 1405 | type = typeof parser; 1406 | inStack = inArray( obj, stack ); 1407 | 1408 | if ( inStack !== -1 ) { 1409 | return "recursion(" + ( inStack - stack.length ) + ")"; 1410 | } 1411 | if ( type === "function" ) { 1412 | stack.push( obj ); 1413 | res = parser.call( this, obj, stack ); 1414 | stack.pop(); 1415 | return res; 1416 | } 1417 | return ( type === "string" ) ? parser : this.parsers.error; 1418 | }, 1419 | typeOf: function( obj ) { 1420 | var type; 1421 | if ( obj === null ) { 1422 | type = "null"; 1423 | } else if ( typeof obj === "undefined" ) { 1424 | type = "undefined"; 1425 | } else if ( QUnit.is( "regexp", obj ) ) { 1426 | type = "regexp"; 1427 | } else if ( QUnit.is( "date", obj ) ) { 1428 | type = "date"; 1429 | } else if ( QUnit.is( "function", obj ) ) { 1430 | type = "function"; 1431 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1432 | type = "window"; 1433 | } else if ( obj.nodeType === 9 ) { 1434 | type = "document"; 1435 | } else if ( obj.nodeType ) { 1436 | type = "node"; 1437 | } else if ( 1438 | 1439 | // native arrays 1440 | toString.call( obj ) === "[object Array]" || 1441 | 1442 | // NodeList objects 1443 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && typeof obj[ 0 ] === "undefined" ) ) ) 1444 | ) { 1445 | type = "array"; 1446 | } else if ( obj.constructor === Error.prototype.constructor ) { 1447 | type = "error"; 1448 | } else { 1449 | type = typeof obj; 1450 | } 1451 | return type; 1452 | }, 1453 | separator: function() { 1454 | return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; 1455 | }, 1456 | // extra can be a number, shortcut for increasing-calling-decreasing 1457 | indent: function( extra ) { 1458 | if ( !this.multiline ) { 1459 | return ""; 1460 | } 1461 | var chr = this.indentChar; 1462 | if ( this.HTML ) { 1463 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1464 | } 1465 | return new Array( this.depth + ( extra || 0 ) ).join( chr ); 1466 | }, 1467 | up: function( a ) { 1468 | this.depth += a || 1; 1469 | }, 1470 | down: function( a ) { 1471 | this.depth -= a || 1; 1472 | }, 1473 | setParser: function( name, parser ) { 1474 | this.parsers[ name ] = parser; 1475 | }, 1476 | // The next 3 are exposed so you can use them 1477 | quote: quote, 1478 | literal: literal, 1479 | join: join, 1480 | // 1481 | depth: 1, 1482 | // This is the list of parsers, to modify them, use dump.setParser 1483 | parsers: { 1484 | window: "[Window]", 1485 | document: "[Document]", 1486 | error: function( error ) { 1487 | return "Error(\"" + error.message + "\")"; 1488 | }, 1489 | unknown: "[Unknown]", 1490 | "null": "null", 1491 | "undefined": "undefined", 1492 | "function": function( fn ) { 1493 | var ret = "function", 1494 | // functions never have name in IE 1495 | name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; 1496 | 1497 | if ( name ) { 1498 | ret += " " + name; 1499 | } 1500 | ret += "( "; 1501 | 1502 | ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1503 | return join( ret, dump.parse( fn, "functionCode" ), "}" ); 1504 | }, 1505 | array: array, 1506 | nodelist: array, 1507 | "arguments": array, 1508 | object: function( map, stack ) { 1509 | /*jshint forin:false */ 1510 | var ret = [], keys, key, val, i, nonEnumerableProperties; 1511 | dump.up(); 1512 | keys = []; 1513 | for ( key in map ) { 1514 | keys.push( key ); 1515 | } 1516 | 1517 | // Some properties are not always enumerable on Error objects. 1518 | nonEnumerableProperties = [ "message", "name" ]; 1519 | for ( i in nonEnumerableProperties ) { 1520 | key = nonEnumerableProperties[ i ]; 1521 | if ( key in map && !( key in keys ) ) { 1522 | keys.push( key ); 1523 | } 1524 | } 1525 | keys.sort(); 1526 | for ( i = 0; i < keys.length; i++ ) { 1527 | key = keys[ i ]; 1528 | val = map[ key ]; 1529 | ret.push( dump.parse( key, "key" ) + ": " + dump.parse( val, undefined, stack ) ); 1530 | } 1531 | dump.down(); 1532 | return join( "{", ret, "}" ); 1533 | }, 1534 | node: function( node ) { 1535 | var len, i, val, 1536 | open = dump.HTML ? "<" : "<", 1537 | close = dump.HTML ? ">" : ">", 1538 | tag = node.nodeName.toLowerCase(), 1539 | ret = open + tag, 1540 | attrs = node.attributes; 1541 | 1542 | if ( attrs ) { 1543 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1544 | val = attrs[ i ].nodeValue; 1545 | 1546 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1547 | // Those have values like undefined, null, 0, false, "" or "inherit". 1548 | if ( val && val !== "inherit" ) { 1549 | ret += " " + attrs[ i ].nodeName + "=" + dump.parse( val, "attribute" ); 1550 | } 1551 | } 1552 | } 1553 | ret += close; 1554 | 1555 | // Show content of TextNode or CDATASection 1556 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1557 | ret += node.nodeValue; 1558 | } 1559 | 1560 | return ret + open + "/" + tag + close; 1561 | }, 1562 | 1563 | // function calls it internally, it's the arguments part of the function 1564 | functionArgs: function( fn ) { 1565 | var args, 1566 | l = fn.length; 1567 | 1568 | if ( !l ) { 1569 | return ""; 1570 | } 1571 | 1572 | args = new Array( l ); 1573 | while ( l-- ) { 1574 | 1575 | // 97 is 'a' 1576 | args[ l ] = String.fromCharCode( 97 + l ); 1577 | } 1578 | return " " + args.join( ", " ) + " "; 1579 | }, 1580 | // object calls it internally, the key part of an item in a map 1581 | key: quote, 1582 | // function calls it internally, it's the content of the function 1583 | functionCode: "[code]", 1584 | // node calls it internally, it's an html attribute value 1585 | attribute: quote, 1586 | string: quote, 1587 | date: quote, 1588 | regexp: literal, 1589 | number: literal, 1590 | "boolean": literal 1591 | }, 1592 | // if true, entities are escaped ( <, >, \t, space and \n ) 1593 | HTML: false, 1594 | // indentation unit 1595 | indentChar: " ", 1596 | // if true, items in a collection, are separated by a \n, else just a space. 1597 | multiline: true 1598 | }; 1599 | 1600 | return dump; 1601 | }()); 1602 | 1603 | // back compat 1604 | QUnit.jsDump = QUnit.dump; 1605 | 1606 | // For browser, export only select globals 1607 | if ( typeof window !== "undefined" ) { 1608 | 1609 | // Deprecated 1610 | // Extend assert methods to QUnit and Global scope through Backwards compatibility 1611 | (function() { 1612 | var i, 1613 | assertions = Assert.prototype; 1614 | 1615 | function applyCurrent( current ) { 1616 | return function() { 1617 | var assert = new Assert( QUnit.config.current ); 1618 | current.apply( assert, arguments ); 1619 | }; 1620 | } 1621 | 1622 | for ( i in assertions ) { 1623 | QUnit[ i ] = applyCurrent( assertions[ i ] ); 1624 | } 1625 | })(); 1626 | 1627 | (function() { 1628 | var i, l, 1629 | keys = [ 1630 | "test", 1631 | "module", 1632 | "expect", 1633 | "asyncTest", 1634 | "start", 1635 | "stop", 1636 | "ok", 1637 | "equal", 1638 | "notEqual", 1639 | "propEqual", 1640 | "notPropEqual", 1641 | "deepEqual", 1642 | "notDeepEqual", 1643 | "strictEqual", 1644 | "notStrictEqual", 1645 | "throws" 1646 | ]; 1647 | 1648 | for ( i = 0, l = keys.length; i < l; i++ ) { 1649 | window[ keys[ i ] ] = QUnit[ keys[ i ] ]; 1650 | } 1651 | })(); 1652 | 1653 | window.QUnit = QUnit; 1654 | } 1655 | 1656 | // For CommonJS environments, export everything 1657 | if ( typeof module !== "undefined" && module.exports ) { 1658 | module.exports = QUnit; 1659 | } 1660 | 1661 | // Get a reference to the global object, like window in browsers 1662 | }( (function() { 1663 | return this; 1664 | })() )); 1665 | 1666 | /*istanbul ignore next */ 1667 | /* 1668 | * Javascript Diff Algorithm 1669 | * By John Resig (http://ejohn.org/) 1670 | * Modified by Chu Alan "sprite" 1671 | * 1672 | * Released under the MIT license. 1673 | * 1674 | * More Info: 1675 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1676 | * 1677 | * Usage: QUnit.diff(expected, actual) 1678 | * 1679 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 1680 | */ 1681 | QUnit.diff = (function() { 1682 | var hasOwn = Object.prototype.hasOwnProperty; 1683 | 1684 | /*jshint eqeqeq:false, eqnull:true */ 1685 | function diff( o, n ) { 1686 | var i, 1687 | ns = {}, 1688 | os = {}; 1689 | 1690 | for ( i = 0; i < n.length; i++ ) { 1691 | if ( !hasOwn.call( ns, n[ i ] ) ) { 1692 | ns[ n[ i ] ] = { 1693 | rows: [], 1694 | o: null 1695 | }; 1696 | } 1697 | ns[ n[ i ] ].rows.push( i ); 1698 | } 1699 | 1700 | for ( i = 0; i < o.length; i++ ) { 1701 | if ( !hasOwn.call( os, o[ i ] ) ) { 1702 | os[ o[ i ] ] = { 1703 | rows: [], 1704 | n: null 1705 | }; 1706 | } 1707 | os[ o[ i ] ].rows.push( i ); 1708 | } 1709 | 1710 | for ( i in ns ) { 1711 | if ( hasOwn.call( ns, i ) ) { 1712 | if ( ns[ i ].rows.length === 1 && hasOwn.call( os, i ) && os[ i ].rows.length === 1 ) { 1713 | n[ ns[ i ].rows[ 0 ] ] = { 1714 | text: n[ ns[ i ].rows[ 0 ] ], 1715 | row: os[ i ].rows[ 0 ] 1716 | }; 1717 | o[ os[ i ].rows[ 0 ] ] = { 1718 | text: o[ os[ i ].rows[ 0 ] ], 1719 | row: ns[ i ].rows[ 0 ] 1720 | }; 1721 | } 1722 | } 1723 | } 1724 | 1725 | for ( i = 0; i < n.length - 1; i++ ) { 1726 | if ( n[ i ].text != null && n[ i + 1 ].text == null && n[ i ].row + 1 < o.length && o[ n[ i ].row + 1 ].text == null && 1727 | n[ i + 1 ] == o[ n[ i ].row + 1 ] ) { 1728 | 1729 | n[ i + 1 ] = { 1730 | text: n[ i + 1 ], 1731 | row: n[ i ].row + 1 1732 | }; 1733 | o[ n[ i ].row + 1 ] = { 1734 | text: o[ n[ i ].row + 1 ], 1735 | row: i + 1 1736 | }; 1737 | } 1738 | } 1739 | 1740 | for ( i = n.length - 1; i > 0; i-- ) { 1741 | if ( n[ i ].text != null && n[ i - 1 ].text == null && n[ i ].row > 0 && o[ n[ i ].row - 1 ].text == null && 1742 | n[ i - 1 ] == o[ n[ i ].row - 1 ] ) { 1743 | 1744 | n[ i - 1 ] = { 1745 | text: n[ i - 1 ], 1746 | row: n[ i ].row - 1 1747 | }; 1748 | o[ n[ i ].row - 1 ] = { 1749 | text: o[ n[ i ].row - 1 ], 1750 | row: i - 1 1751 | }; 1752 | } 1753 | } 1754 | 1755 | return { 1756 | o: o, 1757 | n: n 1758 | }; 1759 | } 1760 | 1761 | return function( o, n ) { 1762 | o = o.replace( /\s+$/, "" ); 1763 | n = n.replace( /\s+$/, "" ); 1764 | 1765 | var i, pre, 1766 | str = "", 1767 | out = diff( o === "" ? [] : o.split( /\s+/ ), n === "" ? [] : n.split( /\s+/ ) ), 1768 | oSpace = o.match( /\s+/g ), 1769 | nSpace = n.match( /\s+/g ); 1770 | 1771 | if ( oSpace == null ) { 1772 | oSpace = [ " " ]; 1773 | } else { 1774 | oSpace.push( " " ); 1775 | } 1776 | 1777 | if ( nSpace == null ) { 1778 | nSpace = [ " " ]; 1779 | } else { 1780 | nSpace.push( " " ); 1781 | } 1782 | 1783 | if ( out.n.length === 0 ) { 1784 | for ( i = 0; i < out.o.length; i++ ) { 1785 | str += "" + out.o[ i ] + oSpace[ i ] + ""; 1786 | } 1787 | } else { 1788 | if ( out.n[ 0 ].text == null ) { 1789 | for ( n = 0; n < out.o.length && out.o[ n ].text == null; n++ ) { 1790 | str += "" + out.o[ n ] + oSpace[ n ] + ""; 1791 | } 1792 | } 1793 | 1794 | for ( i = 0; i < out.n.length; i++ ) { 1795 | if ( out.n[ i ].text == null ) { 1796 | str += "" + out.n[ i ] + nSpace[ i ] + ""; 1797 | } else { 1798 | 1799 | // `pre` initialized at top of scope 1800 | pre = ""; 1801 | 1802 | for ( n = out.n[ i ].row + 1; n < out.o.length && out.o[ n ].text == null; n++ ) { 1803 | pre += "" + out.o[ n ] + oSpace[ n ] + ""; 1804 | } 1805 | str += " " + out.n[ i ].text + nSpace[ i ] + pre; 1806 | } 1807 | } 1808 | } 1809 | 1810 | return str; 1811 | }; 1812 | }()); 1813 | 1814 | (function() { 1815 | 1816 | // Deprecated QUnit.init - Ref #530 1817 | // Re-initialize the configuration options 1818 | QUnit.init = function() { 1819 | var tests, banner, result, qunit, 1820 | config = QUnit.config; 1821 | 1822 | config.stats = { all: 0, bad: 0 }; 1823 | config.moduleStats = { all: 0, bad: 0 }; 1824 | config.started = 0; 1825 | config.updateRate = 1000; 1826 | config.blocking = false; 1827 | config.autostart = true; 1828 | config.autorun = false; 1829 | config.filter = ""; 1830 | config.queue = []; 1831 | config.semaphore = 1; 1832 | 1833 | // Return on non-browser environments 1834 | // This is necessary to not break on node tests 1835 | if ( typeof window === "undefined" ) { 1836 | return; 1837 | } 1838 | 1839 | qunit = id( "qunit" ); 1840 | if ( qunit ) { 1841 | qunit.innerHTML = 1842 | "

" + escapeText( document.title ) + "

" + 1843 | "

" + 1844 | "
" + 1845 | "

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

    " + escapeText( document.title ) + "

    " + 2269 | "

    " + 2270 | "
    " + 2271 | "

    " + 2272 | "
      "; 2273 | } 2274 | 2275 | appendBanner(); 2276 | appendTestResults(); 2277 | appendUserAgent(); 2278 | appendToolbar(); 2279 | storeFixture(); 2280 | }); 2281 | 2282 | QUnit.done(function( details ) { 2283 | var i, key, 2284 | banner = id( "qunit-banner" ), 2285 | tests = id( "qunit-tests" ), 2286 | html = [ 2287 | "Tests completed in ", 2288 | details.runtime, 2289 | " milliseconds.
      ", 2290 | "", 2291 | details.passed, 2292 | " assertions of ", 2293 | details.total, 2294 | " passed, ", 2295 | details.failed, 2296 | " failed." 2297 | ].join( "" ); 2298 | 2299 | if ( banner ) { 2300 | banner.className = details.failed ? "qunit-fail" : "qunit-pass"; 2301 | } 2302 | 2303 | if ( tests ) { 2304 | id( "qunit-testresult" ).innerHTML = html; 2305 | } 2306 | 2307 | if ( config.altertitle && defined.document && document.title ) { 2308 | 2309 | // show ✖ for good, ✔ for bad suite result in title 2310 | // use escape sequences in case file gets loaded with non-utf-8-charset 2311 | document.title = [ 2312 | ( details.failed ? "\u2716" : "\u2714" ), 2313 | document.title.replace( /^[\u2714\u2716] /i, "" ) 2314 | ].join( " " ); 2315 | } 2316 | 2317 | // clear own sessionStorage items if all tests passed 2318 | if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { 2319 | for ( i = 0; i < sessionStorage.length; i++ ) { 2320 | key = sessionStorage.key( i++ ); 2321 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 2322 | sessionStorage.removeItem( key ); 2323 | } 2324 | } 2325 | } 2326 | 2327 | // scroll back to top to show results 2328 | if ( config.scrolltop && window.scrollTo ) { 2329 | window.scrollTo( 0, 0 ); 2330 | } 2331 | }); 2332 | 2333 | function getNameHtml( name, module ) { 2334 | var nameHtml = ""; 2335 | 2336 | if ( module ) { 2337 | nameHtml = "" + escapeText( module ) + ": "; 2338 | } 2339 | 2340 | nameHtml += "" + escapeText( name ) + ""; 2341 | 2342 | return nameHtml; 2343 | } 2344 | 2345 | QUnit.testStart(function( details ) { 2346 | var a, b, li, running, assertList, 2347 | name = getNameHtml( details.name, details.module ), 2348 | tests = id( "qunit-tests" ); 2349 | 2350 | if ( tests ) { 2351 | b = document.createElement( "strong" ); 2352 | b.innerHTML = name; 2353 | 2354 | a = document.createElement( "a" ); 2355 | a.innerHTML = "Rerun"; 2356 | a.href = QUnit.url({ testNumber: details.testNumber }); 2357 | 2358 | li = document.createElement( "li" ); 2359 | li.appendChild( b ); 2360 | li.appendChild( a ); 2361 | li.className = "running"; 2362 | li.id = "qunit-test-output" + details.testNumber; 2363 | 2364 | assertList = document.createElement( "ol" ); 2365 | assertList.className = "qunit-assert-list"; 2366 | 2367 | li.appendChild( assertList ); 2368 | 2369 | tests.appendChild( li ); 2370 | } 2371 | 2372 | running = id( "qunit-testresult" ); 2373 | if ( running ) { 2374 | running.innerHTML = "Running:
      " + name; 2375 | } 2376 | 2377 | }); 2378 | 2379 | QUnit.log(function( details ) { 2380 | var assertList, assertLi, 2381 | message, expected, actual, 2382 | testItem = id( "qunit-test-output" + details.testNumber ); 2383 | 2384 | if ( !testItem ) { 2385 | return; 2386 | } 2387 | 2388 | message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); 2389 | message = "" + message + ""; 2390 | 2391 | // pushFailure doesn't provide details.expected 2392 | // when it calls, it's implicit to also not show expected and diff stuff 2393 | // Also, we need to check details.expected existence, as it can exist and be undefined 2394 | if ( !details.result && hasOwn.call( details, "expected" ) ) { 2395 | expected = escapeText( QUnit.dump.parse( details.expected ) ); 2396 | actual = escapeText( QUnit.dump.parse( details.actual ) ); 2397 | message += ""; 2400 | 2401 | if ( actual !== expected ) { 2402 | message += "" + 2404 | ""; 2406 | } 2407 | 2408 | if ( details.source ) { 2409 | message += ""; 2411 | } 2412 | 2413 | message += "
      Expected:
      " +
      2398 | 			expected +
      2399 | 			"
      Result:
      " +
      2403 | 				actual + "
      Diff:
      " +
      2405 | 				QUnit.diff( expected, actual ) + "
      Source:
      " +
      2410 | 				escapeText( details.source ) + "
      "; 2414 | 2415 | // this occours when pushFailure is set and we have an extracted stack trace 2416 | } else if ( !details.result && details.source ) { 2417 | message += "" + 2418 | "" + 2420 | "
      Source:
      " +
      2419 | 			escapeText( details.source ) + "
      "; 2421 | } 2422 | 2423 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 2424 | 2425 | assertLi = document.createElement( "li" ); 2426 | assertLi.className = details.result ? "pass" : "fail"; 2427 | assertLi.innerHTML = message; 2428 | assertList.appendChild( assertLi ); 2429 | }); 2430 | 2431 | QUnit.testDone(function( details ) { 2432 | var testTitle, time, testItem, assertList, 2433 | good, bad, testCounts, 2434 | tests = id( "qunit-tests" ); 2435 | 2436 | // QUnit.reset() is deprecated and will be replaced for a new 2437 | // fixture reset function on QUnit 2.0/2.1. 2438 | // It's still called here for backwards compatibility handling 2439 | QUnit.reset(); 2440 | 2441 | if ( !tests ) { 2442 | return; 2443 | } 2444 | 2445 | testItem = id( "qunit-test-output" + details.testNumber ); 2446 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 2447 | 2448 | good = details.passed; 2449 | bad = details.failed; 2450 | 2451 | // store result when possible 2452 | if ( config.reorder && defined.sessionStorage ) { 2453 | if ( bad ) { 2454 | sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); 2455 | } else { 2456 | sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); 2457 | } 2458 | } 2459 | 2460 | if ( bad === 0 ) { 2461 | addClass( assertList, "qunit-collapsed" ); 2462 | } 2463 | 2464 | // testItem.firstChild is the test name 2465 | testTitle = testItem.firstChild; 2466 | 2467 | testCounts = bad ? 2468 | "" + bad + ", " + "" + good + ", " : 2469 | ""; 2470 | 2471 | testTitle.innerHTML += " (" + testCounts + 2472 | details.assertions.length + ")"; 2473 | 2474 | addEvent( testTitle, "click", function() { 2475 | toggleClass( assertList, "qunit-collapsed" ); 2476 | }); 2477 | 2478 | time = document.createElement( "span" ); 2479 | time.className = "runtime"; 2480 | time.innerHTML = details.runtime + " ms"; 2481 | 2482 | testItem.className = bad ? "fail" : "pass"; 2483 | 2484 | testItem.insertBefore( time, assertList ); 2485 | }); 2486 | 2487 | if ( !defined.document || document.readyState === "complete" ) { 2488 | config.autorun = true; 2489 | } 2490 | 2491 | if ( defined.document ) { 2492 | addEvent( window, "load", QUnit.load ); 2493 | } 2494 | 2495 | })(); 2496 | -------------------------------------------------------------------------------- /test/resources/sinon-qunit-1.0.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sinon-qunit 1.0.0, 2010/12/09 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 | /*global sinon, QUnit, test*/ 35 | sinon.assert.fail = function (msg) { 36 | QUnit.ok(false, msg); 37 | }; 38 | 39 | sinon.assert.pass = function (assertion) { 40 | QUnit.ok(true, assertion); 41 | }; 42 | 43 | sinon.config = { 44 | injectIntoThis: true, 45 | injectInto: null, 46 | properties: ["spy", "stub", "mock", "clock", "sandbox"], 47 | useFakeTimers: true, 48 | useFakeServer: false 49 | }; 50 | 51 | (function (global) { 52 | var qTest = QUnit.test; 53 | 54 | QUnit.test = global.test = function (testName, expected, callback, async) { 55 | if (arguments.length === 2) { 56 | callback = expected; 57 | expected = null; 58 | } 59 | 60 | return qTest(testName, expected, sinon.test(callback), async); 61 | }; 62 | }(this)); 63 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unit Tests 6 | 7 | 12 | 13 | 14 |
      15 |
      16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | 'use strict'; 4 | 5 | var defaultSettings = { 6 | providers: { 7 | 'foo': 'www.foo.com', 8 | 'bar': 'www.bar.com', 9 | 'baz': 'www.baz.com', 10 | 'qux': 'www.qux.com' 11 | }, 12 | countryMapping: { 13 | 'CN': [ 'foo', 'bar' ], 14 | 'US': [ 'baz', 'qux' ] 15 | }, 16 | asnMapping: { 17 | '123': [ 'bar', 'qux' ], 18 | '234': [ 'bar', 'baz' ] 19 | }, 20 | defaultProviders: [ 'foo', 'bar' ], 21 | lastResortProvider: 'foo', 22 | defaultTtl: 20, 23 | availabilityThresholds: { 24 | normal: 92, 25 | pingdom: 50 26 | }, 27 | sonarThreshold: 0.95, 28 | minValidRtt: 5 29 | }; 30 | 31 | module('doInit'); 32 | 33 | function testDoInit(i) { 34 | return function() { 35 | 36 | var sut, 37 | config = { 38 | requireProvider: this.stub() 39 | }, 40 | testStuff = { 41 | config: config 42 | }; 43 | 44 | i.setup(testStuff); 45 | 46 | sut = new OpenmixApplication(i.settings || defaultSettings); 47 | 48 | // Test 49 | sut.doInit(config); 50 | 51 | // Assert 52 | i.verify(testStuff); 53 | }; 54 | } 55 | 56 | test('default', testDoInit({ 57 | setup: function() { 58 | return; 59 | }, 60 | verify: function(i) { 61 | equal(i.config.requireProvider.callCount, 4, 'Check requireProvider call count'); 62 | equal(i.config.requireProvider.args[3][0], 'foo', 'Check provider alias'); 63 | equal(i.config.requireProvider.args[2][0], 'bar', 'Check provider alias'); 64 | equal(i.config.requireProvider.args[1][0], 'baz', 'Check provider alias'); 65 | equal(i.config.requireProvider.args[0][0], 'qux', 'Check provider alias'); 66 | } 67 | })); 68 | 69 | module('handleRequest'); 70 | 71 | function testHandleRequest(i) { 72 | return function() { 73 | var sut, 74 | request = { 75 | getProbe: this.stub(), 76 | getData: this.stub() 77 | }, 78 | response = { 79 | respond: this.stub(), 80 | setTTL: this.stub(), 81 | setReasonCode: this.stub() 82 | }, 83 | testStuff = { 84 | request: request, 85 | response: response 86 | }; 87 | 88 | i.setup(testStuff); 89 | 90 | sut = new OpenmixApplication(i.settings || defaultSettings); 91 | 92 | // Test 93 | sut.handleRequest(request, response); 94 | 95 | // Assert 96 | i.verify(testStuff); 97 | }; 98 | } 99 | 100 | test('use country providers; baz fastest of those', testHandleRequest({ 101 | avail: { 102 | foo: { avail: 100 }, 103 | bar: { avail: 100 }, 104 | baz: { avail: 100 }, 105 | qux: { avail: 100 } 106 | }, 107 | sonar: { 108 | foo: '0.999999', 109 | bar: '1.000000', 110 | baz: '1.000000', 111 | qux: '1.000000' 112 | }, 113 | rtt: { 114 | foo: { 'http_rtt': 100 }, 115 | bar: { 'http_rtt': 201 }, 116 | baz: { 'http_rtt': 199 }, 117 | qux: { 'http_rtt': 200 } 118 | }, 119 | setup: function(i) { 120 | i.request.country = 'US'; 121 | i.request.getProbe.withArgs('avail').returns(this.avail); 122 | i.request.getProbe.withArgs('http_rtt').returns(this.rtt); 123 | i.request.getData.returns(this.sonar); 124 | }, 125 | verify: function(i) { 126 | equal(i.response.respond.args[0][0], 'baz'); 127 | equal(i.response.respond.args[0][1], 'www.baz.com'); 128 | equal(i.response.setTTL.args[0][0], 20); 129 | } 130 | })); 131 | 132 | test('on bar available', testHandleRequest({ 133 | avail: { 134 | foo: { avail: 91 }, 135 | bar: { avail: 100 }, 136 | baz: { avail: 91 }, 137 | qux: { avail: 91 } 138 | }, 139 | sonar: { 140 | foo: '1.000000', 141 | bar: '1.000000', 142 | baz: '1.000000', 143 | qux: '1.000000' 144 | }, 145 | rtt: { 146 | foo: { 'http_rtt': 200 }, 147 | bar: { 'http_rtt': 201 }, 148 | baz: { 'http_rtt': 200 }, 149 | qux: { 'http_rtt': 200 } 150 | }, 151 | setup: function(i) { 152 | i.request.getProbe.withArgs('avail').returns(this.avail); 153 | i.request.getProbe.withArgs('http_rtt').returns(this.rtt); 154 | i.request.getData.returns(this.sonar); 155 | }, 156 | verify: function(i) { 157 | equal(i.response.respond.args[0][0], 'bar'); 158 | equal(i.response.respond.args[0][1], 'www.bar.com'); 159 | equal(i.response.setReasonCode.args[0][0], 'D'); 160 | equal(i.response.setTTL.args[0][0], 20); 161 | } 162 | })); 163 | 164 | test('no available candidates', testHandleRequest({ 165 | avail: { 166 | foo: { avail: 91 }, 167 | bar: { avail: 91 }, 168 | baz: { avail: 91 }, 169 | qux: { avail: 91 } 170 | }, 171 | sonar: { 172 | foo: '1.000000', 173 | bar: '1.000000', 174 | baz: '1.000000', 175 | qux: '1.000000' 176 | }, 177 | rtt: { 178 | foo: { 'http_rtt': 200 }, 179 | bar: { 'http_rtt': 201 }, 180 | baz: { 'http_rtt': 200 }, 181 | qux: { 'http_rtt': 200 } 182 | }, 183 | setup: function(i) { 184 | i.request.getProbe.withArgs('avail').returns(this.avail); 185 | i.request.getProbe.withArgs('http_rtt').returns(this.rtt); 186 | i.request.getData.returns(this.sonar); 187 | }, 188 | verify: function(i) { 189 | equal(i.response.respond.args[0][0], 'foo'); 190 | equal(i.response.respond.args[0][1], 'www.foo.com'); 191 | equal(i.response.setReasonCode.args[0][0], 'E'); 192 | equal(i.response.setTTL.args[0][0], 20); 193 | } 194 | })); 195 | 196 | test('RTT data for only 1 available candidate', testHandleRequest({ 197 | avail: { 198 | foo: { avail: 100 }, 199 | bar: { avail: 100 }, 200 | baz: { avail: 100 }, 201 | qux: { avail: 100 } 202 | }, 203 | sonar: { 204 | foo: '1.000000', 205 | bar: '1.000000', 206 | baz: '1.000000', 207 | qux: '1.000000' 208 | }, 209 | rtt: { 210 | foo: { 'http_rtt': 4 }, 211 | bar: {}, 212 | baz: {}, 213 | qux: {} 214 | }, 215 | setup: function(i) { 216 | i.request.getProbe.withArgs('avail').returns(this.avail); 217 | i.request.getProbe.withArgs('http_rtt').returns(this.rtt); 218 | i.request.getData.returns(this.sonar); 219 | }, 220 | verify: function(i) { 221 | equal(i.response.respond.args[0][0], 'foo'); 222 | equal(i.response.respond.args[0][1], 'www.foo.com'); 223 | equal(i.response.setReasonCode.args[0][0], 'D'); 224 | equal(i.response.setTTL.args[0][0], 20); 225 | } 226 | })); 227 | 228 | test('RTT data for no available candidates', testHandleRequest({ 229 | avail: { 230 | foo: { avail: 91 }, 231 | bar: { avail: 100 }, 232 | baz: { avail: 100 }, 233 | qux: { avail: 100 } 234 | }, 235 | sonar: { 236 | foo: '1.000000', 237 | bar: '1.000000', 238 | baz: '1.000000', 239 | qux: '1.000000' 240 | }, 241 | rtt: { 242 | foo: { 'http_rtt': 4 }, 243 | bar: {}, 244 | baz: {}, 245 | qux: {} 246 | }, 247 | setup: function(i) { 248 | i.request.getProbe.withArgs('avail').returns(this.avail); 249 | i.request.getProbe.withArgs('http_rtt').returns(this.rtt); 250 | i.request.getData.returns(this.sonar); 251 | }, 252 | verify: function(i) { 253 | equal(i.response.respond.args[0][0], 'foo'); 254 | equal(i.response.respond.args[0][1], 'www.foo.com'); 255 | equal(i.response.setReasonCode.args[0][0], 'E'); 256 | equal(i.response.setTTL.args[0][0], 20); 257 | } 258 | })); 259 | 260 | test('no data', testHandleRequest({ 261 | avail: { 262 | foo: {}, 263 | bar: {}, 264 | baz: {}, 265 | qux: {} 266 | }, 267 | sonar: { 268 | foo: '1.000000', 269 | bar: '1.000000', 270 | baz: '1.000000', 271 | qux: '1.000000' 272 | }, 273 | rtt: { 274 | foo: {}, 275 | bar: {}, 276 | baz: {}, 277 | qux: {} 278 | }, 279 | setup: function(i) { 280 | i.request.getProbe.withArgs('avail').returns(this.avail); 281 | i.request.getProbe.withArgs('http_rtt').returns(this.rtt); 282 | i.request.getData.returns(this.sonar); 283 | }, 284 | verify: function(i) { 285 | equal(i.response.respond.args[0][0], 'foo'); 286 | equal(i.response.respond.args[0][1], 'www.foo.com'); 287 | equal(i.response.setReasonCode.args[0][0], 'E'); 288 | } 289 | })); 290 | 291 | test('no mappings', testHandleRequest({ 292 | settings: { 293 | providers: { 294 | 'foo': 'www.foo.com', 295 | 'bar': 'www.bar.com', 296 | 'baz': 'www.baz.com', 297 | }, 298 | defaultProviders: [ 'foo', 'bar' ], 299 | lastResortProvider: 'foo', 300 | defaultTtl: 20, 301 | availabilityThresholds: { 302 | normal: 92, 303 | pingdom: 50 304 | }, 305 | sonarThreshold: 0.95, 306 | minValidRtt: 5 307 | }, 308 | avail: { 309 | foo: { avail: 100 }, 310 | bar: { avail: 100 }, 311 | baz: { avail: 100 } 312 | }, 313 | sonar: { 314 | foo: '1.000000', 315 | bar: '1.000000', 316 | baz: '1.000000' 317 | }, 318 | rtt: { 319 | foo: { 'http_rtt': 199 }, 320 | bar: { 'http_rtt': 200 }, 321 | baz: { 'http_rtt': 200 } 322 | }, 323 | setup: function(i) { 324 | i.request.getProbe.withArgs('avail').returns(this.avail); 325 | i.request.getProbe.withArgs('http_rtt').returns(this.rtt); 326 | i.request.getData.returns(this.sonar); 327 | }, 328 | verify: function(i) { 329 | equal(i.response.respond.args[0][0], 'foo'); 330 | equal(i.response.respond.args[0][1], 'www.foo.com'); 331 | equal(i.response.setReasonCode.args[0][0], 'A'); 332 | } 333 | })); 334 | 335 | }()); 336 | -------------------------------------------------------------------------------- /validate-js.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors 4 | red='\033[0;31m' 5 | green='\033[0;32m' 6 | yellow='\033[1;33m' 7 | cyan='\033[0;36m' 8 | no_color='\033[0m' 9 | 10 | # Change to the project root directory 11 | cd "$( dirname "${BASH_SOURCE[0]}" )" 12 | 13 | echo 14 | echo -e "${cyan}Validating with Google Closure Compiler${red}" 15 | echo 16 | java -jar test/compiler.jar --js app.js --externs test/openmix-externs.js --compilation_level ADVANCED --js_output_file /dev/null --warning_level VERBOSE --language_in ECMASCRIPT5 17 | echo -e "${no_color}" 18 | 19 | echo -e "${cyan}Validating with JSHint${red}" 20 | echo 21 | node_modules/jshint/bin/jshint --config jshintConfig.json app.js 22 | node_modules/jshint/bin/jshint --config test/jshintConfigTests.json test/tests.js 23 | echo -e "${no_color}" 24 | --------------------------------------------------------------------------------