├── .gitignore ├── test ├── js │ ├── .eslintrc │ ├── test.js │ └── qunit.js ├── pubsub.html └── css │ └── qunit.css ├── .travis.yml ├── bower.json ├── package.json ├── .eslintrc ├── pubsub.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules* -------------------------------------------------------------------------------- /test/js/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "test" : true, 4 | "asyncTest" : true, 5 | "ok" : true, 6 | "start" : true, 7 | "equal" : true, 8 | "deepEqual" : true 9 | } 10 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false # http://docs.travis-ci.com/user/workers/container-based-infrastructure/ 2 | language: node_js 3 | node_js: 4 | - 0.12 5 | - 4 6 | - 5 7 | 8 | before_install: 9 | - mkdir node_modules; 10 | 11 | notifications: 12 | email: 13 | - wdsahadar@gmail.com 14 | 15 | script: "npm test" -------------------------------------------------------------------------------- /test/pubsub.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pubsub qunit test 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubsub-advanced", 3 | "main": "pubsub-advanced", 4 | "version": "1.5.1", 5 | "homepage": "https://github.com/Sahadar/pubsub.js", 6 | "authors": [ 7 | "Wojciech Dłubacz " 8 | ], 9 | "description": "JavaScript pubsub implementation with wildcards, inheritance and multisubscriptions", 10 | "keywords": [ 11 | "pubsub", 12 | "listener", 13 | "vanilla", 14 | "pub/sub", 15 | "publish/subscribe", 16 | "publish", 17 | "event", 18 | "emitter", 19 | "eventemitter", 20 | "subscribe" 21 | ], 22 | "license": "MIT", 23 | "ignore": [ 24 | "**/.*", 25 | "node_modules", 26 | "bower_components", 27 | "test", 28 | "tests" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubsub.js", 3 | "description": "Vanilla JS Pubsub implementation with wildcards and inheritance", 4 | "version": "1.5.1", 5 | "author": "Wojciech Dłubacz ", 6 | "directories" : { 7 | "lib" : ".", 8 | "test" : "test" 9 | }, 10 | "contributors": [ 11 | "nehachaudhary " 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:Sahadar/pubsub.js.git" 16 | }, 17 | "keywords": [ 18 | "pubsub", 19 | "listener", 20 | "vanilla", 21 | "pub/sub", 22 | "publish/subscribe", 23 | "publish", 24 | "event", 25 | "emitter", 26 | "eventemitter", 27 | "subscribe" 28 | ], 29 | "devDependencies": { 30 | "qunit": "~0.7.7", 31 | "qunit-phantomjs-runner": "~2.1.0" 32 | }, 33 | "engines": {"node": ">= 0.6.x"}, 34 | "main": "pubsub.js", 35 | "scripts": { 36 | "test": "phantomjs node_modules/qunit-phantomjs-runner/runner.js test/pubsub.html" 37 | }, 38 | "license": "MIT" 39 | } 40 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | "define": true, 8 | "require": true, 9 | "jQuery": true, 10 | "$" : true, 11 | "module" : true, 12 | "pubsub" : true, 13 | "global" : true 14 | }, 15 | "extends": "eslint:recommended", 16 | "rules": { 17 | "strict": 1, 18 | "block-scoped-var": 2, 19 | "curly": [2,"all"], 20 | "dot-location": [2,"property"], 21 | "eqeqeq": [2,"smart"], 22 | "no-alert": 2, 23 | "no-console": 0, 24 | "no-caller": 2, 25 | "no-eval": 2, 26 | "no-extend-native": 1, 27 | "no-extra-bind": 2, 28 | "no-fallthrough": 1, 29 | "no-floating-decimal": 1, 30 | "no-implicit-coercion": [1,{"allow": ["!!", "+"]}], 31 | "no-implied-eval": 2, 32 | "no-labels": 2, 33 | "no-lone-blocks": 1, 34 | "no-loop-func": 2, 35 | "no-multi-spaces": [2,{ "exceptions": { "Property": true, "VariableDeclarator": true }}], 36 | "no-multi-str": 2, 37 | "no-native-reassign": 2, 38 | "no-new-func": 2, 39 | "no-new-wrappers": 2, 40 | "no-octal-escape": 2, 41 | "no-proto": 1, 42 | "no-return-assign": [2,"always"], 43 | "no-script-url": 2, 44 | "no-self-compare": 2, 45 | "no-sequences": 2, 46 | "no-throw-literal": 2, 47 | "no-unused-expressions": 2, 48 | "no-unused-vars": 1, 49 | "no-unsafe-negation": 2, 50 | "no-useless-call": 2, 51 | "no-void": 2, 52 | "no-warning-comments": 1, 53 | "no-with": 2, 54 | "radix": 2, 55 | "vars-on-top": 0, 56 | "wrap-iife": [2,"inside"], 57 | "yoda": 2, 58 | "no-shadow-restricted-names": 2, 59 | "no-shadow": 1, 60 | "no-undef-init": 2, 61 | "no-undefined": 2, 62 | "array-bracket-spacing": [2,"never",{}], 63 | "brace-style": [2,"1tbs",{}], 64 | "comma-spacing": 2, 65 | "comma-style": 2, 66 | "computed-property-spacing": 2, 67 | "consistent-this": [1,"self"], 68 | "func-style": [2,"declaration", { "allowArrowFunctions": true }], 69 | "indent": [2,"tab"], 70 | "linebreak-style": [2,"unix"], 71 | "new-parens": 2, 72 | "no-array-constructor": 2, 73 | "no-continue": 1, 74 | "no-lonely-if": 1, 75 | "no-nested-ternary": 2, 76 | "no-new-object": 2, 77 | "no-spaced-func": 2, 78 | "no-trailing-spaces": 2, 79 | "no-unneeded-ternary": 2, 80 | "object-curly-spacing": [2,"always",{}], 81 | "operator-linebreak": [2,"after"], 82 | "padded-blocks": [2,"never"], 83 | "quote-props": [1,"as-needed"], 84 | "semi-spacing": 2, 85 | "semi": [2,"always"], 86 | "keyword-spacing": [2, { "overrides": { "if": { "after": false }, "for": { "after": false }, "while": { "after": false }, "catch": { "after": false} } }], 87 | "space-before-blocks": [2,"always"], 88 | "space-before-function-paren": [2,"never"], 89 | "space-in-parens": [2,"never"], 90 | "space-infix-ops": 2, 91 | "space-unary-ops": [2,{"words":true}], 92 | "arrow-parens": [2,"always"], 93 | "arrow-spacing": [2,{"before":true,"after":true}], 94 | "no-const-assign": 2, 95 | "object-shorthand": [2,"never"], 96 | "no-bitwise": 2 97 | } 98 | } -------------------------------------------------------------------------------- /test/css/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0pre-7d65b6b7dea05b3cb1d1d9caebe3ccf5f162126d 2013-02-02 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /pubsub.js: -------------------------------------------------------------------------------- 1 | (function(scope) { 2 | 'use strict'; 3 | var pubsubInstance = null; 4 | var pubsubConfig = null; 5 | 6 | if(typeof pubsub === 'object') { 7 | pubsubConfig = pubsub; 8 | //node.js config from global 9 | } else if(typeof global === 'object' && typeof global.pubsubConfig === 'object') { 10 | pubsubConfig = global.pubsubConfig; 11 | } 12 | 13 | function Pubsub(config) { 14 | var _eventObject = {}; 15 | var options = { 16 | separator : (config && config.separator) ? config.separator : '/', 17 | recurrent : (config && typeof config.recurrent === 'boolean') ? config.recurrent : (false), 18 | depth : (config && typeof config.depth === 'number') ? config.depth : null, 19 | async : (config && typeof config.async === 'boolean') ? config.async : (false), 20 | context : (config && config.context) ? config.context : null, 21 | log : (config && config.log) ? config.log : (false) 22 | }; 23 | 24 | function forEach(dataArray, callback) { 25 | var i = 0, 26 | arrayLength = dataArray.length; 27 | 28 | for(i = 0; i < arrayLength; i++) { 29 | callback(i, dataArray[i]); 30 | } 31 | } 32 | 33 | function executeCallback(subscriptions, args, async) { 34 | async = (typeof async === 'boolean') ? async : options.async; 35 | if(!subscriptions.length) { 36 | return; 37 | } 38 | 39 | // clone array - callbacks can unsubscribe other subscriptions 40 | // reduces a lot performance but is safe 41 | var executedSubscriptions = subscriptions.slice(); 42 | 43 | forEach(executedSubscriptions, function(subscriptionId, subscription) { 44 | if(typeof subscription === 'object' && executedSubscriptions.hasOwnProperty(subscriptionId)) { 45 | if(async) { 46 | setTimeout(function() { 47 | subscription.callback.apply(subscription.context, args); 48 | }, 4); 49 | } else { 50 | subscription.callback.apply(subscription.context, args); 51 | } 52 | } 53 | }); 54 | } 55 | 56 | function executePublishWildcard(nsObject, args) { 57 | var nsElement; 58 | for(nsElement in nsObject) { 59 | if(nsElement[0] !== '_' && nsObject.hasOwnProperty(nsElement)) { 60 | executeCallback(nsObject[nsElement]._events, args); 61 | } 62 | } 63 | } 64 | 65 | function publish(nsObject, args, parts, params) { 66 | // work on copy - not on reference 67 | parts = parts.slice(); 68 | 69 | var iPart = parts.shift(); 70 | var depth = params.depth; 71 | var async = params.async; 72 | var partsLength = params.partsLength; 73 | var recurrent = params.recurrent; 74 | var partNumber = (partsLength - parts.length); 75 | 76 | // parts is empty 77 | if(!iPart) { 78 | executeCallback(nsObject._events, args, async); 79 | return; 80 | } 81 | // handle subscribe wildcard 82 | if(typeof nsObject['*'] !== 'undefined') { 83 | publish(nsObject['*'], args, parts, params); 84 | } 85 | 86 | // handle publish wildcard 87 | if(iPart === '*') { 88 | executePublishWildcard(nsObject, args, async); 89 | } 90 | 91 | // no namespace = leave publish 92 | if(typeof nsObject[iPart] === "undefined") { 93 | if(params.log) { 94 | console.warn('There is no ' + params.nsString + ' subscription'); 95 | } 96 | return; 97 | } 98 | 99 | nsObject = nsObject[iPart]; 100 | 101 | if(recurrent === true && typeof depth !== 'number') { //depth is not defined 102 | executeCallback(nsObject._events, args, async); 103 | if(parts.length === 0) { 104 | return; 105 | } 106 | } else if(recurrent === true && typeof depth === 'number' && partNumber >= (partsLength - depth)) { //if depth is defined 107 | executeCallback(nsObject._events, args, async); 108 | } 109 | 110 | publish(nsObject, args, parts, params); 111 | } 112 | 113 | function executeSubscribeWildcard(nsObject, args, params) { 114 | var parts = params.parts; 115 | var async = params.async; 116 | var nextPart = null; 117 | 118 | if(parts.length === 0) { 119 | executeCallback(nsObject._events, args, async); 120 | } else { 121 | nextPart = parts.shift(); 122 | 123 | if(nsObject[nextPart]) { 124 | executeSubscribeWildcard(nsObject[nextPart], args, { 125 | parts : parts, 126 | async : async, 127 | nsString : params.nsString 128 | }); 129 | } 130 | } 131 | } 132 | 133 | function subscribe(nsString, callback, params) { 134 | var parts = nsString.split(options.separator), 135 | nsObject, //Namespace object to which we attach event 136 | context = (params && typeof params.context !== 'undefined') ? params.context : options.context, 137 | eventObject = null, 138 | i = 0; 139 | 140 | if(!context) { 141 | context = callback; 142 | } 143 | 144 | //Iterating through _eventObject to find proper nsObject 145 | nsObject = _eventObject; 146 | for(i = 0; i < parts.length; i += 1) { 147 | if(typeof nsObject[parts[i]] === "undefined") { 148 | nsObject[parts[i]] = {}; 149 | nsObject[parts[i]]._events = []; 150 | } 151 | nsObject = nsObject[parts[i]]; 152 | } 153 | 154 | eventObject = { 155 | callback : callback, 156 | context : context // "this" parameter in executed function 157 | }; 158 | 159 | nsObject._events.push(eventObject); 160 | return { namespace : parts.join(options.separator), 161 | event : eventObject }; 162 | } 163 | 164 | function unsubscribe(subscribeObject) { 165 | if(subscribeObject === null || typeof subscribeObject === 'undefined') { 166 | return null; 167 | } 168 | var nsString = subscribeObject.namespace, 169 | eventObject = subscribeObject.event, 170 | parts = nsString.split(options.separator), 171 | nsObject, 172 | i = 0; 173 | 174 | //Iterating through _eventObject to find proper nsObject 175 | nsObject = _eventObject; 176 | for(i = 0; i < parts.length; i += 1) { 177 | if(typeof nsObject[parts[i]] === "undefined") { 178 | if(options.log) { 179 | console.error('There is no ' + nsString + ' subscription'); 180 | } 181 | return null; 182 | } 183 | nsObject = nsObject[parts[i]]; 184 | } 185 | 186 | forEach(nsObject._events, function(eventId) { 187 | if(nsObject._events[eventId] === eventObject) { 188 | nsObject._events.splice(eventId, 1); 189 | } 190 | }); 191 | } 192 | 193 | return { 194 | /** 195 | * Publish event 196 | * @param nsString string namespace string splited by dots 197 | * @param args array of arguments given to callbacks 198 | * @param params paramaters possible: 199 | * @param recurrent bool should execution be bubbled throught namespace 200 | * @param depth integer how many namespaces separated by dots will be executed 201 | */ 202 | publish : function(nsString, args, params) { 203 | var parts = nsString.split(options.separator), 204 | recurrent = (typeof params === 'object' && params.recurrent) ? params.recurrent : options.recurrent, // bubbles event throught namespace if true 205 | depth = (typeof params === 'object' && params.depth) ? params.depth : options.depth, 206 | async = (typeof params === 'object' && params.async) ? params.async : options.async, 207 | partsLength = parts.length; 208 | 209 | if(!parts.length) { 210 | if(options.log) { 211 | console.error('Wrong namespace provided ' + nsString); 212 | } 213 | return; 214 | } 215 | 216 | publish(_eventObject, args, parts, { 217 | recurrent : recurrent, 218 | depth : depth, 219 | async : async, 220 | parts : parts, 221 | nsString : nsString, 222 | partsLength : partsLength 223 | }); 224 | }, 225 | /** 226 | * Subscribe event 227 | * @param nsString string namespace string splited by dots 228 | * @param callback function function executed after publishing event 229 | * @param params given params 230 | * @param context object/nothing Optional object which will be used as "this" in callback 231 | */ 232 | subscribe : function(nsString, callback, params) { 233 | var self = this, 234 | subscriptions = []; 235 | 236 | // array of callbacks - multiple subscription 237 | if(typeof callback === 'object' && callback instanceof Array) { 238 | forEach(callback, function(number) { 239 | var oneCallback = callback[number]; 240 | 241 | subscriptions = subscriptions.concat(self.subscribe(nsString, oneCallback, params)); 242 | }); 243 | // array of namespaces - multiple subscription 244 | } else if(typeof nsString === 'object' && nsString instanceof Array) { 245 | forEach(nsString, function(number) { 246 | var namespace = nsString[number]; 247 | 248 | subscriptions = subscriptions.concat(self.subscribe(namespace, callback, params)); 249 | }); 250 | } else { 251 | return subscribe.apply(self, arguments); 252 | } 253 | return subscriptions; 254 | }, 255 | /** 256 | * subscribeOnce event - subscribe once to some event, then unsubscribe immadiately 257 | * @param nsString string namespace string splited by dots 258 | * @param callback function function executed after publishing event 259 | * @param params given params 260 | * @param context object/nothing Optional object which will be used as "this" in callback 261 | */ 262 | subscribeOnce : function(nsString, callback, params) { 263 | var self = this, 264 | subscription = null; 265 | 266 | function subscriptionCallback() { 267 | var context = this; 268 | 269 | callback.apply(context, arguments); 270 | self.unsubscribe(subscription); 271 | } 272 | 273 | subscription = self.subscribe(nsString, subscriptionCallback, params); 274 | return subscription; 275 | }, 276 | /** 277 | * Unsubscribe from given subscription 278 | * @param subscribeObject subscription object given on subscribe (returned from subscription) 279 | */ 280 | unsubscribe : function(subscribeObject) { 281 | var self = this; 282 | 283 | //if we have array of callbacks - multiple subscription 284 | if(subscribeObject instanceof Array) { 285 | forEach(subscribeObject, function(number) { 286 | var oneSubscribtion = subscribeObject[number]; 287 | 288 | unsubscribe.apply(self, [oneSubscribtion]); 289 | }); 290 | } else { 291 | unsubscribe.apply(self, arguments); 292 | } 293 | }, 294 | /** 295 | * newInstance - makes new instance of pubsub object with its own config 296 | * @param params instance configuration 297 | * @param separator separator (default is "/") 298 | * @param recurrent should publish events be bubbled through namespace 299 | * @param async should publish events be asynchronous - not blocking function execution 300 | * @param log console.warn/error every problem 301 | */ 302 | newInstance : function(params) { 303 | return new Pubsub(params); 304 | } 305 | }; //return block 306 | } 307 | pubsubInstance = new Pubsub(pubsubConfig); 308 | 309 | //if sbd's using requirejs library to load pubsub.js 310 | if(typeof define === 'function') { 311 | define(pubsubInstance); 312 | } 313 | 314 | //node.js 315 | if(typeof module === 'object' && module.exports) { 316 | module.exports = pubsubInstance; 317 | } 318 | 319 | if(typeof window === 'object') { 320 | window.pubsub = pubsubInstance; 321 | if(window !== scope) { 322 | scope.pubsub = pubsubInstance; 323 | } 324 | } 325 | })(this); 326 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pubsub.js 2 | ========= 3 | 4 | Dependency free JavaScript pubsub implementation with wildcards, inheritance and multisubscriptions. 5 | 6 | Working smoothly both on frontend and backend side. 7 | 8 | Read documentation, check tests file, be inspired and feel free to use it and/or contribute. 9 | 10 | [![Build Status](https://travis-ci.org/Sahadar/pubsub.js.svg?branch=master)](https://travis-ci.org/Sahadar/pubsub.js) 11 | [![Dependency Status](https://david-dm.org/Sahadar/pubsub.js.svg)](https://david-dm.org/Sahadar/pubsub.js) 12 | [![License](https://img.shields.io/npm/l/pubsub.js.svg)](http://opensource.org/licenses/MIT) 13 | 14 | [Web page](https://sahadar.github.io/pubsub/) 15 | 16 | [NPM pubsub.js link](https://npmjs.org/package/pubsub.js) 17 | 18 | ## Features 19 | 20 | * Currently it is the most advanced dependency-free pub/sub library on npm 21 | * Very fast 22 | * Easy to understand 23 | * Dependency free = using native JavaScript code 24 | * Configurable [link](#change_config) 25 | * Works on server and browser side smoothly [link](#change_config) 26 | * Event inheritance [link](#event_inheritance) 27 | * Wildcards [publish wildcard](#publish_wildcard) [subscribe wildcard](#subscribe_wildcard) 28 | * subscribeOnce method [link](#subscribeOnce) 29 | * Multiple subscriptions [link](#multiple_subscriptions) 30 | * Possibility to make new instances of pubsub with private namespaces scope [link](#new_instance) 31 | * Possibility to publish async events [link](#async_events) 32 | * Possibility to define context for all callbacks by providing pubsub|newInstance "context" param [link](#new_instance_context) 33 | * Possibility to define context for each callback [link](#subscribe_context) 34 | * Controll under event bubbling depth 35 | * Works with *require.js* library 36 | * Written with TDD 37 | * Compiled + gzipped weighs only 1kB, less than 320 lines of code 38 | * Works also on IE 6+ 39 | 40 | ## Installation 41 | * download from Github 42 | * npm: `npm install pubsub.js` 43 | * bower: `bower install pubsub-advanced` 44 | 45 | Default pubsub.js configuration: 46 | ```javascript 47 | separator : '/' // defined namespace separator 48 | context // has dynamic value - when not defined it will be always reference to used callback in subscribe method 49 | recurrent : false // defines inheritance of publish event 50 | async : false // if true - publish events will be asynchronous 51 | log : false // set to true will log unsubscribed namespaces to which You publish event 52 | ``` 53 | 54 | **Using pubsub inside node.js** 55 | ```javascript 56 | var pubsub = require('pubsub.js'); 57 | 58 | pubsub.subscribe('hello/world', function(text) { 59 | console.log(text); 60 | }); 61 | pubsub.publish('hello/world', ['my text']); 62 | ``` 63 | 64 | ## Examples 65 | 66 | ### Basic example 67 | 68 | ```javascript 69 | //subscribe to 'hello/world' namespace 70 | pubsub.subscribe('hello/world', function() { 71 | console.log('hello world!'); 72 | }); 73 | //publish event on 'hello/world' namespace 74 | pubsub.publish('hello/world'); 75 | //prints "hello world" inside console 76 | ``` 77 | 78 | ### Publish with param 79 | 80 | ```javascript 81 | //subscribe to 'hello/world' namespace 82 | pubsub.subscribe('hello/world', function(data) { 83 | console.log(data); 84 | }); 85 | //publish event on 'hello/world' namespace 86 | pubsub.publish('hello/world', ['hello!']); // second parameter is an array of arguments 87 | //prints "hello!" inside console 88 | ``` 89 | 90 | ### Unsubscribe 91 | 92 | ```javascript 93 | //subscribe to 'hello/world' namespace 94 | var subscription = pubsub.subscribe('hello/world', function() { 95 | console.log('hello world!'); 96 | }); 97 | //publish event on 'hello/world' namespace 98 | pubsub.publish('hello/world'); 99 | //prints "hello world" inside console 100 | 101 | //unsubscribe 102 | pubsub.unsubscribe(subscription); 103 | //publish event on 'hello/world' namespace 104 | pubsub.publish('hello/world'); 105 | //nothing happen - we've previously unsubscribed that subscription 106 | ``` 107 | 108 | ### Changing default configuration 109 | 110 | ***Browser*** 111 | 112 | **Before pubsub script loader** - make global variable named "pubsub" with your default configuration 113 | ```javascript 114 | pubsub = { 115 | separator : '.' 116 | context : referenceToContext 117 | } 118 | ``` 119 | **After pubsub load - use it with your configuration, pubsub.js will replace that previous "pubsub" global variable with its own instance** 120 | ```javascript 121 | //subscribe to 'hello.world' namespace 122 | var subscription = pubsub.subscribe('hello.world', function() { 123 | console.log('hello world!'); 124 | }); 125 | //publish event on 'hello.world' namespace 126 | pubsub.publish('hello.world'); 127 | //prints "hello world" inside console 128 | 129 | //unsubscribe 130 | pubsub.unsubscribe(subscription); 131 | //publish event on 'hello.world' namespace 132 | pubsub.publish('hello.world'); 133 | //nothing happen - we've previously unsubscribed that subscription 134 | ``` 135 | 136 | ***Node.js*** 137 | 138 | **Before pubsub require execution** - set global.pubsubConfig variable 139 | ```javascript 140 | global.pubsubConfig = { 141 | separator : '.' 142 | } 143 | ``` 144 | **After pubsub load, it'll have your configuration as in browser example** 145 | 146 | 147 | ### Event inheritance 148 | 149 | ```javascript 150 | //subscribe to 'hello' namespace 151 | var subscription = pubsub.subscribe('hello', function() { 152 | console.log('hello world!'); 153 | }); 154 | //publish event on 'hello/world' namespace 155 | pubsub.publish('hello/world', [], { 156 | recurrent : true 157 | }); 158 | //prints "hello world" inside console 159 | //first event goes to "hello" namespace 160 | //then it tries to execute on "hello/world" but nothing is listening on it 161 | ``` 162 | 163 | ### Method: subscribeOnce 164 | 165 | ```javascript 166 | var iterator = 0; 167 | var data = null; 168 | 169 | pubsub.subscribeOnce('hello/world', function(param) { 170 | data = param; 171 | iterator++; 172 | }); 173 | pubsub.publish('hello/world', ['hello']); 174 | pubsub.publish('hello/world', ['world']); 175 | console.log(iterator); //1 176 | console.log(data); //'hello' 177 | ``` 178 | 179 | ### Publish wildcard "*" 180 | 181 | ```javascript 182 | var number = 0; 183 | 184 | //subscribe to "hello/world" namespace 185 | pubsub.subscribe('hello/world', function() { 186 | number++; 187 | }); 188 | //subscribe to "hello/earth" namespace 189 | pubsub.subscribe('hello/earth', function() { 190 | number++; 191 | }); 192 | //subscribe to "hello/galaxy" namespace 193 | pubsub.subscribe('hello/galaxy', function() { 194 | number++; 195 | }); 196 | //subscribe to "hello/world/inner" namespace 197 | pubsub.subscribe('hello/world/inner', function() { 198 | number++; 199 | }); 200 | 201 | pubsub.publish('hello/*'); 202 | //hello/* executes: 203 | // hello/world, hello/earth, hello/galaxy 204 | // namespace, hello/world/inner is not executed 205 | // 206 | // "*" goes only one namespace deeper 207 | console.log(number); //3 208 | ``` 209 | 210 | ### Subscribe wildcard "*" 211 | 212 | ```javascript 213 | var number = 0; 214 | 215 | var subscription = pubsub.subscribe('hello/*/world', function() { 216 | number += 1; 217 | }); 218 | 219 | pubsub.publish('hello'); // won't handle 220 | pubsub.publish('hello/my'); // won't handle 221 | pubsub.publish('hello/great/galaxy'); // won't handle 222 | 223 | pubsub.publish('hello/my/world'); // handles 224 | pubsub.publish('hello/huge/world'); // handles 225 | pubsub.publish('hello/great/world'); // handles 226 | 227 | console.log(number); // 3 228 | ``` 229 | 230 | ### Multiple subscriptions 231 | 232 | **many namespaces, one callback** 233 | ```javascript 234 | var number = 0; 235 | 236 | var subscription = pubsub.subscribe(['hello/world', 'goodbye/world'], function() { 237 | number++; 238 | }); 239 | 240 | pubsub.publish('hello/world'); 241 | console.log(number); //1 242 | pubsub.publish('goodbye/world'); 243 | console.log(number); //2 244 | pubsub.unsubscribe(subscription); 245 | 246 | pubsub.publish('hello/world'); 247 | console.log(number); //2 248 | pubsub.publish('goodbye/world'); 249 | console.log(number); //2 250 | ``` 251 | 252 | **one namespace, many callbacks** 253 | ```javascript 254 | var number1 = 0; 255 | var number2 = 0; 256 | 257 | var subscription = pubsub.subscribe('hello/world', [function() { 258 | number1++; 259 | }, function() { 260 | number2 += 2; 261 | }]); 262 | 263 | pubsub.publish('hello/world'); 264 | console.log(number1 + ',' + number2); //1,2 265 | pubsub.unsubscribe(subscription); 266 | 267 | pubsub.publish('hello/world'); 268 | console.log(number1 + ',' + number2); //2,4 269 | ``` 270 | 271 | **many namespaces, many callbacks** 272 | ```javascript 273 | var number1 = 0; 274 | var number2 = 0; 275 | 276 | var subscription = pubsub.subscribe(['hello/world', 'goodbye/world'], [function() { 277 | number1++; 278 | }, function() { 279 | number2 += 2; 280 | }]); 281 | 282 | pubsub.publish('hello/world'); 283 | console.log(number1 + ',' + number2); //1,2 284 | pubsub.publish('goodbye/world'); 285 | console.log(number1 + ',' + number2); //2,4 286 | pubsub.unsubscribe(subscription); 287 | 288 | pubsub.publish('hello/world'); 289 | console.log(number1 + ',' + number2); //2,4 290 | pubsub.publish('goodbye/world'); 291 | console.log(number1 + ',' + number2); //2,4 292 | ``` 293 | 294 | ###making new instances with own namespaces scope 295 | ```javascript 296 | var number1 = 0; 297 | var number2 = 0; 298 | 299 | var privatePubsub = pubsub.newInstance(); 300 | 301 | pubsub.subscribe('hello/world', function() { 302 | number1++; 303 | }); 304 | 305 | privatePubsub.subscribe('hello/world', function() { 306 | number2++; 307 | }); 308 | 309 | pubsub.publish('hello/world'); 310 | console.log(number1 + ',' + number2); //1,0 311 | 312 | privatePubsub.publish('hello/world'); 313 | console.log(number1 + ',' + number2); //1,1 314 | ``` 315 | 316 | **making new instances with own context ** 317 | ```javascript 318 | var contextArgument = ["object"]; 319 | var privatePubsub = pubsub.newInstance({ 320 | context : contextArgument 321 | }); 322 | 323 | privatePubsub.subscribe('hello/context', function() { 324 | var that = this; 325 | 326 | console.log(that === contextArgument); //true 327 | }); 328 | privatePubsub.publish('hello/context'); 329 | ``` 330 | 331 | ###Using pubsub asynchronously 332 | ```javascript 333 | var number1 = 0; 334 | 335 | var asyncPubsub = pubsub.newInstance({ 336 | async : true 337 | }); 338 | 339 | asyncPubsub.subscribeOnce('hello/world', function() { 340 | number1++; 341 | console.log(number1); //2 342 | }); 343 | 344 | asyncPubsub.publish('hello/world'); // asynchronous call to 'hello/world' 345 | 346 | number1++; 347 | console.log(number1); //1 348 | ``` 349 | 350 | ###Using context param in subscribe method 351 | ```javascript 352 | var contextArgument = ["object"]; 353 | var privatePubsub = pubsub.newInstance(); 354 | 355 | function callbackFirst() { 356 | var that = this; 357 | 358 | console.log(that === callbackFirst); // true 359 | } 360 | function callbackSecond() { 361 | var that = this; 362 | 363 | console.log(that === contextArgument); // true 364 | } 365 | 366 | var privateSubscribtion1 = privatePubsub.subscribe('hello/context', callbackFirst); 367 | var privateSubscribtion2 = privatePubsub.subscribe('hello/that', callbackSecond, { 368 | context : contextArgument 369 | }); 370 | ``` 371 | 372 | ## Changelog 373 | * v1.5.1 374 | * Fix "context" option for subscribeOnce 375 | * .eslintrc style guide added + code stylistic fixes 376 | * v1.5.0 377 | * Fix issue #8 - "context" option, new test cases 378 | * v1.4.3 379 | * Fix issue #7 - "recurrent" option, new test cases 380 | * v1.4.2 381 | * Documentation changes 382 | * v1.4.1 383 | * Travis integration 384 | * v1.4.0 385 | * Added subscription wildcard "*" 386 | * v1.3.1 387 | * Fixed problem with "window reference error" in node.js environment 388 | * v1.3.0 389 | * Changed the way of using context parameter in subscribe method - API changes to subscribe method! 390 | * v1.2.0 391 | * Changed the way of using event inheritance - API changes to publish method! 392 | * Added possibility to use "publish" asynchronously 393 | * v1.1.0 394 | * reworked core 395 | * changed the way of setting own config 396 | * implemented "newInstance" method 397 | * v1.0.6 398 | * Fixed bug with unsubscription - subscription during publish of the same namespace (test case 3) 399 | * v1.0.5 400 | * Added multisubscription possibilities 401 | * v1.0.4 402 | * Added subscribeOnce method 403 | * v1.0.3 404 | * Changed scope binding in pubsub 405 | * v1.0.2 406 | * Publish wildcard "*" added 407 | * v1.0.1 408 | * Improved performance - about 350% on chrome, 20% on firefox 409 | * v1.0.0 410 | * Every basic test passing 411 | 412 | ## License 413 | 414 | MIT 415 | -------------------------------------------------------------------------------- /test/js/test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | test("Presence test", function() { 4 | ok(typeof pubsub === 'object' && pubsub !== null, "pubsub present"); 5 | ok(typeof pubsub.publish === 'function', "pubsub has method publish"); 6 | ok(typeof pubsub.subscribe === 'function', "pubsub has method subscribe"); 7 | ok(typeof pubsub.subscribeOnce === 'function', "pubsub has method subscribeOnce"); 8 | ok(typeof pubsub.unsubscribe === 'function', "pubsub has method unsubscribe"); 9 | }); 10 | 11 | test("Unsubscribe test (basic)", function() { 12 | var values = {}; 13 | var param1 = "some param1"; 14 | var param2 = "some param2"; 15 | 16 | var subscription = pubsub.subscribe('hello/world4', function(param1, param2) { 17 | values = { 18 | param1 : param1, 19 | param2 : param2 20 | }; 21 | }); 22 | pubsub.publish('hello/world4', [param1, param2]); 23 | ok(values.param1 === param1 && values.param2 === param2, 'Values has proper value'); 24 | pubsub.unsubscribe(subscription); 25 | pubsub.publish('hello/world4', [null, null]); 26 | ok(values.param1 === param1 && values.param2 === param2, 'Values has proper value'); 27 | }); 28 | 29 | test("Unsubscribe test (chained unsubscribe)", function() { 30 | var iterator = 0; 31 | 32 | var subscription1 = pubsub.subscribe('hello/world1', function() { 33 | iterator++; 34 | pubsub.unsubscribe(subscription1); 35 | }); 36 | var subscription2 = pubsub.subscribe('hello/world1', function() { 37 | iterator++; 38 | }); 39 | var subscription3 = pubsub.subscribe('hello/world2', function() { 40 | iterator++; 41 | pubsub.unsubscribe(subscription3); 42 | }); 43 | 44 | pubsub.publish('hello/world1'); 45 | ok(iterator === 2, 'Second subscription executed properly'); 46 | pubsub.unsubscribe(subscription2); 47 | }); 48 | 49 | test("Publish test (flat)", function() { 50 | var iterator = 0; 51 | var subscription = pubsub.subscribe('hello', function() { 52 | iterator += 1; 53 | }); 54 | pubsub.publish('hello'); 55 | pubsub.publish('world'); 56 | ok(iterator === 1, 'Done has proper value'); 57 | pubsub.unsubscribe(subscription); 58 | pubsub.publish('hello'); 59 | ok(iterator === 1, 'Done has proper value'); 60 | }); 61 | 62 | test("Publish test (basic)", function() { 63 | var done = false; 64 | var subscription = pubsub.subscribe('hello/world1', function() { 65 | done = true; 66 | }); 67 | pubsub.publish('hello/world1'); 68 | ok(done === true, 'Done has proper value'); 69 | pubsub.unsubscribe(subscription); 70 | }); 71 | 72 | test("Publish test (param)", function() { 73 | var done = false; 74 | var param = "some param"; 75 | var subscription = pubsub.subscribe('hello/world2', function(param) { 76 | done = param; 77 | }); 78 | pubsub.publish('hello/world2', [param]); 79 | ok(done === param, 'Done has proper value'); 80 | pubsub.unsubscribe(subscription); 81 | }); 82 | 83 | test("Method: subscribeOnce test (with param)", function() { 84 | var iterator = 0; 85 | var done = null; 86 | 87 | ok(typeof pubsub.subscribeOnce === 'function', 'Pubsub has method subscribeOnce'); 88 | pubsub.subscribeOnce('hello/world', function(param) { 89 | done = param; 90 | iterator++; 91 | }); 92 | pubsub.publish('hello/world', ['hello']); 93 | pubsub.publish('hello/world', ['world']); 94 | ok(done === 'hello', 'Done has proper value'); 95 | ok(iterator === 1, 'Subscribtion executed only once'); 96 | }); 97 | 98 | test("Publish test (many params)", function() { 99 | var values = {}; 100 | var param1 = "some param1"; 101 | var param2 = "some param2"; 102 | 103 | var subscription = pubsub.subscribe('hello/world3', function(param1, param2) { 104 | values = { 105 | param1 : param1, 106 | param2 : param2 107 | }; 108 | }); 109 | pubsub.publish('hello/world3', [param1, param2]); 110 | ok(values.param1 === param1 && values.param2 === param2, 'Values has proper values'); 111 | pubsub.unsubscribe(subscription); 112 | }); 113 | 114 | test("Inheritance test (basic)", function() { 115 | var values = {}; 116 | var param1 = "some param1"; 117 | var param2 = "some param2"; 118 | 119 | var subscription = pubsub.subscribe('hello', function(param1, param2) { 120 | values = { 121 | param1 : param1, 122 | param2 : param2 123 | }; 124 | }); 125 | pubsub.publish('hello/world5', [param1, param2], { 126 | recurrent : true 127 | }); 128 | ok(values.param1 === param1 && values.param2 === param2, 'Values has proper value'); 129 | pubsub.unsubscribe(subscription); 130 | }); 131 | 132 | test("Inheritance test (Issue #7)", function() { 133 | var instance = pubsub.newInstance({ 134 | recurrent: true 135 | }); 136 | var counter = 0; 137 | 138 | var subscription = instance.subscribe('hello', function() { 139 | counter += 1; 140 | }); 141 | 142 | instance.publish('hello', []); 143 | 144 | equal(counter, 1, 'Counter executed once'); 145 | instance.unsubscribe(subscription); 146 | }); 147 | 148 | test("Inheritance test2 (Issue #7)", function() { 149 | var instance = pubsub.newInstance({ 150 | recurrent: true 151 | }); 152 | var counter = 0; 153 | 154 | var subscription = instance.subscribe('hello/world', function() { 155 | counter += 1; 156 | }); 157 | 158 | instance.publish('hello/world'); 159 | 160 | equal(counter, 1, 'Counter executed once'); 161 | instance.unsubscribe(subscription); 162 | }); 163 | 164 | test("Inheritance test (Depth)", function() { 165 | var values = {}; 166 | var param1 = "some param1"; 167 | var param2 = "some param2"; 168 | var param3 = "some param3"; 169 | 170 | var instance = pubsub.newInstance({ 171 | recurrent : true, 172 | depth : 2 173 | }); 174 | 175 | var subscription1 = instance.subscribe('hello/world', function(param1, param2) { 176 | values.param1 = param1; 177 | }); 178 | var subscription2 = instance.subscribe('hello/world/one', function(param1, param2) { 179 | values.param2 = param2; 180 | }); 181 | var subscriptionShouldNotExecute = instance.subscribe('hello', function(param1, param2, param3) { 182 | values.param3 = param3; 183 | }); 184 | 185 | instance.publish('hello/world/one/two', [param1, param2, param3]); 186 | 187 | deepEqual(values, { param1 : param1, param2 : param2 }, 'Values has proper value'); 188 | instance.unsubscribe(subscription1); 189 | instance.unsubscribe(subscription2); 190 | instance.unsubscribe(subscriptionShouldNotExecute); 191 | }); 192 | 193 | test("Publish wildcard test (*)", function() { 194 | var number = 0; 195 | 196 | var subscription1 = pubsub.subscribe('hello', function() { 197 | number++; 198 | }); 199 | var subscription2 = pubsub.subscribe('earth', function() { 200 | number++; 201 | }); 202 | var subscription3 = pubsub.subscribe('galaxy', function() { 203 | number++; 204 | }); 205 | var subscription4 = pubsub.subscribe('hello/world', function() { 206 | number++; 207 | }); 208 | 209 | pubsub.publish('*'); 210 | 211 | ok(number === 3, 'Wildcard (*) is working properly'); 212 | pubsub.unsubscribe(subscription1); 213 | pubsub.unsubscribe(subscription2); 214 | pubsub.unsubscribe(subscription3); 215 | pubsub.unsubscribe(subscription4); 216 | }); 217 | 218 | test("Publish wildcard test (hello/*)", function() { 219 | var number = 0; 220 | 221 | var subscription1 = pubsub.subscribe('hello/world', function() { 222 | number++; 223 | }); 224 | var subscription2 = pubsub.subscribe('hello/earth', function() { 225 | number++; 226 | }); 227 | var subscription3 = pubsub.subscribe('hello/galaxy', function() { 228 | number++; 229 | }); 230 | var subscription4 = pubsub.subscribe('hello/world/inner', function() { 231 | number++; 232 | }); 233 | 234 | pubsub.publish('hello/*'); 235 | 236 | ok(number === 3, 'Wildcard (*) is working properly'); 237 | pubsub.unsubscribe(subscription1); 238 | pubsub.unsubscribe(subscription2); 239 | pubsub.unsubscribe(subscription3); 240 | pubsub.unsubscribe(subscription4); 241 | }); 242 | 243 | test("Multiple subscription1 (one namespace, many callbacks)", function() { 244 | var number = 0; 245 | 246 | var subscription = pubsub.subscribe('hello/world', [ 247 | function() { 248 | number++; 249 | }, 250 | function() { 251 | number++; 252 | }, 253 | function() { 254 | number++; 255 | } 256 | ]); 257 | 258 | pubsub.publish('hello/world'); 259 | ok(number === 3, 'Multiple subscription before unsubscribe is working properly'); 260 | pubsub.unsubscribe(subscription); 261 | 262 | pubsub.publish('hello/world'); 263 | ok(number === 3, 'Multiple subscription after unsubscribe is working properly'); 264 | }); 265 | 266 | 267 | test("Multiple subscription2 (many namespaces, one callback)", function() { 268 | var number = 0; 269 | 270 | var subscription = pubsub.subscribe(['hello/world', 'goodbye/world'], function() { 271 | number++; 272 | }); 273 | 274 | pubsub.publish('hello/world'); 275 | ok(number === 1, 'Subscribtion to hello/world before unsubscribe is working properly'); 276 | pubsub.publish('goodbye/world'); 277 | ok(number === 2, 'Subscribtion to goodbye/world before unsubscribe is working properly'); 278 | pubsub.unsubscribe(subscription); 279 | 280 | pubsub.publish('hello/world'); 281 | ok(number === 2, 'Subscribtion to hello/world after unsubscribe is working properly'); 282 | pubsub.publish('goodbye/world'); 283 | ok(number === 2, 'Subscribtion to goodbye/world after unsubscribe is working properly'); 284 | }); 285 | 286 | test("Multiple subscription3 (many namespaces, many callbacks)", function() { 287 | var number1 = 0; 288 | var number2 = 0; 289 | 290 | var subscription = pubsub.subscribe(['hello/world', 'goodbye/world'], [function() { 291 | number1++; 292 | }, function() { 293 | number2 += 2; 294 | }]); 295 | 296 | pubsub.publish('hello/world'); 297 | ok(number1 === 1, 'Subscribtion to hello/world before unsubscribe is working properly (number1)'); 298 | ok(number2 === 2, 'Subscribtion to hello/world before unsubscribe is working properly (number2)'); 299 | pubsub.publish('goodbye/world'); 300 | ok(number1 === 2, 'Subscribtion to goodbye/world before unsubscribe is working properly (number1)'); 301 | ok(number2 === 4, 'Subscribtion to goodbye/world before unsubscribe is working properly (number2)'); 302 | pubsub.unsubscribe(subscription); 303 | 304 | pubsub.publish('hello/world'); 305 | ok(number1 === 2, 'Subscribtion to hello/world after unsubscribe is working properly (number1)'); 306 | ok(number2 === 4, 'Subscribtion to hello/world after unsubscribe is working properly (number2)'); 307 | pubsub.publish('goodbye/world'); 308 | ok(number1 === 2, 'Subscribtion to goodbye/world after unsubscribe is working properly (number1)'); 309 | ok(number2 === 4, 'Subscribtion to goodbye/world after unsubscribe is working properly (number2)'); 310 | }); 311 | 312 | test("Pubsub newInstance with own namespaces scope", function() { 313 | ok(typeof pubsub.newInstance === 'function', "pubsub has method newInstance"); 314 | var number1 = 0; 315 | var number2 = 0; 316 | 317 | var privatePubsub = pubsub.newInstance(); 318 | 319 | var subscription = pubsub.subscribe('hello/world', function() { 320 | number1++; 321 | }); 322 | var privateSubscribtion = privatePubsub.subscribe('hello/world', function() { 323 | number2++; 324 | }); 325 | pubsub.publish('hello/world'); 326 | ok(number1 === 1 && number2 === 0, "Global pubsub publish worked properly"); 327 | privatePubsub.publish('hello/world'); 328 | ok(number1 === 1 && number2 === 1, "Private pubsub publish worked properly"); 329 | privatePubsub.unsubscribe(privateSubscribtion); 330 | privatePubsub.publish('hello/world'); 331 | ok(number1 === 1 && number2 === 1, "Private unsubscribe worked properly"); 332 | pubsub.unsubscribe(subscription); 333 | pubsub.publish('hello/world'); 334 | ok(number1 === 1 && number2 === 1, "Public unsubscribe worked properly"); 335 | }); 336 | 337 | test("Switching config", function() { 338 | ok(typeof pubsub.newInstance === 'function', "pubsub has method newInstance"); 339 | var number1 = 0; 340 | var number2 = 0; 341 | 342 | var privatePubsub = pubsub.newInstance({ 343 | separator : '.' 344 | }); 345 | 346 | var subscription = pubsub.subscribe('hello/world', function() { 347 | number1++; 348 | }); 349 | var privateSubscribtion = privatePubsub.subscribe('hello.world', function() { 350 | number2++; 351 | }); 352 | pubsub.publish('hello/world'); 353 | ok(number1 === 1 && number2 === 0, "Global pubsub publish worked properly"); 354 | privatePubsub.publish('hello.world'); 355 | ok(number1 === 1 && number2 === 1, "Private pubsub publish worked properly"); 356 | privatePubsub.unsubscribe(privateSubscribtion); 357 | privatePubsub.publish('hello.world'); 358 | ok(number1 === 1 && number2 === 1, "Private unsubscribe worked properly"); 359 | pubsub.unsubscribe(subscription); 360 | pubsub.publish('hello/world'); 361 | ok(number1 === 1 && number2 === 1, "Public unsubscribe worked properly"); 362 | }); 363 | 364 | test("Subscription wildcard test (*)", function() { 365 | var number = 0; 366 | 367 | var subscription = pubsub.subscribe('*', function() { 368 | number += 1; 369 | }); 370 | 371 | pubsub.publish('hello'); 372 | pubsub.publish('world'); 373 | pubsub.publish('lord'); 374 | pubsub.publish('globe'); 375 | 376 | ok(number === 4, 'Subscription wildcard is working properly'); 377 | pubsub.unsubscribe(subscription); 378 | 379 | pubsub.publish('hello'); 380 | pubsub.publish('hello/world'); 381 | ok(number === 4, 'Unsubscribe test'); 382 | }); 383 | 384 | test("Subscription wildcard test (hello/*)", function() { 385 | var number = 0; 386 | 387 | var subscription = pubsub.subscribe('hello/*', function() { 388 | number += 1; 389 | }); 390 | 391 | pubsub.publish('hello/world'); 392 | pubsub.publish('hello/globe'); 393 | pubsub.publish('hello/galaxy'); 394 | 395 | ok(number === 3, 'Subscription wildcard is working properly'); 396 | pubsub.unsubscribe(subscription); 397 | 398 | pubsub.publish('hello/world'); 399 | ok(number === 3, 'Subscription wildcard is working properly'); 400 | }); 401 | 402 | test("Subscription wildcard test (hello/*/world)", function() { 403 | var number = 0; 404 | 405 | var subscription = pubsub.subscribe('hello/*/world', function() { 406 | number += 1; 407 | }); 408 | 409 | pubsub.publish('hello'); 410 | pubsub.publish('hello/my'); 411 | pubsub.publish('hello/my/world'); 412 | pubsub.publish('hello/huge/world'); 413 | pubsub.publish('hello/great/world'); 414 | 415 | ok(number === 3, 'Subscription wildcard is working properly'); 416 | 417 | pubsub.publish('hello/great/galaxy'); 418 | ok(number === 3, 'Subscription wildcard is working properly'); 419 | pubsub.unsubscribe(subscription); 420 | 421 | pubsub.publish('hello/great/world'); 422 | ok(number === 3, 'Subscription wildcard is working properly'); 423 | }); 424 | 425 | test("Subscription wildcard test (hello/*/*/world)", function() { 426 | var number = 0; 427 | 428 | var subscription = pubsub.subscribe('hello/*/*/world', function() { 429 | number += 1; 430 | }); 431 | 432 | pubsub.publish('hello/my/green/world'); 433 | pubsub.publish('hello/huge/yellow/world'); 434 | pubsub.publish('hello/great/blue/world'); 435 | 436 | ok(number === 3, 'Subscription wildcard is working properly'); 437 | 438 | pubsub.publish('hello/great/black/galaxy'); 439 | ok(number === 3, 'Subscription wildcard is working properly'); 440 | pubsub.unsubscribe(subscription); 441 | 442 | pubsub.publish('hello/great/green/world'); 443 | ok(number === 3, 'Subscription wildcard is working properly'); 444 | }); 445 | 446 | asyncTest("Async pubsub test (differences)", function() { 447 | ok(typeof pubsub.newInstance === 'function', "pubsub has method newInstance"); 448 | var number1 = 0; 449 | 450 | var asyncPubsub = pubsub.newInstance({ 451 | async : true 452 | }); 453 | 454 | asyncPubsub.subscribeOnce('hello/world', function() { 455 | number1++; 456 | 457 | ok(number1 === 2, "Async pubsub publish worked properly"); 458 | start(); 459 | }); 460 | 461 | asyncPubsub.publish('hello/world', []); 462 | number1++; 463 | ok(number1 === 1, "Async pubsub publish worked properly"); 464 | }); 465 | 466 | asyncTest("Sync pubsub test (differences)", function() { 467 | ok(typeof pubsub.newInstance === 'function', "pubsub has method newInstance"); 468 | var number1 = 0; 469 | 470 | var syncPubsub = pubsub.newInstance(); 471 | 472 | syncPubsub.subscribeOnce('hello/world', function() { 473 | number1++; 474 | 475 | ok(number1 === 1, "Sync pubsub publish worked properly"); 476 | start(); 477 | }); 478 | 479 | syncPubsub.publish('hello/world', []); 480 | number1++; 481 | ok(number1 === 2, "Sync pubsub publish worked properly"); 482 | }); 483 | 484 | test("Pubsub testing context argument in options", function() { 485 | var contextArgument = ["object"]; 486 | var privatePubsub = pubsub.newInstance({ 487 | context : contextArgument 488 | }); 489 | var onceReturn = 0; 490 | 491 | privatePubsub.subscribe('hello/context', function() { 492 | var self = this; 493 | 494 | ok(self === contextArgument, "Context argument in options work correctly for every subscription"); 495 | }); 496 | privatePubsub.subscribe('hello/self', function() { 497 | var self = this; 498 | 499 | ok(self === contextArgument, "Context argument in options work correctly for every subscription"); 500 | }); 501 | privatePubsub.subscribeOnce('hello/once', function() { 502 | var self = this; 503 | 504 | onceReturn++; 505 | ok(self === contextArgument, "Context argument in options work correctly for every subscription"); 506 | }); 507 | 508 | privatePubsub.publish('hello/context'); 509 | privatePubsub.publish('hello/self'); 510 | privatePubsub.publish('hello/once'); 511 | privatePubsub.publish('hello/once'); 512 | equal(onceReturn, 1, "subscribeOnce callback published only once"); 513 | }); 514 | 515 | test("Pubsub testing override of context argument in options", function() { 516 | var contextArgument = ["object"]; 517 | var contextArgument2 = ["object2"]; 518 | var privatePubsub = pubsub.newInstance({ 519 | context : contextArgument 520 | }); 521 | var onceReturn = 0; 522 | 523 | privatePubsub.subscribe('hello/context', function() { 524 | var self = this; 525 | 526 | ok(self === contextArgument, "Context argument in options work correctly for every subscription"); 527 | }); 528 | privatePubsub.subscribe('hello/self', function() { 529 | var self = this; 530 | 531 | ok(self === contextArgument2, "Context argument in private subscription options work correctly"); 532 | }, { 533 | context : contextArgument2 534 | }); 535 | privatePubsub.subscribeOnce('hello/once', function() { 536 | var self = this; 537 | 538 | onceReturn++; 539 | ok(self === contextArgument2, "Context argument in private subscription options work correctly"); 540 | }, { 541 | context : contextArgument2 542 | }); 543 | 544 | privatePubsub.publish('hello/context'); 545 | privatePubsub.publish('hello/self'); 546 | privatePubsub.publish('hello/once'); 547 | privatePubsub.publish('hello/once'); 548 | ok(onceReturn === 1, "subscribeOnce callback published only once"); 549 | }); 550 | 551 | test("Pubsub testing use of private context for one subscribe", function() { 552 | var contextArgument = ["object"]; 553 | var privatePubsub = pubsub.newInstance(); 554 | 555 | function callbackFirst() { 556 | var self = this; 557 | 558 | ok(self === callbackFirst, "No context definition = context is same function"); 559 | } 560 | function callbackSecond() { 561 | var self = this; 562 | 563 | ok(self === contextArgument, "Context argument in subscribe config works correctly"); 564 | } 565 | 566 | privatePubsub.subscribe('hello/context', callbackFirst); 567 | privatePubsub.subscribe('hello/self', callbackSecond, { 568 | context : contextArgument 569 | }); 570 | 571 | privatePubsub.publish('hello/context'); 572 | privatePubsub.publish('hello/self'); 573 | }); 574 | })(); -------------------------------------------------------------------------------- /test/js/qunit.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.17.1 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2015-01-20T19:39Z 10 | */ 11 | 12 | (function( window ) { 13 | 14 | var QUnit, 15 | config, 16 | onErrorFnPrev, 17 | loggingCallbacks = {}, 18 | fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ), 19 | toString = Object.prototype.toString, 20 | hasOwn = Object.prototype.hasOwnProperty, 21 | // Keep a local reference to Date (GH-283) 22 | Date = window.Date, 23 | now = Date.now || function() { 24 | return new Date().getTime(); 25 | }, 26 | globalStartCalled = false, 27 | runStarted = false, 28 | setTimeout = window.setTimeout, 29 | clearTimeout = window.clearTimeout, 30 | defined = { 31 | document: window.document !== undefined, 32 | setTimeout: window.setTimeout !== undefined, 33 | sessionStorage: (function() { 34 | var x = "qunit-test-string"; 35 | try { 36 | sessionStorage.setItem( x, x ); 37 | sessionStorage.removeItem( x ); 38 | return true; 39 | } catch ( e ) { 40 | return false; 41 | } 42 | }()) 43 | }, 44 | /** 45 | * Provides a normalized error string, correcting an issue 46 | * with IE 7 (and prior) where Error.prototype.toString is 47 | * not properly implemented 48 | * 49 | * Based on http://es5.github.com/#x15.11.4.4 50 | * 51 | * @param {String|Error} error 52 | * @return {String} error message 53 | */ 54 | errorString = function( error ) { 55 | var name, message, 56 | errorString = error.toString(); 57 | if ( errorString.substring( 0, 7 ) === "[object" ) { 58 | name = error.name ? error.name.toString() : "Error"; 59 | message = error.message ? error.message.toString() : ""; 60 | if ( name && message ) { 61 | return name + ": " + message; 62 | } else if ( name ) { 63 | return name; 64 | } else if ( message ) { 65 | return message; 66 | } else { 67 | return "Error"; 68 | } 69 | } else { 70 | return errorString; 71 | } 72 | }, 73 | /** 74 | * Makes a clone of an object using only Array or Object as base, 75 | * and copies over the own enumerable properties. 76 | * 77 | * @param {Object} obj 78 | * @return {Object} New object with only the own properties (recursively). 79 | */ 80 | objectValues = function( obj ) { 81 | var key, val, 82 | vals = QUnit.is( "array", obj ) ? [] : {}; 83 | for ( key in obj ) { 84 | if ( hasOwn.call( obj, key ) ) { 85 | val = obj[ key ]; 86 | vals[ key ] = val === Object( val ) ? objectValues( val ) : val; 87 | } 88 | } 89 | return vals; 90 | }; 91 | 92 | QUnit = {}; 93 | 94 | /** 95 | * Config object: Maintain internal state 96 | * Later exposed as QUnit.config 97 | * `config` initialized at top of scope 98 | */ 99 | config = { 100 | // The queue of tests to run 101 | queue: [], 102 | 103 | // block until document ready 104 | blocking: true, 105 | 106 | // by default, run previously failed tests first 107 | // very useful in combination with "Hide passed tests" checked 108 | reorder: true, 109 | 110 | // by default, modify document.title when suite is done 111 | altertitle: true, 112 | 113 | // by default, scroll to top of the page when suite is done 114 | scrolltop: true, 115 | 116 | // when enabled, all tests must call expect() 117 | requireExpects: false, 118 | 119 | // add checkboxes that are persisted in the query-string 120 | // when enabled, the id is set to `true` as a `QUnit.config` property 121 | urlConfig: [ 122 | { 123 | id: "hidepassed", 124 | label: "Hide passed tests", 125 | tooltip: "Only show tests and assertions that fail. Stored as query-strings." 126 | }, 127 | { 128 | id: "noglobals", 129 | label: "Check for Globals", 130 | tooltip: "Enabling this will test if any test introduces new properties on the " + 131 | "`window` object. Stored as query-strings." 132 | }, 133 | { 134 | id: "notrycatch", 135 | label: "No try-catch", 136 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + 137 | "exceptions in IE reasonable. Stored as query-strings." 138 | } 139 | ], 140 | 141 | // Set of all modules. 142 | modules: [], 143 | 144 | // The first unnamed module 145 | currentModule: { 146 | name: "", 147 | tests: [] 148 | }, 149 | 150 | callbacks: {} 151 | }; 152 | 153 | // Push a loose unnamed module to the modules collection 154 | config.modules.push( config.currentModule ); 155 | 156 | // Initialize more QUnit.config and QUnit.urlParams 157 | (function() { 158 | var i, current, 159 | location = window.location || { search: "", protocol: "file:" }, 160 | params = location.search.slice( 1 ).split( "&" ), 161 | length = params.length, 162 | urlParams = {}; 163 | 164 | if ( params[ 0 ] ) { 165 | for ( i = 0; i < length; i++ ) { 166 | current = params[ i ].split( "=" ); 167 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 168 | 169 | // allow just a key to turn on a flag, e.g., test.html?noglobals 170 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 171 | if ( urlParams[ current[ 0 ] ] ) { 172 | urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); 173 | } else { 174 | urlParams[ current[ 0 ] ] = current[ 1 ]; 175 | } 176 | } 177 | } 178 | 179 | if ( urlParams.filter === true ) { 180 | delete urlParams.filter; 181 | } 182 | 183 | QUnit.urlParams = urlParams; 184 | 185 | // String search anywhere in moduleName+testName 186 | config.filter = urlParams.filter; 187 | 188 | config.testId = []; 189 | if ( urlParams.testId ) { 190 | 191 | // Ensure that urlParams.testId is an array 192 | urlParams.testId = [].concat( urlParams.testId ); 193 | for ( i = 0; i < urlParams.testId.length; i++ ) { 194 | config.testId.push( urlParams.testId[ i ] ); 195 | } 196 | } 197 | 198 | // Figure out if we're running the tests from a server or not 199 | QUnit.isLocal = location.protocol === "file:"; 200 | }()); 201 | 202 | // Root QUnit object. 203 | // `QUnit` initialized at top of scope 204 | extend( QUnit, { 205 | 206 | // call on start of module test to prepend name to all tests 207 | module: function( name, testEnvironment ) { 208 | var currentModule = { 209 | name: name, 210 | testEnvironment: testEnvironment, 211 | tests: [] 212 | }; 213 | 214 | // DEPRECATED: handles setup/teardown functions, 215 | // beforeEach and afterEach should be used instead 216 | if ( testEnvironment && testEnvironment.setup ) { 217 | testEnvironment.beforeEach = testEnvironment.setup; 218 | delete testEnvironment.setup; 219 | } 220 | if ( testEnvironment && testEnvironment.teardown ) { 221 | testEnvironment.afterEach = testEnvironment.teardown; 222 | delete testEnvironment.teardown; 223 | } 224 | 225 | config.modules.push( currentModule ); 226 | config.currentModule = currentModule; 227 | }, 228 | 229 | // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. 230 | asyncTest: function( testName, expected, callback ) { 231 | if ( arguments.length === 2 ) { 232 | callback = expected; 233 | expected = null; 234 | } 235 | 236 | QUnit.test( testName, expected, callback, true ); 237 | }, 238 | 239 | test: function( testName, expected, callback, async ) { 240 | var test; 241 | 242 | if ( arguments.length === 2 ) { 243 | callback = expected; 244 | expected = null; 245 | } 246 | 247 | test = new Test({ 248 | testName: testName, 249 | expected: expected, 250 | async: async, 251 | callback: callback 252 | }); 253 | 254 | test.queue(); 255 | }, 256 | 257 | skip: function( testName ) { 258 | var test = new Test({ 259 | testName: testName, 260 | skip: true 261 | }); 262 | 263 | test.queue(); 264 | }, 265 | 266 | // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. 267 | // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. 268 | start: function( count ) { 269 | var globalStartAlreadyCalled = globalStartCalled; 270 | 271 | if ( !config.current ) { 272 | globalStartCalled = true; 273 | 274 | if ( runStarted ) { 275 | throw new Error( "Called start() outside of a test context while already started" ); 276 | } else if ( globalStartAlreadyCalled || count > 1 ) { 277 | throw new Error( "Called start() outside of a test context too many times" ); 278 | } else if ( config.autostart ) { 279 | throw new Error( "Called start() outside of a test context when " + 280 | "QUnit.config.autostart was true" ); 281 | } else if ( !config.pageLoaded ) { 282 | 283 | // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it 284 | config.autostart = true; 285 | return; 286 | } 287 | } else { 288 | 289 | // If a test is running, adjust its semaphore 290 | config.current.semaphore -= count || 1; 291 | 292 | // Don't start until equal number of stop-calls 293 | if ( config.current.semaphore > 0 ) { 294 | return; 295 | } 296 | 297 | // throw an Error if start is called more often than stop 298 | if ( config.current.semaphore < 0 ) { 299 | config.current.semaphore = 0; 300 | 301 | QUnit.pushFailure( 302 | "Called start() while already started (test's semaphore was 0 already)", 303 | sourceFromStacktrace( 2 ) 304 | ); 305 | return; 306 | } 307 | } 308 | 309 | resumeProcessing(); 310 | }, 311 | 312 | // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0. 313 | stop: function( count ) { 314 | 315 | // If there isn't a test running, don't allow QUnit.stop() to be called 316 | if ( !config.current ) { 317 | throw new Error( "Called stop() outside of a test context" ); 318 | } 319 | 320 | // If a test is running, adjust its semaphore 321 | config.current.semaphore += count || 1; 322 | 323 | pauseProcessing(); 324 | }, 325 | 326 | config: config, 327 | 328 | // Safe object type checking 329 | is: function( type, obj ) { 330 | return QUnit.objectType( obj ) === type; 331 | }, 332 | 333 | objectType: function( obj ) { 334 | if ( typeof obj === "undefined" ) { 335 | return "undefined"; 336 | } 337 | 338 | // Consider: typeof null === object 339 | if ( obj === null ) { 340 | return "null"; 341 | } 342 | 343 | var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), 344 | type = match && match[ 1 ] || ""; 345 | 346 | switch ( type ) { 347 | case "Number": 348 | if ( isNaN( obj ) ) { 349 | return "nan"; 350 | } 351 | return "number"; 352 | case "String": 353 | case "Boolean": 354 | case "Array": 355 | case "Date": 356 | case "RegExp": 357 | case "Function": 358 | return type.toLowerCase(); 359 | } 360 | if ( typeof obj === "object" ) { 361 | return "object"; 362 | } 363 | return undefined; 364 | }, 365 | 366 | extend: extend, 367 | 368 | load: function() { 369 | config.pageLoaded = true; 370 | 371 | // Initialize the configuration options 372 | extend( config, { 373 | stats: { all: 0, bad: 0 }, 374 | moduleStats: { all: 0, bad: 0 }, 375 | started: 0, 376 | updateRate: 1000, 377 | autostart: true, 378 | filter: "" 379 | }, true ); 380 | 381 | config.blocking = false; 382 | 383 | if ( config.autostart ) { 384 | resumeProcessing(); 385 | } 386 | } 387 | }); 388 | 389 | // Register logging callbacks 390 | (function() { 391 | var i, l, key, 392 | callbacks = [ "begin", "done", "log", "testStart", "testDone", 393 | "moduleStart", "moduleDone" ]; 394 | 395 | function registerLoggingCallback( key ) { 396 | var loggingCallback = function( callback ) { 397 | if ( QUnit.objectType( callback ) !== "function" ) { 398 | throw new Error( 399 | "QUnit logging methods require a callback function as their first parameters." 400 | ); 401 | } 402 | 403 | config.callbacks[ key ].push( callback ); 404 | }; 405 | 406 | // DEPRECATED: This will be removed on QUnit 2.0.0+ 407 | // Stores the registered functions allowing restoring 408 | // at verifyLoggingCallbacks() if modified 409 | loggingCallbacks[ key ] = loggingCallback; 410 | 411 | return loggingCallback; 412 | } 413 | 414 | for ( i = 0, l = callbacks.length; i < l; i++ ) { 415 | key = callbacks[ i ]; 416 | 417 | // Initialize key collection of logging callback 418 | if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { 419 | config.callbacks[ key ] = []; 420 | } 421 | 422 | QUnit[ key ] = registerLoggingCallback( key ); 423 | } 424 | })(); 425 | 426 | // `onErrorFnPrev` initialized at top of scope 427 | // Preserve other handlers 428 | onErrorFnPrev = window.onerror; 429 | 430 | // Cover uncaught exceptions 431 | // Returning true will suppress the default browser handler, 432 | // returning false will let it run. 433 | window.onerror = function( error, filePath, linerNr ) { 434 | var ret = false; 435 | if ( onErrorFnPrev ) { 436 | ret = onErrorFnPrev( error, filePath, linerNr ); 437 | } 438 | 439 | // Treat return value as window.onerror itself does, 440 | // Only do our handling if not suppressed. 441 | if ( ret !== true ) { 442 | if ( QUnit.config.current ) { 443 | if ( QUnit.config.current.ignoreGlobalErrors ) { 444 | return true; 445 | } 446 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 447 | } else { 448 | QUnit.test( "global failure", extend(function() { 449 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 450 | }, { validTest: true } ) ); 451 | } 452 | return false; 453 | } 454 | 455 | return ret; 456 | }; 457 | 458 | function done() { 459 | var runtime, passed; 460 | 461 | config.autorun = true; 462 | 463 | // Log the last module results 464 | if ( config.previousModule ) { 465 | runLoggingCallbacks( "moduleDone", { 466 | name: config.previousModule.name, 467 | tests: config.previousModule.tests, 468 | failed: config.moduleStats.bad, 469 | passed: config.moduleStats.all - config.moduleStats.bad, 470 | total: config.moduleStats.all, 471 | runtime: now() - config.moduleStats.started 472 | }); 473 | } 474 | delete config.previousModule; 475 | 476 | runtime = now() - config.started; 477 | passed = config.stats.all - config.stats.bad; 478 | 479 | runLoggingCallbacks( "done", { 480 | failed: config.stats.bad, 481 | passed: passed, 482 | total: config.stats.all, 483 | runtime: runtime 484 | }); 485 | } 486 | 487 | // Doesn't support IE6 to IE9 488 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 489 | function extractStacktrace( e, offset ) { 490 | offset = offset === undefined ? 4 : offset; 491 | 492 | var stack, include, i; 493 | 494 | if ( e.stacktrace ) { 495 | 496 | // Opera 12.x 497 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 498 | } else if ( e.stack ) { 499 | 500 | // Firefox, Chrome, Safari 6+, IE10+, PhantomJS and Node 501 | stack = e.stack.split( "\n" ); 502 | if ( /^error$/i.test( stack[ 0 ] ) ) { 503 | stack.shift(); 504 | } 505 | if ( fileName ) { 506 | include = []; 507 | for ( i = offset; i < stack.length; i++ ) { 508 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 509 | break; 510 | } 511 | include.push( stack[ i ] ); 512 | } 513 | if ( include.length ) { 514 | return include.join( "\n" ); 515 | } 516 | } 517 | return stack[ offset ]; 518 | } else if ( e.sourceURL ) { 519 | 520 | // Safari < 6 521 | // exclude useless self-reference for generated Error objects 522 | if ( /qunit.js$/.test( e.sourceURL ) ) { 523 | return; 524 | } 525 | 526 | // for actual exceptions, this is useful 527 | return e.sourceURL + ":" + e.line; 528 | } 529 | } 530 | 531 | function sourceFromStacktrace( offset ) { 532 | var e = new Error(); 533 | if ( !e.stack ) { 534 | try { 535 | throw e; 536 | } catch ( err ) { 537 | // This should already be true in most browsers 538 | e = err; 539 | } 540 | } 541 | return extractStacktrace( e, offset ); 542 | } 543 | 544 | function synchronize( callback, last ) { 545 | if ( QUnit.objectType( callback ) === "array" ) { 546 | while ( callback.length ) { 547 | synchronize( callback.shift() ); 548 | } 549 | return; 550 | } 551 | config.queue.push( callback ); 552 | 553 | if ( config.autorun && !config.blocking ) { 554 | process( last ); 555 | } 556 | } 557 | 558 | function process( last ) { 559 | function next() { 560 | process( last ); 561 | } 562 | var start = now(); 563 | config.depth = ( config.depth || 0 ) + 1; 564 | 565 | while ( config.queue.length && !config.blocking ) { 566 | if ( !defined.setTimeout || config.updateRate <= 0 || 567 | ( ( now() - start ) < config.updateRate ) ) { 568 | if ( config.current ) { 569 | 570 | // Reset async tracking for each phase of the Test lifecycle 571 | config.current.usedAsync = false; 572 | } 573 | config.queue.shift()(); 574 | } else { 575 | setTimeout( next, 13 ); 576 | break; 577 | } 578 | } 579 | config.depth--; 580 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 581 | done(); 582 | } 583 | } 584 | 585 | function begin() { 586 | var i, l, 587 | modulesLog = []; 588 | 589 | // If the test run hasn't officially begun yet 590 | if ( !config.started ) { 591 | 592 | // Record the time of the test run's beginning 593 | config.started = now(); 594 | 595 | verifyLoggingCallbacks(); 596 | 597 | // Delete the loose unnamed module if unused. 598 | if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { 599 | config.modules.shift(); 600 | } 601 | 602 | // Avoid unnecessary information by not logging modules' test environments 603 | for ( i = 0, l = config.modules.length; i < l; i++ ) { 604 | modulesLog.push({ 605 | name: config.modules[ i ].name, 606 | tests: config.modules[ i ].tests 607 | }); 608 | } 609 | 610 | // The test run is officially beginning now 611 | runLoggingCallbacks( "begin", { 612 | totalTests: Test.count, 613 | modules: modulesLog 614 | }); 615 | } 616 | 617 | config.blocking = false; 618 | process( true ); 619 | } 620 | 621 | function resumeProcessing() { 622 | runStarted = true; 623 | 624 | // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) 625 | if ( defined.setTimeout ) { 626 | setTimeout(function() { 627 | if ( config.current && config.current.semaphore > 0 ) { 628 | return; 629 | } 630 | if ( config.timeout ) { 631 | clearTimeout( config.timeout ); 632 | } 633 | 634 | begin(); 635 | }, 13 ); 636 | } else { 637 | begin(); 638 | } 639 | } 640 | 641 | function pauseProcessing() { 642 | config.blocking = true; 643 | 644 | if ( config.testTimeout && defined.setTimeout ) { 645 | clearTimeout( config.timeout ); 646 | config.timeout = setTimeout(function() { 647 | if ( config.current ) { 648 | config.current.semaphore = 0; 649 | QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); 650 | } else { 651 | throw new Error( "Test timed out" ); 652 | } 653 | resumeProcessing(); 654 | }, config.testTimeout ); 655 | } 656 | } 657 | 658 | function saveGlobal() { 659 | config.pollution = []; 660 | 661 | if ( config.noglobals ) { 662 | for ( var key in window ) { 663 | if ( hasOwn.call( window, key ) ) { 664 | // in Opera sometimes DOM element ids show up here, ignore them 665 | if ( /^qunit-test-output/.test( key ) ) { 666 | continue; 667 | } 668 | config.pollution.push( key ); 669 | } 670 | } 671 | } 672 | } 673 | 674 | function checkPollution() { 675 | var newGlobals, 676 | deletedGlobals, 677 | old = config.pollution; 678 | 679 | saveGlobal(); 680 | 681 | newGlobals = diff( config.pollution, old ); 682 | if ( newGlobals.length > 0 ) { 683 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); 684 | } 685 | 686 | deletedGlobals = diff( old, config.pollution ); 687 | if ( deletedGlobals.length > 0 ) { 688 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); 689 | } 690 | } 691 | 692 | // returns a new Array with the elements that are in a but not in b 693 | function diff( a, b ) { 694 | var i, j, 695 | result = a.slice(); 696 | 697 | for ( i = 0; i < result.length; i++ ) { 698 | for ( j = 0; j < b.length; j++ ) { 699 | if ( result[ i ] === b[ j ] ) { 700 | result.splice( i, 1 ); 701 | i--; 702 | break; 703 | } 704 | } 705 | } 706 | return result; 707 | } 708 | 709 | function extend( a, b, undefOnly ) { 710 | for ( var prop in b ) { 711 | if ( hasOwn.call( b, prop ) ) { 712 | 713 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 714 | if ( !( prop === "constructor" && a === window ) ) { 715 | if ( b[ prop ] === undefined ) { 716 | delete a[ prop ]; 717 | } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { 718 | a[ prop ] = b[ prop ]; 719 | } 720 | } 721 | } 722 | } 723 | 724 | return a; 725 | } 726 | 727 | function runLoggingCallbacks( key, args ) { 728 | var i, l, callbacks; 729 | 730 | callbacks = config.callbacks[ key ]; 731 | for ( i = 0, l = callbacks.length; i < l; i++ ) { 732 | callbacks[ i ]( args ); 733 | } 734 | } 735 | 736 | // DEPRECATED: This will be removed on 2.0.0+ 737 | // This function verifies if the loggingCallbacks were modified by the user 738 | // If so, it will restore it, assign the given callback and print a console warning 739 | function verifyLoggingCallbacks() { 740 | var loggingCallback, userCallback; 741 | 742 | for ( loggingCallback in loggingCallbacks ) { 743 | if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { 744 | 745 | userCallback = QUnit[ loggingCallback ]; 746 | 747 | // Restore the callback function 748 | QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; 749 | 750 | // Assign the deprecated given callback 751 | QUnit[ loggingCallback ]( userCallback ); 752 | 753 | if ( window.console && window.console.warn ) { 754 | window.console.warn( 755 | "QUnit." + loggingCallback + " was replaced with a new value.\n" + 756 | "Please, check out the documentation on how to apply logging callbacks.\n" + 757 | "Reference: http://api.qunitjs.com/category/callbacks/" 758 | ); 759 | } 760 | } 761 | } 762 | } 763 | 764 | // from jquery.js 765 | function inArray( elem, array ) { 766 | if ( array.indexOf ) { 767 | return array.indexOf( elem ); 768 | } 769 | 770 | for ( var i = 0, length = array.length; i < length; i++ ) { 771 | if ( array[ i ] === elem ) { 772 | return i; 773 | } 774 | } 775 | 776 | return -1; 777 | } 778 | 779 | function Test( settings ) { 780 | var i, l; 781 | 782 | ++Test.count; 783 | 784 | extend( this, settings ); 785 | this.assertions = []; 786 | this.semaphore = 0; 787 | this.usedAsync = false; 788 | this.module = config.currentModule; 789 | this.stack = sourceFromStacktrace( 3 ); 790 | 791 | // Register unique strings 792 | for ( i = 0, l = this.module.tests; i < l.length; i++ ) { 793 | if ( this.module.tests[ i ].name === this.testName ) { 794 | this.testName += " "; 795 | } 796 | } 797 | 798 | this.testId = generateHash( this.module.name, this.testName ); 799 | 800 | this.module.tests.push({ 801 | name: this.testName, 802 | testId: this.testId 803 | }); 804 | 805 | if ( settings.skip ) { 806 | 807 | // Skipped tests will fully ignore any sent callback 808 | this.callback = function() {}; 809 | this.async = false; 810 | this.expected = 0; 811 | } else { 812 | this.assert = new Assert( this ); 813 | } 814 | } 815 | 816 | Test.count = 0; 817 | 818 | Test.prototype = { 819 | before: function() { 820 | if ( 821 | 822 | // Emit moduleStart when we're switching from one module to another 823 | this.module !== config.previousModule || 824 | 825 | // They could be equal (both undefined) but if the previousModule property doesn't 826 | // yet exist it means this is the first test in a suite that isn't wrapped in a 827 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 828 | // Without this, reporters can get testStart before moduleStart which is a problem. 829 | !hasOwn.call( config, "previousModule" ) 830 | ) { 831 | if ( hasOwn.call( config, "previousModule" ) ) { 832 | runLoggingCallbacks( "moduleDone", { 833 | name: config.previousModule.name, 834 | tests: config.previousModule.tests, 835 | failed: config.moduleStats.bad, 836 | passed: config.moduleStats.all - config.moduleStats.bad, 837 | total: config.moduleStats.all, 838 | runtime: now() - config.moduleStats.started 839 | }); 840 | } 841 | config.previousModule = this.module; 842 | config.moduleStats = { all: 0, bad: 0, started: now() }; 843 | runLoggingCallbacks( "moduleStart", { 844 | name: this.module.name, 845 | tests: this.module.tests 846 | }); 847 | } 848 | 849 | config.current = this; 850 | 851 | this.testEnvironment = extend( {}, this.module.testEnvironment ); 852 | delete this.testEnvironment.beforeEach; 853 | delete this.testEnvironment.afterEach; 854 | 855 | this.started = now(); 856 | runLoggingCallbacks( "testStart", { 857 | name: this.testName, 858 | module: this.module.name, 859 | testId: this.testId 860 | }); 861 | 862 | if ( !config.pollution ) { 863 | saveGlobal(); 864 | } 865 | }, 866 | 867 | run: function() { 868 | var promise; 869 | 870 | config.current = this; 871 | 872 | if ( this.async ) { 873 | QUnit.stop(); 874 | } 875 | 876 | this.callbackStarted = now(); 877 | 878 | if ( config.notrycatch ) { 879 | promise = this.callback.call( this.testEnvironment, this.assert ); 880 | this.resolvePromise( promise ); 881 | return; 882 | } 883 | 884 | try { 885 | promise = this.callback.call( this.testEnvironment, this.assert ); 886 | this.resolvePromise( promise ); 887 | } catch ( e ) { 888 | this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + 889 | this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 890 | 891 | // else next test will carry the responsibility 892 | saveGlobal(); 893 | 894 | // Restart the tests if they're blocking 895 | if ( config.blocking ) { 896 | QUnit.start(); 897 | } 898 | } 899 | }, 900 | 901 | after: function() { 902 | checkPollution(); 903 | }, 904 | 905 | queueHook: function( hook, hookName ) { 906 | var promise, 907 | test = this; 908 | return function runHook() { 909 | config.current = test; 910 | if ( config.notrycatch ) { 911 | promise = hook.call( test.testEnvironment, test.assert ); 912 | test.resolvePromise( promise, hookName ); 913 | return; 914 | } 915 | try { 916 | promise = hook.call( test.testEnvironment, test.assert ); 917 | test.resolvePromise( promise, hookName ); 918 | } catch ( error ) { 919 | test.pushFailure( hookName + " failed on " + test.testName + ": " + 920 | ( error.message || error ), extractStacktrace( error, 0 ) ); 921 | } 922 | }; 923 | }, 924 | 925 | // Currently only used for module level hooks, can be used to add global level ones 926 | hooks: function( handler ) { 927 | var hooks = []; 928 | 929 | // Hooks are ignored on skipped tests 930 | if ( this.skip ) { 931 | return hooks; 932 | } 933 | 934 | if ( this.module.testEnvironment && 935 | QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) { 936 | hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) ); 937 | } 938 | 939 | return hooks; 940 | }, 941 | 942 | finish: function() { 943 | config.current = this; 944 | if ( config.requireExpects && this.expected === null ) { 945 | this.pushFailure( "Expected number of assertions to be defined, but expect() was " + 946 | "not called.", this.stack ); 947 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 948 | this.pushFailure( "Expected " + this.expected + " assertions, but " + 949 | this.assertions.length + " were run", this.stack ); 950 | } else if ( this.expected === null && !this.assertions.length ) { 951 | this.pushFailure( "Expected at least one assertion, but none were run - call " + 952 | "expect(0) to accept zero assertions.", this.stack ); 953 | } 954 | 955 | var i, 956 | bad = 0; 957 | 958 | this.runtime = now() - this.started; 959 | config.stats.all += this.assertions.length; 960 | config.moduleStats.all += this.assertions.length; 961 | 962 | for ( i = 0; i < this.assertions.length; i++ ) { 963 | if ( !this.assertions[ i ].result ) { 964 | bad++; 965 | config.stats.bad++; 966 | config.moduleStats.bad++; 967 | } 968 | } 969 | 970 | runLoggingCallbacks( "testDone", { 971 | name: this.testName, 972 | module: this.module.name, 973 | skipped: !!this.skip, 974 | failed: bad, 975 | passed: this.assertions.length - bad, 976 | total: this.assertions.length, 977 | runtime: this.runtime, 978 | 979 | // HTML Reporter use 980 | assertions: this.assertions, 981 | testId: this.testId, 982 | 983 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead 984 | duration: this.runtime 985 | }); 986 | 987 | // QUnit.reset() is deprecated and will be replaced for a new 988 | // fixture reset function on QUnit 2.0/2.1. 989 | // It's still called here for backwards compatibility handling 990 | QUnit.reset(); 991 | 992 | config.current = undefined; 993 | }, 994 | 995 | queue: function() { 996 | var bad, 997 | test = this; 998 | 999 | if ( !this.valid() ) { 1000 | return; 1001 | } 1002 | 1003 | function run() { 1004 | 1005 | // each of these can by async 1006 | synchronize([ 1007 | function() { 1008 | test.before(); 1009 | }, 1010 | 1011 | test.hooks( "beforeEach" ), 1012 | 1013 | function() { 1014 | test.run(); 1015 | }, 1016 | 1017 | test.hooks( "afterEach" ).reverse(), 1018 | 1019 | function() { 1020 | test.after(); 1021 | }, 1022 | function() { 1023 | test.finish(); 1024 | } 1025 | ]); 1026 | } 1027 | 1028 | // `bad` initialized at top of scope 1029 | // defer when previous test run passed, if storage is available 1030 | bad = QUnit.config.reorder && defined.sessionStorage && 1031 | +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); 1032 | 1033 | if ( bad ) { 1034 | run(); 1035 | } else { 1036 | synchronize( run, true ); 1037 | } 1038 | }, 1039 | 1040 | push: function( result, actual, expected, message ) { 1041 | var source, 1042 | details = { 1043 | module: this.module.name, 1044 | name: this.testName, 1045 | result: result, 1046 | message: message, 1047 | actual: actual, 1048 | expected: expected, 1049 | testId: this.testId, 1050 | runtime: now() - this.started 1051 | }; 1052 | 1053 | if ( !result ) { 1054 | source = sourceFromStacktrace(); 1055 | 1056 | if ( source ) { 1057 | details.source = source; 1058 | } 1059 | } 1060 | 1061 | runLoggingCallbacks( "log", details ); 1062 | 1063 | this.assertions.push({ 1064 | result: !!result, 1065 | message: message 1066 | }); 1067 | }, 1068 | 1069 | pushFailure: function( message, source, actual ) { 1070 | if ( !this instanceof Test ) { 1071 | throw new Error( "pushFailure() assertion outside test context, was " + 1072 | sourceFromStacktrace( 2 ) ); 1073 | } 1074 | 1075 | var details = { 1076 | module: this.module.name, 1077 | name: this.testName, 1078 | result: false, 1079 | message: message || "error", 1080 | actual: actual || null, 1081 | testId: this.testId, 1082 | runtime: now() - this.started 1083 | }; 1084 | 1085 | if ( source ) { 1086 | details.source = source; 1087 | } 1088 | 1089 | runLoggingCallbacks( "log", details ); 1090 | 1091 | this.assertions.push({ 1092 | result: false, 1093 | message: message 1094 | }); 1095 | }, 1096 | 1097 | resolvePromise: function( promise, phase ) { 1098 | var then, message, 1099 | test = this; 1100 | if ( promise != null ) { 1101 | then = promise.then; 1102 | if ( QUnit.objectType( then ) === "function" ) { 1103 | QUnit.stop(); 1104 | then.call( 1105 | promise, 1106 | QUnit.start, 1107 | function( error ) { 1108 | message = "Promise rejected " + 1109 | ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + 1110 | " " + test.testName + ": " + ( error.message || error ); 1111 | test.pushFailure( message, extractStacktrace( error, 0 ) ); 1112 | 1113 | // else next test will carry the responsibility 1114 | saveGlobal(); 1115 | 1116 | // Unblock 1117 | QUnit.start(); 1118 | } 1119 | ); 1120 | } 1121 | } 1122 | }, 1123 | 1124 | valid: function() { 1125 | var include, 1126 | filter = config.filter, 1127 | module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), 1128 | fullName = ( this.module.name + ": " + this.testName ).toLowerCase(); 1129 | 1130 | // Internally-generated tests are always valid 1131 | if ( this.callback && this.callback.validTest ) { 1132 | return true; 1133 | } 1134 | 1135 | if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) { 1136 | return false; 1137 | } 1138 | 1139 | if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) { 1140 | return false; 1141 | } 1142 | 1143 | if ( !filter ) { 1144 | return true; 1145 | } 1146 | 1147 | include = filter.charAt( 0 ) !== "!"; 1148 | if ( !include ) { 1149 | filter = filter.toLowerCase().slice( 1 ); 1150 | } 1151 | 1152 | // If the filter matches, we need to honour include 1153 | if ( fullName.indexOf( filter ) !== -1 ) { 1154 | return include; 1155 | } 1156 | 1157 | // Otherwise, do the opposite 1158 | return !include; 1159 | } 1160 | 1161 | }; 1162 | 1163 | // Resets the test setup. Useful for tests that modify the DOM. 1164 | /* 1165 | DEPRECATED: Use multiple tests instead of resetting inside a test. 1166 | Use testStart or testDone for custom cleanup. 1167 | This method will throw an error in 2.0, and will be removed in 2.1 1168 | */ 1169 | QUnit.reset = function() { 1170 | 1171 | // Return on non-browser environments 1172 | // This is necessary to not break on node tests 1173 | if ( typeof window === "undefined" ) { 1174 | return; 1175 | } 1176 | 1177 | var fixture = defined.document && document.getElementById && 1178 | document.getElementById( "qunit-fixture" ); 1179 | 1180 | if ( fixture ) { 1181 | fixture.innerHTML = config.fixture; 1182 | } 1183 | }; 1184 | 1185 | QUnit.pushFailure = function() { 1186 | if ( !QUnit.config.current ) { 1187 | throw new Error( "pushFailure() assertion outside test context, in " + 1188 | sourceFromStacktrace( 2 ) ); 1189 | } 1190 | 1191 | // Gets current test obj 1192 | var currentTest = QUnit.config.current; 1193 | 1194 | return currentTest.pushFailure.apply( currentTest, arguments ); 1195 | }; 1196 | 1197 | // Based on Java's String.hashCode, a simple but not 1198 | // rigorously collision resistant hashing function 1199 | function generateHash( module, testName ) { 1200 | var hex, 1201 | i = 0, 1202 | hash = 0, 1203 | str = module + "\x1C" + testName, 1204 | len = str.length; 1205 | 1206 | for ( ; i < len; i++ ) { 1207 | hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); 1208 | hash |= 0; 1209 | } 1210 | 1211 | // Convert the possibly negative integer hash code into an 8 character hex string, which isn't 1212 | // strictly necessary but increases user understanding that the id is a SHA-like hash 1213 | hex = ( 0x100000000 + hash ).toString( 16 ); 1214 | if ( hex.length < 8 ) { 1215 | hex = "0000000" + hex; 1216 | } 1217 | 1218 | return hex.slice( -8 ); 1219 | } 1220 | 1221 | function Assert( testContext ) { 1222 | this.test = testContext; 1223 | } 1224 | 1225 | // Assert helpers 1226 | QUnit.assert = Assert.prototype = { 1227 | 1228 | // Specify the number of expected assertions to guarantee that failed test 1229 | // (no assertions are run at all) don't slip through. 1230 | expect: function( asserts ) { 1231 | if ( arguments.length === 1 ) { 1232 | this.test.expected = asserts; 1233 | } else { 1234 | return this.test.expected; 1235 | } 1236 | }, 1237 | 1238 | // Increment this Test's semaphore counter, then return a single-use function that 1239 | // decrements that counter a maximum of once. 1240 | async: function() { 1241 | var test = this.test, 1242 | popped = false; 1243 | 1244 | test.semaphore += 1; 1245 | test.usedAsync = true; 1246 | pauseProcessing(); 1247 | 1248 | return function done() { 1249 | if ( !popped ) { 1250 | test.semaphore -= 1; 1251 | popped = true; 1252 | resumeProcessing(); 1253 | } else { 1254 | test.pushFailure( "Called the callback returned from `assert.async` more than once", 1255 | sourceFromStacktrace( 2 ) ); 1256 | } 1257 | }; 1258 | }, 1259 | 1260 | // Exports test.push() to the user API 1261 | push: function( /* result, actual, expected, message */ ) { 1262 | var assert = this, 1263 | currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; 1264 | 1265 | // Backwards compatibility fix. 1266 | // Allows the direct use of global exported assertions and QUnit.assert.* 1267 | // Although, it's use is not recommended as it can leak assertions 1268 | // to other tests from async tests, because we only get a reference to the current test, 1269 | // not exactly the test where assertion were intended to be called. 1270 | if ( !currentTest ) { 1271 | throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 1272 | } 1273 | 1274 | if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { 1275 | currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", 1276 | sourceFromStacktrace( 2 ) ); 1277 | 1278 | // Allow this assertion to continue running anyway... 1279 | } 1280 | 1281 | if ( !( assert instanceof Assert ) ) { 1282 | assert = currentTest.assert; 1283 | } 1284 | return assert.test.push.apply( assert.test, arguments ); 1285 | }, 1286 | 1287 | /** 1288 | * Asserts rough true-ish result. 1289 | * @name ok 1290 | * @function 1291 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 1292 | */ 1293 | ok: function( result, message ) { 1294 | message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + 1295 | QUnit.dump.parse( result ) ); 1296 | this.push( !!result, result, true, message ); 1297 | }, 1298 | 1299 | /** 1300 | * Assert that the first two arguments are equal, with an optional message. 1301 | * Prints out both actual and expected values. 1302 | * @name equal 1303 | * @function 1304 | * @example equal( format( "{0} bytes.", 2), "2 bytes.", "replaces {0} with next argument" ); 1305 | */ 1306 | equal: function( actual, expected, message ) { 1307 | /*jshint eqeqeq:false */ 1308 | this.push( expected == actual, actual, expected, message ); 1309 | }, 1310 | 1311 | /** 1312 | * @name notEqual 1313 | * @function 1314 | */ 1315 | notEqual: function( actual, expected, message ) { 1316 | /*jshint eqeqeq:false */ 1317 | this.push( expected != actual, actual, expected, message ); 1318 | }, 1319 | 1320 | /** 1321 | * @name propEqual 1322 | * @function 1323 | */ 1324 | propEqual: function( actual, expected, message ) { 1325 | actual = objectValues( actual ); 1326 | expected = objectValues( expected ); 1327 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1328 | }, 1329 | 1330 | /** 1331 | * @name notPropEqual 1332 | * @function 1333 | */ 1334 | notPropEqual: function( actual, expected, message ) { 1335 | actual = objectValues( actual ); 1336 | expected = objectValues( expected ); 1337 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); 1338 | }, 1339 | 1340 | /** 1341 | * @name deepEqual 1342 | * @function 1343 | */ 1344 | deepEqual: function( actual, expected, message ) { 1345 | this.push( QUnit.equiv( actual, expected ), actual, expected, message ); 1346 | }, 1347 | 1348 | /** 1349 | * @name notDeepEqual 1350 | * @function 1351 | */ 1352 | notDeepEqual: function( actual, expected, message ) { 1353 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); 1354 | }, 1355 | 1356 | /** 1357 | * @name strictEqual 1358 | * @function 1359 | */ 1360 | strictEqual: function( actual, expected, message ) { 1361 | this.push( expected === actual, actual, expected, message ); 1362 | }, 1363 | 1364 | /** 1365 | * @name notStrictEqual 1366 | * @function 1367 | */ 1368 | notStrictEqual: function( actual, expected, message ) { 1369 | this.push( expected !== actual, actual, expected, message ); 1370 | }, 1371 | 1372 | "throws": function( block, expected, message ) { 1373 | var actual, expectedType, 1374 | expectedOutput = expected, 1375 | ok = false; 1376 | 1377 | // 'expected' is optional unless doing string comparison 1378 | if ( message == null && typeof expected === "string" ) { 1379 | message = expected; 1380 | expected = null; 1381 | } 1382 | 1383 | this.test.ignoreGlobalErrors = true; 1384 | try { 1385 | block.call( this.test.testEnvironment ); 1386 | } catch (e) { 1387 | actual = e; 1388 | } 1389 | this.test.ignoreGlobalErrors = false; 1390 | 1391 | if ( actual ) { 1392 | expectedType = QUnit.objectType( expected ); 1393 | 1394 | // we don't want to validate thrown error 1395 | if ( !expected ) { 1396 | ok = true; 1397 | expectedOutput = null; 1398 | 1399 | // expected is a regexp 1400 | } else if ( expectedType === "regexp" ) { 1401 | ok = expected.test( errorString( actual ) ); 1402 | 1403 | // expected is a string 1404 | } else if ( expectedType === "string" ) { 1405 | ok = expected === errorString( actual ); 1406 | 1407 | // expected is a constructor, maybe an Error constructor 1408 | } else if ( expectedType === "function" && actual instanceof expected ) { 1409 | ok = true; 1410 | 1411 | // expected is an Error object 1412 | } else if ( expectedType === "object" ) { 1413 | ok = actual instanceof expected.constructor && 1414 | actual.name === expected.name && 1415 | actual.message === expected.message; 1416 | 1417 | // expected is a validation function which returns true if validation passed 1418 | } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { 1419 | expectedOutput = null; 1420 | ok = true; 1421 | } 1422 | 1423 | this.push( ok, actual, expectedOutput, message ); 1424 | } else { 1425 | this.test.pushFailure( message, null, "No exception was thrown." ); 1426 | } 1427 | } 1428 | }; 1429 | 1430 | // Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word 1431 | // Known to us are: Closure Compiler, Narwhal 1432 | (function() { 1433 | /*jshint sub:true */ 1434 | Assert.prototype.raises = Assert.prototype[ "throws" ]; 1435 | }()); 1436 | 1437 | // Test for equality any JavaScript type. 1438 | // Author: Philippe Rathé 1439 | QUnit.equiv = (function() { 1440 | 1441 | // Call the o related callback with the given arguments. 1442 | function bindCallbacks( o, callbacks, args ) { 1443 | var prop = QUnit.objectType( o ); 1444 | if ( prop ) { 1445 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1446 | return callbacks[ prop ].apply( callbacks, args ); 1447 | } else { 1448 | return callbacks[ prop ]; // or undefined 1449 | } 1450 | } 1451 | } 1452 | 1453 | // the real equiv function 1454 | var innerEquiv, 1455 | 1456 | // stack to decide between skip/abort functions 1457 | callers = [], 1458 | 1459 | // stack to avoiding loops from circular referencing 1460 | parents = [], 1461 | parentsB = [], 1462 | 1463 | getProto = Object.getPrototypeOf || function( obj ) { 1464 | /* jshint camelcase: false, proto: true */ 1465 | return obj.__proto__; 1466 | }, 1467 | callbacks = (function() { 1468 | 1469 | // for string, boolean, number and null 1470 | function useStrictEquality( b, a ) { 1471 | 1472 | /*jshint eqeqeq:false */ 1473 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1474 | 1475 | // to catch short annotation VS 'new' annotation of a 1476 | // declaration 1477 | // e.g. var i = 1; 1478 | // var j = new Number(1); 1479 | return a == b; 1480 | } else { 1481 | return a === b; 1482 | } 1483 | } 1484 | 1485 | return { 1486 | "string": useStrictEquality, 1487 | "boolean": useStrictEquality, 1488 | "number": useStrictEquality, 1489 | "null": useStrictEquality, 1490 | "undefined": useStrictEquality, 1491 | 1492 | "nan": function( b ) { 1493 | return isNaN( b ); 1494 | }, 1495 | 1496 | "date": function( b, a ) { 1497 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1498 | }, 1499 | 1500 | "regexp": function( b, a ) { 1501 | return QUnit.objectType( b ) === "regexp" && 1502 | 1503 | // the regex itself 1504 | a.source === b.source && 1505 | 1506 | // and its modifiers 1507 | a.global === b.global && 1508 | 1509 | // (gmi) ... 1510 | a.ignoreCase === b.ignoreCase && 1511 | a.multiline === b.multiline && 1512 | a.sticky === b.sticky; 1513 | }, 1514 | 1515 | // - skip when the property is a method of an instance (OOP) 1516 | // - abort otherwise, 1517 | // initial === would have catch identical references anyway 1518 | "function": function() { 1519 | var caller = callers[ callers.length - 1 ]; 1520 | return caller !== Object && typeof caller !== "undefined"; 1521 | }, 1522 | 1523 | "array": function( b, a ) { 1524 | var i, j, len, loop, aCircular, bCircular; 1525 | 1526 | // b could be an object literal here 1527 | if ( QUnit.objectType( b ) !== "array" ) { 1528 | return false; 1529 | } 1530 | 1531 | len = a.length; 1532 | if ( len !== b.length ) { 1533 | // safe and faster 1534 | return false; 1535 | } 1536 | 1537 | // track reference to avoid circular references 1538 | parents.push( a ); 1539 | parentsB.push( b ); 1540 | for ( i = 0; i < len; i++ ) { 1541 | loop = false; 1542 | for ( j = 0; j < parents.length; j++ ) { 1543 | aCircular = parents[ j ] === a[ i ]; 1544 | bCircular = parentsB[ j ] === b[ i ]; 1545 | if ( aCircular || bCircular ) { 1546 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1547 | loop = true; 1548 | } else { 1549 | parents.pop(); 1550 | parentsB.pop(); 1551 | return false; 1552 | } 1553 | } 1554 | } 1555 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1556 | parents.pop(); 1557 | parentsB.pop(); 1558 | return false; 1559 | } 1560 | } 1561 | parents.pop(); 1562 | parentsB.pop(); 1563 | return true; 1564 | }, 1565 | 1566 | "object": function( b, a ) { 1567 | 1568 | /*jshint forin:false */ 1569 | var i, j, loop, aCircular, bCircular, 1570 | // Default to true 1571 | eq = true, 1572 | aProperties = [], 1573 | bProperties = []; 1574 | 1575 | // comparing constructors is more strict than using 1576 | // instanceof 1577 | if ( a.constructor !== b.constructor ) { 1578 | 1579 | // Allow objects with no prototype to be equivalent to 1580 | // objects with Object as their constructor. 1581 | if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || 1582 | ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { 1583 | return false; 1584 | } 1585 | } 1586 | 1587 | // stack constructor before traversing properties 1588 | callers.push( a.constructor ); 1589 | 1590 | // track reference to avoid circular references 1591 | parents.push( a ); 1592 | parentsB.push( b ); 1593 | 1594 | // be strict: don't ensure hasOwnProperty and go deep 1595 | for ( i in a ) { 1596 | loop = false; 1597 | for ( j = 0; j < parents.length; j++ ) { 1598 | aCircular = parents[ j ] === a[ i ]; 1599 | bCircular = parentsB[ j ] === b[ i ]; 1600 | if ( aCircular || bCircular ) { 1601 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1602 | loop = true; 1603 | } else { 1604 | eq = false; 1605 | break; 1606 | } 1607 | } 1608 | } 1609 | aProperties.push( i ); 1610 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1611 | eq = false; 1612 | break; 1613 | } 1614 | } 1615 | 1616 | parents.pop(); 1617 | parentsB.pop(); 1618 | callers.pop(); // unstack, we are done 1619 | 1620 | for ( i in b ) { 1621 | bProperties.push( i ); // collect b's properties 1622 | } 1623 | 1624 | // Ensures identical properties name 1625 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1626 | } 1627 | }; 1628 | }()); 1629 | 1630 | innerEquiv = function() { // can take multiple arguments 1631 | var args = [].slice.apply( arguments ); 1632 | if ( args.length < 2 ) { 1633 | return true; // end transition 1634 | } 1635 | 1636 | return ( (function( a, b ) { 1637 | if ( a === b ) { 1638 | return true; // catch the most you can 1639 | } else if ( a === null || b === null || typeof a === "undefined" || 1640 | typeof b === "undefined" || 1641 | QUnit.objectType( a ) !== QUnit.objectType( b ) ) { 1642 | 1643 | // don't lose time with error prone cases 1644 | return false; 1645 | } else { 1646 | return bindCallbacks( a, callbacks, [ b, a ] ); 1647 | } 1648 | 1649 | // apply transition with (1..n) arguments 1650 | }( args[ 0 ], args[ 1 ] ) ) && 1651 | innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); 1652 | }; 1653 | 1654 | return innerEquiv; 1655 | }()); 1656 | 1657 | // Based on jsDump by Ariel Flesler 1658 | // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html 1659 | QUnit.dump = (function() { 1660 | function quote( str ) { 1661 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; 1662 | } 1663 | function literal( o ) { 1664 | return o + ""; 1665 | } 1666 | function join( pre, arr, post ) { 1667 | var s = dump.separator(), 1668 | base = dump.indent(), 1669 | inner = dump.indent( 1 ); 1670 | if ( arr.join ) { 1671 | arr = arr.join( "," + s + inner ); 1672 | } 1673 | if ( !arr ) { 1674 | return pre + post; 1675 | } 1676 | return [ pre, inner + arr, base + post ].join( s ); 1677 | } 1678 | function array( arr, stack ) { 1679 | var i = arr.length, 1680 | ret = new Array( i ); 1681 | 1682 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 1683 | return "[object Array]"; 1684 | } 1685 | 1686 | this.up(); 1687 | while ( i-- ) { 1688 | ret[ i ] = this.parse( arr[ i ], undefined, stack ); 1689 | } 1690 | this.down(); 1691 | return join( "[", ret, "]" ); 1692 | } 1693 | 1694 | var reName = /^function (\w+)/, 1695 | dump = { 1696 | 1697 | // objType is used mostly internally, you can fix a (custom) type in advance 1698 | parse: function( obj, objType, stack ) { 1699 | stack = stack || []; 1700 | var res, parser, parserType, 1701 | inStack = inArray( obj, stack ); 1702 | 1703 | if ( inStack !== -1 ) { 1704 | return "recursion(" + ( inStack - stack.length ) + ")"; 1705 | } 1706 | 1707 | objType = objType || this.typeOf( obj ); 1708 | parser = this.parsers[ objType ]; 1709 | parserType = typeof parser; 1710 | 1711 | if ( parserType === "function" ) { 1712 | stack.push( obj ); 1713 | res = parser.call( this, obj, stack ); 1714 | stack.pop(); 1715 | return res; 1716 | } 1717 | return ( parserType === "string" ) ? parser : this.parsers.error; 1718 | }, 1719 | typeOf: function( obj ) { 1720 | var type; 1721 | if ( obj === null ) { 1722 | type = "null"; 1723 | } else if ( typeof obj === "undefined" ) { 1724 | type = "undefined"; 1725 | } else if ( QUnit.is( "regexp", obj ) ) { 1726 | type = "regexp"; 1727 | } else if ( QUnit.is( "date", obj ) ) { 1728 | type = "date"; 1729 | } else if ( QUnit.is( "function", obj ) ) { 1730 | type = "function"; 1731 | } else if ( obj.setInterval !== undefined && 1732 | obj.document !== undefined && 1733 | obj.nodeType === undefined ) { 1734 | type = "window"; 1735 | } else if ( obj.nodeType === 9 ) { 1736 | type = "document"; 1737 | } else if ( obj.nodeType ) { 1738 | type = "node"; 1739 | } else if ( 1740 | 1741 | // native arrays 1742 | toString.call( obj ) === "[object Array]" || 1743 | 1744 | // NodeList objects 1745 | ( typeof obj.length === "number" && obj.item !== undefined && 1746 | ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && 1747 | obj[ 0 ] === undefined ) ) ) 1748 | ) { 1749 | type = "array"; 1750 | } else if ( obj.constructor === Error.prototype.constructor ) { 1751 | type = "error"; 1752 | } else { 1753 | type = typeof obj; 1754 | } 1755 | return type; 1756 | }, 1757 | separator: function() { 1758 | return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; 1759 | }, 1760 | // extra can be a number, shortcut for increasing-calling-decreasing 1761 | indent: function( extra ) { 1762 | if ( !this.multiline ) { 1763 | return ""; 1764 | } 1765 | var chr = this.indentChar; 1766 | if ( this.HTML ) { 1767 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1768 | } 1769 | return new Array( this.depth + ( extra || 0 ) ).join( chr ); 1770 | }, 1771 | up: function( a ) { 1772 | this.depth += a || 1; 1773 | }, 1774 | down: function( a ) { 1775 | this.depth -= a || 1; 1776 | }, 1777 | setParser: function( name, parser ) { 1778 | this.parsers[ name ] = parser; 1779 | }, 1780 | // The next 3 are exposed so you can use them 1781 | quote: quote, 1782 | literal: literal, 1783 | join: join, 1784 | // 1785 | depth: 1, 1786 | maxDepth: 5, 1787 | 1788 | // This is the list of parsers, to modify them, use dump.setParser 1789 | parsers: { 1790 | window: "[Window]", 1791 | document: "[Document]", 1792 | error: function( error ) { 1793 | return "Error(\"" + error.message + "\")"; 1794 | }, 1795 | unknown: "[Unknown]", 1796 | "null": "null", 1797 | "undefined": "undefined", 1798 | "function": function( fn ) { 1799 | var ret = "function", 1800 | 1801 | // functions never have name in IE 1802 | name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; 1803 | 1804 | if ( name ) { 1805 | ret += " " + name; 1806 | } 1807 | ret += "( "; 1808 | 1809 | ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1810 | return join( ret, dump.parse( fn, "functionCode" ), "}" ); 1811 | }, 1812 | array: array, 1813 | nodelist: array, 1814 | "arguments": array, 1815 | object: function( map, stack ) { 1816 | var keys, key, val, i, nonEnumerableProperties, 1817 | ret = []; 1818 | 1819 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 1820 | return "[object Object]"; 1821 | } 1822 | 1823 | dump.up(); 1824 | keys = []; 1825 | for ( key in map ) { 1826 | keys.push( key ); 1827 | } 1828 | 1829 | // Some properties are not always enumerable on Error objects. 1830 | nonEnumerableProperties = [ "message", "name" ]; 1831 | for ( i in nonEnumerableProperties ) { 1832 | key = nonEnumerableProperties[ i ]; 1833 | if ( key in map && !( key in keys ) ) { 1834 | keys.push( key ); 1835 | } 1836 | } 1837 | keys.sort(); 1838 | for ( i = 0; i < keys.length; i++ ) { 1839 | key = keys[ i ]; 1840 | val = map[ key ]; 1841 | ret.push( dump.parse( key, "key" ) + ": " + 1842 | dump.parse( val, undefined, stack ) ); 1843 | } 1844 | dump.down(); 1845 | return join( "{", ret, "}" ); 1846 | }, 1847 | node: function( node ) { 1848 | var len, i, val, 1849 | open = dump.HTML ? "<" : "<", 1850 | close = dump.HTML ? ">" : ">", 1851 | tag = node.nodeName.toLowerCase(), 1852 | ret = open + tag, 1853 | attrs = node.attributes; 1854 | 1855 | if ( attrs ) { 1856 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1857 | val = attrs[ i ].nodeValue; 1858 | 1859 | // IE6 includes all attributes in .attributes, even ones not explicitly 1860 | // set. Those have values like undefined, null, 0, false, "" or 1861 | // "inherit". 1862 | if ( val && val !== "inherit" ) { 1863 | ret += " " + attrs[ i ].nodeName + "=" + 1864 | dump.parse( val, "attribute" ); 1865 | } 1866 | } 1867 | } 1868 | ret += close; 1869 | 1870 | // Show content of TextNode or CDATASection 1871 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1872 | ret += node.nodeValue; 1873 | } 1874 | 1875 | return ret + open + "/" + tag + close; 1876 | }, 1877 | 1878 | // function calls it internally, it's the arguments part of the function 1879 | functionArgs: function( fn ) { 1880 | var args, 1881 | l = fn.length; 1882 | 1883 | if ( !l ) { 1884 | return ""; 1885 | } 1886 | 1887 | args = new Array( l ); 1888 | while ( l-- ) { 1889 | 1890 | // 97 is 'a' 1891 | args[ l ] = String.fromCharCode( 97 + l ); 1892 | } 1893 | return " " + args.join( ", " ) + " "; 1894 | }, 1895 | // object calls it internally, the key part of an item in a map 1896 | key: quote, 1897 | // function calls it internally, it's the content of the function 1898 | functionCode: "[code]", 1899 | // node calls it internally, it's an html attribute value 1900 | attribute: quote, 1901 | string: quote, 1902 | date: quote, 1903 | regexp: literal, 1904 | number: literal, 1905 | "boolean": literal 1906 | }, 1907 | // if true, entities are escaped ( <, >, \t, space and \n ) 1908 | HTML: false, 1909 | // indentation unit 1910 | indentChar: " ", 1911 | // if true, items in a collection, are separated by a \n, else just a space. 1912 | multiline: true 1913 | }; 1914 | 1915 | return dump; 1916 | }()); 1917 | 1918 | // back compat 1919 | QUnit.jsDump = QUnit.dump; 1920 | 1921 | // For browser, export only select globals 1922 | if ( typeof window !== "undefined" ) { 1923 | 1924 | // Deprecated 1925 | // Extend assert methods to QUnit and Global scope through Backwards compatibility 1926 | (function() { 1927 | var i, 1928 | assertions = Assert.prototype; 1929 | 1930 | function applyCurrent( current ) { 1931 | return function() { 1932 | var assert = new Assert( QUnit.config.current ); 1933 | current.apply( assert, arguments ); 1934 | }; 1935 | } 1936 | 1937 | for ( i in assertions ) { 1938 | QUnit[ i ] = applyCurrent( assertions[ i ] ); 1939 | } 1940 | })(); 1941 | 1942 | (function() { 1943 | var i, l, 1944 | keys = [ 1945 | "test", 1946 | "module", 1947 | "expect", 1948 | "asyncTest", 1949 | "start", 1950 | "stop", 1951 | "ok", 1952 | "equal", 1953 | "notEqual", 1954 | "propEqual", 1955 | "notPropEqual", 1956 | "deepEqual", 1957 | "notDeepEqual", 1958 | "strictEqual", 1959 | "notStrictEqual", 1960 | "throws" 1961 | ]; 1962 | 1963 | for ( i = 0, l = keys.length; i < l; i++ ) { 1964 | window[ keys[ i ] ] = QUnit[ keys[ i ] ]; 1965 | } 1966 | })(); 1967 | 1968 | window.QUnit = QUnit; 1969 | } 1970 | 1971 | // For nodejs 1972 | if ( typeof module !== "undefined" && module && module.exports ) { 1973 | module.exports = QUnit; 1974 | 1975 | // For consistency with CommonJS environments' exports 1976 | module.exports.QUnit = QUnit; 1977 | } 1978 | 1979 | // For CommonJS with exports, but without module.exports, like Rhino 1980 | if ( typeof exports !== "undefined" && exports ) { 1981 | exports.QUnit = QUnit; 1982 | } 1983 | 1984 | // Get a reference to the global object, like window in browsers 1985 | }( (function() { 1986 | return this; 1987 | })() )); 1988 | 1989 | /*istanbul ignore next */ 1990 | // jscs:disable maximumLineLength 1991 | /* 1992 | * Javascript Diff Algorithm 1993 | * By John Resig (http://ejohn.org/) 1994 | * Modified by Chu Alan "sprite" 1995 | * 1996 | * Released under the MIT license. 1997 | * 1998 | * More Info: 1999 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2000 | * 2001 | * Usage: QUnit.diff(expected, actual) 2002 | * 2003 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2004 | */ 2005 | QUnit.diff = (function() { 2006 | var hasOwn = Object.prototype.hasOwnProperty; 2007 | 2008 | /*jshint eqeqeq:false, eqnull:true */ 2009 | function diff( o, n ) { 2010 | var i, 2011 | ns = {}, 2012 | os = {}; 2013 | 2014 | for ( i = 0; i < n.length; i++ ) { 2015 | if ( !hasOwn.call( ns, n[ i ] ) ) { 2016 | ns[ n[ i ] ] = { 2017 | rows: [], 2018 | o: null 2019 | }; 2020 | } 2021 | ns[ n[ i ] ].rows.push( i ); 2022 | } 2023 | 2024 | for ( i = 0; i < o.length; i++ ) { 2025 | if ( !hasOwn.call( os, o[ i ] ) ) { 2026 | os[ o[ i ] ] = { 2027 | rows: [], 2028 | n: null 2029 | }; 2030 | } 2031 | os[ o[ i ] ].rows.push( i ); 2032 | } 2033 | 2034 | for ( i in ns ) { 2035 | if ( hasOwn.call( ns, i ) ) { 2036 | if ( ns[ i ].rows.length === 1 && hasOwn.call( os, i ) && os[ i ].rows.length === 1 ) { 2037 | n[ ns[ i ].rows[ 0 ] ] = { 2038 | text: n[ ns[ i ].rows[ 0 ] ], 2039 | row: os[ i ].rows[ 0 ] 2040 | }; 2041 | o[ os[ i ].rows[ 0 ] ] = { 2042 | text: o[ os[ i ].rows[ 0 ] ], 2043 | row: ns[ i ].rows[ 0 ] 2044 | }; 2045 | } 2046 | } 2047 | } 2048 | 2049 | for ( i = 0; i < n.length - 1; i++ ) { 2050 | if ( n[ i ].text != null && n[ i + 1 ].text == null && n[ i ].row + 1 < o.length && o[ n[ i ].row + 1 ].text == null && 2051 | n[ i + 1 ] == o[ n[ i ].row + 1 ] ) { 2052 | 2053 | n[ i + 1 ] = { 2054 | text: n[ i + 1 ], 2055 | row: n[ i ].row + 1 2056 | }; 2057 | o[ n[ i ].row + 1 ] = { 2058 | text: o[ n[ i ].row + 1 ], 2059 | row: i + 1 2060 | }; 2061 | } 2062 | } 2063 | 2064 | for ( i = n.length - 1; i > 0; i-- ) { 2065 | if ( n[ i ].text != null && n[ i - 1 ].text == null && n[ i ].row > 0 && o[ n[ i ].row - 1 ].text == null && 2066 | n[ i - 1 ] == o[ n[ i ].row - 1 ] ) { 2067 | 2068 | n[ i - 1 ] = { 2069 | text: n[ i - 1 ], 2070 | row: n[ i ].row - 1 2071 | }; 2072 | o[ n[ i ].row - 1 ] = { 2073 | text: o[ n[ i ].row - 1 ], 2074 | row: i - 1 2075 | }; 2076 | } 2077 | } 2078 | 2079 | return { 2080 | o: o, 2081 | n: n 2082 | }; 2083 | } 2084 | 2085 | return function( o, n ) { 2086 | o = o.replace( /\s+$/, "" ); 2087 | n = n.replace( /\s+$/, "" ); 2088 | 2089 | var i, pre, 2090 | str = "", 2091 | out = diff( o === "" ? [] : o.split( /\s+/ ), n === "" ? [] : n.split( /\s+/ ) ), 2092 | oSpace = o.match( /\s+/g ), 2093 | nSpace = n.match( /\s+/g ); 2094 | 2095 | if ( oSpace == null ) { 2096 | oSpace = [ " " ]; 2097 | } else { 2098 | oSpace.push( " " ); 2099 | } 2100 | 2101 | if ( nSpace == null ) { 2102 | nSpace = [ " " ]; 2103 | } else { 2104 | nSpace.push( " " ); 2105 | } 2106 | 2107 | if ( out.n.length === 0 ) { 2108 | for ( i = 0; i < out.o.length; i++ ) { 2109 | str += "" + out.o[ i ] + oSpace[ i ] + ""; 2110 | } 2111 | } else { 2112 | if ( out.n[ 0 ].text == null ) { 2113 | for ( n = 0; n < out.o.length && out.o[ n ].text == null; n++ ) { 2114 | str += "" + out.o[ n ] + oSpace[ n ] + ""; 2115 | } 2116 | } 2117 | 2118 | for ( i = 0; i < out.n.length; i++ ) { 2119 | if ( out.n[ i ].text == null ) { 2120 | str += "" + out.n[ i ] + nSpace[ i ] + ""; 2121 | } else { 2122 | 2123 | // `pre` initialized at top of scope 2124 | pre = ""; 2125 | 2126 | for ( n = out.n[ i ].row + 1; n < out.o.length && out.o[ n ].text == null; n++ ) { 2127 | pre += "" + out.o[ n ] + oSpace[ n ] + ""; 2128 | } 2129 | str += " " + out.n[ i ].text + nSpace[ i ] + pre; 2130 | } 2131 | } 2132 | } 2133 | 2134 | return str; 2135 | }; 2136 | }()); 2137 | // jscs:enable 2138 | 2139 | (function() { 2140 | 2141 | // Deprecated QUnit.init - Ref #530 2142 | // Re-initialize the configuration options 2143 | QUnit.init = function() { 2144 | var tests, banner, result, qunit, 2145 | config = QUnit.config; 2146 | 2147 | config.stats = { all: 0, bad: 0 }; 2148 | config.moduleStats = { all: 0, bad: 0 }; 2149 | config.started = 0; 2150 | config.updateRate = 1000; 2151 | config.blocking = false; 2152 | config.autostart = true; 2153 | config.autorun = false; 2154 | config.filter = ""; 2155 | config.queue = []; 2156 | 2157 | // Return on non-browser environments 2158 | // This is necessary to not break on node tests 2159 | if ( typeof window === "undefined" ) { 2160 | return; 2161 | } 2162 | 2163 | qunit = id( "qunit" ); 2164 | if ( qunit ) { 2165 | qunit.innerHTML = 2166 | "

