├── LICENSE ├── README.md ├── app ├── css │ └── styles.css ├── img │ ├── angular.png │ ├── igor.jpg │ ├── misko.jpg │ └── vojta.jpg ├── index.html └── js │ ├── app.js │ ├── lib │ └── angular │ │ ├── angular-mocks.js │ │ ├── angular.js │ │ └── version.txt │ └── puzzle │ ├── slidingPuzzle.js │ └── wordSearchPuzzle.js └── test ├── config.js └── unit ├── appSpec.js └── slidingPuzzleSpec.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pavol Daniš, http://palto.sk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-puzzle 2 | ============== 3 | 4 | Simple puzzle games made with AngularJS to demostrate its awesome features, simplicity and power. 5 | 6 | Take a look at the demo [pdanis.github.com/angular-puzzle](http://pdanis.github.com/angular-puzzle/ "demo") and have fun ;-) 7 | -------------------------------------------------------------------------------- /app/css/styles.css: -------------------------------------------------------------------------------- 1 | /* General */ 2 | body { 3 | font: 12pt 'Arial', 'Helvetica'; 4 | background-color: #f7f7f7; 5 | margin-top: 110px; 6 | margin-left: 20px; 7 | } 8 | fieldset { 9 | margin: 0 0 30px 0; 10 | padding: 18px; 11 | border: 2px solid #ddd; 12 | background-color: white; 13 | border-radius: 10px; 14 | display: inline-block; 15 | } 16 | legend { 17 | font-weight: bold; 18 | padding: 3px 12px; 19 | display: block; 20 | background-color: #e4e4e4; 21 | border-radius: 6px; 22 | } 23 | ul { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /* Puzzle widgets */ 30 | 31 | .sliding-puzzle { 32 | border: 1px solid #ccc; 33 | border-spacing: 0; 34 | } 35 | .sliding-puzzle td { 36 | border-collapse: collapse; 37 | border: 2px outset white; 38 | cursor: pointer; 39 | padding: 0; 40 | } 41 | .sliding-puzzle .puzzle-empty { 42 | border: none; 43 | background-color: #ddd !important; 44 | } 45 | .sliding-puzzle.puzzle-solved td { 46 | border: none; 47 | } 48 | 49 | .word-search-puzzle { 50 | border: 1px solid #999; 51 | } 52 | .word-search-puzzle td { 53 | background-color: white; 54 | -moz-user-select: -moz-none; 55 | -khtml-user-select: none; 56 | -webkit-user-select: none; 57 | -ms-user-select: none; 58 | } 59 | .word-search-puzzle.puzzle-solved td { 60 | background-color: #eee; 61 | } 62 | .word-search-puzzle td span { 63 | display: table-cell; 64 | vertical-align: middle; 65 | width: 30px; 66 | height: 30px; 67 | text-align: center; 68 | user-select: none; 69 | cursor: pointer; 70 | } 71 | .word-search-puzzle .puzzle-found { 72 | background-color: #eee; 73 | } 74 | .word-search-puzzle .puzzle-selected { 75 | background-color: #f5f5f5; 76 | } 77 | .word-search-puzzle .puzzle-message span { 78 | background-color: #ffffcc; 79 | color: red; 80 | font-weight: bold; 81 | border-radius: 15px; 82 | box-shadow: 0 0 5px #444; 83 | } 84 | 85 | 86 | /* Application */ 87 | 88 | #types { 89 | margin: 0 0 40px 0px; 90 | padding: 10px; 91 | list-style: none; 92 | border-bottom: 1px solid #ddd; 93 | position: fixed; 94 | top: 0; 95 | left: 0; 96 | width: 100%; 97 | background-color: #f3f3f3; 98 | } 99 | #types li { 100 | display: inline-block; 101 | padding: 10px; 102 | margin-right: 40px; 103 | } 104 | #types li a { 105 | font-size: 22pt; 106 | color: #999; 107 | } 108 | #types li.selected { 109 | background-color: white; 110 | border-radius: 10px; 111 | box-shadow: 0 0 5px #999; 112 | -webkit-box-shadow: 0 0 5px #999; 113 | -moz-box-shadow: 0 0 5px #999; 114 | } 115 | #types li.selected a { 116 | font-weight: bold; 117 | color: black; 118 | text-decoration: none; 119 | } 120 | 121 | #fork { 122 | position: fixed; 123 | top: 0; 124 | right: 0; 125 | display: block; 126 | width: 149px; 127 | height: 149px; 128 | background: url('https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png') no-repeat; 129 | } 130 | #powered { 131 | position: fixed; 132 | right: 20px; 133 | bottom: 15px; 134 | display: block; 135 | } 136 | #powered img { 137 | opacity: 0.4; 138 | width: 200px; 139 | } 140 | 141 | #sliding-advanced > div { 142 | float: left; 143 | margin: 0 40px 0 0; 144 | min-width: 300px; 145 | } 146 | #sliding-advanced .sliding-puzzle { 147 | clear: both; 148 | margin-top: 30px; 149 | } 150 | #sliding-advanced h2 { 151 | margin: 0 0 10px 0; 152 | font-weight: bold; 153 | } 154 | #sliding-advanced .status { 155 | float: left; 156 | width: 40%; 157 | height: 50px; 158 | } 159 | #sliding-advanced .size { 160 | float: right; 161 | height: 50px; 162 | text-align: right; 163 | } 164 | #sliding-advanced .src input { 165 | width: 98%; 166 | font-family: "Courier New"; 167 | margin-bottom: 4px; 168 | } 169 | 170 | #word-search { 171 | position: relative; 172 | } 173 | #word-search .word-search-puzzle { 174 | float: left; 175 | } 176 | #word-search .words { 177 | list-style: none; 178 | float: left; 179 | margin: 4px 20px 0 0; 180 | } 181 | #word-search .words li { 182 | font-size: 12px; 183 | margin-bottom: 4px; 184 | text-align: center; 185 | } 186 | #word-search .words .found { 187 | color: #bbb; 188 | text-decoration: line-through; 189 | font-weight: normal 190 | } 191 | #word-search .status { 192 | position: absolute; 193 | left: 0; 194 | top: 330px; 195 | text-align: center; 196 | width: 80px; 197 | font-weight: bold; 198 | } 199 | -------------------------------------------------------------------------------- /app/img/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdanis/angular-puzzle/b84650950985cc177d402dc854610c93cf3203c0/app/img/angular.png -------------------------------------------------------------------------------- /app/img/igor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdanis/angular-puzzle/b84650950985cc177d402dc854610c93cf3203c0/app/img/igor.jpg -------------------------------------------------------------------------------- /app/img/misko.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdanis/angular-puzzle/b84650950985cc177d402dc854610c93cf3203c0/app/img/misko.jpg -------------------------------------------------------------------------------- /app/img/vojta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdanis/angular-puzzle/b84650950985cc177d402dc854610c93cf3203c0/app/img/vojta.jpg -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AngularJS puzzle 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 51 | 52 | 66 | 67 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | (function(angular) { 2 | 'use strict'; 3 | 4 | var app = angular.module('puzzleApp', ['slidingPuzzle', 'wordSearchPuzzle']); 5 | 6 | // puzzle types 7 | var types = [ 8 | { id: 'sliding-puzzle', title: 'Sliding puzzle' }, 9 | { id: 'word-search-puzzle', title: 'Word search puzzle' } 10 | ]; 11 | 12 | /** 13 | * Config 14 | */ 15 | app.config(function($routeProvider) { 16 | $routeProvider.when('/:type'); 17 | }); 18 | 19 | /** 20 | * Startup 21 | */ 22 | app.run(function($rootScope, $route, $filter) { 23 | $rootScope.types = types; 24 | $rootScope.type = types[0].id; 25 | 26 | // set type on route change 27 | $rootScope.$on('$routeChangeSuccess', function(event, route) { 28 | $rootScope.type = ($filter('filter')(types, { id: route.params.type }).length ? route.params.type : types[0].id); 29 | }); 30 | }); 31 | 32 | /** 33 | * Advanced sliding puzzle controller 34 | */ 35 | app.controller('slidingAdvancedCtrl', function($scope) { 36 | $scope.puzzles = [ 37 | { src: './img/misko.jpg', title: 'Miško Hevery', rows: 4, cols: 4 }, 38 | { src: './img/igor.jpg', title: 'Igor Minár', rows: 3, cols: 3 }, 39 | { src: './img/vojta.jpg', title: 'Vojta Jína', rows: 4, cols: 3 } 40 | ]; 41 | }); 42 | 43 | /** 44 | * Word search puzzle controller 45 | */ 46 | app.controller('wordSearchCtrl', function($scope) { 47 | $scope.matrix = [ 48 | ['N', 'I', 'G', 'O', 'R', 'Y', 'G', 'S', 'T', 'T', 'A', 'N'], 49 | ['O', 'G', 'G', 'U', 'L', 'C', 'O', 'E', 'P', 'E', 'A', 'S'], 50 | ['I', 'N', 'N', 'R', 'M', 'N', 'O', 'R', 'I', 'M', 'E', 'C'], 51 | ['T', 'I', 'A', 'I', 'O', 'E', 'G', 'V', 'R', 'P', 'V', 'E'], 52 | ['C', 'T', 'T', 'E', 'D', 'D', 'L', 'I', 'C', 'L', 'I', 'N'], 53 | ['E', 'S', 'J', 'P', 'U', 'N', 'E', 'C', 'S', 'A', 'T', 'A'], 54 | ['J', 'E', 'O', 'O', 'L', 'E', 'I', 'E', 'A', 'T', 'C', 'R'], 55 | ['N', 'T', 'V', 'C', 'E', 'P', 'J', 'B', 'V', 'E', 'E', 'I'], 56 | ['I', 'S', 'I', 'S', 'S', 'E', 'S', 'A', 'A', 'W', 'R', 'O'], 57 | ['O', 'K', 'S', 'I', 'M', 'D', 'E', 'S', 'J', 'O', 'I', 'M'], 58 | ['R', 'E', 'L', 'L', 'O', 'R', 'T', 'N', 'O', 'C', 'D', 'E'] 59 | ]; 60 | $scope.words = [ 61 | 'BINDING', 'CONTROLLER', 'DEPENDENCY', 'DIRECTIVE', 'GOOGLE', 'IGOR', 'INJECTION', 'JAVASCRIPT', 62 | 'MISKO', 'MODULES', 'SCENARIO', 'SCOPE', 'SERVICE', 'TEMPLATE', 'TESTING', 'VOJTA' 63 | ]; 64 | }); 65 | 66 | })(window.angular); -------------------------------------------------------------------------------- /app/js/lib/angular/angular-mocks.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @license AngularJS v1.0.1 4 | * (c) 2010-2012 Google, Inc. http://angularjs.org 5 | * License: MIT 6 | * 7 | * TODO(vojta): wrap whole file into closure during build 8 | */ 9 | 10 | /** 11 | * @ngdoc overview 12 | * @name angular.mock 13 | * @description 14 | * 15 | * Namespace from 'angular-mocks.js' which contains testing related code. 16 | */ 17 | angular.mock = {}; 18 | 19 | /** 20 | * ! This is a private undocumented service ! 21 | * 22 | * @name ngMock.$browser 23 | * 24 | * @description 25 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 26 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 27 | * cookies, etc... 28 | * 29 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 30 | * that there are several helper methods available which can be used in tests. 31 | */ 32 | angular.mock.$BrowserProvider = function() { 33 | this.$get = function(){ 34 | return new angular.mock.$Browser(); 35 | }; 36 | }; 37 | 38 | angular.mock.$Browser = function() { 39 | var self = this; 40 | 41 | this.isMock = true; 42 | self.$$url = "http://server/"; 43 | self.$$lastUrl = self.$$url; // used by url polling fn 44 | self.pollFns = []; 45 | 46 | // TODO(vojta): remove this temporary api 47 | self.$$completeOutstandingRequest = angular.noop; 48 | self.$$incOutstandingRequestCount = angular.noop; 49 | 50 | 51 | // register url polling fn 52 | 53 | self.onUrlChange = function(listener) { 54 | self.pollFns.push( 55 | function() { 56 | if (self.$$lastUrl != self.$$url) { 57 | self.$$lastUrl = self.$$url; 58 | listener(self.$$url); 59 | } 60 | } 61 | ); 62 | 63 | return listener; 64 | }; 65 | 66 | self.cookieHash = {}; 67 | self.lastCookieHash = {}; 68 | self.deferredFns = []; 69 | self.deferredNextId = 0; 70 | 71 | self.defer = function(fn, delay) { 72 | delay = delay || 0; 73 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 74 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 75 | return self.deferredNextId++; 76 | }; 77 | 78 | 79 | self.defer.now = 0; 80 | 81 | 82 | self.defer.cancel = function(deferId) { 83 | var fnIndex; 84 | 85 | angular.forEach(self.deferredFns, function(fn, index) { 86 | if (fn.id === deferId) fnIndex = index; 87 | }); 88 | 89 | if (fnIndex !== undefined) { 90 | self.deferredFns.splice(fnIndex, 1); 91 | return true; 92 | } 93 | 94 | return false; 95 | }; 96 | 97 | 98 | /** 99 | * @name ngMock.$browser#defer.flush 100 | * @methodOf ngMock.$browser 101 | * 102 | * @description 103 | * Flushes all pending requests and executes the defer callbacks. 104 | * 105 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 106 | */ 107 | self.defer.flush = function(delay) { 108 | if (angular.isDefined(delay)) { 109 | self.defer.now += delay; 110 | } else { 111 | if (self.deferredFns.length) { 112 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 113 | } else { 114 | throw Error('No deferred tasks to be flushed'); 115 | } 116 | } 117 | 118 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 119 | self.deferredFns.shift().fn(); 120 | } 121 | }; 122 | /** 123 | * @name ngMock.$browser#defer.now 124 | * @propertyOf ngMock.$browser 125 | * 126 | * @description 127 | * Current milliseconds mock time. 128 | */ 129 | 130 | self.$$baseHref = ''; 131 | self.baseHref = function() { 132 | return this.$$baseHref; 133 | }; 134 | }; 135 | angular.mock.$Browser.prototype = { 136 | 137 | /** 138 | * @name ngMock.$browser#poll 139 | * @methodOf ngMock.$browser 140 | * 141 | * @description 142 | * run all fns in pollFns 143 | */ 144 | poll: function poll() { 145 | angular.forEach(this.pollFns, function(pollFn){ 146 | pollFn(); 147 | }); 148 | }, 149 | 150 | addPollFn: function(pollFn) { 151 | this.pollFns.push(pollFn); 152 | return pollFn; 153 | }, 154 | 155 | url: function(url, replace) { 156 | if (url) { 157 | this.$$url = url; 158 | return this; 159 | } 160 | 161 | return this.$$url; 162 | }, 163 | 164 | cookies: function(name, value) { 165 | if (name) { 166 | if (value == undefined) { 167 | delete this.cookieHash[name]; 168 | } else { 169 | if (angular.isString(value) && //strings only 170 | value.length <= 4096) { //strict cookie storage limits 171 | this.cookieHash[name] = value; 172 | } 173 | } 174 | } else { 175 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 176 | this.lastCookieHash = angular.copy(this.cookieHash); 177 | this.cookieHash = angular.copy(this.cookieHash); 178 | } 179 | return this.cookieHash; 180 | } 181 | }, 182 | 183 | notifyWhenNoOutstandingRequests: function(fn) { 184 | fn(); 185 | } 186 | }; 187 | 188 | 189 | /** 190 | * @ngdoc object 191 | * @name ngMock.$exceptionHandlerProvider 192 | * 193 | * @description 194 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors passed 195 | * into the `$exceptionHandler`. 196 | */ 197 | 198 | /** 199 | * @ngdoc object 200 | * @name ngMock.$exceptionHandler 201 | * 202 | * @description 203 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 204 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 205 | * information. 206 | */ 207 | 208 | angular.mock.$ExceptionHandlerProvider = function() { 209 | var handler; 210 | 211 | /** 212 | * @ngdoc method 213 | * @name ngMock.$exceptionHandlerProvider#mode 214 | * @methodOf ngMock.$exceptionHandlerProvider 215 | * 216 | * @description 217 | * Sets the logging mode. 218 | * 219 | * @param {string} mode Mode of operation, defaults to `rethrow`. 220 | * 221 | * - `rethrow`: If any errors are are passed into the handler in tests, it typically 222 | * means that there is a bug in the application or test, so this mock will 223 | * make these tests fail. 224 | * - `log`: Sometimes it is desirable to test that an error is throw, for this case the `log` mode stores the 225 | * error and allows later assertion of it. 226 | * See {@link ngMock.$log#assertEmpty assertEmpty()} and 227 | * {@link ngMock.$log#reset reset()} 228 | */ 229 | this.mode = function(mode) { 230 | switch(mode) { 231 | case 'rethrow': 232 | handler = function(e) { 233 | throw e; 234 | }; 235 | break; 236 | case 'log': 237 | var errors = []; 238 | 239 | handler = function(e) { 240 | if (arguments.length == 1) { 241 | errors.push(e); 242 | } else { 243 | errors.push([].slice.call(arguments, 0)); 244 | } 245 | }; 246 | 247 | handler.errors = errors; 248 | break; 249 | default: 250 | throw Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 251 | } 252 | }; 253 | 254 | this.$get = function() { 255 | return handler; 256 | }; 257 | 258 | this.mode('rethrow'); 259 | }; 260 | 261 | 262 | /** 263 | * @ngdoc service 264 | * @name ngMock.$log 265 | * 266 | * @description 267 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 268 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 269 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 270 | * 271 | */ 272 | angular.mock.$LogProvider = function() { 273 | 274 | function concat(array1, array2, index) { 275 | return array1.concat(Array.prototype.slice.call(array2, index)); 276 | } 277 | 278 | 279 | this.$get = function () { 280 | var $log = { 281 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 282 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 283 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 284 | error: function() { $log.error.logs.push(concat([], arguments, 0)); } 285 | }; 286 | 287 | /** 288 | * @ngdoc method 289 | * @name ngMock.$log#reset 290 | * @methodOf ngMock.$log 291 | * 292 | * @description 293 | * Reset all of the logging arrays to empty. 294 | */ 295 | $log.reset = function () { 296 | /** 297 | * @ngdoc property 298 | * @name ngMock.$log#log.logs 299 | * @propertyOf ngMock.$log 300 | * 301 | * @description 302 | * Array of logged messages. 303 | */ 304 | $log.log.logs = []; 305 | /** 306 | * @ngdoc property 307 | * @name ngMock.$log#warn.logs 308 | * @propertyOf ngMock.$log 309 | * 310 | * @description 311 | * Array of logged messages. 312 | */ 313 | $log.warn.logs = []; 314 | /** 315 | * @ngdoc property 316 | * @name ngMock.$log#info.logs 317 | * @propertyOf ngMock.$log 318 | * 319 | * @description 320 | * Array of logged messages. 321 | */ 322 | $log.info.logs = []; 323 | /** 324 | * @ngdoc property 325 | * @name ngMock.$log#error.logs 326 | * @propertyOf ngMock.$log 327 | * 328 | * @description 329 | * Array of logged messages. 330 | */ 331 | $log.error.logs = []; 332 | }; 333 | 334 | /** 335 | * @ngdoc method 336 | * @name ngMock.$log#assertEmpty 337 | * @methodOf ngMock.$log 338 | * 339 | * @description 340 | * Assert that the all of the logging methods have no logged messages. If messages present, an exception is thrown. 341 | */ 342 | $log.assertEmpty = function() { 343 | var errors = []; 344 | angular.forEach(['error', 'warn', 'info', 'log'], function(logLevel) { 345 | angular.forEach($log[logLevel].logs, function(log) { 346 | angular.forEach(log, function (logItem) { 347 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); 348 | }); 349 | }); 350 | }); 351 | if (errors.length) { 352 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + 353 | "log message was not checked and removed:"); 354 | errors.push(''); 355 | throw new Error(errors.join('\n---------\n')); 356 | } 357 | }; 358 | 359 | $log.reset(); 360 | return $log; 361 | }; 362 | }; 363 | 364 | 365 | (function() { 366 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 367 | 368 | function jsonStringToDate(string){ 369 | var match; 370 | if (match = string.match(R_ISO8061_STR)) { 371 | var date = new Date(0), 372 | tzHour = 0, 373 | tzMin = 0; 374 | if (match[9]) { 375 | tzHour = int(match[9] + match[10]); 376 | tzMin = int(match[9] + match[11]); 377 | } 378 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 379 | date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0)); 380 | return date; 381 | } 382 | return string; 383 | } 384 | 385 | function int(str) { 386 | return parseInt(str, 10); 387 | } 388 | 389 | function padNumber(num, digits, trim) { 390 | var neg = ''; 391 | if (num < 0) { 392 | neg = '-'; 393 | num = -num; 394 | } 395 | num = '' + num; 396 | while(num.length < digits) num = '0' + num; 397 | if (trim) 398 | num = num.substr(num.length - digits); 399 | return neg + num; 400 | } 401 | 402 | 403 | /** 404 | * @ngdoc object 405 | * @name angular.mock.TzDate 406 | * @description 407 | * 408 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 409 | * 410 | * Mock of the Date type which has its timezone specified via constroctor arg. 411 | * 412 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 413 | * offset, so that we can test code that depends on local timezone settings without dependency on 414 | * the time zone settings of the machine where the code is running. 415 | * 416 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 417 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 418 | * 419 | * @example 420 | * !!!! WARNING !!!!! 421 | * This is not a complete Date object so only methods that were implemented can be called safely. 422 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 423 | * 424 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 425 | * incomplete we might be missing some non-standard methods. This can result in errors like: 426 | * "Date.prototype.foo called on incompatible Object". 427 | * 428 | *
 429 |    * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
 430 |    * newYearInBratislava.getTimezoneOffset() => -60;
 431 |    * newYearInBratislava.getFullYear() => 2010;
 432 |    * newYearInBratislava.getMonth() => 0;
 433 |    * newYearInBratislava.getDate() => 1;
 434 |    * newYearInBratislava.getHours() => 0;
 435 |    * newYearInBratislava.getMinutes() => 0;
 436 |    * 
