├── client ├── .tmp │ └── .gitkeep ├── build │ ├── tasks │ │ └── .gitkeep │ └── config │ │ ├── targethtml.coffee │ │ ├── bower.coffee │ │ ├── usemin_prepare.coffee │ │ ├── concat.coffee │ │ ├── clean.coffee │ │ ├── devUpdate.coffee │ │ ├── coffeelint.coffee │ │ ├── htmlmin.coffee │ │ ├── usemin.coffee │ │ ├── less.coffee │ │ ├── shell.coffee │ │ ├── coffee.coffee │ │ ├── ngtemplates.coffee │ │ ├── watch.coffee │ │ ├── copy.coffee │ │ ├── connect.coffee │ │ └── karma.coffee ├── .bowerrc ├── test │ ├── unit │ │ ├── helpers │ │ │ ├── mocks.coffee │ │ │ ├── sinon.coffee │ │ │ ├── chai_sinon.coffee │ │ │ └── chai_routes.coffee │ │ ├── controllers │ │ │ ├── main_ctrl_spec.coffee │ │ │ ├── products │ │ │ │ ├── show_ctrl_spec.coffee │ │ │ │ ├── index_ctrl_spec.coffee │ │ │ │ ├── show_actions_ctrl_spec.coffee │ │ │ │ └── form_ctrl_spec.coffee │ │ │ ├── other_ctrl_spec.coffee │ │ │ └── tasks_ctrl_spec.coffee │ │ ├── base_ctrl_spec.coffee │ │ ├── routes_spec.coffee │ │ └── modules │ │ │ ├── forms_spec.coffee │ │ │ ├── resources_spec.coffee │ │ │ └── alerts_spec.coffee │ ├── integration │ │ ├── helpers │ │ │ ├── expect.coffee │ │ │ ├── page_objects │ │ │ │ ├── other_page.coffee │ │ │ │ ├── alert_view.coffee │ │ │ │ ├── products │ │ │ │ │ ├── index_page │ │ │ │ │ │ ├── table_view.coffee │ │ │ │ │ │ └── row_view.coffee │ │ │ │ │ ├── index_page.coffee │ │ │ │ │ ├── show_page.coffee │ │ │ │ │ └── form_page.coffee │ │ │ │ ├── tasks │ │ │ │ │ ├── form_view.coffee │ │ │ │ │ └── task_view.coffee │ │ │ │ ├── page_object.coffee │ │ │ │ └── tasks_page.coffee │ │ │ └── utils.coffee │ │ ├── other_scenario.coffee │ │ ├── tasks_scenario.coffee │ │ └── products_scenario.coffee │ ├── protractor-conf.coffee │ └── karma-conf.coffee ├── app │ ├── scripts │ │ ├── templates.coffee │ │ ├── application_test.coffee │ │ ├── controllers │ │ │ ├── products │ │ │ │ ├── show_ctrl.coffee │ │ │ │ ├── index_ctrl.coffee │ │ │ │ ├── show_actions_ctrl.coffee │ │ │ │ └── form_ctrl.coffee │ │ │ ├── main_ctrl.coffee │ │ │ ├── other_ctrl.coffee │ │ │ └── tasks_ctrl.coffee │ │ ├── application.coffee │ │ ├── base_ctrl.coffee │ │ ├── modules │ │ │ ├── resources.coffee │ │ │ ├── alerts.coffee │ │ │ └── forms.coffee │ │ └── routes.coffee │ ├── templates │ │ ├── products │ │ │ ├── show │ │ │ │ ├── info.html │ │ │ │ ├── actions.html │ │ │ │ └── details.html │ │ │ ├── show.html │ │ │ ├── list.html │ │ │ └── form.html │ │ ├── partials │ │ │ └── alerts.html │ │ ├── other.html │ │ └── tasks.html │ ├── styles │ │ ├── style.less │ │ └── animation.less │ └── index.html ├── bower.json ├── package.json └── Gruntfile.coffee ├── server ├── log │ └── .gitkeep ├── index.coffee ├── lib │ ├── utils.coffee │ ├── fixtures.coffee │ ├── product_provider.coffee │ └── app.coffee ├── package.json └── test │ ├── app_spec.coffee │ └── product_provider_spec.coffee ├── Procfile ├── script ├── build ├── start-server ├── install-all ├── start-test-server ├── test-unit ├── xvfb ├── lib │ └── server └── test-integration ├── .gitignore ├── .editorconfig ├── .travis.yml ├── shippable.yml ├── LICENSE.txt └── README.md /client/.tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/build/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: coffee server/index.coffee 2 | -------------------------------------------------------------------------------- /client/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "json": "bower.json" 4 | } 5 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run tests 4 | ./script/test-unit 5 | 6 | # build the app 7 | grunt build 8 | -------------------------------------------------------------------------------- /client/test/unit/helpers/mocks.coffee: -------------------------------------------------------------------------------- 1 | angular.module("mocks", []).config ($provide) -> 2 | $provide.value("alertTimeout", 3000) 3 | -------------------------------------------------------------------------------- /client/test/unit/helpers/sinon.coffee: -------------------------------------------------------------------------------- 1 | # Configure Sinon.JS 2 | sinon.config = 3 | useFakeTimers: false 4 | useFakeServer: false 5 | -------------------------------------------------------------------------------- /script/start-server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./script/lib/server 4 | 5 | startServer 6 | (cd client ; grunt server) 7 | stopServer 8 | -------------------------------------------------------------------------------- /server/index.coffee: -------------------------------------------------------------------------------- 1 | app = require("./lib/app") 2 | 3 | port = process.env.PORT or 5000 4 | app.listen port, -> 5 | console.log "listening on port " + port 6 | -------------------------------------------------------------------------------- /script/install-all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install bower grunt-cli -g 4 | (cd client ; npm install) 5 | (cd client ; bower install) 6 | 7 | (cd server ; npm install) 8 | -------------------------------------------------------------------------------- /client/app/scripts/templates.coffee: -------------------------------------------------------------------------------- 1 | # Placeholder for templates cache. 2 | # This fill will be overriden by `grunt ngtemplates` task 3 | angular.module("myApp").run ["$templateCache", ($templateCache) ->] 4 | -------------------------------------------------------------------------------- /script/start-test-server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ./script/lib/server 4 | 5 | startServer 6 | (cd client ; grunt build:dev configureProxies livereload-start connect:integration watch) 7 | stopServer 8 | -------------------------------------------------------------------------------- /client/build/config/targethtml.coffee: -------------------------------------------------------------------------------- 1 | # Produces html-output depending on grunt target 2 | 3 | module.exports = (grunt) -> 4 | 5 | test: 6 | files: 7 | "<%= appConfig.dev %>/index.html": "<%= appConfig.dev %>/index.html" 8 | -------------------------------------------------------------------------------- /client/build/config/bower.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | install: 4 | options: 5 | targetDir: "<%= appConfig.dev %>/components" 6 | layout: "byComponent" 7 | cleanTargetDir: true 8 | install: false 9 | -------------------------------------------------------------------------------- /client/build/config/usemin_prepare.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | options: 4 | dest: "<%= appConfig.dist %>" 5 | 6 | html: [ 7 | "<%= appConfig.dev %>/**/*.html" 8 | "!<%= appConfig.dev %>/templates/**/*.html" 9 | ] 10 | -------------------------------------------------------------------------------- /client/build/config/concat.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | dist: 4 | files: 5 | "<%= appConfig.dev %>/scripts/scripts.js": [ 6 | "<%= appConfig.dev %>/scripts/**/*.js" 7 | "<%= appConfig.app %>/scripts/**/*.js" 8 | ] 9 | -------------------------------------------------------------------------------- /client/test/integration/helpers/expect.coffee: -------------------------------------------------------------------------------- 1 | # Use the external Chai As Promised to deal with resolving promises in expectations 2 | chai = require("chai") 3 | chaiAsPromised = require("chai-as-promised") 4 | chai.use(chaiAsPromised) 5 | 6 | module.exports = chai.expect 7 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/other_page.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./page_object") 2 | 3 | class OtherPage extends PageObject 4 | 5 | @has "sayHelloButton", -> 6 | browser.element @byLabel("Say hello!", "button") 7 | 8 | module.exports = OtherPage 9 | -------------------------------------------------------------------------------- /client/app/scripts/application_test.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module "myApp" 2 | 3 | app.config [ 4 | 5 | "$provide", ($provide) -> 6 | # disable timeouts, see https://github.com/angular/angular.js/issues/2402 7 | $provide.value("alertTimeout", null) 8 | 9 | ] 10 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/products/show_ctrl.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module("myApp") 2 | 3 | class ShowCtrl extends BaseCtrl 4 | 5 | @register app, "products.ShowCtrl" 6 | @inject "$scope", "product" 7 | 8 | initialize: -> 9 | @$scope.product = @product 10 | -------------------------------------------------------------------------------- /client/build/config/clean.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | dev: [ 4 | "<%= appConfig.dev %>/**/*" 5 | "!<%= appConfig.dev %>/.git*" 6 | ] 7 | 8 | dist: [ 9 | "<%= appConfig.dist %>/**/*" 10 | "!<%= appConfig.dist %>/.git*" 11 | ] 12 | -------------------------------------------------------------------------------- /client/build/config/devUpdate.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | main: 4 | options: 5 | updateType: "report" 6 | reportUpdated: false 7 | semver: true 8 | packages: 9 | devDependencies: true 10 | dependencies: true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project files 2 | .idea 3 | *.iml 4 | 5 | # 3hrd party libraries 6 | client/node_modules 7 | client/bower_components 8 | server/node_modules 9 | 10 | # Builds 11 | client/.tmp 12 | client/coverage 13 | 14 | # Other 15 | npm-debug.log 16 | server/log/*.log 17 | -------------------------------------------------------------------------------- /client/build/config/coffeelint.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | options: 4 | max_line_length: 5 | value: 120 6 | level: "warn" 7 | 8 | app: ["Gruntfile.coffee", "<%= appConfig.app %>/scripts/**/*.coffee"] 9 | test: ["<%= appConfig.test %>/**/*.coffee"] 10 | -------------------------------------------------------------------------------- /client/build/config/htmlmin.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | dist: 4 | files: [ 5 | expand: true, 6 | cwd: "<%= appConfig.app %>", 7 | src: [ 8 | "**/*.html" 9 | "!templates/**/*.html" 10 | ], 11 | dest: "<%= appConfig.dist %>" 12 | ] 13 | -------------------------------------------------------------------------------- /client/build/config/usemin.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | options: 4 | dirs: ["<%= appConfig.dist %>"] 5 | 6 | html: [ 7 | "<%= appConfig.dist %>/**/*.html" 8 | "!<%= appConfig.dist %>/templates/**/*.html" 9 | ] 10 | 11 | css: ["<%= appConfig.dist %>/styles/**/*.css"] 12 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/main_ctrl.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module("myApp") 2 | 3 | class MainCtrl extends BaseCtrl 4 | 5 | @register app, "MainCtrl" 6 | @inject "$scope", "$state", "$stateParams" 7 | 8 | initialize: -> 9 | @$scope.$state = @$state 10 | @$scope.$stateParams = @$stateParams 11 | -------------------------------------------------------------------------------- /client/build/config/less.coffee: -------------------------------------------------------------------------------- 1 | # Compile LESS files to CSS 2 | 3 | module.exports = (grunt) -> 4 | 5 | dist: 6 | files: 7 | "<%= appConfig.dev %>/styles/style.css": "<%= appConfig.app %>/styles/style.less" 8 | "<%= appConfig.dev %>/styles/animation.css": "<%= appConfig.app %>/styles/animation.less" 9 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/other_ctrl.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module("myApp") 2 | 3 | class OtherCtrl extends BaseCtrl 4 | 5 | @register app, "OtherCtrl" 6 | @inject "alerts" 7 | 8 | initialize: -> 9 | @name = "This is the other controller" 10 | 11 | sayHello: -> 12 | @alerts.info("Hello World!") 13 | -------------------------------------------------------------------------------- /client/app/templates/products/show/info.html: -------------------------------------------------------------------------------- 1 |
2 |
Name
3 |
{{product.name}}
4 | 5 |
Price with discount
6 |
7 | 8 |
Description
9 |
{{product.description}}
10 |
11 | -------------------------------------------------------------------------------- /client/build/config/shell.coffee: -------------------------------------------------------------------------------- 1 | # Run shell commands 2 | 3 | module.exports = (grunt) -> 4 | 5 | options: 6 | stdout: true 7 | 8 | startServer: 9 | command: "./script/start-server" 10 | 11 | testServer: 12 | command: "mocha --compilers coffee:coffee-script --watch --reporter spec server/test" 13 | -------------------------------------------------------------------------------- /client/app/templates/partials/alerts.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /client/app/templates/other.html: -------------------------------------------------------------------------------- 1 | 4 |

This is a simple list of phones

