├── LICENSE.txt ├── README.md ├── scalyr.js ├── scripts ├── buildScalyr.js ├── includeFiles │ ├── header.js │ └── karma.conf.js └── startJsTester └── src ├── js ├── core.js ├── directives │ ├── slyEvaluate.js │ └── slyRepeat.js ├── lib │ └── gatedScope.js └── thirdparty │ ├── angular-1.2.1.js │ └── angular.js └── tests ├── directives ├── allTests.html ├── slyEvaluateTest.js └── slyRepeatTest.js ├── lib ├── allTests.html └── gatedScopeTest.js ├── scalyrUnitTest.js └── thirdparty ├── angular-mocks.js ├── jasmine-1.3.1 ├── MIT.LICENSE ├── jasmine-html.js ├── jasmine.css └── jasmine.js ├── jasmine ├── MIT.LICENSE ├── jasmine-html.js ├── jasmine.css └── jasmine.js ├── sinon-1.7.1.js └── sinon.js /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Scalyr Inc 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 | Angular Directives by Scalyr 2 | --- 3 | 4 | This is the source code for the optimization directives Scalyr created to 5 | improve the responsiveness of its user interface, as discussed in the blog post 6 | [Optimizing AngularJS: 1200ms to 35ms](https://www.scalyr.com/blog/angularjs-1200ms-to-35ms). 7 | 8 | These directives are meant to demonstrate the optimization techniques discussed 9 | in the blog post. The directives were really only meant for internal use, but 10 | since there was sufficient interest from readers, we are publishing the source. 11 | Unfortunately, we cannot make maintaining this library a high priority. We will 12 | push bug fixes and accept pull requests from other developers, but beyond that, 13 | the source is provided as is. We expect this source will act more as a 14 | starting point for other developers rather than as a complete standalone 15 | Javascript library. 16 | 17 | Also note, because some of the optimization techniques rely on non-public 18 | AngularJS variables and methods, these directives may not work for all versions 19 | of AngularJS. The current tests validate them against 1.2.1. 20 | 21 | Furthermore, the directives were built with particular use cases in mind so 22 | they may not have all of the features you would expect. For example, our 23 | repeat directive 'slyRepeat' does not support animations and other features 24 | that 'ngRepeat' does. 25 | 26 | The [scalyr.js](scalyr.js) file contains the Javascript bundle required to use 27 | the directives. More information for each directive can be found in the 28 | src/js/directives directory. Here is a brief description of what is included: 29 | 30 | 31 | sly 32 | 33 | 34 | 35 | 36 | 37 | 38 | 47 | 60 | 68 | 78 | 91 |
NameDescription
39 | slyEvaluateOnlyWhen 40 | 41 | An attribute directive that prevents updating / evaluating 42 | all bindings and expressions for the current element and its children 43 | unless the object referenced by the attribute's value changes. 44 | It currently assumes the expression contained in the attribute value 45 | evaluates to an object and detects changes only by a change in object reference. 46 |
48 | slyAlwaysEvaluate 49 | 50 | An attribute directive that can only be used in conjunction with the 51 | slyEvaluateOnlyWhen directive. This directive will ensure that 52 | any expression that is being watched will always be evaluated 53 | if it contains the string specified in the attribute value (i.e., 54 | it will ignore whether or not the slyEvaluateOnlyWhen expression has changed.) 55 | This is useful when you wish to check some expressions all the time. 56 | Note, this only works if the directives register a string watch expression 57 | so this may or may not work for some directives depending on their 58 | implementation. 59 |
61 | slyPreventEvaluationWhenHidden 62 | 63 | An attribute directive that will only 64 | evaluate the bindings and expressions for the current element and its children 65 | if the current element is not hidden (detected by the element having the 66 | 'ng-hide' CSS class.) 67 |
69 | slyShow 70 | 71 | An attribute directive that Will hide the element if the expression specified 72 | in the atttribute value evaluates to false. Uses the CSS class 'ng-hide' to 73 | hide the element. This is almost exactly the same as ngShow, but it has the 74 | advantage that it works better with slyPreventEvaluationWhenHidden by 75 | guaranteeing it show expression is always evaluated regardless of the effects 76 | of slyPreventEvaluationWhenHidden. 77 |
79 | slyRepeat 80 | 81 | An attribute directive that is a modified version of the 82 | ngRepeat directive. It is meant to be more efficient for creating and 83 | recreating large lists of elements. In particular, it has an 84 | optimization that will prevent DOM elements from being constantly created 85 | and destroyed as the number of the repeated elements change. It does this 86 | by not destroying DOM elements when they are no longer needed, but instead, 87 | just hiding them. This might not work for all use cases. Cavaets: The 88 | collection expression must evaluate to an array. Animators will not work. 89 | Track By does not work. Use at your own peril. 90 |
92 | 93 | Please contact contact@scalyr.com for any questions or problems. 94 | 95 | == Contributing == 96 | 97 | ``` 98 | npm install -g karma karma-cli karma-jasmine karma-chrome-launcher 99 | ``` 100 | 101 | Then from root directory : 102 | ``` 103 | ./scripts/buildScalyr.js 104 | ./scripts/startJsTester 105 | ``` 106 | -------------------------------------------------------------------------------- /scalyr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license scalyr v1.0.3 3 | * (c) 2013 Scalyr, Inc. http://scalyr.com 4 | * License: MIT 5 | */ 6 | 7 | 'use strict'; 8 | 9 | // You may just depend on the 'sly' module to pull in all of the 10 | // dependencies. 11 | angular.module('sly', ['slyEvaluate', 'slyRepeat']); 12 | /** 13 | * @fileoverview 14 | * Defines core functions used throughout the Scalyr javascript 15 | * code base. This file is included on every page. 16 | * 17 | * @author Steven Czerwinski 18 | */ 19 | 20 | /** 21 | * @param {Object} value The value to check 22 | * @returns {Boolean} True if value is an Array 23 | */ 24 | function isArray(value) { 25 | return Object.prototype.toString.call(value) === '[object Array]'; 26 | } 27 | 28 | /** 29 | * @param {*} value The value to check 30 | * @returns {Boolean} True if value is a Boolean 31 | */ 32 | function isBoolean(value) { 33 | return typeof value == 'boolean'; 34 | } 35 | 36 | /** 37 | * @param {Object} value The value to check 38 | * @returns {Boolean} True if value is a Date object 39 | */ 40 | function isDate(value) { 41 | return Object.prototype.toString.call(value) === '[object Date]'; 42 | } 43 | 44 | /** 45 | * @param {*} value The value to check 46 | * @returns {Boolean} True if value is undefined 47 | */ 48 | function isDefined(value) { 49 | return typeof value != 'undefined'; 50 | } 51 | 52 | /** 53 | * @param {*} value The value to check 54 | * @returns {Boolean} True if value is a Function 55 | */ 56 | function isFunction(value) { 57 | return typeof value == 'function'; 58 | } 59 | 60 | /** 61 | * @param {*} value The value to check 62 | * @returns {Boolean} True if value is null 63 | */ 64 | function isNull(value) { 65 | return value === null; 66 | } 67 | 68 | /** 69 | * @param {*} value The value to check 70 | * @returns {Boolean} True if value is a Number 71 | */ 72 | function isNumber(value) { 73 | return typeof value == 'number'; 74 | } 75 | 76 | /** 77 | * @param {*} value The value to check 78 | * @returns {Boolean} True if value is an Object, not including null 79 | */ 80 | function isObject(value) { 81 | return value !== null && typeof value == 'object'; 82 | } 83 | 84 | /** 85 | * @param {*} value The value to check 86 | * @returns {Boolean} True if value is a string 87 | */ 88 | function isString(value) { 89 | return typeof value == 'string'; 90 | } 91 | 92 | /** 93 | * @param {*} value The value to check 94 | * @returns {Boolean} True if value is undefined 95 | */ 96 | function isUndefined(value) { 97 | return typeof value == 'undefined'; 98 | } 99 | 100 | /** 101 | * Converts a String or Boolean value to a Boolean. 102 | * 103 | * @param {String|Boolean} value The value to convert 104 | * @returns {Boolean} Returns true for any String that is not 105 | * null, empty String, or 'false'. If value is a Boolean, 106 | * returns value 107 | */ 108 | function convertToBoolean(value) { 109 | if (isBoolean(value)) 110 | return value; 111 | return value !== null && value !== '' && value !== 'false'; 112 | } 113 | 114 | /** 115 | * Determines if obj has a property named prop. 116 | * 117 | * @param {Object} obj The object to check 118 | * @returns {Boolean} Returns true if obj has a property named 119 | * prop. Only considers the object's own properties 120 | */ 121 | function hasProperty(obj, prop) { 122 | return obj.hasOwnProperty(prop); 123 | } 124 | 125 | /** 126 | * @param {*} value The value to check 127 | * @returns {Boolean} Returns true if value is a String 128 | * and has zero length, or if null or undefined 129 | */ 130 | function isStringEmpty(value) { 131 | return isNull(value) || isUndefined(value) || 132 | (isString(value) && (value.length == 0)); 133 | } 134 | 135 | /** 136 | * @param {*} value The value to check 137 | * @returns {Boolean} Returns true if value is a String 138 | * and has non-zero length 139 | */ 140 | function isStringNonempty(value) { 141 | return isString(value) && (value.length > 0); 142 | } 143 | 144 | /** 145 | * Returns input with the first letter capitalized. 146 | * The input may not be zero length. 147 | * 148 | * @param {String} input The String to capitalize. 149 | * @returns {String} Returns input with the first letter 150 | * capitalized. 151 | */ 152 | function upperCaseFirstLetter(input) { 153 | return input.charAt(0).toUpperCase() + input.slice(1); 154 | } 155 | 156 | /** 157 | * Returns true if obj1 and obj2 are equal. This should 158 | * only be used for Arrays, Objects, and value types. This is a deep 159 | * comparison, comparing each property and recursive property to 160 | * be equal (not just ===). 161 | * 162 | * Two Objects or values are considered equivalent if at least one of the following is true: 163 | * - Both objects or values pass `===` comparison. 164 | * - Both objects or values are of the same type and all of their properties pass areEqual 165 | * comparison. 166 | * - Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal). 167 | * 168 | * Note, during property comparision, properties with function values are ignores as are property 169 | * names beginning with '$'. 170 | * 171 | * See angular.equal for more details. 172 | * 173 | * @param {Object|Array|value} obj1 The first object 174 | * @param {Object|Array|value} obj2 The second object 175 | * @returns {Boolean} True if the two objects are equal using a deep 176 | * comparison. 177 | */ 178 | function areEqual(obj1, obj2) { 179 | return angular.equals(obj1, obj2); 180 | } 181 | 182 | /** 183 | * @param {Number} a The first Number 184 | * @param {Number} b The second Number 185 | * @returns {Number} The minimum of a and b 186 | */ 187 | function min(a, b) { 188 | return a < b ? a : b; 189 | } 190 | 191 | /** 192 | * @param {Number} a The first Number 193 | * @param {Number} b The second Number 194 | * @returns {Number} The maximum of a and b 195 | */ 196 | function max(a, b) { 197 | return a > b ? a : b; 198 | } 199 | 200 | /** 201 | * Returns true if the specified String begins with prefix. 202 | * 203 | * @param {*} input The input to check 204 | @ @param {String} prefix The prefix 205 | * @returns {Boolean} True if input is a string that begins with prefix 206 | */ 207 | function beginsWith(input, prefix) { 208 | return isString(input) && input.lastIndexOf(prefix, 0) == 0; 209 | } 210 | 211 | /** 212 | * Returns true if the specified String ends with prefix. 213 | * 214 | * @param {*} input The input to check 215 | @ @param {String} postfix The postfix 216 | * @returns {Boolean} True if input is a string that ends with postfix 217 | */ 218 | function endsWith(input, postfix) { 219 | return isString(input) && input.indexOf(postfix, input.length - postfix.length) !== -1; 220 | } 221 | 222 | /** 223 | * Returns a deep copy of source, where source can be an Object or an Array. If a destination is 224 | * provided, all of its elements (for Array) or properties (for Objects) are deleted and then all 225 | * elements/properties from the source are copied to it. If source is not an Object or Array, 226 | * source is returned. 227 | * 228 | * See angular.copy for more details. 229 | * @param {Object|Array} source The source 230 | * @param {Object|Array} destination Optional object to copy the elements to 231 | * @returns {Object|Array} The deep copy of source 232 | */ 233 | function copy(source, destination) { 234 | return angular.copy(source, destination); 235 | } 236 | 237 | /** 238 | * Removes property from obj. 239 | * 240 | * @param {Object} obj The object 241 | * @param {String} property The property name to delete 242 | */ 243 | function removeProperty(obj, property) { 244 | delete obj[property]; 245 | } 246 | 247 | /** 248 | * Removes all properties in the array from obj. 249 | * 250 | * @param {Object} obj The object 251 | * @param {Array} properties The properties to remove 252 | */ 253 | function removeProperties(obj, properties) { 254 | for (var i = 0; i < properties.length; ++i) 255 | delete obj[properties[i]]; 256 | } 257 | 258 | /** 259 | * Invokes the iterator function once for each item in obj collection, which can be either 260 | * an Object or an Array. The iterator function is invoked with iterator(value, key), 261 | * where value is the value of an object property or an array element and key is the 262 | * object property key or array element index. Specifying a context for the function is 263 | * optional. If specified, it becomes 'this' when iterator function is invoked. 264 | * 265 | * See angular.forEach for more details. 266 | * 267 | * @param {Object|Array} The Object or Array over which to iterate 268 | * @param {Function} iterator The iterator function to invoke 269 | * @param {Object} context The value to set for 'this' when invoking the 270 | * iterator function. This is optional 271 | */ 272 | function forEach(obj, iterator, context) { 273 | return angular.forEach(obj, iterator, context); 274 | } 275 | 276 | /** 277 | * Used to define a Scalyr javascript library and optionally declare 278 | * dependencies on other libraries. All javascript code not defined in 279 | * this file should be defined as part of a library. 280 | * 281 | * The first argument is the name to call the library. The second argument 282 | * is either a Constructor object for the library or an array where the last 283 | * element is the Constructor for the library and the first to N-1 are string 284 | * names of the libraries this one depends on. If you do declare dependencies, 285 | * the libraries are passed in the Constructor create method in the same order 286 | * as the strings are defined. 287 | * 288 | * Example: 289 | * defineScalyrJsLibrary('myUtils', function() { 290 | * var fooFunction = function(a, b) { 291 | * return a + b; 292 | * }; 293 | * return { 294 | * foo: fooFunction 295 | * }; 296 | * }); 297 | * 298 | * defineScalyrJsLibrary('anotherUtils', [ 'myUtils', function(myUtils) { 299 | * var barFunction = function(a, b) { 300 | * return myUtils.foo(a, b); 301 | * }; 302 | * return { 303 | * bar: barFunction 304 | * }; 305 | * }); 306 | * 307 | * @param {String} libraryName The name for the library 308 | * @param {Constructor|Array} libraryExporter The exporter for the 309 | * library. See above for details 310 | */ 311 | function defineScalyrJsLibrary(libraryName, libraryExporter) { 312 | var moduleDependencies = []; 313 | if (libraryExporter instanceof Array) { 314 | for (var i = 0; i < libraryExporter.length - 1; ++i) 315 | moduleDependencies.push(libraryExporter[i]); 316 | } 317 | 318 | return angular.module(libraryName, moduleDependencies) 319 | .factory(libraryName, libraryExporter); 320 | } 321 | 322 | /** 323 | * Similar to defineScalyrJsLibary but instead of declaring 324 | * a purely javascript library, this declares an Angular module 325 | * library. The moduleName should be a string used to identify 326 | * this module. The dependencies is an array with the string 327 | * names of Angular modules, Scalyr Angular modules, or Scalyr 328 | * javascript libraries to depend on. The returned object 329 | * can be used to define directives, etc similar to angular.module. 330 | * 331 | * Example: 332 | * defineScalyrAngularModule('slyMyModule', [ 'myTextUtils']) 333 | * .filter('camelCase', function(myTextUtils) { 334 | * return function(input) { 335 | * return myTextUtils.camelCase(input); 336 | * }; 337 | * }); 338 | * 339 | * @param {String} moduleName The name of the module 340 | * @param {Array} dependencies The names of modules to depend on 341 | */ 342 | function defineScalyrAngularModule(moduleName, dependencies) { 343 | return angular.module(moduleName, dependencies); 344 | } 345 | /** 346 | * @fileoverview 347 | * Module: slyEvaluate 348 | * 349 | * Defines several directives related to preventing evaluating watchers 350 | * on scopes under certain conditions. Here's a list of the directives 351 | * and brief descriptions. See down below for more details. 352 | * 353 | * slyEvaluateOnlyWhen: A directive that prevents updating / evaluating 354 | * all bindings for the current element and its children unless 355 | * the expression has changed values. If new children are added, they 356 | * are always evaluated at least once. It currently assumes the 357 | * expression evaluates to an object and detects changes only by 358 | * a change in object reference. 359 | * 360 | * slyAlwaysEvaluate: Can only be used in conjunction with the 361 | * slyEvaluateOnlyWhen directive. This directive will ensure that 362 | * any expression that is being watched will always be evaluated 363 | * if it contains the specified string (i.e., it will ignore whether 364 | * or not the slyEvaluateOnlyWhen expression has changed.) This 365 | * is useful when you wish to check some expressions all the time. 366 | * 367 | * slyPreventEvaluationWhenHidden: Will only evaluate the bindings 368 | * for the current element and its children if the current element 369 | * is not hidden (detected by the element having the ng-hide CSS class.) 370 | * 371 | * slyShow: Will hide the element if the expression evaluates to false. 372 | * Uses ng-hide to hide the element. This is almost exactly the same 373 | * as ngShow, but it has the advantage that it works better with 374 | * slyPreventEvaluationWhenHidden by guaranteeing it will always evaluate 375 | * its show expression to determine if it should or should not be hidden. 376 | */ 377 | defineScalyrAngularModule('slyEvaluate', ['gatedScope']) 378 | /** 379 | * Directive for preventing all bound expressions in the current element and its children 380 | * from being evaluated unless the specified expression evaluates to a different object. 381 | * Currently, the value assigned to the 'slyEvaluateOnlyWhen' must evaluate to an object. 382 | * Also, reference equality is used to determine if the expression has changed. 383 | * TODO: Make this more versatile, similar to $watch. For now, this is all we need. 384 | */ 385 | .directive('slyEvaluateOnlyWhen', ['$parse', function ($parse) { 386 | return { 387 | // We create a new scope just because it helps segment the gated watchers 388 | // from the parent scope. Unclear if this is that important for perf. 389 | scope: true, 390 | restrict: 'A', 391 | compile: function compile(tElement, tAttrs) { 392 | return { 393 | // We need a separate pre-link function because we want to modify the scope before any of the 394 | // children are passed it. 395 | pre: function preLink(scope, element, attrs) { 396 | var previousValue = null; 397 | var initialized = false; 398 | 399 | var expressionToCheck = $parse(attrs['slyEvaluateOnlyWhen']); 400 | var alwaysEvaluateString = null; 401 | if (hasProperty(attrs, 'slyAlwaysEvaluate')) { 402 | alwaysEvaluateString = attrs['slyAlwaysEvaluate']; 403 | if (isStringEmpty(alwaysEvaluateString)) 404 | throw new Exception('Empty string is illegal for value of slyAlwaysEvaluate'); 405 | } 406 | scope.$addWatcherGate(function evaluteOnlyWhenChecker() { 407 | // We should only return true if expressionToCheck evaluates to a value different 408 | // than previousValue. 409 | var currentValue = expressionToCheck(scope); 410 | if (!initialized) { 411 | initialized = true; 412 | previousValue = currentValue; 413 | return true; 414 | } 415 | var result = previousValue !== currentValue; 416 | previousValue = currentValue; 417 | return result; 418 | }, function shouldGateWatcher(watchExpression) { 419 | // Should return true if the given watcher that's about to be registered should 420 | // be gated. 421 | return isNull(alwaysEvaluateString) || 422 | !(isStringNonempty(watchExpression) && (watchExpression.indexOf(alwaysEvaluateString) >= 0)); 423 | }, true /* Evaluate any newly added watchers when they are added */); 424 | }, 425 | }; 426 | }, 427 | }; 428 | }]) 429 | /** 430 | * Directive for overriding the 'slyEvaluateOnlyWhen' expression for the current element. 431 | * This directive takes a single string value. If this string value is found anywhere in 432 | * an expression that normally would not be evaluated due to the 'slyEvaluateOnlyWhen' 433 | * directive, it is evaluated, regardless of whether or not the value for the expression in 434 | * 'slyEvaluateOnlyWhen' has changed. This is very useful when a certain expression used by 435 | * one of the children of the current element should always be evaluated and is not affected 436 | * by the expression specified in slyEvaluateOnlyWhen. 437 | */ 438 | .directive('slyAlwaysEvaluate', function() { 439 | // This is just a place holder to show that slyAlwaysEvaluate is a legal 440 | // directive. The real work for this directive is done in slyEvaluateOnlyWhen. 441 | return { 442 | restrict: 'A', 443 | link: function(scope, element, attrs) { 444 | }, 445 | }; 446 | }) 447 | /** 448 | * Directive for showing an element, very similar to ngShow. However, this directive 449 | * works better with slyPreventEvaluationWhenHidden because it is ensure it always 450 | * will evaluate the show expression to determine if it should be shown or hidden 451 | * even if slyPreventEvaluationWhenHidden is in effect. This directive also uses 452 | * the ng-hide css class to actually hide the element. 453 | * 454 | * NOTE: We might be able to get better performance if we have this directive directly 455 | * perform a callback on slyPreventEvaluationWhenHidden when it is shown/hidden rather 456 | * than having that directive register a watcher on the css class. 457 | */ 458 | .directive('slyShow', ['$animate', function($animate) { 459 | /** 460 | * @param {*} value The input 461 | * @return {Boolean} True if the value is truthy as determined by angular rules. 462 | * 463 | * Note: This is copied from the Angular source because it is not exposed by Angular 464 | * but we want our directive to behave the same as ngShow. Think about moving this 465 | * to core.js. 466 | */ 467 | function toBoolean(value) { 468 | if (value && value.length !== 0) { 469 | var v = ("" + value); 470 | v = isString(v) ? v.toLowerCase() : v; 471 | value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); 472 | } else { 473 | value = false; 474 | } 475 | return value; 476 | } 477 | 478 | return { 479 | restrict: 'A', 480 | link: function slyShowLink(scope, element, attr) { 481 | scope.$watch(attr.slyShow, function ngSlyShowAction(value){ 482 | $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); 483 | }, false, 'slyShow'); }, 484 | }; 485 | }]) 486 | /** 487 | * Directive for preventing all bound expressions in the current element and its children 488 | * from being evaluated if the current element is hidden as determined by whether or not 489 | * it has the ng-hide class. 490 | */ 491 | .directive('slyPreventEvaluationWhenHidden', function () { 492 | return { 493 | restrict: 'A', 494 | // We create a new scope just because it helps segment the gated watchers 495 | // from the parent scope. Unclear if this is that important for perf. 496 | scope: true, 497 | compile: function compile(tElement, tAttrs) { 498 | return { 499 | // We need a separate pre-link function because we want to modify the scope before any of the 500 | // children are passed it. 501 | pre: function preLink(scope, element, attrs) { 502 | scope.$addWatcherGate(function hiddenChecker() { 503 | // Should only return true if the element is not hidden. 504 | return !element.hasClass('ng-hide'); 505 | }, function hiddenDecider(watchExpression, listener, equality, directiveName) { 506 | // Make an exception for slyShow.. do not gate its watcher. 507 | if (isDefined(directiveName) && (directiveName == 'slyShow')) 508 | return false; 509 | return true; 510 | }); 511 | }, 512 | }; 513 | }, 514 | }; 515 | }); 516 | 517 | /** 518 | * @fileoverview 519 | * Module: slyRepeat 520 | * 521 | * Contains the slyRepeat directive, which is is a modified version of the 522 | * ngRepeat directive that is meant to be more efficient for creating and 523 | * recreating large lists of bound elements. In particular, it has an 524 | * optimization that will prevent DOM elements from being constantly created 525 | * and destroyed as the contents of the repeated elements change. It does this 526 | * by not destroying DOM elements when they are no longer needed, but instead, 527 | * just hiding them. This might not work for all use cases, but for it does 528 | * for the ones we do wish to heavily optimize. For eample, through profiling, 529 | * we found that destroying DOM elements when flipping through log view pages 530 | * represented a large chunk of CPU time. 531 | * 532 | * Cavaets: The collection expression must evaluate to an array. Animators 533 | * will not work. Track By does not work. Use at your own peril. 534 | * 535 | * @author Steven Czerwinski 536 | */ 537 | defineScalyrAngularModule('slyRepeat', ['gatedScope']) 538 | .directive('slyRepeat', ['$animate', '$parse', function ($animate, $parse) { 539 | 540 | /** 541 | * Sets the scope contained in elementScope to gate all its 542 | * watchers based on the isActiveForRepeat proprety. 543 | * 544 | * @param {Object} elementScope The object containing the 545 | * scope and isActiveForRepeat properties. 546 | */ 547 | function gateWatchersForScope(elementScope) { 548 | elementScope.scope.$addWatcherGate(function() { 549 | return elementScope.isActiveForRepeat; 550 | }); 551 | } 552 | 553 | return { 554 | restrict: 'A', 555 | scope: true, 556 | transclude: 'element', 557 | priority: 1000, 558 | terminal: true, 559 | compile: function(element, attr, linker) { 560 | // Most of the work is done in the post-link function. 561 | return function($scope, $element, $attr) { 562 | // This code is largely based on ngRepeat. 563 | 564 | // Parse the expression. It should look like: 565 | // x in some-expression 566 | var expression = $attr.slyRepeat; 567 | var match = expression.match(/^\s*(.+)\s+in\s+(.*?)$/); 568 | if (!match) { 569 | throw Error("Expected slyRepeat in form of '_item_ in _collection_' but got '" + 570 | expression + "'."); 571 | } 572 | 573 | var iterVar = match[1]; 574 | var collectionExpr = match[2]; 575 | 576 | match = iterVar.match(/^(?:([\$\w]+))$/); 577 | if (!match) { 578 | throw Error("'item' in 'item in collection' should be identifier but got '" + 579 | lhs + "'."); 580 | } 581 | 582 | // previousElements will store references to the already existing (DOM) elements 583 | // that were last used for the last rendering of this repeat and were visible. 584 | // We will re-use these elements when executing the next rendering of the repeat when 585 | // the iteration value changes. 586 | var previousElements = []; 587 | // previousElementsBuffer will store references to the already existing (DOM) elements 588 | // that are in the page but were not used for the last rendering of this repeat and were 589 | // therefore marked as inactive and not visible. This happens if the length of the repeat 590 | // iteration goes down over time, since we do not remove the elements. If the repeat length 591 | // was first 10, then 5, we will end up with the last 5 elements in the previousElementBuffer. 592 | // We keep this in case the length increases again. 593 | var previousElementBuffer = []; 594 | 595 | var deregisterCallback = $scope.$watchCollection(collectionExpr, function(collection) { 596 | if (!collection) 597 | return; 598 | if (!isArray(collection)) 599 | throw Error("'collection' did not evaluate to an array. expression was " + collectionExpr); 600 | var originalPreviousElementsLength = previousElements.length; 601 | // First, reconcile previousElements and collection with respect to the previousElementBuffer. 602 | // Basically, try to grow previousElements to collection.length if we can. 603 | if ((previousElements.length < collection.length) && (previousElementBuffer.length > 0)) { 604 | var limit = previousElements.length + previousElementBuffer.length; 605 | if (limit > collection.length) 606 | limit = collection.length; 607 | previousElements = previousElements.concat(previousElementBuffer.splice(0, limit - previousElements.length)); 608 | } 609 | 610 | var currentElements = null; 611 | var currentElementBuffer = []; 612 | 613 | var newElements = []; 614 | if (collection.length > previousElements.length) { 615 | // Add in enough elements to account for the larger collection. 616 | for (var i = previousElements.length; i < collection.length; ++i) { 617 | // Need to add in an element for each new item in the collection. 618 | var newElement = { 619 | scope: $scope.$new(), 620 | isActiveForRepeat: true, 621 | }; 622 | 623 | gateWatchersForScope(newElement); 624 | newElement.scope.$index = i; 625 | newElement.scope.$first = (i == 0); 626 | newElements.push(newElement); 627 | } 628 | currentElements = previousElements.concat(newElements); 629 | currentElementBuffer = previousElementBuffer; 630 | } else if (collection.length < previousElements.length) { 631 | for (var i = collection.length; i < previousElements.length; ++i) 632 | previousElements[i].isActiveForRepeat = false; 633 | 634 | currentElementBuffer = previousElements.splice(collection.length, previousElements.length - collection.length).concat( 635 | previousElementBuffer); 636 | currentElements = previousElements; 637 | } else { 638 | currentElements = previousElements; 639 | currentElementBuffer = previousElementBuffer; 640 | } 641 | 642 | // We have to fix up the last and middle values in the scope for each element in 643 | // currentElements, since their roles may have changed with the new length. 644 | // We always have to fix the last element. 645 | if (currentElements.length > 0) { 646 | var firstIndexToFix = currentElements.length - 1; 647 | var lastIndexToFix = currentElements.length - 1; 648 | // We also have to fix any new elements that were added. 649 | if (originalPreviousElementsLength < currentElements.length) { 650 | firstIndexToFix = originalPreviousElementsLength; 651 | } 652 | // And we usually have to fix the element before the first element we modified 653 | // in case it used to be last. 654 | if (firstIndexToFix > 0) { 655 | firstIndexToFix = firstIndexToFix - 1; 656 | } 657 | for (var i = firstIndexToFix; i <= lastIndexToFix; ++i) { 658 | currentElements[i].scope.$last = (i == (currentElements.length - 1)); 659 | currentElements[i].scope.$middle = ((i != 0) && (i != (currentElements.length - 1))); 660 | if (!currentElements[i].isActiveForRepeat) { 661 | // If it is not marked as active, make it active. This is also indicates that 662 | // the element is currently hidden, so we have to unhide it. 663 | currentElements[i].isActiveForRepeat = true; 664 | currentElements[i].element.css('display', ''); 665 | } 666 | } 667 | } 668 | 669 | // Hide all elements that have recently become inactive. 670 | for (var i = 0; i < currentElementBuffer.length; ++i) { 671 | if (currentElementBuffer[i].isActiveForRepeat) 672 | break; 673 | currentElementBuffer[i].element.css('display', 'none'); 674 | } 675 | 676 | // Assign the new value for the iter variable for each scope. 677 | for (var i = 0; i < currentElements.length; ++i) { 678 | currentElements[i].scope[iterVar] = collection[i]; 679 | } 680 | 681 | // We have to go back now and clone the DOM element for any new elements we 682 | // added and link them in. We clone the last DOM element we had created already 683 | // for this Repeat. 684 | var prevElement = $element; 685 | if (previousElements.length > 0) 686 | prevElement = previousElements[previousElements.length - 1].element; 687 | for (var i = 0; i < newElements.length; ++i) { 688 | linker(newElements[i].scope, function(clone) { 689 | $animate.enter(clone, null, prevElement); 690 | prevElement = clone; 691 | newElements[i].element = clone; 692 | }); 693 | } 694 | 695 | previousElements = currentElements; 696 | previousElementBuffer = currentElementBuffer; 697 | }); 698 | $scope.$on('$destroy', function() { 699 | deregisterCallback(); 700 | }); 701 | }; 702 | } 703 | }; 704 | }]); 705 | /** 706 | * @fileoverview 707 | * Defines an extension to angular.Scope that allows for registering 708 | * 'gating functions' on a scope that will prevent all future watchers 709 | * registered on the scope from being evaluated unless the gating function 710 | * returns true. 711 | * 712 | * By depending on this module, the $rootScope instance and angular.Scope 713 | * class are automatically extended to implement this new capability. 714 | * 715 | * Warning, this implementation depends on protected/private variables 716 | * in the angular.Scope implementation and therefore can break in the 717 | * future due to changes in the angular.Scope implementation. Use at 718 | * your own risk. 719 | */ 720 | defineScalyrAngularModule('gatedScope', []) 721 | .config(['$provide', function($provide) { 722 | // We use a decorator to override methods in $rootScope. 723 | $provide.decorator('$rootScope', ['$delegate', '$exceptionHandler', 724 | function ($rootScope, $exceptionHandler) { 725 | 726 | // Make a copy of $rootScope's original methods so that we can access 727 | // them to invoke super methods in the ones we override. 728 | var scopePrototype = {}; 729 | for (var key in $rootScope) { 730 | if (isFunction($rootScope[key])) 731 | scopePrototype[key] = $rootScope[key]; 732 | } 733 | 734 | var Scope = $rootScope.constructor; 735 | 736 | // Hold all of our new methods. 737 | var methodsToAdd = { 738 | }; 739 | 740 | // A constant value that the $digest loop implementation depends on. We 741 | // grab it down below. 742 | var initWatchVal; 743 | 744 | /** 745 | * @param {Boolean} isolate Whether or not the new scope should be isolated. 746 | * @returns {Scope} A new child scope 747 | */ 748 | methodsToAdd.$new = function(isolate) { 749 | // Because of how scope.$new works, the returned result 750 | // should already have our new methods. 751 | var result = scopePrototype.$new.call(this, isolate); 752 | 753 | // We just have to do the work that normally a child class's 754 | // constructor would perform -- initializing our instance vars. 755 | result.$$gatingFunction = this.$$gatingFunction; 756 | result.$$parentGatingFunction = this.$$gatingFunction; 757 | result.$$shouldGateFunction = this.$$shouldGateFunction; 758 | result.$$gatedWatchers = []; 759 | result.$$cleanUpQueue = this.$$cleanUpQueue; 760 | 761 | return result; 762 | }; 763 | 764 | /** 765 | * Digests all of the gated watchers for the specified gating function. 766 | * 767 | * @param {Function} targetGatingFunction The gating function associated 768 | * with the watchers that should be digested 769 | * @returns {Boolean} True if any of the watchers were dirty 770 | */ 771 | methodsToAdd.$digestGated = function gatedScopeDigest(targetGatingFunction) { 772 | // Note, most of this code was stolen from angular's Scope.$digest method. 773 | var watch, value, 774 | watchers, 775 | length, 776 | next, current = this, target = this, last, 777 | dirty = false; 778 | 779 | do { // "traverse the scopes" loop 780 | if (watchers = current.$$gatedWatchers) { 781 | // process our watches 782 | length = watchers.length; 783 | while (length--) { 784 | try { 785 | watch = watchers[length]; 786 | // Scalyr edit: We do not process a watch function if it is does not 787 | // have the same gating function for which $digestGated was invoked. 788 | if (watch.gatingFunction !== targetGatingFunction) 789 | continue; 790 | 791 | // Since we are about to execute the watcher as part of a digestGated 792 | // call, we can remove it from the normal digest queue if it was placed 793 | // there because the watcher was added after the gate function's first 794 | // evaluation. 795 | if (watch && !isNull(watch.cleanUp)) { 796 | watch.cleanUp(); 797 | watch.cleanUp = null; 798 | } 799 | // Most common watches are on primitives, in which case we can short 800 | // circuit it with === operator, only when === fails do we use .equals 801 | if (watch && (value = watch.get(current)) !== (last = watch.last) && 802 | !(watch.eq 803 | ? areEqual(value, last) 804 | : (typeof value == 'number' && typeof last == 'number' 805 | && isNaN(value) && isNaN(last)))) { 806 | dirty = true; 807 | watch.last = watch.eq ? copy(value) : value; 808 | watch.fn(value, ((last === initWatchVal) ? value : last), current); 809 | // Scalyr edit: Removed the logging code for when the ttl is reached 810 | // here because we don't have access to the ttl in this method. 811 | } 812 | } catch (e) { 813 | $exceptionHandler(e); 814 | } 815 | } 816 | } 817 | 818 | // Insanity Warning: scope depth-first traversal 819 | // yes, this code is a bit crazy, but it works and we have tests to prove it! 820 | // Scalyr edit: This insanity warning was from angular. We only modified this 821 | // code by checking the $$gatingFunction because it's a good optimization to only go 822 | // down a child of a parent that has the same gating function as what we are processing 823 | // (since if a parent already has a different gating function, there's no way any 824 | // of its children will have the right one). 825 | if (!(next = ((current.$$gatingFunction === targetGatingFunction && current.$$childHead) 826 | || (current !== target && current.$$nextSibling)))) { 827 | while(current !== target && !(next = current.$$nextSibling)) { 828 | current = current.$parent; 829 | } 830 | } 831 | } while ((current = next)); 832 | 833 | // Mark that this gating function has digested all children. 834 | targetGatingFunction.hasDigested = true; 835 | return dirty; 836 | }; 837 | 838 | /** 839 | * @inherited $watch 840 | * @param directiveName The fourth parameter is a new optional parameter that allows 841 | * directives aware of this abstraction to pass in their own names to identify 842 | * which directive is registering the watch. This is then passed to the 843 | * shouldGateFunction to help determine if the watcher should be gated by the current 844 | * gatingFunction. 845 | */ 846 | methodsToAdd.$watch = function gatedWatch(watchExpression, listener, objectEquality, 847 | directiveName) { 848 | // Determine if we should gate this watcher. 849 | if (!isNull(this.$$gatingFunction) && (isNull(this.$$shouldGateFunction) || 850 | this.$$shouldGateFunction(watchExpression, listener, objectEquality, directiveName))) { 851 | // We do a hack here to just switch out the watchers array with our own 852 | // gated list and then invoke the original watch function. 853 | var tmp = this.$$watchers; 854 | this.$$watchers = this.$$gatedWatchers; 855 | // Invoke original watch function. 856 | var result = scopePrototype.$watch.call(this, watchExpression, listener, objectEquality); 857 | this.$$watchers = tmp; 858 | this.$$gatedWatchers[0].gatingFunction = this.$$gatingFunction; 859 | this.$$gatedWatchers[0].cleanUp = null; 860 | 861 | // We know that the last field of the watcher object will be set to initWatchVal, so we 862 | // grab it here. 863 | initWatchVal = this.$$gatedWatchers[0].last; 864 | var watch = this.$$gatedWatchers[0]; 865 | 866 | // We should make sure the watch expression gets evaluated fully on at least one 867 | // digest cycle even if the gate function is now closed if requested by the gating function's 868 | // value for shouldEvalNewWatchers. We do this by adding in normal watcher that will execute 869 | // the watcher we just added and remove itself after the digest cycle completes. 870 | if (this.$$gatingFunction.shouldEvalNewWatchers && this.$$gatingFunction.hasDigested) { 871 | var self = this; 872 | watch.cleanUp = scopePrototype.$watch.call(self, function() { 873 | if (!isNull(watch.cleanUp)) { 874 | self.$$cleanUpQueue.unshift(watch.cleanUp); 875 | watch.cleanUp = null; 876 | } 877 | var value; 878 | var last = initWatchVal; 879 | 880 | if (watch && (value = watch.get(self)) !== (last = watch.last) && 881 | !(watch.eq 882 | ? areEqual(value, last) 883 | : (typeof value == 'number' && typeof last == 'number' 884 | && isNaN(value) && isNaN(last)))) { 885 | watch.last = watch.eq ? copy(value) : value; 886 | watch.fn(value, ((last === initWatchVal) ? value : last), self); 887 | } 888 | return watch.last; 889 | }); 890 | } 891 | return result; 892 | } else { 893 | return scopePrototype.$watch.call(this, watchExpression, listener, objectEquality); 894 | } 895 | }; 896 | 897 | /** 898 | * @inherited $digest 899 | */ 900 | methodsToAdd.$digest = function gatedDigest() { 901 | // We have to take care if a scope's digest method was invoked that has a 902 | // gating function in the parent scope. In this case, the watcher for that 903 | // gating function is registered in the parent (the one added in gatedWatch), 904 | // and will not be evaluated here. So, we have to manually see if the gating 905 | // function is true and if so, evaluate any gated watchers for that function on 906 | // this scope. This needs to happen to properly support invoking $digest on a 907 | // scope with a parent scope with a gating function. 908 | // NOTE: It is arguable that we are not correctly handling nested gating functions 909 | // here since we do not know if the parent gating function was nested in other gating 910 | // functions and should be evaluated at all. However, if a caller is invoking 911 | // $digest on a particular scope, we assume the caller is doing that because it 912 | // knows the watchers should be evaluated. 913 | var dirty = false; 914 | if (!isNull(this.$$parentGatingFunction) && this.$$parentGatingFunction()) { 915 | var ttl = 5; 916 | do { 917 | dirty = this.$digestGated(this.$$parentGatingFunction); 918 | ttl--; 919 | 920 | if (dirty && !(ttl--)) { 921 | throw Error(TTL + ' $digest() iterations reached for gated watcher. Aborting!\n' + 922 | 'Watchers fired in the last 5 iterations.'); 923 | } 924 | } while (dirty); 925 | } 926 | 927 | dirty = scopePrototype.$digest.call(this) || dirty; 928 | 929 | var cleanUpQueue = this.$$cleanUpQueue; 930 | 931 | while (cleanUpQueue.length) 932 | try { 933 | cleanUpQueue.shift()(); 934 | } catch (e) { 935 | $exceptionHandler(e); 936 | } 937 | 938 | return dirty; 939 | } 940 | 941 | /** 942 | * Modifies this scope so that all future watchers registered by $watch will 943 | * only be evaluated if gatingFunction returns true. Optionally, you may specify 944 | * a function that will be evaluted on every new call to $watch with the arguments 945 | * passed to it, and that watcher will only be gated if the function returns true. 946 | * 947 | * @param {Function} gatingFunction The gating function which controls whether or not all future 948 | * watchers registered on this scope and its children will be evaluated on a given 949 | * digest cycle. The function will be invoked (with no arguments) on every digest 950 | * and if it returns a truthy result, will cause all gated watchers to be evaluated. 951 | * @param {Function} shouldGateFunction The function that controls whether or not 952 | * a new watcher will be gated using gatingFunction. It is evaluated with the 953 | * arguments to $watch and should return true if the watcher created by those 954 | * arguments should be gated 955 | * @param {Boolean} shouldEvalNewWatchers If true, if a watcher is added 956 | * after the gating function has returned true on a previous digest cycle, the 957 | * the new watcher will be evaluated on the next digest cycle even if the 958 | * gating function is currently return false. 959 | */ 960 | methodsToAdd.$addWatcherGate = function(gatingFunction, shouldGateFunction, 961 | shouldEvalNewWatchers) { 962 | var changeCount = 0; 963 | var self = this; 964 | 965 | // Set a watcher that sees if our gating function is true, and if so, digests 966 | // all of our associated watchers. Note, this.$watch could already have a 967 | // gating function associated with it, which means this watch won't be executed 968 | // unless all gating functions before us have evaluated to true. We take special 969 | // care of this nested case below. 970 | 971 | // We handle nested gating function in a special way. If we are a nested gating 972 | // function (meaning there is already one or more gating functions on this scope and 973 | // our parent scopes), then if those parent gating functions every all evaluate to 974 | // true (which we can tell if the watcher we register here is evaluated), then 975 | // we always evaluate our watcher until our gating function returns true. 976 | var hasNestedGates = !isNull(this.$$gatingFunction); 977 | 978 | (function() { 979 | var promotedWatcher = null; 980 | 981 | self.$watch(function() { 982 | if (gatingFunction()) { 983 | if (self.$digestGated(gatingFunction)) 984 | ++changeCount; 985 | } else if (hasNestedGates && isNull(promotedWatcher)) { 986 | promotedWatcher = scopePrototype.$watch.call(self, function() { 987 | if (gatingFunction()) { 988 | promotedWatcher(); 989 | promotedWatcher = null; 990 | if (self.$digestGated(gatingFunction)) 991 | ++changeCount; 992 | } 993 | return changeCount; 994 | }); 995 | } 996 | return changeCount; 997 | }); 998 | })(); 999 | 1000 | 1001 | if (isUndefined(shouldGateFunction)) 1002 | shouldGateFunction = null; 1003 | if (isUndefined(shouldEvalNewWatchers)) 1004 | shouldEvalNewWatchers = false; 1005 | this.$$gatingFunction = gatingFunction; 1006 | this.$$gatingFunction.shouldEvalNewWatchers = shouldEvalNewWatchers; 1007 | this.$$shouldGateFunction = shouldGateFunction; 1008 | }; 1009 | 1010 | // Extend the original Scope object so that when 1011 | // new instances are created, it has the new methods. 1012 | angular.extend(Scope.prototype, methodsToAdd); 1013 | 1014 | // Also extend the $rootScope instance since it was created 1015 | // before we got a chance to extend Scope.prototype. 1016 | angular.extend($rootScope, methodsToAdd); 1017 | 1018 | $rootScope.$$gatingFunction = null; 1019 | $rootScope.$$parentGatingFunction = null; 1020 | $rootScope.$$shouldGateFunction = null; 1021 | $rootScope.$$gatedWatchers = []; 1022 | $rootScope.$$cleanUpQueue = []; 1023 | 1024 | return $rootScope; 1025 | }]); 1026 | }]); 1027 | -------------------------------------------------------------------------------- /scripts/buildScalyr.js: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Usage: buildScalyr.js 4 | # 5 | # Concatenates the necessary javascript files to produce scalyr.js. 6 | 7 | function die() { 8 | echo "$1"; 9 | exit 1; 10 | } 11 | 12 | if [ ! -f "src/js/core.js" ]; then 13 | die "Could not locate core.js. Are you sure you running from the toplevel directory?"; 14 | fi 15 | 16 | 17 | sources=( 18 | "scripts/includeFiles/header.js" 19 | "src/js/core.js" 20 | "src/js/directives/slyEvaluate.js" 21 | "src/js/directives/slyRepeat.js" 22 | "src/js/lib/gatedScope.js" 23 | ) 24 | 25 | if [ -f "scalyr.js" ]; then 26 | rm scalyr.js || die "Could not remove scalyr.js file"; 27 | fi 28 | 29 | for x in ${sources[@]}; do 30 | cat "$x" >> scalyr.js || die "Failed to build scalyr.js file"; 31 | done 32 | 33 | exit 0; 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /scripts/includeFiles/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license scalyr v1.0.3 3 | * (c) 2013 Scalyr, Inc. http://scalyr.com 4 | * License: MIT 5 | */ 6 | 7 | 'use strict'; 8 | 9 | // You may just depend on the 'sly' module to pull in all of the 10 | // dependencies. 11 | angular.module('sly', ['slyEvaluate', 'slyRepeat']); 12 | -------------------------------------------------------------------------------- /scripts/includeFiles/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // This assumes you run karma from the root of the repository. 3 | // 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '../../', 9 | 10 | // Jasmine is our unit test framework. 11 | frameworks: ['jasmine'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'src/js/thirdparty/angular.js', 16 | 'src/tests/thirdparty/angular-mocks.js', 17 | 'src/tests/thirdparty/sinon.js', 18 | 'src/js/core.js', 19 | 'src/js/lib/*.js', 20 | 'src/js/directives/*.js', 21 | 'src/tests/*.js', 22 | 'src/tests/lib/*.js', 23 | 'src/tests/directives/*.js', 24 | { pattern: 'src/tests/directives/*.html', included: false, served: true }, 25 | { pattern: 'src/tests/lib/*.html', included: false, served: true }, 26 | { pattern: 'src/tests/thirdparty/jasmine/*.js', included: false, served: true }, 27 | { pattern: 'src/tests/thirdparty/jasmine/*.css', included: false, served: true }, 28 | ], 29 | 30 | // Tell the http server to sever all files from http://localhost:9876/src/ 31 | proxies: { 32 | '/src/': 'http://localhost:9876/base/src/', 33 | }, 34 | 35 | // list of files to exclude 36 | exclude: [ 37 | ], 38 | 39 | // test results reporter to use 40 | // possible values: 'dots', 'progress', 'junit' 41 | reporters: ['progress'], 42 | 43 | 44 | // web server port 45 | port: 9876, 46 | 47 | 48 | // cli runner port 49 | runnerPort: 9100, 50 | 51 | // enable / disable colors in the output (reporters and logs) 52 | colors: true, 53 | 54 | // level of logging 55 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 56 | logLevel: config.LOG_INFO, 57 | 58 | // enable / disable watching file and executing tests whenever any file changes 59 | autoWatch: true, 60 | 61 | // Start these browsers, currently available: 62 | // - Chrome 63 | // - ChromeCanary 64 | // - Firefox 65 | // - Opera 66 | // - Safari (only Mac) 67 | // - PhantomJS 68 | // - IE (only Windows) 69 | browsers: ['Chrome'], 70 | 71 | 72 | // If browser does not capture in given timeout [ms], kill it 73 | captureTimeout: 60000, 74 | 75 | // Continuous Integration mode 76 | // if true, it capture browsers, run tests and exit 77 | singleRun: false, 78 | 79 | plugins: [ 80 | 'karma-jasmine', 81 | 'karma-chrome-launcher', 82 | ], 83 | }); 84 | } 85 | 86 | -------------------------------------------------------------------------------- /scripts/startJsTester: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Usage: startJsTester 4 | # 5 | # This starts karma, the javascript unit test framework. This script 6 | # is meant to be run on a development machine. It will launch Chrome 7 | # and execute all javascript unit tests, reporting the results to 8 | # stdout. By default, the script will never terminate, but instead watch 9 | # for changes to any of the monitored javascript files and HTML resources 10 | # (defined in a configuration file), rerunning all unit tests with each new 11 | # change and reporting the results to stdout. 12 | # 13 | # The unit test framework also runs a light weight http server on 14 | # port 9876 allowing you to access local files over http, enabling 15 | # direct testing of some of the html files. This is better than just 16 | # using the browser to access the files via file:// because the browser's 17 | # default security policies prevents javascript loaded via file:// to 18 | # load other javascript files. 19 | # 20 | # To use this script, you must have installed node.js and karma. 21 | # To install karma, do the following: 22 | # 1. Download the node.js pkg file from http://nodejs.org and follow instructions. 23 | # 2. sudo npm install -g karma karma-jasmine karma-chrome-launcher 24 | 25 | # The karma configuration file, which details configures which js files 26 | # are included, how to run http server and what files to export, etc, 27 | # is in scripts/includeFiles/karma.conf.js 28 | 29 | function die() { 30 | echo "$1"; 31 | exit 1; 32 | } 33 | 34 | cd "$(dirname $0)" || die "Failed to cd to script directory"; 35 | 36 | karma start includeFiles/karma.conf.js; 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/js/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Defines core functions used throughout the Scalyr javascript 4 | * code base. This file is included on every page. 5 | * 6 | * @author Steven Czerwinski 7 | */ 8 | 9 | /** 10 | * @param {Object} value The value to check 11 | * @returns {Boolean} True if value is an Array 12 | */ 13 | function isArray(value) { 14 | return Object.prototype.toString.call(value) === '[object Array]'; 15 | } 16 | 17 | /** 18 | * @param {*} value The value to check 19 | * @returns {Boolean} True if value is a Boolean 20 | */ 21 | function isBoolean(value) { 22 | return typeof value == 'boolean'; 23 | } 24 | 25 | /** 26 | * @param {Object} value The value to check 27 | * @returns {Boolean} True if value is a Date object 28 | */ 29 | function isDate(value) { 30 | return Object.prototype.toString.call(value) === '[object Date]'; 31 | } 32 | 33 | /** 34 | * @param {*} value The value to check 35 | * @returns {Boolean} True if value is undefined 36 | */ 37 | function isDefined(value) { 38 | return typeof value != 'undefined'; 39 | } 40 | 41 | /** 42 | * @param {*} value The value to check 43 | * @returns {Boolean} True if value is a Function 44 | */ 45 | function isFunction(value) { 46 | return typeof value == 'function'; 47 | } 48 | 49 | /** 50 | * @param {*} value The value to check 51 | * @returns {Boolean} True if value is null 52 | */ 53 | function isNull(value) { 54 | return value === null; 55 | } 56 | 57 | /** 58 | * @param {*} value The value to check 59 | * @returns {Boolean} True if value is a Number 60 | */ 61 | function isNumber(value) { 62 | return typeof value == 'number'; 63 | } 64 | 65 | /** 66 | * @param {*} value The value to check 67 | * @returns {Boolean} True if value is an Object, not including null 68 | */ 69 | function isObject(value) { 70 | return value !== null && typeof value == 'object'; 71 | } 72 | 73 | /** 74 | * @param {*} value The value to check 75 | * @returns {Boolean} True if value is a string 76 | */ 77 | function isString(value) { 78 | return typeof value == 'string'; 79 | } 80 | 81 | /** 82 | * @param {*} value The value to check 83 | * @returns {Boolean} True if value is undefined 84 | */ 85 | function isUndefined(value) { 86 | return typeof value == 'undefined'; 87 | } 88 | 89 | /** 90 | * Converts a String or Boolean value to a Boolean. 91 | * 92 | * @param {String|Boolean} value The value to convert 93 | * @returns {Boolean} Returns true for any String that is not 94 | * null, empty String, or 'false'. If value is a Boolean, 95 | * returns value 96 | */ 97 | function convertToBoolean(value) { 98 | if (isBoolean(value)) 99 | return value; 100 | return value !== null && value !== '' && value !== 'false'; 101 | } 102 | 103 | /** 104 | * Determines if obj has a property named prop. 105 | * 106 | * @param {Object} obj The object to check 107 | * @returns {Boolean} Returns true if obj has a property named 108 | * prop. Only considers the object's own properties 109 | */ 110 | function hasProperty(obj, prop) { 111 | return obj.hasOwnProperty(prop); 112 | } 113 | 114 | /** 115 | * @param {*} value The value to check 116 | * @returns {Boolean} Returns true if value is a String 117 | * and has zero length, or if null or undefined 118 | */ 119 | function isStringEmpty(value) { 120 | return isNull(value) || isUndefined(value) || 121 | (isString(value) && (value.length == 0)); 122 | } 123 | 124 | /** 125 | * @param {*} value The value to check 126 | * @returns {Boolean} Returns true if value is a String 127 | * and has non-zero length 128 | */ 129 | function isStringNonempty(value) { 130 | return isString(value) && (value.length > 0); 131 | } 132 | 133 | /** 134 | * Returns input with the first letter capitalized. 135 | * The input may not be zero length. 136 | * 137 | * @param {String} input The String to capitalize. 138 | * @returns {String} Returns input with the first letter 139 | * capitalized. 140 | */ 141 | function upperCaseFirstLetter(input) { 142 | return input.charAt(0).toUpperCase() + input.slice(1); 143 | } 144 | 145 | /** 146 | * Returns true if obj1 and obj2 are equal. This should 147 | * only be used for Arrays, Objects, and value types. This is a deep 148 | * comparison, comparing each property and recursive property to 149 | * be equal (not just ===). 150 | * 151 | * Two Objects or values are considered equivalent if at least one of the following is true: 152 | * - Both objects or values pass `===` comparison. 153 | * - Both objects or values are of the same type and all of their properties pass areEqual 154 | * comparison. 155 | * - Both values are NaN. (In JavasScript, NaN == NaN => false. But we consider two NaN as equal). 156 | * 157 | * Note, during property comparision, properties with function values are ignores as are property 158 | * names beginning with '$'. 159 | * 160 | * See angular.equal for more details. 161 | * 162 | * @param {Object|Array|value} obj1 The first object 163 | * @param {Object|Array|value} obj2 The second object 164 | * @returns {Boolean} True if the two objects are equal using a deep 165 | * comparison. 166 | */ 167 | function areEqual(obj1, obj2) { 168 | return angular.equals(obj1, obj2); 169 | } 170 | 171 | /** 172 | * @param {Number} a The first Number 173 | * @param {Number} b The second Number 174 | * @returns {Number} The minimum of a and b 175 | */ 176 | function min(a, b) { 177 | return a < b ? a : b; 178 | } 179 | 180 | /** 181 | * @param {Number} a The first Number 182 | * @param {Number} b The second Number 183 | * @returns {Number} The maximum of a and b 184 | */ 185 | function max(a, b) { 186 | return a > b ? a : b; 187 | } 188 | 189 | /** 190 | * Returns true if the specified String begins with prefix. 191 | * 192 | * @param {*} input The input to check 193 | @ @param {String} prefix The prefix 194 | * @returns {Boolean} True if input is a string that begins with prefix 195 | */ 196 | function beginsWith(input, prefix) { 197 | return isString(input) && input.lastIndexOf(prefix, 0) == 0; 198 | } 199 | 200 | /** 201 | * Returns true if the specified String ends with prefix. 202 | * 203 | * @param {*} input The input to check 204 | @ @param {String} postfix The postfix 205 | * @returns {Boolean} True if input is a string that ends with postfix 206 | */ 207 | function endsWith(input, postfix) { 208 | return isString(input) && input.indexOf(postfix, input.length - postfix.length) !== -1; 209 | } 210 | 211 | /** 212 | * Returns a deep copy of source, where source can be an Object or an Array. If a destination is 213 | * provided, all of its elements (for Array) or properties (for Objects) are deleted and then all 214 | * elements/properties from the source are copied to it. If source is not an Object or Array, 215 | * source is returned. 216 | * 217 | * See angular.copy for more details. 218 | * @param {Object|Array} source The source 219 | * @param {Object|Array} destination Optional object to copy the elements to 220 | * @returns {Object|Array} The deep copy of source 221 | */ 222 | function copy(source, destination) { 223 | return angular.copy(source, destination); 224 | } 225 | 226 | /** 227 | * Removes property from obj. 228 | * 229 | * @param {Object} obj The object 230 | * @param {String} property The property name to delete 231 | */ 232 | function removeProperty(obj, property) { 233 | delete obj[property]; 234 | } 235 | 236 | /** 237 | * Removes all properties in the array from obj. 238 | * 239 | * @param {Object} obj The object 240 | * @param {Array} properties The properties to remove 241 | */ 242 | function removeProperties(obj, properties) { 243 | for (var i = 0; i < properties.length; ++i) 244 | delete obj[properties[i]]; 245 | } 246 | 247 | /** 248 | * Invokes the iterator function once for each item in obj collection, which can be either 249 | * an Object or an Array. The iterator function is invoked with iterator(value, key), 250 | * where value is the value of an object property or an array element and key is the 251 | * object property key or array element index. Specifying a context for the function is 252 | * optional. If specified, it becomes 'this' when iterator function is invoked. 253 | * 254 | * See angular.forEach for more details. 255 | * 256 | * @param {Object|Array} The Object or Array over which to iterate 257 | * @param {Function} iterator The iterator function to invoke 258 | * @param {Object} context The value to set for 'this' when invoking the 259 | * iterator function. This is optional 260 | */ 261 | function forEach(obj, iterator, context) { 262 | return angular.forEach(obj, iterator, context); 263 | } 264 | 265 | /** 266 | * Used to define a Scalyr javascript library and optionally declare 267 | * dependencies on other libraries. All javascript code not defined in 268 | * this file should be defined as part of a library. 269 | * 270 | * The first argument is the name to call the library. The second argument 271 | * is either a Constructor object for the library or an array where the last 272 | * element is the Constructor for the library and the first to N-1 are string 273 | * names of the libraries this one depends on. If you do declare dependencies, 274 | * the libraries are passed in the Constructor create method in the same order 275 | * as the strings are defined. 276 | * 277 | * Example: 278 | * defineScalyrJsLibrary('myUtils', function() { 279 | * var fooFunction = function(a, b) { 280 | * return a + b; 281 | * }; 282 | * return { 283 | * foo: fooFunction 284 | * }; 285 | * }); 286 | * 287 | * defineScalyrJsLibrary('anotherUtils', [ 'myUtils', function(myUtils) { 288 | * var barFunction = function(a, b) { 289 | * return myUtils.foo(a, b); 290 | * }; 291 | * return { 292 | * bar: barFunction 293 | * }; 294 | * }); 295 | * 296 | * @param {String} libraryName The name for the library 297 | * @param {Constructor|Array} libraryExporter The exporter for the 298 | * library. See above for details 299 | */ 300 | function defineScalyrJsLibrary(libraryName, libraryExporter) { 301 | var moduleDependencies = []; 302 | if (libraryExporter instanceof Array) { 303 | for (var i = 0; i < libraryExporter.length - 1; ++i) 304 | moduleDependencies.push(libraryExporter[i]); 305 | } 306 | 307 | return angular.module(libraryName, moduleDependencies) 308 | .factory(libraryName, libraryExporter); 309 | } 310 | 311 | /** 312 | * Similar to defineScalyrJsLibary but instead of declaring 313 | * a purely javascript library, this declares an Angular module 314 | * library. The moduleName should be a string used to identify 315 | * this module. The dependencies is an array with the string 316 | * names of Angular modules, Scalyr Angular modules, or Scalyr 317 | * javascript libraries to depend on. The returned object 318 | * can be used to define directives, etc similar to angular.module. 319 | * 320 | * Example: 321 | * defineScalyrAngularModule('slyMyModule', [ 'myTextUtils']) 322 | * .filter('camelCase', function(myTextUtils) { 323 | * return function(input) { 324 | * return myTextUtils.camelCase(input); 325 | * }; 326 | * }); 327 | * 328 | * @param {String} moduleName The name of the module 329 | * @param {Array} dependencies The names of modules to depend on 330 | */ 331 | function defineScalyrAngularModule(moduleName, dependencies) { 332 | return angular.module(moduleName, dependencies); 333 | } 334 | -------------------------------------------------------------------------------- /src/js/directives/slyEvaluate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Module: slyEvaluate 4 | * 5 | * Defines several directives related to preventing evaluating watchers 6 | * on scopes under certain conditions. Here's a list of the directives 7 | * and brief descriptions. See down below for more details. 8 | * 9 | * slyEvaluateOnlyWhen: A directive that prevents updating / evaluating 10 | * all bindings for the current element and its children unless 11 | * the expression has changed values. If new children are added, they 12 | * are always evaluated at least once. It currently assumes the 13 | * expression evaluates to an object and detects changes only by 14 | * a change in object reference. 15 | * 16 | * slyAlwaysEvaluate: Can only be used in conjunction with the 17 | * slyEvaluateOnlyWhen directive. This directive will ensure that 18 | * any expression that is being watched will always be evaluated 19 | * if it contains the specified string (i.e., it will ignore whether 20 | * or not the slyEvaluateOnlyWhen expression has changed.) This 21 | * is useful when you wish to check some expressions all the time. 22 | * 23 | * slyPreventEvaluationWhenHidden: Will only evaluate the bindings 24 | * for the current element and its children if the current element 25 | * is not hidden (detected by the element having the ng-hide CSS class.) 26 | * 27 | * slyShow: Will hide the element if the expression evaluates to false. 28 | * Uses ng-hide to hide the element. This is almost exactly the same 29 | * as ngShow, but it has the advantage that it works better with 30 | * slyPreventEvaluationWhenHidden by guaranteeing it will always evaluate 31 | * its show expression to determine if it should or should not be hidden. 32 | */ 33 | defineScalyrAngularModule('slyEvaluate', ['gatedScope']) 34 | /** 35 | * Directive for preventing all bound expressions in the current element and its children 36 | * from being evaluated unless the specified expression evaluates to a different object. 37 | * Currently, the value assigned to the 'slyEvaluateOnlyWhen' must evaluate to an object. 38 | * Also, reference equality is used to determine if the expression has changed. 39 | * TODO: Make this more versatile, similar to $watch. For now, this is all we need. 40 | */ 41 | .directive('slyEvaluateOnlyWhen', ['$parse', function ($parse) { 42 | return { 43 | // We create a new scope just because it helps segment the gated watchers 44 | // from the parent scope. Unclear if this is that important for perf. 45 | scope: true, 46 | restrict: 'A', 47 | compile: function compile(tElement, tAttrs) { 48 | return { 49 | // We need a separate pre-link function because we want to modify the scope before any of the 50 | // children are passed it. 51 | pre: function preLink(scope, element, attrs) { 52 | var previousValue = null; 53 | var initialized = false; 54 | 55 | var expressionToCheck = $parse(attrs['slyEvaluateOnlyWhen']); 56 | var alwaysEvaluateString = null; 57 | if (hasProperty(attrs, 'slyAlwaysEvaluate')) { 58 | alwaysEvaluateString = attrs['slyAlwaysEvaluate']; 59 | if (isStringEmpty(alwaysEvaluateString)) 60 | throw new Exception('Empty string is illegal for value of slyAlwaysEvaluate'); 61 | } 62 | scope.$addWatcherGate(function evaluteOnlyWhenChecker() { 63 | // We should only return true if expressionToCheck evaluates to a value different 64 | // than previousValue. 65 | var currentValue = expressionToCheck(scope); 66 | if (!initialized) { 67 | initialized = true; 68 | previousValue = currentValue; 69 | return true; 70 | } 71 | var result = previousValue !== currentValue; 72 | previousValue = currentValue; 73 | return result; 74 | }, function shouldGateWatcher(watchExpression) { 75 | // Should return true if the given watcher that's about to be registered should 76 | // be gated. 77 | return isNull(alwaysEvaluateString) || 78 | !(isStringNonempty(watchExpression) && (watchExpression.indexOf(alwaysEvaluateString) >= 0)); 79 | }, true /* Evaluate any newly added watchers when they are added */); 80 | }, 81 | }; 82 | }, 83 | }; 84 | }]) 85 | /** 86 | * Directive for overriding the 'slyEvaluateOnlyWhen' expression for the current element. 87 | * This directive takes a single string value. If this string value is found anywhere in 88 | * an expression that normally would not be evaluated due to the 'slyEvaluateOnlyWhen' 89 | * directive, it is evaluated, regardless of whether or not the value for the expression in 90 | * 'slyEvaluateOnlyWhen' has changed. This is very useful when a certain expression used by 91 | * one of the children of the current element should always be evaluated and is not affected 92 | * by the expression specified in slyEvaluateOnlyWhen. 93 | */ 94 | .directive('slyAlwaysEvaluate', function() { 95 | // This is just a place holder to show that slyAlwaysEvaluate is a legal 96 | // directive. The real work for this directive is done in slyEvaluateOnlyWhen. 97 | return { 98 | restrict: 'A', 99 | link: function(scope, element, attrs) { 100 | }, 101 | }; 102 | }) 103 | /** 104 | * Directive for showing an element, very similar to ngShow. However, this directive 105 | * works better with slyPreventEvaluationWhenHidden because it is ensure it always 106 | * will evaluate the show expression to determine if it should be shown or hidden 107 | * even if slyPreventEvaluationWhenHidden is in effect. This directive also uses 108 | * the ng-hide css class to actually hide the element. 109 | * 110 | * NOTE: We might be able to get better performance if we have this directive directly 111 | * perform a callback on slyPreventEvaluationWhenHidden when it is shown/hidden rather 112 | * than having that directive register a watcher on the css class. 113 | */ 114 | .directive('slyShow', ['$animate', function($animate) { 115 | /** 116 | * @param {*} value The input 117 | * @return {Boolean} True if the value is truthy as determined by angular rules. 118 | * 119 | * Note: This is copied from the Angular source because it is not exposed by Angular 120 | * but we want our directive to behave the same as ngShow. Think about moving this 121 | * to core.js. 122 | */ 123 | function toBoolean(value) { 124 | if (value && value.length !== 0) { 125 | var v = ("" + value); 126 | v = isString(v) ? v.toLowerCase() : v; 127 | value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); 128 | } else { 129 | value = false; 130 | } 131 | return value; 132 | } 133 | 134 | return { 135 | restrict: 'A', 136 | link: function slyShowLink(scope, element, attr) { 137 | scope.$watch(attr.slyShow, function ngSlyShowAction(value){ 138 | $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); 139 | }, false, 'slyShow'); }, 140 | }; 141 | }]) 142 | /** 143 | * Directive for preventing all bound expressions in the current element and its children 144 | * from being evaluated if the current element is hidden as determined by whether or not 145 | * it has the ng-hide class. 146 | */ 147 | .directive('slyPreventEvaluationWhenHidden', function () { 148 | return { 149 | restrict: 'A', 150 | // We create a new scope just because it helps segment the gated watchers 151 | // from the parent scope. Unclear if this is that important for perf. 152 | scope: true, 153 | compile: function compile(tElement, tAttrs) { 154 | return { 155 | // We need a separate pre-link function because we want to modify the scope before any of the 156 | // children are passed it. 157 | pre: function preLink(scope, element, attrs) { 158 | scope.$addWatcherGate(function hiddenChecker() { 159 | // Should only return true if the element is not hidden. 160 | return !element.hasClass('ng-hide'); 161 | }, function hiddenDecider(watchExpression, listener, equality, directiveName) { 162 | // Make an exception for slyShow.. do not gate its watcher. 163 | if (isDefined(directiveName) && (directiveName == 'slyShow')) 164 | return false; 165 | return true; 166 | }); 167 | }, 168 | }; 169 | }, 170 | }; 171 | }); 172 | 173 | -------------------------------------------------------------------------------- /src/js/directives/slyRepeat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Module: slyRepeat 4 | * 5 | * Contains the slyRepeat directive, which is is a modified version of the 6 | * ngRepeat directive that is meant to be more efficient for creating and 7 | * recreating large lists of bound elements. In particular, it has an 8 | * optimization that will prevent DOM elements from being constantly created 9 | * and destroyed as the contents of the repeated elements change. It does this 10 | * by not destroying DOM elements when they are no longer needed, but instead, 11 | * just hiding them. This might not work for all use cases, but for it does 12 | * for the ones we do wish to heavily optimize. For eample, through profiling, 13 | * we found that destroying DOM elements when flipping through log view pages 14 | * represented a large chunk of CPU time. 15 | * 16 | * Cavaets: The collection expression must evaluate to an array. Animators 17 | * will not work. Track By does not work. Use at your own peril. 18 | * 19 | * @author Steven Czerwinski 20 | */ 21 | defineScalyrAngularModule('slyRepeat', ['gatedScope']) 22 | .directive('slyRepeat', ['$animate', '$parse', function ($animate, $parse) { 23 | 24 | /** 25 | * Sets the scope contained in elementScope to gate all its 26 | * watchers based on the isActiveForRepeat proprety. 27 | * 28 | * @param {Object} elementScope The object containing the 29 | * scope and isActiveForRepeat properties. 30 | */ 31 | function gateWatchersForScope(elementScope) { 32 | elementScope.scope.$addWatcherGate(function() { 33 | return elementScope.isActiveForRepeat; 34 | }); 35 | } 36 | 37 | return { 38 | restrict: 'A', 39 | scope: true, 40 | transclude: 'element', 41 | priority: 1000, 42 | terminal: true, 43 | compile: function(element, attr, linker) { 44 | // Most of the work is done in the post-link function. 45 | return function($scope, $element, $attr) { 46 | // This code is largely based on ngRepeat. 47 | 48 | // Parse the expression. It should look like: 49 | // x in some-expression 50 | var expression = $attr.slyRepeat; 51 | var match = expression.match(/^\s*(.+)\s+in\s+(.*?)$/); 52 | if (!match) { 53 | throw Error("Expected slyRepeat in form of '_item_ in _collection_' but got '" + 54 | expression + "'."); 55 | } 56 | 57 | var iterVar = match[1]; 58 | var collectionExpr = match[2]; 59 | 60 | match = iterVar.match(/^(?:([\$\w]+))$/); 61 | if (!match) { 62 | throw Error("'item' in 'item in collection' should be identifier but got '" + 63 | lhs + "'."); 64 | } 65 | 66 | // previousElements will store references to the already existing (DOM) elements 67 | // that were last used for the last rendering of this repeat and were visible. 68 | // We will re-use these elements when executing the next rendering of the repeat when 69 | // the iteration value changes. 70 | var previousElements = []; 71 | // previousElementsBuffer will store references to the already existing (DOM) elements 72 | // that are in the page but were not used for the last rendering of this repeat and were 73 | // therefore marked as inactive and not visible. This happens if the length of the repeat 74 | // iteration goes down over time, since we do not remove the elements. If the repeat length 75 | // was first 10, then 5, we will end up with the last 5 elements in the previousElementBuffer. 76 | // We keep this in case the length increases again. 77 | var previousElementBuffer = []; 78 | 79 | var deregisterCallback = $scope.$watchCollection(collectionExpr, function(collection) { 80 | if (!collection) 81 | return; 82 | if (!isArray(collection)) 83 | throw Error("'collection' did not evaluate to an array. expression was " + collectionExpr); 84 | var originalPreviousElementsLength = previousElements.length; 85 | // First, reconcile previousElements and collection with respect to the previousElementBuffer. 86 | // Basically, try to grow previousElements to collection.length if we can. 87 | if ((previousElements.length < collection.length) && (previousElementBuffer.length > 0)) { 88 | var limit = previousElements.length + previousElementBuffer.length; 89 | if (limit > collection.length) 90 | limit = collection.length; 91 | previousElements = previousElements.concat(previousElementBuffer.splice(0, limit - previousElements.length)); 92 | } 93 | 94 | var currentElements = null; 95 | var currentElementBuffer = []; 96 | 97 | var newElements = []; 98 | if (collection.length > previousElements.length) { 99 | // Add in enough elements to account for the larger collection. 100 | for (var i = previousElements.length; i < collection.length; ++i) { 101 | // Need to add in an element for each new item in the collection. 102 | var newElement = { 103 | scope: $scope.$new(), 104 | isActiveForRepeat: true, 105 | }; 106 | 107 | gateWatchersForScope(newElement); 108 | newElement.scope.$index = i; 109 | newElement.scope.$first = (i == 0); 110 | newElements.push(newElement); 111 | } 112 | currentElements = previousElements.concat(newElements); 113 | currentElementBuffer = previousElementBuffer; 114 | } else if (collection.length < previousElements.length) { 115 | for (var i = collection.length; i < previousElements.length; ++i) 116 | previousElements[i].isActiveForRepeat = false; 117 | 118 | currentElementBuffer = previousElements.splice(collection.length, previousElements.length - collection.length).concat( 119 | previousElementBuffer); 120 | currentElements = previousElements; 121 | } else { 122 | currentElements = previousElements; 123 | currentElementBuffer = previousElementBuffer; 124 | } 125 | 126 | // We have to fix up the last and middle values in the scope for each element in 127 | // currentElements, since their roles may have changed with the new length. 128 | // We always have to fix the last element. 129 | if (currentElements.length > 0) { 130 | var firstIndexToFix = currentElements.length - 1; 131 | var lastIndexToFix = currentElements.length - 1; 132 | // We also have to fix any new elements that were added. 133 | if (originalPreviousElementsLength < currentElements.length) { 134 | firstIndexToFix = originalPreviousElementsLength; 135 | } 136 | // And we usually have to fix the element before the first element we modified 137 | // in case it used to be last. 138 | if (firstIndexToFix > 0) { 139 | firstIndexToFix = firstIndexToFix - 1; 140 | } 141 | for (var i = firstIndexToFix; i <= lastIndexToFix; ++i) { 142 | currentElements[i].scope.$last = (i == (currentElements.length - 1)); 143 | currentElements[i].scope.$middle = ((i != 0) && (i != (currentElements.length - 1))); 144 | if (!currentElements[i].isActiveForRepeat) { 145 | // If it is not marked as active, make it active. This is also indicates that 146 | // the element is currently hidden, so we have to unhide it. 147 | currentElements[i].isActiveForRepeat = true; 148 | currentElements[i].element.css('display', ''); 149 | } 150 | } 151 | } 152 | 153 | // Hide all elements that have recently become inactive. 154 | for (var i = 0; i < currentElementBuffer.length; ++i) { 155 | if (currentElementBuffer[i].isActiveForRepeat) 156 | break; 157 | currentElementBuffer[i].element.css('display', 'none'); 158 | } 159 | 160 | // Assign the new value for the iter variable for each scope. 161 | for (var i = 0; i < currentElements.length; ++i) { 162 | currentElements[i].scope[iterVar] = collection[i]; 163 | } 164 | 165 | // We have to go back now and clone the DOM element for any new elements we 166 | // added and link them in. We clone the last DOM element we had created already 167 | // for this Repeat. 168 | var prevElement = $element; 169 | if (previousElements.length > 0) 170 | prevElement = previousElements[previousElements.length - 1].element; 171 | for (var i = 0; i < newElements.length; ++i) { 172 | linker(newElements[i].scope, function(clone) { 173 | $animate.enter(clone, null, prevElement); 174 | prevElement = clone; 175 | newElements[i].element = clone; 176 | }); 177 | } 178 | 179 | previousElements = currentElements; 180 | previousElementBuffer = currentElementBuffer; 181 | }); 182 | $scope.$on('$destroy', function() { 183 | deregisterCallback(); 184 | }); 185 | }; 186 | } 187 | }; 188 | }]); 189 | -------------------------------------------------------------------------------- /src/js/lib/gatedScope.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Defines an extension to angular.Scope that allows for registering 4 | * 'gating functions' on a scope that will prevent all future watchers 5 | * registered on the scope from being evaluated unless the gating function 6 | * returns true. 7 | * 8 | * By depending on this module, the $rootScope instance and angular.Scope 9 | * class are automatically extended to implement this new capability. 10 | * 11 | * Warning, this implementation depends on protected/private variables 12 | * in the angular.Scope implementation and therefore can break in the 13 | * future due to changes in the angular.Scope implementation. Use at 14 | * your own risk. 15 | */ 16 | defineScalyrAngularModule('gatedScope', []) 17 | .config(['$provide', function($provide) { 18 | // We use a decorator to override methods in $rootScope. 19 | $provide.decorator('$rootScope', ['$delegate', '$exceptionHandler', 20 | function ($rootScope, $exceptionHandler) { 21 | 22 | // Make a copy of $rootScope's original methods so that we can access 23 | // them to invoke super methods in the ones we override. 24 | var scopePrototype = {}; 25 | for (var key in $rootScope) { 26 | if (isFunction($rootScope[key])) 27 | scopePrototype[key] = $rootScope[key]; 28 | } 29 | 30 | var Scope = $rootScope.constructor; 31 | 32 | // Hold all of our new methods. 33 | var methodsToAdd = { 34 | }; 35 | 36 | // A constant value that the $digest loop implementation depends on. We 37 | // grab it down below. 38 | var initWatchVal; 39 | 40 | /** 41 | * @param {Boolean} isolate Whether or not the new scope should be isolated. 42 | * @returns {Scope} A new child scope 43 | */ 44 | methodsToAdd.$new = function(isolate) { 45 | // Because of how scope.$new works, the returned result 46 | // should already have our new methods. 47 | var result = scopePrototype.$new.call(this, isolate); 48 | 49 | // We just have to do the work that normally a child class's 50 | // constructor would perform -- initializing our instance vars. 51 | result.$$gatingFunction = this.$$gatingFunction; 52 | result.$$parentGatingFunction = this.$$gatingFunction; 53 | result.$$shouldGateFunction = this.$$shouldGateFunction; 54 | result.$$gatedWatchers = []; 55 | result.$$cleanUpQueue = this.$$cleanUpQueue; 56 | 57 | return result; 58 | }; 59 | 60 | /** 61 | * Digests all of the gated watchers for the specified gating function. 62 | * 63 | * @param {Function} targetGatingFunction The gating function associated 64 | * with the watchers that should be digested 65 | * @returns {Boolean} True if any of the watchers were dirty 66 | */ 67 | methodsToAdd.$digestGated = function gatedScopeDigest(targetGatingFunction) { 68 | // Note, most of this code was stolen from angular's Scope.$digest method. 69 | var watch, value, 70 | watchers, 71 | length, 72 | next, current = this, target = this, last, 73 | dirty = false; 74 | 75 | do { // "traverse the scopes" loop 76 | if (watchers = current.$$gatedWatchers) { 77 | // process our watches 78 | length = watchers.length; 79 | while (length--) { 80 | try { 81 | watch = watchers[length]; 82 | // Scalyr edit: We do not process a watch function if it is does not 83 | // have the same gating function for which $digestGated was invoked. 84 | if (watch.gatingFunction !== targetGatingFunction) 85 | continue; 86 | 87 | // Since we are about to execute the watcher as part of a digestGated 88 | // call, we can remove it from the normal digest queue if it was placed 89 | // there because the watcher was added after the gate function's first 90 | // evaluation. 91 | if (watch && !isNull(watch.cleanUp)) { 92 | watch.cleanUp(); 93 | watch.cleanUp = null; 94 | } 95 | // Most common watches are on primitives, in which case we can short 96 | // circuit it with === operator, only when === fails do we use .equals 97 | if (watch && (value = watch.get(current)) !== (last = watch.last) && 98 | !(watch.eq 99 | ? areEqual(value, last) 100 | : (typeof value == 'number' && typeof last == 'number' 101 | && isNaN(value) && isNaN(last)))) { 102 | dirty = true; 103 | watch.last = watch.eq ? copy(value) : value; 104 | watch.fn(value, ((last === initWatchVal) ? value : last), current); 105 | // Scalyr edit: Removed the logging code for when the ttl is reached 106 | // here because we don't have access to the ttl in this method. 107 | } 108 | } catch (e) { 109 | $exceptionHandler(e); 110 | } 111 | } 112 | } 113 | 114 | // Insanity Warning: scope depth-first traversal 115 | // yes, this code is a bit crazy, but it works and we have tests to prove it! 116 | // Scalyr edit: This insanity warning was from angular. We only modified this 117 | // code by checking the $$gatingFunction because it's a good optimization to only go 118 | // down a child of a parent that has the same gating function as what we are processing 119 | // (since if a parent already has a different gating function, there's no way any 120 | // of its children will have the right one). 121 | if (!(next = ((current.$$gatingFunction === targetGatingFunction && current.$$childHead) 122 | || (current !== target && current.$$nextSibling)))) { 123 | while(current !== target && !(next = current.$$nextSibling)) { 124 | current = current.$parent; 125 | } 126 | } 127 | } while ((current = next)); 128 | 129 | // Mark that this gating function has digested all children. 130 | targetGatingFunction.hasDigested = true; 131 | return dirty; 132 | }; 133 | 134 | /** 135 | * @inherited $watch 136 | * @param directiveName The fourth parameter is a new optional parameter that allows 137 | * directives aware of this abstraction to pass in their own names to identify 138 | * which directive is registering the watch. This is then passed to the 139 | * shouldGateFunction to help determine if the watcher should be gated by the current 140 | * gatingFunction. 141 | */ 142 | methodsToAdd.$watch = function gatedWatch(watchExpression, listener, objectEquality, 143 | directiveName) { 144 | // Determine if we should gate this watcher. 145 | if (!isNull(this.$$gatingFunction) && (isNull(this.$$shouldGateFunction) || 146 | this.$$shouldGateFunction(watchExpression, listener, objectEquality, directiveName))) { 147 | // We do a hack here to just switch out the watchers array with our own 148 | // gated list and then invoke the original watch function. 149 | var tmp = this.$$watchers; 150 | this.$$watchers = this.$$gatedWatchers; 151 | // Invoke original watch function. 152 | var result = scopePrototype.$watch.call(this, watchExpression, listener, objectEquality); 153 | this.$$watchers = tmp; 154 | this.$$gatedWatchers[0].gatingFunction = this.$$gatingFunction; 155 | this.$$gatedWatchers[0].cleanUp = null; 156 | 157 | // We know that the last field of the watcher object will be set to initWatchVal, so we 158 | // grab it here. 159 | initWatchVal = this.$$gatedWatchers[0].last; 160 | var watch = this.$$gatedWatchers[0]; 161 | 162 | // We should make sure the watch expression gets evaluated fully on at least one 163 | // digest cycle even if the gate function is now closed if requested by the gating function's 164 | // value for shouldEvalNewWatchers. We do this by adding in normal watcher that will execute 165 | // the watcher we just added and remove itself after the digest cycle completes. 166 | if (this.$$gatingFunction.shouldEvalNewWatchers && this.$$gatingFunction.hasDigested) { 167 | var self = this; 168 | watch.cleanUp = scopePrototype.$watch.call(self, function() { 169 | if (!isNull(watch.cleanUp)) { 170 | self.$$cleanUpQueue.unshift(watch.cleanUp); 171 | watch.cleanUp = null; 172 | } 173 | var value; 174 | var last = initWatchVal; 175 | 176 | if (watch && (value = watch.get(self)) !== (last = watch.last) && 177 | !(watch.eq 178 | ? areEqual(value, last) 179 | : (typeof value == 'number' && typeof last == 'number' 180 | && isNaN(value) && isNaN(last)))) { 181 | watch.last = watch.eq ? copy(value) : value; 182 | watch.fn(value, ((last === initWatchVal) ? value : last), self); 183 | } 184 | return watch.last; 185 | }); 186 | } 187 | return result; 188 | } else { 189 | return scopePrototype.$watch.call(this, watchExpression, listener, objectEquality); 190 | } 191 | }; 192 | 193 | /** 194 | * @inherited $digest 195 | */ 196 | methodsToAdd.$digest = function gatedDigest() { 197 | // We have to take care if a scope's digest method was invoked that has a 198 | // gating function in the parent scope. In this case, the watcher for that 199 | // gating function is registered in the parent (the one added in gatedWatch), 200 | // and will not be evaluated here. So, we have to manually see if the gating 201 | // function is true and if so, evaluate any gated watchers for that function on 202 | // this scope. This needs to happen to properly support invoking $digest on a 203 | // scope with a parent scope with a gating function. 204 | // NOTE: It is arguable that we are not correctly handling nested gating functions 205 | // here since we do not know if the parent gating function was nested in other gating 206 | // functions and should be evaluated at all. However, if a caller is invoking 207 | // $digest on a particular scope, we assume the caller is doing that because it 208 | // knows the watchers should be evaluated. 209 | var dirty = false; 210 | if (!isNull(this.$$parentGatingFunction) && this.$$parentGatingFunction()) { 211 | var ttl = 5; 212 | do { 213 | dirty = this.$digestGated(this.$$parentGatingFunction); 214 | ttl--; 215 | 216 | if (dirty && !(ttl--)) { 217 | throw Error(TTL + ' $digest() iterations reached for gated watcher. Aborting!\n' + 218 | 'Watchers fired in the last 5 iterations.'); 219 | } 220 | } while (dirty); 221 | } 222 | 223 | dirty = scopePrototype.$digest.call(this) || dirty; 224 | 225 | var cleanUpQueue = this.$$cleanUpQueue; 226 | 227 | while (cleanUpQueue.length) 228 | try { 229 | cleanUpQueue.shift()(); 230 | } catch (e) { 231 | $exceptionHandler(e); 232 | } 233 | 234 | return dirty; 235 | } 236 | 237 | /** 238 | * Modifies this scope so that all future watchers registered by $watch will 239 | * only be evaluated if gatingFunction returns true. Optionally, you may specify 240 | * a function that will be evaluted on every new call to $watch with the arguments 241 | * passed to it, and that watcher will only be gated if the function returns true. 242 | * 243 | * @param {Function} gatingFunction The gating function which controls whether or not all future 244 | * watchers registered on this scope and its children will be evaluated on a given 245 | * digest cycle. The function will be invoked (with no arguments) on every digest 246 | * and if it returns a truthy result, will cause all gated watchers to be evaluated. 247 | * @param {Function} shouldGateFunction The function that controls whether or not 248 | * a new watcher will be gated using gatingFunction. It is evaluated with the 249 | * arguments to $watch and should return true if the watcher created by those 250 | * arguments should be gated 251 | * @param {Boolean} shouldEvalNewWatchers If true, if a watcher is added 252 | * after the gating function has returned true on a previous digest cycle, the 253 | * the new watcher will be evaluated on the next digest cycle even if the 254 | * gating function is currently return false. 255 | */ 256 | methodsToAdd.$addWatcherGate = function(gatingFunction, shouldGateFunction, 257 | shouldEvalNewWatchers) { 258 | var changeCount = 0; 259 | var self = this; 260 | 261 | // Set a watcher that sees if our gating function is true, and if so, digests 262 | // all of our associated watchers. Note, this.$watch could already have a 263 | // gating function associated with it, which means this watch won't be executed 264 | // unless all gating functions before us have evaluated to true. We take special 265 | // care of this nested case below. 266 | 267 | // We handle nested gating function in a special way. If we are a nested gating 268 | // function (meaning there is already one or more gating functions on this scope and 269 | // our parent scopes), then if those parent gating functions every all evaluate to 270 | // true (which we can tell if the watcher we register here is evaluated), then 271 | // we always evaluate our watcher until our gating function returns true. 272 | var hasNestedGates = !isNull(this.$$gatingFunction); 273 | 274 | (function() { 275 | var promotedWatcher = null; 276 | 277 | self.$watch(function() { 278 | if (gatingFunction()) { 279 | if (self.$digestGated(gatingFunction)) 280 | ++changeCount; 281 | } else if (hasNestedGates && isNull(promotedWatcher)) { 282 | promotedWatcher = scopePrototype.$watch.call(self, function() { 283 | if (gatingFunction()) { 284 | promotedWatcher(); 285 | promotedWatcher = null; 286 | if (self.$digestGated(gatingFunction)) 287 | ++changeCount; 288 | } 289 | return changeCount; 290 | }); 291 | } 292 | return changeCount; 293 | }); 294 | })(); 295 | 296 | 297 | if (isUndefined(shouldGateFunction)) 298 | shouldGateFunction = null; 299 | if (isUndefined(shouldEvalNewWatchers)) 300 | shouldEvalNewWatchers = false; 301 | this.$$gatingFunction = gatingFunction; 302 | this.$$gatingFunction.shouldEvalNewWatchers = shouldEvalNewWatchers; 303 | this.$$shouldGateFunction = shouldGateFunction; 304 | }; 305 | 306 | // Extend the original Scope object so that when 307 | // new instances are created, it has the new methods. 308 | angular.extend(Scope.prototype, methodsToAdd); 309 | 310 | // Also extend the $rootScope instance since it was created 311 | // before we got a chance to extend Scope.prototype. 312 | angular.extend($rootScope, methodsToAdd); 313 | 314 | $rootScope.$$gatingFunction = null; 315 | $rootScope.$$parentGatingFunction = null; 316 | $rootScope.$$shouldGateFunction = null; 317 | $rootScope.$$gatedWatchers = []; 318 | $rootScope.$$cleanUpQueue = []; 319 | 320 | return $rootScope; 321 | }]); 322 | }]); 323 | -------------------------------------------------------------------------------- /src/tests/directives/allTests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests for js/directives directory 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/tests/directives/slyEvaluateTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Test cases for the slyEvaluate module. 4 | * 5 | * @author Steven Czerwinski 6 | */ 7 | describe('slyEvaluate.slyEvaluateOnlyWhen', function() { 8 | // Elements from the test page. 9 | var scope = null; 10 | var page = null; 11 | var span = null; 12 | 13 | // Require the slyEvaluate module. 14 | beforeEach(module('slyEvaluate')); 15 | 16 | // Create the sample DOM elements using sly-evaluate-only-when. 17 | // Specifically, make all updates to page dependent on dataObject 18 | // and have a span containing x. 19 | beforeEach(inject(function($rootScope, $compile) { 20 | scope = $rootScope; 21 | scope.dataObject = { 22 | value: 5, 23 | }; 24 | scope.x = 10; 25 | 26 | page = angular.element( 27 | '
' + 28 | '{{x}}' + 29 | '
'); 30 | 31 | $compile(page)(scope); 32 | scope.$digest(); 33 | 34 | // Set the DOM references. 35 | span = page.find('span'); 36 | })); 37 | 38 | it('should initialize all bindings', function() { 39 | // Just check basic functionality. 40 | expect(span.eq(0).text()).toEqual('10'); 41 | }); 42 | 43 | it('should not update bindings if object has not changed', function() { 44 | // Change x and do a digest. The binding should not have updated 45 | // since object has not changed. 46 | scope.x = 12; 47 | scope.$digest(); 48 | 49 | expect(span.eq(0).text()).toEqual('10'); 50 | }); 51 | 52 | it('should not update bindings even if contents of object has changed', function() { 53 | // Change x and do a digest. The binding should not have updated 54 | // since object has not changed. 55 | scope.x = 12; 56 | scope.dataObject.value = 6; 57 | scope.$digest(); 58 | 59 | expect(span.eq(0).text()).toEqual('10'); 60 | }); 61 | 62 | it('should update bindings after object reference has changed', function() { 63 | // Change both x and dataObject 64 | scope.x = 12; 65 | scope.dataObject = { 66 | value: 5, 67 | }; 68 | scope.$digest(); 69 | 70 | expect(span.eq(0).text()).toEqual('12'); 71 | }); 72 | 73 | it('should evaluate a new child no matter what', 74 | inject(function($rootScope, $compile) { 75 | scope = $rootScope; 76 | scope.dataObject = { 77 | value: 5, 78 | }; 79 | scope.x = 12; 80 | 81 | page = angular.element('
'); 82 | 83 | $compile(page)(scope); 84 | scope.$digest(); 85 | 86 | // We simulate adding a new child to the div by just creating a new element 87 | // and compiling it in. This is a bit hacky and hopefully won't break. 88 | divScope = page.scope(); 89 | span = angular.element('{{x}}'); 90 | $compile(span)(divScope); 91 | 92 | scope.$digest(); 93 | 94 | expect(span.eq(0).text()).toEqual('12'); 95 | })); 96 | }); 97 | 98 | describe('slyEvaluate.slyAlwaysEvaluate', function() { 99 | // Elements from the test page. 100 | var scope = null; 101 | var page = null; 102 | var spanX = null; 103 | var spanY = null; 104 | 105 | // Require the slyEvaluate module. 106 | beforeEach(module('slyEvaluate')); 107 | 108 | // Create the sample DOM elements slyAlwaysEvaluate. Specifically, 109 | // gate all changes on dataObject, but put in an exception for 'x'. 110 | // Create spans containing X and Y. 111 | beforeEach(inject(function($rootScope, $compile) { 112 | scope = $rootScope; 113 | scope.dataObject = { 114 | value: 5, 115 | }; 116 | scope.x = 10; 117 | scope.y = 'a'; 118 | 119 | page = angular.element( 120 | '
' + 121 | '' + 122 | '' + 123 | '
'); 124 | 125 | $compile(page)(scope); 126 | scope.$digest(); 127 | 128 | // Set the DOM references. 129 | var span = page.find('span'); 130 | spanX = span.eq(0); 131 | spanY = span.eq(1); 132 | })); 133 | 134 | it('should initialize bindings correctly', function() { 135 | // Just check basic functionality. 136 | expect(spanX.text()).toEqual('10'); 137 | expect(spanY.text()).toEqual('a'); 138 | }); 139 | 140 | it('should always evaluate expressions containing the exception string', function() { 141 | // If we change both x and y, only x's change should show up after a digest. 142 | scope.x = 15; 143 | scope.y = 'b'; 144 | scope.$digest(); 145 | 146 | expect(spanX.text()).toEqual('15'); 147 | expect(spanY.text()).toEqual('a'); 148 | }); 149 | 150 | it('should always evaluate all expressions when slyEvaluateOnlyWhen object changes', function() { 151 | // If we change both x and y and the dataObject 152 | scope.x = 15; 153 | scope.y = 'b'; 154 | scope.dataObject = { 155 | value: 10, 156 | }; 157 | scope.$digest(); 158 | 159 | expect(spanX.text()).toEqual('15'); 160 | expect(spanY.text()).toEqual('b'); 161 | }); 162 | }); 163 | 164 | describe('slyEvaluate.slyShow', function() { 165 | // Elements from the test page. 166 | var scope = null; 167 | var page = null; 168 | var span = null; 169 | 170 | // Require the slyEvaluate module. 171 | beforeEach(module('slyEvaluate')); 172 | 173 | beforeEach(inject(function($rootScope, $compile) { 174 | scope = $rootScope; 175 | scope.isHidden = false; 176 | scope.x = 1; 177 | 178 | page = angular.element( 179 | '
' + 180 | 'Hi' + 181 | '
'); 182 | 183 | $compile(page)(scope); 184 | scope.$digest(); 185 | 186 | // Set the DOM references. 187 | span = page.find('span').eq(0); 188 | })); 189 | 190 | it('should initialize showing state correctly', function() { 191 | // Just check basic functionality. 192 | expect(span.hasClass('ng-hide')).toBeFalsy(); 193 | }); 194 | 195 | it('should hide element when expression becomes false', function() { 196 | scope.isHidden = true; 197 | scope.$digest(); 198 | expect(span.hasClass('ng-hide')).toBeTruthy(); 199 | }); 200 | 201 | it('should show element when expression becomes true', function() { 202 | scope.isHidden = true; 203 | scope.$digest(); 204 | 205 | scope.isHidden = false; 206 | scope.$digest(); 207 | expect(span.hasClass('ng-hide')).toBeFalsy(); 208 | }); 209 | }); 210 | 211 | describe('slyEvaluate.slyPreventEvaluationWhenHidden', function() { 212 | // Elements from the test page. 213 | var scope = null; 214 | var page = null; 215 | var spanX = null; 216 | 217 | // Require the slyEvaluate module. 218 | beforeEach(module('slyEvaluate')); 219 | 220 | // Create the sample DOM elements slyAlwaysEvaluate. Specifically, 221 | // gate all changes on dataObject, but put in an exception for 'x'. 222 | // Create spans containing X and Y. 223 | beforeEach(inject(function($rootScope, $compile) { 224 | scope = $rootScope; 225 | scope.isHidden = false; 226 | scope.x = 1; 227 | 228 | page = angular.element( 229 | '
' + 230 | '{{x}}' + 231 | '
'); 232 | 233 | $compile(page)(scope); 234 | scope.$digest(); 235 | 236 | // Set the DOM references. 237 | spanX = page.find('span').eq(0); 238 | })); 239 | 240 | it('should initialize bindings correctly', function() { 241 | // Just check basic functionality. 242 | expect(spanX.text()).toEqual('1'); 243 | }); 244 | 245 | it('should not update bindings when hidden', function() { 246 | scope.isHidden = true; 247 | scope.$digest(); 248 | 249 | // Change x to some new value. It should not be updated in the page. 250 | scope.x = 10; 251 | scope.$digest(); 252 | 253 | expect(spanX.text()).toEqual('1'); 254 | }); 255 | 256 | it('should update bindings when shown', function() { 257 | scope.isHidden = true; 258 | scope.$digest(); 259 | 260 | scope.x = 10; 261 | scope.$digest(); 262 | 263 | scope.isHidden = false; 264 | scope.$digest(); 265 | expect(spanX.text()).toEqual('10'); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /src/tests/directives/slyRepeatTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Test cases for the slyRepeat module. 4 | * 5 | * @author Steven Czerwinski 6 | */ 7 | describe('slyRepeat.slyRepeat', function() { 8 | // Items will hold references to the DOM elements 9 | // li from the sample HTML page. 10 | var items = null, 11 | scope = null, 12 | page = null; 13 | 14 | // Require the slyRepeat module. 15 | beforeEach(module('slyRepeat')); 16 | 17 | // Create the sample DOM elements with a repeat directive in them. 18 | beforeEach(inject(function($rootScope, $compile) { 19 | scope = $rootScope; 20 | scope.xValues = [ 5, 6, 7]; 21 | 22 | // Create a list made of values of xValues, where the first word is the 23 | // value from the array, and then the three boolean fields $first, $middle, $last 24 | // which are automatically created for each element scope. 25 | page = angular.element( 26 | '
' + 27 | '
    ' + 28 | '
  • {{y}} first: {{$first}} middle: {{$middle}} last: {{$last}}
  • ' + 29 | '
' + 30 | '
'); 31 | 32 | $compile(page)(scope); 33 | scope.$digest(); 34 | 35 | // Set the DOM references. 36 | items = page.find('li'); 37 | })); 38 | 39 | it('should iterate over items', function() { 40 | // Just check basic functionality. 41 | expect(items.length).toBe(3); 42 | expect(items.eq(0).text()).toBe('5 first: true middle: false last: false'); 43 | expect(items.eq(1).text()).toBe('6 first: false middle: true last: false'); 44 | expect(items.eq(2).text()).toBe('7 first: false middle: false last: true'); 45 | }); 46 | 47 | it('should increase to new collection size', function() { 48 | // If the array changes, so should the elements. 49 | scope.xValues = [ 5, 6, 7, 8 ]; 50 | scope.$digest(); 51 | items = page.find('li'); 52 | expect(items.length).toBe(4); 53 | expect(items.eq(0).text()).toBe('5 first: true middle: false last: false'); 54 | expect(items.eq(1).text()).toBe('6 first: false middle: true last: false'); 55 | expect(items.eq(2).text()).toBe('7 first: false middle: true last: false'); 56 | expect(items.eq(3).text()).toBe('8 first: false middle: false last: true'); 57 | }); 58 | 59 | it('should hide and not re-eval unused elements when collection size decreases', function() { 60 | // Make sure if we shorten the list, unused DOM elements are still there but 61 | // their values have not been updated and they are hidden. 62 | scope.xValues = [ 3, 4 ]; 63 | scope.$digest(); 64 | items = page.find('li'); 65 | expect(items.length).toBe(3); 66 | expect(items.eq(0).text()).toBe('3 first: true middle: false last: false'); 67 | expect(items.eq(1).text()).toBe('4 first: false middle: false last: true'); 68 | expect(items.eq(2).text()).toBe('7 first: false middle: false last: true'); 69 | expect(items.eq(2).css('display')).toBe('none'); 70 | }); 71 | 72 | it('should unhide previously unused elements when collection shrinks then grows', function() { 73 | scope.xValues = [ 3, 4 ]; 74 | scope.$digest(); 75 | scope.xValues = [ 8, 9, 10, 11, 12]; 76 | scope.$digest(); 77 | 78 | items = page.find('li'); 79 | expect(items.length).toBe(5); 80 | expect(items.eq(0).text()).toBe('8 first: true middle: false last: false'); 81 | expect(items.eq(1).text()).toBe('9 first: false middle: true last: false'); 82 | expect(items.eq(2).text()).toBe('10 first: false middle: true last: false'); 83 | expect(items.eq(3).text()).toBe('11 first: false middle: true last: false'); 84 | expect(items.eq(4).text()).toBe('12 first: false middle: false last: true'); 85 | 86 | for (var i = 0; i < 5; ++i) 87 | expect(items.eq(i).css('display')).toBe(''); 88 | }); 89 | 90 | it('should handle shrinking to zero size and then growing back', function() { 91 | scope.xValues = []; 92 | scope.$digest(); 93 | scope.xValues = [ 8, 9, 10, 11, 12]; 94 | scope.$digest(); 95 | 96 | items = page.find('li'); 97 | expect(items.length).toBe(5); 98 | expect(items.eq(0).text()).toBe('8 first: true middle: false last: false'); 99 | expect(items.eq(1).text()).toBe('9 first: false middle: true last: false'); 100 | expect(items.eq(2).text()).toBe('10 first: false middle: true last: false'); 101 | expect(items.eq(3).text()).toBe('11 first: false middle: true last: false'); 102 | expect(items.eq(4).text()).toBe('12 first: false middle: false last: true'); 103 | 104 | for (var i = 0; i < 5; ++i) 105 | expect(items.eq(i).css('display')).toBe(''); 106 | }); 107 | 108 | it('should not evaluate unused elements that have derived scopes', inject(function($compile) { 109 | // We need to verify that our overriden scope creates child scopes that 110 | // still correctly intercept the watch function. To do this, we use a 111 | // controller since controllers cause the outer scope to create a child scope. 112 | scope.values = [ 1, 2, 3, 4]; 113 | 114 | scope.FooBar = function FooBar($scope) { 115 | }; 116 | 117 | page = angular.element( 118 | '
' + 119 | '
' + 120 | '{{x}} ' + 121 | '
' + 122 | '
'); 123 | 124 | $compile(page)(scope); 125 | scope.$digest(); 126 | 127 | // Normal operation. 128 | items = page.find('div'); 129 | expect(items.length).toBe(4); 130 | for (var i = 0; i < 4; ++i) { 131 | expect(items.eq(i).text()).toBe((i + 1) + ' '); 132 | expect(items.eq(i).css('display')).toBe(''); 133 | } 134 | 135 | // Now shrink it. We shouldn't see any errors due to badly evaluated 136 | // elements. 137 | scope.values = [ 5, 6]; 138 | scope.$digest(); 139 | 140 | items = page.find('div'); 141 | expect(items.length).toBe(4); 142 | for (var i = 0; i < 2; ++i) { 143 | expect(items.eq(i).text()).toBe((i + 5) + ' '); 144 | expect(items.eq(i).css('display')).toBe(''); 145 | } 146 | 147 | // Make sure the unused elements have not changed from their 148 | // previous values and are hidden. 149 | for (var i = 2; i < 4; ++i) { 150 | expect(items.eq(i).text()).toBe((i + 1) + ' '); 151 | expect(items.eq(i).css('display')).toBe('none'); 152 | } 153 | })); 154 | }); 155 | -------------------------------------------------------------------------------- /src/tests/lib/allTests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests for js/lib directory 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/tests/lib/gatedScopeTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Tests for the GatedScope abstraction. 4 | * 5 | * @author Steven Czerwinski 6 | */ 7 | describe('GatedScope', function() { 8 | var $rootScope = null; 9 | 10 | beforeEach(module('gatedScope')); 11 | beforeEach(inject(['$rootScope', function(rootScope) { 12 | $rootScope = rootScope; 13 | }])); 14 | 15 | it('should provide rootScope with a $addWatcherGate method', function() { 16 | expect($rootScope.$addWatcherGate).toBeDefined(); 17 | }); 18 | 19 | it('should add a $addWatcherGate method for all new scopes', function() { 20 | expect($rootScope.$new().$addWatcherGate).toBeDefined(); 21 | }); 22 | 23 | it('should add a $addWatcherGate method for all new scopes even if isolated', function() { 24 | expect($rootScope.$new(true).$addWatcherGate).toBeDefined(); 25 | }); 26 | 27 | it('should evaluate all watchers on $digest when no gating', function() { 28 | // Just register a bunch of watchers on a set of scopes and make sure they are 29 | // evaluated. 30 | var childA = $rootScope.$new(); 31 | var childB = $rootScope.$new(); 32 | var grandChild = childA.$new(); 33 | 34 | var counter = 0; 35 | function watcher() { 36 | ++counter; 37 | return 1; 38 | } 39 | 40 | childA.$watch(watcher); 41 | childB.$watch(watcher); 42 | grandChild.$watch(watcher); 43 | 44 | $rootScope.$digest(); 45 | 46 | // Each watcher should be called twice since the first pass of the $digest loop will 47 | // result in each watcher indicating it was dirty. 48 | expect(counter).toEqual(6); 49 | }); 50 | 51 | it('should not evaluate gated watchers when gating function returns false', function() { 52 | // Create a few scopes, set a gate function on one of them, and make sure it 53 | // prevents any watchers on it and its descendents from being evaluated. 54 | var childA = $rootScope.$new(); 55 | childA.$addWatcherGate(function() { 56 | return false; 57 | }); 58 | 59 | var childB = $rootScope.$new(); 60 | var grandChild = childA.$new(); 61 | 62 | var counter = 0; 63 | function watcher() { 64 | ++counter; 65 | return 1; 66 | } 67 | 68 | childA.$watch(watcher); 69 | childB.$watch(watcher); 70 | grandChild.$watch(watcher); 71 | 72 | $rootScope.$digest(); 73 | 74 | // Only the watcher on B should fire, so we get two counts. 75 | expect(counter).toEqual(2); 76 | }); 77 | 78 | it('should evaluate gated watchers when gating function returns true', function() { 79 | var gateClosed = true; 80 | 81 | var childA = $rootScope.$new(); 82 | childA.$addWatcherGate(function() { 83 | return !gateClosed; 84 | }); 85 | 86 | var childB = $rootScope.$new(); 87 | var grandChild = childA.$new(); 88 | 89 | var counter = 0; 90 | var watchedVal = 1; 91 | function watcher() { 92 | ++counter; 93 | return watchedVal; 94 | } 95 | 96 | childA.$watch(watcher); 97 | childB.$watch(watcher); 98 | grandChild.$watch(watcher); 99 | 100 | $rootScope.$digest(); 101 | 102 | // Only the watcher on B should fire when gate closed. 103 | expect(counter).toEqual(2); 104 | 105 | counter = 0; 106 | gateClosed = false; 107 | 108 | $rootScope.$digest(); 109 | // All three watchers will be evaluated twice. 110 | expect(counter).toEqual(6); 111 | 112 | // Next time through, as long as watchedVal doesn't change, 113 | // we should only evaluate each watcher once. 114 | counter = 0; 115 | $rootScope.$digest(); 116 | expect(counter).toEqual(3); 117 | 118 | // When the watched value is dirty, then we have to eval each twice again. 119 | watchedVal = 2; 120 | counter = 0; 121 | $rootScope.$digest(); 122 | expect(counter).toEqual(6); 123 | }); 124 | 125 | it('should only evaluate gated watches on scope whose digest was invoked', function() { 126 | var gateClosed = false; 127 | 128 | var childA = $rootScope.$new(); 129 | childA.$addWatcherGate(function() { 130 | return !gateClosed; 131 | }); 132 | 133 | var childB = childA.$new(); 134 | 135 | var counter = 0; 136 | var watchedVal = 1; 137 | function watcher() { 138 | ++counter; 139 | return watchedVal; 140 | } 141 | 142 | childA.$watch(watcher); 143 | childB.$watch(watcher); 144 | 145 | childB.$digest(); 146 | 147 | // Only the watcher on B should fire since we invoked its digest. 148 | expect(counter).toEqual(2); 149 | 150 | counter = 0; 151 | watchedValue = 2; 152 | 153 | childA.$digest(); 154 | // Both watches should evaluate twice (twice since the first time 155 | // will indicate it is is dirty). 156 | expect(counter).toEqual(4); 157 | }); 158 | 159 | it('should only evaluate gated watchers for gating function that returned true', function() { 160 | var counter1 = 0; 161 | function watcher1() { 162 | ++counter1; 163 | return 1; 164 | } 165 | 166 | var counter2 = 0; 167 | function watcher2() { 168 | ++counter2; 169 | return 1; 170 | } 171 | 172 | // Create two different gate functions, one which returns true, the other that returns 173 | // false. 174 | 175 | var childA = $rootScope.$new(); 176 | childA.$addWatcherGate(function() { 177 | return true; 178 | }); 179 | 180 | childA.$watch(watcher1); 181 | childA.$watch(watcher1); 182 | 183 | var grandChild = childA.$new(); 184 | grandChild.$watch(watcher1); 185 | 186 | grandChild.$addWatcherGate(function() { 187 | return false; 188 | }); 189 | grandChild.$watch(watcher2); 190 | 191 | $rootScope.$digest(); 192 | 193 | // The child watcher should fire six times, while the other zero. 194 | expect(counter1).toEqual(6); 195 | expect(counter2).toEqual(0); 196 | }); 197 | 198 | it('should only add a watcher to the gated list if shouldGateFunction returns true', function() { 199 | var counter = 0; 200 | function watcher() { 201 | ++counter; 202 | return 1; 203 | } 204 | 205 | function myListener(newVal, oldVal) { 206 | } 207 | var shouldGateInvoked = false; 208 | 209 | var childA = $rootScope.$new(); 210 | childA.$addWatcherGate(function() { 211 | return false; 212 | }, function(watchExp, listener, equality, name) { 213 | // We only add a watcher once, so just check it's arguments. 214 | // Probably should be using sinon mocks here. 215 | expect(watchExp).toBe(watcher); 216 | expect(listener).toBe(myListener); 217 | expect(equality).toBeTruthy(); 218 | expect(name).toEqual('testDirective'); 219 | shouldGateInvoked = true; 220 | return false; 221 | }); 222 | 223 | childA.$watch(watcher, myListener, true, 'testDirective'); 224 | 225 | // The watcher should be fired even though gate is returning false since. 226 | $rootScope.$digest(); 227 | expect(counter).toEqual(2); 228 | expect(shouldGateInvoked).toBeTruthy(); 229 | }); 230 | 231 | 232 | it('should keep trying to evaluate nested gate if outer gate ever becomes true', function() { 233 | var counter = 0; 234 | function watcher() { 235 | ++counter; 236 | return 1; 237 | } 238 | 239 | // Create a nested gating function by registering two gating functions, one after another. 240 | // If the first one's gating function ever returns true, then the second one should 241 | // be attempted to be evaluated from then on until its gating function always returns true, 242 | // even if the first one returns false in the meantime. 243 | 244 | var firstGate = false; 245 | var secondGate = false; 246 | 247 | var childA = $rootScope.$new(); 248 | childA.$addWatcherGate(function() { 249 | return firstGate; 250 | }); 251 | 252 | childA.$addWatcherGate(function() { 253 | return secondGate; 254 | }); 255 | 256 | childA.$watch(watcher); 257 | 258 | // When both gates are down, the watcher should not be evaluated. 259 | $rootScope.$digest(); 260 | expect(counter).toEqual(0); 261 | 262 | // When the first gate goes up but the second is still down, then no evaluation. 263 | firstGate = true; 264 | $rootScope.$digest(); 265 | expect(counter).toEqual(0); 266 | 267 | // Lower the first gate. 268 | firstGate = false; 269 | $rootScope.$digest(); 270 | expect(counter).toEqual(0); 271 | 272 | // Now if the second gate comes up, then we should see an evaluation, even though 273 | // the first gate is now closed. 274 | secondGate = true; 275 | $rootScope.$digest(); 276 | expect(counter).toEqual(1); 277 | 278 | // But, we will not see any more evaluations until the first gate is opened again. 279 | $rootScope.$digest(); 280 | expect(counter).toEqual(1); 281 | 282 | // When the first gate is open again, we will see more evaluations. Because the first 283 | // gate will return a signal to indicate it was dirty, we will see two evaluations 284 | // this time. 285 | firstGate = true; 286 | $rootScope.$digest(); 287 | expect(counter).toEqual(3); 288 | 289 | // In steady state with both gates up, we will only see one evaluation per digest loop. 290 | $rootScope.$digest(); 291 | expect(counter).toEqual(4); 292 | }); 293 | 294 | it('should evaluate watchers registered with equality equal to true', function() { 295 | var objectToWatch = { value: 5 }; 296 | 297 | var counter = 0; 298 | function watcher() { 299 | ++counter; 300 | return objectToWatch; 301 | } 302 | 303 | // Add a gate so that all future watchers are evaluated using our version of $digest. 304 | var scope = $rootScope.$new(); 305 | scope.$addWatcherGate(function() { 306 | return true; 307 | }); 308 | 309 | scope.$watch(watcher, function(newValue) { 310 | }, true); 311 | 312 | // When both gates are down, the watcher should not be evaluated. 313 | $rootScope.$digest(); 314 | }); 315 | 316 | it('should evaluate new watchers when gating function has shouldEvalNewWatchers = true', function() { 317 | var gateClosed = false; 318 | 319 | var child = $rootScope.$new(); 320 | child.$addWatcherGate(function() { 321 | return !gateClosed; 322 | }, null, true); 323 | 324 | $rootScope.$digest(); 325 | 326 | gateClosed = true; 327 | 328 | var counter = 0; 329 | var watchedVal = 1; 330 | function watcher() { 331 | ++counter; 332 | return watchedVal; 333 | } 334 | 335 | child.$watch(watcher); 336 | $rootScope.$digest(); 337 | 338 | // Should have been evaluated twice, one for the first dirty cycle, and then for 339 | // cycle it was not dirty on. 340 | expect(counter).toEqual(2); 341 | }); 342 | 343 | it('should not evaluate new watchers when gating function has shouldEvalNewWatchers = false', function() { 344 | var gateClosed = false; 345 | 346 | var child = $rootScope.$new(); 347 | child.$addWatcherGate(function() { 348 | return !gateClosed; 349 | }, null, false); 350 | 351 | $rootScope.$digest(); 352 | 353 | gateClosed = true; 354 | var counter = 0; 355 | var watchedVal = 1; 356 | function watcher() { 357 | ++counter; 358 | return watchedVal; 359 | } 360 | 361 | child.$watch(watcher); 362 | $rootScope.$digest(); 363 | 364 | expect(counter).toEqual(0); 365 | }); 366 | }); 367 | -------------------------------------------------------------------------------- /src/tests/scalyrUnitTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * The core testing abstractions used by Scalyr. 4 | * Currently, we use Jasmine for our tests. This 5 | * file only contains the additional testing support 6 | * code we require. 7 | */ 8 | 9 | /** 10 | * Run all unit tests defined in scope. These unit 11 | * tests are created using Jasmine's 12 | * 'describe' and 'it' methods. This method is meant 13 | * to be invoked in the head of the HTML page created to 14 | * a suite of tests, after all those tests are defined. 15 | */ 16 | function runUnitTests() { 17 | var jasmineEnv = jasmine.getEnv(); 18 | jasmineEnv.updateInterval = 1000; 19 | 20 | var htmlReporter = new jasmine.HtmlReporter(); 21 | 22 | jasmineEnv.addReporter(htmlReporter); 23 | 24 | jasmineEnv.specFilter = function(spec) { 25 | return htmlReporter.specFilter(spec); 26 | }; 27 | 28 | var currentWindowOnload = window.onload; 29 | 30 | window.onload = function() { 31 | if (currentWindowOnload) { 32 | currentWindowOnload(); 33 | } 34 | execJasmine(); 35 | }; 36 | 37 | function execJasmine() { 38 | jasmineEnv.execute(); 39 | } 40 | } 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/tests/thirdparty/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 | -------------------------------------------------------------------------------- /src/tests/thirdparty/jasmine-1.3.1/jasmine-html.js: -------------------------------------------------------------------------------- 1 | jasmine.HtmlReporterHelpers = {}; 2 | 3 | jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { 4 | var el = document.createElement(type); 5 | 6 | for (var i = 2; i < arguments.length; i++) { 7 | var child = arguments[i]; 8 | 9 | if (typeof child === 'string') { 10 | el.appendChild(document.createTextNode(child)); 11 | } else { 12 | if (child) { 13 | el.appendChild(child); 14 | } 15 | } 16 | } 17 | 18 | for (var attr in attrs) { 19 | if (attr == "className") { 20 | el[attr] = attrs[attr]; 21 | } else { 22 | el.setAttribute(attr, attrs[attr]); 23 | } 24 | } 25 | 26 | return el; 27 | }; 28 | 29 | jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { 30 | var results = child.results(); 31 | var status = results.passed() ? 'passed' : 'failed'; 32 | if (results.skipped) { 33 | status = 'skipped'; 34 | } 35 | 36 | return status; 37 | }; 38 | 39 | jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { 40 | var parentDiv = this.dom.summary; 41 | var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; 42 | var parent = child[parentSuite]; 43 | 44 | if (parent) { 45 | if (typeof this.views.suites[parent.id] == 'undefined') { 46 | this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); 47 | } 48 | parentDiv = this.views.suites[parent.id].element; 49 | } 50 | 51 | parentDiv.appendChild(childElement); 52 | }; 53 | 54 | 55 | jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { 56 | for(var fn in jasmine.HtmlReporterHelpers) { 57 | ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; 58 | } 59 | }; 60 | 61 | jasmine.HtmlReporter = function(_doc) { 62 | var self = this; 63 | var doc = _doc || window.document; 64 | 65 | var reporterView; 66 | 67 | var dom = {}; 68 | 69 | // Jasmine Reporter Public Interface 70 | self.logRunningSpecs = false; 71 | 72 | self.reportRunnerStarting = function(runner) { 73 | var specs = runner.specs() || []; 74 | 75 | if (specs.length == 0) { 76 | return; 77 | } 78 | 79 | createReporterDom(runner.env.versionString()); 80 | doc.body.appendChild(dom.reporter); 81 | setExceptionHandling(); 82 | 83 | reporterView = new jasmine.HtmlReporter.ReporterView(dom); 84 | reporterView.addSpecs(specs, self.specFilter); 85 | }; 86 | 87 | self.reportRunnerResults = function(runner) { 88 | reporterView && reporterView.complete(); 89 | }; 90 | 91 | self.reportSuiteResults = function(suite) { 92 | reporterView.suiteComplete(suite); 93 | }; 94 | 95 | self.reportSpecStarting = function(spec) { 96 | if (self.logRunningSpecs) { 97 | self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 98 | } 99 | }; 100 | 101 | self.reportSpecResults = function(spec) { 102 | reporterView.specComplete(spec); 103 | }; 104 | 105 | self.log = function() { 106 | var console = jasmine.getGlobal().console; 107 | if (console && console.log) { 108 | if (console.log.apply) { 109 | console.log.apply(console, arguments); 110 | } else { 111 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 112 | } 113 | } 114 | }; 115 | 116 | self.specFilter = function(spec) { 117 | if (!focusedSpecName()) { 118 | return true; 119 | } 120 | 121 | return spec.getFullName().indexOf(focusedSpecName()) === 0; 122 | }; 123 | 124 | return self; 125 | 126 | function focusedSpecName() { 127 | var specName; 128 | 129 | (function memoizeFocusedSpec() { 130 | if (specName) { 131 | return; 132 | } 133 | 134 | var paramMap = []; 135 | var params = jasmine.HtmlReporter.parameters(doc); 136 | 137 | for (var i = 0; i < params.length; i++) { 138 | var p = params[i].split('='); 139 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 140 | } 141 | 142 | specName = paramMap.spec; 143 | })(); 144 | 145 | return specName; 146 | } 147 | 148 | function createReporterDom(version) { 149 | dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, 150 | dom.banner = self.createDom('div', { className: 'banner' }, 151 | self.createDom('span', { className: 'title' }, "Jasmine "), 152 | self.createDom('span', { className: 'version' }, version)), 153 | 154 | dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), 155 | dom.alert = self.createDom('div', {className: 'alert'}, 156 | self.createDom('span', { className: 'exceptions' }, 157 | self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'), 158 | self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))), 159 | dom.results = self.createDom('div', {className: 'results'}, 160 | dom.summary = self.createDom('div', { className: 'summary' }), 161 | dom.details = self.createDom('div', { id: 'details' })) 162 | ); 163 | } 164 | 165 | function noTryCatch() { 166 | return window.location.search.match(/catch=false/); 167 | } 168 | 169 | function searchWithCatch() { 170 | var params = jasmine.HtmlReporter.parameters(window.document); 171 | var removed = false; 172 | var i = 0; 173 | 174 | while (!removed && i < params.length) { 175 | if (params[i].match(/catch=/)) { 176 | params.splice(i, 1); 177 | removed = true; 178 | } 179 | i++; 180 | } 181 | if (jasmine.CATCH_EXCEPTIONS) { 182 | params.push("catch=false"); 183 | } 184 | 185 | return params.join("&"); 186 | } 187 | 188 | function setExceptionHandling() { 189 | var chxCatch = document.getElementById('no_try_catch'); 190 | 191 | if (noTryCatch()) { 192 | chxCatch.setAttribute('checked', true); 193 | jasmine.CATCH_EXCEPTIONS = false; 194 | } 195 | chxCatch.onclick = function() { 196 | window.location.search = searchWithCatch(); 197 | }; 198 | } 199 | }; 200 | jasmine.HtmlReporter.parameters = function(doc) { 201 | var paramStr = doc.location.search.substring(1); 202 | var params = []; 203 | 204 | if (paramStr.length > 0) { 205 | params = paramStr.split('&'); 206 | } 207 | return params; 208 | } 209 | jasmine.HtmlReporter.sectionLink = function(sectionName) { 210 | var link = '?'; 211 | var params = []; 212 | 213 | if (sectionName) { 214 | params.push('spec=' + encodeURIComponent(sectionName)); 215 | } 216 | if (!jasmine.CATCH_EXCEPTIONS) { 217 | params.push("catch=false"); 218 | } 219 | if (params.length > 0) { 220 | link += params.join("&"); 221 | } 222 | 223 | return link; 224 | }; 225 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter); 226 | jasmine.HtmlReporter.ReporterView = function(dom) { 227 | this.startedAt = new Date(); 228 | this.runningSpecCount = 0; 229 | this.completeSpecCount = 0; 230 | this.passedCount = 0; 231 | this.failedCount = 0; 232 | this.skippedCount = 0; 233 | 234 | this.createResultsMenu = function() { 235 | this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, 236 | this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'), 237 | ' | ', 238 | this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing')); 239 | 240 | this.summaryMenuItem.onclick = function() { 241 | dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, ''); 242 | }; 243 | 244 | this.detailsMenuItem.onclick = function() { 245 | showDetails(); 246 | }; 247 | }; 248 | 249 | this.addSpecs = function(specs, specFilter) { 250 | this.totalSpecCount = specs.length; 251 | 252 | this.views = { 253 | specs: {}, 254 | suites: {} 255 | }; 256 | 257 | for (var i = 0; i < specs.length; i++) { 258 | var spec = specs[i]; 259 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views); 260 | if (specFilter(spec)) { 261 | this.runningSpecCount++; 262 | } 263 | } 264 | }; 265 | 266 | this.specComplete = function(spec) { 267 | this.completeSpecCount++; 268 | 269 | if (isUndefined(this.views.specs[spec.id])) { 270 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom); 271 | } 272 | 273 | var specView = this.views.specs[spec.id]; 274 | 275 | switch (specView.status()) { 276 | case 'passed': 277 | this.passedCount++; 278 | break; 279 | 280 | case 'failed': 281 | this.failedCount++; 282 | break; 283 | 284 | case 'skipped': 285 | this.skippedCount++; 286 | break; 287 | } 288 | 289 | specView.refresh(); 290 | this.refresh(); 291 | }; 292 | 293 | this.suiteComplete = function(suite) { 294 | var suiteView = this.views.suites[suite.id]; 295 | if (isUndefined(suiteView)) { 296 | return; 297 | } 298 | suiteView.refresh(); 299 | }; 300 | 301 | this.refresh = function() { 302 | 303 | if (isUndefined(this.resultsMenu)) { 304 | this.createResultsMenu(); 305 | } 306 | 307 | // currently running UI 308 | if (isUndefined(this.runningAlert)) { 309 | this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" }); 310 | dom.alert.appendChild(this.runningAlert); 311 | } 312 | this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); 313 | 314 | // skipped specs UI 315 | if (isUndefined(this.skippedAlert)) { 316 | this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" }); 317 | } 318 | 319 | this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 320 | 321 | if (this.skippedCount === 1 && isDefined(dom.alert)) { 322 | dom.alert.appendChild(this.skippedAlert); 323 | } 324 | 325 | // passing specs UI 326 | if (isUndefined(this.passedAlert)) { 327 | this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" }); 328 | } 329 | this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); 330 | 331 | // failing specs UI 332 | if (isUndefined(this.failedAlert)) { 333 | this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"}); 334 | } 335 | this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount); 336 | 337 | if (this.failedCount === 1 && isDefined(dom.alert)) { 338 | dom.alert.appendChild(this.failedAlert); 339 | dom.alert.appendChild(this.resultsMenu); 340 | } 341 | 342 | // summary info 343 | this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); 344 | this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; 345 | }; 346 | 347 | this.complete = function() { 348 | dom.alert.removeChild(this.runningAlert); 349 | 350 | this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 351 | 352 | if (this.failedCount === 0) { 353 | dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount))); 354 | } else { 355 | showDetails(); 356 | } 357 | 358 | dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); 359 | }; 360 | 361 | return this; 362 | 363 | function showDetails() { 364 | if (dom.reporter.className.search(/showDetails/) === -1) { 365 | dom.reporter.className += " showDetails"; 366 | } 367 | } 368 | 369 | function isUndefined(obj) { 370 | return typeof obj === 'undefined'; 371 | } 372 | 373 | function isDefined(obj) { 374 | return !isUndefined(obj); 375 | } 376 | 377 | function specPluralizedFor(count) { 378 | var str = count + " spec"; 379 | if (count > 1) { 380 | str += "s" 381 | } 382 | return str; 383 | } 384 | 385 | }; 386 | 387 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView); 388 | 389 | 390 | jasmine.HtmlReporter.SpecView = function(spec, dom, views) { 391 | this.spec = spec; 392 | this.dom = dom; 393 | this.views = views; 394 | 395 | this.symbol = this.createDom('li', { className: 'pending' }); 396 | this.dom.symbolSummary.appendChild(this.symbol); 397 | 398 | this.summary = this.createDom('div', { className: 'specSummary' }, 399 | this.createDom('a', { 400 | className: 'description', 401 | href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()), 402 | title: this.spec.getFullName() 403 | }, this.spec.description) 404 | ); 405 | 406 | this.detail = this.createDom('div', { className: 'specDetail' }, 407 | this.createDom('a', { 408 | className: 'description', 409 | href: '?spec=' + encodeURIComponent(this.spec.getFullName()), 410 | title: this.spec.getFullName() 411 | }, this.spec.getFullName()) 412 | ); 413 | }; 414 | 415 | jasmine.HtmlReporter.SpecView.prototype.status = function() { 416 | return this.getSpecStatus(this.spec); 417 | }; 418 | 419 | jasmine.HtmlReporter.SpecView.prototype.refresh = function() { 420 | this.symbol.className = this.status(); 421 | 422 | switch (this.status()) { 423 | case 'skipped': 424 | break; 425 | 426 | case 'passed': 427 | this.appendSummaryToSuiteDiv(); 428 | break; 429 | 430 | case 'failed': 431 | this.appendSummaryToSuiteDiv(); 432 | this.appendFailureDetail(); 433 | break; 434 | } 435 | }; 436 | 437 | jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { 438 | this.summary.className += ' ' + this.status(); 439 | this.appendToSummary(this.spec, this.summary); 440 | }; 441 | 442 | jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() { 443 | this.detail.className += ' ' + this.status(); 444 | 445 | var resultItems = this.spec.results().getItems(); 446 | var messagesDiv = this.createDom('div', { className: 'messages' }); 447 | 448 | for (var i = 0; i < resultItems.length; i++) { 449 | var result = resultItems[i]; 450 | 451 | if (result.type == 'log') { 452 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 453 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 454 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 455 | 456 | if (result.trace.stack) { 457 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 458 | } 459 | } 460 | } 461 | 462 | if (messagesDiv.childNodes.length > 0) { 463 | this.detail.appendChild(messagesDiv); 464 | this.dom.details.appendChild(this.detail); 465 | } 466 | }; 467 | 468 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) { 469 | this.suite = suite; 470 | this.dom = dom; 471 | this.views = views; 472 | 473 | this.element = this.createDom('div', { className: 'suite' }, 474 | this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) 475 | ); 476 | 477 | this.appendToSummary(this.suite, this.element); 478 | }; 479 | 480 | jasmine.HtmlReporter.SuiteView.prototype.status = function() { 481 | return this.getSpecStatus(this.suite); 482 | }; 483 | 484 | jasmine.HtmlReporter.SuiteView.prototype.refresh = function() { 485 | this.element.className += " " + this.status(); 486 | }; 487 | 488 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView); 489 | 490 | /* @deprecated Use jasmine.HtmlReporter instead 491 | */ 492 | jasmine.TrivialReporter = function(doc) { 493 | this.document = doc || document; 494 | this.suiteDivs = {}; 495 | this.logRunningSpecs = false; 496 | }; 497 | 498 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { 499 | var el = document.createElement(type); 500 | 501 | for (var i = 2; i < arguments.length; i++) { 502 | var child = arguments[i]; 503 | 504 | if (typeof child === 'string') { 505 | el.appendChild(document.createTextNode(child)); 506 | } else { 507 | if (child) { el.appendChild(child); } 508 | } 509 | } 510 | 511 | for (var attr in attrs) { 512 | if (attr == "className") { 513 | el[attr] = attrs[attr]; 514 | } else { 515 | el.setAttribute(attr, attrs[attr]); 516 | } 517 | } 518 | 519 | return el; 520 | }; 521 | 522 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { 523 | var showPassed, showSkipped; 524 | 525 | this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' }, 526 | this.createDom('div', { className: 'banner' }, 527 | this.createDom('div', { className: 'logo' }, 528 | this.createDom('span', { className: 'title' }, "Jasmine"), 529 | this.createDom('span', { className: 'version' }, runner.env.versionString())), 530 | this.createDom('div', { className: 'options' }, 531 | "Show ", 532 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), 533 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), 534 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), 535 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") 536 | ) 537 | ), 538 | 539 | this.runnerDiv = this.createDom('div', { className: 'runner running' }, 540 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), 541 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."), 542 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) 543 | ); 544 | 545 | this.document.body.appendChild(this.outerDiv); 546 | 547 | var suites = runner.suites(); 548 | for (var i = 0; i < suites.length; i++) { 549 | var suite = suites[i]; 550 | var suiteDiv = this.createDom('div', { className: 'suite' }, 551 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), 552 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); 553 | this.suiteDivs[suite.id] = suiteDiv; 554 | var parentDiv = this.outerDiv; 555 | if (suite.parentSuite) { 556 | parentDiv = this.suiteDivs[suite.parentSuite.id]; 557 | } 558 | parentDiv.appendChild(suiteDiv); 559 | } 560 | 561 | this.startedAt = new Date(); 562 | 563 | var self = this; 564 | showPassed.onclick = function(evt) { 565 | if (showPassed.checked) { 566 | self.outerDiv.className += ' show-passed'; 567 | } else { 568 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); 569 | } 570 | }; 571 | 572 | showSkipped.onclick = function(evt) { 573 | if (showSkipped.checked) { 574 | self.outerDiv.className += ' show-skipped'; 575 | } else { 576 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); 577 | } 578 | }; 579 | }; 580 | 581 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { 582 | var results = runner.results(); 583 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; 584 | this.runnerDiv.setAttribute("class", className); 585 | //do it twice for IE 586 | this.runnerDiv.setAttribute("className", className); 587 | var specs = runner.specs(); 588 | var specCount = 0; 589 | for (var i = 0; i < specs.length; i++) { 590 | if (this.specFilter(specs[i])) { 591 | specCount++; 592 | } 593 | } 594 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); 595 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; 596 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); 597 | 598 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); 599 | }; 600 | 601 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { 602 | var results = suite.results(); 603 | var status = results.passed() ? 'passed' : 'failed'; 604 | if (results.totalCount === 0) { // todo: change this to check results.skipped 605 | status = 'skipped'; 606 | } 607 | this.suiteDivs[suite.id].className += " " + status; 608 | }; 609 | 610 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { 611 | if (this.logRunningSpecs) { 612 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 613 | } 614 | }; 615 | 616 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { 617 | var results = spec.results(); 618 | var status = results.passed() ? 'passed' : 'failed'; 619 | if (results.skipped) { 620 | status = 'skipped'; 621 | } 622 | var specDiv = this.createDom('div', { className: 'spec ' + status }, 623 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), 624 | this.createDom('a', { 625 | className: 'description', 626 | href: '?spec=' + encodeURIComponent(spec.getFullName()), 627 | title: spec.getFullName() 628 | }, spec.description)); 629 | 630 | 631 | var resultItems = results.getItems(); 632 | var messagesDiv = this.createDom('div', { className: 'messages' }); 633 | for (var i = 0; i < resultItems.length; i++) { 634 | var result = resultItems[i]; 635 | 636 | if (result.type == 'log') { 637 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 638 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 639 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 640 | 641 | if (result.trace.stack) { 642 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 643 | } 644 | } 645 | } 646 | 647 | if (messagesDiv.childNodes.length > 0) { 648 | specDiv.appendChild(messagesDiv); 649 | } 650 | 651 | this.suiteDivs[spec.suite.id].appendChild(specDiv); 652 | }; 653 | 654 | jasmine.TrivialReporter.prototype.log = function() { 655 | var console = jasmine.getGlobal().console; 656 | if (console && console.log) { 657 | if (console.log.apply) { 658 | console.log.apply(console, arguments); 659 | } else { 660 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 661 | } 662 | } 663 | }; 664 | 665 | jasmine.TrivialReporter.prototype.getLocation = function() { 666 | return this.document.location; 667 | }; 668 | 669 | jasmine.TrivialReporter.prototype.specFilter = function(spec) { 670 | var paramMap = {}; 671 | var params = this.getLocation().search.substring(1).split('&'); 672 | for (var i = 0; i < params.length; i++) { 673 | var p = params[i].split('='); 674 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 675 | } 676 | 677 | if (!paramMap.spec) { 678 | return true; 679 | } 680 | return spec.getFullName().indexOf(paramMap.spec) === 0; 681 | }; 682 | -------------------------------------------------------------------------------- /src/tests/thirdparty/jasmine-1.3.1/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | #HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | #HTMLReporter a { text-decoration: none; } 5 | #HTMLReporter a:hover { text-decoration: underline; } 6 | #HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } 7 | #HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } 8 | #HTMLReporter #jasmine_content { position: fixed; right: 100%; } 9 | #HTMLReporter .version { color: #aaaaaa; } 10 | #HTMLReporter .banner { margin-top: 14px; } 11 | #HTMLReporter .duration { color: #aaaaaa; float: right; } 12 | #HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } 13 | #HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } 14 | #HTMLReporter .symbolSummary li.passed { font-size: 14px; } 15 | #HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } 16 | #HTMLReporter .symbolSummary li.failed { line-height: 9px; } 17 | #HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 18 | #HTMLReporter .symbolSummary li.skipped { font-size: 14px; } 19 | #HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } 20 | #HTMLReporter .symbolSummary li.pending { line-height: 11px; } 21 | #HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } 22 | #HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 23 | #HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 24 | #HTMLReporter .runningAlert { background-color: #666666; } 25 | #HTMLReporter .skippedAlert { background-color: #aaaaaa; } 26 | #HTMLReporter .skippedAlert:first-child { background-color: #333333; } 27 | #HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } 28 | #HTMLReporter .passingAlert { background-color: #a6b779; } 29 | #HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } 30 | #HTMLReporter .failingAlert { background-color: #cf867e; } 31 | #HTMLReporter .failingAlert:first-child { background-color: #b03911; } 32 | #HTMLReporter .results { margin-top: 14px; } 33 | #HTMLReporter #details { display: none; } 34 | #HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } 35 | #HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 36 | #HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 37 | #HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 38 | #HTMLReporter.showDetails .summary { display: none; } 39 | #HTMLReporter.showDetails #details { display: block; } 40 | #HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 41 | #HTMLReporter .summary { margin-top: 14px; } 42 | #HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } 43 | #HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } 44 | #HTMLReporter .summary .specSummary.failed a { color: #b03911; } 45 | #HTMLReporter .description + .suite { margin-top: 0; } 46 | #HTMLReporter .suite { margin-top: 14px; } 47 | #HTMLReporter .suite a { color: #333333; } 48 | #HTMLReporter #details .specDetail { margin-bottom: 28px; } 49 | #HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } 50 | #HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } 51 | #HTMLReporter .resultMessage span.result { display: block; } 52 | #HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 53 | 54 | #TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } 55 | #TrivialReporter a:visited, #TrivialReporter a { color: #303; } 56 | #TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } 57 | #TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } 58 | #TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } 59 | #TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } 60 | #TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } 61 | #TrivialReporter .runner.running { background-color: yellow; } 62 | #TrivialReporter .options { text-align: right; font-size: .8em; } 63 | #TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } 64 | #TrivialReporter .suite .suite { margin: 5px; } 65 | #TrivialReporter .suite.passed { background-color: #dfd; } 66 | #TrivialReporter .suite.failed { background-color: #fdd; } 67 | #TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } 68 | #TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } 69 | #TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } 70 | #TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } 71 | #TrivialReporter .spec.skipped { background-color: #bbb; } 72 | #TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } 73 | #TrivialReporter .passed { background-color: #cfc; display: none; } 74 | #TrivialReporter .failed { background-color: #fbb; } 75 | #TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } 76 | #TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } 77 | #TrivialReporter .resultMessage .mismatch { color: black; } 78 | #TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } 79 | #TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } 80 | #TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } 81 | #TrivialReporter #jasmine_content { position: fixed; right: 100%; } 82 | #TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } 83 | -------------------------------------------------------------------------------- /src/tests/thirdparty/jasmine/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 | -------------------------------------------------------------------------------- /src/tests/thirdparty/jasmine/jasmine-html.js: -------------------------------------------------------------------------------- 1 | jasmine.HtmlReporterHelpers = {}; 2 | 3 | jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { 4 | var el = document.createElement(type); 5 | 6 | for (var i = 2; i < arguments.length; i++) { 7 | var child = arguments[i]; 8 | 9 | if (typeof child === 'string') { 10 | el.appendChild(document.createTextNode(child)); 11 | } else { 12 | if (child) { 13 | el.appendChild(child); 14 | } 15 | } 16 | } 17 | 18 | for (var attr in attrs) { 19 | if (attr == "className") { 20 | el[attr] = attrs[attr]; 21 | } else { 22 | el.setAttribute(attr, attrs[attr]); 23 | } 24 | } 25 | 26 | return el; 27 | }; 28 | 29 | jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { 30 | var results = child.results(); 31 | var status = results.passed() ? 'passed' : 'failed'; 32 | if (results.skipped) { 33 | status = 'skipped'; 34 | } 35 | 36 | return status; 37 | }; 38 | 39 | jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { 40 | var parentDiv = this.dom.summary; 41 | var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; 42 | var parent = child[parentSuite]; 43 | 44 | if (parent) { 45 | if (typeof this.views.suites[parent.id] == 'undefined') { 46 | this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); 47 | } 48 | parentDiv = this.views.suites[parent.id].element; 49 | } 50 | 51 | parentDiv.appendChild(childElement); 52 | }; 53 | 54 | 55 | jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { 56 | for(var fn in jasmine.HtmlReporterHelpers) { 57 | ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; 58 | } 59 | }; 60 | 61 | jasmine.HtmlReporter = function(_doc) { 62 | var self = this; 63 | var doc = _doc || window.document; 64 | 65 | var reporterView; 66 | 67 | var dom = {}; 68 | 69 | // Jasmine Reporter Public Interface 70 | self.logRunningSpecs = false; 71 | 72 | self.reportRunnerStarting = function(runner) { 73 | var specs = runner.specs() || []; 74 | 75 | if (specs.length == 0) { 76 | return; 77 | } 78 | 79 | createReporterDom(runner.env.versionString()); 80 | doc.body.appendChild(dom.reporter); 81 | setExceptionHandling(); 82 | 83 | reporterView = new jasmine.HtmlReporter.ReporterView(dom); 84 | reporterView.addSpecs(specs, self.specFilter); 85 | }; 86 | 87 | self.reportRunnerResults = function(runner) { 88 | reporterView && reporterView.complete(); 89 | }; 90 | 91 | self.reportSuiteResults = function(suite) { 92 | reporterView.suiteComplete(suite); 93 | }; 94 | 95 | self.reportSpecStarting = function(spec) { 96 | if (self.logRunningSpecs) { 97 | self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 98 | } 99 | }; 100 | 101 | self.reportSpecResults = function(spec) { 102 | reporterView.specComplete(spec); 103 | }; 104 | 105 | self.log = function() { 106 | var console = jasmine.getGlobal().console; 107 | if (console && console.log) { 108 | if (console.log.apply) { 109 | console.log.apply(console, arguments); 110 | } else { 111 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 112 | } 113 | } 114 | }; 115 | 116 | self.specFilter = function(spec) { 117 | if (!focusedSpecName()) { 118 | return true; 119 | } 120 | 121 | return spec.getFullName().indexOf(focusedSpecName()) === 0; 122 | }; 123 | 124 | return self; 125 | 126 | function focusedSpecName() { 127 | var specName; 128 | 129 | (function memoizeFocusedSpec() { 130 | if (specName) { 131 | return; 132 | } 133 | 134 | var paramMap = []; 135 | var params = jasmine.HtmlReporter.parameters(doc); 136 | 137 | for (var i = 0; i < params.length; i++) { 138 | var p = params[i].split('='); 139 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 140 | } 141 | 142 | specName = paramMap.spec; 143 | })(); 144 | 145 | return specName; 146 | } 147 | 148 | function createReporterDom(version) { 149 | dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, 150 | dom.banner = self.createDom('div', { className: 'banner' }, 151 | self.createDom('span', { className: 'title' }, "Jasmine "), 152 | self.createDom('span', { className: 'version' }, version)), 153 | 154 | dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), 155 | dom.alert = self.createDom('div', {className: 'alert'}, 156 | self.createDom('span', { className: 'exceptions' }, 157 | self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'), 158 | self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))), 159 | dom.results = self.createDom('div', {className: 'results'}, 160 | dom.summary = self.createDom('div', { className: 'summary' }), 161 | dom.details = self.createDom('div', { id: 'details' })) 162 | ); 163 | } 164 | 165 | function noTryCatch() { 166 | return window.location.search.match(/catch=false/); 167 | } 168 | 169 | function searchWithCatch() { 170 | var params = jasmine.HtmlReporter.parameters(window.document); 171 | var removed = false; 172 | var i = 0; 173 | 174 | while (!removed && i < params.length) { 175 | if (params[i].match(/catch=/)) { 176 | params.splice(i, 1); 177 | removed = true; 178 | } 179 | i++; 180 | } 181 | if (jasmine.CATCH_EXCEPTIONS) { 182 | params.push("catch=false"); 183 | } 184 | 185 | return params.join("&"); 186 | } 187 | 188 | function setExceptionHandling() { 189 | var chxCatch = document.getElementById('no_try_catch'); 190 | 191 | if (noTryCatch()) { 192 | chxCatch.setAttribute('checked', true); 193 | jasmine.CATCH_EXCEPTIONS = false; 194 | } 195 | chxCatch.onclick = function() { 196 | window.location.search = searchWithCatch(); 197 | }; 198 | } 199 | }; 200 | jasmine.HtmlReporter.parameters = function(doc) { 201 | var paramStr = doc.location.search.substring(1); 202 | var params = []; 203 | 204 | if (paramStr.length > 0) { 205 | params = paramStr.split('&'); 206 | } 207 | return params; 208 | } 209 | jasmine.HtmlReporter.sectionLink = function(sectionName) { 210 | var link = '?'; 211 | var params = []; 212 | 213 | if (sectionName) { 214 | params.push('spec=' + encodeURIComponent(sectionName)); 215 | } 216 | if (!jasmine.CATCH_EXCEPTIONS) { 217 | params.push("catch=false"); 218 | } 219 | if (params.length > 0) { 220 | link += params.join("&"); 221 | } 222 | 223 | return link; 224 | }; 225 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter); 226 | jasmine.HtmlReporter.ReporterView = function(dom) { 227 | this.startedAt = new Date(); 228 | this.runningSpecCount = 0; 229 | this.completeSpecCount = 0; 230 | this.passedCount = 0; 231 | this.failedCount = 0; 232 | this.skippedCount = 0; 233 | 234 | this.createResultsMenu = function() { 235 | this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, 236 | this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'), 237 | ' | ', 238 | this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing')); 239 | 240 | this.summaryMenuItem.onclick = function() { 241 | dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, ''); 242 | }; 243 | 244 | this.detailsMenuItem.onclick = function() { 245 | showDetails(); 246 | }; 247 | }; 248 | 249 | this.addSpecs = function(specs, specFilter) { 250 | this.totalSpecCount = specs.length; 251 | 252 | this.views = { 253 | specs: {}, 254 | suites: {} 255 | }; 256 | 257 | for (var i = 0; i < specs.length; i++) { 258 | var spec = specs[i]; 259 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views); 260 | if (specFilter(spec)) { 261 | this.runningSpecCount++; 262 | } 263 | } 264 | }; 265 | 266 | this.specComplete = function(spec) { 267 | this.completeSpecCount++; 268 | 269 | if (isUndefined(this.views.specs[spec.id])) { 270 | this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom); 271 | } 272 | 273 | var specView = this.views.specs[spec.id]; 274 | 275 | switch (specView.status()) { 276 | case 'passed': 277 | this.passedCount++; 278 | break; 279 | 280 | case 'failed': 281 | this.failedCount++; 282 | break; 283 | 284 | case 'skipped': 285 | this.skippedCount++; 286 | break; 287 | } 288 | 289 | specView.refresh(); 290 | this.refresh(); 291 | }; 292 | 293 | this.suiteComplete = function(suite) { 294 | var suiteView = this.views.suites[suite.id]; 295 | if (isUndefined(suiteView)) { 296 | return; 297 | } 298 | suiteView.refresh(); 299 | }; 300 | 301 | this.refresh = function() { 302 | 303 | if (isUndefined(this.resultsMenu)) { 304 | this.createResultsMenu(); 305 | } 306 | 307 | // currently running UI 308 | if (isUndefined(this.runningAlert)) { 309 | this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" }); 310 | dom.alert.appendChild(this.runningAlert); 311 | } 312 | this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); 313 | 314 | // skipped specs UI 315 | if (isUndefined(this.skippedAlert)) { 316 | this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" }); 317 | } 318 | 319 | this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 320 | 321 | if (this.skippedCount === 1 && isDefined(dom.alert)) { 322 | dom.alert.appendChild(this.skippedAlert); 323 | } 324 | 325 | // passing specs UI 326 | if (isUndefined(this.passedAlert)) { 327 | this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" }); 328 | } 329 | this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); 330 | 331 | // failing specs UI 332 | if (isUndefined(this.failedAlert)) { 333 | this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"}); 334 | } 335 | this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount); 336 | 337 | if (this.failedCount === 1 && isDefined(dom.alert)) { 338 | dom.alert.appendChild(this.failedAlert); 339 | dom.alert.appendChild(this.resultsMenu); 340 | } 341 | 342 | // summary info 343 | this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); 344 | this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; 345 | }; 346 | 347 | this.complete = function() { 348 | dom.alert.removeChild(this.runningAlert); 349 | 350 | this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; 351 | 352 | if (this.failedCount === 0) { 353 | dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount))); 354 | } else { 355 | showDetails(); 356 | } 357 | 358 | dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); 359 | }; 360 | 361 | return this; 362 | 363 | function showDetails() { 364 | if (dom.reporter.className.search(/showDetails/) === -1) { 365 | dom.reporter.className += " showDetails"; 366 | } 367 | } 368 | 369 | function isUndefined(obj) { 370 | return typeof obj === 'undefined'; 371 | } 372 | 373 | function isDefined(obj) { 374 | return !isUndefined(obj); 375 | } 376 | 377 | function specPluralizedFor(count) { 378 | var str = count + " spec"; 379 | if (count > 1) { 380 | str += "s" 381 | } 382 | return str; 383 | } 384 | 385 | }; 386 | 387 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView); 388 | 389 | 390 | jasmine.HtmlReporter.SpecView = function(spec, dom, views) { 391 | this.spec = spec; 392 | this.dom = dom; 393 | this.views = views; 394 | 395 | this.symbol = this.createDom('li', { className: 'pending' }); 396 | this.dom.symbolSummary.appendChild(this.symbol); 397 | 398 | this.summary = this.createDom('div', { className: 'specSummary' }, 399 | this.createDom('a', { 400 | className: 'description', 401 | href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()), 402 | title: this.spec.getFullName() 403 | }, this.spec.description) 404 | ); 405 | 406 | this.detail = this.createDom('div', { className: 'specDetail' }, 407 | this.createDom('a', { 408 | className: 'description', 409 | href: '?spec=' + encodeURIComponent(this.spec.getFullName()), 410 | title: this.spec.getFullName() 411 | }, this.spec.getFullName()) 412 | ); 413 | }; 414 | 415 | jasmine.HtmlReporter.SpecView.prototype.status = function() { 416 | return this.getSpecStatus(this.spec); 417 | }; 418 | 419 | jasmine.HtmlReporter.SpecView.prototype.refresh = function() { 420 | this.symbol.className = this.status(); 421 | 422 | switch (this.status()) { 423 | case 'skipped': 424 | break; 425 | 426 | case 'passed': 427 | this.appendSummaryToSuiteDiv(); 428 | break; 429 | 430 | case 'failed': 431 | this.appendSummaryToSuiteDiv(); 432 | this.appendFailureDetail(); 433 | break; 434 | } 435 | }; 436 | 437 | jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { 438 | this.summary.className += ' ' + this.status(); 439 | this.appendToSummary(this.spec, this.summary); 440 | }; 441 | 442 | jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() { 443 | this.detail.className += ' ' + this.status(); 444 | 445 | var resultItems = this.spec.results().getItems(); 446 | var messagesDiv = this.createDom('div', { className: 'messages' }); 447 | 448 | for (var i = 0; i < resultItems.length; i++) { 449 | var result = resultItems[i]; 450 | 451 | if (result.type == 'log') { 452 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 453 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 454 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 455 | 456 | if (result.trace.stack) { 457 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 458 | } 459 | } 460 | } 461 | 462 | if (messagesDiv.childNodes.length > 0) { 463 | this.detail.appendChild(messagesDiv); 464 | this.dom.details.appendChild(this.detail); 465 | } 466 | }; 467 | 468 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) { 469 | this.suite = suite; 470 | this.dom = dom; 471 | this.views = views; 472 | 473 | this.element = this.createDom('div', { className: 'suite' }, 474 | this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) 475 | ); 476 | 477 | this.appendToSummary(this.suite, this.element); 478 | }; 479 | 480 | jasmine.HtmlReporter.SuiteView.prototype.status = function() { 481 | return this.getSpecStatus(this.suite); 482 | }; 483 | 484 | jasmine.HtmlReporter.SuiteView.prototype.refresh = function() { 485 | this.element.className += " " + this.status(); 486 | }; 487 | 488 | jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView); 489 | 490 | /* @deprecated Use jasmine.HtmlReporter instead 491 | */ 492 | jasmine.TrivialReporter = function(doc) { 493 | this.document = doc || document; 494 | this.suiteDivs = {}; 495 | this.logRunningSpecs = false; 496 | }; 497 | 498 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { 499 | var el = document.createElement(type); 500 | 501 | for (var i = 2; i < arguments.length; i++) { 502 | var child = arguments[i]; 503 | 504 | if (typeof child === 'string') { 505 | el.appendChild(document.createTextNode(child)); 506 | } else { 507 | if (child) { el.appendChild(child); } 508 | } 509 | } 510 | 511 | for (var attr in attrs) { 512 | if (attr == "className") { 513 | el[attr] = attrs[attr]; 514 | } else { 515 | el.setAttribute(attr, attrs[attr]); 516 | } 517 | } 518 | 519 | return el; 520 | }; 521 | 522 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { 523 | var showPassed, showSkipped; 524 | 525 | this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' }, 526 | this.createDom('div', { className: 'banner' }, 527 | this.createDom('div', { className: 'logo' }, 528 | this.createDom('span', { className: 'title' }, "Jasmine"), 529 | this.createDom('span', { className: 'version' }, runner.env.versionString())), 530 | this.createDom('div', { className: 'options' }, 531 | "Show ", 532 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), 533 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), 534 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), 535 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") 536 | ) 537 | ), 538 | 539 | this.runnerDiv = this.createDom('div', { className: 'runner running' }, 540 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), 541 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."), 542 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) 543 | ); 544 | 545 | this.document.body.appendChild(this.outerDiv); 546 | 547 | var suites = runner.suites(); 548 | for (var i = 0; i < suites.length; i++) { 549 | var suite = suites[i]; 550 | var suiteDiv = this.createDom('div', { className: 'suite' }, 551 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), 552 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); 553 | this.suiteDivs[suite.id] = suiteDiv; 554 | var parentDiv = this.outerDiv; 555 | if (suite.parentSuite) { 556 | parentDiv = this.suiteDivs[suite.parentSuite.id]; 557 | } 558 | parentDiv.appendChild(suiteDiv); 559 | } 560 | 561 | this.startedAt = new Date(); 562 | 563 | var self = this; 564 | showPassed.onclick = function(evt) { 565 | if (showPassed.checked) { 566 | self.outerDiv.className += ' show-passed'; 567 | } else { 568 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); 569 | } 570 | }; 571 | 572 | showSkipped.onclick = function(evt) { 573 | if (showSkipped.checked) { 574 | self.outerDiv.className += ' show-skipped'; 575 | } else { 576 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); 577 | } 578 | }; 579 | }; 580 | 581 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { 582 | var results = runner.results(); 583 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; 584 | this.runnerDiv.setAttribute("class", className); 585 | //do it twice for IE 586 | this.runnerDiv.setAttribute("className", className); 587 | var specs = runner.specs(); 588 | var specCount = 0; 589 | for (var i = 0; i < specs.length; i++) { 590 | if (this.specFilter(specs[i])) { 591 | specCount++; 592 | } 593 | } 594 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); 595 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; 596 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); 597 | 598 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); 599 | }; 600 | 601 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { 602 | var results = suite.results(); 603 | var status = results.passed() ? 'passed' : 'failed'; 604 | if (results.totalCount === 0) { // todo: change this to check results.skipped 605 | status = 'skipped'; 606 | } 607 | this.suiteDivs[suite.id].className += " " + status; 608 | }; 609 | 610 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { 611 | if (this.logRunningSpecs) { 612 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 613 | } 614 | }; 615 | 616 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { 617 | var results = spec.results(); 618 | var status = results.passed() ? 'passed' : 'failed'; 619 | if (results.skipped) { 620 | status = 'skipped'; 621 | } 622 | var specDiv = this.createDom('div', { className: 'spec ' + status }, 623 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), 624 | this.createDom('a', { 625 | className: 'description', 626 | href: '?spec=' + encodeURIComponent(spec.getFullName()), 627 | title: spec.getFullName() 628 | }, spec.description)); 629 | 630 | 631 | var resultItems = results.getItems(); 632 | var messagesDiv = this.createDom('div', { className: 'messages' }); 633 | for (var i = 0; i < resultItems.length; i++) { 634 | var result = resultItems[i]; 635 | 636 | if (result.type == 'log') { 637 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 638 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 639 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 640 | 641 | if (result.trace.stack) { 642 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 643 | } 644 | } 645 | } 646 | 647 | if (messagesDiv.childNodes.length > 0) { 648 | specDiv.appendChild(messagesDiv); 649 | } 650 | 651 | this.suiteDivs[spec.suite.id].appendChild(specDiv); 652 | }; 653 | 654 | jasmine.TrivialReporter.prototype.log = function() { 655 | var console = jasmine.getGlobal().console; 656 | if (console && console.log) { 657 | if (console.log.apply) { 658 | console.log.apply(console, arguments); 659 | } else { 660 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 661 | } 662 | } 663 | }; 664 | 665 | jasmine.TrivialReporter.prototype.getLocation = function() { 666 | return this.document.location; 667 | }; 668 | 669 | jasmine.TrivialReporter.prototype.specFilter = function(spec) { 670 | var paramMap = {}; 671 | var params = this.getLocation().search.substring(1).split('&'); 672 | for (var i = 0; i < params.length; i++) { 673 | var p = params[i].split('='); 674 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 675 | } 676 | 677 | if (!paramMap.spec) { 678 | return true; 679 | } 680 | return spec.getFullName().indexOf(paramMap.spec) === 0; 681 | }; 682 | -------------------------------------------------------------------------------- /src/tests/thirdparty/jasmine/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | #HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | #HTMLReporter a { text-decoration: none; } 5 | #HTMLReporter a:hover { text-decoration: underline; } 6 | #HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } 7 | #HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } 8 | #HTMLReporter #jasmine_content { position: fixed; right: 100%; } 9 | #HTMLReporter .version { color: #aaaaaa; } 10 | #HTMLReporter .banner { margin-top: 14px; } 11 | #HTMLReporter .duration { color: #aaaaaa; float: right; } 12 | #HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } 13 | #HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } 14 | #HTMLReporter .symbolSummary li.passed { font-size: 14px; } 15 | #HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } 16 | #HTMLReporter .symbolSummary li.failed { line-height: 9px; } 17 | #HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 18 | #HTMLReporter .symbolSummary li.skipped { font-size: 14px; } 19 | #HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } 20 | #HTMLReporter .symbolSummary li.pending { line-height: 11px; } 21 | #HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } 22 | #HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 23 | #HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 24 | #HTMLReporter .runningAlert { background-color: #666666; } 25 | #HTMLReporter .skippedAlert { background-color: #aaaaaa; } 26 | #HTMLReporter .skippedAlert:first-child { background-color: #333333; } 27 | #HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } 28 | #HTMLReporter .passingAlert { background-color: #a6b779; } 29 | #HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } 30 | #HTMLReporter .failingAlert { background-color: #cf867e; } 31 | #HTMLReporter .failingAlert:first-child { background-color: #b03911; } 32 | #HTMLReporter .results { margin-top: 14px; } 33 | #HTMLReporter #details { display: none; } 34 | #HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } 35 | #HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 36 | #HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 37 | #HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 38 | #HTMLReporter.showDetails .summary { display: none; } 39 | #HTMLReporter.showDetails #details { display: block; } 40 | #HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 41 | #HTMLReporter .summary { margin-top: 14px; } 42 | #HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } 43 | #HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } 44 | #HTMLReporter .summary .specSummary.failed a { color: #b03911; } 45 | #HTMLReporter .description + .suite { margin-top: 0; } 46 | #HTMLReporter .suite { margin-top: 14px; } 47 | #HTMLReporter .suite a { color: #333333; } 48 | #HTMLReporter #details .specDetail { margin-bottom: 28px; } 49 | #HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } 50 | #HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } 51 | #HTMLReporter .resultMessage span.result { display: block; } 52 | #HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 53 | 54 | #TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } 55 | #TrivialReporter a:visited, #TrivialReporter a { color: #303; } 56 | #TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } 57 | #TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } 58 | #TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } 59 | #TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } 60 | #TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } 61 | #TrivialReporter .runner.running { background-color: yellow; } 62 | #TrivialReporter .options { text-align: right; font-size: .8em; } 63 | #TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } 64 | #TrivialReporter .suite .suite { margin: 5px; } 65 | #TrivialReporter .suite.passed { background-color: #dfd; } 66 | #TrivialReporter .suite.failed { background-color: #fdd; } 67 | #TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } 68 | #TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } 69 | #TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } 70 | #TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } 71 | #TrivialReporter .spec.skipped { background-color: #bbb; } 72 | #TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } 73 | #TrivialReporter .passed { background-color: #cfc; display: none; } 74 | #TrivialReporter .failed { background-color: #fbb; } 75 | #TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } 76 | #TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } 77 | #TrivialReporter .resultMessage .mismatch { color: black; } 78 | #TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } 79 | #TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } 80 | #TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } 81 | #TrivialReporter #jasmine_content { position: fixed; right: 100%; } 82 | #TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } 83 | --------------------------------------------------------------------------------