├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── backbone.nativeview.js ├── bower.json ├── package.json └── test ├── index.html ├── mocha.opts ├── nativeview.js ├── test.js ├── vendor ├── backbone.js ├── qunit.css ├── qunit.js ├── runner.js └── underscore.js └── view.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | deploy: 6 | api_key: 7 | secure: BSNCzL7RL4Qd09kVIannM4ufZZHzNYFLh8SzJPeiMOKWvyNLtKhpGPLPUn+GSsds3y+S3x6jcRWVasV4jqsQJiFkr8ad+iV5D7TFUmap1K6ckdD7UnpAVVdVBaT6lPX8QTpO6/qKyNz4wZoJFAso88XCkpRXBD0MhJiJp7f+Wb8= 8 | email: amk528@cs.nyu.edu 9 | provider: npm 10 | notifications: 11 | email: false 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Adam Krebs 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Backbone.NativeView 2 | =================== 3 | 4 | A drop-in replacement for Backbone.View that uses only native DOM methods for 5 | element selection and event delegation. It has no dependency on jQuery. 6 | 7 | NOTE: Backbone.NativeView relies on version 1.2.0 of Backbone. 8 | Backbone 1.1.2 is **not compatible** with Backbone.NativeView. 9 | 10 | To Use: 11 | ------- 12 | Load Backbone.NativeView with your favorite module loader or add as a script 13 | tag after you have loaded Backbone in the page. Wherever you had previously 14 | inherited from Backbone.View, you will now inherit from Backbone.NativeView. 15 | 16 | ```js 17 | var MyView = Backbone.NativeView.extend({ 18 | initialize: function(options) { 19 | // ... 20 | } 21 | }); 22 | ``` 23 | 24 | As an alternative, you may extend an existing View's prototype to use native 25 | methods, or even replace Backbone.View itself: 26 | 27 | ```js 28 | var MyBaseView = Backbone.View.extend(Backbone.NativeViewMixin); 29 | ``` 30 | 31 | or 32 | 33 | ```js 34 | var MyBaseView = Backbone.View.extend({ 35 | initialize: function(options) { 36 | // If you go the prototype extension route be sure to set _domEvents in 37 | // initialize yourself. 38 | this._domEvents = []; 39 | } 40 | }); 41 | 42 | _.extend(MyBaseView.prototype, Backbone.NativeViewMixin); 43 | ``` 44 | 45 | or 46 | 47 | ```js 48 | Backbone.View = Backbone.NativeView; 49 | 50 | var MyView = Backbone.View.extend({ 51 | initialize: function(options) { 52 | // ... 53 | } 54 | }); 55 | ``` 56 | 57 | Features: 58 | --------- 59 | Delegation: 60 | ```js 61 | var view = new MyView({el: '#my-element'}); 62 | view.delegate('click', view.clickHandler); 63 | ``` 64 | 65 | Undelegation with event names or listeners, 66 | ```js 67 | view.undelegate('click', view.clickHandler); 68 | view.undelegate('click'); 69 | ``` 70 | 71 | View-scoped element finding: 72 | ```js 73 | // for one matched element 74 | _.first(view.$('.box')).focus(); 75 | 76 | // for multiple matched elements 77 | _.each(view.$('.item'), function(el) { 78 | el.classList.remove('active') 79 | }); 80 | var fields = _.pluck(view.$('.field'), 'innerHTML'); 81 | ``` 82 | 83 | Requirements: 84 | ------------- 85 | NativeView makes use of `querySelector` and `querySelectorAll`. For IE7 and 86 | below you must include a polyfill. 87 | 88 | Notes: 89 | ------ 90 | * The `$el` property no longer exists on Views. Use `el` instead. 91 | * `View#$` returns a NodeList instead of a jQuery context. You can 92 | iterate over either using `_.each`. 93 | 94 | 95 | With many thanks to @wyuenho for his initial code. 96 | 97 | -------------------------------------------------------------------------------- /backbone.nativeview.js: -------------------------------------------------------------------------------- 1 | // Backbone.NativeView.js 0.3.3 2 | // --------------- 3 | 4 | // (c) 2015 Adam Krebs, Jimmy Yuen Ho Wong 5 | // Backbone.NativeView may be freely distributed under the MIT license. 6 | // For all details and documentation: 7 | // https://github.com/akre54/Backbone.NativeView 8 | 9 | (function (factory) { 10 | if (typeof define === 'function' && define.amd) { define(['backbone'], factory); 11 | } else if (typeof module === 'object') { module.exports = factory(require('backbone')); 12 | } else { factory(Backbone); } 13 | }(function (Backbone) { 14 | // Cached regex to match an opening '<' of an HTML tag, possibly left-padded 15 | // with whitespace. 16 | var paddedLt = /^\s*=9 and modern browsers. 40 | var matchesSelector = ElementProto.matches || 41 | ElementProto.webkitMatchesSelector || 42 | ElementProto.mozMatchesSelector || 43 | ElementProto.msMatchesSelector || 44 | ElementProto.oMatchesSelector || 45 | // Make our own `Element#matches` for IE8 46 | function(selector) { 47 | // Use querySelectorAll to find all elements matching the selector, 48 | // then check if the given element is included in that list. 49 | // Executing the query on the parentNode reduces the resulting nodeList, 50 | // (document doesn't have a parentNode). 51 | var nodeList = (this.parentNode || document).querySelectorAll(selector) || []; 52 | return ~indexOf(nodeList, this); 53 | }; 54 | 55 | // Cache Backbone.View for later access in constructor 56 | var BBView = Backbone.View; 57 | 58 | // To extend an existing view to use native methods, extend the View prototype 59 | // with the mixin: _.extend(MyView.prototype, Backbone.NativeViewMixin); 60 | Backbone.NativeViewMixin = { 61 | 62 | _domEvents: null, 63 | 64 | constructor: function() { 65 | this._domEvents = []; 66 | return BBView.apply(this, arguments); 67 | }, 68 | 69 | $: function(selector) { 70 | return this.el.querySelectorAll(selector); 71 | }, 72 | 73 | _removeElement: function() { 74 | this.undelegateEvents(); 75 | if (this.el.parentNode) this.el.parentNode.removeChild(this.el); 76 | }, 77 | 78 | // Apply the `element` to the view. `element` can be a CSS selector, 79 | // a string of HTML, or an Element node. If passed a NodeList or CSS 80 | // selector, uses just the first match. 81 | _setElement: function(element) { 82 | if (typeof element == 'string') { 83 | if (paddedLt.test(element)) { 84 | var el = document.createElement('div'); 85 | el.innerHTML = element; 86 | this.el = el.firstChild; 87 | } else { 88 | this.el = document.querySelector(element); 89 | } 90 | } else if (element && element.length) { 91 | this.el = element[0]; 92 | } else { 93 | this.el = element; 94 | } 95 | }, 96 | 97 | // Set a hash of attributes to the view's `el`. We use the "prop" version 98 | // if available, falling back to `setAttribute` for the catch-all. 99 | _setAttributes: function(attrs) { 100 | for (var attr in attrs) { 101 | attr in this.el ? this.el[attr] = attrs[attr] : this.el.setAttribute(attr, attrs[attr]); 102 | } 103 | }, 104 | 105 | // Make a event delegation handler for the given `eventName` and `selector` 106 | // and attach it to `this.el`. 107 | // If selector is empty, the listener will be bound to `this.el`. If not, a 108 | // new handler that will recursively traverse up the event target's DOM 109 | // hierarchy looking for a node that matches the selector. If one is found, 110 | // the event's `delegateTarget` property is set to it and the return the 111 | // result of calling bound `listener` with the parameters given to the 112 | // handler. 113 | delegate: function(eventName, selector, listener) { 114 | var root = this.el; 115 | 116 | if (!root) { 117 | return; 118 | } 119 | 120 | if (typeof selector === 'function') { 121 | listener = selector; 122 | selector = null; 123 | } 124 | 125 | // Given that `focus` and `blur` events do not bubble, do not delegate these events 126 | if (['focus', 'blur'].indexOf(eventName) !== -1) { 127 | var els = this.el.querySelectorAll(selector); 128 | for (var i = 0, len = els.length; i < len; i++) { 129 | var item = els[i]; 130 | elementAddEventListener.call(item, eventName, listener, false); 131 | this._domEvents.push({el: item, eventName: eventName, handler: listener}); 132 | } 133 | return listener; 134 | } 135 | 136 | var handler = selector ? function (e) { 137 | var node = e.target || e.srcElement; 138 | for (; node && node != root; node = node.parentNode) { 139 | if (matchesSelector.call(node, selector)) { 140 | e.delegateTarget = node; 141 | listener(e); 142 | } 143 | } 144 | } : listener; 145 | 146 | elementAddEventListener.call(this.el, eventName, handler, false); 147 | this._domEvents.push({el: this.el, eventName: eventName, handler: handler, listener: listener, selector: selector}); 148 | return handler; 149 | }, 150 | 151 | // Remove a single delegated event. Either `eventName` or `selector` must 152 | // be included, `selector` and `listener` are optional. 153 | undelegate: function(eventName, selector, listener) { 154 | if (typeof selector === 'function') { 155 | listener = selector; 156 | selector = null; 157 | } 158 | 159 | if (this.el) { 160 | var handlers = this._domEvents.slice(); 161 | var i = handlers.length; 162 | while (i--) { 163 | var item = handlers[i]; 164 | 165 | var match = item.eventName === eventName && 166 | (listener ? item.listener === listener : true) && 167 | (selector ? item.selector === selector : true); 168 | 169 | if (!match) continue; 170 | 171 | elementRemoveEventListener.call(item.el, item.eventName, item.handler, false); 172 | this._domEvents.splice(i, 1); 173 | } 174 | } 175 | return this; 176 | }, 177 | 178 | // Remove all events created with `delegate` from `el` 179 | undelegateEvents: function() { 180 | if (this.el) { 181 | for (var i = 0, len = this._domEvents.length; i < len; i++) { 182 | var item = this._domEvents[i]; 183 | elementRemoveEventListener.call(item.el, item.eventName, item.handler, false); 184 | }; 185 | this._domEvents.length = 0; 186 | } 187 | return this; 188 | } 189 | }; 190 | 191 | Backbone.NativeView = Backbone.View.extend(Backbone.NativeViewMixin); 192 | 193 | return Backbone.NativeView; 194 | })); 195 | 196 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.nativeview", 3 | "version": "0.3.3", 4 | "homepage": "https://github.com/akre54/backbone.nativeview", 5 | "author": "Adam Krebs ", 6 | "description": "A Backbone View powered by native DOM methods", 7 | "main": "backbone.nativeview.js", 8 | "keywords": [ 9 | "Backbone", 10 | "view", 11 | "plugin", 12 | "native", 13 | "DOM", 14 | "change" 15 | ], 16 | "peerDependencies": { 17 | "backbone": ">1.2.0" 18 | }, 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.nativeview", 3 | "version": "0.3.4", 4 | "author": "Adam Krebs ", 5 | "description": "A Backbone View powered by native DOM methods", 6 | "licence": "MIT", 7 | "main": "backbone.nativeview", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/akre54/backbone.nativeview" 11 | }, 12 | "devDependencies": { 13 | "backbone": ">1.2.0", 14 | "phantomjs": "1.9.7-8" 15 | }, 16 | "engines": { 17 | "node": "*", 18 | "npm": "*" 19 | }, 20 | "scripts": { 21 | "test": "phantomjs test/vendor/runner.js test/index.html?noglobals=true" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Backbone Test Suite 6 | 7 | 8 | 9 |
10 |
11 |
12 |

Test

