├── 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 |
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 |
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 |
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 | Actions |
15 | Name |
16 | Price |
17 | Discount |
18 | Description |
19 | Created At |
20 |
21 |
22 |
23 |
25 | | {{product.id}} |
26 |
27 |
28 |
31 |
37 |
38 | |
39 | {{product.name}} |
40 | {{product.priceWithDiscount() | currency}} |
41 |
42 | {{product.discount}}%
43 | ---
44 | |
45 | {{product.description}} |
46 | {{product.createdAt | date}} |
47 |
48 |
49 |
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 |
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 |
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 |
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 |
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 | [](http://travis-ci.org/lucassus/angular-seed)
4 | [](https://gemnasium.com/lucassus/angular-seed)
5 | [](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 |
--------------------------------------------------------------------------------