├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── README.md ├── bower.json ├── gulp.config.js ├── gulpfile.js ├── karma.conf.js ├── package.json ├── src ├── basics │ ├── basics.html │ └── basics.js ├── client │ ├── app │ │ ├── app.module.js │ │ ├── avengers │ │ │ ├── avengers.controller.js │ │ │ ├── avengers.controller.spec.js │ │ │ ├── avengers.html │ │ │ ├── avengers.module.js │ │ │ ├── avengers.route.js │ │ │ └── avengers.route.spec.js │ │ ├── blocks │ │ │ ├── exception │ │ │ │ ├── exception-handler.provider.js │ │ │ │ ├── exception-handler.provider.spec.js │ │ │ │ ├── exception.js │ │ │ │ └── exception.module.js │ │ │ ├── logger │ │ │ │ ├── logger.js │ │ │ │ ├── logger.module.js │ │ │ │ └── logger.spec.js │ │ │ └── router │ │ │ │ ├── route-helper.provider.js │ │ │ │ ├── route-helper.provider.spec.js │ │ │ │ └── router.module.js │ │ ├── core │ │ │ ├── config.js │ │ │ ├── core.module.js │ │ │ ├── dataservice.js │ │ │ └── dataservice.spec.js │ │ ├── dashboard │ │ │ ├── dashboard.controller.js │ │ │ ├── dashboard.controller.spec.js │ │ │ ├── dashboard.html │ │ │ ├── dashboard.module.js │ │ │ ├── dashboard.route.js │ │ │ ├── dashboard.route.spec.js │ │ │ ├── news.controller.js │ │ │ ├── news.controller.spec.js │ │ │ ├── news.html │ │ │ ├── newsService.js │ │ │ └── newsService.spec.js │ │ ├── layout │ │ │ ├── layout.module.js │ │ │ ├── shell.controller.js │ │ │ ├── shell.controller.spec.js │ │ │ ├── shell.html │ │ │ ├── sidebar.controller.js │ │ │ ├── sidebar.controller.spec.js │ │ │ ├── sidebar.html │ │ │ └── topnav.html │ │ └── widgets │ │ │ ├── ccSidebar.js │ │ │ ├── ccSidebar.spec.js │ │ │ ├── ccWidgetHeader.js │ │ │ ├── widgetheader.html │ │ │ └── widgets.module.js │ ├── images │ │ ├── AngularJS-small.png │ │ ├── avengersicon-xs.png │ │ ├── avengersicon.png │ │ ├── busy.gif │ │ ├── gg.png │ │ ├── gruntjs.jpg │ │ └── home.png │ ├── index.html │ ├── specs.html │ ├── styles │ │ └── styles.less │ ├── test-helpers │ │ ├── bard.inject.spec.js │ │ ├── bard.mockService.spec.js │ │ ├── mockData.js │ │ └── stubs.js │ └── tests │ │ └── server-integration │ │ ├── dataservice.spec.js │ │ └── routing.spec.js └── server │ ├── app.js │ ├── data │ ├── maa-cast.json │ └── maa.json │ ├── favicon.ico │ ├── routes │ └── index.js │ └── utils │ ├── errorHandler.js │ └── jsonfileservice.js └── typings └── _references.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Visual Studio Project # 2 | ################### 3 | *.user 4 | *.gpState 5 | *.suo 6 | bin 7 | obj 8 | /packages 9 | /typings/**/*.ts 10 | 11 | # Ignore Node & Bower 12 | ################### 13 | node_modules 14 | build 15 | .tmp 16 | bower_components 17 | **/test/coverage 18 | 19 | 20 | # mongo db 21 | ################### 22 | #Don't commit Mongo Database files 23 | *.lock 24 | *.0 25 | *.1 26 | *.ns 27 | journal 28 | 29 | # Ignore Web Storm # 30 | .idea 31 | 32 | # Compiled source # 33 | ################### 34 | *.com 35 | *.class 36 | *.dll 37 | *.exe 38 | *.o 39 | *.so 40 | 41 | # Packages # 42 | ############ 43 | # it's better to unpack these files and commit the raw source 44 | # git has its own built in compression methods 45 | *.7z 46 | *.dmg 47 | *.gz 48 | *.iso 49 | *.jar 50 | *.rar 51 | *.tar 52 | *.xap 53 | *.zip 54 | 55 | # Logs and databases # 56 | ###################### 57 | *.log 58 | *.sql 59 | *.sqlite 60 | # *.sdf 61 | *.mdf 62 | *.ldf 63 | 64 | # OS generated files # 65 | ###################### 66 | .DS_Store* 67 | ehthumbs.db 68 | Icon? 69 | Thumbs.db 70 | packages 71 | ~$*.pp* 72 | 73 | # Plato generated files # 74 | ###################### 75 | /report 76 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": ["node_modules/**", "bower_components/**"], 3 | 4 | "requireCurlyBraces": [ 5 | "if", 6 | "else", 7 | "for", 8 | "while", 9 | "do", 10 | "try", 11 | "catch" 12 | ], 13 | "requireOperatorBeforeLineBreak": true, 14 | "requireCamelCaseOrUpperCaseIdentifiers": true, 15 | "maximumLineLength": { 16 | "value": 100, 17 | "allowComments": true, 18 | "allowRegex": true 19 | }, 20 | "validateIndentation": 4, 21 | "validateQuoteMarks": "'", 22 | 23 | "disallowMultipleLineStrings": true, 24 | "disallowMixedSpacesAndTabs": true, 25 | "disallowTrailingWhitespace": true, 26 | "disallowSpaceAfterPrefixUnaryOperators": true, 27 | "disallowMultipleVarDecl": null, 28 | 29 | "requireSpaceAfterKeywords": [ 30 | "if", 31 | "else", 32 | "for", 33 | "while", 34 | "do", 35 | "switch", 36 | "return", 37 | "try", 38 | "catch" 39 | ], 40 | "requireSpaceBeforeBinaryOperators": [ 41 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 42 | "&=", "|=", "^=", "+=", 43 | 44 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 45 | "|", "^", "&&", "||", "===", "==", ">=", 46 | "<=", "<", ">", "!=", "!==" 47 | ], 48 | "requireSpaceAfterBinaryOperators": true, 49 | "requireSpacesInConditionalExpression": true, 50 | "requireSpaceBeforeBlockStatements": true, 51 | "requireLineFeedAtFileEnd": true, 52 | "disallowSpacesInsideObjectBrackets": "all", 53 | "disallowSpacesInsideArrayBrackets": "all", 54 | "disallowSpacesInsideParentheses": true, 55 | 56 | "validateJSDoc": { 57 | "checkParamNames": true, 58 | "requireParamTypes": true 59 | }, 60 | 61 | "disallowMultipleLineBreaks": true, 62 | 63 | "disallowCommaBeforeLineBreak": null, 64 | "disallowDanglingUnderscores": null, 65 | "disallowEmptyBlocks": null, 66 | "disallowMultipleLineStrings": null, 67 | "disallowTrailingComma": null, 68 | "requireCommaBeforeLineBreak": null, 69 | "requireDotNotation": null, 70 | "requireMultipleVarDecl": null, 71 | "requireParenthesesAroundIIFE": true 72 | } 73 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "es3": false, 7 | "forin": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": "nofunc", 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonbsp": true, 16 | "nonew": true, 17 | "plusplus": false, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": false, 21 | "strict": false, 22 | "maxparams": 10, 23 | "maxdepth": 5, 24 | "maxstatements": 40, 25 | "maxcomplexity": 8, 26 | "maxlen": 120, 27 | 28 | "asi": false, 29 | "boss": false, 30 | "debug": false, 31 | "eqnull": true, 32 | "esnext": false, 33 | "evil": false, 34 | "expr": false, 35 | "funcscope": false, 36 | "globalstrict": false, 37 | "iterator": false, 38 | "lastsemic": false, 39 | "laxbreak": false, 40 | "laxcomma": false, 41 | "loopfunc": true, 42 | "maxerr": 50, 43 | "moz": false, 44 | "multistr": false, 45 | "notypeof": false, 46 | "proto": false, 47 | "scripturl": false, 48 | "shadow": false, 49 | "sub": true, 50 | "supernew": false, 51 | "validthis": false, 52 | "noyield": false, 53 | 54 | "browser": true, 55 | "node": true, 56 | 57 | "globals": { 58 | "angular": false 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | before_install: 10 | - npm install bower -g 11 | - npm install gulp -g 12 | 13 | script: 14 | - npm install 15 | - bower install 16 | - gulp test --startServers --verbose 17 | 18 | cache: 19 | directories: 20 | - bower_components 21 | - node_modules 22 | 23 | notifications: 24 | email: 25 | - wardbell@hotmail.com 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #AngularJS Patterns Testing - Demo 2 | Demonstrates Angular testing 3 | 4 | [![Build Status](https://travis-ci.org/johnpapa/ng-patterns-testing.svg?branch=master)](https://travis-ci.org/johnpapa/ng-patterns-testing) 5 | 6 | >*Opinionated AngularJS style guide for teams by [@john_papa](//twitter.com/john_papa)* 7 | 8 | >More details about the styles and patterns used in this app can be found in my [AngularJS Style Guide](https://github.com/johnpapa/angularjs-styleguide) and my **AngularJS Patterns: Clean Code**(coming soon) course at [Pluralsight](http://pluralsight.com/training/Authors/Details/john-papa) and working in teams. 9 | > 10 | ## Slides 11 | 12 | The slides for Ward's talk which accompanies this project are available on google docs. 13 | 14 | [See the slides](https://docs.google.com/presentation/d/137lgLMtflW3q4SBKrrjVgiuSAi6GC19T4mNVox-4kV8/present) 15 | 16 | ## TODO 17 | 1. Structure will have specs side by side with code, except cross cutting tests 18 | 2. Remove extraneous code unrelated to tests 19 | 3. Add ui-router 20 | 4. Revise from avengers 21 | 22 | ## Structure 23 | /build (created on the fly) 24 | /gulp 25 | /src 26 | /client 27 | /app 28 | /content 29 | /test 30 | /server 31 | /data 32 | /routes 33 | 34 | 35 | ## Installing Node.js and Bower Packages 36 | - Open terminal 37 | - Type `npm install` 38 | 39 | >Do not be alarmed by the occasional sea of red complaining about 40 | the inability to re-build some library. You can ignore these warnings 41 | because we always use the pre-built libraries that were shipped with 42 | the package. 43 | 44 | ## Cleaning 45 | Over time you might accumulate some old libraries. 46 | 47 | It doesn't hurt to occasionally clear the decks by deleting the 48 | "bower\_components" and "node\_modules" folders and re-installing 49 | with `npm install`. 50 | 51 | ## Installing Bower Packages 52 | `npm install` will install these too, but you can do it manually. 53 | - Open terminal 54 | - Type `bower install` 55 | 56 | 57 | ## Testing 58 | There are two ways to test: in the browser and with karma 59 | 60 | ### Testing in the browser 61 | 62 | * Open a command or terminal window. 63 | 64 | * With browser-sync 65 | * in specs.html, comment out `mocha.checkLeaks()` // false leak report from browser sync 66 | * run `gulp serve-specs`

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 | 4 | 5 | 6 | Mocha/Chai Basic Tests 7 | 8 | 14 | 15 | 16 | 17 | 18 |

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 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 |
CharacterDescription
{{c.name}}{{c.description | limitTo: 2000 }} ...
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: '<i class="fa fa-lock"></i> ' + 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 | <section id="dashboard-view" class="mainbar"> 2 | <section class="matter"> 3 | <div class="container"> 4 | <div class="row"> 5 | <div class="col-md-12"> 6 | <ul class="today-datas"> 7 | <li class="blightblue"> 8 | <div class="pull-left"><i class="fa fa-plane"></i></div> 9 | <div class="datas-text pull-right"> 10 | <span class="bold">Stark Tower</span> New York, New York 11 | </div> 12 | <div class="clearfix"></div> 13 | </li> 14 | 15 | <li class="bblue"> 16 | <div class="pull-left avenger-logo"><i class="fa fa-user"></i></div> 17 | <div class="datas-text pull-right"> 18 | <span class="bold">{{vm.castCount}}</span> Cast 19 | </div> 20 | <div class="clearfix"></div> 21 | </li> 22 | 23 | </ul> 24 | </div> 25 | </div> 26 | <div class="row"> 27 | <div class="col-md-6"> 28 | <div class="widget wblue"> 29 | <div data-cc-widget-header title="Avengers Movie Cast" 30 | allow-collapse="true"></div> 31 | <div class="widget-content text-center text-info"> 32 | <table class="table table-condensed table-striped"> 33 | <thead> 34 | <tr> 35 | <th>Name</th> 36 | <th>Character</th> 37 | </tr> 38 | </thead> 39 | <tbody> 40 | <tr data-ng-repeat="avenger in vm.cast"> 41 | <td>{{avenger.name}}</td> 42 | <td>{{avenger.character}}</td> 43 | </tr> 44 | </tbody> 45 | </table> 46 | </div> 47 | <div class="widget-foot"> 48 | <div class="clearfix"></div> 49 | </div> 50 | </div> 51 | </div> 52 | <div ng-include="'app/dashboard/news.html'" class="col-md-6"></div> 53 | </div> 54 | </div> 55 | </section> 56 | </section> 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: '<i class="fa fa-dashboard"></i> 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 | <div class="widget wlightblue" ng-controller="News"> 2 | <div data-cc-widget-header title="{{title}}" 3 | allow-collapse="true"></div> 4 | <div class="widget-content text-info shuffle-animation" 5 | ng-repeat="item in news"> 6 | <small>{{item.description}}</small> 7 | </div> 8 | <div class="widget-foot"> 9 | <div class="clearfix"></div> 10 | </div> 11 | </div> -------------------------------------------------------------------------------- /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 | <div data-ng-controller="Shell as vm"> 2 | <div id="splash-page" data-ng-show="vm.showSplash" class="dissolve-animation"> 3 | <div class="page-splash"> 4 | <div class="page-splash-message"> 5 | Testing Demo 6 | </div> 7 | </div> 8 | </div> 9 | 10 | <header class="clearfix"> 11 | <div data-ng-include="'app/layout/topnav.html'"></div> 12 | </header> 13 | <section id="content" class="content"> 14 | <div data-ng-include="'app/layout/sidebar.html'"></div> 15 | 16 | <div data-ng-view class="shuffle-animation"></div> 17 | 18 | <div ngplus-overlay 19 | ngplus-overlay-delay-in="50" 20 | ngplus-overlay-delay-out="700" 21 | ngplus-overlay-animation="dissolve-animation"> 22 | <img src="../../images/busy.gif"/> 23 | 24 | <div class="page-spinner-message overlay-message">{{vm.busyMessage}}</div> 25 | </div> 26 | </section> 27 | </div> 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 | <div data-cc-sidebar when-done-animating="vm.sidebarReady()"> 2 | <div data-ng-controller="Sidebar as vm"> 3 | <div class="sidebar-filler"></div> 4 | <div class="sidebar-dropdown"><a href="#">Menu</a></div> 5 | <div class="sidebar-inner"> 6 | <div class="sidebar-widget"></div> 7 | <ul class="navi"> 8 | <li class="nlightblue fade-selection-animation" data-ng-class="vm.isCurrent(r)" 9 | data-ng-repeat="r in vm.navRoutes"> 10 | <a href="#{{r.originalPath}}" 11 | data-ng-bind-html="r.settings.content"></a> 12 | </li> 13 | </ul> 14 | </div> 15 | </div> 16 | </div> 17 | -------------------------------------------------------------------------------- /src/client/app/layout/topnav.html: -------------------------------------------------------------------------------- 1 | <nav class="navbar navbar-fixed-top navbar-inverse"> 2 | <div class="navbar-header"> 3 | <a href="/" class="navbar-brand"><span class="brand-title">{{vm.title}}</span></a> 4 | <a class="btn navbar-btn navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> 5 | <span class="icon-bar"></span> 6 | <span class="icon-bar"></span> 7 | <span class="icon-bar"></span> 8 | </a> 9 | </div> 10 | <div class="navbar-collapse collapse"> 11 | <div class="pull-right navbar-logo"> 12 | <ul class="nav navbar-nav pull-right"> 13 | <li> 14 | <a href="http://www.johnpapa.net/hottowel-angular" target="_blank"> 15 | Created by John Papa 16 | </a> 17 | </li> 18 | <li class="dropdown dropdown-big"> 19 | <a href="http://www.angularjs.org" target="_blank"> 20 | <img src="images/AngularJS-small.png" /> 21 | </a> 22 | </li> 23 | <li> 24 | <a href="http://developer.marvel.com/" target="_blank">Marvel API</a> 25 | </li> 26 | </ul> 27 | </div> 28 | </div> 29 | </nav> 30 | -------------------------------------------------------------------------------- /src/client/app/widgets/ccSidebar.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('app.widgets') 6 | .directive('ccSidebar', ccSidebar); 7 | 8 | /* @ngInject */ 9 | function ccSidebar () { 10 | // Opens and closes the sidebar menu. 11 | // Usage: 12 | // <div data-cc-sidebar"> 13 | // <div data-cc-sidebar whenDoneAnimating="vm.sidebarReady()"> 14 | // Creates: 15 | // <div data-cc-sidebar class="sidebar"> 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 | '<div cc-sidebar when-done-animating="vm.sidebarReady(42)" > \ 27 | <div class="sidebar-dropdown"><a href="">Menu</a></div> \ 28 | <div class="sidebar-inner" style="display: none"></div> \ 29 | </div>'); 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 | // ' <div cc-sidebar when-done-animating="vm.sidebarReady(42)" > 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 | //<div data-cc-widget-header title="vm.map.title"></div> 12 | // Creates: 13 | // <div data-cc-widget-header="" 14 | // title="Avengers Movie" 15 | // allow-collapse="true" </div> 16 | // jscs:enable validateIndentation 17 | var directive = { 18 | // link: link, 19 | scope: { 20 | 'title': '@', 21 | 'subtitle': '@', 22 | 'rightText': '@', 23 | 'allowCollapse': '@' 24 | }, 25 | templateUrl: 'app/widgets/widgetheader.html', 26 | restrict: 'A' 27 | }; 28 | return directive; 29 | 30 | // function link(scope, element, attrs) { 31 | // attrs.$set('class', 'widget-head'); 32 | // } 33 | } 34 | })(); 35 | -------------------------------------------------------------------------------- /src/client/app/widgets/widgetheader.html: -------------------------------------------------------------------------------- 1 | <div class="widget-head"> 2 | <div class="page-title pull-left">{{title}}</div> 3 | <small class="page-title-subtle" data-ng-show="subtitle">({{subtitle}})</small> 4 | <div class="widget-icons pull-right" data-ng-if="allowCollapse"> 5 | <a data-cc-widget-minimize></a> 6 | </div> 7 | <small class="pull-right page-title-subtle" data-ng-show="rightText">{{rightText}}</small> 8 | <div class="clearfix"></div> 9 | </div> -------------------------------------------------------------------------------- /src/client/app/widgets/widgets.module.js: -------------------------------------------------------------------------------- 1 | angular.module('app.widgets', []); 2 | -------------------------------------------------------------------------------- /src/client/images/AngularJS-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/client/images/AngularJS-small.png -------------------------------------------------------------------------------- /src/client/images/avengersicon-xs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/client/images/avengersicon-xs.png -------------------------------------------------------------------------------- /src/client/images/avengersicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/client/images/avengersicon.png -------------------------------------------------------------------------------- /src/client/images/busy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/client/images/busy.gif -------------------------------------------------------------------------------- /src/client/images/gg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/client/images/gg.png -------------------------------------------------------------------------------- /src/client/images/gruntjs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/client/images/gruntjs.jpg -------------------------------------------------------------------------------- /src/client/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/client/images/home.png -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <!-- 3 | ng-strict-di is incompatible with 4 | delayed annotation via ngAnnotate 5 | See https://docs.angularjs.org/guide/di 6 | 7 | <html data-ng-app="app" ng-strict-di> 8 | --> 9 | <html data-ng-app="app"> 10 | 11 | <head> 12 | <meta charset="utf-8" /> 13 | <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1" /> 14 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" /> 15 | <title data-ng-bind="title"> 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 |
44 | Angular Patterns Testing Demo 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/client/specs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ng-patterns-testing 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 |

