Skip to content

Commit

Permalink
Setup Mirage in test and development environments
Browse files Browse the repository at this point in the history
Autoload mirage modules based on directory structure
  • Loading branch information
cherewaty authored and Joe LaSala committed Jul 27, 2017
1 parent 9a87238 commit 6f61d04
Show file tree
Hide file tree
Showing 17 changed files with 401 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": [["env", { "modules": false }], "react"],
"presets": [["env", { "modules": false }], "stage-3", "react"],
"plugins": ["react-hot-loader/babel"]
}
13 changes: 13 additions & 0 deletions mirage/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Mirage from 'mirage-server';

export default function () {
this.namespace = 'eholdings';

this.get('/vendors', ({ vendors }, request) => {
let filteredVendors = vendors.all().filter((vendorModel) => {
return vendorModel.vendorName.includes(request.queryParams.search);
});

return filteredVendors;
});
}
10 changes: 10 additions & 0 deletions mirage/factories/vendor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Factory, faker } from 'mirage-server';

export default Factory.extend({
vendorId: () => faker.random.number(),
vendorName: () => faker.company.companyName(),
vendorToken: null,
isCustomer: false,
packagesTotal: 1,
packagesSelected: 0,
});
7 changes: 7 additions & 0 deletions mirage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
let start = () => {};

if (process.env.NODE_ENV !== 'production') {
start = require('./start').startDevMirage;
}

export default start;
3 changes: 3 additions & 0 deletions mirage/models/vendor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Model } from 'mirage-server';

export default Model.extend();
3 changes: 3 additions & 0 deletions mirage/scenarios/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function(server) {
server.createList('vendor', 3);
}
9 changes: 9 additions & 0 deletions mirage/serializers/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Serializer } from 'mirage-server';

export default Serializer.extend({
serialize(response, request) {
let json = Serializer.prototype.serialize.apply(this, arguments);
json.totalResults = json[this.keyForResource(response)].length;
return json;
}
});
37 changes: 37 additions & 0 deletions mirage/start.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Mirage from 'mirage-server';
import baseConfig from './config';
import camelCase from 'lodash/camelCase';
import '../tests/force-fetch-polyfill';

const moduleTypes = ['factories', 'fixtures', 'scenarios', 'models', 'serializers', 'identity-managers'];

const req = require.context('./', true, /\.js$/);

let modules = moduleTypes.reduce((memo, name) => {
memo[camelCase(name)] = {};
return memo;
}, {});

req.keys().forEach((modulePath) => {
const moduleParts = modulePath.split('/');
const moduleType = moduleParts[1];
const moduleName = moduleParts[2];

if (moduleName) {
const moduleKey = camelCase(moduleName.replace('.js', ''));
modules[moduleType][moduleKey] = req(modulePath).default;
}
});

export function startDevMirage() {
let options = Object.assign(modules, { baseConfig });
return new Mirage(options);
}

export function startTestMirage() {
let testModules = { ...modules };
delete modules.scenarios;

let options = Object.assign(testModules, { baseConfig });
return new Mirage(options);
}
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"babel-loader": "^7.1.1",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-3": "^6.24.1",
"chai": "^4.0.2",
"chai-jquery": "^2.0.0",
"eslint": "^4.3.0",
Expand All @@ -26,13 +27,21 @@
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.3",
"karma-webpack": "^2.0.3",
"lodash": "^4.17.4",
"mirage-server": "cowboyd/mirage-server",
"mocha": "^3.4.2",
"react": "^15.6.1",
"react-addons-test-utils": "^15.6.0",
"react-dom": "^15.6.1",
"react-hot-loader": "next",
"react-test-renderer": "^15.6.1",
"react-trigger-change": "^1.0.2",
"sinon": "^2.4.1",
"sinon-chai": "^2.12.0",
"webpack": "^3.0.0",
"webpack-dev-server": "^2.5.0"
},
"dependencies": {
"isomorphic-fetch": "^2.2.1"
}
}
68 changes: 62 additions & 6 deletions src/components/app.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,65 @@
import React from 'react';
import React, { Component } from 'react';
import fetch from 'isomorphic-fetch';

const App = () => (
<h1>Folio Resource Management</h1>
)
export default class App extends Component {
constructor() {
super(...arguments);

App.displayName = 'App';
this.state = {
search: ''
};
}

export default App;
render () {
let {
search,
searchResults
} = this.state;

return (
<div>
<h1>Folio Resource Management</h1>
<form onSubmit={this.searchSubmit}>
<input
type="search"
name="search"
value={search}
placeholder="Search"
data-test-search-field
onChange={this.handleChange} />
<button data-test-search-submit type="submit" disabled={!search}>Search</button>
</form>
<ul data-test-search-results-list>
{!!searchResults && searchResults.vendors.map((vendor) => (
<li data-test-search-results-item key={vendor.vendorId}>
{vendor.vendorName}
</li>
))}
</ul>
</div>
);
}

get searchSubmit() {
return (e) => {
let { search } = this.state;

e.preventDefault();
fetch(`/eholdings/vendors?search=${search}`)
.then((res) => res.json())
.then((data) => {
this.setState({
searchResults: data
});
});
}
}

get handleChange() {
return (e) => {
this.setState({
search: e.target.value
});
};
}
}
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import startMirage from '../mirage';

