├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .gitignore ├── .jshintrc ├── .projections.json ├── .travis.yml ├── .watchmanconfig ├── LICENSE ├── README.md ├── app ├── adapters │ └── application.js ├── app.js ├── components │ ├── .gitkeep │ └── a-car.js ├── controllers │ └── .gitkeep ├── helpers │ └── .gitkeep ├── index.html ├── mirage │ ├── config.js │ ├── factories │ │ ├── car.js │ │ └── part.js │ └── scenarios │ │ └── default.js ├── models │ ├── .gitkeep │ ├── car.js │ └── part.js ├── router.js ├── routes │ ├── .gitkeep │ ├── car.js │ ├── car │ │ ├── new-part.js │ │ └── parts.js │ ├── cars.js │ └── cars │ │ ├── index.js │ │ └── new.js ├── styles │ └── app.css └── templates │ ├── application.hbs │ ├── car.hbs │ ├── car │ ├── new-part.hbs │ └── parts.hbs │ ├── cars.hbs │ ├── cars │ ├── index.hbs │ └── new.hbs │ └── components │ ├── .gitkeep │ └── a-car.hbs ├── bower.json ├── config └── environment.js ├── ember-cli-build.js ├── package.json ├── public ├── crossdomain.xml └── robots.txt ├── testem.json ├── tests ├── .jshintrc ├── acceptance │ ├── cars-test.js │ └── parts-test.js ├── helpers │ ├── mirage-integration.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── integration │ └── components │ │ └── a-car-test.js ├── test-helper.js └── unit │ └── routes │ └── car │ └── new-part-test.js ├── tutorial.md └── vendor └── .gitkeep /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "server", 4 | "document", 5 | "window", 6 | "-Promise" 7 | ], 8 | "browser": true, 9 | "boss": true, 10 | "curly": true, 11 | "debug": false, 12 | "devel": true, 13 | "eqeqeq": true, 14 | "evil": true, 15 | "forin": false, 16 | "immed": false, 17 | "laxbreak": false, 18 | "newcap": true, 19 | "noarg": true, 20 | "noempty": false, 21 | "nonew": false, 22 | "nomen": false, 23 | "onevar": false, 24 | "plusplus": false, 25 | "regexp": false, 26 | "undef": true, 27 | "sub": true, 28 | "strict": false, 29 | "white": false, 30 | "eqnull": true, 31 | "esnext": true, 32 | "unused": true 33 | } 34 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "app/adapters/*.js": { 3 | "command": "adapter", 4 | "template": [ 5 | "// export default DS.{capitalize}Adapter.extend();", 6 | ] 7 | }, 8 | 9 | "tests/acceptance/*.js": { 10 | "command": "acceptancetest", 11 | "template": [ 12 | "// acceptance test", 13 | ] 14 | }, 15 | 16 | "tests/unit/models/*.js": { 17 | "command": "unittest", 18 | "template": [ 19 | "// unit test", 20 | ] 21 | }, 22 | 23 | "app/components/*.js": { 24 | "command": "component", 25 | "template": [ 26 | "import Ember from 'ember';", 27 | "export default Ember.Component.extend({", 28 | "", 29 | "});", 30 | ] 31 | }, 32 | 33 | "app/controllers/*.js": { 34 | "command": "controller", 35 | "template": [ 36 | "import Ember from 'ember';", 37 | "export default Ember.Controller.extend({", 38 | "", 39 | "});", 40 | ] 41 | }, 42 | 43 | "app/helpers/*.js": { 44 | "command": "helper", 45 | "template": [ 46 | "// Please note that Handlebars helpers will only be found automatically by the", 47 | "// resolver if their name contains a dash (reverse-word, translate-text, etc.)", 48 | "// For more details: http://stefanpenner.github.io/ember-app-kit/guides/using-modules.html", 49 | "", 50 | "export default Ember.Handlebars.makeBoundHelper(function() {", 51 | "", 52 | "});", 53 | ] 54 | }, 55 | 56 | "app/models/*.js": { 57 | "command": "model", 58 | "template": [ 59 | "import DS from 'ember-data'", 60 | "export default DS.Model.extend({", 61 | "", 62 | "});", 63 | ] 64 | }, 65 | 66 | "app/router.js": { 67 | "command": "router" 68 | }, 69 | 70 | "app/routes/*.js": { 71 | "command": "route", 72 | "template": [ 73 | "import Ember from 'ember';", 74 | "export default Ember.Route.extend({", 75 | "", 76 | "});", 77 | ] 78 | 79 | }, 80 | 81 | "app/styles/*.scss": { 82 | "command": "style", 83 | "template": [ 84 | "/* {}.scss */", 85 | "", 86 | ] 87 | }, 88 | 89 | "app/templates/*.hbs": { 90 | "command": "template" 91 | }, 92 | 93 | "app/utils/*.js": { 94 | "command": "util" 95 | }, 96 | 97 | "app/mirage/config.js": { 98 | "command": "mirage" 99 | }, 100 | 101 | "app/mirage/factories/*.js": { 102 | "command": "factory", 103 | "template": [ 104 | "import Mirage from 'ember-cli-mirage';", 105 | "export default Mirage.Factory.extend({", 106 | "});" 107 | ] 108 | }, 109 | 110 | "app/mirage/scenarios/*.js": { 111 | "command": "scenario" 112 | }, 113 | 114 | 115 | "app/views/*.js": { 116 | "command": "view", 117 | "template": [ 118 | "import Ember from 'ember';", 119 | "export default Ember.View.extend({", 120 | "", 121 | "});", 122 | ] 123 | }, 124 | } 125 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | before_install: 13 | - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH 14 | - "npm config set spin false" 15 | - "npm install -g npm@^2" 16 | 17 | install: 18 | - npm install -g bower 19 | - npm install 20 | - bower install 21 | 22 | script: 23 | - npm test 24 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jason Cummings 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mirage-tutorial 2 | 3 | Tutorial found in the at [Hashrocket Blog](http://hashrocket.com/blog/posts/test-driving-a-stubbed-api-in-ember-with-ember-cli-mirage) and the tutorial.md file. 4 | 5 | * [ember.js](http://emberjs.com/) 6 | * [ember-cli](http://www.ember-cli.com/) 7 | * Development Browser Extensions 8 | * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) 9 | * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) 10 | -------------------------------------------------------------------------------- /app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.JSONAPIAdapter.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from 'ember/resolver'; 3 | import loadInitializers from 'ember/load-initializers'; 4 | import config from './config/environment'; 5 | 6 | var App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver: Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/app/components/.gitkeep -------------------------------------------------------------------------------- /app/components/a-car.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'li', 5 | classNames: ['car'] 6 | }); 7 | -------------------------------------------------------------------------------- /app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/app/controllers/.gitkeep -------------------------------------------------------------------------------- /app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/app/helpers/.gitkeep -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MirageTutorial 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | 12 | 13 | 14 | 15 | {{content-for 'head-footer'}} 16 | 17 | 18 | {{content-for 'body'}} 19 | 20 | 21 | 22 | 23 | {{content-for 'body-footer'}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/mirage/config.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | this.get('/cars', (db) => { 3 | let data = {}; 4 | data = db.cars.map((attrs) => { 5 | 6 | let car = { 7 | type: 'cars', 8 | id: attrs.id, 9 | attributes: attrs , 10 | relationships: { 11 | parts: { 12 | data: {} 13 | } 14 | }, 15 | }; 16 | 17 | car.relationships.parts.data = db.parts 18 | .where({car_id: attrs.id}) 19 | .map((attrs) => { 20 | return { 21 | type: 'parts', 22 | id: attrs.id, 23 | attributes: attrs 24 | }; 25 | }); 26 | 27 | return car; 28 | 29 | }); 30 | return { data }; 31 | }); 32 | 33 | this.post('/cars', (db, request) => { 34 | debugger 35 | return JSON.parse(request.requestBody); 36 | }); 37 | 38 | this.get('/cars/:id', (db, request) => { 39 | let car = db.cars.find(request.params.id); 40 | let parts = db.parts.where({car_id: car.id}); 41 | 42 | let data = { 43 | type: 'car', 44 | id: request.params.id, 45 | attributes: car, 46 | relationships: { 47 | parts:{ 48 | data:{} 49 | } 50 | } 51 | }; 52 | 53 | data.relationships.parts.data = parts.map((attrs) => { 54 | return { type: 'parts', id: attrs.id, attributes: attrs }; 55 | }); 56 | 57 | return { data }; 58 | }); 59 | 60 | this.get('parts/:id', (db, request) => { 61 | let part = db.parts.find(request.params.id); 62 | 63 | let data = { 64 | type: 'parts', 65 | id: request.params.id, 66 | attributes: part, 67 | }; 68 | 69 | return { data }; 70 | }); 71 | 72 | this.post('parts', (db, request) => { 73 | return JSON.parse(request.requestBody); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /app/mirage/factories/car.js: -------------------------------------------------------------------------------- 1 | import Mirage from 'ember-cli-mirage'; 2 | export default Mirage.Factory.extend({ 3 | name(i) { return `Car ${i + 1}`;}, 4 | }); 5 | -------------------------------------------------------------------------------- /app/mirage/factories/part.js: -------------------------------------------------------------------------------- 1 | import Mirage from 'ember-cli-mirage'; 2 | export default Mirage.Factory.extend({ 3 | name(i) { return `Part ${i}`; } 4 | }); 5 | -------------------------------------------------------------------------------- /app/mirage/scenarios/default.js: -------------------------------------------------------------------------------- 1 | export default function(server) { 2 | 3 | // Seed your development database using your factories. This 4 | // data will not be loaded in your tests. 5 | 6 | let car = server.create('car'); 7 | server.createList('part', 4, { car_id: car.id }); 8 | } 9 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/app/models/.gitkeep -------------------------------------------------------------------------------- /app/models/car.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string'), 5 | parts: DS.hasMany('part') 6 | }); 7 | -------------------------------------------------------------------------------- /app/models/part.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string'), 5 | car: DS.belongsTo('car') 6 | }); 7 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | var Router = Ember.Router.extend({ 5 | location: config.locationType 6 | }); 7 | 8 | Router.map(function() { 9 | this.route('cars', {}, function(){ 10 | this.route('new', {}); 11 | }); 12 | 13 | this.route('car', { path: '/car/:id' }, function(){ 14 | this.route('parts', {}); 15 | this.route('new-part', {}); 16 | }); 17 | }); 18 | 19 | export default Router; 20 | -------------------------------------------------------------------------------- /app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/app/routes/.gitkeep -------------------------------------------------------------------------------- /app/routes/car.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(params){ 5 | return this.store.find('car', params.id); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/routes/car/new-part.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | actions: { 5 | newPart(name){ 6 | const car = this.modelFor('car'); 7 | const part = this.store.createRecord('part', { name, car }); 8 | part.save().then(() => { 9 | this.transitionTo('car.parts', car); 10 | }); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /app/routes/car/parts.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/routes/cars.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/routes/cars/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(){ 5 | return this.store.findAll('car'); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /app/routes/cars/new.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Route.extend({ 3 | actions: { 4 | createCar(name){ 5 | const car = this.store.createRecord('car', { name }); 6 | car.save() 7 | .then(() => { 8 | this.transitionTo('cars'); 9 | }).catch(() => { 10 | //something that handles failures 11 | }); 12 | } 13 | } 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/app/styles/app.css -------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Ember

2 | {{link-to 'Cars' 'cars.index' id='all-cars'}} 3 | 4 | {{outlet}} 5 | -------------------------------------------------------------------------------- /app/templates/car.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /app/templates/car/new-part.hbs: -------------------------------------------------------------------------------- 1 | New Part 2 | 3 |
4 | {{input name='part-name' value=name}} 5 | 6 |
7 | -------------------------------------------------------------------------------- /app/templates/car/parts.hbs: -------------------------------------------------------------------------------- 1 | 9 | 10 | {{#link-to 'car.new-part' model class='new-part'}} 11 | Add new Part 12 | {{/link-to}} 13 | -------------------------------------------------------------------------------- /app/templates/cars.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /app/templates/cars/index.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{#link-to 'cars.new' id='add-car'}} 8 | Add new car 9 | {{/link-to}} 10 | -------------------------------------------------------------------------------- /app/templates/cars/new.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{input name='car-name' value=name}} 3 | 4 |
5 | -------------------------------------------------------------------------------- /app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /app/templates/components/a-car.hbs: -------------------------------------------------------------------------------- 1 | {{#link-to 'car.parts' car class='car-link'}} 2 | {{car.name}} 3 | {{/link-to}} 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirage-tutorial", 3 | "dependencies": { 4 | "ember": "2.1.0", 5 | "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", 6 | "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", 7 | "ember-data": "2.1.0", 8 | "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.5", 9 | "ember-qunit": "0.4.9", 10 | "ember-qunit-notifications": "0.0.7", 11 | "ember-resolver": "~0.1.18", 12 | "jquery": "^1.11.3", 13 | "loader.js": "ember-cli/loader.js#3.2.1", 14 | "qunit": "~1.18.0", 15 | "pretender": "~0.10.1", 16 | "lodash": "~3.7.0", 17 | "Faker": "~3.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'mirage-tutorial', 6 | environment: environment, 7 | baseURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | } 14 | }, 15 | 16 | APP: { 17 | // Here you can pass flags/options to your application instance 18 | // when it is created 19 | } 20 | }; 21 | 22 | if (environment === 'development') { 23 | // ENV.APP.LOG_RESOLVER = true; 24 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 25 | // ENV.APP.LOG_TRANSITIONS = true; 26 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 27 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 28 | } 29 | 30 | if (environment === 'test') { 31 | // Testem prefers this... 32 | ENV.baseURL = '/'; 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | } 41 | 42 | if (environment === 'production') { 43 | 44 | } 45 | 46 | return ENV; 47 | }; 48 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | var EmberApp = require('ember-cli/lib/broccoli/ember-app'); 3 | 4 | module.exports = function(defaults) { 5 | var app = new EmberApp(defaults, { 6 | // Add options here 7 | }); 8 | 9 | // Use `app.import` to add additional libraries to the generated 10 | // output files. 11 | // 12 | // If you need to use different assets in different 13 | // environments, specify an object as the first parameter. That 14 | // object's keys should be the environment name and the values 15 | // should be the asset to use in that environment. 16 | // 17 | // If the library that you are including contains AMD or ES6 18 | // modules that you would like to import into your application 19 | // please specify an object with the list of modules as keys 20 | // along with the exports of each module as its value. 21 | 22 | return app.toTree(); 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirage-tutorial", 3 | "version": "0.0.0", 4 | "description": "Small description for mirage-tutorial goes here", 5 | "private": true, 6 | "directories": { 7 | "doc": "doc", 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "build": "ember build", 12 | "start": "ember server", 13 | "test": "ember test" 14 | }, 15 | "repository": "", 16 | "engines": { 17 | "node": ">= 0.10.0" 18 | }, 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "broccoli-asset-rev": "^2.1.2", 23 | "ember-cli": "1.13.8", 24 | "ember-cli-app-version": "0.5.0", 25 | "ember-cli-babel": "^5.1.3", 26 | "ember-cli-content-security-policy": "0.4.0", 27 | "ember-cli-dependency-checker": "^1.0.1", 28 | "ember-cli-htmlbars": "0.7.9", 29 | "ember-cli-htmlbars-inline-precompile": "^0.2.0", 30 | "ember-cli-ic-ajax": "0.2.1", 31 | "ember-cli-inject-live-reload": "^1.3.1", 32 | "ember-cli-mirage": "0.1.11", 33 | "ember-cli-qunit": "^1.0.0", 34 | "ember-cli-release": "0.2.3", 35 | "ember-cli-sri": "^1.0.3", 36 | "ember-cli-uglify": "^1.2.0", 37 | "ember-data": "1.13.8", 38 | "ember-disable-proxy-controllers": "^1.0.0", 39 | "ember-export-application-global": "^1.0.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "test_page": "tests/index.html?hidepassed", 4 | "disable_watching": true, 5 | "launch_in_ci": [ 6 | "PhantomJS" 7 | ], 8 | "launch_in_dev": [ 9 | "PhantomJS", 10 | "Chrome" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "server", 4 | "document", 5 | "window", 6 | "location", 7 | "setTimeout", 8 | "$", 9 | "-Promise", 10 | "define", 11 | "console", 12 | "visit", 13 | "exists", 14 | "fillIn", 15 | "click", 16 | "keyEvent", 17 | "triggerEvent", 18 | "find", 19 | "findWithAssert", 20 | "wait", 21 | "DS", 22 | "andThen", 23 | "currentURL", 24 | "currentPath", 25 | "currentRouteName" 26 | ], 27 | "node": false, 28 | "browser": false, 29 | "boss": true, 30 | "curly": true, 31 | "debug": false, 32 | "devel": false, 33 | "eqeqeq": true, 34 | "evil": true, 35 | "forin": false, 36 | "immed": false, 37 | "laxbreak": false, 38 | "newcap": true, 39 | "noarg": true, 40 | "noempty": false, 41 | "nonew": false, 42 | "nomen": false, 43 | "onevar": false, 44 | "plusplus": false, 45 | "regexp": false, 46 | "undef": true, 47 | "sub": true, 48 | "strict": false, 49 | "white": false, 50 | "eqnull": true, 51 | "esnext": true, 52 | "unused": true 53 | } 54 | -------------------------------------------------------------------------------- /tests/acceptance/cars-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { module, test } from 'qunit'; 3 | import startApp from 'mirage-tutorial/tests/helpers/start-app'; 4 | 5 | module('Acceptance | cars', { 6 | beforeEach: function() { 7 | this.application = startApp(); 8 | }, 9 | 10 | afterEach: function() { 11 | Ember.run(this.application, 'destroy'); 12 | } 13 | }); 14 | 15 | test('visiting /cars', function(assert) { 16 | visit('/'); 17 | 18 | click('#all-cars'); 19 | 20 | andThen(() => { 21 | assert.equal(currentURL(), '/cars'); 22 | }); 23 | }); 24 | 25 | test('I see all cars on the index page', (assert) => { 26 | server.create('car'); 27 | visit('/cars'); 28 | 29 | andThen(() => { 30 | const cars = find('li.car'); 31 | assert.equal(cars.length, 1); 32 | }); 33 | }); 34 | 35 | test('I can add a new car', function(assert){ 36 | server.createList('car', 10); visit('/cars'); 37 | 38 | click('#add-car'); fillIn('input[name="car-name"]', 'My new car'); 39 | click('button'); 40 | 41 | andThen(() => { 42 | const newCar = find('li.car:contains("My new car")'); 43 | assert.equal(newCar.text().trim(), "My new car"); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/acceptance/parts-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { module, test } from 'qunit'; 3 | import startApp from 'mirage-tutorial/tests/helpers/start-app'; 4 | 5 | module('Acceptance | parts', { 6 | beforeEach: function() { 7 | this.application = startApp(); 8 | }, 9 | 10 | afterEach: function() { 11 | Ember.run(this.application, 'destroy'); 12 | } 13 | }); 14 | 15 | test('when I click a car, I see its parts', (assert) => { 16 | const car = server.create('car'); 17 | const parts = server.createList('part', 4, { car_id: car.id }); 18 | visit('/cars'); 19 | click('.car-link'); 20 | 21 | andThen(() => { 22 | assert.equal(currentURL(), `/car/${car.id}/parts`); 23 | assert.equal(find('.part').length, parts.length); 24 | }); 25 | }); 26 | 27 | test('I can add a new part to a car', (assert) => { 28 | server.create('car'); 29 | visit('/cars'); 30 | click('.car-link'); 31 | click('.new-part'); 32 | 33 | fillIn('input[name="part-name"]', "My new part"); 34 | click('button'); 35 | andThen(() => { 36 | assert.equal(find('.part').text().trim(), "My new part"); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/helpers/mirage-integration.js: -------------------------------------------------------------------------------- 1 | import mirageInitializer from '../../initializers/ember-cli-mirage'; 2 | 3 | export default function setupMirage(container) { 4 | mirageInitializer.initialize(container); 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember/resolver'; 2 | import config from '../../config/environment'; 3 | 4 | var resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | var application; 7 | 8 | var attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(function() { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MirageTutorial Tests 7 | 8 | 9 | {{content-for 'head'}} 10 | {{content-for 'test-head'}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for 'head-footer'}} 17 | {{content-for 'test-head-footer'}} 18 | 19 | 20 | 21 | {{content-for 'body'}} 22 | {{content-for 'test-body'}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{content-for 'body-footer'}} 30 | {{content-for 'test-body-footer'}} 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/integration/components/a-car-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import startMirage from '../../helpers/mirage-integration'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | moduleForComponent('a-car', 'Integration | Component | a car', { 6 | integration: true, 7 | setup() { 8 | startMirage(this.container); 9 | } 10 | }); 11 | 12 | test('it renders', function(assert) { 13 | const car = server.create('car'); 14 | this.set('car', car); 15 | this.render(hbs`{{a-car car=car}}`); 16 | 17 | assert.equal(this.$().text().trim(), 'Car 1'); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/unit/routes/car/new-part-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:car/new-part', 'Unit | Route | car/new part', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | var route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | ## Stubbing a JSON API spec API with ember-cli-mirage 2 | 3 | When developing a client side javascript app, you won’t always have an API available before you start. Even when you do, you probably don’t want to have your tests reliant on the the API end-points. 4 | 5 | Luckily, there is a great solution to stubbing out an API while building your Ember app; [Ember CLI Mirage](http://www.ember-cli-mirage.com/). Mirage works great when Ember Data is expecting a REST API, but there's some manual conversion that must be done if you want to consume JSON API [^1](http://jsonapi.org/), which I ran into recently on a project. 6 | 7 | In this tutorial we will leverage QUnit and Mirage's factories and API DSL to craft explicit acceptance tests as we build our application. 8 | 9 | I’m going to assume you have some basic knowledge of Ember for this. 10 | 11 |
12 | 13 | ### Setup 14 | ```bash 15 | $ ember new mirage-tutorial 16 | $ cd mirage-tutorial 17 | ``` 18 | 19 | Vim users who use [Vim Projectionist](https://github.com/tpope/vim-projectionist) can curl a set of projections from my Github repo. 20 | 21 | ```bash 22 | $ curl -G https://raw.githubusercontent.com/jsncmgs1/ember-vim-projections/ 23 | master/.projections.json -o .projections.json 24 | ``` 25 | 26 | We will use Ember/Ember Data 2.1.0 for this app, so let's update. 27 | 28 | In your bower.json file: 29 | 30 | change: 31 | "ember": "{your version}" to "ember": "2.1.0" 32 | 33 | and 34 | 35 | "ember-data": "{your version}" to "ember-data": "2.1.0" 36 | 37 | Then `nombom` with: 38 | 39 | ```bash 40 | $ npm cache clear && bower cache clean && rm -rf node_modules bower_components && npm install && bower install 41 | ``` 42 | 43 | Ember and Ember Data should be updated. To check, start your ember server and go to localhost:/4200 and you’ll see the “Welcome to Ember” page. Pull up the Ember 44 | inspector, and click the left sub-nav “Info” button. Ember and Ember-data should both be at 2.1.0. 45 | 46 | We will use the JSONAPI adapter, generate your adapter: 47 | 48 | ```bash 49 | $ ember g adapter application 50 | ``` 51 | In the adapter file, change RESTAdapter to JSONAPIAdapter. 52 | 53 | Now install mirage, then restart your server. 54 | 55 | ```bash 56 | $ ember install ember-cli-mirage 57 | ``` 58 | 59 | Mirage will create a mirage directory under app/. It contains a `config.js` file, a factories directory, and a scenarios directory. 60 | 61 | **Config file**: Mirage wraps Pretender, which intercepts requests that would normally hit your API, and allows you to specify the response that should be sent back. This file is where you specify your API end-points. 62 | 63 | Mirage gives you shorthand syntax for simple routes, but you can create manual routes when shorthand won’t work. [Mirage docs](http://www.ember-cli-mirage.com/docs/v0.1.x/defining-routes/) have a 64 | short and clear description of how to handle your routes. 65 | 66 | **Scenarios**: Mirage creates a `default.js` scenario for you. Inside the scenario you declare all the data you want to seed your development environment with. This data will not be in the test environment. 67 | 68 | **Factories**: Your mirage scenario will use the factories you define to generate your data, and you should use them in your tests as well. 69 | 70 | We will create a simple app that will list our cars and let us create new ones. Our cars also contain parts, which can also be created. While the API team builds their their end, we’ll get started on our end. 71 | 72 |
73 | ### Listing our cars 74 | Let's create a cars acceptance test. 75 | 76 | ```bash 77 | $ ember g acceptance-test cars 78 | ``` 79 | 80 | Ember generates a test for us at `tests/acceptance/cars-test.js`, with a generated test which checks to make sure our route functions. Let's change it to test a link to the cars index on the application template. When writing QUnit, you'll simulate all your user navigations ('click', 'visit', etc), which run asynchronously. Assertions are called in the andThen() callback, which will run after all the async operations are complete. 81 | [^2](http://coryforsyth.com/2014/07/10/demystifing-ember-async-testing/) 82 | 83 | ```javascript 84 | //app/tests/acceptance/cars.js 85 | test('visiting /cars', function(assert) { 86 | visit('/'); 87 | 88 | click('#all-cars'); 89 | 90 | andThen(() => { 91 | assert.equal(currentURL(), '/cars'); 92 | }); 93 | }); 94 | ``` 95 | 96 | Our tests run at localhost:4200/tests. When you go to that page, in the Module drop down in the upper right and corner, choose 'Acceptance | cars'. We will get an error because we don’t have the `#all-cars` link. 97 | 98 | Lets make our test pass. First, we need to create the link. 99 | 100 | ```html 101 | 102 |

Welcome to Ember

103 | {{link-to 'Cars' 'cars.index'}} 104 | 105 | {{outlet}} 106 | ``` 107 | 108 | Now QUnit tells us there's no `cars.index` route. 109 | 110 | ```bash 111 | $ ember g route cars 112 | ``` 113 | 114 | Ember will add the route for you in the `router.js` file. It adds the empty object, but we also need to pass an empty function so that an `cars/index` route is generated. Unfortunately, `this.route('cars', {})` would not create it. 115 | 116 | ```javascript 117 | //router.js 118 | Router.map(function() { 119 | this.route('cars', {}, function(){}); 120 | }); 121 | ``` 122 | 123 | Now check your test page, it passes. 124 | 125 | Lets test that when we go to the cars page, we will actually see some cars. At the bottom of your cars acceptance test: 126 | 127 | ```javascript 128 | //tests/acceptance/cars.js 129 | 130 | test('I see all cars on the index page', (assert) => { 131 | server.create('car'); 132 | visit('/cars'); 133 | 134 | andThen(() => { 135 | const cars = find('li.car'); 136 | assert.equal(cars.length, 1); 137 | }); 138 | }); 139 | 140 | ``` 141 | 142 | `server.create('car')` is telling Mirage to find a factory named 'car', create 1 of those cars, and put them in the Mirage database. When you run the test, it will die due to a Mirage error. I recommend running 143 | your tests with the Chrome debugger open so you can see the errors. 144 | 145 | Mirage will log an error saying it tried to find a ‘car’ factory, and it was not defined. Lets make one at `app/mirage/factories/car.js`. 146 | 147 | ```javascript 148 | // /app/mirage/factories/car.js 149 | import Mirage from 'ember-cli-mirage'; 150 | 151 | export default Mirage.Factory.extend({ 152 | name(i) { return `Car ${i + 1}`;} 153 | }); 154 | ``` 155 | This will create a car with a name attribute. This `(i)` syntax is used for Mirage sequences, the first name will be "Car 1", then "Car 2", etc. 156 | 157 | If we check our tests again, it will fail, finding 0 cars when expecting 1. To get the cars on the page, our `car/index` route will need to load the car model. 158 | 159 | Let’s create our car model. The Ember CLI generators are fantastic, but they will generate some tests that are not in the scope of this tutorial (unit tests). You can remove them, or ignore them for now. However, I wouldn't recommend leaving unused tests around. 160 | 161 | ```bash 162 | $ ember g model car 163 | ``` 164 | 165 | ```javascript 166 | // /app/models/car.js 167 | import DS from 'ember-data'; 168 | 169 | export default DS.Model.extend({ 170 | name: DS.attr('string') 171 | }); 172 | ``` 173 | 174 | And our route/template: 175 | ```bash 176 | $ ember g route cars/index 177 | ``` 178 | 179 | ```javascript 180 | // /app/routes/cars/index.js 181 | import Ember from 'ember'; 182 | 183 | export default Ember.Route.extend({ 184 | model(){ 185 | return this.store.findAll('car'); 186 | } 187 | }); 188 | ``` 189 | 190 | ```html 191 | 192 | 193 | 200 | 201 | ``` 202 | 203 | When we hit the model hook in our route, Ember Data sends out a `GET` request to `/cars`. If you let the test run, the test will seem frozen without the chrome debugger open. Mirage will log an error to the console saying there's no end point for `GET /cars`. 204 | 205 | Let’s create a route for Mirage so it can intercept this request. For the tutorial we will use the longer syntax, because Mirage doesn’t handle JSON API in the shorthand syntax - yet. When the json-api-serializer branch of Mirage gets merged (which should be soon), Mirage will be able to take care of a lot of the payload transforming itself. 206 | 207 | JSON API expects a response with a top level key named 'data', which contains an array of the resources returned. Each resource should have a specified type, 208 | the id of the resource, and the resource attributes. When Mirage responds to a request, it will log the response object in the console for inspection. The object should look like this: 209 | 210 | ```javascript 211 | data: { 212 | [ 213 | { 214 | attributes: { 215 | id: 1, 216 | name: 'Car 1' 217 | }, 218 | id: 1, 219 | type: 'cars' 220 | }, 221 | { 222 | attributes: { 223 | id: 2, 224 | name: 'Car 2' 225 | }, 226 | id: 2, 227 | type: 'cars' 228 | }, 229 | //.... 230 | ] 231 | } 232 | 233 | ``` 234 | 235 | There are other keys as well, such as errors, and relationships. We will expand on relations further in the tutorial. 236 | 237 | ```javascript 238 | // /app/mirage/config.js 239 | 240 | export default function() { 241 | this.get('/cars', (db, request) => { 242 | let data = {}; 243 | data = db.cars.map((attrs) => { 244 | let rec = {type: 'cars', id: attrs.id, attributes: attrs}; 245 | return rec; 246 | }); 247 | 248 | return { data }; 249 | }); 250 | }; 251 | ``` 252 | 253 | When we run our tests again, they pass. If you’d like to see it work in development, generate some cars in `scenarios/default.js`, and go to `localhost:4200/cars`. 254 | 255 | ```javascript 256 | // /app/mirage/scenarios/default.js 257 | export default function(server) { 258 | 259 | // Seed your development database using your factories. This data will not be loaded in your tests. 260 | server.createList('car', 10); 261 | } 262 | ``` 263 | 264 | Whats going on here? 265 | 266 | When we visit the cars route, ember sends us to the cars/index route. The route fires the model hook, where ember data sends out a `GET` request for all of the cars. The mirage route in `mirage/config.js` intercepts the request, gets the cars that we generated in the test, adds them to a JSON API formatted object, and sends it back as the response. No api needed! 267 | 268 | Now that we have a working acceptance test, lets create a car component for our cars to live in. 269 | 270 | ```bash 271 | ember g component a-car 272 | ``` 273 | 274 | Ember created a component integration test, which we'll use. It's easy to setup Mirage for an integration tests. Under `tests/helpers/`, create a file called `mirage-integration.js` 275 | 276 | ```javascript 277 | //tests/helpers/mirage-integration.js 278 | import mirageInitializer from '../../initializers/ember-cli-mirage'; 279 | 280 | export default function setupMirage(container) { 281 | mirageInitializer.initialize(container); 282 | } 283 | ``` 284 | 285 | and in your component test, import the setupMirage function, you will invoke in the moduleForComponent setup hook, passing in this.container. 286 | 287 | ```javascript 288 | //app/tests/integration/components/a-car-test.js 289 | 290 | import { moduleForComponent, test } from 'ember-qunit'; 291 | import setupMirage from '../../helpers/mirage-integration'; 292 | import hbs from 'htmlbars-inline-precompile'; 293 | 294 | moduleForComponent('a-car', 'Integration | Component | a car', { 295 | integration: true, 296 | setup() { 297 | setupMirage(this.container); 298 | } 299 | }); 300 | 301 | test('it renders', function(assert) { 302 | const car = server.create('car'); 303 | this.set('car', car); 304 | this.render(hbs`{{a-car car=car}}`); 305 | 306 | assert.equal(this.$().text().trim(), 'Car 1'); 307 | }); 308 | ``` 309 | 310 | In this test, we create a car, and a component (`this`) and set it on the component. Then we can actually render the template, and assert what the components text should be. Of course we haven't done anything with our component 311 | yet, so the test fails. 312 | 313 | In our `cars/index` template, we're rendering our component inside of an li, with a class of 'car'. Add those attributes to the component. 314 | 315 | ```javascript 316 | import Ember from 'ember'; 317 | 318 | export default Ember.Component.extend({ 319 | tagName: 'li', 320 | classNames: ['car'] 321 | }); 322 | ``` 323 | 324 | Move the `{{car.name}}` expression into the component template, and render the component in the each loop, passing the model into the component. 325 | ```html 326 | 327 | 328 | {{car.name}} 329 | ``` 330 | 331 | ```html 332 | 333 | Cars/Index 334 | 335 | 340 | ``` 341 | 342 | Run the tests, they should pass. 343 | 344 |
345 | ### Adding New Cars 346 | Now that our cars index is tested and working, we need to be able to add more cars to our collection. Let's make a test. 347 | 348 | ```javascript 349 | //tests/acceptance/cars-test.js 350 | test('I can add a new car', function(assert){ 351 | server.createList('car', 10); visit('/cars'); 352 | 353 | click('#add-car'); fillIn('input[name="car-name"]', 'My new car'); 354 | click('button'); 355 | 356 | andThen(() => { 357 | const newCar = find('li.car:contains("My new car")'); 358 | assert.equal(newCar.text().trim(), "My new car"); 359 | }); 360 | }); 361 | ``` 362 | 363 | Our test fails because there's no link with an id of add-car. This link should take us to the cars.new route. In your cars/index template at the bottom of the file, add: 364 | 365 | ```html 366 | 367 | 368 | 369 | {{#link-to 'cars.new' id='add-car'}} 370 | Add new car 371 | {{/link-to}} 372 | ``` 373 | 374 | Now our test fails because we don't have the specified input field. We'll need the cars/new template, we also know that we will need that route. Generating the route will create both for us, as well as adding the route to our router. 375 | 376 | ```bash 377 | ember g route cars/new 378 | ``` 379 | 380 | The router should now look like: 381 | 382 | ```javascript 383 | //router.js 384 | import Ember from 'ember'; 385 | import config from './config/environment'; 386 | 387 | var Router = Ember.Router.extend({ 388 | location: config.locationType 389 | }); 390 | 391 | 392 | Router.map(function() { 393 | this.route('cars', function() { 394 | this.route('new', {}); 395 | }); 396 | }); 397 | 398 | export default Router; 399 | ``` 400 | 401 | Add the form for creating a car to our cars/new template: 402 | 403 | ```html 404 | 405 | New Car 406 | 407 |
408 | {{input name='car-name' value=name}} 409 | 410 |
411 | ``` 412 | 413 | We know we'll need an action to handle the creation of the car, so we'll go ahead and declare that now. Our test will fail because there's nothing to handle the action named createCar yet. My preference is to handle anything related to data in the route when I can, so we'll do that. 414 | 415 | ```javascript 416 | // /app/routes/cars/new.hbs 417 | import Ember from 'ember'; 418 | export default Ember.Route.extend({ 419 | actions: { 420 | createCar(name){ 421 | const car = this.store.createRecord('car', { name }); 422 | 423 | car.save() 424 | .then(() => { 425 | this.transitionTo('cars'); 426 | }).catch(() => { 427 | // something that handles failures 428 | }); 429 | } 430 | } 431 | 432 | }); 433 | ``` 434 | 435 | Now our Ember pieces are hooked up, but the test fails because mirage doesn't see a route that specifies a `POST` request to `/cars`. Add it to the Mirage config file. 436 | 437 | ```javascript 438 | // /app/mirage/config.js 439 | 440 | export default function() { 441 | //... 442 | 443 | this.post('/cars', (db, request) => { 444 | return JSON.parse(request.requestBody); 445 | }); 446 | }; 447 | ``` 448 | 449 | Our JSONAPIAdapter sends the serialized data in the correct format, so all we have to do is parse it, and return it. 450 | 451 | And with that our test should pass. 452 | 453 |
454 | ### Viewing Parts 455 | 456 | I mentioned earlier that our cars contain parts. We'll make it so that when we click our car, we will be taken to that that car's parts page. Let's generate a test for parts. 457 | 458 | ```bash 459 | $ ember g acceptance-test parts 460 | ``` 461 | 462 | Delete the generated test and add the following. 463 | 464 | ```javascript 465 | //tests/acceptance/parts.js 466 | 467 | test('when I click a car, I see its parts', (assert) => { 468 | const car = server.create('car'); 469 | const parts = server.createList('part', 4, { car_id: car.id }); 470 | visit('/cars'); 471 | click('.car-link'); 472 | 473 | andThen(() => { 474 | assert.equal(currentURL(), `/car/${car.id}/parts`); 475 | assert.equal(find('.part').length, parts.length); 476 | }); 477 | }); 478 | ``` 479 | 480 | Our first breakage occurs because Mirage has no part factory. 481 | 482 | ```javascript 483 | //mirage/factories/part.js 484 | 485 | import Mirage from 'ember-cli-mirage'; 486 | 487 | export default Mirage.Factory.extend({ 488 | name(i) { return `Part ${i}`; } 489 | }); 490 | ``` 491 | 492 | Now QUnit yells because we have no links. Turn our list of cars into links, so that when we click on one, we can see that car's parts. 493 | 494 | ```html 495 | 496 | {{#link-to 'car.parts' car class='car-link'}} 497 | {{car.name}} 498 | {{/link-to}} 499 | ``` 500 | 501 | QUnit shames us for not having a car.parts route. 502 | 503 | ```bash 504 | $ ember g route car/parts 505 | ``` 506 | 507 | The router should look like: 508 | ```javascript 509 | //router.js 510 | import Ember from 'ember'; 511 | import config from './config/environment'; 512 | 513 | var Router = Ember.Router.extend({ 514 | location: config.locationType 515 | }); 516 | 517 | Router.map(function() { 518 | this.route('cars', function() { 519 | this.route('new', {}); 520 | }); 521 | 522 | this.route('car', function(){ 523 | this.route('parts', {}); 524 | }); 525 | }); 526 | 527 | export default Router; 528 | ``` 529 | 530 | We'll add a dynamic segment of id to the car path. 531 | 532 | ```javascript 533 | //... 534 | this.route('car', { path: '/car/:id'}, function(){ 535 | this.route('parts'); 536 | }); 537 | //... 538 | }); 539 | 540 | export default Router; 541 | ``` 542 | 543 | Since our route is nested, we need to specify the model for the parent route. 544 | 545 | ```bash 546 | $ ember g route car 547 | 548 | ``` 549 | 550 | In the car route, return the car specified by the id dynamic segment. 551 | 552 | ```javascript 553 | //routes/car.js 554 | import Ember from 'ember'; 555 | 556 | export default Ember.Route.extend({ 557 | model(params){ 558 | return this.store.find('car', params.id); 559 | } 560 | }); 561 | ``` 562 | 563 | We also have to create a Mirage route to GET a single car. At this point in the app, we have had our cars loaded from visiting the index, but a user could go straight to a `car/:id` url, so we need to handle that. 564 | JSON API requires relationship information to be stored in a 'relationships' object. Add it to your mirage config file. 565 | 566 | ```javascript 567 | //mirage/config.js 568 | export default function() { 569 | //... 570 | 571 | this.get('/cars/:id', (db, request) => { 572 | let car = db.cars.find(request.params.id); 573 | let parts = db.parts.where({car_id: car.id}); 574 | 575 | let data = { 576 | type: 'car', 577 | id: request.params.id, 578 | attributes: car, 579 | relationships: { 580 | parts:{ 581 | data:{} 582 | } 583 | } 584 | } 585 | 586 | data.relationships.parts.data = parts.map((attrs) => { 587 | return { type: 'parts', id: attrs.id, attributes: attrs }; 588 | }); 589 | 590 | return { data }; 591 | }); 592 | 593 | } 594 | 595 | ``` 596 | 597 | Additionally, in our Mirage '/cars' route, we are only 598 | returning the car information, not the associated parts. What this means is, if the first page we visit is the '/cars' page, those cars will already be loaded in the store (with no knowledge of any associated parts). 599 | When we go to the cars/part page, the store won't fetch the model, because it's already in the store, so there will be no parts available to render. We should load a cars parts in the cars/index route. 600 | 601 | ```javascript 602 | //mirage/config.js 603 | export default function() { 604 | this.get('/cars', (db, request) => { 605 | let data = {}; 606 | data = db.cars.map((attrs) => { 607 | 608 | let car = { 609 | type: 'cars', 610 | id: attrs.id, 611 | attributes: attrs , 612 | relationships: { 613 | parts: { 614 | data: {} 615 | } 616 | }, 617 | }; 618 | 619 | car.relationships.parts.data = db.parts 620 | .where({car_id: attrs.id}) 621 | .map((attrs) => { 622 | return { 623 | type: 'parts', 624 | id: attrs.id, 625 | attributes: attrs 626 | }; 627 | }); 628 | 629 | return car; 630 | 631 | }); 632 | return { data }; 633 | }); 634 | //.... 635 | ``` 636 | 637 | We also need the Mirage end-points for getting a part. 638 | ```javascript 639 | //mirage/config.js 640 | export default function() { 641 | //... 642 | this.get('parts/:id', (db, request) => { 643 | let part = db.parts.find(request.params.id); 644 | 645 | let data = { 646 | type: 'parts', 647 | id: request.params.id, 648 | attributes: part, 649 | }; 650 | 651 | return { data }; 652 | }); 653 | //... 654 | ``` 655 | 656 | Now we need a part model, and a factory. 657 | 658 | ```javascript 659 | //models/part.js 660 | import DS from 'ember-data'; 661 | 662 | export default DS.Model.extend({ 663 | name: DS.attr('string'), 664 | car: DS.belongsTo('car') 665 | }); 666 | ``` 667 | 668 | ```javascript 669 | import Mirage from 'ember-cli-mirage'; 670 | 671 | export default Mirage.Factory.extend({ 672 | name(i) { return `Part ${i}`; } 673 | }); 674 | ``` 675 | 676 | And update our car model to show the association. 677 | 678 | ```javascript 679 | //models/car.js 680 | import DS from 'ember-data'; 681 | 682 | export default DS.Model.extend({ 683 | name: DS.attr('string'), 684 | parts: DS.hasMany('part') 685 | }); 686 | ``` 687 | 688 | And our template: 689 | ```html 690 | 691 | 692 | Parts 693 | 701 | ``` 702 | And now our test should be green. 703 | 704 | I'll leave converting the part into a component with an integration test as an exercise for you to complete. The steps are the same as they were for cars. 705 | 706 |
707 | ### Adding Parts 708 | Our last test will cover adding parts. At the bottom of your parts acceptance test: 709 | 710 | ```javascript 711 | //tests/acceptance/parts.js 712 | 713 | test('I can add a new part to a car', (assert) => { 714 | server.create('car'); 715 | visit('/cars'); 716 | click('.car-link'); 717 | click('.new-part'); 718 | 719 | fillIn('input[name="part-name"]', "My new part"); 720 | click('button'); 721 | andThen(() => { 722 | assert.equal(find('.part').text().trim(), "My new part"); 723 | }); 724 | }); 725 | ``` 726 | 727 | Our test tells us we don't have a '.new-part' link. in our template: 728 | ```html 729 | 730 | Parts 731 | 732 | 740 | 741 | {{#link-to 'car.new-part' model class='new-part'}} 742 | Add new Part 743 | {{/link-to}} 744 | ``` 745 | 746 | ```bash 747 | $ ember g route car/new-part 748 | ``` 749 | Now we need a 'car.new-part' route. 750 | ```javascript 751 | //router.js 752 | Router.map(function() { 753 | this.route('cars', function() { 754 | this.route('new', {}); 755 | }); 756 | 757 | this.route('car', { path: '/car/:id' }, function(){ 758 | this.route('parts', {}); 759 | this.route('new-part', {}); 760 | }); 761 | }); 762 | ``` 763 | 764 | And a template for our route to render. 765 | ```html 766 | 767 | New Part 768 | 769 |
770 | {{input name='part-name' value=name}} 771 | 772 |
773 | ``` 774 | 775 | ```javascript 776 | //routes/car/new-part.js 777 | import Ember from 'ember'; 778 | 779 | export default Ember.Route.extend({ 780 | actions: { 781 | newPart(name){ 782 | const car = this.modelFor('car'); 783 | const part = this.store.createRecord('part', { name, car }); 784 | part.save().then(() => { 785 | this.transitionTo('car.parts', car); 786 | }); 787 | } 788 | } 789 | }); 790 | ``` 791 | And a Mirage endpoint: 792 | 793 | ```javascript 794 | this.post('parts', (db, request) => { 795 | return JSON.parse(request.requestBody); 796 | }); 797 | ``` 798 | 799 | And we're done! This process will be even easier once Mirage supports JSON API, which is on its way. You can view the source at https://github.com/jsncmgs1/mirage-tutorial.git. 800 |
801 | 1. [Demystifying Ember Async Testing](http://coryforsyth.com/2012/07/10/demystifing-ember-async-testing/) 802 | 2. [JSON API](http://jsonapi.org/) 803 | 3. [Ember CLI Mirage](http://www.ember-cli-mirage.com/) 804 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsncmgs1/mirage-tutorial/1d536b9bf54bbf1e108c3b5a0bfd3f3f07892fe8/vendor/.gitkeep --------------------------------------------------------------------------------