67 |
68 | * Without browser-sync
69 | * run `gulp serve-specs --nosync`
70 | * open a browser to `localhost:7202/specs.html`
71 | * browsing to `localhost:7202` runs the app
72 |
73 | * To stop, either "Ctrl-C" and answer the prompt with "Y" or just close the window.
74 |
75 | ### Testing with karma
76 | * Open a command or terminal window.
77 |
78 | * To just the unit tests, type `gulp autotest`
79 |
80 | * To run both unit and midway tests (spins up a dev server), type `gulp autotest --startServers`
81 |
82 | Testing uses karma, mocha, chai, sinon, ngMidwayTester libraries.
83 |
84 | >"autotest" starts the tests and stays alive, watching for file changes. Type "test" instead if you only want to run the tests once and then exit.
85 |
86 | * To stop, either "Ctrl-C" and answer the prompt with "Y" or just close the window.
87 |
88 | ## Running the app
89 | Runs locally, no database required.
90 |
91 | ### Dev Builds
92 | The dev build does not optimize the deployed code. It simply runs it in place. You can run a dev build in multiple ways.
93 |
94 | ####Option 1 - Serve
95 | Type `gulp serve-dev` and browse to `http://localhost:7202`
96 |
97 | ####Option 2 - Serve and Debug Node
98 | Type `gulp serve-dev-debug` and browse to `http://localhost:7202` for the client and `http://localhost:8080/debug?port-5858` to debug the server.
99 |
100 | ####Option 3 - Serve and Debug Node Breaking on 1st Line
101 | Type `gulp serve-dev-debug-brk` and browse to `http://localhost:7202` for the client and `http://localhost:8080/debug?port-5858` to debug the server.
102 |
103 | ### Staging Build
104 | The staging build is an optimized build. Type `gulp serve-stage` and browse to `http://localhost:7202`
105 |
106 | The optimizations are performed by the gulp tasks and include the following list. See the `gulpfile.js` for details
107 |
108 | - jshint
109 | - preparing Angular's templatecache for html templates
110 | - concat task to bundle css and js, separately
111 | - Angular dependency injection annotations using ngAnnotate
112 | - uglify to minify and mangle javascript
113 | - source maps
114 | - css autoprefixer for vendor prefixes
115 | - minify css
116 | - optimize images
117 | - index.html injection for scripts and links
118 | - deploying all js, css, images, fonts, and index.html
119 |
120 | ## How The App Works
121 | The app is quite simple and has 2 main routes:
122 | - dashboard
123 | - avengers list
124 |
125 | ### The Modules
126 | The app has 4 feature modules and depends on a series of external modules and custom but cross-app modules
127 |
128 | app --> [
129 | app.avengers,
130 | app.dashboard,
131 | app.layout,
132 | app.widgets,
133 | app.core --> [
134 | ngAnimate,
135 | ngRoute,
136 | ngSanitize,
137 | blocks.exception,
138 | blocks.logger,
139 | blocks.router
140 | ]
141 | ]
142 |
143 |
144 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AngularJS Patterns Testing Demo",
3 | "version": "0.0.1",
4 | "description": "AngularJS Patterns Testing Demo",
5 | "authors": [
6 | "John Papa",
7 | "Ward Bell"
8 | ],
9 | "license": "MIT",
10 | "homepage": "https://github.com/johnpapa/ng-patterns-testing",
11 | "ignore": [
12 | "**/.*",
13 | "node_modules",
14 | "bower_components",
15 | "test",
16 | "tests"
17 | ],
18 | "devDependencies": {
19 | "angular-mocks": "~1.3.8",
20 | "bardjs": "~0.1.1",
21 | "sinon": "http://sinonjs.org/releases/sinon-1.12.1.js"
22 | },
23 | "dependencies": {
24 | "angular": "~1.3.8",
25 | "angular-route": "~1.3.8",
26 | "angular-animate": "~1.3.8",
27 | "angular-sanitize": "~1.3.8",
28 | "bootstrap": "~3.3.1",
29 | "jquery": "~2.1.3",
30 | "moment": "~2.8.4",
31 | "toastr": "~2.1.0",
32 | "font-awesome": "~4.2.0",
33 | "extras.angular.plus": "~0.9.2"
34 | },
35 | "exportsOverride": {
36 | "sinon": {
37 | "js": "index.js"
38 | }
39 | },
40 | "resolutions": {
41 | "angular": ">=1.2.25 <1.4.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/gulp.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function() {
2 | var client = './src/client/';
3 | var server = './src/server/';
4 | var clientApp = client + 'app/';
5 | var report = './report/';
6 | var root = './';
7 | var specRunnerFile = 'specs.html';
8 | var temp = './.tmp/';
9 | var wiredep = require('wiredep');
10 | var bowerFiles = wiredep({devDependencies: true})['js'];
11 | var bower = {
12 | json: require('./bower.json'),
13 | directory: './bower_components/',
14 | ignorePath: '../..'
15 | };
16 | var nodeModules = 'node_modules';
17 |
18 | var config = {
19 | /**
20 | * File paths
21 | */
22 | // all javascript that we want to vet
23 | alljs: [
24 | './src/**/*.js',
25 | './*.js'
26 | ],
27 | build: './build/',
28 | client: client,
29 | css: temp + 'styles.css',
30 | fonts: bower.directory + 'font-awesome/fonts/**/*.*',
31 | html: client + '**/*.html',
32 | htmltemplates: clientApp + '**/*.html',
33 | images: client + 'images/**/*.*',
34 | index: client + 'index.html',
35 | // app js, with no specs
36 | js: [
37 | clientApp + '**/*.module.js',
38 | clientApp + '**/*.js',
39 | '!' + clientApp + '**/*.spec.js'
40 | ],
41 | jsOrder: [
42 | '**/app.module.js',
43 | '**/*.module.js',
44 | '**/*.js'
45 | ],
46 | less: client + 'styles/styles.less',
47 | report: report,
48 | root: root,
49 | server: server,
50 | source: 'src/',
51 | stubsjs: [
52 | bower.directory + 'angular-mocks/angular-mocks.js',
53 | client + 'stubs/**/*.js'
54 | ],
55 | temp: temp,
56 |
57 | /**
58 | * optimized files
59 | */
60 | optimized: {
61 | app: 'app.js',
62 | lib: 'lib.js'
63 | },
64 |
65 | /**
66 | * plato
67 | */
68 | plato: {js: clientApp + '**/*.js'},
69 |
70 | /**
71 | * browser sync
72 | */
73 | browserReloadDelay: 1000,
74 |
75 | /**
76 | * template cache
77 | */
78 | templateCache: {
79 | file: 'templates.js',
80 | options: {
81 | module: 'app.core',
82 | root: 'app/',
83 | standAlone: false
84 | }
85 | },
86 |
87 | /**
88 | * Bower and NPM files
89 | */
90 | bower: bower,
91 | packages: [
92 | './package.json',
93 | './bower.json'
94 | ],
95 |
96 | /**
97 | * specs.html, our HTML spec runner
98 | */
99 | specRunner: client + specRunnerFile,
100 | specRunnerFile: specRunnerFile,
101 |
102 | /**
103 | * The sequence of the injections into specs.html:
104 | * 1 testlibraries
105 | * mocha setup
106 | * 2 bower
107 | * 3 js
108 | * 4 spechelpers
109 | * 5 specs
110 | * 6 templates
111 | */
112 | testlibraries: [
113 | nodeModules + '/mocha/mocha.js',
114 | nodeModules + '/chai/chai.js',
115 | nodeModules + '/mocha-clean/index.js',
116 | nodeModules + '/sinon-chai/lib/sinon-chai.js'
117 | ],
118 | specHelpers: [
119 | client + 'test-helpers/*.js',
120 | // Karma complains about this because it composes an invalid path from the CWD;
121 | // fortunately it doesn't matter because it should (and will) run these anyway
122 | '!' + client + 'test-helpers/*.spec.js'
123 | ],
124 | specs: [
125 | client + 'tests/assertions.spec.js',
126 | clientApp + '**/*.spec.js',
127 | client + 'tests/*.spec.js'
128 | ],
129 | serverIntegrationSpecs: [client + 'tests/server-integration/**/*.spec.js'],
130 | specHelperSpecs: [client + 'test-helpers/*.spec.js'],
131 |
132 | /**
133 | * Node settings
134 | */
135 | nodeServer: './src/server/app.js',
136 | defaultPort: '7203'
137 | };
138 |
139 | /**
140 | * wiredep and bower settings
141 | */
142 | config.getWiredepDefaultOptions = function() {
143 | var options = {
144 | bowerJson: config.bower.json,
145 | directory: config.bower.directory,
146 | ignorePath: config.bower.ignorePath
147 | };
148 | return options;
149 | };
150 |
151 | /**
152 | * karma settings
153 | */
154 | config.karma = getKarmaOptions();
155 |
156 | return config;
157 |
158 | ////////////////
159 |
160 | function getKarmaOptions() {
161 | var options = {
162 | files: [].concat(
163 | bowerFiles,
164 | config.specHelpers,
165 | clientApp + '**/*.module.js',
166 | clientApp + '**/*.js',
167 | // client + '/tests/*.spec.js', // NONE AT THE MOMENT
168 | temp + config.templateCache.file,
169 | config.serverIntegrationSpecs
170 | ),
171 | exclude: [],
172 | coverage: {
173 | dir: report + 'coverage',
174 | reporters: [
175 | // reporters not supporting the `file` property
176 | {type: 'html', subdir: 'report-html'},
177 | {type: 'lcov', subdir: 'report-lcov'},
178 | {type: 'text-summary'}
179 | ]
180 | },
181 | preprocessors: {}
182 | };
183 | options.preprocessors[clientApp + '**/!(*.spec)+(.js)'] = ['coverage'];
184 | return options;
185 | }
186 | };
187 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var args = require('yargs').argv;
2 | var browserSync = require('browser-sync');
3 | var config = require('./gulp.config')();
4 | var del = require('del');
5 | var glob = require('glob');
6 | var gulp = require('gulp');
7 | var path = require('path');
8 | var _ = require('lodash');
9 | var $ = require('gulp-load-plugins')({lazy: true});
10 |
11 | var colors = $.util.colors;
12 | var envenv = $.util.env;
13 | var port = process.env.PORT || config.defaultPort;
14 |
15 | /**
16 | * yargs variables can be passed in to alter the behavior, when present.
17 | * Example: gulp serve-dev
18 | *
19 | * --verbose : Various tasks will produce more output to the console.
20 | * --nosync : Don't launch the browser with browser-sync when serving code.
21 | * --debug : Launch debugger with node-inspector.
22 | * --debug-brk: Launch debugger and break on 1st line with node-inspector.
23 | * --startServers: Will start servers for midway tests on the test task.
24 | */
25 |
26 | /**
27 | * List the available gulp tasks
28 | */
29 | gulp.task('help', $.taskListing);
30 | gulp.task('default', ['help']);
31 |
32 | /**
33 | * vet the code and create coverage report
34 | * @return {Stream}
35 | */
36 | gulp.task('vet', function() {
37 | log('Analyzing source with JSHint and JSCS');
38 |
39 | return gulp
40 | .src(config.alljs)
41 | .pipe($.if(args.verbose, $.print()))
42 | .pipe($.jshint())
43 | .pipe($.jshint.reporter('jshint-stylish', {verbose: true}))
44 | .pipe($.jshint.reporter('fail'))
45 | .pipe($.jscs());
46 | });
47 |
48 | /**
49 | * Create a visualizer report
50 | */
51 | gulp.task('plato', function(done) {
52 | log('Analyzing source with Plato');
53 | log('Browse to /report/plato/index.html to see Plato results');
54 |
55 | startPlatoVisualizer(done);
56 | });
57 |
58 | /**
59 | * Compile less to css
60 | * @return {Stream}
61 | */
62 | gulp.task('styles', ['clean-styles'], function() {
63 | log('Compiling Less --> CSS');
64 |
65 | return gulp
66 | .src(config.less)
67 | .pipe($.plumber()) // exit gracefully if something fails after this
68 | .pipe($.less())
69 | // .on('error', errorLogger) // more verbose and dupe output. requires emit.
70 | .pipe($.autoprefixer({browsers: ['last 2 version', '> 5%']}))
71 | .pipe(gulp.dest(config.temp));
72 | });
73 |
74 | /**
75 | * Copy fonts
76 | * @return {Stream}
77 | */
78 | gulp.task('fonts', ['clean-fonts'], function() {
79 | log('Copying fonts');
80 |
81 | return gulp
82 | .src(config.fonts)
83 | .pipe(gulp.dest(config.build + 'fonts'));
84 | });
85 |
86 | /**
87 | * Compress images
88 | * @return {Stream}
89 | */
90 | gulp.task('images', ['clean-images'], function() {
91 | log('Compressing and copying images');
92 |
93 | return gulp
94 | .src(config.images)
95 | .pipe($.imagemin({optimizationLevel: 4}))
96 | .pipe(gulp.dest(config.build + 'images'));
97 | });
98 |
99 | gulp.task('less-watcher', function() {
100 | gulp.watch([config.less], ['styles']);
101 | });
102 |
103 | /**
104 | * Create $templateCache from the html templates
105 | * @return {Stream}
106 | */
107 | gulp.task('templatecache', ['clean-code'], function() {
108 | log('Creating an AngularJS $templateCache');
109 |
110 | return gulp
111 | .src(config.htmltemplates)
112 | .pipe($.if(args.verbose, $.bytediff.start()))
113 | .pipe($.minifyHtml({empty: true}))
114 | .pipe($.if(args.verbose, $.bytediff.stop(bytediffFormatter)))
115 | .pipe($.angularTemplatecache(
116 | config.templateCache.file,
117 | config.templateCache.options
118 | ))
119 | .pipe(gulp.dest(config.temp));
120 | });
121 |
122 | /**
123 | * Wire-up the bower dependencies
124 | * @return {Stream}
125 | */
126 | gulp.task('wiredep', function() {
127 | log('Wiring the bower dependencies into the html');
128 |
129 | var wiredep = require('wiredep').stream;
130 | var options = config.getWiredepDefaultOptions();
131 |
132 | // Only include stubs if flag is enabled
133 | var js = args.stubs ? [].concat(config.js, config.stubsjs) : config.js;
134 |
135 | return gulp
136 | .src(config.index)
137 | .pipe(wiredep(options))
138 | .pipe(inject(js, '', config.jsOrder))
139 | .pipe(gulp.dest(config.client));
140 | });
141 |
142 | gulp.task('inject', ['wiredep', 'styles', 'templatecache'], function() {
143 | log('Wire up css into the html, after files are ready');
144 |
145 | return gulp
146 | .src(config.index)
147 | .pipe(inject(config.css))
148 | .pipe(gulp.dest(config.client));
149 | });
150 |
151 | /**
152 | * Run the spec runner
153 | * @return {Stream}
154 | */
155 | gulp.task('serve-specs', ['build-specs'], function(done) {
156 | log('run the spec runner');
157 | serve(true /* isDev */, true /* specRunner */);
158 | done();
159 | });
160 |
161 | /**
162 | * Inject all the spec files into the specs.html
163 | * @return {Stream}
164 | */
165 | gulp.task('build-specs', ['templatecache'], function(done) {
166 | log('building the spec runner');
167 |
168 | var wiredep = require('wiredep').stream;
169 | var templateCache = config.temp + config.templateCache.file;
170 | var options = config.getWiredepDefaultOptions();
171 | var specs = config.specs;
172 |
173 | if (args.startServers) {
174 | specs = [].concat(specs, config.serverIntegrationSpecs);
175 | }
176 | options.devDependencies = true;
177 |
178 | return gulp
179 | .src(config.specRunner)
180 | .pipe(wiredep(options))
181 | .pipe(inject(config.js, '', config.jsOrder))
182 | .pipe(inject(config.testlibraries, 'testlibraries'))
183 | .pipe(inject(config.specHelpers, 'spechelpers'))
184 | .pipe(inject(specs, 'specs', ['**/*']))
185 | .pipe(inject(templateCache, 'templates'))
186 | .pipe(gulp.dest(config.client));
187 | });
188 |
189 | /**
190 | * Build everything
191 | * This is separate so we can run tests on
192 | * optimize before handling image or fonts
193 | */
194 | gulp.task('build', ['optimize', 'images', 'fonts'], function() {
195 | log('Building everything');
196 |
197 | var msg = {
198 | title: 'gulp build',
199 | subtitle: 'Deployed to the build folder',
200 | message: 'Running `gulp serve-build`'
201 | };
202 | del(config.temp);
203 | log(msg);
204 | notify(msg);
205 | });
206 |
207 | /**
208 | * Optimize all files, move to a build folder,
209 | * and inject them into the new index.html
210 | * @return {Stream}
211 | */
212 | gulp.task('optimize', ['inject', 'test'], function() {
213 | log('Optimizing the js, css, and html');
214 |
215 | var assets = $.useref.assets({searchPath: './'});
216 | // Filters are named for the gulp-useref path
217 | var cssFilter = $.filter('**/*.css');
218 | var jsAppFilter = $.filter('**/' + config.optimized.app);
219 | var jslibFilter = $.filter('**/' + config.optimized.lib);
220 |
221 | var templateCache = config.temp + config.templateCache.file;
222 |
223 | return gulp
224 | .src(config.index)
225 | .pipe($.plumber())
226 | .pipe(inject(templateCache, 'templates'))
227 | .pipe(assets) // Gather all assets from the html with useref
228 | // Get the css
229 | .pipe(cssFilter)
230 | .pipe($.csso())
231 | .pipe(cssFilter.restore())
232 | // Get the custom javascript
233 | .pipe(jsAppFilter)
234 | .pipe($.ngAnnotate({add: true}))
235 | .pipe($.uglify())
236 | .pipe(getHeader())
237 | .pipe(jsAppFilter.restore())
238 | // Get the vendor javascript
239 | .pipe(jslibFilter)
240 | .pipe($.uglify()) // another option is to override wiredep to use min files
241 | .pipe(jslibFilter.restore())
242 | // Take inventory of the file names for future rev numbers
243 | .pipe($.rev())
244 | // Apply the concat and file replacement with useref
245 | .pipe(assets.restore())
246 | .pipe($.useref())
247 | // Replace the file names in the html with rev numbers
248 | .pipe($.revReplace())
249 | .pipe(gulp.dest(config.build));
250 | });
251 |
252 | /**
253 | * Remove all files from the build, temp, and reports folders
254 | * @param {Function} done - callback when complete
255 | */
256 | gulp.task('clean', function(done) {
257 | var delconfig = [].concat(config.build, config.temp, config.report);
258 | log('Cleaning: ' + $.util.colors.blue(delconfig));
259 | del(delconfig, done);
260 | });
261 |
262 | /**
263 | * Remove all fonts from the build folder
264 | * @param {Function} done - callback when complete
265 | */
266 | gulp.task('clean-fonts', function(done) {
267 | clean(config.build + 'fonts/**/*.*', done);
268 | });
269 |
270 | /**
271 | * Remove all images from the build folder
272 | * @param {Function} done - callback when complete
273 | */
274 | gulp.task('clean-images', function(done) {
275 | clean(config.build + 'images/**/*.*', done);
276 | });
277 |
278 | /**
279 | * Remove all styles from the build and temp folders
280 | * @param {Function} done - callback when complete
281 | */
282 | gulp.task('clean-styles', function(done) {
283 | var files = [].concat(
284 | config.temp + '**/*.css',
285 | config.build + 'styles/**/*.css'
286 | );
287 | clean(files, done);
288 | });
289 |
290 | /**
291 | * Remove all js and html from the build and temp folders
292 | * @param {Function} done - callback when complete
293 | */
294 | gulp.task('clean-code', function(done) {
295 | var files = [].concat(
296 | config.temp + '**/*.js',
297 | config.build + 'js/**/*.js',
298 | config.build + '**/*.html'
299 | );
300 | clean(files, done);
301 | });
302 |
303 | /**
304 | * Run specs once and exit
305 | * To start servers and run midway specs as well:
306 | * gulp test --startServers
307 | * @return {Stream}
308 | */
309 | gulp.task('test', ['vet', 'templatecache'], function(done) {
310 | startTests(true /*singleRun*/ , done);
311 | });
312 |
313 | /**
314 | * Run specs and wait.
315 | * Watch for file changes and re-run tests on each change
316 | * To start servers and run midway specs as well:
317 | * gulp autotest --startServers
318 | */
319 | gulp.task('autotest', function(done) {
320 | startTests(false /*singleRun*/ , done);
321 | });
322 |
323 | /**
324 | * serve the dev environment
325 | * --debug-brk or --debug
326 | * --nosync
327 | */
328 | gulp.task('serve-dev', ['inject'], function() {
329 | serve(true /*isDev*/);
330 | });
331 |
332 | /**
333 | * serve the build environment
334 | * --debug-brk or --debug
335 | * --nosync
336 | */
337 | gulp.task('serve-build', ['build'], function() {
338 | serve(false /*isDev*/);
339 | });
340 |
341 | /**
342 | * Bump the version
343 | * --type=pre will bump the prerelease version *.*.*-x
344 | * --type=patch or no flag will bump the patch version *.*.x
345 | * --type=minor will bump the minor version *.x.*
346 | * --type=major will bump the major version x.*.*
347 | * --version=1.2.3 will bump to a specific version and ignore other flags
348 | */
349 | gulp.task('bump', function() {
350 | var msg = 'Bumping versions';
351 | var type = args.type;
352 | var version = args.ver;
353 | var options = {};
354 | if (version) {
355 | options.version = version;
356 | msg += ' to ' + version;
357 | } else {
358 | options.type = type;
359 | msg += ' for a ' + type;
360 | }
361 | log(msg);
362 |
363 | return gulp
364 | .src(config.packages)
365 | .pipe($.print())
366 | .pipe($.bump(options))
367 | .pipe(gulp.dest(config.root));
368 | });
369 |
370 | ////////////////
371 |
372 | /**
373 | * Add watches to build and reload using browser-sync.
374 | * Use this XOR the browser-sync option.files, not both.
375 | * @param {Boolean} isDev - dev or build mode
376 | */
377 | //function addWatchForFileReload(isDev) {
378 | // if (isDev) {
379 | // gulp.watch([config.less], ['styles', browserSync.reload]);
380 | // gulp.watch([config.client + '**/*', '!' + config.less], browserSync.reload)
381 | // .on('change', function(event) { changeEvent(event); });
382 | // }
383 | // else {
384 | // gulp.watch([config.less, config.js, config.html], ['build', browserSync.reload])
385 | // .on('change', function(event) { changeEvent(event); });
386 | // }
387 | //}
388 |
389 | /**
390 | * When files change, log it
391 | * @param {Object} event - event that fired
392 | */
393 | function changeEvent(event) {
394 | var srcPattern = new RegExp('/.*(?=/' + config.source + ')/');
395 | log('File ' + event.path.replace(srcPattern, '') + ' ' + event.type);
396 | }
397 |
398 | /**
399 | * Delete all files in a given path
400 | * @param {Array} path - array of paths to delete
401 | * @param {Function} done - callback when complete
402 | */
403 | function clean(path, done) {
404 | log('Cleaning: ' + $.util.colors.blue(path));
405 | del(path, done);
406 | }
407 |
408 | /**
409 | * Inject files in a sorted sequence at a specified inject label
410 | * @param {Array} src glob pattern for source files
411 | * @param {String} label The label name
412 | * @param {Array} order glob pattern for sort order of the files
413 | * @returns {Stream} The stream
414 | */
415 | function inject(src, label, order) {
416 | var options = {read: false};
417 | if (label) {
418 | options.name = 'inject:' + label;
419 | }
420 |
421 | return $.inject(orderSrc(src, order), options);
422 | }
423 |
424 | /**
425 | * Order a stream
426 | * @param {Stream} src The gulp.src stream
427 | * @param {Array} order Glob array pattern
428 | * @returns {Stream} The ordered stream
429 | */
430 | function orderSrc (src, order) {
431 | //order = order || ['**/*'];
432 | return gulp
433 | .src(src)
434 | .pipe($.if(order, $.order(order)));
435 | }
436 |
437 | /**
438 | * serve the code
439 | * --debug-brk or --debug
440 | * --nosync
441 | * @param {Boolean} isDev - dev or build mode
442 | * @param {Boolean} specRunner - server spec runner html
443 | */
444 | function serve(isDev, specRunner) {
445 | var debug = args.debug || args.debugBrk;
446 | var debugMode = args.debug ? '--debug' : args.debugBrk ? '--debug-brk' : '';
447 | var nodeOptions = getNodeOptions(isDev);
448 |
449 | if (debug) {
450 | runNodeInspector();
451 | nodeOptions.nodeArgs = [debugMode + '=5858'];
452 | }
453 |
454 | if (args.verbose) {
455 | console.log(nodeOptions);
456 | }
457 |
458 | return $.nodemon(nodeOptions)
459 | .on('restart', ['vet'], function(ev) {
460 | log('*** nodemon restarted');
461 | log('files changed:\n' + ev);
462 | setTimeout(function() {
463 | browserSync.notify('reloading now ...');
464 | browserSync.reload({stream: false});
465 | }, config.browserReloadDelay);
466 | })
467 | .on('start', function () {
468 | log('*** nodemon started');
469 | startBrowserSync(isDev, specRunner);
470 | })
471 | .on('crash', function () {
472 | log('*** nodemon crashed: script crashed for some reason');
473 | })
474 | .on('exit', function () {
475 | log('*** nodemon exited cleanly');
476 | });
477 | }
478 |
479 | function getNodeOptions(isDev) {
480 | return {
481 | script: config.nodeServer,
482 | delayTime: 1,
483 | env: {
484 | 'PORT': port,
485 | 'NODE_ENV': isDev ? 'dev' : 'build'
486 | },
487 | watch: [config.server]
488 | };
489 | }
490 |
491 | function runNodeInspector() {
492 | log('Running node-inspector.');
493 | log('Browse to http://localhost:8080/debug?port=5858');
494 | var exec = require('child_process').exec;
495 | exec('node-inspector');
496 | }
497 |
498 | /**
499 | * Start BrowserSync
500 | * --nosync will avoid browserSync
501 | */
502 | function startBrowserSync(isDev, specRunner) {
503 | if (args.nosync || browserSync.active) {
504 | return;
505 | }
506 |
507 | log('Starting BrowserSync on port ' + port);
508 |
509 | // If build: watches the files, builds, and restarts browser-sync.
510 | // If dev: watches less, compiles it to css, browser-sync handles reload
511 | if (isDev) {
512 | gulp.watch([config.less], ['styles'])
513 | .on('change', changeEvent);
514 | } else {
515 | gulp.watch([config.less, config.js, config.html], ['optimize', browserSync.reload])
516 | .on('change', changeEvent);
517 | }
518 |
519 | var options = {
520 | proxy: 'localhost:' + port,
521 | port: 3000,
522 | files: isDev ? [
523 | config.client + '../basics/**/*.*',
524 | config.client + '**/*.*',
525 | '!' + config.less,
526 | config.temp + '**/*.css'
527 | ] : [],
528 | ghostMode: { // these are the defaults t,f,t,t
529 | clicks: true,
530 | location: false,
531 | forms: true,
532 | scroll: true
533 | },
534 | injectChanges: true,
535 | logFileChanges: true,
536 | logLevel: 'debug',
537 | logPrefix: 'gulp-patterns',
538 | notify: true,
539 | reloadDelay: 0 //1000
540 | } ;
541 | if (specRunner) {
542 | options.startPath = config.specRunnerFile;
543 | }
544 |
545 | browserSync(options);
546 | }
547 |
548 | /**
549 | * Start Plato inspector and visualizer
550 | */
551 | function startPlatoVisualizer(done) {
552 | log('Running Plato');
553 |
554 | var files = glob.sync(config.plato.js);
555 | var excludeFiles = /.*\.spec\.js/;
556 | var plato = require('plato');
557 |
558 | var options = {
559 | title: 'Plato Inspections Report',
560 | exclude: excludeFiles
561 | };
562 | var outputDir = config.report + '/plato';
563 |
564 | plato.inspect(files, outputDir, options, platoCompleted);
565 |
566 | function platoCompleted(report) {
567 | var overview = plato.getOverviewReport(report);
568 | if (args.verbose) {
569 | log(overview.summary);
570 | }
571 | if (done) { done(); }
572 | }
573 | }
574 |
575 | /**
576 | * Start the tests using karma.
577 | * @param {boolean} singleRun - True means run once and end (CI), or keep running (dev)
578 | * @param {Function} done - Callback to fire when karma is done
579 | * @return {undefined}
580 | */
581 | function startTests(singleRun, done) {
582 | var child;
583 | var excludeFiles = [];
584 | var fork = require('child_process').fork;
585 | var karma = require('karma').server;
586 | var serverSpecs = config.serverIntegrationSpecs;
587 |
588 | if (args.startServers) {
589 | log('Starting servers');
590 | var savedEnv = process.env;
591 | savedEnv.NODE_ENV = 'dev';
592 | savedEnv.PORT = 8888;
593 | child = fork(config.nodeServer);
594 | } else {
595 | if (serverSpecs && serverSpecs.length) {
596 | excludeFiles = serverSpecs;
597 | }
598 | }
599 |
600 | karma.start({
601 | configFile: __dirname + '/karma.conf.js',
602 | exclude: excludeFiles,
603 | singleRun: !!singleRun
604 | }, karmaCompleted);
605 |
606 | ////////////////
607 |
608 | function karmaCompleted(karmaResult) {
609 | log('Karma completed');
610 | if (child) {
611 | log('shutting down the child process');
612 | child.kill();
613 | }
614 | if (karmaResult === 1) {
615 | done('karma: tests failed with code ' + karmaResult);
616 | } else {
617 | done();
618 | }
619 | }
620 | }
621 |
622 | /**
623 | * Formatter for bytediff to display the size changes after processing
624 | * @param {Object} data - byte data
625 | * @return {String} Difference in bytes, formatted
626 | */
627 | function bytediffFormatter(data) {
628 | var difference = (data.savings > 0) ? ' smaller.' : ' larger.';
629 | return data.fileName + ' went from ' +
630 | (data.startSize / 1000).toFixed(2) + ' kB to ' +
631 | (data.endSize / 1000).toFixed(2) + ' kB and is ' +
632 | formatPercent(1 - data.percent, 2) + '%' + difference;
633 | }
634 |
635 | /**
636 | * Log an error message and emit the end of a task
637 | */
638 | function errorLogger(error) {
639 | log('*** Start of Error ***');
640 | log(error);
641 | log('*** End of Error ***');
642 | this.emit('end');
643 | }
644 |
645 | /**
646 | * Format a number as a percentage
647 | * @param {Number} num Number to format as a percent
648 | * @param {Number} precision Precision of the decimal
649 | * @return {String} Formatted perentage
650 | */
651 | function formatPercent(num, precision) {
652 | return (num * 100).toFixed(precision);
653 | }
654 |
655 | /**
656 | * Format and return the header for files
657 | * @return {String} Formatted file header
658 | */
659 | function getHeader() {
660 | var pkg = require('./package.json');
661 | var template = ['/**',
662 | ' * <%= pkg.name %> - <%= pkg.description %>',
663 | ' * @authors <%= pkg.authors %>',
664 | ' * @version v<%= pkg.version %>',
665 | ' * @link <%= pkg.homepage %>',
666 | ' * @license <%= pkg.license %>',
667 | ' */',
668 | ''
669 | ].join('\n');
670 | return $.header(template, {
671 | pkg: pkg
672 | });
673 | }
674 |
675 | /**
676 | * Log a message or series of messages using chalk's blue color.
677 | * Can pass in a string, object or array.
678 | */
679 | function log(msg) {
680 | if (typeof(msg) === 'object') {
681 | for (var item in msg) {
682 | if (msg.hasOwnProperty(item)) {
683 | $.util.log($.util.colors.blue(msg[item]));
684 | }
685 | }
686 | } else {
687 | $.util.log($.util.colors.blue(msg));
688 | }
689 | }
690 |
691 | /**
692 | * Show OS level notification using node-notifier
693 | */
694 | function notify(options) {
695 | var notifier = require('node-notifier');
696 | var notifyOptions = {
697 | sound: 'Bottle',
698 | contentImage: path.join(__dirname, 'gulp.png'),
699 | icon: path.join(__dirname, 'gulp.png')
700 | };
701 | _.assign(notifyOptions, options);
702 | notifier.notify(notifyOptions);
703 | }
704 |
705 | module.exports = gulp;
706 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | require('mocha-clean');
2 | module.exports = function (config) {
3 | var gulpConfig = require('./gulp.config')();
4 |
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: './',
9 |
10 | // frameworks to use
11 | // some available frameworks: https://npmjs.org/browse/keyword/karma-adapter
12 | frameworks: ['mocha', 'chai', 'sinon', 'chai-sinon'],
13 |
14 | // list of files / patterns to load in the browser
15 | files: gulpConfig.karma.files,
16 |
17 | // list of files to exclude
18 | exclude: [
19 | // Including server-integration tests for now; comment this line out when you want to run them
20 | //'./src/client/tests/server-integration/**/*.spec.js'
21 | ],
22 |
23 | proxies: {
24 | '/': 'http://localhost:8888/'
25 | },
26 |
27 | // preprocess matching files before serving them to the browser
28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
29 | preprocessors: gulpConfig.karma.preprocessors,
30 |
31 | // test results reporter to use
32 | // possible values: 'dots', 'progress', 'coverage'
33 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
34 | reporters: ['progress', 'coverage'],
35 |
36 | coverageReporter: {
37 | type: 'lcov',
38 | dir: 'report/coverage'
39 | },
40 |
41 | // web server port
42 | port: 9876,
43 |
44 | // enable / disable colors in the output (reporters and logs)
45 | colors: true,
46 |
47 | // level of logging
48 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN
49 | // || config.LOG_INFO || config.LOG_DEBUG
50 | logLevel: config.LOG_INFO,
51 |
52 | // enable / disable watching file and executing tests whenever any file changes
53 | autoWatch: true,
54 |
55 | // start these browsers
56 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
57 | // browsers: ['Chrome', 'ChromeCanary', 'FirefoxAurora', 'Safari', 'PhantomJS'],
58 | browsers: ['PhantomJS'],
59 |
60 | // Continuous Integration mode
61 | // if true, Karma captures browsers, runs the tests and exits
62 | singleRun: false
63 | });
64 | };
65 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "AngularJS-Patterns-Testing-Demo",
3 | "version": "0.0.1",
4 | "description": "AngularJS Patterns Testing Demo",
5 | "authors": [
6 | "John Papa",
7 | "Ward Bell"
8 | ],
9 | "license": "MIT",
10 | "homepage": "https://github.com/johnpapa/ng-patterns-testing",
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/johnpapa/ng-patterns-testing.git"
14 | },
15 | "scripts": {
16 | "init": "npm install",
17 | "install": "bower install",
18 | "start": "node src/server/app.js",
19 | "test": "gulp test"
20 | },
21 | "dependencies": {
22 | "body-parser": "^1.10.0",
23 | "compression": "^1.2.0",
24 | "cors": "^2.2.0",
25 | "express": "^4.10.0",
26 | "look": "^0.1.3",
27 | "morgan": "^1.1.1",
28 | "serve-favicon": "^2.0.1"
29 | },
30 | "devDependencies": {
31 | "browser-sync": "^1.5.8",
32 | "chai": "^1.9.1",
33 | "chai-as-promised": "^4.1.1",
34 | "chalk": "^0.5.1",
35 | "dateformat": "^1.0.8-1.2.3",
36 | "debug": "^2.0.0",
37 | "del": "^0.1.3",
38 | "glob": "^4.3.1",
39 | "gulp": "^3.8.11",
40 | "gulp-angular-templatecache": "^1.4.2",
41 | "gulp-autoprefixer": "^2.0.0",
42 | "gulp-bytediff": "^0.2.0",
43 | "gulp-cache": "^0.2.0",
44 | "gulp-concat": "^2.3.3",
45 | "gulp-csso": "^0.2.9",
46 | "gulp-filter": "^1.0.2",
47 | "gulp-header": "^1.2.2",
48 | "gulp-if": "^1.2.5",
49 | "gulp-imagemin": "^2.0.0",
50 | "gulp-inject": "^1.0.1",
51 | "gulp-jscs": "^1.3.0",
52 | "gulp-jshint": "^1.7.1",
53 | "gulp-less": "^3.0.1",
54 | "gulp-load-plugins": "^0.8.0",
55 | "gulp-load-utils": "^0.0.4",
56 | "gulp-minify-html": "^0.1.5",
57 | "gulp-ng-annotate": "^0.4.2",
58 | "gulp-nodemon": "^1.0.4",
59 | "gulp-order": "^1.1.1",
60 | "gulp-plumber": "^0.6.4",
61 | "gulp-print": "^1.1.0",
62 | "gulp-rev": "^2.0.0",
63 | "gulp-rev-replace": "^0.3.1",
64 | "gulp-sourcemaps": "^1.1.5",
65 | "gulp-task-listing": "^1.0.0",
66 | "gulp-uglify": "^1.0.2",
67 | "gulp-useref": "^1.0.2",
68 | "gulp-util": "^3.0.1",
69 | "jshint-stylish": "^1.0.0",
70 | "karma": "^0.12.28",
71 | "karma-chai": "^0.1.0",
72 | "karma-chai-sinon": "^0.1.3",
73 | "karma-chrome-launcher": "^0.1.7",
74 | "karma-coverage": "^0.2.7",
75 | "karma-firefox-launcher": "^0.1.3",
76 | "karma-growl-reporter": "^0.1.1",
77 | "karma-mocha": "^0.1.10",
78 | "karma-phantomjs-launcher": "^0.1.4",
79 | "karma-safari-launcher": "^0.1.1",
80 | "karma-sinon": "^1.0.4",
81 | "lodash": "^2.4.1",
82 | "merge-stream": "^0.1.5",
83 | "minimist": "^1.1.0",
84 | "mocha": "^2.0.0",
85 | "mocha-clean": "^0.4.0",
86 | "node-notifier": "^4.0.3",
87 | "phantomjs": "1.9.12",
88 | "plato": "^1.2.0",
89 | "q": "^1.0.1",
90 | "sinon": "^1.10.3",
91 | "sinon-chai": "^2.5.0",
92 | "wiredep": "^2.1.0",
93 | "yargs": "^1.3.3"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/basics/basics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 | Click to start over.
20 | Click a description title to run its specs only
21 | (see "
22 | ?grep" in address bar).
23 | Click a spec title to see its implementation.
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
38 |
39 |
42 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/basics/basics.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030, -W109 */
2 | /* jscs: disable */
3 | /* Using chai BDD (expect) assertions:
4 | http://chaijs.com/api/bdd/ */
5 |
6 | describe('basics:', function () {
7 | it('true should be true', function () {
8 | expect(true).to.be.true;
9 | });
10 |
11 |
12 |
13 | describe('truthiness', function () {
14 | it('true is truthy', function () {
15 | expect(true).to.be.ok;
16 | });
17 | it('1 is truthy', function () {
18 | expect(1).to.be.ok;
19 | });
20 | it('"false" is truthy', function () {
21 | expect('false').to.be.ok;
22 | });
23 | it('0 is falsey', function () {
24 | expect(0).to.be.not.ok;
25 | });
26 | });
27 |
28 |
29 | describe('array', function () {
30 | var array;
31 | beforeEach(function () {
32 | array = [1,2,3];
33 | });
34 |
35 | it('push should add item to the end', function () {
36 | array.push(5);
37 | expect(array).to.have.length(4);
38 | expect(array[3]).to.equal(5);
39 | });
40 |
41 | it('pop should remove the last one', function () {
42 | var popped = array.pop();
43 | expect(popped).to.equal(3);
44 | expect(array).to.have.length(2);
45 | });
46 | });
47 |
48 |
49 |
50 | // SUT: System Under Test
51 | function setSeed(seed) {
52 |
53 | if (seed === 0) {
54 | // throw new Error('some other error');
55 | throw new Error('0 is a bad seed');
56 | }
57 | return seed;
58 | }
59 |
60 | describe('going to seed', function () {
61 | it('should be ok with 3', function () {
62 | expect(setSeed(3)).to.be.ok;
63 | });
64 |
65 | it('should throw with 0', function () {
66 | expect(function () {
67 | setSeed(0);
68 | }).to.throw(/bad seed/);
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/src/client/app/app.module.js:
--------------------------------------------------------------------------------
1 | angular.module('templates', []);
2 | angular.module('app', [
3 | 'app.core',
4 | 'app.widgets',
5 | 'app.avengers',
6 | 'app.dashboard',
7 | 'app.layout',
8 | 'templates'
9 | ]);
10 |
--------------------------------------------------------------------------------
/src/client/app/avengers/avengers.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.avengers')
6 | .controller('Avengers', Avengers);
7 |
8 | /* @ngInject */
9 | function Avengers(dataservice, logger) {
10 | var vm = this;
11 | vm.avengers = [];
12 | vm.title = 'Avengers';
13 |
14 | activate();
15 |
16 | function activate() {
17 | return getAvengers()
18 | .then(function() {
19 | logger.info('Activated Avengers View');
20 | })
21 | .catch(function(err) {
22 | logger.error('Avengers view activation failed: ' + err);
23 | });
24 | }
25 |
26 | function getAvengers() {
27 | return dataservice.getAvengers().then(function(data) {
28 | vm.avengers = data;
29 | return data;
30 | });
31 | }
32 | }
33 | })();
34 |
--------------------------------------------------------------------------------
/src/client/app/avengers/avengers.controller.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('avengers controller', function() {
3 |
4 | var avengers, controller, getAvengersSpy;
5 |
6 | beforeEach(function() {
7 | // stay fresh! No cross-test pollution.
8 | avengers = mockData.getAvengers();
9 | controller = undefined;
10 | getAvengersSpy = undefined;
11 | });
12 |
13 | // no lingering http requests at the end of a test
14 | bard.verifyNoOutstandingHttpRequests();
15 |
16 | describe('when stub `getAvengers` of the real dataservice', function() {
17 |
18 | beforeEach(function() {
19 | bard.appModule('app.avengers');
20 | bard.inject(this, '$controller', '$log', '$q', '$rootScope', 'dataservice');
21 |
22 | sinon.stub(dataservice, 'getAvengers')
23 | .returns($q.when(avengers));
24 |
25 | controller = $controller('Avengers');
26 | $rootScope.$apply();
27 | });
28 |
29 | it('should be created successfully', function () {
30 | expect(controller).to.be.defined;
31 | });
32 |
33 | it('should have title of Avengers', function() {
34 | expect(controller.title).to.equal('Avengers');
35 | });
36 |
37 | it('should have called `dataservice.getAvengers` once', function() {
38 | expect(dataservice.getAvengers).to.have.been.calledOnce;
39 | });
40 |
41 | it('should have Avengers', function() {
42 | expect(controller.avengers)
43 | .to.have.length(avengers.length);
44 | });
45 |
46 | it('should have logged "Activated"', function() {
47 | expect($log.info.logs).to.match(/Activated/);
48 | });
49 | });
50 |
51 | /////// helpers /////
52 |
53 | // return a fake `getAvengers` method,
54 | // wrapped in a sinon.js spy
55 | function getAvengersFake() {
56 | getAvengersSpy = sinon.spy(function() {
57 | return $q.when(avengers);
58 | });
59 | return getAvengersSpy;
60 | }
61 |
62 | /*****
63 | *
64 | *
65 | *
66 | *
67 | *
68 | *
69 | *
70 | *
71 | * Alternative ways to fake the dataservice dependency
72 | *
73 | * The avengers.spec uses a fake vanilla JS dataservice whose
74 | * 'getAvengers' is mocked with sinon.
75 | *
76 | * Here we demonstrate other ways to fake that method with sinon
77 | */
78 | describe('when stub `getAvengers` of the real dataservice ... with stubs helper', function() {
79 |
80 | beforeEach(function() {
81 |
82 | bard.appModule('app.avengers');
83 | bard.inject(this, '$controller', '$q', '$rootScope', 'dataservice');
84 |
85 | // Use when you repeatedly stub this method ... and only this method
86 | // if you often stub out a bunch of the same methods
87 | // see the "mockService" example below
88 | getAvengersSpy = stubs.getAvengers();
89 |
90 | controller = $controller('Avengers');
91 | $rootScope.$apply();
92 | });
93 |
94 | getAvengersExpectations();
95 | });
96 |
97 | describe('when monkey patch `getAvengers` of the real dataservice', function() {
98 |
99 | beforeEach(function() {
100 |
101 | bard.appModule('app.avengers');
102 | bard.inject(this, '$controller', '$q', '$rootScope', 'dataservice');
103 |
104 | // Replace the `getAvengers` method with a spy;
105 | // almost the same as stubbing `getAvengers`
106 | dataservice.getAvengers = getAvengersFake();
107 |
108 | controller = $controller('Avengers');
109 | $rootScope.$apply();
110 | });
111 |
112 | getAvengersExpectations();
113 | });
114 |
115 | describe('when create fake dataservice with a `getAvengers` stub', function() {
116 |
117 | beforeEach(function() {
118 |
119 | bard.appModule('app.avengers');
120 | bard.inject(this, '$controller', '$q', '$rootScope');
121 |
122 | // Shows EXACTLY what the controller needs from the service
123 | // Controller throws if it asks for anything else.
124 | var dataservice = {
125 | getAvengers: getAvengersFake()
126 | };
127 |
128 | var ctorArgs = {dataservice: dataservice};
129 | controller = $controller('Avengers', ctorArgs);
130 | $rootScope.$apply();
131 | });
132 |
133 | getAvengersExpectations();
134 | });
135 |
136 | describe('when re-register the dataservice with a fake', function() {
137 |
138 | beforeEach(function() {
139 |
140 | // When the service method is widely used, you can
141 | // re-register the `dataservice` with a fake version.
142 | // Then enlist it in the appModule where needed as shown below.
143 | // You would put this function in `bard`
144 | // N.B.: this service defines only the faked members;
145 | // a controller throws if it calls anything else.
146 | function registerFakeDataservice($provide) {
147 | $provide.service('dataservice', function() {
148 | this.getAvengers = getAvengersFake();
149 | });
150 | }
151 |
152 | bard.appModule('app.avengers', registerFakeDataservice);
153 | bard.inject(this, '$controller', '$q', '$rootScope');
154 |
155 | controller = $controller('Avengers');
156 | $rootScope.$apply();
157 | });
158 |
159 | getAvengersExpectations();
160 | });
161 |
162 | // demonstrate that the real dataservice is untouched
163 | // by distortions of it in other tests; no cross-test pollution
164 | describe('when next inject the real dataservice (no harm from previous faking)', function() {
165 |
166 | beforeEach(function () {
167 | bard.appModule('app.avengers');
168 | bard.inject(this, 'dataservice');
169 | });
170 |
171 | it('has the real `getAvengers` method', function() {
172 | // In this test we inspect the method body to prove
173 | // we've got the real service method, not a fake.
174 | // The real method calls $http
175 | var fn = dataservice.getAvengers.toString();
176 | expect(fn).to.match(/\$http/);
177 | });
178 |
179 | it('has `getAvengersCast` method', function() {
180 | expect(dataservice).has.property('getAvengersCast');
181 | });
182 | });
183 |
184 | describe('when decorate the real dataservice with a stub `getAvengers`', function() {
185 |
186 | beforeEach(function() {
187 |
188 | function decorateDataservice($provide) {
189 |
190 | // When the service method is widely used, you can
191 | // decorate the real `dataservice` methods with
192 | // stubbed versions such as `getAvengers`.
193 | // Then enlist it in the appModule where needed as shown below.
194 | // You would put this function in `bard`
195 | // N.B.: this service leaves other real members intact
196 | $provide.decorator('dataservice', function($delegate) {
197 | $delegate.getAvengers = getAvengersFake();
198 | return $delegate;
199 | });
200 | }
201 |
202 | bard.appModule('app.avengers', decorateDataservice);
203 | bard.inject(this, '$controller', '$q', '$rootScope');
204 |
205 | controller = $controller('Avengers');
206 | $rootScope.$apply();
207 | });
208 |
209 | getAvengersExpectations();
210 | });
211 |
212 | describe('when fake the server\'s response with $httpBackend', function() {
213 |
214 | beforeEach(function() {
215 | bard.appModule('app.avengers');
216 | bard.inject(this, '$controller', '$q', '$rootScope', '$httpBackend', 'dataservice');
217 |
218 | // when `dataservice.getAvengers` sends GET request
219 | // simulate the server's JSON response
220 | $httpBackend
221 | .when('GET', '/api/maa')
222 | .respond(200, avengers);
223 |
224 | getAvengersSpy = sinon.spy(dataservice, 'getAvengers');
225 |
226 | controller = $controller('Avengers');
227 | $httpBackend.flush();
228 | });
229 |
230 | bard.verifyNoOutstandingHttpRequests();
231 |
232 | getAvengersExpectations();
233 | });
234 |
235 | describe('when stub all dataservice members with bard.mockService', function() {
236 |
237 | beforeEach(function() {
238 |
239 | bard.appModule('app.avengers');
240 | bard.inject(this, '$controller', '$q', '$rootScope', 'dataservice');
241 |
242 | // Mock multiple service members with a single configuration
243 | // Every service function is stubbed.
244 | // The `_default` is the stubbed return for every unnamed service function
245 | // You could put common stub configurations in stubs.js, e.g.
246 |
247 | // stubs.happyService();
248 | bard.mockService(dataservice, {
249 | getAvengers: $q.when(avengers),
250 | ready: $q.when(dataservice),
251 | _default: $q.when([])
252 | });
253 |
254 | getAvengersSpy = dataservice.getAvengers; // it's a spy!
255 |
256 | controller = $controller('Avengers');
257 | $rootScope.$apply();
258 | });
259 |
260 | getAvengersExpectations();
261 |
262 | //// tests of the mocked dataservice, not the controller ////
263 |
264 | it('can call fake `dataservice.getAvengersCast`', function() {
265 | // `getAvengersCast` was not specifically configured
266 | // and therefore returns the default empty array
267 | dataservice.getAvengersCast().then(function(cast) {
268 | expect(cast).to.have.length(0);
269 | });
270 | $rootScope.$apply();
271 | // verify that `getAvengersCast` is a spy
272 | expect(dataservice.getAvengersCast).to.have.been.calledOnce;
273 | });
274 |
275 | it('can call fake `dataservice.ready`', function() {
276 | dataservice.ready().then(function(data) {
277 | expect(data).to.equal(dataservice);
278 | });
279 | $rootScope.$apply();
280 | // verify this is actually a spy
281 | expect(dataservice.ready).to.have.been.calledOnce;
282 | });
283 | });
284 |
285 | describe('when stub all dataservice members with canned, failing mockService', function() {
286 |
287 | beforeEach(function() {
288 |
289 | bard.appModule('app.avengers');
290 | bard.inject(this, '$controller', '$log', '$q', '$rootScope', 'dataservice');
291 | stubs.sadService();
292 |
293 | controller = $controller('Avengers');
294 | $rootScope.$apply();
295 | });
296 |
297 | it ('`dataservice.getAvengers` was called', function() {
298 | expect(dataservice.getAvengers).to.have.been.calledOnce;
299 | });
300 |
301 | it('have no avengers because `dataservice.getAvengers` failed', function() {
302 | expect(controller.avengers).to.have.length(0);
303 | });
304 |
305 | it('should have logged activation failure as error', function() {
306 | expect($log.error.logs[0]).to.match(/doomed/);
307 | });
308 |
309 | //// tests of the mocked dataservice, not the controller ////
310 |
311 | it('calling fake `dataservice.getAvengersCast` fails', function() {
312 | dataservice.getAvengersCast()
313 | .then(success)
314 | .catch(function(err) {
315 | expect(err).to.match(/doomed/);
316 | });
317 | $rootScope.$apply();
318 | // verify this is actually a spy
319 | expect(dataservice.getAvengersCast).to.have.been.calledOnce;
320 | });
321 |
322 | it('calling fake `dataservice.ready` fails', function() {
323 | dataservice.ready()
324 | .then(success)
325 | .catch(function(err) {
326 | expect(err).to.match(/doomed/);
327 | });
328 | $rootScope.$apply();
329 | // verify this is actually a spy
330 | expect(dataservice.ready).to.have.been.calledOnce;
331 | });
332 |
333 | function success(data) {
334 | expect('should have failed').to.be.false;
335 | }
336 | });
337 | /////// helpers /////
338 |
339 | function getAvengersExpectations() {
340 |
341 | it ('`dataservice.getAvengers` was called', function() {
342 | expect(getAvengersSpy).to.have.been.calledOnce;
343 | });
344 |
345 | it('controller has fake Avengers', function() {
346 | expect(controller.avengers).to.have.length(avengers.length);
347 | });
348 | }
349 | });
350 |
--------------------------------------------------------------------------------
/src/client/app/avengers/avengers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
Character
16 |
Description
17 |
18 |
19 |
20 |
21 |
24 |
{{c.name}}
25 |
{{c.description | limitTo: 2000 }} ...
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/client/app/avengers/avengers.module.js:
--------------------------------------------------------------------------------
1 | angular.module('app.avengers', ['app.core']);
2 |
--------------------------------------------------------------------------------
/src/client/app/avengers/avengers.route.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.avengers')
6 | .run(appRun);
7 |
8 | /* @ngInject */
9 | function appRun(routehelper) {
10 | routehelper.configureRoutes(getRoutes());
11 | }
12 |
13 | function getRoutes() {
14 | return [
15 | {
16 | url: '/avengers',
17 | config: {
18 | templateUrl: 'app/avengers/avengers.html',
19 | controller: 'Avengers',
20 | controllerAs: 'vm',
21 | title: 'avengers',
22 | settings: {
23 | nav: 2,
24 | content: ' Avengers'
25 | }
26 | }
27 | }
28 | ];
29 | }
30 | })();
31 |
--------------------------------------------------------------------------------
/src/client/app/avengers/avengers.route.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('avengers route', function () {
3 | var controller;
4 | var view = 'app/avengers/avengers.html';
5 |
6 | beforeEach(function() {
7 | module('app.avengers', bard.fakeToastr);
8 | bard.inject(this, '$location', '$route', '$rootScope', '$templateCache');
9 | $templateCache.put(view, '');
10 | });
11 |
12 | it('should map /avengers route to avengers View template', function () {
13 | expect($route.routes['/avengers'].templateUrl).to.equal(view);
14 | });
15 |
16 | it('should route / to the avengers View', function () {
17 | $location.path('/avengers');
18 | $rootScope.$apply();
19 | expect($route.current.templateUrl).to.equal(view);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/client/app/blocks/exception/exception-handler.provider.js:
--------------------------------------------------------------------------------
1 | // Include in index.html so that app level exceptions are handled.
2 | // Exclude from testRunner.html which should run exactly what it wants to run
3 | (function() {
4 | 'use strict';
5 |
6 | angular
7 | .module('blocks.exception')
8 | .provider('exceptionHandler', ExceptionHandlerProvider)
9 | .config(config);
10 |
11 | /**
12 | * Must configure the exception handling
13 | * @return {[type]}
14 | */
15 | function ExceptionHandlerProvider() {
16 | this.config = {
17 | appErrorPrefix: undefined
18 | };
19 |
20 | this.configure = function (appErrorPrefix) {
21 | this.config.appErrorPrefix = appErrorPrefix;
22 | };
23 |
24 | this.$get = function() {
25 | return {config: this.config};
26 | };
27 | }
28 |
29 | /**
30 | * Configure by setting an optional string value for appErrorPrefix.
31 | * Accessible via config.appErrorPrefix (via config value).
32 | * @param {[type]} $provide
33 | * @return {[type]}
34 | */
35 | /* @ngInject */
36 | function config($provide) {
37 | $provide.decorator('$exceptionHandler', extendExceptionHandler);
38 | }
39 |
40 | /**
41 | * Extend the $exceptionHandler service to also display a toast.
42 | * @param {Object} $delegate
43 | * @param {Object} exceptionHandler
44 | * @param {Object} logger
45 | * @return {Function} the decorated $exceptionHandler service
46 | */
47 | /* @ngInject */
48 | function extendExceptionHandler($delegate, exceptionHandler, logger) {
49 | return function(exception, cause) {
50 | var appErrorPrefix = exceptionHandler.config.appErrorPrefix || '';
51 | var errorData = {exception: exception, cause: cause};
52 | exception.message = appErrorPrefix + exception.message;
53 |
54 | /**
55 | * Could add the error to a service's collection,
56 | * add errors to $rootScope, log errors to remote web server,
57 | * or log locally. Or throw hard. It is entirely up to you.
58 | * throw exception;
59 | *
60 | * @example
61 | * throw { message: 'error message we added' };
62 | */
63 | logger.error(exception.message, errorData);
64 |
65 | // Delegate to original as LAST step
66 | $delegate(exception, cause);
67 | };
68 | }
69 | })();
70 |
--------------------------------------------------------------------------------
/src/client/app/blocks/exception/exception-handler.provider.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('blocks / exception-handler', function() {
3 | var error;
4 | var errorPrefix = 'TEST: ';
5 | var errorRe = new RegExp(errorPrefix);
6 |
7 | beforeEach(function() {
8 | module('blocks.exception',
9 | bard.fakeToastr,
10 |
11 | // configure with mock app error prefix
12 | function(exceptionHandlerProvider) {
13 | exceptionHandlerProvider.configure(errorPrefix);
14 | }
15 | );
16 |
17 | bard.inject(this, '$exceptionHandler', '$log', '$rootScope');
18 |
19 | throwAndCatch();
20 | });
21 |
22 | it('error thrown within ng is caught and prefixed', function() {
23 | expect(error.message).to.match(errorRe);
24 | });
25 |
26 | it('error thrown within ng is logged as an error', function() {
27 | expect($log.error.logs).to.have.length(1);
28 | expect($log.error.logs[0][0]).to.match(errorRe);
29 | });
30 |
31 | function throwAndCatch() {
32 | try {
33 | // let ng process the test exception
34 | // by throwing inside $apply
35 | $rootScope.$apply(function() {
36 | throw new Error(mocks.errorMessage);
37 | });
38 | }
39 | catch (ex) {
40 | error = ex;
41 | }
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/src/client/app/blocks/exception/exception.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('blocks.exception')
6 | .service('exception', Exception);
7 |
8 | /* @ngInject */
9 | function Exception(logger) {
10 | this.catcher = catcher;
11 |
12 | function catcher(message) {
13 | return function(reason) {
14 | logger.error(message, reason);
15 | };
16 | }
17 | }
18 | })();
19 |
--------------------------------------------------------------------------------
/src/client/app/blocks/exception/exception.module.js:
--------------------------------------------------------------------------------
1 | angular.module('blocks.exception', ['blocks.logger']);
2 |
--------------------------------------------------------------------------------
/src/client/app/blocks/logger/logger.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('blocks.logger')
6 | .factory('logger', logger);
7 |
8 | /* @ngInject */
9 | function logger($log, toastr) {
10 | var service = {
11 | showToasts: true,
12 |
13 | error : error,
14 | info : info,
15 | success : success,
16 | warning : warning,
17 |
18 | // straight to console; bypass toastr
19 | log : $log.log
20 | };
21 |
22 | return service;
23 | /////////////////////
24 |
25 | function error(message, data, title) {
26 | toastr.error(message, title);
27 | $log.error('Error: ' + message, data);
28 | }
29 |
30 | function info(message, data, title) {
31 | toastr.info(message, title);
32 | $log.info('Info: ' + message, data);
33 | }
34 |
35 | function success(message, data, title) {
36 | toastr.success(message, title);
37 | $log.info('Success: ' + message, data);
38 | }
39 |
40 | function warning(message, data, title) {
41 | toastr.warning(message, title);
42 | $log.warn('Warning: ' + message, data);
43 | }
44 | }
45 | }());
46 |
--------------------------------------------------------------------------------
/src/client/app/blocks/logger/logger.module.js:
--------------------------------------------------------------------------------
1 | /* global toastr:false */
2 | angular
3 | .module('blocks.logger', [])
4 | .constant('toastr', toastr);
5 |
--------------------------------------------------------------------------------
/src/client/app/blocks/logger/logger.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('blocks / logger', function() {
3 | describe('when using real toastr', function() {
4 |
5 | beforeEach(module('blocks.logger'));
6 |
7 | // This is the behavior we want to AVOID in our unit tests
8 | it('it writes to the DOM when test is enabled', function() {
9 |
10 | /* jshint -W027 */
11 | return; // remove this return during demo
12 |
13 | inject(function(logger) {
14 | // watch what happens in a browser test runner
15 | logger.error('Test Error');
16 | logger.success('Test Success');
17 | logger.warning('Test Warning');
18 | });
19 | /* jshint +W027 */
20 | });
21 | });
22 |
23 | describe('when replace with test dummy', function() {
24 | var $log;
25 | var toastr;
26 | var logger;
27 | var testLogMsg = 'a test log message';
28 |
29 | // starting with the 'blocks.logger' module ...
30 | beforeEach(module('blocks.logger',
31 |
32 | // ... replace the 'toastr' recipe with empty dummy
33 | // that will throw exception if used
34 | function($provide) {
35 | $provide.constant('toastr', {});
36 | }
37 | ));
38 |
39 | beforeEach(inject(function(_$log_, _logger_, _toastr_) {
40 | $log = _$log_;
41 | logger = _logger_;
42 | toastr = _toastr_;
43 |
44 | }));
45 |
46 | it('`logger.log` does not call toastr', function() {
47 | // would have thrown if it called toastr
48 | logger.log(testLogMsg);
49 | });
50 |
51 | it('`logger.log` does call `$log.log`', function() {
52 | logger.log(testLogMsg);
53 | var logLogs = $log.log.logs;
54 | expect(logLogs).to.have.length(1, '$log.log.logs');
55 | expect(logLogs[0][0]).to.contain(testLogMsg);
56 | });
57 |
58 | it('toastr is called by logger.info', function() {
59 | expect(function() {
60 | toastr.info(testLogMsg);
61 | }).to.throw(TypeError);
62 | });
63 | });
64 |
65 | describe('when stub toastr with sinon', function() {
66 | var toastr;
67 | var testError = 'a test error message';
68 |
69 | // starting with the 'blocks.logger' module ...
70 | beforeEach(module('blocks.logger'));
71 |
72 | // inject toastr and mock its methods
73 | beforeEach(inject(function (_toastr_) {
74 | toastr = _toastr_;
75 | // mock specific methods
76 | sinon.stub(toastr, 'error');
77 | sinon.stub(toastr, 'info');
78 | sinon.stub(toastr, 'success');
79 | sinon.stub(toastr, 'warning');
80 | }));
81 |
82 | // restore toastr's mocked methods
83 | // ESSENTIAL: because stubbing changed the global! toastr
84 | afterEach(function () {
85 | toastr.error.restore();
86 | toastr.info.restore();
87 | toastr.success.restore();
88 | toastr.warning.restore();
89 | });
90 |
91 | it('calls `toastr.info` when log an info message', function() {
92 | inject(function(logger) {
93 | var testInfo = 'a test info message';
94 | logger.info(testInfo);
95 |
96 | expect(toastr.info).to.be.calledOnce;
97 | expect(toastr.info).to.be.calledWith(testInfo);
98 | expect(toastr.info.getCall(0).args).to.have.length(2,
99 | 'info should be called w/ two args');
100 | });
101 | });
102 |
103 | it('calls `toastr.error` when log an error message', function() {
104 | inject(function(logger) {
105 | var testError = 'a test error message';
106 | logger.error(testError);
107 |
108 | expect(toastr.error).to.be.calledOnce;
109 | expect(toastr.error).to.be.calledWith(testError);
110 | expect(toastr.error.getCall(0).args).to.have.length(2,
111 | 'error should be called w/ two args');
112 | });
113 | });
114 | });
115 |
116 | describe('when stub toastr routinely with $provide', function() {
117 | var toastr;
118 |
119 | // Because we need to fake toastr all over the place
120 | // and do so in apps that might not even use toastr
121 | // we create a service "constant" for this purpose.
122 | //
123 | // See this very same method in bard.js
124 | function fakeToastr($provide) {
125 |
126 | toastr = sinon.stub({
127 | info: function() {},
128 | error: function() {},
129 | warning: function() {},
130 | success: function() {}
131 | });
132 |
133 | $provide.constant('toastr', toastr);
134 | }
135 |
136 | // then simply include it among the test module recipes
137 | // as needed ... as we do here
138 | beforeEach(module('blocks.logger', fakeToastr));
139 |
140 | // afterEach not needed because replacing the toastr service each time
141 |
142 | it('calls `toastr.success` when log a success message', inject(function(logger) {
143 | var testSuccess = 'a test success message';
144 | logger.success(testSuccess);
145 |
146 | expect(toastr.success).to.be.calledOnce;
147 | expect(toastr.success).to.be.calledWith(testSuccess);
148 | expect(toastr.success.getCall(0).args).to.have.length(2,
149 | 'success should be called w/ two args');
150 | }));
151 |
152 | it('calls `toastr.warning` when log a warning message', inject(function(logger) {
153 | var testWarning = 'a test warning message';
154 | logger.warning(testWarning);
155 |
156 | expect(toastr.warning).to.be.calledOnce;
157 | expect(toastr.warning).to.be.calledWith(testWarning);
158 | expect(toastr.warning.getCall(0).args).to.have.length(2,
159 | 'warning should be called w/ two args');
160 | }));
161 | });
162 | });
163 |
--------------------------------------------------------------------------------
/src/client/app/blocks/router/route-helper.provider.js:
--------------------------------------------------------------------------------
1 | /* Help configure the ngRoute router */
2 | (function() {
3 | 'use strict';
4 |
5 | angular
6 | .module('blocks.router')
7 | .provider('routehelperConfig', RoutehelperConfig)
8 | .factory('routehelper', routehelper);
9 |
10 | // Must configure via the routehelperConfigProvider
11 | function RoutehelperConfig() {
12 | this.config = {
13 | listenForRouteChange: false // set true for debugging
14 | // These are the other properties we need to set
15 | // $routeProvider: undefined
16 | // docTitle: ''
17 | // resolveAlways: {ready: function(){ } }
18 | };
19 |
20 | this.$get = function() {
21 | return {
22 | config: this.config
23 | };
24 | };
25 | }
26 |
27 | /* @ngInject */
28 | function routehelper($location, $rootScope, $route, logger, routehelperConfig) {
29 | var handlingRouteChangeError = false;
30 | var routeCounts = {
31 | errors: 0,
32 | changes: 0
33 | };
34 | var routes = [];
35 | var $routeProvider = routehelperConfig.config.$routeProvider;
36 |
37 | var service = {
38 | configureRoutes: configureRoutes,
39 | getRoutes: getRoutes,
40 | routeCounts: routeCounts
41 | };
42 |
43 | init();
44 |
45 | return service;
46 | ///////////////
47 |
48 | function configureRoutes(routes) {
49 | routes.forEach(function(route) {
50 | route.config.resolve =
51 | angular.extend(route.config.resolve || {},
52 | routehelperConfig.config.resolveAlways);
53 | $routeProvider.when(route.url, route.config);
54 | });
55 | $routeProvider.otherwise({redirectTo: '/'});
56 | }
57 |
58 | function getRoutes() {
59 | for (var prop in $route.routes) {
60 | if ($route.routes.hasOwnProperty(prop)) {
61 | var route = $route.routes[prop];
62 | var isRoute = !!route.title;
63 | if (isRoute) {
64 | routes.push(route);
65 | }
66 | }
67 | }
68 | return routes;
69 | }
70 |
71 | function handleRoutingErrors() {
72 | // Route cancellation:
73 | // On routing error, go to the dashboard.
74 | // Provide an exit clause if it tries to do it twice.
75 | $rootScope.$on('$routeChangeError',
76 | function(event, current, previous, rejection) {
77 | if (handlingRouteChangeError) {
78 | return;
79 | }
80 | routeCounts.errors++;
81 | handlingRouteChangeError = true;
82 | var destination = (current && (current.title ||
83 | current.name || current.loadedTemplateUrl)) ||
84 | 'unknown target';
85 | var msg = 'Error routing to ' + destination + '. ' + (rejection.msg || '');
86 | logger.warning(msg, [current]);
87 | $location.path('/');
88 | }
89 | );
90 | }
91 |
92 | function init() {
93 | listenForRouteChange();
94 | handleRoutingErrors();
95 | updateDocTitle();
96 | }
97 |
98 | function listenForRouteChange() {
99 | if (!routehelperConfig.config.listenForRouteChange) {
100 | return; // never listen
101 | }
102 | $rootScope.$on('$routeChangeStart',
103 | function(event, current, previous) {
104 | /* jshint maxcomplexity:false */
105 | // check again; could turn listening on/off
106 | if (!routehelperConfig.config.listenForRouteChange) {
107 | return;
108 | }
109 | var dest;
110 | if (current) {
111 | dest = current.title || current.name || 'unnamed';
112 | dest += ' (controller: ' + current.controller;
113 | dest += ' templateUrl: ' + current.templateUrl + ')';
114 | }
115 | if (!dest) {
116 | dest = 'unknown target';
117 | }
118 | var msg = 'Starting route change to ' + dest;
119 | logger.info(msg, [current]);
120 | }
121 | );
122 | }
123 |
124 | function updateDocTitle() {
125 | $rootScope.$on('$routeChangeSuccess',
126 | function(event, current, previous) {
127 | routeCounts.changes++;
128 | handlingRouteChangeError = false;
129 | var title = routehelperConfig.config.docTitle + ' ' + (current.title || '');
130 | $rootScope.title = title; // data bind to
131 | }
132 | );
133 | }
134 | }
135 | })();
136 |
--------------------------------------------------------------------------------
/src/client/app/blocks/router/route-helper.provider.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('blocks / router route-helper', function () {
3 |
4 | var routehelperConfig, testRoute;
5 |
6 | beforeEach(function () {
7 | module('blocks.router', // not bard.appModule which mocks $route!
8 | bard.fakeToastr,
9 | configureRoutehelper);
10 |
11 | bard.inject(this, '$rootScope', '$route', 'routehelper');
12 |
13 | testRoute = getTestRoute();
14 | });
15 |
16 | it('has no routes before configuration', function() {
17 | expect($route.routes).to.be.empty;
18 | });
19 |
20 | it('`configureRoutes` loads a route', function() {
21 | routehelper.configureRoutes([testRoute]);
22 |
23 | expect($route.routes[testRoute.url])
24 | .to.have.property('title', testRoute.config.title, 'route');
25 | });
26 |
27 | it('a loaded route has a resolve with a `ready`', function() {
28 | routehelper.configureRoutes([testRoute]);
29 |
30 | expect($route.routes[testRoute.url])
31 | .to.have.deep.property('resolve.ready');
32 | });
33 |
34 | it('has the \'otherwise\' route after 1st `configureRoutes`', function() {
35 | routehelper.configureRoutes([testRoute]);
36 |
37 | expect($route.routes[null])
38 | .to.have.property('redirectTo');
39 | });
40 |
41 | it('`configureRoutes` can add multiple routes', function() {
42 | var routes = [testRoute, getTestRoute(2), getTestRoute(3)];
43 | routehelper.configureRoutes(routes);
44 |
45 | routes.forEach(function(r) {
46 | expect($route.routes[r.url]).to.not.be.empty;
47 | });
48 | });
49 |
50 | it('`configureRoutes` adds routes each time called', function() {
51 | var routes1 = [testRoute, getTestRoute(2), getTestRoute(3)];
52 | var routes2 = [getTestRoute(4), getTestRoute(5)];
53 |
54 | routehelper.configureRoutes(routes1);
55 | routehelper.configureRoutes(routes2);
56 |
57 | var routes = routes1.concat(routes2);
58 |
59 | routes.forEach(function(r) {
60 | expect($route.routes[r.url]).to.not.be.empty;
61 | });
62 | });
63 |
64 | it('`$route.routes` preserves the order of routes added', function() {
65 | // in fact, it alphabetizes them
66 | // apparently route order must not matter in route resolution
67 |
68 | // these routes are added in non-alpha order
69 | var routeIds = [1, 3, 2, 42, 4];
70 | var routes = routeIds.map(function(id) {return getTestRoute(id);});
71 |
72 | routehelper.configureRoutes(routes);
73 |
74 | var highestIndex = -1;
75 | var actualRoutes = $route.routes;
76 | angular.forEach(actualRoutes, function(route, key) {
77 | if (key === 'null') { return; } // ignore 'otherwise' route
78 | var m = key.match(/\d+/);
79 | if (m === null) {
80 | expect('route=' + key + ' lacks an id').to.be.false;
81 | }
82 | var ix = routeIds.indexOf(+m[0]);
83 | expect(ix).to.be.at.least(highestIndex, key);
84 | highestIndex = ix;
85 | });
86 | });
87 |
88 | it('`getRoutes` returns just the routes with titles', function() {
89 | var routes = [testRoute, getTestRoute(2), getTestRoute(3)];
90 | routehelper.configureRoutes(routes);
91 |
92 | var routeKeys = Object.keys($route.routes);
93 | var titleRoutes = routehelper.getRoutes();
94 |
95 | expect(routeKeys)
96 | .to.have.length.above(titleRoutes.length, '$routes count');
97 | expect(titleRoutes)
98 | .to.have.length(routes.length, 'title routes');
99 | });
100 |
101 | it('later route, w/ duplicate url, wins', function() {
102 |
103 | routehelper.configureRoutes([testRoute]);
104 | testRoute.config.title = 'duplicate';
105 | routehelper.configureRoutes([testRoute]);
106 |
107 | expect($route.routes[testRoute.url])
108 | .to.have.property('title', 'duplicate', 'route');
109 | });
110 |
111 | ////// helpers /////
112 |
113 | function configureRoutehelper ($routeProvider, routehelperConfigProvider) {
114 | // An app module would configure the routehelper
115 | // in this manner during its config phase
116 | var config = routehelperConfigProvider.config;
117 | config.$routeProvider = $routeProvider;
118 | config.docTitle = 'NG-Testing: ';
119 | var resolveAlways = {
120 | ready: function($q) {
121 | return $q.when('test resolve is always ready');
122 | }
123 | };
124 | config.resolveAlways = resolveAlways;
125 |
126 | // Make it available during tests
127 | routehelperConfig = config;
128 | }
129 |
130 | function getTestRoute(index) {
131 | var test = 'test' + (index || '');
132 | return {
133 | url: '/' + test,
134 | config: {
135 | templateUrl: test + '.html',
136 | controller: test + 'Controller',
137 | controllerAs: 'vm',
138 | title: test,
139 | settings: {
140 | nav: index ? index + 1 : 1,
141 | content: ' ' + test
142 | }
143 | }
144 | };
145 | }
146 | });
147 |
--------------------------------------------------------------------------------
/src/client/app/blocks/router/router.module.js:
--------------------------------------------------------------------------------
1 | angular.module('blocks.router', ['ngRoute', 'blocks.logger']);
2 |
--------------------------------------------------------------------------------
/src/client/app/core/config.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular.module('app.core')
5 | .value('config', config())
6 | .config(toastrConfig)
7 | .config(configure);
8 |
9 | function config() {
10 | return {
11 | //Configure the exceptionHandler decorator
12 | appErrorPrefix: '[NG-Testing Error] ',
13 | appTitle: 'Angular Testing Demo',
14 | version: '1.0.0'
15 | };
16 | }
17 |
18 | /* @ngInject */
19 | function toastrConfig(toastr) {
20 | toastr.options.timeOut = 4000;
21 | toastr.options.positionClass = 'toast-bottom-right';
22 | }
23 |
24 | /* @ngInject */
25 | function configure ($logProvider, $routeProvider,
26 | routehelperConfigProvider, exceptionHandlerProvider) {
27 | // turn debugging off/on (no info or warn)
28 | if ($logProvider.debugEnabled) {
29 | $logProvider.debugEnabled(true);
30 | }
31 |
32 | // Configure the common route provider
33 | routehelperConfigProvider.config.$routeProvider = $routeProvider;
34 | routehelperConfigProvider.config.docTitle = 'NG-Testing: ';
35 | var resolveAlways = {
36 | /* @ngInject */
37 | dataservice: function(dataservice) {
38 | return dataservice.ready();
39 | }
40 | };
41 | routehelperConfigProvider.config.resolveAlways = resolveAlways;
42 |
43 | // Configure the common exception handler
44 | exceptionHandlerProvider.configure(config.appErrorPrefix);
45 | }
46 | })();
47 |
--------------------------------------------------------------------------------
/src/client/app/core/core.module.js:
--------------------------------------------------------------------------------
1 | /* global moment:false */
2 | angular
3 | .module('app.core', [
4 | 'ngAnimate', 'ngRoute', 'ngSanitize',
5 | 'blocks.exception', 'blocks.logger', 'blocks.router',
6 | 'ngplus'
7 | ])
8 | .constant('moment', moment);
9 |
--------------------------------------------------------------------------------
/src/client/app/core/dataservice.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.core')
6 | .service('dataservice', Dataservice);
7 |
8 | /* @ngInject */
9 | function Dataservice($http, $q, exception, logger) {
10 | var readyPromise;
11 | var ds = this;
12 | ds.getAvengers = getAvengers;
13 | ds.getAvengersCast = getAvengersCast;
14 | ds.ready = ready;
15 |
16 | function getAvengers() {
17 | return $http.get('/api/maa')
18 | .then(function (response) {
19 | return response.data;
20 | })
21 | .catch(function(message) {
22 | exception.catcher('XHR Failed for getAvengers')(message);
23 | });
24 | }
25 |
26 | function getAvengersCast() {
27 | return $http.get('/api/maaCast')
28 | .then(function (response) {
29 | return response.data;
30 | })
31 | .catch(function(message) {
32 | exception.catcher('XHR Failed for getAvengersCast')(message);
33 | });
34 | }
35 |
36 | function getReady() {
37 | if (!readyPromise) {
38 | // Apps often pre-fetch session data ("prime the app")
39 | // before showing the first view.
40 | // This app doesn't need priming but we add a
41 | // no-op implementation to show how it would work.
42 | logger.info('Primed the app data');
43 | readyPromise = $q.when(ds);
44 | }
45 | return readyPromise;
46 | }
47 |
48 | function ready(promisesArray) {
49 | return getReady()
50 | .then(function() {
51 | return promisesArray ? $q.all(promisesArray) : readyPromise;
52 | })
53 | .catch(exception.catcher('"ready" function failed'));
54 | }
55 |
56 | }
57 | })();
58 |
--------------------------------------------------------------------------------
/src/client/app/core/dataservice.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('core dataservice', function () {
3 |
4 | var $httpFlush;
5 |
6 | beforeEach(function () {
7 | module('app.core', bard.fakeToastr);
8 | bard.inject(this, '$httpBackend', '$rootScope', 'dataservice');
9 | $httpFlush = $httpBackend.flush;
10 | });
11 |
12 | bard.verifyNoOutstandingHttpRequests();
13 |
14 | it('should be registered', function() {
15 | expect(dataservice).not.to.equal(null);
16 | });
17 |
18 | describe('when call getAvengers', function () {
19 | var avengers;
20 | beforeEach(function() {
21 | avengers = mockData.getAvengers();
22 | $httpBackend.when('GET', '/api/maa')
23 | .respond(200, avengers);
24 | });
25 |
26 | it('should return Avengers', function () {
27 | dataservice.getAvengers()
28 | .then(function(data) {
29 | expect(data.length).to.equal(avengers.length);
30 | });
31 | $httpFlush();
32 | });
33 |
34 | it('should contain Black Widow', function () {
35 | dataservice.getAvengers()
36 | .then(function(data) {
37 | var hasBlackWidow = data.some(function (a) {
38 | return a.name.indexOf('Black Widow') >= 0;
39 | });
40 | expect(hasBlackWidow).to.be.true;
41 | });
42 | $httpFlush();
43 | });
44 | });
45 |
46 | describe('when call getAvengersCast', function () {
47 | var cast;
48 | beforeEach(function() {
49 | cast = mockData.getAvengersCast();
50 | $httpBackend.when('GET', '/api/maaCast')
51 | .respond(200, cast);
52 | });
53 |
54 | it('should return cast', function () {
55 | dataservice.getAvengersCast()
56 | .then(function(data) {
57 | expect(data.length).to.equal(cast.length);
58 | });
59 | $httpFlush();
60 | });
61 |
62 | it('should contain Scarlett Johansson', function () {
63 | dataservice.getAvengersCast()
64 | .then(function(data) {
65 | var hasScarlett = data.some(function (c) {
66 | return c.name === 'Scarlett Johansson';
67 | });
68 | expect(hasScarlett).to.be.true;
69 | });
70 | $httpFlush();
71 | });
72 | });
73 |
74 | describe('ready function', function () {
75 |
76 | it('should return a resolved promise with the dataservice itself', function () {
77 | dataservice.ready()
78 | .then(function(data) {
79 | expect(data).to.equal(dataservice);
80 | });
81 | $rootScope.$apply(); // no $http so just flush $q
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/dashboard.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.dashboard')
6 | .controller('Dashboard', Dashboard);
7 |
8 | /* @ngInject */
9 | function Dashboard(dataservice, logger) {
10 | var vm = this;
11 | vm.castCount = 0;
12 | vm.cast = [];
13 | vm.title = 'Dashboard';
14 |
15 | activate();
16 |
17 | function activate() {
18 | return getAvengersCast()
19 | .then(function() {
20 | logger.info('Activated Dashboard View');
21 | })
22 | .catch(function(err) {
23 | logger.error('Dashboard view activation failed: ' + err);
24 | });
25 | }
26 |
27 | function getAvengersCast() {
28 | return dataservice.getAvengersCast().then(function(data) {
29 | vm.cast = data;
30 | vm.castCount = data.length;
31 | return data;
32 | });
33 | }
34 | }
35 | })();
36 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/dashboard.controller.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('dashboard controller', function() {
3 |
4 | var cast = mockData.getAvengersCast();
5 | var controller;
6 |
7 | beforeEach(function() {
8 | bard.appModule('app.dashboard');
9 | bard.inject(this, '$controller', '$log', '$q', '$rootScope', 'dataservice');
10 | });
11 |
12 | beforeEach(function () {
13 | sinon.stub(dataservice, 'getAvengersCast')
14 | .returns($q.when(cast));
15 |
16 | controller = $controller('Dashboard');
17 | $rootScope.$apply();
18 | });
19 |
20 | it('should be created successfully', function () {
21 | expect(controller).to.be.defined;
22 | });
23 |
24 | it('should have title of Dashboard', function () {
25 | expect(controller.title).to.equal('Dashboard');
26 | });
27 |
28 | it('should have called `dataservice.getAvengersCast` once', function() {
29 | expect(dataservice.getAvengersCast).to.have.been.calledOnce;
30 | });
31 |
32 | it('should have the expected avengers cast', function () {
33 | expect(controller.cast).to.have.length(cast.length);
34 | });
35 |
36 | it('should have the expected cast count', function () {
37 | expect(controller.castCount).to.equal(cast.length);
38 | });
39 |
40 | it('should have logged "Activated"', function() {
41 | expect($log.info.logs).to.match(/Activated/);
42 | });
43 |
44 | bard.verifyNoOutstandingHttpRequests();
45 | });
46 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/dashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Stark Tower New York, New York
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{vm.castCount}} Cast
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
Name
36 |
Character
37 |
38 |
39 |
40 |
41 |
{{avenger.name}}
42 |
{{avenger.character}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/dashboard.module.js:
--------------------------------------------------------------------------------
1 | angular.module('app.dashboard', ['app.core']);
2 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/dashboard.route.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.dashboard')
6 | .run(appRun);
7 |
8 | /* @ngInject */
9 | function appRun(routehelper) {
10 | routehelper.configureRoutes(getRoutes());
11 | }
12 |
13 | function getRoutes() {
14 | return [
15 | {
16 | url: '/',
17 | config: {
18 | templateUrl: 'app/dashboard/dashboard.html',
19 | controller: 'Dashboard',
20 | controllerAs: 'vm',
21 | title: 'dashboard',
22 | settings: {
23 | nav: 1,
24 | content: ' Dashboard'
25 | }
26 | }
27 | }
28 | ];
29 | }
30 | })();
31 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/dashboard.route.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('dashboard route', function () {
3 | var controller;
4 | var view = 'app/dashboard/dashboard.html';
5 |
6 | beforeEach(function() {
7 | module('app.dashboard', bard.fakeToastr);
8 | bard.inject(this, '$location', '$route', '$rootScope', '$templateCache');
9 | $templateCache.put(view, '');
10 | });
11 |
12 | it('should map / route to dashboard View template', function () {
13 | expect($route.routes['/'].templateUrl).
14 | to.equal('app/dashboard/dashboard.html');
15 | });
16 |
17 | it('should route / to the dashboard View', function () {
18 | $location.path('/');
19 | $rootScope.$apply();
20 | expect($route.current.templateUrl).to.equal(view);
21 | });
22 |
23 | it('should route /invalid to the otherwise (dashboard) route', function () {
24 | $location.path('/invalid');
25 | $rootScope.$apply();
26 | expect($route.current.templateUrl).to.equal(view);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/news.controller.js:
--------------------------------------------------------------------------------
1 | // A $scope controller
2 | // We strongly favor "controller as" style and discourage mixing styles
3 | // We're deviating from our standard in this one case
4 | // to demonstrate testing of $scope style controllers.
5 | (function() {
6 | 'use strict';
7 |
8 | angular
9 | .module('app.dashboard')
10 | .controller('News', News);
11 |
12 | /* @ngInject */
13 | function News($scope, $interval, $timeout, newsService, logger) {
14 | var refreshHandle, timeoutHandle;
15 |
16 | $scope.news = [];
17 | $scope.title = 'Marvel News';
18 |
19 | activate();
20 | //////////////////////////
21 | function activate() {
22 | $scope.news = [{
23 | title: 'Marvel Avengers',
24 | description: 'No news available at this time'
25 | }];
26 |
27 | // delay first time for demo
28 | timeoutHandle = $timeout(getNews, 2000);
29 |
30 | // get fresh news periodically
31 | refreshHandle = $interval(getNews, 10000);
32 | }
33 |
34 | function getNews() {
35 | return newsService.getTopStories(5)
36 | .then(function(news) {
37 | $scope.news = news;
38 | });
39 | }
40 |
41 | $scope.$on('$destroy', function() {
42 | $timeout.cancel(timeoutHandle);
43 | $interval.cancel(refreshHandle);
44 | });
45 | }
46 | })();
47 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/news.controller.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('dashboard news controller', function() {
3 |
4 | var controller, $scope;
5 | var stories = mockData.getNewsStories();
6 |
7 | beforeEach(function() {
8 | bard.appModule('app.dashboard');
9 | bard.inject(this, '$controller', '$interval', '$q', '$rootScope',
10 | '$timeout', 'newsService');
11 | });
12 |
13 | beforeEach(function () {
14 |
15 | sinon.stub(newsService, 'getTopStories')
16 | .returns($q.when(stories));
17 |
18 | $scope = $rootScope.$new(); // need real $scope for $scope.$on
19 | controller = $controller('News', {$scope: $scope});
20 | $rootScope.$apply();
21 | });
22 |
23 | it('should be created successfully', function () {
24 | expect(controller).to.be.defined;
25 | });
26 |
27 | it('should have title of "Marvel News"', function () {
28 | expect($scope.title).to.equal('Marvel News');
29 | });
30 |
31 | it('should have one news story until newsService loads stories', function () {
32 | expect($scope.news).to.be.length(1);
33 | });
34 |
35 | it('has placeholder story until newsService loads stories', function () {
36 | var story = $scope.news[0];
37 | expect(story.description).to.match(/no news/i, 'story.description');
38 | });
39 |
40 | it('has many stories after newsService loads stories', function () {
41 | $timeout.flush();
42 | expect($scope.news).to.have.length.above(1);
43 | });
44 |
45 | it('refreshes stories periodically', function () {
46 | // Must know at least the minimum interval;
47 | // picked big test interval to trigger many refreshes
48 | $interval.flush(100000);
49 | expect(newsService.getTopStories.callCount).to.be.above(2);
50 | });
51 |
52 | it('stops refreshing stories when the controller is destroyed', function () {
53 | var $destroyEventRaised = false;
54 |
55 | // listen for event when controller (well, its $scope) is destroyed
56 | $scope.$on('$destroy', function() {
57 | $destroyEventRaised = true;
58 | });
59 |
60 | // trigger some newsService activity as time passes
61 | $timeout.flush();
62 | $interval.flush(100000);
63 | var lastCount = newsService.getTopStories.callCount;
64 | expect(lastCount).to.be.above(2);
65 |
66 | // now destroy the controller's scope (as when "close" its view)
67 | // the controller should no longer ask for news refreshes
68 | $scope.$destroy();
69 |
70 | // let more time pass;
71 | $interval.flush(100000);
72 |
73 | expect($destroyEventRaised).to.equal(true,
74 | 'destroy event raised');
75 |
76 | expect(newsService.getTopStories.callCount).to.equal(lastCount,
77 | 'there should have been no more newsService calls');
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/news.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 | {{item.description}}
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/newsService.js:
--------------------------------------------------------------------------------
1 | // Get news about Marvel stuff. Only needed in Dashboard.
2 | (function() {
3 | 'use strict';
4 |
5 | angular
6 | .module('app.dashboard')
7 | .service('newsService', NewsService);
8 |
9 | /* @ngInject */
10 | function NewsService($q, $timeout, exception, logger) {
11 | this.getTopStories = getTopStories;
12 |
13 | ///////////////////////
14 | function getTopStories(count) {
15 | count = (count == null) ? 3 : count;
16 | var deferred = $q.defer();
17 | // simulate 1/2 second latency
18 | $timeout(function() {
19 | deferred.resolve(topStories(count));
20 | }, 500);
21 | return deferred.promise;
22 | }
23 |
24 | // Test data. Source: http://marvel.com/news/
25 | function topStories(count) {
26 | count = Math.max(1, Math.min(count, 5));
27 | var stories = [
28 | {title: 'Avengers Movies',
29 | description: 'The Avengers: Age of Ultron opens in U.S. theaters on May 1st'},
30 | {title: 'Avengers Romance',
31 | description: 'Ooo la la: are Dr. Banner and Natasha getting busy?'},
32 | {title: 'Marvel PSA',
33 | description: 'Earth\'s Heroes Take a Stand in Avengers: No More Bullying #1'},
34 | {title: 'Marvel TV',
35 | description: 'Marvel\'s Agent Carter Debriefs Her First 2 Missions'},
36 | {title: 'Marvel Comics',
37 | description: 'Thor: Meet the new female hero who will wield Mjolnir!'},
38 | {title: 'Marvel Netflix',
39 | description: 'Krysten Ritter to Star in Marvel\'s A.K.A. Jessica Jones'},
40 | {title: 'Marvel Movies',
41 | description: 'Benedict Cumberbatch to Play Doctor Strange'},
42 | {title: 'Marvel Merchandise',
43 | description: 'Let Some Gamma Rays Into Your Life With Hulk Collectibles'},
44 | {title: 'Marvel Animated',
45 | description: 'Spidey Fights Visions of the Future'},
46 | {title: 'Marvel TV',
47 | description: 'Agent Skye Faces Off Against A Familiar Foe'},
48 | {title: 'Marvel Music',
49 | description: 'Guardians of the Galaxy "Awesome Mix Vol. 1"' +
50 | ' Is Certified Awesome by the Grammys'}
51 | ];
52 |
53 | var len = stories.length, results = [];
54 | while (results.length < count) {
55 | var story = stories[Math.floor(Math.random() * len)];
56 | if (results.indexOf(story) === -1) {
57 | results.push(story);
58 | }
59 | }
60 | return results;
61 | }
62 | }
63 | })();
64 |
--------------------------------------------------------------------------------
/src/client/app/dashboard/newsService.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('dashboard newsService call getTopStories', function () {
3 |
4 | var flush;
5 |
6 | beforeEach(function () {
7 | bard.appModule('app.dashboard');
8 | bard.inject(this, '$timeout', 'newsService');
9 |
10 | // We know that the newsService is actually a fake
11 | // so we don't bother pretending we need $httpBackend
12 | // as we do in dataservice.spec.
13 |
14 | // $timeout is used to simulate latency so we'll need
15 | // $timeout flush rather than $httpBackend flush
16 | flush = $timeout.flush;
17 | });
18 |
19 | it('should return 3 stories when called w/ no args', function(done) {
20 | newsService.getTopStories()
21 | .then(function(stories) {
22 |
23 | expect(stories).to.have.length(3);
24 |
25 | }).then(done, done);
26 | flush();
27 | });
28 |
29 | it('should return 1 story when called w/ 1', function(done) {
30 | newsService.getTopStories(1)
31 | .then(function(stories) {
32 |
33 | expect(stories).to.have.length(1);
34 |
35 | }).then(done, done);
36 | flush();
37 | });
38 |
39 | it('should return 1 story when called w/ <1', function(done) {
40 | newsService.getTopStories(0)
41 | .then(function(stories) {
42 |
43 | expect(stories).to.have.length(1);
44 |
45 | }).then(done, done);
46 | flush();
47 | });
48 |
49 | it('should return 5 stories when called w/ 5', function(done) {
50 | newsService.getTopStories(5)
51 | .then(function(stories) {
52 |
53 | expect(stories).to.have.length(5);
54 |
55 | }).then(done, done);
56 | flush();
57 | });
58 |
59 | it('should return 5 stories when called w/ > 5', function(done) {
60 | newsService.getTopStories(6)
61 | .then(function(stories) {
62 |
63 | expect(stories).to.have.length(5);
64 |
65 | }).then(done, done);
66 | flush();
67 | });
68 |
69 | it('should return different story set each call', function(done) {
70 |
71 | // Test could fail if very, very unlucky and service
72 | // randomly returned the same stories in same order twice.
73 |
74 | //bard.inject(this, '$q'); // only need $q in this test.
75 | var $q = this.$injector.get('$q');
76 |
77 | $q.all([
78 | newsService.getTopStories(5),
79 | newsService.getTopStories(5)
80 | ])
81 | .then(function(resolveds) {
82 | var firstSet = resolveds[0];
83 | var secondSet = resolveds[1];
84 | var areDifferent = firstSet.some(function(s, i) {
85 | // is the i-th story of the 1st set different
86 | // from the i-th story of the 2nd set
87 | return secondSet[i] !== s;
88 | });
89 | expect(areDifferent).to.be.true;
90 | })
91 | .then(done, done);
92 |
93 | flush();
94 | });
95 |
96 | });
97 |
--------------------------------------------------------------------------------
/src/client/app/layout/layout.module.js:
--------------------------------------------------------------------------------
1 | angular.module('app.layout', ['app.core']);
2 |
--------------------------------------------------------------------------------
/src/client/app/layout/shell.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.layout')
6 | .controller('Shell', Shell);
7 |
8 | /* @ngInject */
9 | function Shell($timeout, config, logger) {
10 | var vm = this;
11 |
12 | vm.title = config.appTitle;
13 | vm.busyMessage = 'Please wait ...';
14 | vm.isBusy = true;
15 | vm.showSplash = true;
16 |
17 | activate();
18 |
19 | function activate() {
20 | logger.success(config.appTitle + ' loaded!', null);
21 | hideSplash();
22 | }
23 |
24 | function hideSplash() {
25 | //Force a 1 second delay so we can see the splash.
26 | $timeout(function() {
27 | vm.showSplash = false;
28 | }, 1000);
29 | }
30 | }
31 | })();
32 |
--------------------------------------------------------------------------------
/src/client/app/layout/shell.controller.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('layout shell controller', function() {
3 | var controller;
4 | var $log;
5 | var $timeout;
6 |
7 | beforeEach(function() {
8 | module('app.layout');
9 |
10 | inject(function($controller, _$log_, _$timeout_, toastr) {
11 | // Crazy stuff we do to disable the toastr
12 | toastr.info = function() {};
13 | toastr.error = function() {};
14 | toastr.warning = function() {};
15 | toastr.success = function() {};
16 |
17 | $log = _$log_;
18 | $timeout = _$timeout_;
19 | controller = $controller('Shell');
20 | });
21 | });
22 |
23 | it('should have logged success on activation', function() {
24 | // passes if ANY of the logs matches
25 | expect($log.info.logs).to.match(/success/i);
26 | });
27 |
28 | it('should should hide splash after delay', function() {
29 | var vm = controller;
30 | expect(vm.showSplash).to.equal(true, 'showSplash before delay');
31 | $timeout.flush();
32 | expect(vm.showSplash).to.equal(false, 'showSplash after delay');
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/client/app/layout/shell.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Testing Demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
{{vm.busyMessage}}
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/client/app/layout/sidebar.controller.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.layout')
6 | .controller('Sidebar', Sidebar);
7 |
8 | /* @ngInject */
9 | function Sidebar($route, routehelper) {
10 | var vm = this;
11 | vm.isCurrent = isCurrent;
12 | //vm.sidebarReady = function(){console.log('done animating menu')}; // example
13 |
14 | activate();
15 |
16 | function activate() { getNavRoutes(); }
17 |
18 | function getNavRoutes() {
19 | vm.navRoutes = routehelper.getRoutes()
20 | .filter(function(r) {
21 | return r.settings && r.settings.nav;
22 | })
23 | .sort(function(r1, r2) {
24 | return r1.settings.nav - r2.settings.nav;
25 | });
26 | }
27 |
28 | function isCurrent(route) {
29 | if (!route || !route.title || !$route.current || !$route.current.title) {
30 | return '';
31 | }
32 | var menuName = route.title;
33 | return $route.current.title.substr(0, menuName.length) === menuName ? 'current' : '';
34 | }
35 | }
36 | })();
37 |
--------------------------------------------------------------------------------
/src/client/app/layout/sidebar.controller.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W030 */
2 | describe('layout sidebar controller', function () {
3 | var controller;
4 |
5 | beforeEach(function() {
6 | // Setup for entire app because each feature module adds its own routes
7 | // 'templates' populates $templateCache with all views
8 | // so that tests don't try to retrieve view templates from the server.
9 | module('app', 'templates', bard.fakeToastr);
10 | bard.inject(this, '$controller', '$location', '$rootScope', '$route');
11 | });
12 |
13 | beforeEach(function () {
14 | controller = $controller('Sidebar');
15 | });
16 |
17 | it('before navigating, isCurrent() should NOT return `current`', function () {
18 | expect(controller.isCurrent({title: 'invalid'})).not.to.equal('current');
19 | });
20 |
21 | // Confirm that, after navigating successfully,
22 | // controller.isCurrent() returns the class name `current`
23 | // for the router's current route (the browser's current address)
24 | it('after going to `/`, isCurrent() should return `current`', function () {
25 | $location.path('/');
26 | $rootScope.$apply();
27 | expect(controller.isCurrent($route.current)).to.equal('current');
28 | });
29 |
30 | it('after going to `/avengers`, isCurrent() should return `current`', function () {
31 | $location.path('/avengers');
32 | $rootScope.$apply();
33 | expect(controller.isCurrent($route.current)).to.equal('current');
34 | });
35 |
36 | it('after going to an invalid route, isCurrent() should NOT return `current`', function () {
37 | $location.path('/invalid');
38 | $rootScope.$apply();
39 | expect(controller.isCurrent({title: 'invalid'})).not.to.equal('current');
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/client/app/layout/sidebar.html:
--------------------------------------------------------------------------------
1 |
16 | var directive = {
17 | link: link,
18 | restrict: 'A',
19 | scope: {
20 | whenDoneAnimating: '&?'
21 | }
22 | };
23 | return directive;
24 |
25 | function link(scope, element, attrs) {
26 | var $sidebarInner = element.find('.sidebar-inner');
27 | var $dropdownElement = element.find('.sidebar-dropdown a');
28 | element.addClass('sidebar');
29 | $dropdownElement.click(dropdown);
30 |
31 | function dropdown(e) {
32 | var dropClass = 'dropy';
33 | e.preventDefault();
34 | if (!$dropdownElement.hasClass(dropClass)) {
35 | $sidebarInner.slideDown(350, scope.whenDoneAnimating);
36 | $dropdownElement.addClass(dropClass);
37 | } else if ($dropdownElement.hasClass(dropClass)) {
38 | $dropdownElement.removeClass(dropClass);
39 | $sidebarInner.slideUp(350, scope.whenDoneAnimating);
40 | }
41 | }
42 | }
43 | }
44 | })();
45 |
--------------------------------------------------------------------------------
/src/client/app/widgets/ccSidebar.spec.js:
--------------------------------------------------------------------------------
1 | /* jshint -W117, -W109, -W030 */
2 | describe('widgets ccSidebar directive', function () {
3 | var dropdownElement;
4 | var el;
5 | var innerElement;
6 | var isOpenClass = 'dropy';
7 | var scope;
8 |
9 | beforeEach(module('app.widgets'));
10 |
11 | beforeEach(inject(function($compile, $rootScope) {
12 | // The minimum necessary template HTML for this spec.
13 | // Simulates a menu link that opens and closes a dropdown of menu items
14 | // The `when-done-animating` attribute is optional (as is the vm's implementation)
15 | //
16 | // N.B.: the attribute value is supposed to be an expression that invokes a $scope method
17 | // so make sure the expression includes '()', e.g., "vm.sidebarReady(42)"
18 | // no harm if the expression fails ... but then scope.sidebarReady will be undefined.
19 | // All parameters in the expression are passed to vm.sidebarReady ... if it exists
20 | //
21 | // N.B.: We do NOT add this element to the browser DOM (although we could).
22 | // spec runs faster if we don't touch the DOM (even the PhantomJS DOM).
23 |
24 | /*jshint multistr:true */
25 | el = angular.element(
26 | '
');
30 | /*jshint multistr:false */
31 |
32 | // The spec examines changes to these template parts
33 | dropdownElement = el.find('.sidebar-dropdown a'); // the link to click
34 | innerElement = el.find('.sidebar-inner'); // container of menu items
35 |
36 | // ng's $compile service resolves nested directives (there are none in this example)
37 | // and binds the element to the scope (which must be a real ng scope)
38 | scope = $rootScope;
39 | $compile(el)(scope);
40 |
41 | // tell angular to look at the scope values right now
42 | scope.$digest();
43 | }));
44 |
45 | /// tests ///
46 | describe('the isOpenClass', function () {
47 | it('is absent for a closed menu', function () {
48 | hasIsOpenClass(false);
49 | });
50 |
51 | it('is added to a closed menu after clicking', function () {
52 | clickIt();
53 | hasIsOpenClass(true);
54 | });
55 |
56 | it('is present for an open menu', function () {
57 | openDropdown();
58 | hasIsOpenClass(true);
59 | });
60 |
61 | it('is removed from a closed menu after clicking', function () {
62 | openDropdown();
63 | clickIt();
64 | hasIsOpenClass(false);
65 | });
66 | });
67 |
68 | describe('when animating w/ jQuery fx off', function () {
69 | beforeEach(function () {
70 | // remember current state of jQuery's global FX duration switch
71 | this.oldFxOff = $.fx.off;
72 | // when jQuery fx are of, there is zero animation time; no waiting for animation to complete
73 | $.fx.off = true;
74 | // must add to DOM when testing jQuery animation result
75 | el.appendTo(document.body);
76 | });
77 |
78 | afterEach(function () {
79 | $.fx.off = this.oldFxOff;
80 | el.remove();
81 | });
82 |
83 | it('dropdown is visible after opening a closed menu', function () {
84 | dropdownIsVisible(false); // hidden before click
85 | clickIt();
86 | dropdownIsVisible(true); // visible after click
87 | });
88 |
89 | it('dropdown is hidden after closing an open menu', function () {
90 | openDropdown();
91 | dropdownIsVisible(true); // visible before click
92 | clickIt();
93 | dropdownIsVisible(false); // hidden after click
94 | });
95 |
96 | it('click triggers "when-done-animating" expression', function () {
97 |
98 | // spy on directive's callback when the animation is done
99 | var spy = sinon.spy();
100 |
101 | // Recall the pertinent tag in the template ...
102 | // '
103 | // therefore, the directive looks for scope.vm.sidebarReady
104 | // and should call that method with the value '42'
105 | scope.vm = {sidebarReady: spy};
106 |
107 | // tell angular to look again for that vm.sidebarReady property
108 | scope.$digest();
109 |
110 | // spy not called until after click which triggers the animation
111 | expect(spy).not.to.have.been.called;
112 |
113 | // this click triggers an animation
114 | clickIt();
115 |
116 | // verify that the vm's method (sidebarReady) was called with '42'
117 | // FYI: spy.args[0] is the array of args passed to sidebarReady()
118 | expect(spy).to.have.been.calledWith(42);
119 | });
120 | });
121 |
122 | /////// helpers //////
123 |
124 | // put the dropdown in the 'menu open' state
125 | function openDropdown() {
126 | dropdownElement.addClass(isOpenClass);
127 | innerElement.css('display', 'block');
128 | }
129 |
130 | // click the "menu" link
131 | function clickIt() {
132 | dropdownElement.trigger('click');
133 | }
134 |
135 | // assert whether the "menu" link has the class that means 'is open'
136 | function hasIsOpenClass(isTrue) {
137 | var hasClass = dropdownElement.hasClass(isOpenClass);
138 | expect(hasClass).equal(!!isTrue,
139 | 'dropdown has the "is open" class is ' + hasClass);
140 | }
141 |
142 | // assert whether the dropdown container is 'block' (visible) or 'none' (hidden)
143 | function dropdownIsVisible(isTrue) {
144 | var display = innerElement.css('display');
145 | expect(display).to.equal(isTrue ? 'block' : 'none',
146 | 'innerElement display value is ' + display);
147 | }
148 |
149 | ////////// uncomment only during demonstration ///////
150 | // What if you don't know about turning JQuery animation durations off ($.fx.off)?
151 | // You have to write async tests and perhaps guess at animation duration
152 | /*
153 | describe('when animating w/ jQuery fx turned on', function () {
154 | beforeEach(function () {
155 | // must add to DOM when testing jQuery animation result
156 | el.appendTo(document.body);
157 | });
158 |
159 | afterEach(function () {
160 | el.remove();
161 | });
162 |
163 | it('dropdown is visible after opening menu - async', function (done) {
164 |
165 | dropdownIsVisible(false); // should be hidden when we start
166 | clickIt();
167 |
168 | setTimeout(function () {
169 | try{
170 | console.log('async after open animate');
171 | // should be visible after animation
172 | dropdownIsVisible(true);
173 | done();
174 | } catch(e) {
175 | done(e);
176 | }
177 | }, 400); // guess at animation time + a little more
178 | });
179 |
180 | it('dropdown is hidden after closing menu - async', function (done) {
181 | openDropdown();
182 |
183 | dropdownIsVisible(true); // should be visible when we start
184 | clickIt();
185 |
186 | setTimeout(function () {
187 | try{
188 | console.log('async after close animate');
189 | // should be hidden after animation
190 | dropdownIsVisible(false);
191 | done();
192 | } catch(e) {
193 | done(e);
194 | }
195 | }, 400); // guess at animation time; then add a little more
196 | });
197 | });
198 | */
199 | });
200 |
--------------------------------------------------------------------------------
/src/client/app/widgets/ccWidgetHeader.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('app.widgets')
6 | .directive('ccWidgetHeader', ccWidgetHeader);
7 |
8 | function ccWidgetHeader () {
9 | // jscs:disable validateIndentation
10 | //Usage:
11 | //
12 | // Creates:
13 | //
22 | Click to start over.
24 | Click a description title to run its specs only
25 | (see "
26 | ?grep" in address bar).
27 | Click a spec title to see its implementation.
28 |