13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --growl 3 | --colors -------------------------------------------------------------------------------- /test/nativeview.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // When testing alternative View implementations, change this varaible. 4 | var View = Backbone.NativeView; 5 | 6 | var view; 7 | 8 | module("Backbone.NativeView", { 9 | 10 | setup: function() { 11 | view = new View({el: '#testElement'}); 12 | } 13 | 14 | }); 15 | 16 | test("extending", 3, function() { 17 | var BaseView = Backbone.View.extend(); 18 | var NativeView = Backbone.NativeView; 19 | 20 | var ExtendedView = Backbone.View.extend(Backbone.NativeViewMixin); 21 | 22 | var MixinView = _.extend(BaseView, Backbone.NativeViewMixin); 23 | MixinView.prototype.initialize = function(options) { this._domEvents = []; } 24 | 25 | ok((new NativeView)._domEvents); 26 | ok((new ExtendedView)._domEvents); 27 | ok((new MixinView)._domEvents); 28 | }); 29 | 30 | test("View#$", function() { 31 | var result = view.$('h1'); 32 | equal(result.length, 1); 33 | equal(result[0].tagName.toLowerCase(), 'h1'); 34 | equal(result[0].nodeType, 1); 35 | }); 36 | 37 | test("View#setElement", function() { 38 | var result = view.$('h1'); 39 | view.setElement(result); 40 | equal(view.el, result[0]); 41 | }); 42 | 43 | test("delegate and undelegate", 6, function() { 44 | var counter1 = 0, counter2 = 0; 45 | view.delegate('click', function() { counter1++; }); 46 | addEventListener.call(view.el, 'click', function() { counter2++ }); 47 | 48 | click(view.el); 49 | 50 | equal(counter1, 1); 51 | equal(counter2, 1); 52 | equal(view._domEvents.length, 1); 53 | 54 | view.undelegate('click'); 55 | 56 | click(view.el); 57 | 58 | equal(counter1, 1); 59 | equal(counter2, 2); 60 | equal(view._domEvents.length, 0); 61 | }); 62 | 63 | test("undelegating only affects matched handlers", 3, function() { 64 | view.delegate('click', 'h1', function() { ok(false); }); 65 | view.delegate('click', 'div', function() { ok(true); }); 66 | view.undelegate('click', 'h1'); 67 | 68 | _.each(view.$('h1, div'), click); 69 | 70 | // // We don't currently do any selector matching. Fix this 71 | // view.undelegate('click', '.one'); 72 | // _.each(view.$('h1, div'), click); 73 | }); 74 | 75 | 76 | // Cross-browser helpers 77 | var addEventListener = typeof Element != 'undefined' && Element.prototype.addEventListener || function(eventName, listener) { 78 | return this.attachEvent('on' + eventName, listener); 79 | }; 80 | 81 | function click(element) { 82 | var event; 83 | if (document.createEvent) { 84 | event = document.createEvent('MouseEvent'); 85 | var args = [ 86 | 'click', true, true, 87 | // IE 10+ and Firefox require these 88 | event.view, event.detail, event.screenX, event.screenY, event.clientX, 89 | event.clientY, event.ctrlKey, event.altKey, event.shiftKey, 90 | event.metaKey, event.button, event.relatedTarget 91 | ]; 92 | (event.initMouseEvent || event.initEvent).apply(event, args); 93 | } else { 94 | event = document.createEventObject(); 95 | event.type = 'click'; 96 | event.bubbles = true; 97 | event.cancelable = true; 98 | } 99 | 100 | if (element.dispatchEvent) { 101 | return element.dispatchEvent(event); 102 | } 103 | element.fireEvent('onclick', event); 104 | } 105 | })(); 106 | 107 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | require("chai").should(); 2 | global._ = require('underscore'); 3 | global.Backbone = require('backbone'); 4 | require('../backbone.nativeview'); 5 | 6 | describe('Backbone.NativeView', function() { 7 | }); -------------------------------------------------------------------------------- /test/vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - 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 | -------------------------------------------------------------------------------- /test/vendor/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2013 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * https://jquery.org/license/ 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | assert, 15 | config, 16 | onErrorFnPrev, 17 | testId = 0, 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 | setTimeout = window.setTimeout, 24 | defined = { 25 | setTimeout: typeof window.setTimeout !== "undefined", 26 | sessionStorage: (function() { 27 | var x = "qunit-test-string"; 28 | try { 29 | sessionStorage.setItem( x, x ); 30 | sessionStorage.removeItem( x ); 31 | return true; 32 | } catch( e ) { 33 | return false; 34 | } 35 | }()) 36 | }, 37 | /** 38 | * Provides a normalized error string, correcting an issue 39 | * with IE 7 (and prior) where Error.prototype.toString is 40 | * not properly implemented 41 | * 42 | * Based on http://es5.github.com/#x15.11.4.4 43 | * 44 | * @param {String|Error} error 45 | * @return {String} error message 46 | */ 47 | errorString = function( error ) { 48 | var name, message, 49 | errorString = error.toString(); 50 | if ( errorString.substring( 0, 7 ) === "[object" ) { 51 | name = error.name ? error.name.toString() : "Error"; 52 | message = error.message ? error.message.toString() : ""; 53 | if ( name && message ) { 54 | return name + ": " + message; 55 | } else if ( name ) { 56 | return name; 57 | } else if ( message ) { 58 | return message; 59 | } else { 60 | return "Error"; 61 | } 62 | } else { 63 | return errorString; 64 | } 65 | }, 66 | /** 67 | * Makes a clone of an object using only Array or Object as base, 68 | * and copies over the own enumerable properties. 69 | * 70 | * @param {Object} obj 71 | * @return {Object} New object with only the own properties (recursively). 72 | */ 73 | objectValues = function( obj ) { 74 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 75 | /*jshint newcap: false */ 76 | var key, val, 77 | vals = QUnit.is( "array", obj ) ? [] : {}; 78 | for ( key in obj ) { 79 | if ( hasOwn.call( obj, key ) ) { 80 | val = obj[key]; 81 | vals[key] = val === Object(val) ? objectValues(val) : val; 82 | } 83 | } 84 | return vals; 85 | }; 86 | 87 | function Test( settings ) { 88 | extend( this, settings ); 89 | this.assertions = []; 90 | this.testNumber = ++Test.count; 91 | } 92 | 93 | Test.count = 0; 94 | 95 | Test.prototype = { 96 | init: function() { 97 | var a, b, li, 98 | tests = id( "qunit-tests" ); 99 | 100 | if ( tests ) { 101 | b = document.createElement( "strong" ); 102 | b.innerHTML = this.nameHtml; 103 | 104 | // `a` initialized at top of scope 105 | a = document.createElement( "a" ); 106 | a.innerHTML = "Rerun"; 107 | a.href = QUnit.url({ testNumber: this.testNumber }); 108 | 109 | li = document.createElement( "li" ); 110 | li.appendChild( b ); 111 | li.appendChild( a ); 112 | li.className = "running"; 113 | li.id = this.id = "qunit-test-output" + testId++; 114 | 115 | tests.appendChild( li ); 116 | } 117 | }, 118 | setup: function() { 119 | if ( 120 | // Emit moduleStart when we're switching from one module to another 121 | this.module !== config.previousModule || 122 | // They could be equal (both undefined) but if the previousModule property doesn't 123 | // yet exist it means this is the first test in a suite that isn't wrapped in a 124 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 125 | // Without this, reporters can get testStart before moduleStart which is a problem. 126 | !hasOwn.call( config, "previousModule" ) 127 | ) { 128 | if ( hasOwn.call( config, "previousModule" ) ) { 129 | runLoggingCallbacks( "moduleDone", QUnit, { 130 | name: config.previousModule, 131 | failed: config.moduleStats.bad, 132 | passed: config.moduleStats.all - config.moduleStats.bad, 133 | total: config.moduleStats.all 134 | }); 135 | } 136 | config.previousModule = this.module; 137 | config.moduleStats = { all: 0, bad: 0 }; 138 | runLoggingCallbacks( "moduleStart", QUnit, { 139 | name: this.module 140 | }); 141 | } 142 | 143 | config.current = this; 144 | 145 | this.testEnvironment = extend({ 146 | setup: function() {}, 147 | teardown: function() {} 148 | }, this.moduleTestEnvironment ); 149 | 150 | this.started = +new Date(); 151 | runLoggingCallbacks( "testStart", QUnit, { 152 | name: this.testName, 153 | module: this.module 154 | }); 155 | 156 | /*jshint camelcase:false */ 157 | 158 | 159 | /** 160 | * Expose the current test environment. 161 | * 162 | * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. 163 | */ 164 | QUnit.current_testEnvironment = this.testEnvironment; 165 | 166 | /*jshint camelcase:true */ 167 | 168 | if ( !config.pollution ) { 169 | saveGlobal(); 170 | } 171 | if ( config.notrycatch ) { 172 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 173 | return; 174 | } 175 | try { 176 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 177 | } catch( e ) { 178 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 179 | } 180 | }, 181 | run: function() { 182 | config.current = this; 183 | 184 | var running = id( "qunit-testresult" ); 185 | 186 | if ( running ) { 187 | running.innerHTML = "Running:
" + this.nameHtml; 188 | } 189 | 190 | if ( this.async ) { 191 | QUnit.stop(); 192 | } 193 | 194 | this.callbackStarted = +new Date(); 195 | 196 | if ( config.notrycatch ) { 197 | this.callback.call( this.testEnvironment, QUnit.assert ); 198 | this.callbackRuntime = +new Date() - this.callbackStarted; 199 | return; 200 | } 201 | 202 | try { 203 | this.callback.call( this.testEnvironment, QUnit.assert ); 204 | this.callbackRuntime = +new Date() - this.callbackStarted; 205 | } catch( e ) { 206 | this.callbackRuntime = +new Date() - this.callbackStarted; 207 | 208 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 209 | // else next test will carry the responsibility 210 | saveGlobal(); 211 | 212 | // Restart the tests if they're blocking 213 | if ( config.blocking ) { 214 | QUnit.start(); 215 | } 216 | } 217 | }, 218 | teardown: function() { 219 | config.current = this; 220 | if ( config.notrycatch ) { 221 | if ( typeof this.callbackRuntime === "undefined" ) { 222 | this.callbackRuntime = +new Date() - this.callbackStarted; 223 | } 224 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 225 | return; 226 | } else { 227 | try { 228 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 229 | } catch( e ) { 230 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 231 | } 232 | } 233 | checkPollution(); 234 | }, 235 | finish: function() { 236 | config.current = this; 237 | if ( config.requireExpects && this.expected === null ) { 238 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 239 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 240 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 241 | } else if ( this.expected === null && !this.assertions.length ) { 242 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 243 | } 244 | 245 | var i, assertion, a, b, time, li, ol, 246 | test = this, 247 | good = 0, 248 | bad = 0, 249 | tests = id( "qunit-tests" ); 250 | 251 | this.runtime = +new Date() - this.started; 252 | config.stats.all += this.assertions.length; 253 | config.moduleStats.all += this.assertions.length; 254 | 255 | if ( tests ) { 256 | ol = document.createElement( "ol" ); 257 | ol.className = "qunit-assert-list"; 258 | 259 | for ( i = 0; i < this.assertions.length; i++ ) { 260 | assertion = this.assertions[i]; 261 | 262 | li = document.createElement( "li" ); 263 | li.className = assertion.result ? "pass" : "fail"; 264 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 265 | ol.appendChild( li ); 266 | 267 | if ( assertion.result ) { 268 | good++; 269 | } else { 270 | bad++; 271 | config.stats.bad++; 272 | config.moduleStats.bad++; 273 | } 274 | } 275 | 276 | // store result when possible 277 | if ( QUnit.config.reorder && defined.sessionStorage ) { 278 | if ( bad ) { 279 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 280 | } else { 281 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 282 | } 283 | } 284 | 285 | if ( bad === 0 ) { 286 | addClass( ol, "qunit-collapsed" ); 287 | } 288 | 289 | // `b` initialized at top of scope 290 | b = document.createElement( "strong" ); 291 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 292 | 293 | addEvent(b, "click", function() { 294 | var next = b.parentNode.lastChild, 295 | collapsed = hasClass( next, "qunit-collapsed" ); 296 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 297 | }); 298 | 299 | addEvent(b, "dblclick", function( e ) { 300 | var target = e && e.target ? e.target : window.event.srcElement; 301 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 302 | target = target.parentNode; 303 | } 304 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 305 | window.location = QUnit.url({ testNumber: test.testNumber }); 306 | } 307 | }); 308 | 309 | // `time` initialized at top of scope 310 | time = document.createElement( "span" ); 311 | time.className = "runtime"; 312 | time.innerHTML = this.runtime + " ms"; 313 | 314 | // `li` initialized at top of scope 315 | li = id( this.id ); 316 | li.className = bad ? "fail" : "pass"; 317 | li.removeChild( li.firstChild ); 318 | a = li.firstChild; 319 | li.appendChild( b ); 320 | li.appendChild( a ); 321 | li.appendChild( time ); 322 | li.appendChild( ol ); 323 | 324 | } else { 325 | for ( i = 0; i < this.assertions.length; i++ ) { 326 | if ( !this.assertions[i].result ) { 327 | bad++; 328 | config.stats.bad++; 329 | config.moduleStats.bad++; 330 | } 331 | } 332 | } 333 | 334 | runLoggingCallbacks( "testDone", QUnit, { 335 | name: this.testName, 336 | module: this.module, 337 | failed: bad, 338 | passed: this.assertions.length - bad, 339 | total: this.assertions.length, 340 | duration: this.runtime 341 | }); 342 | 343 | QUnit.reset(); 344 | 345 | config.current = undefined; 346 | }, 347 | 348 | queue: function() { 349 | var bad, 350 | test = this; 351 | 352 | synchronize(function() { 353 | test.init(); 354 | }); 355 | function run() { 356 | // each of these can by async 357 | synchronize(function() { 358 | test.setup(); 359 | }); 360 | synchronize(function() { 361 | test.run(); 362 | }); 363 | synchronize(function() { 364 | test.teardown(); 365 | }); 366 | synchronize(function() { 367 | test.finish(); 368 | }); 369 | } 370 | 371 | // `bad` initialized at top of scope 372 | // defer when previous test run passed, if storage is available 373 | bad = QUnit.config.reorder && defined.sessionStorage && 374 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 375 | 376 | if ( bad ) { 377 | run(); 378 | } else { 379 | synchronize( run, true ); 380 | } 381 | } 382 | }; 383 | 384 | // Root QUnit object. 385 | // `QUnit` initialized at top of scope 386 | QUnit = { 387 | 388 | // call on start of module test to prepend name to all tests 389 | module: function( name, testEnvironment ) { 390 | config.currentModule = name; 391 | config.currentModuleTestEnvironment = testEnvironment; 392 | config.modules[name] = true; 393 | }, 394 | 395 | asyncTest: function( testName, expected, callback ) { 396 | if ( arguments.length === 2 ) { 397 | callback = expected; 398 | expected = null; 399 | } 400 | 401 | QUnit.test( testName, expected, callback, true ); 402 | }, 403 | 404 | test: function( testName, expected, callback, async ) { 405 | var test, 406 | nameHtml = "" + escapeText( testName ) + ""; 407 | 408 | if ( arguments.length === 2 ) { 409 | callback = expected; 410 | expected = null; 411 | } 412 | 413 | if ( config.currentModule ) { 414 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 415 | } 416 | 417 | test = new Test({ 418 | nameHtml: nameHtml, 419 | testName: testName, 420 | expected: expected, 421 | async: async, 422 | callback: callback, 423 | module: config.currentModule, 424 | moduleTestEnvironment: config.currentModuleTestEnvironment, 425 | stack: sourceFromStacktrace( 2 ) 426 | }); 427 | 428 | if ( !validTest( test ) ) { 429 | return; 430 | } 431 | 432 | test.queue(); 433 | }, 434 | 435 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 436 | expect: function( asserts ) { 437 | if (arguments.length === 1) { 438 | config.current.expected = asserts; 439 | } else { 440 | return config.current.expected; 441 | } 442 | }, 443 | 444 | start: function( count ) { 445 | // QUnit hasn't been initialized yet. 446 | // Note: RequireJS (et al) may delay onLoad 447 | if ( config.semaphore === undefined ) { 448 | QUnit.begin(function() { 449 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 450 | setTimeout(function() { 451 | QUnit.start( count ); 452 | }); 453 | }); 454 | return; 455 | } 456 | 457 | config.semaphore -= count || 1; 458 | // don't start until equal number of stop-calls 459 | if ( config.semaphore > 0 ) { 460 | return; 461 | } 462 | // ignore if start is called more often then stop 463 | if ( config.semaphore < 0 ) { 464 | config.semaphore = 0; 465 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 466 | return; 467 | } 468 | // A slight delay, to avoid any current callbacks 469 | if ( defined.setTimeout ) { 470 | setTimeout(function() { 471 | if ( config.semaphore > 0 ) { 472 | return; 473 | } 474 | if ( config.timeout ) { 475 | clearTimeout( config.timeout ); 476 | } 477 | 478 | config.blocking = false; 479 | process( true ); 480 | }, 13); 481 | } else { 482 | config.blocking = false; 483 | process( true ); 484 | } 485 | }, 486 | 487 | stop: function( count ) { 488 | config.semaphore += count || 1; 489 | config.blocking = true; 490 | 491 | if ( config.testTimeout && defined.setTimeout ) { 492 | clearTimeout( config.timeout ); 493 | config.timeout = setTimeout(function() { 494 | QUnit.ok( false, "Test timed out" ); 495 | config.semaphore = 1; 496 | QUnit.start(); 497 | }, config.testTimeout ); 498 | } 499 | } 500 | }; 501 | 502 | // `assert` initialized at top of scope 503 | // Assert helpers 504 | // All of these must either call QUnit.push() or manually do: 505 | // - runLoggingCallbacks( "log", .. ); 506 | // - config.current.assertions.push({ .. }); 507 | // We attach it to the QUnit object *after* we expose the public API, 508 | // otherwise `assert` will become a global variable in browsers (#341). 509 | assert = { 510 | /** 511 | * Asserts rough true-ish result. 512 | * @name ok 513 | * @function 514 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 515 | */ 516 | ok: function( result, msg ) { 517 | if ( !config.current ) { 518 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 519 | } 520 | result = !!result; 521 | msg = msg || (result ? "okay" : "failed" ); 522 | 523 | var source, 524 | details = { 525 | module: config.current.module, 526 | name: config.current.testName, 527 | result: result, 528 | message: msg 529 | }; 530 | 531 | msg = "" + escapeText( msg ) + ""; 532 | 533 | if ( !result ) { 534 | source = sourceFromStacktrace( 2 ); 535 | if ( source ) { 536 | details.source = source; 537 | msg += "
Source:
" + escapeText( source ) + "
"; 538 | } 539 | } 540 | runLoggingCallbacks( "log", QUnit, details ); 541 | config.current.assertions.push({ 542 | result: result, 543 | message: msg 544 | }); 545 | }, 546 | 547 | /** 548 | * Assert that the first two arguments are equal, with an optional message. 549 | * Prints out both actual and expected values. 550 | * @name equal 551 | * @function 552 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 553 | */ 554 | equal: function( actual, expected, message ) { 555 | /*jshint eqeqeq:false */ 556 | QUnit.push( expected == actual, actual, expected, message ); 557 | }, 558 | 559 | /** 560 | * @name notEqual 561 | * @function 562 | */ 563 | notEqual: function( actual, expected, message ) { 564 | /*jshint eqeqeq:false */ 565 | QUnit.push( expected != actual, actual, expected, message ); 566 | }, 567 | 568 | /** 569 | * @name propEqual 570 | * @function 571 | */ 572 | propEqual: function( actual, expected, message ) { 573 | actual = objectValues(actual); 574 | expected = objectValues(expected); 575 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 576 | }, 577 | 578 | /** 579 | * @name notPropEqual 580 | * @function 581 | */ 582 | notPropEqual: function( actual, expected, message ) { 583 | actual = objectValues(actual); 584 | expected = objectValues(expected); 585 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 586 | }, 587 | 588 | /** 589 | * @name deepEqual 590 | * @function 591 | */ 592 | deepEqual: function( actual, expected, message ) { 593 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 594 | }, 595 | 596 | /** 597 | * @name notDeepEqual 598 | * @function 599 | */ 600 | notDeepEqual: function( actual, expected, message ) { 601 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 602 | }, 603 | 604 | /** 605 | * @name strictEqual 606 | * @function 607 | */ 608 | strictEqual: function( actual, expected, message ) { 609 | QUnit.push( expected === actual, actual, expected, message ); 610 | }, 611 | 612 | /** 613 | * @name notStrictEqual 614 | * @function 615 | */ 616 | notStrictEqual: function( actual, expected, message ) { 617 | QUnit.push( expected !== actual, actual, expected, message ); 618 | }, 619 | 620 | "throws": function( block, expected, message ) { 621 | var actual, 622 | expectedOutput = expected, 623 | ok = false; 624 | 625 | // 'expected' is optional 626 | if ( typeof expected === "string" ) { 627 | message = expected; 628 | expected = null; 629 | } 630 | 631 | config.current.ignoreGlobalErrors = true; 632 | try { 633 | block.call( config.current.testEnvironment ); 634 | } catch (e) { 635 | actual = e; 636 | } 637 | config.current.ignoreGlobalErrors = false; 638 | 639 | if ( actual ) { 640 | // we don't want to validate thrown error 641 | if ( !expected ) { 642 | ok = true; 643 | expectedOutput = null; 644 | // expected is a regexp 645 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 646 | ok = expected.test( errorString( actual ) ); 647 | // expected is a constructor 648 | } else if ( actual instanceof expected ) { 649 | ok = true; 650 | // expected is a validation function which returns true is validation passed 651 | } else if ( expected.call( {}, actual ) === true ) { 652 | expectedOutput = null; 653 | ok = true; 654 | } 655 | 656 | QUnit.push( ok, actual, expectedOutput, message ); 657 | } else { 658 | QUnit.pushFailure( message, null, "No exception was thrown." ); 659 | } 660 | } 661 | }; 662 | 663 | /** 664 | * @deprecated since 1.8.0 665 | * Kept assertion helpers in root for backwards compatibility. 666 | */ 667 | extend( QUnit, assert ); 668 | 669 | /** 670 | * @deprecated since 1.9.0 671 | * Kept root "raises()" for backwards compatibility. 672 | * (Note that we don't introduce assert.raises). 673 | */ 674 | QUnit.raises = assert[ "throws" ]; 675 | 676 | /** 677 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 678 | * Kept to avoid TypeErrors for undefined methods. 679 | */ 680 | QUnit.equals = function() { 681 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 682 | }; 683 | QUnit.same = function() { 684 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 685 | }; 686 | 687 | // We want access to the constructor's prototype 688 | (function() { 689 | function F() {} 690 | F.prototype = QUnit; 691 | QUnit = new F(); 692 | // Make F QUnit's constructor so that we can add to the prototype later 693 | QUnit.constructor = F; 694 | }()); 695 | 696 | /** 697 | * Config object: Maintain internal state 698 | * Later exposed as QUnit.config 699 | * `config` initialized at top of scope 700 | */ 701 | config = { 702 | // The queue of tests to run 703 | queue: [], 704 | 705 | // block until document ready 706 | blocking: true, 707 | 708 | // when enabled, show only failing tests 709 | // gets persisted through sessionStorage and can be changed in UI via checkbox 710 | hidepassed: false, 711 | 712 | // by default, run previously failed tests first 713 | // very useful in combination with "Hide passed tests" checked 714 | reorder: true, 715 | 716 | // by default, modify document.title when suite is done 717 | altertitle: true, 718 | 719 | // when enabled, all tests must call expect() 720 | requireExpects: false, 721 | 722 | // add checkboxes that are persisted in the query-string 723 | // when enabled, the id is set to `true` as a `QUnit.config` property 724 | urlConfig: [ 725 | { 726 | id: "noglobals", 727 | label: "Check for Globals", 728 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 729 | }, 730 | { 731 | id: "notrycatch", 732 | label: "No try-catch", 733 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 734 | } 735 | ], 736 | 737 | // Set of all modules. 738 | modules: {}, 739 | 740 | // logging callback queues 741 | begin: [], 742 | done: [], 743 | log: [], 744 | testStart: [], 745 | testDone: [], 746 | moduleStart: [], 747 | moduleDone: [] 748 | }; 749 | 750 | // Export global variables, unless an 'exports' object exists, 751 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 752 | if ( typeof exports === "undefined" ) { 753 | extend( window, QUnit.constructor.prototype ); 754 | 755 | // Expose QUnit object 756 | window.QUnit = QUnit; 757 | } 758 | 759 | // Initialize more QUnit.config and QUnit.urlParams 760 | (function() { 761 | var i, 762 | location = window.location || { search: "", protocol: "file:" }, 763 | params = location.search.slice( 1 ).split( "&" ), 764 | length = params.length, 765 | urlParams = {}, 766 | current; 767 | 768 | if ( params[ 0 ] ) { 769 | for ( i = 0; i < length; i++ ) { 770 | current = params[ i ].split( "=" ); 771 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 772 | // allow just a key to turn on a flag, e.g., test.html?noglobals 773 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 774 | urlParams[ current[ 0 ] ] = current[ 1 ]; 775 | } 776 | } 777 | 778 | QUnit.urlParams = urlParams; 779 | 780 | // String search anywhere in moduleName+testName 781 | config.filter = urlParams.filter; 782 | 783 | // Exact match of the module name 784 | config.module = urlParams.module; 785 | 786 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 787 | 788 | // Figure out if we're running the tests from a server or not 789 | QUnit.isLocal = location.protocol === "file:"; 790 | }()); 791 | 792 | // Extend QUnit object, 793 | // these after set here because they should not be exposed as global functions 794 | extend( QUnit, { 795 | assert: assert, 796 | 797 | config: config, 798 | 799 | // Initialize the configuration options 800 | init: function() { 801 | extend( config, { 802 | stats: { all: 0, bad: 0 }, 803 | moduleStats: { all: 0, bad: 0 }, 804 | started: +new Date(), 805 | updateRate: 1000, 806 | blocking: false, 807 | autostart: true, 808 | autorun: false, 809 | filter: "", 810 | queue: [], 811 | semaphore: 1 812 | }); 813 | 814 | var tests, banner, result, 815 | qunit = id( "qunit" ); 816 | 817 | if ( qunit ) { 818 | qunit.innerHTML = 819 | "

