2 | {{#each model as |car|}}
3 | {{a-car car=car}}
4 | {{/each}}
5 |
6 |
7 | {{#link-to 'cars.new' id='add-car'}}
8 | Add new car
9 | {{/link-to}}
10 |
--------------------------------------------------------------------------------
/app/templates/cars/new.hbs:
--------------------------------------------------------------------------------
1 |
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 |
194 | {{#each model as |car|}}
195 |
196 | {{car.name}}
197 |
198 | {{/each}}
199 |
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 |
336 | {{#each model as |car|}}
337 | {{a-car car=car}}
338 | {{/each}}
339 |
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 |
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 |