diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..f0e13c509
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+6.7.0
diff --git a/package.json b/package.json
index 1ea514e1f..e980a72b2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,10 @@
{
"name": "mastodon",
+ "scripts": {
+ "test": "mocha --require ./spec/javascript/setup.js --compilers js:babel-register ./spec/javascript/components/*.test.jsx"
+ },
"devDependencies": {
+ "axios": "^0.14.0",
"babel-plugin-react-transform": "^2.0.2",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-preset-es2015": "^6.13.2",
@@ -8,17 +12,21 @@
"babelify": "^7.3.0",
"browserify": "^13.1.0",
"browserify-incremental": "^3.1.1",
- "react": "^15.3.0",
- "react-dom": "^15.3.0",
- "react-proxy": "^1.1.8",
- "axios": "^0.14.0",
+ "chai": "^3.5.0",
+ "enzyme": "^2.4.1",
"es6-promise": "^3.2.1",
"immutable": "^3.8.1",
+ "jsdom": "^9.6.0",
+ "mocha": "^3.1.1",
"moment": "^2.14.1",
+ "react": "^15.3.2",
"react-addons-perf": "^15.3.2",
"react-addons-pure-render-mixin": "^15.3.1",
+ "react-addons-test-utils": "^15.3.2",
+ "react-dom": "^15.3.0",
"react-immutable-proptypes": "^2.1.0",
"react-notification": "^6.1.1",
+ "react-proxy": "^1.1.8",
"react-redux": "^5.0.0-beta.3",
"react-redux-loading-bar": "^2.3.3",
"react-router": "^2.8.0",
@@ -26,6 +34,7 @@
"redux": "^3.5.2",
"redux-immutable": "^3.0.8",
"redux-thunk": "^2.1.0",
- "reselect": "^2.5.4"
+ "reselect": "^2.5.4",
+ "sinon": "^1.17.6"
}
}
diff --git a/spec/javascript/components/avatar.test.jsx b/spec/javascript/components/avatar.test.jsx
new file mode 100644
index 000000000..79b7d02f4
--- /dev/null
+++ b/spec/javascript/components/avatar.test.jsx
@@ -0,0 +1,12 @@
+import { expect } from 'chai';
+import { render } from 'enzyme';
+
+import Avatar from '../../../app/assets/javascripts/components/components/avatar'
+
+describe('', () => {
+ it('renders an img with the given src', () => {
+ const src = '/path/to/image.jpg';
+ const wrapper = render();
+ expect(wrapper.find(`img[src="${src}"]`)).to.have.length(1);
+ });
+});
diff --git a/spec/javascript/components/button.test.jsx b/spec/javascript/components/button.test.jsx
new file mode 100644
index 000000000..0f16ebe8e
--- /dev/null
+++ b/spec/javascript/components/button.test.jsx
@@ -0,0 +1,14 @@
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+
+import Button from '../../../app/assets/javascripts/components/components/button'
+
+describe('', () => {
+ it('simulates click events', () => {
+ const onClick = sinon.spy();
+ const wrapper = shallow();
+ wrapper.find('button').simulate('click');
+ expect(onClick.calledOnce).to.equal(true);
+ });
+});
diff --git a/spec/javascript/components/loading_indicator.test.jsx b/spec/javascript/components/loading_indicator.test.jsx
new file mode 100644
index 000000000..7039dbfbd
--- /dev/null
+++ b/spec/javascript/components/loading_indicator.test.jsx
@@ -0,0 +1,11 @@
+import { expect } from 'chai';
+import { shallow } from 'enzyme';
+
+import LoadingIndicator from '../../../app/assets/javascripts/components/components/loading_indicator'
+
+describe('', () => {
+ it('renders text that indicates loading', () => {
+ const wrapper = shallow();
+ expect(wrapper.text()).to.match(/loading/i);
+ });
+});
diff --git a/spec/javascript/setup.js b/spec/javascript/setup.js
new file mode 100644
index 000000000..636cdcc7e
--- /dev/null
+++ b/spec/javascript/setup.js
@@ -0,0 +1,22 @@
+/**
+ * http://airbnb.io/enzyme/docs/guides/jsdom.html
+ */
+var jsdom = require('jsdom').jsdom;
+
+var exposedProperties = ['window', 'navigator', 'document'];
+
+global.document = jsdom('');
+global.window = document.defaultView;
+Object.keys(document.defaultView).forEach((property) => {
+ if (typeof global[property] === 'undefined') {
+ exposedProperties.push(property);
+ global[property] = document.defaultView[property];
+ }
+});
+
+global.navigator = {
+ userAgent: 'node.js'
+};
+
+var React = window.React = global.React = require('react');
+var ReactDOM = window.ReactDOM = global.ReactDOM = require('react-dom');