├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── extras └── bookmarklet.js ├── illustration ├── console.png ├── consolidate.dot ├── consolidate.png ├── githubheader.png ├── lift.dot └── lift.png ├── lib ├── async.js ├── css.js ├── obj.js ├── resource.js └── responsive.js ├── originalcss.js ├── ratiocinate.js ├── reference └── default_display_property.txt ├── test.js ├── test ├── anchor_underline.html ├── consolidate_greater_volume.html ├── html_tag_classes.html ├── list-style-format.html ├── margin-top.html ├── media-ignore-print.html ├── media-max-width.html ├── media-min-width.html ├── media-overlapping-width.html ├── scary-class-name.html ├── scary-id.html ├── star-selector.html ├── tag-class-combo.html ├── template.html └── webkit-padding-start.html └── vendor ├── jasmine-1.3.1 ├── MIT.LICENSE └── jasmine.js ├── jquery-1.8.2.js ├── phantom-jasmine └── console-runner.js └── underscore-1.4.2.js /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "forin": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "plusplus": true, 16 | "quotmark": "single", 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_install: "npm install -g jshint" 5 | script: 6 | - jshint ./*.js lib/*.js extras/*.js 7 | - phantomjs test.js 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Joe Nelson. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](illustration/githubheader.png "Logo") 2 | 3 | The CSS Ratiocinator automatically refactors your CSS and generates a 4 | new stylesheet for your site. It works by examining your site's live DOM 5 | in the browser and reverse engineering a new, more elegant definition 6 | that captures styles down to the pixel. 7 | 8 | It addresses the problem of old CSS whose styles accumulate and 9 | contradict each other. After a certain point all CSS seems to grow only 10 | by internal antagonism. The Ratiocinator wipes the slate clean and 11 | provides a harmonious new beginning. 12 | 13 | [![Build Status](https://travis-ci.org/begriffs/css-ratiocinator.png?branch=master)](https://travis-ci.org/begriffs/css-ratiocinator) 14 | 15 | ## Usage 16 | 17 | This program runs from the command line using the 18 | PhantomJS headless browser. 19 | 20 | 1. Install [PhantomJS](http://phantomjs.org/) 21 | 1. Clone this repo 22 | 1. In the cloned directory, run `phantomjs ratiocinate.js URL` 23 | 1. The new CSS will appear. 24 | 1. (optionally) Feed output through [sass-convert](http://blog.derekperez.com/post/816063805/move-you-existing-stylebase-over-to-sass-or-scss) 25 | 26 | ## Faq 27 | 28 |
29 |
Does this thing clobber or remove my styles?
30 |
No. If your page renders differently after using 31 | Ratiocinator-generated style then that's a bug. This software may move 32 | styles around during the assessment stage of its algorithm but the 33 | end result respects the original styles you have created. Note that it 34 | may achieve these styles using a different combination of selectors 35 | and inheritance than your original CSS did.
36 | 37 |
Can it work on a whole site (multiple pages)?
38 |
In principle yes, but not yet. The strategy will be to ajax the 39 | pages together into a single page with multiple body tags. This long 40 | assembled page will provide enough data to the Ratiocinator to create 41 | a new style that respects everything your site does.
42 | 43 |
How is this different from other CSS tidying programs?
44 |
The Ratiocinator does not read your CSS files at all, it ignores 45 | them completely. It relies on the live DOM to infer what your styles 46 | want to accomplish. The thoroughness of the results you get are based 47 | on the quality and completeness of the example DOM that you provide the 48 | program.
49 | 50 |
Does it capture styles for the "sad path?"
51 |
It captures styles for whatever you show it, so if you have styles 52 | for errors or form validation problems or various element states 53 | then you'll need to include the markup to demonstrate them. The most 54 | effective input is a living style guide for your site. You do use a 55 | style guide, don't you?
56 |
57 | 58 | ## Mechanism 59 | 60 | The Ratiocinator proceeds in two phases: assessment and consolidation. 61 | During assessment it determines which nodes will need which styles, 62 | accounting for browser defaults and cascade rules. The browser provides 63 | a full list of computed style for every node, and our first step is to 64 | prune redundancies from cascaded style in a depth-first process called 65 | "lifting." 66 | 67 | ![Lifting](illustration/lift.png "Lifting") 68 | 69 | The last step in assessment is stripping default styles. The final 70 | style needn't specify CSS defaults, so we remove them prior to the 71 | consolidation phase. 72 | 73 | Next comes consolidation, where we find shared pieces of style 74 | throughout the cleaned DOM tree and extract them to CSS declarations. 75 | 76 | ![Consolidating](illustration/consolidate.png "Consolidating") 77 | 78 | In the diagram above, the Ratiocinator will choose to output a 79 | declaration for the styles in red before those in blue. Although there 80 | are more blue items than red in element `aside.foo`, there are more red 81 | elements overall. The red has greater "volume." Hence the Ratiocinator 82 | will extract styles for all elements with class `foo` first and then for 83 | `aside` elements second. 84 | 85 | Finally the Ratiocinator detects media query width breakpoints and 86 | samples the page style between them. It analyzes the responsive 87 | portfolio and extracts common base-style. It outputs the base-style and 88 | each width-specific style with appropriate media queries. 89 | 90 | ## Bugs and Edge Cases 91 | 92 | Currently, anything Phantom considers to be an invalid property, value, 93 | or selector is discarded. This means that the following CSS... 94 | 95 | ```css 96 | body { 97 | display: -webkit-box; 98 | display: -moz-box; 99 | display: -ms-flexbox; 100 | display: -webkit-flex; 101 | display: flex; 102 | } 103 | ::selection{ 104 | background:rgba(246,55,0,0.9); 105 | color:#fff; 106 | } 107 | ::-moz-selection{ 108 | background:rgba(246,55,0,0.9); 109 | color:#fff; 110 | } 111 | ``` 112 | 113 | Is compressed to the following CSS... 114 | 115 | ```css 116 | body { 117 | display: -webkit-box; 118 | } 119 | ``` 120 | 121 | This is an invalid compression. A solution is being worked on in 122 | [issue #52][issue52]. 123 | 124 | ## Contributing 125 | 126 | It is currently very easy to contribute. Just find something that the 127 | Ratiocinator does wrong and tell me. The best complaints are very 128 | specific, preferably made into a new test and submitted via a pull 129 | request. Luckily that's easy too: 130 | 131 | 1. Find some styles that the Ratiocinator is botching. 132 | 1. Think of the smallest example that will illustrate the problem. 133 | 1. Add a new test by copying `test/template.html` and filling in the blanks. 134 | 1. Save your new test in the `test/` folder. 135 | 1. Run `phantomjs test.js` and make sure it fails. 136 | 1. Submit pull request to the `bug-reports` branch. 137 | 138 | ## License 139 | 140 | The CSS Ratiocinator is Copyright © 2012 Joe Nelson. It is free 141 | software, and may be redistributed under the terms specified in the 142 | LICENSE file. 143 | 144 | [issue52]: https://github.com/begriffs/css-ratiocinator/issues/52 "Issue #52" 145 | -------------------------------------------------------------------------------- /extras/bookmarklet.js: -------------------------------------------------------------------------------- 1 | /* global _, jQuery, console, CSS */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | function onScriptsLoaded() { 7 | var $ = jQuery.noConflict(), iframeSrc = $(''); 9 | 10 | console.log('WARNING: this bookmarklet is deprecated.'); 11 | console.log('To use the the full features of Ratiocinator, try either of these options.'); 12 | console.log('1. Use the web interface: http://www.csstrashman.com'); 13 | console.log('2. Run the command-line version: ' + 14 | 'https://github.com/begriffs/css-ratiocinator#usage'); 15 | window.alert( 16 | 'This bookmarklet does not support media queries and is deprecated.' + 17 | 'Visit http://www.csstrashman.com to do a full refactor.' 18 | ); 19 | 20 | iframeSrc.attr('id', 'ratio_src'); 21 | iframeSrc.attr( 22 | 'style', 23 | 'position: absolute; top: 0; left: 0; height: 100%; ' + 24 | 'width: 50%; border-right: 1px solid black;' 25 | ); 26 | iframeSrc.load(function () { 27 | var styles = CSS.simplerStyle( 28 | $(window.frames.ratioSrc.document.getElementsByTagName('html')[0]) 29 | ), 30 | styleTag = $(' 9 | 10 | 19 | 20 | 21 | I have no underline 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/consolidate_greater_volume.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 35 | 36 | 37 |
I'm a div
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/html_tag_classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/list-style-format.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 17 | 18 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/margin-top.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 28 | 29 | 30 | 31 |
Hi
32 | 33 | 34 | -------------------------------------------------------------------------------- /test/media-ignore-print.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 24 | 25 | 26 |

Hi

27 | 28 | 29 | -------------------------------------------------------------------------------- /test/media-max-width.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 45 | 46 | 47 |

Hi

48 | 49 | 50 | -------------------------------------------------------------------------------- /test/media-min-width.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 45 | 46 | 47 |

Hi

48 | 49 | 50 | -------------------------------------------------------------------------------- /test/media-overlapping-width.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 37 | 38 | 39 |

Hi

40 | 41 | 42 | -------------------------------------------------------------------------------- /test/scary-class-name.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 18 | 19 | 20 | 23 |
24 | Two 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /test/scary-id.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 19 | 20 | 21 |
Sanity
22 |
Malarky!
23 | 24 | 25 | -------------------------------------------------------------------------------- /test/star-selector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 19 | 20 | 21 |

Hi

22 |
Hello
23 | 24 | 25 | -------------------------------------------------------------------------------- /test/tag-class-combo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 25 | 26 | 27 |
Hello
28 |
Hi
29 |
Greetings
30 | 31 | 32 | -------------------------------------------------------------------------------- /test/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/webkit-padding-start.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 17 | 18 | 19 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /vendor/jasmine-1.3.1/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /vendor/jasmine-1.3.1/jasmine.js: -------------------------------------------------------------------------------- 1 | var isCommonJS = typeof window == "undefined" && typeof exports == "object"; 2 | 3 | /** 4 | * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. 5 | * 6 | * @namespace 7 | */ 8 | var jasmine = {}; 9 | if (isCommonJS) exports.jasmine = jasmine; 10 | /** 11 | * @private 12 | */ 13 | jasmine.unimplementedMethod_ = function() { 14 | throw new Error("unimplemented method"); 15 | }; 16 | 17 | /** 18 | * Use jasmine.undefined instead of undefined, since undefined is just 19 | * a plain old variable and may be redefined by somebody else. 20 | * 21 | * @private 22 | */ 23 | jasmine.undefined = jasmine.___undefined___; 24 | 25 | /** 26 | * Show diagnostic messages in the console if set to true 27 | * 28 | */ 29 | jasmine.VERBOSE = false; 30 | 31 | /** 32 | * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. 33 | * 34 | */ 35 | jasmine.DEFAULT_UPDATE_INTERVAL = 250; 36 | 37 | /** 38 | * Maximum levels of nesting that will be included when an object is pretty-printed 39 | */ 40 | jasmine.MAX_PRETTY_PRINT_DEPTH = 40; 41 | 42 | /** 43 | * Default timeout interval in milliseconds for waitsFor() blocks. 44 | */ 45 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; 46 | 47 | /** 48 | * By default exceptions thrown in the context of a test are caught by jasmine so that it can run the remaining tests in the suite. 49 | * Set to false to let the exception bubble up in the browser. 50 | * 51 | */ 52 | jasmine.CATCH_EXCEPTIONS = true; 53 | 54 | jasmine.getGlobal = function() { 55 | function getGlobal() { 56 | return this; 57 | } 58 | 59 | return getGlobal(); 60 | }; 61 | 62 | /** 63 | * Allows for bound functions to be compared. Internal use only. 64 | * 65 | * @ignore 66 | * @private 67 | * @param base {Object} bound 'this' for the function 68 | * @param name {Function} function to find 69 | */ 70 | jasmine.bindOriginal_ = function(base, name) { 71 | var original = base[name]; 72 | if (original.apply) { 73 | return function() { 74 | return original.apply(base, arguments); 75 | }; 76 | } else { 77 | // IE support 78 | return jasmine.getGlobal()[name]; 79 | } 80 | }; 81 | 82 | jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); 83 | jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); 84 | jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); 85 | jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); 86 | 87 | jasmine.MessageResult = function(values) { 88 | this.type = 'log'; 89 | this.values = values; 90 | this.trace = new Error(); // todo: test better 91 | }; 92 | 93 | jasmine.MessageResult.prototype.toString = function() { 94 | var text = ""; 95 | for (var i = 0; i < this.values.length; i++) { 96 | if (i > 0) text += " "; 97 | if (jasmine.isString_(this.values[i])) { 98 | text += this.values[i]; 99 | } else { 100 | text += jasmine.pp(this.values[i]); 101 | } 102 | } 103 | return text; 104 | }; 105 | 106 | jasmine.ExpectationResult = function(params) { 107 | this.type = 'expect'; 108 | this.matcherName = params.matcherName; 109 | this.passed_ = params.passed; 110 | this.expected = params.expected; 111 | this.actual = params.actual; 112 | this.message = this.passed_ ? 'Passed.' : params.message; 113 | 114 | var trace = (params.trace || new Error(this.message)); 115 | this.trace = this.passed_ ? '' : trace; 116 | }; 117 | 118 | jasmine.ExpectationResult.prototype.toString = function () { 119 | return this.message; 120 | }; 121 | 122 | jasmine.ExpectationResult.prototype.passed = function () { 123 | return this.passed_; 124 | }; 125 | 126 | /** 127 | * Getter for the Jasmine environment. Ensures one gets created 128 | */ 129 | jasmine.getEnv = function() { 130 | var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); 131 | return env; 132 | }; 133 | 134 | /** 135 | * @ignore 136 | * @private 137 | * @param value 138 | * @returns {Boolean} 139 | */ 140 | jasmine.isArray_ = function(value) { 141 | return jasmine.isA_("Array", value); 142 | }; 143 | 144 | /** 145 | * @ignore 146 | * @private 147 | * @param value 148 | * @returns {Boolean} 149 | */ 150 | jasmine.isString_ = function(value) { 151 | return jasmine.isA_("String", value); 152 | }; 153 | 154 | /** 155 | * @ignore 156 | * @private 157 | * @param value 158 | * @returns {Boolean} 159 | */ 160 | jasmine.isNumber_ = function(value) { 161 | return jasmine.isA_("Number", value); 162 | }; 163 | 164 | /** 165 | * @ignore 166 | * @private 167 | * @param {String} typeName 168 | * @param value 169 | * @returns {Boolean} 170 | */ 171 | jasmine.isA_ = function(typeName, value) { 172 | return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; 173 | }; 174 | 175 | /** 176 | * Pretty printer for expecations. Takes any object and turns it into a human-readable string. 177 | * 178 | * @param value {Object} an object to be outputted 179 | * @returns {String} 180 | */ 181 | jasmine.pp = function(value) { 182 | var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); 183 | stringPrettyPrinter.format(value); 184 | return stringPrettyPrinter.string; 185 | }; 186 | 187 | /** 188 | * Returns true if the object is a DOM Node. 189 | * 190 | * @param {Object} obj object to check 191 | * @returns {Boolean} 192 | */ 193 | jasmine.isDomNode = function(obj) { 194 | return obj.nodeType > 0; 195 | }; 196 | 197 | /** 198 | * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. 199 | * 200 | * @example 201 | * // don't care about which function is passed in, as long as it's a function 202 | * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); 203 | * 204 | * @param {Class} clazz 205 | * @returns matchable object of the type clazz 206 | */ 207 | jasmine.any = function(clazz) { 208 | return new jasmine.Matchers.Any(clazz); 209 | }; 210 | 211 | /** 212 | * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the 213 | * attributes on the object. 214 | * 215 | * @example 216 | * // don't care about any other attributes than foo. 217 | * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"}); 218 | * 219 | * @param sample {Object} sample 220 | * @returns matchable object for the sample 221 | */ 222 | jasmine.objectContaining = function (sample) { 223 | return new jasmine.Matchers.ObjectContaining(sample); 224 | }; 225 | 226 | /** 227 | * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. 228 | * 229 | * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine 230 | * expectation syntax. Spies can be checked if they were called or not and what the calling params were. 231 | * 232 | * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). 233 | * 234 | * Spies are torn down at the end of every spec. 235 | * 236 | * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. 237 | * 238 | * @example 239 | * // a stub 240 | * var myStub = jasmine.createSpy('myStub'); // can be used anywhere 241 | * 242 | * // spy example 243 | * var foo = { 244 | * not: function(bool) { return !bool; } 245 | * } 246 | * 247 | * // actual foo.not will not be called, execution stops 248 | * spyOn(foo, 'not'); 249 | 250 | // foo.not spied upon, execution will continue to implementation 251 | * spyOn(foo, 'not').andCallThrough(); 252 | * 253 | * // fake example 254 | * var foo = { 255 | * not: function(bool) { return !bool; } 256 | * } 257 | * 258 | * // foo.not(val) will return val 259 | * spyOn(foo, 'not').andCallFake(function(value) {return value;}); 260 | * 261 | * // mock example 262 | * foo.not(7 == 7); 263 | * expect(foo.not).toHaveBeenCalled(); 264 | * expect(foo.not).toHaveBeenCalledWith(true); 265 | * 266 | * @constructor 267 | * @see spyOn, jasmine.createSpy, jasmine.createSpyObj 268 | * @param {String} name 269 | */ 270 | jasmine.Spy = function(name) { 271 | /** 272 | * The name of the spy, if provided. 273 | */ 274 | this.identity = name || 'unknown'; 275 | /** 276 | * Is this Object a spy? 277 | */ 278 | this.isSpy = true; 279 | /** 280 | * The actual function this spy stubs. 281 | */ 282 | this.plan = function() { 283 | }; 284 | /** 285 | * Tracking of the most recent call to the spy. 286 | * @example 287 | * var mySpy = jasmine.createSpy('foo'); 288 | * mySpy(1, 2); 289 | * mySpy.mostRecentCall.args = [1, 2]; 290 | */ 291 | this.mostRecentCall = {}; 292 | 293 | /** 294 | * Holds arguments for each call to the spy, indexed by call count 295 | * @example 296 | * var mySpy = jasmine.createSpy('foo'); 297 | * mySpy(1, 2); 298 | * mySpy(7, 8); 299 | * mySpy.mostRecentCall.args = [7, 8]; 300 | * mySpy.argsForCall[0] = [1, 2]; 301 | * mySpy.argsForCall[1] = [7, 8]; 302 | */ 303 | this.argsForCall = []; 304 | this.calls = []; 305 | }; 306 | 307 | /** 308 | * Tells a spy to call through to the actual implemenatation. 309 | * 310 | * @example 311 | * var foo = { 312 | * bar: function() { // do some stuff } 313 | * } 314 | * 315 | * // defining a spy on an existing property: foo.bar 316 | * spyOn(foo, 'bar').andCallThrough(); 317 | */ 318 | jasmine.Spy.prototype.andCallThrough = function() { 319 | this.plan = this.originalValue; 320 | return this; 321 | }; 322 | 323 | /** 324 | * For setting the return value of a spy. 325 | * 326 | * @example 327 | * // defining a spy from scratch: foo() returns 'baz' 328 | * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); 329 | * 330 | * // defining a spy on an existing property: foo.bar() returns 'baz' 331 | * spyOn(foo, 'bar').andReturn('baz'); 332 | * 333 | * @param {Object} value 334 | */ 335 | jasmine.Spy.prototype.andReturn = function(value) { 336 | this.plan = function() { 337 | return value; 338 | }; 339 | return this; 340 | }; 341 | 342 | /** 343 | * For throwing an exception when a spy is called. 344 | * 345 | * @example 346 | * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' 347 | * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); 348 | * 349 | * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' 350 | * spyOn(foo, 'bar').andThrow('baz'); 351 | * 352 | * @param {String} exceptionMsg 353 | */ 354 | jasmine.Spy.prototype.andThrow = function(exceptionMsg) { 355 | this.plan = function() { 356 | throw exceptionMsg; 357 | }; 358 | return this; 359 | }; 360 | 361 | /** 362 | * Calls an alternate implementation when a spy is called. 363 | * 364 | * @example 365 | * var baz = function() { 366 | * // do some stuff, return something 367 | * } 368 | * // defining a spy from scratch: foo() calls the function baz 369 | * var foo = jasmine.createSpy('spy on foo').andCall(baz); 370 | * 371 | * // defining a spy on an existing property: foo.bar() calls an anonymnous function 372 | * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); 373 | * 374 | * @param {Function} fakeFunc 375 | */ 376 | jasmine.Spy.prototype.andCallFake = function(fakeFunc) { 377 | this.plan = fakeFunc; 378 | return this; 379 | }; 380 | 381 | /** 382 | * Resets all of a spy's the tracking variables so that it can be used again. 383 | * 384 | * @example 385 | * spyOn(foo, 'bar'); 386 | * 387 | * foo.bar(); 388 | * 389 | * expect(foo.bar.callCount).toEqual(1); 390 | * 391 | * foo.bar.reset(); 392 | * 393 | * expect(foo.bar.callCount).toEqual(0); 394 | */ 395 | jasmine.Spy.prototype.reset = function() { 396 | this.wasCalled = false; 397 | this.callCount = 0; 398 | this.argsForCall = []; 399 | this.calls = []; 400 | this.mostRecentCall = {}; 401 | }; 402 | 403 | jasmine.createSpy = function(name) { 404 | 405 | var spyObj = function() { 406 | spyObj.wasCalled = true; 407 | spyObj.callCount++; 408 | var args = jasmine.util.argsToArray(arguments); 409 | spyObj.mostRecentCall.object = this; 410 | spyObj.mostRecentCall.args = args; 411 | spyObj.argsForCall.push(args); 412 | spyObj.calls.push({object: this, args: args}); 413 | return spyObj.plan.apply(this, arguments); 414 | }; 415 | 416 | var spy = new jasmine.Spy(name); 417 | 418 | for (var prop in spy) { 419 | spyObj[prop] = spy[prop]; 420 | } 421 | 422 | spyObj.reset(); 423 | 424 | return spyObj; 425 | }; 426 | 427 | /** 428 | * Determines whether an object is a spy. 429 | * 430 | * @param {jasmine.Spy|Object} putativeSpy 431 | * @returns {Boolean} 432 | */ 433 | jasmine.isSpy = function(putativeSpy) { 434 | return putativeSpy && putativeSpy.isSpy; 435 | }; 436 | 437 | /** 438 | * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something 439 | * large in one call. 440 | * 441 | * @param {String} baseName name of spy class 442 | * @param {Array} methodNames array of names of methods to make spies 443 | */ 444 | jasmine.createSpyObj = function(baseName, methodNames) { 445 | if (!jasmine.isArray_(methodNames) || methodNames.length === 0) { 446 | throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); 447 | } 448 | var obj = {}; 449 | for (var i = 0; i < methodNames.length; i++) { 450 | obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); 451 | } 452 | return obj; 453 | }; 454 | 455 | /** 456 | * All parameters are pretty-printed and concatenated together, then written to the current spec's output. 457 | * 458 | * Be careful not to leave calls to jasmine.log in production code. 459 | */ 460 | jasmine.log = function() { 461 | var spec = jasmine.getEnv().currentSpec; 462 | spec.log.apply(spec, arguments); 463 | }; 464 | 465 | /** 466 | * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. 467 | * 468 | * @example 469 | * // spy example 470 | * var foo = { 471 | * not: function(bool) { return !bool; } 472 | * } 473 | * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops 474 | * 475 | * @see jasmine.createSpy 476 | * @param obj 477 | * @param methodName 478 | * @return {jasmine.Spy} a Jasmine spy that can be chained with all spy methods 479 | */ 480 | var spyOn = function(obj, methodName) { 481 | return jasmine.getEnv().currentSpec.spyOn(obj, methodName); 482 | }; 483 | if (isCommonJS) exports.spyOn = spyOn; 484 | 485 | /** 486 | * Creates a Jasmine spec that will be added to the current suite. 487 | * 488 | * // TODO: pending tests 489 | * 490 | * @example 491 | * it('should be true', function() { 492 | * expect(true).toEqual(true); 493 | * }); 494 | * 495 | * @param {String} desc description of this specification 496 | * @param {Function} func defines the preconditions and expectations of the spec 497 | */ 498 | var it = function(desc, func) { 499 | return jasmine.getEnv().it(desc, func); 500 | }; 501 | if (isCommonJS) exports.it = it; 502 | 503 | /** 504 | * Creates a disabled Jasmine spec. 505 | * 506 | * A convenience method that allows existing specs to be disabled temporarily during development. 507 | * 508 | * @param {String} desc description of this specification 509 | * @param {Function} func defines the preconditions and expectations of the spec 510 | */ 511 | var xit = function(desc, func) { 512 | return jasmine.getEnv().xit(desc, func); 513 | }; 514 | if (isCommonJS) exports.xit = xit; 515 | 516 | /** 517 | * Starts a chain for a Jasmine expectation. 518 | * 519 | * It is passed an Object that is the actual value and should chain to one of the many 520 | * jasmine.Matchers functions. 521 | * 522 | * @param {Object} actual Actual value to test against and expected value 523 | * @return {jasmine.Matchers} 524 | */ 525 | var expect = function(actual) { 526 | return jasmine.getEnv().currentSpec.expect(actual); 527 | }; 528 | if (isCommonJS) exports.expect = expect; 529 | 530 | /** 531 | * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. 532 | * 533 | * @param {Function} func Function that defines part of a jasmine spec. 534 | */ 535 | var runs = function(func) { 536 | jasmine.getEnv().currentSpec.runs(func); 537 | }; 538 | if (isCommonJS) exports.runs = runs; 539 | 540 | /** 541 | * Waits a fixed time period before moving to the next block. 542 | * 543 | * @deprecated Use waitsFor() instead 544 | * @param {Number} timeout milliseconds to wait 545 | */ 546 | var waits = function(timeout) { 547 | jasmine.getEnv().currentSpec.waits(timeout); 548 | }; 549 | if (isCommonJS) exports.waits = waits; 550 | 551 | /** 552 | * Waits for the latchFunction to return true before proceeding to the next block. 553 | * 554 | * @param {Function} latchFunction 555 | * @param {String} optional_timeoutMessage 556 | * @param {Number} optional_timeout 557 | */ 558 | var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { 559 | jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); 560 | }; 561 | if (isCommonJS) exports.waitsFor = waitsFor; 562 | 563 | /** 564 | * A function that is called before each spec in a suite. 565 | * 566 | * Used for spec setup, including validating assumptions. 567 | * 568 | * @param {Function} beforeEachFunction 569 | */ 570 | var beforeEach = function(beforeEachFunction) { 571 | jasmine.getEnv().beforeEach(beforeEachFunction); 572 | }; 573 | if (isCommonJS) exports.beforeEach = beforeEach; 574 | 575 | /** 576 | * A function that is called after each spec in a suite. 577 | * 578 | * Used for restoring any state that is hijacked during spec execution. 579 | * 580 | * @param {Function} afterEachFunction 581 | */ 582 | var afterEach = function(afterEachFunction) { 583 | jasmine.getEnv().afterEach(afterEachFunction); 584 | }; 585 | if (isCommonJS) exports.afterEach = afterEach; 586 | 587 | /** 588 | * Defines a suite of specifications. 589 | * 590 | * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared 591 | * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization 592 | * of setup in some tests. 593 | * 594 | * @example 595 | * // TODO: a simple suite 596 | * 597 | * // TODO: a simple suite with a nested describe block 598 | * 599 | * @param {String} description A string, usually the class under test. 600 | * @param {Function} specDefinitions function that defines several specs. 601 | */ 602 | var describe = function(description, specDefinitions) { 603 | return jasmine.getEnv().describe(description, specDefinitions); 604 | }; 605 | if (isCommonJS) exports.describe = describe; 606 | 607 | /** 608 | * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. 609 | * 610 | * @param {String} description A string, usually the class under test. 611 | * @param {Function} specDefinitions function that defines several specs. 612 | */ 613 | var xdescribe = function(description, specDefinitions) { 614 | return jasmine.getEnv().xdescribe(description, specDefinitions); 615 | }; 616 | if (isCommonJS) exports.xdescribe = xdescribe; 617 | 618 | 619 | // Provide the XMLHttpRequest class for IE 5.x-6.x: 620 | jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { 621 | function tryIt(f) { 622 | try { 623 | return f(); 624 | } catch(e) { 625 | } 626 | return null; 627 | } 628 | 629 | var xhr = tryIt(function() { 630 | return new ActiveXObject("Msxml2.XMLHTTP.6.0"); 631 | }) || 632 | tryIt(function() { 633 | return new ActiveXObject("Msxml2.XMLHTTP.3.0"); 634 | }) || 635 | tryIt(function() { 636 | return new ActiveXObject("Msxml2.XMLHTTP"); 637 | }) || 638 | tryIt(function() { 639 | return new ActiveXObject("Microsoft.XMLHTTP"); 640 | }); 641 | 642 | if (!xhr) throw new Error("This browser does not support XMLHttpRequest."); 643 | 644 | return xhr; 645 | } : XMLHttpRequest; 646 | /** 647 | * @namespace 648 | */ 649 | jasmine.util = {}; 650 | 651 | /** 652 | * Declare that a child class inherit it's prototype from the parent class. 653 | * 654 | * @private 655 | * @param {Function} childClass 656 | * @param {Function} parentClass 657 | */ 658 | jasmine.util.inherit = function(childClass, parentClass) { 659 | /** 660 | * @private 661 | */ 662 | var subclass = function() { 663 | }; 664 | subclass.prototype = parentClass.prototype; 665 | childClass.prototype = new subclass(); 666 | }; 667 | 668 | jasmine.util.formatException = function(e) { 669 | var lineNumber; 670 | if (e.line) { 671 | lineNumber = e.line; 672 | } 673 | else if (e.lineNumber) { 674 | lineNumber = e.lineNumber; 675 | } 676 | 677 | var file; 678 | 679 | if (e.sourceURL) { 680 | file = e.sourceURL; 681 | } 682 | else if (e.fileName) { 683 | file = e.fileName; 684 | } 685 | 686 | var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); 687 | 688 | if (file && lineNumber) { 689 | message += ' in ' + file + ' (line ' + lineNumber + ')'; 690 | } 691 | 692 | return message; 693 | }; 694 | 695 | jasmine.util.htmlEscape = function(str) { 696 | if (!str) return str; 697 | return str.replace(/&/g, '&') 698 | .replace(//g, '>'); 700 | }; 701 | 702 | jasmine.util.argsToArray = function(args) { 703 | var arrayOfArgs = []; 704 | for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); 705 | return arrayOfArgs; 706 | }; 707 | 708 | jasmine.util.extend = function(destination, source) { 709 | for (var property in source) destination[property] = source[property]; 710 | return destination; 711 | }; 712 | 713 | /** 714 | * Environment for Jasmine 715 | * 716 | * @constructor 717 | */ 718 | jasmine.Env = function() { 719 | this.currentSpec = null; 720 | this.currentSuite = null; 721 | this.currentRunner_ = new jasmine.Runner(this); 722 | 723 | this.reporter = new jasmine.MultiReporter(); 724 | 725 | this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; 726 | this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; 727 | this.lastUpdate = 0; 728 | this.specFilter = function() { 729 | return true; 730 | }; 731 | 732 | this.nextSpecId_ = 0; 733 | this.nextSuiteId_ = 0; 734 | this.equalityTesters_ = []; 735 | 736 | // wrap matchers 737 | this.matchersClass = function() { 738 | jasmine.Matchers.apply(this, arguments); 739 | }; 740 | jasmine.util.inherit(this.matchersClass, jasmine.Matchers); 741 | 742 | jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); 743 | }; 744 | 745 | 746 | jasmine.Env.prototype.setTimeout = jasmine.setTimeout; 747 | jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; 748 | jasmine.Env.prototype.setInterval = jasmine.setInterval; 749 | jasmine.Env.prototype.clearInterval = jasmine.clearInterval; 750 | 751 | /** 752 | * @returns an object containing jasmine version build info, if set. 753 | */ 754 | jasmine.Env.prototype.version = function () { 755 | if (jasmine.version_) { 756 | return jasmine.version_; 757 | } else { 758 | throw new Error('Version not set'); 759 | } 760 | }; 761 | 762 | /** 763 | * @returns string containing jasmine version build info, if set. 764 | */ 765 | jasmine.Env.prototype.versionString = function() { 766 | if (!jasmine.version_) { 767 | return "version unknown"; 768 | } 769 | 770 | var version = this.version(); 771 | var versionString = version.major + "." + version.minor + "." + version.build; 772 | if (version.release_candidate) { 773 | versionString += ".rc" + version.release_candidate; 774 | } 775 | versionString += " revision " + version.revision; 776 | return versionString; 777 | }; 778 | 779 | /** 780 | * @returns a sequential integer starting at 0 781 | */ 782 | jasmine.Env.prototype.nextSpecId = function () { 783 | return this.nextSpecId_++; 784 | }; 785 | 786 | /** 787 | * @returns a sequential integer starting at 0 788 | */ 789 | jasmine.Env.prototype.nextSuiteId = function () { 790 | return this.nextSuiteId_++; 791 | }; 792 | 793 | /** 794 | * Register a reporter to receive status updates from Jasmine. 795 | * @param {jasmine.Reporter} reporter An object which will receive status updates. 796 | */ 797 | jasmine.Env.prototype.addReporter = function(reporter) { 798 | this.reporter.addReporter(reporter); 799 | }; 800 | 801 | jasmine.Env.prototype.execute = function() { 802 | this.currentRunner_.execute(); 803 | }; 804 | 805 | jasmine.Env.prototype.describe = function(description, specDefinitions) { 806 | var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); 807 | 808 | var parentSuite = this.currentSuite; 809 | if (parentSuite) { 810 | parentSuite.add(suite); 811 | } else { 812 | this.currentRunner_.add(suite); 813 | } 814 | 815 | this.currentSuite = suite; 816 | 817 | var declarationError = null; 818 | try { 819 | specDefinitions.call(suite); 820 | } catch(e) { 821 | declarationError = e; 822 | } 823 | 824 | if (declarationError) { 825 | this.it("encountered a declaration exception", function() { 826 | throw declarationError; 827 | }); 828 | } 829 | 830 | this.currentSuite = parentSuite; 831 | 832 | return suite; 833 | }; 834 | 835 | jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { 836 | if (this.currentSuite) { 837 | this.currentSuite.beforeEach(beforeEachFunction); 838 | } else { 839 | this.currentRunner_.beforeEach(beforeEachFunction); 840 | } 841 | }; 842 | 843 | jasmine.Env.prototype.currentRunner = function () { 844 | return this.currentRunner_; 845 | }; 846 | 847 | jasmine.Env.prototype.afterEach = function(afterEachFunction) { 848 | if (this.currentSuite) { 849 | this.currentSuite.afterEach(afterEachFunction); 850 | } else { 851 | this.currentRunner_.afterEach(afterEachFunction); 852 | } 853 | 854 | }; 855 | 856 | jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { 857 | return { 858 | execute: function() { 859 | } 860 | }; 861 | }; 862 | 863 | jasmine.Env.prototype.it = function(description, func) { 864 | var spec = new jasmine.Spec(this, this.currentSuite, description); 865 | this.currentSuite.add(spec); 866 | this.currentSpec = spec; 867 | 868 | if (func) { 869 | spec.runs(func); 870 | } 871 | 872 | return spec; 873 | }; 874 | 875 | jasmine.Env.prototype.xit = function(desc, func) { 876 | return { 877 | id: this.nextSpecId(), 878 | runs: function() { 879 | } 880 | }; 881 | }; 882 | 883 | jasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) { 884 | if (a.source != b.source) 885 | mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/"); 886 | 887 | if (a.ignoreCase != b.ignoreCase) 888 | mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier"); 889 | 890 | if (a.global != b.global) 891 | mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier"); 892 | 893 | if (a.multiline != b.multiline) 894 | mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier"); 895 | 896 | if (a.sticky != b.sticky) 897 | mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier"); 898 | 899 | return (mismatchValues.length === 0); 900 | }; 901 | 902 | jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { 903 | if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { 904 | return true; 905 | } 906 | 907 | a.__Jasmine_been_here_before__ = b; 908 | b.__Jasmine_been_here_before__ = a; 909 | 910 | var hasKey = function(obj, keyName) { 911 | return obj !== null && obj[keyName] !== jasmine.undefined; 912 | }; 913 | 914 | for (var property in b) { 915 | if (!hasKey(a, property) && hasKey(b, property)) { 916 | mismatchKeys.push("expected has key '" + property + "', but missing from actual."); 917 | } 918 | } 919 | for (property in a) { 920 | if (!hasKey(b, property) && hasKey(a, property)) { 921 | mismatchKeys.push("expected missing key '" + property + "', but present in actual."); 922 | } 923 | } 924 | for (property in b) { 925 | if (property == '__Jasmine_been_here_before__') continue; 926 | if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { 927 | mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); 928 | } 929 | } 930 | 931 | if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { 932 | mismatchValues.push("arrays were not the same length"); 933 | } 934 | 935 | delete a.__Jasmine_been_here_before__; 936 | delete b.__Jasmine_been_here_before__; 937 | return (mismatchKeys.length === 0 && mismatchValues.length === 0); 938 | }; 939 | 940 | jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { 941 | mismatchKeys = mismatchKeys || []; 942 | mismatchValues = mismatchValues || []; 943 | 944 | for (var i = 0; i < this.equalityTesters_.length; i++) { 945 | var equalityTester = this.equalityTesters_[i]; 946 | var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); 947 | if (result !== jasmine.undefined) return result; 948 | } 949 | 950 | if (a === b) return true; 951 | 952 | if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { 953 | return (a == jasmine.undefined && b == jasmine.undefined); 954 | } 955 | 956 | if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { 957 | return a === b; 958 | } 959 | 960 | if (a instanceof Date && b instanceof Date) { 961 | return a.getTime() == b.getTime(); 962 | } 963 | 964 | if (a.jasmineMatches) { 965 | return a.jasmineMatches(b); 966 | } 967 | 968 | if (b.jasmineMatches) { 969 | return b.jasmineMatches(a); 970 | } 971 | 972 | if (a instanceof jasmine.Matchers.ObjectContaining) { 973 | return a.matches(b); 974 | } 975 | 976 | if (b instanceof jasmine.Matchers.ObjectContaining) { 977 | return b.matches(a); 978 | } 979 | 980 | if (jasmine.isString_(a) && jasmine.isString_(b)) { 981 | return (a == b); 982 | } 983 | 984 | if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { 985 | return (a == b); 986 | } 987 | 988 | if (a instanceof RegExp && b instanceof RegExp) { 989 | return this.compareRegExps_(a, b, mismatchKeys, mismatchValues); 990 | } 991 | 992 | if (typeof a === "object" && typeof b === "object") { 993 | return this.compareObjects_(a, b, mismatchKeys, mismatchValues); 994 | } 995 | 996 | //Straight check 997 | return (a === b); 998 | }; 999 | 1000 | jasmine.Env.prototype.contains_ = function(haystack, needle) { 1001 | if (jasmine.isArray_(haystack)) { 1002 | for (var i = 0; i < haystack.length; i++) { 1003 | if (this.equals_(haystack[i], needle)) return true; 1004 | } 1005 | return false; 1006 | } 1007 | return haystack.indexOf(needle) >= 0; 1008 | }; 1009 | 1010 | jasmine.Env.prototype.addEqualityTester = function(equalityTester) { 1011 | this.equalityTesters_.push(equalityTester); 1012 | }; 1013 | /** No-op base class for Jasmine reporters. 1014 | * 1015 | * @constructor 1016 | */ 1017 | jasmine.Reporter = function() { 1018 | }; 1019 | 1020 | //noinspection JSUnusedLocalSymbols 1021 | jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { 1022 | }; 1023 | 1024 | //noinspection JSUnusedLocalSymbols 1025 | jasmine.Reporter.prototype.reportRunnerResults = function(runner) { 1026 | }; 1027 | 1028 | //noinspection JSUnusedLocalSymbols 1029 | jasmine.Reporter.prototype.reportSuiteResults = function(suite) { 1030 | }; 1031 | 1032 | //noinspection JSUnusedLocalSymbols 1033 | jasmine.Reporter.prototype.reportSpecStarting = function(spec) { 1034 | }; 1035 | 1036 | //noinspection JSUnusedLocalSymbols 1037 | jasmine.Reporter.prototype.reportSpecResults = function(spec) { 1038 | }; 1039 | 1040 | //noinspection JSUnusedLocalSymbols 1041 | jasmine.Reporter.prototype.log = function(str) { 1042 | }; 1043 | 1044 | /** 1045 | * Blocks are functions with executable code that make up a spec. 1046 | * 1047 | * @constructor 1048 | * @param {jasmine.Env} env 1049 | * @param {Function} func 1050 | * @param {jasmine.Spec} spec 1051 | */ 1052 | jasmine.Block = function(env, func, spec) { 1053 | this.env = env; 1054 | this.func = func; 1055 | this.spec = spec; 1056 | }; 1057 | 1058 | jasmine.Block.prototype.execute = function(onComplete) { 1059 | if (!jasmine.CATCH_EXCEPTIONS) { 1060 | this.func.apply(this.spec); 1061 | } 1062 | else { 1063 | try { 1064 | this.func.apply(this.spec); 1065 | } catch (e) { 1066 | this.spec.fail(e); 1067 | } 1068 | } 1069 | onComplete(); 1070 | }; 1071 | /** JavaScript API reporter. 1072 | * 1073 | * @constructor 1074 | */ 1075 | jasmine.JsApiReporter = function() { 1076 | this.started = false; 1077 | this.finished = false; 1078 | this.suites_ = []; 1079 | this.results_ = {}; 1080 | }; 1081 | 1082 | jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { 1083 | this.started = true; 1084 | var suites = runner.topLevelSuites(); 1085 | for (var i = 0; i < suites.length; i++) { 1086 | var suite = suites[i]; 1087 | this.suites_.push(this.summarize_(suite)); 1088 | } 1089 | }; 1090 | 1091 | jasmine.JsApiReporter.prototype.suites = function() { 1092 | return this.suites_; 1093 | }; 1094 | 1095 | jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { 1096 | var isSuite = suiteOrSpec instanceof jasmine.Suite; 1097 | var summary = { 1098 | id: suiteOrSpec.id, 1099 | name: suiteOrSpec.description, 1100 | type: isSuite ? 'suite' : 'spec', 1101 | children: [] 1102 | }; 1103 | 1104 | if (isSuite) { 1105 | var children = suiteOrSpec.children(); 1106 | for (var i = 0; i < children.length; i++) { 1107 | summary.children.push(this.summarize_(children[i])); 1108 | } 1109 | } 1110 | return summary; 1111 | }; 1112 | 1113 | jasmine.JsApiReporter.prototype.results = function() { 1114 | return this.results_; 1115 | }; 1116 | 1117 | jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { 1118 | return this.results_[specId]; 1119 | }; 1120 | 1121 | //noinspection JSUnusedLocalSymbols 1122 | jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { 1123 | this.finished = true; 1124 | }; 1125 | 1126 | //noinspection JSUnusedLocalSymbols 1127 | jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { 1128 | }; 1129 | 1130 | //noinspection JSUnusedLocalSymbols 1131 | jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { 1132 | this.results_[spec.id] = { 1133 | messages: spec.results().getItems(), 1134 | result: spec.results().failedCount > 0 ? "failed" : "passed" 1135 | }; 1136 | }; 1137 | 1138 | //noinspection JSUnusedLocalSymbols 1139 | jasmine.JsApiReporter.prototype.log = function(str) { 1140 | }; 1141 | 1142 | jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ 1143 | var results = {}; 1144 | for (var i = 0; i < specIds.length; i++) { 1145 | var specId = specIds[i]; 1146 | results[specId] = this.summarizeResult_(this.results_[specId]); 1147 | } 1148 | return results; 1149 | }; 1150 | 1151 | jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ 1152 | var summaryMessages = []; 1153 | var messagesLength = result.messages.length; 1154 | for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { 1155 | var resultMessage = result.messages[messageIndex]; 1156 | summaryMessages.push({ 1157 | text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, 1158 | passed: resultMessage.passed ? resultMessage.passed() : true, 1159 | type: resultMessage.type, 1160 | message: resultMessage.message, 1161 | trace: { 1162 | stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined 1163 | } 1164 | }); 1165 | } 1166 | 1167 | return { 1168 | result : result.result, 1169 | messages : summaryMessages 1170 | }; 1171 | }; 1172 | 1173 | /** 1174 | * @constructor 1175 | * @param {jasmine.Env} env 1176 | * @param actual 1177 | * @param {jasmine.Spec} spec 1178 | */ 1179 | jasmine.Matchers = function(env, actual, spec, opt_isNot) { 1180 | this.env = env; 1181 | this.actual = actual; 1182 | this.spec = spec; 1183 | this.isNot = opt_isNot || false; 1184 | this.reportWasCalled_ = false; 1185 | }; 1186 | 1187 | // todo: @deprecated as of Jasmine 0.11, remove soon [xw] 1188 | jasmine.Matchers.pp = function(str) { 1189 | throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); 1190 | }; 1191 | 1192 | // todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] 1193 | jasmine.Matchers.prototype.report = function(result, failing_message, details) { 1194 | throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); 1195 | }; 1196 | 1197 | jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { 1198 | for (var methodName in prototype) { 1199 | if (methodName == 'report') continue; 1200 | var orig = prototype[methodName]; 1201 | matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); 1202 | } 1203 | }; 1204 | 1205 | jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { 1206 | return function() { 1207 | var matcherArgs = jasmine.util.argsToArray(arguments); 1208 | var result = matcherFunction.apply(this, arguments); 1209 | 1210 | if (this.isNot) { 1211 | result = !result; 1212 | } 1213 | 1214 | if (this.reportWasCalled_) return result; 1215 | 1216 | var message; 1217 | if (!result) { 1218 | if (this.message) { 1219 | message = this.message.apply(this, arguments); 1220 | if (jasmine.isArray_(message)) { 1221 | message = message[this.isNot ? 1 : 0]; 1222 | } 1223 | } else { 1224 | var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); 1225 | message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; 1226 | if (matcherArgs.length > 0) { 1227 | for (var i = 0; i < matcherArgs.length; i++) { 1228 | if (i > 0) message += ","; 1229 | message += " " + jasmine.pp(matcherArgs[i]); 1230 | } 1231 | } 1232 | message += "."; 1233 | } 1234 | } 1235 | var expectationResult = new jasmine.ExpectationResult({ 1236 | matcherName: matcherName, 1237 | passed: result, 1238 | expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], 1239 | actual: this.actual, 1240 | message: message 1241 | }); 1242 | this.spec.addMatcherResult(expectationResult); 1243 | return jasmine.undefined; 1244 | }; 1245 | }; 1246 | 1247 | 1248 | 1249 | 1250 | /** 1251 | * toBe: compares the actual to the expected using === 1252 | * @param expected 1253 | */ 1254 | jasmine.Matchers.prototype.toBe = function(expected) { 1255 | return this.actual === expected; 1256 | }; 1257 | 1258 | /** 1259 | * toNotBe: compares the actual to the expected using !== 1260 | * @param expected 1261 | * @deprecated as of 1.0. Use not.toBe() instead. 1262 | */ 1263 | jasmine.Matchers.prototype.toNotBe = function(expected) { 1264 | return this.actual !== expected; 1265 | }; 1266 | 1267 | /** 1268 | * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. 1269 | * 1270 | * @param expected 1271 | */ 1272 | jasmine.Matchers.prototype.toEqual = function(expected) { 1273 | return this.env.equals_(this.actual, expected); 1274 | }; 1275 | 1276 | /** 1277 | * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual 1278 | * @param expected 1279 | * @deprecated as of 1.0. Use not.toEqual() instead. 1280 | */ 1281 | jasmine.Matchers.prototype.toNotEqual = function(expected) { 1282 | return !this.env.equals_(this.actual, expected); 1283 | }; 1284 | 1285 | /** 1286 | * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes 1287 | * a pattern or a String. 1288 | * 1289 | * @param expected 1290 | */ 1291 | jasmine.Matchers.prototype.toMatch = function(expected) { 1292 | return new RegExp(expected).test(this.actual); 1293 | }; 1294 | 1295 | /** 1296 | * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch 1297 | * @param expected 1298 | * @deprecated as of 1.0. Use not.toMatch() instead. 1299 | */ 1300 | jasmine.Matchers.prototype.toNotMatch = function(expected) { 1301 | return !(new RegExp(expected).test(this.actual)); 1302 | }; 1303 | 1304 | /** 1305 | * Matcher that compares the actual to jasmine.undefined. 1306 | */ 1307 | jasmine.Matchers.prototype.toBeDefined = function() { 1308 | return (this.actual !== jasmine.undefined); 1309 | }; 1310 | 1311 | /** 1312 | * Matcher that compares the actual to jasmine.undefined. 1313 | */ 1314 | jasmine.Matchers.prototype.toBeUndefined = function() { 1315 | return (this.actual === jasmine.undefined); 1316 | }; 1317 | 1318 | /** 1319 | * Matcher that compares the actual to null. 1320 | */ 1321 | jasmine.Matchers.prototype.toBeNull = function() { 1322 | return (this.actual === null); 1323 | }; 1324 | 1325 | /** 1326 | * Matcher that compares the actual to NaN. 1327 | */ 1328 | jasmine.Matchers.prototype.toBeNaN = function() { 1329 | this.message = function() { 1330 | return [ "Expected " + jasmine.pp(this.actual) + " to be NaN." ]; 1331 | }; 1332 | 1333 | return (this.actual !== this.actual); 1334 | }; 1335 | 1336 | /** 1337 | * Matcher that boolean not-nots the actual. 1338 | */ 1339 | jasmine.Matchers.prototype.toBeTruthy = function() { 1340 | return !!this.actual; 1341 | }; 1342 | 1343 | 1344 | /** 1345 | * Matcher that boolean nots the actual. 1346 | */ 1347 | jasmine.Matchers.prototype.toBeFalsy = function() { 1348 | return !this.actual; 1349 | }; 1350 | 1351 | 1352 | /** 1353 | * Matcher that checks to see if the actual, a Jasmine spy, was called. 1354 | */ 1355 | jasmine.Matchers.prototype.toHaveBeenCalled = function() { 1356 | if (arguments.length > 0) { 1357 | throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); 1358 | } 1359 | 1360 | if (!jasmine.isSpy(this.actual)) { 1361 | throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); 1362 | } 1363 | 1364 | this.message = function() { 1365 | return [ 1366 | "Expected spy " + this.actual.identity + " to have been called.", 1367 | "Expected spy " + this.actual.identity + " not to have been called." 1368 | ]; 1369 | }; 1370 | 1371 | return this.actual.wasCalled; 1372 | }; 1373 | 1374 | /** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ 1375 | jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; 1376 | 1377 | /** 1378 | * Matcher that checks to see if the actual, a Jasmine spy, was not called. 1379 | * 1380 | * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead 1381 | */ 1382 | jasmine.Matchers.prototype.wasNotCalled = function() { 1383 | if (arguments.length > 0) { 1384 | throw new Error('wasNotCalled does not take arguments'); 1385 | } 1386 | 1387 | if (!jasmine.isSpy(this.actual)) { 1388 | throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); 1389 | } 1390 | 1391 | this.message = function() { 1392 | return [ 1393 | "Expected spy " + this.actual.identity + " to not have been called.", 1394 | "Expected spy " + this.actual.identity + " to have been called." 1395 | ]; 1396 | }; 1397 | 1398 | return !this.actual.wasCalled; 1399 | }; 1400 | 1401 | /** 1402 | * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. 1403 | * 1404 | * @example 1405 | * 1406 | */ 1407 | jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { 1408 | var expectedArgs = jasmine.util.argsToArray(arguments); 1409 | if (!jasmine.isSpy(this.actual)) { 1410 | throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); 1411 | } 1412 | this.message = function() { 1413 | var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was."; 1414 | var positiveMessage = ""; 1415 | if (this.actual.callCount === 0) { 1416 | positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called."; 1417 | } else { 1418 | positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but actual calls were " + jasmine.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '') 1419 | } 1420 | return [positiveMessage, invertedMessage]; 1421 | }; 1422 | 1423 | return this.env.contains_(this.actual.argsForCall, expectedArgs); 1424 | }; 1425 | 1426 | /** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ 1427 | jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; 1428 | 1429 | /** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ 1430 | jasmine.Matchers.prototype.wasNotCalledWith = function() { 1431 | var expectedArgs = jasmine.util.argsToArray(arguments); 1432 | if (!jasmine.isSpy(this.actual)) { 1433 | throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); 1434 | } 1435 | 1436 | this.message = function() { 1437 | return [ 1438 | "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", 1439 | "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" 1440 | ]; 1441 | }; 1442 | 1443 | return !this.env.contains_(this.actual.argsForCall, expectedArgs); 1444 | }; 1445 | 1446 | /** 1447 | * Matcher that checks that the expected item is an element in the actual Array. 1448 | * 1449 | * @param {Object} expected 1450 | */ 1451 | jasmine.Matchers.prototype.toContain = function(expected) { 1452 | return this.env.contains_(this.actual, expected); 1453 | }; 1454 | 1455 | /** 1456 | * Matcher that checks that the expected item is NOT an element in the actual Array. 1457 | * 1458 | * @param {Object} expected 1459 | * @deprecated as of 1.0. Use not.toContain() instead. 1460 | */ 1461 | jasmine.Matchers.prototype.toNotContain = function(expected) { 1462 | return !this.env.contains_(this.actual, expected); 1463 | }; 1464 | 1465 | jasmine.Matchers.prototype.toBeLessThan = function(expected) { 1466 | return this.actual < expected; 1467 | }; 1468 | 1469 | jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { 1470 | return this.actual > expected; 1471 | }; 1472 | 1473 | /** 1474 | * Matcher that checks that the expected item is equal to the actual item 1475 | * up to a given level of decimal precision (default 2). 1476 | * 1477 | * @param {Number} expected 1478 | * @param {Number} precision, as number of decimal places 1479 | */ 1480 | jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { 1481 | if (!(precision === 0)) { 1482 | precision = precision || 2; 1483 | } 1484 | return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2); 1485 | }; 1486 | 1487 | /** 1488 | * Matcher that checks that the expected exception was thrown by the actual. 1489 | * 1490 | * @param {String} [expected] 1491 | */ 1492 | jasmine.Matchers.prototype.toThrow = function(expected) { 1493 | var result = false; 1494 | var exception; 1495 | if (typeof this.actual != 'function') { 1496 | throw new Error('Actual is not a function'); 1497 | } 1498 | try { 1499 | this.actual(); 1500 | } catch (e) { 1501 | exception = e; 1502 | } 1503 | if (exception) { 1504 | result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); 1505 | } 1506 | 1507 | var not = this.isNot ? "not " : ""; 1508 | 1509 | this.message = function() { 1510 | if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { 1511 | return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' '); 1512 | } else { 1513 | return "Expected function to throw an exception."; 1514 | } 1515 | }; 1516 | 1517 | return result; 1518 | }; 1519 | 1520 | jasmine.Matchers.Any = function(expectedClass) { 1521 | this.expectedClass = expectedClass; 1522 | }; 1523 | 1524 | jasmine.Matchers.Any.prototype.jasmineMatches = function(other) { 1525 | if (this.expectedClass == String) { 1526 | return typeof other == 'string' || other instanceof String; 1527 | } 1528 | 1529 | if (this.expectedClass == Number) { 1530 | return typeof other == 'number' || other instanceof Number; 1531 | } 1532 | 1533 | if (this.expectedClass == Function) { 1534 | return typeof other == 'function' || other instanceof Function; 1535 | } 1536 | 1537 | if (this.expectedClass == Object) { 1538 | return typeof other == 'object'; 1539 | } 1540 | 1541 | return other instanceof this.expectedClass; 1542 | }; 1543 | 1544 | jasmine.Matchers.Any.prototype.jasmineToString = function() { 1545 | return ''; 1546 | }; 1547 | 1548 | jasmine.Matchers.ObjectContaining = function (sample) { 1549 | this.sample = sample; 1550 | }; 1551 | 1552 | jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) { 1553 | mismatchKeys = mismatchKeys || []; 1554 | mismatchValues = mismatchValues || []; 1555 | 1556 | var env = jasmine.getEnv(); 1557 | 1558 | var hasKey = function(obj, keyName) { 1559 | return obj != null && obj[keyName] !== jasmine.undefined; 1560 | }; 1561 | 1562 | for (var property in this.sample) { 1563 | if (!hasKey(other, property) && hasKey(this.sample, property)) { 1564 | mismatchKeys.push("expected has key '" + property + "', but missing from actual."); 1565 | } 1566 | else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) { 1567 | mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual."); 1568 | } 1569 | } 1570 | 1571 | return (mismatchKeys.length === 0 && mismatchValues.length === 0); 1572 | }; 1573 | 1574 | jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () { 1575 | return ""; 1576 | }; 1577 | // Mock setTimeout, clearTimeout 1578 | // Contributed by Pivotal Computer Systems, www.pivotalsf.com 1579 | 1580 | jasmine.FakeTimer = function() { 1581 | this.reset(); 1582 | 1583 | var self = this; 1584 | self.setTimeout = function(funcToCall, millis) { 1585 | self.timeoutsMade++; 1586 | self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); 1587 | return self.timeoutsMade; 1588 | }; 1589 | 1590 | self.setInterval = function(funcToCall, millis) { 1591 | self.timeoutsMade++; 1592 | self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); 1593 | return self.timeoutsMade; 1594 | }; 1595 | 1596 | self.clearTimeout = function(timeoutKey) { 1597 | self.scheduledFunctions[timeoutKey] = jasmine.undefined; 1598 | }; 1599 | 1600 | self.clearInterval = function(timeoutKey) { 1601 | self.scheduledFunctions[timeoutKey] = jasmine.undefined; 1602 | }; 1603 | 1604 | }; 1605 | 1606 | jasmine.FakeTimer.prototype.reset = function() { 1607 | this.timeoutsMade = 0; 1608 | this.scheduledFunctions = {}; 1609 | this.nowMillis = 0; 1610 | }; 1611 | 1612 | jasmine.FakeTimer.prototype.tick = function(millis) { 1613 | var oldMillis = this.nowMillis; 1614 | var newMillis = oldMillis + millis; 1615 | this.runFunctionsWithinRange(oldMillis, newMillis); 1616 | this.nowMillis = newMillis; 1617 | }; 1618 | 1619 | jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { 1620 | var scheduledFunc; 1621 | var funcsToRun = []; 1622 | for (var timeoutKey in this.scheduledFunctions) { 1623 | scheduledFunc = this.scheduledFunctions[timeoutKey]; 1624 | if (scheduledFunc != jasmine.undefined && 1625 | scheduledFunc.runAtMillis >= oldMillis && 1626 | scheduledFunc.runAtMillis <= nowMillis) { 1627 | funcsToRun.push(scheduledFunc); 1628 | this.scheduledFunctions[timeoutKey] = jasmine.undefined; 1629 | } 1630 | } 1631 | 1632 | if (funcsToRun.length > 0) { 1633 | funcsToRun.sort(function(a, b) { 1634 | return a.runAtMillis - b.runAtMillis; 1635 | }); 1636 | for (var i = 0; i < funcsToRun.length; ++i) { 1637 | try { 1638 | var funcToRun = funcsToRun[i]; 1639 | this.nowMillis = funcToRun.runAtMillis; 1640 | funcToRun.funcToCall(); 1641 | if (funcToRun.recurring) { 1642 | this.scheduleFunction(funcToRun.timeoutKey, 1643 | funcToRun.funcToCall, 1644 | funcToRun.millis, 1645 | true); 1646 | } 1647 | } catch(e) { 1648 | } 1649 | } 1650 | this.runFunctionsWithinRange(oldMillis, nowMillis); 1651 | } 1652 | }; 1653 | 1654 | jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { 1655 | this.scheduledFunctions[timeoutKey] = { 1656 | runAtMillis: this.nowMillis + millis, 1657 | funcToCall: funcToCall, 1658 | recurring: recurring, 1659 | timeoutKey: timeoutKey, 1660 | millis: millis 1661 | }; 1662 | }; 1663 | 1664 | /** 1665 | * @namespace 1666 | */ 1667 | jasmine.Clock = { 1668 | defaultFakeTimer: new jasmine.FakeTimer(), 1669 | 1670 | reset: function() { 1671 | jasmine.Clock.assertInstalled(); 1672 | jasmine.Clock.defaultFakeTimer.reset(); 1673 | }, 1674 | 1675 | tick: function(millis) { 1676 | jasmine.Clock.assertInstalled(); 1677 | jasmine.Clock.defaultFakeTimer.tick(millis); 1678 | }, 1679 | 1680 | runFunctionsWithinRange: function(oldMillis, nowMillis) { 1681 | jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); 1682 | }, 1683 | 1684 | scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { 1685 | jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); 1686 | }, 1687 | 1688 | useMock: function() { 1689 | if (!jasmine.Clock.isInstalled()) { 1690 | var spec = jasmine.getEnv().currentSpec; 1691 | spec.after(jasmine.Clock.uninstallMock); 1692 | 1693 | jasmine.Clock.installMock(); 1694 | } 1695 | }, 1696 | 1697 | installMock: function() { 1698 | jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; 1699 | }, 1700 | 1701 | uninstallMock: function() { 1702 | jasmine.Clock.assertInstalled(); 1703 | jasmine.Clock.installed = jasmine.Clock.real; 1704 | }, 1705 | 1706 | real: { 1707 | setTimeout: jasmine.getGlobal().setTimeout, 1708 | clearTimeout: jasmine.getGlobal().clearTimeout, 1709 | setInterval: jasmine.getGlobal().setInterval, 1710 | clearInterval: jasmine.getGlobal().clearInterval 1711 | }, 1712 | 1713 | assertInstalled: function() { 1714 | if (!jasmine.Clock.isInstalled()) { 1715 | throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); 1716 | } 1717 | }, 1718 | 1719 | isInstalled: function() { 1720 | return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; 1721 | }, 1722 | 1723 | installed: null 1724 | }; 1725 | jasmine.Clock.installed = jasmine.Clock.real; 1726 | 1727 | //else for IE support 1728 | jasmine.getGlobal().setTimeout = function(funcToCall, millis) { 1729 | if (jasmine.Clock.installed.setTimeout.apply) { 1730 | return jasmine.Clock.installed.setTimeout.apply(this, arguments); 1731 | } else { 1732 | return jasmine.Clock.installed.setTimeout(funcToCall, millis); 1733 | } 1734 | }; 1735 | 1736 | jasmine.getGlobal().setInterval = function(funcToCall, millis) { 1737 | if (jasmine.Clock.installed.setInterval.apply) { 1738 | return jasmine.Clock.installed.setInterval.apply(this, arguments); 1739 | } else { 1740 | return jasmine.Clock.installed.setInterval(funcToCall, millis); 1741 | } 1742 | }; 1743 | 1744 | jasmine.getGlobal().clearTimeout = function(timeoutKey) { 1745 | if (jasmine.Clock.installed.clearTimeout.apply) { 1746 | return jasmine.Clock.installed.clearTimeout.apply(this, arguments); 1747 | } else { 1748 | return jasmine.Clock.installed.clearTimeout(timeoutKey); 1749 | } 1750 | }; 1751 | 1752 | jasmine.getGlobal().clearInterval = function(timeoutKey) { 1753 | if (jasmine.Clock.installed.clearTimeout.apply) { 1754 | return jasmine.Clock.installed.clearInterval.apply(this, arguments); 1755 | } else { 1756 | return jasmine.Clock.installed.clearInterval(timeoutKey); 1757 | } 1758 | }; 1759 | 1760 | /** 1761 | * @constructor 1762 | */ 1763 | jasmine.MultiReporter = function() { 1764 | this.subReporters_ = []; 1765 | }; 1766 | jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); 1767 | 1768 | jasmine.MultiReporter.prototype.addReporter = function(reporter) { 1769 | this.subReporters_.push(reporter); 1770 | }; 1771 | 1772 | (function() { 1773 | var functionNames = [ 1774 | "reportRunnerStarting", 1775 | "reportRunnerResults", 1776 | "reportSuiteResults", 1777 | "reportSpecStarting", 1778 | "reportSpecResults", 1779 | "log" 1780 | ]; 1781 | for (var i = 0; i < functionNames.length; i++) { 1782 | var functionName = functionNames[i]; 1783 | jasmine.MultiReporter.prototype[functionName] = (function(functionName) { 1784 | return function() { 1785 | for (var j = 0; j < this.subReporters_.length; j++) { 1786 | var subReporter = this.subReporters_[j]; 1787 | if (subReporter[functionName]) { 1788 | subReporter[functionName].apply(subReporter, arguments); 1789 | } 1790 | } 1791 | }; 1792 | })(functionName); 1793 | } 1794 | })(); 1795 | /** 1796 | * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults 1797 | * 1798 | * @constructor 1799 | */ 1800 | jasmine.NestedResults = function() { 1801 | /** 1802 | * The total count of results 1803 | */ 1804 | this.totalCount = 0; 1805 | /** 1806 | * Number of passed results 1807 | */ 1808 | this.passedCount = 0; 1809 | /** 1810 | * Number of failed results 1811 | */ 1812 | this.failedCount = 0; 1813 | /** 1814 | * Was this suite/spec skipped? 1815 | */ 1816 | this.skipped = false; 1817 | /** 1818 | * @ignore 1819 | */ 1820 | this.items_ = []; 1821 | }; 1822 | 1823 | /** 1824 | * Roll up the result counts. 1825 | * 1826 | * @param result 1827 | */ 1828 | jasmine.NestedResults.prototype.rollupCounts = function(result) { 1829 | this.totalCount += result.totalCount; 1830 | this.passedCount += result.passedCount; 1831 | this.failedCount += result.failedCount; 1832 | }; 1833 | 1834 | /** 1835 | * Adds a log message. 1836 | * @param values Array of message parts which will be concatenated later. 1837 | */ 1838 | jasmine.NestedResults.prototype.log = function(values) { 1839 | this.items_.push(new jasmine.MessageResult(values)); 1840 | }; 1841 | 1842 | /** 1843 | * Getter for the results: message & results. 1844 | */ 1845 | jasmine.NestedResults.prototype.getItems = function() { 1846 | return this.items_; 1847 | }; 1848 | 1849 | /** 1850 | * Adds a result, tracking counts (total, passed, & failed) 1851 | * @param {jasmine.ExpectationResult|jasmine.NestedResults} result 1852 | */ 1853 | jasmine.NestedResults.prototype.addResult = function(result) { 1854 | if (result.type != 'log') { 1855 | if (result.items_) { 1856 | this.rollupCounts(result); 1857 | } else { 1858 | this.totalCount++; 1859 | if (result.passed()) { 1860 | this.passedCount++; 1861 | } else { 1862 | this.failedCount++; 1863 | } 1864 | } 1865 | } 1866 | this.items_.push(result); 1867 | }; 1868 | 1869 | /** 1870 | * @returns {Boolean} True if everything below passed 1871 | */ 1872 | jasmine.NestedResults.prototype.passed = function() { 1873 | return this.passedCount === this.totalCount; 1874 | }; 1875 | /** 1876 | * Base class for pretty printing for expectation results. 1877 | */ 1878 | jasmine.PrettyPrinter = function() { 1879 | this.ppNestLevel_ = 0; 1880 | }; 1881 | 1882 | /** 1883 | * Formats a value in a nice, human-readable string. 1884 | * 1885 | * @param value 1886 | */ 1887 | jasmine.PrettyPrinter.prototype.format = function(value) { 1888 | this.ppNestLevel_++; 1889 | try { 1890 | if (value === jasmine.undefined) { 1891 | this.emitScalar('undefined'); 1892 | } else if (value === null) { 1893 | this.emitScalar('null'); 1894 | } else if (value === jasmine.getGlobal()) { 1895 | this.emitScalar(''); 1896 | } else if (value.jasmineToString) { 1897 | this.emitScalar(value.jasmineToString()); 1898 | } else if (typeof value === 'string') { 1899 | this.emitString(value); 1900 | } else if (jasmine.isSpy(value)) { 1901 | this.emitScalar("spy on " + value.identity); 1902 | } else if (value instanceof RegExp) { 1903 | this.emitScalar(value.toString()); 1904 | } else if (typeof value === 'function') { 1905 | this.emitScalar('Function'); 1906 | } else if (typeof value.nodeType === 'number') { 1907 | this.emitScalar('HTMLNode'); 1908 | } else if (value instanceof Date) { 1909 | this.emitScalar('Date(' + value + ')'); 1910 | } else if (value.__Jasmine_been_here_before__) { 1911 | this.emitScalar(''); 1912 | } else if (jasmine.isArray_(value) || typeof value == 'object') { 1913 | value.__Jasmine_been_here_before__ = true; 1914 | if (jasmine.isArray_(value)) { 1915 | this.emitArray(value); 1916 | } else { 1917 | this.emitObject(value); 1918 | } 1919 | delete value.__Jasmine_been_here_before__; 1920 | } else { 1921 | this.emitScalar(value.toString()); 1922 | } 1923 | } finally { 1924 | this.ppNestLevel_--; 1925 | } 1926 | }; 1927 | 1928 | jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { 1929 | for (var property in obj) { 1930 | if (!obj.hasOwnProperty(property)) continue; 1931 | if (property == '__Jasmine_been_here_before__') continue; 1932 | fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && 1933 | obj.__lookupGetter__(property) !== null) : false); 1934 | } 1935 | }; 1936 | 1937 | jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; 1938 | jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; 1939 | jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; 1940 | jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; 1941 | 1942 | jasmine.StringPrettyPrinter = function() { 1943 | jasmine.PrettyPrinter.call(this); 1944 | 1945 | this.string = ''; 1946 | }; 1947 | jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); 1948 | 1949 | jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { 1950 | this.append(value); 1951 | }; 1952 | 1953 | jasmine.StringPrettyPrinter.prototype.emitString = function(value) { 1954 | this.append("'" + value + "'"); 1955 | }; 1956 | 1957 | jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { 1958 | if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { 1959 | this.append("Array"); 1960 | return; 1961 | } 1962 | 1963 | this.append('[ '); 1964 | for (var i = 0; i < array.length; i++) { 1965 | if (i > 0) { 1966 | this.append(', '); 1967 | } 1968 | this.format(array[i]); 1969 | } 1970 | this.append(' ]'); 1971 | }; 1972 | 1973 | jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { 1974 | if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { 1975 | this.append("Object"); 1976 | return; 1977 | } 1978 | 1979 | var self = this; 1980 | this.append('{ '); 1981 | var first = true; 1982 | 1983 | this.iterateObject(obj, function(property, isGetter) { 1984 | if (first) { 1985 | first = false; 1986 | } else { 1987 | self.append(', '); 1988 | } 1989 | 1990 | self.append(property); 1991 | self.append(' : '); 1992 | if (isGetter) { 1993 | self.append(''); 1994 | } else { 1995 | self.format(obj[property]); 1996 | } 1997 | }); 1998 | 1999 | this.append(' }'); 2000 | }; 2001 | 2002 | jasmine.StringPrettyPrinter.prototype.append = function(value) { 2003 | this.string += value; 2004 | }; 2005 | jasmine.Queue = function(env) { 2006 | this.env = env; 2007 | 2008 | // parallel to blocks. each true value in this array means the block will 2009 | // get executed even if we abort 2010 | this.ensured = []; 2011 | this.blocks = []; 2012 | this.running = false; 2013 | this.index = 0; 2014 | this.offset = 0; 2015 | this.abort = false; 2016 | }; 2017 | 2018 | jasmine.Queue.prototype.addBefore = function(block, ensure) { 2019 | if (ensure === jasmine.undefined) { 2020 | ensure = false; 2021 | } 2022 | 2023 | this.blocks.unshift(block); 2024 | this.ensured.unshift(ensure); 2025 | }; 2026 | 2027 | jasmine.Queue.prototype.add = function(block, ensure) { 2028 | if (ensure === jasmine.undefined) { 2029 | ensure = false; 2030 | } 2031 | 2032 | this.blocks.push(block); 2033 | this.ensured.push(ensure); 2034 | }; 2035 | 2036 | jasmine.Queue.prototype.insertNext = function(block, ensure) { 2037 | if (ensure === jasmine.undefined) { 2038 | ensure = false; 2039 | } 2040 | 2041 | this.ensured.splice((this.index + this.offset + 1), 0, ensure); 2042 | this.blocks.splice((this.index + this.offset + 1), 0, block); 2043 | this.offset++; 2044 | }; 2045 | 2046 | jasmine.Queue.prototype.start = function(onComplete) { 2047 | this.running = true; 2048 | this.onComplete = onComplete; 2049 | this.next_(); 2050 | }; 2051 | 2052 | jasmine.Queue.prototype.isRunning = function() { 2053 | return this.running; 2054 | }; 2055 | 2056 | jasmine.Queue.LOOP_DONT_RECURSE = true; 2057 | 2058 | jasmine.Queue.prototype.next_ = function() { 2059 | var self = this; 2060 | var goAgain = true; 2061 | 2062 | while (goAgain) { 2063 | goAgain = false; 2064 | 2065 | if (self.index < self.blocks.length && !(this.abort && !this.ensured[self.index])) { 2066 | var calledSynchronously = true; 2067 | var completedSynchronously = false; 2068 | 2069 | var onComplete = function () { 2070 | if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { 2071 | completedSynchronously = true; 2072 | return; 2073 | } 2074 | 2075 | if (self.blocks[self.index].abort) { 2076 | self.abort = true; 2077 | } 2078 | 2079 | self.offset = 0; 2080 | self.index++; 2081 | 2082 | var now = new Date().getTime(); 2083 | if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { 2084 | self.env.lastUpdate = now; 2085 | self.env.setTimeout(function() { 2086 | self.next_(); 2087 | }, 0); 2088 | } else { 2089 | if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { 2090 | goAgain = true; 2091 | } else { 2092 | self.next_(); 2093 | } 2094 | } 2095 | }; 2096 | self.blocks[self.index].execute(onComplete); 2097 | 2098 | calledSynchronously = false; 2099 | if (completedSynchronously) { 2100 | onComplete(); 2101 | } 2102 | 2103 | } else { 2104 | self.running = false; 2105 | if (self.onComplete) { 2106 | self.onComplete(); 2107 | } 2108 | } 2109 | } 2110 | }; 2111 | 2112 | jasmine.Queue.prototype.results = function() { 2113 | var results = new jasmine.NestedResults(); 2114 | for (var i = 0; i < this.blocks.length; i++) { 2115 | if (this.blocks[i].results) { 2116 | results.addResult(this.blocks[i].results()); 2117 | } 2118 | } 2119 | return results; 2120 | }; 2121 | 2122 | 2123 | /** 2124 | * Runner 2125 | * 2126 | * @constructor 2127 | * @param {jasmine.Env} env 2128 | */ 2129 | jasmine.Runner = function(env) { 2130 | var self = this; 2131 | self.env = env; 2132 | self.queue = new jasmine.Queue(env); 2133 | self.before_ = []; 2134 | self.after_ = []; 2135 | self.suites_ = []; 2136 | }; 2137 | 2138 | jasmine.Runner.prototype.execute = function() { 2139 | var self = this; 2140 | if (self.env.reporter.reportRunnerStarting) { 2141 | self.env.reporter.reportRunnerStarting(this); 2142 | } 2143 | self.queue.start(function () { 2144 | self.finishCallback(); 2145 | }); 2146 | }; 2147 | 2148 | jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { 2149 | beforeEachFunction.typeName = 'beforeEach'; 2150 | this.before_.splice(0,0,beforeEachFunction); 2151 | }; 2152 | 2153 | jasmine.Runner.prototype.afterEach = function(afterEachFunction) { 2154 | afterEachFunction.typeName = 'afterEach'; 2155 | this.after_.splice(0,0,afterEachFunction); 2156 | }; 2157 | 2158 | 2159 | jasmine.Runner.prototype.finishCallback = function() { 2160 | this.env.reporter.reportRunnerResults(this); 2161 | }; 2162 | 2163 | jasmine.Runner.prototype.addSuite = function(suite) { 2164 | this.suites_.push(suite); 2165 | }; 2166 | 2167 | jasmine.Runner.prototype.add = function(block) { 2168 | if (block instanceof jasmine.Suite) { 2169 | this.addSuite(block); 2170 | } 2171 | this.queue.add(block); 2172 | }; 2173 | 2174 | jasmine.Runner.prototype.specs = function () { 2175 | var suites = this.suites(); 2176 | var specs = []; 2177 | for (var i = 0; i < suites.length; i++) { 2178 | specs = specs.concat(suites[i].specs()); 2179 | } 2180 | return specs; 2181 | }; 2182 | 2183 | jasmine.Runner.prototype.suites = function() { 2184 | return this.suites_; 2185 | }; 2186 | 2187 | jasmine.Runner.prototype.topLevelSuites = function() { 2188 | var topLevelSuites = []; 2189 | for (var i = 0; i < this.suites_.length; i++) { 2190 | if (!this.suites_[i].parentSuite) { 2191 | topLevelSuites.push(this.suites_[i]); 2192 | } 2193 | } 2194 | return topLevelSuites; 2195 | }; 2196 | 2197 | jasmine.Runner.prototype.results = function() { 2198 | return this.queue.results(); 2199 | }; 2200 | /** 2201 | * Internal representation of a Jasmine specification, or test. 2202 | * 2203 | * @constructor 2204 | * @param {jasmine.Env} env 2205 | * @param {jasmine.Suite} suite 2206 | * @param {String} description 2207 | */ 2208 | jasmine.Spec = function(env, suite, description) { 2209 | if (!env) { 2210 | throw new Error('jasmine.Env() required'); 2211 | } 2212 | if (!suite) { 2213 | throw new Error('jasmine.Suite() required'); 2214 | } 2215 | var spec = this; 2216 | spec.id = env.nextSpecId ? env.nextSpecId() : null; 2217 | spec.env = env; 2218 | spec.suite = suite; 2219 | spec.description = description; 2220 | spec.queue = new jasmine.Queue(env); 2221 | 2222 | spec.afterCallbacks = []; 2223 | spec.spies_ = []; 2224 | 2225 | spec.results_ = new jasmine.NestedResults(); 2226 | spec.results_.description = description; 2227 | spec.matchersClass = null; 2228 | }; 2229 | 2230 | jasmine.Spec.prototype.getFullName = function() { 2231 | return this.suite.getFullName() + ' ' + this.description + '.'; 2232 | }; 2233 | 2234 | 2235 | jasmine.Spec.prototype.results = function() { 2236 | return this.results_; 2237 | }; 2238 | 2239 | /** 2240 | * All parameters are pretty-printed and concatenated together, then written to the spec's output. 2241 | * 2242 | * Be careful not to leave calls to jasmine.log in production code. 2243 | */ 2244 | jasmine.Spec.prototype.log = function() { 2245 | return this.results_.log(arguments); 2246 | }; 2247 | 2248 | jasmine.Spec.prototype.runs = function (func) { 2249 | var block = new jasmine.Block(this.env, func, this); 2250 | this.addToQueue(block); 2251 | return this; 2252 | }; 2253 | 2254 | jasmine.Spec.prototype.addToQueue = function (block) { 2255 | if (this.queue.isRunning()) { 2256 | this.queue.insertNext(block); 2257 | } else { 2258 | this.queue.add(block); 2259 | } 2260 | }; 2261 | 2262 | /** 2263 | * @param {jasmine.ExpectationResult} result 2264 | */ 2265 | jasmine.Spec.prototype.addMatcherResult = function(result) { 2266 | this.results_.addResult(result); 2267 | }; 2268 | 2269 | jasmine.Spec.prototype.expect = function(actual) { 2270 | var positive = new (this.getMatchersClass_())(this.env, actual, this); 2271 | positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); 2272 | return positive; 2273 | }; 2274 | 2275 | /** 2276 | * Waits a fixed time period before moving to the next block. 2277 | * 2278 | * @deprecated Use waitsFor() instead 2279 | * @param {Number} timeout milliseconds to wait 2280 | */ 2281 | jasmine.Spec.prototype.waits = function(timeout) { 2282 | var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); 2283 | this.addToQueue(waitsFunc); 2284 | return this; 2285 | }; 2286 | 2287 | /** 2288 | * Waits for the latchFunction to return true before proceeding to the next block. 2289 | * 2290 | * @param {Function} latchFunction 2291 | * @param {String} optional_timeoutMessage 2292 | * @param {Number} optional_timeout 2293 | */ 2294 | jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { 2295 | var latchFunction_ = null; 2296 | var optional_timeoutMessage_ = null; 2297 | var optional_timeout_ = null; 2298 | 2299 | for (var i = 0; i < arguments.length; i++) { 2300 | var arg = arguments[i]; 2301 | switch (typeof arg) { 2302 | case 'function': 2303 | latchFunction_ = arg; 2304 | break; 2305 | case 'string': 2306 | optional_timeoutMessage_ = arg; 2307 | break; 2308 | case 'number': 2309 | optional_timeout_ = arg; 2310 | break; 2311 | } 2312 | } 2313 | 2314 | var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); 2315 | this.addToQueue(waitsForFunc); 2316 | return this; 2317 | }; 2318 | 2319 | jasmine.Spec.prototype.fail = function (e) { 2320 | var expectationResult = new jasmine.ExpectationResult({ 2321 | passed: false, 2322 | message: e ? jasmine.util.formatException(e) : 'Exception', 2323 | trace: { stack: e.stack } 2324 | }); 2325 | this.results_.addResult(expectationResult); 2326 | }; 2327 | 2328 | jasmine.Spec.prototype.getMatchersClass_ = function() { 2329 | return this.matchersClass || this.env.matchersClass; 2330 | }; 2331 | 2332 | jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { 2333 | var parent = this.getMatchersClass_(); 2334 | var newMatchersClass = function() { 2335 | parent.apply(this, arguments); 2336 | }; 2337 | jasmine.util.inherit(newMatchersClass, parent); 2338 | jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); 2339 | this.matchersClass = newMatchersClass; 2340 | }; 2341 | 2342 | jasmine.Spec.prototype.finishCallback = function() { 2343 | this.env.reporter.reportSpecResults(this); 2344 | }; 2345 | 2346 | jasmine.Spec.prototype.finish = function(onComplete) { 2347 | this.removeAllSpies(); 2348 | this.finishCallback(); 2349 | if (onComplete) { 2350 | onComplete(); 2351 | } 2352 | }; 2353 | 2354 | jasmine.Spec.prototype.after = function(doAfter) { 2355 | if (this.queue.isRunning()) { 2356 | this.queue.add(new jasmine.Block(this.env, doAfter, this), true); 2357 | } else { 2358 | this.afterCallbacks.unshift(doAfter); 2359 | } 2360 | }; 2361 | 2362 | jasmine.Spec.prototype.execute = function(onComplete) { 2363 | var spec = this; 2364 | if (!spec.env.specFilter(spec)) { 2365 | spec.results_.skipped = true; 2366 | spec.finish(onComplete); 2367 | return; 2368 | } 2369 | 2370 | this.env.reporter.reportSpecStarting(this); 2371 | 2372 | spec.env.currentSpec = spec; 2373 | 2374 | spec.addBeforesAndAftersToQueue(); 2375 | 2376 | spec.queue.start(function () { 2377 | spec.finish(onComplete); 2378 | }); 2379 | }; 2380 | 2381 | jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { 2382 | var runner = this.env.currentRunner(); 2383 | var i; 2384 | 2385 | for (var suite = this.suite; suite; suite = suite.parentSuite) { 2386 | for (i = 0; i < suite.before_.length; i++) { 2387 | this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); 2388 | } 2389 | } 2390 | for (i = 0; i < runner.before_.length; i++) { 2391 | this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); 2392 | } 2393 | for (i = 0; i < this.afterCallbacks.length; i++) { 2394 | this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this), true); 2395 | } 2396 | for (suite = this.suite; suite; suite = suite.parentSuite) { 2397 | for (i = 0; i < suite.after_.length; i++) { 2398 | this.queue.add(new jasmine.Block(this.env, suite.after_[i], this), true); 2399 | } 2400 | } 2401 | for (i = 0; i < runner.after_.length; i++) { 2402 | this.queue.add(new jasmine.Block(this.env, runner.after_[i], this), true); 2403 | } 2404 | }; 2405 | 2406 | jasmine.Spec.prototype.explodes = function() { 2407 | throw 'explodes function should not have been called'; 2408 | }; 2409 | 2410 | jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { 2411 | if (obj == jasmine.undefined) { 2412 | throw "spyOn could not find an object to spy upon for " + methodName + "()"; 2413 | } 2414 | 2415 | if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { 2416 | throw methodName + '() method does not exist'; 2417 | } 2418 | 2419 | if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { 2420 | throw new Error(methodName + ' has already been spied upon'); 2421 | } 2422 | 2423 | var spyObj = jasmine.createSpy(methodName); 2424 | 2425 | this.spies_.push(spyObj); 2426 | spyObj.baseObj = obj; 2427 | spyObj.methodName = methodName; 2428 | spyObj.originalValue = obj[methodName]; 2429 | 2430 | obj[methodName] = spyObj; 2431 | 2432 | return spyObj; 2433 | }; 2434 | 2435 | jasmine.Spec.prototype.removeAllSpies = function() { 2436 | for (var i = 0; i < this.spies_.length; i++) { 2437 | var spy = this.spies_[i]; 2438 | spy.baseObj[spy.methodName] = spy.originalValue; 2439 | } 2440 | this.spies_ = []; 2441 | }; 2442 | 2443 | /** 2444 | * Internal representation of a Jasmine suite. 2445 | * 2446 | * @constructor 2447 | * @param {jasmine.Env} env 2448 | * @param {String} description 2449 | * @param {Function} specDefinitions 2450 | * @param {jasmine.Suite} parentSuite 2451 | */ 2452 | jasmine.Suite = function(env, description, specDefinitions, parentSuite) { 2453 | var self = this; 2454 | self.id = env.nextSuiteId ? env.nextSuiteId() : null; 2455 | self.description = description; 2456 | self.queue = new jasmine.Queue(env); 2457 | self.parentSuite = parentSuite; 2458 | self.env = env; 2459 | self.before_ = []; 2460 | self.after_ = []; 2461 | self.children_ = []; 2462 | self.suites_ = []; 2463 | self.specs_ = []; 2464 | }; 2465 | 2466 | jasmine.Suite.prototype.getFullName = function() { 2467 | var fullName = this.description; 2468 | for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { 2469 | fullName = parentSuite.description + ' ' + fullName; 2470 | } 2471 | return fullName; 2472 | }; 2473 | 2474 | jasmine.Suite.prototype.finish = function(onComplete) { 2475 | this.env.reporter.reportSuiteResults(this); 2476 | this.finished = true; 2477 | if (typeof(onComplete) == 'function') { 2478 | onComplete(); 2479 | } 2480 | }; 2481 | 2482 | jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { 2483 | beforeEachFunction.typeName = 'beforeEach'; 2484 | this.before_.unshift(beforeEachFunction); 2485 | }; 2486 | 2487 | jasmine.Suite.prototype.afterEach = function(afterEachFunction) { 2488 | afterEachFunction.typeName = 'afterEach'; 2489 | this.after_.unshift(afterEachFunction); 2490 | }; 2491 | 2492 | jasmine.Suite.prototype.results = function() { 2493 | return this.queue.results(); 2494 | }; 2495 | 2496 | jasmine.Suite.prototype.add = function(suiteOrSpec) { 2497 | this.children_.push(suiteOrSpec); 2498 | if (suiteOrSpec instanceof jasmine.Suite) { 2499 | this.suites_.push(suiteOrSpec); 2500 | this.env.currentRunner().addSuite(suiteOrSpec); 2501 | } else { 2502 | this.specs_.push(suiteOrSpec); 2503 | } 2504 | this.queue.add(suiteOrSpec); 2505 | }; 2506 | 2507 | jasmine.Suite.prototype.specs = function() { 2508 | return this.specs_; 2509 | }; 2510 | 2511 | jasmine.Suite.prototype.suites = function() { 2512 | return this.suites_; 2513 | }; 2514 | 2515 | jasmine.Suite.prototype.children = function() { 2516 | return this.children_; 2517 | }; 2518 | 2519 | jasmine.Suite.prototype.execute = function(onComplete) { 2520 | var self = this; 2521 | this.queue.start(function () { 2522 | self.finish(onComplete); 2523 | }); 2524 | }; 2525 | jasmine.WaitsBlock = function(env, timeout, spec) { 2526 | this.timeout = timeout; 2527 | jasmine.Block.call(this, env, null, spec); 2528 | }; 2529 | 2530 | jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); 2531 | 2532 | jasmine.WaitsBlock.prototype.execute = function (onComplete) { 2533 | if (jasmine.VERBOSE) { 2534 | this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); 2535 | } 2536 | this.env.setTimeout(function () { 2537 | onComplete(); 2538 | }, this.timeout); 2539 | }; 2540 | /** 2541 | * A block which waits for some condition to become true, with timeout. 2542 | * 2543 | * @constructor 2544 | * @extends jasmine.Block 2545 | * @param {jasmine.Env} env The Jasmine environment. 2546 | * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. 2547 | * @param {Function} latchFunction A function which returns true when the desired condition has been met. 2548 | * @param {String} message The message to display if the desired condition hasn't been met within the given time period. 2549 | * @param {jasmine.Spec} spec The Jasmine spec. 2550 | */ 2551 | jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { 2552 | this.timeout = timeout || env.defaultTimeoutInterval; 2553 | this.latchFunction = latchFunction; 2554 | this.message = message; 2555 | this.totalTimeSpentWaitingForLatch = 0; 2556 | jasmine.Block.call(this, env, null, spec); 2557 | }; 2558 | jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); 2559 | 2560 | jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; 2561 | 2562 | jasmine.WaitsForBlock.prototype.execute = function(onComplete) { 2563 | if (jasmine.VERBOSE) { 2564 | this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); 2565 | } 2566 | var latchFunctionResult; 2567 | try { 2568 | latchFunctionResult = this.latchFunction.apply(this.spec); 2569 | } catch (e) { 2570 | this.spec.fail(e); 2571 | onComplete(); 2572 | return; 2573 | } 2574 | 2575 | if (latchFunctionResult) { 2576 | onComplete(); 2577 | } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { 2578 | var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); 2579 | this.spec.fail({ 2580 | name: 'timeout', 2581 | message: message 2582 | }); 2583 | 2584 | this.abort = true; 2585 | onComplete(); 2586 | } else { 2587 | this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; 2588 | var self = this; 2589 | this.env.setTimeout(function() { 2590 | self.execute(onComplete); 2591 | }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); 2592 | } 2593 | }; 2594 | 2595 | jasmine.version_= { 2596 | "major": 1, 2597 | "minor": 3, 2598 | "build": 1, 2599 | "revision": 1354556913 2600 | }; 2601 | -------------------------------------------------------------------------------- /vendor/phantom-jasmine/console-runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | Jasmine Reporter that outputs test results to the browser console. 3 | Useful for running in a headless environment such as PhantomJs, ZombieJs etc. 4 | 5 | Usage: 6 | // From your html file that loads jasmine: 7 | jasmine.getEnv().addReporter(new jasmine.ConsoleReporter()); 8 | jasmine.getEnv().execute(); 9 | */ 10 | 11 | (function(jasmine, console) { 12 | if (!jasmine) { 13 | throw "jasmine library isn't loaded!"; 14 | } 15 | 16 | var ANSI = {} 17 | ANSI.color_map = { 18 | "green" : 32, 19 | "red" : 31 20 | } 21 | 22 | ANSI.colorize_text = function(text, color) { 23 | var color_code = this.color_map[color]; 24 | return "\033[" + color_code + "m" + text + "\033[0m"; 25 | } 26 | 27 | var ConsoleReporter = function() { 28 | if (!console || !console.log) { throw "console isn't present!"; } 29 | this.status = this.statuses.stopped; 30 | }; 31 | 32 | var proto = ConsoleReporter.prototype; 33 | proto.statuses = { 34 | stopped : "stopped", 35 | running : "running", 36 | fail : "fail", 37 | success : "success" 38 | }; 39 | 40 | proto.reportRunnerStarting = function(runner) { 41 | this.status = this.statuses.running; 42 | this.start_time = (new Date()).getTime(); 43 | this.executed_specs = 0; 44 | this.passed_specs = 0; 45 | this.log("Starting..."); 46 | }; 47 | 48 | proto.reportRunnerResults = function(runner) { 49 | var failed = this.executed_specs - this.passed_specs; 50 | var spec_str = this.executed_specs + (this.executed_specs === 1 ? " spec, " : " specs, "); 51 | var fail_str = failed + (failed === 1 ? " failure in " : " failures in "); 52 | var color = (failed > 0)? "red" : "green"; 53 | var dur = (new Date()).getTime() - this.start_time; 54 | 55 | this.log(""); 56 | this.log("Finished"); 57 | this.log("-----------------"); 58 | this.log(spec_str + fail_str + (dur/1000) + "s.", color); 59 | 60 | this.status = (failed > 0)? this.statuses.fail : this.statuses.success; 61 | 62 | /* Print something that signals that testing is over so that headless browsers 63 | like PhantomJs know when to terminate. */ 64 | this.log(""); 65 | this.log("ConsoleReporter finished"); 66 | }; 67 | 68 | 69 | proto.reportSpecStarting = function(spec) { 70 | this.executed_specs++; 71 | }; 72 | 73 | proto.reportSpecResults = function(spec) { 74 | if (spec.results().passed()) { 75 | this.passed_specs++; 76 | return; 77 | } 78 | 79 | var resultText = spec.suite.description + " : " + spec.description; 80 | this.log(resultText, "red"); 81 | 82 | var items = spec.results().getItems() 83 | for (var i = 0; i < items.length; i++) { 84 | var trace = items[i].trace.stack || items[i].trace; 85 | this.log(trace, "red"); 86 | } 87 | }; 88 | 89 | proto.reportSuiteResults = function(suite) { 90 | if (!suite.parentSuite) { return; } 91 | var results = suite.results(); 92 | var failed = results.totalCount - results.passedCount; 93 | var color = (failed > 0)? "red" : "green"; 94 | this.log(suite.description + ": " + results.passedCount + " of " + results.totalCount + " passed.", color); 95 | }; 96 | 97 | proto.log = function(str, color) { 98 | var text = (color != undefined)? ANSI.colorize_text(str, color) : str; 99 | console.log(text) 100 | }; 101 | 102 | jasmine.ConsoleReporter = ConsoleReporter; 103 | })(jasmine, console); 104 | 105 | -------------------------------------------------------------------------------- /vendor/underscore-1.4.2.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.4.2 2 | // http://underscorejs.org 3 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 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 `global` 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 push = ArrayProto.push, 25 | slice = ArrayProto.slice, 26 | concat = ArrayProto.concat, 27 | unshift = ArrayProto.unshift, 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.4.2'; 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; 78 | if (nativeForEach && obj.forEach === nativeForEach) { 79 | obj.forEach(iterator, context); 80 | } else if (obj.length === +obj.length) { 81 | for (var i = 0, l = obj.length; i < l; i++) { 82 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 83 | } 84 | } else { 85 | for (var key in obj) { 86 | if (_.has(obj, key)) { 87 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 88 | } 89 | } 90 | } 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[results.length] = iterator.call(context, value, index, list); 101 | }); 102 | return results; 103 | }; 104 | 105 | // **Reduce** builds up a single result from a list of values, aka `inject`, 106 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 107 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 108 | var initial = arguments.length > 2; 109 | if (obj == null) obj = []; 110 | if (nativeReduce && obj.reduce === nativeReduce) { 111 | if (context) iterator = _.bind(iterator, context); 112 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 113 | } 114 | each(obj, function(value, index, list) { 115 | if (!initial) { 116 | memo = value; 117 | initial = true; 118 | } else { 119 | memo = iterator.call(context, memo, value, index, list); 120 | } 121 | }); 122 | if (!initial) throw new TypeError('Reduce of empty array with no initial value'); 123 | return memo; 124 | }; 125 | 126 | // The right-associative version of reduce, also known as `foldr`. 127 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 128 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 129 | var initial = arguments.length > 2; 130 | if (obj == null) obj = []; 131 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 132 | if (context) iterator = _.bind(iterator, context); 133 | return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 134 | } 135 | var length = obj.length; 136 | if (length !== +length) { 137 | var keys = _.keys(obj); 138 | length = keys.length; 139 | } 140 | each(obj, function(value, index, list) { 141 | index = keys ? keys[--length] : --length; 142 | if (!initial) { 143 | memo = obj[index]; 144 | initial = true; 145 | } else { 146 | memo = iterator.call(context, memo, obj[index], index, list); 147 | } 148 | }); 149 | if (!initial) throw new TypeError('Reduce of empty array with no initial value'); 150 | return memo; 151 | }; 152 | 153 | // Return the first value which passes a truth test. Aliased as `detect`. 154 | _.find = _.detect = function(obj, iterator, context) { 155 | var result; 156 | any(obj, function(value, index, list) { 157 | if (iterator.call(context, value, index, list)) { 158 | result = value; 159 | return true; 160 | } 161 | }); 162 | return result; 163 | }; 164 | 165 | // Return all the elements that pass a truth test. 166 | // Delegates to **ECMAScript 5**'s native `filter` if available. 167 | // Aliased as `select`. 168 | _.filter = _.select = function(obj, iterator, context) { 169 | var results = []; 170 | if (obj == null) return results; 171 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 172 | each(obj, function(value, index, list) { 173 | if (iterator.call(context, value, index, list)) results[results.length] = value; 174 | }); 175 | return results; 176 | }; 177 | 178 | // Return all the elements for which a truth test fails. 179 | _.reject = function(obj, iterator, context) { 180 | var results = []; 181 | if (obj == null) return results; 182 | each(obj, function(value, index, list) { 183 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 184 | }); 185 | return results; 186 | }; 187 | 188 | // Determine whether all of the elements match a truth test. 189 | // Delegates to **ECMAScript 5**'s native `every` if available. 190 | // Aliased as `all`. 191 | _.every = _.all = function(obj, iterator, context) { 192 | iterator || (iterator = _.identity); 193 | var result = true; 194 | if (obj == null) return result; 195 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 196 | each(obj, function(value, index, list) { 197 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 198 | }); 199 | return !!result; 200 | }; 201 | 202 | // Determine if at least one element in the object matches a truth test. 203 | // Delegates to **ECMAScript 5**'s native `some` if available. 204 | // Aliased as `any`. 205 | var any = _.some = _.any = function(obj, iterator, context) { 206 | iterator || (iterator = _.identity); 207 | var result = false; 208 | if (obj == null) return result; 209 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 210 | each(obj, function(value, index, list) { 211 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 212 | }); 213 | return !!result; 214 | }; 215 | 216 | // Determine if the array or object contains a given value (using `===`). 217 | // Aliased as `include`. 218 | _.contains = _.include = function(obj, target) { 219 | var found = false; 220 | if (obj == null) return found; 221 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 222 | found = any(obj, function(value) { 223 | return value === target; 224 | }); 225 | return found; 226 | }; 227 | 228 | // Invoke a method (with arguments) on every item in a collection. 229 | _.invoke = function(obj, method) { 230 | var args = slice.call(arguments, 2); 231 | return _.map(obj, function(value) { 232 | return (_.isFunction(method) ? method : value[method]).apply(value, args); 233 | }); 234 | }; 235 | 236 | // Convenience version of a common use case of `map`: fetching a property. 237 | _.pluck = function(obj, key) { 238 | return _.map(obj, function(value){ return value[key]; }); 239 | }; 240 | 241 | // Convenience version of a common use case of `filter`: selecting only objects 242 | // with specific `key:value` pairs. 243 | _.where = function(obj, attrs) { 244 | if (_.isEmpty(attrs)) return []; 245 | return _.filter(obj, function(value) { 246 | for (var key in attrs) { 247 | if (attrs[key] !== value[key]) return false; 248 | } 249 | return true; 250 | }); 251 | }; 252 | 253 | // Return the maximum element or (element-based computation). 254 | // Can't optimize arrays of integers longer than 65,535 elements. 255 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797 256 | _.max = function(obj, iterator, context) { 257 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 258 | return Math.max.apply(Math, obj); 259 | } 260 | if (!iterator && _.isEmpty(obj)) return -Infinity; 261 | var result = {computed : -Infinity}; 262 | each(obj, function(value, index, list) { 263 | var computed = iterator ? iterator.call(context, value, index, list) : value; 264 | computed >= result.computed && (result = {value : value, computed : computed}); 265 | }); 266 | return result.value; 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 | if (!iterator && _.isEmpty(obj)) return Infinity; 275 | var result = {computed : Infinity}; 276 | each(obj, function(value, index, list) { 277 | var computed = iterator ? iterator.call(context, value, index, list) : value; 278 | computed < result.computed && (result = {value : value, computed : computed}); 279 | }); 280 | return result.value; 281 | }; 282 | 283 | // Shuffle an array. 284 | _.shuffle = function(obj) { 285 | var rand; 286 | var index = 0; 287 | var shuffled = []; 288 | each(obj, function(value) { 289 | rand = _.random(index++); 290 | shuffled[index - 1] = shuffled[rand]; 291 | shuffled[rand] = value; 292 | }); 293 | return shuffled; 294 | }; 295 | 296 | // An internal function to generate lookup iterators. 297 | var lookupIterator = function(value) { 298 | return _.isFunction(value) ? value : function(obj){ return obj[value]; }; 299 | }; 300 | 301 | // Sort the object's values by a criterion produced by an iterator. 302 | _.sortBy = function(obj, value, context) { 303 | var iterator = lookupIterator(value); 304 | return _.pluck(_.map(obj, function(value, index, list) { 305 | return { 306 | value : value, 307 | index : index, 308 | criteria : iterator.call(context, value, index, list) 309 | }; 310 | }).sort(function(left, right) { 311 | var a = left.criteria; 312 | var b = right.criteria; 313 | if (a !== b) { 314 | if (a > b || a === void 0) return 1; 315 | if (a < b || b === void 0) return -1; 316 | } 317 | return left.index < right.index ? -1 : 1; 318 | }), 'value'); 319 | }; 320 | 321 | // An internal function used for aggregate "group by" operations. 322 | var group = function(obj, value, context, behavior) { 323 | var result = {}; 324 | var iterator = lookupIterator(value); 325 | each(obj, function(value, index) { 326 | var key = iterator.call(context, value, index, obj); 327 | behavior(result, key, value); 328 | }); 329 | return result; 330 | }; 331 | 332 | // Groups the object's values by a criterion. Pass either a string attribute 333 | // to group by, or a function that returns the criterion. 334 | _.groupBy = function(obj, value, context) { 335 | return group(obj, value, context, function(result, key, value) { 336 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value); 337 | }); 338 | }; 339 | 340 | // Counts instances of an object that group by a certain criterion. Pass 341 | // either a string attribute to count by, or a function that returns the 342 | // criterion. 343 | _.countBy = function(obj, value, context) { 344 | return group(obj, value, context, function(result, key, value) { 345 | if (!_.has(result, key)) result[key] = 0; 346 | result[key]++; 347 | }); 348 | }; 349 | 350 | // Use a comparator function to figure out the smallest index at which 351 | // an object should be inserted so as to maintain order. Uses binary search. 352 | _.sortedIndex = function(array, obj, iterator, context) { 353 | iterator = iterator == null ? _.identity : lookupIterator(iterator); 354 | var value = iterator.call(context, obj); 355 | var low = 0, high = array.length; 356 | while (low < high) { 357 | var mid = (low + high) >>> 1; 358 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 359 | } 360 | return low; 361 | }; 362 | 363 | // Safely convert anything iterable into a real, live array. 364 | _.toArray = function(obj) { 365 | if (!obj) return []; 366 | if (obj.length === +obj.length) return slice.call(obj); 367 | return _.values(obj); 368 | }; 369 | 370 | // Return the number of elements in an object. 371 | _.size = function(obj) { 372 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 373 | }; 374 | 375 | // Array Functions 376 | // --------------- 377 | 378 | // Get the first element of an array. Passing **n** will return the first N 379 | // values in the array. Aliased as `head` and `take`. The **guard** check 380 | // allows it to work with `_.map`. 381 | _.first = _.head = _.take = function(array, n, guard) { 382 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 383 | }; 384 | 385 | // Returns everything but the last entry of the array. Especially useful on 386 | // the arguments object. Passing **n** will return all the values in 387 | // the array, excluding the last N. The **guard** check allows it to work with 388 | // `_.map`. 389 | _.initial = function(array, n, guard) { 390 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 391 | }; 392 | 393 | // Get the last element of an array. Passing **n** will return the last N 394 | // values in the array. The **guard** check allows it to work with `_.map`. 395 | _.last = function(array, n, guard) { 396 | if ((n != null) && !guard) { 397 | return slice.call(array, Math.max(array.length - n, 0)); 398 | } else { 399 | return array[array.length - 1]; 400 | } 401 | }; 402 | 403 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 404 | // Especially useful on the arguments object. Passing an **n** will return 405 | // the rest N values in the array. The **guard** 406 | // check allows it to work with `_.map`. 407 | _.rest = _.tail = _.drop = function(array, n, guard) { 408 | return slice.call(array, (n == null) || guard ? 1 : n); 409 | }; 410 | 411 | // Trim out all falsy values from an array. 412 | _.compact = function(array) { 413 | return _.filter(array, function(value){ return !!value; }); 414 | }; 415 | 416 | // Internal implementation of a recursive `flatten` function. 417 | var flatten = function(input, shallow, output) { 418 | each(input, function(value) { 419 | if (_.isArray(value)) { 420 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 421 | } else { 422 | output.push(value); 423 | } 424 | }); 425 | return output; 426 | }; 427 | 428 | // Return a completely flattened version of an array. 429 | _.flatten = function(array, shallow) { 430 | return flatten(array, shallow, []); 431 | }; 432 | 433 | // Return a version of the array that does not contain the specified value(s). 434 | _.without = function(array) { 435 | return _.difference(array, slice.call(arguments, 1)); 436 | }; 437 | 438 | // Produce a duplicate-free version of the array. If the array has already 439 | // been sorted, you have the option of using a faster algorithm. 440 | // Aliased as `unique`. 441 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 442 | var initial = iterator ? _.map(array, iterator, context) : array; 443 | var results = []; 444 | var seen = []; 445 | each(initial, function(value, index) { 446 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 447 | seen.push(value); 448 | results.push(array[index]); 449 | } 450 | }); 451 | return results; 452 | }; 453 | 454 | // Produce an array that contains the union: each distinct element from all of 455 | // the passed-in arrays. 456 | _.union = function() { 457 | return _.uniq(concat.apply(ArrayProto, arguments)); 458 | }; 459 | 460 | // Produce an array that contains every item shared between all the 461 | // passed-in arrays. 462 | _.intersection = function(array) { 463 | var rest = slice.call(arguments, 1); 464 | return _.filter(_.uniq(array), function(item) { 465 | return _.every(rest, function(other) { 466 | return _.indexOf(other, item) >= 0; 467 | }); 468 | }); 469 | }; 470 | 471 | // Take the difference between one array and a number of other arrays. 472 | // Only the elements present in just the first array will remain. 473 | _.difference = function(array) { 474 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 475 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 476 | }; 477 | 478 | // Zip together multiple lists into a single array -- elements that share 479 | // an index go together. 480 | _.zip = function() { 481 | var args = slice.call(arguments); 482 | var length = _.max(_.pluck(args, 'length')); 483 | var results = new Array(length); 484 | for (var i = 0; i < length; i++) { 485 | results[i] = _.pluck(args, "" + i); 486 | } 487 | return results; 488 | }; 489 | 490 | // Converts lists into objects. Pass either a single array of `[key, value]` 491 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 492 | // the corresponding values. 493 | _.object = function(list, values) { 494 | var result = {}; 495 | for (var i = 0, l = list.length; i < l; i++) { 496 | if (values) { 497 | result[list[i]] = values[i]; 498 | } else { 499 | result[list[i][0]] = list[i][1]; 500 | } 501 | } 502 | return result; 503 | }; 504 | 505 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 506 | // we need this function. Return the position of the first occurrence of an 507 | // item in an array, or -1 if the item is not included in the array. 508 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 509 | // If the array is large and already in sort order, pass `true` 510 | // for **isSorted** to use binary search. 511 | _.indexOf = function(array, item, isSorted) { 512 | if (array == null) return -1; 513 | var i = 0, l = array.length; 514 | if (isSorted) { 515 | if (typeof isSorted == 'number') { 516 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); 517 | } else { 518 | i = _.sortedIndex(array, item); 519 | return array[i] === item ? i : -1; 520 | } 521 | } 522 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 523 | for (; i < l; i++) if (array[i] === item) return i; 524 | return -1; 525 | }; 526 | 527 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 528 | _.lastIndexOf = function(array, item, from) { 529 | if (array == null) return -1; 530 | var hasIndex = from != null; 531 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 532 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 533 | } 534 | var i = (hasIndex ? from : array.length); 535 | while (i--) if (array[i] === item) return i; 536 | return -1; 537 | }; 538 | 539 | // Generate an integer Array containing an arithmetic progression. A port of 540 | // the native Python `range()` function. See 541 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 542 | _.range = function(start, stop, step) { 543 | if (arguments.length <= 1) { 544 | stop = start || 0; 545 | start = 0; 546 | } 547 | step = arguments[2] || 1; 548 | 549 | var len = Math.max(Math.ceil((stop - start) / step), 0); 550 | var idx = 0; 551 | var range = new Array(len); 552 | 553 | while(idx < len) { 554 | range[idx++] = start; 555 | start += step; 556 | } 557 | 558 | return range; 559 | }; 560 | 561 | // Function (ahem) Functions 562 | // ------------------ 563 | 564 | // Reusable constructor function for prototype setting. 565 | var ctor = function(){}; 566 | 567 | // Create a function bound to a given object (assigning `this`, and arguments, 568 | // optionally). Binding with arguments is also known as `curry`. 569 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 570 | // We check for `func.bind` first, to fail fast when `func` is undefined. 571 | _.bind = function bind(func, context) { 572 | var bound, args; 573 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 574 | if (!_.isFunction(func)) throw new TypeError; 575 | args = slice.call(arguments, 2); 576 | return bound = function() { 577 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 578 | ctor.prototype = func.prototype; 579 | var self = new ctor; 580 | var result = func.apply(self, args.concat(slice.call(arguments))); 581 | if (Object(result) === result) return result; 582 | return self; 583 | }; 584 | }; 585 | 586 | // Bind all of an object's methods to that object. Useful for ensuring that 587 | // all callbacks defined on an object belong to it. 588 | _.bindAll = function(obj) { 589 | var funcs = slice.call(arguments, 1); 590 | if (funcs.length == 0) funcs = _.functions(obj); 591 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 592 | return obj; 593 | }; 594 | 595 | // Memoize an expensive function by storing its results. 596 | _.memoize = function(func, hasher) { 597 | var memo = {}; 598 | hasher || (hasher = _.identity); 599 | return function() { 600 | var key = hasher.apply(this, arguments); 601 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 602 | }; 603 | }; 604 | 605 | // Delays a function for the given number of milliseconds, and then calls 606 | // it with the arguments supplied. 607 | _.delay = function(func, wait) { 608 | var args = slice.call(arguments, 2); 609 | return setTimeout(function(){ return func.apply(null, args); }, wait); 610 | }; 611 | 612 | // Defers a function, scheduling it to run after the current call stack has 613 | // cleared. 614 | _.defer = function(func) { 615 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 616 | }; 617 | 618 | // Returns a function, that, when invoked, will only be triggered at most once 619 | // during a given window of time. 620 | _.throttle = function(func, wait) { 621 | var context, args, timeout, throttling, more, result; 622 | var whenDone = _.debounce(function(){ more = throttling = false; }, wait); 623 | return function() { 624 | context = this; args = arguments; 625 | var later = function() { 626 | timeout = null; 627 | if (more) { 628 | result = func.apply(context, args); 629 | } 630 | whenDone(); 631 | }; 632 | if (!timeout) timeout = setTimeout(later, wait); 633 | if (throttling) { 634 | more = true; 635 | } else { 636 | throttling = true; 637 | result = func.apply(context, args); 638 | } 639 | whenDone(); 640 | return result; 641 | }; 642 | }; 643 | 644 | // Returns a function, that, as long as it continues to be invoked, will not 645 | // be triggered. The function will be called after it stops being called for 646 | // N milliseconds. If `immediate` is passed, trigger the function on the 647 | // leading edge, instead of the trailing. 648 | _.debounce = function(func, wait, immediate) { 649 | var timeout, result; 650 | return function() { 651 | var context = this, args = arguments; 652 | var later = function() { 653 | timeout = null; 654 | if (!immediate) result = func.apply(context, args); 655 | }; 656 | var callNow = immediate && !timeout; 657 | clearTimeout(timeout); 658 | timeout = setTimeout(later, wait); 659 | if (callNow) result = func.apply(context, args); 660 | return result; 661 | }; 662 | }; 663 | 664 | // Returns a function that will be executed at most one time, no matter how 665 | // often you call it. Useful for lazy initialization. 666 | _.once = function(func) { 667 | var ran = false, memo; 668 | return function() { 669 | if (ran) return memo; 670 | ran = true; 671 | memo = func.apply(this, arguments); 672 | func = null; 673 | return memo; 674 | }; 675 | }; 676 | 677 | // Returns the first function passed as an argument to the second, 678 | // allowing you to adjust arguments, run code before and after, and 679 | // conditionally execute the original function. 680 | _.wrap = function(func, wrapper) { 681 | return function() { 682 | var args = [func]; 683 | push.apply(args, arguments); 684 | return wrapper.apply(this, args); 685 | }; 686 | }; 687 | 688 | // Returns a function that is the composition of a list of functions, each 689 | // consuming the return value of the function that follows. 690 | _.compose = function() { 691 | var funcs = arguments; 692 | return function() { 693 | var args = arguments; 694 | for (var i = funcs.length - 1; i >= 0; i--) { 695 | args = [funcs[i].apply(this, args)]; 696 | } 697 | return args[0]; 698 | }; 699 | }; 700 | 701 | // Returns a function that will only be executed after being called N times. 702 | _.after = function(times, func) { 703 | if (times <= 0) return func(); 704 | return function() { 705 | if (--times < 1) { 706 | return func.apply(this, arguments); 707 | } 708 | }; 709 | }; 710 | 711 | // Object Functions 712 | // ---------------- 713 | 714 | // Retrieve the names of an object's properties. 715 | // Delegates to **ECMAScript 5**'s native `Object.keys` 716 | _.keys = nativeKeys || function(obj) { 717 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 718 | var keys = []; 719 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 720 | return keys; 721 | }; 722 | 723 | // Retrieve the values of an object's properties. 724 | _.values = function(obj) { 725 | var values = []; 726 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); 727 | return values; 728 | }; 729 | 730 | // Convert an object into a list of `[key, value]` pairs. 731 | _.pairs = function(obj) { 732 | var pairs = []; 733 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); 734 | return pairs; 735 | }; 736 | 737 | // Invert the keys and values of an object. The values must be serializable. 738 | _.invert = function(obj) { 739 | var result = {}; 740 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; 741 | return result; 742 | }; 743 | 744 | // Return a sorted list of the function names available on the object. 745 | // Aliased as `methods` 746 | _.functions = _.methods = function(obj) { 747 | var names = []; 748 | for (var key in obj) { 749 | if (_.isFunction(obj[key])) names.push(key); 750 | } 751 | return names.sort(); 752 | }; 753 | 754 | // Extend a given object with all the properties in passed-in object(s). 755 | _.extend = function(obj) { 756 | each(slice.call(arguments, 1), function(source) { 757 | for (var prop in source) { 758 | obj[prop] = source[prop]; 759 | } 760 | }); 761 | return obj; 762 | }; 763 | 764 | // Return a copy of the object only containing the whitelisted properties. 765 | _.pick = function(obj) { 766 | var copy = {}; 767 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 768 | each(keys, function(key) { 769 | if (key in obj) copy[key] = obj[key]; 770 | }); 771 | return copy; 772 | }; 773 | 774 | // Return a copy of the object without the blacklisted properties. 775 | _.omit = function(obj) { 776 | var copy = {}; 777 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 778 | for (var key in obj) { 779 | if (!_.contains(keys, key)) copy[key] = obj[key]; 780 | } 781 | return copy; 782 | }; 783 | 784 | // Fill in a given object with default properties. 785 | _.defaults = function(obj) { 786 | each(slice.call(arguments, 1), function(source) { 787 | for (var prop in source) { 788 | if (obj[prop] == null) obj[prop] = source[prop]; 789 | } 790 | }); 791 | return obj; 792 | }; 793 | 794 | // Create a (shallow-cloned) duplicate of an object. 795 | _.clone = function(obj) { 796 | if (!_.isObject(obj)) return obj; 797 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 798 | }; 799 | 800 | // Invokes interceptor with the obj, and then returns obj. 801 | // The primary purpose of this method is to "tap into" a method chain, in 802 | // order to perform operations on intermediate results within the chain. 803 | _.tap = function(obj, interceptor) { 804 | interceptor(obj); 805 | return obj; 806 | }; 807 | 808 | // Internal recursive comparison function for `isEqual`. 809 | var eq = function(a, b, aStack, bStack) { 810 | // Identical objects are equal. `0 === -0`, but they aren't identical. 811 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 812 | if (a === b) return a !== 0 || 1 / a == 1 / b; 813 | // A strict comparison is necessary because `null == undefined`. 814 | if (a == null || b == null) return a === b; 815 | // Unwrap any wrapped objects. 816 | if (a instanceof _) a = a._wrapped; 817 | if (b instanceof _) b = b._wrapped; 818 | // Compare `[[Class]]` names. 819 | var className = toString.call(a); 820 | if (className != toString.call(b)) return false; 821 | switch (className) { 822 | // Strings, numbers, dates, and booleans are compared by value. 823 | case '[object String]': 824 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 825 | // equivalent to `new String("5")`. 826 | return a == String(b); 827 | case '[object Number]': 828 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 829 | // other numeric values. 830 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 831 | case '[object Date]': 832 | case '[object Boolean]': 833 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 834 | // millisecond representations. Note that invalid dates with millisecond representations 835 | // of `NaN` are not equivalent. 836 | return +a == +b; 837 | // RegExps are compared by their source patterns and flags. 838 | case '[object RegExp]': 839 | return a.source == b.source && 840 | a.global == b.global && 841 | a.multiline == b.multiline && 842 | a.ignoreCase == b.ignoreCase; 843 | } 844 | if (typeof a != 'object' || typeof b != 'object') return false; 845 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 846 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 847 | var length = aStack.length; 848 | while (length--) { 849 | // Linear search. Performance is inversely proportional to the number of 850 | // unique nested structures. 851 | if (aStack[length] == a) return bStack[length] == b; 852 | } 853 | // Add the first object to the stack of traversed objects. 854 | aStack.push(a); 855 | bStack.push(b); 856 | var size = 0, result = true; 857 | // Recursively compare objects and arrays. 858 | if (className == '[object Array]') { 859 | // Compare array lengths to determine if a deep comparison is necessary. 860 | size = a.length; 861 | result = size == b.length; 862 | if (result) { 863 | // Deep compare the contents, ignoring non-numeric properties. 864 | while (size--) { 865 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 866 | } 867 | } 868 | } else { 869 | // Objects with different constructors are not equivalent, but `Object`s 870 | // from different frames are. 871 | var aCtor = a.constructor, bCtor = b.constructor; 872 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 873 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) { 874 | return false; 875 | } 876 | // Deep compare objects. 877 | for (var key in a) { 878 | if (_.has(a, key)) { 879 | // Count the expected number of properties. 880 | size++; 881 | // Deep compare each member. 882 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 883 | } 884 | } 885 | // Ensure that both objects contain the same number of properties. 886 | if (result) { 887 | for (key in b) { 888 | if (_.has(b, key) && !(size--)) break; 889 | } 890 | result = !size; 891 | } 892 | } 893 | // Remove the first object from the stack of traversed objects. 894 | aStack.pop(); 895 | bStack.pop(); 896 | return result; 897 | }; 898 | 899 | // Perform a deep comparison to check if two objects are equal. 900 | _.isEqual = function(a, b) { 901 | return eq(a, b, [], []); 902 | }; 903 | 904 | // Is a given array, string, or object empty? 905 | // An "empty" object has no enumerable own-properties. 906 | _.isEmpty = function(obj) { 907 | if (obj == null) return true; 908 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 909 | for (var key in obj) if (_.has(obj, key)) return false; 910 | return true; 911 | }; 912 | 913 | // Is a given value a DOM element? 914 | _.isElement = function(obj) { 915 | return !!(obj && obj.nodeType === 1); 916 | }; 917 | 918 | // Is a given value an array? 919 | // Delegates to ECMA5's native Array.isArray 920 | _.isArray = nativeIsArray || function(obj) { 921 | return toString.call(obj) == '[object Array]'; 922 | }; 923 | 924 | // Is a given variable an object? 925 | _.isObject = function(obj) { 926 | return obj === Object(obj); 927 | }; 928 | 929 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 930 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 931 | _['is' + name] = function(obj) { 932 | return toString.call(obj) == '[object ' + name + ']'; 933 | }; 934 | }); 935 | 936 | // Define a fallback version of the method in browsers (ahem, IE), where 937 | // there isn't any inspectable "Arguments" type. 938 | if (!_.isArguments(arguments)) { 939 | _.isArguments = function(obj) { 940 | return !!(obj && _.has(obj, 'callee')); 941 | }; 942 | } 943 | 944 | // Optimize `isFunction` if appropriate. 945 | if (typeof (/./) !== 'function') { 946 | _.isFunction = function(obj) { 947 | return typeof obj === 'function'; 948 | }; 949 | } 950 | 951 | // Is a given object a finite number? 952 | _.isFinite = function(obj) { 953 | return _.isNumber(obj) && isFinite(obj); 954 | }; 955 | 956 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 957 | _.isNaN = function(obj) { 958 | return _.isNumber(obj) && obj != +obj; 959 | }; 960 | 961 | // Is a given value a boolean? 962 | _.isBoolean = function(obj) { 963 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 964 | }; 965 | 966 | // Is a given value equal to null? 967 | _.isNull = function(obj) { 968 | return obj === null; 969 | }; 970 | 971 | // Is a given variable undefined? 972 | _.isUndefined = function(obj) { 973 | return obj === void 0; 974 | }; 975 | 976 | // Shortcut function for checking if an object has a given property directly 977 | // on itself (in other words, not on a prototype). 978 | _.has = function(obj, key) { 979 | return hasOwnProperty.call(obj, key); 980 | }; 981 | 982 | // Utility Functions 983 | // ----------------- 984 | 985 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 986 | // previous owner. Returns a reference to the Underscore object. 987 | _.noConflict = function() { 988 | root._ = previousUnderscore; 989 | return this; 990 | }; 991 | 992 | // Keep the identity function around for default iterators. 993 | _.identity = function(value) { 994 | return value; 995 | }; 996 | 997 | // Run a function **n** times. 998 | _.times = function(n, iterator, context) { 999 | for (var i = 0; i < n; i++) iterator.call(context, i); 1000 | }; 1001 | 1002 | // Return a random integer between min and max (inclusive). 1003 | _.random = function(min, max) { 1004 | if (max == null) { 1005 | max = min; 1006 | min = 0; 1007 | } 1008 | return min + (0 | Math.random() * (max - min + 1)); 1009 | }; 1010 | 1011 | // List of HTML entities for escaping. 1012 | var entityMap = { 1013 | escape: { 1014 | '&': '&', 1015 | '<': '<', 1016 | '>': '>', 1017 | '"': '"', 1018 | "'": ''', 1019 | '/': '/' 1020 | } 1021 | }; 1022 | entityMap.unescape = _.invert(entityMap.escape); 1023 | 1024 | // Regexes containing the keys and values listed immediately above. 1025 | var entityRegexes = { 1026 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1027 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1028 | }; 1029 | 1030 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1031 | _.each(['escape', 'unescape'], function(method) { 1032 | _[method] = function(string) { 1033 | if (string == null) return ''; 1034 | return ('' + string).replace(entityRegexes[method], function(match) { 1035 | return entityMap[method][match]; 1036 | }); 1037 | }; 1038 | }); 1039 | 1040 | // If the value of the named property is a function then invoke it; 1041 | // otherwise, return it. 1042 | _.result = function(object, property) { 1043 | if (object == null) return null; 1044 | var value = object[property]; 1045 | return _.isFunction(value) ? value.call(object) : value; 1046 | }; 1047 | 1048 | // Add your own custom functions to the Underscore object. 1049 | _.mixin = function(obj) { 1050 | each(_.functions(obj), function(name){ 1051 | var func = _[name] = obj[name]; 1052 | _.prototype[name] = function() { 1053 | var args = [this._wrapped]; 1054 | push.apply(args, arguments); 1055 | return result.call(this, func.apply(_, args)); 1056 | }; 1057 | }); 1058 | }; 1059 | 1060 | // Generate a unique integer id (unique within the entire client session). 1061 | // Useful for temporary DOM ids. 1062 | var idCounter = 0; 1063 | _.uniqueId = function(prefix) { 1064 | var id = idCounter++; 1065 | return prefix ? prefix + id : id; 1066 | }; 1067 | 1068 | // By default, Underscore uses ERB-style template delimiters, change the 1069 | // following template settings to use alternative delimiters. 1070 | _.templateSettings = { 1071 | evaluate : /<%([\s\S]+?)%>/g, 1072 | interpolate : /<%=([\s\S]+?)%>/g, 1073 | escape : /<%-([\s\S]+?)%>/g 1074 | }; 1075 | 1076 | // When customizing `templateSettings`, if you don't want to define an 1077 | // interpolation, evaluation or escaping regex, we need one that is 1078 | // guaranteed not to match. 1079 | var noMatch = /(.)^/; 1080 | 1081 | // Certain characters need to be escaped so that they can be put into a 1082 | // string literal. 1083 | var escapes = { 1084 | "'": "'", 1085 | '\\': '\\', 1086 | '\r': 'r', 1087 | '\n': 'n', 1088 | '\t': 't', 1089 | '\u2028': 'u2028', 1090 | '\u2029': 'u2029' 1091 | }; 1092 | 1093 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1094 | 1095 | // JavaScript micro-templating, similar to John Resig's implementation. 1096 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1097 | // and correctly escapes quotes within interpolated code. 1098 | _.template = function(text, data, settings) { 1099 | settings = _.defaults({}, settings, _.templateSettings); 1100 | 1101 | // Combine delimiters into one regular expression via alternation. 1102 | var matcher = new RegExp([ 1103 | (settings.escape || noMatch).source, 1104 | (settings.interpolate || noMatch).source, 1105 | (settings.evaluate || noMatch).source 1106 | ].join('|') + '|$', 'g'); 1107 | 1108 | // Compile the template source, escaping string literals appropriately. 1109 | var index = 0; 1110 | var source = "__p+='"; 1111 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1112 | source += text.slice(index, offset) 1113 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1114 | source += 1115 | escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : 1116 | interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : 1117 | evaluate ? "';\n" + evaluate + "\n__p+='" : ''; 1118 | index = offset + match.length; 1119 | }); 1120 | source += "';\n"; 1121 | 1122 | // If a variable is not specified, place data values in local scope. 1123 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1124 | 1125 | source = "var __t,__p='',__j=Array.prototype.join," + 1126 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1127 | source + "return __p;\n"; 1128 | 1129 | try { 1130 | var render = new Function(settings.variable || 'obj', '_', source); 1131 | } catch (e) { 1132 | e.source = source; 1133 | throw e; 1134 | } 1135 | 1136 | if (data) return render(data, _); 1137 | var template = function(data) { 1138 | return render.call(this, data, _); 1139 | }; 1140 | 1141 | // Provide the compiled function source as a convenience for precompilation. 1142 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1143 | 1144 | return template; 1145 | }; 1146 | 1147 | // Add a "chain" function, which will delegate to the wrapper. 1148 | _.chain = function(obj) { 1149 | return _(obj).chain(); 1150 | }; 1151 | 1152 | // OOP 1153 | // --------------- 1154 | // If Underscore is called as a function, it returns a wrapped object that 1155 | // can be used OO-style. This wrapper holds altered versions of all the 1156 | // underscore functions. Wrapped objects may be chained. 1157 | 1158 | // Helper function to continue chaining intermediate results. 1159 | var result = function(obj) { 1160 | return this._chain ? _(obj).chain() : obj; 1161 | }; 1162 | 1163 | // Add all of the Underscore functions to the wrapper object. 1164 | _.mixin(_); 1165 | 1166 | // Add all mutator Array functions to the wrapper. 1167 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1168 | var method = ArrayProto[name]; 1169 | _.prototype[name] = function() { 1170 | var obj = this._wrapped; 1171 | method.apply(obj, arguments); 1172 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1173 | return result.call(this, obj); 1174 | }; 1175 | }); 1176 | 1177 | // Add all accessor Array functions to the wrapper. 1178 | each(['concat', 'join', 'slice'], function(name) { 1179 | var method = ArrayProto[name]; 1180 | _.prototype[name] = function() { 1181 | return result.call(this, method.apply(this._wrapped, arguments)); 1182 | }; 1183 | }); 1184 | 1185 | _.extend(_.prototype, { 1186 | 1187 | // Start chaining a wrapped Underscore object. 1188 | chain: function() { 1189 | this._chain = true; 1190 | return this; 1191 | }, 1192 | 1193 | // Extracts the result from a wrapped and chained object. 1194 | value: function() { 1195 | return this._wrapped; 1196 | } 1197 | 1198 | }); 1199 | 1200 | }).call(this); 1201 | --------------------------------------------------------------------------------