├── .bowerrc ├── .gitignore ├── .travis.yml ├── README.md ├── bower.json ├── package.json ├── public ├── css │ ├── paper-theme.css │ └── style.css ├── images │ ├── almond-toe-court-shoes.jpeg │ ├── bird-print-dress-black.jpeg │ ├── blazer-deer.jpeg │ ├── blue-suede-shoes.jpg │ ├── cotton-shorts-red.jpeg │ ├── cut-out-dress-pink.jpeg │ ├── feature-tests.png │ ├── flip-flops-blue.jpg │ ├── flip-flops.jpg │ ├── gold-button-cardigan.jpg │ ├── leather-driver-saddle-loafers.jpg │ ├── screenshot-browser.png │ ├── short-sleeve-shirt-green.jpg │ ├── short-sleeve-shirt-grey.jpeg │ ├── unit-tests.png │ └── waistcoat-grey.jpg ├── index.html └── js │ ├── app.js │ ├── shopController.js │ └── shopFactory.js ├── server.js └── test ├── e2e ├── conf.js └── shoppingFeature.js └── unit ├── karma.conf.js └── productFactory.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components/", 3 | "analytics": false, 4 | "timeout": 120000 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | public/bower_components 2 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" 7 | - npm start & 8 | - webdriver-manager update 9 | - webdriver-manager start & 10 | - sleep 3 11 | script: 12 | - ./node_modules/karma/bin/karma start test/unit/karma.conf.js 13 | - ./node_modules/.bin/protractor test/e2e/conf.js --browser=firefox 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/timrobertson0122/AngularShop.svg?branch=master)](https://travis-ci.org/timrobertson0122/AngularShop) 2 | [![Code Climate](https://codeclimate.com/github/timrobertson0122/AngularShop/badges/gpa.svg)](https://codeclimate.com/github/timrobertson0122/AngularShop) 3 | 4 | # AngularShop 5 | 6 | ![Browser](public/images/screenshot-browser.png) 7 | 8 | Brief & Approach 9 | ---------------- 10 | 11 | The task I was set was to develop a responsive website for a clothing retailer, satisfying the user stories listed below. 12 | 13 | Knowing it was expected to be responsive and given the time frame it was an easy decision to implement Bootstrap to speed this process up. I considered building in Rails but felt that, given the limited scope of the app, I could satisfy the user stories in a lighter, front-end framework - as such I felt an SPA in Angular would serve the purpose well, and give me a chance to improve my knowledge of Angular. If I were looking to scale this then Rails would be a wiser choice. 14 | 15 | Naturally, as this project was to be fully test-driven, and Protractor was written by the Angular team for e2e testing, and is written in Jasmine, I opted to use this for my feature tests, with Karma executing my unit tests. I knew these should play nice with Travis-CI, and I opted for CodeClimate to review my test coverage. 16 | 17 | I initially toyed with storing my product data in an external JSON file and using Angular's $http service to provide the data to my app however, given the size of the data, realised it would be quicker to store it as a variable within the factory. Likewise I knew that the controller should not be handling business logic, so moved this out to the factory also, thus adhering to Angular's MVC principles, and simplifying my testing. 18 | 19 | I opted to use Express to serve my app, as whilst I had never used it previously I knew it to be a lightweight and fast framework, which would suit my purposes fine for this project. Finally, I implemented the angular-flash module to provide error messages and improve the customer's experience. 20 | 21 | Technologies 22 | ------------ 23 | 24 | * Developed in AngularJS 25 | * Node Express server 26 | * Tested in Jasmine, Karma for unit tests, Protractor for e2e testing 27 | * Styled with Bootstrap 28 | * angular-flash module for error message handling 29 | 30 | Prerequisites 31 | ------------- 32 | 33 | You will need the following installed locally: 34 | 35 | * Node.js 36 | * NPM 37 | * Bower 38 | * Express 39 | * Protractor with webdriver-manager 40 | 41 | Site Setup 42 | ---------- 43 | 44 | * Execute the following in the command line: 45 | * ```Git clone https://github.com/timrobertson0122/AngularShop.git``` 46 | * ```cd AngularShop``` 47 | * ```bower install``` and ```npm install``` 48 | * ```npm start``` 49 | 50 | Navigate to ```localhost:4567``` in your browser. 51 | 52 | Testing Setup 53 | ------------- 54 | 55 | For unit tests, run ```npm test``` in the command line (from within the project's root directory): 56 | 57 | ![Unit Tests](public/images/unit-tests.png) 58 | 59 | For feature tests, run the following in the command line (from within the project's root directory): 60 | * ```npm start ``` (unless already running) 61 | * ```webdriver-manager start``` 62 | * ```protractor test/e2e/conf.js``` 63 | 64 | ![Feature Tests](public/images/feature-tests.png) 65 | 66 | User Stories 67 | ------------ 68 | 69 | ``` 70 | As a user, I can view the products and their category, price and availability information. 71 | 72 | As a user I can add a product to my shopping cart 73 | 74 | As a user I can remove a product from my shopping cart 75 | 76 | As a user I can view the total price for the products in my shopping cart 77 | 78 | As a user I can apply a voucher to my shopping cart 79 | 80 | As a user I can view the total price for the products in my shopping cart with discounts applied 81 | 82 | As a user I am alerted when I apply an invalid voucher to my shopping cart 83 | 84 | As a user I am unable to add 'out of stock' products to my shopping cart 85 | 86 | Vouchers 87 | -------- 88 | 89 | £5.00 off your order 90 | 91 | £10 off when you spend over £50 92 | 93 | £15.00 off when you have bought at least one footwear item and spent over £75.00 94 | ``` 95 | 96 | Future Features 97 | --------------- 98 | 99 | * Better styling, ensuring visually appealing and fully responsive. Improve UX e.g. hover over image 'Add to Basket' text, make it clearer to see that an item is out of stock. 100 | * Update item quantity when multiple items are added to the basket, rather than a new entry for each duplicate item. 101 | * Consider implementing ng-show directive to only display available vouchers based on current products in shopping basket (would also clarify with product owner as to whether a maximum of one voucher can be applied to any order) 102 | * Individual product info pages 103 | * Attach a database for product information, probably MongoDB 104 | * Establish persistence across page refresh, add authenticated users 105 | * Search functionality using filter directive 106 | * Checkout functionality with payment processing, possibly Stripe 107 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AngularShop", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Tim " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "angular": "~1.4.4", 17 | "angular-flash-alert": "~1.1.1", 18 | "bootstrap": "~3.3.5" 19 | }, 20 | "devDependencies": { 21 | "angular-mocks": "~1.4.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularshop", 3 | "version": "1.0.0", 4 | "description": "", 5 | "engines": { 6 | "node": "0.10.x", 7 | "npm": "2.1.x" 8 | }, 9 | "main": "server.js", 10 | "scripts": { 11 | "postinstall": "bower install", 12 | "prestart": "npm install", 13 | "start": "node server.js", 14 | "test": "karma start ./test/unit/karma.conf.js" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "jasmine": "^2.3.1", 20 | "jasmine-core": "^2.3.4", 21 | "karma": "^0.13.9", 22 | "karma-chrome-launcher": "^0.2.0", 23 | "karma-firefox-launcher": "^0.1.6", 24 | "karma-jasmine": "^0.3.6", 25 | "karma-phantomjs-launcher": "^0.2.1", 26 | "phantomjs": "^1.9.18", 27 | "protractor": "^2.1.0" 28 | }, 29 | "dependencies": { 30 | "bower": "^1.4.1", 31 | "express": "^4.13.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | } 4 | 5 | @media all and (min-width: 768px) { 6 | .navbar-fixed-width { 7 | width: 768px; 8 | margin-left: auto; 9 | margin-right:auto; 10 | } 11 | } 12 | 13 | @media all and (min-width: 992px) { 14 | .navbar-fixed-width { 15 | width: 992px; 16 | margin-left: auto; 17 | margin-right:auto; 18 | } 19 | 20 | } 21 | 22 | @media all and (min-width: 1200px) { 23 | .navbar-fixed-width { 24 | width: 1200px; 25 | margin-left: auto; 26 | margin-right:auto; 27 | } 28 | } 29 | 30 | a { 31 | cursor: pointer; 32 | cursor: hand; 33 | text-decoration: none; 34 | } 35 | 36 | h1, h2, h3, h4, h5, h6 { 37 | text-align: center; 38 | } 39 | 40 | .basket-total { 41 | clear: both; 42 | float: right; 43 | } 44 | 45 | #five-pound-voucher, 46 | #ten-pound-voucher, 47 | #fifteen-pound-voucher { 48 | margin: 5px; 49 | width: 100%; 50 | text-align: center; 51 | } 52 | 53 | .shopping-basket { 54 | padding-left: 0px; 55 | list-style-type: none; 56 | } 57 | 58 | #shopping-basket-container { 59 | border: solid; 60 | border-color: #337ab7; 61 | margin-top: 40px; 62 | padding-bottom: 40px; 63 | background-color: #F5F5F5; 64 | } 65 | 66 | .basket-item, 67 | .remove-basket-item, 68 | .basket-price { 69 | display: inline-block; 70 | padding: 2px; 71 | } 72 | 73 | .header { 74 | background-color: #337ab7; 75 | } 76 | 77 | .input-box { 78 | color: black; 79 | } 80 | 81 | .item-panel { 82 | list-style-type: none; 83 | } 84 | 85 | .no-padding-left { 86 | padding-left: 0px; 87 | } 88 | 89 | .totals { 90 | list-style-type: none; 91 | padding-right: 0px; 92 | } 93 | 94 | .vouchers { 95 | list-style-type: none; 96 | padding-left: 0px; 97 | } 98 | -------------------------------------------------------------------------------- /public/images/almond-toe-court-shoes.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/almond-toe-court-shoes.jpeg -------------------------------------------------------------------------------- /public/images/bird-print-dress-black.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/bird-print-dress-black.jpeg -------------------------------------------------------------------------------- /public/images/blazer-deer.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/blazer-deer.jpeg -------------------------------------------------------------------------------- /public/images/blue-suede-shoes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/blue-suede-shoes.jpg -------------------------------------------------------------------------------- /public/images/cotton-shorts-red.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/cotton-shorts-red.jpeg -------------------------------------------------------------------------------- /public/images/cut-out-dress-pink.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/cut-out-dress-pink.jpeg -------------------------------------------------------------------------------- /public/images/feature-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/feature-tests.png -------------------------------------------------------------------------------- /public/images/flip-flops-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/flip-flops-blue.jpg -------------------------------------------------------------------------------- /public/images/flip-flops.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/flip-flops.jpg -------------------------------------------------------------------------------- /public/images/gold-button-cardigan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/gold-button-cardigan.jpg -------------------------------------------------------------------------------- /public/images/leather-driver-saddle-loafers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/leather-driver-saddle-loafers.jpg -------------------------------------------------------------------------------- /public/images/screenshot-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/screenshot-browser.png -------------------------------------------------------------------------------- /public/images/short-sleeve-shirt-green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/short-sleeve-shirt-green.jpg -------------------------------------------------------------------------------- /public/images/short-sleeve-shirt-grey.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/short-sleeve-shirt-grey.jpeg -------------------------------------------------------------------------------- /public/images/unit-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/unit-tests.png -------------------------------------------------------------------------------- /public/images/waistcoat-grey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timrobertson0122/AngularShop/5598abf88357c9a9d6b0369a7ee3d1f0ef54d21c/public/images/waistcoat-grey.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Shop 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 28 |
29 |
30 |
31 | 45 |
46 |
47 |
48 |

Shopping Basket

49 |
    50 |
  • 51 |
  • 52 |

    {{basketItem.name | limitTo: 30 }}

    53 |

    {{basketItem.price | currency:"£"}}

    54 |
  • 55 |
56 |
57 |
    58 |
  • £5 off voucher
  • 59 |
  • £10 off voucher
  • 60 |
  • £15 off voucher
  • 61 |
  • 62 |

    Basket Total: {{ctrl.products.subTotal() | currency:"£"}}

    63 |
  • 64 |
  • 65 |

    Total Discount: {{ctrl.products.discountTotal() | currency:"£"}}

    66 |
  • 67 |
  • 68 |

    Final Amount : {{ctrl.products.basketTotal| currency:"£"}}

    69 |
  • 70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | var clothesShop = angular.module("ClothesShop", ['flash']); -------------------------------------------------------------------------------- /public/js/shopController.js: -------------------------------------------------------------------------------- 1 | clothesShop.controller('ShopController', ['Products', function(Products) { 2 | 3 | this.products = Products; 4 | 5 | }]); 6 | -------------------------------------------------------------------------------- /public/js/shopFactory.js: -------------------------------------------------------------------------------- 1 | clothesShop.factory('Products', ['Flash', function(Flash) { 2 | 3 | var service = {}; 4 | var shoppingBasket = []; 5 | 6 | service.basketTotal = 0; 7 | service.shoppingBasket = shoppingBasket; 8 | service.fivePoundDiscount = false; 9 | service.tenPoundDiscount = false; 10 | service.fifteenPoundDiscount = false; 11 | 12 | service.productList = { 13 | "Womens Footwear": [{ 14 | name: "Suede Shoes, Blue", 15 | price: 42.00, 16 | quantity: 4, 17 | category: "Womens Footwear", 18 | image: "images/blue-suede-shoes.jpg" 19 | }, { 20 | name: "Almond Toe Court Shoes, Patent Black", 21 | price: 99.00, 22 | quantity: 5, 23 | category: "Womens Footwear", 24 | image: "images/almond-toe-court-shoes.jpeg" 25 | }], 26 | "Womens Casual": [{ 27 | name: "Gold Button Cardigan, Black", 28 | price: 167.00, 29 | quantity: 6, 30 | category: "Womens Casual", 31 | image: "images/gold-button-cardigan.jpg" 32 | }, { 33 | name: "Cotton Shorts, Medium Red", 34 | price: 30.00, 35 | quantity: 5, 36 | category: "Womens Casual", 37 | image: "images/cotton-shorts-red.jpeg" 38 | }], 39 | "Womens Formal": [{ 40 | name: "Bird Print Dress, Black", 41 | price: 270.00, 42 | quantity: 10, 43 | category: "Womens Formal", 44 | image: "images/bird-print-dress-black.jpeg" 45 | }, { 46 | name: "Mid Twist Cut-Out Dress, Pink", 47 | price: 540.00, 48 | quantity: 5, 49 | category: "Womens Formal", 50 | image: "images/cut-out-dress-pink.jpeg" 51 | }], 52 | "Mens Footwear": [{ 53 | name: "Leather Driver Saddle Loafers, Tan", 54 | price: 34.00, 55 | quantity: 12, 56 | category: "Mens Footwear", 57 | image: "images/leather-driver-saddle-loafers.jpg" 58 | }, { 59 | name: "Flip Flops, Red", 60 | price: 19.00, 61 | quantity: 6, 62 | category: "Mens Footwear", 63 | image: "images/flip-flops.jpg" 64 | }, { 65 | name: "Flip Flops, Blue", 66 | price: 19.00, 67 | quantity: 0, 68 | category: "Mens Footwear", 69 | image: "images/flip-flops-blue.jpg" 70 | }], 71 | "Mens Casual": [{ 72 | name: "Fine Stripe Short Sleeve Shirt, Grey", 73 | price: 49.99, 74 | quantity: 9, 75 | category: "Mens Casual", 76 | image: "images/short-sleeve-shirt-grey.jpeg" 77 | }, { 78 | name: "Fine Stripe Short Sleeve Shirt, Green", 79 | price: 39.99, 80 | quantity: 3, 81 | category: "Mens Casual", 82 | image: "images/short-sleeve-shirt-green.jpg" 83 | }], 84 | "Mens Formal": [{ 85 | name: "Sharkskin Waistcoat, Charcoal", 86 | price: 75.00, 87 | quantity: 6, 88 | category: "Mens Formal", 89 | image: "images/waistcoat-grey.jpg" 90 | }, { 91 | name: "Lightweight Patch Pocket Blazer, Deer", 92 | price: 175.50, 93 | quantity: 1, 94 | category: "Mens Formal", 95 | image: "images/blazer-deer.jpeg" 96 | }] 97 | }; 98 | 99 | service.availableDiscounts = function() { 100 | service.fivePoundDiscount = true; 101 | service.tenPoundDiscount = true; 102 | service.fifteenPoundDiscount = true; 103 | }; 104 | 105 | service.getBasketTotal = function() { 106 | var result = 0; 107 | for (var i = shoppingBasket.length - 1; i >= 0; i--) { 108 | result += shoppingBasket[i].price; 109 | }; 110 | result = parseFloat(result.toPrecision(12)); 111 | service.basketTotal = result; 112 | return result; 113 | }; 114 | 115 | service.subTotal = function() { 116 | return service.shoppingBasket.map(function(item) { 117 | return parseFloat(item.price); 118 | }).reduce(function(previousValue, currentValue) { 119 | return previousValue + currentValue; 120 | }, 0); 121 | }; 122 | 123 | service.discountTotal = function() { 124 | var result = service.subTotal() - service.basketTotal; 125 | return result; 126 | }; 127 | 128 | service.itemAvailable = function(item) { 129 | return (parseInt(item.quantity) > 0); 130 | }; 131 | 132 | service.addItemToBasket = function(item) { 133 | if (!service.itemAvailable(item)) { 134 | Flash.create('danger', 'Sorry, that item is out of stock.'); 135 | } else { 136 | shoppingBasket.push(item); 137 | service.shoppingBasketVisible(); 138 | service.availableDiscounts(); 139 | item.quantity--; 140 | }; 141 | service.getBasketTotal(); 142 | }; 143 | 144 | service.removeItemFromBasket = function(item) { 145 | shoppingBasket.pop(item); 146 | item.quantity++; 147 | service.shoppingBasketVisible(); 148 | service.getBasketTotal(); 149 | }; 150 | 151 | service.emptyBasket = function() { 152 | for (var i = shoppingBasket.length - 1; i >= 0; i--) { 153 | shoppingBasket[i].quantity++; 154 | } 155 | shoppingBasket.length = 0; 156 | service.getBasketTotal(); 157 | }; 158 | 159 | service.shoppingBasketVisible = function() { 160 | if (service.shoppingBasket.length > 0) { 161 | return true; 162 | } 163 | }; 164 | 165 | service.ShoesInBasket = function() { 166 | for (var i = shoppingBasket.length - 1; i >= 0; i--) { 167 | if (shoppingBasket[i].category.indexOf("Footwear") >= 0) { 168 | return true; 169 | } 170 | } 171 | }; 172 | 173 | service.applyFivePoundDiscount = function() { 174 | if (service.fivePoundDiscount) { 175 | service.basketTotal -= 5.00; 176 | service.fivePoundDiscount = false; 177 | } 178 | }; 179 | 180 | service.applyTenPoundDiscount = function() { 181 | if (service.basketTotal < 50.00) { 182 | Flash.create('danger', 'Sorry, you must spend over £50.'); 183 | } else if (service.tenPoundDiscount) { 184 | service.basketTotal -= 10.00; 185 | service.tenPoundDiscount = false; 186 | } 187 | }; 188 | 189 | service.applyFifteenPoundDiscount = function() { 190 | if (service.basketTotal < 75.00 || !service.ShoesInBasket()) { 191 | Flash.create('danger', 'Sorry, discount only available for orders over £75 and including at least one item of footwear'); 192 | } else if (service.fifteenPoundDiscount) { 193 | service.basketTotal -= 15.00; 194 | service.fifteenPoundDiscount = false; 195 | } 196 | }; 197 | 198 | return service; 199 | 200 | }]); 201 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var server = require('http').createServer(app); 4 | 5 | app.use(express.static('public')); 6 | app.use('/bower_components', express.static('bower_components')); 7 | 8 | app.get('/', function(request, response){ 9 | response.send('index.html'); 10 | }); 11 | 12 | server.listen(process.env.PORT || 4567, function(){ 13 | console.log("Server listening on port 4567"); 14 | }); 15 | 16 | module.exports = server; -------------------------------------------------------------------------------- /test/e2e/conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | seleniumAddress: 'http://localhost:4444/wd/hub', 3 | specs: ['shoppingFeature.js'], 4 | capabilities: { 5 | 'browserName': 'firefox' 6 | }, 7 | framework: 'jasmine', 8 | 9 | jasmineNodeOpts: { 10 | defaultTimeoutInterval: 30000 11 | } 12 | }; -------------------------------------------------------------------------------- /test/e2e/shoppingFeature.js: -------------------------------------------------------------------------------- 1 | describe('Clothes Shopping Site', function() { 2 | beforeEach(function() { 3 | browser.get('http://localhost:4567'); 4 | }); 5 | 6 | it('has a title', function() { 7 | expect(browser.getTitle()).toEqual('Angular Shop'); 8 | }); 9 | 10 | it('has a list of products with their price etc', function() { 11 | expect(element(by.cssContainingText('.shop-item', 'Almond Toe Court Shoes, Patent Black')).getText()).toContain("Court Shoes"); 12 | }); 13 | 14 | it('initialises with a hidden shopping basket', function() { 15 | expect(element(by.css('.shopping-basket')).isDisplayed()).toBe(false); 16 | }); 17 | 18 | it('the shopping basket displays a total of £0.00 before any items are added', function() { 19 | expect(element(by.css('.basket-header-total')).getText()).toContain('£0.00'); 20 | }); 21 | 22 | it('can add a product to the shopping basket, and show the basket total', function() { 23 | element(by.cssContainingText('.shop-item', 'Almond Toe Court Shoes, Patent Black')).click(); 24 | expect(element(by.css('.basket-header-total')).getText()).toContain('£99.00'); 25 | expect(element(by.css('.shopping-basket')).getText()).toContain('Court Shoes'); 26 | }); 27 | 28 | it('can remove a product from the shopping basket, and reset the total to zero', function() { 29 | element.all(by.id("addToBasketButton")).first().click(); 30 | element(by.css(".remove")).click(); 31 | expect(element(by.css('.shopping-basket')).isDisplayed()).toBe(false); 32 | expect(element(by.css('.basket-header-total')).getText()).toContain('£0.00'); 33 | }); 34 | 35 | xit('cannot add an out of stock item to the basket', function() { 36 | element(by.cssContainingText('.shop-item', 'Flip Flops, Blue')).click(); 37 | expect(element(by.css('errors')).getText()).toContain('Sorry, Sorry, that item is out of stock.'); 38 | }) 39 | 40 | describe('using vouchers', function() { 41 | it('can apply a £5 discount voucher once an item has been added to the basket', function() { 42 | element(by.cssContainingText('.shop-item', 'Almond Toe Court Shoes, Patent Black')).click(); 43 | element(by.id('five-pound-voucher')).click(); 44 | expect(element(by.css('.basket-total')).getText()).toContain('£94.00'); 45 | }); 46 | 47 | it('can only apply one £5 discount voucher once an item has been added to the basket', function() { 48 | element(by.cssContainingText('.shop-item', 'Almond Toe Court Shoes, Patent Black')).click(); 49 | element(by.id('five-pound-voucher')).click(); 50 | element(by.id('five-pound-voucher')).click(); 51 | expect(element(by.css('.basket-total')).getText()).toContain('£94.00'); 52 | }); 53 | 54 | it('can apply a £10 discount voucher when the basket subtotal is greater than £50', function() { 55 | element(by.cssContainingText('.shop-item', 'Almond Toe Court Shoes, Patent Black')).click(); 56 | element(by.id('ten-pound-voucher')).click(); 57 | expect(element(by.css('.basket-total')).getText()).toContain('£89.00'); 58 | }); 59 | 60 | it('can apply a £15 discount voucher when the basket contains at least 1 item of footwear and total is at least £75', function() { 61 | element(by.cssContainingText('.shop-item', 'Almond Toe Court Shoes, Patent Black')).click(); 62 | element(by.id('fifteen-pound-voucher')).click(); 63 | expect(element(by.css('.basket-total')).getText()).toContain('£84.00'); 64 | }); 65 | 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Aug 18 2015 11:20:33 GMT+0100 (BST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '../..', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'public/bower_components/angular/angular.js', 19 | 'public/bower_components/angular-route/angular-route.js', 20 | 'public/bower_components/angular-resource/angular-resource.js', 21 | 'public/bower_components/angular-mocks/angular-mocks.js', 22 | 'public/bower_components/angular-flash-alert/dist/angular-flash.js', 23 | 'public/js/*.js', 24 | 'test/unit/*.spec.js' 25 | ], 26 | 27 | 28 | // list of files to exclude 29 | exclude: [], 30 | 31 | 32 | // preprocess matching files before serving them to the browser 33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 34 | preprocessors: {}, 35 | 36 | 37 | // test results reporter to use 38 | // possible values: 'dots', 'progress' 39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 40 | reporters: ['progress'], 41 | 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | 47 | // enable / disable colors in the output (reporters and logs) 48 | colors: true, 49 | 50 | 51 | // level of logging 52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 53 | logLevel: config.LOG_INFO, 54 | 55 | 56 | // enable / disable watching file and executing tests whenever any file changes 57 | autoWatch: true, 58 | 59 | 60 | // start these browsers 61 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 62 | browsers: ['PhantomJS'], 63 | 64 | 65 | // Continuous Integration mode 66 | // if true, Karma captures browsers, runs the tests and exits 67 | singleRun: true 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /test/unit/productFactory.spec.js: -------------------------------------------------------------------------------- 1 | describe('factory: Products', function() { 2 | 3 | var products; 4 | 5 | beforeEach(function() { 6 | module('ClothesShop'); 7 | }); 8 | 9 | beforeEach(inject(function(Products) { 10 | products = Products 11 | sampleItemShoes = { 12 | name: "Suede Shoes, Blue", 13 | price: 42.00, 14 | quantity: 4, 15 | category: "Womens Footwear" 16 | }; 17 | sampleItemNotShoes = { 18 | name: "Cotton Shorts, Medium Red", 19 | price: 30.00, 20 | quantity: 5, 21 | category: "Womens Casual" 22 | }; 23 | })); 24 | 25 | describe('shopping basket', function() { 26 | 27 | it('is empty upon initialisation', function() { 28 | expect(products.shoppingBasket).toEqual([]); 29 | }); 30 | 31 | it('is hidden upon initialisation', function() { 32 | expect(products.shoppingBasketVisible()).not.toBe(true); 33 | }); 34 | 35 | it('has a total of zero upon initialisation', function() { 36 | expect(products.basketTotal).toEqual(0); 37 | }); 38 | 39 | it('can add a product to the shopping basket', function() { 40 | products.addItemToBasket(sampleItemShoes); 41 | 42 | expect(products.shoppingBasket).toEqual([sampleItemShoes]); 43 | }); 44 | 45 | it('sets the shopping basket to visible when it contains at least one item', function() { 46 | products.addItemToBasket(sampleItemShoes); 47 | 48 | expect(products.shoppingBasketVisible()).toEqual(true); 49 | }); 50 | 51 | it('can add multiple items to the shopping basket', function() { 52 | products.addItemToBasket(sampleItemShoes); 53 | products.addItemToBasket(sampleItemNotShoes); 54 | 55 | expect(products.shoppingBasket).toEqual([sampleItemShoes, sampleItemNotShoes]); 56 | }); 57 | 58 | it('can remove a product from the shopping basket', function() { 59 | products.addItemToBasket(sampleItemShoes); 60 | products.removeItemFromBasket(sampleItemShoes); 61 | 62 | expect(products.shoppingBasket).toEqual([]); 63 | }); 64 | 65 | it('can remove a product from the shopping basket one at a time', function() { 66 | products.addItemToBasket(sampleItemShoes); 67 | products.addItemToBasket(sampleItemShoes); 68 | products.removeItemFromBasket(sampleItemShoes); 69 | 70 | expect(products.shoppingBasket).toEqual([sampleItemShoes]); 71 | }); 72 | 73 | it('can empty the shopping basket of all products', function() { 74 | products.addItemToBasket(sampleItemShoes); 75 | products.addItemToBasket(sampleItemShoes); 76 | products.emptyBasket(); 77 | 78 | expect(products.shoppingBasket).toEqual([]); 79 | }); 80 | 81 | it('sets the shopping basket to hidden when all items are removed', function() { 82 | products.addItemToBasket(sampleItemShoes); 83 | products.emptyBasket(); 84 | 85 | expect(products.shoppingBasketVisible()).not.toBe(true); 86 | }); 87 | 88 | it('knows the total cost of the products in the shopping basket', function() { 89 | products.addItemToBasket(sampleItemShoes); 90 | products.addItemToBasket(sampleItemNotShoes); 91 | 92 | expect(products.basketTotal).toEqual(72.00); 93 | }); 94 | 95 | it('adding a product to the shopping basket reduces that products quantity by one', function() { 96 | expect(sampleItemShoes.quantity).toEqual(4); 97 | products.addItemToBasket(sampleItemShoes); 98 | 99 | expect(sampleItemShoes.quantity).toEqual(3); 100 | }); 101 | 102 | it('wont add a product to the shopping basket when its available quantity is less than one', function() { 103 | for (var i = 5; i >= 0; i--) { 104 | products.addItemToBasket(sampleItemShoes); 105 | }; 106 | 107 | expect(products.shoppingBasket.length).toEqual(4); 108 | }); 109 | }); 110 | 111 | describe('vouchers', function() { 112 | 113 | it('can apply a £5 voucher to any order', function() { 114 | products.addItemToBasket(sampleItemShoes); 115 | expect(products.basketTotal).toEqual(42); 116 | expect(products.fivePoundDiscount).toEqual(true); 117 | products.applyFivePoundDiscount(); 118 | expect(products.basketTotal).toEqual(37); 119 | }); 120 | 121 | it('will only apply a £10 voucher to orders totalling at least £50', function() { 122 | products.addItemToBasket(sampleItemShoes); 123 | products.applyTenPoundDiscount(); 124 | expect(products.basketTotal).toEqual(42); 125 | products.addItemToBasket(sampleItemShoes); 126 | expect(products.tenPoundDiscount).toEqual(true); 127 | products.applyTenPoundDiscount(); 128 | expect(products.basketTotal).toEqual(74); 129 | }); 130 | 131 | it('will only apply a £15 voucher if the order contains shoes and is greater than £75 in value', function() { 132 | products.addItemToBasket(sampleItemNotShoes); 133 | products.applyFifteenPoundDiscount(); 134 | expect(products.basketTotal).toEqual(30); 135 | products.addItemToBasket(sampleItemNotShoes); 136 | products.addItemToBasket(sampleItemNotShoes); 137 | products.applyFifteenPoundDiscount(); 138 | expect(products.basketTotal).toEqual(90); 139 | products.addItemToBasket(sampleItemShoes); 140 | expect(products.fifteenPoundDiscount).toEqual(true); 141 | products.applyFifteenPoundDiscount(); 142 | expect(products.basketTotal).toEqual(117); 143 | }); 144 | 145 | it('will only apply the £5 voucher once', function() { 146 | products.addItemToBasket(sampleItemNotShoes); 147 | products.applyFivePoundDiscount(); 148 | products.applyFivePoundDiscount(); 149 | 150 | expect(products.basketTotal).toEqual(25); 151 | }); 152 | 153 | it('will only apply the £10 voucher once', function() { 154 | products.addItemToBasket(sampleItemShoes); 155 | products.addItemToBasket(sampleItemShoes); 156 | products.applyTenPoundDiscount(); 157 | products.applyTenPoundDiscount(); 158 | 159 | expect(products.basketTotal).toEqual(74); 160 | }); 161 | 162 | it('will only apply the £15 voucher once', function() { 163 | products.addItemToBasket(sampleItemShoes); 164 | products.addItemToBasket(sampleItemShoes); 165 | products.addItemToBasket(sampleItemNotShoes); 166 | products.applyFifteenPoundDiscount(); 167 | products.applyFifteenPoundDiscount(); 168 | 169 | expect(products.basketTotal).toEqual(99); 170 | }); 171 | 172 | it('removes the £5 voucher when the shopping basket is empty', function() { 173 | products.addItemToBasket(sampleItemNotShoes); 174 | products.applyFivePoundDiscount(); 175 | products.removeItemFromBasket(sampleItemNotShoes); 176 | 177 | expect(products.basketTotal).toEqual(0); 178 | }); 179 | 180 | it('removes the £10 voucher when it is no longer valid', function() { 181 | products.addItemToBasket(sampleItemShoes); 182 | products.addItemToBasket(sampleItemShoes); 183 | products.applyTenPoundDiscount(); 184 | expect(products.basketTotal).toEqual(74); 185 | products.removeItemFromBasket(sampleItemShoes); 186 | expect(products.basketTotal).toEqual(42); 187 | }); 188 | 189 | it('removes the £15 voucher when it is no longer valid', function() { 190 | products.addItemToBasket(sampleItemNotShoes); 191 | products.addItemToBasket(sampleItemNotShoes); 192 | products.addItemToBasket(sampleItemShoes); 193 | products.applyFifteenPoundDiscount(); 194 | expect(products.basketTotal).toEqual(87); 195 | products.removeItemFromBasket(sampleItemShoes); 196 | expect(products.basketTotal).toEqual(60); 197 | }) 198 | }); 199 | }); 200 | --------------------------------------------------------------------------------