├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis-deploy.sh ├── .travis.yml ├── LICENSE ├── README.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── server.js ├── src ├── app │ ├── app-component │ │ ├── app-component.js │ │ └── app-component.tpl.html │ ├── app.config.js │ └── app.module.js ├── asset │ └── image │ │ └── testing.png ├── common │ ├── common.module.js │ └── component │ │ └── user-info-component.js ├── feature-a │ ├── feature-a.config.js │ ├── feature-a.module.js │ └── some-component │ │ ├── some-component.js │ │ ├── some-component.test.js │ │ └── some-component.tpl.html ├── feature-b │ ├── feature-b.config.js │ ├── feature-b.constants.js │ ├── feature-b.module.js │ ├── services │ │ ├── todo.service.js │ │ ├── todo.service.spec.js │ │ └── todo.service.test.js │ └── todo-component │ │ ├── todo-component.integration.test.js │ │ ├── todo-component.js │ │ ├── todo-component.test.js │ │ └── todo-component.tpl.html ├── index.html ├── main.css └── main.js ├── webpack.config.js └── webpack.karma.context.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-runtime"], 3 | "presets": ["es2015"] 4 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-defaults", 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "jasmine": true 9 | }, 10 | "globals": { 11 | "angular": false 12 | }, 13 | "ecmaFeatures": { 14 | "modules": true 15 | } 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | bower_components 4 | build 5 | dist 6 | -------------------------------------------------------------------------------- /.travis-deploy.sh: -------------------------------------------------------------------------------- 1 | # go to the out directory and create a *new* Git repo 2 | cd dist 3 | git init 4 | 5 | # inside this git repo we'll pretend to be a new user 6 | git config user.name "Travis CI" 7 | git config user.email "tomas.trajan@gmail.com" 8 | 9 | # The first and only commit to this new Git repo contains all the 10 | # files present with the commit message "Deploy to GitHub Pages". 11 | git add . 12 | git commit -m "Deploy to GitHub Pages" 13 | 14 | # Force push from the current repo's master branch to the remote 15 | # repo's gh-pages branch. (All previous history on the gh-pages branch 16 | # will be lost, since we are overwriting it.) We redirect any output to 17 | # /dev/null to hide any sensitive credential data that might otherwise be exposed. 18 | git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | script: 5 | - npm run ci 6 | - bash ./.travis-deploy.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tomas Trajan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular JS 1.5 - ES6 Module Unit testing and Karma Integration testing seed by [@tomastrajan](https://twitter.com/tomastrajan) [![Build Status](https://travis-ci.org/tomastrajan/angular-js-es6-testing-example.svg?branch=master)](https://travis-ci.org/tomastrajan/angular-js-es6-testing-example) 2 | 3 | Great seed for enterprise projects with heavy focus on unit and integration testing. 4 | 5 | This repository contains two releases: 6 | 7 | 1. `1.4.0` - for Angular JS 1.4 and lower (uses [Component Pattern for Angular JS](https://medium.com/@tomastrajan/component-paradigm-cf32e94ba78b)) 8 | 2. `1.5.0` - for Angular JS 1.5 and above which supports native `.component(name, options)` API 9 | 10 | ## Info 11 | 12 | * original blog post describing [Proper testing of Angular JS 1.X applications with ES6 modules](https://medium.com/@tomastrajan/proper-testing-of-angular-js-applications-with-es6-modules-8cf31113873f) 13 | * [demo project](http://tomastrajan.github.io/angular-js-es6-testing-example/) with examples of mocha unit & karma integration tests 14 | * [presentation](http://slides.com/tomastrajan/angularjs-unit-testing-with-es6-modules) about the concepts used in this repository 15 | * [video](https://www.youtube.com/watch?v=JTkEsu-cEzc) from Angular JS Meetup Zurich 16 | 17 | 18 | ![Components](/src/asset/image/testing.png?raw=true "Proper testing of Angular JS 1.X applications with ES6 modules") 19 | 20 | ## Getting started 21 | 22 | 1. Clone repository `git clone https://github.com/tomastrajan/angular-js-es6-testing-example.git` 23 | 2. Enter project directory `cd angular-js-es6-testing-example` 24 | 3. Install dependencies `npm i` or `npm install` 25 | 26 | ## Scripts 27 | 28 | All scripts are run with `npm run [script]`, for example: `npm run test`. 29 | 30 | * `start` - start development server, try it by opening `http://localhost:8081/webpack-dev-server/index.html` 31 | 32 | * `build` - create dev build, check `build` directory 33 | * `dist` - create production build, check `dist` directory 34 | 35 | * `server_build` - serve content from `build` directory 36 | * `server_dist` - serve content from `dist` directory 37 | 38 | * `lint` - lint code (with ESLint) 39 | * `mocha` - run all unit tests (with Mocha) 40 | * `watch` - run and watch all unit tests (with Mocha) 41 | * `karma` - run all integration tests (with Karma / Jasmine) 42 | * `test` - lint code and run all tests (with Mocha and Karma) 43 | 44 | * `ci` - for Travis CI 45 | 46 | # Tests 47 | 48 | For more detailed info about tests check the original [blog post](https://medium.com/@tomastrajan/proper-testing-of-angular-js-applications-with-es6-modules-8cf31113873f). 49 | 50 | 51 | * `*.test.js` - mocha unit tests 52 | * `*.integration.test.js` - mocha integration tests (manual) 53 | * `*.spec.js` - karma integration tests (spin up Angular JS app context) 54 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | browsers: ['Chrome'], 6 | frameworks: ['jasmine'], 7 | reporters: ['mocha'], 8 | 9 | logLevel: config.LOG_INFO, 10 | autoWatch: true, 11 | singleRun: false, 12 | colors: true, 13 | port: 9876, 14 | 15 | basePath: '', 16 | files: ['webpack.karma.context.js'], 17 | preprocessors: { 'webpack.karma.context.js': ['webpack'] }, 18 | exclude: [], 19 | webpack: { 20 | devtool: 'eval', 21 | module: { 22 | loaders: [ 23 | {test: /\.js$/, loader: 'babel', exclude: /(\.test.js$|node_modules)/}, 24 | {test: /\.css$/, loader: 'style!css'}, 25 | {test: /\.tpl.html/, loader: 'html'}, 26 | {test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/, loader: 'url?limit=50000'} 27 | ] 28 | }, 29 | plugins: [ 30 | new webpack.ProvidePlugin({ 31 | 'window.jQuery': 'jquery' 32 | }) 33 | ] 34 | }, 35 | webpackMiddleware: { 36 | noInfo: true 37 | } 38 | }); 39 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-js-es6-testing-example", 3 | "version": "1.0.0", 4 | "description": "Enhanced testing of Angular JS 1.X applications using ES6 modules", 5 | "main": "src/app.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=DEV webpack-dev-server", 8 | "build": "webpack --display-chunks --display-reasons --display-error-details --display-modules", 9 | "dist": "cross-env NODE_ENV=DIST webpack", 10 | "server_build": "node server.js", 11 | "server_dist": "cross-env NODE_ENV=DIST node server.js", 12 | "lint": "eslint src/", 13 | "mocha": "mocha --compilers js:babel-register ./src/**/*.test.js", 14 | "watch": "mocha --compilers js:babel-register -w ./src/**/*.test.js", 15 | "karma": "karma start", 16 | "test": "npm run lint && npm run mocha && karma start --single-run", 17 | "ci": "npm run lint && npm run mocha && npm run dist" 18 | }, 19 | "author": "tomas.trajan@gmail.com", 20 | "license": "ISC", 21 | "dependencies": { 22 | "angular": "1.5", 23 | "angular-animate": "1.5", 24 | "angular-ui-bootstrap": "0.13.4", 25 | "angular-ui-router": "0.2.15", 26 | "bootstrap": "3.3.5", 27 | "jquery": "2.1.4", 28 | "lodash": "3.10.1" 29 | }, 30 | "devDependencies": { 31 | "angular-mocks": "1.5", 32 | "awesome-typescript-loader": "0.11.3", 33 | "babel-core": "^6.26.3", 34 | "babel-loader": "6.2.1", 35 | "babel-plugin-transform-runtime": "6.4.3", 36 | "babel-preset-es2015": "6.3.13", 37 | "babel-register": "^6.26.0", 38 | "babel-runtime": "6.5", 39 | "chai": "3.2.0", 40 | "chalk": "1.1.0", 41 | "clean-webpack-plugin": "0.1.3", 42 | "connect": "^3.6.6", 43 | "cross-env": "^3.1.3", 44 | "css-loader": "0.15.6", 45 | "eslint": "1.10.3", 46 | "eslint-config-defaults": "8.0.2", 47 | "file-loader": "0.8.4", 48 | "html-loader": "^0.5.5", 49 | "html-webpack-plugin": "2.24.1", 50 | "jasmine-core": "2.3.4", 51 | "karma": "0.13.19", 52 | "karma-chrome-launcher": "0.2.0", 53 | "karma-jasmine": "0.3.6", 54 | "karma-mocha-reporter": "1.1.1", 55 | "karma-webpack": "1.7.0", 56 | "minimist": "1.1.3", 57 | "mocha": "2.2.5", 58 | "ngtemplate-loader": "1.3.0", 59 | "open-browser-webpack-plugin": "0.0.5", 60 | "serve-static": "^1.13.2", 61 | "sinon": "1.17.1", 62 | "style-loader": "0.12.3", 63 | "typescript": "1.5.3", 64 | "url-loader": "0.5.6", 65 | "webpack": "1.10.5", 66 | "webpack-dev-server": "1.10.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var minimist = require('minimist'); 2 | var connect = require('connect'); 3 | var serveStatic = require('serve-static'); 4 | 5 | 6 | var PORT = 8080; 7 | var TARGET_PATH_MAPPING = { 8 | BUILD: './build', 9 | DIST: './dist' 10 | }; 11 | 12 | var TARGET = minimist(process.argv.slice(2)).TARGET || 'BUILD'; 13 | 14 | connect() 15 | .use(serveStatic(TARGET_PATH_MAPPING[TARGET])) 16 | .listen(PORT); 17 | 18 | console.log('Created server for: ' + TARGET + ', listening on port ' + PORT); -------------------------------------------------------------------------------- /src/app/app-component/app-component.js: -------------------------------------------------------------------------------- 1 | export default class AppComponent { 2 | 3 | constructor() { 4 | this.name = 'Tomas'; 5 | } 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/app/app-component/app-component.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 24 | 25 |
26 | 27 | 51 | 52 |
-------------------------------------------------------------------------------- /src/app/app.config.js: -------------------------------------------------------------------------------- 1 | export function routing($urlRouterProvider, $stateProvider) { 2 | 3 | $urlRouterProvider.otherwise('/feature-a'); 4 | 5 | $stateProvider 6 | .state('app', { 7 | abstract: true, 8 | template: '' 9 | }) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uirouter from 'angular-ui-router'; 3 | 4 | import { routing } from './app.config.js'; 5 | 6 | import AppComponent from './app-component/app-component'; 7 | import template from './app-component/app-component.tpl.html'; 8 | 9 | export default angular 10 | .module('main.app', [uirouter]) 11 | .config(routing) 12 | .component('appComponent', { controller: AppComponent, template }) 13 | .name; -------------------------------------------------------------------------------- /src/asset/image/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomastrajan/angular-js-es6-testing-example/40560e53c9d97d60d691b1f03deebbb54b0601dd/src/asset/image/testing.png -------------------------------------------------------------------------------- /src/common/common.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | import UserInfoComponent from './component/user-info-component'; 4 | 5 | export default angular 6 | .module('main.app.common', []) 7 | .component('userInfoComponent', UserInfoComponent) 8 | .name; -------------------------------------------------------------------------------- /src/common/component/user-info-component.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: { 3 | name: '<' 4 | }, 5 | template: 'Hi {{$ctrl.name}}!' 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/feature-a/feature-a.config.js: -------------------------------------------------------------------------------- 1 | export function routing($stateProvider) { 2 | 3 | $stateProvider 4 | .state('app.feature-a', { 5 | url: '/feature-a', 6 | template: '' 7 | }); 8 | } -------------------------------------------------------------------------------- /src/feature-a/feature-a.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uirouter from 'angular-ui-router'; 3 | 4 | import { routing } from './feature-a.config.js'; 5 | 6 | import SomeComponent from './some-component/some-component'; 7 | import template from './some-component/some-component.tpl.html'; 8 | 9 | export default angular 10 | .module('main.app.feature-a', [uirouter]) 11 | .config(routing) 12 | .component('someComponent', { controller: SomeComponent, template }) 13 | .name; -------------------------------------------------------------------------------- /src/feature-a/some-component/some-component.js: -------------------------------------------------------------------------------- 1 | export default class SomeComponent { 2 | 3 | constructor(initialCount = 20) { 4 | this.counter = initialCount; 5 | } 6 | 7 | increment() { 8 | this.counter++; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/feature-a/some-component/some-component.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | 3 | import SomeComponent from './some-component'; 4 | 5 | let component; 6 | 7 | describe('SomeComponent', function() { 8 | 9 | beforeEach(function() { 10 | component = new SomeComponent(); 11 | }); 12 | 13 | it('should start with default counter value = 20', function () { 14 | assert.equal(component.counter, 20); 15 | }); 16 | 17 | it('should accept initial counter value as dependency', function () { 18 | component = new SomeComponent(30); 19 | assert.equal(component.counter, 30); 20 | }); 21 | 22 | it('should increment counter value after increment is called', function () { 23 | assert.equal(component.counter, 20); 24 | component.increment(); 25 | assert.equal(component.counter, 21); 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /src/feature-a/some-component/some-component.tpl.html: -------------------------------------------------------------------------------- 1 |
2 |

Feature A!

3 |

4 | 5 | Feature A is implemented as Component using 6 | "Component Pattern" 7 |

8 | Feature A (and all other components) are implemented using native 9 | .component API available since Angular JS 1.5 10 |

11 |
12 |

Feature A is so great that it even contains counter functionality!

13 | 16 |
17 |

The implemented counter functionality is tested in isolation without instantiating of Angular JS context

18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 |
29 | 30 | import { assert } from 'chai'; 31 | 32 | import SomeComponent from './some-component'; 33 | 34 | let component; 35 | 36 | describe('some-component', function() { 37 | 38 | beforeEach(function() { 39 | component = new SomeComponent(); 40 | }); 41 | 42 | it('should start with counter value 20', function () { 43 | assert.equal(component.counter, 20); 44 | }); 45 | 46 | it('should accept initial counter value as dependency', function () { 47 | component = new SomeComponent(30); 48 | assert.equal(component.counter, 30); 49 | }); 50 | 51 | it('should increment counter value after increment is called', function () { 52 | assert.equal(component.counter, 20); 53 | component.increment(); 54 | assert.equal(component.counter, 21); 55 | }); 56 | 57 | }); 58 | 59 |
60 | 61 |
-------------------------------------------------------------------------------- /src/feature-b/feature-b.config.js: -------------------------------------------------------------------------------- 1 | export function routing($stateProvider) { 2 | 3 | $stateProvider 4 | .state('app.feature-b', { 5 | url: '/feature-b', 6 | template: '' 7 | }); 8 | } -------------------------------------------------------------------------------- /src/feature-b/feature-b.constants.js: -------------------------------------------------------------------------------- 1 | export const EMPTY_TODOS = []; 2 | 3 | export const DEFAULT_TODOS = [ 4 | { label: 'Read blog post', done: true }, 5 | { label: 'Checkout from github & run example', done: true }, 6 | { label: 'Check github repository for implementation details', done: false }, 7 | { label: 'Use in your own project!', done: false } 8 | ]; -------------------------------------------------------------------------------- /src/feature-b/feature-b.module.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import uirouter from 'angular-ui-router'; 3 | 4 | import { routing } from './feature-b.config.js'; 5 | import { DEFAULT_TODOS } from './feature-b.constants.js'; 6 | 7 | import TodoService from './services/todo.service'; 8 | import TodoComponent from './todo-component/todo-component'; 9 | import template from './todo-component/todo-component.tpl.html'; 10 | 11 | export default angular 12 | .module('main.app.feature-b', [uirouter]) 13 | .config(routing) 14 | .component('todoComponent', { controller: TodoComponent, template }) 15 | .service('TodoService', TodoService) 16 | .constant('initialTodos', DEFAULT_TODOS) 17 | .name; -------------------------------------------------------------------------------- /src/feature-b/services/todo.service.js: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | export default class TodoService { 4 | 5 | constructor(initialTodos) { 6 | this.todos = initialTodos; 7 | } 8 | 9 | addTodo(label) { 10 | let todo = { 11 | label, 12 | done: false 13 | }; 14 | this.todos.push(todo); 15 | } 16 | 17 | toggleTodo(label) { 18 | let toggledTodo = _.find(this.todos, function(todo) { 19 | return todo.label === label; 20 | }); 21 | toggledTodo.done = !toggledTodo.done; 22 | } 23 | 24 | removeDoneTodos() { 25 | _.remove(this.todos, function(todo) { 26 | return todo.done; 27 | }); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/feature-b/services/todo.service.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('TodoService', function () { 4 | 5 | var service; 6 | 7 | beforeEach(angular.mock.module('main')); 8 | 9 | beforeEach(angular.mock.module(function ($provide) { 10 | $provide.constant('initialTodos', []); 11 | })); 12 | 13 | beforeEach(angular.mock.inject(function (_TodoService_) { 14 | service = _TodoService_; 15 | })); 16 | 17 | it('should contain empty todos after initialization', function() { 18 | expect(service.todos.length).toBe(0); 19 | }); 20 | 21 | it('should add todo', function () { 22 | service.addTodo('Finish example project'); 23 | expect(service.todos.length).toBe(1); 24 | expect(service.todos[0].label).toBe('Finish example project'); 25 | expect(service.todos[0].done).toBe(false); 26 | }); 27 | 28 | it('should toggle todo', function () { 29 | service.addTodo('Finish example project'); 30 | expect(service.todos[0].done).toBe(false); 31 | 32 | service.toggleTodo('Finish example project'); 33 | expect(service.todos[0].done).toBe(true); 34 | 35 | service.toggleTodo('Finish example project'); 36 | expect(service.todos[0].done).toBe(false); 37 | }); 38 | 39 | it('should remove done todos', function () { 40 | service.addTodo('Todo1'); 41 | service.addTodo('Todo2'); 42 | service.addTodo('Todo3'); 43 | expect(service.todos.length).toBe(3); 44 | 45 | service.toggleTodo('Todo1'); 46 | service.removeDoneTodos(); 47 | expect(service.todos.length).toBe(2); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /src/feature-b/services/todo.service.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | 3 | import TodoService from './todo.service.js'; 4 | 5 | let service; 6 | 7 | describe('TodoService', function() { 8 | 9 | beforeEach(function() { 10 | let initialTodos = []; 11 | service = new TodoService(initialTodos); 12 | }); 13 | 14 | it('should contain empty todos after initialization', function () { 15 | assert.equal(service.todos.length, 0); 16 | }); 17 | 18 | it('should add todo', function () { 19 | service.addTodo('Finish example project'); 20 | assert.equal(service.todos.length, 1); 21 | assert.equal(service.todos[0].label, 'Finish example project'); 22 | assert.equal(service.todos[0].done, false); 23 | }); 24 | 25 | it('should toggle todo', function () { 26 | service.addTodo('Finish example project'); 27 | assert.equal(service.todos[0].done, false); 28 | 29 | service.toggleTodo('Finish example project'); 30 | assert.equal(service.todos[0].done, true); 31 | 32 | service.toggleTodo('Finish example project'); 33 | assert.equal(service.todos[0].done, false); 34 | }); 35 | 36 | it('should remove done todos', function () { 37 | service.addTodo('Todo1'); 38 | service.addTodo('Todo2'); 39 | service.addTodo('Todo3'); 40 | assert.equal(service.todos.length, 3); 41 | 42 | service.toggleTodo('Todo1'); 43 | service.removeDoneTodos(); 44 | assert.equal(service.todos.length, 2); 45 | }); 46 | 47 | }); -------------------------------------------------------------------------------- /src/feature-b/todo-component/todo-component.integration.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | 3 | import TodoComponent from './todo-component.js'; 4 | import TodoService from '../services/todo.service.js'; 5 | 6 | let component; 7 | 8 | describe('TodoComponent with real service (Integration test)', function() { 9 | 10 | beforeEach(function() { 11 | let initialTodos = []; 12 | let todoService = new TodoService(initialTodos); 13 | component = new TodoComponent(todoService); 14 | }); 15 | 16 | it('should contain reference to service\'s todos', function () { 17 | assert.equal(component.todos.length, 0); 18 | }); 19 | 20 | it('should add todo', function () { 21 | component.label = 'Finish example project'; 22 | component.addTodo(); 23 | assert.equal(component.label, ''); 24 | assert.equal(component.todos.length, 1); 25 | assert.equal(component.todos[0].label, 'Finish example project'); 26 | assert.equal(component.todos[0].done, false); 27 | }); 28 | 29 | it('should toggle todo', function () { 30 | component.label = 'Finish example project'; 31 | component.addTodo(); 32 | assert.equal(component.todos[0].done, false); 33 | 34 | component.toggleTodo(component.todos[0]); 35 | assert.equal(component.todos[0].done, true); 36 | 37 | component.toggleTodo(component.todos[0]); 38 | assert.equal(component.todos[0].done, false); 39 | }); 40 | 41 | it('should remove done todos', function () { 42 | component.label = 'Todo1'; 43 | component.addTodo(); 44 | 45 | component.label = 'Todo2'; 46 | component.addTodo(); 47 | 48 | component.label = 'Todo2'; 49 | component.addTodo(); 50 | 51 | assert.equal(component.todos.length, 3); 52 | 53 | component.toggleTodo(component.todos[0]); 54 | component.removeDoneTodos(); 55 | assert.equal(component.todos.length, 2); 56 | }); 57 | 58 | }); -------------------------------------------------------------------------------- /src/feature-b/todo-component/todo-component.js: -------------------------------------------------------------------------------- 1 | export default class TodoComponent { 2 | 3 | constructor(TodoService) { 4 | this.TodoService = TodoService; 5 | this.todos = TodoService.todos; 6 | this.label = ''; 7 | 8 | this.collapse = [true, true, true]; 9 | } 10 | 11 | addTodo() { 12 | this.TodoService.addTodo(this.label); 13 | this.label = ''; 14 | } 15 | 16 | toggleTodo(todo) { 17 | this.TodoService.toggleTodo(todo.label); 18 | } 19 | 20 | removeDoneTodos() { 21 | this.TodoService.removeDoneTodos(); 22 | } 23 | 24 | toggleCollapse(index) { 25 | let originalValue = this.collapse[index]; 26 | this.collapse.forEach((item, i) => { 27 | this.collapse[i] = true; 28 | }); 29 | this.collapse[index] = !originalValue; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/feature-b/todo-component/todo-component.test.js: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | 3 | import TodoComponent from './todo-component.js'; 4 | import TodoService from '../services/todo.service.js'; 5 | 6 | let component; 7 | let mockTodoService; 8 | 9 | describe('TodoComponent with mocked service (unit test)', function() { 10 | 11 | beforeEach(function() { 12 | let initialTodos = []; 13 | let TodoServiceInstance = new TodoService(initialTodos); 14 | mockTodoService = sinon.mock(TodoServiceInstance); 15 | component = new TodoComponent(TodoServiceInstance); 16 | }); 17 | 18 | afterEach(function() { 19 | mockTodoService.restore(); 20 | }); 21 | 22 | it('should add todo', function () { 23 | mockTodoService 24 | .expects('addTodo') 25 | .once() 26 | .withArgs('Finish example project'); 27 | 28 | component.label = 'Finish example project'; 29 | component.addTodo(); 30 | 31 | mockTodoService.verify(); 32 | }); 33 | 34 | it('should toggle todo', function () { 35 | let mockTodo = {label: 'Add unit tests', done: false}; 36 | mockTodoService 37 | .expects('toggleTodo') 38 | .twice() 39 | .withArgs(mockTodo.label); 40 | 41 | component.toggleTodo(mockTodo); 42 | component.toggleTodo(mockTodo); 43 | 44 | mockTodoService.verify(); 45 | }); 46 | 47 | it('should remove done todos', function () { 48 | mockTodoService 49 | .expects('removeDoneTodos') 50 | .once(); 51 | 52 | component.removeDoneTodos(); 53 | 54 | mockTodoService.verify(); 55 | }); 56 | 57 | }); -------------------------------------------------------------------------------- /src/feature-b/todo-component/todo-component.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |

Feature B!

6 |

Feature B is implementation of simple Todo app

7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 | 16 |
    17 |
  • 18 | 19 | 20 | {{todo.label}} 21 |
  • 22 |
23 |

No `todos` yet, try to add some...

24 |
25 |
26 |
27 | 28 | 29 |
30 |
31 |
32 |
33 | 34 | 37 | 40 |
41 |
42 |
43 |
44 |
45 |

The functionality implemnted in TodoService is tested in isolation without instantiating of Angular JS context

46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 | 56 | {{$ctrl.collapse[0] ? 'Show' : 'Hide'}} service test code 57 | 58 | 59 | {{$ctrl.collapse[1] ? 'Show' : 'Hide'}} component test code 60 | 61 | 62 | {{$ctrl.collapse[2] ? 'Show' : 'Hide'}} component integration test code 63 | 64 | 65 |
66 |
67 | 68 |
69 |
 70 |                 
 71 |                     import { assert } from 'chai';
 72 | 
 73 |                     import TodoService from './todo.service.js';
 74 | 
 75 |                     let service;
 76 | 
 77 |                     describe('TodoService', function() {
 78 | 
 79 |                         beforeEach(function() {
 80 |                             service = new TodoService();
 81 |                         });
 82 | 
 83 |                         it('should contain empty todos after initialization', function () {
 84 |                             assert.equal(service.todos.length, 0);
 85 |                         });
 86 | 
 87 |                         it('should add todo', function () {
 88 |                             service.addTodo('Finish example project');
 89 |                             assert.equal(service.todos.length, 1);
 90 |                             assert.equal(service.todos[0].label, 'Finish example project');
 91 |                             assert.equal(service.todos[0].done, false);
 92 |                         });
 93 | 
 94 |                         it('should toggle todo', function () {
 95 |                             service.addTodo('Finish example project');
 96 |                             assert.equal(service.todos[0].done, false);
 97 | 
 98 |                             service.toggleTodo('Finish example project');
 99 |                             assert.equal(service.todos[0].done, true);
100 | 
101 |                             service.toggleTodo('Finish example project');
102 |                             assert.equal(service.todos[0].done, false);
103 |                         });
104 | 
105 |                         it('should remove done todos', function () {
106 |                             service.addTodo('Todo1');
107 |                             service.addTodo('Todo2');
108 |                             service.addTodo('Todo3');
109 |                             assert.equal(service.todos.length, 3);
110 | 
111 |                             service.toggleTodo('Todo1');
112 |                             service.removeDoneTodos();
113 |                             assert.equal(service.todos.length, 2);
114 |                         });
115 | 
116 |                     });
117 |                 
118 |                 
119 |
120 | 121 |
122 |
123 |                 
124 |                     import { assert } from 'chai';
125 |                     import * as sinon from 'sinon';
126 | 
127 |                     import TodoComponent from './todo-component.js';
128 |                     import TodoService from '../services/todo.service.js';
129 | 
130 |                     let component;
131 |                     let mockTodoService;
132 | 
133 |                     describe('TodoComponent with mocked service (unit test)', function() {
134 | 
135 |                         beforeEach(function() {
136 |                             let initialTodos = [];
137 |                             let TodoServiceInstance = new TodoService(initialTodos);
138 |                             mockTodoService = sinon.mock(TodoServiceInstance);
139 |                             component = new TodoComponent(TodoServiceInstance);
140 |                         });
141 | 
142 |                         afterEach(function() {
143 |                             mockTodoService.restore();
144 |                         });
145 | 
146 |                         it('should add todo', function () {
147 |                             mockTodoService
148 |                                 .expects('addTodo')
149 |                                 .once()
150 |                                 .withArgs('Finish example project');
151 | 
152 |                             component.label = 'Finish example project';
153 |                             component.addTodo();
154 | 
155 |                             mockTodoService.verify();
156 |                         });
157 | 
158 |                         it('should toggle todo', function () {
159 |                             let mockTodo = {label: 'Add unit tests', done: false};
160 |                             mockTodoService
161 |                                 .expects('toggleTodo')
162 |                                 .twice()
163 |                                 .withArgs(mockTodo.label);
164 | 
165 |                             component.toggleTodo(mockTodo);
166 |                             component.toggleTodo(mockTodo);
167 | 
168 |                             mockTodoService.verify();
169 |                         });
170 | 
171 |                         it('should remove done todos', function () {
172 |                             mockTodoService
173 |                                 .expects('removeDoneTodos')
174 |                                 .once();
175 | 
176 |                             component.removeDoneTodos();
177 | 
178 |                             mockTodoService.verify();
179 |                         });
180 | 
181 |                     });
182 |                 
183 |                 
184 |
185 | 186 |
187 |
188 |                 
189 |                     import { assert } from 'chai';
190 | 
191 |                     import TodoComponent from './todo-component.js';
192 |                     import TodoService from '../services/todo.service.js';
193 | 
194 |                     let component;
195 | 
196 |                     describe('TodoComponent with real service (Integration test)', function() {
197 | 
198 |                         beforeEach(function() {
199 |                             let initialTodos = [];
200 |                             let todoService = new TodoService(initialTodos);
201 |                             component = new TodoComponent(todoService);
202 |                         });
203 | 
204 |                         it('should contain reference to service\'s todos', function () {
205 |                             assert.equal(component.todos.length, 0);
206 |                         });
207 | 
208 |                         it('should add todo', function () {
209 |                             component.label = 'Finish example project';
210 |                             component.addTodo();
211 |                             assert.equal(component.label, '');
212 |                             assert.equal(component.todos.length, 1);
213 |                             assert.equal(component.todos[0].label, 'Finish example project');
214 |                             assert.equal(component.todos[0].done, false);
215 |                         });
216 | 
217 |                         it('should toggle todo', function () {
218 |                             component.label = 'Finish example project';
219 |                             component.addTodo();
220 |                             assert.equal(component.todos[0].done, false);
221 | 
222 |                             component.toggleTodo(component.todos[0]);
223 |                             assert.equal(component.todos[0].done, true);
224 | 
225 |                             component.toggleTodo(component.todos[0]);
226 |                             assert.equal(component.todos[0].done, false);
227 |                         });
228 | 
229 |                         it('should remove done todos', function () {
230 |                             component.label = 'Todo1';
231 |                             component.addTodo();
232 | 
233 |                             component.label = 'Todo2';
234 |                             component.addTodo();
235 | 
236 |                             component.label = 'Todo2';
237 |                             component.addTodo();
238 | 
239 |                             assert.equal(component.todos.length, 3);
240 | 
241 |                             component.toggleTodo(component.todos[0]);
242 |                             component.removeDoneTodos();
243 |                             assert.equal(component.todos.length, 2);
244 |                         });
245 | 
246 |                     });
247 |                 
248 |                 
249 |
250 | 251 |
252 |
253 | 254 |
-------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Proper testing of Angular 1.X with ES6 modules 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles 2 | -------------------------------------------------- */ 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | body { 8 | /* Margin bottom by footer height */ 9 | margin-bottom: 145px; 10 | padding-top: 80px; 11 | } 12 | .footer { 13 | position: absolute; 14 | bottom: 0; 15 | width: 100%; 16 | /* Set the fixed height of the footer here */ 17 | height: auto; 18 | background-color: #f5f5f5; 19 | } 20 | 21 | 22 | /* Custom page CSS 23 | -------------------------------------------------- */ 24 | /* Not required for template or sticky footer method. */ 25 | 26 | body > .container { 27 | padding: 60px 0; 28 | } 29 | .row { 30 | padding: 0px 15px 31 | } 32 | 33 | .footer > .container { 34 | padding: 15px; 35 | text-align: center; 36 | } 37 | 38 | .footer > .container a { 39 | font-weight: bold; 40 | } 41 | 42 | .footer > .container a:hover { 43 | text-decoration: none; 44 | } 45 | 46 | .footer .row div { 47 | padding: 10px 0px 0px 0px; 48 | } 49 | 50 | ul { 51 | list-style-type: none; 52 | margin-left: 0px; 53 | padding-left: 0px; 54 | } 55 | 56 | 57 | .todo { 58 | cursor: pointer; 59 | } 60 | .todo .done { 61 | color: #006600; 62 | } 63 | 64 | user-info-component { 65 | position: relative; 66 | top: 15px; 67 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // Styles 2 | import 'bootstrap/dist/css/bootstrap.min.css'; 3 | import './main.css'; 4 | 5 | // 3rd party modules 6 | //import bootstrap from 'bootstrap'; 7 | import angular from 'angular'; 8 | import angularAnimate from 'angular-animate'; 9 | import angularUiBootstrap from 'angular-ui-bootstrap'; 10 | 11 | // Modules 12 | import app from './app/app.module'; 13 | import common from './common/common.module'; 14 | import featureA from './feature-a/feature-a.module'; 15 | import featureB from './feature-b/feature-b.module'; 16 | 17 | angular.module('main', [ 18 | angularAnimate, angularUiBootstrap, app, common, featureA, featureB 19 | ]); 20 | 21 | angular.element(document).ready(() => { 22 | angular.bootstrap(document, ['main']); 23 | }); 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var chalk = require('chalk'); 3 | var webpack = require('webpack'); 4 | 5 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | var CleanWebpackPlugin = require('clean-webpack-plugin'); 7 | var OpenBrowserWebpackPlugin = require('open-browser-webpack-plugin'); 8 | 9 | var PARAMS_DEFAULT = { 10 | entry: { 11 | main: './src/main.js', 12 | vendor: ['lodash', 'jquery', 'bootstrap', 'angular', 'angular-animate', 'angular-ui-bootstrap'] 13 | }, 14 | output: { 15 | filename: '[name].[chunkhash].js', 16 | sourceMapFilename: '[name].[chunkhash].map' 17 | }, 18 | plugins: [ 19 | new HtmlWebpackPlugin({ 20 | template: './src/index.html', 21 | inject: 'body' 22 | }), 23 | new webpack.ProvidePlugin({ 24 | // https://github.com/angular/angular.js/blob/v1.5.9/src/Angular.js#L1821-L1823 25 | // http://blog.johnnyreilly.com/2016/05/the-mysterious-case-of-webpack-angular-and-jquery.html 26 | 'window.jQuery': 'jquery', 27 | 'jQuery': 'jquery' 28 | }), 29 | new webpack.optimize.DedupePlugin() 30 | ], 31 | devServer: { 32 | port: 8081 33 | } 34 | }; 35 | var PARAMS_PER_TARGET = { 36 | DEV: { 37 | devtool: 'inline-source-map', 38 | output: { 39 | filename: '[name].js' 40 | }, 41 | plugins: [ 42 | new webpack.optimize.CommonsChunkPlugin({ 43 | name: 'vendor', 44 | filename: 'vendor.js' 45 | }), 46 | new OpenBrowserWebpackPlugin({ 47 | url: 'http://localhost:' + PARAMS_DEFAULT.devServer.port 48 | }) 49 | ] 50 | }, 51 | BUILD: { 52 | output: { 53 | path: './build' 54 | }, 55 | devtool: 'source-map', 56 | plugins: [ 57 | new CleanWebpackPlugin(['build']), 58 | new webpack.optimize.CommonsChunkPlugin({ 59 | name: 'vendor', 60 | filename: 'vendor.[chunkhash].js', 61 | minChunks: Infinity 62 | }) 63 | ] 64 | }, 65 | DIST: { 66 | output: { 67 | path: './dist', 68 | // TODO remove hack-fix when gh-pages work again 69 | publicPath: '/angular-js-es6-testing-example/' 70 | }, 71 | plugins: [ 72 | new CleanWebpackPlugin(['dist']), 73 | new webpack.optimize.CommonsChunkPlugin({ 74 | name: 'vendor', 75 | filename: 'vendor.[chunkhash].js', 76 | minChunks: Infinity 77 | }), 78 | new webpack.optimize.UglifyJsPlugin({ 79 | mangle: false 80 | }) 81 | ] 82 | } 83 | }; 84 | var TARGET = process.env.NODE_ENV || 'BUILD'; 85 | var params = _.merge(PARAMS_DEFAULT, PARAMS_PER_TARGET[TARGET], _mergeArraysCustomizer); 86 | 87 | _printBuildInfo(params); 88 | 89 | module.exports = { 90 | resolve: params.resolve, 91 | entry: params.entry, 92 | output: params.output, 93 | module: { 94 | loaders: [ 95 | {test: /\.js$/, loader: 'babel-loader', exclude: /(\.test.js$|node_modules)/}, 96 | {test: /\.css$/, loader: 'style-loader!css-loader'}, 97 | {test: /\.tpl.html/, loader: 'html-loader'}, 98 | {test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/, loader: 'url-loader?limit=50000'} 99 | ] 100 | }, 101 | plugins: params.plugins, 102 | devServer: params.devServer, 103 | devtool: params.devtool 104 | }; 105 | 106 | function _printBuildInfo(params) { 107 | console.log('\nStarting ' + chalk.bold.green('"' + TARGET + '"') + ' build'); 108 | if (TARGET === 'DEV') { 109 | console.log('Dev server: ' + 110 | chalk.bold.yellow('http://localhost:' + params.devServer.port + '/webpack-dev-server/index.html') + '\n\n'); 111 | } else { 112 | console.log('\n\n'); 113 | } 114 | } 115 | 116 | function _mergeArraysCustomizer(a, b) { 117 | if (_.isArray(a)) { 118 | return a.concat(b); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /webpack.karma.context.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import mocks from 'angular-mocks'; 3 | 4 | import * as main from './src/main'; 5 | 6 | let context = require.context('./src', true, /\.spec\.js/); 7 | context.keys().forEach(context); --------------------------------------------------------------------------------