diff --git a/.babelrc b/.babelrc
index 76ab698..4ad2886 100644
--- a/.babelrc
+++ b/.babelrc
@@ -15,5 +15,10 @@
}
]
]
- ]
+ ],
+ "env": {
+ "test": {
+ "plugins": ["istanbul"]
+ }
+ }
}
diff --git a/.gitignore b/.gitignore
index cdcd44e..e5d46d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ umd
node_modules
npm-debug.log
coverage
+.nyc_output
\ No newline at end of file
diff --git a/package.json b/package.json
index a1e5b0e..76a2090 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,17 @@
"build-es6": "NODE_ENV=production webpack --output-library-target commonjs2 src/index.js ./es6/index.js",
"build-umd": "NODE_ENV=production webpack src/index.js umd/stormpath-sdk-react.js",
"build-min": "NODE_ENV=production webpack -p src/index.js umd/stormpath-sdk-react.min.js",
- "build-dist": "NODE_ENV=production webpack src/index.js dist/stormpath-sdk-react.js && NODE_ENV=production webpack -p src/index.js dist/stormpath-sdk-react.min.js"
+ "build-dist": "NODE_ENV=production webpack src/index.js dist/stormpath-sdk-react.js && NODE_ENV=production webpack -p src/index.js dist/stormpath-sdk-react.min.js",
+ "test": "better-npm-run test",
+ "test:watch": "mocha -w test/helpers/browser.js test/*.spec.js test/**/*.spec.js"
+ },
+ "betterScripts": {
+ "test": {
+ "env": {
+ "NODE_ENV": "test"
+ },
+ "command": "nyc _mocha --report html -- test/helpers/browser.js test/*.spec.js test/**/*.spec.js"
+ }
},
"devDependencies": {
"babel-cli": "^6.18.0",
@@ -33,14 +43,20 @@
"babel-eslint": "^7.1.0",
"babel-loader": "^6.2.7",
"babel-plugin-dev-expression": "^0.2.1",
+ "babel-plugin-istanbul": "^4.0.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-es2015-classes": "^6.18.0",
"babel-plugin-transform-proto-to-assign": "^6.9.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.16.0",
+ "babel-register": "^6.23.0",
+ "better-npm-run": "0.0.14",
"bundle-loader": "^0.5.4",
+ "chai": "^3.5.0",
+ "chai-spies": "^0.7.1",
"css-loader": "^0.25.0",
+ "enzyme": "^2.7.1",
"eslint": "^3.9.1",
"eslint-plugin-react": "^6.6.0",
"fbjs": "^0.8.5",
@@ -50,12 +66,16 @@
"history": "^2.1.2",
"invariant": "^2.2.1",
"isparta-loader": "^2.0.0",
+ "istanbul": "^0.4.5",
+ "jsdom": "^9.11.0",
"keymirror": "^0.1.1",
+ "mocha": "^3.2.0",
+ "nyc": "^10.1.2",
"pretty-bytes": "^4.0.2",
"qs": "^6.3.0",
"react": "^15.3.2",
"react-addons-css-transition-group": "^15.3.2",
- "react-addons-test-utils": "15.3.2",
+ "react-addons-test-utils": "^15.3.2",
"react-dom": "^15.3.2",
"react-router": "^3.0.0",
"react-static-container": "^1.0.1",
@@ -80,5 +100,29 @@
],
"dependencies": {
"xtend": "^4.0.1"
+ },
+ "nyc": {
+ "lines": "20",
+ "statements": "20",
+ "branches": "20",
+ "functions": "20",
+ "sourceMap": false,
+ "instrument": false,
+ "include": "src/**/*.js",
+ "exclude": [
+ "dist/**/*",
+ "es6/**/*",
+ "lib/**/*",
+ "test/**/*",
+ "umd/**/*"
+ ],
+ "reporter": [
+ "lcov",
+ "text-summary"
+ ],
+ "check-coverage": true,
+ "require": [
+ "./test/helpers/browser.js"
+ ]
}
}
diff --git a/src/actions/TokenActions.js b/src/actions/TokenActions.js
index 23358e3..5d20f9b 100644
--- a/src/actions/TokenActions.js
+++ b/src/actions/TokenActions.js
@@ -1,6 +1,7 @@
import context from './../context';
import TokenConstants from './../constants/TokenConstants';
+/* istanbul ignore next */
function dispatch(event) {
setTimeout(() => {
context.getDispatcher().dispatch(event);
@@ -8,8 +9,17 @@ function dispatch(event) {
}
class TokenActions {
+ constructor(dispatch) {
+ this.dispatch = dispatch;
+ }
+
+ // Allows for setting mock dispatch in tests
+ setDispatch(dispatch) {
+ this.dispatch = dispatch;
+ }
+
set(type, token, callback) {
- dispatch({
+ this.dispatch({
type: TokenConstants.TOKEN_SET,
options: {
type: type,
@@ -20,7 +30,7 @@ class TokenActions {
}
refresh(token, callback) {
- dispatch({
+ this.dispatch({
type: TokenConstants.TOKEN_REFRESH,
options: {
token: token
@@ -30,4 +40,4 @@ class TokenActions {
}
}
-export default new TokenActions()
+export default new TokenActions(dispatch)
diff --git a/src/components/Authenticated.js b/src/components/Authenticated.js
index ddadf7f..ddb451a 100644
--- a/src/components/Authenticated.js
+++ b/src/components/Authenticated.js
@@ -11,6 +11,7 @@ export default class Authenticated extends React.Component {
let authenticated = user !== undefined;
if (authenticated && this.props.inGroup) {
+ /* istanbul ignore else */
if (user.groups) {
authenticated = utils.groupsMatchExpression(user.groups, this.props.inGroup);
} else {
diff --git a/src/context.js b/src/context.js
index 02a6e33..fe09321 100644
--- a/src/context.js
+++ b/src/context.js
@@ -29,6 +29,10 @@ class Context {
this.tokenStore = tokenStore;
}
+ getTokenStore() {
+ return this.tokenStore;
+ }
+
setSessionStore(sessionStore) {
this.sessionStore = sessionStore;
}
diff --git a/src/dispatchers/FluxDispatcher.js b/src/dispatchers/FluxDispatcher.js
index f185ccc..841dd31 100644
--- a/src/dispatchers/FluxDispatcher.js
+++ b/src/dispatchers/FluxDispatcher.js
@@ -1,8 +1,11 @@
import { Dispatcher } from 'flux';
+// Extracted for testability
+const defaultDispatcher = new Dispatcher();
+
export default class FluxDispatcher {
- constructor(reducer) {
- this.dispatcher = new Dispatcher();
+ constructor(reducer, dispatcher = defaultDispatcher) {
+ this.dispatcher = dispatcher;
this.register(reducer);
}
diff --git a/test/ContextTest.js b/test/ContextTest.js
deleted file mode 100644
index e69de29..0000000
diff --git a/test/actions/TokenActions.spec.js b/test/actions/TokenActions.spec.js
new file mode 100644
index 0000000..c39cb9f
--- /dev/null
+++ b/test/actions/TokenActions.spec.js
@@ -0,0 +1,54 @@
+import TokenActions from '../../src/actions/TokenActions';
+import TokenConstants from '../../src/constants/TokenConstants';
+import chai, {expect} from 'chai';
+import spies from 'chai-spies';
+
+chai.use(spies);
+
+describe('TokenActions', () => {
+ let dispatchSpy;
+ let callback;
+
+ beforeEach(() => {
+ dispatchSpy = chai.spy(({callback}) => callback());
+ callback = () => ({});
+
+ TokenActions.setDispatch(dispatchSpy);
+ });
+
+ describe('set action', () => {
+ it('should dispatch a token set action', () => {
+ const type = 'sometype';
+ const token = 'sometoken';
+
+ TokenActions.set(type, token, callback);
+
+ expect(dispatchSpy).to.have.been.calledOnce;
+ expect(dispatchSpy).to.have.been.called.with({
+ type: TokenConstants.TOKEN_SET,
+ options: {
+ type,
+ token,
+ },
+ callback,
+ });
+ });
+ });
+
+ describe('refresh action', () => {
+ it('should dispatch a refresh action', () => {
+ const token = 'sometoken';
+
+ TokenActions.refresh(token, callback);
+
+ expect(dispatchSpy).to.have.been.calledOnce;
+ expect(dispatchSpy).to.have.been.called.with({
+ type: TokenConstants.TOKEN_REFRESH,
+ options: {
+ token,
+ },
+ callback,
+ });
+ });
+ });
+});
diff --git a/test/components/Authenticated.spec.js b/test/components/Authenticated.spec.js
new file mode 100644
index 0000000..9a96f91
--- /dev/null
+++ b/test/components/Authenticated.spec.js
@@ -0,0 +1,82 @@
+import React from 'react';
+import {shallow} from 'enzyme';
+import {expect} from 'chai';
+
+import Authenticated from '../../src/components/Authenticated';
+
+describe('', () => {
+ let userContext;
+ let noUserContext;
+
+ before(() => {
+ userContext = {
+ user: {
+ groups: {
+ basic: true,
+ unbasic: true
+ }
+ }
+ };
+
+ noUserContext = {};
+ });
+
+ describe('without inGroup property', () => {
+ it('should render content if there is a user', () => {
+ const element = shallow(
+ Shown,
+ {context: userContext}
+ );
+
+ expect(element).to.be.ok;
+ expect(element.text()).to.equal('Shown');
+ });
+
+ it('should not render content if there is no user', () => {
+ const element = shallow(
+ Not Shown,
+ {context: noUserContext}
+ );
+
+ expect(element).to.be.ok;
+ expect(element.text()).not.to.be.ok;
+ expect(element.text()).not.to.equal('Not Shown');
+ });
+ });
+
+
+ describe('with inGroup property', () => {
+ it('should not render content if there is no user', () => {
+ const element = shallow(
+ Not Shown,
+ {context: noUserContext}
+ );
+
+ expect(element).to.be.ok;
+ expect(element.text()).not.to.be.ok;
+ expect(element.text()).not.to.equal('Not Shown');
+ });
+
+ it('should not render content if the user is not in the group', () => {
+ const element = shallow(
+ Not Shown,
+ {context: userContext}
+ );
+
+ expect(element).to.be.ok;
+ expect(element.text()).not.to.be.ok;
+ expect(element.text()).not.to.equal('Not Shown');
+ });
+
+ it('should render content if the user is in the group', () => {
+ const element = shallow(
+ Shown,
+ {context: userContext}
+ );
+
+ expect(element).to.be.ok;
+ expect(element.text()).to.be.ok;
+ expect(element.text()).to.equal('Shown');
+ });
+ });
+});
diff --git a/test/context.spec.js b/test/context.spec.js
new file mode 100644
index 0000000..dc2011e
--- /dev/null
+++ b/test/context.spec.js
@@ -0,0 +1,38 @@
+import chai, {expect} from 'chai';
+
+import context from '../src/context';
+
+describe('Context', () => {
+ it('should have empty initial values', () => {
+ expect(context.router).not.to.be.ok;
+ expect(context.dispatcher).not.to.be.ok;
+ expect(context.tokenStore).not.to.be.ok;
+ expect(context.sessionStore).not.to.be.ok;
+ expect(context.userStore).not.to.be.ok;
+ });
+
+ it('should have setters for all values', () => {
+ context.setRouter('router');
+ expect(context.router).to.equal('router');
+
+ context.setDispatcher('dispatcher');
+ expect(context.dispatcher).to.equal('dispatcher');
+
+ context.setTokenStore('tokenStore');
+ expect(context.tokenStore).to.equal('tokenStore');
+
+ context.setSessionStore('sessionStore');
+ expect(context.sessionStore).to.equal('sessionStore');
+
+ context.setUserStore('userStore');
+ expect(context.userStore).to.equal('userStore');
+ });
+
+ it('should have getters for all values', () => {
+ expect(context.router).to.equal(context.getRouter());
+ expect(context.dispatcher).to.equal(context.getDispatcher());
+ expect(context.tokenStore).to.equal(context.getTokenStore());
+ expect(context.sessionStore).to.equal(context.getSessionStore());
+ expect(context.userStore).to.equal(context.getUserStore());
+ });
+});
diff --git a/test/dispatchers/FluxDispatcher.spec.js b/test/dispatchers/FluxDispatcher.spec.js
new file mode 100644
index 0000000..9d83628
--- /dev/null
+++ b/test/dispatchers/FluxDispatcher.spec.js
@@ -0,0 +1,55 @@
+import chai, {expect} from 'chai';
+import spies from 'chai-spies';
+
+import FluxDispatcher from '../../src/dispatchers/FluxDispatcher';
+
+chai.use(spies);
+
+describe('FluxDispatcher', () => {
+ let reducer;
+ let dispatcher;
+ let fluxDispatcher;
+
+ beforeEach(function() {
+ reducer = chai.spy();
+
+ dispatcher = {
+ register: chai.spy(),
+ dispatch: chai.spy(),
+ };
+
+ fluxDispatcher = new FluxDispatcher(reducer, dispatcher);
+ });
+
+ describe('constructor', () => {
+ it('should register the reducer to the dispatcher when constructed', () => {
+ expect(dispatcher.register).to.have.been.calledOnce;
+ expect(fluxDispatcher.dispatcher).to.equal(dispatcher);
+ });
+ });
+
+ describe('dispatch method', () => {
+ let event;
+
+ before(() => {
+ event = {
+ type: 'eventType',
+ options: {
+ optionable: true
+ },
+ callback: () => ({})
+ };
+ });
+
+ it('should call the set dispatcher with the destructured data', () => {
+ fluxDispatcher.dispatch(event);
+
+ expect(dispatcher.dispatch).to.have.been.calledOnce;
+ expect(dispatcher.dispatch).to.have.been.called.with.exactly({
+ actionType: event.type,
+ options: event.options,
+ callback: event.callback,
+ });
+ });
+ });
+});
diff --git a/test/dispatchers/ReduxDispatcher.spec.js b/test/dispatchers/ReduxDispatcher.spec.js
new file mode 100644
index 0000000..e0c248b
--- /dev/null
+++ b/test/dispatchers/ReduxDispatcher.spec.js
@@ -0,0 +1,34 @@
+import chai, {expect} from 'chai';
+import spies from 'chai-spies';
+
+import ReduxDispatcher from '../../src/dispatchers/ReduxDispatcher';
+
+chai.use(spies);
+
+describe('ReduxDispatcher', () => {
+ let reducer;
+ let store;
+ let reduxDispatcher;
+ let event;
+
+ before(() => {
+ reducer = chai.spy();
+ store = {
+ dispatch: chai.spy()
+ };
+
+ reduxDispatcher = new ReduxDispatcher(reducer, store);
+
+ event = {type: 'event'};
+ });
+
+ it('should call the reducer and dispatch the event on .dispatch()', () => {
+ reduxDispatcher.dispatch(event);
+
+ expect(reducer).to.have.been.calledOnce;
+ expect(reducer).to.have.been.called.with.exactly(event);
+
+ expect(store.dispatch).to.have.been.calledOnce;
+ expect(store.dispatch).to.have.been.called.with.exactly(event);
+ });
+});
diff --git a/test/helpers/browser.js b/test/helpers/browser.js
new file mode 100644
index 0000000..5bfa72b
--- /dev/null
+++ b/test/helpers/browser.js
@@ -0,0 +1,21 @@
+// Taken from https://semaphoreci.com/community/tutorials/testing-react-components-with-enzyme-and-mocha
+require('babel-register')();
+
+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'
+};
+
+global.documentRef = document;
diff --git a/test/helpers/storage-mock.js b/test/helpers/storage-mock.js
new file mode 100644
index 0000000..e546876
--- /dev/null
+++ b/test/helpers/storage-mock.js
@@ -0,0 +1,27 @@
+import chai from 'chai';
+import spies from 'chai-spies';
+
+chai.use(spies);
+
+export default class StorageMock {
+ constructor(name) {
+ this.name = name;
+ this._storage = {};
+
+ this.getItem = chai.spy(this._get.bind(this));
+ this.setItem = chai.spy(this._set.bind(this));
+ this.removeItem = chai.spy(this._remove.bind(this));
+ }
+
+ _get(name) {
+ return this._storage[name];
+ }
+
+ _set(name, value) {
+ this._storage[name] = String(value);
+ }
+
+ _remove(name) {
+ delete this._storage[name];
+ }
+}
diff --git a/test/storage/LocalStorage.spec.js b/test/storage/LocalStorage.spec.js
new file mode 100644
index 0000000..9d1a923
--- /dev/null
+++ b/test/storage/LocalStorage.spec.js
@@ -0,0 +1,113 @@
+import chai, {expect} from 'chai';
+import spies from 'chai-spies';
+
+import StorageMock from '../helpers/storage-mock';
+import LocalStorage from '../../src/storage/LocalStorage';
+
+chai.use(spies);
+
+describe('LocalStorage storage', () => {
+ before(() => {
+ global.localStorage = new StorageMock('local');
+ global.sessionStorage = new StorageMock('session');
+ });
+
+ describe('constructor', () => {
+ it('should use sessionStorage if type is not `local`', () => {
+ let storage = new LocalStorage();
+ let storageOther = new LocalStorage('something');
+
+ expect(storage.storage).to.equal(global.sessionStorage);
+ expect(storageOther.storage).to.equal(global.sessionStorage);
+ expect(storage.storage).not.to.equal(global.localStorage);
+ expect(storageOther.storage).not.to.equal(global.localStorage);
+ });
+
+ it('should use localStorage if type is `local`', () => {
+ let storage = new LocalStorage('local');
+
+ expect(storage.storage).to.equal(global.localStorage);
+ expect(storage.storage).not.to.equal(global.sessionStorage);
+ });
+ });
+
+ describe('with storage present on device/browser', () => {
+ describe('get', () => {
+ const key = 'key';
+ const value = 'value';
+
+ it('should retrieve an item by name from local storage', (done) => {
+ const storage = new LocalStorage();
+
+ global.sessionStorage._set(key, value);
+
+ storage.get(key).then((storageValue) => {
+ expect(storageValue).to.equal(value);
+ done();
+ }).catch(done);
+ });
+ });
+
+ describe('set', () => {
+ const key = 'key';
+ const value = 123;
+
+ it('should store an item by name in local storage, stringifying it', (done) => {
+ const storage = new LocalStorage();
+
+ storage.set(key, value).then(() => {
+ expect(global.sessionStorage._get(key)).not.to.equal(value);
+ expect(global.sessionStorage._get(key)).to.equal(String(value));
+ done();
+ }).catch(done);
+ });
+ });
+
+ describe('remove', () => {
+ const key = 'key';
+ const value = 'value';
+
+ it('should remove an item by name from local storage', (done) => {
+ const storage = new LocalStorage();
+ global.sessionStorage._set(key, value);
+
+ expect(global.sessionStorage._get(key)).to.equal(value);
+
+ storage.remove(key).then(() => {
+ expect(global.sessionStorage._get(key)).not.to.be.ok;
+ done();
+ }).catch(done);
+ });
+ });
+ });
+
+ describe('without storage present on device/browser', () => {
+ let storage;
+ before(function() {
+ global.sessionStorage = null;
+ storage = new LocalStorage();
+ });
+
+ it('should throw on get', (done) => {
+ storage.get('key').catch((err) => {
+ expect(err).to.be.ok;
+ done();
+ });
+ });
+
+ it('should throw on set', (done) => {
+ storage.set('key', 'value').catch((err) => {
+ expect(err).to.be.ok;
+ done();
+ });
+ });
+
+ it('should throw on remove', (done) => {
+ storage.remove('key').catch((err) => {
+ expect(err).to.be.ok;
+ done();
+ });
+ });
+ });
+
+});
diff --git a/test/stores/BaseStore.spec.js b/test/stores/BaseStore.spec.js
new file mode 100644
index 0000000..bddda37
--- /dev/null
+++ b/test/stores/BaseStore.spec.js
@@ -0,0 +1,45 @@
+import chai, {expect} from 'chai';
+import spies from 'chai-spies';
+
+import BaseStore from '../../src/stores/BaseStore';
+
+chai.use(spies);
+
+describe('BaseStore', () => {
+ let store;
+
+ before(() => {
+ store = new BaseStore();
+
+ chai.spy.on(store, 'emit');
+ chai.spy.on(store, 'on');
+ chai.spy.on(store, 'removeListener');
+ });
+
+ it('should emit a `changed` event on .emitChange()', () => {
+ const value = value;
+
+ store.emitChange(value);
+
+ expect(store.emit).to.have.been.calledOnce;
+ expect(store.emit).to.have.been.called.with.exactly('changed', value);
+ });
+
+ it('should add a `changed` event listener on .addChangeListener()', () => {
+ const cb = () => ({});
+
+ store.addChangeListener(cb);
+
+ expect(store.on).to.have.been.calledOnce;
+ expect(store.on).to.have.been.called.with.exactly('changed', cb);
+ });
+
+ it('should remove a `changed` event listener on .removeChangeListener()', () => {
+ const cb = () => ({});
+
+ store.removeChangeListener(cb);
+
+ expect(store.removeListener).to.have.been.calledOnce;
+ expect(store.removeListener).to.have.been.called.with.exactly('changed', cb);
+ });
+});
diff --git a/test/stores/SessionStore.spec.js b/test/stores/SessionStore.spec.js
new file mode 100644
index 0000000..a617d1c
--- /dev/null
+++ b/test/stores/SessionStore.spec.js
@@ -0,0 +1,147 @@
+import chai, {expect} from 'chai';
+import spies from 'chai-spies';
+
+chai.use(spies);
+
+import SessionStore from '../../src/stores/SessionStore';
+
+describe('SessionStore', () => {
+ let sessionStore;
+
+ describe('.set(session)', () => {
+ let session;
+ let otherSession;
+
+ beforeEach(() => {
+ sessionStore = new SessionStore();
+ chai.spy.on(sessionStore, 'emitChange');
+
+ sessionStore.session = undefined;
+
+ session = {
+ name: 'A',
+ };
+
+ otherSession = {
+ groups: {
+ href: 'groupHref',
+ items: [
+ {
+ name: 'a',
+ status: 'ENABLED'
+ },
+ {
+ name: 'b',
+ status: 'DISABLED'
+ }
+ ]
+ },
+ name: 'B',
+ };
+
+ });
+
+ it('should set the session', () => {
+ expect(sessionStore.session).not.to.be.ok;
+
+ sessionStore.set(session);
+
+ expect(sessionStore.session).to.be.ok;
+ expect(sessionStore.session.name).to.equal(session.name);
+ });
+
+ it('should use only enabled groups', () => {
+ sessionStore.set(otherSession);
+
+ expect(sessionStore.session).to.be.ok;
+ expect(sessionStore.session.groups).to.deep.equal({
+ a: true
+ });
+ });
+
+ it('should emit a changed event when a new session is set', () => {
+ sessionStore.set(session);
+ expect(sessionStore.emitChange).to.have.been.calledOnce;
+ expect(sessionStore.emitChange).to.have.been.called.with.exactly(session);
+ });
+
+ it('should not emit a changed event when the smae session is set', () => {
+ sessionStore.set(session);
+ sessionStore.emitChange.reset(); // Resets the spy internals
+
+ sessionStore.set(session);
+
+ expect(sessionStore.emitChange).not.to.have.been.called;
+ });
+ });
+
+ describe('.get()', () => {
+ let session;
+
+ before(() => {
+ sessionStore = new SessionStore();
+ session = {name: 'session'};
+ sessionStore.set(session);
+ });
+
+ it('should retrieve the session', () => {
+ expect(sessionStore.get()).to.equal(session);
+ });
+ });
+
+ describe('.empty()', () => {
+ let session;
+
+ before(() => {
+ sessionStore = new SessionStore();
+ session = {name: 'session'};
+ });
+
+ it('should return true if the session is empty', () => {
+ expect(sessionStore.empty()).to.be.true;
+ });
+
+ it('should return false if the session is set', () => {
+ sessionStore.set(session);
+
+ expect(sessionStore.empty()).to.be.false;
+ });
+ });
+
+ describe('.reset()', () => {
+ let session;
+
+ beforeEach(() => {
+ sessionStore = new SessionStore();
+ session = {name: 'session'};
+ sessionStore.set(session);
+ });
+
+ it('should empty the session', () => {
+ expect(sessionStore.empty()).to.be.false;
+
+ sessionStore.reset();
+
+ expect(sessionStore.empty()).to.be.true;
+ });
+
+ it('should emit an event if the sessionStore is not empty previously', () => {
+ chai.spy.on(sessionStore, 'emitChange');
+
+ sessionStore.reset();
+
+ expect(sessionStore.emitChange).to.have.been.calledOnce;
+ expect(sessionStore.emitChange).to.have.been.called.with.exactly(undefined);
+ });
+
+ it('should not emit an event if the session was empty previously', () => {
+ sessionStore.reset();
+ expect(sessionStore.empty()).to.be.true;
+
+ chai.spy.on(sessionStore, 'emitChange');
+ sessionStore.reset();
+
+ expect(sessionStore.emitChange).not.to.have.been.called;
+ });
+ });
+});
diff --git a/test/utils.spec.js b/test/utils.spec.js
new file mode 100644
index 0000000..b87e111
--- /dev/null
+++ b/test/utils.spec.js
@@ -0,0 +1,75 @@
+import {expect} from 'chai';
+import {shallow} from 'enzyme';
+import React from 'react';
+
+import utils from '../src/utils';
+
+describe('utils', () => {
+ describe('nopElement', () => {
+ it('should produce an empty span', () => {
+ const element = shallow(utils.nopElement);
+
+ expect(element).to.be.ok;
+ expect(element.type()).to.equal('span');
+ expect(element.text()).not.to.be.ok;
+ });
+ });
+
+ describe('uuid', () => {
+ it('should produce a unique string', () => {
+ const count = 10;
+ const uuids = [];
+
+ for (let i = 0; i < count; i++) {
+ uuids.push(utils.uuid());
+ }
+
+ for(let i = 0; i < count; i++) {
+ expect(uuids[i]).to.be.a.string;
+ expect(uuids[i]).not.to.equal(uuids[(i + 1) % count]);
+ }
+ });
+ });
+
+ describe('containsWord', () => {
+ it('should return true if the word if the testWord contains any of the given words', () => {
+ const testWord = 'this is just a test';
+ const words = ['best', 'test', 'rest'];
+
+ expect(utils.containsWord(testWord, words)).to.be.true;
+ });
+
+ it('should match partial matches (shorter word inside of longer word)', () => {
+ const testWord = 'this is just a testing run';
+ const words = ['best', 'test', 'rest'];
+
+ expect(utils.containsWord(testWord, words)).to.be.true;
+ });
+
+ it('should match words of different case', () => {
+ const testWord = 'this is just a test'.toUpperCase();
+ const words = ['best', 'test', 'rest'];
+
+ expect(utils.containsWord(testWord, words)).to.be.true;
+ });
+
+ it('should return false if none of the words are contained in the test string', () => {
+ const testWord = 'this is just an experimental run';
+ const words = ['best', 'test', 'rest'];
+
+ expect(utils.containsWord(testWord, words)).to.be.false;
+ });
+ });
+
+ describe('takeProp', () => {
+ it('should take the first prop from the object it finds on the prop list', () => {
+ const data = {a: 1, b: 2, c:3};
+ expect(utils.takeProp(data, 'a', 'b')).to.equal(data.a);
+ expect(utils.takeProp(data, 'b', 'a')).to.equal(data.b);
+ });
+
+ it('should return undefined if no props are matched', () => {
+ expect(utils.takeProp({a: 1, b: 2}, 'c', 'd', 'e', 'f')).to.be.undefined;
+ });
+ });
+});