├── .bowerrc ├── .gitignore ├── .jshintignore ├── .jshintrc ├── LICENSE ├── README.md ├── app.json ├── bower.json ├── css └── app.less ├── gulp ├── css.js ├── js.js ├── rev.js └── server.js ├── gulpfile.js ├── index.html.ejs ├── karma.conf.js ├── package.json ├── protractor.chrome.conf.js ├── protractor.conf.js ├── public ├── angularjs.png └── favicon.ico ├── server.js ├── src ├── github │ ├── github_repo.svc.js │ ├── github_user.svc.js │ └── module.js ├── module.js ├── routes.js └── users │ ├── module.js │ ├── show_user.ctrl.js │ └── users.ctrl.js ├── templates ├── home.html ├── nav.html ├── show_user.html └── users.html ├── test ├── e2e │ ├── init.js │ └── users.spec.js └── karma │ ├── github │ ├── github_repo.svc.spec.js │ └── github_user.svc.spec.js │ ├── routes.spec.js │ └── users │ ├── show_user.ctrl.spec.js │ └── users.ctrl.spec.js └── wercker.yml /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "assets" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | assets 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | assets 3 | coverage 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "bitwise": true, 4 | "curly": false, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "laxcomma": true, 12 | "maxcomplexity": 10, 13 | "maxdepth": 5, 14 | "maxparams": 5, 15 | "maxstatements": 25, 16 | "newcap": true, 17 | "noarg": true, 18 | "node": true, 19 | "noempty": true, 20 | "nonbsp": true, 21 | "nonew": true, 22 | "trailing": true, 23 | "undef": true, 24 | "unused": true, 25 | "globals": { 26 | "expect": true 27 | }, 28 | "predef": [ 29 | "afterEach", 30 | "angular", 31 | "beforeEach", 32 | "browser", 33 | "by", 34 | "describe", 35 | "element", 36 | "inject", 37 | "it", 38 | "jasmine", 39 | "protractor", 40 | "sinon" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software 4 | for any purpose with or without fee is hereby granted, provided 5 | that the above copyright notice and this permission notice 6 | appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE 11 | LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES 12 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 13 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 14 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Example Angular App 2 | =================== 3 | 4 | [![wercker status](https://app.wercker.com/status/7226856f48f0ccaa877efd6302126765/s/master "wercker status")](https://app.wercker.com/project/bykey/7226856f48f0ccaa877efd6302126765) 5 | 6 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 7 | 8 | This is a demo app you can use to get starting writing an angular app of your own. It simply shows some GitHub info. Fully tested and production ready. 9 | 10 | Features include: 11 | 12 | * Angular.js 13 | * ng-route 14 | * gulp for building css/js using [the concat method](https://medium.com/@dickeyxxx/best-practices-for-building-angular-js-apps-266c1a4a6917) 15 | * minified assets with aggressive cache-compatible cdn filenames (rev) 16 | * uses angular templatecache to preload templates 17 | * bower for external css/js assets 18 | * jshint for linting JS style 19 | * Bootstrap for UI 20 | * Less for writing CSS 21 | * static asset node server w/ gzip 22 | * mocha/chai for testing 23 | * karma unit testing w/ PhantomJS 24 | * Protractor end-to-end testing with Firefox and Chrome 25 | * [CI on wercker](https://app.wercker.com/project/bykey/7226856f48f0ccaa877efd6302126765) 26 | * Heroku-ready 27 | 28 | [See it in action](https://angular-boilerplate.herokuapp.com/) 29 | 30 | Running Locally 31 | =============== 32 | 33 | Get the repo 34 | 35 | $ git clone https://github.com/dickeyxxx/angular-boilerplate 36 | $ cd angular-boilerplate 37 | 38 | Install dependencies 39 | 40 | $ npm install -g gulp karma-cli protractor 41 | $ npm install 42 | 43 | Install protractor dependencies (chromedriver) 44 | 45 | $ ./node_modules/.bin/webdriver-manager update 46 | 47 | 48 | Run the test suite 49 | 50 | $ npm test 51 | 52 | Alternatively run just one of the test components 53 | 54 | $ jshint . 55 | $ karma start 56 | $ protractor 57 | 58 | Structure 59 | ========= 60 | 61 | ``` 62 | │ 63 | ├── LICENSE ISC 64 | ├── README.md this file 65 | ├── app.json defines how to deploy on Heroku 66 | ├── bower.json defines third-party css/js - downloads to /assets 67 | ├── gulpfile.js includes gulp files in /gulp - also defines meta gulp tasks 68 | ├── index.html.ejs layout html file 69 | ├── karma.conf.js karma test config 70 | ├── package.json npm dependencies and scripts 71 | ├── protractor.chrome.conf.js runs e2e tests on chrome 72 | ├── protractor.conf.js runs e2e tests on firefox 73 | ├── server.js static node server - hosts /assets, /public and index.html.ejs 74 | ├── wercker.yml CI configuration 75 | │ 76 | ├── assets/ served from /assets with 1-year cache expiration - not intended to be committed 77 | │   ├── app.css compiled css assets 78 | │   ├── app.js compiles js assets 79 | │   ├── rev-manifest.json mapping from assets to assets with hash in filename 80 | │   ├── app-89c296c2.css copy of app.css with hash of contents in filename for aggressive CDN caching 81 | │   ├── app-9b04b598.js copy of app.js with hash of contents in filename for aggressive CDN caching 82 | │   └── ... all bower components - can be rev'd in gulp/rev.js 83 | │ 84 | ├── css/ less files 85 | │   ├── app.less main less file 86 | │   └── ... other less files - can also require files from /assets 87 | │ 88 | ├── coverage/ code coverage html 89 | │ 90 | ├── gulp/ gulp tasks - gulpfile.js will pick up any js file here 91 | │   ├── css.js compiles less -> css 92 | │   ├── js.js concatenates + minifies js 93 | │   ├── rev.js aggressive cdn cache filename creator 94 | │   └── server.js runs a dev server 95 | │ 96 | ├── public/ any public files that need to be served - served from / 97 | │   ├── angularjs.png 98 | │   └── favicon.ico 99 | │ 100 | ├── src/ angular files 101 | │   ├── module.js defines the 'app' ng-module 102 | │   ├── routes.js routing info for ng-route 103 | │   ├── github/ 104 | │   │   ├── module.js defines the 'github' ng-module 105 | │   │   ├── github_repo.svc.js service that gets repos from GitHub via JSONP 106 | │   │   └── github_user.svc.js service that gets users from GitHub via JSONP 107 | │   └── users/ 108 | │   ├── module.js defines the 'users' ng-module 109 | │   ├── show_user.ctrl.js controller for showing a single user's info + repos 110 | │   └── users.ctrl.js controller for showing many users + searching through users 111 | │ 112 | ├── templates/ angular templates 113 | │   ├── home.html main template 114 | │   ├── nav.html header nav template (included via ng-include in /index.html.ejs) 115 | │   ├── show_user.html template for users.ShowUserCtrl 116 | │   └── users.html template for users.UsersCtrl 117 | │ 118 | └── test/ 119 |    ├── e2e/ feature/protractor tests 120 |    │   ├── init.js boots a server for e2e tests] 121 |    │   └── users.spec.js tests the flow of finding a user and one of their repos 122 |    └── karma/ unit/karma tests 123 |    ├── routes.spec.js 124 |    ├── github/ 125 |    │   ├── github_repo.svc.spec.js 126 |    │   └── github_user.svc.spec.js 127 |    └── users/ 128 |    ├── show_user.ctrl.spec.js 129 |    └── users.ctrl.spec.js 130 | ``` 131 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular.js Boilerplate", 3 | "description": "A barebones Angular.js app", 4 | "website": "https://angular-boilerplate.herokuapp.com", 5 | "repository": "https://github.com/dickeyxxx/angular-boilerplate", 6 | "logo": "https://angular-boilerplate.herokuapp.com/angularjs.png", 7 | "keywords": ["node", "express", "static", "angular"], 8 | "success_url": "/", 9 | "env": { 10 | "NODE_ENV": "production" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-boilerplate", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "dickeyxxx " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "angular": "~1.2.21", 17 | "angular-mocks": "~1.2.21", 18 | "angular-route": "~1.2.21", 19 | "bootstrap": "~3.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /css/app.less: -------------------------------------------------------------------------------- 1 | @import "bootstrap/less/bootstrap.less"; 2 | 3 | [ng-cloak] { 4 | display: none; 5 | } 6 | -------------------------------------------------------------------------------- /gulp/css.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | var less = require('gulp-less') 3 | var sourcemaps = require('gulp-sourcemaps') 4 | var plumber = require('gulp-plumber') 5 | 6 | gulp.task('css', function () { 7 | return gulp.src('css/app.less') 8 | .pipe(sourcemaps.init()) 9 | .pipe(plumber()) // prevents compilation errors from killing gulp 10 | .pipe(less({ 11 | paths: ['assets'] 12 | })) 13 | .pipe(sourcemaps.write()) 14 | .pipe(gulp.dest('assets')) 15 | }) 16 | 17 | gulp.task('css:watch', ['css'], function () { 18 | gulp.watch('css/**/*.less', ['css']) 19 | }) 20 | -------------------------------------------------------------------------------- /gulp/js.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var gulp = require('gulp') 3 | var concat = require('gulp-concat') 4 | var ngAnnotate = require('gulp-ng-annotate') 5 | var plumber = require('gulp-plumber') 6 | var sourcemaps = require('gulp-sourcemaps') 7 | var uglify = require('gulp-uglify') 8 | 9 | gulp.task('js', function () { 10 | return gulp.src(['src/**/module.js', 'src/**/*.js']) 11 | .pipe(sourcemaps.init()) 12 | .pipe(plumber()) 13 | .pipe(concat('app.js')) 14 | .pipe(ngAnnotate()) 15 | .pipe(uglify()) 16 | .pipe(sourcemaps.write()) 17 | .pipe(gulp.dest('assets')) 18 | }) 19 | 20 | gulp.task('js:watch', ['js'], function () { 21 | gulp.watch('src/**/*.js', ['js']) 22 | }) 23 | -------------------------------------------------------------------------------- /gulp/rev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var gulp = require('gulp') 3 | var rev = require('gulp-rev') 4 | 5 | var files = [ 6 | 'assets/app.css', 7 | 'assets/app.js', 8 | 'assets/angular/angular.min.js', 9 | 'assets/angular-route/angular-route.min.js' 10 | ] 11 | 12 | gulp.task('rev', ['js', 'css'], function () { 13 | gulp.src(files) 14 | .pipe(rev()) 15 | .pipe(gulp.dest('assets')) 16 | .pipe(rev.manifest()) 17 | .pipe(gulp.dest('assets')) 18 | }) 19 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | 3 | gulp.task('server', function () { 4 | require(__dirname + '/../server') 5 | }) 6 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs') 3 | var gulp = require('gulp') 4 | 5 | fs.readdirSync(__dirname + '/gulp').forEach(function (module) { 6 | require(__dirname + '/gulp/' + module) 7 | }) 8 | 9 | gulp.task('build', ['rev']) 10 | gulp.task('default', ['js:watch', 'css:watch', 'server']) 11 | -------------------------------------------------------------------------------- /index.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | angular-boilerplate 6 | 7 | 8 | 9 |
10 |
11 | <% if (production) { %> 12 | 13 | 14 | <% } else { %> 15 | 16 | 17 | <% } %> 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Aug 01 2014 18:40:13 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'chai', 'sinon-chai'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'assets/angular/angular.js', 19 | 'assets/angular-route/angular-route.js', 20 | 'assets/angular-mocks/angular-mocks.js', 21 | 'src/**/module.js', 22 | 'src/**/*.js', 23 | 'test/karma/**/*.spec.js' 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | ], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: { 35 | 'src/**/*.js': ['coverage'] 36 | }, 37 | 38 | 39 | // test results reporter to use 40 | // possible values: 'dots', 'progress' 41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 42 | reporters: ['spec', 'coverage'], 43 | 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | 49 | // enable / disable colors in the output (reporters and logs) 50 | colors: true, 51 | 52 | 53 | // level of logging 54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | logLevel: config.LOG_INFO, 56 | 57 | 58 | // enable / disable watching file and executing tests whenever any file changes 59 | autoWatch: true, 60 | 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['PhantomJS'], 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, Karma captures browsers, runs the tests and exits 69 | singleRun: false, 70 | 71 | coverageReporter: { 72 | reporters: [ 73 | {type: 'html', dir: 'coverage/'}, 74 | {type: 'text-summary'} 75 | ] 76 | } 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-boilerplate", 3 | "version": "2.0.0", 4 | "license": "ISC", 5 | "repository": { 6 | "github": "dickeyxxx/angular-boilerplate" 7 | }, 8 | "engines": { 9 | "node": "0.10.30" 10 | }, 11 | "scripts": { 12 | "test": "jshint . && gulp build && ./node_modules/karma/bin/karma start --single-run && protractor", 13 | "start": "node server.js", 14 | "postinstall": "bower install && gulp build" 15 | }, 16 | "devDependencies": { 17 | "chai": "^1.9.1", 18 | "chai-as-promised": "^4.1.1", 19 | "jshint": "^2.5.2", 20 | "karma": "^0.12.21", 21 | "karma-chai": "^0.1.0", 22 | "karma-coverage": "^0.2.5", 23 | "karma-mocha": "^0.1.6", 24 | "karma-phantomjs-launcher": "^0.1.4", 25 | "karma-sinon-chai": "^0.2.0", 26 | "karma-spec-reporter": "0.0.13", 27 | "mocha": "^1.21.3", 28 | "protractor": "^1.0.0" 29 | }, 30 | "dependencies": { 31 | "bower": "^1.3.8", 32 | "compression": "^1.0.9", 33 | "ejs": "^1.0.0", 34 | "express": "^4.7.4", 35 | "gulp": "^3.8.7", 36 | "gulp-angular-templatecache": "^1.2.1", 37 | "gulp-concat": "^2.3.4", 38 | "gulp-less": "^1.3.3", 39 | "gulp-ng-annotate": "^0.3.0", 40 | "gulp-plumber": "^0.6.4", 41 | "gulp-rev": "^1.0.0", 42 | "gulp-sourcemaps": "^1.1.0", 43 | "gulp-uglify": "^0.3.1", 44 | "morgan": "^1.2.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /protractor.chrome.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | capabilities: { 3 | browserName: 'chrome' 4 | }, 5 | onPrepare: './test/e2e/init.js', 6 | 7 | framework: 'mocha', 8 | specs: [ 9 | 'test/e2e/**/*.spec.js' 10 | ], 11 | 12 | mochaOpts: { 13 | enableTimeouts: false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | capabilities: { 3 | browserName: 'firefox' 4 | }, 5 | 6 | onPrepare: './test/e2e/init.js', 7 | 8 | framework: 'mocha', 9 | specs: [ 10 | 'test/e2e/**/*.spec.js' 11 | ], 12 | 13 | mochaOpts: { 14 | enableTimeouts: false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /public/angularjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdx/angular-boilerplate/054a0ae36bb52228ae6c8f028ec54c5824fd085a/public/angularjs.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdx/angular-boilerplate/054a0ae36bb52228ae6c8f028ec54c5824fd085a/public/favicon.ico -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var express = require('express') 3 | var logger = require('morgan') 4 | var compression = require('compression') 5 | var app = express() 6 | var production = (process.env.NODE_ENV === 'production') 7 | 8 | if (process.env.LOG_LEVEL !== 'warn') { 9 | // use morgan logger 10 | app.use(logger(production ? 'combined' : 'dev')) 11 | } 12 | 13 | // gzip 14 | app.use(compression()) 15 | 16 | // serve static files, cached in production 17 | var cache 18 | if (production) { 19 | cache = '1 year' 20 | } else { 21 | cache = 0 22 | } 23 | app.use('/assets', express.static(__dirname + '/assets', {maxAge: cache})) 24 | app.use('/templates', express.static(__dirname + '/templates')) 25 | app.use(express.static(__dirname + '/public')) 26 | 27 | var assets 28 | if (production) { 29 | assets = require(__dirname + '/assets/rev-manifest.json') 30 | } 31 | app.locals.asset = function (filename) { 32 | if (assets && assets[filename]) { 33 | return "/assets/" + assets[filename] 34 | } else { 35 | // never use revved assets in dev mode 36 | return "/assets/" + filename 37 | } 38 | } 39 | 40 | // allows index.html.ejs to see if we're running in production 41 | app.locals.production = production 42 | 43 | app.get('*', function (req, res) { 44 | res.render(__dirname + '/index.html.ejs') 45 | }) 46 | 47 | var server = app.listen(process.env.PORT || 3000, function () { 48 | console.log('%d listening on port %d', process.pid, server.address().port) 49 | }) 50 | -------------------------------------------------------------------------------- /src/github/github_repo.svc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('github') 3 | .service('GithubRepoSvc', function ($http) { 4 | this.fetchByUser = function (login) { 5 | return $http.jsonp('https://api.github.com/users/' + login + '/repos?sort=updated&callback=JSON_CALLBACK') 6 | .then(function (response) { 7 | return response.data.data 8 | }) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /src/github/github_user.svc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('github') 3 | .service('GithubUserSvc', function ($http) { 4 | this.fetch = function () { 5 | return $http.jsonp('https://api.github.com/users?callback=JSON_CALLBACK') 6 | .then(function (response) { 7 | return response.data.data 8 | }) 9 | } 10 | 11 | this.search = function (q) { 12 | return $http.jsonp('https://api.github.com/search/users?q=' + q + '&callback=JSON_CALLBACK') 13 | .then(function (response) { 14 | return response.data.data.items 15 | }) 16 | } 17 | 18 | this.find = function (login) { 19 | return $http.jsonp('http://api.github.com/users/' + login + '?callback=JSON_CALLBACK') 20 | .then(function (response) { 21 | return response.data.data 22 | }) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/github/module.js: -------------------------------------------------------------------------------- 1 | angular.module('github', []) 2 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | angular.module('app', [ 2 | 'ngRoute', 3 | 'users' 4 | ]) 5 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | angular.module('app') 2 | .config(function ($routeProvider, $locationProvider) { 3 | $locationProvider.html5Mode(true) 4 | $routeProvider 5 | .when('/', {templateUrl: '/templates/home.html'}) 6 | .when('/users', {controller: 'UsersCtrl', templateUrl: '/templates/users.html'}) 7 | .when('/users/:login', {controller: 'ShowUserCtrl', templateUrl: '/templates/show_user.html'}) 8 | }) 9 | -------------------------------------------------------------------------------- /src/users/module.js: -------------------------------------------------------------------------------- 1 | angular.module('users', [ 2 | 'github' 3 | ]) 4 | -------------------------------------------------------------------------------- /src/users/show_user.ctrl.js: -------------------------------------------------------------------------------- 1 | angular.module('users') 2 | .controller('ShowUserCtrl', function ($scope, $routeParams, GithubUserSvc, GithubRepoSvc) { 3 | GithubUserSvc.find($routeParams.login).then(function (user) { 4 | $scope.user = user 5 | }) 6 | 7 | GithubRepoSvc.fetchByUser($routeParams.login).then(function (repos) { 8 | $scope.repos = repos 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/users/users.ctrl.js: -------------------------------------------------------------------------------- 1 | angular.module('users') 2 | .controller('UsersCtrl', function ($scope, GithubUserSvc) { 3 | GithubUserSvc.fetch().then(function (users) { 4 | $scope.users = users 5 | }) 6 | 7 | $scope.search = function (q) { 8 | GithubUserSvc.search(q).then(function (users) { 9 | $scope.users = users 10 | }) 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 |
2 |

angular-boilerplate

3 | See the code 4 |
5 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /templates/show_user.html: -------------------------------------------------------------------------------- 1 |
2 |

{{user.login}}

3 |

Followers: {{user.followers}}

4 |

{{user.location}}

5 |

{{user.company}}

6 |

{{user.website}}

7 |

{{user.blog}}

8 | 9 |

Repositories

10 | 15 |
16 | -------------------------------------------------------------------------------- /templates/users.html: -------------------------------------------------------------------------------- 1 |
2 |

Users

3 |
4 |
5 | 6 |
7 | 8 |
9 | 16 |
17 | -------------------------------------------------------------------------------- /test/e2e/init.js: -------------------------------------------------------------------------------- 1 | // Pick a random port between 3001-4000 2 | var port = Math.floor(Math.random() * 1000) + 3001 3 | browser.params.baseUrl = 'http://localhost:' + port 4 | process.env.PORT = port 5 | process.env.LOG_LEVEL = 'warn' 6 | require('../../server') 7 | -------------------------------------------------------------------------------- /test/e2e/users.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai') 3 | chai.use(require('chai-as-promised')) 4 | var expect = chai.expect 5 | 6 | describe('users', function () { 7 | it('can find this repo', function () { 8 | browser.get(browser.params.baseUrl) 9 | element(by.css('nav a.users')).click() 10 | var search = element(by.model('q')) 11 | search.sendKeys('dickey').sendKeys(protractor.Key.ENTER) 12 | element(by.cssContainingText('.users a', 'dickeyxxx')).click() 13 | var repo = element(by.cssContainingText('.repos a', 'dickeyxxx/ng-modules')) 14 | expect(repo.getAttribute('href')).to.eventually.equal('https://github.com/dickeyxxx/ng-modules') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/karma/github/github_repo.svc.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('github', function () { 4 | describe('github_repo.svc', function () { 5 | beforeEach(module('github')) 6 | var GithubRepoSvc, $httpBackend 7 | 8 | var repos = [ 9 | {full_name: 'dickeyxxx/ng-modules'}, 10 | {full_name: 'defunkt/unicorn'} 11 | ] 12 | 13 | beforeEach(inject(function (_GithubRepoSvc_, _$httpBackend_) { 14 | GithubRepoSvc = _GithubRepoSvc_ 15 | $httpBackend = _$httpBackend_ 16 | })) 17 | 18 | describe('#fetchByUser', function () { 19 | beforeEach(function () { 20 | $httpBackend 21 | .when('JSONP', 'https://api.github.com/users/undefined/repos?sort=updated&callback=JSON_CALLBACK') 22 | .respond({data: repos}) 23 | }) 24 | 25 | it('loads the repos for a user', function () { 26 | GithubRepoSvc.fetchByUser().then(function (repos) { 27 | expect(repos).to.have.length(2) 28 | }) 29 | }) 30 | }) 31 | 32 | afterEach(function () { 33 | $httpBackend.flush() 34 | $httpBackend.verifyNoOutstandingExpectation() 35 | $httpBackend.verifyNoOutstandingRequest() 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/karma/github/github_user.svc.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('github', function () { 4 | describe('github_user.svc', function () { 5 | beforeEach(module('github')) 6 | var GithubUserSvc, $httpBackend 7 | 8 | var users = [ 9 | {login: 'dickeyxxx'}, 10 | {login: 'defunkt'} 11 | ] 12 | 13 | beforeEach(inject(function (_GithubUserSvc_, _$httpBackend_) { 14 | GithubUserSvc = _GithubUserSvc_ 15 | $httpBackend = _$httpBackend_ 16 | })) 17 | 18 | describe('#fetch', function () { 19 | beforeEach(function () { 20 | $httpBackend 21 | .when('JSONP', 'https://api.github.com/users?callback=JSON_CALLBACK') 22 | .respond({data: users}) 23 | }) 24 | 25 | it('pulls down users', function () { 26 | GithubUserSvc.fetch().then(function (users) { 27 | expect(users).to.have.length(2) 28 | }) 29 | }) 30 | }) 31 | 32 | describe('#find', function () { 33 | beforeEach(function () { 34 | $httpBackend 35 | .when('JSONP', 'http://api.github.com/users/dickeyxxx?callback=JSON_CALLBACK') 36 | .respond({data: users[0]}) 37 | }) 38 | 39 | it('pulls down dickeyxxx', function () { 40 | GithubUserSvc.find('dickeyxxx').then(function (users) { 41 | expect(users.login).to.equal('dickeyxxx') 42 | }) 43 | }) 44 | }) 45 | 46 | describe('#search', function () { 47 | beforeEach(function () { 48 | $httpBackend 49 | .when('JSONP', 'https://api.github.com/search/users?q=dickeyxxx&callback=JSON_CALLBACK') 50 | .respond({data: {items: users}}) 51 | }) 52 | 53 | it('searches for dickeyxxx', function () { 54 | GithubUserSvc.search('dickeyxxx').then(function (users) { 55 | expect(users).to.have.length(2) 56 | }) 57 | }) 58 | }) 59 | 60 | afterEach(function () { 61 | $httpBackend.flush() 62 | $httpBackend.verifyNoOutstandingExpectation() 63 | $httpBackend.verifyNoOutstandingRequest() 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /test/karma/routes.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('app', function () { 4 | beforeEach(module('app')) 5 | 6 | describe('routes', function () { 7 | var $route 8 | 9 | beforeEach(inject(function (_$route_) { 10 | $route = _$route_ 11 | })) 12 | 13 | it('has a root route', function () { 14 | expect($route.routes['/'].templateUrl).to.equal('/templates/home.html') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/karma/users/show_user.ctrl.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('users', function () { 4 | describe('show_user.ctrl', function () { 5 | beforeEach(module('users')) 6 | var $scope 7 | 8 | var $routeParams = {login: 'dickeyxxx'} 9 | var fakeGithubService = {} 10 | var fakeGithubRepoService = {} 11 | 12 | beforeEach(inject (function ($q) { 13 | fakeGithubService.find = function (login) { 14 | var deferred = $q.defer() 15 | deferred.resolve({login: login}) 16 | return deferred.promise 17 | } 18 | fakeGithubRepoService.fetchByUser = function () { 19 | var deferred = $q.defer() 20 | deferred.resolve([ 21 | {name: 'foo'}, 22 | {name: 'bar'} 23 | ]) 24 | return deferred.promise 25 | } 26 | })) 27 | 28 | beforeEach(inject(function ($rootScope, $controller) { 29 | $scope = $rootScope.$new() 30 | $controller('ShowUserCtrl', { 31 | $scope: $scope, 32 | $routeParams: $routeParams, 33 | GithubUserSvc: fakeGithubService, 34 | GithubRepoSvc: fakeGithubRepoService 35 | }) 36 | })) 37 | 38 | it('loads the user', function () { 39 | $scope.$digest() 40 | expect($scope.user.login).to.equal('dickeyxxx') 41 | }) 42 | 43 | it("loads the user's repos", function () { 44 | $scope.$digest() 45 | expect($scope.repos).to.have.length(2) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/karma/users/users.ctrl.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('users', function () { 4 | describe('users.ctrl', function () { 5 | beforeEach(module('users')) 6 | var $scope 7 | 8 | var fakeGithubService = {} 9 | 10 | var users = [ 11 | {login: 'dickeyxxx'}, 12 | {login: 'defunkt'} 13 | ] 14 | 15 | beforeEach(inject (function ($q) { 16 | fakeGithubService.fetch = function () { 17 | var deferred = $q.defer() 18 | deferred.resolve(users) 19 | return deferred.promise 20 | } 21 | fakeGithubService.search = function () { 22 | var deferred = $q.defer() 23 | deferred.resolve([users[0]]) 24 | return deferred.promise 25 | } 26 | })) 27 | 28 | beforeEach(inject(function ($rootScope, $controller) { 29 | $scope = $rootScope.$new() 30 | $controller('UsersCtrl', { 31 | $scope: $scope, 32 | GithubUserSvc: fakeGithubService 33 | }) 34 | })) 35 | 36 | it('loads the users', function () { 37 | $scope.$digest() 38 | expect($scope.users).to.have.length(2) 39 | }) 40 | 41 | it('searches for users', function () { 42 | sinon.spy(fakeGithubService, 'search') 43 | $scope.search('dickeyxxx') 44 | $scope.$digest() 45 | expect($scope.users).to.have.length(1) 46 | expect(fakeGithubService.search).to.have.been.calledWith('dickeyxxx') 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/nodejs 2 | packages: 3 | - firefox 4 | build: 5 | steps: 6 | - script: 7 | name: Enable virtual display 8 | code: |- 9 | # Start xvfb which gives the context an virtual display 10 | # which is required for tests that require an GUI 11 | export DISPLAY=:99.0 12 | start-stop-daemon --start --quiet --pidfile /tmp/xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1024x768x24 -ac +extension GLX +render -noreset 13 | # Give xvfb time to start. 3 seconds is the default for all xvfb-run commands. 14 | sleep 3 15 | 16 | - install-packages: 17 | packages: firefox 18 | 19 | - npm-install 20 | 21 | - script: 22 | name: update webdriver 23 | code: ./node_modules/.bin/webdriver-manager update 24 | 25 | - npm-test 26 | 27 | deploy: 28 | steps: 29 | - heroku-deploy: 30 | key-name: DEPLOY_KEY 31 | --------------------------------------------------------------------------------