diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e68d2fe --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "stage-2", "react"] +} diff --git a/.gitignore b/.gitignore index dd9400d..acede02 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ npm-debug.log .idea/ node_modules/ +coverage/ build/ diff --git a/package.json b/package.json index 52e9fd3..9c123a0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "clean-test": "rm -rf build/test/", "lint": "eslint . --debug --ignore-path .gitignore && csscomb -l -v client/", "fix": "eslint . --fix --ignore-path .gitignore && csscomb client/", - "unit": "npm-run-all clean-test build:test && mocha --recursive build/test/", + "unit": "NODE_PATH=. mocha --compilers js:babel-register --recursive", + "coverage": "NODE_PATH=. istanbul cover _mocha -- --compilers js:babel-register --reporter dot --recursive", "test": "npm-run-all lint unit", "copy-public": "mkdir -p build/ && cp -r client/public/ build/public/", "build:test": "webpack --config webpack.test.config.js --progress", @@ -31,6 +32,7 @@ "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "babel-preset-stage-2": "^6.3.13", + "babel-register": "^6.5.2", "body-parser": "^1.14.2", "chai": "^3.5.0", "connect-redis": "^3.0.1", @@ -48,6 +50,7 @@ "hiredis": "^0.4.1", "history": "^1.17.0", "image-webpack-loader": "^1.6.2", + "istanbul": "^v1.0.0-alpha", "json-loader": "^0.5.4", "mocha": "^2.4.5", "mongodb": "^2.1.4", diff --git a/test/client/components/page/page-type-todos-example.test.js b/test/client/components/page/page-type-todos-example.test.js index 0b1c87e..fc8cb85 100644 --- a/test/client/components/page/page-type-todos-example.test.js +++ b/test/client/components/page/page-type-todos-example.test.js @@ -5,11 +5,16 @@ import ComponentMock from 'test/mocks/component'; import PageTypeTodosExampleModel from 'client/components/page/mods/type/todos-example/model'; let component = new ComponentMock(); -let model = new PageTypeTodosExampleModel(component); let sandbox = sinon.sandbox.create(); +let model; describe('PageTypeTodosExample', () => { - afterEach(() => sandbox.restore()); + beforeEach(() => model = new PageTypeTodosExampleModel(component)); + + afterEach(() => { + model.destroy(); + sandbox.restore(); + }); describe('#load()', () => { it('should set correct state after loading', () => { diff --git a/test/client/components/page/page-type-welcome.test.js b/test/client/components/page/page-type-welcome.test.js index ed2a1e2..afa486f 100644 --- a/test/client/components/page/page-type-welcome.test.js +++ b/test/client/components/page/page-type-welcome.test.js @@ -5,11 +5,16 @@ import ComponentMock from 'test/mocks/component'; import PageTypeWelcomeModel from 'client/components/page/mods/type/welcome/model'; let component = new ComponentMock(); -let model = new PageTypeWelcomeModel(component); let sandbox = sinon.sandbox.create(); +let model; describe('PageTypeWelcome', () => { - afterEach(() => sandbox.restore()); + beforeEach(() => model = new PageTypeWelcomeModel(component)); + + afterEach(() => { + model.destroy(); + sandbox.restore(); + }); describe('#load()', () => { it('should set correct state after loading', () => { diff --git a/test/client/components/todos/todos-footer.test.js b/test/client/components/todos/todos-footer.test.js index acd4ff3..dafb8d0 100644 --- a/test/client/components/todos/todos-footer.test.js +++ b/test/client/components/todos/todos-footer.test.js @@ -5,9 +5,13 @@ import ComponentMock from 'test/mocks/component'; import TodosFooterModel from 'client/components/todos/elems/footer/model'; let component = new ComponentMock(); -let model = new TodosFooterModel(component); +let model; describe('TodosFooter', () => { + beforeEach(() => model = new TodosFooterModel(component)); + + afterEach(() => model.destroy()); + describe('on "TodosUpdatedList" event', () => { it('should calculate the number of uncompleted todos', () => { Dispatcher.emit(EVENTS.TodosUpdatedList, { diff --git a/test/client/components/todos/todos-header.test.js b/test/client/components/todos/todos-header.test.js index 8c0c2d0..1e22869 100644 --- a/test/client/components/todos/todos-header.test.js +++ b/test/client/components/todos/todos-header.test.js @@ -5,9 +5,13 @@ import ComponentMock from 'test/mocks/component'; import TodosHeaderModel from 'client/components/todos/elems/header/model'; let component = new ComponentMock(); -let model = new TodosHeaderModel(component); +let model; describe('TodosHeader', () => { + beforeEach(() => model = new TodosHeaderModel(component)); + + afterEach(() => model.destroy()); + describe('on "TodosUpdatedList" event', () => { it('should have the checked checkbox, if all todos are completed', () => { Dispatcher.emit(EVENTS.TodosUpdatedList, { diff --git a/test/client/components/todos/todos-item.test.js b/test/client/components/todos/todos-item.test.js index 39cfd11..d5bd303 100644 --- a/test/client/components/todos/todos-item.test.js +++ b/test/client/components/todos/todos-item.test.js @@ -5,9 +5,13 @@ import ComponentMock from 'test/mocks/component'; import TodosItemModel from 'client/components/todos/elems/item/model'; let component = new ComponentMock({ id: 1, text: 'test' }); -let model = new TodosItemModel(component); +let model; describe('TodosItem', () => { + beforeEach(() => model = new TodosItemModel(component)); + + afterEach(() => model.destroy()); + describe('#toggle(makeCompleted)', () => { it('should emit the "TodosToggleItem" event', done => { Dispatcher.once(EVENTS.TodosToggleItem, data => { diff --git a/test/client/components/todos/todos-list.test.js b/test/client/components/todos/todos-list.test.js index 12eea33..8202e64 100644 --- a/test/client/components/todos/todos-list.test.js +++ b/test/client/components/todos/todos-list.test.js @@ -7,13 +7,19 @@ import ComponentMock from 'test/mocks/component'; import TodosListModel from 'client/components/todos/elems/list/model'; let component = new ComponentMock(); -let model = new TodosListModel(component); let sandbox = sinon.sandbox.create(); +let model; describe('TodosList', () => { - beforeEach(() => component.state = { todos: [] }); + beforeEach(() => { + component.state = { todos: [] }; + model = new TodosListModel(component); + }); - afterEach(() => sandbox.restore()); + afterEach(() => { + model.destroy(); + sandbox.restore(); + }); it('should call #addTodo(text) on the "TodosCreateItem" event', () => { let spy = sandbox.spy(model, 'addTodo'); @@ -238,6 +244,15 @@ describe('TodosList', () => { }); describe('#deleteCompleted()', () => { + it('should not toggle todos if todos are undefined', () => { + let spy = sandbox.spy(API, 'remove'); + + component.state = {}; + model.deleteCompleted(); + + expect(spy.called).to.be.false; + }); + it('should call the "/api/components/Todos" gate', () => { sandbox.stub(API, 'remove', gate => { expect(gate).to.be.equal('/api/components/Todos'); diff --git a/test/lib/api.test.js b/test/lib/api.test.js index b4ac4c2..ba15585 100644 --- a/test/lib/api.test.js +++ b/test/lib/api.test.js @@ -1,7 +1,7 @@ -import 'expose?FormData!form-data'; -import 'expose?fetch!node-fetch'; import { expect } from 'chai'; import nock from 'nock'; +import fetch from 'node-fetch'; +import FormData from 'form-data'; import API from 'lib/api'; const HOST = 'http://test'; @@ -9,7 +9,17 @@ const PATH = '/api/component'; const GATE = HOST + PATH; describe('API', () => { - afterEach(() => nock.cleanAll()); + beforeEach(() => { + global.FormData = FormData; + global.fetch = fetch; + }); + + afterEach(() => { + delete global.FormData; + delete global.fetch; + + nock.cleanAll(); + }); describe('.getQueryString(path, query)', () => { it('should return a correct path if query params are undefined', () => { diff --git a/test/lib/component-model.test.js b/test/lib/component-model.test.js index e0b64aa..b6689fa 100644 --- a/test/lib/component-model.test.js +++ b/test/lib/component-model.test.js @@ -5,17 +5,21 @@ import ComponentModel from 'lib/component-model'; import ComponentMock from 'test/mocks/component'; let component = new ComponentMock(); -let model = new ComponentModel(component); let sandbox = sinon.sandbox.create(); +let model; describe('ComponentModel', () => { - afterEach(() => sandbox.restore()); - beforeEach(() => { + model = new ComponentModel(component); component.props = { text: 'props' }; component.state = { text: 'state' }; }); + afterEach(() => { + model.destroy(); + sandbox.restore(); + }); + describe('#props', () => { it('should return component props', () => { expect(model.props).to.be.deep.equal(component.props); diff --git a/test/lib/dispatcher.test.js b/test/lib/dispatcher.test.js index a106117..fd9302c 100644 --- a/test/lib/dispatcher.test.js +++ b/test/lib/dispatcher.test.js @@ -3,36 +3,37 @@ import { expect } from 'chai'; import sinon from 'sinon'; import Dispatcher from 'lib/dispatcher'; -let sandbox = sinon.sandbox.create(); +let EVENT = 'event'; +let spy; describe('Dispatcher', () => { - afterEach(() => sandbox.restore()); + afterEach(() => Dispatcher.off(EVENT, spy)); describe('.emit(event, data)', () => { it('should emit an event', () => { - let spy = sandbox.spy(EventEmitter.prototype, 'emit'); + spy = sinon.spy(EventEmitter.prototype, 'emit'); - Dispatcher.emit('event', 'data'); + Dispatcher.emit(EVENT, 'data'); - expect(spy.calledWithExactly('event', 'data')).to.be.true; + expect(spy.calledWithExactly(EVENT, 'data')).to.be.true; }); }); describe('.on(event, listener)', () => { it('should handle multiple events', () => { - let spy = sandbox.spy(); + spy = sinon.spy(); - Dispatcher.on('event', spy); - Dispatcher.emit('event'); - Dispatcher.emit('event'); + Dispatcher.on(EVENT, spy); + Dispatcher.emit(EVENT); + Dispatcher.emit(EVENT); expect(spy.callCount).to.be.equal(2); }); it('should pass data to the handler', () => { - let spy = sandbox.spy(); + spy = sinon.spy(); - Dispatcher.on('event', spy).emit('event', 'data'); + Dispatcher.on(EVENT, spy).emit(EVENT, 'data'); expect(spy.calledWithExactly('data')).to.be.true; }); @@ -40,19 +41,19 @@ describe('Dispatcher', () => { describe('.once(event, listener)', () => { it('should handle an event once', () => { - let spy = sandbox.spy(); + spy = sinon.spy(); - Dispatcher.once('event', spy); - Dispatcher.emit('event'); - Dispatcher.emit('event'); + Dispatcher.once(EVENT, spy); + Dispatcher.emit(EVENT); + Dispatcher.emit(EVENT); expect(spy.callCount).to.be.equal(1); }); it('should pass data to the handler', () => { - let spy = sandbox.spy(); + spy = sinon.spy(); - Dispatcher.once('event', spy).emit('event', 'data'); + Dispatcher.once(EVENT, spy).emit(EVENT, 'data'); expect(spy.calledWithExactly('data')).to.be.true; }); @@ -60,9 +61,9 @@ describe('Dispatcher', () => { describe('.off(event, listener)', () => { it('should remove an event listener', () => { - let spy = sandbox.spy(); + spy = sinon.spy(); - Dispatcher.on('event', spy).off('event', spy).emit('event'); + Dispatcher.on(EVENT, spy).off(EVENT, spy).emit(EVENT); expect(spy.called).to.be.false; }); diff --git a/test/server/models/todos.test.js b/test/server/models/todos.test.js index 5c5bd0b..1f9a570 100644 --- a/test/server/models/todos.test.js +++ b/test/server/models/todos.test.js @@ -186,23 +186,35 @@ describe('Todos model', () => { expect(mongodb.argumentsStack[1][0]._id).to.be.equal(SESSION_ID); }); - it('should toggle todos and save them', () => { + it('should toggle selected todos and save them', () => { mongodb.data = [{ items: [ { id: '56b8a6a8fd0b51ae4bf72887', text: 'test', isCompleted: false + }, + { + id: '56b8a6a8fd0b51ae4bf72888', + text: 'test', + isCompleted: false } ] }]; return Model.toggle({ db: mongodb }, true, '56b8a6a8fd0b51ae4bf72887') - .then(items => expect(items).to.be.deep.equal([{ - id: '56b8a6a8fd0b51ae4bf72887', - text: 'test', - isCompleted: true - }])); + .then(items => expect(items).to.be.deep.equal([ + { + id: '56b8a6a8fd0b51ae4bf72887', + text: 'test', + isCompleted: true + }, + { + id: '56b8a6a8fd0b51ae4bf72888', + text: 'test', + isCompleted: false + } + ])); }); it('should build the right update query', () => { diff --git a/test/server/providers/page-type-todos-example.test.js b/test/server/providers/page-type-todos-example.test.js index e3326ca..5258b3f 100644 --- a/test/server/providers/page-type-todos-example.test.js +++ b/test/server/providers/page-type-todos-example.test.js @@ -20,4 +20,11 @@ describe('PageTypeTodosExample provider', () => { } })); }); + + it('should not fetch data if it is already defined', () => { + let prevData = { PageTypeTodosExample: {} }; + + return provider(null, prevData) + .then(data => expect(data.PageTypeTodosExample).to.be.equal(prevData.PageTypeTodosExample)); + }); }); diff --git a/test/server/providers/page-type-welcome.test.js b/test/server/providers/page-type-welcome.test.js index fc671e6..20e32ff 100644 --- a/test/server/providers/page-type-welcome.test.js +++ b/test/server/providers/page-type-welcome.test.js @@ -11,4 +11,11 @@ describe('PageTypeWelcome provider', () => { } })); }); + + it('should not fetch data if it is already defined', () => { + let prevData = { PageTypeWelcome: {} }; + + return provider(null, prevData) + .then(data => expect(data.PageTypeWelcome).to.be.equal(prevData.PageTypeWelcome)); + }); }); diff --git a/test/server/providers/todos.test.js b/test/server/providers/todos.test.js index b5b3080..bfa6ca3 100644 --- a/test/server/providers/todos.test.js +++ b/test/server/providers/todos.test.js @@ -14,4 +14,11 @@ describe('Todos provider', () => { return provider() .then(data => expect(data.Todos).to.be.deep.equal({ items: [] })); }); + + it('should not fetch data if it is already defined', () => { + let prevData = { Todos: {} }; + + return provider(null, prevData) + .then(data => expect(data.Todos).to.be.equal(prevData.Todos)); + }); }); diff --git a/webpack.client.config.js b/webpack.client.config.js index ee5f96b..3667a11 100644 --- a/webpack.client.config.js +++ b/webpack.client.config.js @@ -55,10 +55,7 @@ module.exports = { { test: /\.jsx?$/, exclude: /node_modules/, - loader: 'babel', - query: { - presets: ['es2015', 'stage-2', 'react'] - } + loader: 'babel' }, { test: /\.css$/, diff --git a/webpack.server.config.js b/webpack.server.config.js index c76e58f..5e744a2 100644 --- a/webpack.server.config.js +++ b/webpack.server.config.js @@ -52,10 +52,7 @@ module.exports = { { test: /\.jsx?$/, exclude: /node_modules/, - loader: 'babel', - query: { - presets: ['es2015', 'stage-2', 'react'] - } + loader: 'babel' }, { test: /\.node$/, diff --git a/webpack.test.config.js b/webpack.test.config.js deleted file mode 100644 index 5468696..0000000 --- a/webpack.test.config.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -let path = require('path'); -let glob = require('glob'); - -const ENTRIES = glob.sync(path.join(__dirname, 'test/**/*.test.js')).reduce((entries, entry) => { - entries[path.relative(path.join(__dirname, 'test'), entry)] = entry; - - return entries; -}, {}); - -module.exports = { - entry: ENTRIES, - - output: { - path: path.join(__dirname, 'build', 'test'), - filename: '[name]', - libraryTarget: 'commonjs2' - }, - - target: 'node', - - node: { - console: false, - global: false, - process: false, - Buffer: false, - __filename: false, - __dirname: false - }, - - cache: true, - - resolve: { - root: __dirname - }, - - module: { - loaders: [ - { - test: /\.jsx?$/, - exclude: /node_modules/, - loader: 'babel', - query: { - presets: ['es2015', 'stage-2'] - } - }, - { - test: /\.node$/, - loader: 'node' - }, - { - test: /\.json$/, - loader: 'json' - } - ] - } -};