5 | 6 | {{other.name}} 7 | 8 |
9 | 13 |
14 | -------------------------------------------------------------------------------- /client/app/templates/products/show/actions.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Edit 4 | 5 | 6 | 9 |
10 | -------------------------------------------------------------------------------- /client/build/config/coffee.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | # compile coffeescript sources 4 | dev: 5 | options: 6 | sourceMap: true 7 | 8 | files: [ 9 | expand: true 10 | cwd: "<%= appConfig.dev %>/scripts" 11 | src: "**/*.coffee" 12 | dest: "<%= appConfig.dev %>/scripts" 13 | ext: ".js" 14 | ] 15 | -------------------------------------------------------------------------------- /script/test-unit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run test for the server side application 4 | ./server/node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register --reporter spec server/test 5 | 6 | # ..check if it pass 7 | if [ $? -ne 0 ]; then 8 | echo "Failed!" 9 | exit 1 10 | fi 11 | 12 | # ..run specs for client side 13 | (cd client ; grunt test) 14 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/alert_view.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./page_object") 2 | 3 | class AlertView extends PageObject 4 | 5 | @has "success", -> @findAlert "success" 6 | @has "info", -> @findAlert "info" 7 | 8 | findAlert: (type) -> 9 | browser.element @By.css("div.alert-#{type} span") 10 | 11 | module.exports = AlertView 12 | -------------------------------------------------------------------------------- /client/app/templates/products/show/details.html: -------------------------------------------------------------------------------- 1 |
2 |
ID
3 |
4 | 5 |
Headline
6 |
7 | 8 |
Manufacturer
9 |
{{product.manufacturer}}
10 | 11 |
Created At
12 |
13 |
14 | -------------------------------------------------------------------------------- /client/app/scripts/application.coffee: -------------------------------------------------------------------------------- 1 | # The entry point for the application 2 | 3 | app = angular.module "myApp", [ 4 | "ngAnimate" 5 | "ui.router" 6 | "pasvaz.bindonce" 7 | 8 | "myApp.templates" 9 | "myApp.alerts" 10 | "myApp.resources" 11 | "myApp.forms" 12 | ] 13 | 14 | app.config [ 15 | 16 | "$provide", ($provide) -> 17 | $provide.value("alertTimeout", 3000) 18 | 19 | ] 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | [*.coffee] 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.js] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.css] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.html] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.md] 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/products/index_ctrl.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module("myApp") 2 | 3 | class IndexCtrl extends BaseCtrl 4 | 5 | @register app, "products.IndexCtrl" 6 | @inject "alerts", "products" 7 | 8 | deleteProduct: (product) -> 9 | promise = product.$delete() 10 | promise.then => 11 | index = @products.indexOf(product) 12 | @products.splice(index, 1) if index isnt -1 13 | 14 | @alerts.info "Product was deleted" 15 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/products/show_actions_ctrl.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module("myApp") 2 | 3 | class ShowActionsCtrl extends BaseCtrl 4 | 5 | @register app, "products.ShowActionsCtrl" 6 | @inject "$scope", "$state", "$window", "alerts" 7 | 8 | deleteProduct: -> 9 | return unless @$window.confirm("Are you sure?") 10 | 11 | promise = @$scope.product.$delete() 12 | promise.then => 13 | @alerts.info "Product was deleted" 14 | @$state.go "products.list" 15 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/products/index_page/table_view.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./../../page_object") 2 | RowView = require("./row_view") 3 | 4 | class TableView extends PageObject 5 | 6 | constructor: (@table) -> 7 | @locator = @By.repeater("product in index.products") 8 | 9 | @has "productNames", -> 10 | @table.all(@locator.column("product.name")) 11 | 12 | rowAt: (index) -> 13 | new RowView(@table, @locator.row(index)) 14 | 15 | module.exports = TableView 16 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/tasks/form_view.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./../page_object") 2 | 3 | class FormView extends PageObject 4 | 5 | constructor: (@base) -> 6 | 7 | @has "nameField", -> @base.element @By.input("task.name") 8 | 9 | @has "doneCheckbox", -> @base.element @By.input("task.done") 10 | 11 | @has "submitButton", -> @base.element @byLabel("Add", "button") 12 | 13 | setName: (value) -> 14 | @nameField.clear() 15 | @nameField.sendKeys value 16 | 17 | module.exports = FormView 18 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/products/index_page.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./../page_object") 2 | TableView = require("./index_page/table_view") 3 | 4 | class IndexPage extends PageObject 5 | 6 | @has "greeting", -> 7 | browser.element @By.binding("You have {{index.products.length}} products") 8 | 9 | @has "createButton", -> 10 | browser.element @byLabel("Create new product") 11 | 12 | @has "table", -> 13 | table = browser.element @By.css("table.products") 14 | new TableView(table) 15 | 16 | module.exports = IndexPage 17 | -------------------------------------------------------------------------------- /server/lib/utils.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | 3 | # Pauses for the given time in milliseconds 4 | sleep: (millis = 1000) -> 5 | time = new Date().getTime() + millis 6 | while new Date().getTime() <= time 7 | "do nothing" 8 | 9 | # Pauses for 0.5s / 1s or 1.5s 10 | randomSleep: (times = [500, 1000, 1500]) -> 11 | time = @randomItemFrom times 12 | @sleep time 13 | 14 | # Returns a random element from the given collection 15 | randomItemFrom: (collection) -> 16 | indx = Math.floor(Math.random() * collection.length) 17 | collection[indx] 18 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/tasks/task_view.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./../page_object") 2 | 3 | class TaskView extends PageObject 4 | 5 | constructor: (@element) -> 6 | 7 | @has "checkbox", -> 8 | @element.element @By.css("input[type=checkbox]") 9 | 10 | @has "label", -> 11 | @element.element @By.css("label span") 12 | 13 | isCompleted: -> 14 | d = protractor.promise.defer() 15 | 16 | @label.getAttribute("class").then (cls) -> 17 | d.fulfill cls.indexOf("done") isnt -1 18 | 19 | d.promise 20 | 21 | module.exports = TaskView 22 | -------------------------------------------------------------------------------- /client/test/unit/controllers/main_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Controller `MainCtrl`", -> 2 | 3 | beforeEach module "myApp" 4 | 5 | ctrl = null 6 | $scope = null 7 | 8 | # Initialize the controller 9 | beforeEach inject ($rootScope, $controller) -> 10 | $scope = $rootScope.$new() 11 | 12 | ctrl = $controller "MainCtrl", 13 | $scope: $scope 14 | 15 | describe "$scope", -> 16 | 17 | it "has `$state`", -> 18 | expect($scope.$state).to.not.be.undefined 19 | 20 | it "has `$stateParams`", -> 21 | expect($scope.$stateParams).to.not.be.undefined 22 | -------------------------------------------------------------------------------- /client/test/integration/helpers/utils.coffee: -------------------------------------------------------------------------------- 1 | request = require("request") 2 | fs = require("fs") 3 | 4 | module.exports = 5 | 6 | # Load fixtures and wait until the request is completed 7 | loadFixtures: -> 8 | baseUrl = browser.baseUrl 9 | 10 | fixturesLoaded = false 11 | request.post "#{baseUrl}/api/_loadFixtures.json", -> fixturesLoaded = true 12 | browser.wait -> fixturesLoaded 13 | 14 | takeScreenshot: (fileName = "screenshot-#{new Date()}") -> 15 | browser.takeScreenshot().then (screenshot) -> 16 | fs.writeFileSync("#{fileName}.png", screenshot, "base64") 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | - node_js 3 | 4 | node_js: 5 | - 0.10.28 6 | 7 | before_install: 8 | # install global dependencies 9 | - travis_retry npm install bower grunt-cli -g 10 | 11 | # install dependencies for both client and server side 12 | - (cd client ; travis_retry npm install) 13 | - (cd client ; travis_retry bower install) 14 | - (cd server ; travis_retry npm install) 15 | 16 | # install selenium 17 | - (cd client ; ./node_modules/protractor/bin/webdriver-manager update) 18 | 19 | # run xvfb 20 | - export DISPLAY=:99.0 21 | - sh -e /etc/init.d/xvfb start 22 | 23 | script: 24 | - ./script/test-unit 25 | - ./script/test-integration 26 | -------------------------------------------------------------------------------- /client/build/config/ngtemplates.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | # Grunt build task to concatenate & pre-load your AngularJS templates 4 | # https://github.com/ericclemmons/grunt-angular-templates 5 | 6 | options: 7 | # strinp `app/` prefix from the path 8 | url: (url) -> url.replace /^app\//, "" 9 | 10 | module: "myApp.templates" 11 | standalone: true 12 | 13 | htmlmin: 14 | collapseWhitespace: true, 15 | collapseBooleanAttributes: true 16 | 17 | app: 18 | cwd: "<%= appConfig.app %>" 19 | src: [ 20 | "templates/**/*.html" 21 | "views/**/*.html" 22 | ] 23 | dest: "<%= appConfig.dev %>/scripts/templates.js" 24 | -------------------------------------------------------------------------------- /client/test/unit/controllers/products/show_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Controller `products.ShowCtrl`", -> 2 | 3 | beforeEach module "myApp", -> 4 | 5 | $scope = null 6 | ctrl = null 7 | 8 | beforeEach inject ($rootScope, $controller, Products) -> 9 | $scope = $rootScope.$new() 10 | product = new Products(id: 123, name: "foo") 11 | 12 | ctrl = $controller "products.ShowCtrl", 13 | $scope: $scope 14 | product: product 15 | 16 | describe "$scope", -> 17 | 18 | it "has a product", -> 19 | expect($scope.product).to.not.be.undefined 20 | expect($scope.product).to.have.property "id", 123 21 | expect($scope.product).to.have.property "name", "foo" 22 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-seed", 3 | "version": "0.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/lucassus/angular-seed.git" 7 | }, 8 | "main": "./index.coffee", 9 | "dependencies": { 10 | "coffee-script": "1.7.1", 11 | 12 | "express": "4.4.0", 13 | "morgan": "1.1.1", 14 | "body-parser": "1.3.0", 15 | 16 | "lodash": "2.4.1", 17 | "Faker": "0.7.2" 18 | }, 19 | "devDependencies": { 20 | "mocha": "1.20.0", 21 | "chai": "1.9.1", 22 | "supertest": "0.13.0", 23 | 24 | "nodemon": "1.1.1", 25 | "request": "2.36.0" 26 | }, 27 | "engines": { 28 | "node": "0.10.x", 29 | "npm": "1.2.x" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/page_object.coffee: -------------------------------------------------------------------------------- 1 | # Base class for all page objects 2 | class PageObject 3 | 4 | # Alias for a collection of element locators 5 | By: protractor.By 6 | 7 | # Locates the first element containing `label` text 8 | byLabel: (label, tag = "a") -> 9 | @By.xpath ".//#{tag}[contains(text(), '#{label}')]" 10 | 11 | # Waits until all animations stop 12 | waitForAnimations: -> 13 | browser.wait => 14 | animated = browser.findElement @By.css(".ng-animate") 15 | animated.then (animated) -> animated.length is 0 16 | 17 | # Define element on the page 18 | @has: (name, getter) -> 19 | Object.defineProperty @::, name, get: getter 20 | 21 | module.exports = PageObject 22 | -------------------------------------------------------------------------------- /script/xvfb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | XVFB=/usr/bin/Xvfb 4 | DISPLAY_NUMBER=99 5 | XVFBARGS=":$DISPLAY_NUMBER -ac -screen 0 1024x768x16" 6 | PIDFILE=/tmp/xvfb_${DISPLAY_NUMBER}.pid 7 | 8 | case "$1" in 9 | start) 10 | echo -n "Starting virtual X frame buffer: Xvfb" 11 | /sbin/start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile --background --exec $XVFB -- $XVFBARGS 12 | echo "." 13 | ;; 14 | 15 | stop) 16 | echo -n "Stopping virtual X frame buffer: Xvfb" 17 | /sbin/start-stop-daemon --stop --quiet --pidfile $PIDFILE 18 | echo "." 19 | ;; 20 | 21 | restart) 22 | $0 stop 23 | $0 start 24 | ;; 25 | *) 26 | 27 | echo "Usage: script/xvfb {start|stop|restart}" 28 | exit 1 29 | esac 30 | 31 | exit 0 32 | -------------------------------------------------------------------------------- /client/app/templates/products/show.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 19 | 20 |
21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /client/test/protractor-conf.coffee: -------------------------------------------------------------------------------- 1 | exports.config = 2 | # Run with selenium standalone server 3 | # seleniumServerJar: null # use default location 4 | # seleniumPort: 4444 5 | 6 | chromeOnly: false 7 | 8 | capabilities: 9 | browserName: "firefox" 10 | 11 | # spec patterns 12 | specs: [ 13 | "integration/*_scenario.coffee" 14 | ] 15 | 16 | # A base URL for your application under test. Calls to protractor.get() 17 | # with relative paths will be prepended with this. 18 | baseUrl: "http://localhost:9010" 19 | 20 | # Use mocha (currently in beta) 21 | framework: "mocha" 22 | 23 | # Options to be passed to mocha 24 | # See the full list at http://visionmedia.github.io/mocha/ 25 | mochaOpts: 26 | ui: "bdd" 27 | reporter: "list" 28 | -------------------------------------------------------------------------------- /client/app/scripts/base_ctrl.coffee: -------------------------------------------------------------------------------- 1 | class @BaseCtrl 2 | 3 | @register: (app, name) -> 4 | name ?= @name or @toString().match(/function\s*(.*?)\(/)?[1] 5 | app = angular.module(app) if typeof app is "string" 6 | app.controller name, this 7 | 8 | @inject: (annotations...) -> 9 | ANNOTATION_REG = /^(\S+)(\s+as\s+(\w+))?$/ 10 | 11 | @annotations = _.map annotations, (annotation) -> 12 | match = annotation.match(ANNOTATION_REG) 13 | name: match[1], identifier: match[3] or match[1] 14 | 15 | @$inject = _.map @annotations, (annotation) -> annotation.name 16 | 17 | constructor: (dependencies...) -> 18 | for annotation, index in @constructor.annotations 19 | this[annotation.identifier] = dependencies[index] 20 | 21 | @initialize?() 22 | -------------------------------------------------------------------------------- /client/app/scripts/modules/resources.coffee: -------------------------------------------------------------------------------- 1 | resources = angular.module("myApp.resources", ["ngResource"]) 2 | 3 | resources.factory "Products", [ 4 | "$resource", ($resource) -> 5 | Products = $resource "/api/products/:id.json", id: "@id", 6 | query: { method: "GET", isArray: true } 7 | get: { method: "GET" } 8 | 9 | angular.extend Products.prototype, 10 | # Returns true when the product is persisted (has an id) 11 | persisted: -> !!@id 12 | 13 | # Returns price with discount 14 | priceWithDiscount: -> 15 | return @price unless @hasDiscount() 16 | return @price * (1 - @discount / 100.0) 17 | 18 | # Returns true when the product has a discount 19 | hasDiscount: -> @discount? and @discount > 0 20 | 21 | Products 22 | ] 23 | -------------------------------------------------------------------------------- /client/test/unit/controllers/other_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Controller `OtherCtrl`", -> 2 | 3 | beforeEach module "myApp", ($provide) -> 4 | # stub `alerts` service 5 | $provide.decorator "alerts", ($delegate) -> 6 | sinon.stub($delegate) 7 | $delegate 8 | 9 | ctrl = null 10 | 11 | # Initialize the controller 12 | beforeEach inject ($controller) -> 13 | ctrl = $controller "OtherCtrl" 14 | 15 | it "has a name", -> 16 | expect(ctrl.name).to.equal "This is the other controller" 17 | 18 | describe "#sayHello()", -> 19 | 20 | it "displays the flash message", inject (alerts) -> 21 | # When 22 | ctrl.sayHello() 23 | 24 | # Then 25 | expect(alerts.info).to.be.called 26 | expect(alerts.info).to.be.calledWith("Hello World!") 27 | -------------------------------------------------------------------------------- /client/app/styles/style.less: -------------------------------------------------------------------------------- 1 | #footer { 2 | height: 60px; 3 | background-color: #f5f5f5; 4 | & > .container { 5 | padding-left: 15px; 6 | padding-right: 15px; 7 | } 8 | } 9 | 10 | code { 11 | font-size: 80%; 12 | } 13 | 14 | #wrap { 15 | min-height: 100%; 16 | height: auto !important; 17 | height: 100%; 18 | margin: 0 auto -60px; 19 | padding: 0 0 60px; 20 | & > .container { 21 | padding: 60px 15px 0; 22 | } 23 | } 24 | 25 | html,body { 26 | height: 100%; 27 | } 28 | 29 | .container { 30 | .credit { 31 | margin: 20px 0; 32 | } 33 | } 34 | 35 | // tasks list 36 | .done { 37 | text-decoration: line-through; 38 | color: #808080; 39 | } 40 | 41 | // remove the dotted outline for tabs 42 | .navbar, .nav-tabs li.active { 43 | a { 44 | outline: 0px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/build/config/watch.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | coffee: 4 | files: [ 5 | "<%= appConfig.app %>/scripts/**/*.coffee" 6 | ] 7 | tasks: [ 8 | "copy:coffee" 9 | "coffee:dev" 10 | "ngtemplates" 11 | "timestamp" 12 | ] 13 | 14 | html: 15 | files: [ 16 | "<%= appConfig.app %>/**/*.html" 17 | "!<%= appConfig.app %>/templates/**/*.html" 18 | ] 19 | tasks: ["copy:dev", "timestamp"] 20 | 21 | templates: 22 | files: ["<%= appConfig.app %>/templates/**/*.html"] 23 | tasks: ["ngtemplates", "timestamp"] 24 | 25 | css: 26 | files: ["<%= appConfig.app %>/styles/**/*.less"] 27 | tasks: ["less", "timestamp"] 28 | 29 | livereload: 30 | files: ["<%= appConfig.dev %>/**/*"] 31 | tasks: ["livereload", "timestamp"] 32 | -------------------------------------------------------------------------------- /server/lib/fixtures.coffee: -------------------------------------------------------------------------------- 1 | faker = require("Faker") 2 | 3 | module.exports = 4 | 5 | products: -> 6 | products = [ 7 | { 8 | name: "HTC Wildfire", description: "Old android phone", 9 | manufacturer: "HTC", 10 | price: 499.99, discount: 10 11 | } 12 | { name: "iPhone", price: 2500 } 13 | { name: "Nexus One", price: 1000, discount: 7 } 14 | { name: "Nexus 7", price: 1200, discount: 12 } 15 | { name: "Samsung Galaxy Note", price: 2699, discount: 0 } 16 | { name: "Samsung S4", price: 3000, discount: 2 } 17 | ] 18 | 19 | for product in products 20 | product.description ?= faker.Lorem.paragraphs(3) 21 | product.manufacturer ?= faker.Company.companyName() 22 | product.headline ?= faker.Company.catchPhrase() 23 | 24 | products 25 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/products/form_ctrl.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module("myApp") 2 | 3 | class FormCtrl extends BaseCtrl 4 | 5 | @register app, "products.FormCtrl" 6 | @inject "$scope", "$location", "alerts", "product as remote" 7 | 8 | initialize: -> 9 | @reset() 10 | 11 | save: (product) -> 12 | promise = product.$save() 13 | successMessage = if product.persisted() then "Product was updated" else "Product was created" 14 | 15 | promise.then => 16 | @alerts.success successMessage 17 | @$location.path "/products" 18 | 19 | reset: -> 20 | @product = angular.copy(@remote) 21 | @$scope.product = @product 22 | 23 | delete: -> 24 | promise = @product.$delete() 25 | promise.then => 26 | @alerts.info "Product was deleted" 27 | @$location.path "/products" 28 | -------------------------------------------------------------------------------- /client/build/config/copy.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | coffee: 4 | files: [ 5 | expand: true 6 | dot: true 7 | cwd: '<%= appConfig.app %>/scripts' 8 | dest: '<%= appConfig.dev %>/scripts' 9 | src: '**/*.coffee' 10 | ] 11 | 12 | # copy resources from the app directory 13 | dev: 14 | files: [ 15 | expand: true 16 | cwd: "<%= appConfig.app %>" 17 | dest: "<%= appConfig.dev %>" 18 | src: [ 19 | "*.{ico,txt}" 20 | "**/*.js" 21 | "**/*.html" 22 | "images/**/*.{gif,webp}" 23 | ] 24 | ] 25 | 26 | # copy fonts to the dist directory 27 | dist: 28 | files: [ 29 | expand: true 30 | cwd: "<%= appConfig.dev %>/components/font-awesome" 31 | dest: "<%= appConfig.dist %>" 32 | src: ["fonts/**/*"] 33 | ] 34 | -------------------------------------------------------------------------------- /shippable.yml: -------------------------------------------------------------------------------- 1 | # Build Environment 2 | build_environment: Ubuntu 12.04 3 | 4 | # language setting 5 | language: node_js 6 | 7 | # version numbers, testing against two versions of node 8 | node_js: 9 | - 0.10.28 10 | 11 | addons: 12 | firefox: 47.0.1 13 | 14 | install: 15 | # install global dependencies 16 | - npm install bower grunt-cli -g 17 | 18 | # install dependencies for both client and server side 19 | - (cd client ; npm install) 20 | - (cd client ; bower install --allow-root) 21 | - (cd server ; npm install) 22 | 23 | # install selenium 24 | - (cd client ; ./node_modules/protractor/bin/webdriver-manager update) 25 | 26 | before_script: 27 | 28 | # run xvfb 29 | - export DISPLAY=:99.0 30 | - ./script/xvfb start 31 | 32 | after_script: 33 | - ./script/xvfb stop 34 | 35 | script: 36 | - ./script/test-unit 37 | - ./script/test-integration 38 | -------------------------------------------------------------------------------- /client/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular grunt seed", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "jquery": "2.1.1", 6 | "lodash": "2.4.1", 7 | 8 | "angular": "1.3.0-beta.10", 9 | "angular-resource": "1.3.0-beta.10", 10 | "angular-animate": "1.3.0-beta.10", 11 | "angular-messages": "1.3.0-beta.10", 12 | "angular-ui-router": "0.2.10", 13 | "angular-bindonce": "0.3.1", 14 | 15 | "bootstrap": "3.1.1", 16 | "font-awesome": "4.1.0" 17 | }, 18 | "devDependencies": { 19 | "angular-mocks": "1.3.0-beta.10" 20 | }, 21 | "exportsOverride": { 22 | "angular-ui-router": { 23 | "/": ["release/angular-ui-router.js"] 24 | }, 25 | "font-awesome": { 26 | "css": ["css/font-awesome.css", "css/font-awesome-ie7.css"], 27 | "fonts": ["fonts/*"] 28 | }, 29 | "lodash": { 30 | "/": ["dist/lodash.js"] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/test/unit/helpers/chai_sinon.coffee: -------------------------------------------------------------------------------- 1 | # Custom chai matchers for SinonJS 2 | 3 | Assertion = chai.Assertion 4 | 5 | # Pass if the spy was called at least once 6 | Assertion.addProperty "called", -> 7 | subject = @_obj 8 | 9 | @assert subject.called, 10 | "expected #{angular.mock.dump(subject)} to have been called", 11 | "expected #{angular.mock.dump(subject)} to not have been called" 12 | 13 | # Pass if the spy was called at least once with the provided arguments 14 | Assertion.addMethod "calledWith", (args...) -> 15 | subject = @_obj 16 | 17 | new Assertion(subject).to.be.called 18 | 19 | @assert subject.calledWith.apply(subject, args), 20 | "expected #{angular.mock.dump(subject)} to have been called with #\{exp} but it was called with #\{act}", 21 | "expected #{angular.mock.dump(subject)} to not have been called with #\{exp}", 22 | args, 23 | subject.lastCall.args 24 | -------------------------------------------------------------------------------- /script/lib/server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function startServer() { 4 | SERVER_PORT=5000 5 | 6 | # Return true when the server is already running 7 | function isRunning() { 8 | nc -vz localhost "$SERVER_PORT" &> /dev/null 9 | } 10 | 11 | if isRunning; then 12 | echo "The backend server is already running on port $SERVER_PORT" 13 | exit 1 14 | fi 15 | 16 | # start the backend server 17 | PORT="$SERVER_PORT" nodemon --watch server ./server/index.coffee & 18 | 19 | # ..grab its PID 20 | SERVER_PID=$! 21 | 22 | # ..and wait till it's fully operational 23 | echo -n "Waiting for the backend server." 24 | while ! isRunning; do 25 | echo -n "." 26 | sleep 0.1 27 | done 28 | 29 | echo "The backend server is fully operational PID=$SERVER_PID" 30 | } 31 | 32 | function stopServer() { 33 | echo "Killing the backend server PID=$SERVER_PID" 34 | kill "$SERVER_PID" 35 | } 36 | -------------------------------------------------------------------------------- /client/test/unit/helpers/chai_routes.coffee: -------------------------------------------------------------------------------- 1 | # Custom chai matchers for ngRoute 2 | 3 | Assertion = chai.Assertion 4 | 5 | Assertion.addMethod "templateUrl", (url) -> 6 | subject = @_obj 7 | 8 | actualTemplateUrl = subject.templateUrl 9 | @assert actualTemplateUrl is url, 10 | "expected #\{this} to have templateUrl #\{exp} but it was #\{act}", 11 | "expected #\{this} to have templateUrl #\{exp}", 12 | url, 13 | actualTemplateUrl 14 | 15 | Assertion.addMethod "controller", (ctrl) -> 16 | subject = @_obj 17 | 18 | actualController = subject.controller 19 | @assert actualController is ctrl, 20 | "expected #\{this} to have controller #\{exp} but it was #\{act}", 21 | "expected #\{this} to have controller #\{exp}", 22 | ctrl, 23 | actualController 24 | 25 | Assertion.addMethod "resolve", (name) -> 26 | subject = @_obj 27 | new Assertion(subject.resolve[name]).to.not.be.undefined 28 | -------------------------------------------------------------------------------- /script/test-integration: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | (cd client ; ./node_modules/protractor/bin/webdriver-manager update) 4 | 5 | (cd client ; grunt build:test) 6 | 7 | cd client ; grunt configureProxies connect:integration watch & 8 | PID=$! 9 | cd ../ 10 | echo "Client server is running PID=$PID" 11 | 12 | function isRunning() { 13 | nc -vz localhost 9010 &> /dev/null 14 | } 15 | 16 | echo -n "Waiting for the frontend server." 17 | while ! isRunning; do 18 | echo -n "." 19 | sleep 0.1 20 | done 21 | 22 | echo -e "\nRunning integration specs." 23 | 24 | (cd client ; ./node_modules/protractor/bin/protractor test/protractor-conf.coffee --verbose --includeStackTrace) 25 | 26 | function stopAll() { 27 | echo "Killing the frontend server PID=$PID" 28 | kill "$PID" 29 | } 30 | 31 | # ..check if it pass 32 | if [ $? -ne 0 ]; then 33 | stopAll 34 | 35 | echo "Failed!" 36 | exit 1 37 | else 38 | stopAll 39 | fi 40 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/tasks_page.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./page_object") 2 | TaskView = require("./tasks/task_view") 3 | FormView = require("./tasks/form_view") 4 | 5 | class TasksPage extends PageObject 6 | 7 | @has "remaining", -> 8 | browser.element @By.css("span#remaining") 9 | 10 | @has "archiveButton", -> 11 | browser.element @byLabel("archive") 12 | 13 | @has "tasks", -> 14 | browser.findElements @By.css("ul#tasks li") 15 | 16 | @has "form", -> 17 | form = browser.element @By.css("form[name=taskForm]") 18 | new FormView(form) 19 | 20 | taskAt: (index) -> 21 | taskElement = browser.element @By.repeater("task in tasks.tasks").row(index) 22 | new TaskView(taskElement) 23 | 24 | tasksCount: -> 25 | @waitForAnimations() 26 | 27 | d = protractor.promise.defer() 28 | @tasks.then (tasks) -> d.fulfill tasks.length 29 | d.promise 30 | 31 | module.exports = TasksPage 32 | -------------------------------------------------------------------------------- /client/test/integration/other_scenario.coffee: -------------------------------------------------------------------------------- 1 | expect = require("./helpers/expect") 2 | utils = require("./helpers/utils") 3 | 4 | AlertView = require("./helpers/page_objects/alert_view") 5 | OtherPage = require("./helpers/page_objects/other_page") 6 | 7 | describe "Other page", -> 8 | alertView = null 9 | otherPage = null 10 | 11 | beforeEach -> 12 | browser.get "/#/other" 13 | 14 | alertView = new AlertView() 15 | otherPage = new OtherPage() 16 | 17 | it "displays a valid page title", -> 18 | expect(browser.getCurrentUrl()).to.eventually.match /#\/other$/ 19 | expect(browser.getTitle()).to.eventually.eq "Angular Seed" 20 | 21 | describe "click on `Say hello!` button", -> 22 | beforeEach -> otherPage.sayHelloButton.click() 23 | 24 | it "displays an alert message", -> 25 | expect(alertView.info.isDisplayed()).to.eventually.be.true 26 | expect(alertView.info.getText()).to.eventually.eq "Hello World!" 27 | -------------------------------------------------------------------------------- /client/build/config/connect.coffee: -------------------------------------------------------------------------------- 1 | livereloadSnippet = require("grunt-contrib-livereload/lib/utils").livereloadSnippet 2 | proxySnippet = require("grunt-connect-proxy/lib/utils").proxyRequest 3 | 4 | mountFolder = (connect, dir) -> 5 | connect.static require("path").resolve(dir) 6 | 7 | module.exports = (grunt, appConfig) -> 8 | 9 | options: 10 | hostname: "localhost" 11 | 12 | proxies: [ 13 | context: "/api" 14 | host: "localhost" 15 | port: 5000 16 | https: false 17 | changeOrigin: false 18 | ] 19 | 20 | integration: 21 | options: 22 | port: 9010 23 | middleware: (connect) -> 24 | [ 25 | proxySnippet 26 | mountFolder(connect, appConfig.dev) 27 | ] 28 | 29 | livereload: 30 | options: 31 | port: 9000 32 | middleware: (connect) -> 33 | [ 34 | livereloadSnippet 35 | proxySnippet 36 | mountFolder(connect, appConfig.dev) 37 | ] 38 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/products/show_page.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./../page_object") 2 | 3 | class ShowPage extends PageObject 4 | 5 | constructor: -> 6 | 7 | @has "editButton", -> 8 | browser.element @byLabel("Edit") 9 | 10 | @has "deleteButton", -> 11 | browser.element @byLabel("Delete", "button") 12 | 13 | @has "product", -> 14 | listElement = browser.element @By.xpath("//dl") 15 | 16 | byProperty = (name) => 17 | listElement.element @By.binding("product.#{name}") 18 | 19 | Object.create Object::, 20 | name: get: -> byProperty("name") 21 | description: get: -> byProperty("description") 22 | manufacturer: get: -> byProperty("manufacturer") 23 | 24 | @has "tabs", -> 25 | browser.element @By.css(".nav-tabs") 26 | 27 | @has "tabDetails", -> 28 | @tabs.element @byLabel("Details") 29 | 30 | @has "tabActions", -> 31 | @tabs.element @byLabel("Actions") 32 | 33 | module.exports = ShowPage 34 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/products/index_page/row_view.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./../../page_object") 2 | 3 | class RowView extends PageObject 4 | 5 | constructor: (@table, @locator) -> 6 | @row = table.element(locator) 7 | 8 | # row values 9 | @has "id", -> @findField("product.id") 10 | @has "name", -> @findField("product.name") 11 | @has "description", -> @findField("product.description") 12 | 13 | # row action buttons 14 | @has "actionButton", -> 15 | @row.element @byLabel("Action", "button") 16 | 17 | @has "showButton", -> 18 | @actionButton.click() 19 | @row.element @byLabel("Show") 20 | 21 | @has "editButton", -> 22 | @actionButton.click() 23 | @row.element @byLabel("Edit") 24 | 25 | @has "deleteButton", -> 26 | @actionButton.click() 27 | @row.element @byLabel("Delete") 28 | 29 | findField: (binding) -> 30 | @table.element @locator.column(binding) 31 | 32 | module.exports = RowView 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Łukasz Bandzarewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/tasks_ctrl.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module("myApp") 2 | 3 | class TasksCtrl extends BaseCtrl 4 | 5 | @register app, "TasksCtrl" 6 | @inject "$scope" 7 | 8 | initialize: -> 9 | @tasks = [ 10 | { name: "First task", done: false } 11 | { name: "Completed task", done: true } 12 | { name: "Second task", done: false } 13 | ] 14 | 15 | @master = { name: "", done: false } 16 | @reset() 17 | 18 | incompleteTasks: -> 19 | task for task in @tasks when not task.done 20 | 21 | tasksCount: -> 22 | @tasks.length 23 | 24 | remainingTasksCount: -> 25 | @incompleteTasks().length 26 | 27 | archive: -> 28 | @tasks = @incompleteTasks() 29 | 30 | reset: -> 31 | @$scope.task = angular.copy(@master) 32 | 33 | form = @$scope.taskForm 34 | if form 35 | form.$setPristine() 36 | form.$submitted = false 37 | 38 | addTask: (task) -> 39 | # TODO pass form here 40 | form = @$scope.taskForm 41 | 42 | # TODO use custom submit method 43 | form.$submitted = true 44 | return unless form.$valid 45 | 46 | @tasks.push angular.copy(task) 47 | @reset() 48 | -------------------------------------------------------------------------------- /client/build/config/karma.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | extractOptions = (key, opts = {}) -> 4 | options = grunt.option(key) or opts.default 5 | options = options.replace(/[\s\[\]]/, "") 6 | options.split(",") 7 | 8 | # Extract browsers list from the command line 9 | # For example `grunt test --browsers=Chrome,Firefox` 10 | # Currently available browsers: 11 | # - Chrome 12 | # - ChromeCanary 13 | # - Firefox 14 | # - Opera 15 | # - Safari (only Mac) 16 | # - PhantomJS 17 | # - IE (only Windows) 18 | parseBrowsers = (opts = {}) -> 19 | opts.default or= "PhantomJS" 20 | extractOptions("browsers", opts) 21 | 22 | # common options for all karma's modes (unit, watch, coverage, e2e) 23 | options: 24 | configFile: "<%= appConfig.test %>/karma-conf.coffee" 25 | browsers: parseBrowsers(default: "PhantomJS") 26 | colors: true 27 | 28 | # If browser does not capture in given timeout [ms], kill it 29 | captureTimeout: 5000 30 | 31 | # single run karma for unit tests 32 | unit: 33 | singleRun: true 34 | 35 | # run karma for unit tests in watch mode 36 | watch: 37 | singleRun: false 38 | autoWatch: true 39 | -------------------------------------------------------------------------------- /client/app/styles/animation.less: -------------------------------------------------------------------------------- 1 | // some ng-animations 2 | 3 | /* 4 | The animate class is apart of the element and the ng-enter class 5 | is attached to the element once the enter animation event is triggered 6 | */ 7 | .reveal-animation.ng-enter { 8 | -webkit-transition: 1s linear all; /* Safari/Chrome */ 9 | -moz-transition: 1s linear all; /* Firefox */ 10 | -o-transition: 1s linear all; /* Opera */ 11 | transition: 1s linear all; /* IE10+ and Future Browsers */ 12 | 13 | /* The animation preparation code */ 14 | opacity: 0; 15 | } 16 | 17 | /* 18 | Keep in mind that you want to combine both CSS 19 | classes together to avoid any CSS-specificity 20 | conflicts 21 | */ 22 | .reveal-animation.ng-enter.ng-enter-active { 23 | /* The animation code itself */ 24 | opacity: 1; 25 | } 26 | 27 | .repeat-item.ng-enter,.repeat-item.ng-leave { 28 | -webkit-transition: 0.5s linear all; 29 | -moz-transition: 0.5s linear all; 30 | -o-transition: 0.5s linear all; 31 | transition: 0.5s linear all; 32 | } 33 | 34 | .repeat-item.ng-leave, .repeat-item.ng-enter.ng-enter-active { 35 | opacity: 1; 36 | } 37 | 38 | .repeat-item.ng-enter, .repeat-item.ng-leave.ng-leave-active { 39 | opacity: 0; 40 | } 41 | -------------------------------------------------------------------------------- /client/test/integration/helpers/page_objects/products/form_page.coffee: -------------------------------------------------------------------------------- 1 | PageObject = require("./../page_object") 2 | 3 | class FormPage extends PageObject 4 | 5 | @has "nameField", -> @findField "product.name" 6 | @has "priceField", -> @findField "product.price" 7 | @has "discountField", -> @findField "product.discount" 8 | @has "descriptionField", -> @findField "product.description", @By.textarea 9 | 10 | setName: (name) -> @setFieldValue @nameField, name 11 | setPrice: (price) -> @setFieldValue @priceField, price 12 | setDiscount: (discount) -> @setFieldValue @discountField, discount 13 | setDescription: (description) -> @setFieldValue @descriptionField, description 14 | 15 | @has "submitButton", -> 16 | browser.findElement @By.xpath("//button[@type='submit']") 17 | 18 | @has "resetButton", -> 19 | browser.findElement @byLabel("Reset") 20 | 21 | @has "cancelButton", -> 22 | browser.findElement @byLabel("Cancel") 23 | 24 | findField: (model, findBy = @By.model) -> 25 | browser.findElement findBy(model) 26 | 27 | setFieldValue: (field, value) -> 28 | field.clear() 29 | field.sendKeys value 30 | 31 | module.exports = FormPage 32 | -------------------------------------------------------------------------------- /client/test/unit/controllers/products/index_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Controller `products.IndexCtrl`", -> 2 | 3 | # stub external services 4 | beforeEach module "myApp", ($provide) -> 5 | $provide.decorator "alerts", ($delegate) -> 6 | sinon.stub($delegate) 7 | $delegate 8 | 9 | $scope = null 10 | ctrl = null 11 | 12 | beforeEach inject ($rootScope, $controller) -> 13 | $scope = $rootScope.$new() 14 | 15 | ctrl = $controller "products.IndexCtrl", 16 | $scope: $scope 17 | products: [{ name: "one" }, { name: "two" }] 18 | 19 | it "has a list of products", -> 20 | expect(ctrl.products).to.not.be.undefined 21 | expect(ctrl.products.length).to.equal 2 22 | 23 | describe "#deleteProduct()", -> 24 | product = null 25 | 26 | beforeEach inject ($q) -> 27 | deferred = $q.defer() 28 | deferred.resolve(true) # always resolved 29 | 30 | product = $delete: sinon.stub().returns deferred.promise 31 | $scope.$apply -> ctrl.deleteProduct(product) 32 | 33 | it "deletes the product", -> 34 | expect(product.$delete).to.be.called 35 | 36 | it "sets an alert", inject (alerts) -> 37 | expect(alerts.info).to.be.calledWith "Product was deleted" 38 | -------------------------------------------------------------------------------- /server/lib/product_provider.coffee: -------------------------------------------------------------------------------- 1 | _ = require("lodash") 2 | 3 | # Responsible for returning and updating the data for products 4 | class ProductProvider 5 | 6 | constructor: (products = []) -> 7 | @products = [] 8 | @save(products) 9 | 10 | findAll: (callback = ->) -> 11 | callback(null, @products) 12 | 13 | findById: (id, callback = ->) -> 14 | product = _.findWhere(@products, id: parseInt(id)) 15 | callback(null, product) 16 | 17 | save: (products, callback = ->) -> 18 | if typeof(products.length) is "undefined" 19 | products = [products] 20 | 21 | for product in products 22 | product.id = @_nextId() 23 | product.createdAt = new Date() 24 | 25 | @products.push(product) 26 | 27 | callback(null, products) 28 | 29 | update: (id, params, callback = ->) -> 30 | @findById id, (error, product) -> 31 | _.extend(product, _.omit(params, "id")) 32 | callback(null, product) 33 | 34 | destroy: (id, callback = ->) -> 35 | @findById id, (error, product) => 36 | @products = _.reject @products, (row) -> row.id is product.id 37 | callback(null, product) 38 | 39 | destroyAll: (callback = ->) -> 40 | @products = [] 41 | callback() 42 | 43 | # @private 44 | _nextId: -> 45 | @_currentId or= 0 46 | ++@_currentId 47 | 48 | module.exports = ProductProvider 49 | -------------------------------------------------------------------------------- /client/app/templates/tasks.html: -------------------------------------------------------------------------------- 1 | 4 |

