├── logo.png ├── misc ├── art │ └── logo.idraw ├── demo │ ├── views │ │ ├── demo.html │ │ ├── article.html │ │ ├── subreddit.html │ │ ├── commenter.html │ │ └── reddit.html │ ├── js │ │ ├── service │ │ │ └── github.js │ │ ├── directive │ │ │ ├── typeahead.js │ │ │ └── commenter.js │ │ └── app.js │ ├── index.html │ └── css │ │ └── app.css ├── test │ ├── helpers.js │ └── angular-mocks.js ├── changelog.tpl.md └── validate-commit-msg.js ├── .gitignore ├── template └── comments │ ├── comments.html │ └── comment.html ├── .travis.yml ├── docs ├── css │ └── style.css └── nav.html ├── bower.json ├── package.json ├── karma.conf.js ├── README.md ├── CHANGELOG.md ├── src ├── test │ └── comments.spec.js └── comments.js └── Gruntfile.js /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caitp/ui-comments/HEAD/logo.png -------------------------------------------------------------------------------- /misc/art/logo.idraw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caitp/ui-comments/HEAD/misc/art/logo.idraw -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.html.js 2 | .DS_Store 3 | .grunt 4 | /node_modules 5 | /dist 6 | *-SNAPSHOT* 7 | /bower_components 8 | -------------------------------------------------------------------------------- /template/comments/comments.html: -------------------------------------------------------------------------------- 1 |
4 |
{{article.score}}
9 | {{article.author}} 10 | 11 |{{article.title}}
15 | 16 | 17 |Nested, Reddit-style comments directives for AngularJS
75 |Customizable comment templates, customizable comment controllers, and built-in support for nesting and sorting comments. It's just lovely stuff, innit.
76 |77 | 79 | Code on Github 80 | 83 | Download (<%= pkg.version%>) 84 | API Documentation 87 |
88 |
14 | * angular.module('example', ['ui.comments'])
15 | * .config(function(commentsConfig) {
16 | * commentsConfig.set('commentController', 'MyController');
17 | * commentsConfig.set({
18 | * containerTemplate: 'my/custom/views/comments.html'
19 | * });
20 | * });
21 | *
22 | *
23 | * Injected as a service, it is simply the configuration object in its current state.
24 | *
25 | * It is wise not to write to the service outside of config blocks, because the
26 | * set() method provides some safety checks to ensure that only valid values are
27 | * written. It should not be necessary for an application to inject commentsConfig anywhere
28 | * except config blocks.
29 | */
30 | .provider('commentsConfig', function() {
31 | var config = {
32 | /**
33 | * @ngdoc property
34 | * @name ui.comments.commentsConfig#containerTemplate
35 | * @propertyOf ui.comments.commentsConfig
36 | *
37 | * @description
38 | *
39 | * The template URL for collections of comments. Support for inline templates is not yet
40 | * available, and so this must be a valid URL or cached ng-template
41 | */
42 | containerTemplate: 'template/comments/comments.html',
43 | /**
44 | * @ngdoc property
45 | * @name ui.comments.commentsConfig#commentTemplate
46 | * @propertyOf ui.comments.commentsConfig
47 | *
48 | * @description
49 | *
50 | * The template URL for a single comment. Support for inline templates is not yet
51 | * available, and so this must be a valid URL or cached ng-template
52 | *
53 | * If this template manually includes a {@link ui.comments.directive:comments comments}
54 | * directive, it will result in an infinite $compile loop. Instead,
55 | * {@link ui.comments.directive:comment comment} generates child collections programmatically.
56 | * Currently, these are simply appended to the body of the comment.
57 | */
58 | commentTemplate: 'template/comments/comment.html',
59 | /**
60 | * @ngdoc property
61 | * @name ui.comments.commentsConfig#orderBy
62 | * @propertyOf ui.comments.commentsConfig
63 | *
64 | * @description
65 | *
66 | * Presently, this configuration item is not actually used.
67 | *
68 | * **TODO**: Its intended purpose is to provide a default comment ordering rule. However,
69 | * currently there is no machinery for ordering templates at all. This is intended for a later
70 | * release.
71 | */
72 | orderBy: 'best',
73 | /**
74 | * @ngdoc property
75 | * @name ui.comments.commentsConfig#commentController
76 | * @propertyOf ui.comments.commentsConfig
77 | *
78 | * @description
79 | *
80 | * Custom controller to be instantiated for each comment. The instantiated controller is
81 | * given the property `$element` in scope. This allows the instantiated controller
82 | * to bind to comment events.
83 | *
84 | * The controller may be specified either as a registered controller (string), a function,
85 | * or by array notation.
86 | *
87 | */
88 | commentController: undefined,
89 | /**
90 | * @ngdoc property
91 | * @name ui.comments.commentsConfig#depthLimit
92 | * @propertyOf ui.comments.commentsConfig
93 | *
94 | * @description
95 | *
96 | * Default maximum depth of comments to display in a comments collection. When the depth
97 | * limit is exceeded, no further child comments collections shall be created.
98 | *
99 | * The depth limit may also be specified via the `comment-depth-limit` attribute for the
100 | * {@link ui.comments.directive:comments comments} directive.
101 | */
102 | depthLimit: 5
103 | };
104 | var emptyController = function() {};
105 | function stringSetter(setting, value) {
106 | if (typeof value === 'string') {
107 | config[setting] = value;
108 | }
109 | }
110 | function controllerSetter(setting, value) {
111 | if (value && (angular.isString(value) && value.length ||
112 | angular.isFunction(value) ||
113 | angular.isArray(value))) {
114 | config[setting] = value;
115 | } else {
116 | config[setting] = emptyController;
117 | }
118 | }
119 | function numberSetter(setting, value) {
120 | if (typeof value === 'number') {
121 | config[setting] = value;
122 | }
123 | }
124 |
125 | var setters = {
126 | 'containerTemplate': stringSetter,
127 | 'commentTemplate': stringSetter,
128 | 'orderBy': stringSetter,
129 | 'commentController': controllerSetter,
130 | 'depthLimit': numberSetter
131 | };
132 | this.$get = function() {
133 | return config;
134 | };
135 |
136 | /**
137 | * @ngdoc function
138 | * @name ui.comments.commentsConfig#set
139 | * @methodOf ui.comments.commentsConfig
140 | * @function
141 | *
142 | * @description
143 | *
144 | * _When injected into a config block_, this method allows the manipulate the comments
145 | * configuration.
146 | *
147 | * This method performs validation and only permits the setting of known properties, and
148 | * will only set values of acceptable types. Further validation, such as detecting whether or
149 | * not a controller is actually registered, is not performed.
150 | *
151 | * @param {string|object} name Either the name of the property to be accessed, or an object
152 | * containing keys and values to extend the configuration with.
153 | *
154 | * @param {*} value The value to set the named key to. Its type depends on the
155 | * property being set.
156 | *
157 | * @returns {undefined} Currently, this method is not chainable.
158 | */
159 | this.set = function(name, value) {
160 | var fn, key, props, i;
161 | if (typeof name === 'string') {
162 | fn = setters[name];
163 | if (fn) {
164 | fn(name, value);
165 | }
166 | } else if (typeof name === 'object') {
167 | props = Object.keys(name);
168 | for(i=0; i287 | *302 | * 303 | * **IMPORANT**: Do **not** use the {@link ui.comments.directive:comments comments} directive in a 304 | * {@link ui.comments.commentsConfig#commentTemplate commentTemplate}. This will cause an 305 | * infinite {@link http://docs.angularjs.org/api/ng.$compile $compile} loop, and eat a lot of 306 | * memory. 307 | */ 308 | .directive('comment', function($compile, commentsConfig, $controller, $exceptionHandler, $timeout) { 309 | return { 310 | require: ['^comments', 'comment'], 311 | restrict: 'EA', 312 | transclude: true, 313 | replace: true, 314 | templateUrl: function() { return commentsConfig.commentTemplate; }, 315 | scope: { 316 | comment: '=commentData' 317 | }, 318 | controller: function($scope) {}, 319 | link: function(scope, elem, attr, ctrls) { 320 | var comments = ctrls[0], comment = ctrls[1]; 321 | var controller = commentsConfig.commentController, controllerInstance; 322 | 323 | scope.commentDepth = comments.commentsDepth; 324 | scope.commentDepthLimit = (comments.commentsRoot || comments).commentsDepthLimit; 325 | comment.comments = comments; 326 | 327 | if (controller) { 328 | controllerInstance = $controller(controller, { 329 | '$scope': scope, 330 | '$element': elem 331 | }); 332 | if (controllerInstance) { 333 | elem.data('$CommentController', controllerInstance); 334 | } 335 | } 336 | if (elem.parent().attr('child-comments') === 'true') { 337 | elem.addClass('child-comment'); 338 | } 339 | var children = false, compiled, 340 | sub = $compile(''), 342 | transclude; 343 | // Notify controller without bubbling 344 | function notify(scope, name, data) { 345 | if (!controllerInstance) { return; } 346 | var namedListeners = scope.$$listeners[name] || [], i, length, args = [data]; 347 | for (i=0, length=namedListeners.length; i288 | * 299 | * 300 | *301 | *
422 | *438 | */ 439 | .directive('commentsTransclude', function() { 440 | return { 441 | restrict: 'EA', 442 | require: '^comment', 443 | link: function(scope, element, attr, comment) { 444 | attr.$addClass('comments-transclude'); 445 | comment.commentsTransclude = element; 446 | } 447 | }; 448 | }); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var markdown = require('node-markdown').Markdown; 2 | var bower = require('bower'); 3 | 4 | module.exports = function(grunt) { 5 | 6 | grunt.loadNpmTasks('grunt-contrib-watch'); 7 | grunt.loadNpmTasks('grunt-contrib-concat'); 8 | grunt.loadNpmTasks('grunt-contrib-copy'); 9 | grunt.loadNpmTasks('grunt-contrib-jshint'); 10 | grunt.loadNpmTasks('grunt-contrib-uglify'); 11 | grunt.loadNpmTasks('grunt-html2js'); 12 | grunt.loadNpmTasks('grunt-karma'); 13 | grunt.loadNpmTasks('grunt-conventional-changelog'); 14 | grunt.loadNpmTasks('grunt-ngdocs-caitp'); 15 | grunt.loadNpmTasks('grunt-ngmin'); 16 | grunt.loadNpmTasks('grunt-gh-pages'); 17 | grunt.loadNpmTasks('grunt-contrib-connect'); 18 | grunt.loadNpmTasks('grunt-contrib-clean'); 19 | 20 | // Project configuration. 21 | grunt.util.linefeed = '\n'; 22 | var pkg = grunt.file.readJSON('package.json'); 23 | grunt.initConfig({ 24 | modules: [], 25 | pkg: pkg, 26 | dist: 'dist', 27 | filename: 'ui-comments', 28 | meta: { 29 | modules: 'angular.module("ui.comments");', 30 | tplmodules: 'angular.module("ui.comments.tpls", [<%= tplModules %>]);', 31 | all: 'angular.module("ui.comments", ["ui.comments.tpls", <%= srcModules %>]);' 32 | }, 33 | delta: { 34 | html: { 35 | files: ['template/**/*.html'], 36 | tasks: ['html2js', 'karma:watch:run'] 37 | }, 38 | js: { 39 | files: ['src/**/*.js'], 40 | // we don't need to jshint here, it slows down everything else. 41 | tasks: ['karma:watch:run'] 42 | } 43 | }, 44 | concat: { 45 | dist: { 46 | options: { 47 | banner: '<%= meta.all %>\n<%= meta.tplmodules %>\n' 48 | }, 49 | src: [], // src filled in by build task 50 | dest: '<%= dist %>/<%= filename %>-<%= pkg.version %>.js' 51 | } 52 | }, 53 | copy: { 54 | demohtml: { 55 | options: { 56 | // process html files with gruntfile config 57 | processContent: grunt.template.process 58 | }, 59 | files: [{ 60 | expand: true, 61 | src: ["**/*.html"], 62 | cwd: "misc/demo/", 63 | dest: "<%= dist %>/" 64 | }] 65 | }, 66 | demoassets: { 67 | files: [{ 68 | expand: true, 69 | // Don't re-copy html files, we process those 70 | src: ["**/**/*", "!**/*.html"], 71 | cwd: "misc/demo", 72 | dest: "<%= dist %>/" 73 | }] 74 | }, 75 | bower: { 76 | files: [{ 77 | expand: true, 78 | src: ["*.min.js", "*.min.js.map"], 79 | cwd: "bower_components/angular", 80 | dest: "<%= dist %>/js" 81 | }, { 82 | expand: true, 83 | src: ["*.min.js", "*.min.js.map"], 84 | cwd: "bower_components/angular-animate", 85 | dest: "<%= dist %>/js" 86 | }, { 87 | expand: true, 88 | src: ["*.min.js", "*.min.js.map"], 89 | cwd: "bower_components/angular-route", 90 | dest: "<%= dist %>/js" 91 | }, { 92 | expand: true, 93 | src: ["*.min.js", "*.min.js.map"], 94 | cwd: "bower_components/angular-sanitize", 95 | dest: "<%= dist %>/js" 96 | }, { 97 | expand: true, 98 | src: ["*.min.js", "*.min.js.map"], 99 | cwd: "bower_components/bootstrap/dist/js", 100 | dest: "<%= dist %>/js" 101 | }, { 102 | expand: true, 103 | src: ["bootstrap.min.css"], 104 | cwd: "bower_components/bootstrap/dist/css", 105 | dest: "<%= dist %>/css" 106 | }, { 107 | expand: true, 108 | src: ["*"], 109 | cwd: "bower_components/bootstrap/dist/fonts", 110 | dest: "<%= dist %>/fonts" 111 | }, { 112 | expand: true, 113 | src: ["jquery.min.js", "jquery.min.map"], 114 | cwd: "bower_components/jquery/", 115 | dest: "<%= dist %>/js" 116 | }, { 117 | expand: true, 118 | src: ["moment.min.js"], 119 | cwd: "bower_components/momentjs/min", 120 | dest: "<%= dist %>/js" 121 | }, { 122 | expand: true, 123 | src: ["*.min.css"], 124 | cwd: "bower_components/font-awesome/css", 125 | dest: "<%= dist %>/css" 126 | }, { 127 | expand: true, 128 | src: ["*"], 129 | cwd: "bower_components/font-awesome/fonts", 130 | dest: "<%= dist %>/fonts" 131 | }] 132 | } 133 | }, 134 | uglify: { 135 | dist: { 136 | options: { 137 | mangle: { 138 | except: ['angular'] 139 | }, 140 | }, 141 | files: { 142 | '<%= dist %>/<%= filename %>-<%= pkg.version %>.min.js': 143 | '<%= dist %>/<%= filename %>-<%= pkg.version %>.js' 144 | } 145 | } 146 | }, 147 | html2js: { 148 | dist: { 149 | options: { 150 | module: null, // no bundle module for all the html2js templates 151 | base: '.' 152 | }, 153 | files: [{ 154 | expand: true, 155 | src: ['template/**/*.html'], 156 | ext: '.html.js' 157 | }] 158 | }, 159 | }, 160 | jshint: { 161 | options: { 162 | curly: true, 163 | immed: true, 164 | newcap: true, 165 | noarg: true, 166 | sub: true, 167 | boss: true, 168 | eqnull: true, 169 | maxlen: 100, 170 | trailing: true, 171 | undef: true, 172 | }, 173 | gruntfile: { 174 | options: { 175 | node: true, 176 | globals: { 177 | "console": true 178 | } 179 | }, 180 | files: [{ 181 | src: 'Gruntfile.js' 182 | }] 183 | }, 184 | sources: { 185 | options: { 186 | globals: { 187 | "console": true, 188 | angular: true, 189 | jQuery: true, 190 | document: true 191 | } 192 | }, 193 | files: [{ 194 | src: ['src/*.js', '!src/*.spec.js'] 195 | }] 196 | } 197 | }, 198 | karma: { 199 | options: { 200 | configFile: 'karma.conf.js' 201 | }, 202 | watch: { 203 | background: true 204 | }, 205 | continuous: { 206 | singleRun: true 207 | }, 208 | jenkins: { 209 | singleRun: true, 210 | colors: false, 211 | reporter: ['dots', 'junit'], 212 | browsers: [ 213 | 'Chrome', 214 | 'ChromeCanary', 215 | 'Firefox', 216 | 'Opera', 217 | '/Users/jenkins/bin/safari.sh', 218 | '/Users/jenkins/bin/ie9.sh' 219 | ] 220 | }, 221 | travis: { 222 | singleRun: true, 223 | browsers: ['PhantomJS', 'Firefox'] 224 | } 225 | }, 226 | changelog: { 227 | options: { 228 | dest: 'CHANGELOG.md', 229 | templateFile: 'misc/changelog.tpl.md', 230 | github: 'caitp/ui-comments' 231 | } 232 | }, 233 | shell: { 234 | // We use %version% and evaluate it at run-time, because 235 | // <%= pkg.version is only evaluated once 236 | 'release-prepare': [ 237 | 'grunt before-test after-test', 238 | 'grunt clean:dist', 239 | 'grunt version', // remove "-SNAPSHOT" 240 | 'grunt before-test after-test', 241 | 'grunt docgen:%version%', 242 | 'grunt changelog', 243 | ], 244 | 'release-complete': [ 245 | 'git commit CHANGELOG.md package.json -m "chore(release): v%version%"', 246 | 'git tag v%version%', 247 | ], 248 | 'release-start': [ 249 | 'grunt version:%PATCHTYPE%:"SNAPSHOT"', 250 | 'git commit package.json -m "chore(release): Starting v%version%"' 251 | ] 252 | }, 253 | ngmin: { 254 | lib: { 255 | src: ['<%= dist %>/ui-comments-<%= pkg.version %>.js'], 256 | dest: '<%= dist %>/ui-comments-<%= pkg.version %>.js' 257 | } 258 | }, 259 | ngdocs: { 260 | options: { 261 | dest: "<%= dist %>/docs", 262 | scripts: [ 263 | 'angular.js', 264 | '<%= concat.dist.dest %>' 265 | ], 266 | styles: [ 267 | 'docs/css/style.css' 268 | ], 269 | navTemplate: 'docs/nav.html', 270 | title: 'UI Comments', 271 | image: 'logo.png', 272 | imageLink: 'http://caitp.github.io/ui-comments', 273 | titleLink: 'http://caitp.github.io/ui-comments', 274 | html5Mode: false, 275 | analytics: { 276 | account: 'UA-44389518-1', 277 | domainName: 'caitp.github.io' 278 | } 279 | }, 280 | api: { 281 | src: ["src/comments.js", "src/**/*.ngdoc"], 282 | title: "API Documentation" 283 | } 284 | }, 285 | 'gh-pages': { 286 | 'gh-pages': { 287 | options: { 288 | base: '<%= dist %>', 289 | repo: 'https://github.com/caitp/ui-comments.git', 290 | message: 'gh-pages v<%= pkg.version %>', 291 | add: true 292 | }, 293 | src: ['**/*'] 294 | } 295 | }, 296 | connect: { 297 | docs: { 298 | options: { 299 | port: process.env.PORT || '3000', 300 | base: '<%= dist %>/docs', 301 | keepalive: true 302 | } 303 | }, 304 | dev: { 305 | options: { 306 | port: process.env.PORT || '3000', 307 | base: '<%= dist %>', 308 | keepalive: true 309 | } 310 | } 311 | }, 312 | clean: { 313 | dist: { 314 | src: ['<%= dist %>', 'dist'] 315 | } 316 | } 317 | }); 318 | 319 | // register before and after test tasks so we don't have to change cli 320 | // options on the CI server 321 | grunt.registerTask('before-test', ['enforce', 'jshint', 'html2js']); 322 | grunt.registerTask('after-test', ['build', 'copy']); 323 | 324 | // Rename our watch task to 'delta', then make actual 'watch' task build 325 | // things, then start test server 326 | grunt.renameTask('watch', 'delta'); 327 | grunt.registerTask('watch', ['bower', 'before-test', 'after-test', 'karma:watch', 'delta']); 328 | 329 | // Default task. 330 | grunt.registerTask('default', ['bower', 'before-test', 'test', 'after-test']); 331 | grunt.registerTask('all', ['default']); 332 | 333 | grunt.registerTask('enforce', 'Install commit message enforce script if it doesn\'t exist', 334 | function() { 335 | if (!grunt.file.exists('.git/hooks/commit-msg')) { 336 | grunt.file.copy('misc/validate-commit-msg.js', '.git/hooks/commit-msg'); 337 | require('fs').chmodSync('.git/hooks/commit-msg', '0755'); 338 | } 339 | }); 340 | 341 | // Test 342 | grunt.registerTask('test', 'Run tests on singleRun karma server', function() { 343 | // This task can be executed in 3 different environments: local, Travis-CI, 344 | // and Jenkins-CI. We need to take settings for each one into account 345 | if (process.env.TRAVIS) { 346 | grunt.task.run('karma:travis'); 347 | } else { 348 | grunt.task.run(this.args.length ? 'karma:jenkins' : 'karma:continuous'); 349 | } 350 | }); 351 | 352 | // Shell commands 353 | grunt.registerMultiTask('shell', 'Run shell commands', function() { 354 | var self = this, sh = require('shelljs'); 355 | self.data.forEach(function(cmd) { 356 | cmd = cmd.replace('%version%', grunt.file.readJSON('package.json').version); 357 | cmd = cmd.replace('%PATCHTYPE%', grunt.option('patch') && 'patch' || 358 | grunt.option('major') && 359 | 'major' || 'minor'); 360 | grunt.log.ok(cmd); 361 | var result = sh.exec(cmd, {silent: true }); 362 | if (result.code !== 0) { 363 | grunt.fatal(result.output); 364 | } 365 | }); 366 | }); 367 | 368 | // Version management 369 | function setVersion(type, suffix) { 370 | var file = 'package.json', 371 | VERSION_REGEX = /([\'|\"]version[\'|\"][ ]*:[ ]*[\'|\"])([\d|.]*)(-\w+)*([\'|\"])/, 372 | contents = grunt.file.read(file), 373 | version; 374 | contents = contents.replace(VERSION_REGEX, function(match, left, center) { 375 | version = center; 376 | if (type) { 377 | version = require('semver').inc(version, type); 378 | } 379 | // semver.inc strips suffix, if it existed 380 | if (suffix) { 381 | version += '-' + suffix; 382 | } 383 | return left + version + '"'; 384 | }); 385 | grunt.log.ok('Version set to ' + version.cyan); 386 | grunt.file.write(file, contents); 387 | return version; 388 | } 389 | 390 | grunt.registerTask('version', 'Set version. If no arguments, it just takes off suffix', 391 | function() { 392 | setVersion(this.args[0], this.args[1]); 393 | }); 394 | 395 | // Dist 396 | grunt.registerTask('dist', 'Override dist directory', function() { 397 | var dir = this.args[0]; 398 | if (dir) { 399 | grunt.config('dist', dir ); 400 | } 401 | }); 402 | 403 | var foundModules = {}; 404 | function findModule(name) { 405 | if (foundModules[name]) { return; } 406 | foundModules[name] = true; 407 | 408 | function breakup(text, separator) { 409 | return text.replace(/[A-Z]/g, function (match) { 410 | return separator + match; 411 | }); 412 | } 413 | function ucwords(text) { 414 | return text.replace(/^([a-z])|\s+([a-z])/g, function ($1) { 415 | return $1.toUpperCase(); 416 | }); 417 | } 418 | function enquote(str) { 419 | return '"' + str + '"'; 420 | } 421 | 422 | var modname = name; 423 | if (name === 'comments') { 424 | modname = 'comments.directive'; 425 | } 426 | var module = { 427 | name: name, 428 | moduleName: enquote('ui.' + modname), 429 | displayName: ucwords(breakup(name, ' ')), 430 | srcFiles: grunt.file.expand({ignore: "*.spec.js"}, "src/"+name+".js"), 431 | tplFiles: grunt.file.expand("template/"+name+"/*.html"), 432 | tpljsFiles: grunt.file.expand("template/"+name+"/*.html.js"), 433 | tplModules: grunt.file.expand("template/"+name+"/*.html").map(enquote), 434 | dependencies: dependenciesForModule(name), 435 | docs: { 436 | md: grunt.file.expand("src/"+name+"/docs/*.md") 437 | .map(grunt.file.read).map(markdown).join("\n"), 438 | js: grunt.file.expand("src/"+name+"/docs/*.js") 439 | .map(grunt.file.read).join("\n"), 440 | html: grunt.file.expand("src/"+name+"/docs/*.html") 441 | .map(grunt.file.read).join("\n") 442 | } 443 | }; 444 | module.dependencies.forEach(findModule); 445 | grunt.config('modules', grunt.config('modules').concat(module)); 446 | } 447 | 448 | function dependenciesForModule(name) { 449 | var deps = []; 450 | grunt.file.expand('src/*.js') 451 | .map(grunt.file.read) 452 | .forEach(function(contents) { 453 | //Strategy: find where module is declared, 454 | //and from there get everything inside the [] and split them by comma 455 | var moduleDeclIndex = contents.indexOf('angular.module('); 456 | var depArrayStart = contents.indexOf('[', moduleDeclIndex); 457 | var depArrayEnd = contents.indexOf(']', depArrayStart); 458 | var dependencies = contents.substring(depArrayStart + 1, depArrayEnd); 459 | dependencies.split(',').forEach(function(dep) { 460 | if (dep.indexOf('ui.comments.') > -1) { 461 | var depName = dep.trim().replace('ui.comments.','').replace(/['"]/g,''); 462 | if (deps.indexOf(depName) < 0) { 463 | deps.push(depName); 464 | //Get dependencies for this new dependency 465 | deps = deps.concat(dependenciesForModule(depName)); 466 | } 467 | } 468 | }); 469 | }); 470 | return deps; 471 | } 472 | 473 | // Build 474 | grunt.registerTask('build', 'Create ui-comments build files', function() { 475 | var _ = grunt.util._; 476 | 477 | grunt.file.expand({ 478 | filter: 'isFile', cwd: '.' 479 | }, 'src/*.js').forEach(function(file) { 480 | findModule(file.split('/')[1].split('.')[0]); 481 | }); 482 | 483 | var modules = grunt.config('modules'); 484 | grunt.config('srcModules', _.pluck(modules, 'moduleName')); 485 | grunt.config('tplModules', _.pluck(modules, 'tplModules') 486 | .filter(function(tpls) { 487 | return tpls.length > 0; 488 | }) 489 | ); 490 | grunt.config('demoModules', 491 | modules.filter(function(module) { 492 | return module.docs.md && module.docs.js && module.docs.html; 493 | }) 494 | .sort(function(a, b) { 495 | if (a.name < b.name) { return -1; } 496 | if (a.name > b.name) { return 1; } 497 | return 0; 498 | })); 499 | 500 | var srcFiles = _.pluck(modules, 'srcFiles'); 501 | var tpljsFiles = _.pluck(modules, 'tpljsFiles'); 502 | 503 | grunt.config('concat.dist.src', 504 | grunt.config('concat.dist.src') 505 | .concat(srcFiles) 506 | .concat(tpljsFiles)); 507 | 508 | grunt.task.run(['concat', 'ngmin', 'uglify', 'ngdocs']); 509 | }); 510 | 511 | grunt.registerTask('docgen', function() { 512 | var self = this; 513 | if (typeof self.args[0] === 'string') { 514 | grunt.config('pkg.version', self.args[0]); 515 | } 516 | grunt.task.mark().run('gh-pages'); 517 | }); 518 | 519 | grunt.registerTask('bower', 'Install Bower packages.', function() { 520 | var done = this.async(); 521 | bower.commands.install() 522 | .on('log', function (result) { 523 | grunt.log.ok('bower: ' + result.id + ' ' + result.data.endpoint.name); 524 | }) 525 | .on('error', grunt.fail.warn.bind(grunt.fail)) 526 | .on('end', done); 527 | }); 528 | 529 | return grunt; 530 | }; 531 | -------------------------------------------------------------------------------- /misc/test/angular-mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.28 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 8 | 'use strict'; 9 | 10 | /** 11 | * @ngdoc object 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 $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.$$checkUrlChange = angular.noop; 67 | 68 | self.cookieHash = {}; 69 | self.lastCookieHash = {}; 70 | self.deferredFns = []; 71 | self.deferredNextId = 0; 72 | 73 | self.defer = function(fn, delay) { 74 | delay = delay || 0; 75 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 76 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 77 | return self.deferredNextId++; 78 | }; 79 | 80 | 81 | /** 82 | * @name $browser#defer.now 83 | * 84 | * @description 85 | * Current milliseconds mock time. 86 | */ 87 | self.defer.now = 0; 88 | 89 | 90 | self.defer.cancel = function(deferId) { 91 | var fnIndex; 92 | 93 | angular.forEach(self.deferredFns, function(fn, index) { 94 | if (fn.id === deferId) fnIndex = index; 95 | }); 96 | 97 | if (fnIndex !== undefined) { 98 | self.deferredFns.splice(fnIndex, 1); 99 | return true; 100 | } 101 | 102 | return false; 103 | }; 104 | 105 | 106 | /** 107 | * @name $browser#defer.flush 108 | * 109 | * @description 110 | * Flushes all pending requests and executes the defer callbacks. 111 | * 112 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 113 | */ 114 | self.defer.flush = function(delay) { 115 | if (angular.isDefined(delay)) { 116 | self.defer.now += delay; 117 | } else { 118 | if (self.deferredFns.length) { 119 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 120 | } else { 121 | throw new Error('No deferred tasks to be flushed'); 122 | } 123 | } 124 | 125 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 126 | self.deferredFns.shift().fn(); 127 | } 128 | }; 129 | 130 | self.$$baseHref = ''; 131 | self.baseHref = function() { 132 | return this.$$baseHref; 133 | }; 134 | }; 135 | angular.mock.$Browser.prototype = { 136 | 137 | /** 138 | * @name $browser#poll 139 | * 140 | * @description 141 | * run all fns in pollFns 142 | */ 143 | poll: function poll() { 144 | angular.forEach(this.pollFns, function(pollFn){ 145 | pollFn(); 146 | }); 147 | }, 148 | 149 | addPollFn: function(pollFn) { 150 | this.pollFns.push(pollFn); 151 | return pollFn; 152 | }, 153 | 154 | url: function(url, replace) { 155 | if (url) { 156 | this.$$url = url; 157 | return this; 158 | } 159 | 160 | return this.$$url; 161 | }, 162 | 163 | cookies: function(name, value) { 164 | if (name) { 165 | if (angular.isUndefined(value)) { 166 | delete this.cookieHash[name]; 167 | } else { 168 | if (angular.isString(value) && //strings only 169 | value.length <= 4096) { //strict cookie storage limits 170 | this.cookieHash[name] = value; 171 | } 172 | } 173 | } else { 174 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 175 | this.lastCookieHash = angular.copy(this.cookieHash); 176 | this.cookieHash = angular.copy(this.cookieHash); 177 | } 178 | return this.cookieHash; 179 | } 180 | }, 181 | 182 | notifyWhenNoOutstandingRequests: function(fn) { 183 | fn(); 184 | } 185 | }; 186 | 187 | 188 | /** 189 | * @ngdoc provider 190 | * @name $exceptionHandlerProvider 191 | * 192 | * @description 193 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors 194 | * passed into the `$exceptionHandler`. 195 | */ 196 | 197 | /** 198 | * @ngdoc service 199 | * @name $exceptionHandler 200 | * 201 | * @description 202 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 203 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 204 | * information. 205 | * 206 | * 207 | * ```js 208 | * describe('$exceptionHandlerProvider', function() { 209 | * 210 | * it('should capture log messages and exceptions', function() { 211 | * 212 | * module(function($exceptionHandlerProvider) { 213 | * $exceptionHandlerProvider.mode('log'); 214 | * }); 215 | * 216 | * inject(function($log, $exceptionHandler, $timeout) { 217 | * $timeout(function() { $log.log(1); }); 218 | * $timeout(function() { $log.log(2); throw 'banana peel'; }); 219 | * $timeout(function() { $log.log(3); }); 220 | * expect($exceptionHandler.errors).toEqual([]); 221 | * expect($log.assertEmpty()); 222 | * $timeout.flush(); 223 | * expect($exceptionHandler.errors).toEqual(['banana peel']); 224 | * expect($log.log.logs).toEqual([[1], [2], [3]]); 225 | * }); 226 | * }); 227 | * }); 228 | * ``` 229 | */ 230 | 231 | angular.mock.$ExceptionHandlerProvider = function() { 232 | var handler; 233 | 234 | /** 235 | * @ngdoc method 236 | * @name $exceptionHandlerProvider#mode 237 | * 238 | * @description 239 | * Sets the logging mode. 240 | * 241 | * @param {string} mode Mode of operation, defaults to `rethrow`. 242 | * 243 | * - `rethrow`: If any errors are passed into the handler in tests, it typically 244 | * means that there is a bug in the application or test, so this mock will 245 | * make these tests fail. 246 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` 247 | * mode stores an array of errors in `$exceptionHandler.errors`, to allow later 248 | * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and 249 | * {@link ngMock.$log#reset reset()} 250 | */ 251 | this.mode = function(mode) { 252 | switch(mode) { 253 | case 'rethrow': 254 | handler = function(e) { 255 | throw e; 256 | }; 257 | break; 258 | case 'log': 259 | var errors = []; 260 | 261 | handler = function(e) { 262 | if (arguments.length == 1) { 263 | errors.push(e); 264 | } else { 265 | errors.push([].slice.call(arguments, 0)); 266 | } 267 | }; 268 | 269 | handler.errors = errors; 270 | break; 271 | default: 272 | throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 273 | } 274 | }; 275 | 276 | this.$get = function() { 277 | return handler; 278 | }; 279 | 280 | this.mode('rethrow'); 281 | }; 282 | 283 | 284 | /** 285 | * @ngdoc service 286 | * @name $log 287 | * 288 | * @description 289 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 290 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 291 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 292 | * 293 | */ 294 | angular.mock.$LogProvider = function() { 295 | var debug = true; 296 | 297 | function concat(array1, array2, index) { 298 | return array1.concat(Array.prototype.slice.call(array2, index)); 299 | } 300 | 301 | this.debugEnabled = function(flag) { 302 | if (angular.isDefined(flag)) { 303 | debug = flag; 304 | return this; 305 | } else { 306 | return debug; 307 | } 308 | }; 309 | 310 | this.$get = function () { 311 | var $log = { 312 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 313 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 314 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 315 | error: function() { $log.error.logs.push(concat([], arguments, 0)); }, 316 | debug: function() { 317 | if (debug) { 318 | $log.debug.logs.push(concat([], arguments, 0)); 319 | } 320 | } 321 | }; 322 | 323 | /** 324 | * @ngdoc method 325 | * @name $log#reset 326 | * 327 | * @description 328 | * Reset all of the logging arrays to empty. 329 | */ 330 | $log.reset = function () { 331 | /** 332 | * @ngdoc property 333 | * @name $log#log.logs 334 | * 335 | * @description 336 | * Array of messages logged using {@link ngMock.$log#log}. 337 | * 338 | * @example 339 | * ```js 340 | * $log.log('Some Log'); 341 | * var first = $log.log.logs.unshift(); 342 | * ``` 343 | */ 344 | $log.log.logs = []; 345 | /** 346 | * @ngdoc property 347 | * @name $log#info.logs 348 | * 349 | * @description 350 | * Array of messages logged using {@link ngMock.$log#info}. 351 | * 352 | * @example 353 | * ```js 354 | * $log.info('Some Info'); 355 | * var first = $log.info.logs.unshift(); 356 | * ``` 357 | */ 358 | $log.info.logs = []; 359 | /** 360 | * @ngdoc property 361 | * @name $log#warn.logs 362 | * 363 | * @description 364 | * Array of messages logged using {@link ngMock.$log#warn}. 365 | * 366 | * @example 367 | * ```js 368 | * $log.warn('Some Warning'); 369 | * var first = $log.warn.logs.unshift(); 370 | * ``` 371 | */ 372 | $log.warn.logs = []; 373 | /** 374 | * @ngdoc property 375 | * @name $log#error.logs 376 | * 377 | * @description 378 | * Array of messages logged using {@link ngMock.$log#error}. 379 | * 380 | * @example 381 | * ```js 382 | * $log.error('Some Error'); 383 | * var first = $log.error.logs.unshift(); 384 | * ``` 385 | */ 386 | $log.error.logs = []; 387 | /** 388 | * @ngdoc property 389 | * @name $log#debug.logs 390 | * 391 | * @description 392 | * Array of messages logged using {@link ngMock.$log#debug}. 393 | * 394 | * @example 395 | * ```js 396 | * $log.debug('Some Error'); 397 | * var first = $log.debug.logs.unshift(); 398 | * ``` 399 | */ 400 | $log.debug.logs = []; 401 | }; 402 | 403 | /** 404 | * @ngdoc method 405 | * @name $log#assertEmpty 406 | * 407 | * @description 408 | * Assert that the all of the logging methods have no logged messages. If messages present, an 409 | * exception is thrown. 410 | */ 411 | $log.assertEmpty = function() { 412 | var errors = []; 413 | angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { 414 | angular.forEach($log[logLevel].logs, function(log) { 415 | angular.forEach(log, function (logItem) { 416 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + 417 | (logItem.stack || '')); 418 | }); 419 | }); 420 | }); 421 | if (errors.length) { 422 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ 423 | "an expected log message was not checked and removed:"); 424 | errors.push(''); 425 | throw new Error(errors.join('\n---------\n')); 426 | } 427 | }; 428 | 429 | $log.reset(); 430 | return $log; 431 | }; 432 | }; 433 | 434 | 435 | /** 436 | * @ngdoc service 437 | * @name $interval 438 | * 439 | * @description 440 | * Mock implementation of the $interval service. 441 | * 442 | * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to 443 | * move forward by `millis` milliseconds and trigger any functions scheduled to run in that 444 | * time. 445 | * 446 | * @param {function()} fn A function that should be called repeatedly. 447 | * @param {number} delay Number of milliseconds between each function call. 448 | * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat 449 | * indefinitely. 450 | * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise 451 | * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. 452 | * @returns {promise} A promise which will be notified on each iteration. 453 | */ 454 | angular.mock.$IntervalProvider = function() { 455 | this.$get = ['$rootScope', '$q', 456 | function($rootScope, $q) { 457 | var repeatFns = [], 458 | nextRepeatId = 0, 459 | now = 0; 460 | 461 | var $interval = function(fn, delay, count, invokeApply) { 462 | var deferred = $q.defer(), 463 | promise = deferred.promise, 464 | iteration = 0, 465 | skipApply = (angular.isDefined(invokeApply) && !invokeApply); 466 | 467 | count = (angular.isDefined(count)) ? count : 0; 468 | promise.then(null, null, fn); 469 | 470 | promise.$$intervalId = nextRepeatId; 471 | 472 | function tick() { 473 | deferred.notify(iteration++); 474 | 475 | if (count > 0 && iteration >= count) { 476 | var fnIndex; 477 | deferred.resolve(iteration); 478 | 479 | angular.forEach(repeatFns, function(fn, index) { 480 | if (fn.id === promise.$$intervalId) fnIndex = index; 481 | }); 482 | 483 | if (fnIndex !== undefined) { 484 | repeatFns.splice(fnIndex, 1); 485 | } 486 | } 487 | 488 | if (!skipApply) $rootScope.$apply(); 489 | } 490 | 491 | repeatFns.push({ 492 | nextTime:(now + delay), 493 | delay: delay, 494 | fn: tick, 495 | id: nextRepeatId, 496 | deferred: deferred 497 | }); 498 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 499 | 500 | nextRepeatId++; 501 | return promise; 502 | }; 503 | /** 504 | * @ngdoc method 505 | * @name $interval#cancel 506 | * 507 | * @description 508 | * Cancels a task associated with the `promise`. 509 | * 510 | * @param {promise} promise A promise from calling the `$interval` function. 511 | * @returns {boolean} Returns `true` if the task was successfully cancelled. 512 | */ 513 | $interval.cancel = function(promise) { 514 | if(!promise) return false; 515 | var fnIndex; 516 | 517 | angular.forEach(repeatFns, function(fn, index) { 518 | if (fn.id === promise.$$intervalId) fnIndex = index; 519 | }); 520 | 521 | if (fnIndex !== undefined) { 522 | repeatFns[fnIndex].deferred.reject('canceled'); 523 | repeatFns.splice(fnIndex, 1); 524 | return true; 525 | } 526 | 527 | return false; 528 | }; 529 | 530 | /** 531 | * @ngdoc method 532 | * @name $interval#flush 533 | * @description 534 | * 535 | * Runs interval tasks scheduled to be run in the next `millis` milliseconds. 536 | * 537 | * @param {number=} millis maximum timeout amount to flush up until. 538 | * 539 | * @return {number} The amount of time moved forward. 540 | */ 541 | $interval.flush = function(millis) { 542 | now += millis; 543 | while (repeatFns.length && repeatFns[0].nextTime <= now) { 544 | var task = repeatFns[0]; 545 | task.fn(); 546 | task.nextTime += task.delay; 547 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 548 | } 549 | return millis; 550 | }; 551 | 552 | return $interval; 553 | }]; 554 | }; 555 | 556 | 557 | /* jshint -W101 */ 558 | /* The R_ISO8061_STR regex is never going to fit into the 100 char limit! 559 | * This directive should go inside the anonymous function but a bug in JSHint means that it would 560 | * not be enacted early enough to prevent the warning. 561 | */ 562 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 563 | 564 | function jsonStringToDate(string) { 565 | var match; 566 | if (match = string.match(R_ISO8061_STR)) { 567 | var date = new Date(0), 568 | tzHour = 0, 569 | tzMin = 0; 570 | if (match[9]) { 571 | tzHour = int(match[9] + match[10]); 572 | tzMin = int(match[9] + match[11]); 573 | } 574 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 575 | date.setUTCHours(int(match[4]||0) - tzHour, 576 | int(match[5]||0) - tzMin, 577 | int(match[6]||0), 578 | int(match[7]||0)); 579 | return date; 580 | } 581 | return string; 582 | } 583 | 584 | function int(str) { 585 | return parseInt(str, 10); 586 | } 587 | 588 | function padNumber(num, digits, trim) { 589 | var neg = ''; 590 | if (num < 0) { 591 | neg = '-'; 592 | num = -num; 593 | } 594 | num = '' + num; 595 | while(num.length < digits) num = '0' + num; 596 | if (trim) 597 | num = num.substr(num.length - digits); 598 | return neg + num; 599 | } 600 | 601 | 602 | /** 603 | * @ngdoc type 604 | * @name angular.mock.TzDate 605 | * @description 606 | * 607 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 608 | * 609 | * Mock of the Date type which has its timezone specified via constructor arg. 610 | * 611 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 612 | * offset, so that we can test code that depends on local timezone settings without dependency on 613 | * the time zone settings of the machine where the code is running. 614 | * 615 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 616 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 617 | * 618 | * @example 619 | * !!!! WARNING !!!!! 620 | * This is not a complete Date object so only methods that were implemented can be called safely. 621 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 622 | * 623 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 624 | * incomplete we might be missing some non-standard methods. This can result in errors like: 625 | * "Date.prototype.foo called on incompatible Object". 626 | * 627 | * ```js 628 | * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); 629 | * newYearInBratislava.getTimezoneOffset() => -60; 630 | * newYearInBratislava.getFullYear() => 2010; 631 | * newYearInBratislava.getMonth() => 0; 632 | * newYearInBratislava.getDate() => 1; 633 | * newYearInBratislava.getHours() => 0; 634 | * newYearInBratislava.getMinutes() => 0; 635 | * newYearInBratislava.getSeconds() => 0; 636 | * ``` 637 | * 638 | */ 639 | angular.mock.TzDate = function (offset, timestamp) { 640 | var self = new Date(0); 641 | if (angular.isString(timestamp)) { 642 | var tsStr = timestamp; 643 | 644 | self.origDate = jsonStringToDate(timestamp); 645 | 646 | timestamp = self.origDate.getTime(); 647 | if (isNaN(timestamp)) 648 | throw { 649 | name: "Illegal Argument", 650 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 651 | }; 652 | } else { 653 | self.origDate = new Date(timestamp); 654 | } 655 | 656 | var localOffset = new Date(timestamp).getTimezoneOffset(); 657 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 658 | self.date = new Date(timestamp + self.offsetDiff); 659 | 660 | self.getTime = function() { 661 | return self.date.getTime() - self.offsetDiff; 662 | }; 663 | 664 | self.toLocaleDateString = function() { 665 | return self.date.toLocaleDateString(); 666 | }; 667 | 668 | self.getFullYear = function() { 669 | return self.date.getFullYear(); 670 | }; 671 | 672 | self.getMonth = function() { 673 | return self.date.getMonth(); 674 | }; 675 | 676 | self.getDate = function() { 677 | return self.date.getDate(); 678 | }; 679 | 680 | self.getHours = function() { 681 | return self.date.getHours(); 682 | }; 683 | 684 | self.getMinutes = function() { 685 | return self.date.getMinutes(); 686 | }; 687 | 688 | self.getSeconds = function() { 689 | return self.date.getSeconds(); 690 | }; 691 | 692 | self.getMilliseconds = function() { 693 | return self.date.getMilliseconds(); 694 | }; 695 | 696 | self.getTimezoneOffset = function() { 697 | return offset * 60; 698 | }; 699 | 700 | self.getUTCFullYear = function() { 701 | return self.origDate.getUTCFullYear(); 702 | }; 703 | 704 | self.getUTCMonth = function() { 705 | return self.origDate.getUTCMonth(); 706 | }; 707 | 708 | self.getUTCDate = function() { 709 | return self.origDate.getUTCDate(); 710 | }; 711 | 712 | self.getUTCHours = function() { 713 | return self.origDate.getUTCHours(); 714 | }; 715 | 716 | self.getUTCMinutes = function() { 717 | return self.origDate.getUTCMinutes(); 718 | }; 719 | 720 | self.getUTCSeconds = function() { 721 | return self.origDate.getUTCSeconds(); 722 | }; 723 | 724 | self.getUTCMilliseconds = function() { 725 | return self.origDate.getUTCMilliseconds(); 726 | }; 727 | 728 | self.getDay = function() { 729 | return self.date.getDay(); 730 | }; 731 | 732 | // provide this method only on browsers that already have it 733 | if (self.toISOString) { 734 | self.toISOString = function() { 735 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 736 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 737 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 738 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 739 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 740 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 741 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; 742 | }; 743 | } 744 | 745 | //hide all methods not implemented in this mock that the Date prototype exposes 746 | var unimplementedMethods = ['getUTCDay', 747 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 748 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 749 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 750 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 751 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 752 | 753 | angular.forEach(unimplementedMethods, function(methodName) { 754 | self[methodName] = function() { 755 | throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 756 | }; 757 | }); 758 | 759 | return self; 760 | }; 761 | 762 | //make "tzDateInstance instanceof Date" return true 763 | angular.mock.TzDate.prototype = Date.prototype; 764 | /* jshint +W101 */ 765 | 766 | angular.mock.animate = angular.module('ngAnimateMock', ['ng']) 767 | 768 | .config(['$provide', function($provide) { 769 | 770 | var reflowQueue = []; 771 | $provide.value('$$animateReflow', function(fn) { 772 | var index = reflowQueue.length; 773 | reflowQueue.push(fn); 774 | return function cancel() { 775 | reflowQueue.splice(index, 1); 776 | }; 777 | }); 778 | 779 | $provide.decorator('$animate', function($delegate, $$asyncCallback) { 780 | var animate = { 781 | queue : [], 782 | enabled : $delegate.enabled, 783 | triggerCallbacks : function() { 784 | $$asyncCallback.flush(); 785 | }, 786 | triggerReflow : function() { 787 | angular.forEach(reflowQueue, function(fn) { 788 | fn(); 789 | }); 790 | reflowQueue = []; 791 | } 792 | }; 793 | 794 | angular.forEach( 795 | ['enter','leave','move','addClass','removeClass','setClass'], function(method) { 796 | animate[method] = function() { 797 | animate.queue.push({ 798 | event : method, 799 | element : arguments[0], 800 | args : arguments 801 | }); 802 | $delegate[method].apply($delegate, arguments); 803 | }; 804 | }); 805 | 806 | return animate; 807 | }); 808 | 809 | }]); 810 | 811 | 812 | /** 813 | * @ngdoc function 814 | * @name angular.mock.dump 815 | * @description 816 | * 817 | * *NOTE*: this is not an injectable instance, just a globally available function. 818 | * 819 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for 820 | * debugging. 821 | * 822 | * This method is also available on window, where it can be used to display objects on debug 823 | * console. 824 | * 825 | * @param {*} object - any object to turn into string. 826 | * @return {string} a serialized string of the argument 827 | */ 828 | angular.mock.dump = function(object) { 829 | return serialize(object); 830 | 831 | function serialize(object) { 832 | var out; 833 | 834 | if (angular.isElement(object)) { 835 | object = angular.element(object); 836 | out = angular.element(''); 837 | angular.forEach(object, function(element) { 838 | out.append(angular.element(element).clone()); 839 | }); 840 | out = out.html(); 841 | } else if (angular.isArray(object)) { 842 | out = []; 843 | angular.forEach(object, function(o) { 844 | out.push(serialize(o)); 845 | }); 846 | out = '[ ' + out.join(', ') + ' ]'; 847 | } else if (angular.isObject(object)) { 848 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 849 | out = serializeScope(object); 850 | } else if (object instanceof Error) { 851 | out = object.stack || ('' + object.name + ': ' + object.message); 852 | } else { 853 | // TODO(i): this prevents methods being logged, 854 | // we should have a better way to serialize objects 855 | out = angular.toJson(object, true); 856 | } 857 | } else { 858 | out = String(object); 859 | } 860 | 861 | return out; 862 | } 863 | 864 | function serializeScope(scope, offset) { 865 | offset = offset || ' '; 866 | var log = [offset + 'Scope(' + scope.$id + '): {']; 867 | for ( var key in scope ) { 868 | if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { 869 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 870 | } 871 | } 872 | var child = scope.$$childHead; 873 | while(child) { 874 | log.push(serializeScope(child, offset + ' ')); 875 | child = child.$$nextSibling; 876 | } 877 | log.push('}'); 878 | return log.join('\n' + offset); 879 | } 880 | }; 881 | 882 | /** 883 | * @ngdoc service 884 | * @name $httpBackend 885 | * @description 886 | * Fake HTTP backend implementation suitable for unit testing applications that use the 887 | * {@link ng.$http $http service}. 888 | * 889 | * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less 890 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 891 | * 892 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 893 | * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or 894 | * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is 895 | * to verify whether a certain request has been sent or not, or alternatively just let the 896 | * application make requests, respond with pre-trained responses and assert that the end result is 897 | * what we expect it to be. 898 | * 899 | * This mock implementation can be used to respond with static or dynamic responses via the 900 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 901 | * 902 | * When an Angular application needs some data from a server, it calls the $http service, which 903 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 904 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 905 | * the requests and respond with some testing data without sending a request to a real server. 906 | * 907 | * There are two ways to specify what test data should be returned as http responses by the mock 908 | * backend when the code under test makes http requests: 909 | * 910 | * - `$httpBackend.expect` - specifies a request expectation 911 | * - `$httpBackend.when` - specifies a backend definition 912 | * 913 | * 914 | * # Request Expectations vs Backend Definitions 915 | * 916 | * Request expectations provide a way to make assertions about requests made by the application and 917 | * to define responses for those requests. The test will fail if the expected requests are not made 918 | * or they are made in the wrong order. 919 | * 920 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 921 | * if a particular request was made or not, it just returns a trained response if a request is made. 922 | * The test will pass whether or not the request gets made during testing. 923 | * 924 | * 925 | *423 | * 434 | * 435 | * 436 | *437 | *
| Request expectations | Backend definitions | |
|---|---|---|
| Syntax | 929 | *.expect(...).respond(...) | 930 | *.when(...).respond(...) | 931 | *
| Typical usage | 934 | *strict unit tests | 935 | *loose (black-box) unit testing | 936 | *
| Fulfills multiple requests | 939 | *NO | 940 | *YES | 941 | *
| Order of requests matters | 944 | *YES | 945 | *NO | 946 | *
| Request required | 949 | *YES | 950 | *NO | 951 | *
| Response required | 954 | *optional (see below) | 955 | *YES | 956 | *