" + escapeText( document.title ) + "

" + 820 | "

" + 821 | "
" + 822 | "

" + 823 | "
    "; 824 | } 825 | 826 | tests = id( "qunit-tests" ); 827 | banner = id( "qunit-banner" ); 828 | result = id( "qunit-testresult" ); 829 | 830 | if ( tests ) { 831 | tests.innerHTML = ""; 832 | } 833 | 834 | if ( banner ) { 835 | banner.className = ""; 836 | } 837 | 838 | if ( result ) { 839 | result.parentNode.removeChild( result ); 840 | } 841 | 842 | if ( tests ) { 843 | result = document.createElement( "p" ); 844 | result.id = "qunit-testresult"; 845 | result.className = "result"; 846 | tests.parentNode.insertBefore( result, tests ); 847 | result.innerHTML = "Running...
     "; 848 | } 849 | }, 850 | 851 | // Resets the test setup. Useful for tests that modify the DOM. 852 | /* 853 | DEPRECATED: Use multiple tests instead of resetting inside a test. 854 | Use testStart or testDone for custom cleanup. 855 | This method will throw an error in 2.0, and will be removed in 2.1 856 | */ 857 | reset: function() { 858 | var fixture = id( "qunit-fixture" ); 859 | if ( fixture ) { 860 | fixture.innerHTML = config.fixture; 861 | } 862 | }, 863 | 864 | // Trigger an event on an element. 865 | // @example triggerEvent( document.body, "click" ); 866 | triggerEvent: function( elem, type, event ) { 867 | if ( document.createEvent ) { 868 | event = document.createEvent( "MouseEvents" ); 869 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 870 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 871 | 872 | elem.dispatchEvent( event ); 873 | } else if ( elem.fireEvent ) { 874 | elem.fireEvent( "on" + type ); 875 | } 876 | }, 877 | 878 | // Safe object type checking 879 | is: function( type, obj ) { 880 | return QUnit.objectType( obj ) === type; 881 | }, 882 | 883 | objectType: function( obj ) { 884 | if ( typeof obj === "undefined" ) { 885 | return "undefined"; 886 | // consider: typeof null === object 887 | } 888 | if ( obj === null ) { 889 | return "null"; 890 | } 891 | 892 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 893 | type = match && match[1] || ""; 894 | 895 | switch ( type ) { 896 | case "Number": 897 | if ( isNaN(obj) ) { 898 | return "nan"; 899 | } 900 | return "number"; 901 | case "String": 902 | case "Boolean": 903 | case "Array": 904 | case "Date": 905 | case "RegExp": 906 | case "Function": 907 | return type.toLowerCase(); 908 | } 909 | if ( typeof obj === "object" ) { 910 | return "object"; 911 | } 912 | return undefined; 913 | }, 914 | 915 | push: function( result, actual, expected, message ) { 916 | if ( !config.current ) { 917 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 918 | } 919 | 920 | var output, source, 921 | details = { 922 | module: config.current.module, 923 | name: config.current.testName, 924 | result: result, 925 | message: message, 926 | actual: actual, 927 | expected: expected 928 | }; 929 | 930 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 931 | message = "" + message + ""; 932 | output = message; 933 | 934 | if ( !result ) { 935 | expected = escapeText( QUnit.jsDump.parse(expected) ); 936 | actual = escapeText( QUnit.jsDump.parse(actual) ); 937 | output += ""; 938 | 939 | if ( actual !== expected ) { 940 | output += ""; 941 | output += ""; 942 | } 943 | 944 | source = sourceFromStacktrace(); 945 | 946 | if ( source ) { 947 | details.source = source; 948 | output += ""; 949 | } 950 | 951 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 952 | } 953 | 954 | runLoggingCallbacks( "log", QUnit, details ); 955 | 956 | config.current.assertions.push({ 957 | result: !!result, 958 | message: output 959 | }); 960 | }, 961 | 962 | pushFailure: function( message, source, actual ) { 963 | if ( !config.current ) { 964 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 965 | } 966 | 967 | var output, 968 | details = { 969 | module: config.current.module, 970 | name: config.current.testName, 971 | result: false, 972 | message: message 973 | }; 974 | 975 | message = escapeText( message ) || "error"; 976 | message = "" + message + ""; 977 | output = message; 978 | 979 | output += ""; 980 | 981 | if ( actual ) { 982 | output += ""; 983 | } 984 | 985 | if ( source ) { 986 | details.source = source; 987 | output += ""; 988 | } 989 | 990 | output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 991 | 992 | runLoggingCallbacks( "log", QUnit, details ); 993 | 994 | config.current.assertions.push({ 995 | result: false, 996 | message: output 997 | }); 998 | }, 999 | 1000 | url: function( params ) { 1001 | params = extend( extend( {}, QUnit.urlParams ), params ); 1002 | var key, 1003 | querystring = "?"; 1004 | 1005 | for ( key in params ) { 1006 | if ( hasOwn.call( params, key ) ) { 1007 | querystring += encodeURIComponent( key ) + "=" + 1008 | encodeURIComponent( params[ key ] ) + "&"; 1009 | } 1010 | } 1011 | return window.location.protocol + "//" + window.location.host + 1012 | window.location.pathname + querystring.slice( 0, -1 ); 1013 | }, 1014 | 1015 | extend: extend, 1016 | id: id, 1017 | addEvent: addEvent, 1018 | addClass: addClass, 1019 | hasClass: hasClass, 1020 | removeClass: removeClass 1021 | // load, equiv, jsDump, diff: Attached later 1022 | }); 1023 | 1024 | /** 1025 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 1026 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 1027 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 1028 | * Doing this allows us to tell if the following methods have been overwritten on the actual 1029 | * QUnit object. 1030 | */ 1031 | extend( QUnit.constructor.prototype, { 1032 | 1033 | // Logging callbacks; all receive a single argument with the listed properties 1034 | // run test/logs.html for any related changes 1035 | begin: registerLoggingCallback( "begin" ), 1036 | 1037 | // done: { failed, passed, total, runtime } 1038 | done: registerLoggingCallback( "done" ), 1039 | 1040 | // log: { result, actual, expected, message } 1041 | log: registerLoggingCallback( "log" ), 1042 | 1043 | // testStart: { name } 1044 | testStart: registerLoggingCallback( "testStart" ), 1045 | 1046 | // testDone: { name, failed, passed, total, duration } 1047 | testDone: registerLoggingCallback( "testDone" ), 1048 | 1049 | // moduleStart: { name } 1050 | moduleStart: registerLoggingCallback( "moduleStart" ), 1051 | 1052 | // moduleDone: { name, failed, passed, total } 1053 | moduleDone: registerLoggingCallback( "moduleDone" ) 1054 | }); 1055 | 1056 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 1057 | config.autorun = true; 1058 | } 1059 | 1060 | QUnit.load = function() { 1061 | runLoggingCallbacks( "begin", QUnit, {} ); 1062 | 1063 | // Initialize the config, saving the execution queue 1064 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, 1065 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, 1066 | numModules = 0, 1067 | moduleNames = [], 1068 | moduleFilterHtml = "", 1069 | urlConfigHtml = "", 1070 | oldconfig = extend( {}, config ); 1071 | 1072 | QUnit.init(); 1073 | extend(config, oldconfig); 1074 | 1075 | config.blocking = false; 1076 | 1077 | len = config.urlConfig.length; 1078 | 1079 | for ( i = 0; i < len; i++ ) { 1080 | val = config.urlConfig[i]; 1081 | if ( typeof val === "string" ) { 1082 | val = { 1083 | id: val, 1084 | label: val, 1085 | tooltip: "[no tooltip available]" 1086 | }; 1087 | } 1088 | config[ val.id ] = QUnit.urlParams[ val.id ]; 1089 | urlConfigHtml += ""; 1095 | } 1096 | for ( i in config.modules ) { 1097 | if ( config.modules.hasOwnProperty( i ) ) { 1098 | moduleNames.push(i); 1099 | } 1100 | } 1101 | numModules = moduleNames.length; 1102 | moduleNames.sort( function( a, b ) { 1103 | return a.localeCompare( b ); 1104 | }); 1105 | moduleFilterHtml += ""; 1116 | 1117 | // `userAgent` initialized at top of scope 1118 | userAgent = id( "qunit-userAgent" ); 1119 | if ( userAgent ) { 1120 | userAgent.innerHTML = navigator.userAgent; 1121 | } 1122 | 1123 | // `banner` initialized at top of scope 1124 | banner = id( "qunit-header" ); 1125 | if ( banner ) { 1126 | banner.innerHTML = "" + banner.innerHTML + " "; 1127 | } 1128 | 1129 | // `toolbar` initialized at top of scope 1130 | toolbar = id( "qunit-testrunner-toolbar" ); 1131 | if ( toolbar ) { 1132 | // `filter` initialized at top of scope 1133 | filter = document.createElement( "input" ); 1134 | filter.type = "checkbox"; 1135 | filter.id = "qunit-filter-pass"; 1136 | 1137 | addEvent( filter, "click", function() { 1138 | var tmp, 1139 | ol = document.getElementById( "qunit-tests" ); 1140 | 1141 | if ( filter.checked ) { 1142 | ol.className = ol.className + " hidepass"; 1143 | } else { 1144 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 1145 | ol.className = tmp.replace( / hidepass /, " " ); 1146 | } 1147 | if ( defined.sessionStorage ) { 1148 | if (filter.checked) { 1149 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 1150 | } else { 1151 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 1152 | } 1153 | } 1154 | }); 1155 | 1156 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1157 | filter.checked = true; 1158 | // `ol` initialized at top of scope 1159 | ol = document.getElementById( "qunit-tests" ); 1160 | ol.className = ol.className + " hidepass"; 1161 | } 1162 | toolbar.appendChild( filter ); 1163 | 1164 | // `label` initialized at top of scope 1165 | label = document.createElement( "label" ); 1166 | label.setAttribute( "for", "qunit-filter-pass" ); 1167 | label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); 1168 | label.innerHTML = "Hide passed tests"; 1169 | toolbar.appendChild( label ); 1170 | 1171 | urlConfigCheckboxesContainer = document.createElement("span"); 1172 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; 1173 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); 1174 | // For oldIE support: 1175 | // * Add handlers to the individual elements instead of the container 1176 | // * Use "click" instead of "change" 1177 | // * Fallback from event.target to event.srcElement 1178 | addEvents( urlConfigCheckboxes, "click", function( event ) { 1179 | var params = {}, 1180 | target = event.target || event.srcElement; 1181 | params[ target.name ] = target.checked ? true : undefined; 1182 | window.location = QUnit.url( params ); 1183 | }); 1184 | toolbar.appendChild( urlConfigCheckboxesContainer ); 1185 | 1186 | if (numModules > 1) { 1187 | moduleFilter = document.createElement( "span" ); 1188 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); 1189 | moduleFilter.innerHTML = moduleFilterHtml; 1190 | addEvent( moduleFilter.lastChild, "change", function() { 1191 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1192 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1193 | 1194 | window.location = QUnit.url({ 1195 | module: ( selectedModule === "" ) ? undefined : selectedModule, 1196 | // Remove any existing filters 1197 | filter: undefined, 1198 | testNumber: undefined 1199 | }); 1200 | }); 1201 | toolbar.appendChild(moduleFilter); 1202 | } 1203 | } 1204 | 1205 | // `main` initialized at top of scope 1206 | main = id( "qunit-fixture" ); 1207 | if ( main ) { 1208 | config.fixture = main.innerHTML; 1209 | } 1210 | 1211 | if ( config.autostart ) { 1212 | QUnit.start(); 1213 | } 1214 | }; 1215 | 1216 | addEvent( window, "load", QUnit.load ); 1217 | 1218 | // `onErrorFnPrev` initialized at top of scope 1219 | // Preserve other handlers 1220 | onErrorFnPrev = window.onerror; 1221 | 1222 | // Cover uncaught exceptions 1223 | // Returning true will suppress the default browser handler, 1224 | // returning false will let it run. 1225 | window.onerror = function ( error, filePath, linerNr ) { 1226 | var ret = false; 1227 | if ( onErrorFnPrev ) { 1228 | ret = onErrorFnPrev( error, filePath, linerNr ); 1229 | } 1230 | 1231 | // Treat return value as window.onerror itself does, 1232 | // Only do our handling if not suppressed. 1233 | if ( ret !== true ) { 1234 | if ( QUnit.config.current ) { 1235 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1236 | return true; 1237 | } 1238 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1239 | } else { 1240 | QUnit.test( "global failure", extend( function() { 1241 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1242 | }, { validTest: validTest } ) ); 1243 | } 1244 | return false; 1245 | } 1246 | 1247 | return ret; 1248 | }; 1249 | 1250 | function done() { 1251 | config.autorun = true; 1252 | 1253 | // Log the last module results 1254 | if ( config.currentModule ) { 1255 | runLoggingCallbacks( "moduleDone", QUnit, { 1256 | name: config.currentModule, 1257 | failed: config.moduleStats.bad, 1258 | passed: config.moduleStats.all - config.moduleStats.bad, 1259 | total: config.moduleStats.all 1260 | }); 1261 | } 1262 | delete config.previousModule; 1263 | 1264 | var i, key, 1265 | banner = id( "qunit-banner" ), 1266 | tests = id( "qunit-tests" ), 1267 | runtime = +new Date() - config.started, 1268 | passed = config.stats.all - config.stats.bad, 1269 | html = [ 1270 | "Tests completed in ", 1271 | runtime, 1272 | " milliseconds.
    ", 1273 | "", 1274 | passed, 1275 | " assertions of ", 1276 | config.stats.all, 1277 | " passed, ", 1278 | config.stats.bad, 1279 | " failed." 1280 | ].join( "" ); 1281 | 1282 | if ( banner ) { 1283 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1284 | } 1285 | 1286 | if ( tests ) { 1287 | id( "qunit-testresult" ).innerHTML = html; 1288 | } 1289 | 1290 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1291 | // show ✖ for good, ✔ for bad suite result in title 1292 | // use escape sequences in case file gets loaded with non-utf-8-charset 1293 | document.title = [ 1294 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1295 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1296 | ].join( " " ); 1297 | } 1298 | 1299 | // clear own sessionStorage items if all tests passed 1300 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1301 | // `key` & `i` initialized at top of scope 1302 | for ( i = 0; i < sessionStorage.length; i++ ) { 1303 | key = sessionStorage.key( i++ ); 1304 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1305 | sessionStorage.removeItem( key ); 1306 | } 1307 | } 1308 | } 1309 | 1310 | // scroll back to top to show results 1311 | if ( window.scrollTo ) { 1312 | window.scrollTo(0, 0); 1313 | } 1314 | 1315 | runLoggingCallbacks( "done", QUnit, { 1316 | failed: config.stats.bad, 1317 | passed: passed, 1318 | total: config.stats.all, 1319 | runtime: runtime 1320 | }); 1321 | } 1322 | 1323 | /** @return Boolean: true if this test should be ran */ 1324 | function validTest( test ) { 1325 | var include, 1326 | filter = config.filter && config.filter.toLowerCase(), 1327 | module = config.module && config.module.toLowerCase(), 1328 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1329 | 1330 | // Internally-generated tests are always valid 1331 | if ( test.callback && test.callback.validTest === validTest ) { 1332 | delete test.callback.validTest; 1333 | return true; 1334 | } 1335 | 1336 | if ( config.testNumber ) { 1337 | return test.testNumber === config.testNumber; 1338 | } 1339 | 1340 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1341 | return false; 1342 | } 1343 | 1344 | if ( !filter ) { 1345 | return true; 1346 | } 1347 | 1348 | include = filter.charAt( 0 ) !== "!"; 1349 | if ( !include ) { 1350 | filter = filter.slice( 1 ); 1351 | } 1352 | 1353 | // If the filter matches, we need to honour include 1354 | if ( fullName.indexOf( filter ) !== -1 ) { 1355 | return include; 1356 | } 1357 | 1358 | // Otherwise, do the opposite 1359 | return !include; 1360 | } 1361 | 1362 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1363 | // Later Safari and IE10 are supposed to support error.stack as well 1364 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1365 | function extractStacktrace( e, offset ) { 1366 | offset = offset === undefined ? 3 : offset; 1367 | 1368 | var stack, include, i; 1369 | 1370 | if ( e.stacktrace ) { 1371 | // Opera 1372 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1373 | } else if ( e.stack ) { 1374 | // Firefox, Chrome 1375 | stack = e.stack.split( "\n" ); 1376 | if (/^error$/i.test( stack[0] ) ) { 1377 | stack.shift(); 1378 | } 1379 | if ( fileName ) { 1380 | include = []; 1381 | for ( i = offset; i < stack.length; i++ ) { 1382 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 1383 | break; 1384 | } 1385 | include.push( stack[ i ] ); 1386 | } 1387 | if ( include.length ) { 1388 | return include.join( "\n" ); 1389 | } 1390 | } 1391 | return stack[ offset ]; 1392 | } else if ( e.sourceURL ) { 1393 | // Safari, PhantomJS 1394 | // hopefully one day Safari provides actual stacktraces 1395 | // exclude useless self-reference for generated Error objects 1396 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1397 | return; 1398 | } 1399 | // for actual exceptions, this is useful 1400 | return e.sourceURL + ":" + e.line; 1401 | } 1402 | } 1403 | function sourceFromStacktrace( offset ) { 1404 | try { 1405 | throw new Error(); 1406 | } catch ( e ) { 1407 | return extractStacktrace( e, offset ); 1408 | } 1409 | } 1410 | 1411 | /** 1412 | * Escape text for attribute or text content. 1413 | */ 1414 | function escapeText( s ) { 1415 | if ( !s ) { 1416 | return ""; 1417 | } 1418 | s = s + ""; 1419 | // Both single quotes and double quotes (for attributes) 1420 | return s.replace( /['"<>&]/g, function( s ) { 1421 | switch( s ) { 1422 | case "'": 1423 | return "'"; 1424 | case "\"": 1425 | return """; 1426 | case "<": 1427 | return "<"; 1428 | case ">": 1429 | return ">"; 1430 | case "&": 1431 | return "&"; 1432 | } 1433 | }); 1434 | } 1435 | 1436 | function synchronize( callback, last ) { 1437 | config.queue.push( callback ); 1438 | 1439 | if ( config.autorun && !config.blocking ) { 1440 | process( last ); 1441 | } 1442 | } 1443 | 1444 | function process( last ) { 1445 | function next() { 1446 | process( last ); 1447 | } 1448 | var start = new Date().getTime(); 1449 | config.depth = config.depth ? config.depth + 1 : 1; 1450 | 1451 | while ( config.queue.length && !config.blocking ) { 1452 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1453 | config.queue.shift()(); 1454 | } else { 1455 | setTimeout( next, 13 ); 1456 | break; 1457 | } 1458 | } 1459 | config.depth--; 1460 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1461 | done(); 1462 | } 1463 | } 1464 | 1465 | function saveGlobal() { 1466 | config.pollution = []; 1467 | 1468 | if ( config.noglobals ) { 1469 | for ( var key in window ) { 1470 | if ( hasOwn.call( window, key ) ) { 1471 | // in Opera sometimes DOM element ids show up here, ignore them 1472 | if ( /^qunit-test-output/.test( key ) ) { 1473 | continue; 1474 | } 1475 | config.pollution.push( key ); 1476 | } 1477 | } 1478 | } 1479 | } 1480 | 1481 | function checkPollution() { 1482 | var newGlobals, 1483 | deletedGlobals, 1484 | old = config.pollution; 1485 | 1486 | saveGlobal(); 1487 | 1488 | newGlobals = diff( config.pollution, old ); 1489 | if ( newGlobals.length > 0 ) { 1490 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1491 | } 1492 | 1493 | deletedGlobals = diff( old, config.pollution ); 1494 | if ( deletedGlobals.length > 0 ) { 1495 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1496 | } 1497 | } 1498 | 1499 | // returns a new Array with the elements that are in a but not in b 1500 | function diff( a, b ) { 1501 | var i, j, 1502 | result = a.slice(); 1503 | 1504 | for ( i = 0; i < result.length; i++ ) { 1505 | for ( j = 0; j < b.length; j++ ) { 1506 | if ( result[i] === b[j] ) { 1507 | result.splice( i, 1 ); 1508 | i--; 1509 | break; 1510 | } 1511 | } 1512 | } 1513 | return result; 1514 | } 1515 | 1516 | function extend( a, b ) { 1517 | for ( var prop in b ) { 1518 | if ( hasOwn.call( b, prop ) ) { 1519 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 1520 | if ( !( prop === "constructor" && a === window ) ) { 1521 | if ( b[ prop ] === undefined ) { 1522 | delete a[ prop ]; 1523 | } else { 1524 | a[ prop ] = b[ prop ]; 1525 | } 1526 | } 1527 | } 1528 | } 1529 | 1530 | return a; 1531 | } 1532 | 1533 | /** 1534 | * @param {HTMLElement} elem 1535 | * @param {string} type 1536 | * @param {Function} fn 1537 | */ 1538 | function addEvent( elem, type, fn ) { 1539 | // Standards-based browsers 1540 | if ( elem.addEventListener ) { 1541 | elem.addEventListener( type, fn, false ); 1542 | // IE 1543 | } else { 1544 | elem.attachEvent( "on" + type, fn ); 1545 | } 1546 | } 1547 | 1548 | /** 1549 | * @param {Array|NodeList} elems 1550 | * @param {string} type 1551 | * @param {Function} fn 1552 | */ 1553 | function addEvents( elems, type, fn ) { 1554 | var i = elems.length; 1555 | while ( i-- ) { 1556 | addEvent( elems[i], type, fn ); 1557 | } 1558 | } 1559 | 1560 | function hasClass( elem, name ) { 1561 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1562 | } 1563 | 1564 | function addClass( elem, name ) { 1565 | if ( !hasClass( elem, name ) ) { 1566 | elem.className += (elem.className ? " " : "") + name; 1567 | } 1568 | } 1569 | 1570 | function removeClass( elem, name ) { 1571 | var set = " " + elem.className + " "; 1572 | // Class name may appear multiple times 1573 | while ( set.indexOf(" " + name + " ") > -1 ) { 1574 | set = set.replace(" " + name + " " , " "); 1575 | } 1576 | // If possible, trim it for prettiness, but not necessarily 1577 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); 1578 | } 1579 | 1580 | function id( name ) { 1581 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1582 | document.getElementById( name ); 1583 | } 1584 | 1585 | function registerLoggingCallback( key ) { 1586 | return function( callback ) { 1587 | config[key].push( callback ); 1588 | }; 1589 | } 1590 | 1591 | // Supports deprecated method of completely overwriting logging callbacks 1592 | function runLoggingCallbacks( key, scope, args ) { 1593 | var i, callbacks; 1594 | if ( QUnit.hasOwnProperty( key ) ) { 1595 | QUnit[ key ].call(scope, args ); 1596 | } else { 1597 | callbacks = config[ key ]; 1598 | for ( i = 0; i < callbacks.length; i++ ) { 1599 | callbacks[ i ].call( scope, args ); 1600 | } 1601 | } 1602 | } 1603 | 1604 | // Test for equality any JavaScript type. 1605 | // Author: Philippe Rathé 1606 | QUnit.equiv = (function() { 1607 | 1608 | // Call the o related callback with the given arguments. 1609 | function bindCallbacks( o, callbacks, args ) { 1610 | var prop = QUnit.objectType( o ); 1611 | if ( prop ) { 1612 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1613 | return callbacks[ prop ].apply( callbacks, args ); 1614 | } else { 1615 | return callbacks[ prop ]; // or undefined 1616 | } 1617 | } 1618 | } 1619 | 1620 | // the real equiv function 1621 | var innerEquiv, 1622 | // stack to decide between skip/abort functions 1623 | callers = [], 1624 | // stack to avoiding loops from circular referencing 1625 | parents = [], 1626 | parentsB = [], 1627 | 1628 | getProto = Object.getPrototypeOf || function ( obj ) { 1629 | /*jshint camelcase:false */ 1630 | return obj.__proto__; 1631 | }, 1632 | callbacks = (function () { 1633 | 1634 | // for string, boolean, number and null 1635 | function useStrictEquality( b, a ) { 1636 | /*jshint eqeqeq:false */ 1637 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1638 | // to catch short annotation VS 'new' annotation of a 1639 | // declaration 1640 | // e.g. var i = 1; 1641 | // var j = new Number(1); 1642 | return a == b; 1643 | } else { 1644 | return a === b; 1645 | } 1646 | } 1647 | 1648 | return { 1649 | "string": useStrictEquality, 1650 | "boolean": useStrictEquality, 1651 | "number": useStrictEquality, 1652 | "null": useStrictEquality, 1653 | "undefined": useStrictEquality, 1654 | 1655 | "nan": function( b ) { 1656 | return isNaN( b ); 1657 | }, 1658 | 1659 | "date": function( b, a ) { 1660 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1661 | }, 1662 | 1663 | "regexp": function( b, a ) { 1664 | return QUnit.objectType( b ) === "regexp" && 1665 | // the regex itself 1666 | a.source === b.source && 1667 | // and its modifiers 1668 | a.global === b.global && 1669 | // (gmi) ... 1670 | a.ignoreCase === b.ignoreCase && 1671 | a.multiline === b.multiline && 1672 | a.sticky === b.sticky; 1673 | }, 1674 | 1675 | // - skip when the property is a method of an instance (OOP) 1676 | // - abort otherwise, 1677 | // initial === would have catch identical references anyway 1678 | "function": function() { 1679 | var caller = callers[callers.length - 1]; 1680 | return caller !== Object && typeof caller !== "undefined"; 1681 | }, 1682 | 1683 | "array": function( b, a ) { 1684 | var i, j, len, loop, aCircular, bCircular; 1685 | 1686 | // b could be an object literal here 1687 | if ( QUnit.objectType( b ) !== "array" ) { 1688 | return false; 1689 | } 1690 | 1691 | len = a.length; 1692 | if ( len !== b.length ) { 1693 | // safe and faster 1694 | return false; 1695 | } 1696 | 1697 | // track reference to avoid circular references 1698 | parents.push( a ); 1699 | parentsB.push( b ); 1700 | for ( i = 0; i < len; i++ ) { 1701 | loop = false; 1702 | for ( j = 0; j < parents.length; j++ ) { 1703 | aCircular = parents[j] === a[i]; 1704 | bCircular = parentsB[j] === b[i]; 1705 | if ( aCircular || bCircular ) { 1706 | if ( a[i] === b[i] || aCircular && bCircular ) { 1707 | loop = true; 1708 | } else { 1709 | parents.pop(); 1710 | parentsB.pop(); 1711 | return false; 1712 | } 1713 | } 1714 | } 1715 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1716 | parents.pop(); 1717 | parentsB.pop(); 1718 | return false; 1719 | } 1720 | } 1721 | parents.pop(); 1722 | parentsB.pop(); 1723 | return true; 1724 | }, 1725 | 1726 | "object": function( b, a ) { 1727 | /*jshint forin:false */ 1728 | var i, j, loop, aCircular, bCircular, 1729 | // Default to true 1730 | eq = true, 1731 | aProperties = [], 1732 | bProperties = []; 1733 | 1734 | // comparing constructors is more strict than using 1735 | // instanceof 1736 | if ( a.constructor !== b.constructor ) { 1737 | // Allow objects with no prototype to be equivalent to 1738 | // objects with Object as their constructor. 1739 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1740 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1741 | return false; 1742 | } 1743 | } 1744 | 1745 | // stack constructor before traversing properties 1746 | callers.push( a.constructor ); 1747 | 1748 | // track reference to avoid circular references 1749 | parents.push( a ); 1750 | parentsB.push( b ); 1751 | 1752 | // be strict: don't ensure hasOwnProperty and go deep 1753 | for ( i in a ) { 1754 | loop = false; 1755 | for ( j = 0; j < parents.length; j++ ) { 1756 | aCircular = parents[j] === a[i]; 1757 | bCircular = parentsB[j] === b[i]; 1758 | if ( aCircular || bCircular ) { 1759 | if ( a[i] === b[i] || aCircular && bCircular ) { 1760 | loop = true; 1761 | } else { 1762 | eq = false; 1763 | break; 1764 | } 1765 | } 1766 | } 1767 | aProperties.push(i); 1768 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1769 | eq = false; 1770 | break; 1771 | } 1772 | } 1773 | 1774 | parents.pop(); 1775 | parentsB.pop(); 1776 | callers.pop(); // unstack, we are done 1777 | 1778 | for ( i in b ) { 1779 | bProperties.push( i ); // collect b's properties 1780 | } 1781 | 1782 | // Ensures identical properties name 1783 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1784 | } 1785 | }; 1786 | }()); 1787 | 1788 | innerEquiv = function() { // can take multiple arguments 1789 | var args = [].slice.apply( arguments ); 1790 | if ( args.length < 2 ) { 1791 | return true; // end transition 1792 | } 1793 | 1794 | return (function( a, b ) { 1795 | if ( a === b ) { 1796 | return true; // catch the most you can 1797 | } else if ( a === null || b === null || typeof a === "undefined" || 1798 | typeof b === "undefined" || 1799 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1800 | return false; // don't lose time with error prone cases 1801 | } else { 1802 | return bindCallbacks(a, callbacks, [ b, a ]); 1803 | } 1804 | 1805 | // apply transition with (1..n) arguments 1806 | }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); 1807 | }; 1808 | 1809 | return innerEquiv; 1810 | }()); 1811 | 1812 | /** 1813 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1814 | * http://flesler.blogspot.com Licensed under BSD 1815 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1816 | * 1817 | * @projectDescription Advanced and extensible data dumping for Javascript. 1818 | * @version 1.0.0 1819 | * @author Ariel Flesler 1820 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1821 | */ 1822 | QUnit.jsDump = (function() { 1823 | function quote( str ) { 1824 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; 1825 | } 1826 | function literal( o ) { 1827 | return o + ""; 1828 | } 1829 | function join( pre, arr, post ) { 1830 | var s = jsDump.separator(), 1831 | base = jsDump.indent(), 1832 | inner = jsDump.indent(1); 1833 | if ( arr.join ) { 1834 | arr = arr.join( "," + s + inner ); 1835 | } 1836 | if ( !arr ) { 1837 | return pre + post; 1838 | } 1839 | return [ pre, inner + arr, base + post ].join(s); 1840 | } 1841 | function array( arr, stack ) { 1842 | var i = arr.length, ret = new Array(i); 1843 | this.up(); 1844 | while ( i-- ) { 1845 | ret[i] = this.parse( arr[i] , undefined , stack); 1846 | } 1847 | this.down(); 1848 | return join( "[", ret, "]" ); 1849 | } 1850 | 1851 | var reName = /^function (\w+)/, 1852 | jsDump = { 1853 | // type is used mostly internally, you can fix a (custom)type in advance 1854 | parse: function( obj, type, stack ) { 1855 | stack = stack || [ ]; 1856 | var inStack, res, 1857 | parser = this.parsers[ type || this.typeOf(obj) ]; 1858 | 1859 | type = typeof parser; 1860 | inStack = inArray( obj, stack ); 1861 | 1862 | if ( inStack !== -1 ) { 1863 | return "recursion(" + (inStack - stack.length) + ")"; 1864 | } 1865 | if ( type === "function" ) { 1866 | stack.push( obj ); 1867 | res = parser.call( this, obj, stack ); 1868 | stack.pop(); 1869 | return res; 1870 | } 1871 | return ( type === "string" ) ? parser : this.parsers.error; 1872 | }, 1873 | typeOf: function( obj ) { 1874 | var type; 1875 | if ( obj === null ) { 1876 | type = "null"; 1877 | } else if ( typeof obj === "undefined" ) { 1878 | type = "undefined"; 1879 | } else if ( QUnit.is( "regexp", obj) ) { 1880 | type = "regexp"; 1881 | } else if ( QUnit.is( "date", obj) ) { 1882 | type = "date"; 1883 | } else if ( QUnit.is( "function", obj) ) { 1884 | type = "function"; 1885 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1886 | type = "window"; 1887 | } else if ( obj.nodeType === 9 ) { 1888 | type = "document"; 1889 | } else if ( obj.nodeType ) { 1890 | type = "node"; 1891 | } else if ( 1892 | // native arrays 1893 | toString.call( obj ) === "[object Array]" || 1894 | // NodeList objects 1895 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1896 | ) { 1897 | type = "array"; 1898 | } else if ( obj.constructor === Error.prototype.constructor ) { 1899 | type = "error"; 1900 | } else { 1901 | type = typeof obj; 1902 | } 1903 | return type; 1904 | }, 1905 | separator: function() { 1906 | return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; 1907 | }, 1908 | // extra can be a number, shortcut for increasing-calling-decreasing 1909 | indent: function( extra ) { 1910 | if ( !this.multiline ) { 1911 | return ""; 1912 | } 1913 | var chr = this.indentChar; 1914 | if ( this.HTML ) { 1915 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1916 | } 1917 | return new Array( this.depth + ( extra || 0 ) ).join(chr); 1918 | }, 1919 | up: function( a ) { 1920 | this.depth += a || 1; 1921 | }, 1922 | down: function( a ) { 1923 | this.depth -= a || 1; 1924 | }, 1925 | setParser: function( name, parser ) { 1926 | this.parsers[name] = parser; 1927 | }, 1928 | // The next 3 are exposed so you can use them 1929 | quote: quote, 1930 | literal: literal, 1931 | join: join, 1932 | // 1933 | depth: 1, 1934 | // This is the list of parsers, to modify them, use jsDump.setParser 1935 | parsers: { 1936 | window: "[Window]", 1937 | document: "[Document]", 1938 | error: function(error) { 1939 | return "Error(\"" + error.message + "\")"; 1940 | }, 1941 | unknown: "[Unknown]", 1942 | "null": "null", 1943 | "undefined": "undefined", 1944 | "function": function( fn ) { 1945 | var ret = "function", 1946 | // functions never have name in IE 1947 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 1948 | 1949 | if ( name ) { 1950 | ret += " " + name; 1951 | } 1952 | ret += "( "; 1953 | 1954 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1955 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1956 | }, 1957 | array: array, 1958 | nodelist: array, 1959 | "arguments": array, 1960 | object: function( map, stack ) { 1961 | /*jshint forin:false */ 1962 | var ret = [ ], keys, key, val, i; 1963 | QUnit.jsDump.up(); 1964 | keys = []; 1965 | for ( key in map ) { 1966 | keys.push( key ); 1967 | } 1968 | keys.sort(); 1969 | for ( i = 0; i < keys.length; i++ ) { 1970 | key = keys[ i ]; 1971 | val = map[ key ]; 1972 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1973 | } 1974 | QUnit.jsDump.down(); 1975 | return join( "{", ret, "}" ); 1976 | }, 1977 | node: function( node ) { 1978 | var len, i, val, 1979 | open = QUnit.jsDump.HTML ? "<" : "<", 1980 | close = QUnit.jsDump.HTML ? ">" : ">", 1981 | tag = node.nodeName.toLowerCase(), 1982 | ret = open + tag, 1983 | attrs = node.attributes; 1984 | 1985 | if ( attrs ) { 1986 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1987 | val = attrs[i].nodeValue; 1988 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1989 | // Those have values like undefined, null, 0, false, "" or "inherit". 1990 | if ( val && val !== "inherit" ) { 1991 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 1992 | } 1993 | } 1994 | } 1995 | ret += close; 1996 | 1997 | // Show content of TextNode or CDATASection 1998 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1999 | ret += node.nodeValue; 2000 | } 2001 | 2002 | return ret + open + "/" + tag + close; 2003 | }, 2004 | // function calls it internally, it's the arguments part of the function 2005 | functionArgs: function( fn ) { 2006 | var args, 2007 | l = fn.length; 2008 | 2009 | if ( !l ) { 2010 | return ""; 2011 | } 2012 | 2013 | args = new Array(l); 2014 | while ( l-- ) { 2015 | // 97 is 'a' 2016 | args[l] = String.fromCharCode(97+l); 2017 | } 2018 | return " " + args.join( ", " ) + " "; 2019 | }, 2020 | // object calls it internally, the key part of an item in a map 2021 | key: quote, 2022 | // function calls it internally, it's the content of the function 2023 | functionCode: "[code]", 2024 | // node calls it internally, it's an html attribute value 2025 | attribute: quote, 2026 | string: quote, 2027 | date: quote, 2028 | regexp: literal, 2029 | number: literal, 2030 | "boolean": literal 2031 | }, 2032 | // if true, entities are escaped ( <, >, \t, space and \n ) 2033 | HTML: false, 2034 | // indentation unit 2035 | indentChar: " ", 2036 | // if true, items in a collection, are separated by a \n, else just a space. 2037 | multiline: true 2038 | }; 2039 | 2040 | return jsDump; 2041 | }()); 2042 | 2043 | // from jquery.js 2044 | function inArray( elem, array ) { 2045 | if ( array.indexOf ) { 2046 | return array.indexOf( elem ); 2047 | } 2048 | 2049 | for ( var i = 0, length = array.length; i < length; i++ ) { 2050 | if ( array[ i ] === elem ) { 2051 | return i; 2052 | } 2053 | } 2054 | 2055 | return -1; 2056 | } 2057 | 2058 | /* 2059 | * Javascript Diff Algorithm 2060 | * By John Resig (http://ejohn.org/) 2061 | * Modified by Chu Alan "sprite" 2062 | * 2063 | * Released under the MIT license. 2064 | * 2065 | * More Info: 2066 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2067 | * 2068 | * Usage: QUnit.diff(expected, actual) 2069 | * 2070 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2071 | */ 2072 | QUnit.diff = (function() { 2073 | /*jshint eqeqeq:false, eqnull:true */ 2074 | function diff( o, n ) { 2075 | var i, 2076 | ns = {}, 2077 | os = {}; 2078 | 2079 | for ( i = 0; i < n.length; i++ ) { 2080 | if ( !hasOwn.call( ns, n[i] ) ) { 2081 | ns[ n[i] ] = { 2082 | rows: [], 2083 | o: null 2084 | }; 2085 | } 2086 | ns[ n[i] ].rows.push( i ); 2087 | } 2088 | 2089 | for ( i = 0; i < o.length; i++ ) { 2090 | if ( !hasOwn.call( os, o[i] ) ) { 2091 | os[ o[i] ] = { 2092 | rows: [], 2093 | n: null 2094 | }; 2095 | } 2096 | os[ o[i] ].rows.push( i ); 2097 | } 2098 | 2099 | for ( i in ns ) { 2100 | if ( hasOwn.call( ns, i ) ) { 2101 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { 2102 | n[ ns[i].rows[0] ] = { 2103 | text: n[ ns[i].rows[0] ], 2104 | row: os[i].rows[0] 2105 | }; 2106 | o[ os[i].rows[0] ] = { 2107 | text: o[ os[i].rows[0] ], 2108 | row: ns[i].rows[0] 2109 | }; 2110 | } 2111 | } 2112 | } 2113 | 2114 | for ( i = 0; i < n.length - 1; i++ ) { 2115 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 2116 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 2117 | 2118 | n[ i + 1 ] = { 2119 | text: n[ i + 1 ], 2120 | row: n[i].row + 1 2121 | }; 2122 | o[ n[i].row + 1 ] = { 2123 | text: o[ n[i].row + 1 ], 2124 | row: i + 1 2125 | }; 2126 | } 2127 | } 2128 | 2129 | for ( i = n.length - 1; i > 0; i-- ) { 2130 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 2131 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 2132 | 2133 | n[ i - 1 ] = { 2134 | text: n[ i - 1 ], 2135 | row: n[i].row - 1 2136 | }; 2137 | o[ n[i].row - 1 ] = { 2138 | text: o[ n[i].row - 1 ], 2139 | row: i - 1 2140 | }; 2141 | } 2142 | } 2143 | 2144 | return { 2145 | o: o, 2146 | n: n 2147 | }; 2148 | } 2149 | 2150 | return function( o, n ) { 2151 | o = o.replace( /\s+$/, "" ); 2152 | n = n.replace( /\s+$/, "" ); 2153 | 2154 | var i, pre, 2155 | str = "", 2156 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 2157 | oSpace = o.match(/\s+/g), 2158 | nSpace = n.match(/\s+/g); 2159 | 2160 | if ( oSpace == null ) { 2161 | oSpace = [ " " ]; 2162 | } 2163 | else { 2164 | oSpace.push( " " ); 2165 | } 2166 | 2167 | if ( nSpace == null ) { 2168 | nSpace = [ " " ]; 2169 | } 2170 | else { 2171 | nSpace.push( " " ); 2172 | } 2173 | 2174 | if ( out.n.length === 0 ) { 2175 | for ( i = 0; i < out.o.length; i++ ) { 2176 | str += "" + out.o[i] + oSpace[i] + ""; 2177 | } 2178 | } 2179 | else { 2180 | if ( out.n[0].text == null ) { 2181 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 2182 | str += "" + out.o[n] + oSpace[n] + ""; 2183 | } 2184 | } 2185 | 2186 | for ( i = 0; i < out.n.length; i++ ) { 2187 | if (out.n[i].text == null) { 2188 | str += "" + out.n[i] + nSpace[i] + ""; 2189 | } 2190 | else { 2191 | // `pre` initialized at top of scope 2192 | pre = ""; 2193 | 2194 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 2195 | pre += "" + out.o[n] + oSpace[n] + ""; 2196 | } 2197 | str += " " + out.n[i].text + nSpace[i] + pre; 2198 | } 2199 | } 2200 | } 2201 | 2202 | return str; 2203 | }; 2204 | }()); 2205 | 2206 | // for CommonJS environments, export everything 2207 | if ( typeof exports !== "undefined" ) { 2208 | extend( exports, QUnit.constructor.prototype ); 2209 | } 2210 | 2211 | // get at whatever the global object is, like window in browsers 2212 | }( (function() {return this;}.call()) )); 2213 | -------------------------------------------------------------------------------- /test/vendor/runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QtWebKit-powered headless test runner using PhantomJS 3 | * 4 | * PhantomJS binaries: http://phantomjs.org/download.html 5 | * Requires PhantomJS 1.6+ (1.7+ recommended) 6 | * 7 | * Run with: 8 | * phantomjs runner.js [url-of-your-qunit-testsuite] 9 | * 10 | * e.g. 11 | * phantomjs runner.js http://localhost/qunit/test/index.html 12 | */ 13 | 14 | /*jshint latedef:false */ 15 | /*global phantom:false, require:false, console:false, window:false, QUnit:false */ 16 | 17 | (function() { 18 | 'use strict'; 19 | 20 | var args = require('system').args; 21 | 22 | // arg[0]: scriptName, args[1...]: arguments 23 | if (args.length !== 2) { 24 | console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite]'); 25 | phantom.exit(1); 26 | } 27 | 28 | var url = args[1], 29 | page = require('webpage').create(); 30 | 31 | // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) 32 | page.onConsoleMessage = function(msg) { 33 | console.log(msg); 34 | }; 35 | 36 | page.onInitialized = function() { 37 | page.evaluate(addPolyfills); 38 | page.evaluate(addLogging); 39 | }; 40 | 41 | page.onCallback = function(message) { 42 | var result, 43 | failed; 44 | 45 | if (message) { 46 | if (message.name === 'QUnit.done') { 47 | result = message.data; 48 | failed = !result || result.failed; 49 | 50 | phantom.exit(failed ? 1 : 0); 51 | } 52 | } 53 | }; 54 | 55 | page.open(url, function(status) { 56 | if (status !== 'success') { 57 | console.error('Unable to access network: ' + status); 58 | phantom.exit(1); 59 | } else { 60 | // Cannot do this verification with the 'DOMContentLoaded' handler because it 61 | // will be too late to attach it if a page does not have any script tags. 62 | var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); 63 | if (qunitMissing) { 64 | console.error('The `QUnit` object is not present on this page.'); 65 | phantom.exit(1); 66 | } 67 | 68 | // Do nothing... the callback mechanism will handle everything! 69 | } 70 | }); 71 | 72 | function addPolyfills() { 73 | function CustomEvent ( event, params ) { 74 | params = params || { bubbles: false, cancelable: false, detail: undefined }; 75 | var evt = document.createEvent( 'CustomEvent' ); 76 | evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); 77 | return evt; 78 | }; 79 | 80 | CustomEvent.prototype = window.Event.prototype; 81 | 82 | window.CustomEvent = CustomEvent; 83 | } 84 | 85 | function addLogging() { 86 | window.document.addEventListener('DOMContentLoaded', function() { 87 | var current_test_assertions = []; 88 | 89 | QUnit.log(function(details) { 90 | var response; 91 | 92 | // Ignore passing assertions 93 | if (details.result) { 94 | return; 95 | } 96 | 97 | response = details.message || ''; 98 | 99 | if (typeof details.expected !== 'undefined') { 100 | if (response) { 101 | response += ', '; 102 | } 103 | 104 | response += 'expected: ' + details.expected + ', but was: ' + details.actual; 105 | if (details.source) { 106 | response += "\n" + details.source; 107 | } 108 | } 109 | 110 | current_test_assertions.push('Failed assertion: ' + response); 111 | }); 112 | 113 | QUnit.testDone(function(result) { 114 | var i, 115 | len, 116 | name = result.module + ': ' + result.name; 117 | 118 | if (result.failed) { 119 | console.log('Test failed: ' + name); 120 | 121 | for (i = 0, len = current_test_assertions.length; i < len; i++) { 122 | console.log(' ' + current_test_assertions[i]); 123 | } 124 | } 125 | 126 | current_test_assertions.length = 0; 127 | }); 128 | 129 | QUnit.done(function(result) { 130 | console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); 131 | 132 | if (typeof window.callPhantom === 'function') { 133 | window.callPhantom({ 134 | 'name': 'QUnit.done', 135 | 'data': result 136 | }); 137 | } 138 | }); 139 | }, false); 140 | } 141 | })(); 142 | -------------------------------------------------------------------------------- /test/vendor/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.6.0 2 | // http://underscorejs.org 3 | // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `exports` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Establish the object that gets returned to break out of a loop iteration. 18 | var breaker = {}; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 22 | 23 | // Create quick reference variables for speed access to core prototypes. 24 | var 25 | push = ArrayProto.push, 26 | slice = ArrayProto.slice, 27 | concat = ArrayProto.concat, 28 | toString = ObjProto.toString, 29 | hasOwnProperty = ObjProto.hasOwnProperty; 30 | 31 | // All **ECMAScript 5** native function implementations that we hope to use 32 | // are declared here. 33 | var 34 | nativeForEach = ArrayProto.forEach, 35 | nativeMap = ArrayProto.map, 36 | nativeReduce = ArrayProto.reduce, 37 | nativeReduceRight = ArrayProto.reduceRight, 38 | nativeFilter = ArrayProto.filter, 39 | nativeEvery = ArrayProto.every, 40 | nativeSome = ArrayProto.some, 41 | nativeIndexOf = ArrayProto.indexOf, 42 | nativeLastIndexOf = ArrayProto.lastIndexOf, 43 | nativeIsArray = Array.isArray, 44 | nativeKeys = Object.keys, 45 | nativeBind = FuncProto.bind; 46 | 47 | // Create a safe reference to the Underscore object for use below. 48 | var _ = function(obj) { 49 | if (obj instanceof _) return obj; 50 | if (!(this instanceof _)) return new _(obj); 51 | this._wrapped = obj; 52 | }; 53 | 54 | // Export the Underscore object for **Node.js**, with 55 | // backwards-compatibility for the old `require()` API. If we're in 56 | // the browser, add `_` as a global object via a string identifier, 57 | // for Closure Compiler "advanced" mode. 58 | if (typeof exports !== 'undefined') { 59 | if (typeof module !== 'undefined' && module.exports) { 60 | exports = module.exports = _; 61 | } 62 | exports._ = _; 63 | } else { 64 | root._ = _; 65 | } 66 | 67 | // Current version. 68 | _.VERSION = '1.6.0'; 69 | 70 | // Collection Functions 71 | // -------------------- 72 | 73 | // The cornerstone, an `each` implementation, aka `forEach`. 74 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 75 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 76 | var each = _.each = _.forEach = function(obj, iterator, context) { 77 | if (obj == null) return obj; 78 | if (nativeForEach && obj.forEach === nativeForEach) { 79 | obj.forEach(iterator, context); 80 | } else if (obj.length === +obj.length) { 81 | for (var i = 0, length = obj.length; i < length; i++) { 82 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 83 | } 84 | } else { 85 | var keys = _.keys(obj); 86 | for (var i = 0, length = keys.length; i < length; i++) { 87 | if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; 88 | } 89 | } 90 | return obj; 91 | }; 92 | 93 | // Return the results of applying the iterator to each element. 94 | // Delegates to **ECMAScript 5**'s native `map` if available. 95 | _.map = _.collect = function(obj, iterator, context) { 96 | var results = []; 97 | if (obj == null) return results; 98 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 99 | each(obj, function(value, index, list) { 100 | results.push(iterator.call(context, value, index, list)); 101 | }); 102 | return results; 103 | }; 104 | 105 | var reduceError = 'Reduce of empty array with no initial value'; 106 | 107 | // **Reduce** builds up a single result from a list of values, aka `inject`, 108 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 109 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 110 | var initial = arguments.length > 2; 111 | if (obj == null) obj = []; 112 | if (nativeReduce && obj.reduce === nativeReduce) { 113 | if (context) iterator = _.bind(iterator, context); 114 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 115 | } 116 | each(obj, function(value, index, list) { 117 | if (!initial) { 118 | memo = value; 119 | initial = true; 120 | } else { 121 | memo = iterator.call(context, memo, value, index, list); 122 | } 123 | }); 124 | if (!initial) throw new TypeError(reduceError); 125 | return memo; 126 | }; 127 | 128 | // The right-associative version of reduce, also known as `foldr`. 129 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 130 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 131 | var initial = arguments.length > 2; 132 | if (obj == null) obj = []; 133 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 134 | if (context) iterator = _.bind(iterator, context); 135 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 136 | } 137 | var length = obj.length; 138 | if (length !== +length) { 139 | var keys = _.keys(obj); 140 | length = keys.length; 141 | } 142 | each(obj, function(value, index, list) { 143 | index = keys ? keys[--length] : --length; 144 | if (!initial) { 145 | memo = obj[index]; 146 | initial = true; 147 | } else { 148 | memo = iterator.call(context, memo, obj[index], index, list); 149 | } 150 | }); 151 | if (!initial) throw new TypeError(reduceError); 152 | return memo; 153 | }; 154 | 155 | // Return the first value which passes a truth test. Aliased as `detect`. 156 | _.find = _.detect = function(obj, predicate, context) { 157 | var result; 158 | any(obj, function(value, index, list) { 159 | if (predicate.call(context, value, index, list)) { 160 | result = value; 161 | return true; 162 | } 163 | }); 164 | return result; 165 | }; 166 | 167 | // Return all the elements that pass a truth test. 168 | // Delegates to **ECMAScript 5**'s native `filter` if available. 169 | // Aliased as `select`. 170 | _.filter = _.select = function(obj, predicate, context) { 171 | var results = []; 172 | if (obj == null) return results; 173 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context); 174 | each(obj, function(value, index, list) { 175 | if (predicate.call(context, value, index, list)) results.push(value); 176 | }); 177 | return results; 178 | }; 179 | 180 | // Return all the elements for which a truth test fails. 181 | _.reject = function(obj, predicate, context) { 182 | return _.filter(obj, function(value, index, list) { 183 | return !predicate.call(context, value, index, list); 184 | }, context); 185 | }; 186 | 187 | // Determine whether all of the elements match a truth test. 188 | // Delegates to **ECMAScript 5**'s native `every` if available. 189 | // Aliased as `all`. 190 | _.every = _.all = function(obj, predicate, context) { 191 | predicate || (predicate = _.identity); 192 | var result = true; 193 | if (obj == null) return result; 194 | if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context); 195 | each(obj, function(value, index, list) { 196 | if (!(result = result && predicate.call(context, value, index, list))) return breaker; 197 | }); 198 | return !!result; 199 | }; 200 | 201 | // Determine if at least one element in the object matches a truth test. 202 | // Delegates to **ECMAScript 5**'s native `some` if available. 203 | // Aliased as `any`. 204 | var any = _.some = _.any = function(obj, predicate, context) { 205 | predicate || (predicate = _.identity); 206 | var result = false; 207 | if (obj == null) return result; 208 | if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context); 209 | each(obj, function(value, index, list) { 210 | if (result || (result = predicate.call(context, value, index, list))) return breaker; 211 | }); 212 | return !!result; 213 | }; 214 | 215 | // Determine if the array or object contains a given value (using `===`). 216 | // Aliased as `include`. 217 | _.contains = _.include = function(obj, target) { 218 | if (obj == null) return false; 219 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 220 | return any(obj, function(value) { 221 | return value === target; 222 | }); 223 | }; 224 | 225 | // Invoke a method (with arguments) on every item in a collection. 226 | _.invoke = function(obj, method) { 227 | var args = slice.call(arguments, 2); 228 | var isFunc = _.isFunction(method); 229 | return _.map(obj, function(value) { 230 | return (isFunc ? method : value[method]).apply(value, args); 231 | }); 232 | }; 233 | 234 | // Convenience version of a common use case of `map`: fetching a property. 235 | _.pluck = function(obj, key) { 236 | return _.map(obj, _.property(key)); 237 | }; 238 | 239 | // Convenience version of a common use case of `filter`: selecting only objects 240 | // containing specific `key:value` pairs. 241 | _.where = function(obj, attrs) { 242 | return _.filter(obj, _.matches(attrs)); 243 | }; 244 | 245 | // Convenience version of a common use case of `find`: getting the first object 246 | // containing specific `key:value` pairs. 247 | _.findWhere = function(obj, attrs) { 248 | return _.find(obj, _.matches(attrs)); 249 | }; 250 | 251 | // Return the maximum element or (element-based computation). 252 | // Can't optimize arrays of integers longer than 65,535 elements. 253 | // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) 254 | _.max = function(obj, iterator, context) { 255 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 256 | return Math.max.apply(Math, obj); 257 | } 258 | var result = -Infinity, lastComputed = -Infinity; 259 | each(obj, function(value, index, list) { 260 | var computed = iterator ? iterator.call(context, value, index, list) : value; 261 | if (computed > lastComputed) { 262 | result = value; 263 | lastComputed = computed; 264 | } 265 | }); 266 | return result; 267 | }; 268 | 269 | // Return the minimum element (or element-based computation). 270 | _.min = function(obj, iterator, context) { 271 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 272 | return Math.min.apply(Math, obj); 273 | } 274 | var result = Infinity, lastComputed = Infinity; 275 | each(obj, function(value, index, list) { 276 | var computed = iterator ? iterator.call(context, value, index, list) : value; 277 | if (computed < lastComputed) { 278 | result = value; 279 | lastComputed = computed; 280 | } 281 | }); 282 | return result; 283 | }; 284 | 285 | // Shuffle an array, using the modern version of the 286 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). 287 | _.shuffle = function(obj) { 288 | var rand; 289 | var index = 0; 290 | var shuffled = []; 291 | each(obj, function(value) { 292 | rand = _.random(index++); 293 | shuffled[index - 1] = shuffled[rand]; 294 | shuffled[rand] = value; 295 | }); 296 | return shuffled; 297 | }; 298 | 299 | // Sample **n** random values from a collection. 300 | // If **n** is not specified, returns a single random element. 301 | // The internal `guard` argument allows it to work with `map`. 302 | _.sample = function(obj, n, guard) { 303 | if (n == null || guard) { 304 | if (obj.length !== +obj.length) obj = _.values(obj); 305 | return obj[_.random(obj.length - 1)]; 306 | } 307 | return _.shuffle(obj).slice(0, Math.max(0, n)); 308 | }; 309 | 310 | // An internal function to generate lookup iterators. 311 | var lookupIterator = function(value) { 312 | if (value == null) return _.identity; 313 | if (_.isFunction(value)) return value; 314 | return _.property(value); 315 | }; 316 | 317 | // Sort the object's values by a criterion produced by an iterator. 318 | _.sortBy = function(obj, iterator, context) { 319 | iterator = lookupIterator(iterator); 320 | return _.pluck(_.map(obj, function(value, index, list) { 321 | return { 322 | value: value, 323 | index: index, 324 | criteria: iterator.call(context, value, index, list) 325 | }; 326 | }).sort(function(left, right) { 327 | var a = left.criteria; 328 | var b = right.criteria; 329 | if (a !== b) { 330 | if (a > b || a === void 0) return 1; 331 | if (a < b || b === void 0) return -1; 332 | } 333 | return left.index - right.index; 334 | }), 'value'); 335 | }; 336 | 337 | // An internal function used for aggregate "group by" operations. 338 | var group = function(behavior) { 339 | return function(obj, iterator, context) { 340 | var result = {}; 341 | iterator = lookupIterator(iterator); 342 | each(obj, function(value, index) { 343 | var key = iterator.call(context, value, index, obj); 344 | behavior(result, key, value); 345 | }); 346 | return result; 347 | }; 348 | }; 349 | 350 | // Groups the object's values by a criterion. Pass either a string attribute 351 | // to group by, or a function that returns the criterion. 352 | _.groupBy = group(function(result, key, value) { 353 | _.has(result, key) ? result[key].push(value) : result[key] = [value]; 354 | }); 355 | 356 | // Indexes the object's values by a criterion, similar to `groupBy`, but for 357 | // when you know that your index values will be unique. 358 | _.indexBy = group(function(result, key, value) { 359 | result[key] = value; 360 | }); 361 | 362 | // Counts instances of an object that group by a certain criterion. Pass 363 | // either a string attribute to count by, or a function that returns the 364 | // criterion. 365 | _.countBy = group(function(result, key) { 366 | _.has(result, key) ? result[key]++ : result[key] = 1; 367 | }); 368 | 369 | // Use a comparator function to figure out the smallest index at which 370 | // an object should be inserted so as to maintain order. Uses binary search. 371 | _.sortedIndex = function(array, obj, iterator, context) { 372 | iterator = lookupIterator(iterator); 373 | var value = iterator.call(context, obj); 374 | var low = 0, high = array.length; 375 | while (low < high) { 376 | var mid = (low + high) >>> 1; 377 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 378 | } 379 | return low; 380 | }; 381 | 382 | // Safely create a real, live array from anything iterable. 383 | _.toArray = function(obj) { 384 | if (!obj) return []; 385 | if (_.isArray(obj)) return slice.call(obj); 386 | if (obj.length === +obj.length) return _.map(obj, _.identity); 387 | return _.values(obj); 388 | }; 389 | 390 | // Return the number of elements in an object. 391 | _.size = function(obj) { 392 | if (obj == null) return 0; 393 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 394 | }; 395 | 396 | // Array Functions 397 | // --------------- 398 | 399 | // Get the first element of an array. Passing **n** will return the first N 400 | // values in the array. Aliased as `head` and `take`. The **guard** check 401 | // allows it to work with `_.map`. 402 | _.first = _.head = _.take = function(array, n, guard) { 403 | if (array == null) return void 0; 404 | if ((n == null) || guard) return array[0]; 405 | if (n < 0) return []; 406 | return slice.call(array, 0, n); 407 | }; 408 | 409 | // Returns everything but the last entry of the array. Especially useful on 410 | // the arguments object. Passing **n** will return all the values in 411 | // the array, excluding the last N. The **guard** check allows it to work with 412 | // `_.map`. 413 | _.initial = function(array, n, guard) { 414 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 415 | }; 416 | 417 | // Get the last element of an array. Passing **n** will return the last N 418 | // values in the array. The **guard** check allows it to work with `_.map`. 419 | _.last = function(array, n, guard) { 420 | if (array == null) return void 0; 421 | if ((n == null) || guard) return array[array.length - 1]; 422 | return slice.call(array, Math.max(array.length - n, 0)); 423 | }; 424 | 425 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 426 | // Especially useful on the arguments object. Passing an **n** will return 427 | // the rest N values in the array. The **guard** 428 | // check allows it to work with `_.map`. 429 | _.rest = _.tail = _.drop = function(array, n, guard) { 430 | return slice.call(array, (n == null) || guard ? 1 : n); 431 | }; 432 | 433 | // Trim out all falsy values from an array. 434 | _.compact = function(array) { 435 | return _.filter(array, _.identity); 436 | }; 437 | 438 | // Internal implementation of a recursive `flatten` function. 439 | var flatten = function(input, shallow, output) { 440 | if (shallow && _.every(input, _.isArray)) { 441 | return concat.apply(output, input); 442 | } 443 | each(input, function(value) { 444 | if (_.isArray(value) || _.isArguments(value)) { 445 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 446 | } else { 447 | output.push(value); 448 | } 449 | }); 450 | return output; 451 | }; 452 | 453 | // Flatten out an array, either recursively (by default), or just one level. 454 | _.flatten = function(array, shallow) { 455 | return flatten(array, shallow, []); 456 | }; 457 | 458 | // Return a version of the array that does not contain the specified value(s). 459 | _.without = function(array) { 460 | return _.difference(array, slice.call(arguments, 1)); 461 | }; 462 | 463 | // Split an array into two arrays: one whose elements all satisfy the given 464 | // predicate, and one whose elements all do not satisfy the predicate. 465 | _.partition = function(array, predicate) { 466 | var pass = [], fail = []; 467 | each(array, function(elem) { 468 | (predicate(elem) ? pass : fail).push(elem); 469 | }); 470 | return [pass, fail]; 471 | }; 472 | 473 | // Produce a duplicate-free version of the array. If the array has already 474 | // been sorted, you have the option of using a faster algorithm. 475 | // Aliased as `unique`. 476 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 477 | if (_.isFunction(isSorted)) { 478 | context = iterator; 479 | iterator = isSorted; 480 | isSorted = false; 481 | } 482 | var initial = iterator ? _.map(array, iterator, context) : array; 483 | var results = []; 484 | var seen = []; 485 | each(initial, function(value, index) { 486 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 487 | seen.push(value); 488 | results.push(array[index]); 489 | } 490 | }); 491 | return results; 492 | }; 493 | 494 | // Produce an array that contains the union: each distinct element from all of 495 | // the passed-in arrays. 496 | _.union = function() { 497 | return _.uniq(_.flatten(arguments, true)); 498 | }; 499 | 500 | // Produce an array that contains every item shared between all the 501 | // passed-in arrays. 502 | _.intersection = function(array) { 503 | var rest = slice.call(arguments, 1); 504 | return _.filter(_.uniq(array), function(item) { 505 | return _.every(rest, function(other) { 506 | return _.contains(other, item); 507 | }); 508 | }); 509 | }; 510 | 511 | // Take the difference between one array and a number of other arrays. 512 | // Only the elements present in just the first array will remain. 513 | _.difference = function(array) { 514 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 515 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 516 | }; 517 | 518 | // Zip together multiple lists into a single array -- elements that share 519 | // an index go together. 520 | _.zip = function() { 521 | var length = _.max(_.pluck(arguments, 'length').concat(0)); 522 | var results = new Array(length); 523 | for (var i = 0; i < length; i++) { 524 | results[i] = _.pluck(arguments, '' + i); 525 | } 526 | return results; 527 | }; 528 | 529 | // Converts lists into objects. Pass either a single array of `[key, value]` 530 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 531 | // the corresponding values. 532 | _.object = function(list, values) { 533 | if (list == null) return {}; 534 | var result = {}; 535 | for (var i = 0, length = list.length; i < length; i++) { 536 | if (values) { 537 | result[list[i]] = values[i]; 538 | } else { 539 | result[list[i][0]] = list[i][1]; 540 | } 541 | } 542 | return result; 543 | }; 544 | 545 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 546 | // we need this function. Return the position of the first occurrence of an 547 | // item in an array, or -1 if the item is not included in the array. 548 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 549 | // If the array is large and already in sort order, pass `true` 550 | // for **isSorted** to use binary search. 551 | _.indexOf = function(array, item, isSorted) { 552 | if (array == null) return -1; 553 | var i = 0, length = array.length; 554 | if (isSorted) { 555 | if (typeof isSorted == 'number') { 556 | i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); 557 | } else { 558 | i = _.sortedIndex(array, item); 559 | return array[i] === item ? i : -1; 560 | } 561 | } 562 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 563 | for (; i < length; i++) if (array[i] === item) return i; 564 | return -1; 565 | }; 566 | 567 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 568 | _.lastIndexOf = function(array, item, from) { 569 | if (array == null) return -1; 570 | var hasIndex = from != null; 571 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 572 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 573 | } 574 | var i = (hasIndex ? from : array.length); 575 | while (i--) if (array[i] === item) return i; 576 | return -1; 577 | }; 578 | 579 | // Generate an integer Array containing an arithmetic progression. A port of 580 | // the native Python `range()` function. See 581 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 582 | _.range = function(start, stop, step) { 583 | if (arguments.length <= 1) { 584 | stop = start || 0; 585 | start = 0; 586 | } 587 | step = arguments[2] || 1; 588 | 589 | var length = Math.max(Math.ceil((stop - start) / step), 0); 590 | var idx = 0; 591 | var range = new Array(length); 592 | 593 | while(idx < length) { 594 | range[idx++] = start; 595 | start += step; 596 | } 597 | 598 | return range; 599 | }; 600 | 601 | // Function (ahem) Functions 602 | // ------------------ 603 | 604 | // Reusable constructor function for prototype setting. 605 | var ctor = function(){}; 606 | 607 | // Create a function bound to a given object (assigning `this`, and arguments, 608 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if 609 | // available. 610 | _.bind = function(func, context) { 611 | var args, bound; 612 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 613 | if (!_.isFunction(func)) throw new TypeError; 614 | args = slice.call(arguments, 2); 615 | return bound = function() { 616 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 617 | ctor.prototype = func.prototype; 618 | var self = new ctor; 619 | ctor.prototype = null; 620 | var result = func.apply(self, args.concat(slice.call(arguments))); 621 | if (Object(result) === result) return result; 622 | return self; 623 | }; 624 | }; 625 | 626 | // Partially apply a function by creating a version that has had some of its 627 | // arguments pre-filled, without changing its dynamic `this` context. _ acts 628 | // as a placeholder, allowing any combination of arguments to be pre-filled. 629 | _.partial = function(func) { 630 | var boundArgs = slice.call(arguments, 1); 631 | return function() { 632 | var position = 0; 633 | var args = boundArgs.slice(); 634 | for (var i = 0, length = args.length; i < length; i++) { 635 | if (args[i] === _) args[i] = arguments[position++]; 636 | } 637 | while (position < arguments.length) args.push(arguments[position++]); 638 | return func.apply(this, args); 639 | }; 640 | }; 641 | 642 | // Bind a number of an object's methods to that object. Remaining arguments 643 | // are the method names to be bound. Useful for ensuring that all callbacks 644 | // defined on an object belong to it. 645 | _.bindAll = function(obj) { 646 | var funcs = slice.call(arguments, 1); 647 | if (funcs.length === 0) throw new Error('bindAll must be passed function names'); 648 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 649 | return obj; 650 | }; 651 | 652 | // Memoize an expensive function by storing its results. 653 | _.memoize = function(func, hasher) { 654 | var memo = {}; 655 | hasher || (hasher = _.identity); 656 | return function() { 657 | var key = hasher.apply(this, arguments); 658 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 659 | }; 660 | }; 661 | 662 | // Delays a function for the given number of milliseconds, and then calls 663 | // it with the arguments supplied. 664 | _.delay = function(func, wait) { 665 | var args = slice.call(arguments, 2); 666 | return setTimeout(function(){ return func.apply(null, args); }, wait); 667 | }; 668 | 669 | // Defers a function, scheduling it to run after the current call stack has 670 | // cleared. 671 | _.defer = function(func) { 672 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 673 | }; 674 | 675 | // Returns a function, that, when invoked, will only be triggered at most once 676 | // during a given window of time. Normally, the throttled function will run 677 | // as much as it can, without ever going more than once per `wait` duration; 678 | // but if you'd like to disable the execution on the leading edge, pass 679 | // `{leading: false}`. To disable execution on the trailing edge, ditto. 680 | _.throttle = function(func, wait, options) { 681 | var context, args, result; 682 | var timeout = null; 683 | var previous = 0; 684 | options || (options = {}); 685 | var later = function() { 686 | previous = options.leading === false ? 0 : _.now(); 687 | timeout = null; 688 | result = func.apply(context, args); 689 | context = args = null; 690 | }; 691 | return function() { 692 | var now = _.now(); 693 | if (!previous && options.leading === false) previous = now; 694 | var remaining = wait - (now - previous); 695 | context = this; 696 | args = arguments; 697 | if (remaining <= 0) { 698 | clearTimeout(timeout); 699 | timeout = null; 700 | previous = now; 701 | result = func.apply(context, args); 702 | context = args = null; 703 | } else if (!timeout && options.trailing !== false) { 704 | timeout = setTimeout(later, remaining); 705 | } 706 | return result; 707 | }; 708 | }; 709 | 710 | // Returns a function, that, as long as it continues to be invoked, will not 711 | // be triggered. The function will be called after it stops being called for 712 | // N milliseconds. If `immediate` is passed, trigger the function on the 713 | // leading edge, instead of the trailing. 714 | _.debounce = function(func, wait, immediate) { 715 | var timeout, args, context, timestamp, result; 716 | 717 | var later = function() { 718 | var last = _.now() - timestamp; 719 | if (last < wait) { 720 | timeout = setTimeout(later, wait - last); 721 | } else { 722 | timeout = null; 723 | if (!immediate) { 724 | result = func.apply(context, args); 725 | context = args = null; 726 | } 727 | } 728 | }; 729 | 730 | return function() { 731 | context = this; 732 | args = arguments; 733 | timestamp = _.now(); 734 | var callNow = immediate && !timeout; 735 | if (!timeout) { 736 | timeout = setTimeout(later, wait); 737 | } 738 | if (callNow) { 739 | result = func.apply(context, args); 740 | context = args = null; 741 | } 742 | 743 | return result; 744 | }; 745 | }; 746 | 747 | // Returns a function that will be executed at most one time, no matter how 748 | // often you call it. Useful for lazy initialization. 749 | _.once = function(func) { 750 | var ran = false, memo; 751 | return function() { 752 | if (ran) return memo; 753 | ran = true; 754 | memo = func.apply(this, arguments); 755 | func = null; 756 | return memo; 757 | }; 758 | }; 759 | 760 | // Returns the first function passed as an argument to the second, 761 | // allowing you to adjust arguments, run code before and after, and 762 | // conditionally execute the original function. 763 | _.wrap = function(func, wrapper) { 764 | return _.partial(wrapper, func); 765 | }; 766 | 767 | // Returns a function that is the composition of a list of functions, each 768 | // consuming the return value of the function that follows. 769 | _.compose = function() { 770 | var funcs = arguments; 771 | return function() { 772 | var args = arguments; 773 | for (var i = funcs.length - 1; i >= 0; i--) { 774 | args = [funcs[i].apply(this, args)]; 775 | } 776 | return args[0]; 777 | }; 778 | }; 779 | 780 | // Returns a function that will only be executed after being called N times. 781 | _.after = function(times, func) { 782 | return function() { 783 | if (--times < 1) { 784 | return func.apply(this, arguments); 785 | } 786 | }; 787 | }; 788 | 789 | // Object Functions 790 | // ---------------- 791 | 792 | // Retrieve the names of an object's properties. 793 | // Delegates to **ECMAScript 5**'s native `Object.keys` 794 | _.keys = function(obj) { 795 | if (!_.isObject(obj)) return []; 796 | if (nativeKeys) return nativeKeys(obj); 797 | var keys = []; 798 | for (var key in obj) if (_.has(obj, key)) keys.push(key); 799 | return keys; 800 | }; 801 | 802 | // Retrieve the values of an object's properties. 803 | _.values = function(obj) { 804 | var keys = _.keys(obj); 805 | var length = keys.length; 806 | var values = new Array(length); 807 | for (var i = 0; i < length; i++) { 808 | values[i] = obj[keys[i]]; 809 | } 810 | return values; 811 | }; 812 | 813 | // Convert an object into a list of `[key, value]` pairs. 814 | _.pairs = function(obj) { 815 | var keys = _.keys(obj); 816 | var length = keys.length; 817 | var pairs = new Array(length); 818 | for (var i = 0; i < length; i++) { 819 | pairs[i] = [keys[i], obj[keys[i]]]; 820 | } 821 | return pairs; 822 | }; 823 | 824 | // Invert the keys and values of an object. The values must be serializable. 825 | _.invert = function(obj) { 826 | var result = {}; 827 | var keys = _.keys(obj); 828 | for (var i = 0, length = keys.length; i < length; i++) { 829 | result[obj[keys[i]]] = keys[i]; 830 | } 831 | return result; 832 | }; 833 | 834 | // Return a sorted list of the function names available on the object. 835 | // Aliased as `methods` 836 | _.functions = _.methods = function(obj) { 837 | var names = []; 838 | for (var key in obj) { 839 | if (_.isFunction(obj[key])) names.push(key); 840 | } 841 | return names.sort(); 842 | }; 843 | 844 | // Extend a given object with all the properties in passed-in object(s). 845 | _.extend = function(obj) { 846 | each(slice.call(arguments, 1), function(source) { 847 | if (source) { 848 | for (var prop in source) { 849 | obj[prop] = source[prop]; 850 | } 851 | } 852 | }); 853 | return obj; 854 | }; 855 | 856 | // Return a copy of the object only containing the whitelisted properties. 857 | _.pick = function(obj) { 858 | var copy = {}; 859 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 860 | each(keys, function(key) { 861 | if (key in obj) copy[key] = obj[key]; 862 | }); 863 | return copy; 864 | }; 865 | 866 | // Return a copy of the object without the blacklisted properties. 867 | _.omit = function(obj) { 868 | var copy = {}; 869 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 870 | for (var key in obj) { 871 | if (!_.contains(keys, key)) copy[key] = obj[key]; 872 | } 873 | return copy; 874 | }; 875 | 876 | // Fill in a given object with default properties. 877 | _.defaults = function(obj) { 878 | each(slice.call(arguments, 1), function(source) { 879 | if (source) { 880 | for (var prop in source) { 881 | if (obj[prop] === void 0) obj[prop] = source[prop]; 882 | } 883 | } 884 | }); 885 | return obj; 886 | }; 887 | 888 | // Create a (shallow-cloned) duplicate of an object. 889 | _.clone = function(obj) { 890 | if (!_.isObject(obj)) return obj; 891 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 892 | }; 893 | 894 | // Invokes interceptor with the obj, and then returns obj. 895 | // The primary purpose of this method is to "tap into" a method chain, in 896 | // order to perform operations on intermediate results within the chain. 897 | _.tap = function(obj, interceptor) { 898 | interceptor(obj); 899 | return obj; 900 | }; 901 | 902 | // Internal recursive comparison function for `isEqual`. 903 | var eq = function(a, b, aStack, bStack) { 904 | // Identical objects are equal. `0 === -0`, but they aren't identical. 905 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). 906 | if (a === b) return a !== 0 || 1 / a == 1 / b; 907 | // A strict comparison is necessary because `null == undefined`. 908 | if (a == null || b == null) return a === b; 909 | // Unwrap any wrapped objects. 910 | if (a instanceof _) a = a._wrapped; 911 | if (b instanceof _) b = b._wrapped; 912 | // Compare `[[Class]]` names. 913 | var className = toString.call(a); 914 | if (className != toString.call(b)) return false; 915 | switch (className) { 916 | // Strings, numbers, dates, and booleans are compared by value. 917 | case '[object String]': 918 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 919 | // equivalent to `new String("5")`. 920 | return a == String(b); 921 | case '[object Number]': 922 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 923 | // other numeric values. 924 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 925 | case '[object Date]': 926 | case '[object Boolean]': 927 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 928 | // millisecond representations. Note that invalid dates with millisecond representations 929 | // of `NaN` are not equivalent. 930 | return +a == +b; 931 | // RegExps are compared by their source patterns and flags. 932 | case '[object RegExp]': 933 | return a.source == b.source && 934 | a.global == b.global && 935 | a.multiline == b.multiline && 936 | a.ignoreCase == b.ignoreCase; 937 | } 938 | if (typeof a != 'object' || typeof b != 'object') return false; 939 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 940 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 941 | var length = aStack.length; 942 | while (length--) { 943 | // Linear search. Performance is inversely proportional to the number of 944 | // unique nested structures. 945 | if (aStack[length] == a) return bStack[length] == b; 946 | } 947 | // Objects with different constructors are not equivalent, but `Object`s 948 | // from different frames are. 949 | var aCtor = a.constructor, bCtor = b.constructor; 950 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 951 | _.isFunction(bCtor) && (bCtor instanceof bCtor)) 952 | && ('constructor' in a && 'constructor' in b)) { 953 | return false; 954 | } 955 | // Add the first object to the stack of traversed objects. 956 | aStack.push(a); 957 | bStack.push(b); 958 | var size = 0, result = true; 959 | // Recursively compare objects and arrays. 960 | if (className == '[object Array]') { 961 | // Compare array lengths to determine if a deep comparison is necessary. 962 | size = a.length; 963 | result = size == b.length; 964 | if (result) { 965 | // Deep compare the contents, ignoring non-numeric properties. 966 | while (size--) { 967 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 968 | } 969 | } 970 | } else { 971 | // Deep compare objects. 972 | for (var key in a) { 973 | if (_.has(a, key)) { 974 | // Count the expected number of properties. 975 | size++; 976 | // Deep compare each member. 977 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 978 | } 979 | } 980 | // Ensure that both objects contain the same number of properties. 981 | if (result) { 982 | for (key in b) { 983 | if (_.has(b, key) && !(size--)) break; 984 | } 985 | result = !size; 986 | } 987 | } 988 | // Remove the first object from the stack of traversed objects. 989 | aStack.pop(); 990 | bStack.pop(); 991 | return result; 992 | }; 993 | 994 | // Perform a deep comparison to check if two objects are equal. 995 | _.isEqual = function(a, b) { 996 | return eq(a, b, [], []); 997 | }; 998 | 999 | // Is a given array, string, or object empty? 1000 | // An "empty" object has no enumerable own-properties. 1001 | _.isEmpty = function(obj) { 1002 | if (obj == null) return true; 1003 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 1004 | for (var key in obj) if (_.has(obj, key)) return false; 1005 | return true; 1006 | }; 1007 | 1008 | // Is a given value a DOM element? 1009 | _.isElement = function(obj) { 1010 | return !!(obj && obj.nodeType === 1); 1011 | }; 1012 | 1013 | // Is a given value an array? 1014 | // Delegates to ECMA5's native Array.isArray 1015 | _.isArray = nativeIsArray || function(obj) { 1016 | return toString.call(obj) == '[object Array]'; 1017 | }; 1018 | 1019 | // Is a given variable an object? 1020 | _.isObject = function(obj) { 1021 | return obj === Object(obj); 1022 | }; 1023 | 1024 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 1025 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 1026 | _['is' + name] = function(obj) { 1027 | return toString.call(obj) == '[object ' + name + ']'; 1028 | }; 1029 | }); 1030 | 1031 | // Define a fallback version of the method in browsers (ahem, IE), where 1032 | // there isn't any inspectable "Arguments" type. 1033 | if (!_.isArguments(arguments)) { 1034 | _.isArguments = function(obj) { 1035 | return !!(obj && _.has(obj, 'callee')); 1036 | }; 1037 | } 1038 | 1039 | // Optimize `isFunction` if appropriate. 1040 | if (typeof (/./) !== 'function') { 1041 | _.isFunction = function(obj) { 1042 | return typeof obj === 'function'; 1043 | }; 1044 | } 1045 | 1046 | // Is a given object a finite number? 1047 | _.isFinite = function(obj) { 1048 | return isFinite(obj) && !isNaN(parseFloat(obj)); 1049 | }; 1050 | 1051 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 1052 | _.isNaN = function(obj) { 1053 | return _.isNumber(obj) && obj != +obj; 1054 | }; 1055 | 1056 | // Is a given value a boolean? 1057 | _.isBoolean = function(obj) { 1058 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 1059 | }; 1060 | 1061 | // Is a given value equal to null? 1062 | _.isNull = function(obj) { 1063 | return obj === null; 1064 | }; 1065 | 1066 | // Is a given variable undefined? 1067 | _.isUndefined = function(obj) { 1068 | return obj === void 0; 1069 | }; 1070 | 1071 | // Shortcut function for checking if an object has a given property directly 1072 | // on itself (in other words, not on a prototype). 1073 | _.has = function(obj, key) { 1074 | return hasOwnProperty.call(obj, key); 1075 | }; 1076 | 1077 | // Utility Functions 1078 | // ----------------- 1079 | 1080 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 1081 | // previous owner. Returns a reference to the Underscore object. 1082 | _.noConflict = function() { 1083 | root._ = previousUnderscore; 1084 | return this; 1085 | }; 1086 | 1087 | // Keep the identity function around for default iterators. 1088 | _.identity = function(value) { 1089 | return value; 1090 | }; 1091 | 1092 | _.constant = function(value) { 1093 | return function () { 1094 | return value; 1095 | }; 1096 | }; 1097 | 1098 | _.property = function(key) { 1099 | return function(obj) { 1100 | return obj[key]; 1101 | }; 1102 | }; 1103 | 1104 | // Returns a predicate for checking whether an object has a given set of `key:value` pairs. 1105 | _.matches = function(attrs) { 1106 | return function(obj) { 1107 | if (obj === attrs) return true; //avoid comparing an object to itself. 1108 | for (var key in attrs) { 1109 | if (attrs[key] !== obj[key]) 1110 | return false; 1111 | } 1112 | return true; 1113 | } 1114 | }; 1115 | 1116 | // Run a function **n** times. 1117 | _.times = function(n, iterator, context) { 1118 | var accum = Array(Math.max(0, n)); 1119 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1120 | return accum; 1121 | }; 1122 | 1123 | // Return a random integer between min and max (inclusive). 1124 | _.random = function(min, max) { 1125 | if (max == null) { 1126 | max = min; 1127 | min = 0; 1128 | } 1129 | return min + Math.floor(Math.random() * (max - min + 1)); 1130 | }; 1131 | 1132 | // A (possibly faster) way to get the current timestamp as an integer. 1133 | _.now = Date.now || function() { return new Date().getTime(); }; 1134 | 1135 | // List of HTML entities for escaping. 1136 | var entityMap = { 1137 | escape: { 1138 | '&': '&', 1139 | '<': '<', 1140 | '>': '>', 1141 | '"': '"', 1142 | "'": ''' 1143 | } 1144 | }; 1145 | entityMap.unescape = _.invert(entityMap.escape); 1146 | 1147 | // Regexes containing the keys and values listed immediately above. 1148 | var entityRegexes = { 1149 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1150 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1151 | }; 1152 | 1153 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1154 | _.each(['escape', 'unescape'], function(method) { 1155 | _[method] = function(string) { 1156 | if (string == null) return ''; 1157 | return ('' + string).replace(entityRegexes[method], function(match) { 1158 | return entityMap[method][match]; 1159 | }); 1160 | }; 1161 | }); 1162 | 1163 | // If the value of the named `property` is a function then invoke it with the 1164 | // `object` as context; otherwise, return it. 1165 | _.result = function(object, property) { 1166 | if (object == null) return void 0; 1167 | var value = object[property]; 1168 | return _.isFunction(value) ? value.call(object) : value; 1169 | }; 1170 | 1171 | // Add your own custom functions to the Underscore object. 1172 | _.mixin = function(obj) { 1173 | each(_.functions(obj), function(name) { 1174 | var func = _[name] = obj[name]; 1175 | _.prototype[name] = function() { 1176 | var args = [this._wrapped]; 1177 | push.apply(args, arguments); 1178 | return result.call(this, func.apply(_, args)); 1179 | }; 1180 | }); 1181 | }; 1182 | 1183 | // Generate a unique integer id (unique within the entire client session). 1184 | // Useful for temporary DOM ids. 1185 | var idCounter = 0; 1186 | _.uniqueId = function(prefix) { 1187 | var id = ++idCounter + ''; 1188 | return prefix ? prefix + id : id; 1189 | }; 1190 | 1191 | // By default, Underscore uses ERB-style template delimiters, change the 1192 | // following template settings to use alternative delimiters. 1193 | _.templateSettings = { 1194 | evaluate : /<%([\s\S]+?)%>/g, 1195 | interpolate : /<%=([\s\S]+?)%>/g, 1196 | escape : /<%-([\s\S]+?)%>/g 1197 | }; 1198 | 1199 | // When customizing `templateSettings`, if you don't want to define an 1200 | // interpolation, evaluation or escaping regex, we need one that is 1201 | // guaranteed not to match. 1202 | var noMatch = /(.)^/; 1203 | 1204 | // Certain characters need to be escaped so that they can be put into a 1205 | // string literal. 1206 | var escapes = { 1207 | "'": "'", 1208 | '\\': '\\', 1209 | '\r': 'r', 1210 | '\n': 'n', 1211 | '\t': 't', 1212 | '\u2028': 'u2028', 1213 | '\u2029': 'u2029' 1214 | }; 1215 | 1216 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1217 | 1218 | // JavaScript micro-templating, similar to John Resig's implementation. 1219 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1220 | // and correctly escapes quotes within interpolated code. 1221 | _.template = function(text, data, settings) { 1222 | var render; 1223 | settings = _.defaults({}, settings, _.templateSettings); 1224 | 1225 | // Combine delimiters into one regular expression via alternation. 1226 | var matcher = new RegExp([ 1227 | (settings.escape || noMatch).source, 1228 | (settings.interpolate || noMatch).source, 1229 | (settings.evaluate || noMatch).source 1230 | ].join('|') + '|$', 'g'); 1231 | 1232 | // Compile the template source, escaping string literals appropriately. 1233 | var index = 0; 1234 | var source = "__p+='"; 1235 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1236 | source += text.slice(index, offset) 1237 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1238 | 1239 | if (escape) { 1240 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1241 | } 1242 | if (interpolate) { 1243 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1244 | } 1245 | if (evaluate) { 1246 | source += "';\n" + evaluate + "\n__p+='"; 1247 | } 1248 | index = offset + match.length; 1249 | return match; 1250 | }); 1251 | source += "';\n"; 1252 | 1253 | // If a variable is not specified, place data values in local scope. 1254 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1255 | 1256 | source = "var __t,__p='',__j=Array.prototype.join," + 1257 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1258 | source + "return __p;\n"; 1259 | 1260 | try { 1261 | render = new Function(settings.variable || 'obj', '_', source); 1262 | } catch (e) { 1263 | e.source = source; 1264 | throw e; 1265 | } 1266 | 1267 | if (data) return render(data, _); 1268 | var template = function(data) { 1269 | return render.call(this, data, _); 1270 | }; 1271 | 1272 | // Provide the compiled function source as a convenience for precompilation. 1273 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1274 | 1275 | return template; 1276 | }; 1277 | 1278 | // Add a "chain" function, which will delegate to the wrapper. 1279 | _.chain = function(obj) { 1280 | return _(obj).chain(); 1281 | }; 1282 | 1283 | // OOP 1284 | // --------------- 1285 | // If Underscore is called as a function, it returns a wrapped object that 1286 | // can be used OO-style. This wrapper holds altered versions of all the 1287 | // underscore functions. Wrapped objects may be chained. 1288 | 1289 | // Helper function to continue chaining intermediate results. 1290 | var result = function(obj) { 1291 | return this._chain ? _(obj).chain() : obj; 1292 | }; 1293 | 1294 | // Add all of the Underscore functions to the wrapper object. 1295 | _.mixin(_); 1296 | 1297 | // Add all mutator Array functions to the wrapper. 1298 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1299 | var method = ArrayProto[name]; 1300 | _.prototype[name] = function() { 1301 | var obj = this._wrapped; 1302 | method.apply(obj, arguments); 1303 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1304 | return result.call(this, obj); 1305 | }; 1306 | }); 1307 | 1308 | // Add all accessor Array functions to the wrapper. 1309 | each(['concat', 'join', 'slice'], function(name) { 1310 | var method = ArrayProto[name]; 1311 | _.prototype[name] = function() { 1312 | return result.call(this, method.apply(this._wrapped, arguments)); 1313 | }; 1314 | }); 1315 | 1316 | _.extend(_.prototype, { 1317 | 1318 | // Start chaining a wrapped Underscore object. 1319 | chain: function() { 1320 | this._chain = true; 1321 | return this; 1322 | }, 1323 | 1324 | // Extracts the result from a wrapped and chained object. 1325 | value: function() { 1326 | return this._wrapped; 1327 | } 1328 | 1329 | }); 1330 | 1331 | // AMD registration happens at the end for compatibility with AMD loaders 1332 | // that may not enforce next-turn semantics on modules. Even though general 1333 | // practice for AMD registration is to be anonymous, underscore registers 1334 | // as a named module because, like jQuery, it is a base library that is 1335 | // popular enough to be bundled in a third party lib, but not be part of 1336 | // an AMD load request. Those cases could generate an error when an 1337 | // anonymous define() is called outside of a loader request. 1338 | if (typeof define === 'function' && define.amd) { 1339 | define('underscore', [], function() { 1340 | return _; 1341 | }); 1342 | } 1343 | }).call(this); 1344 | -------------------------------------------------------------------------------- /test/view.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | module("Backbone.NativeView"); 3 | Backbone.View = Backbone.NativeView; 4 | 5 | test("constructor", 3, function() { 6 | var view = new Backbone.View({ 7 | id : 'test-view', 8 | className : 'test-view', 9 | other : 'non-special-option' 10 | }); 11 | equal(view.el.id, 'test-view'); 12 | equal(view.el.className, 'test-view'); 13 | equal(view.el.other, void 0); 14 | }); 15 | 16 | test("$", 2, function() { 17 | var view = new Backbone.View; 18 | view.setElement('

    test

    '); 19 | var result = view.$('a b'); 20 | 21 | strictEqual(result[0].innerHTML, 'test'); 22 | ok(result.length === +result.length); 23 | }); 24 | 25 | test("$el", function() { 26 | var view = new Backbone.View; 27 | view.setElement('

    test

    '); 28 | strictEqual(view.el.nodeType, 1); 29 | 30 | if (Backbone.$) { 31 | ok(view.$el instanceof Backbone.$); 32 | strictEqual(view.$el[0], view.el); 33 | } 34 | }); 35 | 36 | test("initialize", 1, function() { 37 | var View = Backbone.View.extend({ 38 | initialize: function() { 39 | this.one = 1; 40 | } 41 | }); 42 | 43 | strictEqual(new View().one, 1); 44 | }); 45 | 46 | test("delegateEvents", 6, function() { 47 | var counter1 = 0, counter2 = 0; 48 | 49 | var view = new Backbone.View({el: '#testElement'}); 50 | view.increment = function(){ counter1++; }; 51 | addEventListener.call(view.el, 'click', function(){ counter2++; }); 52 | 53 | var events = {'click h1': 'increment'}; 54 | 55 | view.delegateEvents(events); 56 | click(view.$('h1')[0]); 57 | equal(counter1, 1); 58 | equal(counter2, 1); 59 | 60 | click(view.$('h1')[0]); 61 | equal(counter1, 2); 62 | equal(counter2, 2); 63 | 64 | view.delegateEvents(events); 65 | click(view.$('h1')[0]); 66 | equal(counter1, 3); 67 | equal(counter2, 3); 68 | }); 69 | 70 | test("delegate", 2, function() { 71 | var view = new Backbone.View({el: '#testElement'}); 72 | view.delegate('click', 'h1', function() { 73 | ok(true); 74 | }); 75 | view.delegate('click', function() { 76 | ok(true); 77 | }); 78 | click(view.$('h1')[0]); 79 | }); 80 | 81 | test("delegateEvents allows functions for callbacks", 3, function() { 82 | var view = new Backbone.View({el: '

    '}); 83 | view.counter = 0; 84 | 85 | var events = { 86 | click: function() { 87 | this.counter++; 88 | } 89 | }; 90 | 91 | document.body.appendChild(view.el); 92 | 93 | view.delegateEvents(events); 94 | click(view.el); 95 | equal(view.counter, 1); 96 | 97 | click(view.el); 98 | equal(view.counter, 2); 99 | 100 | view.delegateEvents(events); 101 | click(view.el); 102 | equal(view.counter, 3); 103 | 104 | document.body.removeChild(view.el); 105 | }); 106 | 107 | test("delegateEvents ignore undefined methods", 0, function() { 108 | var view = new Backbone.View({el: '

    '}); 109 | 110 | document.body.appendChild(view.el); 111 | 112 | view.delegateEvents({'click': 'undefinedMethod'}); 113 | click(view.el); 114 | 115 | document.body.removeChild(view.el); 116 | }); 117 | 118 | test("undelegateEvents", 6, function() { 119 | var counter1 = 0, counter2 = 0; 120 | 121 | var view = new Backbone.View({el: '#testElement'}); 122 | view.increment = function(){ counter1++; }; 123 | addEventListener.call(view.el, 'click', function(){ counter2++; }); 124 | 125 | var events = {'click h1': 'increment'}; 126 | 127 | view.delegateEvents(events); 128 | click(view.$('h1')[0]); 129 | equal(counter1, 1); 130 | equal(counter2, 1); 131 | 132 | view.undelegateEvents(); 133 | click(view.$('h1')[0]); 134 | equal(counter1, 1); 135 | equal(counter2, 2); 136 | 137 | view.delegateEvents(events); 138 | click(view.$('h1')[0]); 139 | equal(counter1, 2); 140 | equal(counter2, 3); 141 | }); 142 | 143 | test("undelegate", 0, function() { 144 | var view = new Backbone.View({el: '#testElement'}); 145 | view.delegate('click', function() { ok(false); }); 146 | view.delegate('click', 'h1', function() { ok(false); }); 147 | 148 | view.undelegate('click'); 149 | 150 | click(view.$('h1')[0]); 151 | click(view.el); 152 | }); 153 | 154 | test("undelegate with passed handler", 1, function() { 155 | var view = new Backbone.View({el: '#testElement'}); 156 | var listener = function() { ok(false); }; 157 | view.delegate('click', listener); 158 | view.delegate('click', function() { ok(true); }); 159 | view.undelegate('click', listener); 160 | click(view.el); 161 | }); 162 | 163 | test("undelegate with selector", 2, function() { 164 | var view = new Backbone.View({el: '#testElement'}); 165 | view.delegate('click', function() { ok(true); }); 166 | view.delegate('click', 'h1', function() { ok(false); }); 167 | view.undelegate('click', 'h1'); 168 | click(view.$('h1')[0]); 169 | click(view.el); 170 | }); 171 | 172 | test("undelegate with handler and selector", 2, function() { 173 | var view = new Backbone.View({el: '#testElement'}); 174 | view.delegate('click', function() { ok(true); }); 175 | var handler = function(){ ok(false); }; 176 | view.delegate('click', 'h1', handler); 177 | view.undelegate('click', 'h1', handler); 178 | click(view.$('h1')[0]); 179 | click(view.el); 180 | }); 181 | 182 | test("_createElement produces the correct DOM el", 1, function() { 183 | var View = Backbone.View.extend({ 184 | tagName: 'span' 185 | }); 186 | 187 | equal(new View().el.tagName, 'SPAN'); 188 | }); 189 | 190 | test("_ensureElement with DOM node el", 1, function() { 191 | var View = Backbone.View.extend({ 192 | el: document.body 193 | }); 194 | 195 | equal(new View().el, document.body); 196 | }); 197 | 198 | test("_ensureElement with string el", 3, function() { 199 | var View = Backbone.View.extend({ 200 | el: "body" 201 | }); 202 | strictEqual(new View().el, document.body); 203 | 204 | View = Backbone.View.extend({ 205 | el: "#testElement > h1" 206 | }); 207 | var children = document.getElementById('testElement').childNodes; 208 | var h1 = _.findWhere(children, {nodeType: 1}); 209 | strictEqual(new View().el, h1); 210 | 211 | View = Backbone.View.extend({ 212 | el: "#nonexistent" 213 | }); 214 | ok(!new View().el); 215 | }); 216 | 217 | test("with className and id functions", 2, function() { 218 | var View = Backbone.View.extend({ 219 | className: function() { 220 | return 'className'; 221 | }, 222 | id: function() { 223 | return 'id'; 224 | } 225 | }); 226 | 227 | strictEqual(new View().el.className, 'className'); 228 | strictEqual(new View().el.id, 'id'); 229 | }); 230 | 231 | test("with attributes", 2, function() { 232 | var View = Backbone.View.extend({ 233 | attributes: { 234 | id: 'id', 235 | 'class': 'class' 236 | } 237 | }); 238 | 239 | strictEqual(new View().el.className, 'class'); 240 | strictEqual(new View().el.id, 'id'); 241 | }); 242 | 243 | test("with attributes as a function", 1, function() { 244 | var View = Backbone.View.extend({ 245 | attributes: function() { 246 | return {'class': 'dynamic'}; 247 | } 248 | }); 249 | 250 | strictEqual(new View().el.className, 'dynamic'); 251 | }); 252 | 253 | test("multiple views per element", 3, function() { 254 | var count = 0; 255 | var el = document.createElement('p'); 256 | 257 | var View = Backbone.View.extend({ 258 | el: el, 259 | events: { 260 | click: function() { 261 | count++; 262 | } 263 | } 264 | }); 265 | 266 | document.body.appendChild(el); 267 | 268 | var view1 = new View; 269 | click(el); 270 | equal(1, count); 271 | 272 | var view2 = new View; 273 | click(el); 274 | equal(3, count); 275 | 276 | view1.delegateEvents(); 277 | click(el); 278 | equal(5, count); 279 | 280 | document.body.removeChild(el); 281 | }); 282 | 283 | test("custom events", 2, function() { 284 | var count = 0; 285 | 286 | var View = Backbone.View.extend({ 287 | el: 'body', 288 | events: function() { 289 | return {"fake$event": "run"}; 290 | }, 291 | run: function() { 292 | count++; 293 | } 294 | }); 295 | 296 | var view = new View; 297 | var event = new CustomEvent("fake$event"); 298 | trigger(document.body, event, 'fake$event'); 299 | trigger(document.body, event, 'fake$event'); 300 | equal(count, 2); 301 | 302 | view.undelegate("fake$event"); 303 | trigger(document.body, event, 'fake$event'); 304 | equal(count, 2); 305 | }); 306 | 307 | test("#1048 - setElement uses provided object.", 2, function() { 308 | var el = document.body; 309 | 310 | var view = new Backbone.View({el: el}); 311 | ok(view.el === el); 312 | 313 | view.setElement(el = document.body); 314 | ok(view.el === el); 315 | }); 316 | 317 | test("#986 - Undelegate before changing element.", 1, function() { 318 | var button1 = document.createElement('button'); 319 | var button2 = document.createElement('button'); 320 | 321 | document.body.appendChild(button1); 322 | document.body.appendChild(button2); 323 | 324 | var View = Backbone.View.extend({ 325 | events: { 326 | click: function(e) { 327 | ok(view.el === e.target || e.srcElement); 328 | } 329 | } 330 | }); 331 | 332 | var view = new View({el: button1}); 333 | view.setElement(button2); 334 | 335 | click(button1); 336 | click(button2); 337 | 338 | document.body.removeChild(button1); 339 | document.body.removeChild(button2); 340 | }); 341 | 342 | test("#1172 - Clone attributes object", 2, function() { 343 | var View = Backbone.View.extend({ 344 | attributes: {foo: 'bar'} 345 | }); 346 | 347 | var view1 = new View({id: 'foo'}); 348 | strictEqual(view1.el.id, 'foo'); 349 | 350 | var view2 = new View(); 351 | ok(!view2.el.id); 352 | }); 353 | 354 | test("#1228 - tagName can be provided as a function", 1, function() { 355 | var View = Backbone.View.extend({ 356 | tagName: function() { 357 | return 'p'; 358 | } 359 | }); 360 | 361 | ok(new View().el.tagName.toLowerCase() == 'p'); 362 | }); 363 | 364 | test("views stopListening", 0, function() { 365 | var View = Backbone.View.extend({ 366 | initialize: function() { 367 | this.listenTo(this.model, 'all x', function(){ ok(false); }); 368 | this.listenTo(this.collection, 'all x', function(){ ok(false); }); 369 | } 370 | }); 371 | 372 | var view = new View({ 373 | model: new Backbone.Model, 374 | collection: new Backbone.Collection 375 | }); 376 | 377 | view.stopListening(); 378 | view.model.trigger('x'); 379 | view.collection.trigger('x'); 380 | }); 381 | 382 | test("Provide function for el.", 2, function() { 383 | var View = Backbone.View.extend({ 384 | el: function() { 385 | return "

    "; 386 | } 387 | }); 388 | 389 | var view = new View; 390 | ok(view.el.tagName.toLowerCase() == 'p'); 391 | ok(view.$('a').length != 0); 392 | }); 393 | 394 | test("remove", 1, function() { 395 | var view = new Backbone.View; 396 | document.body.appendChild(view.el); 397 | 398 | view.delegate('click', function() { ok(false); }); 399 | view.listenTo(view, 'all x', function() { ok(false); }); 400 | 401 | view.remove(); 402 | click(view.el); 403 | view.trigger('x'); 404 | 405 | // In IE8 and below, parentNode still exists but is not document.body. 406 | notEqual(view.el.parentNode, document.body); 407 | }); 408 | 409 | test("events passed in options", 1, function() { 410 | var counter = 0; 411 | 412 | var View = Backbone.View.extend({ 413 | el: '#testElement', 414 | increment: function() { 415 | counter++; 416 | } 417 | }); 418 | 419 | var view = new View({ 420 | events: { 421 | 'click h1': 'increment' 422 | } 423 | }); 424 | 425 | click(view.$('h1')[0]); 426 | click(view.$('h1')[0]); 427 | equal(counter, 2); 428 | }); 429 | 430 | // Cross-browser helpers 431 | var ElementProto = (typeof Element != 'undefined' && Element.prototype) || {}; 432 | 433 | var addEventListener = ElementProto.addEventListener || function(eventName, listener) { 434 | return this.attachEvent('on' + eventName, listener); 435 | }; 436 | 437 | function trigger(element, event, eventName) { 438 | if (element.dispatchEvent) { 439 | element.dispatchEvent(event); 440 | } else { 441 | element.fireEvent(eventName, event); 442 | } 443 | } 444 | 445 | function click(element) { 446 | var event; 447 | if (document.createEvent) { 448 | event = document.createEvent('MouseEvent'); 449 | var args = [ 450 | 'click', true, true, 451 | // IE 10+ and Firefox require these 452 | event.view, event.detail, event.screenX, event.screenY, event.clientX, 453 | event.clientY, event.ctrlKey, event.altKey, event.shiftKey, 454 | event.metaKey, event.button, event.relatedTarget 455 | ]; 456 | (event.initMouseEvent || event.initEvent).apply(event, args); 457 | } else { 458 | event = document.createEventObject(); 459 | event.type = 'click'; 460 | event.bubbles = true; 461 | event.cancelable = true; 462 | } 463 | trigger(element, event, 'onclick'); 464 | } 465 | 466 | })(); 467 | --------------------------------------------------------------------------------