Very simple TODO list

5 | 6 | {{tasks.remainingTasksCount()}} of {{tasks.tasksCount()}} remaining 7 | [ archive ] 8 | 9 | 17 | 18 |
19 |
21 | 22 | 24 | Task name is required 26 |
27 | 28 |
29 | 33 |
34 | 35 | 38 | 39 | 44 |
45 | -------------------------------------------------------------------------------- /server/lib/app.coffee: -------------------------------------------------------------------------------- 1 | express = require("express") 2 | fs = require("fs") 3 | path = require("path") 4 | 5 | app = express() 6 | 7 | # configure logger 8 | logFile = fs.createWriteStream(path.join(__dirname, "../log/express.log"), flags: "a") 9 | morgan = require("morgan") 10 | app.use morgan(stream: logFile) 11 | 12 | bodyParser = require("body-parser") 13 | app.use bodyParser() 14 | 15 | app.use express.static(path.join(__dirname, "../../client/dist")) 16 | 17 | utils = require("./utils") 18 | 19 | ProductProvider = require("./product_provider") 20 | productProvider = new ProductProvider() 21 | 22 | # bootstrap with dummy data 23 | fixtures = require("./fixtures") 24 | productProvider.save fixtures.products() 25 | 26 | api = express.Router() 27 | 28 | api.get "/products.json", (req, res) -> 29 | productProvider.findAll (error, products) -> 30 | res.send products 31 | 32 | api.post "/products.json", (req, res) -> 33 | productProvider.save req.body, (error, products) -> 34 | res.send products[0] 35 | 36 | api.post "/products/:id.json", (req, res) -> 37 | id = req.params.id 38 | params = req.body 39 | 40 | productProvider.update id, params, (error, product) -> 41 | res.send product 42 | 43 | api.get "/products/:id.json", (req, res) -> 44 | id = req.params.id 45 | 46 | productProvider.findById id, (error, product) -> 47 | res.send if product? then product else 404 48 | 49 | api.delete "/products/:id.json", (req, res) -> 50 | id = req.params.id 51 | 52 | productProvider.destroy id, (error, product) -> 53 | res.send product 54 | 55 | api.post "/_loadFixtures.json", (req, res) -> 56 | productProvider.destroyAll -> 57 | productProvider.save fixtures.products(), -> 58 | res.send 200 59 | 60 | app.use "/api", api 61 | 62 | module.exports = app 63 | -------------------------------------------------------------------------------- /client/app/scripts/modules/alerts.coffee: -------------------------------------------------------------------------------- 1 | alerts = angular.module("myApp.alerts", ["myApp.templates"]) 2 | 3 | class AlertsController 4 | @$inject = ["$scope", "alerts"] 5 | constructor: (@$scope, @alerts) -> 6 | @$scope.alertMessages = @alerts.messages 7 | 8 | @$scope.disposeAlert = (id) => 9 | @alerts.dispose(id) 10 | 11 | alerts.controller "alerts", AlertsController 12 | 13 | alerts.factory "alerts", [ 14 | "$log", "$timeout", "alertTimeout", ($log, $timeout, alertTimeout) -> 15 | lastId: 0 16 | messages: [] 17 | 18 | # Returns a next id for the new message 19 | nextId: -> 20 | @lastId += 1 21 | 22 | push: (type, text) -> 23 | id = @nextId() 24 | $log.info("Alert [#{id}, #{type}]", text) 25 | 26 | @messages.push(id: id, type: type, text: text) 27 | @delayedDispose(id) 28 | 29 | id 30 | 31 | # Helper methods for various alerts types 32 | info: (text) -> @push("info", text) 33 | success: (text) -> @push("success", text) 34 | warning: (text) -> @push("warning", text) 35 | error: (text) -> @push("error", text) 36 | 37 | # Disposes a message with the given id 38 | dispose: (id) -> 39 | at = @messages.map((message) -> message.id).indexOf(id) 40 | @messages.splice(at, 1) 41 | 42 | # Dispose the message after the given time in milliseconds 43 | delayedDispose: (id) -> 44 | if alertTimeout? and alertTimeout > 0 45 | disposeTheAlert = => 46 | $log.info("Disposing alert", id, "after", alertTimeout, "milliseconds") 47 | @dispose(id) 48 | $timeout(disposeTheAlert, alertTimeout) 49 | ] 50 | 51 | alerts.directive "alerts", -> 52 | restrict: "E" 53 | transclude: true 54 | 55 | templateUrl: "templates/partials/alerts.html" 56 | replace: true 57 | 58 | controller: "alerts" 59 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-seed", 3 | "version": "0.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/lucassus/angular-seed.git" 7 | }, 8 | "dependencies": {}, 9 | "devDependencies": { 10 | "chai": "1.9.1", 11 | "chai-as-promised": "4.1.1", 12 | "coffee-script": "1.7.1", 13 | "grunt": "0.4.5", 14 | "grunt-angular-templates": "0.5.5", 15 | "grunt-bower-task": "0.3.4", 16 | "grunt-coffeelint": "0.0.10", 17 | "grunt-connect-proxy": "0.1.10", 18 | "grunt-contrib-clean": "0.5.0", 19 | "grunt-contrib-coffee": "0.10.1", 20 | "grunt-contrib-concat": "0.4.0", 21 | "grunt-contrib-connect": "0.7.1", 22 | "grunt-contrib-copy": "0.5.0", 23 | "grunt-contrib-cssmin": "0.9.0", 24 | "grunt-contrib-htmlmin": "0.3.0", 25 | "grunt-contrib-less": "0.11.1", 26 | "grunt-contrib-livereload": "0.1.2", 27 | "grunt-contrib-uglify": "0.4.0", 28 | "grunt-contrib-watch": "0.6.1", 29 | "grunt-dev-update": "0.5.8", 30 | "grunt-karma": "0.8.3", 31 | "grunt-regarde": "0.1.1", 32 | "grunt-shell": "0.7.0", 33 | "grunt-targethtml": "0.2.6", 34 | "grunt-usemin": "2.1.1", 35 | "karma": "0.12.16", 36 | "karma-chai": "0.1.0", 37 | "karma-chrome-launcher": "0.1.4", 38 | "karma-coffee-preprocessor": "0.2.1", 39 | "karma-coverage": "0.2.4", 40 | "karma-firefox-launcher": "0.1.3", 41 | "karma-mocha": "0.1.3", 42 | "karma-ng-html2js-preprocessor": "0.1.0", 43 | "karma-opera-launcher": "0.1.0", 44 | "karma-phantomjs-launcher": "0.1.4", 45 | "karma-sinon": "1.0.3", 46 | "karma-spec-reporter": "0.0.13", 47 | "load-grunt-tasks": "0.4.0", 48 | "protractor": "0.24.0", 49 | "request": "^2.36.0", 50 | "supertest": "0.13.0" 51 | }, 52 | "engines": { 53 | "node": "0.10.x", 54 | "npm": "1.2.x" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/app/templates/products/list.html: -------------------------------------------------------------------------------- 1 | 4 |