437 | * 438 | */ 439 | angular.mock.TzDate = function (offset, timestamp) { 440 | var self = new Date(0); 441 | if (angular.isString(timestamp)) { 442 | var tsStr = timestamp; 443 | 444 | self.origDate = jsonStringToDate(timestamp); 445 | 446 | timestamp = self.origDate.getTime(); 447 | if (isNaN(timestamp)) 448 | throw { 449 | name: "Illegal Argument", 450 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 451 | }; 452 | } else { 453 | self.origDate = new Date(timestamp); 454 | } 455 | 456 | var localOffset = new Date(timestamp).getTimezoneOffset(); 457 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 458 | self.date = new Date(timestamp + self.offsetDiff); 459 | 460 | self.getTime = function() { 461 | return self.date.getTime() - self.offsetDiff; 462 | }; 463 | 464 | self.toLocaleDateString = function() { 465 | return self.date.toLocaleDateString(); 466 | }; 467 | 468 | self.getFullYear = function() { 469 | return self.date.getFullYear(); 470 | }; 471 | 472 | self.getMonth = function() { 473 | return self.date.getMonth(); 474 | }; 475 | 476 | self.getDate = function() { 477 | return self.date.getDate(); 478 | }; 479 | 480 | self.getHours = function() { 481 | return self.date.getHours(); 482 | }; 483 | 484 | self.getMinutes = function() { 485 | return self.date.getMinutes(); 486 | }; 487 | 488 | self.getSeconds = function() { 489 | return self.date.getSeconds(); 490 | }; 491 | 492 | self.getTimezoneOffset = function() { 493 | return offset * 60; 494 | }; 495 | 496 | self.getUTCFullYear = function() { 497 | return self.origDate.getUTCFullYear(); 498 | }; 499 | 500 | self.getUTCMonth = function() { 501 | return self.origDate.getUTCMonth(); 502 | }; 503 | 504 | self.getUTCDate = function() { 505 | return self.origDate.getUTCDate(); 506 | }; 507 | 508 | self.getUTCHours = function() { 509 | return self.origDate.getUTCHours(); 510 | }; 511 | 512 | self.getUTCMinutes = function() { 513 | return self.origDate.getUTCMinutes(); 514 | }; 515 | 516 | self.getUTCSeconds = function() { 517 | return self.origDate.getUTCSeconds(); 518 | }; 519 | 520 | self.getUTCMilliseconds = function() { 521 | return self.origDate.getUTCMilliseconds(); 522 | }; 523 | 524 | self.getDay = function() { 525 | return self.date.getDay(); 526 | }; 527 | 528 | // provide this method only on browsers that already have it 529 | if (self.toISOString) { 530 | self.toISOString = function() { 531 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 532 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 533 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 534 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 535 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 536 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 537 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' 538 | } 539 | } 540 | 541 | //hide all methods not implemented in this mock that the Date prototype exposes 542 | var unimplementedMethods = ['getMilliseconds', 'getUTCDay', 543 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 544 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 545 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 546 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 547 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 548 | 549 | angular.forEach(unimplementedMethods, function(methodName) { 550 | self[methodName] = function() { 551 | throw Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 552 | }; 553 | }); 554 | 555 | return self; 556 | }; 557 | 558 | //make "tzDateInstance instanceof Date" return true 559 | angular.mock.TzDate.prototype = Date.prototype; 560 | })(); 561 | 562 | 563 | /** 564 | * @ngdoc function 565 | * @name angular.mock.debug 566 | * @description 567 | * 568 | * *NOTE*: this is not an injectable instance, just a globally available function. 569 | * 570 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for debugging. 571 | * 572 | * This method is also available on window, where it can be used to display objects on debug console. 573 | * 574 | * @param {*} object - any object to turn into string. 575 | * @return {string} a serialized string of the argument 576 | */ 577 | angular.mock.dump = function(object) { 578 | return serialize(object); 579 | 580 | function serialize(object) { 581 | var out; 582 | 583 | if (angular.isElement(object)) { 584 | object = angular.element(object); 585 | out = angular.element('
'); 586 | angular.forEach(object, function(element) { 587 | out.append(angular.element(element).clone()); 588 | }); 589 | out = out.html(); 590 | } else if (angular.isArray(object)) { 591 | out = []; 592 | angular.forEach(object, function(o) { 593 | out.push(serialize(o)); 594 | }); 595 | out = '[ ' + out.join(', ') + ' ]'; 596 | } else if (angular.isObject(object)) { 597 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 598 | out = serializeScope(object); 599 | } else if (object instanceof Error) { 600 | out = object.stack || ('' + object.name + ': ' + object.message); 601 | } else { 602 | out = angular.toJson(object, true); 603 | } 604 | } else { 605 | out = String(object); 606 | } 607 | 608 | return out; 609 | } 610 | 611 | function serializeScope(scope, offset) { 612 | offset = offset || ' '; 613 | var log = [offset + 'Scope(' + scope.$id + '): {']; 614 | for ( var key in scope ) { 615 | if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) { 616 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 617 | } 618 | } 619 | var child = scope.$$childHead; 620 | while(child) { 621 | log.push(serializeScope(child, offset + ' ')); 622 | child = child.$$nextSibling; 623 | } 624 | log.push('}'); 625 | return log.join('\n' + offset); 626 | } 627 | }; 628 | 629 | /** 630 | * @ngdoc object 631 | * @name ngMock.$httpBackend 632 | * @description 633 | * Fake HTTP backend implementation suitable for unit testing application that use the 634 | * {@link ng.$http $http service}. 635 | * 636 | * *Note*: For fake http backend implementation suitable for end-to-end testing or backend-less 637 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 638 | * 639 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 640 | * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or 641 | * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is 642 | * to verify whether a certain request has been sent or not, or alternatively just let the 643 | * application make requests, respond with pre-trained responses and assert that the end result is 644 | * what we expect it to be. 645 | * 646 | * This mock implementation can be used to respond with static or dynamic responses via the 647 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 648 | * 649 | * When an Angular application needs some data from a server, it calls the $http service, which 650 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 651 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 652 | * the requests and respond with some testing data without sending a request to real server. 653 | * 654 | * There are two ways to specify what test data should be returned as http responses by the mock 655 | * backend when the code under test makes http requests: 656 | * 657 | * - `$httpBackend.expect` - specifies a request expectation 658 | * - `$httpBackend.when` - specifies a backend definition 659 | * 660 | * 661 | * # Request Expectations vs Backend Definitions 662 | * 663 | * Request expectations provide a way to make assertions about requests made by the application and 664 | * to define responses for those requests. The test will fail if the expected requests are not made 665 | * or they are made in the wrong order. 666 | * 667 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 668 | * if a particular request was made or not, it just returns a trained response if a request is made. 669 | * The test will pass whether or not the request gets made during testing. 670 | * 671 | * 672 | * 673 | * 674 | * 675 | * 676 | * 677 | * 678 | * 679 | * 680 | * 681 | * 682 | * 683 | * 684 | * 685 | * 686 | * 687 | * 688 | * 689 | * 690 | * 691 | * 692 | * 693 | * 694 | * 695 | * 696 | * 697 | * 698 | * 699 | * 700 | * 701 | * 702 | * 703 | * 704 | *
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
705 | * 706 | * In cases where both backend definitions and request expectations are specified during unit 707 | * testing, the request expectations are evaluated first. 708 | * 709 | * If a request expectation has no response specified, the algorithm will search your backend 710 | * definitions for an appropriate response. 711 | * 712 | * If a request didn't match any expectation or if the expectation doesn't have the response 713 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 714 | * the request. The response from the first matched definition is returned. 715 | * 716 | * 717 | * # Flushing HTTP requests 718 | * 719 | * The $httpBackend used in production, always responds to requests with responses asynchronously. 720 | * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are 721 | * hard to write, follow and maintain. At the same time the testing mock, can't respond 722 | * synchronously because that would change the execution of the code under test. For this reason the 723 | * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending 724 | * requests and thus preserving the async api of the backend, while allowing the test to execute 725 | * synchronously. 726 | * 727 | * 728 | * # Unit testing with mock $httpBackend 729 | * 730 | *
 731 |    // controller
 732 |    function MyController($scope, $http) {
 733 |      $http.get('/auth.py').success(function(data) {
 734 |        $scope.user = data;
 735 |      });
 736 | 
 737 |      this.saveMessage = function(message) {
 738 |        $scope.status = 'Saving...';
 739 |        $http.post('/add-msg.py', message).success(function(response) {
 740 |          $scope.status = '';
 741 |        }).error(function() {
 742 |          $scope.status = 'ERROR!';
 743 |        });
 744 |      };
 745 |    }
 746 | 
 747 |    // testing controller
 748 |    var $http;
 749 | 
 750 |    beforeEach(inject(function($injector) {
 751 |      $httpBackend = $injector.get('$httpBackend');
 752 | 
 753 |      // backend definition common for all tests
 754 |      $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
 755 |    }));
 756 | 
 757 | 
 758 |    afterEach(function() {
 759 |      $httpBackend.verifyNoOutstandingExpectation();
 760 |      $httpBackend.verifyNoOutstandingRequest();
 761 |    });
 762 | 
 763 | 
 764 |    it('should fetch authentication token', function() {
 765 |      $httpBackend.expectGET('/auth.py');
 766 |      var controller = scope.$new(MyController);
 767 |      $httpBackend.flush();
 768 |    });
 769 | 
 770 | 
 771 |    it('should send msg to server', function() {
 772 |      // now you don’t care about the authentication, but
 773 |      // the controller will still send the request and
 774 |      // $httpBackend will respond without you having to
 775 |      // specify the expectation and response for this request
 776 |      $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
 777 | 
 778 |      var controller = scope.$new(MyController);
 779 |      $httpBackend.flush();
 780 |      controller.saveMessage('message content');
 781 |      expect(controller.status).toBe('Saving...');
 782 |      $httpBackend.flush();
 783 |      expect(controller.status).toBe('');
 784 |    });
 785 | 
 786 | 
 787 |    it('should send auth header', function() {
 788 |      $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
 789 |        // check if the header was send, if it wasn't the expectation won't
 790 |        // match the request and the test will fail
 791 |        return headers['Authorization'] == 'xxx';
 792 |      }).respond(201, '');
 793 | 
 794 |      var controller = scope.$new(MyController);
 795 |      controller.saveMessage('whatever');
 796 |      $httpBackend.flush();
 797 |    });
 798 |    