Angular Patterns: Testing

21 |

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 |

29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/client/test-helpers/bard.inject.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint -W117, -W030 */ 2 | describe('bard.inject', function() { 3 | 'use strict'; 4 | 5 | var origDebugging; 6 | 7 | before(function() { 8 | origDebugging = bard.debugging(); 9 | // uncomment to turn bard debug logging on for this spec file 10 | // bard.debugging(true); 11 | }); 12 | 13 | after(function() { 14 | bard.debugging(origDebugging); // restore bard debug logging 15 | }); 16 | 17 | beforeEach(module(function($provide) { 18 | // define a 'nutz' service for testing injector 19 | $provide.service('nutz', function() {}); 20 | })); 21 | 22 | beforeEach('bard.inject.spec top beforeEach', function() { 23 | // Confirm no window pollution from a prior bard.inject() call 24 | bard.log('bard.inject.spec top beforeEach'); 25 | expect(window.$log).to.not.exist; 26 | expect(window.nutz).to.not.exist; 27 | expect(window.baz).to.not.exist; 28 | expect(window.foo).to.not.exist; 29 | }); 30 | 31 | describe('(describe #1):', function() { 32 | it('window.$log and window.nutz should not exist', function() { 33 | expect(window.$log).to.not.exist; 34 | expect(window.nutz).to.not.exist; 35 | }); 36 | }); 37 | 38 | describe('(describe #2):', function() { 39 | 40 | beforeEach('bard.inject.spec describe #2 beforeEach', function() { 41 | bard.log('bard.inject.spec (describe #2) beforeEach'); 42 | bard.inject(this, ['$log', 'nutz']); 43 | }); 44 | 45 | it('true is true', function() { 46 | expect(true).to.be.true; 47 | }); 48 | 49 | it('$log exists', function() { 50 | expect($log).to.exist; 51 | }); 52 | 53 | it('nutz exists', function() { 54 | expect(nutz).to.exist; 55 | }); 56 | }); 57 | 58 | describe('(describe #3):', function() { 59 | 60 | beforeEach('bard.inject.spec describe #3 beforeEach', function() { 61 | bard.log('bard.inject.spec (describe #3) beforeEach'); 62 | // window.$log and window.nutz should not exist before any test 63 | expect(window.$log).to.not.exist; 64 | expect(window.nutz).to.not.exist; 65 | }); 66 | 67 | // Although inject() puts injectables in the window, 68 | // it also removes them after each test 69 | // Notice ... no private vars for $log or nutz! 70 | // ... no injecting of them either. 71 | 72 | it('should set window.$log and window.nutz when call inject w/ string params', function() { 73 | 74 | bard.inject(this, '$log', 'nutz'); 75 | 76 | expect($log).to.exist; 77 | expect(nutz).to.exist; 78 | 79 | // They are actually in the window 80 | expect(window.$log).to.exist; 81 | expect(window.nutz).to.exist; 82 | 83 | }); 84 | 85 | it('should set window.$log and window.nutz when call inject with string array', function() { 86 | 87 | bard.inject(this, ['$log', 'nutz']); 88 | 89 | expect($log).to.exist; 90 | expect(nutz).to.exist; 91 | }); 92 | 93 | it('should set window.$log and window.nutz when call inject with a function', function() { 94 | 95 | bard.inject(this, function($log, nutz) { 96 | // do stuff just as if we called ngMocks.inject 97 | $log.info('use the injected $log'); 98 | }); 99 | 100 | expect($log).to.exist; 101 | expect(nutz).to.exist; 102 | 103 | expect($log.info.logs[0][0]) 104 | .to.equal('use the injected $log', 105 | '$log.info should have been called: '); 106 | }); 107 | 108 | // reinforcing the point that inject adds to globals, not local fn scope 109 | it('locally defined $log hides the $log injected by inject', function() { 110 | var $log; // declaration hides the one in window.$log created by inject 111 | 112 | bard.inject.bind(this)('$q', '$log'); 113 | 114 | expect($log).to.not.exist; 115 | expect(window.$log).to.exist; 116 | }); 117 | 118 | it('should set window.$log & window.foo when call inject("$log","block.foo")', function() { 119 | // register this ridiculous value for just this test 120 | module(function($provide) { 121 | $provide.value('block.foo', 'foo'); 122 | }); 123 | 124 | // Can inject a service with a dotted name! 125 | bard.inject(this, '$log', 'block.foo'); 126 | 127 | expect($log).to.exist; 128 | expect(foo).to.exist; 129 | expect(window.foo).to.exist; 130 | }); 131 | 132 | // This afterEach would fail because it is called BEFORE 133 | // the outer one created by bard to handle window cleaning 134 | // ----------------------------------------------------- 135 | // afterEach('Describe #2 afterEach', function() { 136 | // console.log('---Describe #2 afterEach'); 137 | // // Should have cleaned up after itself 138 | // expect(window.$log).to.not.exist; 139 | // expect(window.nutz).to.not.exist; 140 | // }); 141 | }); 142 | 143 | describe('(describe #4):', function() { 144 | it('window.$log and window.nutz should not exist', function() { 145 | expect(window.$log).to.not.exist; 146 | expect(window.nutz).to.not.exist; 147 | }); 148 | }); 149 | 150 | describe('(describe #5):', function() { 151 | beforeEach(function() { 152 | bard.log('bard.inject.spec (describe #5) beforeEach'); 153 | // register this ridiculous value for just this describe 154 | module(function($provide) { 155 | $provide.value('baz', 'baz'); 156 | }); 157 | 158 | bard.inject(this, 'baz'); // get baz in outer describe 159 | }); 160 | 161 | describe('in nested describe', function() { 162 | it('baz is available from parent describe', function() { 163 | expect(baz).to.exist; 164 | }); 165 | 166 | it('baz from inject() is same object as baz from direct injection', function() { 167 | 168 | inject(function(_baz_) { 169 | expect(baz).to.equal(_baz_); 170 | }); 171 | }); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/client/test-helpers/bard.mockService.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint -W117, -W030 */ 2 | describe('bard.mockService', function() { 3 | 'use strict'; 4 | 5 | var mockService = bard.mockService; 6 | var flush; 7 | var sandbox; 8 | 9 | beforeEach(function() { 10 | module(); 11 | bard.inject(this, '$q', '$rootScope', '$window'); 12 | sandbox = sinon.sandbox.create(); 13 | flush = function() { $rootScope.$apply(); }; 14 | }); 15 | 16 | afterEach(function() { 17 | sandbox.restore(); 18 | }); 19 | 20 | describe('when execute the "real" DoWork service described in the usage example', function() { 21 | var service; 22 | 23 | beforeEach(function() { 24 | service = getDoWorkService(); 25 | }); 26 | 27 | it('`doWork1` returns a resolved promise with the "real" results', function() { 28 | service.doWork1(1, 2) 29 | .then(function(results) { 30 | expect(results).to.deep.equal([1, 2]); 31 | }); 32 | flush(); 33 | }); 34 | 35 | it('`doWork2` calls alert and returns the "real" results', function() { 36 | var alert = sandbox.stub($window, 'alert'); 37 | bard.addGlobals(this, 'alert'); // because sinon adds it! 38 | var results = service.doWork2(); 39 | expect(results).to.equal('pointless'); 40 | expect(alert).to.have.been.calledWith('Hi there'); 41 | }); 42 | 43 | it('`doWork3` returns a resolved promise with the "real" results', function() { 44 | service.doWork3(1, 2) 45 | .then(function(results) { 46 | expect(results).to.deep.equal(['a1', 'a2']); 47 | }); 48 | flush(); 49 | }); 50 | 51 | it('`doWork4` returns the "real" results', function() { 52 | var results = service.doWork4(1, 2); 53 | expect(results).to.equal('Hi from doWork4'); 54 | }); 55 | 56 | it('does not have a `doWork5`', function() { 57 | expect(service).to.not.have.property('doWork5'); 58 | }); 59 | 60 | it('`isActive` should be true', function() { 61 | expect(service.isActive).to.be.true; 62 | }); 63 | }); 64 | 65 | describe('when mock the DoWork service as described in the usage example', function() { 66 | var service; 67 | 68 | beforeEach(function() { 69 | service = mockService(getDoWorkService(), 70 | { // config in the usage example 71 | doWork1: $q.when([{name: 'Bob'}, {name: 'Sally'}]), 72 | doWork2: undefined, 73 | doWork4: function() { return 'Now for a different kind of work';}, 74 | doWork5: $q.reject('bad boy!'), 75 | isActive: false, 76 | _default: $q.when([]) 77 | }); 78 | }); 79 | 80 | it('`doWork1` returns a resolved promise with the fake results', function() { 81 | service.doWork1(1, 2) 82 | .then(function(results) { 83 | expect(results).to.deep.equal([{name: 'Bob'}, {name: 'Sally'}]); 84 | }); 85 | // verify `doWork1` is a spy 86 | expect(service.doWork1).to.have.been.calledWith(1, 2); 87 | flush(); 88 | }); 89 | 90 | it('`doWork2` returns nothing', function() { 91 | var results = service.doWork2(1, 2); 92 | expect(results).to.not.be.defined; 93 | // verify `doWork2` is a spy 94 | expect(service.doWork2).to.have.been.calledWith(1, 2); 95 | }); 96 | 97 | it('`doWork3` returns a resolved promise with config._default (empty array)', function() { 98 | service.doWork3(1, 2).then(expectEmptyArray); 99 | // verify `doWork3` is a spy 100 | expect(service.doWork3).to.have.been.calledWith(1, 2); 101 | flush(); 102 | }); 103 | 104 | it('`doWork4` returns the fake results', function() { 105 | var results = service.doWork4(1, 2); 106 | expect(results).to.match(/different/); 107 | // verify `doWork4` is NOT a spy 108 | expect(service.doWork4).to.not.have.property('restore'); 109 | }); 110 | 111 | it('`doWork5` returns a rejected promise with the faked error', function() { 112 | service.doWork5() 113 | .then(function() { 114 | // Should not come here! 115 | expect('should have failed').to.be.true; 116 | }) 117 | .catch(function(err) { 118 | expect(err).to.match(/bad/); 119 | }); 120 | // verify `doWork5` is a spy 121 | expect(service.doWork5).to.have.been.called; 122 | flush(); 123 | }); 124 | 125 | it('`isActive` should have changed to false', function() { 126 | expect(service.isActive).to.be.false; 127 | }); 128 | }); 129 | 130 | describe('when mock one async method of the DoWork service and default the rest', function() { 131 | // typical usage when mocking dataservice for a controller 132 | // mock the method(s) of interest; let the others do the minimum necessary 133 | var service; 134 | 135 | beforeEach(function() { 136 | service = mockService(getDoWorkService(), 137 | { // config in the usage example 138 | doWork1: $q.when([1, 2, 3]), 139 | _default: $q.when([]) 140 | }); 141 | }); 142 | 143 | it('`doWork1` returns a resolved promise with the fake results', function() { 144 | service.doWork1('foo').then(function(results) { 145 | expect(results).to.deep.equal([1, 2, 3]); 146 | }); 147 | flush(); 148 | }); 149 | 150 | it('`doWork2`-`doWork4` each return resolved promise with empty array', function() { 151 | service.doWork2('could').then(expectEmptyArray); 152 | service.doWork3('be').then(expectEmptyArray); 153 | service.doWork4('anything').then(expectEmptyArray); 154 | flush(); 155 | }); 156 | }); 157 | 158 | describe('when mock one async method of the DoWork service and omit _default', function() { 159 | var service; 160 | 161 | beforeEach(function() { 162 | service = mockService(getDoWorkService(), 163 | { // config in the usage example 164 | doWork1: $q.when([1, 2, 3]) 165 | }); 166 | }); 167 | 168 | it('`doWork1` returns a resolved promise with the fake results', function() { 169 | service.doWork1('foo').then(function(results) { 170 | expect(results).to.deep.equal([1, 2, 3]); 171 | }); 172 | flush(); 173 | }); 174 | 175 | it('`doWork2`-`doWork4` are stubbed to return nothing', function() { 176 | expect(service.doWork2('could')).to.not.be.defined; 177 | expect(service.doWork3('be')).to.not.be.defined; 178 | expect(service.doWork4('anything')).to.not.be.defined; 179 | // but they are stubbed 180 | expect(service.doWork2).to.have.been.calledWith('could'); 181 | expect(service.doWork3).to.have.been.calledWith('be'); 182 | expect(service.doWork4).to.have.been.calledWith('anything'); 183 | 184 | flush(); 185 | }); 186 | }); 187 | 188 | ///// helpers ///// 189 | 190 | // create the exampel DoWork service from bard.mockService usage doc 191 | function getDoWorkService() { 192 | return { 193 | doWork1: function doWork1(a, b) { 194 | return $q.when([].slice.apply(arguments)); 195 | }, 196 | doWork2: function doWork2() { 197 | $window.alert('Hi there'); // something we do NOT want to do in a test 198 | return 'pointless'; 199 | }, 200 | doWork3: function doWork3() { 201 | var args = [].slice.apply(arguments); 202 | // (1, 2) -> [a1, a2] 203 | var results = args.map(function(a) { return 'a' + a;}); 204 | return $q.when(results); 205 | }, 206 | doWork4: function() { 207 | return 'Hi from doWork4'; 208 | }, 209 | isActive: true 210 | }; 211 | } 212 | 213 | function expectEmptyArray(results) { 214 | expect(results).to.deep.equal([]); 215 | } 216 | }); 217 | -------------------------------------------------------------------------------- /src/client/test-helpers/mockData.js: -------------------------------------------------------------------------------- 1 | /* jshint -W079, -W101, -W109, maxlen:120, quotmark:double */ 2 | /* jscs:disable validateQuoteMarks, maximumLineLength */ 3 | var mockData = (function() { 4 | return { 5 | getAvengers: getAvengers, 6 | getAvengersCast: getAvengersCast, 7 | getNewsStories: getNewsStories 8 | }; 9 | 10 | function getAvengers() { 11 | return [ 12 | { 13 | "id": 1017109, 14 | "name": "Black Widow/Natasha Romanoff (MAA)", 15 | "description": "Natasha Romanoff, also known as Black Widow, is a world-renowned super" + 16 | " spy and one of S.H.I.E.L.D.\'s top agents. Her hand-to-hand combat skills, intelligence," + 17 | " and unpredictability make her a deadly secret weapon. True to her mysterious nature," + 18 | " Black Widow comes and goes as she pleases, but always appears exactly when her particular" + 19 | " skills are needed.", 20 | "thumbnail": { 21 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/a/03/523219743a99b", 22 | "extension": "jpg" 23 | } 24 | }, 25 | { 26 | "id": 1017105, 27 | "name": "Captain America/Steve Rogers (MAA)", 28 | "description": "During World War II, Steve Rogers enlisted in the military and was injected" + 29 | " with a super-serum that turned him into super-soldier Captain America! He\'s a" + 30 | " skilled strategist and even more skilled with his shield, but it\'s his courage" + 31 | " and good heart that makes Captain America both a leader and a true hero. ", 32 | "thumbnail": { 33 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/3/10/52321928eaa72", 34 | "extension": "jpg" 35 | } 36 | }, 37 | { 38 | "id": 1017108, 39 | "name": "Hawkeye/Clint Barton (MAA)", 40 | "description": "Hawkeye is an expert archer with an attitude just as on-target as his aim." + 41 | " His stealth combat experience and his ability to hit any target with any projectile" + 42 | " make him a valuable member of the Avengers. However, he refuses to let things get" + 43 | " too serious, as he has as many jokes as he does arrows!", 44 | "thumbnail": { 45 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/4/03/5232198a81c17", 46 | "extension": "jpg" 47 | } 48 | }, 49 | { 50 | "id": 1017104, 51 | "name": "Iron Man/Tony Stark (MAA)", 52 | "description": "Tony Stark is the genius inventor/billionaire/philanthropist owner of Stark" + 53 | " Industries. With his super high-tech Iron Man suit, he is practically indestructible," + 54 | " able to fly, and has a large selection of weapons to choose from - but it\'s Tony\'s" + 55 | " quick thinking and ability to adapt and improvise that make him an effective leader" + 56 | " of the Avengers.", 57 | "thumbnail": { 58 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/2/d0/5232190d42df2", 59 | "extension": "jpg" 60 | } 61 | }, 62 | { 63 | "id": 1017106, 64 | "name": "Thor (MAA)", 65 | "description": "Thor is the Asgardian Prince of Thunder, the son of Odin, and the realm\'s" + 66 | " mightiest warrior. He loves the thrill of battle and is always eager to show off his" + 67 | " power to the other Avengers, especially the Hulk. Thor\'s legendary Uru hammer," + 68 | " Mjolnir, gives him the power to control thunder and the ability to fly. He\'s found" + 69 | " a new home on Earth and will defend it as his own... even if he doesn\'t understand" + 70 | " its sayings and customs.", 71 | "thumbnail": { 72 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/2/03/52321948a51f2", 73 | "extension": "jpg" 74 | } 75 | }, 76 | { 77 | "id": 42, 78 | "name": "Spooky", 79 | "description": "Spooky has no known super power." + 80 | " He\'s just some guy that likes to run around with a sheet on his head.", 81 | "thumbnail": { 82 | "path": "http://images.clipartpanda.com/ghost-clip-art-1216306562167833124lemmling_Cartoon_ghost.svg.med", 83 | "extension": "png" 84 | } 85 | } 86 | ]; 87 | } 88 | function getAvengersCast() { 89 | return [ 90 | {"name": "Robert Downey Jr.", "character": "Tony Stark \/ Iron Man"}, 91 | {"name": "John Papa", "character": "Spooky"}, 92 | {"name": "Scarlett Johansson", "character": "Natasha Romanoff \/ Black Widow"}, 93 | {"name": "Gwyneth Paltrow", "character": "Pepper Potts"}, 94 | ]; 95 | } 96 | function getNewsStories() { 97 | return [ 98 | {title: "Avengers Movies", 99 | description: "The Avengers: Age of Ultron opens in U.S. theaters on May 1st"}, 100 | {title: "Avengers Romance", 101 | description: "Ooo la la: are Dr. Banner and Natasha getting busy?"}, 102 | {title: "Marvel PSA", 103 | description: "Earth's Mightiest Heroes Take a Stand in Avengers"}, 104 | {title: "Marvel TV", 105 | description: "Marvel's Agent Carter Debriefs Her First 2 Missions"}, 106 | {title: "Marvel Comics", 107 | description: "Thor: Meet the new female hero who will wield Mjolnir!"} 108 | ]; 109 | } 110 | })(); 111 | -------------------------------------------------------------------------------- /src/client/test-helpers/stubs.js: -------------------------------------------------------------------------------- 1 | /*jshint -W117 */ 2 | /** 3 | * Stubs for commonly stubbed dataservice methods. 4 | **/ 5 | var stubs = (function() { 6 | return { 7 | getAvengers: getAvengers, 8 | getAvengersCast: getAvengersCast, 9 | happyService: happyService, 10 | sadService: sadService 11 | }; 12 | 13 | function getAvengers() { 14 | return stubIt('getAvengers', mockData.getAvengers()); 15 | } 16 | 17 | function getAvengersCast() { 18 | return stubIt('getAvengersCast', mockData.getAvengersCast()); 19 | } 20 | 21 | //////////////////////////// 22 | function stubIt(method, returnValue) { 23 | // stubIt is paranoid 24 | // it (re)injects dataservice and $q 25 | var result; 26 | inject(function(dataservice, $q) { 27 | result = sinon.stub(dataservice, method) 28 | .returns($q.when(returnValue)); 29 | }); 30 | return result; 31 | } 32 | 33 | ///////////////////// 34 | 35 | // The following mockService factories assume 36 | // dataservice and $q were previously injected and are global 37 | 38 | function happyService() { 39 | var ok = $q.when.bind($q); 40 | return bard.mockService(dataservice, { 41 | getAvengers: ok(mockData.getAvengers()), 42 | getAvengersCast: ok(mockData.getAvengersCast()), 43 | ready: ok(dataservice), 44 | _default: ok([]) 45 | }); 46 | } 47 | 48 | function sadService() { 49 | return bard.mockService(dataservice, { 50 | _default: $q.reject('this method was doomed') 51 | }); 52 | } 53 | 54 | })(); 55 | -------------------------------------------------------------------------------- /src/client/tests/server-integration/dataservice.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint -W117, -W030 */ 2 | /** 3 | * Demonstrates use of bard's real $http and $q 4 | * restoring the ability to make AJAX calls to the server 5 | * while retaining all the goodness of ngMocks. 6 | * 7 | * An alternative to the ngMidwayTester 8 | */ 9 | 10 | describe('Server: dataservice', function() { 11 | var dataservice; 12 | 13 | beforeEach(bard.asyncModule('app')); 14 | 15 | beforeEach(inject(function(_dataservice_) { 16 | dataservice = _dataservice_; 17 | })); 18 | 19 | describe('when call getAvengers', function () { 20 | 21 | it('should get at least 6 Avengers', function (done) { 22 | dataservice.getAvengers() 23 | .then(function(data) { 24 | expect(data).to.have.length.above(6); 25 | }) 26 | .then(done, done); 27 | }); 28 | 29 | it('should contain Black Widow', function (done) { 30 | dataservice.getAvengers() 31 | .then(function(data) { 32 | var hasBlackWidow = data && data.some(function foundHer(avenger) { 33 | return avenger.name.indexOf('Black Widow') >= 0; 34 | }); 35 | expect(hasBlackWidow).to.be.true; 36 | }) 37 | .then(done, done); 38 | }); 39 | }); 40 | 41 | describe('when call getAvengersCast', function () { 42 | 43 | it('should get at least 10 cast members', function (done) { 44 | dataservice.getAvengersCast() 45 | .then(function(data) { 46 | expect(data).to.have.length.above(10); 47 | }) 48 | .then(done, done); 49 | }); 50 | 51 | it('should contain Scarlett Johansson', function (done) { 52 | dataservice.getAvengersCast() 53 | .then(function(data) { 54 | var hasScarlett = data && data.some(function foundHer(avenger) { 55 | return avenger.name === 'Scarlett Johansson'; 56 | }); 57 | expect(hasScarlett).to.be.true; 58 | }) 59 | .then(done, done); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/client/tests/server-integration/routing.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint -W117, -W109, -W030 */ 2 | //http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-karma.html 3 | //https://github.com/yearofmoo-articles/AngularJS-Testing-Article 4 | describe('Server: routing', function() { 5 | var current; // current route 6 | var tester; 7 | var wrap; 8 | 9 | beforeEach(function() { 10 | bard.asyncModule('app', bard.ngRouteTester({mockLocationPaths: false})); 11 | bard.inject(this, 'ngRouteTester'); 12 | tester = ngRouteTester; 13 | wrap = bard.wrapWithDone; 14 | }); 15 | 16 | describe('when go to `/`', function() { 17 | 18 | beforeEach(function(done) { 19 | tester.visit('/', wrap(function() { 20 | current = tester.$route.current; 21 | //To inspect Dashboard binding in DOM, call $apply again 22 | //tester.$rootScope.$apply(); 23 | }, done)); 24 | }); 25 | 26 | it('the current path should be `/`', function() { 27 | expect(tester.path()).to.equal('/'); 28 | }); 29 | it('the current controller name should be `Dashboard`', function() { 30 | expect(current.controller).to.equal('Dashboard'); 31 | }); 32 | it('the view template should be for the `Dashboard` view', function() { 33 | expect(current.templateUrl).to.match(/dashboard\/dashboard\.html/); 34 | }); 35 | it('should have loaded the `Dashboard` view', function(done) { 36 | // When the DOM is ready, assert got the dashboard view 37 | tester.until(elemIsReady, wrap(hasDashboardView, done)); 38 | 39 | function hasDashboardView() { 40 | var elem = tester.viewElement(); 41 | // look for the tell-tale id with jQuery's find() 42 | expect(elem.find('#dashboard-view')) 43 | .to.have.length(1, elem.html()); 44 | } 45 | }); 46 | }); 47 | 48 | describe('when go to `/avengers`', function() { 49 | 50 | beforeEach(function(done) { 51 | tester.visit('/avengers', wrap(function() { 52 | current = tester.$route.current; 53 | //To inspect Avengers binding in DOM, call $apply again 54 | //tester.$rootScope.$apply(); 55 | }, done)); 56 | }); 57 | 58 | it('the current path should be `/avengers`', function() { 59 | expect(tester.path()).to.equal('/avengers'); 60 | }); 61 | it('the current controller name should be `Avengers`', function() { 62 | expect(current.controller).to.equal('Avengers'); 63 | }); 64 | it('the view template should be for the `Avengers` view', function() { 65 | expect(current.templateUrl).to.match(/avengers\/avengers\.html/); 66 | }); 67 | it('should have loaded the `Avengers` view', function(done) { 68 | // When the DOM is ready, assert got the avengers view 69 | tester.until(elemIsReady, wrap(hasAvengersView, done)); 70 | 71 | function hasAvengersView() { 72 | var elem = tester.viewElement(); 73 | // look for the tell-tale id with vanilla JS 74 | expect(elem[0].querySelector('#avengers-view')) 75 | .to.not.equal(null, elem.html()); 76 | } 77 | 78 | }); 79 | }); 80 | 81 | function elemIsReady() { 82 | return !!tester.viewElement()[0]; 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*jshint node:true*/ 3 | 'use strict'; 4 | 5 | require('look').start(); // perf output to http://localhost:5959 6 | var express = require('express'); 7 | var app = express(); 8 | var bodyParser = require('body-parser'); 9 | var compress = require('compression'); 10 | var errorHandler = require('./utils/errorHandler')(); 11 | var favicon = require('serve-favicon'); 12 | var logger = require('morgan'); 13 | var port = process.env.PORT || 7206; 14 | var staticFiles = express.static; 15 | var routes; 16 | 17 | var environment = process.env.NODE_ENV; 18 | 19 | app.use(favicon(__dirname + '/favicon.ico')); 20 | app.use(bodyParser.urlencoded({extended: true})); 21 | app.use(bodyParser.json()); 22 | app.use(logger('dev')); 23 | app.use(compress()); // Compress response data with gzip 24 | app.use(require('cors')()); // enable ALL CORS requests 25 | app.use(errorHandler.init); 26 | 27 | app.use('/api', require('./routes')); 28 | 29 | console.log('About to crank up node'); 30 | console.log('PORT=' + port); 31 | console.log('NODE_ENV=' + environment); 32 | 33 | var source = ''; 34 | 35 | app.get('/ping', function(req, res, next) { 36 | console.log(req.body); 37 | res.send('pong'); 38 | }); 39 | 40 | switch (environment){ 41 | case 'build': 42 | console.log('** BUILD **'); 43 | app.use('/', staticFiles('./build/')); 44 | break; 45 | default: 46 | console.log('** DEV **'); 47 | app.use(staticFiles('./src/basics/')); 48 | app.use(staticFiles('./src/client/')); 49 | app.use(staticFiles('./')); 50 | app.use(staticFiles('./.tmp/')); 51 | app.use('/*', staticFiles('./src/client/index.html')); 52 | break; 53 | } 54 | 55 | app.listen(port, function() { 56 | console.log('Express server listening on port ' + port); 57 | console.log('env = ' + app.get('env') + 58 | '\n__dirname = ' + __dirname + 59 | '\nprocess.cwd = ' + process.cwd()); 60 | }); 61 | -------------------------------------------------------------------------------- /src/server/data/maa-cast.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "Robert Downey Jr.", "character": "Tony Stark \/ Iron Man"}, 3 | {"name": "Chris Hemsworth", "character": "Thor"}, 4 | {"name": "Chris Evans", "character": "Steve Rogers \/ Captain America"}, 5 | {"name": "Mark Ruffalo", "character": "Bruce Banner \/ The Hulk"}, 6 | {"name": "Scarlett Johansson", "character": "Natasha Romanoff \/ Black Widow"}, 7 | {"name": "Jeremy Renner", "character": "Clint Barton \/ Hawkeye"}, 8 | {"name": "Gwyneth Paltrow", "character": "Pepper Potts"}, 9 | {"name": "Samuel L. Jackson", "character": "Nick Fury"}, 10 | {"name": "Paul Bettany", "character": "Jarvis"}, 11 | {"name": "Tom Hiddleston", "character": "Loki"}, 12 | {"name": "Clark Gregg", "character": "Agent Phil Coulson"} 13 | ] -------------------------------------------------------------------------------- /src/server/data/maa.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": 200, 4 | "status": "Ok", 5 | "copyright": "© 2014 MARVEL", 6 | "attributionText": "Data provided by Marvel. © 2014 MARVEL", 7 | "attributionHTML": "Data provided by Marvel. © 2014 MARVEL", 8 | "etag": "90c4f4cccfd5340ce2dd99146ee5ff6aed497e69", 9 | "data": { 10 | "offset": 0, 11 | "limit": 20, 12 | "total": 7, 13 | "count": 7, 14 | "results": [ 15 | { 16 | "id": 1017109, 17 | "name": "Black Widow/Natasha Romanoff (MAA)", 18 | "description": "Natasha Romanoff, also known as Black Widow, is a world-renowned super spy and one of S.H.I.E.L.D.'s top agents. Her hand-to-hand combat skills, intelligence, and unpredictability make her a deadly secret weapon. True to her mysterious nature, Black Widow comes and goes as she pleases, but always appears exactly when her particular skills are needed.", 19 | "modified": "2013-09-18T15:52:01-0400", 20 | "thumbnail": { 21 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/a/03/523219743a99b", 22 | "extension": "jpg" 23 | }, 24 | "resourceURI": "http://gateway.marvel.com/v1/public/characters/1017109", 25 | "comics": { 26 | "available": 0, 27 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017109/comics", 28 | "items": [], 29 | "returned": 0 30 | }, 31 | "series": { 32 | "available": 0, 33 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017109/series", 34 | "items": [], 35 | "returned": 0 36 | }, 37 | "stories": { 38 | "available": 0, 39 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017109/stories", 40 | "items": [], 41 | "returned": 0 42 | }, 43 | "events": { 44 | "available": 0, 45 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017109/events", 46 | "items": [], 47 | "returned": 0 48 | }, 49 | "urls": [ 50 | { 51 | "type": "detail", 52 | "url": "http://marvel.com/comics/characters/1017109/black_widownatasha_romanoff_maa?utm_campaign=apiRef&utm_source=28a2b529d14dd7bd6ef673b35df7c8c2" 53 | } 54 | ] 55 | }, 56 | { 57 | "id": 1017105, 58 | "name": "Captain America/Steve Rogers (MAA)", 59 | "description": "During World War II, Steve Rogers enlisted in the military and was injected with a super-serum that turned him into super-soldier Captain America! He's a skilled strategist and even more skilled with his shield, but it's his courage and good heart that makes Captain America both a leader and a true hero. ", 60 | "modified": "2013-09-18T15:49:41-0400", 61 | "thumbnail": { 62 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/3/10/52321928eaa72", 63 | "extension": "jpg" 64 | }, 65 | "resourceURI": "http://gateway.marvel.com/v1/public/characters/1017105", 66 | "comics": { 67 | "available": 0, 68 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017105/comics", 69 | "items": [], 70 | "returned": 0 71 | }, 72 | "series": { 73 | "available": 0, 74 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017105/series", 75 | "items": [], 76 | "returned": 0 77 | }, 78 | "stories": { 79 | "available": 0, 80 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017105/stories", 81 | "items": [], 82 | "returned": 0 83 | }, 84 | "events": { 85 | "available": 0, 86 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017105/events", 87 | "items": [], 88 | "returned": 0 89 | }, 90 | "urls": [ 91 | { 92 | "type": "detail", 93 | "url": "http://marvel.com/comics/characters/1017105/captain_americasteve_rogers_maa?utm_campaign=apiRef&utm_source=28a2b529d14dd7bd6ef673b35df7c8c2" 94 | } 95 | ] 96 | }, 97 | { 98 | "id": 1017110, 99 | "name": "Falcon/Sam Wilson (MAA)", 100 | "description": "Recruited from S.H.I.E.L.D. by his hero and mentor Tony Stark, Falcon is the Avengers' newest and youngest recruit. Like Tony, Sam is a genius with machines and technology. What he lacks in experience, Sam makes up in enthusiasm and determination. Falcon's suit of armor comes fully stocked with holographic wings, explosive flechettes, and retractable talons.", 101 | "modified": "2013-09-18T15:52:17-0400", 102 | "thumbnail": { 103 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/3/10/523219c347dd1", 104 | "extension": "jpg" 105 | }, 106 | "resourceURI": "http://gateway.marvel.com/v1/public/characters/1017110", 107 | "comics": { 108 | "available": 0, 109 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017110/comics", 110 | "items": [], 111 | "returned": 0 112 | }, 113 | "series": { 114 | "available": 0, 115 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017110/series", 116 | "items": [], 117 | "returned": 0 118 | }, 119 | "stories": { 120 | "available": 0, 121 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017110/stories", 122 | "items": [], 123 | "returned": 0 124 | }, 125 | "events": { 126 | "available": 0, 127 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017110/events", 128 | "items": [], 129 | "returned": 0 130 | }, 131 | "urls": [ 132 | { 133 | "type": "detail", 134 | "url": "http://marvel.com/comics/characters/1017110/falconsam_wilson_maa?utm_campaign=apiRef&utm_source=28a2b529d14dd7bd6ef673b35df7c8c2" 135 | } 136 | ] 137 | }, 138 | { 139 | "id": 1017108, 140 | "name": "Hawkeye/Clint Barton (MAA)", 141 | "description": "Hawkeye is an expert archer with an attitude just as on-target as his aim. His stealth combat experience and his ability to hit any target with any projectile make him a valuable member of the Avengers. However, he refuses to let things get too serious, as he has as many jokes as he does arrows!", 142 | "modified": "2013-09-18T15:48:47-0400", 143 | "thumbnail": { 144 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/4/03/5232198a81c17", 145 | "extension": "jpg" 146 | }, 147 | "resourceURI": "http://gateway.marvel.com/v1/public/characters/1017108", 148 | "comics": { 149 | "available": 0, 150 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017108/comics", 151 | "items": [], 152 | "returned": 0 153 | }, 154 | "series": { 155 | "available": 0, 156 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017108/series", 157 | "items": [], 158 | "returned": 0 159 | }, 160 | "stories": { 161 | "available": 0, 162 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017108/stories", 163 | "items": [], 164 | "returned": 0 165 | }, 166 | "events": { 167 | "available": 0, 168 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017108/events", 169 | "items": [], 170 | "returned": 0 171 | }, 172 | "urls": [ 173 | { 174 | "type": "detail", 175 | "url": "http://marvel.com/comics/characters/1017108/hawkeyeclint_barton_maa?utm_campaign=apiRef&utm_source=28a2b529d14dd7bd6ef673b35df7c8c2" 176 | } 177 | ] 178 | }, 179 | { 180 | "id": 1017107, 181 | "name": "Hulk/Bruce Banner (MAA)", 182 | "description": "Scientist Bruce Banner was transformed into the Hulk as a result to gamma radiation exposure. Over 8 feet tall and weighing 1,040 pounds, it's Hulk's strength that makes him the strongest hero in the Marvel Universe! Hulk smashes all threats that dare disturb the peace and friendship he has found in the Avengers. ", 183 | "modified": "2013-09-18T15:49:50-0400", 184 | "thumbnail": { 185 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/0/03/523219b086a17", 186 | "extension": "jpg" 187 | }, 188 | "resourceURI": "http://gateway.marvel.com/v1/public/characters/1017107", 189 | "comics": { 190 | "available": 0, 191 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017107/comics", 192 | "items": [], 193 | "returned": 0 194 | }, 195 | "series": { 196 | "available": 0, 197 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017107/series", 198 | "items": [], 199 | "returned": 0 200 | }, 201 | "stories": { 202 | "available": 0, 203 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017107/stories", 204 | "items": [], 205 | "returned": 0 206 | }, 207 | "events": { 208 | "available": 0, 209 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017107/events", 210 | "items": [], 211 | "returned": 0 212 | }, 213 | "urls": [ 214 | { 215 | "type": "detail", 216 | "url": "http://marvel.com/comics/characters/1017107/hulkbruce_banner_maa?utm_campaign=apiRef&utm_source=28a2b529d14dd7bd6ef673b35df7c8c2" 217 | } 218 | ] 219 | }, 220 | { 221 | "id": 1017104, 222 | "name": "Iron Man/Tony Stark (MAA)", 223 | "description": "Tony Stark is the genius inventor/billionaire/philanthropist owner of Stark Industries. With his super high-tech Iron Man suit, he is practically indestructible, able to fly, and has a large selection of weapons to choose from - but it's Tony's quick thinking and ability to adapt and improvise that make him an effective leader of the Avengers. ", 224 | "modified": "2013-09-18T15:49:18-0400", 225 | "thumbnail": { 226 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/2/d0/5232190d42df2", 227 | "extension": "jpg" 228 | }, 229 | "resourceURI": "http://gateway.marvel.com/v1/public/characters/1017104", 230 | "comics": { 231 | "available": 0, 232 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017104/comics", 233 | "items": [], 234 | "returned": 0 235 | }, 236 | "series": { 237 | "available": 0, 238 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017104/series", 239 | "items": [], 240 | "returned": 0 241 | }, 242 | "stories": { 243 | "available": 0, 244 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017104/stories", 245 | "items": [], 246 | "returned": 0 247 | }, 248 | "events": { 249 | "available": 0, 250 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017104/events", 251 | "items": [], 252 | "returned": 0 253 | }, 254 | "urls": [ 255 | { 256 | "type": "detail", 257 | "url": "http://marvel.com/comics/characters/1017104/iron_mantony_stark_maa?utm_campaign=apiRef&utm_source=28a2b529d14dd7bd6ef673b35df7c8c2" 258 | } 259 | ] 260 | }, 261 | { 262 | "id": 1017106, 263 | "name": "Thor (MAA)", 264 | "description": "Thor is the Asgardian Prince of Thunder, the son of Odin, and the realm's mightiest warrior. He loves the thrill of battle and is always eager to show off his power to the other Avengers, especially the Hulk. Thor's legendary Uru hammer, Mjolnir, gives him the power to control thunder and the ability to fly. He's found a new home on Earth and will defend it as his own... even if he doesn't understand its sayings and customs.", 265 | "modified": "2013-09-18T15:49:30-0400", 266 | "thumbnail": { 267 | "path": "http://i.annihil.us/u/prod/marvel/i/mg/2/03/52321948a51f2", 268 | "extension": "jpg" 269 | }, 270 | "resourceURI": "http://gateway.marvel.com/v1/public/characters/1017106", 271 | "comics": { 272 | "available": 0, 273 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017106/comics", 274 | "items": [], 275 | "returned": 0 276 | }, 277 | "series": { 278 | "available": 0, 279 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017106/series", 280 | "items": [], 281 | "returned": 0 282 | }, 283 | "stories": { 284 | "available": 0, 285 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017106/stories", 286 | "items": [], 287 | "returned": 0 288 | }, 289 | "events": { 290 | "available": 0, 291 | "collectionURI": "http://gateway.marvel.com/v1/public/characters/1017106/events", 292 | "items": [], 293 | "returned": 0 294 | }, 295 | "urls": [ 296 | { 297 | "type": "detail", 298 | "url": "http://marvel.com/comics/characters/1017106/thor_maa?utm_campaign=apiRef&utm_source=28a2b529d14dd7bd6ef673b35df7c8c2" 299 | } 300 | ] 301 | } 302 | ] 303 | } 304 | } 305 | ] -------------------------------------------------------------------------------- /src/server/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpapa/ng-patterns-testing/0359ea707b0814efd86478a281f73f66c6db566c/src/server/favicon.ico -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | var dataDir = '/../data/'; 2 | var jsonfileservice = require('../utils/jsonfileservice'); 3 | var router = require('express').Router(); 4 | 5 | module.exports = router; 6 | 7 | router.get('/maa', getMaa); 8 | router.get('/maaCast', getMaaCast); 9 | 10 | //////////// 11 | 12 | function getMaa(req, res, next) { 13 | var json = jsonfileservice.getDataFromJsonFile(dataDir + 'maa.json'); 14 | var maa = json[0].data.results; 15 | maa.forEach(function(character) { 16 | var pos = character.name.indexOf('(MAA)'); 17 | character.name = character.name.substr(0, pos - 1); 18 | }); 19 | res.json(maa); 20 | } 21 | 22 | function getMaaCast(req, res, next) { 23 | var cast = jsonfileservice.getDataFromJsonFile(dataDir + 'maa-cast.json'); 24 | res.json(cast); 25 | } 26 | -------------------------------------------------------------------------------- /src/server/utils/errorHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | var service = { 3 | init: init, 4 | logErrors: logErrors 5 | }; 6 | return service; 7 | 8 | function init(err, req, res, next) { 9 | var status = err.statusCode || 500; 10 | if (err.message) { 11 | res.send(status, err.message); 12 | } else { 13 | res.send(status, err); 14 | } 15 | next(); 16 | } 17 | 18 | /* Our fall through error logger and errorHandler */ 19 | function logErrors(err, req, res, next) { 20 | var status = err.statusCode || 500; 21 | console.error(status + ' ' + (err.message ? err.message : err)); 22 | if (err.stack) { 23 | console.error(err.stack); 24 | } 25 | next(err); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/server/utils/jsonfileservice.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = { 4 | getDataFromJsonFile: getDataFromJsonFile 5 | }; 6 | 7 | function getDataFromJsonFile(file) { 8 | var filepath = __dirname + file; 9 | var data = readJsonFileSync(filepath); 10 | return data; 11 | } 12 | 13 | function readJsonFileSync(filepath, encoding) { 14 | var file = fs.readFileSync(filepath, encoding || 'utf8'); 15 | return JSON.parse(file); 16 | } 17 | -------------------------------------------------------------------------------- /typings/_references.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// --------------------------------------------------------------------------------