├── .gitignore ├── README.md ├── app ├── css │ ├── .gitignore │ └── app.css ├── img │ ├── .gitignore │ ├── 8-ball.png │ ├── back.png │ ├── baked-potato.png │ ├── dinosaur.png │ ├── kronos.png │ ├── rocket.png │ ├── skinny-unicorn.png │ ├── that-guy.png │ └── zeppelin.png ├── index-async.html ├── index.html ├── js │ ├── app.js │ └── game.js ├── lib │ └── angular │ │ ├── angular-cookies.js │ │ ├── angular-cookies.min.js │ │ ├── angular-loader.js │ │ ├── angular-loader.min.js │ │ ├── angular-resource.js │ │ ├── angular-resource.min.js │ │ ├── angular-sanitize.js │ │ ├── angular-sanitize.min.js │ │ ├── angular.js │ │ ├── angular.min.js │ │ └── version.txt └── partials │ ├── .gitignore │ ├── partial1.html │ └── partial2.html ├── config ├── jsTestDriver-scenario.conf ├── jsTestDriver.conf ├── jstd-scenario-adapter-config.js ├── testacular-e2e.conf.js └── testacular.conf.js ├── files.js ├── logs └── .gitignore ├── scripts ├── e2e-test.bat ├── e2e-test.sh ├── test.bat ├── test.sh ├── watchr.rb └── web-server.js └── test ├── e2e ├── runner.html └── scenarios.js ├── lib ├── angular │ ├── angular-mocks.js │ ├── angular-scenario.js │ ├── jstd-scenario-adapter.js │ └── version.txt ├── jasmine-jstd-adapter │ ├── JasmineAdapter.js │ └── version.txt ├── jasmine │ ├── MIT.LICENSE │ ├── index.js │ ├── jasmine-html.js │ ├── jasmine.css │ ├── jasmine.js │ ├── jasmine_favicon.png │ └── version.txt └── jstestdriver │ ├── JsTestDriver.jar │ └── version.txt └── unit ├── appSpec.js └── gameSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | nbproject 3 | manifest.mf 4 | build.xml 5 | 6 | .project 7 | .settings 8 | .idea/* 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Memory-Game 2 | =========== 3 | 4 | AngularJS example app that implements the famous memory game of finding matching pairs of cards. Popular with toddlers everywhere! 5 | 6 | 7 | View demo at: 8 | -------------------------------------------------------------------------------- /app/css/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/css/.gitignore -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | /* app css stylesheet */ 2 | 3 | div { 4 | font-size: 30px; 5 | } 6 | 7 | .container { 8 | width: 165px; 9 | height: 200px; 10 | position: relative; 11 | -webkit-perspective: 800px; 12 | -moz-perspective: 800px; 13 | -ms-perspective: 800px; 14 | -o-perspective: 800px; 15 | perspective: 800px; 16 | } 17 | 18 | .card { 19 | width: 100%; 20 | height: 100%; 21 | -webkit-transition: -webkit-transform 1s; 22 | -moz-transition: -moz-transform 1s; 23 | -ms-transition: -ms-transform 1s; 24 | -o-transition: -o-transform 1s; 25 | transition: transform 1s; 26 | -webkit-transform-style: preserve-3d; 27 | -moz-transform-style: preserve-3d; 28 | -ms-transform-style: preserve-3d; 29 | -o-transform-style: preserve-3d; 30 | transform-style: preserve-3d; 31 | } 32 | 33 | .card.flipped { 34 | -webkit-transform: rotateY( 180deg ); 35 | -moz-transform: rotateY( 180deg ); 36 | -ms-transform: rotateY( 180deg ); 37 | -o-transform: rotateY( 180deg ); 38 | transform: rotateY( 180deg ); 39 | } 40 | 41 | .card img { 42 | display: block; 43 | height: 100%; 44 | width: 100%; 45 | position: absolute; 46 | -webkit-backface-visibility: hidden; 47 | -moz-backface-visibility: hidden; 48 | -ms-backface-visibility: hidden; 49 | -o-backface-visibility: hidden; 50 | backface-visibility: hidden; 51 | } 52 | 53 | 54 | 55 | .card .back { 56 | background: blue; 57 | -webkit-transform: rotateY( 180deg ); 58 | -moz-transform: rotateY( 180deg ); 59 | -ms-transform: rotateY( 180deg ); 60 | -o-transform: rotateY( 180deg ); 61 | transform: rotateY( 180deg ); 62 | } 63 | 64 | .message { 65 | width: 660px; 66 | text-align: center; 67 | } 68 | -------------------------------------------------------------------------------- /app/img/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/.gitignore -------------------------------------------------------------------------------- /app/img/8-ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/8-ball.png -------------------------------------------------------------------------------- /app/img/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/back.png -------------------------------------------------------------------------------- /app/img/baked-potato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/baked-potato.png -------------------------------------------------------------------------------- /app/img/dinosaur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/dinosaur.png -------------------------------------------------------------------------------- /app/img/kronos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/kronos.png -------------------------------------------------------------------------------- /app/img/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/rocket.png -------------------------------------------------------------------------------- /app/img/skinny-unicorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/skinny-unicorn.png -------------------------------------------------------------------------------- /app/img/that-guy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/that-guy.png -------------------------------------------------------------------------------- /app/img/zeppelin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/img/zeppelin.png -------------------------------------------------------------------------------- /app/index-async.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 43 | My AngularJS App 44 | 45 | 46 | 47 | 51 | 52 |
53 | 54 |
Angular seed app: v
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Memory Game 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Pairs left to match: {{game.unmatchedPairs}}
13 |
Matching: {{game.firstPick.title}}
14 | 15 | 16 | 17 | 30 | 31 |
18 | 19 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 |
32 | 33 |
{{game.message}}
34 | 35 | 36 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* App Controllers */ 3 | 4 | 5 | var memoryGameApp = angular.module('memoryGameApp', []); 6 | 7 | 8 | memoryGameApp.factory('game', function() { 9 | var tileNames = ['8-ball', 'kronos', 'baked-potato', 'dinosaur', 'rocket', 'skinny-unicorn', 10 | 'that-guy', 'zeppelin']; 11 | 12 | return new Game(tileNames); 13 | }); 14 | 15 | 16 | memoryGameApp.controller('GameCtrl', function GameCtrl($scope, game) { 17 | $scope.game = game; 18 | }); 19 | 20 | 21 | //usages: 22 | //- in the repeater as: 23 | //- card currently being matched as: 24 | 25 | memoryGameApp.directive('mgCard', function() { 26 | return { 27 | restrict: 'E', 28 | // instead of inlining the template string here, one could use templateUrl: 'mg-card.html' 29 | // and then either create a mg-card.html file with the content or add 30 | // element to 31 | // index.html 32 | template: '
' + 33 | '
' + 34 | '' + 35 | '' + 36 | '
' + 37 | '
', 38 | scope: { 39 | tile: '=' 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /app/js/game.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* Memory Game Models and Business Logic */ 3 | 4 | function Tile(title) { 5 | this.title = title; 6 | this.flipped = false; 7 | } 8 | 9 | Tile.prototype.flip = function() { 10 | this.flipped = !this.flipped; 11 | } 12 | 13 | 14 | 15 | function Game(tileNames) { 16 | var tileDeck = makeDeck(tileNames); 17 | 18 | this.grid = makeGrid(tileDeck); 19 | this.message = Game.MESSAGE_CLICK; 20 | this.unmatchedPairs = tileNames.length; 21 | 22 | this.flipTile = function(tile) { 23 | if (tile.flipped) { 24 | return; 25 | } 26 | 27 | tile.flip(); 28 | 29 | if (!this.firstPick || this.secondPick) { 30 | 31 | if (this.secondPick) { 32 | this.firstPick.flip(); 33 | this.secondPick.flip(); 34 | this.firstPick = this.secondPick = undefined; 35 | } 36 | 37 | this.firstPick = tile; 38 | this.message = Game.MESSAGE_ONE_MORE; 39 | 40 | } else { 41 | 42 | if (this.firstPick.title === tile.title) { 43 | this.unmatchedPairs--; 44 | this.message = (this.unmatchedPairs > 0) ? Game.MESSAGE_MATCH : Game.MESSAGE_WON; 45 | this.firstPick = this.secondPick = undefined; 46 | } else { 47 | this.secondPick = tile; 48 | this.message = Game.MESSAGE_MISS; 49 | } 50 | } 51 | } 52 | } 53 | 54 | Game.MESSAGE_CLICK = 'Click on a tile.'; 55 | Game.MESSAGE_ONE_MORE = 'Pick one more card.' 56 | Game.MESSAGE_MISS = 'Try again.'; 57 | Game.MESSAGE_MATCH = 'Good job! Keep going.'; 58 | Game.MESSAGE_WON = 'You win!'; 59 | 60 | 61 | 62 | /* Create an array with two of each tileName in it */ 63 | function makeDeck(tileNames) { 64 | var tileDeck = []; 65 | tileNames.forEach(function(name) { 66 | tileDeck.push(new Tile(name)); 67 | tileDeck.push(new Tile(name)); 68 | }); 69 | 70 | return tileDeck; 71 | } 72 | 73 | 74 | function makeGrid(tileDeck) { 75 | var gridDimension = Math.sqrt(tileDeck.length), 76 | grid = []; 77 | 78 | for (var row = 0; row < gridDimension; row++) { 79 | grid[row] = []; 80 | for (var col = 0; col < gridDimension; col++) { 81 | grid[row][col] = removeRandomTile(tileDeck); 82 | } 83 | } 84 | 85 | return grid; 86 | } 87 | 88 | 89 | function removeRandomTile(tileDeck) { 90 | var i = Math.floor(Math.random()*tileDeck.length); 91 | return tileDeck.splice(i, 1)[0]; 92 | } 93 | 94 | -------------------------------------------------------------------------------- /app/lib/angular/angular-cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngCookies 12 | */ 13 | 14 | 15 | angular.module('ngCookies', ['ng']). 16 | /** 17 | * @ngdoc object 18 | * @name ngCookies.$cookies 19 | * @requires $browser 20 | * 21 | * @description 22 | * Provides read/write access to browser's cookies. 23 | * 24 | * Only a simple Object is exposed and by adding or removing properties to/from 25 | * this object, new cookies are created/deleted at the end of current $eval. 26 | * 27 | * @example 28 | 29 | 30 | 38 | 39 | 40 | */ 41 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 42 | var cookies = {}, 43 | lastCookies = {}, 44 | lastBrowserCookies, 45 | runEval = false, 46 | copy = angular.copy, 47 | isUndefined = angular.isUndefined; 48 | 49 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 50 | $browser.addPollFn(function() { 51 | var currentCookies = $browser.cookies(); 52 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 53 | lastBrowserCookies = currentCookies; 54 | copy(currentCookies, lastCookies); 55 | copy(currentCookies, cookies); 56 | if (runEval) $rootScope.$apply(); 57 | } 58 | })(); 59 | 60 | runEval = true; 61 | 62 | //at the end of each eval, push cookies 63 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 64 | // strings or browser refuses to store some cookies, we update the model in the push fn. 65 | $rootScope.$watch(push); 66 | 67 | return cookies; 68 | 69 | 70 | /** 71 | * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. 72 | */ 73 | function push() { 74 | var name, 75 | value, 76 | browserCookies, 77 | updated; 78 | 79 | //delete any cookies deleted in $cookies 80 | for (name in lastCookies) { 81 | if (isUndefined(cookies[name])) { 82 | $browser.cookies(name, undefined); 83 | } 84 | } 85 | 86 | //update all cookies updated in $cookies 87 | for(name in cookies) { 88 | value = cookies[name]; 89 | if (!angular.isString(value)) { 90 | if (angular.isDefined(lastCookies[name])) { 91 | cookies[name] = lastCookies[name]; 92 | } else { 93 | delete cookies[name]; 94 | } 95 | } else if (value !== lastCookies[name]) { 96 | $browser.cookies(name, value); 97 | updated = true; 98 | } 99 | } 100 | 101 | //verify what was actually stored 102 | if (updated){ 103 | updated = false; 104 | browserCookies = $browser.cookies(); 105 | 106 | for (name in cookies) { 107 | if (cookies[name] !== browserCookies[name]) { 108 | //delete or reset all cookies that the browser dropped from $cookies 109 | if (isUndefined(browserCookies[name])) { 110 | delete cookies[name]; 111 | } else { 112 | cookies[name] = browserCookies[name]; 113 | } 114 | updated = true; 115 | } 116 | } 117 | } 118 | } 119 | }]). 120 | 121 | 122 | /** 123 | * @ngdoc object 124 | * @name ngCookies.$cookieStore 125 | * @requires $cookies 126 | * 127 | * @description 128 | * Provides a key-value (string-object) storage, that is backed by session cookies. 129 | * Objects put or retrieved from this storage are automatically serialized or 130 | * deserialized by angular's toJson/fromJson. 131 | * @example 132 | */ 133 | factory('$cookieStore', ['$cookies', function($cookies) { 134 | 135 | return { 136 | /** 137 | * @ngdoc method 138 | * @name ngCookies.$cookieStore#get 139 | * @methodOf ngCookies.$cookieStore 140 | * 141 | * @description 142 | * Returns the value of given cookie key 143 | * 144 | * @param {string} key Id to use for lookup. 145 | * @returns {Object} Deserialized cookie value. 146 | */ 147 | get: function(key) { 148 | return angular.fromJson($cookies[key]); 149 | }, 150 | 151 | /** 152 | * @ngdoc method 153 | * @name ngCookies.$cookieStore#put 154 | * @methodOf ngCookies.$cookieStore 155 | * 156 | * @description 157 | * Sets a value for given cookie key 158 | * 159 | * @param {string} key Id for the `value`. 160 | * @param {Object} value Value to be stored. 161 | */ 162 | put: function(key, value) { 163 | $cookies[key] = angular.toJson(value); 164 | }, 165 | 166 | /** 167 | * @ngdoc method 168 | * @name ngCookies.$cookieStore#remove 169 | * @methodOf ngCookies.$cookieStore 170 | * 171 | * @description 172 | * Remove given cookie 173 | * 174 | * @param {string} key Id of the key-value pair to delete. 175 | */ 176 | remove: function(key) { 177 | delete $cookies[key]; 178 | } 179 | }; 180 | 181 | }]); 182 | 183 | 184 | })(window, window.angular); 185 | -------------------------------------------------------------------------------- /app/lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.6 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(m,f,l){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,c){var b={},g={},h,i=!1,j=f.copy,k=f.isUndefined;c.addPollFn(function(){var a=c.cookies();h!=a&&(h=a,j(a,g),j(a,b),i&&d.$apply())})();i=!0;d.$watch(function(){var a,e,d;for(a in g)k(b[a])&&c.cookies(a,l);for(a in b)e=b[a],f.isString(e)?e!==g[a]&&(c.cookies(a,e),d=!0):f.isDefined(g[a])?b[a]=g[a]:delete b[a];if(d)for(a in e=c.cookies(),b)b[a]!==e[a]&&(k(e[a])?delete b[a]:b[a]=e[a])});return b}]).factory("$cookieStore", 7 | ["$cookies",function(d){return{get:function(c){return f.fromJson(d[c])},put:function(c,b){d[c]=f.toJson(b)},remove:function(c){delete d[c]}}}])})(window,window.angular); 8 | -------------------------------------------------------------------------------- /app/lib/angular/angular-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | 7 | ( 8 | 9 | /** 10 | * @ngdoc interface 11 | * @name angular.Module 12 | * @description 13 | * 14 | * Interface for configuring angular {@link angular.module modules}. 15 | */ 16 | 17 | function setupModuleLoader(window) { 18 | 19 | function ensure(obj, name, factory) { 20 | return obj[name] || (obj[name] = factory()); 21 | } 22 | 23 | return ensure(ensure(window, 'angular', Object), 'module', function() { 24 | /** @type {Object.} */ 25 | var modules = {}; 26 | 27 | /** 28 | * @ngdoc function 29 | * @name angular.module 30 | * @description 31 | * 32 | * The `angular.module` is a global place for creating and registering Angular modules. All 33 | * modules (angular core or 3rd party) that should be available to an application must be 34 | * registered using this mechanism. 35 | * 36 | * 37 | * # Module 38 | * 39 | * A module is a collocation of services, directives, filters, and configuration information. Module 40 | * is used to configure the {@link AUTO.$injector $injector}. 41 | * 42 | *
 43 |      * // Create a new module
 44 |      * var myModule = angular.module('myModule', []);
 45 |      *
 46 |      * // register a new service
 47 |      * myModule.value('appName', 'MyCoolApp');
 48 |      *
 49 |      * // configure existing services inside initialization blocks.
 50 |      * myModule.config(function($locationProvider) {
 51 | 'use strict';
 52 |      *   // Configure existing providers
 53 |      *   $locationProvider.hashPrefix('!');
 54 |      * });
 55 |      * 
56 | * 57 | * Then you can create an injector and load your modules like this: 58 | * 59 | *
 60 |      * var injector = angular.injector(['ng', 'MyModule'])
 61 |      * 