799 | */ 800 | angular.mock.$HttpBackendProvider = function() { 801 | this.$get = [createHttpBackendMock]; 802 | }; 803 | 804 | /** 805 | * General factory function for $httpBackend mock. 806 | * Returns instance for unit testing (when no arguments specified): 807 | * - passing through is disabled 808 | * - auto flushing is disabled 809 | * 810 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 811 | * - passing through (delegating request to real backend) is enabled 812 | * - auto flushing is enabled 813 | * 814 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 815 | * @param {Object=} $browser Auto-flushing enabled if specified 816 | * @return {Object} Instance of $httpBackend mock 817 | */ 818 | function createHttpBackendMock($delegate, $browser) { 819 | var definitions = [], 820 | expectations = [], 821 | responses = [], 822 | responsesPush = angular.bind(responses, responses.push); 823 | 824 | function createResponse(status, data, headers) { 825 | if (angular.isFunction(status)) return status; 826 | 827 | return function() { 828 | return angular.isNumber(status) 829 | ? [status, data, headers] 830 | : [200, status, data]; 831 | }; 832 | } 833 | 834 | // TODO(vojta): change params to: method, url, data, headers, callback 835 | function $httpBackend(method, url, data, callback, headers) { 836 | var xhr = new MockXhr(), 837 | expectation = expectations[0], 838 | wasExpected = false; 839 | 840 | function prettyPrint(data) { 841 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 842 | ? data 843 | : angular.toJson(data); 844 | } 845 | 846 | if (expectation && expectation.match(method, url)) { 847 | if (!expectation.matchData(data)) 848 | throw Error('Expected ' + expectation + ' with different data\n' + 849 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 850 | 851 | if (!expectation.matchHeaders(headers)) 852 | throw Error('Expected ' + expectation + ' with different headers\n' + 853 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 854 | prettyPrint(headers)); 855 | 856 | expectations.shift(); 857 | 858 | if (expectation.response) { 859 | responses.push(function() { 860 | var response = expectation.response(method, url, data, headers); 861 | xhr.$$respHeaders = response[2]; 862 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 863 | }); 864 | return; 865 | } 866 | wasExpected = true; 867 | } 868 | 869 | var i = -1, definition; 870 | while ((definition = definitions[++i])) { 871 | if (definition.match(method, url, data, headers || {})) { 872 | if (definition.response) { 873 | // if $browser specified, we do auto flush all requests 874 | ($browser ? $browser.defer : responsesPush)(function() { 875 | var response = definition.response(method, url, data, headers); 876 | xhr.$$respHeaders = response[2]; 877 | callback(response[0], response[1], xhr.getAllResponseHeaders()); 878 | }); 879 | } else if (definition.passThrough) { 880 | $delegate(method, url, data, callback, headers); 881 | } else throw Error('No response defined !'); 882 | return; 883 | } 884 | } 885 | throw wasExpected ? 886 | Error('No response defined !') : 887 | Error('Unexpected request: ' + method + ' ' + url + '\n' + 888 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 889 | } 890 | 891 | /** 892 | * @ngdoc method 893 | * @name ngMock.$httpBackend#when 894 | * @methodOf ngMock.$httpBackend 895 | * @description 896 | * Creates a new backend definition. 897 | * 898 | * @param {string} method HTTP method. 899 | * @param {string|RegExp} url HTTP url. 900 | * @param {(string|RegExp)=} data HTTP request body. 901 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 902 | * object and returns true if the headers match the current definition. 903 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 904 | * request is handled. 905 | * 906 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 907 | * – The respond method takes a set of static data to be returned or a function that can return 908 | * an array containing response status (number), response data (string) and response headers 909 | * (Object). 910 | */ 911 | $httpBackend.when = function(method, url, data, headers) { 912 | var definition = new MockHttpExpectation(method, url, data, headers), 913 | chain = { 914 | respond: function(status, data, headers) { 915 | definition.response = createResponse(status, data, headers); 916 | } 917 | }; 918 | 919 | if ($browser) { 920 | chain.passThrough = function() { 921 | definition.passThrough = true; 922 | }; 923 | } 924 | 925 | definitions.push(definition); 926 | return chain; 927 | }; 928 | 929 | /** 930 | * @ngdoc method 931 | * @name ngMock.$httpBackend#whenGET 932 | * @methodOf ngMock.$httpBackend 933 | * @description 934 | * Creates a new backend definition for GET requests. For more info see `when()`. 935 | * 936 | * @param {string|RegExp} url HTTP url. 937 | * @param {(Object|function(Object))=} headers HTTP headers. 938 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 939 | * request is handled. 940 | */ 941 | 942 | /** 943 | * @ngdoc method 944 | * @name ngMock.$httpBackend#whenHEAD 945 | * @methodOf ngMock.$httpBackend 946 | * @description 947 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 948 | * 949 | * @param {string|RegExp} url HTTP url. 950 | * @param {(Object|function(Object))=} headers HTTP headers. 951 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 952 | * request is handled. 953 | */ 954 | 955 | /** 956 | * @ngdoc method 957 | * @name ngMock.$httpBackend#whenDELETE 958 | * @methodOf ngMock.$httpBackend 959 | * @description 960 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 961 | * 962 | * @param {string|RegExp} url HTTP url. 963 | * @param {(Object|function(Object))=} headers HTTP headers. 964 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 965 | * request is handled. 966 | */ 967 | 968 | /** 969 | * @ngdoc method 970 | * @name ngMock.$httpBackend#whenPOST 971 | * @methodOf ngMock.$httpBackend 972 | * @description 973 | * Creates a new backend definition for POST requests. For more info see `when()`. 974 | * 975 | * @param {string|RegExp} url HTTP url. 976 | * @param {(string|RegExp)=} data HTTP request body. 977 | * @param {(Object|function(Object))=} headers HTTP headers. 978 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 979 | * request is handled. 980 | */ 981 | 982 | /** 983 | * @ngdoc method 984 | * @name ngMock.$httpBackend#whenPUT 985 | * @methodOf ngMock.$httpBackend 986 | * @description 987 | * Creates a new backend definition for PUT requests. For more info see `when()`. 988 | * 989 | * @param {string|RegExp} url HTTP url. 990 | * @param {(string|RegExp)=} data HTTP request body. 991 | * @param {(Object|function(Object))=} headers HTTP headers. 992 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 993 | * request is handled. 994 | */ 995 | 996 | /** 997 | * @ngdoc method 998 | * @name ngMock.$httpBackend#whenJSONP 999 | * @methodOf ngMock.$httpBackend 1000 | * @description 1001 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1002 | * 1003 | * @param {string|RegExp} url HTTP url. 1004 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1005 | * request is handled. 1006 | */ 1007 | createShortMethods('when'); 1008 | 1009 | 1010 | /** 1011 | * @ngdoc method 1012 | * @name ngMock.$httpBackend#expect 1013 | * @methodOf ngMock.$httpBackend 1014 | * @description 1015 | * Creates a new request expectation. 1016 | * 1017 | * @param {string} method HTTP method. 1018 | * @param {string|RegExp} url HTTP url. 1019 | * @param {(string|RegExp)=} data HTTP request body. 1020 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1021 | * object and returns true if the headers match the current expectation. 1022 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1023 | * request is handled. 1024 | * 1025 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1026 | * – The respond method takes a set of static data to be returned or a function that can return 1027 | * an array containing response status (number), response data (string) and response headers 1028 | * (Object). 1029 | */ 1030 | $httpBackend.expect = function(method, url, data, headers) { 1031 | var expectation = new MockHttpExpectation(method, url, data, headers); 1032 | expectations.push(expectation); 1033 | return { 1034 | respond: function(status, data, headers) { 1035 | expectation.response = createResponse(status, data, headers); 1036 | } 1037 | }; 1038 | }; 1039 | 1040 | 1041 | /** 1042 | * @ngdoc method 1043 | * @name ngMock.$httpBackend#expectGET 1044 | * @methodOf ngMock.$httpBackend 1045 | * @description 1046 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1047 | * 1048 | * @param {string|RegExp} url HTTP url. 1049 | * @param {Object=} headers HTTP headers. 1050 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1051 | * request is handled. See #expect for more info. 1052 | */ 1053 | 1054 | /** 1055 | * @ngdoc method 1056 | * @name ngMock.$httpBackend#expectHEAD 1057 | * @methodOf ngMock.$httpBackend 1058 | * @description 1059 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1060 | * 1061 | * @param {string|RegExp} url HTTP url. 1062 | * @param {Object=} headers HTTP headers. 1063 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1064 | * request is handled. 1065 | */ 1066 | 1067 | /** 1068 | * @ngdoc method 1069 | * @name ngMock.$httpBackend#expectDELETE 1070 | * @methodOf ngMock.$httpBackend 1071 | * @description 1072 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1073 | * 1074 | * @param {string|RegExp} url HTTP url. 1075 | * @param {Object=} headers HTTP headers. 1076 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1077 | * request is handled. 1078 | */ 1079 | 1080 | /** 1081 | * @ngdoc method 1082 | * @name ngMock.$httpBackend#expectPOST 1083 | * @methodOf ngMock.$httpBackend 1084 | * @description 1085 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1086 | * 1087 | * @param {string|RegExp} url HTTP url. 1088 | * @param {(string|RegExp)=} data HTTP request body. 1089 | * @param {Object=} headers HTTP headers. 1090 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1091 | * request is handled. 1092 | */ 1093 | 1094 | /** 1095 | * @ngdoc method 1096 | * @name ngMock.$httpBackend#expectPUT 1097 | * @methodOf ngMock.$httpBackend 1098 | * @description 1099 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1100 | * 1101 | * @param {string|RegExp} url HTTP url. 1102 | * @param {(string|RegExp)=} data HTTP request body. 1103 | * @param {Object=} headers HTTP headers. 1104 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1105 | * request is handled. 1106 | */ 1107 | 1108 | /** 1109 | * @ngdoc method 1110 | * @name ngMock.$httpBackend#expectPATCH 1111 | * @methodOf ngMock.$httpBackend 1112 | * @description 1113 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1114 | * 1115 | * @param {string|RegExp} url HTTP url. 1116 | * @param {(string|RegExp)=} data HTTP request body. 1117 | * @param {Object=} headers HTTP headers. 1118 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1119 | * request is handled. 1120 | */ 1121 | 1122 | /** 1123 | * @ngdoc method 1124 | * @name ngMock.$httpBackend#expectJSONP 1125 | * @methodOf ngMock.$httpBackend 1126 | * @description 1127 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1128 | * 1129 | * @param {string|RegExp} url HTTP url. 1130 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1131 | * request is handled. 1132 | */ 1133 | createShortMethods('expect'); 1134 | 1135 | 1136 | /** 1137 | * @ngdoc method 1138 | * @name ngMock.$httpBackend#flush 1139 | * @methodOf ngMock.$httpBackend 1140 | * @description 1141 | * Flushes all pending requests using the trained responses. 1142 | * 1143 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1144 | * all pending requests will be flushed. If there are no pending requests when the flush method 1145 | * is called an exception is thrown (as this typically a sign of programming error). 1146 | */ 1147 | $httpBackend.flush = function(count) { 1148 | if (!responses.length) throw Error('No pending request to flush !'); 1149 | 1150 | if (angular.isDefined(count)) { 1151 | while (count--) { 1152 | if (!responses.length) throw Error('No more pending request to flush !'); 1153 | responses.shift()(); 1154 | } 1155 | } else { 1156 | while (responses.length) { 1157 | responses.shift()(); 1158 | } 1159 | } 1160 | $httpBackend.verifyNoOutstandingExpectation(); 1161 | }; 1162 | 1163 | 1164 | /** 1165 | * @ngdoc method 1166 | * @name ngMock.$httpBackend#verifyNoOutstandingExpectation 1167 | * @methodOf ngMock.$httpBackend 1168 | * @description 1169 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1170 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1171 | * 1172 | * Typically, you would call this method following each test case that asserts requests using an 1173 | * "afterEach" clause. 1174 | * 1175 | *
1176 |    *   afterEach($httpBackend.verifyExpectations);
1177 |    * 
1178 | */ 1179 | $httpBackend.verifyNoOutstandingExpectation = function() { 1180 | if (expectations.length) { 1181 | throw Error('Unsatisfied requests: ' + expectations.join(', ')); 1182 | } 1183 | }; 1184 | 1185 | 1186 | /** 1187 | * @ngdoc method 1188 | * @name ngMock.$httpBackend#verifyNoOutstandingRequest 1189 | * @methodOf ngMock.$httpBackend 1190 | * @description 1191 | * Verifies that there are no outstanding requests that need to be flushed. 1192 | * 1193 | * Typically, you would call this method following each test case that asserts requests using an 1194 | * "afterEach" clause. 1195 | * 1196 | *
1197 |    *   afterEach($httpBackend.verifyNoOutstandingRequest);
1198 |    * 
1199 | */ 1200 | $httpBackend.verifyNoOutstandingRequest = function() { 1201 | if (responses.length) { 1202 | throw Error('Unflushed requests: ' + responses.length); 1203 | } 1204 | }; 1205 | 1206 | 1207 | /** 1208 | * @ngdoc method 1209 | * @name ngMock.$httpBackend#resetExpectations 1210 | * @methodOf ngMock.$httpBackend 1211 | * @description 1212 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1213 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1214 | * $httpBackend mock. 1215 | */ 1216 | $httpBackend.resetExpectations = function() { 1217 | expectations.length = 0; 1218 | responses.length = 0; 1219 | }; 1220 | 1221 | return $httpBackend; 1222 | 1223 | 1224 | function createShortMethods(prefix) { 1225 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1226 | $httpBackend[prefix + method] = function(url, headers) { 1227 | return $httpBackend[prefix](method, url, undefined, headers) 1228 | } 1229 | }); 1230 | 1231 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1232 | $httpBackend[prefix + method] = function(url, data, headers) { 1233 | return $httpBackend[prefix](method, url, data, headers) 1234 | } 1235 | }); 1236 | } 1237 | } 1238 | 1239 | function MockHttpExpectation(method, url, data, headers) { 1240 | 1241 | this.data = data; 1242 | this.headers = headers; 1243 | 1244 | this.match = function(m, u, d, h) { 1245 | if (method != m) return false; 1246 | if (!this.matchUrl(u)) return false; 1247 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1248 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1249 | return true; 1250 | }; 1251 | 1252 | this.matchUrl = function(u) { 1253 | if (!url) return true; 1254 | if (angular.isFunction(url.test)) return url.test(u); 1255 | return url == u; 1256 | }; 1257 | 1258 | this.matchHeaders = function(h) { 1259 | if (angular.isUndefined(headers)) return true; 1260 | if (angular.isFunction(headers)) return headers(h); 1261 | return angular.equals(headers, h); 1262 | }; 1263 | 1264 | this.matchData = function(d) { 1265 | if (angular.isUndefined(data)) return true; 1266 | if (data && angular.isFunction(data.test)) return data.test(d); 1267 | if (data && !angular.isString(data)) return angular.toJson(data) == d; 1268 | return data == d; 1269 | }; 1270 | 1271 | this.toString = function() { 1272 | return method + ' ' + url; 1273 | }; 1274 | } 1275 | 1276 | function MockXhr() { 1277 | 1278 | // hack for testing $http, $httpBackend 1279 | MockXhr.$$lastInstance = this; 1280 | 1281 | this.open = function(method, url, async) { 1282 | this.$$method = method; 1283 | this.$$url = url; 1284 | this.$$async = async; 1285 | this.$$reqHeaders = {}; 1286 | this.$$respHeaders = {}; 1287 | }; 1288 | 1289 | this.send = function(data) { 1290 | this.$$data = data; 1291 | }; 1292 | 1293 | this.setRequestHeader = function(key, value) { 1294 | this.$$reqHeaders[key] = value; 1295 | }; 1296 | 1297 | this.getResponseHeader = function(name) { 1298 | // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last 1299 | var header = this.$$respHeaders[name]; 1300 | if (header) return header; 1301 | 1302 | name = angular.lowercase(name); 1303 | header = this.$$respHeaders[name]; 1304 | if (header) return header; 1305 | 1306 | header = undefined; 1307 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1308 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1309 | }); 1310 | return header; 1311 | }; 1312 | 1313 | this.getAllResponseHeaders = function() { 1314 | var lines = []; 1315 | 1316 | angular.forEach(this.$$respHeaders, function(value, key) { 1317 | lines.push(key + ': ' + value); 1318 | }); 1319 | return lines.join('\n'); 1320 | }; 1321 | 1322 | this.abort = angular.noop; 1323 | } 1324 | 1325 | 1326 | /** 1327 | * @ngdoc function 1328 | * @name ngMock.$timeout 1329 | * @description 1330 | * 1331 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1332 | * that adds a "flush" method. 1333 | */ 1334 | 1335 | /** 1336 | * @ngdoc method 1337 | * @name ngMock.$timeout#flush 1338 | * @methodOf ngMock.$timeout 1339 | * @description 1340 | * 1341 | * Flushes the queue of pending tasks. 1342 | */ 1343 | 1344 | /** 1345 | * 1346 | */ 1347 | angular.mock.$RootElementProvider = function() { 1348 | this.$get = function() { 1349 | return angular.element('
'); 1350 | } 1351 | }; 1352 | 1353 | /** 1354 | * @ngdoc overview 1355 | * @name ngMock 1356 | * @description 1357 | * 1358 | * The `ngMock` is an angular module which is used with `ng` module and adds unit-test configuration as well as useful 1359 | * mocks to the {@link AUTO.$injector $injector}. 1360 | */ 1361 | angular.module('ngMock', ['ng']).provider({ 1362 | $browser: angular.mock.$BrowserProvider, 1363 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1364 | $log: angular.mock.$LogProvider, 1365 | $httpBackend: angular.mock.$HttpBackendProvider, 1366 | $rootElement: angular.mock.$RootElementProvider 1367 | }).config(function($provide) { 1368 | $provide.decorator('$timeout', function($delegate, $browser) { 1369 | $delegate.flush = function() { 1370 | $browser.defer.flush(); 1371 | }; 1372 | return $delegate; 1373 | }); 1374 | }); 1375 | 1376 | 1377 | /** 1378 | * @ngdoc overview 1379 | * @name ngMockE2E 1380 | * @description 1381 | * 1382 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1383 | * Currently there is only one mock present in this module - 1384 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1385 | */ 1386 | angular.module('ngMockE2E', ['ng']).config(function($provide) { 1387 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1388 | }); 1389 | 1390 | /** 1391 | * @ngdoc object 1392 | * @name ngMockE2E.$httpBackend 1393 | * @description 1394 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1395 | * applications that use the {@link ng.$http $http service}. 1396 | * 1397 | * *Note*: For fake http backend implementation suitable for unit testing please see 1398 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1399 | * 1400 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1401 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1402 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1403 | * templates from a webserver). 1404 | * 1405 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1406 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1407 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1408 | * templates or static files from the webserver). To configure the backend with this behavior 1409 | * use the `passThrough` request handler of `when` instead of `respond`. 1410 | * 1411 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1412 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1413 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1414 | * 1415 | * To setup the application to run with this http backend, you have to create a module that depends 1416 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1417 | * 1418 | *
1419 |  *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
1420 |  *   myAppDev.run(function($httpBackend) {
1421 |  *     phones = [{name: 'phone1'}, {name: 'phone2'}];
1422 |  *
1423 |  *     // returns the current list of phones
1424 |  *     $httpBackend.whenGET('/phones').respond(phones);
1425 |  *
1426 |  *     // adds a new phone to the phones array
1427 |  *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
1428 |  *       phones.push(angular.fromJSON(data));
1429 |  *     });
1430 |  *     $httpBackend.whenGET(/^\/templates\//).passThrough();
1431 |  *     //...
1432 |  *   });
1433 |  * 
1434 | * 1435 | * Afterwards, bootstrap your app with this new module. 1436 | */ 1437 | 1438 | /** 1439 | * @ngdoc method 1440 | * @name ngMockE2E.$httpBackend#when 1441 | * @methodOf ngMockE2E.$httpBackend 1442 | * @description 1443 | * Creates a new backend definition. 1444 | * 1445 | * @param {string} method HTTP method. 1446 | * @param {string|RegExp} url HTTP url. 1447 | * @param {(string|RegExp)=} data HTTP request body. 1448 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1449 | * object and returns true if the headers match the current definition. 1450 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1451 | * control how a matched request is handled. 1452 | * 1453 | * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1454 | * – The respond method takes a set of static data to be returned or a function that can return 1455 | * an array containing response status (number), response data (string) and response headers 1456 | * (Object). 1457 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1458 | * handler, will be pass through to the real backend (an XHR request will be made to the 1459 | * server. 1460 | */ 1461 | 1462 | /** 1463 | * @ngdoc method 1464 | * @name ngMockE2E.$httpBackend#whenGET 1465 | * @methodOf ngMockE2E.$httpBackend 1466 | * @description 1467 | * Creates a new backend definition for GET requests. For more info see `when()`. 1468 | * 1469 | * @param {string|RegExp} url HTTP url. 1470 | * @param {(Object|function(Object))=} headers HTTP headers. 1471 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1472 | * control how a matched request is handled. 1473 | */ 1474 | 1475 | /** 1476 | * @ngdoc method 1477 | * @name ngMockE2E.$httpBackend#whenHEAD 1478 | * @methodOf ngMockE2E.$httpBackend 1479 | * @description 1480 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1481 | * 1482 | * @param {string|RegExp} url HTTP url. 1483 | * @param {(Object|function(Object))=} headers HTTP headers. 1484 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1485 | * control how a matched request is handled. 1486 | */ 1487 | 1488 | /** 1489 | * @ngdoc method 1490 | * @name ngMockE2E.$httpBackend#whenDELETE 1491 | * @methodOf ngMockE2E.$httpBackend 1492 | * @description 1493 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1494 | * 1495 | * @param {string|RegExp} url HTTP url. 1496 | * @param {(Object|function(Object))=} headers HTTP headers. 1497 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1498 | * control how a matched request is handled. 1499 | */ 1500 | 1501 | /** 1502 | * @ngdoc method 1503 | * @name ngMockE2E.$httpBackend#whenPOST 1504 | * @methodOf ngMockE2E.$httpBackend 1505 | * @description 1506 | * Creates a new backend definition for POST requests. For more info see `when()`. 1507 | * 1508 | * @param {string|RegExp} url HTTP url. 1509 | * @param {(string|RegExp)=} data HTTP request body. 1510 | * @param {(Object|function(Object))=} headers HTTP headers. 1511 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1512 | * control how a matched request is handled. 1513 | */ 1514 | 1515 | /** 1516 | * @ngdoc method 1517 | * @name ngMockE2E.$httpBackend#whenPUT 1518 | * @methodOf ngMockE2E.$httpBackend 1519 | * @description 1520 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1521 | * 1522 | * @param {string|RegExp} url HTTP url. 1523 | * @param {(string|RegExp)=} data HTTP request body. 1524 | * @param {(Object|function(Object))=} headers HTTP headers. 1525 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1526 | * control how a matched request is handled. 1527 | */ 1528 | 1529 | /** 1530 | * @ngdoc method 1531 | * @name ngMockE2E.$httpBackend#whenPATCH 1532 | * @methodOf ngMockE2E.$httpBackend 1533 | * @description 1534 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1535 | * 1536 | * @param {string|RegExp} url HTTP url. 1537 | * @param {(string|RegExp)=} data HTTP request body. 1538 | * @param {(Object|function(Object))=} headers HTTP headers. 1539 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1540 | * control how a matched request is handled. 1541 | */ 1542 | 1543 | /** 1544 | * @ngdoc method 1545 | * @name ngMockE2E.$httpBackend#whenJSONP 1546 | * @methodOf ngMockE2E.$httpBackend 1547 | * @description 1548 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1549 | * 1550 | * @param {string|RegExp} url HTTP url. 1551 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1552 | * control how a matched request is handled. 1553 | */ 1554 | angular.mock.e2e = {}; 1555 | angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock]; 1556 | 1557 | 1558 | angular.mock.clearDataCache = function() { 1559 | var key, 1560 | cache = angular.element.cache; 1561 | 1562 | for(key in cache) { 1563 | if (cache.hasOwnProperty(key)) { 1564 | var handle = cache[key].handle; 1565 | 1566 | handle && angular.element(handle.elem).unbind(); 1567 | delete cache[key]; 1568 | } 1569 | } 1570 | }; 1571 | 1572 | 1573 | window.jstestdriver && (function(window) { 1574 | /** 1575 | * Global method to output any number of objects into JSTD console. Useful for debugging. 1576 | */ 1577 | window.dump = function() { 1578 | var args = []; 1579 | angular.forEach(arguments, function(arg) { 1580 | args.push(angular.mock.dump(arg)); 1581 | }); 1582 | jstestdriver.console.log.apply(jstestdriver.console, args); 1583 | if (window.console) { 1584 | window.console.log.apply(window.console, args); 1585 | } 1586 | }; 1587 | })(window); 1588 | 1589 | 1590 | window.jasmine && (function(window) { 1591 | 1592 | afterEach(function() { 1593 | var spec = getCurrentSpec(); 1594 | spec.$injector = null; 1595 | spec.$modules = null; 1596 | angular.mock.clearDataCache(); 1597 | }); 1598 | 1599 | function getCurrentSpec() { 1600 | return jasmine.getEnv().currentSpec; 1601 | } 1602 | 1603 | function isSpecRunning() { 1604 | var spec = getCurrentSpec(); 1605 | return spec && spec.queue.running; 1606 | } 1607 | 1608 | /** 1609 | * @ngdoc function 1610 | * @name angular.mock.module 1611 | * @description 1612 | * 1613 | * *NOTE*: This is function is also published on window for easy access.
1614 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1615 | * 1616 | * This function registers a module configuration code. It collects the configuration information 1617 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1618 | * 1619 | * See {@link angular.mock.inject inject} for usage example 1620 | * 1621 | * @param {...(string|Function)} fns any number of modules which are represented as string 1622 | * aliases or as anonymous module initialization functions. The modules are used to 1623 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. 1624 | */ 1625 | window.module = angular.mock.module = function() { 1626 | var moduleFns = Array.prototype.slice.call(arguments, 0); 1627 | return isSpecRunning() ? workFn() : workFn; 1628 | ///////////////////// 1629 | function workFn() { 1630 | var spec = getCurrentSpec(); 1631 | if (spec.$injector) { 1632 | throw Error('Injector already created, can not register a module!'); 1633 | } else { 1634 | var modules = spec.$modules || (spec.$modules = []); 1635 | angular.forEach(moduleFns, function(module) { 1636 | modules.push(module); 1637 | }); 1638 | } 1639 | } 1640 | }; 1641 | 1642 | /** 1643 | * @ngdoc function 1644 | * @name angular.mock.inject 1645 | * @description 1646 | * 1647 | * *NOTE*: This is function is also published on window for easy access.
1648 | * *NOTE*: Only available with {@link http://pivotal.github.com/jasmine/ jasmine}. 1649 | * 1650 | * The inject function wraps a function into an injectable function. The inject() creates new 1651 | * instance of {@link AUTO.$injector $injector} per test, which is then used for 1652 | * resolving references. 1653 | * 1654 | * See also {@link angular.mock.module module} 1655 | * 1656 | * Example of what a typical jasmine tests looks like with the inject method. 1657 | *
1658 |    *
1659 |    *   angular.module('myApplicationModule', [])
1660 |    *       .value('mode', 'app')
1661 |    *       .value('version', 'v1.0.1');
1662 |    *
1663 |    *
1664 |    *   describe('MyApp', function() {
1665 |    *
1666 |    *     // You need to load modules that you want to test,
1667 |    *     // it loads only the "ng" module by default.
1668 |    *     beforeEach(module('myApplicationModule'));
1669 |    *
1670 |    *
1671 |    *     // inject() is used to inject arguments of all given functions
1672 |    *     it('should provide a version', inject(function(mode, version) {
1673 |    *       expect(version).toEqual('v1.0.1');
1674 |    *       expect(mode).toEqual('app');
1675 |    *     }));
1676 |    *
1677 |    *
1678 |    *     // The inject and module method can also be used inside of the it or beforeEach
1679 |    *     it('should override a version and test the new version is injected', function() {
1680 |    *       // module() takes functions or strings (module aliases)
1681 |    *       module(function($provide) {
1682 |    *         $provide.value('version', 'overridden'); // override version here
1683 |    *       });
1684 |    *
1685 |    *       inject(function(version) {
1686 |    *         expect(version).toEqual('overridden');
1687 |    *       });
1688 |    *     ));
1689 |    *   });
1690 |    *
1691 |    * 
1692 | * 1693 | * @param {...Function} fns any number of functions which will be injected using the injector. 1694 | */ 1695 | window.inject = angular.mock.inject = function() { 1696 | var blockFns = Array.prototype.slice.call(arguments, 0); 1697 | var stack = new Error('Declaration Location').stack; 1698 | return isSpecRunning() ? workFn() : workFn; 1699 | ///////////////////// 1700 | function workFn() { 1701 | var spec = getCurrentSpec(); 1702 | var modules = spec.$modules || []; 1703 | modules.unshift('ngMock'); 1704 | modules.unshift('ng'); 1705 | var injector = spec.$injector; 1706 | if (!injector) { 1707 | injector = spec.$injector = angular.injector(modules); 1708 | } 1709 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 1710 | try { 1711 | injector.invoke(blockFns[i] || angular.noop, this); 1712 | } catch (e) { 1713 | if(e.stack) e.stack += '\n' + stack; 1714 | throw e; 1715 | } 1716 | } 1717 | } 1718 | } 1719 | })(window); 1720 | -------------------------------------------------------------------------------- /app/js/lib/angular/angular.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(T,aa,p){'use strict';function m(b,a,c){var d;if(b)if(M(b))for(d in b)d!="prototype"&&d!="length"&&d!="name"&&b.hasOwnProperty(d)&&a.call(c,b[d],d);else if(b.forEach&&b.forEach!==m)b.forEach(a,c);else if(J(b)&&va(b.length))for(d=0;d=0&&b.splice(c,1);return a}function U(b,a){if(na(b)||b&&b.$evalAsync&&b.$watch)throw z("Can't copy Window or Scope");if(a){if(b=== 10 | a)throw z("Can't copy equivalent objects or arrays");if(K(b)){for(;a.length;)a.pop();for(var c=0;c2?ga.call(arguments,2):[];return M(a)&&!(a instanceof RegExp)?c.length? 12 | function(){return arguments.length?a.apply(b,c.concat(ga.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}:a}function hc(b,a){var c=a;/^\$+/.test(b)?c=p:na(a)?c="$WINDOW":a&&aa===a?c="$DOCUMENT":a&&a.$evalAsync&&a.$watch&&(c="$SCOPE");return c}function ba(b,a){return JSON.stringify(b,hc,a?" ":null)}function mb(b){return G(b)?JSON.parse(b):b}function Wa(b){b&&b.length!==0?(b=C(""+b),b=!(b=="f"||b=="0"||b=="false"||b=="no"||b=="n"||b=="[]")):b=!1; 13 | return b}function oa(b){b=u(b).clone();try{b.html("")}catch(a){}return u("
").append(b).html().match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+C(b)})}function Xa(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.split("="),d=decodeURIComponent(c[0]),a[d]=s(c[1])?decodeURIComponent(c[1]):!0)});return a}function nb(b){var a=[];m(b,function(b,d){a.push(Ya(d,!0)+(b===!0?"":"="+Ya(b,!0)))});return a.length?a.join("&"):""}function Za(b){return Ya(b,!0).replace(/%26/gi,"&").replace(/%3D/gi, 14 | "=").replace(/%2B/gi,"+")}function Ya(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(a?null:/%20/g,"+")}function ic(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,h=["ng:app","ng-app","x-ng-app","data-ng-app"],f=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;m(h,function(a){h[a]=!0;c(aa.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(m(b.querySelectorAll("."+a),c),m(b.querySelectorAll("."+a+"\\:"),c),m(b.querySelectorAll("["+ 15 | a+"]"),c))});m(d,function(a){if(!e){var b=f.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):m(a.attributes,function(b){if(!e&&h[b.name])e=a,g=b.value})}});e&&a(e,g?[g]:[])}function ob(b,a){b=u(b);a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");var c=pb(a);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,h){a.$apply(function(){b.data("$injector",h);c(b)(a)})}]);return c}function $a(b,a){a=a||"_";return b.replace(jc, 16 | function(b,d){return(d?a:"")+b.toLowerCase()})}function pa(b,a,c){if(!b)throw new z("Argument '"+(a||"?")+"' is "+(c||"required"));return b}function qa(b,a,c){c&&K(b)&&(b=b[b.length-1]);pa(M(b),a,"not a function, got "+(b&&typeof b=="object"?b.constructor.name||"Object":typeof b));return b}function kc(b){function a(a,b,e){return a[b]||(a[b]=e())}return a(a(b,"angular",Object),"module",function(){var b={};return function(d,e,g){e&&b.hasOwnProperty(d)&&(b[d]=null);return a(b,d,function(){function a(c, 17 | d,e){return function(){b[e||"push"]([c,d,arguments]);return k}}if(!e)throw z("No module: "+d);var b=[],c=[],j=a("$injector","invoke"),k={_invokeQueue:b,_runBlocks:c,requires:e,name:d,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:j,run:function(a){c.push(a); 18 | return this}};g&&j(g);return k})}})}function qb(b){return b.replace(lc,function(a,b,d,e){return e?d.toUpperCase():d}).replace(mc,"Moz$1")}function ab(b,a){function c(){var e;for(var b=[this],c=a,h,f,i,j,k,l,n;b.length;){h=b.shift();f=0;for(i=h.length;f 
"+b;a.removeChild(a.firstChild);bb(this,a.childNodes);this.remove()}else bb(this,b)}function cb(b){return b.cloneNode(!0)}function ra(b){rb(b);for(var a=0,b=b.childNodes||[];a 21 | -1}function vb(b,a){a&&m(a.split(" "),function(a){b.className=Q((" "+b.className+" ").replace(/[\n\t]/g," ").replace(" "+Q(a)+" "," "))})}function wb(b,a){a&&m(a.split(" "),function(a){if(!Ca(b,a))b.className=Q(b.className+" "+Q(a))})}function bb(b,a){if(a)for(var a=!a.nodeName&&s(a.length)&&!na(a)?a:[a],c=0;c4096&&c.warn("Cookie '"+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"),V.length>20&&c.warn("Cookie '"+a+"' possibly not set or overflowed because too many cookies were already set ("+ 33 | V.length+" > 20 )")}else{if(i.cookie!==I){I=i.cookie;d=I.split("; ");V={};for(f=0;f0&&(V[unescape(e.substring(0,g))]=unescape(e.substring(g+1)))}return V}};f.defer=function(a,b){var c;o++;c=l(function(){delete r[c];e(a)},b||0);r[c]=!0;return c};f.defer.cancel=function(a){return r[a]?(delete r[a],n(a),e(x),!0):!1}}function vc(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new uc(b,d,a,c)}]}function wc(){this.$get=function(){function b(b, 34 | d){function e(a){if(a!=l){if(n){if(n==a)n=a.n}else n=a;g(a.n,a.p);g(a,l);l=a;l.n=null}}function g(a,b){if(a!=b){if(a)a.p=b;if(b)b.n=a}}if(b in a)throw z("cacheId "+b+" taken");var h=0,f=D({},d,{id:b}),i={},j=d&&d.capacity||Number.MAX_VALUE,k={},l=null,n=null;return a[b]={put:function(a,b){var c=k[a]||(k[a]={key:a});e(c);v(b)||(a in i||h++,i[a]=b,h>j&&this.remove(n.key))},get:function(a){var b=k[a];if(b)return e(b),i[a]},remove:function(a){var b=k[a];if(b==l)l=b.p;if(b==n)n=b.n;g(b.n,b.p);delete k[a]; 35 | delete i[a];h--},removeAll:function(){i={};h=0;k={};l=n=null},destroy:function(){k=f=i=null;delete a[b]},info:function(){return D({},f,{size:h})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function xc(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Bb(b){var a={},c="Directive",d=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,e=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,g="Template must have exactly one root element. was: "; 36 | this.directive=function f(d,e){G(d)?(pa(e,"directive"),a.hasOwnProperty(d)||(a[d]=[],b.factory(d+c,["$injector","$exceptionHandler",function(b,c){var e=[];m(a[d],function(a){try{var f=b.invoke(a);if(M(f))f={compile:B(f)};else if(!f.compile&&f.link)f.compile=B(f.link);f.priority=f.priority||0;f.name=f.name||d;f.require=f.require||f.controller&&f.name;f.restrict=f.restrict||"A";e.push(f)}catch(g){c(g)}});return e}])),a[d].push(e)):m(d,lb(f));return this};this.$get=["$injector","$interpolate","$exceptionHandler", 37 | "$http","$templateCache","$parse","$controller","$rootScope",function(b,i,j,k,l,n,r,o){function w(a,b,c){a instanceof u||(a=u(a));m(a,function(b,c){b.nodeType==3&&(a[c]=u(b).wrap("").parent()[0])});var d=t(a,b,a,c);return function(b,c){pa(b,"scope");var e=c?ta.clone.call(a):a;e.data("$scope",b);q(e,"ng-scope");c&&c(e,b);d&&d(b,e,e);return e}}function q(a,b){try{a.addClass(b)}catch(c){}}function t(a,b,c,d){function e(a,c,d,g){for(var j,i,n,k,l,o=0,r=0,q=f.length;oE.priority)break;if(B=E.scope)N("isolated scope",y,E,F),J(B)&&(q(F,"ng-isolate-scope"),y=E),q(F,"ng-scope"),A=A||E;W=E.name;if(B=E.controller)s=s||{},N("'"+W+"' controller",s[W],E,F),s[W]=E;if(B=E.transclude)N("transclusion",x,E,F),x=E,l=E.priority,B=="element"?($=u(b),F=c.$$element=u("<\!-- "+W+": "+c[W]+" --\>"),b=F[0],Ga(e,u($[0]),b),v=w($,d,l)):($=u(cb(b)).contents(),F.html(""),v=w($,d));if(B=E.template)if(N("template",I,E,F),I=E,$=u("
"+Q(B)+"
").contents(),b=$[0],E.replace){if($.length!= 44 | 1||b.nodeType!==1)throw new z(g+B);Ga(e,F,b);W={$attr:{}};a=a.concat(X(b,a.splice(C+1,a.length-(C+1)),W));L(c,W);H=a.length}else F.html(B);if(E.templateUrl)N("template",I,E,F),I=E,k=V(a.splice(C,a.length-C),k,F,c,e,E.replace,v),H=a.length;else if(E.compile)try{D=E.compile(F,c,v),M(D)?f(null,D):D&&f(D.pre,D.post)}catch(O){j(O,oa(F))}if(E.terminal)k.terminal=!0,l=Math.max(l,E.priority)}k.scope=A&&A.scope;k.transclude=x&&v;return k}function y(d,e,g,i){var n=!1;if(a.hasOwnProperty(e))for(var k,e=b.get(e+ 45 | c),l=0,o=e.length;lk.priority)&&k.restrict.indexOf(g)!=-1)d.push(k),n=!0}catch(r){j(r)}return n}function L(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){e.charAt(0)!="$"&&(b[e]&&(d+=(e==="style"?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b,function(b,f){f=="class"?(q(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):f=="style"?e.attr("style",e.attr("style")+";"+b):f.charAt(0)!="$"&&!a.hasOwnProperty(f)&&(a[f]=b,d[f]=c[f])})}function V(a,b,c,d,e,f,j){var i= 46 | [],n,o,r=c[0],q=a.shift(),w=D({},q,{controller:null,templateUrl:null,transclude:null});c.html("");k.get(q.templateUrl,{cache:l}).success(function(k){var l,q;if(f){q=u("
"+Q(k)+"
").contents();l=q[0];if(q.length!=1||l.nodeType!==1)throw new z(g+k);k={$attr:{}};Ga(e,c,l);X(l,a,k);L(d,k)}else l=r,c.html(k);a.unshift(w);n=A(a,c,d,j);for(o=t(c.contents(),j);i.length;){var m=i.pop(),k=i.pop();q=i.pop();var y=i.pop(),I=l;q!==r&&(I=cb(l),Ga(k,u(q),I));n(function(){b(o,y,I,e,m)},y,I,e,m)}i=null}).error(function(a, 47 | b,c,d){throw z("Failed to load template: "+d.url);});return function(a,c,d,e,f){i?(i.push(c),i.push(d),i.push(e),i.push(f)):n(function(){b(o,c,d,e,f)},c,d,e,f)}}function I(a,b){return b.priority-a.priority}function N(a,b,c,d){if(b)throw z("Multiple directives ["+b.name+", "+c.name+"] asking for "+a+" on: "+oa(d));}function F(a,b){var c=i(b,!0);c&&a.push({priority:0,compile:B(function(a,b){var d=b.parent(),e=d.data("$binding")||[];e.push(c);q(d.data("$binding",e),"ng-binding");a.$watch(c,function(a){b[0].nodeValue= 48 | a})})})}function W(a,b,c,d){var e=i(c,!0);e&&b.push({priority:100,compile:B(function(a,b,c){b=c.$$observers||(c.$$observers={});d==="class"&&(e=i(c[d],!0));c[d]=p;(b[d]||(b[d]=[])).$$inter=!0;(c.$$observers&&c.$$observers[d].$$scope||a).$watch(e,function(a){c.$set(d,a)})})})}function Ga(a,b,c){var d=b[0],e=d.parentNode,f,g;if(a){f=0;for(g=a.length;f0){var e=N[0],f=e.text;if(f==a||f==b||f==c||f==d||!a&&!b&&!c&&!d)return e}return!1}function f(b,c,d,f){return(b=h(b,c,d,f))?(a&&!b.json&&e("is not valid json",b),N.shift(),b):!1}function i(a){f(a)||e("is unexpected, expecting ["+ 66 | a+"]",h())}function j(a,b){return function(c,d){return a(c,d,b)}}function k(a,b,c){return function(d,f){return b(d,f,a,c)}}function l(){for(var a=[];;)if(N.length>0&&!h("}",")",";","]")&&a.push(v()),!f(";"))return a.length==1?a[0]:function(b,c){for(var d,f=0;f","<=",">="))a=k(a,b.fn,q());return a}function t(){for(var a=m(),b;b=f("*","/","%");)a=k(a,b.fn,m());return a}function m(){var a;return f("+")?A():(a=f("-"))?k(V,a.fn,m()):(a=f("!"))?j(a.fn,m()):A()}function A(){var a; 68 | if(f("("))a=v(),i(")");else if(f("["))a=y();else if(f("{"))a=L();else{var b=f();(a=b.fn)||e("not a primary expression",b)}for(var c;b=f("(","[",".");)b.text==="("?(a=u(a,c),c=null):b.text==="["?(c=a,a=da(a)):b.text==="."?(c=a,a=s(a)):e("IMPOSSIBLE");return a}function y(){var a=[];if(g().text!="]"){do a.push(F());while(f(","))}i("]");return function(b,c){for(var d=[],f=0;f1;d++){var e=a.shift(),g=b[e];g||(g={},b[e]=g);b=g}return b[a.shift()]=c}function eb(b,a,c){if(!a)return b;for(var a=a.split("."),d,e=b,g=a.length,h=0;h7),hasEvent:function(c){if(c=="input"&&Z==9)return!1;if(v(a[c])){var e=b.document.createElement("div");a[c]="on"+c in e}return a[c]},csp:!1}}]}function Tc(){this.$get=B(T)}function Mb(b){var a={},c,d,e;if(!b)return a;m(b.split("\n"),function(b){e=b.indexOf(":");c=C(Q(b.substr(0,e))); 89 | d=Q(b.substr(e+1));c&&(a[c]?a[c]+=", "+d:a[c]=d)});return a}function Nb(b){var a=J(b)?b:p;return function(c){a||(a=Mb(b));return c?a[C(c)]||null:a}}function Ob(b,a,c){if(M(c))return c(b,a);m(c,function(c){b=c(b,a)});return b}function Uc(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d=this.defaults={transformResponse:[function(d){G(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=mb(d,!0)));return d}],transformRequest:[function(a){return J(a)&&Sa.apply(a)!=="[object File]"?ba(a):a}],headers:{common:{Accept:"application/json, text/plain, */*", 90 | "X-Requested-With":"XMLHttpRequest"},post:{"Content-Type":"application/json;charset=utf-8"},put:{"Content-Type":"application/json;charset=utf-8"}}},e=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,i,j,k){function l(a){function c(a){var b=D({},a,{data:Ob(a.data,a.headers,f)});return 200<=a.status&&a.status<300?b:j.reject(b)}a.method=la(a.method);var e=a.transformRequest||d.transformRequest,f=a.transformResponse||d.transformResponse, 91 | g=d.headers,g=D({"X-XSRF-TOKEN":b.cookies()["XSRF-TOKEN"]},g.common,g[C(a.method)],a.headers),e=Ob(a.data,Nb(g),e),i;v(a.data)&&delete g["Content-Type"];i=n(a,e,g);i=i.then(c,c);m(w,function(a){i=a(i)});i.success=function(b){i.then(function(c){b(c.data,c.status,c.headers,a)});return i};i.error=function(b){i.then(null,function(c){b(c.data,c.status,c.headers,a)});return i};return i}function n(b,c,d){function e(a,b,c){m&&(200<=a&&a<300?m.put(w,[a,b,Mb(c)]):m.remove(w));f(b,a,c);i.$apply()}function f(a, 92 | c,d){c=Math.max(c,0);(200<=c&&c<300?n.resolve:n.reject)({data:a,status:c,headers:Nb(d),config:b})}function h(){var a=Ua(l.pendingRequests,b);a!==-1&&l.pendingRequests.splice(a,1)}var n=j.defer(),k=n.promise,m,p,w=r(b.url,b.params);l.pendingRequests.push(b);k.then(h,h);b.cache&&b.method=="GET"&&(m=J(b.cache)?b.cache:o);if(m)if(p=m.get(w))if(p.then)return p.then(h,h),p;else K(p)?f(p[1],p[0],U(p[2])):f(p,200,{});else m.put(w,k);p||a(b.method,w,c,e,d,b.timeout,b.withCredentials);return k}function r(a, 93 | b){if(!b)return a;var c=[];dc(b,function(a,b){a==null||a==p||(J(a)&&(a=ba(a)),c.push(encodeURIComponent(b)+"="+encodeURIComponent(a)))});return a+(a.indexOf("?")==-1?"?":"&")+c.join("&")}var o=c("$http"),w=[];m(e,function(a){w.push(G(a)?k.get(a):k.invoke(a))});l.pendingRequests=[];(function(a){m(arguments,function(a){l[a]=function(b,c){return l(D(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){l[a]=function(b,c,d){return l(D(d||{},{method:a,url:b, 94 | data:c}))}})})("post","put");l.defaults=d;return l}]}function Vc(){this.$get=["$browser","$window","$document",function(b,a,c){return Wc(b,Xc,b.defer,a.angular.callbacks,c[0],a.location.protocol.replace(":",""))}]}function Wc(b,a,c,d,e,g){function h(a,b){var c=e.createElement("script"),d=function(){e.body.removeChild(c);b&&b()};c.type="text/javascript";c.src=a;Z?c.onreadystatechange=function(){/loaded|complete/.test(c.readyState)&&d()}:c.onload=c.onerror=d;e.body.appendChild(c)}return function(e, 95 | i,j,k,l,n,r){function o(a,c,d,e){c=(i.match(Fb)||["",g])[1]=="file"?d?200:404:c;a(c==1223?204:c,d,e);b.$$completeOutstandingRequest(x)}b.$$incOutstandingRequestCount();i=i||b.url();if(C(e)=="jsonp"){var p="_"+(d.counter++).toString(36);d[p]=function(a){d[p].data=a};h(i.replace("JSON_CALLBACK","angular.callbacks."+p),function(){d[p].data?o(k,200,d[p].data):o(k,-2);delete d[p]})}else{var q=new a;q.open(e,i,!0);m(l,function(a,b){a&&q.setRequestHeader(b,a)});var t;q.onreadystatechange=function(){q.readyState== 96 | 4&&o(k,t||q.status,q.responseText,q.getAllResponseHeaders())};if(r)q.withCredentials=!0;q.send(j||"");n>0&&c(function(){t=-1;q.abort()},n)}}}function Yc(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January,February,March,April,May,June,July,August,September,October,November,December".split(","), 97 | SHORTMONTH:"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec".split(","),DAY:"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(","),SHORTDAY:"Sun,Mon,Tue,Wed,Thu,Fri,Sat".split(","),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return b===1?"one":"other"}}}}function Zc(){this.$get=["$rootScope","$browser","$q", 98 | "$exceptionHandler",function(b,a,c,d){function e(e,f,i){var j=c.defer(),k=j.promise,l=s(i)&&!i,f=a.defer(function(){try{j.resolve(e())}catch(a){j.reject(a),d(a)}l||b.$apply()},f),i=function(){delete g[k.$$timeoutId]};k.$$timeoutId=f;g[f]=j;k.then(i,i);return k}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),a.defer.cancel(b.$$timeoutId)):!1};return e}]}function Pb(b){function a(a,e){return b.factory(a+c,e)}var c="Filter";this.register=a;this.$get=["$injector", 99 | function(a){return function(b){return a.get(b+c)}}];a("currency",Qb);a("date",Rb);a("filter",$c);a("json",ad);a("limitTo",bd);a("lowercase",cd);a("number",Sb);a("orderBy",Tb);a("uppercase",dd)}function $c(){return function(b,a){if(!(b instanceof Array))return b;var c=[];c.check=function(a){for(var b=0;b 100 | -1;case "object":for(var c in a)if(c.charAt(0)!=="$"&&d(a[c],b))return!0;return!1;case "array":for(c=0;c=k+l)for(var j=h.length-k,n=0;n0||e>-c)e+=c;e===0&&c==-12&&(e=12);return hb(e,a,d)}}function La(b,a){return function(c,d){var e=c["get"+b](),g=la(a?"SHORT"+b:b);return d[g][e]}}function Rb(b){function a(a){var b;if(b=a.match(c)){var a=new Date(0),g=0,h=0;b[9]&&(g=H(b[9]+b[10]),h=H(b[9]+b[11]));a.setUTCFullYear(H(b[1]),H(b[2])-1,H(b[3]));a.setUTCHours(H(b[4]||0)-g,H(b[5]||0)-h,H(b[6]||0),H(b[7]||0))}return a} 104 | var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;return function(c,e){var g="",h=[],f,i,e=e||"mediumDate",e=b.DATETIME_FORMATS[e]||e;G(c)&&(c=ed.test(c)?H(c):a(c));va(c)&&(c=new Date(c));if(!ma(c))return c;for(;e;)(i=fd.exec(e))?(h=h.concat(ga.call(i,1)),e=h.pop()):(h.push(e),e=null);m(h,function(a){f=gd[a];g+=f?f(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ad(){return function(b){return ba(b,!0)}} 105 | function bd(){return function(b,a){if(!(b instanceof Array))return b;var a=H(a),c=[],d,e;if(!b||!(b instanceof Array))return c;a>b.length?a=b.length:a<-b.length&&(a=-b.length);a>0?(d=0,e=a):(d=b.length+a,e=b.length);for(;dl?(d.$setValidity("maxlength",!1),p):(d.$setValidity("maxlength",!0),a)};d.$parsers.push(c);d.$formatters.push(c)}}function ib(b,a){b="ngClass"+b;return R(function(c,d,e){c.$watch(e[b],function(b,e){if(a===!0||c.$index% 111 | 2===a)e&&b!==e&&(J(e)&&!K(e)&&(e=Ta(e,function(a,b){if(a)return b})),d.removeClass(K(e)?e.join(" "):e)),J(b)&&!K(b)&&(b=Ta(b,function(a,b){if(a)return b})),b&&d.addClass(K(b)?b.join(" "):b)},!0)})}var C=function(b){return G(b)?b.toLowerCase():b},la=function(b){return G(b)?b.toUpperCase():b},z=T.Error,Z=H((/msie (\d+)/.exec(C(navigator.userAgent))||[])[1]),u,ha,ga=[].slice,Ra=[].push,Sa=Object.prototype.toString,Yb=T.angular||(T.angular={}),sa,Cb,Y=["0","0","0"];x.$inject=[];ya.$inject=[];Cb=Z<9?function(b){b= 112 | b.nodeName?b:b[0];return b.scopeName&&b.scopeName!="HTML"?la(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var jc=/[A-Z]/g,hd={full:"1.0.1",major:1,minor:0,dot:1,codeName:"thorium-shielding"},Ba=P.cache={},Aa=P.expando="ng-"+(new Date).getTime(),nc=1,id=T.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},tb=T.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b, 113 | a,c){b.detachEvent("on"+a,c)},lc=/([\:\-\_]+(.))/g,mc=/^moz([A-Z])/,ta=P.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;this.bind("DOMContentLoaded",a);P(T).bind("load",a)},toString:function(){var b=[];m(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return b>=0?u(this[b]):u(this[this.length+b])},length:0,push:Ra,sort:[].sort,splice:[].splice},Ea={};m("multiple,selected,checked,disabled,readOnly,required".split(","),function(b){Ea[C(b)]=b});var zb={}; 114 | m("input,select,option,textarea,button,form".split(","),function(b){zb[la(b)]=!0});m({data:ub,inheritedData:Da,scope:function(b){return Da(b,"$scope")},controller:xb,injector:function(b){return Da(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Ca,css:function(b,a,c){a=qb(a);if(s(c))b.style[a]=c;else{var d;Z<=8&&(d=b.currentStyle&&b.currentStyle[a],d===""&&(d="auto"));d=d||b.style[a];Z<=8&&(d=d===""?p:d);return d}},attr:function(b,a,c){var d=C(a);if(Ea[d])if(s(c))c?(b[a]=!0, 115 | b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||x).specified?d:p;else if(s(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),b===null?p:b},prop:function(b,a,c){if(s(c))b[a]=c;else return b[a]},text:D(Z<9?function(b,a){if(b.nodeType==1){if(v(a))return b.innerText;b.innerText=a}else{if(v(a))return b.nodeValue;b.nodeValue=a}}:function(b,a){if(v(a))return b.textContent;b.textContent=a},{$dv:""}),val:function(b,a){if(v(a))return b.value; 116 | b.value=a},html:function(b,a){if(v(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)}, 124 | "&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Kc={n:"\n",f:"\u000c",r:"\r",t:"\t",v:"\u000b","'":"'",'"':'"'},gb={},Xc=T.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(a){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(c){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(d){}throw new z("This browser does not support XMLHttpRequest."); 125 | };Pb.$inject=["$provide"];Qb.$inject=["$locale"];Sb.$inject=["$locale"];var Vb=".",gd={yyyy:O("FullYear",4),yy:O("FullYear",2,0,!0),y:O("FullYear",1),MMMM:La("Month"),MMM:La("Month",!0),MM:O("Month",2,1),M:O("Month",1,1),dd:O("Date",2),d:O("Date",1),HH:O("Hours",2),H:O("Hours",1),hh:O("Hours",2,-12),h:O("Hours",1,-12),mm:O("Minutes",2),m:O("Minutes",1),ss:O("Seconds",2),s:O("Seconds",1),EEEE:La("Day"),EEE:La("Day",!0),a:function(a,c){return a.getHours()<12?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=a.getTimezoneOffset(); 126 | return hb(a/60,2)+hb(Math.abs(a%60),2)}},fd=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,ed=/^\d+$/;Rb.$inject=["$locale"];var cd=B(C),dd=B(la);Tb.$inject=["$parse"];var jd=B({restrict:"E",compile:function(a,c){c.href||c.$set("href","");return function(a,c){c.bind("click",function(a){c.attr("href")||a.preventDefault()})}}}),jb={};m(Ea,function(a,c){var d=ea("ng-"+c);jb[d]=function(){return{priority:100,compile:function(){return function(a,g,h){a.$watch(h[d],function(a){h.$set(c, 127 | !!a)})}}}}});m(["src","href"],function(a){var c=ea("ng-"+a);jb[c]=function(){return{priority:99,link:function(d,e,g){g.$observe(c,function(c){g.$set(a,c);Z&&e.prop(a,c)})}}}});var Oa={$addControl:x,$removeControl:x,$setValidity:x,$setDirty:x};Wb.$inject=["$element","$attrs","$scope"];var Ra={name:"form",restrict:"E",controller:Wb,compile:function(){return{pre:function(a,c,d,e){d.action||c.bind("submit",function(a){a.preventDefault()});var g=c.parent().controller("form"),h=d.name||d.ngForm;h&&(a[h]= 128 | e);g&&c.bind("$destroy",function(){g.$removeControl(e);h&&(a[h]=p);D(e,Oa)})}}}},kd=B(Ra),ld=B(D(U(Ra),{restrict:"EAC"})),md=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,nd=/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/,od=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,$b={text:Qa,number:function(a,c,d,e,g,h){Qa(a,c,d,e,g,h);e.$parsers.push(function(a){var c=S(a);return c||od.test(a)?(e.$setValidity("number",!0),a===""?null:c?a:parseFloat(a)):(e.$setValidity("number", 129 | !1),p)});e.$formatters.push(function(a){return S(a)?"":""+a});if(d.min){var f=parseFloat(d.min),a=function(a){return!S(a)&&ai?(e.$setValidity("max",!1),p):(e.$setValidity("max",!0),a)};e.$parsers.push(d);e.$formatters.push(d)}e.$formatters.push(function(a){return S(a)||va(a)?(e.$setValidity("number",!0),a):(e.$setValidity("number",!1), 130 | p)})},url:function(a,c,d,e,g,h){Qa(a,c,d,e,g,h);a=function(a){return S(a)||md.test(a)?(e.$setValidity("url",!0),a):(e.$setValidity("url",!1),p)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,g,h){Qa(a,c,d,e,g,h);a=function(a){return S(a)||nd.test(a)?(e.$setValidity("email",!0),a):(e.$setValidity("email",!1),p)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){v(d.name)&&c.attr("name",wa());c.bind("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})}); 131 | e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,h=d.ngFalseValue;G(g)||(g=!0);G(h)||(h=!1);c.bind("click",function(){a.$apply(function(){e.$setViewValue(c[0].checked)})});e.$render=function(){c[0].checked=e.$viewValue};e.$formatters.push(function(a){return a===g});e.$parsers.push(function(a){return a?g:h})},hidden:x,button:x,submit:x,reset:x},ac=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel", 132 | link:function(d,e,g,h){h&&($b[C(g.type)]||$b.text)(d,e,g,h,c,a)}}}],Na="ng-valid",Ma="ng-invalid",Pa="ng-pristine",Xb="ng-dirty",pd=["$scope","$exceptionHandler","$attrs","$element","$parse",function(a,c,d,e,g){function h(a,c){c=c?"-"+$a(c,"-"):"";e.removeClass((a?Ma:Na)+c).addClass((a?Na:Ma)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var g=g(d.ngModel), 133 | f=g.assign;if(!f)throw z(Db+d.ngModel+" ("+oa(e)+")");this.$render=x;var i=e.inheritedData("$formController")||Oa,j=0,k=this.$error={};e.addClass(Pa);h(!0);this.$setValidity=function(a,c){if(k[a]!==!c){if(c){if(k[a]&&j--,!j)h(!0),this.$valid=!0,this.$invalid=!1}else h(!1),this.$invalid=!0,this.$valid=!1,j++;k[a]=!c;h(c,a);i.$setValidity(a,c,this)}};this.$setViewValue=function(d){this.$viewValue=d;if(this.$pristine)this.$dirty=!0,this.$pristine=!1,e.removeClass(Pa).addClass(Xb),i.$setDirty();m(this.$parsers, 134 | function(a){d=a(d)});if(this.$modelValue!==d)this.$modelValue=d,f(a,d),m(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};var l=this;a.$watch(g,function(a){if(l.$modelValue!==a){var c=l.$formatters,d=c.length;for(l.$modelValue=a;d--;)a=c[d](a);if(l.$viewValue!==a)l.$viewValue=a,l.$render()}})}],qd=function(){return{require:["ngModel","^?form"],controller:pd,link:function(a,c,d,e){var g=e[0],h=e[1]||Oa;h.$addControl(g);c.bind("$destroy",function(){h.$removeControl(g)})}}},rd=B({require:"ngModel", 135 | link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),bc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var g=function(a){if(d.required&&(S(a)||a===!1))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g);d.$observe("required",function(){g(e.$viewValue)})}}}},sd=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])|| 136 | d.ngList||",",h=function(a){var c=[];a&&m(a.split(g),function(a){a&&c.push(Q(a))});return c};e.$parsers.push(h);e.$formatters.push(function(a){return K(a)&&!fa(h(e.$viewValue),a)?a.join(", "):p})}}},td=/^(true|false|\d+)$/,ud=function(){return{priority:100,compile:function(a,c){return td.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a,c,g){a.$watch(g.ngValue,function(a){g.$set("value",a,!1)})}}}},vd=R(function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBind); 137 | a.$watch(d.ngBind,function(a){c.text(a==p?"":a)})}),wd=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}],xd=[function(){return function(a,c,d){c.addClass("ng-binding").data("$binding",d.ngBindHtmlUnsafe);a.$watch(d.ngBindHtmlUnsafe,function(a){c.html(a||"")})}}],yd=ib("",!0),zd=ib("Odd",0),Ad=ib("Even",1),Bd=R({compile:function(a,c){c.$set("ngCloak",p);a.removeClass("ng-cloak")}}), 138 | Cd=[function(){return{scope:!0,controller:"@"}}],Dd=["$sniffer",function(a){return{priority:1E3,compile:function(){a.csp=!0}}}],cc={};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave".split(" "),function(a){var c=ea("ng-"+a);cc[c]=["$parse",function(d){return function(e,g,h){var f=d(h[c]);g.bind(C(a),function(a){e.$apply(function(){f(e,{$event:a})})})}}]});var Ed=R(function(a,c,d){c.bind("submit",function(){a.$apply(d.ngSubmit)})}),Fd=["$http","$templateCache", 139 | "$anchorScroll","$compile",function(a,c,d,e){return{restrict:"ECA",terminal:!0,compile:function(g,h){var f=h.ngInclude||h.src,i=h.onload||"",j=h.autoscroll;return function(g,h){var n=0,m,o=function(){m&&(m.$destroy(),m=null);h.html("")};g.$watch(f,function(f){var q=++n;f?a.get(f,{cache:c}).success(function(a){q===n&&(m&&m.$destroy(),m=g.$new(),h.html(a),e(h.contents())(m),s(j)&&(!j||g.$eval(j))&&d(),m.$emit("$includeContentLoaded"),g.$eval(i))}).error(function(){q===n&&o()}):o()})}}}}],Gd=R({compile:function(){return{pre:function(a, 140 | c,d){a.$eval(d.ngInit)}}}}),Hd=R({terminal:!0,priority:1E3}),Id=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,g,h){var f=h.count,i=g.attr(h.$attr.when),j=h.offset||0,k=e.$eval(i),l={};m(k,function(a,e){l[e]=c(a.replace(d,"{{"+f+"-"+j+"}}"))});e.$watch(function(){var c=parseFloat(e.$eval(f));return isNaN(c)?"":(k[c]||(c=a.pluralCat(c-j)),l[c](e,g,!0))},function(a){g.text(a)})}}}],Jd=R({transclude:"element",priority:1E3,terminal:!0,compile:function(a,c,d){return function(a, 141 | c,h){var f=h.ngRepeat,h=f.match(/^\s*(.+)\s+in\s+(.*)\s*$/),i,j,k;if(!h)throw z("Expected ngRepeat in form of '_item_ in _collection_' but got '"+f+"'.");f=h[1];i=h[2];h=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!h)throw z("'item' in 'item in collection' should be identifier or (key, value) but got '"+f+"'.");j=h[3]||h[1];k=h[2];var l=new db;a.$watch(function(a){var e,f,h=a.$eval(i),m=fc(h,!0),p,u=new db,A,y,v,s,z=c;if(K(h))v=h||[];else{v=[];for(A in h)h.hasOwnProperty(A)&&A.charAt(0)!= 142 | "$"&&v.push(A);v.sort()}e=0;for(f=v.length;ex;)u.pop().element.remove()}for(;v.length>w;)v.pop()[0].element.remove()}var h;if(!(h=w.match(d)))throw z("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '"+w+"'.");var j=c(h[2]||h[1]),k=h[4]||h[6],l=h[5],m=c(h[3]||""),n=c(h[2]?h[1]:k),r=c(h[7]),v=[[{element:f,label:""}]];q&&(a(q)(e),q.removeClass("ng-scope"),q.remove());f.html("");f.bind("change",function(){e.$apply(function(){var a, 152 | c=r(e)||[],d={},h,i,j,m,q,s;if(o){i=[];m=0;for(s=v.length;m@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak{display:none;}ng\\:form{display:block;}'); 158 | -------------------------------------------------------------------------------- /app/js/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.1 -------------------------------------------------------------------------------- /app/js/puzzle/slidingPuzzle.js: -------------------------------------------------------------------------------- 1 | (function(angular) { 2 | 'use strict'; 3 | 4 | var module = angular.module('slidingPuzzle', []); 5 | 6 | /** 7 | * Service 8 | */ 9 | module.factory('slidingPuzzle', function() { 10 | function shuffle(a) { 11 | var q; 12 | for (var j, x, i = a.length; i; j = parseInt(Math.random() * i, 10), x = a[--i], a[i] = a[j], a[j] = x) { q = 0; } 13 | return a; 14 | } 15 | 16 | function SlidingPuzzle(rows, cols) { 17 | /** 18 | * Puzzle grid 19 | * @type {Array} 20 | */ 21 | this.grid = []; 22 | 23 | /** 24 | * Moves count 25 | * @type {Number} 26 | */ 27 | this.moves = 0; 28 | 29 | /** 30 | * Moves tile 31 | * @param srow 32 | * @param scol 33 | */ 34 | this.move = function(srow, scol) { 35 | var dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]], 36 | tref, trow, tcol; 37 | 38 | for (var d = 0; d < dirs.length; d++) { 39 | trow = srow + dirs[d][0]; 40 | tcol = scol + dirs[d][1]; 41 | if (this.grid[trow] && this.grid[trow][tcol] && this.grid[trow][tcol].empty) { 42 | tref = this.grid[srow][scol]; 43 | this.grid[srow][scol] = this.grid[trow][tcol]; 44 | this.grid[trow][tcol] = tref; 45 | this.moves++; 46 | } 47 | } 48 | }; 49 | 50 | /** 51 | * Shuffles grid 52 | */ 53 | this.shuffle = function() { 54 | var tiles = []; 55 | this.traverse(function(tile) { 56 | tiles.push(tile); 57 | }); 58 | shuffle(tiles); 59 | this.traverse(function(tile, row, col) { 60 | this.grid[row][col] = tiles.shift(); 61 | }); 62 | this.moves = 0; 63 | }; 64 | 65 | /** 66 | * Solves puzzle 67 | */ 68 | this.solve = function() { 69 | var tiles = []; 70 | this.traverse(function(tile) { 71 | tiles.push(tile); 72 | }); 73 | tiles.sort(function(x, y) { 74 | return (x.id - y.id); 75 | }); 76 | this.traverse(function(tile, row, col) { 77 | this.grid[row][col] = tiles.shift(); 78 | }); 79 | }; 80 | 81 | /** 82 | * Is solved? 83 | * @type {Boolean} 84 | */ 85 | this.isSolved = function() { 86 | var id = 1; 87 | for (var row = 0; row < rows; row++) { 88 | for (var col = 0; col < cols; col++) { 89 | if (this.grid[row][col].id !== id++) { 90 | return false; 91 | } 92 | } 93 | } 94 | return true; 95 | }; 96 | 97 | /** 98 | * Traverses grid and executes fn on every tile 99 | * @param fn 100 | */ 101 | this.traverse = function(fn) { 102 | for (var row = 0; row < rows; row++) { 103 | for (var col = 0; col < cols; col++) { 104 | fn.call(this, this.grid && this.grid[row] ? this.grid[row][col] : undefined, row, col); 105 | } 106 | } 107 | }; 108 | 109 | // initialize grid 110 | var id = 1; 111 | this.traverse(function(tile, row, col) { 112 | if (!this.grid[row]) { 113 | this.grid[row] = []; 114 | } 115 | this.grid[row][col] = { 116 | id: id++, 117 | empty: (row === rows - 1) && (col === cols - 1) 118 | }; 119 | if (this.grid[row][col].empty) { 120 | this.empty = this.grid[row][col]; 121 | } 122 | }); 123 | } 124 | 125 | return function(rows, cols) { 126 | return new SlidingPuzzle(rows, cols); 127 | }; 128 | }); 129 | 130 | /** 131 | * Directive 132 | */ 133 | module.directive('slidingPuzzle', function(slidingPuzzle) { 134 | return { 135 | restrict: 'EA', 136 | replace: true, 137 | template: '' + 138 | '' + 139 | '' + 140 | '' + 141 | '
', 142 | scope: { 143 | size: '@', 144 | src: '@', 145 | api: '=' 146 | }, 147 | link: function(scope, element, attrs) { 148 | var rows, cols, 149 | loading = true, 150 | image = new Image(); 151 | 152 | function create() { 153 | scope.puzzle = slidingPuzzle(rows, cols); 154 | 155 | if (attrs.api) { 156 | scope.api = scope.puzzle; 157 | } 158 | 159 | tile(); 160 | } 161 | 162 | function tile() { 163 | if (loading) { 164 | return; 165 | } 166 | 167 | var width = image.width / cols, 168 | height = image.height / rows; 169 | 170 | scope.puzzle.traverse(function(tile, row, col) { 171 | tile.style = { 172 | width: width + 'px', 173 | height: height + 'px', 174 | background: (tile.empty ? 'none' : "url('" + scope.src + "') no-repeat -" + (col * width) + 'px -' + (row * height) + 'px') 175 | }; 176 | }); 177 | 178 | scope.puzzle.shuffle(); 179 | } 180 | 181 | attrs.$observe('size', function(size) { 182 | size = size.split('x'); 183 | if (size[0] >= 2 && size[1] >= 2) { 184 | rows = size[0]; 185 | cols = size[1]; 186 | create(); 187 | } 188 | }); 189 | 190 | attrs.$observe('src', function(src) { 191 | loading = true; 192 | image.src = src; 193 | image.onload = function() { 194 | loading = false; 195 | scope.$apply(function() { 196 | tile(); 197 | }); 198 | }; 199 | }); 200 | } 201 | }; 202 | }); 203 | })(window.angular); 204 | -------------------------------------------------------------------------------- /app/js/puzzle/wordSearchPuzzle.js: -------------------------------------------------------------------------------- 1 | (function(angular) { 2 | 'use strict'; 3 | 4 | var module = angular.module('wordSearchPuzzle', []); 5 | 6 | /** 7 | * Service 8 | */ 9 | module.factory('wordSearchPuzzle', function() { 10 | /** 11 | * Word search puzzle 12 | * @param matrix 13 | * @param words 14 | * @constructor 15 | */ 16 | function WordSearchPuzzle(matrix, words) { 17 | var maxRow = 0, 18 | maxCol = 0; 19 | 20 | /** 21 | * Matrix 22 | * @type {Array} 23 | */ 24 | this.matrix = []; 25 | 26 | /** 27 | * Words 28 | * @type {Array} 29 | */ 30 | this.words = []; 31 | 32 | /** 33 | * Is solved? 34 | * @type {Boolean} 35 | */ 36 | this.solved = false; 37 | 38 | // parse matrix and words 39 | angular.forEach(matrix, function(letters, row) { 40 | angular.forEach(letters, function(letter, col) { 41 | var item = { 42 | letter: letter, 43 | col: col, 44 | row: row, 45 | used: false 46 | }; 47 | if (!this.matrix[row]) { 48 | this.matrix[row] = []; 49 | } 50 | this.matrix[row].push(item); 51 | maxCol = col; 52 | }, this); 53 | maxRow = row; 54 | }, this); 55 | angular.forEach(words, function(word) { 56 | this.words.push({ 57 | name: word 58 | }); 59 | }, this); 60 | 61 | /** 62 | * Returns matrix item by coords 63 | * @param col 64 | * @param row 65 | * @return {*} 66 | */ 67 | this.getItem = function(col, row) { 68 | return (this.matrix[row] ? this.matrix[row][col] : undefined); 69 | }; 70 | 71 | /** 72 | * Returns matrix items by start/end coords 73 | * @param colFrom 74 | * @param rowFrom 75 | * @param colTo 76 | * @param rowTo 77 | * @return {Array} 78 | */ 79 | this.getItems = function(colFrom, rowFrom, colTo, rowTo) { 80 | var items = []; 81 | 82 | if (rowTo > maxRow) { 83 | rowTo = maxRow; 84 | } 85 | if (colTo > maxCol) { 86 | colTo = maxCol; 87 | } 88 | 89 | if (this.getItem(colTo, rowTo) === undefined) { 90 | return items; 91 | } 92 | 93 | if (colFrom === colTo || rowFrom === rowTo || Math.abs(colTo - colFrom) === Math.abs(rowTo - rowFrom)) { 94 | var shiftX = (colFrom === colTo ? 0 : (colTo > colFrom ? 1 : -1)), 95 | shiftY = (rowFrom === rowTo ? 0 : (rowTo > rowFrom ? 1 : -1)), 96 | col = colFrom, 97 | row = rowFrom; 98 | 99 | items.push(this.getItem(col, row)); 100 | do { 101 | col += shiftX; 102 | row += shiftY; 103 | items.push(this.getItem(col, row)); 104 | } while (col !== colTo || row !== rowTo); 105 | } 106 | 107 | return items; 108 | }; 109 | 110 | /** 111 | * Check items - find word 112 | * @param items 113 | */ 114 | this.lookup = function(items) { 115 | if (!items.length) { 116 | return; 117 | } 118 | 119 | // create words 120 | var words = ['']; 121 | angular.forEach(items, function(item) { 122 | words[0] += item.letter; 123 | }); 124 | words.push(words[0].split('').reverse().join('')); // word in reverse order (when selecting) 125 | 126 | // check words 127 | this.solved = true; 128 | angular.forEach(this.words, function(word) { 129 | if (word.found) { 130 | return; 131 | } 132 | angular.forEach(words, function(itemWord) { 133 | if (word.name === itemWord) { 134 | word.found = true; 135 | angular.forEach(items, function(item) { 136 | item.found = true; 137 | }); 138 | } 139 | }); 140 | if (!word.found) { 141 | this.solved = false; 142 | } 143 | }, this); 144 | }; 145 | 146 | /** 147 | * Solves puzzle 148 | */ 149 | this.solve = function() { 150 | var start = {}, 151 | directions = { 152 | N: [0, -1], E: [1, 0], S: [0, 1], W: [-1, 0], 153 | NE: [1, -1], NW: [-1, -1], SE: [1, 1], SW: [-1, 1] 154 | }; 155 | 156 | // group items by letters for faster search 157 | angular.forEach(this.matrix, function(items) { 158 | angular.forEach(items, function(item) { 159 | if (!start[item.letter]) { 160 | start[item.letter] = []; 161 | } 162 | start[item.letter].push(item); 163 | }); 164 | }); 165 | 166 | angular.forEach(this.words, function(word) { 167 | angular.forEach(start[word.name.charAt(0)], function(start) { 168 | if (word.found) { 169 | return; 170 | } 171 | angular.forEach(directions, function(shift) { 172 | if (word.found) { 173 | return; 174 | } 175 | this.lookup(this.getItems( 176 | start.col, start.row, 177 | start.col + (word.name.length - 1) * shift[0], 178 | start.row + (word.name.length - 1) * shift[1] 179 | )); 180 | }, this); 181 | }, this); 182 | }, this); 183 | }; 184 | } 185 | 186 | return function(matrix, words) { 187 | return new WordSearchPuzzle(matrix, words); 188 | }; 189 | }); 190 | 191 | /** 192 | * Directive 193 | */ 194 | module.directive('wordSearchPuzzle', function(wordSearchPuzzle) { 195 | return { 196 | restrict: 'EA', 197 | replace: true, 198 | template: '' + 199 | '' + 200 | '' + 205 | '' + 206 | '
' + 203 | ' {{item.letter}}' + 204 | '
', 207 | scope: { 208 | matrix: '=', 209 | words: '=', 210 | api: '=' 211 | }, 212 | link: function(scope, element, attrs) { 213 | var selectFrom; 214 | 215 | // setup puzzle 216 | scope.$watch('matrix', function(matrix) { 217 | scope.puzzle = wordSearchPuzzle(matrix, scope.words); 218 | 219 | // link service 220 | if (attrs.api) { 221 | scope.api = scope.puzzle; 222 | } 223 | }); 224 | 225 | /** 226 | * Selected items 227 | * @type {Array} 228 | */ 229 | scope.selected = []; 230 | 231 | /** 232 | * Selection start 233 | * @param item 234 | */ 235 | scope.selectStart = function(item) { 236 | selectFrom = item; 237 | }; 238 | 239 | /** 240 | * Selection enter (over) 241 | * @param item 242 | */ 243 | scope.selectEnter = function(item) { 244 | if (selectFrom) { 245 | scope.selected = scope.puzzle.getItems(selectFrom.col, selectFrom.row, item.col, item.row); 246 | } 247 | }; 248 | 249 | /** 250 | * Selection end 251 | */ 252 | scope.selectEnd = function() { 253 | selectFrom = null; 254 | scope.puzzle.lookup(scope.selected); 255 | scope.selected = []; 256 | }; 257 | 258 | // propagate selection state to matrix 259 | scope.$watch('selected', function(newItems, oldItems) { 260 | angular.forEach(oldItems, function(item) { 261 | item.selected = false; 262 | }); 263 | angular.forEach(newItems, function(item) { 264 | item.selected = true; 265 | }); 266 | }); 267 | } 268 | }; 269 | }); 270 | })(window.angular); 271 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | basePath = '../'; 2 | 3 | port = 8088; 4 | 5 | files = [ 6 | JASMINE, 7 | JASMINE_ADAPTER, 8 | 'app/js/lib/angular/angular.js', 9 | 'app/js/lib/angular/angular-mocks.js', 10 | 'app/js/puzzle/*.js', 11 | 'app/js/*.js', 12 | 'test/unit/*.js' 13 | ]; 14 | 15 | // list of files to exclude 16 | exclude = []; 17 | 18 | // use dots reporter, as travis terminal does not support escaping sequences 19 | // possible values: 'dots' || 'progress' 20 | reporter = 'dots'; 21 | 22 | // cli runner port 23 | runnerPort = 9100; 24 | 25 | // enable / disable colors in the output (reporters and logs) 26 | colors = true; 27 | 28 | // level of logging 29 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 30 | logLevel = LOG_INFO | LOG_WARN | LOG_ERROR; 31 | 32 | // enable / disable watching file and executing tests whenever any file changes 33 | autoWatch = true; 34 | 35 | // polling interval in ms (ignored on OS that support inotify) 36 | autoWatchInterval = 1; 37 | 38 | // Start these browsers, currently available: 39 | // - Chrome 40 | // - ChromeCanary 41 | // - Firefox 42 | // - Opera 43 | // - Safari 44 | // - PhantomJS 45 | browsers = ['PhantomJS']; 46 | -------------------------------------------------------------------------------- /test/unit/appSpec.js: -------------------------------------------------------------------------------- 1 | describe('App', function() { 2 | 3 | beforeEach(module('puzzleApp')); 4 | 5 | describe('advancedCtrl', function() { 6 | var ctrl, scope; 7 | 8 | beforeEach(inject(function($controller, $rootScope) { 9 | scope = $rootScope.$new(); 10 | ctrl = $controller('slidingAdvancedCtrl', { 11 | $scope: scope 12 | }); 13 | })); 14 | 15 | it('should publish the puzzles model', function() { 16 | expect(scope.puzzles).toBeDefined(); 17 | }); 18 | 19 | it('should have correct size set in model', function() { 20 | scope.puzzles.forEach(function(item) { 21 | expect(item.rows).toBeGreaterThan(0); 22 | expect(item.cols).toBeGreaterThan(0); 23 | }); 24 | }); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/slidingPuzzleSpec.js: -------------------------------------------------------------------------------- 1 | describe('Sliding puzzle', function() { 2 | var puzzle, 3 | rows = 2, 4 | cols = 3; 5 | 6 | function fireEvent(element, event) { 7 | var evt; 8 | if (document.createEvent) { 9 | evt = document.createEvent('HTMLEvents'); 10 | evt.initEvent(event, true, true); 11 | return !element.dispatchEvent(evt); 12 | } else { 13 | evt = document.createEventObject(); 14 | return element.fireEvent('on' + event, evt); 15 | } 16 | } 17 | 18 | function click(element) { 19 | fireEvent(element, 'click'); 20 | } 21 | 22 | beforeEach(module('slidingPuzzle')); 23 | 24 | /** 25 | * Controller tests 26 | */ 27 | describe('puzzle object', function() { 28 | beforeEach(inject(function(slidingPuzzle) { 29 | puzzle = slidingPuzzle(rows, cols); 30 | })); 31 | 32 | function tile(row, col) { 33 | return puzzle.grid[row][col]; 34 | } 35 | 36 | it('should initialize puzzle grid by size', function() { 37 | expect(puzzle.grid).toBeDefined(); 38 | expect(puzzle.grid.length).toBe(rows); 39 | expect(puzzle.grid[0].length).toBe(cols); 40 | }); 41 | 42 | it('should have empty flag on last item', function() { 43 | expect(tile(rows - 1, cols - 1).empty).toBe(true); 44 | expect(tile(0, 0).empty).toBe(false); 45 | }); 46 | 47 | it('should move movable item', function() { 48 | var x = rows - 1, 49 | y = cols - 2, 50 | item = tile(x, y); 51 | 52 | puzzle.move(x, y); 53 | expect(tile(x, y)).not.toBe(item); 54 | expect(tile(x, y + 1)).toBe(item); 55 | expect(tile(x, y).empty).toBe(true); 56 | }); 57 | 58 | it('should not move non-movable first item', function() { 59 | var x = 0, 60 | y = 0, 61 | item = tile(x, y); 62 | 63 | puzzle.move(x, y); 64 | expect(tile(x, y)).toBe(item); 65 | }); 66 | 67 | it('should return correct "solved" status', function() { 68 | var x = rows - 1, 69 | y = cols - 2, 70 | item = tile(x, y); 71 | 72 | expect(puzzle.isSolved()).toBe(true); 73 | puzzle.move(x, y); 74 | expect(puzzle.isSolved()).toBe(false); 75 | puzzle.move(x, y + 1); 76 | expect(puzzle.isSolved()).toBe(true); 77 | }); 78 | 79 | it('should solve puzzle when "solve" called', function() { 80 | var x = rows - 1, 81 | y = cols - 2, 82 | item = tile(x, y); 83 | 84 | expect(puzzle.isSolved()).toBe(true); 85 | puzzle.move(x, y); 86 | expect(puzzle.isSolved()).toBe(false); 87 | puzzle.solve(); 88 | expect(puzzle.isSolved()).toBe(true); 89 | }); 90 | 91 | }); 92 | 93 | /** 94 | * Directive tests 95 | * @todo image mock, background positioning test 96 | */ 97 | describe('puzzle directive', function() { 98 | var scope, template, templateScope, 99 | size = rows + 'x' + cols; 100 | 101 | function tile(row, col) { 102 | return template.find('tr').eq(row).find('td').eq(col); 103 | } 104 | 105 | beforeEach(function() { 106 | inject(function($compile, $rootScope) { 107 | scope = $rootScope.$new(); 108 | scope.size = size; 109 | template = $compile('')(scope); 110 | templateScope = template.scope(); 111 | scope.$apply(); 112 | }); 113 | }); 114 | 115 | it('should initialize puzzle object in directive scope (only)', function() { 116 | expect(templateScope.puzzle).toBeDefined(); 117 | expect(scope.puzzle).toBeUndefined(); 118 | }); 119 | 120 | it('should assign the puzzle object into parent scope by "api" accessor', function() { 121 | expect(typeof(scope.api)).toBe('object'); 122 | expect(scope.api.grid).toBeDefined(); 123 | expect(scope.api.grid.length).toBe(2); 124 | expect(scope.api.grid[0].length).toBe(3); 125 | }); 126 | 127 | it('should initialize size in directive scope', function() { 128 | expect(templateScope.size).toBe(size); 129 | }); 130 | 131 | it('should create 2x3 table', function() { 132 | expect(template.find('tr').length).toBe(2); 133 | expect(template.find('td').length).toBe(6); 134 | }); 135 | 136 | it('should have puzzle-solved class on init', function() { 137 | expect(template.hasClass('puzzle-solved')).toBe(true); 138 | }); 139 | 140 | it('should have puzzle-empty class on last element', function() { 141 | expect(tile(rows - 1, cols - 1).hasClass('puzzle-empty')).toBe(true); 142 | }); 143 | 144 | it('should regenerate puzzle when size changed', function() { 145 | scope.size = '4x4'; 146 | scope.$apply(); 147 | expect(template.find('tr').length).toBe(4); 148 | expect(template.find('td').length).toBe(16); 149 | }); 150 | 151 | it('should "move" movable tile on click', function() { 152 | var x = rows - 1, 153 | y = cols - 2, 154 | node = tile(x, y)[0]; 155 | 156 | expect(node.title).toBe('5'); 157 | click(node); 158 | expect(tile(x, y)[0]).not.toBe('5'); 159 | expect(tile(x, y + 1)[0].title).toBe('5'); 160 | expect(tile(x, y).hasClass('puzzle-empty')).toBe(true); 161 | }); 162 | 163 | it('should not "move" non-movable first tile on click', function() { 164 | var node = tile(0, 0)[0]; 165 | expect(node.title).toBe('1'); 166 | click(node); 167 | expect(tile(0, 0)[0].title).toBe('1'); 168 | }); 169 | 170 | }); 171 | 172 | }); --------------------------------------------------------------------------------