" + escapeText( document.title ) + "

" + 2167 | "

" + 2168 | "
" + 2169 | "

" + 2170 | "
    "; 2171 | } 2172 | 2173 | tests = id( "qunit-tests" ); 2174 | banner = id( "qunit-banner" ); 2175 | result = id( "qunit-testresult" ); 2176 | 2177 | if ( tests ) { 2178 | tests.innerHTML = ""; 2179 | } 2180 | 2181 | if ( banner ) { 2182 | banner.className = ""; 2183 | } 2184 | 2185 | if ( result ) { 2186 | result.parentNode.removeChild( result ); 2187 | } 2188 | 2189 | if ( tests ) { 2190 | result = document.createElement( "p" ); 2191 | result.id = "qunit-testresult"; 2192 | result.className = "result"; 2193 | tests.parentNode.insertBefore( result, tests ); 2194 | result.innerHTML = "Running...
     "; 2195 | } 2196 | }; 2197 | 2198 | // Don't load the HTML Reporter on non-Browser environments 2199 | if ( typeof window === "undefined" ) { 2200 | return; 2201 | } 2202 | 2203 | var config = QUnit.config, 2204 | hasOwn = Object.prototype.hasOwnProperty, 2205 | defined = { 2206 | document: window.document !== undefined, 2207 | sessionStorage: (function() { 2208 | var x = "qunit-test-string"; 2209 | try { 2210 | sessionStorage.setItem( x, x ); 2211 | sessionStorage.removeItem( x ); 2212 | return true; 2213 | } catch ( e ) { 2214 | return false; 2215 | } 2216 | }()) 2217 | }, 2218 | modulesList = []; 2219 | 2220 | /** 2221 | * Escape text for attribute or text content. 2222 | */ 2223 | function escapeText( s ) { 2224 | if ( !s ) { 2225 | return ""; 2226 | } 2227 | s = s + ""; 2228 | 2229 | // Both single quotes and double quotes (for attributes) 2230 | return s.replace( /['"<>&]/g, function( s ) { 2231 | switch ( s ) { 2232 | case "'": 2233 | return "'"; 2234 | case "\"": 2235 | return """; 2236 | case "<": 2237 | return "<"; 2238 | case ">": 2239 | return ">"; 2240 | case "&": 2241 | return "&"; 2242 | } 2243 | }); 2244 | } 2245 | 2246 | /** 2247 | * @param {HTMLElement} elem 2248 | * @param {string} type 2249 | * @param {Function} fn 2250 | */ 2251 | function addEvent( elem, type, fn ) { 2252 | if ( elem.addEventListener ) { 2253 | 2254 | // Standards-based browsers 2255 | elem.addEventListener( type, fn, false ); 2256 | } else if ( elem.attachEvent ) { 2257 | 2258 | // support: IE <9 2259 | elem.attachEvent( "on" + type, fn ); 2260 | } 2261 | } 2262 | 2263 | /** 2264 | * @param {Array|NodeList} elems 2265 | * @param {string} type 2266 | * @param {Function} fn 2267 | */ 2268 | function addEvents( elems, type, fn ) { 2269 | var i = elems.length; 2270 | while ( i-- ) { 2271 | addEvent( elems[ i ], type, fn ); 2272 | } 2273 | } 2274 | 2275 | function hasClass( elem, name ) { 2276 | return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; 2277 | } 2278 | 2279 | function addClass( elem, name ) { 2280 | if ( !hasClass( elem, name ) ) { 2281 | elem.className += ( elem.className ? " " : "" ) + name; 2282 | } 2283 | } 2284 | 2285 | function toggleClass( elem, name ) { 2286 | if ( hasClass( elem, name ) ) { 2287 | removeClass( elem, name ); 2288 | } else { 2289 | addClass( elem, name ); 2290 | } 2291 | } 2292 | 2293 | function removeClass( elem, name ) { 2294 | var set = " " + elem.className + " "; 2295 | 2296 | // Class name may appear multiple times 2297 | while ( set.indexOf( " " + name + " " ) >= 0 ) { 2298 | set = set.replace( " " + name + " ", " " ); 2299 | } 2300 | 2301 | // trim for prettiness 2302 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); 2303 | } 2304 | 2305 | function id( name ) { 2306 | return defined.document && document.getElementById && document.getElementById( name ); 2307 | } 2308 | 2309 | function getUrlConfigHtml() { 2310 | var i, j, val, 2311 | escaped, escapedTooltip, 2312 | selection = false, 2313 | len = config.urlConfig.length, 2314 | urlConfigHtml = ""; 2315 | 2316 | for ( i = 0; i < len; i++ ) { 2317 | val = config.urlConfig[ i ]; 2318 | if ( typeof val === "string" ) { 2319 | val = { 2320 | id: val, 2321 | label: val 2322 | }; 2323 | } 2324 | 2325 | escaped = escapeText( val.id ); 2326 | escapedTooltip = escapeText( val.tooltip ); 2327 | 2328 | if ( config[ val.id ] === undefined ) { 2329 | config[ val.id ] = QUnit.urlParams[ val.id ]; 2330 | } 2331 | 2332 | if ( !val.value || typeof val.value === "string" ) { 2333 | urlConfigHtml += ""; 2339 | } else { 2340 | urlConfigHtml += ""; 2369 | } 2370 | } 2371 | 2372 | return urlConfigHtml; 2373 | } 2374 | 2375 | // Handle "click" events on toolbar checkboxes and "change" for select menus. 2376 | // Updates the URL with the new state of `config.urlConfig` values. 2377 | function toolbarChanged() { 2378 | var updatedUrl, value, 2379 | field = this, 2380 | params = {}; 2381 | 2382 | // Detect if field is a select menu or a checkbox 2383 | if ( "selectedIndex" in field ) { 2384 | value = field.options[ field.selectedIndex ].value || undefined; 2385 | } else { 2386 | value = field.checked ? ( field.defaultValue || true ) : undefined; 2387 | } 2388 | 2389 | params[ field.name ] = value; 2390 | updatedUrl = setUrl( params ); 2391 | 2392 | if ( "hidepassed" === field.name && "replaceState" in window.history ) { 2393 | config[ field.name ] = value || false; 2394 | if ( value ) { 2395 | addClass( id( "qunit-tests" ), "hidepass" ); 2396 | } else { 2397 | removeClass( id( "qunit-tests" ), "hidepass" ); 2398 | } 2399 | 2400 | // It is not necessary to refresh the whole page 2401 | window.history.replaceState( null, "", updatedUrl ); 2402 | } else { 2403 | window.location = updatedUrl; 2404 | } 2405 | } 2406 | 2407 | function setUrl( params ) { 2408 | var key, 2409 | querystring = "?"; 2410 | 2411 | params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); 2412 | 2413 | for ( key in params ) { 2414 | if ( hasOwn.call( params, key ) ) { 2415 | if ( params[ key ] === undefined ) { 2416 | continue; 2417 | } 2418 | querystring += encodeURIComponent( key ); 2419 | if ( params[ key ] !== true ) { 2420 | querystring += "=" + encodeURIComponent( params[ key ] ); 2421 | } 2422 | querystring += "&"; 2423 | } 2424 | } 2425 | return location.protocol + "//" + location.host + 2426 | location.pathname + querystring.slice( 0, -1 ); 2427 | } 2428 | 2429 | function applyUrlParams() { 2430 | var selectBox = id( "qunit-modulefilter" ), 2431 | selection = decodeURIComponent( selectBox.options[ selectBox.selectedIndex ].value ), 2432 | filter = id( "qunit-filter-input" ).value; 2433 | 2434 | window.location = setUrl({ 2435 | module: ( selection === "" ) ? undefined : selection, 2436 | filter: ( filter === "" ) ? undefined : filter, 2437 | 2438 | // Remove testId filter 2439 | testId: undefined 2440 | }); 2441 | } 2442 | 2443 | function toolbarUrlConfigContainer() { 2444 | var urlConfigContainer = document.createElement( "span" ); 2445 | 2446 | urlConfigContainer.innerHTML = getUrlConfigHtml(); 2447 | addClass( urlConfigContainer, "qunit-url-config" ); 2448 | 2449 | // For oldIE support: 2450 | // * Add handlers to the individual elements instead of the container 2451 | // * Use "click" instead of "change" for checkboxes 2452 | addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); 2453 | addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); 2454 | 2455 | return urlConfigContainer; 2456 | } 2457 | 2458 | function toolbarLooseFilter() { 2459 | var filter = document.createElement( "form" ), 2460 | label = document.createElement( "label" ), 2461 | input = document.createElement( "input" ), 2462 | button = document.createElement( "button" ); 2463 | 2464 | addClass( filter, "qunit-filter" ); 2465 | 2466 | label.innerHTML = "Filter: "; 2467 | 2468 | input.type = "text"; 2469 | input.value = config.filter || ""; 2470 | input.name = "filter"; 2471 | input.id = "qunit-filter-input"; 2472 | 2473 | button.innerHTML = "Go"; 2474 | 2475 | label.appendChild( input ); 2476 | 2477 | filter.appendChild( label ); 2478 | filter.appendChild( button ); 2479 | addEvent( filter, "submit", function( ev ) { 2480 | applyUrlParams(); 2481 | 2482 | if ( ev && ev.preventDefault ) { 2483 | ev.preventDefault(); 2484 | } 2485 | 2486 | return false; 2487 | }); 2488 | 2489 | return filter; 2490 | } 2491 | 2492 | function toolbarModuleFilterHtml() { 2493 | var i, 2494 | moduleFilterHtml = ""; 2495 | 2496 | if ( !modulesList.length ) { 2497 | return false; 2498 | } 2499 | 2500 | modulesList.sort(function( a, b ) { 2501 | return a.localeCompare( b ); 2502 | }); 2503 | 2504 | moduleFilterHtml += "" + 2505 | ""; 2516 | 2517 | return moduleFilterHtml; 2518 | } 2519 | 2520 | function toolbarModuleFilter() { 2521 | var toolbar = id( "qunit-testrunner-toolbar" ), 2522 | moduleFilter = document.createElement( "span" ), 2523 | moduleFilterHtml = toolbarModuleFilterHtml(); 2524 | 2525 | if ( !toolbar || !moduleFilterHtml ) { 2526 | return false; 2527 | } 2528 | 2529 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 2530 | moduleFilter.innerHTML = moduleFilterHtml; 2531 | 2532 | addEvent( moduleFilter.lastChild, "change", applyUrlParams ); 2533 | 2534 | toolbar.appendChild( moduleFilter ); 2535 | } 2536 | 2537 | function appendToolbar() { 2538 | var toolbar = id( "qunit-testrunner-toolbar" ); 2539 | 2540 | if ( toolbar ) { 2541 | toolbar.appendChild( toolbarUrlConfigContainer() ); 2542 | toolbar.appendChild( toolbarLooseFilter() ); 2543 | } 2544 | } 2545 | 2546 | function appendHeader() { 2547 | var header = id( "qunit-header" ); 2548 | 2549 | if ( header ) { 2550 | header.innerHTML = "" + header.innerHTML + " "; 2553 | } 2554 | } 2555 | 2556 | function appendBanner() { 2557 | var banner = id( "qunit-banner" ); 2558 | 2559 | if ( banner ) { 2560 | banner.className = ""; 2561 | } 2562 | } 2563 | 2564 | function appendTestResults() { 2565 | var tests = id( "qunit-tests" ), 2566 | result = id( "qunit-testresult" ); 2567 | 2568 | if ( result ) { 2569 | result.parentNode.removeChild( result ); 2570 | } 2571 | 2572 | if ( tests ) { 2573 | tests.innerHTML = ""; 2574 | result = document.createElement( "p" ); 2575 | result.id = "qunit-testresult"; 2576 | result.className = "result"; 2577 | tests.parentNode.insertBefore( result, tests ); 2578 | result.innerHTML = "Running...
     "; 2579 | } 2580 | } 2581 | 2582 | function storeFixture() { 2583 | var fixture = id( "qunit-fixture" ); 2584 | if ( fixture ) { 2585 | config.fixture = fixture.innerHTML; 2586 | } 2587 | } 2588 | 2589 | function appendUserAgent() { 2590 | var userAgent = id( "qunit-userAgent" ); 2591 | if ( userAgent ) { 2592 | userAgent.innerHTML = ""; 2593 | userAgent.appendChild( document.createTextNode( navigator.userAgent ) ); 2594 | } 2595 | } 2596 | 2597 | function appendTestsList( modules ) { 2598 | var i, l, x, z, test, moduleObj; 2599 | 2600 | for ( i = 0, l = modules.length; i < l; i++ ) { 2601 | moduleObj = modules[ i ]; 2602 | 2603 | if ( moduleObj.name ) { 2604 | modulesList.push( moduleObj.name ); 2605 | } 2606 | 2607 | for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { 2608 | test = moduleObj.tests[ x ]; 2609 | 2610 | appendTest( test.name, test.testId, moduleObj.name ); 2611 | } 2612 | } 2613 | } 2614 | 2615 | function appendTest( name, testId, moduleName ) { 2616 | var title, rerunTrigger, testBlock, assertList, 2617 | tests = id( "qunit-tests" ); 2618 | 2619 | if ( !tests ) { 2620 | return; 2621 | } 2622 | 2623 | title = document.createElement( "strong" ); 2624 | title.innerHTML = getNameHtml( name, moduleName ); 2625 | 2626 | rerunTrigger = document.createElement( "a" ); 2627 | rerunTrigger.innerHTML = "Rerun"; 2628 | rerunTrigger.href = setUrl({ testId: testId }); 2629 | 2630 | testBlock = document.createElement( "li" ); 2631 | testBlock.appendChild( title ); 2632 | testBlock.appendChild( rerunTrigger ); 2633 | testBlock.id = "qunit-test-output-" + testId; 2634 | 2635 | assertList = document.createElement( "ol" ); 2636 | assertList.className = "qunit-assert-list"; 2637 | 2638 | testBlock.appendChild( assertList ); 2639 | 2640 | tests.appendChild( testBlock ); 2641 | } 2642 | 2643 | // HTML Reporter initialization and load 2644 | QUnit.begin(function( details ) { 2645 | var qunit = id( "qunit" ); 2646 | 2647 | // Fixture is the only one necessary to run without the #qunit element 2648 | storeFixture(); 2649 | 2650 | if ( qunit ) { 2651 | qunit.innerHTML = 2652 | "

    " + escapeText( document.title ) + "

    " + 2653 | "

    " + 2654 | "
    " + 2655 | "

    " + 2656 | "
      "; 2657 | } 2658 | 2659 | appendHeader(); 2660 | appendBanner(); 2661 | appendTestResults(); 2662 | appendUserAgent(); 2663 | appendToolbar(); 2664 | appendTestsList( details.modules ); 2665 | toolbarModuleFilter(); 2666 | 2667 | if ( qunit && config.hidepassed ) { 2668 | addClass( qunit.lastChild, "hidepass" ); 2669 | } 2670 | }); 2671 | 2672 | QUnit.done(function( details ) { 2673 | var i, key, 2674 | banner = id( "qunit-banner" ), 2675 | tests = id( "qunit-tests" ), 2676 | html = [ 2677 | "Tests completed in ", 2678 | details.runtime, 2679 | " milliseconds.
      ", 2680 | "", 2681 | details.passed, 2682 | " assertions of ", 2683 | details.total, 2684 | " passed, ", 2685 | details.failed, 2686 | " failed." 2687 | ].join( "" ); 2688 | 2689 | if ( banner ) { 2690 | banner.className = details.failed ? "qunit-fail" : "qunit-pass"; 2691 | } 2692 | 2693 | if ( tests ) { 2694 | id( "qunit-testresult" ).innerHTML = html; 2695 | } 2696 | 2697 | if ( config.altertitle && defined.document && document.title ) { 2698 | 2699 | // show ✖ for good, ✔ for bad suite result in title 2700 | // use escape sequences in case file gets loaded with non-utf-8-charset 2701 | document.title = [ 2702 | ( details.failed ? "\u2716" : "\u2714" ), 2703 | document.title.replace( /^[\u2714\u2716] /i, "" ) 2704 | ].join( " " ); 2705 | } 2706 | 2707 | // clear own sessionStorage items if all tests passed 2708 | if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { 2709 | for ( i = 0; i < sessionStorage.length; i++ ) { 2710 | key = sessionStorage.key( i++ ); 2711 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 2712 | sessionStorage.removeItem( key ); 2713 | } 2714 | } 2715 | } 2716 | 2717 | // scroll back to top to show results 2718 | if ( config.scrolltop && window.scrollTo ) { 2719 | window.scrollTo( 0, 0 ); 2720 | } 2721 | }); 2722 | 2723 | function getNameHtml( name, module ) { 2724 | var nameHtml = ""; 2725 | 2726 | if ( module ) { 2727 | nameHtml = "" + escapeText( module ) + ": "; 2728 | } 2729 | 2730 | nameHtml += "" + escapeText( name ) + ""; 2731 | 2732 | return nameHtml; 2733 | } 2734 | 2735 | QUnit.testStart(function( details ) { 2736 | var running, testBlock; 2737 | 2738 | testBlock = id( "qunit-test-output-" + details.testId ); 2739 | if ( testBlock ) { 2740 | testBlock.className = "running"; 2741 | } else { 2742 | 2743 | // Report later registered tests 2744 | appendTest( details.name, details.testId, details.module ); 2745 | } 2746 | 2747 | running = id( "qunit-testresult" ); 2748 | if ( running ) { 2749 | running.innerHTML = "Running:
      " + getNameHtml( details.name, details.module ); 2750 | } 2751 | 2752 | }); 2753 | 2754 | QUnit.log(function( details ) { 2755 | var assertList, assertLi, 2756 | message, expected, actual, 2757 | testItem = id( "qunit-test-output-" + details.testId ); 2758 | 2759 | if ( !testItem ) { 2760 | return; 2761 | } 2762 | 2763 | message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); 2764 | message = "" + message + ""; 2765 | message += "@ " + details.runtime + " ms"; 2766 | 2767 | // pushFailure doesn't provide details.expected 2768 | // when it calls, it's implicit to also not show expected and diff stuff 2769 | // Also, we need to check details.expected existence, as it can exist and be undefined 2770 | if ( !details.result && hasOwn.call( details, "expected" ) ) { 2771 | expected = escapeText( QUnit.dump.parse( details.expected ) ); 2772 | actual = escapeText( QUnit.dump.parse( details.actual ) ); 2773 | message += ""; 2776 | 2777 | if ( actual !== expected ) { 2778 | message += "" + 2780 | ""; 2782 | } 2783 | 2784 | if ( details.source ) { 2785 | message += ""; 2787 | } 2788 | 2789 | message += "
      Expected:
      " +
      2774 | 			expected +
      2775 | 			"
      Result:
      " +
      2779 | 				actual + "
      Diff:
      " +
      2781 | 				QUnit.diff( expected, actual ) + "
      Source:
      " +
      2786 | 				escapeText( details.source ) + "
      "; 2790 | 2791 | // this occours when pushFailure is set and we have an extracted stack trace 2792 | } else if ( !details.result && details.source ) { 2793 | message += "" + 2794 | "" + 2796 | "
      Source:
      " +
      2795 | 			escapeText( details.source ) + "
      "; 2797 | } 2798 | 2799 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 2800 | 2801 | assertLi = document.createElement( "li" ); 2802 | assertLi.className = details.result ? "pass" : "fail"; 2803 | assertLi.innerHTML = message; 2804 | assertList.appendChild( assertLi ); 2805 | }); 2806 | 2807 | QUnit.testDone(function( details ) { 2808 | var testTitle, time, testItem, assertList, 2809 | good, bad, testCounts, skipped, 2810 | tests = id( "qunit-tests" ); 2811 | 2812 | if ( !tests ) { 2813 | return; 2814 | } 2815 | 2816 | testItem = id( "qunit-test-output-" + details.testId ); 2817 | 2818 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 2819 | 2820 | good = details.passed; 2821 | bad = details.failed; 2822 | 2823 | // store result when possible 2824 | if ( config.reorder && defined.sessionStorage ) { 2825 | if ( bad ) { 2826 | sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); 2827 | } else { 2828 | sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); 2829 | } 2830 | } 2831 | 2832 | if ( bad === 0 ) { 2833 | addClass( assertList, "qunit-collapsed" ); 2834 | } 2835 | 2836 | // testItem.firstChild is the test name 2837 | testTitle = testItem.firstChild; 2838 | 2839 | testCounts = bad ? 2840 | "" + bad + ", " + "" + good + ", " : 2841 | ""; 2842 | 2843 | testTitle.innerHTML += " (" + testCounts + 2844 | details.assertions.length + ")"; 2845 | 2846 | if ( details.skipped ) { 2847 | testItem.className = "skipped"; 2848 | skipped = document.createElement( "em" ); 2849 | skipped.className = "qunit-skipped-label"; 2850 | skipped.innerHTML = "skipped"; 2851 | testItem.insertBefore( skipped, testTitle ); 2852 | } else { 2853 | addEvent( testTitle, "click", function() { 2854 | toggleClass( assertList, "qunit-collapsed" ); 2855 | }); 2856 | 2857 | testItem.className = bad ? "fail" : "pass"; 2858 | 2859 | time = document.createElement( "span" ); 2860 | time.className = "runtime"; 2861 | time.innerHTML = details.runtime + " ms"; 2862 | testItem.insertBefore( time, assertList ); 2863 | } 2864 | }); 2865 | 2866 | if ( !defined.document || document.readyState === "complete" ) { 2867 | config.pageLoaded = true; 2868 | config.autorun = true; 2869 | } 2870 | 2871 | if ( defined.document ) { 2872 | addEvent( window, "load", QUnit.load ); 2873 | } 2874 | 2875 | })(); --------------------------------------------------------------------------------