62 | * 63 | * However it's more likely that you'll just use 64 | * {@link ng.directive:ngApp ngApp} or 65 | * {@link angular.bootstrap} to simplify this process for you. 66 | * 67 | * @param {!string} name The name of the module to create or retrieve. 68 | * @param {Array.=} requires If specified then new module is being created. If unspecified then the 69 | * the module is being retrieved for further configuration. 70 | * @param {Function} configFn Optional configuration function for the module. Same as 71 | * {@link angular.Module#config Module#config()}. 72 | * @returns {module} new module with the {@link angular.Module} api. 73 | */ 74 | return function module(name, requires, configFn) { 75 | if (requires && modules.hasOwnProperty(name)) { 76 | modules[name] = null; 77 | } 78 | return ensure(modules, name, function() { 79 | if (!requires) { 80 | throw Error('No module: ' + name); 81 | } 82 | 83 | /** @type {!Array.>} */ 84 | var invokeQueue = []; 85 | 86 | /** @type {!Array.} */ 87 | var runBlocks = []; 88 | 89 | var config = invokeLater('$injector', 'invoke'); 90 | 91 | /** @type {angular.Module} */ 92 | var moduleInstance = { 93 | // Private state 94 | _invokeQueue: invokeQueue, 95 | _runBlocks: runBlocks, 96 | 97 | /** 98 | * @ngdoc property 99 | * @name angular.Module#requires 100 | * @propertyOf angular.Module 101 | * @returns {Array.} List of module names which must be loaded before this module. 102 | * @description 103 | * Holds the list of modules which the injector will load before the current module is loaded. 104 | */ 105 | requires: requires, 106 | 107 | /** 108 | * @ngdoc property 109 | * @name angular.Module#name 110 | * @propertyOf angular.Module 111 | * @returns {string} Name of the module. 112 | * @description 113 | */ 114 | name: name, 115 | 116 | 117 | /** 118 | * @ngdoc method 119 | * @name angular.Module#provider 120 | * @methodOf angular.Module 121 | * @param {string} name service name 122 | * @param {Function} providerType Construction function for creating new instance of the service. 123 | * @description 124 | * See {@link AUTO.$provide#provider $provide.provider()}. 125 | */ 126 | provider: invokeLater('$provide', 'provider'), 127 | 128 | /** 129 | * @ngdoc method 130 | * @name angular.Module#factory 131 | * @methodOf angular.Module 132 | * @param {string} name service name 133 | * @param {Function} providerFunction Function for creating new instance of the service. 134 | * @description 135 | * See {@link AUTO.$provide#factory $provide.factory()}. 136 | */ 137 | factory: invokeLater('$provide', 'factory'), 138 | 139 | /** 140 | * @ngdoc method 141 | * @name angular.Module#service 142 | * @methodOf angular.Module 143 | * @param {string} name service name 144 | * @param {Function} constructor A constructor function that will be instantiated. 145 | * @description 146 | * See {@link AUTO.$provide#service $provide.service()}. 147 | */ 148 | service: invokeLater('$provide', 'service'), 149 | 150 | /** 151 | * @ngdoc method 152 | * @name angular.Module#value 153 | * @methodOf angular.Module 154 | * @param {string} name service name 155 | * @param {*} object Service instance object. 156 | * @description 157 | * See {@link AUTO.$provide#value $provide.value()}. 158 | */ 159 | value: invokeLater('$provide', 'value'), 160 | 161 | /** 162 | * @ngdoc method 163 | * @name angular.Module#constant 164 | * @methodOf angular.Module 165 | * @param {string} name constant name 166 | * @param {*} object Constant value. 167 | * @description 168 | * Because the constant are fixed, they get applied before other provide methods. 169 | * See {@link AUTO.$provide#constant $provide.constant()}. 170 | */ 171 | constant: invokeLater('$provide', 'constant', 'unshift'), 172 | 173 | /** 174 | * @ngdoc method 175 | * @name angular.Module#filter 176 | * @methodOf angular.Module 177 | * @param {string} name Filter name. 178 | * @param {Function} filterFactory Factory function for creating new instance of filter. 179 | * @description 180 | * See {@link ng.$filterProvider#register $filterProvider.register()}. 181 | */ 182 | filter: invokeLater('$filterProvider', 'register'), 183 | 184 | /** 185 | * @ngdoc method 186 | * @name angular.Module#controller 187 | * @methodOf angular.Module 188 | * @param {string} name Controller name. 189 | * @param {Function} constructor Controller constructor function. 190 | * @description 191 | * See {@link ng.$controllerProvider#register $controllerProvider.register()}. 192 | */ 193 | controller: invokeLater('$controllerProvider', 'register'), 194 | 195 | /** 196 | * @ngdoc method 197 | * @name angular.Module#directive 198 | * @methodOf angular.Module 199 | * @param {string} name directive name 200 | * @param {Function} directiveFactory Factory function for creating new instance of 201 | * directives. 202 | * @description 203 | * See {@link ng.$compileProvider#directive $compileProvider.directive()}. 204 | */ 205 | directive: invokeLater('$compileProvider', 'directive'), 206 | 207 | /** 208 | * @ngdoc method 209 | * @name angular.Module#config 210 | * @methodOf angular.Module 211 | * @param {Function} configFn Execute this function on module load. Useful for service 212 | * configuration. 213 | * @description 214 | * Use this method to register work which needs to be performed on module loading. 215 | */ 216 | config: config, 217 | 218 | /** 219 | * @ngdoc method 220 | * @name angular.Module#run 221 | * @methodOf angular.Module 222 | * @param {Function} initializationFn Execute this function after injector creation. 223 | * Useful for application initialization. 224 | * @description 225 | * Use this method to register work which should be performed when the injector is done 226 | * loading all modules. 227 | */ 228 | run: function(block) { 229 | runBlocks.push(block); 230 | return this; 231 | } 232 | }; 233 | 234 | if (configFn) { 235 | config(configFn); 236 | } 237 | 238 | return moduleInstance; 239 | 240 | /** 241 | * @param {string} provider 242 | * @param {string} method 243 | * @param {String=} insertMethod 244 | * @returns {angular.Module} 245 | */ 246 | function invokeLater(provider, method, insertMethod) { 247 | return function() { 248 | invokeQueue[insertMethod || 'push']([provider, method, arguments]); 249 | return moduleInstance; 250 | } 251 | } 252 | }); 253 | }; 254 | }); 255 | 256 | } 257 | 258 | )(window); 259 | 260 | /** 261 | * Closure compiler type information 262 | * 263 | * @typedef { { 264 | * requires: !Array., 265 | * invokeQueue: !Array.>, 266 | * 267 | * service: function(string, Function):angular.Module, 268 | * factory: function(string, Function):angular.Module, 269 | * value: function(string, *):angular.Module, 270 | * 271 | * filter: function(string, Function):angular.Module, 272 | * 273 | * init: function(Function):angular.Module 274 | * } } 275 | */ 276 | angular.Module; 277 | 278 | -------------------------------------------------------------------------------- /app/lib/angular/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.6 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(i){'use strict';function d(c,b,e){return c[b]||(c[b]=e())}return d(d(i,"angular",Object),"module",function(){var c={};return function(b,e,f){e&&c.hasOwnProperty(b)&&(c[b]=null);return d(c,b,function(){function a(a,b,d){return function(){c[d||"push"]([a,b,arguments]);return g}}if(!e)throw Error("No module: "+b);var c=[],d=[],h=a("$injector","invoke"),g={_invokeQueue:c,_runBlocks:d,requires:e,name:b,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"), 7 | value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:h,run:function(a){d.push(a);return this}};f&&h(f);return g})}})})(window); 8 | -------------------------------------------------------------------------------- /app/lib/angular/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngResource 12 | * @description 13 | */ 14 | 15 | /** 16 | * @ngdoc object 17 | * @name ngResource.$resource 18 | * @requires $http 19 | * 20 | * @description 21 | * A factory which creates a resource object that lets you interact with 22 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 23 | * 24 | * The returned resource object has action methods which provide high-level behaviors without 25 | * the need to interact with the low level {@link ng.$http $http} service. 26 | * 27 | * # Installation 28 | * To use $resource make sure you have included the `angular-resource.js` that comes in Angular 29 | * package. You can also find this file on Google CDN, bower as well as at 30 | * {@link http://code.angularjs.org/ code.angularjs.org}. 31 | * 32 | * Finally load the module in your application: 33 | * 34 | * angular.module('app', ['ngResource']); 35 | * 36 | * and you are ready to get started! 37 | * 38 | * @param {string} url A parameterized URL template with parameters prefixed by `:` as in 39 | * `/user/:username`. If you are using a URL with a port number (e.g. 40 | * `http://example.com:8080/api`), you'll need to escape the colon character before the port 41 | * number, like this: `$resource('http://example.com\\:8080/api')`. 42 | * 43 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 44 | * `actions` methods. 45 | * 46 | * Each key value in the parameter object is first bound to url template if present and then any 47 | * excess keys are appended to the url search query after the `?`. 48 | * 49 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 50 | * URL `/path/greet?salutation=Hello`. 51 | * 52 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 53 | * the data object (useful for non-GET operations). 54 | * 55 | * @param {Object.=} actions Hash with declaration of custom action that should extend the 56 | * default set of resource actions. The declaration should be created in the following format: 57 | * 58 | * {action1: {method:?, params:?, isArray:?}, 59 | * action2: {method:?, params:?, isArray:?}, 60 | * ...} 61 | * 62 | * Where: 63 | * 64 | * - `action` – {string} – The name of action. This name becomes the name of the method on your 65 | * resource object. 66 | * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, 67 | * and `JSONP` 68 | * - `params` – {object=} – Optional set of pre-bound parameters for this action. 69 | * - isArray – {boolean=} – If true then the returned object for this action is an array, see 70 | * `returns` section. 71 | * 72 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 73 | * optionally extended with custom `actions`. The default set contains these actions: 74 | * 75 | * { 'get': {method:'GET'}, 76 | * 'save': {method:'POST'}, 77 | * 'query': {method:'GET', isArray:true}, 78 | * 'remove': {method:'DELETE'}, 79 | * 'delete': {method:'DELETE'} }; 80 | * 81 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 82 | * destination and parameters. When the data is returned from the server then the object is an 83 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 84 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 85 | * read, update, delete) on server-side data like this: 86 | *
 87 |         var User = $resource('/user/:userId', {userId:'@id'});
 88 |         var user = User.get({userId:123}, function() {
 89 |           user.abc = true;
 90 |           user.$save();
 91 |         });
 92 |      
93 | * 94 | * It is important to realize that invoking a $resource object method immediately returns an 95 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 96 | * server the existing reference is populated with the actual data. This is a useful trick since 97 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 98 | * object results in no rendering, once the data arrives from the server then the object is 99 | * populated with the data and the view automatically re-renders itself showing the new data. This 100 | * means that in most case one never has to write a callback function for the action methods. 101 | * 102 | * The action methods on the class object or instance object can be invoked with the following 103 | * parameters: 104 | * 105 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 106 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 107 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 108 | * 109 | * 110 | * @example 111 | * 112 | * # Credit card resource 113 | * 114 | *
115 |      // Define CreditCard class
116 |      var CreditCard = $resource('/user/:userId/card/:cardId',
117 |       {userId:123, cardId:'@id'}, {
118 |        charge: {method:'POST', params:{charge:true}}
119 |       });
120 | 
121 |      // We can retrieve a collection from the server
122 |      var cards = CreditCard.query(function() {
123 |        // GET: /user/123/card
124 |        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
125 | 
126 |        var card = cards[0];
127 |        // each item is an instance of CreditCard
128 |        expect(card instanceof CreditCard).toEqual(true);
129 |        card.name = "J. Smith";
130 |        // non GET methods are mapped onto the instances
131 |        card.$save();
132 |        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
133 |        // server returns: {id:456, number:'1234', name: 'J. Smith'};
134 | 
135 |        // our custom method is mapped as well.
136 |        card.$charge({amount:9.99});
137 |        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
138 |      });
139 | 
140 |      // we can create an instance as well
141 |      var newCard = new CreditCard({number:'0123'});
142 |      newCard.name = "Mike Smith";
143 |      newCard.$save();
144 |      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
145 |      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
146 |      expect(newCard.id).toEqual(789);
147 |  * 
148 | * 149 | * The object returned from this function execution is a resource "class" which has "static" method 150 | * for each action in the definition. 151 | * 152 | * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. 153 | * When the data is returned from the server then the object is an instance of the resource type and 154 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 155 | * operations (create, read, update, delete) on server-side data. 156 | 157 |
158 |      var User = $resource('/user/:userId', {userId:'@id'});
159 |      var user = User.get({userId:123}, function() {
160 |        user.abc = true;
161 |        user.$save();
162 |      });
163 |    
164 | * 165 | * It's worth noting that the success callback for `get`, `query` and other method gets passed 166 | * in the response that came from the server as well as $http header getter function, so one 167 | * could rewrite the above example and get access to http headers as: 168 | * 169 |
170 |      var User = $resource('/user/:userId', {userId:'@id'});
171 |      User.get({userId:123}, function(u, getResponseHeaders){
172 |        u.abc = true;
173 |        u.$save(function(u, putResponseHeaders) {
174 |          //u => saved user object
175 |          //putResponseHeaders => $http header getter
176 |        });
177 |      });
178 |    
179 | 180 | * # Buzz client 181 | 182 | Let's look at what a buzz client created with the `$resource` service looks like: 183 | 184 | 185 | 205 | 206 |
207 | 208 | 209 |
210 |
211 |

212 | 213 | {{item.actor.name}} 214 | Expand replies: {{item.links.replies[0].count}} 215 |

216 | {{item.object.content | html}} 217 |
218 | 219 | {{reply.actor.name}}: {{reply.content | html}} 220 |
221 |
222 |
223 |
224 | 225 | 226 |
227 | */ 228 | angular.module('ngResource', ['ng']). 229 | factory('$resource', ['$http', '$parse', function($http, $parse) { 230 | var DEFAULT_ACTIONS = { 231 | 'get': {method:'GET'}, 232 | 'save': {method:'POST'}, 233 | 'query': {method:'GET', isArray:true}, 234 | 'remove': {method:'DELETE'}, 235 | 'delete': {method:'DELETE'} 236 | }; 237 | var noop = angular.noop, 238 | forEach = angular.forEach, 239 | extend = angular.extend, 240 | copy = angular.copy, 241 | isFunction = angular.isFunction, 242 | getter = function(obj, path) { 243 | return $parse(path)(obj); 244 | }; 245 | 246 | /** 247 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 248 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 249 | * segments: 250 | * segment = *pchar 251 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 252 | * pct-encoded = "%" HEXDIG HEXDIG 253 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 254 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 255 | * / "*" / "+" / "," / ";" / "=" 256 | */ 257 | function encodeUriSegment(val) { 258 | return encodeUriQuery(val, true). 259 | replace(/%26/gi, '&'). 260 | replace(/%3D/gi, '='). 261 | replace(/%2B/gi, '+'); 262 | } 263 | 264 | 265 | /** 266 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom 267 | * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be 268 | * encoded per http://tools.ietf.org/html/rfc3986: 269 | * query = *( pchar / "/" / "?" ) 270 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 271 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 272 | * pct-encoded = "%" HEXDIG HEXDIG 273 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 274 | * / "*" / "+" / "," / ";" / "=" 275 | */ 276 | function encodeUriQuery(val, pctEncodeSpaces) { 277 | return encodeURIComponent(val). 278 | replace(/%40/gi, '@'). 279 | replace(/%3A/gi, ':'). 280 | replace(/%24/g, '$'). 281 | replace(/%2C/gi, ','). 282 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 283 | } 284 | 285 | function Route(template, defaults) { 286 | this.template = template = template + '#'; 287 | this.defaults = defaults || {}; 288 | var urlParams = this.urlParams = {}; 289 | forEach(template.split(/\W/), function(param){ 290 | if (param && (new RegExp("(^|[^\\\\]):" + param + "\\W").test(template))) { 291 | urlParams[param] = true; 292 | } 293 | }); 294 | this.template = template.replace(/\\:/g, ':'); 295 | } 296 | 297 | Route.prototype = { 298 | url: function(params) { 299 | var self = this, 300 | url = this.template, 301 | val, 302 | encodedVal; 303 | 304 | params = params || {}; 305 | forEach(this.urlParams, function(_, urlParam){ 306 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 307 | if (angular.isDefined(val) && val !== null) { 308 | encodedVal = encodeUriSegment(val); 309 | url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1"); 310 | } else { 311 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W)", "g"), function(match, 312 | leadingSlashes, tail) { 313 | if (tail.charAt(0) == '/') { 314 | return tail; 315 | } else { 316 | return leadingSlashes + tail; 317 | } 318 | }); 319 | } 320 | }); 321 | url = url.replace(/\/?#$/, ''); 322 | var query = []; 323 | forEach(params, function(value, key){ 324 | if (!self.urlParams[key]) { 325 | query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); 326 | } 327 | }); 328 | query.sort(); 329 | url = url.replace(/\/*$/, ''); 330 | return url + (query.length ? '?' + query.join('&') : ''); 331 | } 332 | }; 333 | 334 | 335 | function ResourceFactory(url, paramDefaults, actions) { 336 | var route = new Route(url); 337 | 338 | actions = extend({}, DEFAULT_ACTIONS, actions); 339 | 340 | function extractParams(data, actionParams){ 341 | var ids = {}; 342 | actionParams = extend({}, paramDefaults, actionParams); 343 | forEach(actionParams, function(value, key){ 344 | ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; 345 | }); 346 | return ids; 347 | } 348 | 349 | function Resource(value){ 350 | copy(value || {}, this); 351 | } 352 | 353 | forEach(actions, function(action, name) { 354 | action.method = angular.uppercase(action.method); 355 | var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH'; 356 | Resource[name] = function(a1, a2, a3, a4) { 357 | var params = {}; 358 | var data; 359 | var success = noop; 360 | var error = null; 361 | switch(arguments.length) { 362 | case 4: 363 | error = a4; 364 | success = a3; 365 | //fallthrough 366 | case 3: 367 | case 2: 368 | if (isFunction(a2)) { 369 | if (isFunction(a1)) { 370 | success = a1; 371 | error = a2; 372 | break; 373 | } 374 | 375 | success = a2; 376 | error = a3; 377 | //fallthrough 378 | } else { 379 | params = a1; 380 | data = a2; 381 | success = a3; 382 | break; 383 | } 384 | case 1: 385 | if (isFunction(a1)) success = a1; 386 | else if (hasBody) data = a1; 387 | else params = a1; 388 | break; 389 | case 0: break; 390 | default: 391 | throw "Expected between 0-4 arguments [params, data, success, error], got " + 392 | arguments.length + " arguments."; 393 | } 394 | 395 | var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); 396 | $http({ 397 | method: action.method, 398 | url: route.url(extend({}, extractParams(data, action.params || {}), params)), 399 | data: data 400 | }).then(function(response) { 401 | var data = response.data; 402 | 403 | if (data) { 404 | if (action.isArray) { 405 | value.length = 0; 406 | forEach(data, function(item) { 407 | value.push(new Resource(item)); 408 | }); 409 | } else { 410 | copy(data, value); 411 | } 412 | } 413 | (success||noop)(value, response.headers); 414 | }, error); 415 | 416 | return value; 417 | }; 418 | 419 | 420 | Resource.prototype['$' + name] = function(a1, a2, a3) { 421 | var params = extractParams(this), 422 | success = noop, 423 | error; 424 | 425 | switch(arguments.length) { 426 | case 3: params = a1; success = a2; error = a3; break; 427 | case 2: 428 | case 1: 429 | if (isFunction(a1)) { 430 | success = a1; 431 | error = a2; 432 | } else { 433 | params = a1; 434 | success = a2 || noop; 435 | } 436 | case 0: break; 437 | default: 438 | throw "Expected between 1-3 arguments [params, success, error], got " + 439 | arguments.length + " arguments."; 440 | } 441 | var data = hasBody ? this : undefined; 442 | Resource[name].call(this, params, data, success, error); 443 | }; 444 | }); 445 | 446 | Resource.bind = function(additionalParamDefaults){ 447 | return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 448 | }; 449 | 450 | return Resource; 451 | } 452 | 453 | return ResourceFactory; 454 | }]); 455 | 456 | 457 | })(window, window.angular); 458 | -------------------------------------------------------------------------------- /app/lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.6 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(C,d,w){'use strict';d.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(x,y){function s(b,e){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,e?"%20":"+")}function t(b,e){this.template=b+="#";this.defaults=e||{};var a=this.urlParams={};h(b.split(/\W/),function(f){f&&RegExp("(^|[^\\\\]):"+f+"\\W").test(b)&&(a[f]=!0)});this.template=b.replace(/\\:/g,":")}function u(b,e,a){function f(m,a){var b= 7 | {},a=o({},e,a);h(a,function(a,z){var c;a.charAt&&a.charAt(0)=="@"?(c=a.substr(1),c=y(c)(m)):c=a;b[z]=c});return b}function g(a){v(a||{},this)}var k=new t(b),a=o({},A,a);h(a,function(a,b){a.method=d.uppercase(a.method);var e=a.method=="POST"||a.method=="PUT"||a.method=="PATCH";g[b]=function(b,c,d,B){var j={},i,l=p,q=null;switch(arguments.length){case 4:q=B,l=d;case 3:case 2:if(r(c)){if(r(b)){l=b;q=c;break}l=c;q=d}else{j=b;i=c;l=d;break}case 1:r(b)?l=b:e?i=b:j=b;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+ 8 | arguments.length+" arguments.";}var n=this instanceof g?this:a.isArray?[]:new g(i);x({method:a.method,url:k.url(o({},f(i,a.params||{}),j)),data:i}).then(function(b){var c=b.data;if(c)a.isArray?(n.length=0,h(c,function(a){n.push(new g(a))})):v(c,n);(l||p)(n,b.headers)},q);return n};g.prototype["$"+b]=function(a,d,h){var m=f(this),j=p,i;switch(arguments.length){case 3:m=a;j=d;i=h;break;case 2:case 1:r(a)?(j=a,i=d):(m=a,j=d||p);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+ 9 | arguments.length+" arguments.";}g[b].call(this,m,e?this:w,j,i)}});g.bind=function(d){return u(b,o({},e,d),a)};return g}var A={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},p=d.noop,h=d.forEach,o=d.extend,v=d.copy,r=d.isFunction;t.prototype={url:function(b){var e=this,a=this.template,f,g,b=b||{};h(this.urlParams,function(h,c){f=b.hasOwnProperty(c)?b[c]:e.defaults[c];d.isDefined(f)&&f!==null?(g=s(f,!0).replace(/%26/gi,"&").replace(/%3D/gi, 10 | "=").replace(/%2B/gi,"+"),a=a.replace(RegExp(":"+c+"(\\W)","g"),g+"$1")):a=a.replace(RegExp("(/?):"+c+"(\\W)","g"),function(a,b,c){return c.charAt(0)=="/"?c:b+c})});var a=a.replace(/\/?#$/,""),k=[];h(b,function(a,b){e.urlParams[b]||k.push(s(b)+"="+s(a))});k.sort();a=a.replace(/\/*$/,"");return a+(k.length?"?"+k.join("&"):"")}};return u}])})(window,window.angular); 11 | -------------------------------------------------------------------------------- /app/lib/angular/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngSanitize 12 | * @description 13 | */ 14 | 15 | /* 16 | * HTML Parser By Misko Hevery (misko@hevery.com) 17 | * based on: HTML Parser By John Resig (ejohn.org) 18 | * Original code by Erik Arvidsson, Mozilla Public License 19 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 20 | * 21 | * // Use like so: 22 | * htmlParser(htmlString, { 23 | * start: function(tag, attrs, unary) {}, 24 | * end: function(tag) {}, 25 | * chars: function(text) {}, 26 | * comment: function(text) {} 27 | * }); 28 | * 29 | */ 30 | 31 | 32 | /** 33 | * @ngdoc service 34 | * @name ngSanitize.$sanitize 35 | * @function 36 | * 37 | * @description 38 | * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are 39 | * then serialized back to properly escaped html string. This means that no unsafe input can make 40 | * it into the returned string, however, since our parser is more strict than a typical browser 41 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 42 | * browser, won't make it through the sanitizer. 43 | * 44 | * @param {string} html Html input. 45 | * @returns {string} Sanitized html. 46 | * 47 | * @example 48 | 49 | 50 | 58 |
59 | Snippet: 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
FilterSourceRendered
html filter 69 |
<div ng-bind-html="snippet">
</div>
70 |
72 |
73 |
no filter
<div ng-bind="snippet">
</div>
unsafe html filter
<div ng-bind-html-unsafe="snippet">
</div>
86 |
87 |
88 | 89 | it('should sanitize the html snippet ', function() { 90 | expect(using('#html-filter').element('div').html()). 91 | toBe('

an html\nclick here\nsnippet

'); 92 | }); 93 | 94 | it('should escape snippet without any filter', function() { 95 | expect(using('#escaped-html').element('div').html()). 96 | toBe("<p style=\"color:blue\">an html\n" + 97 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 98 | "snippet</p>"); 99 | }); 100 | 101 | it('should inline raw snippet if filtered as unsafe', function() { 102 | expect(using('#html-unsafe-filter').element("div").html()). 103 | toBe("

an html\n" + 104 | "click here\n" + 105 | "snippet

"); 106 | }); 107 | 108 | it('should update', function() { 109 | input('snippet').enter('new text'); 110 | expect(using('#html-filter').binding('snippet')).toBe('new text'); 111 | expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); 112 | expect(using('#html-unsafe-filter').binding("snippet")).toBe('new text'); 113 | }); 114 |
115 |
116 | */ 117 | var $sanitize = function(html) { 118 | var buf = []; 119 | htmlParser(html, htmlSanitizeWriter(buf)); 120 | return buf.join(''); 121 | }; 122 | 123 | 124 | // Regular Expressions for parsing tags and attributes 125 | var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, 126 | END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, 127 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 128 | BEGIN_TAG_REGEXP = /^/g, 131 | CDATA_REGEXP = //g, 132 | URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, 133 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) 134 | 135 | 136 | // Good source of info about elements and attributes 137 | // http://dev.w3.org/html5/spec/Overview.html#semantics 138 | // http://simon.html5.org/html-elements 139 | 140 | // Safe Void Elements - HTML5 141 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 142 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 143 | 144 | // Elements that you can, intentionally, leave open (and which close themselves) 145 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 146 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 147 | optionalEndTagInlineElements = makeMap("rp,rt"), 148 | optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); 149 | 150 | // Safe Block Elements - HTML5 151 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + 152 | "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + 153 | "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 154 | 155 | // Inline Elements - HTML5 156 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + 157 | "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + 158 | "span,strike,strong,sub,sup,time,tt,u,var")); 159 | 160 | 161 | // Special Elements (can contain anything) 162 | var specialElements = makeMap("script,style"); 163 | 164 | var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); 165 | 166 | //Attributes that have href and hence need to be sanitized 167 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); 168 | var validAttrs = angular.extend({}, uriAttrs, makeMap( 169 | 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 170 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 171 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 172 | 'scope,scrolling,shape,span,start,summary,target,title,type,'+ 173 | 'valign,value,vspace,width')); 174 | 175 | function makeMap(str) { 176 | var obj = {}, items = str.split(','), i; 177 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 178 | return obj; 179 | } 180 | 181 | 182 | /** 183 | * @example 184 | * htmlParser(htmlString, { 185 | * start: function(tag, attrs, unary) {}, 186 | * end: function(tag) {}, 187 | * chars: function(text) {}, 188 | * comment: function(text) {} 189 | * }); 190 | * 191 | * @param {string} html string 192 | * @param {object} handler 193 | */ 194 | function htmlParser( html, handler ) { 195 | var index, chars, match, stack = [], last = html; 196 | stack.last = function() { return stack[ stack.length - 1 ]; }; 197 | 198 | while ( html ) { 199 | chars = true; 200 | 201 | // Make sure we're not in a script or style element 202 | if ( !stack.last() || !specialElements[ stack.last() ] ) { 203 | 204 | // Comment 205 | if ( html.indexOf(""); 207 | 208 | if ( index >= 0 ) { 209 | if (handler.comment) handler.comment( html.substring( 4, index ) ); 210 | html = html.substring( index + 3 ); 211 | chars = false; 212 | } 213 | 214 | // end tag 215 | } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { 216 | match = html.match( END_TAG_REGEXP ); 217 | 218 | if ( match ) { 219 | html = html.substring( match[0].length ); 220 | match[0].replace( END_TAG_REGEXP, parseEndTag ); 221 | chars = false; 222 | } 223 | 224 | // start tag 225 | } else if ( BEGIN_TAG_REGEXP.test(html) ) { 226 | match = html.match( START_TAG_REGEXP ); 227 | 228 | if ( match ) { 229 | html = html.substring( match[0].length ); 230 | match[0].replace( START_TAG_REGEXP, parseStartTag ); 231 | chars = false; 232 | } 233 | } 234 | 235 | if ( chars ) { 236 | index = html.indexOf("<"); 237 | 238 | var text = index < 0 ? html : html.substring( 0, index ); 239 | html = index < 0 ? "" : html.substring( index ); 240 | 241 | if (handler.chars) handler.chars( decodeEntities(text) ); 242 | } 243 | 244 | } else { 245 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ 246 | text = text. 247 | replace(COMMENT_REGEXP, "$1"). 248 | replace(CDATA_REGEXP, "$1"); 249 | 250 | if (handler.chars) handler.chars( decodeEntities(text) ); 251 | 252 | return ""; 253 | }); 254 | 255 | parseEndTag( "", stack.last() ); 256 | } 257 | 258 | if ( html == last ) { 259 | throw "Parse Error: " + html; 260 | } 261 | last = html; 262 | } 263 | 264 | // Clean up any remaining tags 265 | parseEndTag(); 266 | 267 | function parseStartTag( tag, tagName, rest, unary ) { 268 | tagName = angular.lowercase(tagName); 269 | if ( blockElements[ tagName ] ) { 270 | while ( stack.last() && inlineElements[ stack.last() ] ) { 271 | parseEndTag( "", stack.last() ); 272 | } 273 | } 274 | 275 | if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { 276 | parseEndTag( "", tagName ); 277 | } 278 | 279 | unary = voidElements[ tagName ] || !!unary; 280 | 281 | if ( !unary ) 282 | stack.push( tagName ); 283 | 284 | var attrs = {}; 285 | 286 | rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { 287 | var value = doubleQuotedValue 288 | || singleQoutedValue 289 | || unqoutedValue 290 | || ''; 291 | 292 | attrs[name] = decodeEntities(value); 293 | }); 294 | if (handler.start) handler.start( tagName, attrs, unary ); 295 | } 296 | 297 | function parseEndTag( tag, tagName ) { 298 | var pos = 0, i; 299 | tagName = angular.lowercase(tagName); 300 | if ( tagName ) 301 | // Find the closest opened tag of the same type 302 | for ( pos = stack.length - 1; pos >= 0; pos-- ) 303 | if ( stack[ pos ] == tagName ) 304 | break; 305 | 306 | if ( pos >= 0 ) { 307 | // Close all the open elements, up the stack 308 | for ( i = stack.length - 1; i >= pos; i-- ) 309 | if (handler.end) handler.end( stack[ i ] ); 310 | 311 | // Remove the open elements from the stack 312 | stack.length = pos; 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * decodes all entities into regular string 319 | * @param value 320 | * @returns {string} A string with decoded entities. 321 | */ 322 | var hiddenPre=document.createElement("pre"); 323 | function decodeEntities(value) { 324 | hiddenPre.innerHTML=value.replace(//g, '>'); 343 | } 344 | 345 | /** 346 | * create an HTML/XML writer which writes to buffer 347 | * @param {Array} buf use buf.jain('') to get out sanitized html string 348 | * @returns {object} in the form of { 349 | * start: function(tag, attrs, unary) {}, 350 | * end: function(tag) {}, 351 | * chars: function(text) {}, 352 | * comment: function(text) {} 353 | * } 354 | */ 355 | function htmlSanitizeWriter(buf){ 356 | var ignore = false; 357 | var out = angular.bind(buf, buf.push); 358 | return { 359 | start: function(tag, attrs, unary){ 360 | tag = angular.lowercase(tag); 361 | if (!ignore && specialElements[tag]) { 362 | ignore = tag; 363 | } 364 | if (!ignore && validElements[tag] == true) { 365 | out('<'); 366 | out(tag); 367 | angular.forEach(attrs, function(value, key){ 368 | var lkey=angular.lowercase(key); 369 | if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { 370 | out(' '); 371 | out(key); 372 | out('="'); 373 | out(encodeEntities(value)); 374 | out('"'); 375 | } 376 | }); 377 | out(unary ? '/>' : '>'); 378 | } 379 | }, 380 | end: function(tag){ 381 | tag = angular.lowercase(tag); 382 | if (!ignore && validElements[tag] == true) { 383 | out(''); 386 | } 387 | if (tag == ignore) { 388 | ignore = false; 389 | } 390 | }, 391 | chars: function(chars){ 392 | if (!ignore) { 393 | out(encodeEntities(chars)); 394 | } 395 | } 396 | }; 397 | } 398 | 399 | 400 | // define ngSanitize module and register $sanitize service 401 | angular.module('ngSanitize', []).value('$sanitize', $sanitize); 402 | 403 | /** 404 | * @ngdoc directive 405 | * @name ngSanitize.directive:ngBindHtml 406 | * 407 | * @description 408 | * Creates a binding that will sanitize the result of evaluating the `expression` with the 409 | * {@link ngSanitize.$sanitize $sanitize} service and innerHTML the result into the current element. 410 | * 411 | * See {@link ngSanitize.$sanitize $sanitize} docs for examples. 412 | * 413 | * @element ANY 414 | * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. 415 | */ 416 | angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) { 417 | return function(scope, element, attr) { 418 | element.addClass('ng-binding').data('$binding', attr.ngBindHtml); 419 | scope.$watch(attr.ngBindHtml, function ngBindHtmlWatchAction(value) { 420 | value = $sanitize(value); 421 | element.html(value || ''); 422 | }); 423 | }; 424 | }]); 425 | 426 | /** 427 | * @ngdoc filter 428 | * @name ngSanitize.filter:linky 429 | * @function 430 | * 431 | * @description 432 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 433 | * plain email address links. 434 | * 435 | * @param {string} text Input text. 436 | * @returns {string} Html-linkified text. 437 | * 438 | * @usage 439 | 440 | * 441 | * @example 442 | 443 | 444 | 454 |
455 | Snippet: 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 467 | 470 | 471 | 472 | 473 | 474 | 475 | 476 |
FilterSourceRendered
linky filter 465 |
<div ng-bind-html="snippet | linky">
</div>
466 |
468 |
469 |
no filter
<div ng-bind="snippet">
</div>
477 | 478 | 479 | it('should linkify the snippet with urls', function() { 480 | expect(using('#linky-filter').binding('snippet | linky')). 481 | toBe('Pretty text with some links: ' + 482 | 'http://angularjs.org/, ' + 483 | 'us@somewhere.org, ' + 484 | 'another@somewhere.org, ' + 485 | 'and one more: ftp://127.0.0.1/.'); 486 | }); 487 | 488 | it ('should not linkify snippet without the linky filter', function() { 489 | expect(using('#escaped-html').binding('snippet')). 490 | toBe("Pretty text with some links:\n" + 491 | "http://angularjs.org/,\n" + 492 | "mailto:us@somewhere.org,\n" + 493 | "another@somewhere.org,\n" + 494 | "and one more: ftp://127.0.0.1/."); 495 | }); 496 | 497 | it('should update', function() { 498 | input('snippet').enter('new http://link.'); 499 | expect(using('#linky-filter').binding('snippet | linky')). 500 | toBe('new http://link.'); 501 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 502 | }); 503 | 504 | 505 | */ 506 | angular.module('ngSanitize').filter('linky', function() { 507 | var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, 508 | MAILTO_REGEXP = /^mailto:/; 509 | 510 | return function(text) { 511 | if (!text) return text; 512 | var match; 513 | var raw = text; 514 | var html = []; 515 | // TODO(vojta): use $sanitize instead 516 | var writer = htmlSanitizeWriter(html); 517 | var url; 518 | var i; 519 | while ((match = raw.match(LINKY_URL_REGEXP))) { 520 | // We can not end in these as they are sometimes found at the end of the sentence 521 | url = match[0]; 522 | // if we did not match ftp/http/mailto then assume mailto 523 | if (match[2] == match[3]) url = 'mailto:' + url; 524 | i = match.index; 525 | writer.chars(raw.substr(0, i)); 526 | writer.start('a', {href:url}); 527 | writer.chars(match[0].replace(MAILTO_REGEXP, '')); 528 | writer.end('a'); 529 | raw = raw.substring(i + match[0].length); 530 | } 531 | writer.chars(raw); 532 | return html.join(''); 533 | }; 534 | }); 535 | 536 | 537 | })(window, window.angular); 538 | -------------------------------------------------------------------------------- /app/lib/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.6 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,g){'use strict';function i(a){var d={},a=a.split(","),b;for(b=0;b=0;e--)if(f[e]==b)break;if(e>=0){for(c=f.length-1;c>=e;c--)d.end&&d.end(f[c]);f.length= 7 | e}}var c,h,f=[],j=a;for(f.last=function(){return f[f.length-1]};a;){h=!0;if(!f.last()||!q[f.last()]){if(a.indexOf("<\!--")===0)c=a.indexOf("--\>"),c>=0&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),h=!1);else if(B.test(a)){if(c=a.match(r))a=a.substring(c[0].length),c[0].replace(r,e),h=!1}else if(C.test(a)&&(c=a.match(s)))a=a.substring(c[0].length),c[0].replace(s,b),h=!1;h&&(c=a.indexOf("<"),h=c<0?a:a.substring(0,c),a=c<0?"":a.substring(c),d.chars&&d.chars(k(h)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+ 8 | f.last()+"[^>]*>","i"),function(b,a){a=a.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(a));return""}),e("",f.last());if(a==j)throw"Parse Error: "+a;j=a}e()}function k(a){l.innerHTML=a.replace(//g,">")}function u(a){var d=!1,b=g.bind(a,a.push);return{start:function(a,c,h){a=g.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]== 9 | !0&&(b("<"),b(a),g.forEach(c,function(a,c){var e=g.lowercase(c);if(G[e]==!0&&(w[e]!==!0||a.match(H)))b(" "),b(c),b('="'),b(t(a)),b('"')}),b(h?"/>":">"))},end:function(a){a=g.lowercase(a);!d&&v[a]==!0&&(b(""));a==d&&(d=!1)},chars:function(a){d||b(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^/g, 10 | E=//g,H=/^((ftp|https?):\/\/|mailto:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=g.extend({},y,x),m=g.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=g.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 11 | q=i("script,style"),v=g.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=g.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[]; 12 | z(a,u(d));return d.join("")});g.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,b,e){b.addClass("ng-binding").data("$binding",e.ngBindHtml);d.$watch(e.ngBindHtml,function(c){c=a(c);b.html(c||"")})}}]);g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(b){if(!b)return b;for(var e=b,c=[],h=u(c),f,g;b=e.match(a);)f=b[0],b[2]==b[3]&&(f="mailto:"+f),g=b.index, 13 | h.chars(e.substr(0,g)),h.start("a",{href:f}),h.chars(b[0].replace(d,"")),h.end("a"),e=e.substring(g+b[0].length);h.chars(e);return c.join("")}})})(window,window.angular); 14 | -------------------------------------------------------------------------------- /app/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/partials/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/app/partials/.gitignore -------------------------------------------------------------------------------- /app/partials/partial1.html: -------------------------------------------------------------------------------- 1 |

This is the partial for view 1.

2 | -------------------------------------------------------------------------------- /app/partials/partial2.html: -------------------------------------------------------------------------------- 1 |

This is the partial for view 2.

2 |

3 | Showing of 'interpolate' filter: 4 | {{ 'Current version is v%VERSION%.' | interpolate }} 5 |

6 | -------------------------------------------------------------------------------- /config/jsTestDriver-scenario.conf: -------------------------------------------------------------------------------- 1 | server: http://localhost:9877 2 | 3 | load: 4 | - test/lib/angular/angular-scenario.js 5 | - config/jstd-scenario-adapter-config.js 6 | - test/lib/angular/jstd-scenario-adapter.js 7 | - test/e2e/scenarios.js 8 | 9 | proxy: 10 | - {matcher: "*", server: "http://localhost:8000"} 11 | -------------------------------------------------------------------------------- /config/jsTestDriver.conf: -------------------------------------------------------------------------------- 1 | server: http://localhost:9876 2 | 3 | load: 4 | - test/lib/jasmine/jasmine.js 5 | - test/lib/jasmine-jstd-adapter/JasmineAdapter.js 6 | - app/lib/angular/angular.js 7 | - test/lib/angular/angular-mocks.js 8 | - app/js/*.js 9 | - test/unit/*.js 10 | 11 | exclude: 12 | -------------------------------------------------------------------------------- /config/jstd-scenario-adapter-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for jstd scenario adapter 3 | */ 4 | var jstdScenarioAdapter = { 5 | relativeUrlPrefix: '/test/e2e/' 6 | }; 7 | -------------------------------------------------------------------------------- /config/testacular-e2e.conf.js: -------------------------------------------------------------------------------- 1 | basePath = '../'; 2 | 3 | files = [ 4 | ANGULAR_SCENARIO, 5 | ANGULAR_SCENARIO_ADAPTER, 6 | 'test/e2e/**/*.js' 7 | ]; 8 | 9 | autoWatch = false; 10 | 11 | browsers = ['Chrome']; 12 | 13 | singleRun = true; 14 | 15 | proxies = { 16 | '/': 'http://localhost:8000/' 17 | }; 18 | 19 | junitReporter = { 20 | outputFile: 'test_out/e2e.xml', 21 | suite: 'e2e' 22 | }; 23 | -------------------------------------------------------------------------------- /config/testacular.conf.js: -------------------------------------------------------------------------------- 1 | basePath = '../'; 2 | 3 | files = [ 4 | JASMINE, 5 | JASMINE_ADAPTER, 6 | 'app/lib/angular/angular.js', 7 | 'app/lib/angular/angular-*.js', 8 | 'test/lib/angular/angular-mocks.js', 9 | 'app/js/**/*.js', 10 | 'test/unit/**/*.js' 11 | ]; 12 | 13 | autoWatch = true; 14 | 15 | browsers = ['Chrome']; 16 | 17 | junitReporter = { 18 | outputFile: 'test_out/unit.xml', 19 | suite: 'unit' 20 | }; 21 | -------------------------------------------------------------------------------- /files.js: -------------------------------------------------------------------------------- 1 | files = [ 2 | JASMINE, 3 | JASMINE_ADAPTER, 4 | 'app/lib/angular/angular.js', 5 | 'app/js/app.js', 6 | 'app/js/game.js', 7 | 'test/lib/angular/angular-mocks.js', 8 | 'test/unit/*.js' 9 | ]; 10 | autoWatch = true; 11 | autoWatchInterval = 1; 12 | logLevel = LOG_INFO; 13 | logColors = true; 14 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /scripts/e2e-test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for running e2e tests 4 | REM You have to run server and capture some browser first 5 | REM 6 | REM Requirements: 7 | REM - NodeJS (http://nodejs.org/) 8 | REM - Testacular (npm install -g testacular) 9 | 10 | set BASE_DIR=%~dp0 11 | testacular start "%BASE_DIR%\..\config\testacular-e2e.conf.js" %* 12 | -------------------------------------------------------------------------------- /scripts/e2e-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | 5 | echo "" 6 | echo "Starting Testacular Server (http://vojtajina.github.com/testacular)" 7 | echo "-------------------------------------------------------------------" 8 | 9 | testacular start $BASE_DIR/../config/testacular-e2e.conf.js $* 10 | -------------------------------------------------------------------------------- /scripts/test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for running unit tests 4 | REM You have to run server and capture some browser first 5 | REM 6 | REM Requirements: 7 | REM - NodeJS (http://nodejs.org/) 8 | REM - Testacular (npm install -g testacular) 9 | 10 | set BASE_DIR=%~dp0 11 | testacular start "%BASE_DIR%\..\config\testacular.conf.js" %* 12 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | 5 | echo "" 6 | echo "Starting Testacular Server (http://vojtajina.github.com/testacular)" 7 | echo "-------------------------------------------------------------------" 8 | 9 | testacular start $BASE_DIR/../config/testacular.conf.js $* 10 | -------------------------------------------------------------------------------- /scripts/watchr.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env watchr 2 | 3 | # config file for watchr http://github.com/mynyml/watchr 4 | # install: gem install watchr 5 | # run: watch watchr.rb 6 | # note: make sure that you have jstd server running (server.sh) and a browser captured 7 | 8 | log_file = File.expand_path(File.dirname(__FILE__) + '/../logs/jstd.log') 9 | 10 | `cd ..` 11 | `touch #{log_file}` 12 | 13 | puts "String watchr... log file: #{log_file}" 14 | 15 | watch( '(app/js|test/unit)' ) do 16 | `echo "\n\ntest run started @ \`date\`" > #{log_file}` 17 | `scripts/test.sh &> #{log_file}` 18 | end 19 | 20 | -------------------------------------------------------------------------------- /scripts/web-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var util = require('util'), 4 | http = require('http'), 5 | fs = require('fs'), 6 | url = require('url'), 7 | events = require('events'); 8 | 9 | var DEFAULT_PORT = 8000; 10 | 11 | function main(argv) { 12 | new HttpServer({ 13 | 'GET': createServlet(StaticServlet), 14 | 'HEAD': createServlet(StaticServlet) 15 | }).start(Number(argv[2]) || DEFAULT_PORT); 16 | } 17 | 18 | function escapeHtml(value) { 19 | return value.toString(). 20 | replace('<', '<'). 21 | replace('>', '>'). 22 | replace('"', '"'); 23 | } 24 | 25 | function createServlet(Class) { 26 | var servlet = new Class(); 27 | return servlet.handleRequest.bind(servlet); 28 | } 29 | 30 | /** 31 | * An Http server implementation that uses a map of methods to decide 32 | * action routing. 33 | * 34 | * @param {Object} Map of method => Handler function 35 | */ 36 | function HttpServer(handlers) { 37 | this.handlers = handlers; 38 | this.server = http.createServer(this.handleRequest_.bind(this)); 39 | } 40 | 41 | HttpServer.prototype.start = function(port) { 42 | this.port = port; 43 | this.server.listen(port); 44 | util.puts('Http Server running at http://localhost:' + port + '/'); 45 | }; 46 | 47 | HttpServer.prototype.parseUrl_ = function(urlString) { 48 | var parsed = url.parse(urlString); 49 | parsed.pathname = url.resolve('/', parsed.pathname); 50 | return url.parse(url.format(parsed), true); 51 | }; 52 | 53 | HttpServer.prototype.handleRequest_ = function(req, res) { 54 | var logEntry = req.method + ' ' + req.url; 55 | if (req.headers['user-agent']) { 56 | logEntry += ' ' + req.headers['user-agent']; 57 | } 58 | util.puts(logEntry); 59 | req.url = this.parseUrl_(req.url); 60 | var handler = this.handlers[req.method]; 61 | if (!handler) { 62 | res.writeHead(501); 63 | res.end(); 64 | } else { 65 | handler.call(this, req, res); 66 | } 67 | }; 68 | 69 | /** 70 | * Handles static content. 71 | */ 72 | function StaticServlet() {} 73 | 74 | StaticServlet.MimeMap = { 75 | 'txt': 'text/plain', 76 | 'html': 'text/html', 77 | 'css': 'text/css', 78 | 'xml': 'application/xml', 79 | 'json': 'application/json', 80 | 'js': 'application/javascript', 81 | 'jpg': 'image/jpeg', 82 | 'jpeg': 'image/jpeg', 83 | 'gif': 'image/gif', 84 | 'png': 'image/png', 85 |   'svg': 'image/svg+xml' 86 | }; 87 | 88 | StaticServlet.prototype.handleRequest = function(req, res) { 89 | var self = this; 90 | var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/g, function(match, hex){ 91 | return String.fromCharCode(parseInt(hex, 16)); 92 | }); 93 | var parts = path.split('/'); 94 | if (parts[parts.length-1].charAt(0) === '.') 95 | return self.sendForbidden_(req, res, path); 96 | fs.stat(path, function(err, stat) { 97 | if (err) 98 | return self.sendMissing_(req, res, path); 99 | if (stat.isDirectory()) 100 | return self.sendDirectory_(req, res, path); 101 | return self.sendFile_(req, res, path); 102 | }); 103 | } 104 | 105 | StaticServlet.prototype.sendError_ = function(req, res, error) { 106 | res.writeHead(500, { 107 | 'Content-Type': 'text/html' 108 | }); 109 | res.write('\n'); 110 | res.write('Internal Server Error\n'); 111 | res.write('

Internal Server Error

'); 112 | res.write('
' + escapeHtml(util.inspect(error)) + '
'); 113 | util.puts('500 Internal Server Error'); 114 | util.puts(util.inspect(error)); 115 | }; 116 | 117 | StaticServlet.prototype.sendMissing_ = function(req, res, path) { 118 | path = path.substring(1); 119 | res.writeHead(404, { 120 | 'Content-Type': 'text/html' 121 | }); 122 | res.write('\n'); 123 | res.write('404 Not Found\n'); 124 | res.write('

Not Found

'); 125 | res.write( 126 | '

The requested URL ' + 127 | escapeHtml(path) + 128 | ' was not found on this server.

' 129 | ); 130 | res.end(); 131 | util.puts('404 Not Found: ' + path); 132 | }; 133 | 134 | StaticServlet.prototype.sendForbidden_ = function(req, res, path) { 135 | path = path.substring(1); 136 | res.writeHead(403, { 137 | 'Content-Type': 'text/html' 138 | }); 139 | res.write('\n'); 140 | res.write('403 Forbidden\n'); 141 | res.write('

Forbidden

'); 142 | res.write( 143 | '

You do not have permission to access ' + 144 | escapeHtml(path) + ' on this server.

' 145 | ); 146 | res.end(); 147 | util.puts('403 Forbidden: ' + path); 148 | }; 149 | 150 | StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) { 151 | res.writeHead(301, { 152 | 'Content-Type': 'text/html', 153 | 'Location': redirectUrl 154 | }); 155 | res.write('\n'); 156 | res.write('301 Moved Permanently\n'); 157 | res.write('

Moved Permanently

'); 158 | res.write( 159 | '

The document has moved here.

' 162 | ); 163 | res.end(); 164 | util.puts('301 Moved Permanently: ' + redirectUrl); 165 | }; 166 | 167 | StaticServlet.prototype.sendFile_ = function(req, res, path) { 168 | var self = this; 169 | var file = fs.createReadStream(path); 170 | res.writeHead(200, { 171 | 'Content-Type': StaticServlet. 172 | MimeMap[path.split('.').pop()] || 'text/plain' 173 | }); 174 | if (req.method === 'HEAD') { 175 | res.end(); 176 | } else { 177 | file.on('data', res.write.bind(res)); 178 | file.on('close', function() { 179 | res.end(); 180 | }); 181 | file.on('error', function(error) { 182 | self.sendError_(req, res, error); 183 | }); 184 | } 185 | }; 186 | 187 | StaticServlet.prototype.sendDirectory_ = function(req, res, path) { 188 | var self = this; 189 | if (path.match(/[^\/]$/)) { 190 | req.url.pathname += '/'; 191 | var redirectUrl = url.format(url.parse(url.format(req.url))); 192 | return self.sendRedirect_(req, res, redirectUrl); 193 | } 194 | fs.readdir(path, function(err, files) { 195 | if (err) 196 | return self.sendError_(req, res, error); 197 | 198 | if (!files.length) 199 | return self.writeDirectoryIndex_(req, res, path, []); 200 | 201 | var remaining = files.length; 202 | files.forEach(function(fileName, index) { 203 | fs.stat(path + '/' + fileName, function(err, stat) { 204 | if (err) 205 | return self.sendError_(req, res, err); 206 | if (stat.isDirectory()) { 207 | files[index] = fileName + '/'; 208 | } 209 | if (!(--remaining)) 210 | return self.writeDirectoryIndex_(req, res, path, files); 211 | }); 212 | }); 213 | }); 214 | }; 215 | 216 | StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) { 217 | path = path.substring(1); 218 | res.writeHead(200, { 219 | 'Content-Type': 'text/html' 220 | }); 221 | if (req.method === 'HEAD') { 222 | res.end(); 223 | return; 224 | } 225 | res.write('\n'); 226 | res.write('' + escapeHtml(path) + '\n'); 227 | res.write('\n'); 230 | res.write('

Directory: ' + escapeHtml(path) + '

'); 231 | res.write('
    '); 232 | files.forEach(function(fileName) { 233 | if (fileName.charAt(0) !== '.') { 234 | res.write('
  1. ' + 236 | escapeHtml(fileName) + '
  2. '); 237 | } 238 | }); 239 | res.write('
'); 240 | res.end(); 241 | }; 242 | 243 | // Must be last, 244 | main(process.argv); 245 | -------------------------------------------------------------------------------- /test/e2e/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/e2e/scenarios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ 4 | 5 | describe('my app', function() { 6 | 7 | beforeEach(function() { 8 | browser().navigateTo('../../app/index.html'); 9 | }); 10 | 11 | 12 | it('should automatically redirect to /view1 when location hash/fragment is empty', function() { 13 | expect(browser().location().url()).toBe("/view1"); 14 | }); 15 | 16 | 17 | describe('view1', function() { 18 | 19 | beforeEach(function() { 20 | browser().navigateTo('#/view1'); 21 | }); 22 | 23 | 24 | it('should render view1 when user navigates to /view1', function() { 25 | expect(element('[ng-view] p:first').text()). 26 | toMatch(/partial for view 1/); 27 | }); 28 | 29 | }); 30 | 31 | 32 | describe('view2', function() { 33 | 34 | beforeEach(function() { 35 | browser().navigateTo('#/view2'); 36 | }); 37 | 38 | 39 | it('should render view2 when user navigates to /view2', function() { 40 | expect(element('[ng-view] p:first').text()). 41 | toMatch(/partial for view 2/); 42 | }); 43 | 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/lib/angular/angular-mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.6 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | * 6 | * TODO(vojta): wrap whole file into closure during build 7 | */ 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name angular.mock 12 | * @description 13 | * 14 | * Namespace from 'angular-mocks.js' which contains testing related code. 15 | */ 16 | angular.mock = {}; 17 | 18 | /** 19 | * ! This is a private undocumented service ! 20 | * 21 | * @name ngMock.$browser 22 | * 23 | * @description 24 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 25 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 26 | * cookies, etc... 27 | * 28 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 29 | * that there are several helper methods available which can be used in tests. 30 | */ 31 | angular.mock.$BrowserProvider = function() { 32 | this.$get = function(){ 33 | return new angular.mock.$Browser(); 34 | }; 35 | }; 36 | 37 | angular.mock.$Browser = function() { 38 | var self = this; 39 | 40 | this.isMock = true; 41 | self.$$url = "http://server/"; 42 | self.$$lastUrl = self.$$url; // used by url polling fn 43 | self.pollFns = []; 44 | 45 | // TODO(vojta): remove this temporary api 46 | self.$$completeOutstandingRequest = angular.noop; 47 | self.$$incOutstandingRequestCount = angular.noop; 48 | 49 | 50 | // register url polling fn 51 | 52 | self.onUrlChange = function(listener) { 53 | self.pollFns.push( 54 | function() { 55 | if (self.$$lastUrl != self.$$url) { 56 | self.$$lastUrl = self.$$url; 57 | listener(self.$$url); 58 | } 59 | } 60 | ); 61 | 62 | return listener; 63 | }; 64 | 65 | self.cookieHash = {}; 66 | self.lastCookieHash = {}; 67 | self.deferredFns = []; 68 | self.deferredNextId = 0; 69 | 70 | self.defer = function(fn, delay) { 71 | delay = delay || 0; 72 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 73 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 74 | return self.deferredNextId++; 75 | }; 76 | 77 | 78 | self.defer.now = 0; 79 | 80 | 81 | self.defer.cancel = function(deferId) { 82 | var fnIndex; 83 | 84 | angular.forEach(self.deferredFns, function(fn, index) { 85 | if (fn.id === deferId) fnIndex = index; 86 | }); 87 | 88 | if (fnIndex !== undefined) { 89 | self.deferredFns.splice(fnIndex, 1); 90 | return true; 91 | } 92 | 93 | return false; 94 | }; 95 | 96 | 97 | /** 98 | * @name ngMock.$browser#defer.flush 99 | * @methodOf ngMock.$browser 100 | * 101 | * @description 102 | * Flushes all pending requests and executes the defer callbacks. 103 | * 104 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 105 | */ 106 | self.defer.flush = function(delay) { 107 | if (angular.isDefined(delay)) { 108 | self.defer.now += delay; 109 | } else { 110 | if (self.deferredFns.length) { 111 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 112 | } else { 113 | throw Error('No deferred tasks to be flushed'); 114 | } 115 | } 116 | 117 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 118 | self.deferredFns.shift().fn(); 119 | } 120 | }; 121 | /** 122 | * @name ngMock.$browser#defer.now 123 | * @propertyOf ngMock.$browser 124 | * 125 | * @description 126 | * Current milliseconds mock time. 127 | */ 128 | 129 | self.$$baseHref = ''; 130 | self.baseHref = function() { 131 | return this.$$baseHref; 132 | }; 133 | }; 134 | angular.mock.$Browser.prototype = { 135 | 136 | /** 137 | * @name ngMock.$browser#poll 138 | * @methodOf ngMock.$browser 139 | * 140 | * @description 141 | * run all fns in pollFns 142 | */ 143 | poll: function poll() { 144 | angular.forEach(this.pollFns, function(pollFn){ 145 | pollFn(); 146 | }); 147 | }, 148 | 149 | addPollFn: function(pollFn) { 150 | this.pollFns.push(pollFn); 151 | return pollFn; 152 | }, 153 | 154 | url: function(url, replace) { 155 | if (url) { 156 | this.$$url = url; 157 | return this; 158 | } 159 | 160 | return this.$$url; 161 | }, 162 | 163 | cookies: function(name, value) { 164 | if (name) { 165 | if (value == undefined) { 166 | delete this.cookieHash[name]; 167 | } else { 168 | if (angular.isString(value) && //strings only 169 | value.length <= 4096) { //strict cookie storage limits 170 | this.cookieHash[name] = value; 171 | } 172 | } 173 | } else { 174 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 175 | this.lastCookieHash = angular.copy(this.cookieHash); 176 | this.cookieHash = angular.copy(this.cookieHash); 177 | } 178 | return this.cookieHash; 179 | } 180 | }, 181 | 182 | notifyWhenNoOutstandingRequests: function(fn) { 183 | fn(); 184 | } 185 | }; 186 | 187 | 188 | /** 189 | * @ngdoc object 190 | * @name ngMock.$exceptionHandlerProvider 191 | * 192 | * @description 193 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed 194 | * into the `$exceptionHandler`. 195 | */ 196 | 197 | /** 198 | * @ngdoc object 199 | * @name ngMock.$exceptionHandler 200 | * 201 | * @description 202 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 203 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 204 | * information. 205 | * 206 | * 207 | *
 208 |  *   describe('$exceptionHandlerProvider', function() {
 209 |  *
 210 |  *     it('should capture log messages and exceptions', function() {
 211 |  *
 212 |  *       module(function($exceptionHandlerProvider) {
 213 |  *         $exceptionHandlerProvider.mode('log');
 214 |  *       });
 215 |  *
 216 |  *       inject(function($log, $exceptionHandler, $timeout) {
 217 |  *         $timeout(function() { $log.log(1); });
 218 |  *         $timeout(function() { $log.log(2); throw 'banana peel'; });
 219 |  *         $timeout(function() { $log.log(3); });
 220 |  *         expect($exceptionHandler.errors).toEqual([]);
 221 |  *         expect($log.assertEmpty());
 222 |  *         $timeout.flush();
 223 |  *         expect($exceptionHandler.errors).toEqual(['banana peel']);
 224 |  *         expect($log.log.logs).toEqual([[1], [2], [3]]);
 225 |  *       });
 226 |  *     });
 227 |  *   });
 228 |  * 
229 | */ 230 | 231 | angular.mock.$ExceptionHandlerProvider = function() { 232 | var handler; 233 | 234 | /** 235 | * @ngdoc method 236 | * @name ngMock.$exceptionHandlerProvider#mode 237 | * @methodOf ngMock.$exceptionHandlerProvider 238 | * 239 | * @description 240 | * Sets the logging mode. 241 | * 242 | * @param {string} mode Mode of operation, defaults to `rethrow`. 243 | * 244 | * - `rethrow`: If any errors are are passed into the handler in tests, it typically 245 | * means that there is a bug in the application or test, so this mock will 246 | * make these tests fail. 247 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` mode stores an 248 | * array of errors in `$exceptionHandler.errors`, to allow later assertion of them. 249 | * See {@link ngMock.$log#assertEmpty assertEmpty()} and 250 | * {@link ngMock.$log#reset reset()} 251 | */ 252 | this.mode = function(mode) { 253 | switch(mode) { 254 | case 'rethrow': 255 | handler = function(e) { 256 | throw e; 257 | }; 258 | break; 259 | case 'log': 260 | var errors = []; 261 | 262 | handler = function(e) { 263 | if (arguments.length == 1) { 264 | errors.push(e); 265 | } else { 266 | errors.push([].slice.call(arguments, 0)); 267 | } 268 | }; 269 | 270 | handler.errors = errors; 271 | break; 272 | default: 273 | throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 274 | } 275 | }; 276 | 277 | this.$get = function() { 278 | return handler; 279 | }; 280 | 281 | this.mode('rethrow'); 282 | }; 283 | 284 | 285 | /** 286 | * @ngdoc service 287 | * @name ngMock.$log 288 | * 289 | * @description 290 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 291 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 292 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 293 | * 294 | */ 295 | angular.mock.$LogProvider = function() { 296 | 297 | function concat(array1, array2, index) { 298 | return array1.concat(Array.prototype.slice.call(array2, index)); 299 | } 300 | 301 | 302 | this.$get = function () { 303 | var $log = { 304 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 305 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 306 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 307 | error: function() { $log.error.logs.push(concat([], arguments, 0)); } 308 | }; 309 | 310 | /** 311 | * @ngdoc method 312 | * @name ngMock.$log#reset 313 | * @methodOf ngMock.$log 314 | * 315 | * @description 316 | * Reset all of the logging arrays to empty. 317 | */ 318 | $log.reset = function () { 319 | /** 320 | * @ngdoc property 321 | * @name ngMock.$log#log.logs 322 | * @propertyOf ngMock.$log 323 | * 324 | * @description 325 | * Array of logged messages. 326 | */ 327 | $log.log.logs = []; 328 | /** 329 | * @ngdoc property 330 | * @name ngMock.$log#warn.logs 331 | * @propertyOf ngMock.$log 332 | * 333 | * @description 334 | * Array of logged messages. 335 | */ 336 | $log.warn.logs = []; 337 | /** 338 | * @ngdoc property 339 | * @name ngMock.$log#info.logs 340 | * @propertyOf ngMock.$log 341 | * 342 | * @description 343 | * Array of logged messages. 344 | */ 345 | $log.info.logs = []; 346 | /** 347 | * @ngdoc property 348 | * @name ngMock.$log#error.logs 349 | * @propertyOf ngMock.$log 350 | * 351 | * @description 352 | * Array of logged messages. 353 | */ 354 | $log.error.logs = []; 355 | }; 356 | 357 | /** 358 | * @ngdoc method 359 | * @name ngMock.$log#assertEmpty 360 | * @methodOf ngMock.$log 361 | * 362 | * @description 363 | * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. 364 | */ 365 | $log.assertEmpty = function() { 366 | var errors = []; 367 | angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { 368 | angular.forEach($log[logLevel].logs, function(log) { 369 | angular.forEach(log, function (logItem) { 370 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); 371 | }); 372 | }); 373 | }); 374 | if (errors.length) { 375 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + 376 | "log message was not checked and removed:"); 377 | errors.push(''); 378 | throw new Error(errors.join('\n---------\n')); 379 | } 380 | }; 381 | 382 | $log.reset(); 383 | return $log; 384 | }; 385 | }; 386 | 387 | 388 | (function() { 389 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 390 | 391 | function jsonStringToDate(string){ 392 | var match; 393 | if (match = string.match(R_ISO8061_STR)) { 394 | var date = new Date(0), 395 | tzHour = 0, 396 | tzMin = 0; 397 | if (match[9]) { 398 | tzHour = int(match[9] + match[10]); 399 | tzMin = int(match[9] + match[11]); 400 | } 401 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 402 | date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); 403 | return date; 404 | } 405 | return string; 406 | } 407 | 408 | function int(str) { 409 | return parseInt(str, 10); 410 | } 411 | 412 | function padNumber(num, digits, trim) { 413 | var neg = ''; 414 | if (num < 0) { 415 | neg = '-'; 416 | num = -num; 417 | } 418 | num = '' + num; 419 | while(num.length < digits) num = '0' + num; 420 | if (trim) 421 | num = num.substr(num.length - digits); 422 | return neg + num; 423 | } 424 | 425 | 426 | /** 427 | * @ngdoc object 428 | * @name angular.mock.TzDate 429 | * @description 430 | * 431 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 432 | * 433 | * Mock of the Date type which has its timezone specified via constructor arg. 434 | * 435 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 436 | * offset, so that we can test code that depends on local timezone settings without dependency on 437 | * the time zone settings of the machine where the code is running. 438 | * 439 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 440 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 441 | * 442 | * @example 443 | * !!!! WARNING !!!!! 444 | * This is not a complete Date object so only methods that were implemented can be called safely. 445 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 446 | * 447 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 448 | * incomplete we might be missing some non-standard methods. This can result in errors like: 449 | * "Date.prototype.foo called on incompatible Object". 450 | * 451 | *
 452 |    * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
 453 |    * newYearInBratislava.getTimezoneOffset() => -60;
 454 |    * newYearInBratislava.getFullYear() => 2010;
 455 |    * newYearInBratislava.getMonth() => 0;
 456 |    * newYearInBratislava.getDate() => 1;
 457 |    * newYearInBratislava.getHours() => 0;
 458 |    * newYearInBratislava.getMinutes() => 0;
 459 |    * 
460 | * 461 | */ 462 | angular.mock.TzDate = function (offset, timestamp) { 463 | var self = new Date(0); 464 | if (angular.isString(timestamp)) { 465 | var tsStr = timestamp; 466 | 467 | self.origDate = jsonStringToDate(timestamp); 468 | 469 | timestamp = self.origDate.getTime(); 470 | if (isNaN(timestamp)) 471 | throw { 472 | name: "Illegal Argument", 473 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 474 | }; 475 | } else { 476 | self.origDate = new Date(timestamp); 477 | } 478 | 479 | var localOffset = new Date(timestamp).getTimezoneOffset(); 480 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 481 | self.date = new Date(timestamp + self.offsetDiff); 482 | 483 | self.getTime = function() { 484 | return self.date.getTime() - self.offsetDiff; 485 | }; 486 | 487 | self.toLocaleDateString = function() { 488 | return self.date.toLocaleDateString(); 489 | }; 490 | 491 | self.getFullYear = function() { 492 | return self.date.getFullYear(); 493 | }; 494 | 495 | self.getMonth = function() { 496 | return self.date.getMonth(); 497 | }; 498 | 499 | self.getDate = function() { 500 | return self.date.getDate(); 501 | }; 502 | 503 | self.getHours = function() { 504 | return self.date.getHours(); 505 | }; 506 | 507 | self.getMinutes = function() { 508 | return self.date.getMinutes(); 509 | }; 510 | 511 | self.getSeconds = function() { 512 | return self.date.getSeconds(); 513 | }; 514 | 515 | self.getTimezoneOffset = function() { 516 | return offset * 60; 517 | }; 518 | 519 | self.getUTCFullYear = function() { 520 | return self.origDate.getUTCFullYear(); 521 | }; 522 | 523 | self.getUTCMonth = function() { 524 | return self.origDate.getUTCMonth(); 525 | }; 526 | 527 | self.getUTCDate = function() { 528 | return self.origDate.getUTCDate(); 529 | }; 530 | 531 | self.getUTCHours = function() { 532 | return self.origDate.getUTCHours(); 533 | }; 534 | 535 | self.getUTCMinutes = function() { 536 | return self.origDate.getUTCMinutes(); 537 | }; 538 | 539 | self.getUTCSeconds = function() { 540 | return self.origDate.getUTCSeconds(); 541 | }; 542 | 543 | self.getUTCMilliseconds = function() { 544 | return self.origDate.getUTCMilliseconds(); 545 | }; 546 | 547 | self.getDay = function() { 548 | return self.date.getDay(); 549 | }; 550 | 551 | // provide this method only on browsers that already have it 552 | if (self.toISOString) { 553 | self.toISOString = function() { 554 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 555 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 556 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 557 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 558 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 559 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 560 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' 561 | } 562 | } 563 | 564 | //hide all methods not implemented in this mock that the Date prototype exposes 565 | var unimplementedMethods = ['getMilliseconds', 'getUTCDay', 566 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 567 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 568 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 569 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 570 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 571 | 572 | angular.forEach(unimplementedMethods, function(methodName) { 573 | self[methodName] = function() { 574 | throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 575 | }; 576 | }); 577 | 578 | return self; 579 | }; 580 | 581 | //make "tzDateInstance instanceof Date" return true 582 | angular.mock.TzDate.prototype = Date.prototype; 583 | })(); 584 | 585 | 586 | /** 587 | * @ngdoc function 588 | * @name angular.mock.dump 589 | * @description 590 | * 591 | * *NOTE*: this is not an injectable instance, just a globally available function. 592 | * 593 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. 594 | * 595 | * This method is also available on window, where it can be used to display objects on debug console. 596 | * 597 | * @param {*} object - any object to turn into string. 598 | * @return {string} a serialized string of the argument 599 | */ 600 | angular.mock.dump = function(object) { 601 | return serialize(object); 602 | 603 | function serialize(object) { 604 | var out; 605 | 606 | if (angular.isElement(object)) { 607 | object = angular.element(object); 608 | out = angular.element('
'); 609 | angular.forEach(object, function(element) { 610 | out.append(angular.element(element).clone()); 611 | }); 612 | out = out.html(); 613 | } else if (angular.isArray(object)) { 614 | out = []; 615 | angular.forEach(object, function(o) { 616 | out.push(serialize(o)); 617 | }); 618 | out = '[ ' + out.join(', ') + ' ]'; 619 | } else if (angular.isObject(object)) { 620 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 621 | out = serializeScope(object); 622 | } else if (object instanceof Error) { 623 | out = object.stack || ('' + object.name + ': ' + object.message); 624 | } else { 625 | out = angular.toJson(object, true); 626 | } 627 | } else { 628 | out = String(object); 629 | } 630 | 631 | return out; 632 | } 633 | 634 | function serializeScope(scope, offset) { 635 | offset = offset || ' '; 636 | var log = [offset + 'Scope(' + scope.$id + '): {']; 637 | for ( var key in scope ) { 638 | if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { 639 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 640 | } 641 | } 642 | var child = scope.$$childHead; 643 | while(child) { 644 | log.push(serializeScope(child, offset + ' ')); 645 | child = child.$$nextSibling; 646 | } 647 | log.push('}'); 648 | return log.join('\n' + offset); 649 | } 650 | }; 651 | 652 | /** 653 | * @ngdoc object 654 | * @name ngMock.$httpBackend 655 | * @description 656 | * Fake HTTP backend implementation suitable for unit testing application that use the 657 | * {@link ng.$http $http service}. 658 | * 659 | * *Note*: For fake http backend implementation suitable for end-to-end testing or backend-less 660 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 661 | * 662 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 663 | * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or 664 | * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is 665 | * to verify whether a certain request has been sent or not, or alternatively just let the 666 | * application make requests, respond with pre-trained responses and assert that the end result is 667 | * what we expect it to be. 668 | * 669 | * This mock implementation can be used to respond with static or dynamic responses via the 670 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 671 | * 672 | * When an Angular application needs some data from a server, it calls the $http service, which 673 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 674 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 675 | * the requests and respond with some testing data without sending a request to real server. 676 | * 677 | * There are two ways to specify what test data should be returned as http responses by the mock 678 | * backend when the code under test makes http requests: 679 | * 680 | * - `$httpBackend.expect` - specifies a request expectation 681 | * - `$httpBackend.when` - specifies a backend definition 682 | * 683 | * 684 | * # Request Expectations vs Backend Definitions 685 | * 686 | * Request expectations provide a way to make assertions about requests made by the application and 687 | * to define responses for those requests. The test will fail if the expected requests are not made 688 | * or they are made in the wrong order. 689 | * 690 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 691 | * if a particular request was made or not, it just returns a trained response if a request is made. 692 | * The test will pass whether or not the request gets made during testing. 693 | * 694 | * 695 | * 696 | * 697 | * 698 | * 699 | * 700 | * 701 | * 702 | * 703 | * 704 | * 705 | * 706 | * 707 | * 708 | * 709 | * 710 | * 711 | * 712 | * 713 | * 714 | * 715 | * 716 | * 717 | * 718 | * 719 | * 720 | * 721 | * 722 | * 723 | * 724 | * 725 | * 726 | * 727 | *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
728 | * 729 | * In cases where both backend definitions and request expectations are specified during unit 730 | * testing, the request expectations are evaluated first. 731 | * 732 | * If a request expectation has no response specified, the algorithm will search your backend 733 | * definitions for an appropriate response. 734 | * 735 | * If a request didn't match any expectation or if the expectation doesn't have the response 736 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 737 | * the request. The response from the first matched definition is returned. 738 | * 739 | * 740 | * # Flushing HTTP requests 741 | * 742 | * The $httpBackend used in production, always responds to requests with responses asynchronously. 743 | * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are 744 | * hard to write, follow and maintain. At the same time the testing mock, can't respond 745 | * synchronously because that would change the execution of the code under test. For this reason the 746 | * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending 747 | * requests and thus preserving the async api of the backend, while allowing the test to execute 748 | * synchronously. 749 | * 750 | * 751 | * # Unit testing with mock $httpBackend 752 | * 753 | *
 754 |    // controller
 755 |    function MyController($scope, $http) {
 756 |      $http.get('/auth.py').success(function(data) {
 757 |        $scope.user = data;
 758 |      });
 759 | 
 760 |      this.saveMessage = function(message) {
 761 |        $scope.status = 'Saving...';
 762 |        $http.post('/add-msg.py', message).success(function(response) {
 763 |          $scope.status = '';
 764 |        }).error(function() {
 765 |          $scope.status = 'ERROR!';
 766 |        });
 767 |      };
 768 |    }
 769 | 
 770 |    // testing controller
 771 |    var $httpBackend;
 772 | 
 773 |    beforeEach(inject(function($injector) {
 774 |      $httpBackend = $injector.get('$httpBackend');
 775 | 
 776 |      // backend definition common for all tests
 777 |      $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
 778 |    }));
 779 | 
 780 | 
 781 |    afterEach(function() {
 782 |      $httpBackend.verifyNoOutstandingExpectation();
 783 |      $httpBackend.verifyNoOutstandingRequest();
 784 |    });
 785 | 
 786 | 
 787 |    it('should fetch authentication token', function() {
 788 |      $httpBackend.expectGET('/auth.py');
 789 |      var controller = scope.$new(MyController);
 790 |      $httpBackend.flush();
 791 |    });
 792 | 
 793 | 
 794 |    it('should send msg to server', function() {
 795 |      // now you don’t care about the authentication, but
 796 |      // the controller will still send the request and
 797 |      // $httpBackend will respond without you having to
 798 |      // specify the expectation and response for this request
 799 |      $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
 800 | 
 801 |      var controller = scope.$new(MyController);
 802 |      $httpBackend.flush();
 803 |      controller.saveMessage('message content');
 804 |      expect(controller.status).toBe('Saving...');
 805 |      $httpBackend.flush();
 806 |      expect(controller.status).toBe('');
 807 |    });
 808 | 
 809 | 
 810 |    it('should send auth header', function() {
 811 |      $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
 812 |        // check if the header was send, if it wasn't the expectation won't
 813 |        // match the request and the test will fail
 814 |        return headers['Authorization'] == 'xxx';
 815 |      }).respond(201, '');
 816 | 
 817 |      var controller = scope.$new(MyController);
 818 |      controller.saveMessage('whatever');
 819 |      $httpBackend.flush();
 820 |    });
 821 |    
822 | */ 823 | angular.mock.$HttpBackendProvider = function() { 824 | this.$get = [createHttpBackendMock]; 825 | }; 826 | 827 | /** 828 | * General factory function for $httpBackend mock. 829 | * Returns instance for unit testing (when no arguments specified): 830 | * - passing through is disabled 831 | * - auto flushing is disabled 832 | * 833 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 834 | * - passing through (delegating request to real backend) is enabled 835 | * - auto flushing is enabled 836 | * 837 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 838 | * @param {Object=} $browser Auto-flushing enabled if specified 839 | * @return {Object} Instance of $httpBackend mock 840 | */ 841 | function createHttpBackendMock($delegate, $browser) { 842 | var definitions = [], 843 | expectations = [], 844 | responses = [], 845 | responsesPush = angular.bind(responses, responses.push); 846 | 847 | function createResponse(status, data, headers) { 848 | if (angular.isFunction(status)) return status; 849 | 850 | return function() { 851 | return angular.isNumber(status) 852 | ? [status, data, headers] 853 | : [200, status, data]; 854 | }; 855 | } 856 | 857 | // TODO(vojta): change params to: method, url, data, headers, callback 858 | function $httpBackend(method, url, data, callback, headers) { 859 | var xhr = new MockXhr(), 860 | expectation = expectations[0], 861 | wasExpected = false; 862 | 863 | function prettyPrint(data) { 864 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 865 | ? data 866 | : angular.toJson(data); 867 | } 868 | 869 | if (expectation && expectation.match(method, url)) { 870 | if (!expectation.matchData(data)) 871 | throw Error('Expected ' + expectation + ' with different data\n' + 872 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 873 | 874 | if (!expectation.matchHeaders(headers)) 875 | throw Error('Expected ' + expectation + ' with different headers\n' + 876 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 877 | prettyPrint(headers)); 878 | 879 | expectations.shift(); 880 | 881 | if (expectation.response) { 882 | responses.push(function() { 883 | var response = expectation.response(method, url, data, headers); 884 | xhr.$$respHeaders = response[2]; 885 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 886 | }); 887 | return; 888 | } 889 | wasExpected = true; 890 | } 891 | 892 | var i = -1, definition; 893 | while ((definition = definitions[++i])) { 894 | if (definition.match(method, url, data, headers || {})) { 895 | if (definition.response) { 896 | // if $browser specified, we do auto flush all requests 897 | ($browser ? $browser.defer : responsesPush)(function() { 898 | var response = definition.response(method, url, data, headers); 899 | xhr.$$respHeaders = response[2]; 900 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 901 | }); 902 | } else if (definition.passThrough) { 903 | $delegate(method, url, data, callback, headers); 904 | } else throw Error('No response defined !'); 905 | return; 906 | } 907 | } 908 | throw wasExpected ? 909 | Error('No response defined !') : 910 | Error('Unexpected request: ' + method + ' ' + url + '\n' + 911 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 912 | } 913 | 914 | /** 915 | * @ngdoc method 916 | * @name ngMock.$httpBackend#when 917 | * @methodOf ngMock.$httpBackend 918 | * @description 919 | * Creates a new backend definition. 920 | * 921 | * @param {string} method HTTP method. 922 | * @param {string|RegExp} url HTTP url. 923 | * @param {(string|RegExp)=} data HTTP request body. 924 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 925 | * object and returns true if the headers match the current definition. 926 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 927 | * request is handled. 928 | * 929 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 930 | * – The respond method takes a set of static data to be returned or a function that can return 931 | * an array containing response status (number), response data (string) and response headers 932 | * (Object). 933 | */ 934 | $httpBackend.when = function(method, url, data, headers) { 935 | var definition = new MockHttpExpectation(method, url, data, headers), 936 | chain = { 937 | respond: function(status, data, headers) { 938 | definition.response = createResponse(status, data, headers); 939 | } 940 | }; 941 | 942 | if ($browser) { 943 | chain.passThrough = function() { 944 | definition.passThrough = true; 945 | }; 946 | } 947 | 948 | definitions.push(definition); 949 | return chain; 950 | }; 951 | 952 | /** 953 | * @ngdoc method 954 | * @name ngMock.$httpBackend#whenGET 955 | * @methodOf ngMock.$httpBackend 956 | * @description 957 | * Creates a new backend definition for GET requests. For more info see `when()`. 958 | * 959 | * @param {string|RegExp} url HTTP url. 960 | * @param {(Object|function(Object))=} headers HTTP headers. 961 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 962 | * request is handled. 963 | */ 964 | 965 | /** 966 | * @ngdoc method 967 | * @name ngMock.$httpBackend#whenHEAD 968 | * @methodOf ngMock.$httpBackend 969 | * @description 970 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 971 | * 972 | * @param {string|RegExp} url HTTP url. 973 | * @param {(Object|function(Object))=} headers HTTP headers. 974 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 975 | * request is handled. 976 | */ 977 | 978 | /** 979 | * @ngdoc method 980 | * @name ngMock.$httpBackend#whenDELETE 981 | * @methodOf ngMock.$httpBackend 982 | * @description 983 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 984 | * 985 | * @param {string|RegExp} url HTTP url. 986 | * @param {(Object|function(Object))=} headers HTTP headers. 987 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 988 | * request is handled. 989 | */ 990 | 991 | /** 992 | * @ngdoc method 993 | * @name ngMock.$httpBackend#whenPOST 994 | * @methodOf ngMock.$httpBackend 995 | * @description 996 | * Creates a new backend definition for POST requests. For more info see `when()`. 997 | * 998 | * @param {string|RegExp} url HTTP url. 999 | * @param {(string|RegExp)=} data HTTP request body. 1000 | * @param {(Object|function(Object))=} headers HTTP headers. 1001 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1002 | * request is handled. 1003 | */ 1004 | 1005 | /** 1006 | * @ngdoc method 1007 | * @name ngMock.$httpBackend#whenPUT 1008 | * @methodOf ngMock.$httpBackend 1009 | * @description 1010 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1011 | * 1012 | * @param {string|RegExp} url HTTP url. 1013 | * @param {(string|RegExp)=} data HTTP request body. 1014 | * @param {(Object|function(Object))=} headers HTTP headers. 1015 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1016 | * request is handled. 1017 | */ 1018 | 1019 | /** 1020 | * @ngdoc method 1021 | * @name ngMock.$httpBackend#whenJSONP 1022 | * @methodOf ngMock.$httpBackend 1023 | * @description 1024 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1025 | * 1026 | * @param {string|RegExp} url HTTP url. 1027 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1028 | * request is handled. 1029 | */ 1030 | createShortMethods('when'); 1031 | 1032 | 1033 | /** 1034 | * @ngdoc method 1035 | * @name ngMock.$httpBackend#expect 1036 | * @methodOf ngMock.$httpBackend 1037 | * @description 1038 | * Creates a new request expectation. 1039 | * 1040 | * @param {string} method HTTP method. 1041 | * @param {string|RegExp} url HTTP url. 1042 | * @param {(string|RegExp)=} data HTTP request body. 1043 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1044 | * object and returns true if the headers match the current expectation. 1045 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1046 | * request is handled. 1047 | * 1048 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1049 | * – The respond method takes a set of static data to be returned or a function that can return 1050 | * an array containing response status (number), response data (string) and response headers 1051 | * (Object). 1052 | */ 1053 | $httpBackend.expect = function(method, url, data, headers) { 1054 | var expectation = new MockHttpExpectation(method, url, data, headers); 1055 | expectations.push(expectation); 1056 | return { 1057 | respond: function(status, data, headers) { 1058 | expectation.response = createResponse(status, data, headers); 1059 | } 1060 | }; 1061 | }; 1062 | 1063 | 1064 | /** 1065 | * @ngdoc method 1066 | * @name ngMock.$httpBackend#expectGET 1067 | * @methodOf ngMock.$httpBackend 1068 | * @description 1069 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1070 | * 1071 | * @param {string|RegExp} url HTTP url. 1072 | * @param {Object=} headers HTTP headers. 1073 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1074 | * request is handled. See #expect for more info. 1075 | */ 1076 | 1077 | /** 1078 | * @ngdoc method 1079 | * @name ngMock.$httpBackend#expectHEAD 1080 | * @methodOf ngMock.$httpBackend 1081 | * @description 1082 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1083 | * 1084 | * @param {string|RegExp} url HTTP url. 1085 | * @param {Object=} headers HTTP headers. 1086 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1087 | * request is handled. 1088 | */ 1089 | 1090 | /** 1091 | * @ngdoc method 1092 | * @name ngMock.$httpBackend#expectDELETE 1093 | * @methodOf ngMock.$httpBackend 1094 | * @description 1095 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1096 | * 1097 | * @param {string|RegExp} url HTTP url. 1098 | * @param {Object=} headers HTTP headers. 1099 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1100 | * request is handled. 1101 | */ 1102 | 1103 | /** 1104 | * @ngdoc method 1105 | * @name ngMock.$httpBackend#expectPOST 1106 | * @methodOf ngMock.$httpBackend 1107 | * @description 1108 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1109 | * 1110 | * @param {string|RegExp} url HTTP url. 1111 | * @param {(string|RegExp)=} data HTTP request body. 1112 | * @param {Object=} headers HTTP headers. 1113 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1114 | * request is handled. 1115 | */ 1116 | 1117 | /** 1118 | * @ngdoc method 1119 | * @name ngMock.$httpBackend#expectPUT 1120 | * @methodOf ngMock.$httpBackend 1121 | * @description 1122 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1123 | * 1124 | * @param {string|RegExp} url HTTP url. 1125 | * @param {(string|RegExp)=} data HTTP request body. 1126 | * @param {Object=} headers HTTP headers. 1127 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1128 | * request is handled. 1129 | */ 1130 | 1131 | /** 1132 | * @ngdoc method 1133 | * @name ngMock.$httpBackend#expectPATCH 1134 | * @methodOf ngMock.$httpBackend 1135 | * @description 1136 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1137 | * 1138 | * @param {string|RegExp} url HTTP url. 1139 | * @param {(string|RegExp)=} data HTTP request body. 1140 | * @param {Object=} headers HTTP headers. 1141 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1142 | * request is handled. 1143 | */ 1144 | 1145 | /** 1146 | * @ngdoc method 1147 | * @name ngMock.$httpBackend#expectJSONP 1148 | * @methodOf ngMock.$httpBackend 1149 | * @description 1150 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1151 | * 1152 | * @param {string|RegExp} url HTTP url. 1153 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1154 | * request is handled. 1155 | */ 1156 | createShortMethods('expect'); 1157 | 1158 | 1159 | /** 1160 | * @ngdoc method 1161 | * @name ngMock.$httpBackend#flush 1162 | * @methodOf ngMock.$httpBackend 1163 | * @description 1164 | * Flushes all pending requests using the trained responses. 1165 | * 1166 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1167 | * all pending requests will be flushed. If there are no pending requests when the flush method 1168 | * is called an exception is thrown (as this typically a sign of programming error). 1169 | */ 1170 | $httpBackend.flush = function(count) { 1171 | if (!responses.length) throw Error('No pending request to flush !'); 1172 | 1173 | if (angular.isDefined(count)) { 1174 | while (count--) { 1175 | if (!responses.length) throw Error('No more pending request to flush !'); 1176 | responses.shift()(); 1177 | } 1178 | } else { 1179 | while (responses.length) { 1180 | responses.shift()(); 1181 | } 1182 | } 1183 | $httpBackend.verifyNoOutstandingExpectation(); 1184 | }; 1185 | 1186 | 1187 | /** 1188 | * @ngdoc method 1189 | * @name ngMock.$httpBackend#verifyNoOutstandingExpectation 1190 | * @methodOf ngMock.$httpBackend 1191 | * @description 1192 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1193 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1194 | * 1195 | * Typically, you would call this method following each test case that asserts requests using an 1196 | * "afterEach" clause. 1197 | * 1198 | *
1199 |    *   afterEach($httpBackend.verifyExpectations);
1200 |    * 
1201 | */ 1202 | $httpBackend.verifyNoOutstandingExpectation = function() { 1203 | if (expectations.length) { 1204 | throw Error('Unsatisfied requests: ' + expectations.join(', ')); 1205 | } 1206 | }; 1207 | 1208 | 1209 | /** 1210 | * @ngdoc method 1211 | * @name ngMock.$httpBackend#verifyNoOutstandingRequest 1212 | * @methodOf ngMock.$httpBackend 1213 | * @description 1214 | * Verifies that there are no outstanding requests that need to be flushed. 1215 | * 1216 | * Typically, you would call this method following each test case that asserts requests using an 1217 | * "afterEach" clause. 1218 | * 1219 | *
1220 |    *   afterEach($httpBackend.verifyNoOutstandingRequest);
1221 |    * 
1222 | */ 1223 | $httpBackend.verifyNoOutstandingRequest = function() { 1224 | if (responses.length) { 1225 | throw Error('Unflushed requests: ' + responses.length); 1226 | } 1227 | }; 1228 | 1229 | 1230 | /** 1231 | * @ngdoc method 1232 | * @name ngMock.$httpBackend#resetExpectations 1233 | * @methodOf ngMock.$httpBackend 1234 | * @description 1235 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1236 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1237 | * $httpBackend mock. 1238 | */ 1239 | $httpBackend.resetExpectations = function() { 1240 | expectations.length = 0; 1241 | responses.length = 0; 1242 | }; 1243 | 1244 | return $httpBackend; 1245 | 1246 | 1247 | function createShortMethods(prefix) { 1248 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1249 | $httpBackend[prefix + method] = function(url, headers) { 1250 | return $httpBackend[prefix](method, url, undefined, headers) 1251 | } 1252 | }); 1253 | 1254 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1255 | $httpBackend[prefix + method] = function(url, data, headers) { 1256 | return $httpBackend[prefix](method, url, data, headers) 1257 | } 1258 | }); 1259 | } 1260 | } 1261 | 1262 | function MockHttpExpectation(method, url, data, headers) { 1263 | 1264 | this.data = data; 1265 | this.headers = headers; 1266 | 1267 | this.match = function(m, u, d, h) { 1268 | if (method != m) return false; 1269 | if (!this.matchUrl(u)) return false; 1270 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1271 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1272 | return true; 1273 | }; 1274 | 1275 | this.matchUrl = function(u) { 1276 | if (!url) return true; 1277 | if (angular.isFunction(url.test)) return url.test(u); 1278 | return url == u; 1279 | }; 1280 | 1281 | this.matchHeaders = function(h) { 1282 | if (angular.isUndefined(headers)) return true; 1283 | if (angular.isFunction(headers)) return headers(h); 1284 | return angular.equals(headers, h); 1285 | }; 1286 | 1287 | this.matchData = function(d) { 1288 | if (angular.isUndefined(data)) return true; 1289 | if (data && angular.isFunction(data.test)) return data.test(d); 1290 | if (data && !angular.isString(data)) return angular.toJson(data) == d; 1291 | return data == d; 1292 | }; 1293 | 1294 | this.toString = function() { 1295 | return method + ' ' + url; 1296 | }; 1297 | } 1298 | 1299 | function MockXhr() { 1300 | 1301 | // hack for testing $http, $httpBackend 1302 | MockXhr.$$lastInstance = this; 1303 | 1304 | this.open = function(method, url, async) { 1305 | this.$$method = method; 1306 | this.$$url = url; 1307 | this.$$async = async; 1308 | this.$$reqHeaders = {}; 1309 | this.$$respHeaders = {}; 1310 | }; 1311 | 1312 | this.send = function(data) { 1313 | this.$$data = data; 1314 | }; 1315 | 1316 | this.setRequestHeader = function(key, value) { 1317 | this.$$reqHeaders[key] = value; 1318 | }; 1319 | 1320 | this.getResponseHeader = function(name) { 1321 | // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last 1322 | var header = this.$$respHeaders[name]; 1323 | if (header) return header; 1324 | 1325 | name = angular.lowercase(name); 1326 | header = this.$$respHeaders[name]; 1327 | if (header) return header; 1328 | 1329 | header = undefined; 1330 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1331 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1332 | }); 1333 | return header; 1334 | }; 1335 | 1336 | this.getAllResponseHeaders = function() { 1337 | var lines = []; 1338 | 1339 | angular.forEach(this.$$respHeaders, function(value, key) { 1340 | lines.push(key + ': ' + value); 1341 | }); 1342 | return lines.join('\n'); 1343 | }; 1344 | 1345 | this.abort = angular.noop; 1346 | } 1347 | 1348 | 1349 | /** 1350 | * @ngdoc function 1351 | * @name ngMock.$timeout 1352 | * @description 1353 | * 1354 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1355 | * that adds a "flush" method. 1356 | */ 1357 | 1358 | /** 1359 | * @ngdoc method 1360 | * @name ngMock.$timeout#flush 1361 | * @methodOf ngMock.$timeout 1362 | * @description 1363 | * 1364 | * Flushes the queue of pending tasks. 1365 | */ 1366 | 1367 | /** 1368 | * 1369 | */ 1370 | angular.mock.$RootElementProvider = function() { 1371 | this.$get = function() { 1372 | return angular.element('
'); 1373 | } 1374 | }; 1375 | 1376 | /** 1377 | * @ngdoc overview 1378 | * @name ngMock 1379 | * @description 1380 | * 1381 | * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful 1382 | * mocks to the {@link AUTO.$injector $injector}. 1383 | */ 1384 | angular.module('ngMock', ['ng']).provider({ 1385 | $browser: angular.mock.$BrowserProvider, 1386 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1387 | $log: angular.mock.$LogProvider, 1388 | $httpBackend: angular.mock.$HttpBackendProvider, 1389 | $rootElement: angular.mock.$RootElementProvider 1390 | }).config(function($provide) { 1391 | $provide.decorator('$timeout', function($delegate, $browser) { 1392 | $delegate.flush = function() { 1393 | $browser.defer.flush(); 1394 | }; 1395 | return $delegate; 1396 | }); 1397 | }); 1398 | 1399 | 1400 | /** 1401 | * @ngdoc overview 1402 | * @name ngMockE2E 1403 | * @description 1404 | * 1405 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1406 | * Currently there is only one mock present in this module - 1407 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1408 | */ 1409 | angular.module('ngMockE2E', ['ng']).config(function($provide) { 1410 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1411 | }); 1412 | 1413 | /** 1414 | * @ngdoc object 1415 | * @name ngMockE2E.$httpBackend 1416 | * @description 1417 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1418 | * applications that use the {@link ng.$http $http service}. 1419 | * 1420 | * *Note*: For fake http backend implementation suitable for unit testing please see 1421 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1422 | * 1423 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1424 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1425 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1426 | * templates from a webserver). 1427 | * 1428 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1429 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1430 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1431 | * templates or static files from the webserver). To configure the backend with this behavior 1432 | * use the `passThrough` request handler of `when` instead of `respond`. 1433 | * 1434 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1435 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1436 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1437 | * 1438 | * To setup the application to run with this http backend, you have to create a module that depends 1439 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1440 | * 1441 | *
1442 |  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1443 |  *   myAppDev.run(function($httpBackend) {
1444 |  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
1445 |  *
1446 |  *     // returns the current list of phones
1447 |  *     $httpBackend.whenGET('/phones').respond(phones);
1448 |  *
1449 |  *     // adds a new phone to the phones array
1450 |  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1451 |  *       phones.push(angular.fromJSON(data));
1452 |  *     });
1453 |  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
1454 |  *     //...
1455 |  *   });
1456 |  * 
1457 | * 1458 | * Afterwards, bootstrap your app with this new module. 1459 | */ 1460 | 1461 | /** 1462 | * @ngdoc method 1463 | * @name ngMockE2E.$httpBackend#when 1464 | * @methodOf ngMockE2E.$httpBackend 1465 | * @description 1466 | * Creates a new backend definition. 1467 | * 1468 | * @param {string} method HTTP method. 1469 | * @param {string|RegExp} url HTTP url. 1470 | * @param {(string|RegExp)=} data HTTP request body. 1471 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1472 | * object and returns true if the headers match the current definition. 1473 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1474 | * control how a matched request is handled. 1475 | * 1476 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1477 | * – The respond method takes a set of static data to be returned or a function that can return 1478 | * an array containing response status (number), response data (string) and response headers 1479 | * (Object). 1480 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1481 | * handler, will be pass through to the real backend (an XHR request will be made to the 1482 | * server. 1483 | */ 1484 | 1485 | /** 1486 | * @ngdoc method 1487 | * @name ngMockE2E.$httpBackend#whenGET 1488 | * @methodOf ngMockE2E.$httpBackend 1489 | * @description 1490 | * Creates a new backend definition for GET requests. For more info see `when()`. 1491 | * 1492 | * @param {string|RegExp} url HTTP url. 1493 | * @param {(Object|function(Object))=} headers HTTP headers. 1494 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1495 | * control how a matched request is handled. 1496 | */ 1497 | 1498 | /** 1499 | * @ngdoc method 1500 | * @name ngMockE2E.$httpBackend#whenHEAD 1501 | * @methodOf ngMockE2E.$httpBackend 1502 | * @description 1503 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1504 | * 1505 | * @param {string|RegExp} url HTTP url. 1506 | * @param {(Object|function(Object))=} headers HTTP headers. 1507 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1508 | * control how a matched request is handled. 1509 | */ 1510 | 1511 | /** 1512 | * @ngdoc method 1513 | * @name ngMockE2E.$httpBackend#whenDELETE 1514 | * @methodOf ngMockE2E.$httpBackend 1515 | * @description 1516 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1517 | * 1518 | * @param {string|RegExp} url HTTP url. 1519 | * @param {(Object|function(Object))=} headers HTTP headers. 1520 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1521 | * control how a matched request is handled. 1522 | */ 1523 | 1524 | /** 1525 | * @ngdoc method 1526 | * @name ngMockE2E.$httpBackend#whenPOST 1527 | * @methodOf ngMockE2E.$httpBackend 1528 | * @description 1529 | * Creates a new backend definition for POST requests. For more info see `when()`. 1530 | * 1531 | * @param {string|RegExp} url HTTP url. 1532 | * @param {(string|RegExp)=} data HTTP request body. 1533 | * @param {(Object|function(Object))=} headers HTTP headers. 1534 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1535 | * control how a matched request is handled. 1536 | */ 1537 | 1538 | /** 1539 | * @ngdoc method 1540 | * @name ngMockE2E.$httpBackend#whenPUT 1541 | * @methodOf ngMockE2E.$httpBackend 1542 | * @description 1543 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1544 | * 1545 | * @param {string|RegExp} url HTTP url. 1546 | * @param {(string|RegExp)=} data HTTP request body. 1547 | * @param {(Object|function(Object))=} headers HTTP headers. 1548 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1549 | * control how a matched request is handled. 1550 | */ 1551 | 1552 | /** 1553 | * @ngdoc method 1554 | * @name ngMockE2E.$httpBackend#whenPATCH 1555 | * @methodOf ngMockE2E.$httpBackend 1556 | * @description 1557 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1558 | * 1559 | * @param {string|RegExp} url HTTP url. 1560 | * @param {(string|RegExp)=} data HTTP request body. 1561 | * @param {(Object|function(Object))=} headers HTTP headers. 1562 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1563 | * control how a matched request is handled. 1564 | */ 1565 | 1566 | /** 1567 | * @ngdoc method 1568 | * @name ngMockE2E.$httpBackend#whenJSONP 1569 | * @methodOf ngMockE2E.$httpBackend 1570 | * @description 1571 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1572 | * 1573 | * @param {string|RegExp} url HTTP url. 1574 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1575 | * control how a matched request is handled. 1576 | */ 1577 | angular.mock.e2e = {}; 1578 | angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock]; 1579 | 1580 | 1581 | angular.mock.clearDataCache = function() { 1582 | var key, 1583 | cache = angular.element.cache; 1584 | 1585 | for(key in cache) { 1586 | if (cache.hasOwnProperty(key)) { 1587 | var handle = cache[key].handle; 1588 | 1589 | handle && angular.element(handle.elem).unbind(); 1590 | delete cache[key]; 1591 | } 1592 | } 1593 | }; 1594 | 1595 | 1596 | window.jstestdriver && (function(window) { 1597 | /** 1598 | * Global method to output any number of objects into JSTD console. Useful for debugging. 1599 | */ 1600 | window.dump = function() { 1601 | var args = []; 1602 | angular.forEach(arguments, function(arg) { 1603 | args.push(angular.mock.dump(arg)); 1604 | }); 1605 | jstestdriver.console.log.apply(jstestdriver.console, args); 1606 | if (window.console) { 1607 | window.console.log.apply(window.console, args); 1608 | } 1609 | }; 1610 | })(window); 1611 | 1612 | 1613 | window.jasmine && (function(window) { 1614 | 1615 | afterEach(function() { 1616 | var spec = getCurrentSpec(); 1617 | var injector = spec.$injector; 1618 | 1619 | spec.$injector = null; 1620 | spec.$modules = null; 1621 | 1622 | if (injector) { 1623 | injector.get('$rootElement').unbind(); 1624 | injector.get('$browser').pollFns.length = 0; 1625 | } 1626 | 1627 | angular.mock.clearDataCache(); 1628 | 1629 | // clean up jquery's fragment cache 1630 | angular.forEach(angular.element.fragments, function(val, key) { 1631 | delete angular.element.fragments[key]; 1632 | }); 1633 | 1634 | MockXhr.$$lastInstance = null; 1635 | 1636 | angular.forEach(angular.callbacks, function(val, key) { 1637 | delete angular.callbacks[key]; 1638 | }); 1639 | angular.callbacks.counter = 0; 1640 | }); 1641 | 1642 | function getCurrentSpec() { 1643 | return jasmine.getEnv().currentSpec; 1644 | } 1645 | 1646 | function isSpecRunning() { 1647 | var spec = getCurrentSpec(); 1648 | return spec && spec.queue.running; 1649 | } 1650 | 1651 | /** 1652 | * @ngdoc function 1653 | * @name angular.mock.module 1654 | * @description 1655 | * 1656 | * *NOTE*: This function is also published on window for easy access.
1657 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1658 | * 1659 | * This function registers a module configuration code. It collects the configuration information 1660 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1661 | * 1662 | * See {@link angular.mock.inject inject} for usage example 1663 | * 1664 | * @param {...(string|Function)} fns any number of modules which are represented as string 1665 | * aliases or as anonymous module initialization functions. The modules are used to 1666 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. 1667 | */ 1668 | window.module = angular.mock.module = function() { 1669 | var moduleFns = Array.prototype.slice.call(arguments, 0); 1670 | return isSpecRunning() ? workFn() : workFn; 1671 | ///////////////////// 1672 | function workFn() { 1673 | var spec = getCurrentSpec(); 1674 | if (spec.$injector) { 1675 | throw Error('Injector already created, can not register a module!'); 1676 | } else { 1677 | var modules = spec.$modules || (spec.$modules = []); 1678 | angular.forEach(moduleFns, function(module) { 1679 | modules.push(module); 1680 | }); 1681 | } 1682 | } 1683 | }; 1684 | 1685 | /** 1686 | * @ngdoc function 1687 | * @name angular.mock.inject 1688 | * @description 1689 | * 1690 | <<<<<<< HEAD 1691 | * *NOTE*: This is function is also published on window for easy access.
1692 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1693 | ======= 1694 | * *NOTE*: This function is also published on window for easy access.
1695 | >>>>>>> 8dca056... docs(mocks): fix typos 1696 | * 1697 | * The inject function wraps a function into an injectable function. The inject() creates new 1698 | * instance of {@link AUTO.$injector $injector} per test, which is then used for 1699 | * resolving references. 1700 | * 1701 | * See also {@link angular.mock.module module} 1702 | * 1703 | * Example of what a typical jasmine tests looks like with the inject method. 1704 | *
1705 |    *
1706 |    *   angular.module('myApplicationModule', [])
1707 |    *       .value('mode', 'app')
1708 |    *       .value('version', 'v1.0.1');
1709 |    *
1710 |    *
1711 |    *   describe('MyApp', function() {
1712 |    *
1713 |    *     // You need to load modules that you want to test,
1714 |    *     // it loads only the "ng" module by default.
1715 |    *     beforeEach(module('myApplicationModule'));
1716 |    *
1717 |    *
1718 |    *     // inject() is used to inject arguments of all given functions
1719 |    *     it('should provide a version', inject(function(mode, version) {
1720 |    *       expect(version).toEqual('v1.0.1');
1721 |    *       expect(mode).toEqual('app');
1722 |    *     }));
1723 |    *
1724 |    *
1725 |    *     // The inject and module method can also be used inside of the it or beforeEach
1726 |    *     it('should override a version and test the new version is injected', function() {
1727 |    *       // module() takes functions or strings (module aliases)
1728 |    *       module(function($provide) {
1729 |    *         $provide.value('version', 'overridden'); // override version here
1730 |    *       });
1731 |    *
1732 |    *       inject(function(version) {
1733 |    *         expect(version).toEqual('overridden');
1734 |    *       });
1735 |    *     ));
1736 |    *   });
1737 |    *
1738 |    * 
1739 | * 1740 | * @param {...Function} fns any number of functions which will be injected using the injector. 1741 | */ 1742 | window.inject = angular.mock.inject = function() { 1743 | var blockFns = Array.prototype.slice.call(arguments, 0); 1744 | var errorForStack = new Error('Declaration Location'); 1745 | return isSpecRunning() ? workFn() : workFn; 1746 | ///////////////////// 1747 | function workFn() { 1748 | var spec = getCurrentSpec(); 1749 | var modules = spec.$modules || []; 1750 | modules.unshift('ngMock'); 1751 | modules.unshift('ng'); 1752 | var injector = spec.$injector; 1753 | if (!injector) { 1754 | injector = spec.$injector = angular.injector(modules); 1755 | } 1756 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 1757 | try { 1758 | injector.invoke(blockFns[i] || angular.noop, this); 1759 | } catch (e) { 1760 | if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; 1761 | throw e; 1762 | } finally { 1763 | errorForStack = null; 1764 | } 1765 | } 1766 | } 1767 | }; 1768 | })(window); 1769 | -------------------------------------------------------------------------------- /test/lib/angular/jstd-scenario-adapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.0rc1 3 | * (c) 2010-2011 AngularJS http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window) { 7 | 'use strict'; 8 | 9 | /** 10 | * JSTestDriver adapter for angular scenario tests 11 | * 12 | * Example of jsTestDriver.conf for running scenario tests with JSTD: 13 |
 14 |     server: http://localhost:9877
 15 | 
 16 |     load:
 17 |       - lib/angular-scenario.js
 18 |       - lib/jstd-scenario-adapter-config.js
 19 |       - lib/jstd-scenario-adapter.js
 20 |       # your test files go here #
 21 | 
 22 |     proxy:
 23 |      - {matcher: "/your-prefix/*", server: "http://localhost:8000/"}
 24 |   
25 | * 26 | * For more information on how to configure jstd proxy, see {@link http://code.google.com/p/js-test-driver/wiki/Proxy} 27 | * Note the order of files - it's important ! 28 | * 29 | * Example of jstd-scenario-adapter-config.js 30 |
 31 |     var jstdScenarioAdapter = {
 32 |       relativeUrlPrefix: '/your-prefix/'
 33 |     };
 34 |   
35 | * 36 | * Whenever you use browser().navigateTo('relativeUrl') in your scenario test, the relativeUrlPrefix will be prepended. 37 | * You have to configure this to work together with JSTD proxy. 38 | * 39 | * Let's assume you are using the above configuration (jsTestDriver.conf and jstd-scenario-adapter-config.js): 40 | * Now, when you call browser().navigateTo('index.html') in your scenario test, the browser will open /your-prefix/index.html. 41 | * That matches the proxy, so JSTD will proxy this request to http://localhost:8000/index.html. 42 | */ 43 | 44 | /** 45 | * Custom type of test case 46 | * 47 | * @const 48 | * @see jstestdriver.TestCaseInfo 49 | */ 50 | var SCENARIO_TYPE = 'scenario'; 51 | 52 | /** 53 | * Plugin for JSTestDriver 54 | * Connection point between scenario's jstd output and jstestdriver. 55 | * 56 | * @see jstestdriver.PluginRegistrar 57 | */ 58 | function JstdPlugin() { 59 | var nop = function() {}; 60 | 61 | this.reportResult = nop; 62 | this.reportEnd = nop; 63 | this.runScenario = nop; 64 | 65 | this.name = 'Angular Scenario Adapter'; 66 | 67 | /** 68 | * Called for each JSTD TestCase 69 | * 70 | * Handles only SCENARIO_TYPE test cases. There should be only one fake TestCase. 71 | * Runs all scenario tests (under one fake TestCase) and report all results to JSTD. 72 | * 73 | * @param {jstestdriver.TestRunConfiguration} configuration 74 | * @param {Function} onTestDone 75 | * @param {Function} onAllTestsComplete 76 | * @returns {boolean} True if this type of test is handled by this plugin, false otherwise 77 | */ 78 | this.runTestConfiguration = function(configuration, onTestDone, onAllTestsComplete) { 79 | if (configuration.getTestCaseInfo().getType() != SCENARIO_TYPE) return false; 80 | 81 | this.reportResult = onTestDone; 82 | this.reportEnd = onAllTestsComplete; 83 | this.runScenario(); 84 | 85 | return true; 86 | }; 87 | 88 | this.getTestRunsConfigurationFor = function(testCaseInfos, expressions, testRunsConfiguration) { 89 | testRunsConfiguration.push( 90 | new jstestdriver.TestRunConfiguration( 91 | new jstestdriver.TestCaseInfo( 92 | 'Angular Scenario Tests', function() {}, SCENARIO_TYPE), [])); 93 | 94 | return true; 95 | }; 96 | } 97 | 98 | /** 99 | * Singleton instance of the plugin 100 | * Accessed using closure by: 101 | * - jstd output (reports to this plugin) 102 | * - initScenarioAdapter (register the plugin to jstd) 103 | */ 104 | var plugin = new JstdPlugin(); 105 | 106 | /** 107 | * Initialise scenario jstd-adapter 108 | * (only if jstestdriver is defined) 109 | * 110 | * @param {Object} jstestdriver Undefined when run from browser (without jstd) 111 | * @param {Function} initScenarioAndRun Function that inits scenario and runs all the tests 112 | * @param {Object=} config Configuration object, supported properties: 113 | * - relativeUrlPrefix: prefix for all relative links when navigateTo() 114 | */ 115 | function initScenarioAdapter(jstestdriver, initScenarioAndRun, config) { 116 | if (jstestdriver) { 117 | // create and register ScenarioPlugin 118 | jstestdriver.pluginRegistrar.register(plugin); 119 | plugin.runScenario = initScenarioAndRun; 120 | 121 | /** 122 | * HACK (angular.scenario.Application.navigateTo) 123 | * 124 | * We need to navigate to relative urls when running from browser (without JSTD), 125 | * because we want to allow running scenario tests without creating its own virtual host. 126 | * For example: http://angular.local/build/docs/docs-scenario.html 127 | * 128 | * On the other hand, when running with JSTD, we need to navigate to absolute urls, 129 | * because of JSTD proxy. (proxy, because of same domain policy) 130 | * 131 | * So this hack is applied only if running with JSTD and change all relative urls to absolute. 132 | */ 133 | var appProto = angular.scenario.Application.prototype, 134 | navigateTo = appProto.navigateTo, 135 | relativeUrlPrefix = config && config.relativeUrlPrefix || '/'; 136 | 137 | appProto.navigateTo = function(url, loadFn, errorFn) { 138 | if (url.charAt(0) != '/' && url.charAt(0) != '#' && 139 | url != 'about:blank' && !url.match(/^https?/)) { 140 | url = relativeUrlPrefix + url; 141 | } 142 | 143 | return navigateTo.call(this, url, loadFn, errorFn); 144 | }; 145 | } 146 | } 147 | 148 | /** 149 | * Builds proper TestResult object from given model spec 150 | * 151 | * TODO(vojta) report error details 152 | * 153 | * @param {angular.scenario.ObjectModel.Spec} spec 154 | * @returns {jstestdriver.TestResult} 155 | */ 156 | function createTestResultFromSpec(spec) { 157 | var map = { 158 | success: 'PASSED', 159 | error: 'ERROR', 160 | failure: 'FAILED' 161 | }; 162 | 163 | return new jstestdriver.TestResult( 164 | spec.fullDefinitionName, 165 | spec.name, 166 | jstestdriver.TestResult.RESULT[map[spec.status]], 167 | spec.error || '', 168 | spec.line || '', 169 | spec.duration); 170 | } 171 | 172 | /** 173 | * Generates JSTD output (jstestdriver.TestResult) 174 | */ 175 | angular.scenario.output('jstd', function(context, runner, model) { 176 | model.on('SpecEnd', function(spec) { 177 | plugin.reportResult(createTestResultFromSpec(spec)); 178 | }); 179 | 180 | model.on('RunnerEnd', function() { 181 | plugin.reportEnd(); 182 | }); 183 | }); 184 | initScenarioAdapter(window.jstestdriver, angular.scenario.setUpAndRun, window.jstdScenarioAdapter); 185 | })(window); 186 | -------------------------------------------------------------------------------- /test/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/lib/jasmine-jstd-adapter/JasmineAdapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Jasmine JsTestDriver Adapter. 3 | * @author misko@hevery.com (Misko Hevery) 4 | */ 5 | (function(window) { 6 | var rootDescribes = new Describes(window); 7 | var describePath = []; 8 | rootDescribes.collectMode(); 9 | 10 | var JASMINE_TYPE = 'jasmine test case'; 11 | TestCase('Jasmine Adapter Tests', null, JASMINE_TYPE); 12 | 13 | var jasminePlugin = { 14 | name:'jasmine', 15 | 16 | getTestRunsConfigurationFor: function(testCaseInfos, expressions, testRunsConfiguration) { 17 | for (var i = 0; i < testCaseInfos.length; i++) { 18 | if (testCaseInfos[i].getType() == JASMINE_TYPE) { 19 | testRunsConfiguration.push(new jstestdriver.TestRunConfiguration(testCaseInfos[i], [])); 20 | } 21 | } 22 | return false; 23 | }, 24 | 25 | runTestConfiguration: function(testRunConfiguration, onTestDone, onTestRunConfigurationComplete){ 26 | if (testRunConfiguration.getTestCaseInfo().getType() != JASMINE_TYPE) return false; 27 | 28 | var jasmineEnv = jasmine.currentEnv_ = new jasmine.Env(); 29 | rootDescribes.playback(); 30 | var specLog = jstestdriver.console.log_ = []; 31 | var start; 32 | jasmineEnv.specFilter = function(spec) { 33 | return rootDescribes.isExclusive(spec); 34 | }; 35 | jasmineEnv.reporter = { 36 | log: function(str){ 37 | specLog.push(str); 38 | }, 39 | 40 | reportRunnerStarting: function(runner) { }, 41 | 42 | reportSpecStarting: function(spec) { 43 | specLog = jstestdriver.console.log_ = []; 44 | start = new Date().getTime(); 45 | }, 46 | 47 | reportSpecResults: function(spec) { 48 | var suite = spec.suite; 49 | var results = spec.results(); 50 | if (results.skipped) return; 51 | var end = new Date().getTime(); 52 | var messages = []; 53 | var resultItems = results.getItems(); 54 | var state = 'passed'; 55 | for ( var i = 0; i < resultItems.length; i++) { 56 | if (!resultItems[i].passed()) { 57 | state = resultItems[i].message.match(/AssertionError:/) ? 'error' : 'failed'; 58 | messages.push({ 59 | message: resultItems[i].toString(), 60 | name: resultItems[i].trace.name, 61 | stack: formatStack(resultItems[i].trace.stack) 62 | }); 63 | } 64 | } 65 | onTestDone( 66 | new jstestdriver.TestResult( 67 | suite.getFullName(), 68 | spec.description, 69 | state, 70 | jstestdriver.angular.toJson(messages), 71 | specLog.join('\n'), 72 | end - start)); 73 | }, 74 | 75 | reportSuiteResults: function(suite) {}, 76 | 77 | reportRunnerResults: function(runner) { 78 | onTestRunConfigurationComplete(); 79 | } 80 | }; 81 | jasmineEnv.execute(); 82 | return true; 83 | }, 84 | 85 | onTestsFinish: function(){ 86 | jasmine.currentEnv_ = null; 87 | rootDescribes.collectMode(); 88 | } 89 | }; 90 | jstestdriver.pluginRegistrar.register(jasminePlugin); 91 | 92 | function formatStack(stack) { 93 | var lines = (stack||'').split(/\r?\n/); 94 | var frames = []; 95 | for (i = 0; i < lines.length; i++) { 96 | if (!lines[i].match(/\/jasmine[\.-]/)) { 97 | frames.push(lines[i].replace(/https?:\/\/\w+(:\d+)?\/test\//, '').replace(/^\s*/, ' ')); 98 | } 99 | } 100 | return frames.join('\n'); 101 | } 102 | 103 | function noop(){} 104 | function Describes(window){ 105 | var describes = {}; 106 | var beforeEachs = {}; 107 | var afterEachs = {}; 108 | // Here we store: 109 | // 0: everyone runs 110 | // 1: run everything under ddescribe 111 | // 2: run only iits (ignore ddescribe) 112 | var exclusive = 0; 113 | var collectMode = true; 114 | intercept('describe', describes); 115 | intercept('xdescribe', describes); 116 | intercept('beforeEach', beforeEachs); 117 | intercept('afterEach', afterEachs); 118 | 119 | function intercept(functionName, collection){ 120 | window[functionName] = function(desc, fn){ 121 | if (collectMode) { 122 | collection[desc] = function(){ 123 | jasmine.getEnv()[functionName](desc, fn); 124 | }; 125 | } else { 126 | jasmine.getEnv()[functionName](desc, fn); 127 | } 128 | }; 129 | } 130 | window.ddescribe = function(name, fn){ 131 | if (exclusive < 1) { 132 | exclusive = 1; // run ddescribe only 133 | } 134 | window.describe(name, function(){ 135 | var oldIt = window.it; 136 | window.it = function(name, fn){ 137 | fn.exclusive = 1; // run anything under ddescribe 138 | oldIt(name, fn); 139 | }; 140 | try { 141 | fn.call(this); 142 | } finally { 143 | window.it = oldIt; 144 | }; 145 | }); 146 | }; 147 | window.iit = function(name, fn){ 148 | exclusive = fn.exclusive = 2; // run only iits 149 | jasmine.getEnv().it(name, fn); 150 | }; 151 | 152 | 153 | this.collectMode = function() { 154 | collectMode = true; 155 | exclusive = 0; // run everything 156 | }; 157 | this.playback = function(){ 158 | collectMode = false; 159 | playback(beforeEachs); 160 | playback(afterEachs); 161 | playback(describes); 162 | 163 | function playback(set) { 164 | for ( var name in set) { 165 | set[name](); 166 | } 167 | } 168 | }; 169 | 170 | this.isExclusive = function(spec) { 171 | if (exclusive) { 172 | var blocks = spec.queue.blocks; 173 | for ( var i = 0; i < blocks.length; i++) { 174 | if (blocks[i].func.exclusive >= exclusive) { 175 | return true; 176 | } 177 | } 178 | return false; 179 | } 180 | return true; 181 | }; 182 | } 183 | 184 | })(window); 185 | 186 | // Patch Jasmine for proper stack traces 187 | jasmine.Spec.prototype.fail = function (e) { 188 | var expectationResult = new jasmine.ExpectationResult({ 189 | passed: false, 190 | message: e ? jasmine.util.formatException(e) : 'Exception' 191 | }); 192 | // PATCH 193 | if (e) { 194 | expectationResult.trace = e; 195 | } 196 | this.results_.addResult(expectationResult); 197 | }; 198 | 199 | -------------------------------------------------------------------------------- /test/lib/jasmine-jstd-adapter/version.txt: -------------------------------------------------------------------------------- 1 | da92db714142b49f9cf61db664e782bb0ccad80b @ 2011-10-18 2 | -------------------------------------------------------------------------------- /test/lib/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 | -------------------------------------------------------------------------------- /test/lib/jasmine/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var sys = require('sys'); 3 | var path = require('path'); 4 | 5 | var filename = __dirname + '/jasmine.js'; 6 | global.window = { 7 | setTimeout: setTimeout, 8 | clearTimeout: clearTimeout, 9 | setInterval: setInterval, 10 | clearInterval: clearInterval 11 | }; 12 | var src = fs.readFileSync(filename); 13 | var jasmine = process.compile(src + '\njasmine;', filename); 14 | delete global.window; 15 | 16 | function noop(){} 17 | 18 | jasmine.executeSpecsInFolder = function(folder, done, isVerbose, showColors, matcher){ 19 | var log = []; 20 | var columnCounter = 0; 21 | var start = 0; 22 | var elapsed = 0; 23 | var verbose = isVerbose || false; 24 | var fileMatcher = new RegExp(matcher || "\.js$"); 25 | var colors = showColors || false; 26 | var specs = jasmine.getAllSpecFiles(folder, fileMatcher); 27 | 28 | var ansi = { 29 | green: '\033[32m', 30 | red: '\033[31m', 31 | yellow: '\033[33m', 32 | none: '\033[0m' 33 | }; 34 | 35 | for (var i = 0, len = specs.length; i < len; ++i){ 36 | var filename = specs[i]; 37 | require(filename.replace(/\.*$/, "")); 38 | } 39 | 40 | var jasmineEnv = jasmine.getEnv(); 41 | jasmineEnv.reporter = { 42 | log: function(str){ 43 | }, 44 | 45 | reportSpecStarting: function(runner) { 46 | }, 47 | 48 | reportRunnerStarting: function(runner) { 49 | sys.puts('Started'); 50 | start = Number(new Date); 51 | }, 52 | 53 | reportSuiteResults: function(suite) { 54 | var specResults = suite.results(); 55 | var path = []; 56 | while(suite) { 57 | path.unshift(suite.description); 58 | suite = suite.parentSuite; 59 | } 60 | var description = path.join(' '); 61 | 62 | if (verbose) 63 | log.push('Spec ' + description); 64 | 65 | specResults.items_.forEach(function(spec){ 66 | if (spec.failedCount > 0 && spec.description) { 67 | if (!verbose) 68 | log.push(description); 69 | log.push(' it ' + spec.description); 70 | spec.items_.forEach(function(result){ 71 | log.push(' ' + result.trace.stack + '\n'); 72 | }); 73 | } 74 | }); 75 | }, 76 | 77 | reportSpecResults: function(spec) { 78 | var result = spec.results(); 79 | var msg = ''; 80 | if (result.passed()) 81 | { 82 | msg = (colors) ? (ansi.green + '.' + ansi.none) : '.'; 83 | // } else if (result.skipped) { TODO: Research why "result.skipped" returns false when "xit" is called on a spec? 84 | // msg = (colors) ? (ansi.yellow + '*' + ansi.none) : '*'; 85 | } else { 86 | msg = (colors) ? (ansi.red + 'F' + ansi.none) : 'F'; 87 | } 88 | sys.print(msg); 89 | if (columnCounter++ < 50) return; 90 | columnCounter = 0; 91 | sys.print('\n'); 92 | }, 93 | 94 | 95 | reportRunnerResults: function(runner) { 96 | elapsed = (Number(new Date) - start) / 1000; 97 | sys.puts('\n'); 98 | log.forEach(function(log){ 99 | sys.puts(log); 100 | }); 101 | sys.puts('Finished in ' + elapsed + ' seconds'); 102 | 103 | var summary = jasmine.printRunnerResults(runner); 104 | if(colors) 105 | { 106 | if(runner.results().failedCount === 0 ) 107 | sys.puts(ansi.green + summary + ansi.none); 108 | else 109 | sys.puts(ansi.red + summary + ansi.none); 110 | } else { 111 | sys.puts(summary); 112 | } 113 | (done||noop)(runner, log); 114 | } 115 | }; 116 | jasmineEnv.execute(); 117 | }; 118 | 119 | jasmine.getAllSpecFiles = function(dir, matcher){ 120 | var specs = []; 121 | 122 | if (fs.statSync(dir).isFile() && dir.match(matcher)) { 123 | specs.push(dir); 124 | } else { 125 | var files = fs.readdirSync(dir); 126 | for (var i = 0, len = files.length; i < len; ++i){ 127 | var filename = dir + '/' + files[i]; 128 | if (fs.statSync(filename).isFile() && filename.match(matcher)){ 129 | specs.push(filename); 130 | }else if (fs.statSync(filename).isDirectory()){ 131 | var subfiles = this.getAllSpecFiles(filename, matcher); 132 | subfiles.forEach(function(result){ 133 | specs.push(result); 134 | }); 135 | } 136 | } 137 | } 138 | 139 | return specs; 140 | }; 141 | 142 | jasmine.printRunnerResults = function(runner){ 143 | var results = runner.results(); 144 | var suites = runner.suites(); 145 | var msg = ''; 146 | msg += suites.length + ' test' + ((suites.length === 1) ? '' : 's') + ', '; 147 | msg += results.totalCount + ' assertion' + ((results.totalCount === 1) ? '' : 's') + ', '; 148 | msg += results.failedCount + ' failure' + ((results.failedCount === 1) ? '' : 's') + '\n'; 149 | return msg; 150 | }; 151 | 152 | function now(){ 153 | return new Date().getTime(); 154 | } 155 | 156 | jasmine.asyncSpecWait = function(){ 157 | var wait = jasmine.asyncSpecWait; 158 | wait.start = now(); 159 | wait.done = false; 160 | (function innerWait(){ 161 | waits(10); 162 | runs(function() { 163 | if (wait.start + wait.timeout < now()) { 164 | expect('timeout waiting for spec').toBeNull(); 165 | } else if (wait.done) { 166 | wait.done = false; 167 | } else { 168 | innerWait(); 169 | } 170 | }); 171 | })(); 172 | }; 173 | jasmine.asyncSpecWait.timeout = 4 * 1000; 174 | jasmine.asyncSpecDone = function(){ 175 | jasmine.asyncSpecWait.done = true; 176 | }; 177 | 178 | for ( var key in jasmine) { 179 | exports[key] = jasmine[key]; 180 | } -------------------------------------------------------------------------------- /test/lib/jasmine/jasmine-html.js: -------------------------------------------------------------------------------- 1 | jasmine.TrivialReporter = function(doc) { 2 | this.document = doc || document; 3 | this.suiteDivs = {}; 4 | this.logRunningSpecs = false; 5 | }; 6 | 7 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { 8 | var el = document.createElement(type); 9 | 10 | for (var i = 2; i < arguments.length; i++) { 11 | var child = arguments[i]; 12 | 13 | if (typeof child === 'string') { 14 | el.appendChild(document.createTextNode(child)); 15 | } else { 16 | if (child) { el.appendChild(child); } 17 | } 18 | } 19 | 20 | for (var attr in attrs) { 21 | if (attr == "className") { 22 | el[attr] = attrs[attr]; 23 | } else { 24 | el.setAttribute(attr, attrs[attr]); 25 | } 26 | } 27 | 28 | return el; 29 | }; 30 | 31 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { 32 | var showPassed, showSkipped; 33 | 34 | this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, 35 | this.createDom('div', { className: 'banner' }, 36 | this.createDom('div', { className: 'logo' }, 37 | this.createDom('span', { className: 'title' }, "Jasmine"), 38 | this.createDom('span', { className: 'version' }, runner.env.versionString())), 39 | this.createDom('div', { className: 'options' }, 40 | "Show ", 41 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), 42 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), 43 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), 44 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") 45 | ) 46 | ), 47 | 48 | this.runnerDiv = this.createDom('div', { className: 'runner running' }, 49 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), 50 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."), 51 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) 52 | ); 53 | 54 | this.document.body.appendChild(this.outerDiv); 55 | 56 | var suites = runner.suites(); 57 | for (var i = 0; i < suites.length; i++) { 58 | var suite = suites[i]; 59 | var suiteDiv = this.createDom('div', { className: 'suite' }, 60 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), 61 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); 62 | this.suiteDivs[suite.id] = suiteDiv; 63 | var parentDiv = this.outerDiv; 64 | if (suite.parentSuite) { 65 | parentDiv = this.suiteDivs[suite.parentSuite.id]; 66 | } 67 | parentDiv.appendChild(suiteDiv); 68 | } 69 | 70 | this.startedAt = new Date(); 71 | 72 | var self = this; 73 | showPassed.onclick = function(evt) { 74 | if (showPassed.checked) { 75 | self.outerDiv.className += ' show-passed'; 76 | } else { 77 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); 78 | } 79 | }; 80 | 81 | showSkipped.onclick = function(evt) { 82 | if (showSkipped.checked) { 83 | self.outerDiv.className += ' show-skipped'; 84 | } else { 85 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); 86 | } 87 | }; 88 | }; 89 | 90 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { 91 | var results = runner.results(); 92 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; 93 | this.runnerDiv.setAttribute("class", className); 94 | //do it twice for IE 95 | this.runnerDiv.setAttribute("className", className); 96 | var specs = runner.specs(); 97 | var specCount = 0; 98 | for (var i = 0; i < specs.length; i++) { 99 | if (this.specFilter(specs[i])) { 100 | specCount++; 101 | } 102 | } 103 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); 104 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; 105 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); 106 | 107 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); 108 | }; 109 | 110 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { 111 | var results = suite.results(); 112 | var status = results.passed() ? 'passed' : 'failed'; 113 | if (results.totalCount === 0) { // todo: change this to check results.skipped 114 | status = 'skipped'; 115 | } 116 | this.suiteDivs[suite.id].className += " " + status; 117 | }; 118 | 119 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { 120 | if (this.logRunningSpecs) { 121 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 122 | } 123 | }; 124 | 125 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { 126 | var results = spec.results(); 127 | var status = results.passed() ? 'passed' : 'failed'; 128 | if (results.skipped) { 129 | status = 'skipped'; 130 | } 131 | var specDiv = this.createDom('div', { className: 'spec ' + status }, 132 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), 133 | this.createDom('a', { 134 | className: 'description', 135 | href: '?spec=' + encodeURIComponent(spec.getFullName()), 136 | title: spec.getFullName() 137 | }, spec.description)); 138 | 139 | 140 | var resultItems = results.getItems(); 141 | var messagesDiv = this.createDom('div', { className: 'messages' }); 142 | for (var i = 0; i < resultItems.length; i++) { 143 | var result = resultItems[i]; 144 | 145 | if (result.type == 'log') { 146 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 147 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 148 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 149 | 150 | if (result.trace.stack) { 151 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 152 | } 153 | } 154 | } 155 | 156 | if (messagesDiv.childNodes.length > 0) { 157 | specDiv.appendChild(messagesDiv); 158 | } 159 | 160 | this.suiteDivs[spec.suite.id].appendChild(specDiv); 161 | }; 162 | 163 | jasmine.TrivialReporter.prototype.log = function() { 164 | var console = jasmine.getGlobal().console; 165 | if (console && console.log) { 166 | if (console.log.apply) { 167 | console.log.apply(console, arguments); 168 | } else { 169 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 170 | } 171 | } 172 | }; 173 | 174 | jasmine.TrivialReporter.prototype.getLocation = function() { 175 | return this.document.location; 176 | }; 177 | 178 | jasmine.TrivialReporter.prototype.specFilter = function(spec) { 179 | var paramMap = {}; 180 | var params = this.getLocation().search.substring(1).split('&'); 181 | for (var i = 0; i < params.length; i++) { 182 | var p = params[i].split('='); 183 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 184 | } 185 | 186 | if (!paramMap.spec) { 187 | return true; 188 | } 189 | return spec.getFullName().indexOf(paramMap.spec) === 0; 190 | }; 191 | -------------------------------------------------------------------------------- /test/lib/jasmine/jasmine.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; 3 | } 4 | 5 | 6 | .jasmine_reporter a:visited, .jasmine_reporter a { 7 | color: #303; 8 | } 9 | 10 | .jasmine_reporter a:hover, .jasmine_reporter a:active { 11 | color: blue; 12 | } 13 | 14 | .run_spec { 15 | float:right; 16 | padding-right: 5px; 17 | font-size: .8em; 18 | text-decoration: none; 19 | } 20 | 21 | .jasmine_reporter { 22 | margin: 0 5px; 23 | } 24 | 25 | .banner { 26 | color: #303; 27 | background-color: #fef; 28 | padding: 5px; 29 | } 30 | 31 | .logo { 32 | float: left; 33 | font-size: 1.1em; 34 | padding-left: 5px; 35 | } 36 | 37 | .logo .version { 38 | font-size: .6em; 39 | padding-left: 1em; 40 | } 41 | 42 | .runner.running { 43 | background-color: yellow; 44 | } 45 | 46 | 47 | .options { 48 | text-align: right; 49 | font-size: .8em; 50 | } 51 | 52 | 53 | 54 | 55 | .suite { 56 | border: 1px outset gray; 57 | margin: 5px 0; 58 | padding-left: 1em; 59 | } 60 | 61 | .suite .suite { 62 | margin: 5px; 63 | } 64 | 65 | .suite.passed { 66 | background-color: #dfd; 67 | } 68 | 69 | .suite.failed { 70 | background-color: #fdd; 71 | } 72 | 73 | .spec { 74 | margin: 5px; 75 | padding-left: 1em; 76 | clear: both; 77 | } 78 | 79 | .spec.failed, .spec.passed, .spec.skipped { 80 | padding-bottom: 5px; 81 | border: 1px solid gray; 82 | } 83 | 84 | .spec.failed { 85 | background-color: #fbb; 86 | border-color: red; 87 | } 88 | 89 | .spec.passed { 90 | background-color: #bfb; 91 | border-color: green; 92 | } 93 | 94 | .spec.skipped { 95 | background-color: #bbb; 96 | } 97 | 98 | .messages { 99 | border-left: 1px dashed gray; 100 | padding-left: 1em; 101 | padding-right: 1em; 102 | } 103 | 104 | .passed { 105 | background-color: #cfc; 106 | display: none; 107 | } 108 | 109 | .failed { 110 | background-color: #fbb; 111 | } 112 | 113 | .skipped { 114 | color: #777; 115 | background-color: #eee; 116 | display: none; 117 | } 118 | 119 | 120 | /*.resultMessage {*/ 121 | /*white-space: pre;*/ 122 | /*}*/ 123 | 124 | .resultMessage span.result { 125 | display: block; 126 | line-height: 2em; 127 | color: black; 128 | } 129 | 130 | .resultMessage .mismatch { 131 | color: black; 132 | } 133 | 134 | .stackTrace { 135 | white-space: pre; 136 | font-size: .8em; 137 | margin-left: 10px; 138 | max-height: 5em; 139 | overflow: auto; 140 | border: 1px inset red; 141 | padding: 1em; 142 | background: #eef; 143 | } 144 | 145 | .finished-at { 146 | padding-left: 1em; 147 | font-size: .6em; 148 | } 149 | 150 | .show-passed .passed, 151 | .show-skipped .skipped { 152 | display: block; 153 | } 154 | 155 | 156 | #jasmine_content { 157 | position:fixed; 158 | right: 100%; 159 | } 160 | 161 | .runner { 162 | border: 1px solid gray; 163 | display: block; 164 | margin: 5px 0; 165 | padding: 2px 0 2px 10px; 166 | } 167 | -------------------------------------------------------------------------------- /test/lib/jasmine/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/test/lib/jasmine/jasmine_favicon.png -------------------------------------------------------------------------------- /test/lib/jasmine/version.txt: -------------------------------------------------------------------------------- 1 | 1.1.0 2 | -------------------------------------------------------------------------------- /test/lib/jstestdriver/JsTestDriver.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IgorMinar/Memory-Game/3ccaf3a3d8d3c0c7706a23d604e76ac5692cfbe4/test/lib/jstestdriver/JsTestDriver.jar -------------------------------------------------------------------------------- /test/lib/jstestdriver/version.txt: -------------------------------------------------------------------------------- 1 | 1.3.3d 2 | -------------------------------------------------------------------------------- /test/unit/appSpec.js: -------------------------------------------------------------------------------- 1 | /* jasmine specs for controllers go here */ 2 | 3 | describe('MemoryGameApp', function() { 4 | 5 | beforeEach(module('memoryGameApp')); 6 | 7 | 8 | describe('GameCtrl', function(){ 9 | var gameCtrl, scope; 10 | 11 | beforeEach(inject(function($controller, $rootScope){ 12 | scope = $rootScope.$new(); 13 | gameCtrl = $controller('GameCtrl', {$scope: scope}); 14 | })); 15 | 16 | 17 | it('should publish the game model', function() { 18 | expect(scope.game).toBeDefined(); 19 | }); 20 | }); 21 | 22 | 23 | describe('game', function(){ 24 | var game; 25 | 26 | 27 | beforeEach(inject(function(_game_){ 28 | game = _game_; 29 | })); 30 | 31 | 32 | it('should create a game with 8 tile pairs', function() { 33 | expect(game.unmatchedPairs).toBe(8); 34 | }); 35 | }); 36 | 37 | 38 | describe('mgCard directive', function() { 39 | it('should render a card using divs and bind it to a tile', inject(function($compile, $rootScope) { 40 | var tile = new Tile('sampleTile'), 41 | element; 42 | 43 | $rootScope.tileModel = tile; 44 | element = $compile('')($rootScope); 45 | $rootScope.$apply(); 46 | 47 | expect(element.find('div').find('div').find('img').eq(1).attr('src')). 48 | toBe('img/sampleTile.png'); 49 | })); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/gameSpec.js: -------------------------------------------------------------------------------- 1 | describe('Game', function() { 2 | 3 | var tileNames = ['turtle', 'hammer'], 4 | game; 5 | 6 | beforeEach(function() { 7 | game = new Game(tileNames); 8 | }); 9 | 10 | 11 | describe('init', function() { 12 | 13 | it('should create a 2x2 grid', function() { 14 | expect(game.grid.length).toBe(2); 15 | expect(game.grid[0].length).toBe(2); 16 | expect(game.grid[1].length).toBe(2); 17 | }); 18 | 19 | 20 | it('should initialize unmatchedPairs and message', function() { 21 | expect(game.message).toBe('Click on a tile.'); 22 | expect(game.unmatchedPairs).toBe(2); 23 | }); 24 | }); 25 | 26 | 27 | describe('flip', function() { 28 | var tile1a, tile1b, tile2a, tile2b; 29 | 30 | 31 | beforeEach(function() { 32 | // this is messy because Game constructor is non-deterministic - it should be refactored 33 | tile1a = game.grid[0][0]; 34 | 35 | if (tile1a.title === game.grid[0][1].title) { 36 | tile1b = game.grid[0][1]; 37 | tile2a = game.grid[1][0]; 38 | tile2b = game.grid[1][1]; 39 | } else { 40 | tile2a = game.grid[0][1]; 41 | 42 | if (tile1a.title === game.grid[1][0].title) { 43 | tile1b = game.grid[1][0]; 44 | tile2b = game.grid[1][1]; 45 | } else { 46 | tile1b = game.grid[1][1]; 47 | tile2b = game.grid[1][0]; 48 | } 49 | } 50 | }); 51 | 52 | 53 | it('should set the flipped flag on the tile being flipped', function() { 54 | expect(tile1a.flipped).toBe(false); 55 | game.flipTile(tile1a); 56 | expect(tile1a.flipped).toBe(true); 57 | }); 58 | 59 | 60 | it('should turn back first two cards when miss and third card is flipped', function() { 61 | game.flipTile(tile1a); 62 | game.flipTile(tile2a); 63 | expect(tile1a.flipped).toBe(true); 64 | expect(tile2a.flipped).toBe(true); 65 | expect(tile1b.flipped).toBe(false); 66 | 67 | game.flipTile(tile1b); 68 | expect(tile1a.flipped).toBe(false); 69 | expect(tile2a.flipped).toBe(false); 70 | expect(tile1b.flipped).toBe(true); 71 | expect(game.unmatchedPairs).toBe(2); 72 | }); 73 | 74 | 75 | it('should keep the first two cards turned when hit and third card is flipped', function() { 76 | game.flipTile(tile1a); 77 | game.flipTile(tile1b); 78 | expect(tile1a.flipped).toBe(true); 79 | expect(tile1b.flipped).toBe(true); 80 | expect(tile2a.flipped).toBe(false); 81 | expect(tile2b.flipped).toBe(false); 82 | expect(game.unmatchedPairs).toBe(1); 83 | 84 | game.flipTile(tile2a); 85 | expect(tile1a.flipped).toBe(true); 86 | expect(tile1b.flipped).toBe(true); 87 | expect(tile2a.flipped).toBe(true); 88 | expect(tile2b.flipped).toBe(false); 89 | expect(game.unmatchedPairs).toBe(1); 90 | 91 | game.flipTile(tile2b); 92 | expect(tile1a.flipped).toBe(true); 93 | expect(tile1b.flipped).toBe(true); 94 | expect(tile2a.flipped).toBe(true); 95 | expect(tile2b.flipped).toBe(true); 96 | expect(game.unmatchedPairs).toBe(0); 97 | }); 98 | 99 | 100 | it('should keep the message model up to date during the game', function() { 101 | expect(game.message).toBe(Game.MESSAGE_CLICK); 102 | 103 | game.flipTile(tile1a); 104 | expect(game.message).toBe(Game.MESSAGE_ONE_MORE); 105 | 106 | game.flipTile(tile2b); 107 | expect(game.message).toBe(Game.MESSAGE_MISS); 108 | 109 | game.flipTile(tile1b); 110 | expect(game.message).toBe(Game.MESSAGE_ONE_MORE); 111 | 112 | game.flipTile(tile1a); 113 | expect(game.message).toBe(Game.MESSAGE_MATCH); 114 | 115 | game.flipTile(tile2b); 116 | expect(game.message).toBe(Game.MESSAGE_ONE_MORE); 117 | 118 | game.flipTile(tile2a); 119 | expect(game.message).toBe(Game.MESSAGE_WON); 120 | }); 121 | }); 122 | }); 123 | --------------------------------------------------------------------------------