import App from './components/app';

Expand All @@ -18,4 +19,8 @@ if (module.hot) {
module.hot.accept('./components/app', render);
}

if (process.env.NODE_ENV === 'development') {
window.mirage = startMirage();
}

render();
50 changes: 43 additions & 7 deletions tests/app-test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,57 @@
/* global it */
/* global describe, beforeEach */
import { expect } from 'chai';
import $ from 'jquery';
import sinon from 'sinon';
import it from './it-will';

import { describeApplication } from './helpers';
import {
describeApplication,
triggerChange
} from './helpers';

describeApplication('eHoldings', function() {
beforeEach(function() {
this.server.create('vendor', { vendorName: 'Vendor1' });
this.server.create('vendor', { vendorName: 'Vendor2' });
this.server.create('vendor', { vendorName: 'Vendor3' });
});

it('should render the app', function() {
expect($('h1')).to.have.text('Folio Resource Management');
});

it('has a searchbox with options to search for vendor, package and title');
it('has a searchbox with options to search for vendor, package and title', function() {
expect($('[data-test-search-field]')).to.exist;
});

describe("searching for a vendor", function() {
beforeEach(function() {
let $input = $('[data-test-search-field]').val('Vendor');
triggerChange($input.get(0));

$('[data-test-search-submit]').trigger('click');
});

it("displays vendor entries related to 'Vendor'", function() {
expect($('[data-test-search-results-item]')).to.have.lengthOf(3);
});

describe("searching for the vendor 'ebsco'", function() {
it("displays vendor entries related to 'ebsco'");
it("displays the name, number of packages available, and packages subscribed to for each vendor");

describe("clicking on first result", function() {
describe("filtering the search results further", function() {
beforeEach(function() {
let $input = $('[data-test-search-field]').val('Vendor1');
triggerChange($input.get(0));

$('[data-test-search-submit]').trigger('click');
});

it("only shows a single result", function() {
expect($('[data-test-search-results-item]')).to.have.lengthOf(1);
});
});

describe("clicking on a result", function() {
it("shows vendor details");
it("shows packages for vendor");
});
Expand All @@ -30,6 +66,6 @@ describeApplication('eHoldings', function() {
});

describe("encountering a server error", function() {
it("dies with grace");
it("dies with dignity");
});
});
8 changes: 8 additions & 0 deletions tests/force-fetch-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This will force fetch to be polyfilled with XHR for pretender.
//
// We do this inside of another imported file becuase it needs to happen
// before the fetch is first imported. Imports are hoisted to the top so
// settings this explicity before importing the app still won't work; it
// needs to be inside another import that happens before the app import.
window.fetch = undefined;

11 changes: 11 additions & 0 deletions tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
import React from 'react';
import chai from 'chai';
import chaiJquery from 'chai-jquery';
import chaiSinon from 'sinon-chai';
import $ from 'jquery';
import { render, unmountComponentAtNode } from 'react-dom';
import { startTestMirage } from '../mirage/start';

import App from '../src/components/app';

// use jquery matchers
chai.use((chai, utils) => chaiJquery(chai, utils, $));

// use sinon matchers
chai.use(chaiSinon);

// helper to trigger native change events for react elements
export { default as triggerChange } from 'react-trigger-change';

/*
* TODO: FIX THIS DESCRIPTION
* Sets up the entire Folio application, mounts it with enzyme, and tears it down
Expand Down Expand Up @@ -38,9 +46,12 @@ export function describeApplication(name, setup) {
document.body.appendChild(rootElement);

this.app = render(<App/>, rootElement);
this.server = startTestMirage();
this.server.logging = false;
});

afterEach(function() {
this.server.shutdown();
unmountComponentAtNode(rootElement);
document.body.removeChild(rootElement);
rootElement = null;
Expand Down
58 changes: 58 additions & 0 deletions tests/it-will.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const originalIt = window.it;

// This turns every call of `it` into a "convergent assertion." The
// assertion is run every 10ms until it is either true, or it times
// out. This makes it incredibly robust in the face of asynchronous
// operations which could happen instantly, or they could happen after
// 1.5 seconds. The assertion doesn't care until it's reflected
// in the UI.
//
// The only caveat is that all assertions should be "pure" that is to
// say, completely without side-effects.
//
// good:
// it('has some state', function() {
// expect(thing).to.be('awesome');
// });
//
// bad:
// it('twiddles when clicked', function() {
// click('.a-button');
// expect(thing).to.be('set');
// });
export default function itWill(...args) {
if (args.length <= 1) {
return originalIt(...args);
} else {
let [name, assertion] = args;

return originalIt(name, function() {
let timeout = this.timeout();
let interval = 10;
let start = new Date().getTime();
let error = null;
let test = this;

return new Promise(function(resolve, reject) {
(function loop() {
try {
let res = assertion.call(test);
if (res && typeof res.then === 'function') {
res.then(resolve);
} else {
resolve();
}
} catch(e) {
error = e;
let now = new Date().getTime();
if (now - start + interval >= timeout) {
reject(e);
} else {
setTimeout(loop, interval);
}
}
})();
});
});
}
}
Loading

0 comments on commit 6f61d04

Please sign in to comment.