You have {{index.products.length}} products

5 | 6 | 7 | Create new product 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 |
#ActionsNamePriceDiscountDescriptionCreated At
{{product.id}} 27 |
28 | 31 | 37 |
38 |
{{product.name}}{{product.priceWithDiscount() | currency}} 42 | {{product.discount}}% 43 | --- 44 | {{product.description}}{{product.createdAt | date}}
50 | -------------------------------------------------------------------------------- /client/app/scripts/modules/forms.coffee: -------------------------------------------------------------------------------- 1 | forms = angular.module("myApp.forms", ["ngMessages"]) 2 | 3 | # Custom submit directive 4 | # * calls the given expression only when the form is valid 5 | # * marks the current form as submitted 6 | forms.directive "mySubmit", [ 7 | "$parse", "$log", ($parse, $log) -> 8 | 9 | require: "form" 10 | 11 | compile: (element, attrs) -> 12 | onSubmit = $parse(attrs.mySubmit) 13 | 14 | (scope, element, attrs, formCtrl) -> 15 | 16 | element.on "submit", (event) -> 17 | $log.debug "Submitting the form '#{formCtrl.$name}'", formCtrl 18 | 19 | scope.$apply -> 20 | onSubmit(scope, $event: event) if formCtrl.$valid 21 | formCtrl.$submitted = true 22 | 23 | ] 24 | 25 | # Wrapper for `ngMessages` directive 26 | forms.directive "myMessages", [ 27 | -> 28 | 29 | scope: true 30 | replace: true 31 | transclude: true 32 | require: "^form" 33 | 34 | link: (scope, element, attrs, formCtrl) -> 35 | field = formCtrl[attrs.myMessages] 36 | 37 | scope.touched = -> formCtrl.$submitted or field.$dirty 38 | scope.messages = -> field.$error 39 | 40 | template: """ 41 |
44 | """ 45 | ] 46 | 47 | # Toggles bootstrap classes (has-error, has-success) 48 | # TODO write specs for this directive 49 | forms.directive "myErrorFor", [ 50 | -> 51 | 52 | restrict: "A" 53 | scope: true 54 | replace: true 55 | transclude: true 56 | require: "^form" 57 | 58 | link: (scope, element, attrs, formCtrl) -> 59 | field = formCtrl[attrs.myErrorFor] 60 | 61 | touched = -> formCtrl.$submitted or field.$dirty 62 | 63 | scope.hasError = -> touched() and field.$invalid 64 | scope.hasSuccess = -> touched() and field.$valid 65 | 66 | template: """ 67 |
69 | """ 70 | 71 | ] 72 | -------------------------------------------------------------------------------- /client/test/unit/base_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "BaseCtrl", -> 2 | 3 | app = null 4 | beforeEach -> 5 | app = angular.module("testApp", []) 6 | app.value "foo", "this is foo" 7 | app.value "my.foo", "this is other foo" 8 | 9 | describe "basic usage", -> 10 | 11 | beforeEach -> 12 | class MyCtrl extends BaseCtrl 13 | 14 | @register app 15 | @inject "$scope", "foo" 16 | 17 | initialize: -> 18 | @$scope.foo = @foo 19 | @$scope.bar = "bar" 20 | 21 | module "testApp" 22 | 23 | it "registers the controller with all dependencies", inject ($rootScope, $controller) -> 24 | $scope = $rootScope.$new() 25 | $controller "MyCtrl", $scope: $scope 26 | 27 | expect($scope.foo).to.eq "this is foo" 28 | expect($scope.bar).to.eq "bar" 29 | 30 | describe "when the controller name is specified", -> 31 | 32 | beforeEach -> 33 | class MyCtrl extends BaseCtrl 34 | 35 | @register app, "my.controller" 36 | @inject "$scope" 37 | 38 | initialize: -> 39 | @$scope.foo = "foo" 40 | 41 | module "testApp" 42 | 43 | it "register controller at the different name", inject ($rootScope, $controller) -> 44 | $scope = $rootScope.$new() 45 | $controller "my.controller", $scope: $scope 46 | 47 | expect($scope.foo).to.eq "foo" 48 | 49 | describe "when different name for a dependency is specified", -> 50 | 51 | beforeEach -> 52 | class OtherCtrl extends BaseCtrl 53 | 54 | @register app 55 | @inject "$scope", "my.foo as foobar" 56 | 57 | initialize: -> 58 | @$scope.foo = @foobar 59 | 60 | module "testApp" 61 | 62 | it "registers the dependency under different name", inject ($rootScope, $controller) -> 63 | $scope = $rootScope.$new() 64 | $controller "OtherCtrl", $scope: $scope 65 | 66 | expect($scope.foo).to.eq "this is other foo" 67 | 68 | describe "when the module name is given", -> 69 | 70 | beforeEach -> 71 | class OtherCtrl extends BaseCtrl 72 | 73 | @register "testApp" 74 | @inject "$scope", "my.foo as foobar" 75 | 76 | initialize: -> 77 | @$scope.foo = @foobar 78 | 79 | module "testApp" 80 | 81 | it "registers under the given module", inject ($rootScope, $controller) -> 82 | $scope = $rootScope.$new() 83 | $controller "OtherCtrl", $scope: $scope 84 | 85 | expect($scope.foo).to.eq "this is other foo" 86 | -------------------------------------------------------------------------------- /client/test/unit/controllers/products/show_actions_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Controller `products.ShowActionsCtrl`", -> 2 | 3 | # stub external services 4 | beforeEach module "myApp", ($provide) -> 5 | $provide.value "$state", sinon.stub(go: ->) 6 | $provide.value "$window", confirm: sinon.stub() 7 | 8 | $provide.decorator "alerts", ($delegate) -> 9 | sinon.stub($delegate) 10 | $delegate 11 | 12 | $scope = null 13 | ctrl = null 14 | 15 | beforeEach inject ($rootScope, $controller, Products) -> 16 | $scope = $rootScope.$new() 17 | product = new Products(id: 123, name: "foo") 18 | $scope.product = product 19 | 20 | ctrl = $controller "products.ShowActionsCtrl", 21 | $scope: $scope 22 | 23 | describe "$scope", -> 24 | 25 | it "has a product", -> 26 | expect($scope.product).to.not.be.undefined 27 | expect($scope.product).to.have.property "id", 123 28 | expect($scope.product).to.have.property "name", "foo" 29 | 30 | describe "#deleteProduct()", -> 31 | beforeEach inject ($window) -> 32 | $window.confirm.returns(false) 33 | 34 | it "displays a confirmation dialog", inject ($window) -> 35 | ctrl.deleteProduct() 36 | 37 | expect($window.confirm.called).to.be.true 38 | expect($window.confirm.calledWith("Are you sure?")).to.be.true 39 | 40 | context "when the confirmation dialog was accepted", -> 41 | beforeEach inject ($httpBackend) -> 42 | $httpBackend.expectDELETE("/api/products/123.json").respond 200 43 | 44 | beforeEach inject ($window, $httpBackend) -> 45 | $window.confirm.returns(true) 46 | 47 | ctrl.deleteProduct() 48 | $httpBackend.flush() 49 | 50 | it "deletes the product", inject ($httpBackend) -> 51 | $httpBackend.verifyNoOutstandingExpectation() 52 | $httpBackend.verifyNoOutstandingRequest() 53 | 54 | it "sets an alert", inject (alerts) -> 55 | expect(alerts.info).to.be.calledWith "Product was deleted" 56 | 57 | it "redirects to the products list page", inject ($state) -> 58 | expect($state.go).to.be.calledWith "products.list" 59 | 60 | confirm "when the confirmation was not accepted", -> 61 | beforeEach inject ($window) -> 62 | $window.confirm.returns(false) 63 | ctrl.deleteProduct() 64 | 65 | it "does not delete a product", inject ($httpBackend) -> 66 | $httpBackend.verifyNoOutstandingExpectation() 67 | $httpBackend.verifyNoOutstandingRequest() 68 | -------------------------------------------------------------------------------- /client/app/scripts/routes.coffee: -------------------------------------------------------------------------------- 1 | # Defines routes for the application 2 | 3 | app = angular.module "myApp" 4 | 5 | app.config [ 6 | "$stateProvider", "$urlRouterProvider", ($stateProvider, $urlRouterProvider) -> 7 | 8 | # For any unmatched url, redirect to /products 9 | $urlRouterProvider.otherwise "/products" 10 | 11 | $stateProvider 12 | .state("products", { 13 | abstract: true 14 | url: "/products" 15 | template: "" 16 | }) 17 | 18 | .state("products.list", { 19 | url: "" 20 | templateUrl: "templates/products/list.html" 21 | controller: "products.IndexCtrl as index" 22 | resolve: 23 | products: ["Products", (Products) -> Products.query().$promise] 24 | }) 25 | 26 | .state("products.create", { 27 | url: "/create" 28 | templateUrl: "templates/products/form.html" 29 | controller: "products.FormCtrl as form" 30 | resolve: 31 | product: ["Products", (Products) -> new Products()] 32 | }) 33 | 34 | .state("products.edit", { 35 | url: "/:id/edit" 36 | templateUrl: "templates/products/form.html" 37 | controller: "products.FormCtrl as form" 38 | resolve: 39 | product: ["Products", "$stateParams", (Products, $stateParams) -> 40 | Products.get(id: $stateParams.id).$promise 41 | ] 42 | }) 43 | 44 | .state("products.show", { 45 | abstract: true 46 | url: "/:id" 47 | templateUrl: "templates/products/show.html" 48 | controller: "products.ShowCtrl as show" 49 | resolve: 50 | product: ["Products", "$stateParams", (Products, $stateParams) -> 51 | Products.get(id: $stateParams.id).$promise 52 | ] 53 | }) 54 | 55 | .state("products.show.info", { 56 | url: "" 57 | templateUrl: "templates/products/show/info.html" 58 | }) 59 | 60 | .state("products.show.details", { 61 | url: "/details" 62 | templateUrl: "templates/products/show/details.html" 63 | }) 64 | 65 | .state("products.show.actions", { 66 | url: "/actions" 67 | templateUrl: "templates/products/show/actions.html" 68 | controller: "products.ShowActionsCtrl as actions" 69 | }) 70 | 71 | .state("other", { 72 | url: "/other", 73 | templateUrl: "templates/other.html" 74 | controller: "OtherCtrl as other" 75 | }) 76 | 77 | .state("tasks", { 78 | url: "/tasks" 79 | templateUrl: "templates/tasks.html" 80 | controller: "TasksCtrl as tasks" 81 | }) 82 | ] 83 | -------------------------------------------------------------------------------- /client/test/integration/tasks_scenario.coffee: -------------------------------------------------------------------------------- 1 | expect = require("./helpers/expect") 2 | utils = require("./helpers/utils") 3 | 4 | TasksPage = require("./helpers/page_objects/tasks_page") 5 | 6 | describe "Tasks page", -> 7 | tasksPage = null 8 | 9 | beforeEach -> 10 | browser.get "/#/tasks" 11 | 12 | tasksPage = new TasksPage() 13 | 14 | it "displays a valid page title", -> 15 | expect(browser.getCurrentUrl()).to.eventually.match /#\/tasks$/ 16 | expect(browser.getTitle()).to.eventually.eq "Angular Seed" 17 | 18 | describe "tasks list", -> 19 | 20 | it "displays all tasks", -> 21 | expect(tasksPage.tasksCount()).to.eventually.eq 3 22 | expect(tasksPage.remaining.getText()).to.eventually.eq "2 of 3 remaining" 23 | 24 | task = tasksPage.taskAt(0) 25 | expect(task.isCompleted()).to.eventually.be.false 26 | 27 | completedTask = tasksPage.taskAt(2) 28 | expect(completedTask.isCompleted()).to.eventually.be.true 29 | 30 | describe "click on `archive` button", -> 31 | beforeEach -> 32 | tasksPage.archiveButton.click() 33 | 34 | it "archives all completed tasks", -> 35 | expect(tasksPage.tasksCount()).to.eventually.eq 2 36 | expect(tasksPage.remaining.getText()).to.eventually.eq "2 of 2 remaining" 37 | 38 | describe "click on task's checkbox", -> 39 | task = null 40 | 41 | describe "when a task is not completed", -> 42 | beforeEach -> 43 | task = tasksPage.taskAt(1) 44 | task.checkbox.click() 45 | 46 | it "marks the task as completed", -> 47 | expect(task.isCompleted()).to.eventually.be.true 48 | expect(tasksPage.remaining.getText()).to.eventually.eq "1 of 3 remaining" 49 | 50 | describe "when a task is completed", -> 51 | beforeEach -> 52 | task = tasksPage.taskAt(2) 53 | task.checkbox.click() 54 | 55 | it "marks the task as not completed", -> 56 | expect(task.isCompleted()).to.eventually.be.false 57 | expect(tasksPage.remaining.getText()).to.eventually.eq "3 of 3 remaining" 58 | 59 | describe "new task form", -> 60 | form = null 61 | beforeEach -> form = tasksPage.form 62 | 63 | describe "fill in the name and click `create` button", -> 64 | 65 | it "creates a new task", -> 66 | form.setName "New task" 67 | form.submitButton.click() 68 | 69 | expect(tasksPage.tasksCount()).to.eventually.eq 4 70 | expect(tasksPage.remaining.getText()).to.eventually.eq "3 of 4 remaining" 71 | 72 | task = tasksPage.taskAt(2) 73 | expect(task.isCompleted()).to.eventually.be.false 74 | expect(task.label.getText()).to.eventually.eq "New task" 75 | -------------------------------------------------------------------------------- /client/Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | # load all grunt tasks 3 | require("load-grunt-tasks")(grunt) 4 | grunt.loadTasks("build/tasks") 5 | 6 | # configurable paths 7 | appConfig = 8 | app: "app" 9 | test: "test" 10 | dist: "dist" 11 | dev: ".tmp" 12 | 13 | config = (name) -> 14 | require("./build/config/#{name}")(grunt, appConfig) 15 | 16 | grunt.initConfig 17 | appConfig: appConfig 18 | pkg: grunt.file.readJSON("package.json") 19 | 20 | watch: config("watch") 21 | coffee: config("coffee") 22 | less: config("less") 23 | concat: config("concat") 24 | targethtml: config("targethtml") 25 | useminPrepare: config("usemin_prepare") 26 | usemin: config("usemin") 27 | htmlmin: config("htmlmin") 28 | copy: config("copy") 29 | coffeelint: config("coffeelint") 30 | ngtemplates: config("ngtemplates") 31 | bower: config("bower") 32 | karma: config("karma") 33 | clean: config("clean") 34 | connect: config("connect") 35 | shell: config("shell") 36 | devUpdate: config("devUpdate") 37 | 38 | grunt.renameTask "regarde", "watch" 39 | 40 | grunt.registerTask "timestamp", -> grunt.log.subhead "--- timestamp: #{new Date()}" 41 | 42 | grunt.registerTask "build:dev", [ 43 | "clean" 44 | "bower" 45 | "coffeelint" 46 | "copy:coffee" 47 | "coffee" 48 | "less" 49 | "copy:dev" 50 | "ngtemplates" 51 | ] 52 | 53 | grunt.registerTask "build:test", [ 54 | "build:dev" 55 | "targethtml:test" 56 | ] 57 | 58 | grunt.registerTask "server", [ 59 | "build:dev" 60 | 61 | "configureProxies" 62 | "livereload-start" 63 | "connect:livereload" 64 | "watch" 65 | ] 66 | 67 | # run unit tests 68 | grunt.registerTask "test:unit", [ 69 | "karma:unit" 70 | ] 71 | 72 | # run unit tests in the watch mode 73 | grunt.registerTask "test:unit:watch", [ 74 | "karma:watch" 75 | ] 76 | 77 | # run unit tests in the watch mode 78 | grunt.registerTask "test:watch", [ 79 | "test:unit:watch" 80 | ] 81 | 82 | grunt.registerTask "test", [ 83 | "karma:unit" 84 | ] 85 | 86 | grunt.registerTask "build:dist", [ 87 | "test:unit" 88 | 89 | "build:dev" 90 | "useminPrepare" 91 | "htmlmin" 92 | "concat" 93 | "copy:dist" 94 | "usemin" 95 | "uglify" 96 | "cssmin" 97 | ] 98 | 99 | grunt.renameTask "build:dist", "build" 100 | 101 | # Used during heroku deployment 102 | grunt.registerTask "heroku:production", ["build"] 103 | 104 | grunt.registerTask "default", ["test"] 105 | -------------------------------------------------------------------------------- /client/test/karma-conf.coffee: -------------------------------------------------------------------------------- 1 | # Karma configuration 2 | module.exports = (config) -> 3 | config.set 4 | basePath: "../" 5 | 6 | frameworks: [ 7 | "mocha" 8 | "chai" 9 | "sinon" 10 | ] 11 | 12 | # list of files / patterns to load in the browser 13 | files: [ 14 | "bower_components/jquery/dist/jquery.js" 15 | "bower_components/lodash/dist/lodash.js" 16 | 17 | "bower_components/angular/angular.js" 18 | "bower_components/angular-mocks/angular-mocks.js" 19 | "bower_components/angular-resource/angular-resource.js" 20 | "bower_components/angular-animate/angular-animate.js" 21 | "bower_components/angular-messages/angular-messages.js" 22 | "bower_components/angular-ui-router/release/angular-ui-router.js" 23 | "bower_components/angular-bindonce/bindonce.js" 24 | 25 | "app/templates/**/*.html" 26 | 27 | "app/scripts/modules/**/*.coffee" 28 | "app/scripts/application.coffee" 29 | "app/scripts/routes.coffee" 30 | "app/scripts/base_ctrl.coffee" 31 | "app/scripts/controllers/**/*.coffee" 32 | 33 | "test/unit/helpers/**/*.coffee" 34 | "test/unit/**/*_spec.coffee" 35 | ] 36 | 37 | preprocessors: 38 | "**/*.html": ["html2js"] 39 | 40 | "app/scripts/**/*.coffee": ["coverage"] 41 | "test/unit/**/*.coffee": ["coffee"] 42 | 43 | ngHtml2JsPreprocessor: 44 | stripPrefix: "app/" 45 | moduleName: "myApp.templates" 46 | 47 | # Test results reporter to use. Possible values: dots || progress || growl 48 | reporters: ["dots", "coverage"] 49 | 50 | # html - produces a bunch of HTML files with annotated source code 51 | # lcovonly - produces an lcov.info file 52 | # lcov - produces html + lcov files. This is the default format 53 | # cobertura - produces a cobertura-coverage.xml file for easy Hudson integration 54 | # text-summary - produces a compact text summary of coverage, typically to console 55 | # text - produces a detailed text table with coverage for all files 56 | coverageReporter: 57 | reporters: [ 58 | { type: "html" } 59 | { type: "text", file: "karma-coverage.txt" } 60 | { type: "text-summary" } 61 | { type: "cobertura" } 62 | ] 63 | dir: "coverage" 64 | 65 | # web server port 66 | port: 8080 67 | 68 | # cli runner port 69 | runnerPort: 9100 70 | 71 | # enable / disable watching file and executing tests whenever any file changes 72 | autoWatch: true 73 | 74 | # Start these browsers, currently available: 75 | # - Chrome 76 | # - ChromeCanary 77 | # - Firefox 78 | # - Opera 79 | # - Safari (only Mac) 80 | # - PhantomJS 81 | # - IE (only Windows) 82 | browsers: ["PhantomJS"] 83 | 84 | # Continuous Integration mode 85 | # if true, it capture browsers, run tests and exit 86 | singleRun: false 87 | 88 | # level of logging 89 | # possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 90 | logLevel: config.LOG_WARN 91 | -------------------------------------------------------------------------------- /server/test/app_spec.coffee: -------------------------------------------------------------------------------- 1 | app = require("../lib/app") 2 | expect = require("chai").expect 3 | request = require("supertest") 4 | 5 | describe "The application", -> 6 | 7 | describe "GET /api/products.json", -> 8 | 9 | it "returns a list of products", (done) -> 10 | request(app) 11 | .get("/api/products.json") 12 | .set("Accept", "application/json") 13 | .expect("Content-Type", /json/) 14 | .expect(200) 15 | .end (error, resp) -> 16 | products = resp.body 17 | 18 | expect(products).to.not.be.undefined 19 | expect(products.length).to.equal 6 20 | 21 | done() 22 | 23 | describe "POST /api/products.json", -> 24 | 25 | it "creates a product", (done) -> 26 | request(app) 27 | .post("/api/products.json") 28 | .set("Accept", "application/json") 29 | .send(name: "New product") 30 | .expect("Content-Type", /json/) 31 | .expect(200) 32 | .end (error, resp) -> 33 | product = resp.body 34 | 35 | expect(product).to.not.be.undefined 36 | expect(product.id).to.equal 7 37 | expect(product.name).to.equal "New product" 38 | 39 | done() 40 | 41 | describe "POST /api/products/:id.json", -> 42 | 43 | it "updates a product", (done) -> 44 | request(app) 45 | .post("/api/products/2.json") 46 | .set("Accept", "application/json") 47 | .send(name: "New name", description: "New description") 48 | .expect("Content-Type", /json/) 49 | .expect(200) 50 | .end (error, resp) -> 51 | product = resp.body 52 | 53 | expect(product).to.not.be.undefined 54 | expect(product.id).to.equal 2 55 | expect(product.name).to.equal "New name" 56 | expect(product.description).to.equal "New description" 57 | 58 | done() 59 | 60 | describe "GET /api/products/:id.json", -> 61 | 62 | context "when the prouct can be found", -> 63 | 64 | it "finds a product", (done) -> 65 | request(app) 66 | .get("/api/products/1.json") 67 | .set("Accept", "application/json") 68 | .expect("Content-Type", /json/) 69 | .expect(200) 70 | .end (error, resp) -> 71 | product = resp.body 72 | 73 | expect(product).to.not.be.undefined 74 | expect(product.id).to.equal 1 75 | expect(product.name).to.equal "HTC Wildfire" 76 | 77 | done() 78 | 79 | context "when the product cannot be found", -> 80 | 81 | it "raises 404 error", (done) -> 82 | request(app) 83 | .get("/api/products/123.json") 84 | .set("Accept", "application/json") 85 | .expect(404, done) 86 | 87 | xdescribe "DELETE /api/products/:id.json", -> 88 | 89 | context "when the product can be found", -> 90 | 91 | it "deletes the product", (done) -> 92 | request(app) 93 | .delete("/api/products/3.json") 94 | .set("Accept", "application/json") 95 | .expect("Content-Type", /json/) 96 | .expect(200) 97 | .end (error, resp) -> 98 | done() 99 | -------------------------------------------------------------------------------- /client/test/unit/controllers/tasks_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Controller `TasksCtrl`", -> 2 | 3 | beforeEach module "myApp" 4 | 5 | ctrl = null 6 | $scope = null 7 | 8 | # Initialize the controller and a mock scope 9 | beforeEach inject ($rootScope, $controller) -> 10 | $scope = $rootScope.$new() 11 | ctrl = $controller "TasksCtrl", $scope: $scope 12 | 13 | it "assigns tasks", -> 14 | expect(ctrl.tasks).to.not.be.undefined 15 | expect(ctrl.tasks.length).to.equal 3 16 | 17 | describe "#archive()", -> 18 | beforeEach -> 19 | ctrl.tasks = [ 20 | { done: false }, { done: true }, { done: true } 21 | ] 22 | 23 | it "removes completed task from the list", -> 24 | expect(ctrl.tasks.length).to.equal 3 25 | 26 | ctrl.archive() 27 | expect(ctrl.tasks.length).to.equal 1 28 | 29 | describe "#tasksCount()", -> 30 | 31 | it "returns the number of all tasks", -> 32 | ctrl.tasks = [{}, {}, {}] 33 | expect(ctrl.tasksCount()).to.equal 3 34 | 35 | describe "#remainingTasksCount()", -> 36 | 37 | describe "when task list is empty" ,-> 38 | beforeEach -> ctrl.tasks = [] 39 | 40 | it "returns 0", -> 41 | expect(ctrl.remainingTasksCount()).to.equal 0 42 | 43 | describe "when task list contains some uncompleted tasks", -> 44 | beforeEach -> 45 | ctrl.tasks = [ 46 | { done: false }, { done: false }, { done: true } 47 | ] 48 | 49 | it "returns > 0", -> 50 | expect(ctrl.remainingTasksCount()).to.equal 2 51 | 52 | describe "when all tasks are completed", -> 53 | beforeEach -> 54 | ctrl.tasks = [ 55 | { done: true }, { done: true }, { done: true } 56 | ] 57 | 58 | it "returns 0", -> 59 | expect(ctrl.remainingTasksCount()).to.equal 0 60 | 61 | describe "#addTask()", -> 62 | 63 | describe "when the form is valid", -> 64 | 65 | beforeEach -> 66 | $scope.taskForm = $valid: true 67 | sinon.stub(ctrl, "reset") 68 | 69 | it "adds a new task", -> 70 | # Given 71 | newTask = name: "New task", done: false 72 | 73 | # When 74 | ctrl.addTask(newTask) 75 | 76 | # Then 77 | expect(ctrl.tasks.length).to.equal 4 78 | 79 | lastTask = ctrl.tasks[3] 80 | expect(lastTask).to.not.be.undefined 81 | expect(lastTask.name).to.equal newTask.name 82 | expect(lastTask.done).to.equal newTask.done 83 | 84 | it "resets the form", -> 85 | # When 86 | ctrl.addTask({}) 87 | 88 | # Then 89 | expect(ctrl.reset).to.be.called 90 | 91 | describe "when the form is not valid", -> 92 | beforeEach -> $scope.taskForm = $valid: false 93 | 94 | it "does nothing", -> 95 | # When 96 | ctrl.addTask({}) 97 | 98 | # Then 99 | expect(ctrl.tasks.length).to.equal 3 100 | 101 | it "does not reset the form", -> 102 | # Given 103 | mock = sinon.mock(ctrl).expects("reset").never() 104 | 105 | # When 106 | ctrl.addTask({}) 107 | 108 | # Then 109 | expect(mock).to.not.be.called 110 | mock.verify() 111 | -------------------------------------------------------------------------------- /client/test/unit/controllers/products/form_ctrl_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Controller `products.FormCtrl`", -> 2 | 3 | # stub external services 4 | beforeEach module "myApp", ($provide) -> 5 | $provide.factory "product", (Products) -> new Products(id: 123, name: "foo") 6 | 7 | $provide.decorator "$location", ($delegate) -> 8 | sinon.stub($delegate, "path") 9 | $delegate 10 | 11 | $provide.decorator "alerts", ($delegate) -> 12 | sinon.stub($delegate) 13 | 14 | $scope = null 15 | ctrl = null 16 | 17 | beforeEach inject ($rootScope, $controller) -> 18 | $scope = $rootScope.$new() 19 | $scope.productForm = {} 20 | 21 | ctrl = $controller "products.FormCtrl", $scope: $scope 22 | 23 | describe "$scope", -> 24 | 25 | it "has a product", -> 26 | expect($scope.product).not.to.be.undefined 27 | expect($scope.product).to.have.property "id", 123 28 | expect($scope.product).to.have.property "name", "foo" 29 | 30 | describe "#save()", -> 31 | 32 | context "when the form is valid", -> 33 | 34 | itSavesAProduct = -> 35 | it "saves a product", inject ($httpBackend) -> 36 | $httpBackend.verifyNoOutstandingExpectation() 37 | $httpBackend.verifyNoOutstandingRequest() 38 | 39 | itSetsAnAlertTo = (message) -> 40 | it "sets an alert", inject (alerts) -> 41 | expect(alerts.success).to.be.calledWith message 42 | 43 | itRedirectsToTheProductsListPage = -> 44 | it "redirects to the products list page", inject ($location) -> 45 | expect($location.path).to.be.calledWith "/products" 46 | 47 | context "on update", -> 48 | beforeEach inject ($httpBackend, product) -> 49 | $httpBackend.expectPOST("/api/products/123.json", id: 123, name: "bar").respond id: 123, name: "bar" 50 | 51 | product.name = "bar" 52 | ctrl.save(product) 53 | $httpBackend.flush() 54 | 55 | itSavesAProduct() 56 | itSetsAnAlertTo "Product was updated" 57 | itRedirectsToTheProductsListPage() 58 | 59 | context "on create", -> 60 | beforeEach inject ($httpBackend, product) -> 61 | $httpBackend.expectPOST("/api/products.json", name: "foo").respond id: 124, name: "foo" 62 | 63 | product.id = undefined 64 | product.name = "foo" 65 | 66 | ctrl.save(product) 67 | $httpBackend.flush() 68 | 69 | itSavesAProduct() 70 | itSetsAnAlertTo "Product was created" 71 | itRedirectsToTheProductsListPage() 72 | 73 | describe "#reset()", -> 74 | 75 | it "rollbacks product changes", -> 76 | ctrl.product.name = "new name" 77 | ctrl.reset() 78 | expect(ctrl.product).to.have.property "name", "foo" 79 | 80 | describe "#delete()", -> 81 | beforeEach inject ($httpBackend) -> 82 | $httpBackend.expectDELETE("/api/products/123.json").respond id: 123, name: "foo" 83 | 84 | ctrl.delete() 85 | $httpBackend.flush() 86 | 87 | it "deletes the product", inject ($httpBackend) -> 88 | $httpBackend.verifyNoOutstandingExpectation() 89 | $httpBackend.verifyNoOutstandingRequest() 90 | 91 | it "sets an alert", inject (alerts) -> 92 | expect(alerts.info).to.be.calledWith "Product was deleted" 93 | 94 | it "redirects to the products list page", inject ($location) -> 95 | expect($location.path).to.be.calledWith "/products" 96 | -------------------------------------------------------------------------------- /client/test/unit/routes_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Application routes", -> 2 | 3 | # stub `Products` service 4 | beforeEach module "myApp.resources", ($provide) -> 5 | products = [{ id: 123, name: "foo" }, { id: 234, name: "bar" }] 6 | 7 | class Products 8 | @query: sinon.stub().returns $promise: then: (callback) -> callback(products) 9 | @get: sinon.stub().returns $promise: then: (callback) -> callback(products[0]) 10 | 11 | $provide.value "Products", Products 12 | 13 | return 14 | 15 | beforeEach module "myApp" 16 | 17 | navigateTo = (to, params = {}) -> 18 | # for some reason `$route` has to be injected here 19 | beforeEach inject ($rootScope, $state) -> 20 | $rootScope.$apply -> $state.go(to, params) 21 | 22 | describe "route `/products`", -> 23 | navigateTo "products.list" 24 | 25 | it "is recognized", inject ($state) -> 26 | expect($state.current) 27 | .to.have.templateUrl("templates/products/list.html") 28 | .and.to.have.controller("products.IndexCtrl as index") 29 | .and.to.resolve("products") 30 | 31 | it "queries for the products", inject (Products) -> 32 | expect(Products.query).to.be.called 33 | 34 | it "loads the products", inject ($state) -> 35 | products = $state.$current.locals.resolve.$$values.products 36 | 37 | expect(products).to.have.length 2 38 | expect(products).to.satisfy (collection) -> _.findWhere(collection, id: 123) 39 | expect(products).to.satisfy (collection) -> _.findWhere(collection, id: 234) 40 | 41 | describe "route `/products/create`", -> 42 | navigateTo "products.create" 43 | 44 | it "is recognized", inject ($state) -> 45 | expect($state.current) 46 | .to.have.templateUrl("templates/products/form.html") 47 | .and.to.have.controller("products.FormCtrl as form") 48 | .and.to.resolve("product") 49 | 50 | it "resolves with a new product instance", inject ($state, Products) -> 51 | product = $state.$current.locals.resolve.$$values.product 52 | expect(product).to.be.instanceOf(Products) 53 | expect(product.id).to.be.undefined 54 | 55 | describe "route `/products/:id`", -> 56 | itIsRecognized = -> 57 | it "is recognized", inject ($state) -> 58 | expect($state.$current.parent) 59 | .to.have.templateUrl("templates/products/show.html") 60 | .and.to.have.controller("products.ShowCtrl as show") 61 | .and.to.resolve("product") 62 | 63 | itQueriesForAProduct = -> 64 | it "queries for a product", inject ($state, Products) -> 65 | expect(Products.get).to.be.calledWith(id: "123") 66 | 67 | itLoadsAProduct = -> 68 | it "loads a product", inject ($state) -> 69 | product = $state.$current.locals.resolve.$$values.product 70 | expect(product.id).to.equal 123 71 | expect(product.name).to.equal "foo" 72 | 73 | describe "`info` tab", -> 74 | navigateTo "products.show.info", id: 123 75 | 76 | itIsRecognized() 77 | itQueriesForAProduct() 78 | itLoadsAProduct() 79 | 80 | describe "`details` tab", -> 81 | navigateTo "products.show.details", id: 123 82 | 83 | itIsRecognized() 84 | itQueriesForAProduct() 85 | itLoadsAProduct() 86 | 87 | describe "route `/other`", -> 88 | navigateTo "other" 89 | 90 | it "is recognized", inject ($state) -> 91 | expect($state.current) 92 | .to.have.templateUrl("templates/other.html") 93 | .and.to.have.controller("OtherCtrl as other") 94 | 95 | describe "route `/tasks`", -> 96 | navigateTo "tasks" 97 | 98 | it "is recognized", inject ($state) -> 99 | expect($state.current) 100 | .to.have.templateUrl("templates/tasks.html") 101 | .and.to.have.controller("TasksCtrl as tasks") 102 | -------------------------------------------------------------------------------- /client/app/templates/products/form.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
7 | 8 |
9 | 10 | 11 |
12 | 16 | 17 |
18 | Name is required 19 | Name is too short 20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 | 30 |
31 | $ 32 | 35 |
36 | 37 |
38 | Price is required 39 | Minimum price is $0.01 40 |
41 |
42 | 43 |
44 | 45 | 46 |
47 | 49 | % 50 |
51 | 52 |
53 | Minimum value is 0 54 | Maximum value is 100 55 |
56 |
57 | 58 |
59 | 60 | 61 |
62 | $ 63 | 65 |
66 |
67 |
68 | 69 |
70 | 71 | 72 |
73 | 76 |
77 |
78 | 79 |
80 |
81 | 89 | 90 | 91 | Reset 92 | 93 | 94 | Delete 97 | 98 | Cancel 99 |
100 |
101 |
102 | -------------------------------------------------------------------------------- /client/test/unit/modules/forms_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Module `forms`", -> 2 | 3 | beforeEach module "myApp.forms" 4 | 5 | describe "Directive `mySubmit`", -> 6 | 7 | $scope = null 8 | element = null 9 | 10 | beforeEach inject ($rootScope, $compile) -> 11 | $scope = $rootScope.$new() 12 | $scope.item = name: "" 13 | $scope.save = sinon.spy() 14 | 15 | element = angular.element """ 16 |
17 | 18 | 19 |
20 | """ 21 | element = $compile(element)($scope) 22 | 23 | describe "when the form is valid", -> 24 | beforeEach -> 25 | $scope.$apply -> $scope.item.name = "foo" 26 | 27 | it "calls `save` method", -> 28 | element.find("button[type=submit]").click() 29 | expect($scope.save).to.be.calledWith($scope.item, $scope.testForm) 30 | 31 | it "marks the form as submitted", -> 32 | expect($scope.testForm.$submitted).to.be.undefined 33 | element.find("button[type=submit]").click() 34 | expect($scope.testForm.$submitted).to.be.true 35 | 36 | describe "when the form is invalid", -> 37 | beforeEach -> 38 | $scope.$apply -> $scope.item.name = "" 39 | 40 | it "does not call `save` method", -> 41 | element.find("button[type=submit]").click() 42 | expect($scope.save).to.not.be.called 43 | 44 | describe "Directive `myMessages`", -> 45 | 46 | $scope = null 47 | element = null 48 | 49 | beforeEach inject ($rootScope, $compile) -> 50 | $scope = $rootScope.$new() 51 | $scope.user = login: "" 52 | 53 | element = angular.element """ 54 |
55 | 57 |
58 | login is too short 59 | login is required 60 |
61 | 62 | 63 |
64 | """ 65 | element = $compile(element)($scope) 66 | 67 | it "hides errors by default", -> 68 | expect(element.find("div[my-messages]").text()).to.not.include("login is required") 69 | expect(element.find("div[my-messages]").text()).to.not.include("login is too short") 70 | 71 | describe "on change", -> 72 | 73 | describe "when the input is valid", -> 74 | beforeEach -> 75 | $scope.$apply -> $scope.user.login = "foobar" 76 | 77 | it "hides the errors", -> 78 | expect(element.find("div[my-messages]").text()).to.not.include("login is required") 79 | expect(element.find("div[my-messages]").text()).to.not.include("login is too short") 80 | 81 | describe "when the input is invalid", -> 82 | beforeEach -> 83 | inputEl = element.find("input[name=login]") 84 | inputEl.val("foo").trigger("input") 85 | 86 | it "shows the errors", -> 87 | expect(element.find("div[my-messages]").text()).to.include("login is too short") 88 | 89 | describe "on submit", -> 90 | 91 | describe "when the form is valid", -> 92 | beforeEach -> 93 | $scope.$apply -> $scope.user.login = "foobar" 94 | element.find("button[type=submit]").click() 95 | 96 | it "hides the errors", -> 97 | expect(element.find("div[my-messages]").text()).to.not.include("login is required") 98 | expect(element.find("div[my-messages]").text()).to.not.include("login is too short") 99 | 100 | describe "when the form is invalid", -> 101 | beforeEach -> 102 | element.find("button[type=submit]").click() 103 | 104 | it "shows the errors", -> 105 | expect(element.find("div[my-messages]").text()).to.include("login is required") 106 | -------------------------------------------------------------------------------- /server/test/product_provider_spec.coffee: -------------------------------------------------------------------------------- 1 | expect = require("chai").expect 2 | ProductProvider = require("../lib/product_provider") 3 | 4 | describe "ProductProvider", -> 5 | productProvider = null 6 | beforeEach -> 7 | products = [ 8 | { name: "one" } 9 | { name: "two" } 10 | ] 11 | productProvider = new ProductProvider(products) 12 | 13 | describe "a new instance", -> 14 | 15 | it "saves a prducts", -> 16 | expect(productProvider.products.length).to.equal 2 17 | 18 | describe "saved products", -> 19 | 20 | it "generates ids", -> 21 | products = productProvider.products 22 | expect(products[0].id).to.equal 1 23 | expect(products[1].id).to.equal 2 24 | 25 | it "assings createdAt date", -> 26 | products = productProvider.products 27 | expect(products[0].createdAt).to.not.be.undefined 28 | expect(products[1].createdAt).to.not.be.undefined 29 | 30 | describe "#findAll()", -> 31 | 32 | it "finds all products", (done) -> 33 | 34 | productProvider.findAll (error, products) -> 35 | expect(products.length).to.equal 2 36 | done() 37 | 38 | describe "#findById()", -> 39 | 40 | context "when the product can be found", -> 41 | 42 | it "returns the product", (done) -> 43 | 44 | productProvider.findById 1, (error, product) -> 45 | expect(product).not.to.be.undefined 46 | expect(product.id).to.equal 1 47 | expect(product.name).to.equal "one" 48 | done() 49 | 50 | context "when the product cannot be found", -> 51 | 52 | it "returns undefined", (done) -> 53 | 54 | productProvider.findById 3, (error, product) -> 55 | expect(product).to.be.undefined 56 | done() 57 | 58 | describe "#save()", -> 59 | 60 | context "when the single product is given", -> 61 | product = null 62 | beforeEach (done) -> 63 | productProvider.save name: "third", (error, _newProducts_) -> 64 | product = _newProducts_[0] 65 | done() 66 | 67 | it "saves a product", -> 68 | expect(product).not.to.be.undefined 69 | expect(product.name).to.equal "third" 70 | 71 | describe "new record", -> 72 | 73 | it "generates an id for the product", -> 74 | expect(product.id).to.equal 3 75 | 76 | it "assigns createdAt date", -> 77 | expect(product.createdAt).to.not.be.undefined 78 | 79 | context "when the array of products is given", -> 80 | products = null 81 | beforeEach (done) -> 82 | productProvider.save [{ name: "third" }, { name: "forth" }], (error, _newProducts_) -> 83 | products = _newProducts_ 84 | done() 85 | 86 | it "saves the products", -> 87 | expect(products.length).to.equal 2 88 | expect(products[0].name).to.equal "third" 89 | expect(products[1].name).to.equal "forth" 90 | 91 | describe "new records", -> 92 | 93 | it "generates ids for all new records", -> 94 | expect(products[0].id).to.equal 3 95 | expect(products[1].id).to.equal 4 96 | 97 | it "assigns createdAt date for all new records", -> 98 | expect(products[0].createdAt).to.not.be.undefined 99 | expect(products[1].createdAt).to.not.be.undefined 100 | 101 | describe "#update()", -> 102 | 103 | it "updates a product", (done) -> 104 | params = 105 | id: "unknown" 106 | name: "New name" 107 | description: "New description" 108 | price: 99.99 109 | 110 | productProvider.update 1, params, (error, product) -> 111 | expect(product).to.not.be.undefined 112 | 113 | expect(product.id).to.equal 1 114 | expect(product.name).to.equal params.name 115 | expect(product.description).to.equal params.description 116 | expect(product.price).to.equal params.price 117 | 118 | done() 119 | 120 | describe "#destroy()", -> 121 | 122 | it "deletes a product", (done) -> 123 | 124 | productProvider.destroy 1, (error, product) -> 125 | expect(product).to.not.be.undefined 126 | expect(product.id).to.equal 1 127 | 128 | productProvider.findById product.id, (error, product) -> 129 | expect(product).to.be.undefined 130 | done() 131 | 132 | describe "#destroyAll()", -> 133 | 134 | it "deletes all products", (done) -> 135 | 136 | productProvider.destroyAll -> 137 | expect(productProvider.products.length).to.equal 0 138 | done() 139 | -------------------------------------------------------------------------------- /client/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular Seed 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom AngularJS seed project 2 | 3 | [![Build status](https://secure.travis-ci.org/lucassus/angular-seed.png)](http://travis-ci.org/lucassus/angular-seed) 4 | [![Dependency Status](https://gemnasium.com/lucassus/angular-seed.png)](https://gemnasium.com/lucassus/angular-seed) 5 | [![Stories in Ready](https://badge.waffle.io/lucassus/angular-seed.png?label=ready)](https://waffle.io/lucassus/angular-seed) 6 | 7 | This is a custom AngularJS seed project based on grunt the JavaScript task runner. 8 | 9 | * AngularJS 1.2.0 10 | * CoffeeScript and Less support 11 | * Bower for front-end packages management 12 | * Full support for unit and end2end tests 13 | * Unit tests with Mocha, Chai and SinonJS 14 | * Generates test code coverage for the unit tests 15 | * Support for protractor integration tests 16 | * Support for Karma Test Runner (formerly Testacular) 17 | * Continuous Integration ready ready via `grunt test:ci` task 18 | * Grunt task runner along with several useful plugins 19 | * Production release minification and angular template caching 20 | * ..and a lot more 21 | 22 | Demo: http://lucassus-angular-seed.herokuapp.com 23 | 24 | ## Directory structure 25 | 26 | * `./app` - contains CoffeeScript sources, styles, images, fonts and other assets 27 | * `./app/scripts` - CoffeeScript sources 28 | * `./app/styles` - stylesheets 29 | * `./app/views` - html templates used by AngularJS 30 | * `./test` - contains tests for the application 31 | * `./tests/integration` - protractor integration specs 32 | * `./test/e2e` - AngularJS end2end scenarios 33 | * `./tests/unit` - unit tests for AngularJS components 34 | 35 | Third-party libraries 36 | 37 | * `./bower_components` - components dowloaded by `bower install` command 38 | * `./custom_components` - you could put custom components here 39 | * `./node_modules` - command dowloaded by `npm install` command 40 | 41 | Generated stuff 42 | 43 | * `./dev` - compiled development release 44 | * `./dist` - created by `grunt build` command, contains the minified production release of the app 45 | 46 | ## Bootstrap 47 | 48 | Install nodejs v0.10.12 from the sources: 49 | 50 | ``` 51 | sudo apt-get install build-essential openssl libssl-dev pkg-config 52 | 53 | wget http://nodejs.org/dist/v0.10.12/node-v0.10.12.tar.gz 54 | tar -xzf node-v0.10.12.tar.gz 55 | 56 | cd node-v0.10.7 57 | ./configure 58 | make 59 | sudo make install 60 | ``` 61 | 62 | ## Install grunt, nodemon and bower globally 63 | 64 | ``` 65 | sudo npm install -g grunt-cli 66 | sudo npm install -g nodemon 67 | sudo npm install -g bower 68 | ``` 69 | 70 | ### Run the app 71 | 72 | ``` 73 | npm install 74 | bower install 75 | script/start-server 76 | ``` 77 | 78 | Navigate to `http://localhost:9000` 79 | 80 | ## Running tests 81 | 82 | By default all tests are executed in PhantomJS browser 83 | 84 | * `grunt test:unit` or `grunt test` - run unit tests 85 | * `grunt test:unit:watch` or 86 | * `grunt test:watch` - run unit tests in watch mode 87 | * `grunt test --coverage-reporter=html` - generate html code coverage report 88 | 89 | Run test against specific browsers 90 | 91 | `grunt test:unit --browsers=Chrome,Firefox,Opera,PhantomJS` 92 | 93 | ## Running integration tests 94 | 95 | * `script/test-unit` - run unit tests 96 | * `script/test-integration` - run integration specs 97 | 98 | ### How to develop specs 99 | 100 | * install standalone Selenium `node_modules/protractor/bin/webdriver-manager update` 101 | * start the app in the `test` evn `script/start-test-server` 102 | * run it with `grunt coffee:test && protractor dev/test/protractor-conf.js` 103 | 104 | ### WebDriver and PhantomJS 105 | 106 | * stop selenium 107 | * run PhantomJS with WebDriver support `phantomjs --webdriver=4444` 108 | * setup protractor `browserName: "phantomjs"` 109 | * run specs 110 | 111 | ## Running tests for the server side application 112 | 113 | `mocha --compilers coffee:coffee-script --watch --reporter spec server/test` 114 | 115 | ### How to debug failing specs 116 | 117 | Put `debugger` in the failing spec: 118 | 119 | ```coffee 120 | describe "Failing spec", -> 121 | 122 | it "should run smoothly", -> 123 | debugger # this is like setting a breakpoint 124 | failMiserably() 125 | ``` 126 | 127 | Run karma in Chrome browser: 128 | 129 | `grunt test:unit:watch --browsers=Chrome` 130 | 131 | * Go to the newly opened Chrome Browser 132 | * Open Chrome's DevTools and refresh the page 133 | * Now in the source tab you should see the execution stopped at the debugger 134 | 135 | Run karma directly without CoffeeScript compilation: 136 | 137 | `karma start test/karma-conf.coffee --single-run` 138 | 139 | or with auto watch option: 140 | 141 | `karma start test/karma-conf.coffee` 142 | 143 | or 144 | 145 | `karma test:unit:watch` 146 | 147 | ### Running tests headlessly 148 | 149 | Start Xvfb and export DISPLAY variable: 150 | 151 | ``` 152 | ./script/xvfb start 153 | export DISPLAY=:99 154 | ``` 155 | 156 | Perform single run: 157 | 158 | `grunt test --browsers=Firefox,Chrome,Opera,PhantomJS` 159 | 160 | or 161 | 162 | `grunt test:watch --browsers=Chrome` 163 | 164 | ## Build process 165 | 166 | `script/build` will build the minified production release. 167 | 168 | `(cd dist/ ; python -m SimpleHTTPServer 8000)` will serve a static assets from `./dist` directory. 169 | 170 | Navigate to `http://localhost:8000` to see the production release. 171 | 172 | # Heroku deployment 173 | 174 | ``` 175 | git co heroku-production 176 | git merge master 177 | grunt build 178 | git push heroku heroku-production:master -f 179 | ``` 180 | -------------------------------------------------------------------------------- /client/test/unit/modules/resources_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Module `myApp.resources`", -> 2 | 3 | beforeEach module "myApp.resources" 4 | 5 | describe "Service `Products`", -> 6 | $rootScope = null 7 | $httpBackend = null 8 | Products = null 9 | 10 | beforeEach inject (_$rootScope_, _$httpBackend_, _Products_) -> 11 | $rootScope = _$rootScope_ 12 | $httpBackend = _$httpBackend_ 13 | Products = _Products_ 14 | 15 | it "is defined", -> 16 | expect(Products).to.not.be.undefined 17 | 18 | describe ".query()", -> 19 | before -> @products = [{ name: "foo" }, { name: "bar" }] 20 | 21 | it "is defined", -> 22 | expect(Products.query).to.not.be.undefined 23 | 24 | it "queries for the records", -> 25 | $httpBackend.whenGET("/api/products.json").respond(@products) 26 | promise = Products.query().$promise 27 | $httpBackend.flush() 28 | 29 | products = null 30 | $rootScope.$apply -> 31 | promise.then (_products_) -> products = _products_ 32 | 33 | expect(products).to.have.length 2 34 | 35 | describe ".get()", -> 36 | before -> @product = id: 123, name: "foo bar" 37 | 38 | it "is defined", -> 39 | expect(Products.get).to.not.be.undefined 40 | 41 | it "queries for a product", -> 42 | $httpBackend.whenGET("/api/products/123.json").respond(@product) 43 | promise = Products.get(id: 123).$promise 44 | expect(promise).to.not.be.undefined 45 | $httpBackend.flush() 46 | 47 | product = null 48 | $rootScope.$apply -> 49 | promise.then (_product_) -> product = _product_ 50 | 51 | expect(product).to.not.be.null 52 | expect(product.id).to.equal 123 53 | expect(product.name).to.equal "foo bar" 54 | 55 | describe "#$save()", -> 56 | product = null 57 | 58 | context "when the `id` is not given", -> 59 | beforeEach inject (Products) -> 60 | product = new Products(name: "foo") 61 | 62 | it "creates a new record", -> 63 | $httpBackend.whenPOST("/api/products.json").respond({}) 64 | 65 | promise = product.$save() 66 | expect(promise).to.not.be.undefined 67 | 68 | $httpBackend.flush() 69 | 70 | context "when the `id` is given", -> 71 | beforeEach inject (Products) -> 72 | product = new Products(id: 4567, name: "foo") 73 | 74 | it "updates the record", -> 75 | $httpBackend.whenPOST("/api/products/4567.json").respond({}) 76 | 77 | promise = product.$save() 78 | expect(promise).to.not.be.undefined 79 | 80 | $httpBackend.flush() 81 | 82 | describe "#persisted()", -> 83 | product = null 84 | beforeEach -> product = new Products() 85 | 86 | context "when the product has an id", -> 87 | beforeEach -> product.id = 123 88 | 89 | it "returns true", -> 90 | expect(product.persisted()).to.be.true 91 | 92 | context "when the product does not have an id", -> 93 | beforeEach -> product.id = null 94 | 95 | it "returns false", -> 96 | expect(product.persisted()).to.be.false 97 | 98 | describe "#priceWithDiscount()", -> 99 | product = null 100 | beforeEach inject (Products) -> 101 | product = new Products() 102 | 103 | it "is defined", -> 104 | expect(product.priceWithDiscount).to.not.be.undefined 105 | 106 | context "when discount is not defined", -> 107 | beforeEach -> 108 | product.discount = undefined 109 | product.price = 9.99 110 | 111 | it "returns the base price", -> 112 | expect(product.priceWithDiscount()).to.equal 9.99 113 | 114 | context "when discount is defined", -> 115 | beforeEach -> 116 | product.discount = 22 117 | product.price = 100 118 | 119 | it "returns price with discount", -> 120 | expect(product.priceWithDiscount()).to.equal 78 121 | 122 | describe "#hasDiscount()", -> 123 | # custom chai property for checking product's discount 124 | chai.Assertion.addProperty "discount", -> 125 | subject = @_obj 126 | 127 | @assert subject.hasDiscount(), 128 | "expected #\{this} to have discount", 129 | "expected #\{this} to not have discount" 130 | 131 | product = null 132 | beforeEach inject (Products) -> 133 | product = new Products() 134 | 135 | it "is defined", -> 136 | expect(product.hasDiscount).to.not.be.undefined 137 | 138 | context "when the discount is not defined", -> 139 | beforeEach -> product.discount = undefined 140 | 141 | it "returns false", -> 142 | expect(product).to.not.have.discount 143 | 144 | context "when the @discount is null", -> 145 | beforeEach -> product.discount = null 146 | 147 | it "returns false", -> 148 | expect(product).to.not.have.discount 149 | 150 | context "when the @discount is 0", -> 151 | beforeEach -> product.discount = 0 152 | 153 | it "returns false", -> 154 | expect(product).to.not.have.discount 155 | 156 | context "when the @discount < 0", -> 157 | beforeEach -> product.discount = -10 158 | 159 | it "returns false", -> 160 | expect(product).to.not.have.discount 161 | 162 | context "when the @discount is > 0", -> 163 | beforeEach -> product.discount = 10 164 | 165 | it "returns true", -> 166 | expect(product).to.have.discount 167 | -------------------------------------------------------------------------------- /client/test/unit/modules/alerts_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Module `myApp.alerts`", -> 2 | 3 | beforeEach module "myApp.alerts" 4 | beforeEach module "mocks" 5 | 6 | describe "Controller `alerts`", -> 7 | $scope = null 8 | alerts = null 9 | 10 | beforeEach inject ($injector, $rootScope, $controller) -> 11 | $scope = $rootScope.$new() 12 | 13 | alerts = $injector.get("alerts") 14 | 15 | $controller "alerts", 16 | $scope: $scope, 17 | alerts: alerts 18 | 19 | it "assings flash messages", -> 20 | expect($scope.alertMessages).to.not.be.undefined 21 | expect($scope.alertMessages).to.be.empty 22 | 23 | # When 24 | alerts.info("Test message.") 25 | 26 | # Then 27 | lastMessage = $scope.alertMessages[0] 28 | expect(lastMessage.id).to.equal 1 29 | expect(lastMessage.type).to.equal "info" 30 | expect(lastMessage.text).to.equal "Test message." 31 | 32 | describe "#disposeAlert()", -> 33 | it "disposes an alert at the given index", -> 34 | # Given 35 | alerts.info("Information..") 36 | alerts.error("Error..") 37 | spy = sinon.spy(alerts, "dispose") 38 | 39 | # When 40 | $scope.disposeAlert(2) 41 | 42 | # Then 43 | expect(spy).to.be.called 44 | expect(spy).to.be.calledWith 2 45 | 46 | firstMessage = _.findWhere($scope.alertMessages, id: 1) 47 | expect(firstMessage).to.not.be.undefined 48 | expect(firstMessage.type).to.equal "info" 49 | expect(firstMessage.text).to.equal "Information.." 50 | 51 | secondMessage = _.findWhere($scope.alertMessages, id: 2) 52 | expect(secondMessage).to.be.undefined 53 | 54 | describe "Directive `alerts`", -> 55 | $scope = null 56 | element = null 57 | 58 | beforeEach inject ($rootScope, $compile) -> 59 | $scope = $rootScope 60 | 61 | # compite the html snippet 62 | element = angular.element "" 63 | linkFn = $compile(element) 64 | linkFn($scope) 65 | 66 | $scope.$digest() 67 | 68 | it "renders alerts", -> 69 | $scope.$apply -> $scope.alertMessages = [ 70 | type: "info", text: "Test message" 71 | ] 72 | expect(element.find(".alert-info").length).to.equal(1) 73 | 74 | describe "Service `alerts`", -> 75 | it "is defined", inject (alerts) -> 76 | expect(alerts).to.not.be.undefined 77 | 78 | describe "#nextId()", -> 79 | it "return the next id for the new flash message", inject (alerts) -> 80 | expect(alerts.nextId()).to.equal(1) 81 | alerts.nextId() for [1..4] 82 | expect(alerts.nextId()).to.equal(6) 83 | 84 | describe "#push()", -> 85 | spy = null 86 | 87 | beforeEach inject (alerts) -> 88 | spy = sinon.spy(alerts, "delayedDispose") 89 | 90 | it "returns an id for the new flash message", inject (alerts) -> 91 | expect(alerts.push("info", "Test..")).to.equal 1 92 | expect(spy).to.be.calledWith 1 93 | 94 | expect(alerts.push("error", "Test error..")).to.equal 2 95 | expect(spy).to.be.calledWith 2 96 | 97 | describe "#info()", -> 98 | it "pushesh the given message", inject (alerts) -> 99 | # Given 100 | testMessage = "This is a test message!" 101 | otherTestMessage = "This is a second test message!" 102 | 103 | # When 104 | alerts.info(testMessage) 105 | expect(spy).to.be.calledWith 1 106 | 107 | alerts.error(otherTestMessage) 108 | expect(spy).to.be.calledWith 2 109 | 110 | # Then 111 | firstMessage = _.findWhere(alerts.messages, id: 1) 112 | expect(firstMessage).to.not.be.undefined 113 | expect(firstMessage.type).to.equal "info" 114 | expect(firstMessage.text).to.equal testMessage 115 | 116 | secondMessage = _.findWhere(alerts.messages, id: 2) 117 | expect(secondMessage).to.not.be.undefined 118 | expect(secondMessage.type).to.equal "error" 119 | expect(secondMessage.text).to.equal otherTestMessage 120 | 121 | describe "#error()", -> 122 | it "pushesh the given message", inject (alerts) -> 123 | # Given 124 | testMessage = "This is a test message!" 125 | 126 | # When 127 | alerts.error(testMessage) 128 | expect(spy).to.be.calledWith 1 129 | 130 | # Then 131 | lastMessage = _.findWhere(alerts.messages, id: 1) 132 | expect(lastMessage).to.not.be.undefined 133 | expect(lastMessage.type).to.equal "error" 134 | 135 | describe "#success()", -> 136 | 137 | it "pushesh the given message", inject (alerts) -> 138 | # Given 139 | testMessage = "This is a test message!" 140 | 141 | # When 142 | alerts.success(testMessage) 143 | expect(spy).to.be.calledWith 1 144 | 145 | # Then 146 | lastMessage = _.findWhere(alerts.messages, id: 1) 147 | expect(lastMessage).to.not.be.undefined 148 | expect(lastMessage.type).to.equal "success" 149 | 150 | describe "#warning()", -> 151 | 152 | it "pushesh the given message", inject (alerts) -> 153 | # Given 154 | testMessage = "This is a test message!" 155 | 156 | # When 157 | alerts.warning(testMessage) 158 | expect(spy).to.be.calledWith 1 159 | 160 | # Then 161 | lastMessage = _.findWhere(alerts.messages, id: 1) 162 | expect(lastMessage).to.not.be.undefined 163 | expect(lastMessage.type).to.equal "warning" 164 | 165 | describe "#dispose()", -> 166 | it "removes a message with the given id", inject (alerts) -> 167 | # Given 168 | alerts.info("First message") 169 | alerts.info("Second message") 170 | alerts.info("Third message") 171 | alerts.error("Error message") 172 | 173 | # When 174 | alerts.dispose(2) 175 | 176 | # Then 177 | firstMessage = _.findWhere(alerts.messages, text: "First message") 178 | expect(firstMessage).to.not.be.undefined 179 | 180 | secondMessage = _.findWhere(alerts.messages, text: "Second message") 181 | expect(secondMessage).to.be.undefined 182 | 183 | thirdMessage = _.findWhere(alerts.messages, text: "Third message") 184 | expect(thirdMessage).to.not.be.undefined 185 | 186 | forthMessage = _.findWhere(alerts.messages, type: "error", text: "Error message") 187 | expect(forthMessage).to.not.be.undefined 188 | 189 | describe "#delayedDispose()", -> 190 | 191 | it "removes a message after the given time", inject (alerts, $timeout) -> 192 | # Given 193 | alerts.info("First message") 194 | 195 | # When 196 | alerts.delayedDispose(1) 197 | 198 | # Then 199 | lastMessage = _.findWhere(alerts.message, id: 1) 200 | expect(lastMessage).to.be.defined 201 | 202 | # When 203 | $timeout.flush() 204 | 205 | # Then 206 | expect(alerts.messages).to.be.empty 207 | disposedMessage = _.findWhere(alerts.message, id: 1) 208 | expect(disposedMessage).to.not.be.defined 209 | -------------------------------------------------------------------------------- /client/test/integration/products_scenario.coffee: -------------------------------------------------------------------------------- 1 | expect = require("./helpers/expect") 2 | utils = require("./helpers/utils") 3 | 4 | AlertView = require("./helpers/page_objects/alert_view") 5 | IndexPage = require("./helpers/page_objects/products/index_page") 6 | FormPage = require("./helpers/page_objects/products/form_page") 7 | ShowPage = require("./helpers/page_objects/products/show_page") 8 | 9 | describe.only "Products page", -> 10 | alertView = null 11 | indexPage = null 12 | 13 | # mock the backend 14 | beforeEach -> 15 | # TODO use the real server, maintain spike-ng-mocks-in-integration-specs 16 | mockScript = -> 17 | products = [ 18 | { 19 | id: 1, name: "HTC Wildfire", description: "Old android phone", 20 | manufacturer: "HTC", 21 | price: 499.99, discount: 10 22 | } 23 | { id: 2, name: "iPhone", price: 2500 } 24 | { id: 3, name: "Nexus One", price: 1000, discount: 7 } 25 | { id: 4, name: "Nexus 7", price: 1200, discount: 12 } 26 | { id: 5, name: "Samsung Galaxy Note", price: 2699, discount: 0 } 27 | { id: 6, name: "Samsung S4", price: 3000, discount: 2 } 28 | ] 29 | 30 | angular.module("httpBackendMock", ["ngMockE2E"]) 31 | .run ($httpBackend) -> 32 | productUrlRegexp = /\/api\/products\/(\d+).json/ 33 | 34 | # stub list 35 | $httpBackend.whenGET("/api/products.json").respond(products) 36 | 37 | # stub get 38 | $httpBackend.whenGET(productUrlRegexp).respond (method, url, data) -> 39 | id = url.match(productUrlRegexp)[1] 40 | product = _.findWhere(products, id: parseInt(id)) 41 | 42 | if product? 43 | [200, angular.toJson(product)] 44 | else 45 | [404] 46 | 47 | # stub create 48 | $httpBackend.whenPOST("/api/products.json").respond (method, url, data) -> 49 | product = angular.fromJson(data) 50 | product.id = _.last(products).id + 1 51 | products.push(product) 52 | 53 | [201] 54 | 55 | # stub update 56 | $httpBackend.whenPOST(productUrlRegexp).respond (method, url, data) -> 57 | id = url.match(productUrlRegexp)[1] 58 | product = _.findWhere(products, id: parseInt(id)) 59 | 60 | params = angular.fromJson(data) 61 | product[field] = value for field, value of params 62 | 63 | [201, angular.toJson(product)] 64 | 65 | # stub delete 66 | $httpBackend.whenDELETE(productUrlRegexp).respond (method, url, data) -> 67 | id = url.match(productUrlRegexp)[1] 68 | product = _.findWhere(products, id: parseInt(id)) 69 | index = products.indexOf(product) 70 | 71 | products.splice(index, 1) if index isnt -1 72 | 73 | [200] 74 | 75 | $httpBackend.whenGET(/.*/).passThrough() 76 | 77 | browser.addMockModule("httpBackendMock", mockScript) 78 | 79 | beforeEach -> 80 | alertView = new AlertView() 81 | indexPage = new IndexPage() 82 | 83 | browser.get "/" 84 | 85 | it "displays a valid page title", -> 86 | expect(browser.getCurrentUrl()).to.eventually.match /#\/products$/ 87 | expect(browser.getTitle()).to.eventually.eq "Angular Seed" 88 | 89 | describe "products list page", -> 90 | 91 | it "displays the list of products", -> 92 | expect(indexPage.greeting.getText()).to.eventually.eq "You have 6 products" 93 | 94 | indexPage.table.productNames.then (names) -> 95 | expect(names.length).to.eq 6 96 | 97 | expect(names[0].getText()).to.eventually.eq "HTC Wildfire" 98 | expect(names[1].getText()).to.eventually.eq "Nexus One" 99 | expect(names[2].getText()).to.eventually.eq "Nexus 7" 100 | expect(names[3].getText()).to.eventually.eq "iPhone" 101 | expect(names[4].getText()).to.eventually.eq "Samsung Galaxy Note" 102 | expect(names[5].getText()).to.eventually.eq "Samsung S4" 103 | 104 | it "displays correct columns", -> 105 | row = indexPage.table.rowAt(0) 106 | 107 | expect(row.id.isDisplayed()).to.eventually.be.true 108 | 109 | expect(row.name.isDisplayed()).to.eventually.be.true 110 | expect(row.name.getText()).to.eventually.eq "HTC Wildfire" 111 | 112 | expect(row.description.isDisplayed()).to.eventually.be.true 113 | expect(row.description.isDisplayed()).to.eventually.be.true 114 | 115 | describe "click on `delete` button", -> 116 | beforeEach -> 117 | row = indexPage.table.rowAt(1) 118 | row.deleteButton.click() 119 | 120 | it "deletes the product", -> 121 | expect(indexPage.greeting.getText()).to.eventually.eq "You have 5 products" 122 | 123 | it "sets an alert message ", -> 124 | expect(alertView.info.isDisplayed()).to.eventually.be.true 125 | expect(alertView.info.getText()).to.eventually.eq "Product was deleted" 126 | 127 | describe "create new product", -> 128 | formPage = null 129 | 130 | beforeEach -> 131 | indexPage.createButton.click() 132 | formPage = new FormPage() 133 | 134 | it "displays a form for creating a new product", -> 135 | expect(formPage.nameField.getAttribute("value")).to.eventually.eq "" 136 | expect(formPage.descriptionField.getAttribute("value")).to.eventually.eq "" 137 | expect(formPage.submitButton.getText()).to.eventually.eq "Create" 138 | 139 | describe "click on `create` button", -> 140 | beforeEach -> 141 | formPage.setName "New product" 142 | formPage.setPrice "9.99" 143 | formPage.setDescription "this is the description" 144 | formPage.submitButton.click() 145 | 146 | it "creates new product", -> 147 | expect(alertView.success.isDisplayed()).to.eventually.be.true 148 | expect(alertView.success.getText()).to.eventually.eq "Product was created" 149 | expect(indexPage.greeting.getText()).to.eventually.eq "You have 7 products" 150 | 151 | it "redirects to the products page", -> 152 | expect(browser.getCurrentUrl()).to.eventually.match /#\/products$/ 153 | 154 | describe "click on `reset` button", -> 155 | beforeEach -> 156 | formPage.setName "New product" 157 | formPage.setPrice "9.99" 158 | formPage.setDescription "this is the description" 159 | 160 | it "clears the form", -> 161 | formPage.resetButton.click() 162 | 163 | expect(formPage.nameField.getAttribute("value")).to.eventually.eq "" 164 | expect(formPage.priceField.getAttribute("value")).to.eventually.eq "" 165 | expect(formPage.descriptionField.getText()).to.eventually.eq "" 166 | 167 | describe "edit a product", -> 168 | formPage = null 169 | 170 | beforeEach -> 171 | row = indexPage.table.rowAt(2) 172 | row.editButton.click() 173 | 174 | formPage = new FormPage() 175 | 176 | it "displays a form for updating the product", -> 177 | expect(formPage.nameField.getAttribute("value")).to.eventually.eq "Nexus 7" 178 | expect(formPage.priceField.getAttribute("value")).to.eventually.eq "1200" 179 | expect(formPage.discountField.getAttribute("value")).to.eventually.eq "12" 180 | expect(formPage.submitButton.getText()).to.eventually.eq "Update" 181 | 182 | describe "click on `update` button", -> 183 | beforeEach -> 184 | formPage.setName "New name" 185 | formPage.setPrice "199.99" 186 | formPage.setDescription "this is the new description" 187 | formPage.submitButton.click() 188 | 189 | it "updates the product", -> 190 | expect(alertView.success.isDisplayed()).to.eventually.be.true 191 | expect(alertView.success.getText()).to.eventually.eq "Product was updated" 192 | 193 | it "redirects to the products page", -> 194 | expect(browser.getCurrentUrl()).to.eventually.match /#\/products$/ 195 | 196 | describe "click on `reset` button", -> 197 | beforeEach -> 198 | formPage.setName "New name" 199 | formPage.setPrice "199.99" 200 | 201 | it "rollbacks all changes", -> 202 | formPage.resetButton.click() 203 | 204 | expect(formPage.nameField.getAttribute("value")).to.eventually.eq "Nexus 7" 205 | expect(formPage.priceField.getAttribute("value")).to.eventually.eq "1200" 206 | 207 | describe "show a product", -> 208 | showPage = null 209 | 210 | beforeEach -> 211 | row = indexPage.table.rowAt(0) 212 | row.showButton.click() 213 | 214 | showPage = new ShowPage() 215 | 216 | it "displays the product basic info", -> 217 | expect(showPage.product.name.getText()).to.eventually.eq "HTC Wildfire" 218 | expect(showPage.product.description.getText()).to.eventually.eq "Old android phone" 219 | 220 | describe "switch to details tab", -> 221 | beforeEach -> showPage.tabDetails.click() 222 | 223 | it "displays product details", -> 224 | expect(browser.getCurrentUrl()).to.eventually.match /#\/products\/\d+\/details/ 225 | expect(showPage.product.manufacturer.getText()).to.eventually.eq "HTC" 226 | 227 | describe "`Actions` tab", -> 228 | beforeEach -> showPage.tabActions.click() 229 | 230 | describe "click on `edit` button", -> 231 | beforeEach -> showPage.editButton.click() 232 | 233 | it "navigates to edit product page", -> 234 | expect(browser.getCurrentUrl()).to.eventually.match /#\/products\/\d+\/edit/ 235 | 236 | describe "click on `delete` button", -> 237 | beforeEach -> 238 | # click on the delete button 239 | showPage.deleteButton.click() 240 | 241 | # ..verify that the confirmation dialog is present 242 | alert = browser.switchTo().alert() 243 | expect(alert.getText()).to.eventually.eq "Are you sure?" 244 | 245 | # ..accept the confirmation 246 | alert.accept() 247 | 248 | it "deletes the product", -> 249 | expect(alertView.info.isDisplayed()).to.eventually.be.true 250 | expect(alertView.info.getText()).to.eventually.eq "Product was deleted" 251 | expect(indexPage.greeting.getText()).to.eventually.eq "You have 5 products" 252 | 253 | it "redirects to the products page", -> 254 | expect(browser.getCurrentUrl()).to.eventually.match /#\/products$/ 255 | --------------------------------------------------------------------------------